diff --git a/.travis.yml b/.travis.yml index 8f155e19..eee120f2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -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 diff --git a/CHANGELOG.md b/CHANGELOG.md index 66a55f29..30736195 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/package-lock.json b/package-lock.json index 3bbeeb1c..1aad2d52 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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": { diff --git a/package.json b/package.json index 07e4033f..09dc06f4 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/common/SimplePaginator.js b/src/common/SimplePaginator.js new file mode 100644 index 00000000..abc59c6c --- /dev/null +++ b/src/common/SimplePaginator.js @@ -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(currentPage, pagesCount).map((page, index) => ( + + {page} + + ))} + = pagesCount}> + + + + ); +}; + +SimplePaginator.propTypes = propTypes; + +export default SimplePaginator; diff --git a/src/common/SimplePaginator.scss b/src/common/SimplePaginator.scss new file mode 100644 index 00000000..62fdd446 --- /dev/null +++ b/src/common/SimplePaginator.scss @@ -0,0 +1,3 @@ +.simple-paginator { + user-select: none; +} diff --git a/src/visits/SortableBarGraph.js b/src/visits/SortableBarGraph.js index c51245f0..7e236e76 100644 --- a/src/visits/SortableBarGraph.js +++ b/src/visits/SortableBarGraph.js @@ -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 ( - - - this.setState({ currentPage: currentPage - 1 })} /> - - {rangeOf(pagesCount, (page) => ( - - this.setState({ currentPage: page })}>{page} - - ))} - = pagesCount}> - this.setState({ currentPage: currentPage + 1 })} /> - - - ); + return ; } render() { diff --git a/test/common/SimplePaginator.test.js b/test/common/SimplePaginator.test.js new file mode 100644 index 00000000..29101786 --- /dev/null +++ b/test/common/SimplePaginator.test.js @@ -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('', () => { + let wrapper; + const createWrapper = (pagesCount, currentPage = 1) => { + wrapper = shallow(); + + 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); + }); + }); +}); diff --git a/test/visits/SortableBarGraph.test.js b/test/visits/SortableBarGraph.test.js index 6a49c720..e947d7f8 100644 --- a/test/visits/SortableBarGraph.test.js +++ b/test/visits/SortableBarGraph.test.js @@ -49,12 +49,10 @@ describe('', () => { 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('', () => { 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(); }); };