Created Pagination

Signed-off-by: soupette <cyril@strapi.io>
This commit is contained in:
soupette 2021-09-01 12:49:30 +02:00
parent d035309812
commit cc71e8f310
9 changed files with 681 additions and 34 deletions

View File

@ -17,6 +17,7 @@ const PageSize = () => {
const handleChange = e => {
setQuery({
pageSize: e,
page: 1,
});
};

View File

@ -0,0 +1,142 @@
/**
* Pagination
*
* The component works as follows
* `1` , 2, 3, ... 10
* 1, `2`, 3, ... 10
* 1, 2, `3`, 4, ... 10
* 1, 2, 3, `4`, 5, ... 10
* 1, ..,4, `5`, 6, ... 10
*
* 1, ...., 8, 9, `10`
* 1, ...., 8, `9`, 10
* 1, ...., 7, `8`, 9, 10
* 1, ... 6, `7`, 8, 9, 10
*/
import React from 'react';
import {
NextLink,
Pagination as PaginationCompo,
PreviousLink,
Dots,
PageLink,
} from '@strapi/parts';
import PropTypes from 'prop-types';
import { useQueryParams } from '@strapi/helper-plugin';
import { useLocation } from 'react-router-dom';
import { stringify } from 'qs';
const Pagination = ({ pagination: { pageCount } }) => {
const [{ query }] = useQueryParams();
const activePage = parseInt(query.page, 10);
const { pathname } = useLocation();
const makeSearch = page => stringify({ ...query, page }, { encode: false });
const nextSearch = makeSearch(activePage + (pageCount > 1 ? 1 : 0));
const previousSearch = makeSearch(activePage - 1);
const firstLinks = [
<PageLink key={1} number={1} to={`${pathname}?${makeSearch(1)}`}>
Go to page 1
</PageLink>,
];
let firstLinksToCreate = [];
let lastLinks = [];
let lastLinksToCreate = [];
const middleLinks = [];
if (pageCount > 1) {
lastLinks.push(
<PageLink key={pageCount} number={pageCount} to={`${pathname}?${makeSearch(pageCount)}`}>
Go to page {pageCount}
</PageLink>
);
}
if (activePage === 1 && pageCount >= 3) {
firstLinksToCreate = [2];
}
if (activePage === 2 && pageCount >= 3) {
firstLinksToCreate = pageCount === 5 ? [2, 3, 4] : [2, 3];
}
if (activePage === 4 && pageCount >= 3) {
firstLinksToCreate = [2];
}
if (activePage === pageCount && pageCount >= 3) {
lastLinksToCreate = [pageCount - 1];
}
if (activePage === pageCount - 2 && pageCount >= 3) {
lastLinksToCreate = [activePage + 1, activePage, activePage - 1];
}
if (activePage === pageCount - 3 && pageCount >= 3 && activePage > 5) {
lastLinksToCreate = [activePage + 2, activePage + 1, activePage, activePage - 1];
}
if (activePage === pageCount - 1 && pageCount >= 3) {
lastLinksToCreate = [activePage, activePage - 1];
}
lastLinksToCreate.forEach(number => {
lastLinks.unshift(
<PageLink key={number} number={number} to={`${pathname}?${makeSearch(number)}`}>
Go to page {number}
</PageLink>
);
});
firstLinksToCreate.forEach(number => {
firstLinks.push(
<PageLink key={number} number={number} to={`${pathname}?${makeSearch(number)}`}>
Go to page {number}
</PageLink>
);
});
if (![1, 2].includes(activePage) && activePage < pageCount - 3) {
const middleLinksToCreate = [activePage - 1, activePage, activePage + 1];
middleLinksToCreate.forEach(number => {
middleLinks.push(
<PageLink key={number} number={number} to={`${pathname}?${makeSearch(number)}`}>
Go to page {number}
</PageLink>
);
});
}
const shouldShowDotsAfterFirstLink =
pageCount > 5 || (pageCount === 5 && (activePage === 1 || activePage === 5));
const shouldShowMiddleDots = middleLinks.length > 2 && activePage > 4 && pageCount > 5;
const beforeDotsLinks = shouldShowMiddleDots
? pageCount - activePage - 1
: pageCount - firstLinks.length - lastLinks.length;
const afterDotsLength = shouldShowMiddleDots
? pageCount - firstLinks.length - lastLinks.length
: pageCount - activePage - 1;
return (
<PaginationCompo activePage={activePage} pageCount={pageCount}>
<PreviousLink to={`${pathname}?${previousSearch}`}>Go to previous page</PreviousLink>
{firstLinks}
{shouldShowMiddleDots && <Dots>And {beforeDotsLinks} links</Dots>}
{middleLinks}
{shouldShowDotsAfterFirstLink && <Dots>And {afterDotsLength} links</Dots>}
{lastLinks}
<NextLink to={`${pathname}?${nextSearch}`}>Go to next page</NextLink>
</PaginationCompo>
);
};
Pagination.propTypes = {
pagination: PropTypes.shape({ pageCount: PropTypes.number.isRequired }).isRequired,
};
export default Pagination;

View File

@ -1,32 +1,35 @@
import React from 'react';
import { Box, Row, NextLink, Pagination, PreviousLink, Dots, PageLink } from '@strapi/parts';
import PropTypes from 'prop-types';
import { Box, Row } from '@strapi/parts';
import Pagination from './Pagination';
import PageSize from './PageSize';
const PaginationFooter = () => {
const PaginationFooter = ({ pagination }) => {
return (
<Box paddingTop={6}>
<Row justifyContent="space-between">
<PageSize />
<Pagination activePage={1} pageCount={26}>
<PreviousLink to="/1">Go to previous page</PreviousLink>
<PageLink number={1} to="/1">
Go to page 1
</PageLink>
<PageLink number={2} to="/2">
Go to page 2
</PageLink>
<Dots>And 23 other links</Dots>
<PageLink number={25} to="/25">
Go to page 3
</PageLink>
<PageLink number={26} to="/26">
Go to page 26
</PageLink>
<NextLink to="/3">Go to next page</NextLink>
</Pagination>
<Pagination pagination={pagination} />
</Row>
</Box>
);
};
PaginationFooter.defaultProps = {
pagination: {
pageCount: 0,
pageSize: 10,
total: 0,
},
};
PaginationFooter.propTypes = {
pagination: PropTypes.shape({
page: PropTypes.number,
pageCount: PropTypes.number,
pageSize: PropTypes.number,
total: PropTypes.number,
}),
};
export default PaginationFooter;

View File

@ -0,0 +1,467 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`DynamicTable renders and matches the snapshot 1`] = `
.c25 {
border: 0;
-webkit-clip: rect(0 0 0 0);
clip: rect(0 0 0 0);
height: 1px;
margin: -1px;
overflow: hidden;
padding: 0;
position: absolute;
width: 1px;
}
.c4 {
font-weight: 500;
font-size: 0.75rem;
line-height: 1.33;
color: #32324d;
}
.c9 {
font-weight: 400;
font-size: 0.875rem;
line-height: 1.43;
color: #32324d;
}
.c13 {
font-weight: 400;
font-size: 0.875rem;
line-height: 1.43;
color: #8e8ea9;
}
.c23 {
font-weight: 400;
font-size: 0.75rem;
line-height: 1.33;
color: #32324d;
}
.c0 {
padding-top: 24px;
}
.c8 {
padding-right: 16px;
padding-left: 16px;
}
.c10 {
padding-left: 12px;
}
.c1 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-flex-direction: row;
-ms-flex-direction: row;
flex-direction: row;
-webkit-box-pack: justify;
-webkit-justify-content: space-between;
-ms-flex-pack: justify;
justify-content: space-between;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
}
.c2 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-flex-direction: row;
-ms-flex-direction: row;
flex-direction: row;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
}
.c18 {
border: 0;
-webkit-clip: rect(0 0 0 0);
clip: rect(0 0 0 0);
height: 1px;
margin: -1px;
overflow: hidden;
padding: 0;
position: absolute;
width: 1px;
}
.c3 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-flex-direction: column;
-ms-flex-direction: column;
flex-direction: column;
}
.c3 > * {
margin-top: 0;
margin-bottom: 0;
}
.c3 > * + * {
margin-top: 0px;
}
.c15 > * + * {
margin-left: 4px;
}
.c21 {
line-height: revert;
}
.c16 {
padding: 12px;
border-radius: 4px;
-webkit-text-decoration: none;
text-decoration: none;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
}
.c19 {
padding: 12px;
border-radius: 4px;
box-shadow: 0px 1px 4px rgba(33,33,52,0.1);
-webkit-text-decoration: none;
text-decoration: none;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
}
.c20 {
color: #271fe0;
background: #ffffff;
}
.c20:hover {
box-shadow: 0px 1px 4px rgba(33,33,52,0.1);
}
.c22 {
color: #32324d;
}
.c22:hover {
box-shadow: 0px 1px 4px rgba(33,33,52,0.1);
}
.c17 {
font-size: 0.7rem;
pointer-events: none;
}
.c17 svg path {
fill: #c0c0cf;
}
.c17:focus svg path,
.c17:hover svg path {
fill: #c0c0cf;
}
.c24 {
font-size: 0.7rem;
}
.c24 svg path {
fill: #666687;
}
.c24:focus svg path,
.c24:hover svg path {
fill: #4a4a6a;
}
.c6 {
position: absolute;
left: 0;
right: 0;
bottom: 0;
top: 0;
width: 100%;
background: transparent;
border: none;
}
.c6:focus {
outline: none;
}
.c5 {
position: relative;
border: 1px solid #dcdce4;
padding-right: 12px;
border-radius: 4px;
background: #ffffff;
overflow: hidden;
}
.c5:focus-within {
border: 1px solid #4945ff;
}
.c11 {
background: transparent;
border: none;
position: relative;
z-index: 1;
}
.c11 svg {
height: 0.6875rem;
width: 0.6875rem;
}
.c11 svg path {
fill: #666687;
}
.c12 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
background: none;
border: none;
}
.c12 svg {
width: 0.375rem;
}
.c7 {
min-height: 2.5rem;
}
.c14 {
margin-left: 5px;
}
<div>
<div
class="c0"
>
<div
class="c1"
>
<div
class="c2"
>
<div>
<div
class="c3"
>
<span
class="c4"
for="select-1"
id="select-1-label"
/>
<div
class="c5"
>
<button
aria-disabled="false"
aria-expanded="false"
aria-haspopup="listbox"
aria-labelledby="select-1-label select-1-content"
class="c6"
id="select-1"
type="button"
/>
<div
class="c1 c7"
>
<div
class="c2"
>
<div
class="c8"
>
<span
class="c9"
id="select-1-content"
>
10
</span>
</div>
</div>
<div
class="c2"
>
<button
aria-hidden="true"
class="c10 c11 c12"
tabindex="-1"
>
<svg
fill="none"
height="1em"
viewBox="0 0 14 8"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
clip-rule="evenodd"
d="M14 .889a.86.86 0 01-.26.625L7.615 7.736A.834.834 0 017 8a.834.834 0 01-.615-.264L.26 1.514A.861.861 0 010 .889c0-.24.087-.45.26-.625A.834.834 0 01.875 0h12.25c.237 0 .442.088.615.264a.86.86 0 01.26.625z"
fill="#32324D"
fill-rule="evenodd"
/>
</svg>
</button>
</div>
</div>
</div>
</div>
</div>
<label
class="c13 c14"
>
Entries per page
</label>
</div>
<nav
aria-label="pagination"
class=""
>
<ul
class="c2 c15"
>
<li>
<a
aria-current="page"
aria-disabled="true"
class="c16 c17 active"
href="/settings/user"
tabindex="-1"
>
<div
class="c18"
>
Go to previous page
</div>
<svg
aria-hidden="true"
fill="none"
height="1em"
viewBox="0 0 10 16"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M9.88 14.12L3.773 8 9.88 1.88 8 0 0 8l8 8 1.88-1.88z"
fill="#32324D"
/>
</svg>
</a>
</li>
<li>
<a
aria-current="page"
class="c19 c20 active"
href="/settings/user?pageSize=10&page=1&sort=firstname"
>
<div
class="c18"
>
Go to page 1
</div>
<span
aria-hidden="true"
class="c4 c21"
>
1
</span>
</a>
</li>
<li>
<a
aria-current="page"
class="c16 c22 active"
href="/settings/user?pageSize=10&page=2&sort=firstname"
>
<div
class="c18"
>
Go to page
2
</div>
<span
aria-hidden="true"
class="c23 c21"
>
2
</span>
</a>
</li>
<li>
<a
aria-current="page"
aria-disabled="false"
class="c16 c24 active"
href="/settings/user?pageSize=10&page=2&sort=firstname"
>
<div
class="c18"
>
Go to next page
</div>
<svg
aria-hidden="true"
fill="none"
height="1em"
viewBox="0 0 10 16"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M0 1.88L6.107 8 0 14.12 1.88 16l8-8-8-8L0 1.88z"
fill="#32324D"
/>
</svg>
</a>
</li>
</ul>
</nav>
</div>
</div>
<div
class="c25"
>
<p
aria-live="polite"
id="live-region-log"
role="log"
/>
<p
aria-live="polite"
id="live-region-status"
role="status"
/>
<p
aria-live="assertive"
id="live-region-alert"
role="alert"
/>
</div>
</div>
`;

View File

@ -0,0 +1,34 @@
import React from 'react';
import { render } from '@testing-library/react';
import { IntlProvider } from 'react-intl';
import { createMemoryHistory } from 'history';
import { Router, Route } from 'react-router-dom';
import Theme from '../../../../../components/Theme';
import PaginationFooter from '../index';
const makeApp = (history, pagination) => {
return (
<IntlProvider messages={{ en: {} }} textComponent="span" locale="en">
<Theme>
<Router history={history}>
<Route path="/settings/user">
<PaginationFooter pagination={pagination} />
</Route>
</Router>
</Theme>
</IntlProvider>
);
};
describe('DynamicTable', () => {
it('renders and matches the snapshot', () => {
const history = createMemoryHistory();
const pagination = { pageCount: 2 };
history.push('/settings/user?pageSize=10&page=1&sort=firstname');
const app = makeApp(history, pagination);
const { container } = render(app);
expect(container).toMatchSnapshot();
});
});

View File

@ -281,7 +281,7 @@ const ListPage = () => {
withBulkActions
withMainAction={canDelete}
/>
<PaginationFooter />
<PaginationFooter pagination={data?.pagination} />
</>
)}
</CustomContentLayout>

