Skip to content

Commit

Permalink
DataViews: Add a utility to share filtering, sorting and pagination l…
Browse files Browse the repository at this point in the history
…ogic (#59897)

Co-authored-by: youknowriad <youknowriad@git.wordpress.org>
Co-authored-by: oandregal <oandregal@git.wordpress.org>
Co-authored-by: mcsf <mcsf@git.wordpress.org>
Co-authored-by: ntsekouras <ntsekouras@git.wordpress.org>
  • Loading branch information
5 people committed Mar 21, 2024
1 parent 00efc92 commit e836da4
Show file tree
Hide file tree
Showing 11 changed files with 545 additions and 276 deletions.
4 changes: 4 additions & 0 deletions packages/dataviews/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
14 changes: 2 additions & 12 deletions packages/dataviews/src/dataviews.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = () => {};
Expand Down Expand Up @@ -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,
Expand Down
154 changes: 154 additions & 0 deletions packages/dataviews/src/filter-and-sort-data-view.js
Original file line number Diff line number Diff line change
@@ -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,
},
};
}
2 changes: 1 addition & 1 deletion packages/dataviews/src/index.js
Original file line number Diff line number Diff line change
@@ -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';
17 changes: 17 additions & 0 deletions packages/dataviews/src/normalize-fields.js
Original file line number Diff line number Diff line change
@@ -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,
};
} );
}
76 changes: 75 additions & 1 deletion packages/dataviews/src/stories/fixtures.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,76 +21,87 @@ 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,
title: 'Space',
description: 'Space description',
image: 'https://live.staticflickr.com/5678/21911065441_92e2d44708_b.jpg',
type: 'Not a planet',
categories: [ 'Space' ],
},
{
id: 3,
title: 'NASA',
description: 'NASA photo',
image: 'https://live.staticflickr.com/742/21712365770_8f70a2c91e_b.jpg',
type: 'Not a planet',
categories: [ 'NASA' ],
},
{
id: 4,
title: 'Neptune',
description: 'Neptune description',
image: 'https://live.staticflickr.com/5725/21726228300_51333bd62c_b.jpg',
type: 'Ice giant',
categories: [ 'Space', 'Planet', 'Solar system' ],
},
{
id: 5,
title: 'Mercury',
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,
title: 'Earth',
description: 'Earth description',
image: 'https://live.staticflickr.com/5725/21726228300_51333bd62c_b.jpg',
type: 'Terrestrial',
categories: [ 'Space', 'Planet', 'Solar system' ],
},
{
id: 8,
title: 'Mars',
description: 'Mars description',
image: 'https://live.staticflickr.com/5725/21726228300_51333bd62c_b.jpg',
type: 'Terrestrial',
categories: [ 'Space', 'Planet', 'Solar system' ],
},
{
id: 9,
title: 'Jupiter',
description: 'Jupiter description',
image: 'https://live.staticflickr.com/5725/21726228300_51333bd62c_b.jpg',
type: 'Gas giant',
categories: [ 'Space', 'Planet', 'Solar system' ],
},
{
id: 10,
title: 'Saturn',
description: 'Saturn description',
image: 'https://live.staticflickr.com/5725/21726228300_51333bd62c_b.jpg',
type: 'Gas giant',
categories: [ 'Space', 'Planet', 'Solar system' ],
},
{
id: 11,
title: 'Uranus',
description: 'Uranus description',
image: 'https://live.staticflickr.com/5725/21726228300_51333bd62c_b.jpg',
type: 'Ice giant',
categories: [ 'Space', 'Ice giant', 'Solar system' ],
},
];

Expand Down Expand Up @@ -135,3 +146,66 @@ export const actions = [
callback() {},
},
];

export const fields = [
{
header: 'Image',
id: 'image',
render: ( { item } ) => {
return (
<img src={ item.image } alt="" style={ { width: '100%' } } />
);
},
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,
},
];
Loading

0 comments on commit e836da4

Please sign in to comment.