Merge pull request #158 from acelaya-forks/feature/improved-pagination

Feature/improved pagination
This commit is contained in:
Alejandro Celaya 2019-09-22 11:44:54 +02:00 committed by GitHub
commit 2c93e9a587
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 381 additions and 30 deletions

View file

@ -15,7 +15,7 @@ install:
before_script:
- echo "Building commit range ${TRAVIS_COMMIT_RANGE}"
- export MUTATION_FILES=$(git diff ${TRAVIS_COMMIT_RANGE:-origin/master} --name-only | grep src/ | paste -sd ",")
- export MUTATION_FILES=$(git diff ${TRAVIS_COMMIT_RANGE:-origin/master} --name-only | grep -E 'src\/(.*).(js|ts|jsx|tsx)' | paste -sd ",")
script:
- npm run lint

View file

@ -24,7 +24,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
#### Fixed
* [#151](https://github.com/shlinkio/shlink-web-client/issues/151) Bugfix: When no order is specified, the order by indicator(triangle) still indicate ASC on column header.
* [#151](https://github.com/shlinkio/shlink-web-client/issues/151) Fixed "order by" indicator (caret) still indicate ASC on column header when no order is specified.
* [#157](https://github.com/shlinkio/shlink-web-client/issues/157) Fixed pagination control on graphs expanding too much when lots of pages need to be rendered.
## 2.1.0 - 2019-05-19

252
package-lock.json generated
View file

@ -905,6 +905,110 @@
"resolved": "https://registry.yarnpkg.com/@icons/material/-/material-0.2.4.tgz",
"integrity": "sha1-6QyfcXaLNzbnbX3WeD/Gwq+oi8g="
},
"@jest/console": {
"version": "24.9.0",
"resolved": "https://registry.npmjs.org/@jest/console/-/console-24.9.0.tgz",
"integrity": "sha512-Zuj6b8TnKXi3q4ymac8EQfc3ea/uhLeCGThFqXeC8H9/raaH8ARPUTdId+XyGd03Z4In0/VjD2OYFcBF09fNLQ==",
"dev": true,
"requires": {
"@jest/source-map": "^24.9.0",
"chalk": "^2.0.1",
"slash": "^2.0.0"
},
"dependencies": {
"slash": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz",
"integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==",
"dev": true
}
}
},
"@jest/fake-timers": {
"version": "24.9.0",
"resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-24.9.0.tgz",
"integrity": "sha512-eWQcNa2YSwzXWIMC5KufBh3oWRIijrQFROsIqt6v/NS9Io/gknw1jsAC9c+ih/RQX4A3O7SeWAhQeN0goKhT9A==",
"dev": true,
"requires": {
"@jest/types": "^24.9.0",
"jest-message-util": "^24.9.0",
"jest-mock": "^24.9.0"
},
"dependencies": {
"jest-message-util": {
"version": "24.9.0",
"resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-24.9.0.tgz",
"integrity": "sha512-oCj8FiZ3U0hTP4aSui87P4L4jC37BtQwUMqk+zk/b11FR19BJDeZsZAvIHutWnmtw7r85UmR3CEWZ0HWU2mAlw==",
"dev": true,
"requires": {
"@babel/code-frame": "^7.0.0",
"@jest/test-result": "^24.9.0",
"@jest/types": "^24.9.0",
"@types/stack-utils": "^1.0.1",
"chalk": "^2.0.1",
"micromatch": "^3.1.10",
"slash": "^2.0.0",
"stack-utils": "^1.0.1"
}
},
"jest-mock": {
"version": "24.9.0",
"resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-24.9.0.tgz",
"integrity": "sha512-3BEYN5WbSq9wd+SyLDES7AHnjH9A/ROBwmz7l2y+ol+NtSFO8DYiEBzoO1CeFc9a8DYy10EO4dDFVv/wN3zl1w==",
"dev": true,
"requires": {
"@jest/types": "^24.9.0"
}
},
"slash": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz",
"integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==",
"dev": true
}
}
},
"@jest/source-map": {
"version": "24.9.0",
"resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-24.9.0.tgz",
"integrity": "sha512-/Xw7xGlsZb4MJzNDgB7PW5crou5JqWiBQaz6xyPd3ArOg2nfn/PunV8+olXbbEZzNl591o5rWKE9BRDaFAuIBg==",
"dev": true,
"requires": {
"callsites": "^3.0.0",
"graceful-fs": "^4.1.15",
"source-map": "^0.6.0"
},
"dependencies": {
"callsites": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
"integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
"dev": true
}
}
},
"@jest/test-result": {
"version": "24.9.0",
"resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-24.9.0.tgz",
"integrity": "sha512-XEFrHbBonBJ8dGp2JmF8kP/nQI/ImPpygKHwQ/SY+es59Z3L5PI4Qb9TQQMAEeYsThG1xF0k6tmG0tIKATNiiA==",
"dev": true,
"requires": {
"@jest/console": "^24.9.0",
"@jest/types": "^24.9.0",
"@types/istanbul-lib-coverage": "^2.0.0"
}
},
"@jest/types": {
"version": "24.9.0",
"resolved": "https://registry.npmjs.org/@jest/types/-/types-24.9.0.tgz",
"integrity": "sha512-XKK7ze1apu5JWQ5eZjHITP66AX+QsLlbaJRBGYr8pNzwcAE2JVkwnf0yqjHTsDRcjR0mujy/NmZMXw5kl+kGBw==",
"dev": true,
"requires": {
"@types/istanbul-lib-coverage": "^2.0.0",
"@types/istanbul-reports": "^1.1.1",
"@types/yargs": "^13.0.0"
}
},
"@mrmlnc/readdir-enhanced": {
"version": "2.2.1",
"resolved": "https://registry.yarnpkg.com/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz",
@ -1176,6 +1280,31 @@
"loader-utils": "^1.1.0"
}
},
"@types/istanbul-lib-coverage": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.1.tgz",
"integrity": "sha512-hRJD2ahnnpLgsj6KWMYSrmXkM3rm2Dl1qkx6IOFD5FnuNPXJIG5L0dhgKXCYTRMGzU4n0wImQ/xfmRc4POUFlg==",
"dev": true
},
"@types/istanbul-lib-report": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-1.1.1.tgz",
"integrity": "sha512-3BUTyMzbZa2DtDI2BkERNC6jJw2Mr2Y0oGI7mRxYNBPxppbtEK1F66u3bKwU2g+wxwWI7PAoRpJnOY1grJqzHg==",
"dev": true,
"requires": {
"@types/istanbul-lib-coverage": "*"
}
},
"@types/istanbul-reports": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-1.1.1.tgz",
"integrity": "sha512-UpYjBi8xefVChsCoBpKShdxTllC9pwISirfoZsUa2AAdQg/Jd2KQGtSbw+ya7GPo7x/wAPlH6JBhKhAsXUEZNA==",
"dev": true,
"requires": {
"@types/istanbul-lib-coverage": "*",
"@types/istanbul-lib-report": "*"
}
},
"@types/node": {
"version": "10.12.18",
"resolved": "https://registry.yarnpkg.com/@types/node/-/node-10.12.18.tgz",
@ -1188,6 +1317,27 @@
"integrity": "sha1-SP2YwVYf5xi2FzPa7Ub/EVtJbhg=",
"dev": true
},
"@types/stack-utils": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-1.0.1.tgz",
"integrity": "sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw==",
"dev": true
},
"@types/yargs": {
"version": "13.0.2",
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-13.0.2.tgz",
"integrity": "sha512-lwwgizwk/bIIU+3ELORkyuOgDjCh7zuWDFqRtPPhhVgq9N1F7CvLNKg1TX4f2duwtKQ0p044Au9r1PLIXHrIzQ==",
"dev": true,
"requires": {
"@types/yargs-parser": "*"
}
},
"@types/yargs-parser": {
"version": "13.1.0",
"resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-13.1.0.tgz",
"integrity": "sha512-gCubfBUZ6KxzoibJ+SCUc/57Ms1jz5NjHe4+dI2krNmU5zCPAphyLJYyTOg06ueIyfj+SaCUqmzun7ImlxDcKg==",
"dev": true
},
"@webassemblyjs/ast": {
"version": "1.7.11",
"resolved": "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.7.11.tgz",
@ -9772,13 +9922,95 @@
}
},
"jest-each": {
"version": "23.6.0",
"resolved": "https://registry.yarnpkg.com/jest-each/-/jest-each-23.6.0.tgz",
"integrity": "sha1-ugw6gqgFQ4cBYTnHM6BSQtPXFXU=",
"version": "24.9.0",
"resolved": "https://registry.npmjs.org/jest-each/-/jest-each-24.9.0.tgz",
"integrity": "sha512-ONi0R4BvW45cw8s2Lrx8YgbeXL1oCQ/wIDwmsM3CqM/nlblNCPmnC3IPQlMbRFZu3wKdQ2U8BqM6lh3LJ5Bsog==",
"dev": true,
"requires": {
"@jest/types": "^24.9.0",
"chalk": "^2.0.1",
"pretty-format": "^23.6.0"
"jest-get-type": "^24.9.0",
"jest-util": "^24.9.0",
"pretty-format": "^24.9.0"
},
"dependencies": {
"ansi-regex": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
"integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==",
"dev": true
},
"callsites": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
"integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
"dev": true
},
"ci-info": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz",
"integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==",
"dev": true
},
"is-ci": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz",
"integrity": "sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==",
"dev": true,
"requires": {
"ci-info": "^2.0.0"
}
},
"jest-get-type": {
"version": "24.9.0",
"resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-24.9.0.tgz",
"integrity": "sha512-lUseMzAley4LhIcpSP9Jf+fTrQ4a1yHQwLNeeVa2cEmbCGeoZAtYPOIv8JaxLD/sUpKxetKGP+gsHl8f8TSj8Q==",
"dev": true
},
"jest-util": {
"version": "24.9.0",
"resolved": "https://registry.npmjs.org/jest-util/-/jest-util-24.9.0.tgz",
"integrity": "sha512-x+cZU8VRmOJxbA1K5oDBdxQmdq0OIdADarLxk0Mq+3XS4jgvhG/oKGWcIDCtPG0HgjxOYvF+ilPJQsAyXfbNOg==",
"dev": true,
"requires": {
"@jest/console": "^24.9.0",
"@jest/fake-timers": "^24.9.0",
"@jest/source-map": "^24.9.0",
"@jest/test-result": "^24.9.0",
"@jest/types": "^24.9.0",
"callsites": "^3.0.0",
"chalk": "^2.0.1",
"graceful-fs": "^4.1.15",
"is-ci": "^2.0.0",
"mkdirp": "^0.5.1",
"slash": "^2.0.0",
"source-map": "^0.6.0"
}
},
"pretty-format": {
"version": "24.9.0",
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-24.9.0.tgz",
"integrity": "sha512-00ZMZUiHaJrNfk33guavqgvfJS30sLYf0f8+Srklv0AMPodGGHcoHgksZ3OThYnIvOd+8yMCn0YiEOogjlgsnA==",
"dev": true,
"requires": {
"@jest/types": "^24.9.0",
"ansi-regex": "^4.0.0",
"ansi-styles": "^3.2.0",
"react-is": "^16.8.4"
}
},
"react-is": {
"version": "16.9.0",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.9.0.tgz",
"integrity": "sha512-tJBzzzIgnnRfEm046qRcURvwQnZVXmuCbscxUO5RWrGTXpon2d4c8mI0D8WE6ydVIm29JiLB6+RslkIvym9Rjw==",
"dev": true
},
"slash": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz",
"integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==",
"dev": true
}
}
},
"jest-environment-jsdom": {
@ -9909,6 +10141,18 @@
"jest-snapshot": "^23.6.0",
"jest-util": "^23.4.0",
"pretty-format": "^23.6.0"
},
"dependencies": {
"jest-each": {
"version": "23.6.0",
"resolved": "https://registry.npmjs.org/jest-each/-/jest-each-23.6.0.tgz",
"integrity": "sha512-x7V6M/WGJo6/kLoissORuvLIeAoyo2YqLOoCDkohgJ4XOXSqOtyvr8FbInlAWS77ojBsZrafbozWoKVRdtxFCg==",
"dev": true,
"requires": {
"chalk": "^2.0.1",
"pretty-format": "^23.6.0"
}
}
}
},
"jest-leak-detector": {

View file

@ -103,6 +103,7 @@
"html-webpack-plugin": "^4.0.0-alpha.2",
"identity-obj-proxy": "^3.0.0",
"jest": "^23.6.0",
"jest-each": "^24.9.0",
"jest-pnp-resolver": "^1.0.1",
"jest-resolve": "^23.6.0",
"mini-css-extract-plugin": "^0.4.3",

View file

@ -0,0 +1,65 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Pagination, PaginationItem, PaginationLink } from 'reactstrap';
import { range, max, min } from 'ramda';
import './SimplePaginator.scss';
const propTypes = {
pagesCount: PropTypes.number.isRequired,
currentPage: PropTypes.number.isRequired,
setCurrentPage: PropTypes.func.isRequired,
};
export const ellipsis = '...';
const pagination = (currentPage, pageCount) => {
const delta = 2;
const pages = range(
max(delta, currentPage - delta),
min(pageCount - 1, currentPage + delta) + 1
);
if (currentPage - delta > delta) {
pages.unshift(ellipsis);
}
if (currentPage + delta < pageCount - 1) {
pages.push(ellipsis);
}
pages.unshift(1);
pages.push(pageCount);
return pages;
};
const SimplePaginator = ({ pagesCount, currentPage, setCurrentPage }) => {
if (pagesCount < 2) {
return null;
}
const onClick = (page) => () => setCurrentPage(page);
return (
<Pagination listClassName="flex-wrap justify-content-center mb-0 simple-paginator">
<PaginationItem disabled={currentPage <= 1}>
<PaginationLink previous tag="span" onClick={onClick(currentPage - 1)} />
</PaginationItem>
{pagination(currentPage, pagesCount).map((page, index) => (
<PaginationItem
key={page !== ellipsis ? page : `${page}_${index}`}
active={page === currentPage}
disabled={page === ellipsis}
>
<PaginationLink tag="span" onClick={onClick(page)}>{page}</PaginationLink>
</PaginationItem>
))}
<PaginationItem disabled={currentPage >= pagesCount}>
<PaginationLink next tag="span" onClick={onClick(currentPage + 1)} />
</PaginationItem>
</Pagination>
);
};
SimplePaginator.propTypes = propTypes;
export default SimplePaginator;

