ra-core
Version:
Core components of react-admin, a frontend Framework for building admin applications on top of REST services, using ES6, React
224 lines • 8.93 kB
JavaScript
import { useCallback, useEffect, useRef, useState } from 'react';
import get from 'lodash/get.js';
import isEqual from 'lodash/isEqual.js';
import { removeEmpty } from "../../util/index.js";
import { useResourceContext } from "../../core/index.js";
import usePaginationState from "../usePaginationState.js";
import useSortState from "../useSortState.js";
import { useRecordSelection } from "./useRecordSelection.js";
import { flattenObject } from "../../dataProvider/fetch.js";
import { defaultExporter } from "../../export/index.js";
const refetch = () => {
throw new Error('refetch is not available for a ListContext built from useList based on local data');
};
/**
* Handle filtering, sorting and pagination on local data.
*
* Returns the data and callbacks expected by <ListContext>.
*
* @example
* const data = [
* { id: 1, name: 'Arnold' },
* { id: 2, name: 'Sylvester' },
* { id: 3, name: 'Jean-Claude' },
* ]
*
* const MyComponent = () => {
* const listContext = useList({ data });
* return (
* <ListContextProvider value={listContext}>
* <Datagrid>
* <TextField source="id" />
* <TextField source="name" />
* </Datagrid>
* </ListContextProvider>
* );
* };
*
* @param {UseListOptions} props
* @param {RaRecord[]} props.data An array of records
* @param {Boolean} props.isFetching: Optional. A boolean indicating whether the data is being loaded
* @param {Boolean} props.isLoading: Optional. A boolean indicating whether the data has been loaded at least once
* @param {Error | String} props.error: Optional. The error if any occurred while loading the data
* @param {Object} props.filter: Optional. An object containing the filters applied on the data
* @param {Number} props.page: Optional. The initial page index
* @param {Number} props.perPage: Optional. The initial page size
* @param {SortPayload} props.sort: Optional. The initial sort (field and order)
* @param {filterCallback} prop.filterCallback Optional. A function that allows you to make a custom filter
*/
export const useList = (props) => {
const { data, error, filter = defaultFilter, isFetching = false, isLoading = false, isPaused = false, isPending = false, isPlaceholderData = false, page: initialPage = 1, perPage: initialPerPage = 1000, sort: initialSort, filterCallback = defaultFilterCallback, exporter = defaultExporter, } = props;
const resource = useResourceContext(props);
const [finalItems, setFinalItems] = useState(() => ({
data,
total: data ? data.length : undefined,
}));
// pagination logic
const { page, setPage, perPage, setPerPage } = usePaginationState({
page: initialPage,
perPage: initialPerPage,
});
// sort logic
const { sort, setSort: setSortState } = useSortState(initialSort);
const setSort = useCallback((sort) => {
setSortState(sort);
setPage(1);
}, [setPage, setSortState]);
// selection logic
const [selectedIds, selectionModifiers] = useRecordSelection(resource
? {
resource,
}
: { disableSyncWithStore: true });
const onUnselectItems = useCallback((fromAllStoreKeys) => {
return selectionModifiers.unselect(selectedIds, fromAllStoreKeys);
}, [selectedIds, selectionModifiers]);
// filter logic
const filterRef = useRef(filter);
const [displayedFilters, setDisplayedFilters] = useState({});
const [filterValues, setFilterValues] = useState(filter);
const hideFilter = useCallback((filterName) => {
setDisplayedFilters(previousState => {
const { [filterName]: _, ...newState } = previousState;
return newState;
});
setFilterValues(previousState => {
const { [filterName]: _, ...newState } = previousState;
return newState;
});
}, [setDisplayedFilters, setFilterValues]);
const showFilter = useCallback((filterName, defaultValue) => {
setDisplayedFilters(previousState => ({
...previousState,
[filterName]: true,
}));
setFilterValues(previousState => removeEmpty({
...previousState,
[filterName]: defaultValue,
}));
}, [setDisplayedFilters, setFilterValues]);
const setFilters = useCallback((filters, displayedFilters = undefined) => {
setFilterValues(removeEmpty(filters));
if (displayedFilters) {
setDisplayedFilters(displayedFilters);
}
setPage(1);
}, [setDisplayedFilters, setFilterValues, setPage]);
// handle filter prop change
useEffect(() => {
if (!isEqual(filter, filterRef.current)) {
filterRef.current = filter;
setFilterValues(filter);
}
}, [filter]);
const applyFilterAndSort = useCallback((records) => {
let tempData = records;
if (filterValues) {
const flattenFilterValues = flattenObject(filterValues);
tempData = records
.filter(record => Object.entries(flattenFilterValues).every(([filterName, filterValue]) => {
const recordValue = get(record, filterName);
const result = Array.isArray(recordValue)
? Array.isArray(filterValue)
? recordValue.some(item => filterValue.includes(item))
: recordValue.includes(filterValue)
: Array.isArray(filterValue)
? filterValue.includes(recordValue)
: filterName === 'q' // special full-text filter
? Object.keys(record).some(key => typeof record[key] ===
'string' &&
record[key]
.toLowerCase()
.includes(filterValue.toLowerCase()))
: filterValue == recordValue; // eslint-disable-line eqeqeq
return result;
}))
.filter(filterCallback);
}
if (sort.field) {
tempData = tempData.sort((a, b) => {
if (get(a, sort.field) > get(b, sort.field)) {
return sort.order === 'ASC' ? 1 : -1;
}
if (get(a, sort.field) < get(b, sort.field)) {
return sort.order === 'ASC' ? -1 : 1;
}
return 0;
});
}
return tempData;
}, [filterValues, filterCallback, sort.field, sort.order]);
// We do all the data processing (filtering, sorting, paginating) client-side
useEffect(() => {
if (isPending || !data)
return;
const filteredAndSorted = applyFilterAndSort(data);
const filteredLength = filteredAndSorted.length;
const paginatedData = filteredAndSorted.slice((page - 1) * perPage, page * perPage);
setFinalItems({
data: paginatedData,
total: filteredLength,
});
}, // eslint-disable-next-line react-hooks/exhaustive-deps
[
// eslint-disable-next-line react-hooks/exhaustive-deps
JSON.stringify(data),
applyFilterAndSort,
isPending,
page,
perPage,
setFinalItems,
]);
const onSelectAll = useCallback(() => {
const allIds = data?.map(({ id }) => id) || [];
selectionModifiers.select(allIds);
}, [data, selectionModifiers]);
const getData = useCallback(async ({ maxResults } = {}) => {
if (isPending || !data) {
return [];
}
const filteredAndSorted = applyFilterAndSort(data);
if (maxResults != null) {
return filteredAndSorted.slice(0, maxResults);
}
return filteredAndSorted;
}, [applyFilterAndSort, data, isPending]);
return {
sort,
data: isPending ? undefined : finalItems?.data ?? [],
defaultTitle: '',
error: error ?? null,
displayedFilters,
exporter,
filterValues,
hasNextPage: finalItems?.total == null
? false
: page * perPage < finalItems.total,
hasPreviousPage: page > 1,
hideFilter,
isFetching,
isLoading,
isPaused,
isPending,
isPlaceholderData,
onSelect: selectionModifiers.select,
onSelectAll,
onToggleItem: selectionModifiers.toggle,
onUnselectItems,
page,
perPage,
resource: '',
refetch,
selectedIds,
setFilters,
setPage,
setPerPage,
setSort,
showFilter,
total: finalItems?.total,
getData,
};
};
const defaultFilter = {};
const defaultFilterCallback = (record) => Boolean(record);
//# sourceMappingURL=useList.js.map