Fixed conflicts

This commit is contained in:
Alejandro Celaya 2022-02-08 19:40:51 +01:00
commit 338c2a1191
51 changed files with 508 additions and 903 deletions

View file

@ -17,6 +17,8 @@
"ignorePatterns": ["src/service*.ts"], "ignorePatterns": ["src/service*.ts"],
"rules": { "rules": {
"complexity": "off", "complexity": "off",
"@typescript-eslint/no-unnecessary-type-assertion": "off" "@typescript-eslint/no-unnecessary-type-assertion": "off",
"@typescript-eslint/no-unsafe-return": "off",
"@typescript-eslint/no-unsafe-call": "off"
} }
} }

View file

@ -11,6 +11,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
### Changed ### Changed
* [#567](https://github.com/shlinkio/shlink-web-client/pull/567) Improved Shlink 3.0.0 compatibility by checking the `INVALID_SHORT_URL_DELETION` error code when deleting short URLs. * [#567](https://github.com/shlinkio/shlink-web-client/pull/567) Improved Shlink 3.0.0 compatibility by checking the `INVALID_SHORT_URL_DELETION` error code when deleting short URLs.
* [#524](https://github.com/shlinkio/shlink-web-client/pull/524) Updated to react-router v6.
### Deprecated ### Deprecated
* *Nothing* * *Nothing*

527
package-lock.json generated
View file

@ -35,7 +35,7 @@
"react-external-link": "^1.2.0", "react-external-link": "^1.2.0",
"react-leaflet": "^3.1.0", "react-leaflet": "^3.1.0",
"react-redux": "^7.2.2", "react-redux": "^7.2.2",
"react-router-dom": "^5.2.0", "react-router-dom": "^6.2.1",
"react-swipeable": "^6.0.1", "react-swipeable": "^6.0.1",
"react-tag-autocomplete": "^6.1.0", "react-tag-autocomplete": "^6.1.0",
"reactstrap": "^8.9.0", "reactstrap": "^8.9.0",
@ -71,7 +71,6 @@
"@types/react-dom": "^17.0.1", "@types/react-dom": "^17.0.1",
"@types/react-leaflet": "^2.5.2", "@types/react-leaflet": "^2.5.2",
"@types/react-redux": "^7.1.16", "@types/react-redux": "^7.1.16",
"@types/react-router-dom": "^5.1.7",
"@types/react-tag-autocomplete": "^6.1.0", "@types/react-tag-autocomplete": "^6.1.0",
"@types/uuid": "^8.3.0", "@types/uuid": "^8.3.0",
"@wojtekmaj/enzyme-adapter-react-17": "^0.6.5", "@wojtekmaj/enzyme-adapter-react-17": "^0.6.5",
@ -2194,11 +2193,14 @@
} }
}, },
"node_modules/@babel/runtime": { "node_modules/@babel/runtime": {
"version": "7.2.0", "version": "7.17.0",
"resolved": "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.2.0.tgz", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.17.0.tgz",
"integrity": "sha1-sD5C7t31iY4AZG5MhA+ge6jcrX8=", "integrity": "sha512-etcO/ohMNaNA2UBdaXBBSX/3aEzFMRrVfaPv8Ptc0k+cWpWW0QFiGZ2XnVqQZI1Cf734LbPGmqBKWESfW4x/dQ==",
"dependencies": { "dependencies": {
"regenerator-runtime": "^0.12.0" "regenerator-runtime": "^0.13.4"
},
"engines": {
"node": ">=6.9.0"
} }
}, },
"node_modules/@babel/runtime-corejs3": { "node_modules/@babel/runtime-corejs3": {
@ -2211,12 +2213,6 @@
"regenerator-runtime": "^0.13.4" "regenerator-runtime": "^0.13.4"
} }
}, },
"node_modules/@babel/runtime-corejs3/node_modules/regenerator-runtime": {
"version": "0.13.7",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz",
"integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew==",
"dev": true
},
"node_modules/@babel/template": { "node_modules/@babel/template": {
"version": "7.16.0", "version": "7.16.0",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.16.0.tgz", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.16.0.tgz",
@ -4408,12 +4404,6 @@
"@types/node": "*" "@types/node": "*"
} }
}, },
"node_modules/@types/history": {
"version": "4.7.8",
"resolved": "https://registry.npmjs.org/@types/history/-/history-4.7.8.tgz",
"integrity": "sha512-S78QIYirQcUoo6UJZx9CSP0O2ix9IaeAXwQi26Rhr/+mg7qqPy8TzaxHSUut7eGjL8WmLccT7/MXf304WjqHcA==",
"dev": true
},
"node_modules/@types/hoist-non-react-statics": { "node_modules/@types/hoist-non-react-statics": {
"version": "3.3.1", "version": "3.3.1",
"resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz", "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz",
@ -4619,27 +4609,6 @@
"redux": "^4.0.0" "redux": "^4.0.0"
} }
}, },
"node_modules/@types/react-router": {
"version": "5.1.11",
"resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.11.tgz",
"integrity": "sha512-ofHbZMlp0Y2baOHgsWBQ4K3AttxY61bDMkwTiBOkPg7U6C/3UwwB5WaIx28JmSVi/eX3uFEMRo61BV22fDQIvg==",
"dev": true,
"dependencies": {
"@types/history": "*",
"@types/react": "*"
}
},
"node_modules/@types/react-router-dom": {
"version": "5.1.7",
"resolved": "https://registry.npmjs.org/@types/react-router-dom/-/react-router-dom-5.1.7.tgz",
"integrity": "sha512-D5mHD6TbdV/DNHYsnwBTv+y73ei+mMjrkGrla86HthE4/PVvL1J94Bu3qABU+COXzpL23T1EZapVVpwHuBXiUg==",
"dev": true,
"dependencies": {
"@types/history": "*",
"@types/react": "*",
"@types/react-router": "*"
}
},
"node_modules/@types/react-tag-autocomplete": { "node_modules/@types/react-tag-autocomplete": {
"version": "6.1.0", "version": "6.1.0",
"resolved": "https://registry.npmjs.org/@types/react-tag-autocomplete/-/react-tag-autocomplete-6.1.0.tgz", "resolved": "https://registry.npmjs.org/@types/react-tag-autocomplete/-/react-tag-autocomplete-6.1.0.tgz",
@ -5639,21 +5608,6 @@
"node": ">=6.0" "node": ">=6.0"
} }
}, },
"node_modules/aria-query/node_modules/@babel/runtime": {
"version": "7.13.9",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.13.9.tgz",
"integrity": "sha512-aY2kU+xgJ3dJ1eU6FMB9EH8dIe8dmusF1xEku52joLvw6eAFN0AI+WxCLDnpev2LEejWBAy2sBvBOBAjI3zmvA==",
"dev": true,
"dependencies": {
"regenerator-runtime": "^0.13.4"
}
},
"node_modules/aria-query/node_modules/regenerator-runtime": {
"version": "0.13.7",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz",
"integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew==",
"dev": true
},
"node_modules/arr-diff": { "node_modules/arr-diff": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz", "resolved": "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz",
@ -6757,15 +6711,6 @@
"resolve": "^1.12.0" "resolve": "^1.12.0"
} }
}, },
"node_modules/babel-plugin-macros/node_modules/@babel/runtime": {
"version": "7.12.5",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.12.5.tgz",
"integrity": "sha512-plcc+hbExy3McchJCEQG3knOsuh3HH+Prx1P6cLIkET/0dLuQDEnrT+s27Axgc9bqfsmNUNHfscgMUdBpC9xfg==",
"dev": true,
"dependencies": {
"regenerator-runtime": "^0.13.4"
}
},
"node_modules/babel-plugin-macros/node_modules/cosmiconfig": { "node_modules/babel-plugin-macros/node_modules/cosmiconfig": {
"version": "6.0.0", "version": "6.0.0",
"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-6.0.0.tgz", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-6.0.0.tgz",
@ -6822,12 +6767,6 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/babel-plugin-macros/node_modules/regenerator-runtime": {
"version": "0.13.7",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz",
"integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew==",
"dev": true
},
"node_modules/babel-plugin-macros/node_modules/resolve-from": { "node_modules/babel-plugin-macros/node_modules/resolve-from": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
@ -7895,12 +7834,6 @@
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
"dev": true "dev": true
}, },
"node_modules/babel-preset-react-app/node_modules/regenerator-runtime": {
"version": "0.13.7",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz",
"integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew==",
"dev": true
},
"node_modules/babel-preset-react-app/node_modules/regenerator-transform": { "node_modules/babel-preset-react-app/node_modules/regenerator-transform": {
"version": "0.14.5", "version": "0.14.5",
"resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.14.5.tgz", "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.14.5.tgz",
@ -12429,27 +12362,12 @@
"eslint": "^3 || ^4 || ^5 || ^6 || ^7" "eslint": "^3 || ^4 || ^5 || ^6 || ^7"
} }
}, },
"node_modules/eslint-plugin-jsx-a11y/node_modules/@babel/runtime": {
"version": "7.13.9",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.13.9.tgz",
"integrity": "sha512-aY2kU+xgJ3dJ1eU6FMB9EH8dIe8dmusF1xEku52joLvw6eAFN0AI+WxCLDnpev2LEejWBAy2sBvBOBAjI3zmvA==",
"dev": true,
"dependencies": {
"regenerator-runtime": "^0.13.4"
}
},
"node_modules/eslint-plugin-jsx-a11y/node_modules/emoji-regex": { "node_modules/eslint-plugin-jsx-a11y/node_modules/emoji-regex": {
"version": "9.2.2", "version": "9.2.2",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
"dev": true "dev": true
}, },
"node_modules/eslint-plugin-jsx-a11y/node_modules/regenerator-runtime": {
"version": "0.13.7",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz",
"integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew==",
"dev": true
},
"node_modules/eslint-plugin-node": { "node_modules/eslint-plugin-node": {
"version": "11.1.0", "version": "11.1.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-node/-/eslint-plugin-node-11.1.0.tgz", "resolved": "https://registry.npmjs.org/eslint-plugin-node/-/eslint-plugin-node-11.1.0.tgz",
@ -15454,16 +15372,11 @@
"dev": true "dev": true
}, },
"node_modules/history": { "node_modules/history": {
"version": "4.10.1", "version": "5.2.0",
"resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz", "resolved": "https://registry.npmjs.org/history/-/history-5.2.0.tgz",
"integrity": "sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==", "integrity": "sha512-uPSF6lAJb3nSePJ43hN3eKj1dTWpN9gMod0ZssbFTIsen+WehTmEadgL+kg78xLJFdRfrrC//SavDzmRVdE+Ig==",
"dependencies": { "dependencies": {
"@babel/runtime": "^7.1.2", "@babel/runtime": "^7.7.6"
"loose-envify": "^1.2.0",
"resolve-pathname": "^3.0.0",
"tiny-invariant": "^1.0.2",
"tiny-warning": "^1.0.0",
"value-equal": "^1.0.1"
} }
}, },
"node_modules/hmac-drbg": { "node_modules/hmac-drbg": {
@ -19379,32 +19292,6 @@
"node": ">=4" "node": ">=4"
} }
}, },
"node_modules/mini-create-react-context": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/mini-create-react-context/-/mini-create-react-context-0.4.0.tgz",
"integrity": "sha512-b0TytUgFSbgFJGzJqXPKCFCBWigAjpjo+Fl7Vf7ZbKRDptszpppKxXH6DRXEABZ/gcEQczeb0iZ7JvL8e8jjCA==",
"dependencies": {
"@babel/runtime": "^7.5.5",
"tiny-warning": "^1.0.3"
},
"peerDependencies": {
"prop-types": "^15.0.0",
"react": "^0.14.0 || ^15.0.0 || ^16.0.0"
}
},
"node_modules/mini-create-react-context/node_modules/@babel/runtime": {
"version": "7.11.2",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.11.2.tgz",
"integrity": "sha512-TeWkU52so0mPtDcaCTxNBI/IHiz0pZgr8VEFqXFtZWpYD08ZB6FaSwVAS8MKRQAP3bYKiVjwysOJgMFY28o6Tw==",
"dependencies": {
"regenerator-runtime": "^0.13.4"
}
},
"node_modules/mini-create-react-context/node_modules/regenerator-runtime": {
"version": "0.13.7",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz",
"integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew=="
},
"node_modules/mini-css-extract-plugin": { "node_modules/mini-css-extract-plugin": {
"version": "1.3.1", "version": "1.3.1",
"resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-1.3.1.tgz", "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-1.3.1.tgz",
@ -21113,6 +21000,7 @@
"version": "1.7.0", "version": "1.7.0",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.7.0.tgz", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.7.0.tgz",
"integrity": "sha1-Wf3g9DW62suhA6hOnTvGTpa5k30=", "integrity": "sha1-Wf3g9DW62suhA6hOnTvGTpa5k30=",
"dev": true,
"dependencies": { "dependencies": {
"isarray": "0.0.1" "isarray": "0.0.1"
} }
@ -21120,7 +21008,8 @@
"node_modules/path-to-regexp/node_modules/isarray": { "node_modules/path-to-regexp/node_modules/isarray": {
"version": "0.0.1", "version": "0.0.1",
"resolved": "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz", "resolved": "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz",
"integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=",
"dev": true
}, },
"node_modules/path-type": { "node_modules/path-type": {
"version": "4.0.0", "version": "4.0.0",
@ -26602,12 +26491,6 @@
"asap": "~2.0.6" "asap": "~2.0.6"
} }
}, },
"node_modules/react-app-polyfill/node_modules/regenerator-runtime": {
"version": "0.13.7",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz",
"integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew==",
"dev": true
},
"node_modules/react-chartjs-2": { "node_modules/react-chartjs-2": {
"version": "3.0.4", "version": "3.0.4",
"resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-3.0.4.tgz", "resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-3.0.4.tgz",
@ -27022,59 +26905,33 @@
} }
} }
}, },
"node_modules/react-redux/node_modules/@babel/runtime": {
"version": "7.12.5",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.12.5.tgz",
"integrity": "sha512-plcc+hbExy3McchJCEQG3knOsuh3HH+Prx1P6cLIkET/0dLuQDEnrT+s27Axgc9bqfsmNUNHfscgMUdBpC9xfg==",
"dependencies": {
"regenerator-runtime": "^0.13.4"
}
},
"node_modules/react-redux/node_modules/react-is": { "node_modules/react-redux/node_modules/react-is": {
"version": "16.13.1", "version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
}, },
"node_modules/react-redux/node_modules/regenerator-runtime": {
"version": "0.13.7",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz",
"integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew=="
},
"node_modules/react-router": { "node_modules/react-router": {
"version": "5.2.0", "version": "6.2.1",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-5.2.0.tgz", "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.2.1.tgz",
"integrity": "sha512-smz1DUuFHRKdcJC0jobGo8cVbhO3x50tCL4icacOlcwDOEQPq4TMqwx3sY1TP+DvtTgz4nm3thuo7A+BK2U0Dw==", "integrity": "sha512-2fG0udBtxou9lXtK97eJeET2ki5//UWfQSl1rlJ7quwe6jrktK9FCCc8dQb5QY6jAv3jua8bBQRhhDOM/kVRsg==",
"dependencies": { "dependencies": {
"@babel/runtime": "^7.1.2", "history": "^5.2.0"
"history": "^4.9.0",
"hoist-non-react-statics": "^3.1.0",
"loose-envify": "^1.3.1",
"mini-create-react-context": "^0.4.0",
"path-to-regexp": "^1.7.0",
"prop-types": "^15.6.2",
"react-is": "^16.6.0",
"tiny-invariant": "^1.0.2",
"tiny-warning": "^1.0.0"
}, },
"peerDependencies": { "peerDependencies": {
"react": ">=15" "react": ">=16.8"
} }
}, },
"node_modules/react-router-dom": { "node_modules/react-router-dom": {
"version": "5.2.0", "version": "6.2.1",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.2.0.tgz", "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.2.1.tgz",
"integrity": "sha512-gxAmfylo2QUjcwxI63RhQ5G85Qqt4voZpUXSEqCwykV0baaOTQDR1f0PmY8AELqIyVc0NEZUj0Gov5lNGcXgsA==", "integrity": "sha512-I6Zax+/TH/cZMDpj3/4Fl2eaNdcvoxxHoH1tYOREsQ22OKDYofGebrNm6CTPUcvLvZm63NL/vzCYdjf9CUhqmA==",
"dependencies": { "dependencies": {
"@babel/runtime": "^7.1.2", "history": "^5.2.0",
"history": "^4.9.0", "react-router": "6.2.1"
"loose-envify": "^1.3.1",
"prop-types": "^15.6.2",
"react-router": "5.2.0",
"tiny-invariant": "^1.0.2",
"tiny-warning": "^1.0.0"
}, },
"peerDependencies": { "peerDependencies": {
"react": ">=15" "react": ">=16.8",
"react-dom": ">=16.8"
} }
}, },
"node_modules/react-shallow-renderer": { "node_modules/react-shallow-renderer": {
@ -27156,19 +27013,6 @@
"react-dom": ">=16.3.0" "react-dom": ">=16.3.0"
} }
}, },
"node_modules/reactstrap/node_modules/@babel/runtime": {
"version": "7.13.8",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.13.8.tgz",
"integrity": "sha512-CwQljpw6qSayc0fRG1soxHAKs1CnQMOChm4mlQP6My0kf9upVGizj/KhlTTgyUnETmHpcUXjaluNAkteRFuafg==",
"dependencies": {
"regenerator-runtime": "^0.13.4"
}
},
"node_modules/reactstrap/node_modules/regenerator-runtime": {
"version": "0.13.7",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz",
"integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew=="
},
"node_modules/read-pkg": { "node_modules/read-pkg": {
"version": "5.2.0", "version": "5.2.0",
"resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz",
@ -27381,9 +27225,9 @@
} }
}, },
"node_modules/regenerator-runtime": { "node_modules/regenerator-runtime": {
"version": "0.12.1", "version": "0.13.9",
"resolved": "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.12.1.tgz", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz",
"integrity": "sha1-+hpxVEdkwDb4xJsToIsllMn4oN4=" "integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA=="
}, },
"node_modules/regenerator-transform": { "node_modules/regenerator-transform": {
"version": "0.14.5", "version": "0.14.5",
@ -27394,21 +27238,6 @@
"@babel/runtime": "^7.8.4" "@babel/runtime": "^7.8.4"
} }
}, },
"node_modules/regenerator-transform/node_modules/@babel/runtime": {
"version": "7.13.8",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.13.8.tgz",
"integrity": "sha512-CwQljpw6qSayc0fRG1soxHAKs1CnQMOChm4mlQP6My0kf9upVGizj/KhlTTgyUnETmHpcUXjaluNAkteRFuafg==",
"dev": true,
"dependencies": {
"regenerator-runtime": "^0.13.4"
}
},
"node_modules/regenerator-transform/node_modules/regenerator-runtime": {
"version": "0.13.7",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz",
"integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew==",
"dev": true
},
"node_modules/regex-not": { "node_modules/regex-not": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.yarnpkg.com/regex-not/-/regex-not-1.0.2.tgz", "resolved": "https://registry.yarnpkg.com/regex-not/-/regex-not-1.0.2.tgz",
@ -27880,11 +27709,6 @@
"node": ">=4" "node": ">=4"
} }
}, },
"node_modules/resolve-pathname": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/resolve-pathname/-/resolve-pathname-3.0.0.tgz",
"integrity": "sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng=="
},
"node_modules/resolve-url": { "node_modules/resolve-url": {
"version": "0.2.1", "version": "0.2.1",
"resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz",
@ -31802,16 +31626,6 @@
"integrity": "sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=", "integrity": "sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=",
"dev": true "dev": true
}, },
"node_modules/tiny-invariant": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.1.0.tgz",
"integrity": "sha512-ytxQvrb1cPc9WBEI/HSeYYoGD0kWnGEOR8RY6KomWLBVhqz0RgTwVO9dLrGz7dC+nN9llyI7OKAgRq8Vq4ZBSw=="
},
"node_modules/tiny-warning": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz",
"integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA=="
},
"node_modules/tinycolor2": { "node_modules/tinycolor2": {
"version": "1.4.2", "version": "1.4.2",
"resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.4.2.tgz", "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.4.2.tgz",
@ -33068,11 +32882,6 @@
"spdx-expression-parse": "^3.0.0" "spdx-expression-parse": "^3.0.0"
} }
}, },
"node_modules/value-equal": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/value-equal/-/value-equal-1.0.1.tgz",
"integrity": "sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw=="
},
"node_modules/vary": { "node_modules/vary": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
@ -34384,15 +34193,6 @@
"node": ">=10.0.0" "node": ">=10.0.0"
} }
}, },
"node_modules/workbox-build/node_modules/@babel/runtime": {
"version": "7.14.0",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.14.0.tgz",
"integrity": "sha512-JELkvo/DlpNdJ7dlyw/eY7E0suy5i5GQH+Vlxaq1nsNJ+H7f4Vtv3jMeCEgRhZZQFXTjldYfQgv2qmM6M1v5wA==",
"dev": true,
"dependencies": {
"regenerator-runtime": "^0.13.4"
}
},
"node_modules/workbox-build/node_modules/fast-json-stable-stringify": { "node_modules/workbox-build/node_modules/fast-json-stable-stringify": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
@ -34411,12 +34211,6 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/workbox-build/node_modules/regenerator-runtime": {
"version": "0.13.7",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz",
"integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew==",
"dev": true
},
"node_modules/workbox-build/node_modules/source-map": { "node_modules/workbox-build/node_modules/source-map": {
"version": "0.8.0-beta.0", "version": "0.8.0-beta.0",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.8.0-beta.0.tgz", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.8.0-beta.0.tgz",
@ -36554,11 +36348,11 @@
} }
}, },
"@babel/runtime": { "@babel/runtime": {
"version": "7.2.0", "version": "7.17.0",
"resolved": "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.2.0.tgz", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.17.0.tgz",
"integrity": "sha1-sD5C7t31iY4AZG5MhA+ge6jcrX8=", "integrity": "sha512-etcO/ohMNaNA2UBdaXBBSX/3aEzFMRrVfaPv8Ptc0k+cWpWW0QFiGZ2XnVqQZI1Cf734LbPGmqBKWESfW4x/dQ==",
"requires": { "requires": {
"regenerator-runtime": "^0.12.0" "regenerator-runtime": "^0.13.4"
} }
}, },
"@babel/runtime-corejs3": { "@babel/runtime-corejs3": {
@ -36569,14 +36363,6 @@
"requires": { "requires": {
"core-js-pure": "^3.0.0", "core-js-pure": "^3.0.0",
"regenerator-runtime": "^0.13.4" "regenerator-runtime": "^0.13.4"
},
"dependencies": {
"regenerator-runtime": {
"version": "0.13.7",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz",
"integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew==",
"dev": true
}
} }
}, },
"@babel/template": { "@babel/template": {
@ -38204,12 +37990,6 @@
"@types/node": "*" "@types/node": "*"
} }
}, },
"@types/history": {
"version": "4.7.8",
"resolved": "https://registry.npmjs.org/@types/history/-/history-4.7.8.tgz",
"integrity": "sha512-S78QIYirQcUoo6UJZx9CSP0O2ix9IaeAXwQi26Rhr/+mg7qqPy8TzaxHSUut7eGjL8WmLccT7/MXf304WjqHcA==",
"dev": true
},
"@types/hoist-non-react-statics": { "@types/hoist-non-react-statics": {
"version": "3.3.1", "version": "3.3.1",
"resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz", "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz",
@ -38415,27 +38195,6 @@
"redux": "^4.0.0" "redux": "^4.0.0"
} }
}, },
"@types/react-router": {
"version": "5.1.11",
"resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.11.tgz",
"integrity": "sha512-ofHbZMlp0Y2baOHgsWBQ4K3AttxY61bDMkwTiBOkPg7U6C/3UwwB5WaIx28JmSVi/eX3uFEMRo61BV22fDQIvg==",
"dev": true,
"requires": {
"@types/history": "*",
"@types/react": "*"
}
},
"@types/react-router-dom": {
"version": "5.1.7",
"resolved": "https://registry.npmjs.org/@types/react-router-dom/-/react-router-dom-5.1.7.tgz",
"integrity": "sha512-D5mHD6TbdV/DNHYsnwBTv+y73ei+mMjrkGrla86HthE4/PVvL1J94Bu3qABU+COXzpL23T1EZapVVpwHuBXiUg==",
"dev": true,
"requires": {
"@types/history": "*",
"@types/react": "*",
"@types/react-router": "*"
}
},
"@types/react-tag-autocomplete": { "@types/react-tag-autocomplete": {
"version": "6.1.0", "version": "6.1.0",
"resolved": "https://registry.npmjs.org/@types/react-tag-autocomplete/-/react-tag-autocomplete-6.1.0.tgz", "resolved": "https://registry.npmjs.org/@types/react-tag-autocomplete/-/react-tag-autocomplete-6.1.0.tgz",
@ -39224,23 +38983,6 @@
"requires": { "requires": {
"@babel/runtime": "^7.10.2", "@babel/runtime": "^7.10.2",
"@babel/runtime-corejs3": "^7.10.2" "@babel/runtime-corejs3": "^7.10.2"
},
"dependencies": {
"@babel/runtime": {
"version": "7.13.9",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.13.9.tgz",
"integrity": "sha512-aY2kU+xgJ3dJ1eU6FMB9EH8dIe8dmusF1xEku52joLvw6eAFN0AI+WxCLDnpev2LEejWBAy2sBvBOBAjI3zmvA==",
"dev": true,
"requires": {
"regenerator-runtime": "^0.13.4"
}
},
"regenerator-runtime": {
"version": "0.13.7",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz",
"integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew==",
"dev": true
}
} }
}, },
"arr-diff": { "arr-diff": {
@ -40062,15 +39804,6 @@
"resolve": "^1.12.0" "resolve": "^1.12.0"
}, },
"dependencies": { "dependencies": {
"@babel/runtime": {
"version": "7.12.5",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.12.5.tgz",
"integrity": "sha512-plcc+hbExy3McchJCEQG3knOsuh3HH+Prx1P6cLIkET/0dLuQDEnrT+s27Axgc9bqfsmNUNHfscgMUdBpC9xfg==",
"dev": true,
"requires": {
"regenerator-runtime": "^0.13.4"
}
},
"cosmiconfig": { "cosmiconfig": {
"version": "6.0.0", "version": "6.0.0",
"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-6.0.0.tgz", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-6.0.0.tgz",
@ -40112,12 +39845,6 @@
"integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
"dev": true "dev": true
}, },
"regenerator-runtime": {
"version": "0.13.7",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz",
"integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew==",
"dev": true
},
"resolve-from": { "resolve-from": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
@ -40980,12 +40707,6 @@
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
"dev": true "dev": true
}, },
"regenerator-runtime": {
"version": "0.13.7",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz",
"integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew==",
"dev": true
},
"regenerator-transform": { "regenerator-transform": {
"version": "0.14.5", "version": "0.14.5",
"resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.14.5.tgz", "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.14.5.tgz",
@ -44728,26 +44449,11 @@
"language-tags": "^1.0.5" "language-tags": "^1.0.5"
}, },
"dependencies": { "dependencies": {
"@babel/runtime": {
"version": "7.13.9",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.13.9.tgz",
"integrity": "sha512-aY2kU+xgJ3dJ1eU6FMB9EH8dIe8dmusF1xEku52joLvw6eAFN0AI+WxCLDnpev2LEejWBAy2sBvBOBAjI3zmvA==",
"dev": true,
"requires": {
"regenerator-runtime": "^0.13.4"
}
},
"emoji-regex": { "emoji-regex": {
"version": "9.2.2", "version": "9.2.2",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
"dev": true "dev": true
},
"regenerator-runtime": {
"version": "0.13.7",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz",
"integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew==",
"dev": true
} }
} }
}, },
@ -46919,16 +46625,11 @@
"dev": true "dev": true
}, },
"history": { "history": {
"version": "4.10.1", "version": "5.2.0",
"resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz", "resolved": "https://registry.npmjs.org/history/-/history-5.2.0.tgz",
"integrity": "sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==", "integrity": "sha512-uPSF6lAJb3nSePJ43hN3eKj1dTWpN9gMod0ZssbFTIsen+WehTmEadgL+kg78xLJFdRfrrC//SavDzmRVdE+Ig==",
"requires": { "requires": {
"@babel/runtime": "^7.1.2", "@babel/runtime": "^7.7.6"
"loose-envify": "^1.2.0",
"resolve-pathname": "^3.0.0",
"tiny-invariant": "^1.0.2",
"tiny-warning": "^1.0.0",
"value-equal": "^1.0.1"
} }
}, },
"hmac-drbg": { "hmac-drbg": {
@ -49941,30 +49642,6 @@
"integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==",
"dev": true "dev": true
}, },
"mini-create-react-context": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/mini-create-react-context/-/mini-create-react-context-0.4.0.tgz",
"integrity": "sha512-b0TytUgFSbgFJGzJqXPKCFCBWigAjpjo+Fl7Vf7ZbKRDptszpppKxXH6DRXEABZ/gcEQczeb0iZ7JvL8e8jjCA==",
"requires": {
"@babel/runtime": "^7.5.5",
"tiny-warning": "^1.0.3"
},
"dependencies": {
"@babel/runtime": {
"version": "7.11.2",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.11.2.tgz",
"integrity": "sha512-TeWkU52so0mPtDcaCTxNBI/IHiz0pZgr8VEFqXFtZWpYD08ZB6FaSwVAS8MKRQAP3bYKiVjwysOJgMFY28o6Tw==",
"requires": {
"regenerator-runtime": "^0.13.4"
}
},
"regenerator-runtime": {
"version": "0.13.7",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz",
"integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew=="
}
}
},
"mini-css-extract-plugin": { "mini-css-extract-plugin": {
"version": "1.3.1", "version": "1.3.1",
"resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-1.3.1.tgz", "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-1.3.1.tgz",
@ -51298,6 +50975,7 @@
"version": "1.7.0", "version": "1.7.0",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.7.0.tgz", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.7.0.tgz",
"integrity": "sha1-Wf3g9DW62suhA6hOnTvGTpa5k30=", "integrity": "sha1-Wf3g9DW62suhA6hOnTvGTpa5k30=",
"dev": true,
"requires": { "requires": {
"isarray": "0.0.1" "isarray": "0.0.1"
}, },
@ -51305,7 +50983,8 @@
"isarray": { "isarray": {
"version": "0.0.1", "version": "0.0.1",
"resolved": "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz", "resolved": "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz",
"integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=",
"dev": true
} }
} }
}, },
@ -55656,12 +55335,6 @@
"requires": { "requires": {
"asap": "~2.0.6" "asap": "~2.0.6"
} }
},
"regenerator-runtime": {
"version": "0.13.7",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz",
"integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew==",
"dev": true
} }
} }
}, },
@ -55972,55 +55645,28 @@
"react-is": "^16.13.1" "react-is": "^16.13.1"
}, },
"dependencies": { "dependencies": {
"@babel/runtime": {
"version": "7.12.5",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.12.5.tgz",
"integrity": "sha512-plcc+hbExy3McchJCEQG3knOsuh3HH+Prx1P6cLIkET/0dLuQDEnrT+s27Axgc9bqfsmNUNHfscgMUdBpC9xfg==",
"requires": {
"regenerator-runtime": "^0.13.4"
}
},
"react-is": { "react-is": {
"version": "16.13.1", "version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
},
"regenerator-runtime": {
"version": "0.13.7",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz",
"integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew=="
} }
} }
}, },
"react-router": { "react-router": {
"version": "5.2.0", "version": "6.2.1",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-5.2.0.tgz", "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.2.1.tgz",
"integrity": "sha512-smz1DUuFHRKdcJC0jobGo8cVbhO3x50tCL4icacOlcwDOEQPq4TMqwx3sY1TP+DvtTgz4nm3thuo7A+BK2U0Dw==", "integrity": "sha512-2fG0udBtxou9lXtK97eJeET2ki5//UWfQSl1rlJ7quwe6jrktK9FCCc8dQb5QY6jAv3jua8bBQRhhDOM/kVRsg==",
"requires": { "requires": {
"@babel/runtime": "^7.1.2", "history": "^5.2.0"
"history": "^4.9.0",
"hoist-non-react-statics": "^3.1.0",
"loose-envify": "^1.3.1",
"mini-create-react-context": "^0.4.0",
"path-to-regexp": "^1.7.0",
"prop-types": "^15.6.2",
"react-is": "^16.6.0",
"tiny-invariant": "^1.0.2",
"tiny-warning": "^1.0.0"
} }
}, },
"react-router-dom": { "react-router-dom": {
"version": "5.2.0", "version": "6.2.1",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.2.0.tgz", "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.2.1.tgz",
"integrity": "sha512-gxAmfylo2QUjcwxI63RhQ5G85Qqt4voZpUXSEqCwykV0baaOTQDR1f0PmY8AELqIyVc0NEZUj0Gov5lNGcXgsA==", "integrity": "sha512-I6Zax+/TH/cZMDpj3/4Fl2eaNdcvoxxHoH1tYOREsQ22OKDYofGebrNm6CTPUcvLvZm63NL/vzCYdjf9CUhqmA==",
"requires": { "requires": {
"@babel/runtime": "^7.1.2", "history": "^5.2.0",
"history": "^4.9.0", "react-router": "6.2.1"
"loose-envify": "^1.3.1",
"prop-types": "^15.6.2",
"react-router": "5.2.0",
"tiny-invariant": "^1.0.2",
"tiny-warning": "^1.0.0"
} }
}, },
"react-shallow-renderer": { "react-shallow-renderer": {
@ -56082,21 +55728,6 @@
"prop-types": "^15.5.8", "prop-types": "^15.5.8",
"react-popper": "^1.3.6", "react-popper": "^1.3.6",
"react-transition-group": "^2.3.1" "react-transition-group": "^2.3.1"
},
"dependencies": {
"@babel/runtime": {
"version": "7.13.8",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.13.8.tgz",
"integrity": "sha512-CwQljpw6qSayc0fRG1soxHAKs1CnQMOChm4mlQP6My0kf9upVGizj/KhlTTgyUnETmHpcUXjaluNAkteRFuafg==",
"requires": {
"regenerator-runtime": "^0.13.4"
}
},
"regenerator-runtime": {
"version": "0.13.7",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz",
"integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew=="
}
} }
}, },
"read-pkg": { "read-pkg": {
@ -56278,9 +55909,9 @@
} }
}, },
"regenerator-runtime": { "regenerator-runtime": {
"version": "0.12.1", "version": "0.13.9",
"resolved": "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.12.1.tgz", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz",
"integrity": "sha1-+hpxVEdkwDb4xJsToIsllMn4oN4=" "integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA=="
}, },
"regenerator-transform": { "regenerator-transform": {
"version": "0.14.5", "version": "0.14.5",
@ -56289,23 +55920,6 @@
"dev": true, "dev": true,
"requires": { "requires": {
"@babel/runtime": "^7.8.4" "@babel/runtime": "^7.8.4"
},
"dependencies": {
"@babel/runtime": {
"version": "7.13.8",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.13.8.tgz",
"integrity": "sha512-CwQljpw6qSayc0fRG1soxHAKs1CnQMOChm4mlQP6My0kf9upVGizj/KhlTTgyUnETmHpcUXjaluNAkteRFuafg==",
"dev": true,
"requires": {
"regenerator-runtime": "^0.13.4"
}
},
"regenerator-runtime": {
"version": "0.13.7",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz",
"integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew==",
"dev": true
}
} }
}, },
"regex-not": { "regex-not": {
@ -56672,11 +56286,6 @@
"integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
"dev": true "dev": true
}, },
"resolve-pathname": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/resolve-pathname/-/resolve-pathname-3.0.0.tgz",
"integrity": "sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng=="
},
"resolve-url": { "resolve-url": {
"version": "0.2.1", "version": "0.2.1",
"resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz",
@ -59698,16 +59307,6 @@
"integrity": "sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=", "integrity": "sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=",
"dev": true "dev": true
}, },
"tiny-invariant": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.1.0.tgz",
"integrity": "sha512-ytxQvrb1cPc9WBEI/HSeYYoGD0kWnGEOR8RY6KomWLBVhqz0RgTwVO9dLrGz7dC+nN9llyI7OKAgRq8Vq4ZBSw=="
},
"tiny-warning": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz",
"integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA=="
},
"tinycolor2": { "tinycolor2": {
"version": "1.4.2", "version": "1.4.2",
"resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.4.2.tgz", "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.4.2.tgz",
@ -60660,11 +60259,6 @@
"spdx-expression-parse": "^3.0.0" "spdx-expression-parse": "^3.0.0"
} }
}, },
"value-equal": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/value-equal/-/value-equal-1.0.1.tgz",
"integrity": "sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw=="
},
"vary": { "vary": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
@ -61689,15 +61283,6 @@
"workbox-window": "^6.1.5" "workbox-window": "^6.1.5"
}, },
"dependencies": { "dependencies": {
"@babel/runtime": {
"version": "7.14.0",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.14.0.tgz",
"integrity": "sha512-JELkvo/DlpNdJ7dlyw/eY7E0suy5i5GQH+Vlxaq1nsNJ+H7f4Vtv3jMeCEgRhZZQFXTjldYfQgv2qmM6M1v5wA==",
"dev": true,
"requires": {
"regenerator-runtime": "^0.13.4"
}
},
"fast-json-stable-stringify": { "fast-json-stable-stringify": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
@ -61710,12 +61295,6 @@
"integrity": "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==", "integrity": "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==",
"dev": true "dev": true
}, },
"regenerator-runtime": {
"version": "0.13.7",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz",
"integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew==",
"dev": true
},
"source-map": { "source-map": {
"version": "0.8.0-beta.0", "version": "0.8.0-beta.0",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.8.0-beta.0.tgz", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.8.0-beta.0.tgz",

View file

@ -50,7 +50,7 @@
"react-external-link": "^1.2.0", "react-external-link": "^1.2.0",
"react-leaflet": "^3.1.0", "react-leaflet": "^3.1.0",
"react-redux": "^7.2.2", "react-redux": "^7.2.2",
"react-router-dom": "^5.2.0", "react-router-dom": "^6.2.1",
"react-swipeable": "^6.0.1", "react-swipeable": "^6.0.1",
"react-tag-autocomplete": "^6.1.0", "react-tag-autocomplete": "^6.1.0",
"reactstrap": "^8.9.0", "reactstrap": "^8.9.0",
@ -86,7 +86,6 @@
"@types/react-dom": "^17.0.1", "@types/react-dom": "^17.0.1",
"@types/react-leaflet": "^2.5.2", "@types/react-leaflet": "^2.5.2",
"@types/react-redux": "^7.1.16", "@types/react-redux": "^7.1.16",
"@types/react-router-dom": "^5.1.7",
"@types/react-tag-autocomplete": "^6.1.0", "@types/react-tag-autocomplete": "^6.1.0",
"@types/uuid": "^8.3.0", "@types/uuid": "^8.3.0",
"@wojtekmaj/enzyme-adapter-react-17": "^0.6.5", "@wojtekmaj/enzyme-adapter-react-17": "^0.6.5",

View file

@ -1,5 +1,5 @@
import { useEffect, FC } from 'react'; import { useEffect, FC } from 'react';
import { Route, RouteChildrenProps, Switch } from 'react-router-dom'; import { Route, Routes, useLocation } from 'react-router-dom';
import classNames from 'classnames'; import classNames from 'classnames';
import NotFound from '../common/NotFound'; import NotFound from '../common/NotFound';
import { ServersMap } from '../servers/data'; import { ServersMap } from '../servers/data';
@ -9,7 +9,7 @@ import { AppUpdateBanner } from '../common/AppUpdateBanner';
import { forceUpdate } from '../utils/helpers/sw'; import { forceUpdate } from '../utils/helpers/sw';
import './App.scss'; import './App.scss';
interface AppProps extends RouteChildrenProps { interface AppProps {
fetchServers: () => void; fetchServers: () => void;
servers: ServersMap; servers: ServersMap;
settings: Settings; settings: Settings;
@ -26,7 +26,8 @@ const App = (
Settings: FC, Settings: FC,
ManageServers: FC, ManageServers: FC,
ShlinkVersionsContainer: FC, ShlinkVersionsContainer: FC,
) => ({ fetchServers, servers, settings, appUpdated, resetAppUpdate, location }: AppProps) => { ) => ({ fetchServers, servers, settings, appUpdated, resetAppUpdate }: AppProps) => {
const location = useLocation();
const isHome = location.pathname === '/'; const isHome = location.pathname === '/';
useEffect(() => { useEffect(() => {
@ -44,15 +45,15 @@ const App = (
<div className="app"> <div className="app">
<div className={classNames('shlink-wrapper', { 'd-flex d-md-block align-items-center': isHome })}> <div className={classNames('shlink-wrapper', { 'd-flex d-md-block align-items-center': isHome })}>
<Switch> <Routes>
<Route exact path="/" component={Home} /> <Route index element={<Home />} />
<Route exact path="/settings" component={Settings} /> <Route path="/settings" element={<Settings />} />
<Route exact path="/manage-servers" component={ManageServers} /> <Route path="/manage-servers" element={<ManageServers />} />
<Route exact path="/server/create" component={CreateServer} /> <Route path="/server/create" element={<CreateServer />} />
<Route exact path="/server/:serverId/edit" component={EditServer} /> <Route path="/server/:serverId/edit" element={<EditServer />} />
<Route path="/server/:serverId" component={MenuLayout} /> <Route path="/server/:serverId/*" element={<MenuLayout />} />
<Route component={NotFound} /> <Route path="*" element={<NotFound />} />
</Switch> </Routes>
</div> </div>
<div className="shlink-footer"> <div className="shlink-footer">

View file

@ -1,9 +1,9 @@
import Bottle, { Decorator } from 'bottlejs'; import Bottle from 'bottlejs';
import { appUpdateAvailable, resetAppUpdate } from '../reducers/appUpdates'; import { appUpdateAvailable, resetAppUpdate } from '../reducers/appUpdates';
import App from '../App'; import App from '../App';
import { ConnectDecorator } from '../../container/types'; import { ConnectDecorator } from '../../container/types';
const provideServices = (bottle: Bottle, connect: ConnectDecorator, withRouter: Decorator) => { const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
// Components // Components
bottle.serviceFactory( bottle.serviceFactory(
'App', 'App',
@ -18,7 +18,6 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator, withRouter:
'ShlinkVersionsContainer', 'ShlinkVersionsContainer',
); );
bottle.decorator('App', connect([ 'servers', 'settings', 'appUpdated' ], [ 'fetchServers', 'resetAppUpdate' ])); bottle.decorator('App', connect([ 'servers', 'settings', 'appUpdated' ], [ 'fetchServers', 'resetAppUpdate' ]));
bottle.decorator('App', withRouter);
// Actions // Actions
bottle.serviceFactory('appUpdateAvailable', () => appUpdateAvailable); bottle.serviceFactory('appUpdateAvailable', () => appUpdateAvailable);

View file

@ -8,9 +8,8 @@ import {
} from '@fortawesome/free-solid-svg-icons'; } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { FC } from 'react'; import { FC } from 'react';
import { NavLink, NavLinkProps } from 'react-router-dom'; import { NavLink, NavLinkProps, useLocation } from 'react-router-dom';
import classNames from 'classnames'; import classNames from 'classnames';
import { Location } from 'history';
import { DeleteServerButtonProps } from '../servers/DeleteServerButton'; import { DeleteServerButtonProps } from '../servers/DeleteServerButton';
import { isServerWithId, SelectedServer } from '../servers/data'; import { isServerWithId, SelectedServer } from '../servers/data';
import { supportsDomainRedirects } from '../utils/helpers/features'; import { supportsDomainRedirects } from '../utils/helpers/features';
@ -28,8 +27,7 @@ interface AsideMenuItemProps extends NavLinkProps {
const AsideMenuItem: FC<AsideMenuItemProps> = ({ children, to, className, ...rest }) => ( const AsideMenuItem: FC<AsideMenuItemProps> = ({ children, to, className, ...rest }) => (
<NavLink <NavLink
className={classNames('aside-menu__item', className)} className={({ isActive }) => classNames('aside-menu__item', className, { 'aside-menu__item--selected': isActive })}
activeClassName="aside-menu__item--selected"
to={to} to={to}
{...rest} {...rest}
> >
@ -42,11 +40,11 @@ const AsideMenu = (DeleteServerButton: FC<DeleteServerButtonProps>) => (
) => { ) => {
const hasId = isServerWithId(selectedServer); const hasId = isServerWithId(selectedServer);
const serverId = hasId ? selectedServer.id : ''; const serverId = hasId ? selectedServer.id : '';
const { pathname } = useLocation();
const addManageDomainsLink = supportsDomainRedirects(selectedServer); const addManageDomainsLink = supportsDomainRedirects(selectedServer);
const asideClass = classNames('aside-menu', { const asideClass = classNames('aside-menu', {
'aside-menu--hidden': !showOnMobile, 'aside-menu--hidden': !showOnMobile,
}); });
const shortUrlsIsActive = (_: null, location: Location) => location.pathname.match('/list-short-urls') !== null;
const buildPath = (suffix: string) => `/server/${serverId}${suffix}`; const buildPath = (suffix: string) => `/server/${serverId}${suffix}`;
return ( return (
@ -56,7 +54,10 @@ const AsideMenu = (DeleteServerButton: FC<DeleteServerButtonProps>) => (
<FontAwesomeIcon fixedWidth icon={overviewIcon} /> <FontAwesomeIcon fixedWidth icon={overviewIcon} />
<span className="aside-menu__item-text">Overview</span> <span className="aside-menu__item-text">Overview</span>
</AsideMenuItem> </AsideMenuItem>
<AsideMenuItem to={buildPath('/list-short-urls/1')} isActive={shortUrlsIsActive}> <AsideMenuItem
to={buildPath('/list-short-urls/1')}
className={classNames({ 'aside-menu__item--selected': pathname.match('/list-short-urls') !== null })}
>
<FontAwesomeIcon fixedWidth icon={listIcon} /> <FontAwesomeIcon fixedWidth icon={listIcon} />
<span className="aside-menu__item-text">List short URLs</span> <span className="aside-menu__item-text">List short URLs</span>
</AsideMenuItem> </AsideMenuItem>

View file

@ -1,6 +1,6 @@
import { useEffect } from 'react'; import { useEffect } from 'react';
import { isEmpty, values } from 'ramda'; import { isEmpty, values } from 'ramda';
import { Link, RouteChildrenProps } from 'react-router-dom'; import { Link, useNavigate } from 'react-router-dom';
import { Card, Row } from 'reactstrap'; import { Card, Row } from 'reactstrap';
import { ExternalLink } from 'react-external-link'; import { ExternalLink } from 'react-external-link';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
@ -10,11 +10,12 @@ import { ServersMap } from '../servers/data';
import { ShlinkLogo } from './img/ShlinkLogo'; import { ShlinkLogo } from './img/ShlinkLogo';
import './Home.scss'; import './Home.scss';
export interface HomeProps extends RouteChildrenProps { export interface HomeProps {
servers: ServersMap; servers: ServersMap;
} }
const Home = ({ servers, history }: HomeProps) => { const Home = ({ servers }: HomeProps) => {
const navigate = useNavigate();
const serversList = values(servers); const serversList = values(servers);
const hasServers = !isEmpty(serversList); const hasServers = !isEmpty(serversList);
@ -22,7 +23,7 @@ const Home = ({ servers, history }: HomeProps) => {
// Try to redirect to the first server marked as auto-connect // Try to redirect to the first server marked as auto-connect
const autoConnectServer = serversList.find(({ autoConnect }) => autoConnect); const autoConnectServer = serversList.find(({ autoConnect }) => autoConnect);
autoConnectServer && history.push(`/server/${autoConnectServer.id}`); autoConnectServer && navigate(`/server/${autoConnectServer.id}`);
}, []); }, []);
return ( return (

View file

@ -1,16 +1,16 @@
import { faChevronDown as arrowIcon, faCogs as cogsIcon } from '@fortawesome/free-solid-svg-icons'; import { faChevronDown as arrowIcon, faCogs as cogsIcon } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { FC, useEffect } from 'react'; import { FC, useEffect } from 'react';
import { Link } from 'react-router-dom'; import { Link, useLocation } from 'react-router-dom';
import { Collapse, Nav, Navbar, NavbarBrand, NavbarToggler, NavItem, NavLink } from 'reactstrap'; import { Collapse, Nav, Navbar, NavbarBrand, NavbarToggler, NavItem, NavLink } from 'reactstrap';
import classNames from 'classnames'; import classNames from 'classnames';
import { RouteComponentProps } from 'react-router';
import { useToggle } from '../utils/helpers/hooks'; import { useToggle } from '../utils/helpers/hooks';
import { ShlinkLogo } from './img/ShlinkLogo'; import { ShlinkLogo } from './img/ShlinkLogo';
import './MainHeader.scss'; import './MainHeader.scss';
const MainHeader = (ServersDropdown: FC) => ({ location }: RouteComponentProps) => { const MainHeader = (ServersDropdown: FC) => () => {
const [ isOpen, toggleOpen, , close ] = useToggle(); const [ isOpen, toggleOpen, , close ] = useToggle();
const location = useLocation();
const { pathname } = location; const { pathname } = location;
useEffect(close, [ location ]); useEffect(close, [ location ]);

View file

@ -1,5 +1,5 @@
import { FC, useEffect } from 'react'; import { FC, useEffect } from 'react';
import { Redirect, Route, Switch } from 'react-router-dom'; import { Navigate, Route, Routes, useLocation } from 'react-router-dom';
import { faBars as burgerIcon } from '@fortawesome/free-solid-svg-icons'; import { faBars as burgerIcon } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import classNames from 'classnames'; import classNames from 'classnames';
@ -24,7 +24,8 @@ const MenuLayout = (
Overview: FC, Overview: FC,
EditShortUrl: FC, EditShortUrl: FC,
ManageDomains: FC, ManageDomains: FC,
) => withSelectedServer(({ location, selectedServer }) => { ) => withSelectedServer(({ selectedServer }) => {
const location = useLocation();
const [ sidebarVisible, toggleSidebar, showSidebar, hideSidebar ] = useToggle(); const [ sidebarVisible, toggleSidebar, showSidebar, hideSidebar ] = useToggle();
useEffect(() => hideSidebar(), [ location ]); useEffect(() => hideSidebar(), [ location ]);
@ -48,22 +49,23 @@ const MenuLayout = (
<AsideMenu selectedServer={selectedServer} showOnMobile={sidebarVisible} /> <AsideMenu selectedServer={selectedServer} showOnMobile={sidebarVisible} />
<div className="menu-layout__container" onClick={() => hideSidebar()}> <div className="menu-layout__container" onClick={() => hideSidebar()}>
<div className="container-xl"> <div className="container-xl">
<Switch> <Routes>
<Redirect exact from="/server/:serverId" to="/server/:serverId/overview" /> <Route index element={<Navigate replace to="overview" />} />
<Route exact path="/server/:serverId/overview" component={Overview} /> <Route path="/overview" element={<Overview />} />
<Route exact path="/server/:serverId/list-short-urls/:page" component={ShortUrlsList} /> <Route path="/list-short-urls/:page" element={<ShortUrlsList />} />
<Route exact path="/server/:serverId/create-short-url" component={CreateShortUrl} /> <Route path="/create-short-url" element={<CreateShortUrl />} />
<Route path="/server/:serverId/short-code/:shortCode/visits" component={ShortUrlVisits} /> <Route path="/short-code/:shortCode/visits/*" element={<ShortUrlVisits />} />
<Route path="/server/:serverId/short-code/:shortCode/edit" component={EditShortUrl} /> <Route path="/short-code/:shortCode/edit" element={<EditShortUrl />} />
<Route path="/server/:serverId/tag/:tag/visits" component={TagVisits} /> <Route path="/tag/:tag/visits/*" element={<TagVisits />} />
{addOrphanVisitsRoute && <Route path="/server/:serverId/orphan-visits" component={OrphanVisits} />} {addOrphanVisitsRoute && <Route path="/orphan-visits/*" element={<OrphanVisits />} />}
{addNonOrphanVisitsRoute && <Route path="/server/:serverId/non-orphan-visits" component={NonOrphanVisits} />} {addNonOrphanVisitsRoute && <Route path="/non-orphan-visits/*" element={<NonOrphanVisits />} />}
<Route exact path="/server/:serverId/manage-tags" component={TagsList} /> <Route path="/manage-tags" element={<TagsList />} />
{addManageDomainsRoute && <Route exact path="/server/:serverId/manage-domains" component={ManageDomains} />} {addManageDomainsRoute && <Route path="/manage-domains" element={<ManageDomains />} />}
<Route <Route
render={() => <NotFound to={`/server/${selectedServer.id}/list-short-urls/1`}>List short URLs</NotFound>} path="*"
element={<NotFound to={`/server/${selectedServer.id}/list-short-urls/1`}>List short URLs</NotFound>}
/> />
</Switch> </Routes>
</div> </div>
</div> </div>
</div> </div>

View file

@ -1,7 +1,9 @@
import { PropsWithChildren, useEffect } from 'react'; import { FC, useEffect } from 'react';
import { RouteComponentProps } from 'react-router'; import { useLocation } from 'react-router-dom';
const ScrollToTop = (): FC => ({ children }) => {
const location = useLocation();
const ScrollToTop = () => ({ location, children }: PropsWithChildren<RouteComponentProps>) => {
useEffect(() => { useEffect(() => {
scrollTo(0, 0); scrollTo(0, 0);
}, [ location ]); }, [ location ]);

View file

@ -1,5 +1,5 @@
import axios from 'axios'; import axios from 'axios';
import Bottle, { Decorator } from 'bottlejs'; import Bottle from 'bottlejs';
import ScrollToTop from '../ScrollToTop'; import ScrollToTop from '../ScrollToTop';
import MainHeader from '../MainHeader'; import MainHeader from '../MainHeader';
import Home from '../Home'; import Home from '../Home';
@ -11,7 +11,7 @@ import { ConnectDecorator } from '../../container/types';
import { withoutSelectedServer } from '../../servers/helpers/withoutSelectedServer'; import { withoutSelectedServer } from '../../servers/helpers/withoutSelectedServer';
import { ImageDownloader } from './ImageDownloader'; import { ImageDownloader } from './ImageDownloader';
const provideServices = (bottle: Bottle, connect: ConnectDecorator, withRouter: Decorator) => { const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
// Services // Services
bottle.constant('window', (global as any).window); bottle.constant('window', (global as any).window);
bottle.constant('console', global.console); bottle.constant('console', global.console);
@ -21,14 +21,11 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator, withRouter:
// Components // Components
bottle.serviceFactory('ScrollToTop', ScrollToTop); bottle.serviceFactory('ScrollToTop', ScrollToTop);
bottle.decorator('ScrollToTop', withRouter);
bottle.serviceFactory('MainHeader', MainHeader, 'ServersDropdown'); bottle.serviceFactory('MainHeader', MainHeader, 'ServersDropdown');
bottle.decorator('MainHeader', withRouter);
bottle.serviceFactory('Home', () => Home); bottle.serviceFactory('Home', () => Home);
bottle.decorator('Home', withoutSelectedServer); bottle.decorator('Home', withoutSelectedServer);
bottle.decorator('Home', withRouter);
bottle.decorator('Home', connect([ 'servers' ], [ 'resetSelectedServer' ])); bottle.decorator('Home', connect([ 'servers' ], [ 'resetSelectedServer' ]));
bottle.serviceFactory( bottle.serviceFactory(
@ -48,7 +45,6 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator, withRouter:
'ManageDomains', 'ManageDomains',
); );
bottle.decorator('MenuLayout', connect([ 'selectedServer' ], [ 'selectServer' ])); bottle.decorator('MenuLayout', connect([ 'selectedServer' ], [ 'selectServer' ]));
bottle.decorator('MenuLayout', withRouter);
bottle.serviceFactory('AsideMenu', AsideMenu, 'DeleteServerButton'); bottle.serviceFactory('AsideMenu', AsideMenu, 'DeleteServerButton');

View file

@ -1,5 +1,4 @@
import Bottle, { IContainer } from 'bottlejs'; import Bottle, { IContainer } from 'bottlejs';
import { withRouter } from 'react-router-dom';
import { connect as reduxConnect } from 'react-redux'; import { connect as reduxConnect } from 'react-redux';
import { pick } from 'ramda'; import { pick } from 'ramda';
import provideApiServices from '../api/services/provideServices'; import provideApiServices from '../api/services/provideServices';
@ -34,11 +33,11 @@ const connect: ConnectDecorator = (propsFromState: string[] | null, actionServic
actionServiceNames.reduce(mapActionService, {}), actionServiceNames.reduce(mapActionService, {}),
); );
provideAppServices(bottle, connect, withRouter); provideAppServices(bottle, connect);
provideCommonServices(bottle, connect, withRouter); provideCommonServices(bottle, connect);
provideApiServices(bottle); provideApiServices(bottle);
provideShortUrlsServices(bottle, connect, withRouter); provideShortUrlsServices(bottle, connect);
provideServersServices(bottle, connect, withRouter); provideServersServices(bottle, connect);
provideTagsServices(bottle, connect); provideTagsServices(bottle, connect);
provideVisitsServices(bottle, connect); provideVisitsServices(bottle, connect);
provideUtilsServices(bottle); provideUtilsServices(bottle);

View file

@ -1,5 +1,6 @@
import { FC, useEffect } from 'react'; import { FC, useEffect } from 'react';
import { pipe } from 'ramda'; import { pipe } from 'ramda';
import { useParams } from 'react-router-dom';
import { CreateVisit } from '../../visits/types'; import { CreateVisit } from '../../visits/types';
import { MercureInfo } from '../reducers/mercureInfo'; import { MercureInfo } from '../reducers/mercureInfo';
import { bindToMercureTopic } from './index'; import { bindToMercureTopic } from './index';
@ -12,17 +13,19 @@ export interface MercureBoundProps {
export function boundToMercureHub<T = {}>( export function boundToMercureHub<T = {}>(
WrappedComponent: FC<MercureBoundProps & T>, WrappedComponent: FC<MercureBoundProps & T>,
getTopicsForProps: (props: T) => string[], getTopicsForProps: (props: T, routeParams: any) => string[],
) { ) {
const pendingUpdates = new Set<CreateVisit>(); const pendingUpdates = new Set<CreateVisit>();
return (props: MercureBoundProps & T) => { return (props: MercureBoundProps & T) => {
const { createNewVisits, loadMercureInfo, mercureInfo } = props; const { createNewVisits, loadMercureInfo, mercureInfo } = props;
const { interval } = mercureInfo; const { interval } = mercureInfo;
const params = useParams();
useEffect(() => { useEffect(() => {
const onMessage = (visit: CreateVisit) => interval ? pendingUpdates.add(visit) : createNewVisits([ visit ]); const onMessage = (visit: CreateVisit) => interval ? pendingUpdates.add(visit) : createNewVisits([ visit ]);
const closeEventSource = bindToMercureTopic(mercureInfo, getTopicsForProps(props), onMessage, loadMercureInfo); const topics = getTopicsForProps(props, params);
const closeEventSource = bindToMercureTopic(mercureInfo, topics, onMessage, loadMercureInfo);
if (!interval) { if (!interval) {
return closeEventSource; return closeEventSource;

View file

@ -1,10 +1,10 @@
import { FC, useEffect, useState } from 'react'; import { FC, useEffect, useState } from 'react';
import { v4 as uuid } from 'uuid'; import { v4 as uuid } from 'uuid';
import { RouterProps } from 'react-router';
import { Button } from 'reactstrap'; import { Button } from 'reactstrap';
import { useNavigate } from 'react-router-dom';
import { Result } from '../utils/Result'; import { Result } from '../utils/Result';
import { NoMenuLayout } from '../common/NoMenuLayout'; import { NoMenuLayout } from '../common/NoMenuLayout';
import { StateFlagTimeout, useToggle } from '../utils/helpers/hooks'; import { StateFlagTimeout, useGoBack, useToggle } from '../utils/helpers/hooks';
import { ServerForm } from './helpers/ServerForm'; import { ServerForm } from './helpers/ServerForm';
import { ImportServersBtnProps } from './helpers/ImportServersBtn'; import { ImportServersBtnProps } from './helpers/ImportServersBtn';
import { ServerData, ServersMap, ServerWithId } from './data'; import { ServerData, ServersMap, ServerWithId } from './data';
@ -12,7 +12,7 @@ import { DuplicatedServersModal } from './helpers/DuplicatedServersModal';
const SHOW_IMPORT_MSG_TIME = 4000; const SHOW_IMPORT_MSG_TIME = 4000;
interface CreateServerProps extends RouterProps { interface CreateServerProps {
createServer: (server: ServerWithId) => void; createServer: (server: ServerWithId) => void;
servers: ServersMap; servers: ServersMap;
} }
@ -27,8 +27,10 @@ const ImportResult = ({ type }: { type: 'error' | 'success' }) => (
); );
const CreateServer = (ImportServersBtn: FC<ImportServersBtnProps>, useStateFlagTimeout: StateFlagTimeout) => ( const CreateServer = (ImportServersBtn: FC<ImportServersBtnProps>, useStateFlagTimeout: StateFlagTimeout) => (
{ servers, createServer, history: { push, goBack } }: CreateServerProps, { servers, createServer }: CreateServerProps,
) => { ) => {
const navigate = useNavigate();
const goBack = useGoBack();
const hasServers = !!Object.keys(servers).length; const hasServers = !!Object.keys(servers).length;
const [ serversImported, setServersImported ] = useStateFlagTimeout(false, SHOW_IMPORT_MSG_TIME); const [ serversImported, setServersImported ] = useStateFlagTimeout(false, SHOW_IMPORT_MSG_TIME);
const [ errorImporting, setErrorImporting ] = useStateFlagTimeout(false, SHOW_IMPORT_MSG_TIME); const [ errorImporting, setErrorImporting ] = useStateFlagTimeout(false, SHOW_IMPORT_MSG_TIME);
@ -42,7 +44,7 @@ const CreateServer = (ImportServersBtn: FC<ImportServersBtnProps>, useStateFlagT
const id = uuid(); const id = uuid();
createServer({ ...serverData, id }); createServer({ ...serverData, id });
push(`/server/${id}`); navigate(`/server/${id}`);
}; };
useEffect(() => { useEffect(() => {

View file

@ -1,6 +1,6 @@
import { FC } from 'react'; import { FC } from 'react';
import { Modal, ModalBody, ModalFooter, ModalHeader } from 'reactstrap'; import { Modal, ModalBody, ModalFooter, ModalHeader } from 'reactstrap';
import { RouterProps } from 'react-router'; import { useNavigate } from 'react-router-dom';
import { ServerWithId } from './data'; import { ServerWithId } from './data';
export interface DeleteServerModalProps { export interface DeleteServerModalProps {
@ -10,17 +10,18 @@ export interface DeleteServerModalProps {
redirectHome?: boolean; redirectHome?: boolean;
} }
interface DeleteServerModalConnectProps extends DeleteServerModalProps, RouterProps { interface DeleteServerModalConnectProps extends DeleteServerModalProps {
deleteServer: (server: ServerWithId) => void; deleteServer: (server: ServerWithId) => void;
} }
const DeleteServerModal: FC<DeleteServerModalConnectProps> = ( const DeleteServerModal: FC<DeleteServerModalConnectProps> = (
{ server, toggle, isOpen, deleteServer, history, redirectHome = true }, { server, toggle, isOpen, deleteServer, redirectHome = true },
) => { ) => {
const navigate = useNavigate();
const closeModal = () => { const closeModal = () => {
deleteServer(server); deleteServer(server);
toggle(); toggle();
redirectHome && history.push('/'); redirectHome && navigate('/');
}; };
return ( return (

View file

@ -1,6 +1,7 @@
import { FC } from 'react'; import { FC } from 'react';
import { Button } from 'reactstrap'; import { Button } from 'reactstrap';
import { NoMenuLayout } from '../common/NoMenuLayout'; import { NoMenuLayout } from '../common/NoMenuLayout';
import { useGoBack } from '../utils/helpers/hooks';
import { ServerForm } from './helpers/ServerForm'; import { ServerForm } from './helpers/ServerForm';
import { withSelectedServer } from './helpers/withSelectedServer'; import { withSelectedServer } from './helpers/withSelectedServer';
import { isServerWithId, ServerData } from './data'; import { isServerWithId, ServerData } from './data';
@ -9,9 +10,9 @@ interface EditServerProps {
editServer: (serverId: string, serverData: ServerData) => void; editServer: (serverId: string, serverData: ServerData) => void;
} }
export const EditServer = (ServerError: FC) => withSelectedServer<EditServerProps>(( export const EditServer = (ServerError: FC) => withSelectedServer<EditServerProps>(({ editServer, selectedServer }) => {
{ editServer, selectedServer, history: { goBack } }, const goBack = useGoBack();
) => {
if (!isServerWithId(selectedServer)) { if (!isServerWithId(selectedServer)) {
return null; return null;
} }

View file

@ -1,6 +1,6 @@
import { FC, useEffect } from 'react'; import { FC, useEffect } from 'react';
import { Card, CardBody, CardHeader, Row } from 'reactstrap'; import { Card, CardBody, CardHeader, Row } from 'reactstrap';
import { Link, useHistory } from 'react-router-dom'; import { Link, useNavigate } from 'react-router-dom';
import { ITEMS_IN_OVERVIEW_PAGE, ShortUrlsList as ShortUrlsListState } from '../short-urls/reducers/shortUrlsList'; import { ITEMS_IN_OVERVIEW_PAGE, ShortUrlsList as ShortUrlsListState } from '../short-urls/reducers/shortUrlsList';
import { prettify } from '../utils/helpers/numbers'; import { prettify } from '../utils/helpers/numbers';
import { TagsList } from '../tags/reducers/tagsList'; import { TagsList } from '../tags/reducers/tagsList';
@ -44,7 +44,7 @@ export const Overview = (
const serverId = getServerId(selectedServer); const serverId = getServerId(selectedServer);
const linkToOrphanVisits = supportsOrphanVisits(selectedServer); const linkToOrphanVisits = supportsOrphanVisits(selectedServer);
const linkToNonOrphanVisits = supportsNonOrphanVisits(selectedServer); const linkToNonOrphanVisits = supportsNonOrphanVisits(selectedServer);
const history = useHistory(); const navigate = useNavigate();
useEffect(() => { useEffect(() => {
listShortUrls({ itemsPerPage: ITEMS_IN_OVERVIEW_PAGE, orderBy: { field: 'dateCreated', dir: 'DESC' } }); listShortUrls({ itemsPerPage: ITEMS_IN_OVERVIEW_PAGE, orderBy: { field: 'dateCreated', dir: 'DESC' } });
@ -103,7 +103,7 @@ export const Overview = (
shortUrlsList={shortUrlsList} shortUrlsList={shortUrlsList}
selectedServer={selectedServer} selectedServer={selectedServer}
className="mb-0" className="mb-0"
onTagClick={(tag) => history.push(`/server/${serverId}/list-short-urls/1?tags=${encodeURIComponent(tag)}`)} onTagClick={(tag) => navigate(`/server/${serverId}/list-short-urls/1?tags=${encodeURIComponent(tag)}`)}
/> />
</CardBody> </CardBody>
</Card> </Card>

View file

@ -1,21 +1,22 @@
import { FC, useEffect } from 'react'; import { FC, useEffect } from 'react';
import { RouteComponentProps } from 'react-router'; import { useParams } from 'react-router-dom';
import Message from '../../utils/Message'; import Message from '../../utils/Message';
import { isNotFoundServer, SelectedServer } from '../data'; import { isNotFoundServer, SelectedServer } from '../data';
import { NoMenuLayout } from '../../common/NoMenuLayout'; import { NoMenuLayout } from '../../common/NoMenuLayout';
interface WithSelectedServerProps extends RouteComponentProps<{ serverId: string }> { interface WithSelectedServerProps {
selectServer: (serverId: string) => void; selectServer: (serverId: string) => void;
selectedServer: SelectedServer; selectedServer: SelectedServer;
} }
export function withSelectedServer<T = {}>(WrappedComponent: FC<WithSelectedServerProps & T>, ServerError: FC) { export function withSelectedServer<T = {}>(WrappedComponent: FC<WithSelectedServerProps & T>, ServerError: FC) {
return (props: WithSelectedServerProps & T) => { return (props: WithSelectedServerProps & T) => {
const { selectServer, selectedServer, match } = props; const params = useParams<{ serverId: string }>();
const { selectServer, selectedServer } = props;
useEffect(() => { useEffect(() => {
selectServer(match.params.serverId); params.serverId && selectServer(params.serverId);
}, [ match.params.serverId ]); }, [ params.serverId ]);
if (!selectedServer) { if (!selectedServer) {
return ( return (

View file

@ -1,5 +1,5 @@
import csvjson from 'csvjson'; import csvjson from 'csvjson';
import Bottle, { Decorator } from 'bottlejs'; import Bottle from 'bottlejs';
import CreateServer from '../CreateServer'; import CreateServer from '../CreateServer';
import ServersDropdown from '../ServersDropdown'; import ServersDropdown from '../ServersDropdown';
import DeleteServerModal from '../DeleteServerModal'; import DeleteServerModal from '../DeleteServerModal';
@ -20,7 +20,7 @@ import { ManageServersRowDropdown } from '../ManageServersRowDropdown';
import { ServersImporter } from './ServersImporter'; import { ServersImporter } from './ServersImporter';
import ServersExporter from './ServersExporter'; import ServersExporter from './ServersExporter';
const provideServices = (bottle: Bottle, connect: ConnectDecorator, withRouter: Decorator) => { const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
// Components // Components
bottle.serviceFactory( bottle.serviceFactory(
'ManageServers', 'ManageServers',
@ -48,7 +48,6 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator, withRouter:
bottle.decorator('ServersDropdown', connect([ 'servers', 'selectedServer' ])); bottle.decorator('ServersDropdown', connect([ 'servers', 'selectedServer' ]));
bottle.serviceFactory('DeleteServerModal', () => DeleteServerModal); bottle.serviceFactory('DeleteServerModal', () => DeleteServerModal);
bottle.decorator('DeleteServerModal', withRouter);
bottle.decorator('DeleteServerModal', connect(null, [ 'deleteServer' ])); bottle.decorator('DeleteServerModal', connect(null, [ 'deleteServer' ]));
bottle.serviceFactory('DeleteServerButton', DeleteServerButton, 'DeleteServerModal'); bottle.serviceFactory('DeleteServerButton', DeleteServerButton, 'DeleteServerModal');

View file

@ -1,9 +1,9 @@
import { FC, useEffect, useMemo } from 'react'; import { FC, useEffect, useMemo } from 'react';
import { RouteComponentProps } from 'react-router';
import { Button, Card } from 'reactstrap'; import { Button, Card } from 'reactstrap';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faArrowLeft } from '@fortawesome/free-solid-svg-icons'; import { faArrowLeft } from '@fortawesome/free-solid-svg-icons';
import { ExternalLink } from 'react-external-link'; import { ExternalLink } from 'react-external-link';
import { useLocation, useParams } from 'react-router-dom';
import { SelectedServer } from '../servers/data'; import { SelectedServer } from '../servers/data';
import { Settings, ShortUrlCreationSettings } from '../settings/reducers/settings'; import { Settings, ShortUrlCreationSettings } from '../settings/reducers/settings';
import { OptionalString } from '../utils/utils'; import { OptionalString } from '../utils/utils';
@ -11,13 +11,13 @@ import { parseQuery } from '../utils/helpers/query';
import Message from '../utils/Message'; import Message from '../utils/Message';
import { Result } from '../utils/Result'; import { Result } from '../utils/Result';
import { ShlinkApiError } from '../api/ShlinkApiError'; import { ShlinkApiError } from '../api/ShlinkApiError';
import { useToggle } from '../utils/helpers/hooks'; import { useGoBack, useToggle } from '../utils/helpers/hooks';
import { ShortUrlFormProps } from './ShortUrlForm'; import { ShortUrlFormProps } from './ShortUrlForm';
import { ShortUrlDetail } from './reducers/shortUrlDetail'; import { ShortUrlDetail } from './reducers/shortUrlDetail';
import { EditShortUrlData, ShortUrl, ShortUrlData } from './data'; import { EditShortUrlData, ShortUrl, ShortUrlData } from './data';
import { ShortUrlEdition } from './reducers/shortUrlEdition'; import { ShortUrlEdition } from './reducers/shortUrlEdition';
interface EditShortUrlConnectProps extends RouteComponentProps<{ shortCode: string }> { interface EditShortUrlConnectProps {
settings: Settings; settings: Settings;
selectedServer: SelectedServer; selectedServer: SelectedServer;
shortUrlDetail: ShortUrlDetail; shortUrlDetail: ShortUrlDetail;
@ -48,9 +48,6 @@ const getInitialState = (shortUrl?: ShortUrl, settings?: ShortUrlCreationSetting
}; };
export const EditShortUrl = (ShortUrlForm: FC<ShortUrlFormProps>) => ({ export const EditShortUrl = (ShortUrlForm: FC<ShortUrlFormProps>) => ({
history: { goBack },
match: { params },
location: { search },
settings: { shortUrlCreation: shortUrlCreationSettings }, settings: { shortUrlCreation: shortUrlCreationSettings },
selectedServer, selectedServer,
shortUrlDetail, shortUrlDetail,
@ -58,6 +55,9 @@ export const EditShortUrl = (ShortUrlForm: FC<ShortUrlFormProps>) => ({
shortUrlEdition, shortUrlEdition,
editShortUrl, editShortUrl,
}: EditShortUrlConnectProps) => { }: EditShortUrlConnectProps) => {
const { search } = useLocation();
const params = useParams<{ shortCode: string }>();
const goBack = useGoBack();
const { loading, error, errorData, shortUrl } = shortUrlDetail; const { loading, error, errorData, shortUrl } = shortUrlDetail;
const { saving, error: savingError, errorData: savingErrorData } = shortUrlEdition; const { saving, error: savingError, errorData: savingErrorData } = shortUrlEdition;
const { domain } = parseQuery<{ domain?: string }>(search); const { domain } = parseQuery<{ domain?: string }>(search);
@ -68,7 +68,7 @@ export const EditShortUrl = (ShortUrlForm: FC<ShortUrlFormProps>) => ({
const [ savingSucceeded,, isSuccessful, isNotSuccessful ] = useToggle(); const [ savingSucceeded,, isSuccessful, isNotSuccessful ] = useToggle();
useEffect(() => { useEffect(() => {
getShortUrlDetail(params.shortCode, domain); params.shortCode && getShortUrlDetail(params.shortCode, domain);
}, []); }, []);
if (loading) { if (loading) {

View file

@ -2,7 +2,6 @@ import { faTags as tagsIcon } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { isEmpty, pipe } from 'ramda'; import { isEmpty, pipe } from 'ramda';
import { parseISO } from 'date-fns'; import { parseISO } from 'date-fns';
import { RouteChildrenProps } from 'react-router-dom';
import SearchField from '../utils/SearchField'; import SearchField from '../utils/SearchField';
import Tag from '../tags/helpers/Tag'; import Tag from '../tags/helpers/Tag';
import { DateRangeSelector } from '../utils/dates/DateRangeSelector'; import { DateRangeSelector } from '../utils/dates/DateRangeSelector';
@ -12,19 +11,17 @@ import { DateRange } from '../utils/dates/types';
import { supportsAllTagsFiltering } from '../utils/helpers/features'; import { supportsAllTagsFiltering } from '../utils/helpers/features';
import { SelectedServer } from '../servers/data'; import { SelectedServer } from '../servers/data';
import { TooltipToggleSwitch } from '../utils/TooltipToggleSwitch'; import { TooltipToggleSwitch } from '../utils/TooltipToggleSwitch';
import { ShortUrlListRouteParams, useShortUrlsQuery } from './helpers/hooks'; import { useShortUrlsQuery } from './helpers/hooks';
import './ShortUrlsFilteringBar.scss'; import './ShortUrlsFilteringBar.scss';
export type ShortUrlsFilteringProps = RouteChildrenProps<ShortUrlListRouteParams> & { interface ShortUrlsFilteringProps {
selectedServer: SelectedServer; selectedServer: SelectedServer;
}; }
const dateOrNull = (date?: string) => date ? parseISO(date) : null; const dateOrNull = (date?: string) => date ? parseISO(date) : null;
const ShortUrlsFilteringBar = (colorGenerator: ColorGenerator) => ( const ShortUrlsFilteringBar = (colorGenerator: ColorGenerator) => ({ selectedServer }: ShortUrlsFilteringProps) => {
{ selectedServer, ...rest }: ShortUrlsFilteringProps, const [{ search, tags, startDate, endDate, tagsMode = 'any' }, toFirstPage ] = useShortUrlsQuery();
) => {
const [{ search, tags, startDate, endDate, tagsMode = 'any' }, toFirstPage ] = useShortUrlsQuery(rest);
const selectedTags = tags?.split(',') ?? []; const selectedTags = tags?.split(',') ?? [];
const setDates = pipe( const setDates = pipe(
({ startDate, endDate }: DateRange) => ({ ({ startDate, endDate }: DateRange) => ({

View file

@ -1,7 +1,7 @@
import { pipe } from 'ramda'; import { pipe } from 'ramda';
import { FC, useEffect, useMemo, useState } from 'react'; import { FC, useEffect, useMemo, useState } from 'react';
import { RouteComponentProps } from 'react-router';
import { Card } from 'reactstrap'; import { Card } from 'reactstrap';
import { useLocation, useParams } from 'react-router-dom';
import { OrderingDropdown } from '../utils/OrderingDropdown'; import { OrderingDropdown } from '../utils/OrderingDropdown';
import { determineOrderDir, OrderDir } from '../utils/helpers/ordering'; import { determineOrderDir, OrderDir } from '../utils/helpers/ordering';
import { getServerId, SelectedServer } from '../servers/data'; import { getServerId, SelectedServer } from '../servers/data';
@ -13,10 +13,10 @@ import { DEFAULT_SHORT_URLS_ORDERING, Settings } from '../settings/reducers/sett
import { ShortUrlsList as ShortUrlsListState } from './reducers/shortUrlsList'; import { ShortUrlsList as ShortUrlsListState } from './reducers/shortUrlsList';
import { ShortUrlsTableProps } from './ShortUrlsTable'; import { ShortUrlsTableProps } from './ShortUrlsTable';
import Paginator from './Paginator'; import Paginator from './Paginator';
import { ShortUrlListRouteParams, useShortUrlsQuery } from './helpers/hooks'; import { useShortUrlsQuery } from './helpers/hooks';
import { ShortUrlsOrderableFields, SHORT_URLS_ORDERABLE_FIELDS } from './data'; import { ShortUrlsOrderableFields, SHORT_URLS_ORDERABLE_FIELDS } from './data';
interface ShortUrlsListProps extends RouteComponentProps<ShortUrlListRouteParams> { interface ShortUrlsListProps {
selectedServer: SelectedServer; selectedServer: SelectedServer;
shortUrlsList: ShortUrlsListState; shortUrlsList: ShortUrlsListState;
listShortUrls: (params: ShlinkShortUrlsListParams) => void; listShortUrls: (params: ShlinkShortUrlsListParams) => void;
@ -25,18 +25,14 @@ interface ShortUrlsListProps extends RouteComponentProps<ShortUrlListRouteParams
const ShortUrlsList = (ShortUrlsTable: FC<ShortUrlsTableProps>, ShortUrlsFilteringBar: FC) => boundToMercureHub(({ const ShortUrlsList = (ShortUrlsTable: FC<ShortUrlsTableProps>, ShortUrlsFilteringBar: FC) => boundToMercureHub(({
listShortUrls, listShortUrls,
match,
location,
history,
shortUrlsList, shortUrlsList,
selectedServer, selectedServer,
settings, settings,
}: ShortUrlsListProps) => { }: ShortUrlsListProps) => {
const serverId = getServerId(selectedServer); const serverId = getServerId(selectedServer);
const [ const { page } = useParams();
{ tags, search, startDate, endDate, orderBy, tagsMode }, const location = useLocation();
toFirstPage, const [{ tags, search, startDate, endDate, orderBy, tagsMode }, toFirstPage ] = useShortUrlsQuery();
] = useShortUrlsQuery({ history, match, location });
const [ actualOrderBy, setActualOrderBy ] = useState( const [ actualOrderBy, setActualOrderBy ] = useState(
// This separated state handling is needed to be able to fall back to settings value, but only once when loaded // This separated state handling is needed to be able to fall back to settings value, but only once when loaded
orderBy ?? settings.shortUrlsList?.defaultOrdering ?? DEFAULT_SHORT_URLS_ORDERING, orderBy ?? settings.shortUrlsList?.defaultOrdering ?? DEFAULT_SHORT_URLS_ORDERING,
@ -58,7 +54,7 @@ const ShortUrlsList = (ShortUrlsTable: FC<ShortUrlsTableProps>, ShortUrlsFilteri
useEffect(() => { useEffect(() => {
listShortUrls({ listShortUrls({
page: match.params.page, page,
searchTerm: search, searchTerm: search,
tags: selectedTags, tags: selectedTags,
startDate, startDate,
@ -66,7 +62,7 @@ const ShortUrlsList = (ShortUrlsTable: FC<ShortUrlsTableProps>, ShortUrlsFilteri
orderBy: actualOrderBy, orderBy: actualOrderBy,
tagsMode, tagsMode,
}); });
}, [ match.params.page, search, selectedTags, startDate, endDate, actualOrderBy, tagsMode ]); }, [ page, search, selectedTags, startDate, endDate, actualOrderBy, tagsMode ]);
return ( return (
<> <>

View file

@ -1,4 +1,4 @@
import { RouteChildrenProps } from 'react-router-dom'; import { useParams, useLocation, useNavigate } from 'react-router-dom';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { isEmpty, pipe } from 'ramda'; import { isEmpty, pipe } from 'ramda';
import { parseQuery, stringifyQuery } from '../../utils/helpers/query'; import { parseQuery, stringifyQuery } from '../../utils/helpers/query';
@ -6,7 +6,6 @@ import { ShortUrlsOrder, ShortUrlsOrderableFields } from '../data';
import { orderToString, stringToOrder } from '../../utils/helpers/ordering'; import { orderToString, stringToOrder } from '../../utils/helpers/ordering';
import { TagsFilteringMode } from '../../api/types'; import { TagsFilteringMode } from '../../api/types';
type ServerIdRouteProps = RouteChildrenProps<{ serverId: string }>;
type ToFirstPage = (extra: Partial<ShortUrlsFiltering>) => void; type ToFirstPage = (extra: Partial<ShortUrlsFiltering>) => void;
export interface ShortUrlListRouteParams { export interface ShortUrlListRouteParams {
@ -30,9 +29,11 @@ interface ShortUrlsFiltering extends ShortUrlsQueryCommon {
orderBy?: ShortUrlsOrder; orderBy?: ShortUrlsOrder;
} }
export const useShortUrlsQuery = ( export const useShortUrlsQuery = (): [ShortUrlsFiltering, ToFirstPage] => {
{ history, location, match }: ServerIdRouteProps, const navigate = useNavigate();
): [ShortUrlsFiltering, ToFirstPage] => { const location = useLocation();
const params = useParams<{ serverId: string }>();
const query = useMemo( const query = useMemo(
pipe( pipe(
() => parseQuery<ShortUrlsQuery>(location.search), () => parseQuery<ShortUrlsQuery>(location.search),
@ -49,7 +50,7 @@ export const useShortUrlsQuery = (
const evolvedQuery = stringifyQuery(normalizedQuery); const evolvedQuery = stringifyQuery(normalizedQuery);
const queryString = isEmpty(evolvedQuery) ? '' : `?${evolvedQuery}`; const queryString = isEmpty(evolvedQuery) ? '' : `?${evolvedQuery}`;
history.push(`/server/${match?.params.serverId}/list-short-urls/1${queryString}`); navigate(`/server/${params.serverId}/list-short-urls/1${queryString}`);
}; };
return [ query, toFirstPageWithExtra ]; return [ query, toFirstPageWithExtra ];

View file

@ -1,4 +1,4 @@
import Bottle, { Decorator } from 'bottlejs'; import Bottle from 'bottlejs';
import ShortUrlsFilteringBar from '../ShortUrlsFilteringBar'; import ShortUrlsFilteringBar from '../ShortUrlsFilteringBar';
import ShortUrlsList from '../ShortUrlsList'; import ShortUrlsList from '../ShortUrlsList';
import ShortUrlsRow from '../helpers/ShortUrlsRow'; import ShortUrlsRow from '../helpers/ShortUrlsRow';
@ -17,7 +17,7 @@ import { ShortUrlForm } from '../ShortUrlForm';
import { EditShortUrl } from '../EditShortUrl'; import { EditShortUrl } from '../EditShortUrl';
import { getShortUrlDetail } from '../reducers/shortUrlDetail'; import { getShortUrlDetail } from '../reducers/shortUrlDetail';
const provideServices = (bottle: Bottle, connect: ConnectDecorator, withRouter: Decorator) => { const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
// Components // Components
bottle.serviceFactory('ShortUrlsList', ShortUrlsList, 'ShortUrlsTable', 'ShortUrlsFilteringBar'); bottle.serviceFactory('ShortUrlsList', ShortUrlsList, 'ShortUrlsTable', 'ShortUrlsFilteringBar');
bottle.decorator('ShortUrlsList', connect( bottle.decorator('ShortUrlsList', connect(
@ -49,10 +49,8 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator, withRouter:
bottle.serviceFactory('QrCodeModal', QrCodeModal, 'ImageDownloader', 'ForServerVersion'); bottle.serviceFactory('QrCodeModal', QrCodeModal, 'ImageDownloader', 'ForServerVersion');
bottle.decorator('QrCodeModal', connect([ 'selectedServer' ])); bottle.decorator('QrCodeModal', connect([ 'selectedServer' ]));
// Services
bottle.serviceFactory('ShortUrlsFilteringBar', ShortUrlsFilteringBar, 'ColorGenerator'); bottle.serviceFactory('ShortUrlsFilteringBar', ShortUrlsFilteringBar, 'ColorGenerator');
bottle.decorator('ShortUrlsFilteringBar', connect([ 'selectedServer' ])); bottle.decorator('ShortUrlsFilteringBar', connect([ 'selectedServer' ]));
bottle.decorator('ShortUrlsFilteringBar', withRouter);
// Actions // Actions
bottle.serviceFactory('listShortUrls', listShortUrls, 'buildShlinkApiClient'); bottle.serviceFactory('listShortUrls', listShortUrls, 'buildShlinkApiClient');

View file

@ -1,6 +1,6 @@
import { FC, useEffect, useRef } from 'react'; import { FC, useEffect, useRef } from 'react';
import { splitEvery } from 'ramda'; import { splitEvery } from 'ramda';
import { RouteChildrenProps } from 'react-router'; import { useLocation } from 'react-router-dom';
import { SimpleCard } from '../utils/SimpleCard'; import { SimpleCard } from '../utils/SimpleCard';
import SimplePaginator from '../common/SimplePaginator'; import SimplePaginator from '../common/SimplePaginator';
import { useQueryState } from '../utils/helpers/hooks'; import { useQueryState } from '../utils/helpers/hooks';
@ -18,10 +18,11 @@ export interface TagsTableProps extends TagsListChildrenProps {
const TAGS_PER_PAGE = 20; // TODO Allow customizing this value in settings const TAGS_PER_PAGE = 20; // TODO Allow customizing this value in settings
export const TagsTable = (TagsTableRow: FC<TagsTableRowProps>) => ( export const TagsTable = (TagsTableRow: FC<TagsTableRowProps>) => (
{ sortedTags, selectedServer, location, orderByColumn, currentOrder }: TagsTableProps & RouteChildrenProps, { sortedTags, selectedServer, orderByColumn, currentOrder }: TagsTableProps,
) => { ) => {
const isFirstLoad = useRef(true); const isFirstLoad = useRef(true);
const { page: pageFromQuery = 1 } = parseQuery<{ page?: number | string }>(location.search); const { search } = useLocation();
const { page: pageFromQuery = 1 } = parseQuery<{ page?: number | string }>(search);
const [ page, setPage ] = useQueryState<number>('page', Number(pageFromQuery)); const [ page, setPage ] = useQueryState<number>('page', Number(pageFromQuery));
const pages = splitEvery(TAGS_PER_PAGE, sortedTags); const pages = splitEvery(TAGS_PER_PAGE, sortedTags);
const showPaginator = pages.length > 1; const showPaginator = pages.length > 1;

View file

@ -1,5 +1,4 @@
import Bottle, { IContainer } from 'bottlejs'; import Bottle, { IContainer } from 'bottlejs';
import { withRouter } from 'react-router-dom';
import TagsSelector from '../helpers/TagsSelector'; import TagsSelector from '../helpers/TagsSelector';
import TagCard from '../TagCard'; import TagCard from '../TagCard';
import DeleteTagConfirmModal from '../helpers/DeleteTagConfirmModal'; import DeleteTagConfirmModal from '../helpers/DeleteTagConfirmModal';
@ -30,7 +29,6 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
bottle.serviceFactory('TagsTableRow', TagsTableRow, 'DeleteTagConfirmModal', 'EditTagModal', 'ColorGenerator'); bottle.serviceFactory('TagsTableRow', TagsTableRow, 'DeleteTagConfirmModal', 'EditTagModal', 'ColorGenerator');
bottle.serviceFactory('TagsTable', TagsTable, 'TagsTableRow'); bottle.serviceFactory('TagsTable', TagsTable, 'TagsTableRow');
bottle.decorator('TagsTable', withRouter);
bottle.serviceFactory('TagsList', TagsList, 'TagsCards', 'TagsTable'); bottle.serviceFactory('TagsList', TagsList, 'TagsCards', 'TagsTable');
bottle.decorator('TagsList', connect( bottle.decorator('TagsList', connect(

View file

@ -1,5 +1,6 @@
import { useState, useRef, EffectCallback, DependencyList, useEffect } from 'react'; import { useState, useRef, EffectCallback, DependencyList, useEffect } from 'react';
import { useSwipeable as useReactSwipeable } from 'react-swipeable'; import { useSwipeable as useReactSwipeable } from 'react-swipeable';
import { useNavigate } from 'react-router-dom';
import { parseQuery, stringifyQuery } from './query'; import { parseQuery, stringifyQuery } from './query';
const DEFAULT_DELAY = 2000; const DEFAULT_DELAY = 2000;
@ -75,3 +76,9 @@ export const useEffectExceptFirstTime = (callback: EffectCallback, deps: Depende
isFirstLoad.current = false; isFirstLoad.current = false;
}, deps); }, deps);
}; };
export const useGoBack = () => {
const navigate = useNavigate();
return () => navigate(-1);
};

View file

@ -1,7 +1,7 @@
import { RouteComponentProps } from 'react-router';
import { boundToMercureHub } from '../mercure/helpers/boundToMercureHub'; import { boundToMercureHub } from '../mercure/helpers/boundToMercureHub';
import { ShlinkVisitsParams } from '../api/types'; import { ShlinkVisitsParams } from '../api/types';
import { Topics } from '../mercure/helpers/Topics'; import { Topics } from '../mercure/helpers/Topics';
import { useGoBack } from '../utils/helpers/hooks';
import VisitsStats from './VisitsStats'; import VisitsStats from './VisitsStats';
import { NormalizedVisit, VisitsInfo, VisitsParams } from './types'; import { NormalizedVisit, VisitsInfo, VisitsParams } from './types';
import { VisitsExporter } from './services/VisitsExporter'; import { VisitsExporter } from './services/VisitsExporter';
@ -9,21 +9,20 @@ import { CommonVisitsProps } from './types/CommonVisitsProps';
import { toApiParams } from './types/helpers'; import { toApiParams } from './types/helpers';
import { NonOrphanVisitsHeader } from './NonOrphanVisitsHeader'; import { NonOrphanVisitsHeader } from './NonOrphanVisitsHeader';
export interface NonOrphanVisitsProps extends CommonVisitsProps, RouteComponentProps { export interface NonOrphanVisitsProps extends CommonVisitsProps {
getNonOrphanVisits: (params?: ShlinkVisitsParams, doIntervalFallback?: boolean) => void; getNonOrphanVisits: (params?: ShlinkVisitsParams, doIntervalFallback?: boolean) => void;
nonOrphanVisits: VisitsInfo; nonOrphanVisits: VisitsInfo;
cancelGetNonOrphanVisits: () => void; cancelGetNonOrphanVisits: () => void;
} }
export const NonOrphanVisits = ({ exportVisits }: VisitsExporter) => boundToMercureHub(({ export const NonOrphanVisits = ({ exportVisits }: VisitsExporter) => boundToMercureHub(({
history: { goBack },
match: { url },
getNonOrphanVisits, getNonOrphanVisits,
nonOrphanVisits, nonOrphanVisits,
cancelGetNonOrphanVisits, cancelGetNonOrphanVisits,
settings, settings,
selectedServer, selectedServer,
}: NonOrphanVisitsProps) => { }: NonOrphanVisitsProps) => {
const goBack = useGoBack();
const exportCsv = (visits: NormalizedVisit[]) => exportVisits('non_orphan_visits.csv', visits); const exportCsv = (visits: NormalizedVisit[]) => exportVisits('non_orphan_visits.csv', visits);
const loadVisits = (params: VisitsParams, doIntervalFallback?: boolean) => const loadVisits = (params: VisitsParams, doIntervalFallback?: boolean) =>
getNonOrphanVisits(toApiParams(params), doIntervalFallback); getNonOrphanVisits(toApiParams(params), doIntervalFallback);
@ -33,7 +32,6 @@ export const NonOrphanVisits = ({ exportVisits }: VisitsExporter) => boundToMerc
getVisits={loadVisits} getVisits={loadVisits}
cancelGetVisits={cancelGetNonOrphanVisits} cancelGetVisits={cancelGetNonOrphanVisits}
visitsInfo={nonOrphanVisits} visitsInfo={nonOrphanVisits}
baseUrl={url}
settings={settings} settings={settings}
exportCsv={exportCsv} exportCsv={exportCsv}
selectedServer={selectedServer} selectedServer={selectedServer}

View file

@ -1,7 +1,7 @@
import { RouteComponentProps } from 'react-router';
import { boundToMercureHub } from '../mercure/helpers/boundToMercureHub'; import { boundToMercureHub } from '../mercure/helpers/boundToMercureHub';
import { ShlinkVisitsParams } from '../api/types'; import { ShlinkVisitsParams } from '../api/types';
import { Topics } from '../mercure/helpers/Topics'; import { Topics } from '../mercure/helpers/Topics';
import { useGoBack } from '../utils/helpers/hooks';
import VisitsStats from './VisitsStats'; import VisitsStats from './VisitsStats';
import { OrphanVisitsHeader } from './OrphanVisitsHeader'; import { OrphanVisitsHeader } from './OrphanVisitsHeader';
import { NormalizedVisit, OrphanVisitType, VisitsInfo, VisitsParams } from './types'; import { NormalizedVisit, OrphanVisitType, VisitsInfo, VisitsParams } from './types';
@ -9,7 +9,7 @@ import { VisitsExporter } from './services/VisitsExporter';
import { CommonVisitsProps } from './types/CommonVisitsProps'; import { CommonVisitsProps } from './types/CommonVisitsProps';
import { toApiParams } from './types/helpers'; import { toApiParams } from './types/helpers';
export interface OrphanVisitsProps extends CommonVisitsProps, RouteComponentProps { export interface OrphanVisitsProps extends CommonVisitsProps {
getOrphanVisits: ( getOrphanVisits: (
params?: ShlinkVisitsParams, params?: ShlinkVisitsParams,
orphanVisitsType?: OrphanVisitType, orphanVisitsType?: OrphanVisitType,
@ -20,14 +20,13 @@ export interface OrphanVisitsProps extends CommonVisitsProps, RouteComponentProp
} }
export const OrphanVisits = ({ exportVisits }: VisitsExporter) => boundToMercureHub(({ export const OrphanVisits = ({ exportVisits }: VisitsExporter) => boundToMercureHub(({
history: { goBack },
match: { url },
getOrphanVisits, getOrphanVisits,
orphanVisits, orphanVisits,
cancelGetOrphanVisits, cancelGetOrphanVisits,
settings, settings,
selectedServer, selectedServer,
}: OrphanVisitsProps) => { }: OrphanVisitsProps) => {
const goBack = useGoBack();
const exportCsv = (visits: NormalizedVisit[]) => exportVisits('orphan_visits.csv', visits); const exportCsv = (visits: NormalizedVisit[]) => exportVisits('orphan_visits.csv', visits);
const loadVisits = (params: VisitsParams, doIntervalFallback?: boolean) => const loadVisits = (params: VisitsParams, doIntervalFallback?: boolean) =>
getOrphanVisits(toApiParams(params), params.filter?.orphanVisitsType, doIntervalFallback); getOrphanVisits(toApiParams(params), params.filter?.orphanVisitsType, doIntervalFallback);
@ -37,7 +36,6 @@ export const OrphanVisits = ({ exportVisits }: VisitsExporter) => boundToMercure
getVisits={loadVisits} getVisits={loadVisits}
cancelGetVisits={cancelGetOrphanVisits} cancelGetVisits={cancelGetOrphanVisits}
visitsInfo={orphanVisits} visitsInfo={orphanVisits}
baseUrl={url}
settings={settings} settings={settings}
exportCsv={exportCsv} exportCsv={exportCsv}
selectedServer={selectedServer} selectedServer={selectedServer}

View file

@ -1,10 +1,11 @@
import { useEffect } from 'react'; import { useEffect } from 'react';
import { RouteComponentProps } from 'react-router'; import { useLocation, useParams } from 'react-router-dom';
import { boundToMercureHub } from '../mercure/helpers/boundToMercureHub'; import { boundToMercureHub } from '../mercure/helpers/boundToMercureHub';
import { ShlinkVisitsParams } from '../api/types'; import { ShlinkVisitsParams } from '../api/types';
import { parseQuery } from '../utils/helpers/query'; import { parseQuery } from '../utils/helpers/query';
import { Topics } from '../mercure/helpers/Topics'; import { Topics } from '../mercure/helpers/Topics';
import { ShortUrlDetail } from '../short-urls/reducers/shortUrlDetail'; import { ShortUrlDetail } from '../short-urls/reducers/shortUrlDetail';
import { useGoBack } from '../utils/helpers/hooks';
import { ShortUrlVisits as ShortUrlVisitsState } from './reducers/shortUrlVisits'; import { ShortUrlVisits as ShortUrlVisitsState } from './reducers/shortUrlVisits';
import ShortUrlVisitsHeader from './ShortUrlVisitsHeader'; import ShortUrlVisitsHeader from './ShortUrlVisitsHeader';
import VisitsStats from './VisitsStats'; import VisitsStats from './VisitsStats';
@ -13,7 +14,7 @@ import { NormalizedVisit, VisitsParams } from './types';
import { CommonVisitsProps } from './types/CommonVisitsProps'; import { CommonVisitsProps } from './types/CommonVisitsProps';
import { toApiParams } from './types/helpers'; import { toApiParams } from './types/helpers';
export interface ShortUrlVisitsProps extends CommonVisitsProps, RouteComponentProps<{ shortCode: string }> { export interface ShortUrlVisitsProps extends CommonVisitsProps {
getShortUrlVisits: (shortCode: string, query?: ShlinkVisitsParams, doIntervalFallback?: boolean) => void; getShortUrlVisits: (shortCode: string, query?: ShlinkVisitsParams, doIntervalFallback?: boolean) => void;
shortUrlVisits: ShortUrlVisitsState; shortUrlVisits: ShortUrlVisitsState;
getShortUrlDetail: Function; getShortUrlDetail: Function;
@ -22,9 +23,6 @@ export interface ShortUrlVisitsProps extends CommonVisitsProps, RouteComponentPr
} }
const ShortUrlVisits = ({ exportVisits }: VisitsExporter) => boundToMercureHub(({ const ShortUrlVisits = ({ exportVisits }: VisitsExporter) => boundToMercureHub(({
history: { goBack },
match: { params, url },
location: { search },
shortUrlVisits, shortUrlVisits,
shortUrlDetail, shortUrlDetail,
getShortUrlVisits, getShortUrlVisits,
@ -33,7 +31,9 @@ const ShortUrlVisits = ({ exportVisits }: VisitsExporter) => boundToMercureHub((
settings, settings,
selectedServer, selectedServer,
}: ShortUrlVisitsProps) => { }: ShortUrlVisitsProps) => {
const { shortCode } = params; const { shortCode = '' } = useParams<{ shortCode: string }>();
const { search } = useLocation();
const goBack = useGoBack();
const { domain } = parseQuery<{ domain?: string }>(search); const { domain } = parseQuery<{ domain?: string }>(search);
const loadVisits = (params: VisitsParams, doIntervalFallback?: boolean) => const loadVisits = (params: VisitsParams, doIntervalFallback?: boolean) =>
getShortUrlVisits(shortCode, { ...toApiParams(params), domain }, doIntervalFallback); getShortUrlVisits(shortCode, { ...toApiParams(params), domain }, doIntervalFallback);
@ -51,7 +51,6 @@ const ShortUrlVisits = ({ exportVisits }: VisitsExporter) => boundToMercureHub((
getVisits={loadVisits} getVisits={loadVisits}
cancelGetVisits={cancelGetShortUrlVisits} cancelGetVisits={cancelGetShortUrlVisits}
visitsInfo={shortUrlVisits} visitsInfo={shortUrlVisits}
baseUrl={url}
domain={domain} domain={domain}
settings={settings} settings={settings}
exportCsv={exportCsv} exportCsv={exportCsv}
@ -60,6 +59,6 @@ const ShortUrlVisits = ({ exportVisits }: VisitsExporter) => boundToMercureHub((
<ShortUrlVisitsHeader shortUrlDetail={shortUrlDetail} shortUrlVisits={shortUrlVisits} goBack={goBack} /> <ShortUrlVisitsHeader shortUrlDetail={shortUrlDetail} shortUrlVisits={shortUrlVisits} goBack={goBack} />
</VisitsStats> </VisitsStats>
); );
}, ({ match }) => [ Topics.shortUrlVisits(match.params.shortCode) ]); }, (_, params) => [ Topics.shortUrlVisits(params.shortCode) ]);
export default ShortUrlVisits; export default ShortUrlVisits;

View file

@ -1,8 +1,9 @@
import { RouteComponentProps } from 'react-router'; import { useParams } from 'react-router-dom';
import { boundToMercureHub } from '../mercure/helpers/boundToMercureHub'; import { boundToMercureHub } from '../mercure/helpers/boundToMercureHub';
import ColorGenerator from '../utils/services/ColorGenerator'; import ColorGenerator from '../utils/services/ColorGenerator';
import { ShlinkVisitsParams } from '../api/types'; import { ShlinkVisitsParams } from '../api/types';
import { Topics } from '../mercure/helpers/Topics'; import { Topics } from '../mercure/helpers/Topics';
import { useGoBack } from '../utils/helpers/hooks';
import { TagVisits as TagVisitsState } from './reducers/tagVisits'; import { TagVisits as TagVisitsState } from './reducers/tagVisits';
import TagVisitsHeader from './TagVisitsHeader'; import TagVisitsHeader from './TagVisitsHeader';
import VisitsStats from './VisitsStats'; import VisitsStats from './VisitsStats';
@ -11,22 +12,21 @@ import { NormalizedVisit } from './types';
import { CommonVisitsProps } from './types/CommonVisitsProps'; import { CommonVisitsProps } from './types/CommonVisitsProps';
import { toApiParams } from './types/helpers'; import { toApiParams } from './types/helpers';
export interface TagVisitsProps extends CommonVisitsProps, RouteComponentProps<{ tag: string }> { export interface TagVisitsProps extends CommonVisitsProps {
getTagVisits: (tag: string, query?: ShlinkVisitsParams, doIntervalFallback?: boolean) => void; getTagVisits: (tag: string, query?: ShlinkVisitsParams, doIntervalFallback?: boolean) => void;
tagVisits: TagVisitsState; tagVisits: TagVisitsState;
cancelGetTagVisits: () => void; cancelGetTagVisits: () => void;
} }
const TagVisits = (colorGenerator: ColorGenerator, { exportVisits }: VisitsExporter) => boundToMercureHub(({ const TagVisits = (colorGenerator: ColorGenerator, { exportVisits }: VisitsExporter) => boundToMercureHub(({
history: { goBack },
match: { params, url },
getTagVisits, getTagVisits,
tagVisits, tagVisits,
cancelGetTagVisits, cancelGetTagVisits,
settings, settings,
selectedServer, selectedServer,
}: TagVisitsProps) => { }: TagVisitsProps) => {
const { tag } = params; const goBack = useGoBack();
const { tag = '' } = useParams();
const loadVisits = (params: ShlinkVisitsParams, doIntervalFallback?: boolean) => const loadVisits = (params: ShlinkVisitsParams, doIntervalFallback?: boolean) =>
getTagVisits(tag, toApiParams(params), doIntervalFallback); getTagVisits(tag, toApiParams(params), doIntervalFallback);
const exportCsv = (visits: NormalizedVisit[]) => exportVisits(`tag_${tag}_visits.csv`, visits); const exportCsv = (visits: NormalizedVisit[]) => exportVisits(`tag_${tag}_visits.csv`, visits);
@ -36,7 +36,6 @@ const TagVisits = (colorGenerator: ColorGenerator, { exportVisits }: VisitsExpor
getVisits={loadVisits} getVisits={loadVisits}
cancelGetVisits={cancelGetTagVisits} cancelGetVisits={cancelGetTagVisits}
visitsInfo={tagVisits} visitsInfo={tagVisits}
baseUrl={url}
settings={settings} settings={settings}
exportCsv={exportCsv} exportCsv={exportCsv}
selectedServer={selectedServer} selectedServer={selectedServer}

View file

@ -4,7 +4,7 @@ import { Button, Card, Nav, NavLink, Progress, Row } from 'reactstrap';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faCalendarAlt, faMapMarkedAlt, faList, faChartPie, faFileDownload } from '@fortawesome/free-solid-svg-icons'; import { faCalendarAlt, faMapMarkedAlt, faList, faChartPie, faFileDownload } from '@fortawesome/free-solid-svg-icons';
import { IconDefinition } from '@fortawesome/fontawesome-common-types'; import { IconDefinition } from '@fortawesome/fontawesome-common-types';
import { Route, Switch, NavLink as RouterNavLink, Redirect } from 'react-router-dom'; import { Route, Routes, NavLink as RouterNavLink, Navigate } from 'react-router-dom';
import { Location } from 'history'; import { Location } from 'history';
import classNames from 'classnames'; import classNames from 'classnames';
import { DateRangeSelector } from '../utils/dates/DateRangeSelector'; import { DateRangeSelector } from '../utils/dates/DateRangeSelector';
@ -33,7 +33,6 @@ export interface VisitsStatsProps {
settings: Settings; settings: Settings;
selectedServer: SelectedServer; selectedServer: SelectedServer;
cancelGetVisits: () => void; cancelGetVisits: () => void;
baseUrl: string;
domain?: string; domain?: string;
exportCsv: (visits: NormalizedVisit[]) => void; exportCsv: (visits: NormalizedVisit[]) => void;
isOrphanVisits?: boolean; isOrphanVisits?: boolean;
@ -48,10 +47,10 @@ interface VisitsNavLinkProps {
type Section = 'byTime' | 'byContext' | 'byLocation' | 'list'; type Section = 'byTime' | 'byContext' | 'byLocation' | 'list';
const sections: Record<Section, VisitsNavLinkProps> = { const sections: Record<Section, VisitsNavLinkProps> = {
byTime: { title: 'By time', subPath: '', icon: faCalendarAlt }, byTime: { title: 'By time', subPath: 'by-time', icon: faCalendarAlt },
byContext: { title: 'By context', subPath: '/by-context', icon: faChartPie }, byContext: { title: 'By context', subPath: 'by-context', icon: faChartPie },
byLocation: { title: 'By location', subPath: '/by-location', icon: faMapMarkedAlt }, byLocation: { title: 'By location', subPath: 'by-location', icon: faMapMarkedAlt },
list: { title: 'List', subPath: '/list', icon: faList }, list: { title: 'List', subPath: 'list', icon: faList },
}; };
let selectedBar: string | undefined; let selectedBar: string | undefined;
@ -74,7 +73,6 @@ const VisitsStats: FC<VisitsStatsProps> = ({
visitsInfo, visitsInfo,
getVisits, getVisits,
cancelGetVisits, cancelGetVisits,
baseUrl,
domain, domain,
settings, settings,
exportCsv, exportCsv,
@ -95,7 +93,7 @@ const VisitsStats: FC<VisitsStatsProps> = ({
const buildSectionUrl = (subPath?: string) => { const buildSectionUrl = (subPath?: string) => {
const query = domain ? `?domain=${domain}` : ''; const query = domain ? `?domain=${domain}` : '';
return !subPath ? `${baseUrl}${query}` : `${baseUrl}${subPath}${query}`; return !subPath ? `${query}` : `${subPath}${query}`;
}; };
const normalizedVisits = useMemo(() => normalizeVisits(visits), [ visits ]); const normalizedVisits = useMemo(() => normalizeVisits(visits), [ visits ]);
const { os, browsers, referrers, countries, cities, citiesForMap, visitedUrls } = useMemo( const { os, browsers, referrers, countries, cities, citiesForMap, visitedUrls } = useMemo(
@ -166,104 +164,120 @@ const VisitsStats: FC<VisitsStatsProps> = ({
</Nav> </Nav>
</Card> </Card>
<Row> <Row>
<Switch> <Routes>
<Route exact path={baseUrl}> <Route
<div className="col-12 mt-3"> path={sections.byTime.subPath}
<LineChartCard element={(
title="Visits during time" <div className="col-12 mt-3">
visits={normalizedVisits} <LineChartCard
highlightedVisits={highlightedVisits} title="Visits during time"
highlightedLabel={highlightedLabel} visits={normalizedVisits}
setSelectedVisits={setSelectedVisits} highlightedVisits={highlightedVisits}
/>
</div>
</Route>
<Route exact path={`${baseUrl}${sections.byContext.subPath}`}>
<div className={classNames('mt-3 col-lg-6', { 'col-xl-4': !isOrphanVisits })}>
<DoughnutChartCard title="Operating systems" stats={os} />
</div>
<div className={classNames('mt-3 col-lg-6', { 'col-xl-4': !isOrphanVisits })}>
<DoughnutChartCard title="Browsers" stats={browsers} />
</div>
<div className={classNames('mt-3', { 'col-xl-4': !isOrphanVisits, 'col-lg-6': isOrphanVisits })}>
<SortableBarChartCard
title="Referrers"
stats={referrers}
withPagination={false}
highlightedStats={highlightedVisitsToStats(highlightedVisits, 'referer')}
highlightedLabel={highlightedLabel}
sortingItems={{
name: 'Referrer name',
amount: 'Visits amount',
}}
onClick={highlightVisitsForProp('referer')}
/>
</div>
{isOrphanVisits && (
<div className="mt-3 col-lg-6">
<SortableBarChartCard
title="Visited URLs"
stats={visitedUrls}
highlightedLabel={highlightedLabel} highlightedLabel={highlightedLabel}
highlightedStats={highlightedVisitsToStats(highlightedVisits, 'visitedUrl')} setSelectedVisits={setSelectedVisits}
sortingItems={{
visitedUrl: 'Visited URL',
amount: 'Visits amount',
}}
onClick={highlightVisitsForProp('visitedUrl')}
/> />
</div> </div>
)} )}
</Route> />
<Route exact path={`${baseUrl}${sections.byLocation.subPath}`}> <Route
<div className="col-lg-6 mt-3"> path={sections.byContext.subPath}
<SortableBarChartCard element={(
title="Countries" <>
stats={countries} <div className={classNames('mt-3 col-lg-6', { 'col-xl-4': !isOrphanVisits })}>
highlightedStats={highlightedVisitsToStats(highlightedVisits, 'country')} <DoughnutChartCard title="Operating systems" stats={os} />
highlightedLabel={highlightedLabel} </div>
sortingItems={{ <div className={classNames('mt-3 col-lg-6', { 'col-xl-4': !isOrphanVisits })}>
name: 'Country name', <DoughnutChartCard title="Browsers" stats={browsers} />
amount: 'Visits amount', </div>
}} <div className={classNames('mt-3', { 'col-xl-4': !isOrphanVisits, 'col-lg-6': isOrphanVisits })}>
onClick={highlightVisitsForProp('country')} <SortableBarChartCard
/> title="Referrers"
</div> stats={referrers}
<div className="col-lg-6 mt-3"> withPagination={false}
<SortableBarChartCard highlightedStats={highlightedVisitsToStats(highlightedVisits, 'referer')}
title="Cities" highlightedLabel={highlightedLabel}
stats={cities} sortingItems={{
highlightedStats={highlightedVisitsToStats(highlightedVisits, 'city')} name: 'Referrer name',
highlightedLabel={highlightedLabel} amount: 'Visits amount',
extraHeaderContent={(activeCities: string[]) => }}
mapLocations.length > 0 && onClick={highlightVisitsForProp('referer')}
<OpenMapModalBtn modalTitle="Cities" locations={mapLocations} activeCities={activeCities} /> />
} </div>
sortingItems={{ {isOrphanVisits && (
name: 'City name', <div className="mt-3 col-lg-6">
amount: 'Visits amount', <SortableBarChartCard
}} title="Visited URLs"
onClick={highlightVisitsForProp('city')} stats={visitedUrls}
/> highlightedLabel={highlightedLabel}
</div> highlightedStats={highlightedVisitsToStats(highlightedVisits, 'visitedUrl')}
</Route> sortingItems={{
visitedUrl: 'Visited URL',
amount: 'Visits amount',
}}
onClick={highlightVisitsForProp('visitedUrl')}
/>
</div>
)}
</>
)}
/>
<Route exact path={`${baseUrl}${sections.list.subPath}`}> <Route
<div className="col-12"> path={sections.byLocation.subPath}
<VisitsTable element={(
visits={normalizedVisits} <>
selectedVisits={highlightedVisits} <div className="col-lg-6 mt-3">
setSelectedVisits={setSelectedVisits} <SortableBarChartCard
isOrphanVisits={isOrphanVisits} title="Countries"
selectedServer={selectedServer} stats={countries}
/> highlightedStats={highlightedVisitsToStats(highlightedVisits, 'country')}
</div> highlightedLabel={highlightedLabel}
</Route> sortingItems={{
name: 'Country name',
amount: 'Visits amount',
}}
onClick={highlightVisitsForProp('country')}
/>
</div>
<div className="col-lg-6 mt-3">
<SortableBarChartCard
title="Cities"
stats={cities}
highlightedStats={highlightedVisitsToStats(highlightedVisits, 'city')}
highlightedLabel={highlightedLabel}
extraHeaderContent={(activeCities: string[]) =>
mapLocations.length > 0 &&
<OpenMapModalBtn modalTitle="Cities" locations={mapLocations} activeCities={activeCities} />
}
sortingItems={{
name: 'City name',
amount: 'Visits amount',
}}
onClick={highlightVisitsForProp('city')}
/>
</div>
</>
)}
/>
<Redirect to={baseUrl} /> <Route
</Switch> path={sections.list.subPath}
element={(
<div className="col-12">
<VisitsTable
visits={normalizedVisits}
selectedVisits={highlightedVisits}
setSelectedVisits={setSelectedVisits}
isOrphanVisits={isOrphanVisits}
selectedServer={selectedServer}
/>
</div>
)}
/>
<Route path="*" element={<Navigate replace to={buildSectionUrl(sections.byTime.subPath)} />} />
</Routes>
</Row> </Row>
</> </>
); );

View file

@ -1,12 +1,15 @@
import { shallow, ShallowWrapper } from 'enzyme'; import { shallow, ShallowWrapper } from 'enzyme';
import { Route } from 'react-router-dom'; import { Route, useLocation } from 'react-router-dom';
import { Mock } from 'ts-mockery'; import { Mock } from 'ts-mockery';
import { match } from 'react-router';
import { History, Location } from 'history';
import { Settings } from '../../src/settings/reducers/settings'; import { Settings } from '../../src/settings/reducers/settings';
import appFactory from '../../src/app/App'; import appFactory from '../../src/app/App';
import { AppUpdateBanner } from '../../src/common/AppUpdateBanner'; import { AppUpdateBanner } from '../../src/common/AppUpdateBanner';
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useLocation: jest.fn().mockReturnValue({}),
}));
describe('<App />', () => { describe('<App />', () => {
let wrapper: ShallowWrapper; let wrapper: ShallowWrapper;
const MainHeader = () => null; const MainHeader = () => null;
@ -21,7 +24,7 @@ describe('<App />', () => {
() => null, () => null,
ShlinkVersions, ShlinkVersions,
); );
const createWrapper = (pathname = '') => { const createWrapper = () => {
wrapper = shallow( wrapper = shallow(
<App <App
fetchServers={() => {}} fetchServers={() => {}}
@ -29,16 +32,14 @@ describe('<App />', () => {
settings={Mock.all<Settings>()} settings={Mock.all<Settings>()}
appUpdated={false} appUpdated={false}
resetAppUpdate={() => {}} resetAppUpdate={() => {}}
match={Mock.all<match>()}
history={Mock.all<History>()}
location={Mock.of<Location>({ pathname })}
/>, />,
); );
return wrapper; return wrapper;
}; };
afterEach(() => wrapper.unmount()); afterEach(jest.clearAllMocks);
afterEach(() => wrapper?.unmount());
it('renders children components', () => { it('renders children components', () => {
const wrapper = createWrapper(); const wrapper = createWrapper();
@ -52,12 +53,12 @@ describe('<App />', () => {
const wrapper = createWrapper(); const wrapper = createWrapper();
const routes = wrapper.find(Route); const routes = wrapper.find(Route);
const expectedPaths = [ const expectedPaths = [
'/', undefined,
'/settings', '/settings',
'/manage-servers', '/manage-servers',
'/server/create', '/server/create',
'/server/:serverId/edit', '/server/:serverId/edit',
'/server/:serverId', '/server/:serverId/*',
]; ];
expect.assertions(expectedPaths.length + 1); expect.assertions(expectedPaths.length + 1);
@ -72,7 +73,9 @@ describe('<App />', () => {
[ '/bar', 'shlink-wrapper' ], [ '/bar', 'shlink-wrapper' ],
[ '/', 'shlink-wrapper d-flex d-md-block align-items-center' ], [ '/', 'shlink-wrapper d-flex d-md-block align-items-center' ],
])('renders expected classes on shlink-wrapper based on current pathname', (pathname, expectedClasses) => { ])('renders expected classes on shlink-wrapper based on current pathname', (pathname, expectedClasses) => {
const wrapper = createWrapper(pathname); (useLocation as any).mockReturnValue({ pathname }); // eslint-disable-line @typescript-eslint/no-unsafe-call
const wrapper = createWrapper();
const shlinkWrapper = wrapper.find('.shlink-wrapper'); const shlinkWrapper = wrapper.find('.shlink-wrapper');
expect(shlinkWrapper.prop('className')).toEqual(expectedClasses); expect(shlinkWrapper.prop('className')).toEqual(expectedClasses);

View file

@ -3,6 +3,11 @@ import { Mock } from 'ts-mockery';
import asideMenuCreator from '../../src/common/AsideMenu'; import asideMenuCreator from '../../src/common/AsideMenu';
import { ReachableServer } from '../../src/servers/data'; import { ReachableServer } from '../../src/servers/data';
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useLocation: jest.fn().mockReturnValue({ pathname: '' }),
}));
describe('<AsideMenu />', () => { describe('<AsideMenu />', () => {
let wrapped: ShallowWrapper; let wrapped: ShallowWrapper;
const DeleteServerButton = () => null; const DeleteServerButton = () => null;

View file

@ -1,19 +1,18 @@
import { shallow, ShallowWrapper } from 'enzyme'; import { shallow, ShallowWrapper } from 'enzyme';
import { Mock } from 'ts-mockery'; import { Mock } from 'ts-mockery';
import { RouteChildrenProps } from 'react-router-dom';
import Home, { HomeProps } from '../../src/common/Home'; import Home, { HomeProps } from '../../src/common/Home';
import { ServerWithId } from '../../src/servers/data'; import { ServerWithId } from '../../src/servers/data';
import { ShlinkLogo } from '../../src/common/img/ShlinkLogo'; import { ShlinkLogo } from '../../src/common/img/ShlinkLogo';
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useNavigate: jest.fn().mockReturnValue(jest.fn()),
}));
describe('<Home />', () => { describe('<Home />', () => {
let wrapped: ShallowWrapper; let wrapped: ShallowWrapper;
const defaultProps = {
...Mock.all<RouteChildrenProps>(),
resetSelectedServer: jest.fn(),
servers: {},
};
const createComponent = (props: Partial<HomeProps> = {}) => { const createComponent = (props: Partial<HomeProps> = {}) => {
const actualProps = { ...defaultProps, ...props }; const actualProps = { resetSelectedServer: jest.fn(), servers: {}, ...props };
wrapped = shallow(<Home {...actualProps} />); wrapped = shallow(<Home {...actualProps} />);

View file

@ -1,24 +1,28 @@
import { shallow, ShallowWrapper } from 'enzyme'; import { shallow, ShallowWrapper } from 'enzyme';
import { Mock } from 'ts-mockery'; import { useLocation } from 'react-router-dom';
import { match } from 'react-router';
import { History, Location } from 'history';
import { Collapse, NavbarToggler, NavLink } from 'reactstrap'; import { Collapse, NavbarToggler, NavLink } from 'reactstrap';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import createMainHeader from '../../src/common/MainHeader'; import createMainHeader from '../../src/common/MainHeader';
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useLocation: jest.fn().mockReturnValue({}),
}));
describe('<MainHeader />', () => { describe('<MainHeader />', () => {
const ServersDropdown = () => null; const ServersDropdown = () => null;
const MainHeader = createMainHeader(ServersDropdown); const MainHeader = createMainHeader(ServersDropdown);
let wrapper: ShallowWrapper; let wrapper: ShallowWrapper;
const createWrapper = (pathname = '') => { const createWrapper = (pathname = '') => {
const location = Mock.of<Location>({ pathname }); (useLocation as any).mockReturnValue({ pathname });
wrapper = shallow(<MainHeader history={Mock.all<History>()} location={location} match={Mock.all<match>()} />); wrapper = shallow(<MainHeader />);
return wrapper; return wrapper;
}; };
afterEach(jest.clearAllMocks);
afterEach(() => wrapper?.unmount()); afterEach(() => wrapper?.unmount());
it('renders ServersDropdown', () => { it('renders ServersDropdown', () => {

View file

@ -1,34 +1,31 @@
import { shallow, ShallowWrapper } from 'enzyme'; import { shallow, ShallowWrapper } from 'enzyme';
import { History, Location } from 'history'; import { Route, useParams } from 'react-router-dom';
import { match } from 'react-router'; // eslint-disable-line @typescript-eslint/no-unused-vars
import { Route } from 'react-router-dom';
import { Mock } from 'ts-mockery'; import { Mock } from 'ts-mockery';
import createMenuLayout from '../../src/common/MenuLayout'; import createMenuLayout from '../../src/common/MenuLayout';
import { NonReachableServer, NotFoundServer, ReachableServer, SelectedServer } from '../../src/servers/data'; import { NonReachableServer, NotFoundServer, ReachableServer, SelectedServer } from '../../src/servers/data';
import { NoMenuLayout } from '../../src/common/NoMenuLayout'; import { NoMenuLayout } from '../../src/common/NoMenuLayout';
import { SemVer } from '../../src/utils/helpers/version'; import { SemVer } from '../../src/utils/helpers/version';
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useParams: jest.fn().mockReturnValue({}),
useLocation: jest.fn().mockReturnValue({}),
}));
describe('<MenuLayout />', () => { describe('<MenuLayout />', () => {
const ServerError = jest.fn(); const ServerError = jest.fn();
const C = jest.fn(); const C = jest.fn();
const MenuLayout = createMenuLayout(C, C, C, C, C, C, C, C, ServerError, C, C, C); const MenuLayout = createMenuLayout(C, C, C, C, C, C, C, C, ServerError, C, C, C);
let wrapper: ShallowWrapper; let wrapper: ShallowWrapper;
const createWrapper = (selectedServer: SelectedServer) => { const createWrapper = (selectedServer: SelectedServer) => {
wrapper = shallow( (useParams as any).mockReturnValue({ serverId: 'abc123' });
<MenuLayout
selectServer={jest.fn()} wrapper = shallow(<MenuLayout selectServer={jest.fn()} selectedServer={selectedServer} />);
selectedServer={selectedServer}
history={Mock.all<History>()}
location={Mock.all<Location>()}
match={Mock.of<match<{ serverId: string }>>({
params: { serverId: 'abc123' },
})}
/>,
);
return wrapper; return wrapper;
}; };
afterEach(jest.clearAllMocks);
afterEach(() => wrapper?.unmount()); afterEach(() => wrapper?.unmount());
it.each([ it.each([
@ -49,17 +46,18 @@ describe('<MenuLayout />', () => {
}); });
it.each([ it.each([
[ '2.5.0' as SemVer, 8 ], [ '2.5.0' as SemVer, 9 ],
[ '2.6.0' as SemVer, 9 ], [ '2.6.0' as SemVer, 10 ],
[ '2.7.0' as SemVer, 9 ], [ '2.7.0' as SemVer, 10 ],
[ '2.8.0' as SemVer, 10 ], [ '2.8.0' as SemVer, 11 ],
[ '2.10.0' as SemVer, 10 ], [ '2.10.0' as SemVer, 11 ],
[ '3.0.0' as SemVer, 11 ], [ '3.0.0' as SemVer, 12 ],
])('has expected amount of routes based on selected server\'s version', (version, expectedAmountOfRoutes) => { ])('has expected amount of routes based on selected server\'s version', (version, expectedAmountOfRoutes) => {
const selectedServer = Mock.of<ReachableServer>({ version }); const selectedServer = Mock.of<ReachableServer>({ version });
const wrapper = createWrapper(selectedServer).dive(); const wrapper = createWrapper(selectedServer).dive();
const routes = wrapper.find(Route); const routes = wrapper.find(Route);
expect(routes).toHaveLength(expectedAmountOfRoutes); expect(routes).toHaveLength(expectedAmountOfRoutes);
expect(routes.findWhere((element) => element.prop('index'))).toHaveLength(1);
}); });
}); });

View file

@ -1,15 +1,18 @@
import { shallow, ShallowWrapper } from 'enzyme'; import { shallow, ShallowWrapper } from 'enzyme';
import { Mock } from 'ts-mockery';
import { RouteComponentProps } from 'react-router';
import createScrollToTop from '../../src/common/ScrollToTop'; import createScrollToTop from '../../src/common/ScrollToTop';
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useLocation: jest.fn().mockReturnValue({}),
}));
describe('<ScrollToTop />', () => { describe('<ScrollToTop />', () => {
let wrapper: ShallowWrapper; let wrapper: ShallowWrapper;
beforeEach(() => { beforeEach(() => {
const ScrollToTop = createScrollToTop(); const ScrollToTop = createScrollToTop();
wrapper = shallow(<ScrollToTop {...Mock.all<RouteComponentProps>()}>Foobar</ScrollToTop>); wrapper = shallow(<ScrollToTop>Foobar</ScrollToTop>);
}); });
afterEach(() => wrapper.unmount()); afterEach(() => wrapper.unmount());

View file

@ -1,27 +1,29 @@
import { shallow, ShallowWrapper } from 'enzyme'; import { shallow, ShallowWrapper } from 'enzyme';
import { Mock } from 'ts-mockery'; import { Mock } from 'ts-mockery';
import { History } from 'history'; import { useNavigate } from 'react-router-dom';
import createServerConstruct from '../../src/servers/CreateServer'; import createServerConstruct from '../../src/servers/CreateServer';
import { ServerForm } from '../../src/servers/helpers/ServerForm'; import { ServerForm } from '../../src/servers/helpers/ServerForm';
import { ServerWithId } from '../../src/servers/data'; import { ServerWithId } from '../../src/servers/data';
import { DuplicatedServersModal } from '../../src/servers/helpers/DuplicatedServersModal'; import { DuplicatedServersModal } from '../../src/servers/helpers/DuplicatedServersModal';
jest.mock('react-router-dom', () => ({ ...jest.requireActual('react-router-dom'), useNavigate: jest.fn() }));
describe('<CreateServer />', () => { describe('<CreateServer />', () => {
let wrapper: ShallowWrapper; let wrapper: ShallowWrapper;
const ImportServersBtn = () => null; const ImportServersBtn = () => null;
const createServerMock = jest.fn(); const createServerMock = jest.fn();
const push = jest.fn(); const navigate = jest.fn();
const goBack = jest.fn();
const historyMock = Mock.of<History>({ push, goBack });
const servers = { foo: Mock.all<ServerWithId>() }; const servers = { foo: Mock.all<ServerWithId>() };
const createWrapper = (serversImported = false, importFailed = false) => { const createWrapper = (serversImported = false, importFailed = false) => {
(useNavigate as any).mockReturnValue(navigate);
const useStateFlagTimeout = jest.fn() const useStateFlagTimeout = jest.fn()
.mockReturnValueOnce([ serversImported, () => '' ]) .mockReturnValueOnce([ serversImported, () => '' ])
.mockReturnValueOnce([ importFailed, () => '' ]) .mockReturnValueOnce([ importFailed, () => '' ])
.mockReturnValue([]); .mockReturnValue([]);
const CreateServer = createServerConstruct(ImportServersBtn, useStateFlagTimeout); const CreateServer = createServerConstruct(ImportServersBtn, useStateFlagTimeout);
wrapper = shallow(<CreateServer createServer={createServerMock} history={historyMock} servers={servers} />); wrapper = shallow(<CreateServer createServer={createServerMock} servers={servers} />);
return wrapper; return wrapper;
}; };
@ -68,7 +70,7 @@ describe('<CreateServer />', () => {
wrapper.find(DuplicatedServersModal).simulate('save'); wrapper.find(DuplicatedServersModal).simulate('save');
expect(createServerMock).toHaveBeenCalledTimes(1); expect(createServerMock).toHaveBeenCalledTimes(1);
expect(push).toHaveBeenCalledTimes(1); expect(navigate).toHaveBeenCalledTimes(1);
}); });
it('goes back on modal discard', () => { it('goes back on modal discard', () => {
@ -76,6 +78,6 @@ describe('<CreateServer />', () => {
wrapper.find(DuplicatedServersModal).simulate('discard'); wrapper.find(DuplicatedServersModal).simulate('discard');
expect(goBack).toHaveBeenCalledTimes(1); expect(navigate).toHaveBeenCalledWith(-1);
}); });
}); });

View file

@ -1,25 +1,28 @@
import { shallow, ShallowWrapper } from 'enzyme'; import { shallow, ShallowWrapper } from 'enzyme';
import { Modal, ModalBody, ModalFooter, ModalHeader } from 'reactstrap'; import { Modal, ModalBody, ModalFooter, ModalHeader } from 'reactstrap';
import { History } from 'history';
import { Mock } from 'ts-mockery'; import { Mock } from 'ts-mockery';
import { useNavigate } from 'react-router-dom';
import DeleteServerModal from '../../src/servers/DeleteServerModal'; import DeleteServerModal from '../../src/servers/DeleteServerModal';
import { ServerWithId } from '../../src/servers/data'; import { ServerWithId } from '../../src/servers/data';
jest.mock('react-router-dom', () => ({ ...jest.requireActual('react-router-dom'), useNavigate: jest.fn() }));
describe('<DeleteServerModal />', () => { describe('<DeleteServerModal />', () => {
let wrapper: ShallowWrapper; let wrapper: ShallowWrapper;
const deleteServerMock = jest.fn(); const deleteServerMock = jest.fn();
const push = jest.fn(); const navigate = jest.fn();
const toggleMock = jest.fn(); const toggleMock = jest.fn();
const serverName = 'the_server_name'; const serverName = 'the_server_name';
beforeEach(() => { beforeEach(() => {
(useNavigate as any).mockReturnValue(navigate);
wrapper = shallow( wrapper = shallow(
<DeleteServerModal <DeleteServerModal
server={Mock.of<ServerWithId>({ name: serverName })} server={Mock.of<ServerWithId>({ name: serverName })}
toggle={toggleMock} toggle={toggleMock}
isOpen={true} isOpen={true}
deleteServer={deleteServerMock} deleteServer={deleteServerMock}
history={Mock.of<History>({ push })}
/>, />,
); );
}); });
@ -48,7 +51,7 @@ describe('<DeleteServerModal />', () => {
expect(toggleMock).toHaveBeenCalledTimes(1); expect(toggleMock).toHaveBeenCalledTimes(1);
expect(deleteServerMock).not.toHaveBeenCalled(); expect(deleteServerMock).not.toHaveBeenCalled();
expect(push).not.toHaveBeenCalled(); expect(navigate).not.toHaveBeenCalled();
}); });
it('deletes server when clicking accept button', () => { it('deletes server when clicking accept button', () => {
@ -58,6 +61,6 @@ describe('<DeleteServerModal />', () => {
expect(toggleMock).toHaveBeenCalledTimes(1); expect(toggleMock).toHaveBeenCalledTimes(1);
expect(deleteServerMock).toHaveBeenCalledTimes(1); expect(deleteServerMock).toHaveBeenCalledTimes(1);
expect(push).toHaveBeenCalledTimes(1); expect(navigate).toHaveBeenCalledTimes(1);
}); });
}); });

View file

@ -1,43 +1,34 @@
import { mount, ReactWrapper } from 'enzyme'; import { mount, ReactWrapper } from 'enzyme';
import { Mock } from 'ts-mockery'; import { Mock } from 'ts-mockery';
import { History, Location } from 'history'; import { useNavigate } from 'react-router-dom';
import { match } from 'react-router'; // eslint-disable-line @typescript-eslint/no-unused-vars
import { EditServer as editServerConstruct } from '../../src/servers/EditServer'; import { EditServer as editServerConstruct } from '../../src/servers/EditServer';
import { ServerForm } from '../../src/servers/helpers/ServerForm'; import { ServerForm } from '../../src/servers/helpers/ServerForm';
import { ReachableServer } from '../../src/servers/data'; import { ReachableServer } from '../../src/servers/data';
jest.mock('react-router-dom', () => ({ ...jest.requireActual('react-router-dom'), useNavigate: jest.fn() }));
describe('<EditServer />', () => { describe('<EditServer />', () => {
let wrapper: ReactWrapper; let wrapper: ReactWrapper;
const ServerError = jest.fn(); const ServerError = jest.fn();
const editServerMock = jest.fn(); const editServerMock = jest.fn();
const goBack = jest.fn(); const navigate = jest.fn();
const historyMock = Mock.of<History>({ goBack });
const match = Mock.of<match<{ serverId: string }>>({
params: { serverId: 'abc123' },
});
const selectedServer = Mock.of<ReachableServer>({ const selectedServer = Mock.of<ReachableServer>({
id: 'abc123', id: 'abc123',
name: 'name', name: 'name',
url: 'url', url: 'url',
apiKey: 'apiKey', apiKey: 'apiKey',
}); });
const EditServer = editServerConstruct(ServerError);
beforeEach(() => { beforeEach(() => {
const EditServer = editServerConstruct(ServerError); (useNavigate as any).mockReturnValue(navigate);
wrapper = mount( wrapper = mount(
<EditServer <EditServer editServer={editServerMock} selectedServer={selectedServer} selectServer={jest.fn()} />,
editServer={editServerMock}
history={historyMock}
match={match}
location={Mock.all<Location>()}
selectedServer={selectedServer}
selectServer={jest.fn()}
/>,
); );
}); });
afterEach(jest.resetAllMocks); afterEach(jest.clearAllMocks);
afterEach(() => wrapper?.unmount()); afterEach(() => wrapper?.unmount());
it('renders components', () => { it('renders components', () => {
@ -50,6 +41,6 @@ describe('<EditServer />', () => {
form.simulate('submit', {}); form.simulate('submit', {});
expect(editServerMock).toHaveBeenCalledTimes(1); expect(editServerMock).toHaveBeenCalledTimes(1);
expect(goBack).toHaveBeenCalledTimes(1); expect(navigate).toHaveBeenCalledWith(-1);
}); });
}); });

View file

@ -1,7 +1,6 @@
import { shallow, ShallowWrapper } from 'enzyme'; import { shallow, ShallowWrapper } from 'enzyme';
import { Mock } from 'ts-mockery'; import { Mock } from 'ts-mockery';
import { History, Location } from 'history'; import { useLocation, useParams } from 'react-router-dom';
import { match } from 'react-router'; // eslint-disable-line @typescript-eslint/no-unused-vars
import { EditShortUrl as createEditShortUrl } from '../../src/short-urls/EditShortUrl'; import { EditShortUrl as createEditShortUrl } from '../../src/short-urls/EditShortUrl';
import { Settings } from '../../src/settings/reducers/settings'; import { Settings } from '../../src/settings/reducers/settings';
import { ShortUrlDetail } from '../../src/short-urls/reducers/shortUrlDetail'; import { ShortUrlDetail } from '../../src/short-urls/reducers/shortUrlDetail';
@ -9,29 +8,32 @@ import { ShortUrlEdition } from '../../src/short-urls/reducers/shortUrlEdition';
import { ShlinkApiError } from '../../src/api/ShlinkApiError'; import { ShlinkApiError } from '../../src/api/ShlinkApiError';
import { ShortUrl } from '../../src/short-urls/data'; import { ShortUrl } from '../../src/short-urls/data';
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useNavigate: jest.fn().mockReturnValue(jest.fn()),
useParams: jest.fn().mockReturnValue({}),
useLocation: jest.fn().mockReturnValue({}),
}));
describe('<EditShortUrl />', () => { describe('<EditShortUrl />', () => {
let wrapper: ShallowWrapper; let wrapper: ShallowWrapper;
const ShortUrlForm = () => null; const ShortUrlForm = () => null;
const goBack = jest.fn();
const getShortUrlDetail = jest.fn(); const getShortUrlDetail = jest.fn();
const editShortUrl = jest.fn(async () => Promise.resolve()); const editShortUrl = jest.fn(async () => Promise.resolve());
const shortUrlCreation = { validateUrls: true }; const shortUrlCreation = { validateUrls: true };
const EditShortUrl = createEditShortUrl(ShortUrlForm);
const createWrapper = (detail: Partial<ShortUrlDetail> = {}, edition: Partial<ShortUrlEdition> = {}) => { const createWrapper = (detail: Partial<ShortUrlDetail> = {}, edition: Partial<ShortUrlEdition> = {}) => {
const EditSHortUrl = createEditShortUrl(ShortUrlForm); (useParams as any).mockReturnValue({ shortCode: 'the_base_url' });
(useLocation as any).mockReturnValue({ search: '' });
wrapper = shallow( wrapper = shallow(
<EditSHortUrl <EditShortUrl
settings={Mock.of<Settings>({ shortUrlCreation })} settings={Mock.of<Settings>({ shortUrlCreation })}
selectedServer={null} selectedServer={null}
shortUrlDetail={Mock.of<ShortUrlDetail>(detail)} shortUrlDetail={Mock.of<ShortUrlDetail>(detail)}
shortUrlEdition={Mock.of<ShortUrlEdition>(edition)} shortUrlEdition={Mock.of<ShortUrlEdition>(edition)}
getShortUrlDetail={getShortUrlDetail} getShortUrlDetail={getShortUrlDetail}
editShortUrl={editShortUrl} editShortUrl={editShortUrl}
history={Mock.of<History>({ goBack })}
location={Mock.all<Location>()}
match={Mock.of<match<{ shortCode: string }>>({
params: { shortCode: 'the_base_url' },
})}
/>, />,
); );

View file

@ -1,29 +1,30 @@
import { shallow, ShallowWrapper } from 'enzyme'; import { shallow, ShallowWrapper } from 'enzyme';
import { Mock } from 'ts-mockery'; import { Mock } from 'ts-mockery';
import { History, Location } from 'history';
import { match } from 'react-router';
import { formatISO } from 'date-fns'; import { formatISO } from 'date-fns';
import filteringBarCreator, { ShortUrlsFilteringProps } from '../../src/short-urls/ShortUrlsFilteringBar'; import { useLocation, useNavigate } from 'react-router-dom';
import filteringBarCreator from '../../src/short-urls/ShortUrlsFilteringBar';
import SearchField from '../../src/utils/SearchField'; import SearchField from '../../src/utils/SearchField';
import Tag from '../../src/tags/helpers/Tag'; import Tag from '../../src/tags/helpers/Tag';
import { DateRangeSelector } from '../../src/utils/dates/DateRangeSelector'; import { DateRangeSelector } from '../../src/utils/dates/DateRangeSelector';
import ColorGenerator from '../../src/utils/services/ColorGenerator'; import ColorGenerator from '../../src/utils/services/ColorGenerator';
import { ShortUrlListRouteParams } from '../../src/short-urls/helpers/hooks';
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useNavigate: jest.fn(),
useParams: jest.fn().mockReturnValue({ serverId: '1' }),
useLocation: jest.fn().mockReturnValue({}),
}));
describe('<ShortUrlsFilteringBar />', () => { describe('<ShortUrlsFilteringBar />', () => {
let wrapper: ShallowWrapper; let wrapper: ShallowWrapper;
const ShortUrlsFilteringBar = filteringBarCreator(Mock.all<ColorGenerator>()); const ShortUrlsFilteringBar = filteringBarCreator(Mock.all<ColorGenerator>());
const push = jest.fn(); const navigate = jest.fn();
const now = new Date(); const now = new Date();
const createWrapper = (props: Partial<ShortUrlsFilteringProps> = {}) => { const createWrapper = (search = '') => {
wrapper = shallow( (useLocation as any).mockReturnValue({ search });
<ShortUrlsFilteringBar (useNavigate as any).mockReturnValue(navigate);
history={Mock.of<History>({ push })}
location={Mock.of<Location>({ search: '' })} wrapper = shallow(<ShortUrlsFilteringBar />);
match={Mock.of<match<ShortUrlListRouteParams>>({ params: { serverId: '1' } })}
{...props}
/>,
);
return wrapper; return wrapper;
}; };
@ -44,7 +45,7 @@ describe('<ShortUrlsFilteringBar />', () => {
[ '', 0 ], [ '', 0 ],
[ 'foo=bar', 0 ], [ 'foo=bar', 0 ],
])('renders the proper amount of tags', (search, expectedTagComps) => { ])('renders the proper amount of tags', (search, expectedTagComps) => {
const wrapper = createWrapper({ location: Mock.of<Location>({ search }) }); const wrapper = createWrapper(search);
expect(wrapper.find(Tag)).toHaveLength(expectedTagComps); expect(wrapper.find(Tag)).toHaveLength(expectedTagComps);
}); });
@ -53,18 +54,18 @@ describe('<ShortUrlsFilteringBar />', () => {
const wrapper = createWrapper(); const wrapper = createWrapper();
const searchField = wrapper.find(SearchField); const searchField = wrapper.find(SearchField);
expect(push).not.toHaveBeenCalled(); expect(navigate).not.toHaveBeenCalled();
searchField.simulate('change', 'search-term'); searchField.simulate('change', 'search-term');
expect(push).toHaveBeenCalledWith('/server/1/list-short-urls/1?search=search-term'); expect(navigate).toHaveBeenCalledWith('/server/1/list-short-urls/1?search=search-term');
}); });
it('redirects to first page when a tag is removed', () => { it('redirects to first page when a tag is removed', () => {
const wrapper = createWrapper({ location: Mock.of<Location>({ search: 'tags=foo,bar' }) }); const wrapper = createWrapper('tags=foo,bar');
const tag = wrapper.find(Tag).first(); const tag = wrapper.find(Tag).first();
expect(push).not.toHaveBeenCalled(); expect(navigate).not.toHaveBeenCalled();
tag.simulate('close'); tag.simulate('close');
expect(push).toHaveBeenCalledWith('/server/1/list-short-urls/1?tags=bar'); expect(navigate).toHaveBeenCalledWith('/server/1/list-short-urls/1?tags=bar');
}); });
it.each([ it.each([
@ -78,8 +79,8 @@ describe('<ShortUrlsFilteringBar />', () => {
const wrapper = createWrapper(); const wrapper = createWrapper();
const dateRange = wrapper.find(DateRangeSelector); const dateRange = wrapper.find(DateRangeSelector);
expect(push).not.toHaveBeenCalled(); expect(navigate).not.toHaveBeenCalled();
dateRange.simulate('datesChange', dates); dateRange.simulate('datesChange', dates);
expect(push).toHaveBeenCalledWith(`/server/1/list-short-urls/1?${expectedQuery}`); expect(navigate).toHaveBeenCalledWith(`/server/1/list-short-urls/1?${expectedQuery}`);
}); });
}); });

View file

@ -1,8 +1,7 @@
import { shallow, ShallowWrapper } from 'enzyme'; import { shallow, ShallowWrapper } from 'enzyme';
import { ReactElement } from 'react'; import { ReactElement } from 'react';
import { Mock } from 'ts-mockery'; import { Mock } from 'ts-mockery';
import { History, Location } from 'history'; import { useNavigate } from 'react-router-dom';
import { match } from 'react-router';
import shortUrlsListCreator from '../../src/short-urls/ShortUrlsList'; import shortUrlsListCreator from '../../src/short-urls/ShortUrlsList';
import { ShortUrlsOrderableFields, ShortUrl, ShortUrlsOrder } from '../../src/short-urls/data'; import { ShortUrlsOrderableFields, ShortUrl, ShortUrlsOrder } from '../../src/short-urls/data';
import { MercureBoundProps } from '../../src/mercure/helpers/boundToMercureHub'; import { MercureBoundProps } from '../../src/mercure/helpers/boundToMercureHub';
@ -10,15 +9,21 @@ import { ShortUrlsList as ShortUrlsListModel } from '../../src/short-urls/reduce
import { OrderingDropdown } from '../../src/utils/OrderingDropdown'; import { OrderingDropdown } from '../../src/utils/OrderingDropdown';
import Paginator from '../../src/short-urls/Paginator'; import Paginator from '../../src/short-urls/Paginator';
import { ReachableServer } from '../../src/servers/data'; import { ReachableServer } from '../../src/servers/data';
import { ShortUrlListRouteParams } from '../../src/short-urls/helpers/hooks';
import { Settings } from '../../src/settings/reducers/settings'; import { Settings } from '../../src/settings/reducers/settings';
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useNavigate: jest.fn().mockReturnValue(jest.fn()),
useParams: jest.fn().mockReturnValue({}),
useLocation: jest.fn().mockReturnValue({ search: '?tags=test%20tag&search=example.com' }),
}));
describe('<ShortUrlsList />', () => { describe('<ShortUrlsList />', () => {
let wrapper: ShallowWrapper; let wrapper: ShallowWrapper;
const ShortUrlsTable = () => null; const ShortUrlsTable = () => null;
const ShortUrlsFilteringBar = () => null; const ShortUrlsFilteringBar = () => null;
const listShortUrlsMock = jest.fn(); const listShortUrlsMock = jest.fn();
const push = jest.fn(); const navigate = jest.fn();
const shortUrlsList = Mock.of<ShortUrlsListModel>({ const shortUrlsList = Mock.of<ShortUrlsListModel>({
shortUrls: { shortUrls: {
data: [ data: [
@ -36,20 +41,19 @@ describe('<ShortUrlsList />', () => {
<ShortUrlsList <ShortUrlsList
{...Mock.of<MercureBoundProps>({ mercureInfo: { loading: true } })} {...Mock.of<MercureBoundProps>({ mercureInfo: { loading: true } })}
listShortUrls={listShortUrlsMock} listShortUrls={listShortUrlsMock}
match={Mock.of<match<ShortUrlListRouteParams>>({ params: {} })}
location={Mock.of<Location>({ search: '?tags=test%20tag&search=example.com' })}
shortUrlsList={shortUrlsList} shortUrlsList={shortUrlsList}
history={Mock.of<History>({ push })}
selectedServer={Mock.of<ReachableServer>({ id: '1' })} selectedServer={Mock.of<ReachableServer>({ id: '1' })}
settings={Mock.of<Settings>({ shortUrlsList: { defaultOrdering } })} settings={Mock.of<Settings>({ shortUrlsList: { defaultOrdering } })}
/>, />,
).dive(); // Dive is needed as this component is wrapped in a HOC ).dive(); // Dive is needed as this component is wrapped in a HOC
beforeEach(() => { beforeEach(() => {
(useNavigate as any).mockReturnValue(navigate);
wrapper = createWrapper(); wrapper = createWrapper();
}); });
afterEach(jest.resetAllMocks); afterEach(jest.clearAllMocks);
afterEach(() => wrapper?.unmount()); afterEach(() => wrapper?.unmount());
it('wraps expected components', () => { it('wraps expected components', () => {
@ -68,10 +72,10 @@ describe('<ShortUrlsList />', () => {
wrapper.find(ShortUrlsTable).simulate('tagClick', 'bar'); wrapper.find(ShortUrlsTable).simulate('tagClick', 'bar');
wrapper.find(ShortUrlsTable).simulate('tagClick', 'baz'); wrapper.find(ShortUrlsTable).simulate('tagClick', 'baz');
expect(push).toHaveBeenCalledTimes(3); expect(navigate).toHaveBeenCalledTimes(3);
expect(push).toHaveBeenNthCalledWith(1, expect.stringContaining(`tags=${encodeURIComponent('test tag,foo')}`)); expect(navigate).toHaveBeenNthCalledWith(1, expect.stringContaining(`tags=${encodeURIComponent('test tag,foo')}`));
expect(push).toHaveBeenNthCalledWith(2, expect.stringContaining(`tags=${encodeURIComponent('test tag,bar')}`)); expect(navigate).toHaveBeenNthCalledWith(2, expect.stringContaining(`tags=${encodeURIComponent('test tag,bar')}`));
expect(push).toHaveBeenNthCalledWith(3, expect.stringContaining(`tags=${encodeURIComponent('test tag,baz')}`)); expect(navigate).toHaveBeenNthCalledWith(3, expect.stringContaining(`tags=${encodeURIComponent('test tag,baz')}`));
}); });
it('invokes order icon rendering', () => { it('invokes order icon rendering', () => {

View file

@ -1,13 +1,14 @@
import { Mock } from 'ts-mockery'; import { Mock } from 'ts-mockery';
import { shallow, ShallowWrapper } from 'enzyme'; import { shallow, ShallowWrapper } from 'enzyme';
import { match } from 'react-router'; import { useLocation } from 'react-router-dom';
import { Location, History } from 'history';
import { TagsTable as createTagsTable } from '../../src/tags/TagsTable'; import { TagsTable as createTagsTable } from '../../src/tags/TagsTable';
import { SelectedServer } from '../../src/servers/data'; import { SelectedServer } from '../../src/servers/data';
import { rangeOf } from '../../src/utils/utils'; import { rangeOf } from '../../src/utils/utils';
import SimplePaginator from '../../src/common/SimplePaginator'; import SimplePaginator from '../../src/common/SimplePaginator';
import { NormalizedTag } from '../../src/tags/data'; import { NormalizedTag } from '../../src/tags/data';
jest.mock('react-router-dom', () => ({ ...jest.requireActual('react-router-dom'), useLocation: jest.fn() }));
describe('<TagsTable />', () => { describe('<TagsTable />', () => {
const TagsTableRow = () => null; const TagsTableRow = () => null;
const orderByColumn = jest.fn(); const orderByColumn = jest.fn();
@ -15,14 +16,13 @@ describe('<TagsTable />', () => {
const tags = (amount: number) => rangeOf(amount, (i) => `tag_${i}`); const tags = (amount: number) => rangeOf(amount, (i) => `tag_${i}`);
let wrapper: ShallowWrapper; let wrapper: ShallowWrapper;
const createWrapper = (sortedTags: string[] = [], search = '') => { const createWrapper = (sortedTags: string[] = [], search = '') => {
(useLocation as any).mockReturnValue({ search });
wrapper = shallow( wrapper = shallow(
<TagsTable <TagsTable
sortedTags={sortedTags.map((tag) => Mock.of<NormalizedTag>({ tag }))} sortedTags={sortedTags.map((tag) => Mock.of<NormalizedTag>({ tag }))}
selectedServer={Mock.all<SelectedServer>()} selectedServer={Mock.all<SelectedServer>()}
currentOrder={{}} currentOrder={{}}
history={Mock.all<History>()}
location={Mock.of<Location>({ search })}
match={Mock.all<match>()}
orderByColumn={() => orderByColumn} orderByColumn={() => orderByColumn}
/>, />,
); );
@ -30,11 +30,6 @@ describe('<TagsTable />', () => {
return wrapper; return wrapper;
}; };
beforeEach(() => {
(global as any).location = { search: '', pathname: '' };
(global as any).history = { pushState: jest.fn() };
});
afterEach(jest.clearAllMocks); afterEach(jest.clearAllMocks);
afterEach(() => wrapper?.unmount()); afterEach(() => wrapper?.unmount());

View file

@ -1,7 +1,5 @@
import { shallow } from 'enzyme'; import { shallow } from 'enzyme';
import { Mock } from 'ts-mockery'; import { Mock } from 'ts-mockery';
import { History, Location } from 'history';
import { match } from 'react-router';
import { NonOrphanVisits as createNonOrphanVisits } from '../../src/visits/NonOrphanVisits'; import { NonOrphanVisits as createNonOrphanVisits } from '../../src/visits/NonOrphanVisits';
import { MercureBoundProps } from '../../src/mercure/helpers/boundToMercureHub'; import { MercureBoundProps } from '../../src/mercure/helpers/boundToMercureHub';
import { VisitsInfo } from '../../src/visits/types'; import { VisitsInfo } from '../../src/visits/types';
@ -11,9 +9,14 @@ import { Settings } from '../../src/settings/reducers/settings';
import { VisitsExporter } from '../../src/visits/services/VisitsExporter'; import { VisitsExporter } from '../../src/visits/services/VisitsExporter';
import { SelectedServer } from '../../src/servers/data'; import { SelectedServer } from '../../src/servers/data';
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useNavigate: jest.fn().mockReturnValue(jest.fn()),
useParams: jest.fn().mockReturnValue({}),
}));
describe('<NonOrphanVisits />', () => { describe('<NonOrphanVisits />', () => {
it('wraps visits stats and header', () => { it('wraps visits stats and header', () => {
const goBack = jest.fn();
const getNonOrphanVisits = jest.fn(); const getNonOrphanVisits = jest.fn();
const cancelGetNonOrphanVisits = jest.fn(); const cancelGetNonOrphanVisits = jest.fn();
const nonOrphanVisits = Mock.all<VisitsInfo>(); const nonOrphanVisits = Mock.all<VisitsInfo>();
@ -25,9 +28,6 @@ describe('<NonOrphanVisits />', () => {
getNonOrphanVisits={getNonOrphanVisits} getNonOrphanVisits={getNonOrphanVisits}
nonOrphanVisits={nonOrphanVisits} nonOrphanVisits={nonOrphanVisits}
cancelGetNonOrphanVisits={cancelGetNonOrphanVisits} cancelGetNonOrphanVisits={cancelGetNonOrphanVisits}
history={Mock.of<History>({ goBack })}
location={Mock.all<Location>()}
match={Mock.of<match>({ url: 'the_base_url' })}
settings={Mock.all<Settings>()} settings={Mock.all<Settings>()}
selectedServer={Mock.all<SelectedServer>()} selectedServer={Mock.all<SelectedServer>()}
/>, />,
@ -39,9 +39,8 @@ describe('<NonOrphanVisits />', () => {
expect(header).toHaveLength(1); expect(header).toHaveLength(1);
expect(stats.prop('cancelGetVisits')).toEqual(cancelGetNonOrphanVisits); expect(stats.prop('cancelGetVisits')).toEqual(cancelGetNonOrphanVisits);
expect(stats.prop('visitsInfo')).toEqual(nonOrphanVisits); expect(stats.prop('visitsInfo')).toEqual(nonOrphanVisits);
expect(stats.prop('baseUrl')).toEqual('the_base_url');
expect(stats.prop('isOrphanVisits')).not.toBeDefined(); expect(stats.prop('isOrphanVisits')).not.toBeDefined();
expect(header.prop('nonOrphanVisits')).toEqual(nonOrphanVisits); expect(header.prop('nonOrphanVisits')).toEqual(nonOrphanVisits);
expect(header.prop('goBack')).toEqual(goBack); expect(header.prop('goBack')).toEqual(expect.any(Function));
}); });
}); });

View file

@ -1,7 +1,5 @@
import { shallow } from 'enzyme'; import { shallow } from 'enzyme';
import { Mock } from 'ts-mockery'; import { Mock } from 'ts-mockery';
import { History, Location } from 'history';
import { match } from 'react-router';
import { OrphanVisits as createOrphanVisits } from '../../src/visits/OrphanVisits'; import { OrphanVisits as createOrphanVisits } from '../../src/visits/OrphanVisits';
import { MercureBoundProps } from '../../src/mercure/helpers/boundToMercureHub'; import { MercureBoundProps } from '../../src/mercure/helpers/boundToMercureHub';
import { VisitsInfo } from '../../src/visits/types'; import { VisitsInfo } from '../../src/visits/types';
@ -11,9 +9,14 @@ import { Settings } from '../../src/settings/reducers/settings';
import { VisitsExporter } from '../../src/visits/services/VisitsExporter'; import { VisitsExporter } from '../../src/visits/services/VisitsExporter';
import { SelectedServer } from '../../src/servers/data'; import { SelectedServer } from '../../src/servers/data';
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useNavigate: jest.fn().mockReturnValue(jest.fn()),
useParams: jest.fn().mockReturnValue({}),
}));
describe('<OrphanVisits />', () => { describe('<OrphanVisits />', () => {
it('wraps visits stats and header', () => { it('wraps visits stats and header', () => {
const goBack = jest.fn();
const getOrphanVisits = jest.fn(); const getOrphanVisits = jest.fn();
const cancelGetOrphanVisits = jest.fn(); const cancelGetOrphanVisits = jest.fn();
const orphanVisits = Mock.all<VisitsInfo>(); const orphanVisits = Mock.all<VisitsInfo>();
@ -25,9 +28,6 @@ describe('<OrphanVisits />', () => {
getOrphanVisits={getOrphanVisits} getOrphanVisits={getOrphanVisits}
orphanVisits={orphanVisits} orphanVisits={orphanVisits}
cancelGetOrphanVisits={cancelGetOrphanVisits} cancelGetOrphanVisits={cancelGetOrphanVisits}
history={Mock.of<History>({ goBack })}
location={Mock.all<Location>()}
match={Mock.of<match>({ url: 'the_base_url' })}
settings={Mock.all<Settings>()} settings={Mock.all<Settings>()}
selectedServer={Mock.all<SelectedServer>()} selectedServer={Mock.all<SelectedServer>()}
/>, />,
@ -39,9 +39,8 @@ describe('<OrphanVisits />', () => {
expect(header).toHaveLength(1); expect(header).toHaveLength(1);
expect(stats.prop('cancelGetVisits')).toEqual(cancelGetOrphanVisits); expect(stats.prop('cancelGetVisits')).toEqual(cancelGetOrphanVisits);
expect(stats.prop('visitsInfo')).toEqual(orphanVisits); expect(stats.prop('visitsInfo')).toEqual(orphanVisits);
expect(stats.prop('baseUrl')).toEqual('the_base_url');
expect(stats.prop('isOrphanVisits')).toEqual(true); expect(stats.prop('isOrphanVisits')).toEqual(true);
expect(header.prop('orphanVisits')).toEqual(orphanVisits); expect(header.prop('orphanVisits')).toEqual(orphanVisits);
expect(header.prop('goBack')).toEqual(goBack); expect(header.prop('goBack')).toEqual(expect.any(Function));
}); });
}); });

View file

@ -1,8 +1,6 @@
import { shallow, ShallowWrapper } from 'enzyme'; import { shallow, ShallowWrapper } from 'enzyme';
import { identity } from 'ramda'; import { identity } from 'ramda';
import { Mock } from 'ts-mockery'; import { Mock } from 'ts-mockery';
import { History, Location } from 'history';
import { match } from 'react-router'; // eslint-disable-line @typescript-eslint/no-unused-vars
import createShortUrlVisits, { ShortUrlVisitsProps } from '../../src/visits/ShortUrlVisits'; import createShortUrlVisits, { ShortUrlVisitsProps } from '../../src/visits/ShortUrlVisits';
import ShortUrlVisitsHeader from '../../src/visits/ShortUrlVisitsHeader'; import ShortUrlVisitsHeader from '../../src/visits/ShortUrlVisitsHeader';
import { ShortUrlVisits as ShortUrlVisitsState } from '../../src/visits/reducers/shortUrlVisits'; import { ShortUrlVisits as ShortUrlVisitsState } from '../../src/visits/reducers/shortUrlVisits';
@ -11,16 +9,16 @@ import VisitsStats from '../../src/visits/VisitsStats';
import { MercureBoundProps } from '../../src/mercure/helpers/boundToMercureHub'; import { MercureBoundProps } from '../../src/mercure/helpers/boundToMercureHub';
import { VisitsExporter } from '../../src/visits/services/VisitsExporter'; import { VisitsExporter } from '../../src/visits/services/VisitsExporter';
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useNavigate: jest.fn().mockReturnValue(jest.fn()),
useLocation: jest.fn().mockReturnValue({ search: '' }),
useParams: jest.fn().mockReturnValue({ shortCode: 'abc123' }),
}));
describe('<ShortUrlVisits />', () => { describe('<ShortUrlVisits />', () => {
let wrapper: ShallowWrapper; let wrapper: ShallowWrapper;
const getShortUrlVisitsMock = jest.fn(); const getShortUrlVisitsMock = jest.fn();
const match = Mock.of<match<{ shortCode: string }>>({
params: { shortCode: 'abc123' },
});
const location = Mock.of<Location>({ search: '' });
const history = Mock.of<History>({
goBack: jest.fn(),
});
const ShortUrlVisits = createShortUrlVisits(Mock.all<VisitsExporter>()); const ShortUrlVisits = createShortUrlVisits(Mock.all<VisitsExporter>());
beforeEach(() => { beforeEach(() => {
@ -30,9 +28,6 @@ describe('<ShortUrlVisits />', () => {
{...Mock.of<MercureBoundProps>({ mercureInfo: {} })} {...Mock.of<MercureBoundProps>({ mercureInfo: {} })}
getShortUrlDetail={identity} getShortUrlDetail={identity}
getShortUrlVisits={getShortUrlVisitsMock} getShortUrlVisits={getShortUrlVisitsMock}
match={match}
location={location}
history={history}
shortUrlVisits={Mock.of<ShortUrlVisitsState>({ loading: true, visits: [] })} shortUrlVisits={Mock.of<ShortUrlVisitsState>({ loading: true, visits: [] })}
shortUrlDetail={Mock.all<ShortUrlDetail>()} shortUrlDetail={Mock.all<ShortUrlDetail>()}
cancelGetShortUrlVisits={() => {}} cancelGetShortUrlVisits={() => {}}
@ -40,8 +35,8 @@ describe('<ShortUrlVisits />', () => {
).dive(); // Dive is needed as this component is wrapped in a HOC ).dive(); // Dive is needed as this component is wrapped in a HOC
}); });
afterEach(jest.clearAllMocks);
afterEach(() => wrapper.unmount()); afterEach(() => wrapper.unmount());
afterEach(jest.resetAllMocks);
it('renders visit stats and visits header', () => { it('renders visit stats and visits header', () => {
const visitStats = wrapper.find(VisitsStats); const visitStats = wrapper.find(VisitsStats);

View file

@ -1,7 +1,5 @@
import { shallow, ShallowWrapper } from 'enzyme'; import { shallow, ShallowWrapper } from 'enzyme';
import { Mock } from 'ts-mockery'; import { Mock } from 'ts-mockery';
import { History } from 'history';
import { match } from 'react-router'; // eslint-disable-line @typescript-eslint/no-unused-vars
import createTagVisits, { TagVisitsProps } from '../../src/visits/TagVisits'; import createTagVisits, { TagVisitsProps } from '../../src/visits/TagVisits';
import TagVisitsHeader from '../../src/visits/TagVisitsHeader'; import TagVisitsHeader from '../../src/visits/TagVisitsHeader';
import ColorGenerator from '../../src/utils/services/ColorGenerator'; import ColorGenerator from '../../src/utils/services/ColorGenerator';
@ -10,15 +8,16 @@ import VisitsStats from '../../src/visits/VisitsStats';
import { MercureBoundProps } from '../../src/mercure/helpers/boundToMercureHub'; import { MercureBoundProps } from '../../src/mercure/helpers/boundToMercureHub';
import { VisitsExporter } from '../../src/visits/services/VisitsExporter'; import { VisitsExporter } from '../../src/visits/services/VisitsExporter';
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useNavigate: jest.fn().mockReturnValue(jest.fn()),
useLocation: jest.fn().mockReturnValue({}),
useParams: jest.fn().mockReturnValue({ tag: 'foo' }),
}));
describe('<TagVisits />', () => { describe('<TagVisits />', () => {
let wrapper: ShallowWrapper; let wrapper: ShallowWrapper;
const getTagVisitsMock = jest.fn(); const getTagVisitsMock = jest.fn();
const match = Mock.of<match<{ tag: string }>>({
params: { tag: 'foo' },
});
const history = Mock.of<History>({
goBack: jest.fn(),
});
beforeEach(() => { beforeEach(() => {
const TagVisits = createTagVisits(Mock.all<ColorGenerator>(), Mock.all<VisitsExporter>()); const TagVisits = createTagVisits(Mock.all<ColorGenerator>(), Mock.all<VisitsExporter>());
@ -28,8 +27,6 @@ describe('<TagVisits />', () => {
{...Mock.all<TagVisitsProps>()} {...Mock.all<TagVisitsProps>()}
{...Mock.of<MercureBoundProps>({ mercureInfo: {} })} {...Mock.of<MercureBoundProps>({ mercureInfo: {} })}
getTagVisits={getTagVisitsMock} getTagVisits={getTagVisitsMock}
match={match}
history={history}
tagVisits={Mock.of<TagVisitsStats>({ loading: true, visits: [] })} tagVisits={Mock.of<TagVisitsStats>({ loading: true, visits: [] })}
cancelGetTagVisits={() => {}} cancelGetTagVisits={() => {}}
/>, />,

View file

@ -1,6 +1,8 @@
import { shallow, ShallowWrapper } from 'enzyme'; import { shallow, ShallowWrapper } from 'enzyme';
import { Button, Progress } from 'reactstrap'; import { Button, Progress } from 'reactstrap';
import { sum } from 'ramda';
import { Mock } from 'ts-mockery'; import { Mock } from 'ts-mockery';
import { Route } from 'react-router-dom';
import VisitStats from '../../src/visits/VisitsStats'; import VisitStats from '../../src/visits/VisitsStats';
import Message from '../../src/utils/Message'; import Message from '../../src/utils/Message';
import { Visit, VisitsInfo } from '../../src/visits/types'; import { Visit, VisitsInfo } from '../../src/visits/types';
@ -12,7 +14,7 @@ import { SelectedServer } from '../../src/servers/data';
import { SortableBarChartCard } from '../../src/visits/charts/SortableBarChartCard'; import { SortableBarChartCard } from '../../src/visits/charts/SortableBarChartCard';
import { DoughnutChartCard } from '../../src/visits/charts/DoughnutChartCard'; import { DoughnutChartCard } from '../../src/visits/charts/DoughnutChartCard';
describe('<VisitStats />', () => { describe('<VisitsStats />', () => {
const visits = [ Mock.all<Visit>(), Mock.all<Visit>(), Mock.all<Visit>() ]; const visits = [ Mock.all<Visit>(), Mock.all<Visit>(), Mock.all<Visit>() ];
let wrapper: ShallowWrapper; let wrapper: ShallowWrapper;
@ -25,7 +27,6 @@ describe('<VisitStats />', () => {
getVisits={getVisitsMock} getVisits={getVisitsMock}
visitsInfo={Mock.of<VisitsInfo>(visitsInfo)} visitsInfo={Mock.of<VisitsInfo>(visitsInfo)}
cancelGetVisits={() => {}} cancelGetVisits={() => {}}
baseUrl={''}
settings={Mock.all<Settings>()} settings={Mock.all<Settings>()}
exportCsv={exportCsv} exportCsv={exportCsv}
selectedServer={Mock.all<SelectedServer>()} selectedServer={Mock.all<SelectedServer>()}
@ -76,18 +77,27 @@ describe('<VisitStats />', () => {
it('renders expected amount of charts', () => { it('renders expected amount of charts', () => {
const wrapper = createComponent({ loading: false, error: false, visits }); const wrapper = createComponent({ loading: false, error: false, visits });
const charts = wrapper.find(DoughnutChartCard); const total = sum(wrapper.find(Route).map((element) => {
const sortableCharts = wrapper.find(SortableBarChartCard); const ElementComponents = () => element.prop('element');
const lineChart = wrapper.find(LineChartCard); // @ts-expect-error Wrapped element
const table = wrapper.find(VisitsTable); const wrappedElement = shallow(<ElementComponents />);
expect(charts.length + sortableCharts.length + lineChart.length).toEqual(6); const charts = wrappedElement.find(DoughnutChartCard);
expect(table).toHaveLength(1); const sortableCharts = wrappedElement.find(SortableBarChartCard);
const lineChart = wrappedElement.find(LineChartCard);
const table = wrappedElement.find(VisitsTable);
return charts.length + sortableCharts.length + lineChart.length + table.length;
}));
expect(total).toEqual(7);
}); });
it('holds the map button content generator on cities chart extraHeaderContent', () => { it('holds the map button content generator on cities chart extraHeaderContent', () => {
const wrapper = createComponent({ loading: false, error: false, visits }); const wrapper = createComponent({ loading: false, error: false, visits });
const citiesChart = wrapper.find(SortableBarChartCard).find('[title="Cities"]'); const ElementComponent = () => wrapper.find(Route).findWhere((element) => element.prop('path') === 'by-location')
.prop('element');
const citiesChart = shallow(<ElementComponent />).find(SortableBarChartCard).find('[title="Cities"]');
const extraHeaderContent = citiesChart.prop('extraHeaderContent'); const extraHeaderContent = citiesChart.prop('extraHeaderContent');
expect(extraHeaderContent).toHaveLength(1); expect(extraHeaderContent).toHaveLength(1);