ra-core
Version:
Core components of react-admin, a frontend Framework for building admin applications on top of REST services, using ES6, React
274 lines (256 loc) • 8.58 kB
text/typescript
import { isValidElement, useEffect, useMemo } from 'react';
import type {
InfiniteQueryObserverBaseResult,
InfiniteData,
} from '@tanstack/react-query';
import { useAuthenticated, useRequireAccess } from '../../auth';
import { useTranslate } from '../../i18n';
import { useNotify } from '../../notification';
import { useInfiniteGetList } from '../../dataProvider';
import { defaultExporter } from '../../export';
import { useResourceContext, useGetResourceLabel } from '../../core';
import { useRecordSelection } from './useRecordSelection';
import { useListParams } from './useListParams';
import { useSelectAll } from './useSelectAll';
import type { UseInfiniteGetListOptions } from '../../dataProvider';
import type { ListControllerResult } from './useListController';
import type {
RaRecord,
SortPayload,
FilterPayload,
Exporter,
GetInfiniteListResult,
} from '../../types';
/**
* Prepare data for the InfiniteList view
*
* @param {Object} props The props passed to the InfiniteList component.
*
* @return {Object} controllerProps Fetched and computed data for the List view
*
* @example
*
* import { useInfiniteListController } from 'react-admin';
* import ListView from './ListView';
*
* const MyList = props => {
* const controllerProps = useInfiniteListController(props);
* return <ListView {...controllerProps} {...props} />;
* }
*/
export const useInfiniteListController = <
RecordType extends RaRecord = any,
ErrorType = Error,
>(
props: InfiniteListControllerProps<RecordType, ErrorType> = {}
): InfiniteListControllerResult<RecordType, ErrorType> => {
const {
debounce = 500,
disableAuthentication = false,
disableSyncWithLocation = false,
exporter = defaultExporter,
filter,
filterDefaultValues,
perPage = 10,
queryOptions,
sort,
storeKey,
} = props;
const resource = useResourceContext(props);
const { meta, ...otherQueryOptions } = queryOptions ?? {};
if (!resource) {
throw new Error(
`<InfiniteList> was called outside of a ResourceContext and without a resource prop. You must set the resource prop.`
);
}
if (filter && isValidElement(filter)) {
throw new Error(
'<InfiniteList> received a React element as `filter` props. If you intended to set the list filter elements, use the `filters` (with an s) prop instead. The `filter` prop is internal and should not be set by the developer.'
);
}
const { isPending: isPendingAuthenticated } = useAuthenticated({
enabled: !disableAuthentication,
});
const { isPending: isPendingCanAccess } = useRequireAccess<RecordType>({
action: 'list',
resource,
enabled: !disableAuthentication && !isPendingAuthenticated,
});
const translate = useTranslate();
const notify = useNotify();
const [query, queryModifiers] = useListParams({
debounce,
disableSyncWithLocation,
filterDefaultValues,
perPage,
resource,
sort,
storeKey,
});
const [selectedIds, selectionModifiers] = useRecordSelection({ resource });
const {
data,
total,
error,
isLoading,
isPending,
isFetching,
hasNextPage,
hasPreviousPage,
fetchNextPage,
isFetchingNextPage,
fetchPreviousPage,
isFetchingPreviousPage,
refetch,
meta: responseMeta,
} = useInfiniteGetList<RecordType, ErrorType>(
resource,
{
pagination: {
page: query.page,
perPage: query.perPage,
},
sort: { field: query.sort, order: query.order },
filter: { ...query.filter, ...filter },
meta,
},
{
enabled:
(!isPendingAuthenticated && !isPendingCanAccess) ||
disableAuthentication,
placeholderData: previousData => previousData,
retry: false,
onError: error =>
notify(
(error as Error)?.message || 'ra.notification.http_error',
{
type: 'error',
messageArgs: {
_: (error as Error)?.message,
},
}
),
...otherQueryOptions,
}
);
const onSelectAll = useSelectAll({
resource,
sort: { field: query.sort, order: query.order },
filter: { ...query.filter, ...filter },
});
// change page if there is no data
useEffect(() => {
if (
query.page <= 0 ||
(!isFetching &&
query.page > 1 &&
(data == null || data?.pages.length === 0))
) {
// Query for a page that doesn't exist, set page to 1
queryModifiers.setPage(1);
return;
}
if (total == null) {
return;
}
const totalPages = Math.ceil(total / query.perPage) || 1;
if (!isFetching && query.page > totalPages) {
// Query for a page out of bounds, set page to the last existing page
// It occurs when deleting the last element of the last page
queryModifiers.setPage(totalPages);
}
}, [isFetching, query.page, query.perPage, data, queryModifiers, total]);
const currentSort = useMemo(
() => ({
field: query.sort,
order: query.order,
}),
[query.sort, query.order]
);
const getResourceLabel = useGetResourceLabel();
const defaultTitle = translate(`resources.${resource}.page.list`, {
_: translate('ra.page.list', {
name: getResourceLabel(resource, 2),
}),
});
const unwrappedData = useMemo(
() => data?.pages?.reduce((acc, page) => [...acc, ...page.data], []),
[data]
);
return {
sort: currentSort,
data: unwrappedData,
defaultTitle,
displayedFilters: query.displayedFilters,
error,
exporter,
filter,
filterValues: query.filterValues,
hideFilter: queryModifiers.hideFilter,
isFetching,
isLoading,
isPending,
onSelect: selectionModifiers.select,
onSelectAll,
onToggleItem: selectionModifiers.toggle,
onUnselectItems: selectionModifiers.clearSelection,
page: query.page,
perPage: query.perPage,
refetch,
resource,
selectedIds,
setFilters: queryModifiers.setFilters,
setPage: queryModifiers.setPage,
setPerPage: queryModifiers.setPerPage,
setSort: queryModifiers.setSort,
showFilter: queryModifiers.showFilter,
total: total,
hasNextPage,
hasPreviousPage,
fetchNextPage,
isFetchingNextPage,
fetchPreviousPage,
isFetchingPreviousPage,
meta: responseMeta,
} as InfiniteListControllerResult<RecordType, ErrorType>;
};
export interface InfiniteListControllerProps<
RecordType extends RaRecord = any,
ErrorType = Error,
> {
debounce?: number;
disableAuthentication?: boolean;
/**
* Whether to disable the synchronization of the list parameters with the current location (URL search parameters)
*/
disableSyncWithLocation?: boolean;
exporter?: Exporter | false;
filter?: FilterPayload;
filterDefaultValues?: object;
perPage?: number;
queryOptions?: UseInfiniteGetListOptions<RecordType, ErrorType>;
resource?: string;
sort?: SortPayload;
storeKey?: string | false;
}
export type InfiniteListControllerResult<
RecordType extends RaRecord = any,
ErrorType = Error,
> = ListControllerResult<RecordType> & {
fetchNextPage: InfiniteQueryObserverBaseResult<
InfiniteData<GetInfiniteListResult<RecordType>>,
ErrorType
>['fetchNextPage'];
fetchPreviousPage: InfiniteQueryObserverBaseResult<
InfiniteData<GetInfiniteListResult<RecordType>>,
ErrorType
>['fetchPreviousPage'];
isFetchingNextPage: InfiniteQueryObserverBaseResult<
InfiniteData<GetInfiniteListResult<RecordType>>,
ErrorType
>['isFetchingNextPage'];
isFetchingPreviousPage: InfiniteQueryObserverBaseResult<
InfiniteData<GetInfiniteListResult<RecordType>>,
ErrorType
>['isFetchingPreviousPage'];
};