View File

@ -51,7 +51,10 @@ describe('ADMIN | Pages | USERS | ListPage', () => {
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
afterAll(() => {
server.close();
jest.resetAllMocks();
});
it('renders and matches the snapshot', () => {
const history = createMemoryHistory();
@ -370,8 +373,8 @@ describe('ADMIN | Pages | USERS | ListPage', () => {
const { getByText } = render(app);
await waitFor(() => {
expect(getByText('soup soup')).toBeInTheDocument();
expect(getByText('dummy dummy')).toBeInTheDocument();
expect(getByText('soup')).toBeInTheDocument();
expect(getByText('dummy')).toBeInTheDocument();
expect(getByText('Active')).toBeInTheDocument();
expect(getByText('Inactive')).toBeInTheDocument();
});

View File

@ -17,7 +17,7 @@ const handlers = [
firstname: 'soup',
id: 1,
isActive: true,
lastname: 'soup',
lastname: 'soupette',
roles: [
{
id: 1,
@ -30,7 +30,7 @@ const handlers = [
firstname: 'dummy',
id: 2,
isActive: false,
lastname: 'dummy',
lastname: 'dum test',
roles: [
{
id: 1,

View File

@ -6,15 +6,12 @@ const tableHeaders = [
{
name: 'firstname',
key: 'firstname',
metadatas: { label: 'Name', sortable: true },
// eslint-disable-next-line react/prop-types
cellFormatter: ({ firstname, lastname }) => {
return (
<Text>
{firstname} {lastname}
</Text>
);
},
metadatas: { label: 'Firstname', sortable: true },
},
{
name: 'lastname',
key: 'lastname',
metadatas: { label: 'Lastname', sortable: true },
},
{
key: 'email',