View file

@ -0,0 +1,3 @@
.simple-paginator {
user-select: none;
}

View file

@ -1,10 +1,10 @@
import React from 'react';
import PropTypes from 'prop-types';
import { fromPairs, head, keys, pipe, prop, reverse, sortBy, splitEvery, toLower, toPairs, type } from 'ramda';
import { Pagination, PaginationItem, PaginationLink } from 'reactstrap';
import SortingDropdown from '../utils/SortingDropdown';
import PaginationDropdown from '../utils/PaginationDropdown';
import { rangeOf, roundTen } from '../utils/utils';
import SimplePaginator from '../common/SimplePaginator';
import GraphCard from './GraphCard';
const { max } = Math;
@ -66,22 +66,9 @@ export default class SortableBarGraph extends React.Component {
renderPagination(pagesCount) {
const { currentPage } = this.state;
const setCurrentPage = (currentPage) => this.setState({ currentPage });
return (
<Pagination listClassName="flex-wrap mb-0">
<PaginationItem disabled={currentPage === 1}>
<PaginationLink previous tag="span" onClick={() => this.setState({ currentPage: currentPage - 1 })} />
</PaginationItem>
{rangeOf(pagesCount, (page) => (
<PaginationItem key={page} active={page === currentPage}>
<PaginationLink tag="span" onClick={() => this.setState({ currentPage: page })}>{page}</PaginationLink>
</PaginationItem>
))}
<PaginationItem disabled={currentPage >= pagesCount}>
<PaginationLink next tag="span" onClick={() => this.setState({ currentPage: currentPage + 1 })} />
</PaginationItem>
</Pagination>
);
return <SimplePaginator currentPage={currentPage} pagesCount={pagesCount} setCurrentPage={setCurrentPage} />;
}
render() {

View file

@ -0,0 +1,53 @@
import React from 'react';
import { shallow } from 'enzyme';
import { identity } from 'ramda';
import each from 'jest-each';
import { PaginationItem } from 'reactstrap';
import SimplePaginator, { ellipsis } from '../../src/common/SimplePaginator';
describe('<SimplePaginator />', () => {
let wrapper;
const createWrapper = (pagesCount, currentPage = 1) => {
wrapper = shallow(<SimplePaginator pagesCount={pagesCount} currentPage={currentPage} setCurrentPage={identity} />);
return wrapper;
};
afterEach(() => wrapper && wrapper.unmount());
each([ -3, -2, 0, 1 ]).it('renders empty when the amount of pages is smaller than 2', (pagesCount) => {
expect(createWrapper(pagesCount).text()).toEqual('');
});
describe('ellipsis are rendered where expected', () => {
const getItemsForPages = (pagesCount, currentPage) => {
const paginator = createWrapper(pagesCount, currentPage);
const items = paginator.find(PaginationItem);
const itemsWithEllipsis = items.filterWhere((item) => item.key() && item.key().includes(ellipsis));
return { items, itemsWithEllipsis };
};
it('renders first ellipsis', () => {
const { items, itemsWithEllipsis } = getItemsForPages(9, 7);
expect(items.at(2).html()).toContain(ellipsis);
expect(itemsWithEllipsis).toHaveLength(1);
});
it('renders last ellipsis', () => {
const { items, itemsWithEllipsis } = getItemsForPages(9, 2);
expect(items.at(items.length - 3).html()).toContain(ellipsis);
expect(itemsWithEllipsis).toHaveLength(1);
});
it('renders both ellipsis', () => {
const { items, itemsWithEllipsis } = getItemsForPages(20, 9);
expect(items.at(2).html()).toContain(ellipsis);
expect(items.at(items.length - 3).html()).toContain(ellipsis);
expect(itemsWithEllipsis).toHaveLength(2);
});
});
});

View file

@ -49,12 +49,10 @@ describe('<SortableBarGraph />', () => {
assert = (sortName, sortDir, expectedKeys, expectedValues, done) => {
dropdown.prop('onChange')(sortName, sortDir);
setImmediate(() => {
const graphCard = wrapper.find(GraphCard);
const statsKeys = keys(graphCard.prop('stats'));
const statsValues = values(graphCard.prop('stats'));
const stats = wrapper.find(GraphCard).prop('stats');
expect(statsKeys).toEqual(expectedKeys);
expect(statsValues).toEqual(expectedValues);
expect(keys(stats)).toEqual(expectedKeys);
expect(values(stats)).toEqual(expectedValues);
done();
});
};
@ -80,10 +78,9 @@ describe('<SortableBarGraph />', () => {
assert = (itemsPerPage, expectedStats, done) => {
dropdown.prop('setValue')(itemsPerPage);
setImmediate(() => {
const graphCard = wrapper.find(GraphCard);
const statsKeys = keys(graphCard.prop('stats'));
const stats = wrapper.find(GraphCard).prop('stats');
expect(statsKeys).toEqual(expectedStats);
expect(keys(stats)).toEqual(expectedStats);
done();
});
};