Skip to content
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

Fix pagination on bulk delete #4894

Merged
merged 21 commits into from
Jul 24, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
070de12
Fix pagination on bulk delete
JulienMattiussi Jun 4, 2020
8fcdfe8
fix tests
JulienMattiussi Jun 4, 2020
3d2b2ef
fix unit tests
JulienMattiussi Jun 4, 2020
e70acd6
little fixes on review
JulienMattiussi Jun 18, 2020
809f669
refactor test (review)
JulienMattiussi Jun 18, 2020
76740a5
Use a useMemo instead of a useCallback to compute the nb of pages in …
Luwangel Jul 23, 2020
3637749
Fix the getNumberOrDefault method that returns the default value inst…
Luwangel Jul 23, 2020
e6357bc
Compute the total pages for a list and pass it to the list context
Luwangel Jul 23, 2020
0774a43
Set the page to the last existing page in the useListController if qu…
Luwangel Jul 23, 2020
8850de6
Use totalPages in Pagination
Luwangel Jul 23, 2020
fa8ec88
Remove console.log
Luwangel Jul 23, 2020
e3c9cf4
Sanitize the total pages prop in the ListView
Luwangel Jul 23, 2020
f08f96f
Fix total page out of bound limit for material ui in Pagination
Luwangel Jul 23, 2020
9e499ad
Fix a problem with Pagination when trying to pass negative pages thro…
Luwangel Jul 23, 2020
956f627
Write a new test for negative pages
Luwangel Jul 23, 2020
3d33bfb
Fix the ListControllerProps TypeScript type
Luwangel Jul 23, 2020
446ed85
Make the total pages optionnal in TypeScript types
Luwangel Jul 24, 2020
8116dbb
Count the total pages everywhere and not make it optional
Luwangel Jul 24, 2020
7c75b30
Remove the totalPages from contexts because it could be recomputed us…
Luwangel Jul 24, 2020
6e46f41
Apply reviews
Luwangel Jul 24, 2020
8223ab0
Fix useEffect in useListController
Luwangel Jul 24, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/ra-core/src/controller/useListController.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@ describe('useListController', () => {
list: {
params: {},
cachedRequests: {},
ids: [],
},
},
},
Expand Down
21 changes: 19 additions & 2 deletions packages/ra-core/src/controller/useListController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,15 +184,31 @@ const useListController = <RecordType = Record>(

const finalIds = typeof total === 'undefined' ? defaultIds : ids;

const totalPages = useMemo(() => {
return Math.ceil(total / query.perPage) || 1;
}, [query.perPage, total]);

useEffect(() => {
if (
query.page <= 0 ||
(!loading && query.page > 1 && (finalIds || []).length === 0)
) {
// query for a page that doesn't exist, set page to 1
// Query for a page that doesn't exist, set page to 1
queryModifiers.setPage(1);
} else if (!loading && 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);
}
}, [loading, query.page, finalIds, queryModifiers, total, defaultIds]);
}, [
loading,
query.page,
finalIds,
queryModifiers,
total,
totalPages,
defaultIds,
]);

const currentSort = useMemo(
() => ({
Expand Down Expand Up @@ -268,6 +284,7 @@ export const injectedProps = [
'setSort',
'showFilter',
'total',
'totalPages',
'version',
];

Expand Down
28 changes: 27 additions & 1 deletion packages/ra-core/src/controller/useListParams.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { getQuery } from './useListParams';
import { getQuery, getNumberOrDefault } from './useListParams';
import {
SORT_DESC,
SORT_ASC,
Expand Down Expand Up @@ -156,4 +156,30 @@ describe('useListParams', () => {
});
});
});

