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();