From 78e450e5428be854f035cccd91ff588794bb1bf7 Mon Sep 17 00:00:00 2001 From: ntsekouras Date: Thu, 1 Feb 2024 18:31:00 +0200 Subject: [PATCH 01/29] DataViews: Redesign of filters --- package-lock.json | 93 +++++++- packages/dataviews/package.json | 2 + packages/dataviews/src/add-filter.js | 252 +++------------------- packages/dataviews/src/dataviews.js | 32 +-- packages/dataviews/src/filter-summary.js | 237 ++++++++++---------- packages/dataviews/src/filters.js | 29 +-- packages/dataviews/src/search-widget.js | 123 +++++++++++ packages/dataviews/src/style.scss | 151 +++++++++++-- packages/dataviews/src/utils.js | 15 ++ packages/dataviews/src/view-table.js | 200 ++++------------- packages/dataviews/src/with-separators.js | 28 +++ 11 files changed, 587 insertions(+), 575 deletions(-) create mode 100644 packages/dataviews/src/search-widget.js create mode 100644 packages/dataviews/src/with-separators.js diff --git a/package-lock.json b/package-lock.json index 3768fe12dcc020..321c4fbaa28d76 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3986,9 +3986,9 @@ "integrity": "sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==" }, "node_modules/@babel/runtime": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.22.10.tgz", - "integrity": "sha512-21t/fkKLMZI4pqP2wlmsQAWnYW1PDyKyyUV4vCi+B25ydmdaYTKXPwCj0BzSUnZf4seIiYvSA3jcZ3gdsMFkLQ==", + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.9.tgz", + "integrity": "sha512-0CX6F+BI2s9dkUqr08KFrAIZgNFj75rdBU/DjCyYLIaV/quFjkk6T+EJ2LkZHyZTbEV4L5p97mNkUsHl2wLFAw==", "dependencies": { "regenerator-runtime": "^0.14.0" }, @@ -36818,6 +36818,15 @@ "integrity": "sha512-q9JtQJKjpsVxCRVgQ+WapguSbKC3SQ5HEzFGPAJMStgh3QjCawp00UKv3MTTAArTmGmmPUvllHZoNbZ3gs0I+Q==", "dev": true }, + "node_modules/match-sorter": { + "version": "6.3.3", + "resolved": "https://registry.npmjs.org/match-sorter/-/match-sorter-6.3.3.tgz", + "integrity": "sha512-sgiXxrRijEe0SzHKGX4HouCpfHRPnqteH42UdMEW7BlWy990ZkzcvonJGv4Uu9WE7Y1f8Yocm91+4qFPCbmNww==", + "dependencies": { + "@babel/runtime": "^7.23.8", + "remove-accents": "0.5.0" + } + }, "node_modules/mathjs": { "version": "10.6.4", "resolved": "https://registry.npmjs.org/mathjs/-/mathjs-10.6.4.tgz", @@ -54311,6 +54320,7 @@ "version": "0.4.1", "license": "GPL-2.0-or-later", "dependencies": { + "@ariakit/react": "^0.3.12", "@babel/runtime": "^7.16.0", "@wordpress/a11y": "file:../a11y", "@wordpress/components": "file:../components", @@ -54322,6 +54332,7 @@ "@wordpress/primitives": "file:../primitives", "@wordpress/private-apis": "file:../private-apis", "classnames": "^2.3.1", + "match-sorter": "^6.3.3", "remove-accents": "^0.5.0" }, "engines": { @@ -54331,6 +54342,41 @@ "react": "^18.0.0" } }, + "packages/dataviews/node_modules/@ariakit/core": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@ariakit/core/-/core-0.4.1.tgz", + "integrity": "sha512-Rdhw0/K0x+50gFvzuMW9wp+WJxpkrgiMgegRTOZSU92bv1K+6XfQWnlieIkLt2FC7pZGrDpGlS4C7ztEVF+JRg==" + }, + "packages/dataviews/node_modules/@ariakit/react": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@ariakit/react/-/react-0.4.1.tgz", + "integrity": "sha512-hKfCYjc3MFW20kn2dcvejB5zbYt/uU33Teq82c414/utf5sEoeRF+bxjNku8x1baJby9/SDP6zj2IgWPuedFNA==", + "dependencies": { + "@ariakit/react-core": "0.4.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/ariakit" + }, + "peerDependencies": { + "react": "^17.0.0 || ^18.0.0", + "react-dom": "^17.0.0 || ^18.0.0" + } + }, + "packages/dataviews/node_modules/@ariakit/react-core": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@ariakit/react-core/-/react-core-0.4.1.tgz", + "integrity": "sha512-cwDczl9XWBloXNg0CuHmJtBfEe7qF265JE0Pwlcp8wMSY9PsJeb0mKBlTygUPKn/FsKpKGaYSI7DlDntbcZciw==", + "dependencies": { + "@ariakit/core": "0.4.1", + "@floating-ui/dom": "^1.0.0", + "use-sync-external-store": "^1.2.0" + }, + "peerDependencies": { + "react": "^17.0.0 || ^18.0.0", + "react-dom": "^17.0.0 || ^18.0.0" + } + }, "packages/date": { "name": "@wordpress/date", "version": "4.50.0", @@ -58772,9 +58818,9 @@ "integrity": "sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==" }, "@babel/runtime": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.22.10.tgz", - "integrity": "sha512-21t/fkKLMZI4pqP2wlmsQAWnYW1PDyKyyUV4vCi+B25ydmdaYTKXPwCj0BzSUnZf4seIiYvSA3jcZ3gdsMFkLQ==", + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.9.tgz", + "integrity": "sha512-0CX6F+BI2s9dkUqr08KFrAIZgNFj75rdBU/DjCyYLIaV/quFjkk6T+EJ2LkZHyZTbEV4L5p97mNkUsHl2wLFAw==", "requires": { "regenerator-runtime": "^0.14.0" }, @@ -69334,6 +69380,7 @@ "@wordpress/dataviews": { "version": "file:packages/dataviews", "requires": { + "@ariakit/react": "^0.3.12", "@babel/runtime": "^7.16.0", "@wordpress/a11y": "file:../a11y", "@wordpress/components": "file:../components", @@ -69345,7 +69392,32 @@ "@wordpress/primitives": "file:../primitives", "@wordpress/private-apis": "file:../private-apis", "classnames": "^2.3.1", + "match-sorter": "^6.3.3", "remove-accents": "^0.5.0" + }, + "dependencies": { + "@ariakit/core": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@ariakit/core/-/core-0.4.1.tgz", + "integrity": "sha512-Rdhw0/K0x+50gFvzuMW9wp+WJxpkrgiMgegRTOZSU92bv1K+6XfQWnlieIkLt2FC7pZGrDpGlS4C7ztEVF+JRg==" + }, + "@ariakit/react": { + "version": "https://registry.npmjs.org/@ariakit/react/-/react-0.4.1.tgz", + "integrity": "sha512-hKfCYjc3MFW20kn2dcvejB5zbYt/uU33Teq82c414/utf5sEoeRF+bxjNku8x1baJby9/SDP6zj2IgWPuedFNA==", + "requires": { + "@ariakit/react-core": "0.4.1" + } + }, + "@ariakit/react-core": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@ariakit/react-core/-/react-core-0.4.1.tgz", + "integrity": "sha512-cwDczl9XWBloXNg0CuHmJtBfEe7qF265JE0Pwlcp8wMSY9PsJeb0mKBlTygUPKn/FsKpKGaYSI7DlDntbcZciw==", + "requires": { + "@ariakit/core": "0.4.1", + "@floating-ui/dom": "^1.0.0", + "use-sync-external-store": "^1.2.0" + } + } } }, "@wordpress/date": { @@ -84857,6 +84929,15 @@ "integrity": "sha512-q9JtQJKjpsVxCRVgQ+WapguSbKC3SQ5HEzFGPAJMStgh3QjCawp00UKv3MTTAArTmGmmPUvllHZoNbZ3gs0I+Q==", "dev": true }, + "match-sorter": { + "version": "6.3.3", + "resolved": "https://registry.npmjs.org/match-sorter/-/match-sorter-6.3.3.tgz", + "integrity": "sha512-sgiXxrRijEe0SzHKGX4HouCpfHRPnqteH42UdMEW7BlWy990ZkzcvonJGv4Uu9WE7Y1f8Yocm91+4qFPCbmNww==", + "requires": { + "@babel/runtime": "^7.23.8", + "remove-accents": "0.5.0" + } + }, "mathjs": { "version": "10.6.4", "resolved": "https://registry.npmjs.org/mathjs/-/mathjs-10.6.4.tgz", diff --git a/packages/dataviews/package.json b/packages/dataviews/package.json index 16266bc032d204..d112013677aac7 100644 --- a/packages/dataviews/package.json +++ b/packages/dataviews/package.json @@ -27,6 +27,7 @@ "types": "build-types", "sideEffects": false, "dependencies": { + "@ariakit/react": "^0.3.12", "@babel/runtime": "^7.16.0", "@wordpress/a11y": "file:../a11y", "@wordpress/components": "file:../components", @@ -38,6 +39,7 @@ "@wordpress/primitives": "file:../primitives", "@wordpress/private-apis": "file:../private-apis", "classnames": "^2.3.1", + "match-sorter": "^6.3.3", "remove-accents": "^0.5.0" }, "peerDependencies": { diff --git a/packages/dataviews/src/add-filter.js b/packages/dataviews/src/add-filter.js index 99078fb5e82e69..ee7d8ba9a0db94 100644 --- a/packages/dataviews/src/add-filter.js +++ b/packages/dataviews/src/add-filter.js @@ -5,79 +5,39 @@ import { privateApis as componentsPrivateApis, Button, } from '@wordpress/components'; -import { funnel } from '@wordpress/icons'; -import { __, sprintf } from '@wordpress/i18n'; -import { Children, Fragment } from '@wordpress/element'; +import { plus } from '@wordpress/icons'; +import { __ } from '@wordpress/i18n'; /** * Internal dependencies */ +import WithSeparators from './with-separators'; import { unlock } from './lock-unlock'; -import { LAYOUT_LIST, OPERATORS } from './constants'; -import { DropdownMenuRadioItemCustom } from './dropdown-menu-helper'; const { DropdownMenuV2: DropdownMenu, DropdownMenuGroupV2: DropdownMenuGroup, DropdownMenuItemV2: DropdownMenuItem, - DropdownMenuRadioItemV2: DropdownMenuRadioItem, - DropdownMenuSeparatorV2: DropdownMenuSeparator, DropdownMenuItemLabelV2: DropdownMenuItemLabel, - DropdownMenuItemHelpTextV2: DropdownMenuItemHelpText, } = unlock( componentsPrivateApis ); -function WithSeparators( { children } ) { - return Children.toArray( children ) - .filter( Boolean ) - .map( ( child, i ) => ( - - { i > 0 && } - { child } - - ) ); -} - export default function AddFilter( { filters, view, onChangeView } ) { if ( filters.length === 0 ) { return null; } - - const filterCount = view.filters.reduce( ( acc, filter ) => { - if ( filter.value !== undefined ) { - return acc + 1; - } - return acc; - }, 0 ); - - const isPrimary = ( field ) => - filters.some( ( f ) => f.field === field && f.isPrimary ); - let isResetDisabled = true; - if ( - view.filters?.length > 0 && - ( view.filters.some( ( filter ) => filter.value !== undefined ) || - view.filters.some( - ( filter ) => - filter.value === undefined && ! isPrimary( filter.field ) - ) ) - ) { - isResetDisabled = false; - } - + const inactiveFilters = filters.filter( ( filter ) => ! filter.isVisible ); return ( - { view.type === LAYOUT_LIST && filterCount > 0 ? ( - - { filterCount } - - ) : null } + { __( 'Add fiter' ) } } style={ { @@ -86,191 +46,31 @@ export default function AddFilter( { filters, view, onChangeView } ) { > - { filters.map( ( filter ) => { - const filterInView = view.filters.find( - ( f ) => f.field === filter.field - ); - const otherFilters = view.filters.filter( - ( f ) => f.field !== filter.field - ); - const activeElement = filter.elements.find( - ( element ) => element.value === filterInView?.value - ); - const activeOperator = - filterInView?.operator || filter.operators[ 0 ]; + { inactiveFilters.map( ( filter ) => { return ( - + + { filter.name } + + ); } ) } - { - onChangeView( { - ...view, - page: 1, - filters: [], - } ); - } } - > - - { __( 'Reset filters' ) } - - ); diff --git a/packages/dataviews/src/dataviews.js b/packages/dataviews/src/dataviews.js index cff15ec304c23c..44d985930f22e9 100644 --- a/packages/dataviews/src/dataviews.js +++ b/packages/dataviews/src/dataviews.js @@ -76,27 +76,20 @@ export default function DataViews( { }, [ fields ] ); return (
- + - - { search && ( - - ) } - - - { ( view.type === LAYOUT_TABLE || - view.type === LAYOUT_GRID ) && ( + ) } + { [ LAYOUT_TABLE, LAYOUT_GRID ].includes( view.type ) && ( + + + { if ( activeElement === undefined ) { @@ -62,127 +54,124 @@ const FilterText = ( { activeElement, filterInView, filter } ) => { ); }; -function WithSeparators( { children } ) { - return Children.toArray( children ) - .filter( Boolean ) - .map( ( child, i ) => ( - - { i > 0 && } - { child } - - ) ); +function OperatorSelector( { filter, view, onChangeView } ) { + const operatorOptions = filter.operators?.map( ( operator ) => ( { + value: operator, + label: OPERATORS[ operator ]?.label, + } ) ); + const currentFilter = view.filters.find( + ( _filter ) => _filter.field === filter.field + ); + const value = currentFilter?.operator || filter.operators[ 0 ]; + return ( + + { filter.name } + { + const newFilters = currentFilter + ? [ + ...view.filters.map( ( _filter ) => { + if ( _filter.field === filter.field ) { + return { + ..._filter, + operator: newValue, + }; + } + return _filter; + } ), + ] + : [ + ...view.filters, + { + field: filter.field, + operator: newValue, + }, + ]; + onChangeView( { + ...view, + page: 1, + filters: newFilters, + } ); + } } + size="compact" + __nextHasNoMarginBottom + hideLabelFromVision + /> + + ); } -export default function FilterSummary( { filter, view, onChangeView } ) { - const filterInView = view.filters.find( ( f ) => f.field === filter.field ); - const otherFilters = view.filters.filter( - ( f ) => f.field !== filter.field +function ResetFilter( { filter, view, onChangeView, filters } ) { + const isPrimary = ( field ) => + filters.some( ( f ) => f.field === field && f.isPrimary ); + const isDisabled = ! view.filters?.some( + ( _filter ) => + _filter.value !== undefined || ! isPrimary( _filter.field ) ); + return ( + + ); +} + +export default function FilterSummary( props ) { + const { filter, view } = props; + const filterInView = view.filters.find( ( f ) => f.field === filter.field ); const activeElement = filter.elements.find( ( element ) => element.value === filterInView?.value ); - const activeOperator = filterInView?.operator || filter.operators[ 0 ]; - return ( - + ( + - } - > - - - { filter.elements.map( ( element ) => { - const isActive = activeElement?.value === element.value; - return ( - - onChangeView( { - ...view, - page: 1, - filters: [ - ...otherFilters, - { - field: filter.field, - operator: activeOperator, - value: isActive - ? undefined - : element.value, - }, - ], - } ) - } - > - - { element.label } - - { !! element.description && ( - - { element.description } - - ) } - - ); - } ) } - - { filter.operators.length > 1 && ( - - ) } - - + ) } + renderContent={ () => { + return ( + + + } + > + + + + + + ); + } } + /> ); } diff --git a/packages/dataviews/src/filters.js b/packages/dataviews/src/filters.js index a3dc1936056206..51ab1d98f983d6 100644 --- a/packages/dataviews/src/filters.js +++ b/packages/dataviews/src/filters.js @@ -9,22 +9,8 @@ import { memo } from '@wordpress/element'; import FilterSummary from './filter-summary'; import AddFilter from './add-filter'; import ResetFilters from './reset-filters'; -import { - ENUMERATION_TYPE, - OPERATOR_IN, - OPERATOR_NOT_IN, - LAYOUT_LIST, -} from './constants'; - -const sanitizeOperators = ( field ) => { - let operators = field.filterBy?.operators; - if ( ! operators || ! Array.isArray( operators ) ) { - operators = [ OPERATOR_IN, OPERATOR_NOT_IN ]; - } - return operators.filter( ( operator ) => - [ OPERATOR_IN, OPERATOR_NOT_IN ].includes( operator ) - ); -}; +import { sanitizeOperators } from './utils'; +import { ENUMERATION_TYPE, LAYOUT_LIST } from './constants'; const Filters = memo( function Filters( { fields, view, onChangeView } ) { const filters = []; @@ -53,11 +39,11 @@ const Filters = memo( function Filters( { fields, view, onChangeView } ) { isVisible: isPrimary || view.filters.some( - ( f ) => - f.field === field.id && - [ OPERATOR_IN, OPERATOR_NOT_IN ].includes( - f.operator - ) + ( f ) => f.field === field.id + // && + // [ OPERATOR_IN, OPERATOR_NOT_IN ].includes( + // f.operator + // ) ), isPrimary, } ); @@ -85,6 +71,7 @@ const Filters = memo( function Filters( { fields, view, onChangeView } ) { filter={ filter } view={ view } onChangeView={ onChangeView } + filters={ filters } /> ); } ), diff --git a/packages/dataviews/src/search-widget.js b/packages/dataviews/src/search-widget.js new file mode 100644 index 00000000000000..ebfc6d0e8633da --- /dev/null +++ b/packages/dataviews/src/search-widget.js @@ -0,0 +1,123 @@ +/** + * External dependencies + */ +// eslint-disable-next-line no-restricted-imports +import * as Ariakit from '@ariakit/react'; +import { matchSorter } from 'match-sorter'; + +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import { useState, useMemo, useDeferredValue } from '@wordpress/element'; +import { + BaseControl, + Icon, + __experimentalVStack as VStack, +} from '@wordpress/components'; +import { check, search } from '@wordpress/icons'; + +export default function SearchWidget( { filter, view, onChangeView } ) { + const [ searchValue, setSearchValue ] = useState( '' ); + const deferredSearchValue = useDeferredValue( searchValue ); + const selectedFilter = view.filters.find( + ( _filter ) => _filter.field === filter.field + ); + const selectedValues = selectedFilter?.value; + const matches = useMemo( () => { + return matchSorter( filter.elements, deferredSearchValue ?? '', { + keys: [ 'label' ], + } ); + }, [ filter.elements, deferredSearchValue ] ); + return ( + { + const currentFilter = view.filters.find( + ( _filter ) => _filter.field === filter.field + ); + const newFilters = currentFilter + ? [ + ...view.filters.map( ( _filter ) => { + if ( _filter.field === filter.field ) { + return { + ..._filter, + operator: + currentFilter.operator || + filter.operators[ 0 ], + value, + }; + } + return _filter; + } ), + ] + : [ + ...view.filters, + { + field: filter.field, + operator: filter.operators[ 0 ], + value, + }, + ]; + onChangeView( { + ...view, + page: 1, + filters: newFilters, + } ); + } } + setValue={ setSearchValue } + > + +
+ +
+ +
+
+
+ + + { matches.map( ( element ) => { + return ( + + + { selectedValues === element.value && ( + + ) } + + + + { element.label } + + { !! element.description && ( + + { element.description } + + ) } + + + ); + } ) } + { ! matches.length &&

{ __( 'No results found' ) }

} +
+
+
+ ); +} diff --git a/packages/dataviews/src/style.scss b/packages/dataviews/src/style.scss index 0d804da18e6b01..d08b125d4e7b4d 100644 --- a/packages/dataviews/src/style.scss +++ b/packages/dataviews/src/style.scss @@ -11,13 +11,16 @@ } .dataviews-filters__view-actions { - padding: $grid-unit-15 $grid-unit-40; + padding: $grid-unit-15 $grid-unit-40 0; .components-search-control { flex-grow: 1; - max-width: 240px; } } +.dataviews-filters__container { + padding: 0 $grid-unit-40; +} + .dataviews-filters__view-actions.components-h-stack { align-items: center; } @@ -26,24 +29,6 @@ position: relative; } -.dataviews-filters-count { - position: absolute; - top: 0; - right: 0; - height: $grid-unit-20; - color: var(--wp-components-color-accent-inverted, $white); - background-color: var(--wp-components-color-accent, var(--wp-admin-theme-color, #007cba)); - border-radius: $grid-unit-10; - min-width: $grid-unit-20; - padding: 0 $grid-unit-05; - transform: translateX(40%) translateY(-40%); - display: flex; - align-items: center; - justify-content: center; - font-size: 11px; - font-weight: 300; -} - .dataviews-pagination { margin-top: auto; position: sticky; @@ -507,3 +492,129 @@ .dataviews-view-grid__card.has-no-pointer-events * { pointer-events: none; } +.dataviews-filter-summary__popover { + .components-popover__content { + width: 230px; + } + + .dataviews-filter-summary__popover-separator { + border: none; + height: 1px; + background-color: #e0e0e0; + margin-block: calc(4px * 2); + margin-inline: 0; + outline: 2px solid transparent; + } +} + +.dataviews-search-widget-filter-combobox-list { + height: 180px; + overflow: auto; + padding: 2px; + + .dataviews-search-widget-filter-combobox-item { + display: flex; + align-items: center; + gap: $grid-unit-05; + border-radius: $radius-block-ui; + transition: all 0.05s ease-in-out; + @include reduce-motion("transition"); + box-sizing: border-box; + padding: 6px; + + &[data-active-item] { + background-color: var(--wp-components-color-accent, var(--wp-admin-theme-color, #3858e9)); + color: #fff; + + .dataviews-search-widget-filter-combobox-item-check { + fill: #fff; + } + + .dataviews-search-widget-filter-combobox-item-description { + color: #fff; + } + } + + &:hover, + &:focus { + box-shadow: inset 0 0 0 var(--wp-admin-border-width-focus) var(--wp-components-color-accent, var(--wp-admin-theme-color, #007cba)); + } + + .dataviews-search-widget-filter-combobox-item-check { + width: 24px; + height: 24px; + flex-shrink: 0; + } + + .dataviews-search-widget-filter-combobox-item-value { + [data-user-value] { + font-weight: 600; + } + } + + .dataviews-search-widget-filter-combobox-item-description { + display: block; + overflow: hidden; + text-overflow: ellipsis; + font-size: $helptext-font-size; + line-height: 16px; + color: $gray-700; + } + } +} + +.dataviews-search-widget-filter-combobox__wrapper { + position: relative; + + .dataviews-search-widget-filter-combobox__input-wrapper { + position: relative; + + .dataviews-search-widget-filter-combobox__input { + @include input-control; + display: block; + padding: 0 $grid-unit-10 0 $grid-unit-40; + background: $gray-100; + border: none; + width: 100%; + height: $grid-unit-40; + + // Unset inherited values. + margin-left: 0; + margin-right: 0; + + /* Fonts smaller than 16px causes mobile safari to zoom. */ + font-size: $mobile-text-min-font-size; + @include break-small { + font-size: $default-font-size; + } + + &:focus { + background: $white; + box-shadow: inset 0 0 0 var(--wp-admin-border-width-focus) var(--wp-components-color-accent, var(--wp-admin-theme-color, #007cba)); + } + + &::placeholder { + color: $gray-700; + } + + &::-webkit-search-decoration, + &::-webkit-search-cancel-button, + &::-webkit-search-results-button, + &::-webkit-search-results-decoration { + -webkit-appearance: none; + } + } + + .dataviews-search-widget-filter-combobox__icon { + position: absolute; + left: $grid-unit-05; + top: 50%; + transform: translateY(-50%); + display: flex; + align-items: center; + justify-content: center; + width: $icon-size; + } + } +} + diff --git a/packages/dataviews/src/utils.js b/packages/dataviews/src/utils.js index 2aa454b272c80e..9dec7db83cc759 100644 --- a/packages/dataviews/src/utils.js +++ b/packages/dataviews/src/utils.js @@ -1,3 +1,8 @@ +/** + * Internal dependencies + */ +import { OPERATOR_IN, OPERATOR_NOT_IN } from './constants'; + /** * Helper util to sort data by text fields, when sorting is done client side. * @@ -49,3 +54,13 @@ export function getPaginationResults( { data, view } ) { }, }; } + +export const sanitizeOperators = ( field ) => { + let operators = field.filterBy?.operators; + if ( ! operators || ! Array.isArray( operators ) ) { + operators = [ OPERATOR_IN, OPERATOR_NOT_IN ]; + } + return operators.filter( ( operator ) => + [ OPERATOR_IN, OPERATOR_NOT_IN ].includes( operator ) + ); +}; diff --git a/packages/dataviews/src/view-table.js b/packages/dataviews/src/view-table.js index b424a30747053d..51b5ec3a91fb4a 100644 --- a/packages/dataviews/src/view-table.js +++ b/packages/dataviews/src/view-table.js @@ -16,8 +16,6 @@ import { CheckboxControl, } from '@wordpress/components'; import { - Children, - Fragment, forwardRef, useEffect, useId, @@ -28,20 +26,18 @@ import { /** * Internal dependencies */ +import SingleSelectionCheckbox from './single-selection-checkbox'; +import WithSeparators from './with-separators'; import { unlock } from './lock-unlock'; import ItemActions from './item-actions'; import { ENUMERATION_TYPE, OPERATORS, SORTING_DIRECTIONS } from './constants'; -import { DropdownMenuRadioItemCustom } from './dropdown-menu-helper'; -import SingleSelectionCheckbox from './single-selection-checkbox'; const { DropdownMenuV2: DropdownMenu, DropdownMenuGroupV2: DropdownMenuGroup, DropdownMenuItemV2: DropdownMenuItem, DropdownMenuRadioItemV2: DropdownMenuRadioItem, - DropdownMenuSeparatorV2: DropdownMenuSeparator, DropdownMenuItemLabelV2: DropdownMenuItemLabel, - DropdownMenuItemHelpTextV2: DropdownMenuItemHelpText, } = unlock( componentsPrivateApis ); const sortArrows = { asc: '↑', desc: '↓' }; @@ -61,31 +57,21 @@ const HeaderMenu = forwardRef( function HeaderMenu( ref ) { const isHidable = field.enableHiding !== false; - const isSortable = field.enableSorting !== false; const isSorted = view.sort?.field === field.id; - - let filter, filterInView, activeElement, activeOperator, otherFilters; const operators = sanitizeOperators( field ); - if ( field.type === ENUMERATION_TYPE && operators.length > 0 ) { - filter = { - field: field.id, - operators, - elements: field.elements || [], - }; - filterInView = view.filters.find( ( f ) => f.field === filter.field ); - otherFilters = view.filters.filter( ( f ) => f.field !== filter.field ); - activeElement = filter.elements.find( - ( element ) => element.value === filterInView?.value - ); - activeOperator = filterInView?.operator || filter.operators[ 0 ]; - } - const isFilterable = !! filter; - - if ( ! isSortable && ! isHidable && ! isFilterable ) { + // Filter can be added: + // 1. If the field is not already part of a view's filters. + // 2. If the field meets the type and operator requirements. + // 3. If it's not primary. If it is, it should be already visible. + const canAddFilter = + ! view.filters?.some( ( _filter ) => field.id === _filter.field ) && + field.type === ENUMERATION_TYPE && + !! operators.length && + ! field.filterBy?.isPrimary; + if ( ! isSortable && ! isHidable && ! canAddFilter ) { return field.header; } - return ( ) } + { canAddFilter && ( + + } + onClick={ () => { + onChangeView( { + ...view, + page: 1, + filters: [ + ...( view.filters || [] ), + { + field: field.id, + value: undefined, + }, + ], + } ); + } } + > + + { __( 'Add filter' ) } + + + + ) } { isHidable && ( } @@ -165,149 +175,11 @@ const HeaderMenu = forwardRef( function HeaderMenu( ) } - { isFilterable && ( - - } - suffix={ - activeElement && ( - - ) - } - > - - { __( 'Filter by' ) } - - - } - > - - - { filter.elements.map( ( element ) => { - const isActive = - activeElement?.value === - element.value; - return ( - { - onChangeView( { - ...view, - page: 1, - filters: [ - ...otherFilters, - { - field: filter.field, - operator: - activeOperator, - value: isActive - ? undefined - : element.value, - }, - ], - } ); - } } - > - - { element.label } - - { !! element.description && ( - - { element.description } - - ) } - - ); - } ) } - - { filter.operators.length > 1 && ( - - ) } - - - - ) } ); } ); -function WithSeparators( { children } ) { - return Children.toArray( children ) - .filter( Boolean ) - .map( ( child, i ) => ( - - { i > 0 && } - { child } - - ) ); -} - function BulkSelectionCheckbox( { selection, onSelectionChange, data } ) { const areAllSelected = selection.length === data.length; return ( diff --git a/packages/dataviews/src/with-separators.js b/packages/dataviews/src/with-separators.js new file mode 100644 index 00000000000000..be2ebf26a9e889 --- /dev/null +++ b/packages/dataviews/src/with-separators.js @@ -0,0 +1,28 @@ +/** + * WordPress dependencies + */ +import { Children, Fragment } from '@wordpress/element'; +import { privateApis as componentsPrivateApis } from '@wordpress/components'; + +/** + * Internal dependencies + */ +import { unlock } from './lock-unlock'; + +const { DropdownMenuSeparatorV2: DropdownMenuSeparator } = unlock( + componentsPrivateApis +); + +export default function WithSeparators( { + children, + separator = , +} ) { + return Children.toArray( children ) + .filter( Boolean ) + .map( ( child, i ) => ( + + { i > 0 && separator } + { child } + + ) ); +} From 663e4de9f26693dbf1807a6d51093949eb153a46 Mon Sep 17 00:00:00 2001 From: ntsekouras Date: Thu, 1 Feb 2024 18:44:10 +0200 Subject: [PATCH 02/29] fix ComboboxItemValue --- packages/dataviews/src/search-widget.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/dataviews/src/search-widget.js b/packages/dataviews/src/search-widget.js index ebfc6d0e8633da..fba875cdc39950 100644 --- a/packages/dataviews/src/search-widget.js +++ b/packages/dataviews/src/search-widget.js @@ -103,9 +103,10 @@ export default function SearchWidget( { filter, view, onChangeView } ) { ) } - - { element.label } - + { !! element.description && ( { element.description } From 3196de53c159bfc60071ca8e38d90034405085c9 Mon Sep 17 00:00:00 2001 From: ntsekouras Date: Fri, 2 Feb 2024 10:16:30 +0200 Subject: [PATCH 03/29] show filter summary in list view --- packages/dataviews/src/filters.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/dataviews/src/filters.js b/packages/dataviews/src/filters.js index 51ab1d98f983d6..2bb6ce7a6c9ab4 100644 --- a/packages/dataviews/src/filters.js +++ b/packages/dataviews/src/filters.js @@ -61,7 +61,7 @@ const Filters = memo( function Filters( { fields, view, onChangeView } ) { const filterComponents = [ addFilter, ...filters.map( ( filter ) => { - if ( ! filter.isVisible || view.type === LAYOUT_LIST ) { + if ( ! filter.isVisible ) { return null; } From 4107dd2b5c882c81f37096a57e6aedf917609301 Mon Sep 17 00:00:00 2001 From: Nik Tsekouras Date: Fri, 2 Feb 2024 15:13:27 +0200 Subject: [PATCH 04/29] Update packages/dataviews/src/add-filter.js Co-authored-by: Andrew Hayward --- packages/dataviews/src/add-filter.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/dataviews/src/add-filter.js b/packages/dataviews/src/add-filter.js index ee7d8ba9a0db94..08604ad3a00e62 100644 --- a/packages/dataviews/src/add-filter.js +++ b/packages/dataviews/src/add-filter.js @@ -37,7 +37,7 @@ export default function AddFilter( { filters, view, onChangeView } ) { variant="secondary" disabled={ ! inactiveFilters.length } > - { __( 'Add fiter' ) } + { __( 'Add filter' ) } } style={ { From f5704183e2be2bbbf439d524892f85b3224f489b Mon Sep 17 00:00:00 2001 From: ntsekouras Date: Fri, 2 Feb 2024 16:25:30 +0200 Subject: [PATCH 05/29] fix reset filter/filters --- packages/dataviews/src/filter-summary.js | 84 ++++++++++++------------ packages/dataviews/src/filters.js | 1 - packages/dataviews/src/reset-filters.js | 24 +++---- 3 files changed, 50 insertions(+), 59 deletions(-) diff --git a/packages/dataviews/src/filter-summary.js b/packages/dataviews/src/filter-summary.js index b6857b60e2fcc3..968620f5cc41d7 100644 --- a/packages/dataviews/src/filter-summary.js +++ b/packages/dataviews/src/filter-summary.js @@ -66,52 +66,50 @@ function OperatorSelector( { filter, view, onChangeView } ) { return ( { filter.name } - { - const newFilters = currentFilter - ? [ - ...view.filters.map( ( _filter ) => { - if ( _filter.field === filter.field ) { - return { - ..._filter, - operator: newValue, - }; - } - return _filter; - } ), - ] - : [ - ...view.filters, - { - field: filter.field, - operator: newValue, - }, - ]; - onChangeView( { - ...view, - page: 1, - filters: newFilters, - } ); - } } - size="compact" - __nextHasNoMarginBottom - hideLabelFromVision - /> + { operatorOptions.length > 1 && ( + { + const newFilters = currentFilter + ? [ + ...view.filters.map( ( _filter ) => { + if ( _filter.field === filter.field ) { + return { + ..._filter, + operator: newValue, + }; + } + return _filter; + } ), + ] + : [ + ...view.filters, + { + field: filter.field, + operator: newValue, + }, + ]; + onChangeView( { + ...view, + page: 1, + filters: newFilters, + } ); + } } + size="compact" + __nextHasNoMarginBottom + hideLabelFromVision + /> + ) } ); } -function ResetFilter( { filter, view, onChangeView, filters } ) { - const isPrimary = ( field ) => - filters.some( ( f ) => f.field === field && f.isPrimary ); - const isDisabled = ! view.filters?.some( - ( _filter ) => - _filter.value !== undefined || ! isPrimary( _filter.field ) - ); +function ResetFilter( { filter, view, onChangeView } ) { + const isDisabled = + view.filters.find( ( _filter ) => _filter.field === filter.field ) + ?.value === undefined && filter.isPrimary; return ( ); } diff --git a/packages/dataviews/src/filters.js b/packages/dataviews/src/filters.js index 2bb6ce7a6c9ab4..8750feba6d0a79 100644 --- a/packages/dataviews/src/filters.js +++ b/packages/dataviews/src/filters.js @@ -71,7 +71,6 @@ const Filters = memo( function Filters( { fields, view, onChangeView } ) { filter={ filter } view={ view } onChangeView={ onChangeView } - filters={ filters } /> ); } ), diff --git a/packages/dataviews/src/reset-filters.js b/packages/dataviews/src/reset-filters.js index 97a13d80b383f8..10149bf331c8ea 100644 --- a/packages/dataviews/src/reset-filters.js +++ b/packages/dataviews/src/reset-filters.js @@ -6,21 +6,15 @@ import { __ } from '@wordpress/i18n'; export default function ResetFilter( { filters, view, onChangeView } ) { const isPrimary = ( field ) => - filters.some( ( f ) => f.field === field && f.isPrimary ); - let isDisabled = true; - if ( view.search !== '' ) { - isDisabled = false; - } else if ( - view.filters?.length > 0 && - ( view.filters.some( ( filter ) => filter.value !== undefined ) || - view.filters.some( - ( filter ) => - filter.value === undefined && ! isPrimary( filter.field ) - ) ) - ) { - isDisabled = false; - } - + filters.some( + ( _filter ) => _filter.field === field && _filter.isPrimary + ); + const isDisabled = + ! view.search && + ! view.filters?.some( + ( _filter ) => + _filter.value !== undefined || ! isPrimary( _filter.field ) + ); return ( } - style={ { - minWidth: '230px', - } } > { inactiveFilters.map( ( filter ) => { return ( diff --git a/packages/dataviews/src/filters.js b/packages/dataviews/src/filters.js index 8750feba6d0a79..518ec250a6c53b 100644 --- a/packages/dataviews/src/filters.js +++ b/packages/dataviews/src/filters.js @@ -59,7 +59,6 @@ const Filters = memo( function Filters( { fields, view, onChangeView } ) { /> ); const filterComponents = [ - addFilter, ...filters.map( ( filter ) => { if ( ! filter.isVisible ) { return null; @@ -74,6 +73,7 @@ const Filters = memo( function Filters( { fields, view, onChangeView } ) { /> ); } ), + addFilter, ]; if ( filterComponents.length > 1 && view.type !== LAYOUT_LIST ) { From b2de9b4329fb4bf7a7f77cca272e3ce17814192b Mon Sep 17 00:00:00 2001 From: James Koster Date: Mon, 5 Feb 2024 14:44:54 +0000 Subject: [PATCH 12/29] Condition selector --- packages/dataviews/src/filter-summary.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/dataviews/src/filter-summary.js b/packages/dataviews/src/filter-summary.js index 1395410688fa4e..223eb29f84030a 100644 --- a/packages/dataviews/src/filter-summary.js +++ b/packages/dataviews/src/filter-summary.js @@ -70,10 +70,9 @@ function OperatorSelector( { filter, view, onChangeView } ) { justify="flex-start" className="dataviews-filter-summary__operators-container" > - { filter.name } { operatorOptions.length > 1 && ( { @@ -102,9 +101,9 @@ function OperatorSelector( { filter, view, onChangeView } ) { filters: newFilters, } ); } } - size="compact" + size="small" __nextHasNoMarginBottom - hideLabelFromVision + labelPosition="side" /> ) } From dbb3300ef8d2c573b8e3477a5ac6aaf5e959182e Mon Sep 17 00:00:00 2001 From: James Koster Date: Mon, 5 Feb 2024 15:34:01 +0000 Subject: [PATCH 13/29] Filter config popover --- packages/dataviews/src/filter-summary.js | 39 ++++++++++++------------ packages/dataviews/src/search-widget.js | 1 - packages/dataviews/src/style.scss | 33 +++++++++++--------- 3 files changed, 39 insertions(+), 34 deletions(-) diff --git a/packages/dataviews/src/filter-summary.js b/packages/dataviews/src/filter-summary.js index 223eb29f84030a..26b2fffda03d26 100644 --- a/packages/dataviews/src/filter-summary.js +++ b/packages/dataviews/src/filter-summary.js @@ -115,24 +115,26 @@ function ResetFilter( { filter, view, onChangeView } ) { view.filters.find( ( _filter ) => _filter.field === filter.field ) ?.value === undefined && filter.isPrimary; return ( - +
+ +
); } @@ -169,7 +171,6 @@ export default function FilterSummary( props ) { -
); diff --git a/packages/dataviews/src/search-widget.js b/packages/dataviews/src/search-widget.js index 0a0a80c85afae9..107e58db00a3d4 100644 --- a/packages/dataviews/src/search-widget.js +++ b/packages/dataviews/src/search-widget.js @@ -93,7 +93,6 @@ export default function SearchWidget( { filter, view, onChangeView } ) {
-
Date: Tue, 6 Feb 2024 11:17:40 +0200 Subject: [PATCH 14/29] address feedback part3 --- packages/dataviews/src/filter-summary.js | 7 +++++-- packages/dataviews/src/filters.js | 7 ++++--- packages/dataviews/src/search-widget.js | 3 +-- packages/dataviews/src/style.scss | 15 ++++++++++++++- 4 files changed, 24 insertions(+), 8 deletions(-) diff --git a/packages/dataviews/src/filter-summary.js b/packages/dataviews/src/filter-summary.js index 26b2fffda03d26..d3ff27c840da5b 100644 --- a/packages/dataviews/src/filter-summary.js +++ b/packages/dataviews/src/filter-summary.js @@ -66,10 +66,13 @@ function OperatorSelector( { filter, view, onChangeView } ) { const value = currentFilter?.operator || filter.operators[ 0 ]; return ( + + { filter.name } + { operatorOptions.length > 1 && ( ) } diff --git a/packages/dataviews/src/filters.js b/packages/dataviews/src/filters.js index 518ec250a6c53b..54f8ede1ce4bea 100644 --- a/packages/dataviews/src/filters.js +++ b/packages/dataviews/src/filters.js @@ -10,7 +10,7 @@ import FilterSummary from './filter-summary'; import AddFilter from './add-filter'; import ResetFilters from './reset-filters'; import { sanitizeOperators } from './utils'; -import { ENUMERATION_TYPE, LAYOUT_LIST } from './constants'; +import { ENUMERATION_TYPE } from './constants'; const Filters = memo( function Filters( { fields, view, onChangeView } ) { const filters = []; @@ -49,7 +49,8 @@ const Filters = memo( function Filters( { fields, view, onChangeView } ) { } ); } } ); - + // Sort filters by primary property. We need the primary filters to be first. + filters.sort( ( a, b ) => b.isPrimary - a.isPrimary ); // Type coercion to numbers. const addFilter = ( 1 && view.type !== LAYOUT_LIST ) { + if ( filterComponents.length > 1 ) { filterComponents.push( @@ -105,7 +105,6 @@ export default function SearchWidget( { filter, view, onChangeView } ) { className="dataviews-search-widget-filter-combobox-item" hideOnClick={ false } setValueOnClick={ false } - focusOnHover > { selectedValues === element.value && ( diff --git a/packages/dataviews/src/style.scss b/packages/dataviews/src/style.scss index f3c8ea29f56144..765003df8803c0 100644 --- a/packages/dataviews/src/style.scss +++ b/packages/dataviews/src/style.scss @@ -514,6 +514,11 @@ box-sizing: border-box; padding: $grid-unit-10 $grid-unit-15; cursor: default; + margin-block-end: 2px; + + &:last-child { + margin-block-end: 0; + } &[data-active-item] { background-color: var(--wp-components-color-accent, var(--wp-admin-theme-color, #3858e9)); @@ -619,9 +624,17 @@ &:empty { display: none; } + + .dataviews-filter-summary__operators-filter-name { + font-size: 11px; + font-weight: 500; + line-height: 1.4; + color: $gray-900; + text-transform: uppercase; + } } .dataviews-filter-summary__reset { padding: $grid-unit-05; border-top: 1px solid $gray-200; -} \ No newline at end of file +} From 3a5a9523b7d296af6213f145992b8fc239200574 Mon Sep 17 00:00:00 2001 From: ntsekouras Date: Tue, 6 Feb 2024 12:40:36 +0200 Subject: [PATCH 15/29] handle focus loss on removing filter and on newly added ones when closing --- packages/dataviews/src/add-filter.js | 6 +++++- packages/dataviews/src/filter-summary.js | 16 ++++++++++++++-- packages/dataviews/src/filters.js | 5 ++++- 3 files changed, 23 insertions(+), 4 deletions(-) diff --git a/packages/dataviews/src/add-filter.js b/packages/dataviews/src/add-filter.js index b6c328d2e26194..9d6111e3e94634 100644 --- a/packages/dataviews/src/add-filter.js +++ b/packages/dataviews/src/add-filter.js @@ -8,6 +8,7 @@ import { import { plus } from '@wordpress/icons'; import { __ } from '@wordpress/i18n'; import { useDispatch } from '@wordpress/data'; +import { forwardRef } from '@wordpress/element'; /** * Internal dependencies @@ -21,7 +22,7 @@ const { DropdownMenuItemLabelV2: DropdownMenuItemLabel, } = unlock( componentsPrivateApis ); -export default function AddFilter( { filters, view, onChangeView } ) { +function AddFilter( { filters, view, onChangeView }, ref ) { const { setOpenFilterOnMount } = useDispatch( dataviewsStore ); if ( filters.length === 0 ) { return null; @@ -37,6 +38,7 @@ export default function AddFilter( { filters, view, onChangeView } ) { className="dataviews-filters-button" variant="tertiary" disabled={ ! inactiveFilters.length } + ref={ ref } > { __( 'Add filter' ) } @@ -70,3 +72,5 @@ export default function AddFilter( { filters, view, onChangeView } ) { ); } + +export default forwardRef( AddFilter ); diff --git a/packages/dataviews/src/filter-summary.js b/packages/dataviews/src/filter-summary.js index d3ff27c840da5b..421ec5f3598ae8 100644 --- a/packages/dataviews/src/filter-summary.js +++ b/packages/dataviews/src/filter-summary.js @@ -11,6 +11,7 @@ import { } from '@wordpress/components'; import { __, sprintf } from '@wordpress/i18n'; import { useSelect } from '@wordpress/data'; +import { useRef } from '@wordpress/element'; /** * Internal dependencies @@ -113,7 +114,7 @@ function OperatorSelector( { filter, view, onChangeView } ) { ); } -function ResetFilter( { filter, view, onChangeView } ) { +function ResetFilter( { filter, view, onChangeView, addFilterRef } ) { const isDisabled = view.filters.find( ( _filter ) => _filter.field === filter.field ) ?.value === undefined && filter.isPrimary; @@ -133,6 +134,11 @@ function ResetFilter( { filter, view, onChangeView } ) { ( _filter ) => _filter.field !== filter.field ), } ); + // If the filter is not primary and can be removed, it will be added + // back to the available filters from `Add filter` component. + if ( ! filter.isPrimary ) { + addFilterRef.current?.focus(); + } } } > { filter.isPrimary ? __( 'Reset' ) : __( 'Remove' ) } @@ -142,6 +148,7 @@ function ResetFilter( { filter, view, onChangeView } ) { } export default function FilterSummary( props ) { + const toggleRef = useRef(); const { filter, view } = props; const filterToOpenOnMount = useSelect( ( select ) => select( dataviewsStore ).getOpenFilterOnMount(), @@ -156,11 +163,16 @@ export default function FilterSummary( props ) { defaultOpen={ filterToOpenOnMount === filter.field } contentClassName="dataviews-filter-summary__popover" popoverProps={ { placement: 'bottom-start', role: 'dialog' } } - renderToggle={ ( { onToggle } ) => ( + onClose={ () => { + toggleRef.current?.focus(); + } } + renderToggle={ ( { isOpen, onToggle } ) => (