describe('getNumberOrDefault', () => {
it('should return the parsed number', () => {
const result = getNumberOrDefault('29', 2);

expect(result).toEqual(29);
});

it('should return the default number when passing a not valid number', () => {
const result = getNumberOrDefault('covfefe', 2);

expect(result).toEqual(2);
});

it('should return the default number when passing an undefined number', () => {
const result = getNumberOrDefault(undefined, 2);

expect(result).toEqual(2);
});

it('should not return the default number when passing "0"', () => {
const result = getNumberOrDefault('0', 2);

expect(result).toEqual(0);
});
});
Luwangel marked this conversation as resolved.
Show resolved Hide resolved
});
17 changes: 11 additions & 6 deletions packages/ra-core/src/controller/useListParams.ts
Original file line number Diff line number Diff line change
Expand Up @@ -342,12 +342,13 @@ export const getQuery = ({
query.sort = sort.field;
query.order = sort.order;
}
if (!query.perPage) {
if (query.perPage == null) {
query.perPage = perPage;
}
if (!query.page) {
if (query.page == null) {
query.page = 1;
}

return {
...query,
page: getNumberOrDefault(query.page, 1),
Expand All @@ -358,9 +359,13 @@ export const getQuery = ({
export const getNumberOrDefault = (
possibleNumber: string | number | undefined,
defaultValue: number
) =>
(typeof possibleNumber === 'string'
? parseInt(possibleNumber, 10)
: possibleNumber) || defaultValue;
) => {
const parsedNumber =
typeof possibleNumber === 'string'
? parseInt(possibleNumber, 10)
: possibleNumber;

return isNaN(parsedNumber) ? defaultValue : parsedNumber;
};
Luwangel marked this conversation as resolved.
Show resolved Hide resolved

export default useListParams;
1 change: 0 additions & 1 deletion packages/ra-ui-materialui/src/field/ArrayField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,6 @@ export const ArrayField: FC<ArrayFieldProps> = memo<ArrayFieldProps>(
setData(data);
}, [record, source, fieldKey]);

// @ts-ignore
return (
<ListContext.Provider
value={{
Expand Down
41 changes: 36 additions & 5 deletions packages/ra-ui-materialui/src/list/Pagination.spec.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as React from 'react';
import expect from 'expect';
import { render, cleanup } from '@testing-library/react';
import { render, cleanup, wait } from '@testing-library/react';
import { ThemeProvider } from '@material-ui/styles';
import { createMuiTheme } from '@material-ui/core/styles';
import { ListContext } from 'ra-core';
Expand Down Expand Up @@ -43,19 +43,50 @@ describe('<Pagination />', () => {
expect(queryByText('ra.navigation.no_results')).toBeNull();
});

it('should not display a pagination limit on an out of bounds page', () => {
it('should display a pagination limit on an out of bounds page (more than total pages)', async () => {
Luwangel marked this conversation as resolved.
Show resolved Hide resolved
jest.spyOn(console, 'error').mockImplementationOnce(() => {});
const setPage = jest.fn().mockReturnValue(null);
const { queryByText } = render(
<ThemeProvider theme={theme}>
<ListContext.Provider
value={{ ...defaultProps, total: 10, page: 2 }}
value={{
...defaultProps,
total: 10,
page: 2, // Query the page 2 but there is only 1 page
perPage: 10,
setPage,
}}
>
<Pagination />
</ListContext.Provider>
</ThemeProvider>
);
// mui TablePagination displays a warning in that case, and that's normal
expect(queryByText('ra.navigation.no_results')).toBeNull();
// mui TablePagination displays no more a warning in that case
// Then useEffect fallbacks on a valid page
expect(queryByText('ra.navigation.no_results')).not.toBeNull();
Luwangel marked this conversation as resolved.
Show resolved Hide resolved
});

it('should display a pagination limit on an out of bounds page (less than 0)', async () => {
jest.spyOn(console, 'error').mockImplementationOnce(() => {});
const setPage = jest.fn().mockReturnValue(null);
const { queryByText } = render(
<ThemeProvider theme={theme}>
<ListContext.Provider
value={{
...defaultProps,
total: 10,
page: -2, // Query the page -2 😱
perPage: 10,
setPage,
}}
>
<Pagination />
</ListContext.Provider>
</ThemeProvider>
);
// mui TablePagination displays no more a warning in that case
// Then useEffect fallbacks on a valid page
expect(queryByText('ra.navigation.no_results')).not.toBeNull();
});
});

Expand Down
19 changes: 9 additions & 10 deletions packages/ra-ui-materialui/src/list/Pagination.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as React from 'react';
import { useEffect, useCallback, FC, ReactElement } from 'react';
import { useCallback, useMemo, FC, ReactElement } from 'react';
import PropTypes from 'prop-types';
import {
TablePagination,
Expand Down Expand Up @@ -30,25 +30,23 @@ const Pagination: FC<PaginationProps> = props => {
setPage,
setPerPage,
} = useListContext(props);
useEffect(() => {
if (page < 1 || isNaN(page)) {
setPage(1);
}
}, [page, setPage]);

const translate = useTranslate();
const isSmall = useMediaQuery((theme: Theme) =>
theme.breakpoints.down('sm')
);

const getNbPages = () => Math.ceil(total / perPage) || 1;
const totalPages = useMemo(() => {
return Math.ceil(total / perPage) || 1;
}, [perPage, total]);

/**
* Warning: material-ui's page is 0-based
*/
const handlePageChange = useCallback(
(event, page) => {
event && event.stopPropagation();
if (page < 0 || page > getNbPages() - 1) {
if (page < 0 || page > totalPages - 1) {
throw new Error(
translate('ra.navigation.page_out_of_boundaries', {
page: page + 1,
Expand All @@ -57,7 +55,7 @@ const Pagination: FC<PaginationProps> = props => {
}
setPage(page + 1);
},
[total, perPage, setPage, translate] // eslint-disable-line react-hooks/exhaustive-deps
[totalPages, setPage, translate]
);

const handlePerPageChange = useCallback(
Expand All @@ -77,7 +75,8 @@ const Pagination: FC<PaginationProps> = props => {
[translate]
);

if (total === 0) {
// Avoid rendering TablePagination if "page" value is invalid
if (total === 0 || page < 1 || page > totalPages) {
return loading ? <Toolbar variant="dense" /> : limit;
}

Expand Down