UNPKG

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
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