-
Notifications
You must be signed in to change notification settings - Fork 4.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
DataViews: Add a utility to share filtering, sorting and pagination logic #59897
Changes from 6 commits
3eae8dc
8b82613
5ab7006
1de0ecd
8cf63f6
982ddff
f68b016
8d05881
9b4dc96
f83be7c
69e4a90
b7e1a6b
fed1229
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,140 @@ | ||
/** | ||
* 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 filterAndSortDataView( data, view, fields ) { | ||
youknowriad marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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 ) => { | ||
return filter.value.includes( field.getValue( { item } ) ); | ||
youknowriad marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} ); | ||
} else if ( | ||
filter.operator === OPERATOR_IS_NONE && | ||
filter?.value?.length > 0 | ||
) { | ||
filteredData = filteredData.filter( ( item ) => { | ||
return ! filter.value.includes( | ||
field.getValue( { item } ) | ||
); | ||
} ); | ||
} 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 } ); | ||
} ); | ||
} | ||
} ); | ||
} | ||
youknowriad marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
// Handle sorting. | ||
if ( view.sort ) { | ||
const fieldId = view.sort.field; | ||
const fieldToSort = _fields.find( ( field ) => { | ||
return field.id === fieldId; | ||
} ); | ||
filteredData.sort( ( a, b ) => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What about sortable fields that are not text(ex Date)? Maybe the
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, something like that would be good (especially when we introduce the "field types" more formally. |
||
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, | ||
}, | ||
}; | ||
} |
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 { filterAndSortDataView } from './filter-and-sort-data-view'; |
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, | ||
}; | ||
} ); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Annoying that we still need to use third-party libraries like
remove-accent
for this kind of stuff.Intl.Collator#compare is almost what we want, but it doesn't do partial matches.
</rant>
:)There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I just copied what we had before, now let me merge my PR and do your follow-up :P
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hah, I know, this was just a rant in passing :P
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh hey, it's @tyxla's package! No disrespect meant! 😅