diff --git a/packages/dataviews/CHANGELOG.md b/packages/dataviews/CHANGELOG.md index a8be2d4e685190..cd61d8d5918b27 100644 --- a/packages/dataviews/CHANGELOG.md +++ b/packages/dataviews/CHANGELOG.md @@ -9,6 +9,10 @@ - Two new operators have been added: `isAll` and `isNotAll`. These are meant to represent `AND` operations. For example, `Category is all: Book, Review, Science Fiction` would represent all items that have all three categories selected. - 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. +### Breaking changes + +- Removed the `getPaginationResults` and `sortByTextFields` utils and replaced them with a unique `filterSortAndPaginate` function. + ## 0.7.0 (2024-03-06) ## 0.6.0 (2024-02-21) diff --git a/packages/dataviews/src/dataviews.js b/packages/dataviews/src/dataviews.js index ff658ecebfeb60..8b7d3f4819c8f6 100644 --- a/packages/dataviews/src/dataviews.js +++ b/packages/dataviews/src/dataviews.js @@ -13,6 +13,7 @@ import Filters from './filters'; import Search from './search'; import { VIEW_LAYOUTS, LAYOUT_TABLE, LAYOUT_GRID } from './constants'; import BulkActions from './bulk-actions'; +import { normalizeFields } from './normalize-fields'; const defaultGetItemId = ( item ) => item.id; const defaultOnSelectionChange = () => {}; @@ -76,18 +77,7 @@ export default function DataViews( { const ViewComponent = VIEW_LAYOUTS.find( ( v ) => v.type === view.type ).component; - const _fields = useMemo( () => { - return fields.map( ( field ) => { - const getValue = - field.getValue || ( ( { item } ) => item[ field.id ] ); - - return { - ...field, - getValue, - render: field.render || getValue, - }; - } ); - }, [ fields ] ); + const _fields = useMemo( () => normalizeFields( fields ), [ fields ] ); const hasPossibleBulkAction = useSomeItemHasAPossibleBulkAction( actions, diff --git a/packages/dataviews/src/filter-and-sort-data-view.js b/packages/dataviews/src/filter-and-sort-data-view.js new file mode 100644 index 00000000000000..590078047a0f66 --- /dev/null +++ b/packages/dataviews/src/filter-and-sort-data-view.js @@ -0,0 +1,154 @@ +/** + * External dependencies + */ +import removeAccents from 'remove-accents'; + +/** + * Internal dependencies + */ +import { + OPERATOR_IS, + OPERATOR_IS_NOT, + OPERATOR_IS_NONE, + OPERATOR_IS_ANY, + OPERATOR_IS_ALL, + OPERATOR_IS_NOT_ALL, +} from './constants'; +import { normalizeFields } from './normalize-fields'; + +function normalizeSearchInput( input = '' ) { + return removeAccents( input.trim().toLowerCase() ); +} + +const EMPTY_ARRAY = []; + +/** + * Applies the filtering, sorting and pagination to the raw data based on the view configuration. + * + * @param {any[]} data Raw data. + * @param {Object} view View config. + * @param {Object[]} fields Fields config. + * + * @return {Object} { data: any[], paginationInfo: { totalItems: number, totalPages: number } } + */ +export function filterSortAndPaginate( data, view, fields ) { + if ( ! data ) { + return { + data: EMPTY_ARRAY, + paginationInfo: { totalItems: 0, totalPages: 0 }, + }; + } + const _fields = normalizeFields( fields ); + let filteredData = [ ...data ]; + // Handle global search. + if ( view.search ) { + const normalizedSearch = normalizeSearchInput( view.search ); + filteredData = filteredData.filter( ( item ) => { + return _fields + .filter( ( field ) => field.enableGlobalSearch ) + .map( ( field ) => { + return normalizeSearchInput( field.getValue( { item } ) ); + } ) + .some( ( field ) => field.includes( normalizedSearch ) ); + } ); + } + + if ( view.filters.length > 0 ) { + view.filters.forEach( ( filter ) => { + const field = _fields.find( + ( _field ) => _field.id === filter.field + ); + if ( + filter.operator === OPERATOR_IS_ANY && + filter?.value?.length > 0 + ) { + filteredData = filteredData.filter( ( item ) => { + const fieldValue = field.getValue( { item } ); + if ( Array.isArray( fieldValue ) ) { + return filter.value.some( ( filterValue ) => + fieldValue.includes( filterValue ) + ); + } else if ( typeof fieldValue === 'string' ) { + return filter.value.includes( fieldValue ); + } + return false; + } ); + } else if ( + filter.operator === OPERATOR_IS_NONE && + filter?.value?.length > 0 + ) { + filteredData = filteredData.filter( ( item ) => { + const fieldValue = field.getValue( { item } ); + if ( Array.isArray( fieldValue ) ) { + return ! filter.value.some( ( filterValue ) => + fieldValue.includes( filterValue ) + ); + } else if ( typeof fieldValue === 'string' ) { + return ! filter.value.includes( fieldValue ); + } + return false; + } ); + } else if ( + filter.operator === OPERATOR_IS_ALL && + filter?.value?.length > 0 + ) { + filteredData = filteredData.filter( ( item ) => { + return filter.value.every( ( value ) => { + return field.getValue( { item } ).includes( value ); + } ); + } ); + } else if ( + filter.operator === OPERATOR_IS_NOT_ALL && + filter?.value?.length > 0 + ) { + filteredData = filteredData.filter( ( item ) => { + return filter.value.every( ( value ) => { + return ! field.getValue( { item } ).includes( value ); + } ); + } ); + } else if ( filter.operator === OPERATOR_IS ) { + filteredData = filteredData.filter( ( item ) => { + return filter.value === field.getValue( { item } ); + } ); + } else if ( filter.operator === OPERATOR_IS_NOT ) { + filteredData = filteredData.filter( ( item ) => { + return filter.value !== field.getValue( { item } ); + } ); + } + } ); + } + + // Handle sorting. + if ( view.sort ) { + const fieldId = view.sort.field; + const fieldToSort = _fields.find( ( field ) => { + return field.id === fieldId; + } ); + filteredData.sort( ( a, b ) => { + const valueA = fieldToSort.getValue( { item: a } ) ?? ''; + const valueB = fieldToSort.getValue( { item: b } ) ?? ''; + return view.sort.direction === 'asc' + ? valueA.localeCompare( valueB ) + : valueB.localeCompare( valueA ); + } ); + } + + // Handle pagination. + const hasPagination = view.page && view.perPage; + const start = hasPagination ? ( view.page - 1 ) * view.perPage : 0; + const totalItems = filteredData?.length || 0; + const totalPages = hasPagination + ? Math.ceil( totalItems / view.perPage ) + : 1; + filteredData = hasPagination + ? filteredData?.slice( start, start + view.perPage ) + : filteredData; + + return { + data: filteredData, + paginationInfo: { + totalItems, + totalPages, + }, + }; +} diff --git a/packages/dataviews/src/index.js b/packages/dataviews/src/index.js index 7b0a5d35abf92d..6b23f450a97e1c 100644 --- a/packages/dataviews/src/index.js +++ b/packages/dataviews/src/index.js @@ -1,3 +1,3 @@ export { default as DataViews } from './dataviews'; -export { sortByTextFields, getPaginationResults } from './utils'; export { VIEW_LAYOUTS } from './constants'; +export { filterSortAndPaginate } from './filter-and-sort-data-view'; diff --git a/packages/dataviews/src/normalize-fields.js b/packages/dataviews/src/normalize-fields.js new file mode 100644 index 00000000000000..db43de31dfc0d1 --- /dev/null +++ b/packages/dataviews/src/normalize-fields.js @@ -0,0 +1,17 @@ +/** + * Apply default values and normalize the fields config. + * + * @param {Object[]} fields Raw Fields. + * @return {Object[]} Normalized fields. + */ +export function normalizeFields( fields ) { + return fields.map( ( field ) => { + const getValue = field.getValue || ( ( { item } ) => item[ field.id ] ); + + return { + ...field, + getValue, + render: field.render || getValue, + }; + } ); +} diff --git a/packages/dataviews/src/stories/fixtures.js b/packages/dataviews/src/stories/fixtures.js index d9413cef89db5c..48b93084fa830d 100644 --- a/packages/dataviews/src/stories/fixtures.js +++ b/packages/dataviews/src/stories/fixtures.js @@ -21,6 +21,7 @@ export const data = [ description: 'Apollo description', image: 'https://live.staticflickr.com/5725/21726228300_51333bd62c_b.jpg', type: 'Not a planet', + categories: [ 'Space', 'NASA' ], }, { id: 2, @@ -28,6 +29,7 @@ export const data = [ description: 'Space description', image: 'https://live.staticflickr.com/5678/21911065441_92e2d44708_b.jpg', type: 'Not a planet', + categories: [ 'Space' ], }, { id: 3, @@ -35,6 +37,7 @@ export const data = [ description: 'NASA photo', image: 'https://live.staticflickr.com/742/21712365770_8f70a2c91e_b.jpg', type: 'Not a planet', + categories: [ 'NASA' ], }, { id: 4, @@ -42,6 +45,7 @@ export const data = [ description: 'Neptune description', image: 'https://live.staticflickr.com/5725/21726228300_51333bd62c_b.jpg', type: 'Ice giant', + categories: [ 'Space', 'Planet', 'Solar system' ], }, { id: 5, @@ -49,13 +53,15 @@ export const data = [ description: 'Mercury description', image: 'https://live.staticflickr.com/5725/21726228300_51333bd62c_b.jpg', type: 'Terrestrial', + categories: [ 'Space', 'Planet', 'Solar system' ], }, { id: 6, title: 'Venus', - description: 'Venus description', + description: 'La planète Vénus', image: 'https://live.staticflickr.com/5725/21726228300_51333bd62c_b.jpg', type: 'Terrestrial', + categories: [ 'Space', 'Planet', 'Solar system' ], }, { id: 7, @@ -63,6 +69,7 @@ export const data = [ description: 'Earth description', image: 'https://live.staticflickr.com/5725/21726228300_51333bd62c_b.jpg', type: 'Terrestrial', + categories: [ 'Space', 'Planet', 'Solar system' ], }, { id: 8, @@ -70,6 +77,7 @@ export const data = [ description: 'Mars description', image: 'https://live.staticflickr.com/5725/21726228300_51333bd62c_b.jpg', type: 'Terrestrial', + categories: [ 'Space', 'Planet', 'Solar system' ], }, { id: 9, @@ -77,6 +85,7 @@ export const data = [ description: 'Jupiter description', image: 'https://live.staticflickr.com/5725/21726228300_51333bd62c_b.jpg', type: 'Gas giant', + categories: [ 'Space', 'Planet', 'Solar system' ], }, { id: 10, @@ -84,6 +93,7 @@ export const data = [ description: 'Saturn description', image: 'https://live.staticflickr.com/5725/21726228300_51333bd62c_b.jpg', type: 'Gas giant', + categories: [ 'Space', 'Planet', 'Solar system' ], }, { id: 11, @@ -91,6 +101,7 @@ export const data = [ description: 'Uranus description', image: 'https://live.staticflickr.com/5725/21726228300_51333bd62c_b.jpg', type: 'Ice giant', + categories: [ 'Space', 'Ice giant', 'Solar system' ], }, ]; @@ -135,3 +146,66 @@ export const actions = [ callback() {}, }, ]; + +export const fields = [ + { + header: 'Image', + id: 'image', + render: ( { item } ) => { + return ( + + ); + }, + width: 50, + enableSorting: false, + }, + { + header: 'Title', + id: 'title', + maxWidth: 400, + enableHiding: false, + enableGlobalSearch: true, + }, + { + header: 'Type', + id: 'type', + maxWidth: 400, + enableHiding: false, + type: 'enumeration', + elements: [ + { value: 'Not a planet', label: 'Not a planet' }, + { value: 'Ice giant', label: 'Ice giant' }, + { value: 'Terrestrial', label: 'Terrestrial' }, + { value: 'Gas giant', label: 'Gas giant' }, + ], + }, + { + header: 'Description', + id: 'description', + maxWidth: 200, + enableSorting: false, + enableGlobalSearch: true, + }, + { + header: 'Categories', + id: 'categories', + type: 'enumeration', + elements: [ + { value: 'Space', label: 'Space' }, + { value: 'NASA', label: 'NASA' }, + { value: 'Planet', label: 'Planet' }, + { value: 'Solar system', label: 'Solar system' }, + { value: 'Ice giant', label: 'Ice giant' }, + ], + filterBy: { + operators: [ 'isAny', 'isNone', 'isAll', 'isNotAll' ], + }, + getValue: ( { item } ) => { + return item.categories; + }, + render: ( { item } ) => { + return item.categories.join( ',' ); + }, + enableSorting: false, + }, +]; diff --git a/packages/dataviews/src/stories/index.story.js b/packages/dataviews/src/stories/index.story.js index f064098fefef51..bf4c1e1b1dba86 100644 --- a/packages/dataviews/src/stories/index.story.js +++ b/packages/dataviews/src/stories/index.story.js @@ -7,13 +7,9 @@ import { useState, useMemo, useCallback } from '@wordpress/element'; * Internal dependencies */ import { DataViews } from '../index'; -import { DEFAULT_VIEW, actions, data } from './fixtures'; -import { - LAYOUT_GRID, - LAYOUT_TABLE, - OPERATOR_IS_NONE, - OPERATOR_IS_ANY, -} from '../constants'; +import { DEFAULT_VIEW, actions, data, fields } from './fixtures'; +import { LAYOUT_GRID, LAYOUT_TABLE } from '../constants'; +import { filterSortAndPaginate } from '../filter-and-sort-data-view'; const meta = { title: 'DataViews/DataViews', @@ -31,114 +27,10 @@ const defaultConfigPerViewType = { }, }; -function normalizeSearchInput( input = '' ) { - return input.trim().toLowerCase(); -} - -const fields = [ - { - header: 'Image', - id: 'image', - render: ( { item } ) => { - return ( - - ); - }, - width: 50, - enableSorting: false, - }, - { - header: 'Title', - id: 'title', - maxWidth: 400, - enableHiding: false, - }, - { - header: 'Type', - id: 'type', - maxWidth: 400, - enableHiding: false, - type: 'enumeration', - elements: [ - { value: 'Not a planet', label: 'Not a planet' }, - { value: 'Ice giant', label: 'Ice giant' }, - { value: 'Terrestrial', label: 'Terrestrial' }, - { value: 'Gas giant', label: 'Gas giant' }, - ], - }, - { - header: 'Description', - id: 'description', - maxWidth: 200, - enableSorting: false, - }, -]; - export const Default = ( props ) => { const [ view, setView ] = useState( DEFAULT_VIEW ); - const { shownData, paginationInfo } = useMemo( () => { - let filteredData = [ ...data ]; - // Handle global search. - if ( view.search ) { - const normalizedSearch = normalizeSearchInput( view.search ); - filteredData = filteredData.filter( ( item ) => { - return [ - normalizeSearchInput( item.title ), - normalizeSearchInput( item.description ), - ].some( ( field ) => field.includes( normalizedSearch ) ); - } ); - } - - if ( view.filters.length > 0 ) { - view.filters.forEach( ( filter ) => { - if ( - filter.field === 'type' && - filter.operator === OPERATOR_IS_ANY && - filter?.value?.length > 0 - ) { - filteredData = filteredData.filter( ( item ) => { - return filter.value.includes( item.type ); - } ); - } else if ( - filter.field === 'type' && - filter.operator === OPERATOR_IS_NONE && - filter?.value?.length > 0 - ) { - filteredData = filteredData.filter( ( item ) => { - return ! filter.value.includes( item.type ); - } ); - } - } ); - } - - // Handle sorting. - if ( view.sort ) { - const stringSortingFields = [ 'title' ]; - const fieldId = view.sort.field; - if ( stringSortingFields.includes( fieldId ) ) { - const fieldToSort = fields.find( ( field ) => { - return field.id === fieldId; - } ); - filteredData.sort( ( a, b ) => { - const valueA = fieldToSort.getValue( { item: a } ) ?? ''; - const valueB = fieldToSort.getValue( { item: b } ) ?? ''; - return view.sort.direction === 'asc' - ? valueA.localeCompare( valueB ) - : valueB.localeCompare( valueA ); - } ); - } - } - // Handle pagination. - const start = ( view.page - 1 ) * view.perPage; - const totalItems = filteredData?.length || 0; - filteredData = filteredData?.slice( start, start + view.perPage ); - return { - shownData: filteredData, - paginationInfo: { - totalItems, - totalPages: Math.ceil( totalItems / view.perPage ), - }, - }; + const { data: shownData, paginationInfo } = useMemo( () => { + return filterSortAndPaginate( data, view, fields ); }, [ view ] ); const onChangeView = useCallback( ( newView ) => { diff --git a/packages/dataviews/src/test/filter-and-sort-data-view.js b/packages/dataviews/src/test/filter-and-sort-data-view.js new file mode 100644 index 00000000000000..763502762441d7 --- /dev/null +++ b/packages/dataviews/src/test/filter-and-sort-data-view.js @@ -0,0 +1,276 @@ +/** + * Internal dependencies + */ +import { filterSortAndPaginate } from '../filter-and-sort-data-view'; +import { data, fields } from '../stories/fixtures'; + +describe( 'filters', () => { + it( 'should return empty if the data is empty', () => { + expect( filterSortAndPaginate( null, {}, [] ) ).toStrictEqual( { + data: [], + paginationInfo: { totalItems: 0, totalPages: 0 }, + } ); + } ); + + it( 'should return the same data if no filters are applied', () => { + expect( + filterSortAndPaginate( + data, + { + filters: [], + }, + [] + ) + ).toStrictEqual( { + data, + paginationInfo: { totalItems: data.length, totalPages: 1 }, + } ); + } ); + + it( 'should search using searchable fields (title)', () => { + const { data: result } = filterSortAndPaginate( + data, + { + search: 'Neptu', + filters: [], + }, + fields + ); + expect( result ).toHaveLength( 1 ); + expect( result[ 0 ].title ).toBe( 'Neptune' ); + } ); + + it( 'should search using searchable fields (description)', () => { + const { data: result } = filterSortAndPaginate( + data, + { + search: 'photo', + filters: [], + }, + fields + ); + expect( result ).toHaveLength( 1 ); + expect( result[ 0 ].description ).toBe( 'NASA photo' ); + } ); + + it( 'should perform case-insensitive and accent-insensitive search', () => { + const { data: result } = filterSortAndPaginate( + data, + { + search: 'nete ven', + filters: [], + }, + fields + ); + expect( result ).toHaveLength( 1 ); + expect( result[ 0 ].description ).toBe( 'La planète Vénus' ); + } ); + + it( 'should search using IS filter', () => { + const { data: result } = filterSortAndPaginate( + data, + { + filters: [ + { + field: 'type', + operator: 'is', + value: 'Ice giant', + }, + ], + }, + fields + ); + expect( result ).toHaveLength( 2 ); + expect( result[ 0 ].title ).toBe( 'Neptune' ); + expect( result[ 1 ].title ).toBe( 'Uranus' ); + } ); + + it( 'should search using IS NOT filter', () => { + const { data: result } = filterSortAndPaginate( + data, + { + filters: [ + { + field: 'type', + operator: 'isNot', + value: 'Ice giant', + }, + ], + }, + fields + ); + expect( result ).toHaveLength( 9 ); + expect( result[ 0 ].title ).toBe( 'Apollo' ); + expect( result[ 1 ].title ).toBe( 'Space' ); + expect( result[ 2 ].title ).toBe( 'NASA' ); + expect( result[ 3 ].title ).toBe( 'Mercury' ); + expect( result[ 4 ].title ).toBe( 'Venus' ); + expect( result[ 5 ].title ).toBe( 'Earth' ); + expect( result[ 6 ].title ).toBe( 'Mars' ); + expect( result[ 7 ].title ).toBe( 'Jupiter' ); + expect( result[ 8 ].title ).toBe( 'Saturn' ); + } ); + + it( 'should search using IS ANY filter for STRING values', () => { + const { data: result } = filterSortAndPaginate( + data, + { + filters: [ + { + field: 'type', + operator: 'isAny', + value: [ 'Ice giant' ], + }, + ], + }, + fields + ); + expect( result ).toHaveLength( 2 ); + expect( result[ 0 ].title ).toBe( 'Neptune' ); + expect( result[ 1 ].title ).toBe( 'Uranus' ); + } ); + + it( 'should search using IS NONE filter for STRING values', () => { + const { data: result } = filterSortAndPaginate( + data, + { + filters: [ + { + field: 'type', + operator: 'isNone', + value: [ 'Ice giant', 'Gas giant', 'Terrestrial' ], + }, + ], + }, + fields + ); + expect( result ).toHaveLength( 3 ); + expect( result[ 0 ].title ).toBe( 'Apollo' ); + expect( result[ 1 ].title ).toBe( 'Space' ); + expect( result[ 2 ].title ).toBe( 'NASA' ); + } ); + + it( 'should search using IS ANY filter for ARRAY values', () => { + const { data: result } = filterSortAndPaginate( + data, + { + filters: [ + { + field: 'categories', + operator: 'isAny', + value: [ 'NASA' ], + }, + ], + }, + fields + ); + expect( result ).toHaveLength( 2 ); + expect( result[ 0 ].title ).toBe( 'Apollo' ); + expect( result[ 1 ].title ).toBe( 'NASA' ); + } ); + + it( 'should search using IS NONE filter for ARRAY values', () => { + const { data: result } = filterSortAndPaginate( + data, + { + filters: [ + { + field: 'categories', + operator: 'isNone', + value: [ 'Space' ], + }, + ], + }, + fields + ); + expect( result ).toHaveLength( 1 ); + expect( result[ 0 ].title ).toBe( 'NASA' ); + } ); + + it( 'should search using IS ALL filter', () => { + const { data: result } = filterSortAndPaginate( + data, + { + filters: [ + { + field: 'categories', + operator: 'isAll', + value: [ 'Planet', 'Solar system' ], + }, + ], + }, + fields + ); + expect( result ).toHaveLength( 7 ); + expect( result[ 0 ].title ).toBe( 'Neptune' ); + expect( result[ 1 ].title ).toBe( 'Mercury' ); + expect( result[ 2 ].title ).toBe( 'Venus' ); + expect( result[ 3 ].title ).toBe( 'Earth' ); + expect( result[ 4 ].title ).toBe( 'Mars' ); + expect( result[ 5 ].title ).toBe( 'Jupiter' ); + expect( result[ 6 ].title ).toBe( 'Saturn' ); + } ); + + it( 'should search using IS NOT ALL filter', () => { + const { data: result } = filterSortAndPaginate( + data, + { + filters: [ + { + field: 'categories', + operator: 'isNotAll', + value: [ 'Planet', 'Solar system' ], + }, + ], + }, + fields + ); + expect( result ).toHaveLength( 3 ); + expect( result[ 0 ].title ).toBe( 'Apollo' ); + expect( result[ 1 ].title ).toBe( 'Space' ); + expect( result[ 2 ].title ).toBe( 'NASA' ); + } ); +} ); + +describe( 'sorting', () => { + it( 'should sort', () => { + const { data: result } = filterSortAndPaginate( + data, + { + sort: { field: 'title', direction: 'desc' }, + filters: [ + { + field: 'type', + operator: 'isAny', + value: [ 'Ice giant' ], + }, + ], + }, + fields + ); + expect( result ).toHaveLength( 2 ); + expect( result[ 0 ].title ).toBe( 'Uranus' ); + expect( result[ 1 ].title ).toBe( 'Neptune' ); + } ); +} ); + +describe( 'pagination', () => { + it( 'should paginate', () => { + const { data: result, paginationInfo } = filterSortAndPaginate( + data, + { + perPage: 2, + page: 2, + filters: [], + }, + fields + ); + expect( result ).toHaveLength( 2 ); + expect( result[ 0 ].title ).toBe( 'NASA' ); + expect( result[ 1 ].title ).toBe( 'Neptune' ); + expect( paginationInfo ).toStrictEqual( { + totalItems: data.length, + totalPages: 6, + } ); + } ); +} ); diff --git a/packages/dataviews/src/utils.js b/packages/dataviews/src/utils.js index 979e4a505e57b2..cf861f36e06ace 100644 --- a/packages/dataviews/src/utils.js +++ b/packages/dataviews/src/utils.js @@ -9,58 +9,6 @@ import { OPERATOR_IS_NONE, } from './constants'; -/** - * Helper util to sort data by text fields, when sorting is done client side. - * - * @param {Object} params Function params. - * @param {Object[]} params.data Data to sort. - * @param {Object} params.view Current view object. - * @param {Object[]} params.fields Array of available fields. - * @param {string[]} params.textFields Array of the field ids to sort. - * - * @return {Object[]} Sorted data. - */ -export const sortByTextFields = ( { data, view, fields, textFields } ) => { - const sortedData = [ ...data ]; - const fieldId = view.sort.field; - if ( textFields.includes( fieldId ) ) { - const fieldToSort = fields.find( ( field ) => { - return field.id === fieldId; - } ); - sortedData.sort( ( a, b ) => { - const valueA = fieldToSort.getValue( { item: a } ) ?? ''; - const valueB = fieldToSort.getValue( { item: b } ) ?? ''; - return view.sort.direction === 'asc' - ? valueA.localeCompare( valueB ) - : valueB.localeCompare( valueA ); - } ); - } - return sortedData; -}; - -/** - * Helper util to get the paginated data and the paginateInfo needed, - * when pagination is done client side. - * - * @param {Object} params Function params. - * @param {Object[]} params.data Available data. - * @param {Object} params.view Current view object. - * - * @return {Object} Paginated data and paginationInfo. - */ -export function getPaginationResults( { data, view } ) { - const start = ( view.page - 1 ) * view.perPage; - const totalItems = data?.length || 0; - data = data?.slice( start, start + view.perPage ); - return { - data, - paginationInfo: { - totalItems, - totalPages: Math.ceil( totalItems / view.perPage ), - }, - }; -} - export const sanitizeOperators = ( field ) => { let operators = field.filterBy?.operators; diff --git a/packages/edit-site/src/components/page-patterns/index.js b/packages/edit-site/src/components/page-patterns/index.js index 9b200b48982d5f..62cfd5ff79b5cc 100644 --- a/packages/edit-site/src/components/page-patterns/index.js +++ b/packages/edit-site/src/components/page-patterns/index.js @@ -20,11 +20,7 @@ import { BlockPreview, privateApis as blockEditorPrivateApis, } from '@wordpress/block-editor'; -import { - DataViews, - sortByTextFields, - getPaginationResults, -} from '@wordpress/dataviews'; +import { DataViews, filterSortAndPaginate } from '@wordpress/dataviews'; import { Icon, header, @@ -337,27 +333,12 @@ export default function DataviewsPatterns() { } }, [ categoryId, previousCategoryId ] ); const { data, paginationInfo } = useMemo( () => { - if ( ! patterns ) { - return { - data: EMPTY_ARRAY, - paginationInfo: { totalItems: 0, totalPages: 0 }, - }; - } - let filteredData = [ ...patterns ]; - // Handle sorting. - if ( view.sort ) { - filteredData = sortByTextFields( { - data: filteredData, - view, - fields, - textFields: [ 'title', 'author' ], - } ); - } - // Handle pagination. - return getPaginationResults( { - data: filteredData, - view, - } ); + // Since filters are applied server-side, + // we need to remove them from the view + const viewWithoutFilters = { ...view }; + delete viewWithoutFilters.search; + viewWithoutFilters.filters = []; + return filterSortAndPaginate( patterns, viewWithoutFilters, fields ); }, [ patterns, view, fields ] ); const actions = useMemo( 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 32e058cd86be38..c2f3af84abce80 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 @@ -2,7 +2,6 @@ * External dependencies */ import classnames from 'classnames'; -import removeAccents from 'remove-accents'; /** * WordPress dependencies @@ -22,11 +21,7 @@ import { BlockPreview, privateApis as blockEditorPrivateApis, } from '@wordpress/block-editor'; -import { - DataViews, - sortByTextFields, - getPaginationResults, -} from '@wordpress/dataviews'; +import { DataViews, filterSortAndPaginate } from '@wordpress/dataviews'; import { privateApis as routerPrivateApis } from '@wordpress/router'; /** @@ -41,7 +36,6 @@ import { TEMPLATE_PART_POST_TYPE, ENUMERATION_TYPE, OPERATOR_IS_ANY, - OPERATOR_IS_NONE, LAYOUT_GRID, LAYOUT_TABLE, LAYOUT_LIST, @@ -93,10 +87,6 @@ const DEFAULT_VIEW = { filters: [], }; -function normalizeSearchInput( input = '' ) { - return removeAccents( input.trim().toLowerCase() ); -} - function Title( { item, viewType } ) { if ( viewType === LAYOUT_LIST ) { return decodeEntities( item.title?.rendered ) || __( '(no title)' ); @@ -297,6 +287,7 @@ export default function PageTemplatesTemplateParts( { postType } ) { ), maxWidth: 400, enableHiding: false, + enableGlobalSearch: true, }, ]; if ( postType === TEMPLATE_POST_TYPE ) { @@ -324,6 +315,7 @@ export default function PageTemplatesTemplateParts( { postType } ) { maxWidth: 400, minWidth: 320, enableSorting: false, + enableGlobalSearch: true, } ); } // TODO: The plan is to support fields reordering, which would require an API like `order` or something @@ -343,66 +335,7 @@ export default function PageTemplatesTemplateParts( { postType } ) { }, [ postType, authors, view.type ] ); const { data, paginationInfo } = useMemo( () => { - if ( ! records ) { - return { - data: EMPTY_ARRAY, - paginationInfo: { totalItems: 0, totalPages: 0 }, - }; - } - let filteredData = [ ...records ]; - // Handle global search. - if ( view.search ) { - const normalizedSearch = normalizeSearchInput( view.search ); - filteredData = filteredData.filter( ( item ) => { - const title = item.title?.rendered || item.slug; - return ( - normalizeSearchInput( title ).includes( - normalizedSearch - ) || - normalizeSearchInput( item.description ).includes( - normalizedSearch - ) - ); - } ); - } - - // Handle filters. - if ( view.filters.length > 0 ) { - view.filters.forEach( ( filter ) => { - if ( - filter.field === 'author' && - filter.operator === OPERATOR_IS_ANY && - filter?.value?.length > 0 - ) { - filteredData = filteredData.filter( ( item ) => { - return filter.value.includes( item.author_text ); - } ); - } else if ( - filter.field === 'author' && - filter.operator === OPERATOR_IS_NONE && - filter?.value?.length > 0 - ) { - filteredData = filteredData.filter( ( item ) => { - return ! filter.value.includes( item.author_text ); - } ); - } - } ); - } - - // Handle sorting. - if ( view.sort ) { - filteredData = sortByTextFields( { - data: filteredData, - view, - fields, - textFields: [ 'title', 'author' ], - } ); - } - // Handle pagination. - return getPaginationResults( { - data: filteredData, - view, - } ); + return filterSortAndPaginate( records, view, fields ); }, [ records, view, fields ] ); const resetTemplateAction = useResetTemplateAction();