Compare commits

..

22 Commits

Author SHA1 Message Date
Alejandro Celaya
e9cef8a029 Merge pull request #375 from acelaya-forks/feature/servers-import-error
Feature/servers import error
2020-12-30 21:04:59 +01:00
Alejandro Celaya
e577eb48d6 Changed env for github workflows from ubuntu-latest to ubuntu-20.04 2020-12-30 20:58:36 +01:00
Alejandro Celaya
d08a69954a Updated changelog 2020-12-30 20:53:14 +01:00
Alejandro Celaya
fe81bfccef Fixed importing servers in android due to wrong mime type 2020-12-30 20:52:05 +01:00
Alejandro Celaya
4869435aca Changed linting order 2020-12-30 20:10:37 +01:00
Alejandro Celaya
0822cebb10 Merge pull request #374 from acelaya-forks/feature/responsive-table
Feature/responsive table
2020-12-30 20:09:42 +01:00
Alejandro Celaya
01a18f2342 Updated changelog 2020-12-30 20:05:53 +01:00
Alejandro Celaya
a22274f382 Increased breakpoint in which short URLs table collapses 2020-12-30 20:05:04 +01:00
Alejandro Celaya
c0098ac7fd Merge pull request #373 from acelaya-forks/feature/ui-fixes
Fixed minor UI glitches in visits section
2020-12-30 19:52:24 +01:00
Alejandro Celaya
ba5a99dc2a Fixed minor UI glitches in visits section 2020-12-30 19:48:02 +01:00
Alejandro Celaya
1927ad2d3a Merge pull request #370 from acelaya-forks/feature/stryker-updates
Updated stryker
2020-12-25 19:13:57 +01:00
Alejandro Celaya
0356a0204d Updated stryker 2020-12-25 19:10:35 +01:00
Alejandro Celaya
3bf64bee1e Merge pull request #369 from acelaya-forks/feature/consistent-dropdowns
Feature/consistent dropdowns
2020-12-25 11:25:48 +01:00
Alejandro Celaya
da484374a1 Renamed Dropdnown.scss to DropdownBtn.scss for consistency with component 2020-12-25 11:21:39 +01:00
Alejandro Celaya
7b9447b717 Updated changelog 2020-12-25 11:17:57 +01:00
Alejandro Celaya
e583eb2759 Ensured sorting dropdown for short URLs is not enclosed inside card 2020-12-25 11:15:49 +01:00
Alejandro Celaya
93b4de60f6 Improved sorting dropdown to display order field and order dir 2020-12-25 11:06:10 +01:00
Alejandro Celaya
16f4f7eac8 Reused dropdown-btn styles in sorting dropdown 2020-12-25 10:54:49 +01:00
Alejandro Celaya
90d4fe72db Renamed Dropdown component to DropdownBtn 2020-12-25 10:43:36 +01:00
Alejandro Celaya
e1298cfa81 Created Dropdown test 2020-12-25 10:39:54 +01:00
Alejandro Celaya
6be3a1223f Created common Dropdown component for style consistency 2020-12-25 10:29:25 +01:00
Alejandro Celaya
81d24432a9 Updated app gif 2020-12-24 10:58:59 +01:00
29 changed files with 350 additions and 448 deletions

View File

@@ -9,7 +9,7 @@ on:
jobs:
lint:
continue-on-error: true
runs-on: ubuntu-latest
runs-on: ubuntu-20.04
steps:
- name: Checkout code
uses: actions/checkout@v2
@@ -21,7 +21,7 @@ jobs:
- run: npm run lint
unit-tests:
runs-on: ubuntu-latest
runs-on: ubuntu-20.04
steps:
- name: Checkout code
uses: actions/checkout@v2
@@ -38,7 +38,7 @@ jobs:
mutation-tests:
continue-on-error: true
runs-on: ubuntu-latest
runs-on: ubuntu-20.04
steps:
- name: Checkout code
uses: actions/checkout@v2
@@ -52,7 +52,7 @@ jobs:
- run: npm run mutate -- --mutate=$(git diff origin/main --name-only | grep -E 'src\/(.*).(ts|tsx)$' | paste -sd ",")
build-docker-image:
runs-on: ubuntu-latest
runs-on: ubuntu-20.04
steps:
- name: Checkout code
uses: actions/checkout@v2

View File

@@ -9,7 +9,7 @@ on:
jobs:
build:
runs-on: ubuntu-latest
runs-on: ubuntu-20.04
steps:
- name: Checkout code
uses: actions/checkout@v2

View File

@@ -7,7 +7,7 @@ on:
jobs:
build:
runs-on: ubuntu-latest
runs-on: ubuntu-20.04
steps:
- name: Checkout code
uses: actions/checkout@v2

View File

@@ -4,6 +4,27 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org).
## [3.0.1] - 2020-12-30
### Added
* *Nothing*
### Changed
* *Nothing*
### Deprecated
* *Nothing*
### Removed
* *Nothing*
### Fixed
* [#364](https://github.com/shlinkio/shlink-web-client/issues/364) Fixed all dropdowns so that they are consistently styled.
* [#366](https://github.com/shlinkio/shlink-web-client/issues/366) Fixed text in visits menu jumping to next line in some tablet resolutions.
* [#367](https://github.com/shlinkio/shlink-web-client/issues/367) Removed conflicting overflow in visits table for mobile devices.
* [#365](https://github.com/shlinkio/shlink-web-client/issues/365) Fixed weird rendering of short URLs list in tablets.
* [#372](https://github.com/shlinkio/shlink-web-client/issues/372) Fixed importing servers in Android devices.
## [3.0.0] - 2020-12-22
### Added
* [#340](https://github.com/shlinkio/shlink-web-client/issues/340) Added new "overview" page, showing basic information of the active server.

384
package-lock.json generated
View File

@@ -3810,12 +3810,12 @@
}
},
"@stryker-mutator/api": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/@stryker-mutator/api/-/api-4.1.2.tgz",
"integrity": "sha512-Oq4RO+FEhcaJciCwM2HuYJaKfnyijmQEGkxCrNK2y5NVTF7lIfTZblLuxEh6+QmTnxuagRxpA+kfgj8j4tmpcw==",
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/@stryker-mutator/api/-/api-4.3.1.tgz",
"integrity": "sha512-2eXVXh7yhRML4a6tymx5WU8KmA1BNemIqx7aWnh8Mqx58WSTBfbyipm4JsS7/mzrDLQAguQmqTqsdNJlMkr1eQ==",
"dev": true,
"requires": {
"mutation-testing-report-schema": "~1.4.3",
"mutation-testing-report-schema": "~1.5.2",
"surrial": "~2.0.2",
"tslib": "~2.0.0"
},
@@ -3829,18 +3829,18 @@
}
},
"@stryker-mutator/core": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/@stryker-mutator/core/-/core-4.1.2.tgz",
"integrity": "sha512-3PgOHHy4IQNP4Gd1yBrLXJQ7CY73Xepzb+LAg4Wdp8pMcjouzTKE+PL+Wjq5AkhF6DjgwF5deCk94GpbikKoPQ==",
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/@stryker-mutator/core/-/core-4.3.1.tgz",
"integrity": "sha512-QjLaO7tvIOlKou6D1iOys46fmbJM42Ps2Wn5hP7G2cBZHB3DgjfYtCSgwF/fgoFybHPukI/8O/2gm302N/jgow==",
"dev": true,
"requires": {
"@stryker-mutator/api": "4.1.2",
"@stryker-mutator/instrumenter": "4.1.2",
"@stryker-mutator/util": "4.1.2",
"@stryker-mutator/api": "4.3.1",
"@stryker-mutator/instrumenter": "4.3.1",
"@stryker-mutator/util": "4.3.1",
"ajv": "~6.12.0",
"chalk": "~4.1.0",
"commander": "~6.2.0",
"execa": "~4.0.2",
"execa": "~5.0.0",
"file-url": "~3.0.0",
"get-port": "~5.0.0",
"glob": "~7.1.2",
@@ -3850,8 +3850,8 @@
"log4js": "~6.2.1",
"minimatch": "~3.0.4",
"mkdirp": "~1.0.3",
"mutation-testing-elements": "~1.4.3",
"mutation-testing-metrics": "~1.4.3",
"mutation-testing-elements": "~1.5.2",
"mutation-testing-metrics": "~1.5.2",
"npm-run-path": "~4.0.1",
"progress": "~2.0.0",
"rimraf": "~3.0.0",
@@ -3861,70 +3861,9 @@
"tree-kill": "~1.2.2",
"tslib": "~2.0.0",
"typed-inject": "~3.0.0",
"typed-rest-client": "~1.7.1"
"typed-rest-client": "~1.8.0"
},
"dependencies": {
"ajv": {
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"dev": true,
"requires": {
"fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0",
"json-schema-traverse": "^0.4.1",
"uri-js": "^4.2.2"
}
},
"ansi-regex": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz",
"integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==",
"dev": true
},
"ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dev": true,
"requires": {
"color-convert": "^2.0.1"
}
},
"chalk": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz",
"integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==",
"dev": true,
"requires": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
}
},
"cli-cursor": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz",
"integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==",
"dev": true,
"requires": {
"restore-cursor": "^3.1.0"
}
},
"cli-width": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz",
"integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==",
"dev": true
},
"color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dev": true,
"requires": {
"color-name": "~1.1.4"
}
},
"cross-spawn": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
@@ -3937,77 +3876,32 @@
}
},
"execa": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/execa/-/execa-4.0.3.tgz",
"integrity": "sha512-WFDXGHckXPWZX19t1kCsXzOpqX9LWYNqn4C+HqZlk/V0imTkzJZqf87ZBhvpHaftERYknpk0fjSylnXVlVgI0A==",
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/execa/-/execa-5.0.0.tgz",
"integrity": "sha512-ov6w/2LCiuyO4RLYGdpFGjkcs0wMTgGE8PrkTHikeUy5iJekXyPIKUjifk5CsE0pt7sMCrMZ3YNqoCj6idQOnQ==",
"dev": true,
"requires": {
"cross-spawn": "^7.0.0",
"get-stream": "^5.0.0",
"human-signals": "^1.1.1",
"cross-spawn": "^7.0.3",
"get-stream": "^6.0.0",
"human-signals": "^2.1.0",
"is-stream": "^2.0.0",
"merge-stream": "^2.0.0",
"npm-run-path": "^4.0.0",
"onetime": "^5.1.0",
"signal-exit": "^3.0.2",
"npm-run-path": "^4.0.1",
"onetime": "^5.1.2",
"signal-exit": "^3.0.3",
"strip-final-newline": "^2.0.0"
}
},
"fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
"dev": true
},
"figures": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz",
"integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==",
"dev": true,
"requires": {
"escape-string-regexp": "^1.0.5"
}
},
"get-stream": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz",
"integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==",
"dev": true,
"requires": {
"pump": "^3.0.0"
}
},
"has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.0.tgz",
"integrity": "sha512-A1B3Bh1UmL0bidM/YX2NsCOTnGJePL9rO/M+Mw3m9f2gUpfokS0hi5Eah0WSUEWZdZhIZtMjkIYS7mDfOqNHbg==",
"dev": true
},
"inquirer": {
"version": "7.3.3",
"resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.3.3.tgz",
"integrity": "sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA==",
"dev": true,
"requires": {
"ansi-escapes": "^4.2.1",
"chalk": "^4.1.0",
"cli-cursor": "^3.1.0",
"cli-width": "^3.0.0",
"external-editor": "^3.0.3",
"figures": "^3.0.0",
"lodash": "^4.17.19",
"mute-stream": "0.0.8",
"run-async": "^2.4.0",
"rxjs": "^6.6.0",
"string-width": "^4.1.0",
"strip-ansi": "^6.0.0",
"through": "^2.3.6"
}
},
"is-fullwidth-code-point": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
"human-signals": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz",
"integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==",
"dev": true
},
"is-stream": {
@@ -4016,12 +3910,6 @@
"integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==",
"dev": true
},
"lodash": {
"version": "4.17.20",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz",
"integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==",
"dev": true
},
"mimic-fn": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
@@ -4034,12 +3922,6 @@
"integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
"dev": true
},
"mute-stream": {
"version": "0.0.8",
"resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz",
"integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==",
"dev": true
},
"npm-run-path": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz",
@@ -4064,16 +3946,6 @@
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
"dev": true
},
"restore-cursor": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz",
"integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==",
"dev": true,
"requires": {
"onetime": "^5.1.0",
"signal-exit": "^3.0.2"
}
},
"rimraf": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
@@ -4083,12 +3955,6 @@
"glob": "^7.1.3"
}
},
"run-async": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz",
"integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==",
"dev": true
},
"shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@@ -4104,41 +3970,18 @@
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
"dev": true
},
"signal-exit": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz",
"integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==",
"dev": true
},
"source-map": {
"version": "0.7.3",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz",
"integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==",
"dev": true
},
"string-width": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz",
"integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==",
"dev": true,
"requires": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^6.0.0"
}
},
"strip-ansi": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz",
"integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==",
"dev": true,
"requires": {
"ansi-regex": "^5.0.0"
}
},
"supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"dev": true,
"requires": {
"has-flag": "^4.0.0"
}
},
"tslib": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.3.tgz",
@@ -4157,9 +4000,9 @@
}
},
"@stryker-mutator/instrumenter": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/@stryker-mutator/instrumenter/-/instrumenter-4.1.2.tgz",
"integrity": "sha512-uPmEPiQ2iMOy/ekxMBOSTqcu3bCjOaD0cCi/VaUqgkuCHUftfFUjk0TP+6whL26QKGfi4Zm9WzNKvswOgmB9yw==",
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/@stryker-mutator/instrumenter/-/instrumenter-4.3.1.tgz",
"integrity": "sha512-NaYopLzD7VrwsryeNDrDEZ3YLXNZXmzQn6A4PukX7t1ETc+4YW0cj0q57IN0UydnYLCEhj6MHmxZAuUZmvTL4Q==",
"dev": true,
"requires": {
"@babel/core": "~7.12.3",
@@ -4169,62 +4012,21 @@
"@babel/plugin-proposal-decorators": "~7.12.1 ",
"@babel/plugin-proposal-private-methods": "^7.12.1",
"@babel/preset-typescript": "~7.12.1 ",
"@stryker-mutator/api": "4.1.2",
"@stryker-mutator/util": "4.1.2",
"@stryker-mutator/api": "4.3.1",
"@stryker-mutator/util": "4.3.1",
"angular-html-parser": "~1.7.0"
},
"dependencies": {
"@babel/generator": {
"version": "7.12.5",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.12.5.tgz",
"integrity": "sha512-m16TQQJ8hPt7E+OS/XVQg/7U184MLXtvuGbCdA7na61vha+ImkyyNM/9DDA0unYCVZn3ZOhng+qz48/KBOT96A==",
"dev": true,
"requires": {
"@babel/types": "^7.12.5",
"jsesc": "^2.5.1",
"source-map": "^0.5.0"
}
},
"@babel/parser": {
"version": "7.12.5",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.12.5.tgz",
"integrity": "sha512-FVM6RZQ0mn2KCf1VUED7KepYeUWoVShczewOCfm3nzoBybaih51h+sYVVGthW9M6lPByEPTQf+xm27PBdlpwmQ==",
"dev": true
},
"@babel/types": {
"version": "7.12.6",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.6.tgz",
"integrity": "sha512-hwyjw6GvjBLiyy3W0YQf0Z5Zf4NpYejUnKFcfcUhZCSffoBBp30w6wP2Wn6pk31jMYZvcOrB/1b7cGXvEoKogA==",
"dev": true,
"requires": {
"@babel/helper-validator-identifier": "^7.10.4",
"lodash": "^4.17.19",
"to-fast-properties": "^2.0.0"
}
},
"lodash": {
"version": "4.17.20",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz",
"integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==",
"dev": true
},
"source-map": {
"version": "0.5.7",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
"integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=",
"dev": true
}
}
},
"@stryker-mutator/jest-runner": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/@stryker-mutator/jest-runner/-/jest-runner-4.1.2.tgz",
"integrity": "sha512-vxj3hUGjAqtCbZTsh+I99Q4xjmD/hXxLU0N7Ey7LXucqfvJErANbVvv+6md0YgpVbQnR9BTl59A09gZFTiEe3g==",
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/@stryker-mutator/jest-runner/-/jest-runner-4.3.1.tgz",
"integrity": "sha512-19jj9mUR92DDBHveT1xOL5i8H2TynJZ5zNR0oFuoco2p8woXNH8CfjMh42NomawD60r4KqupiKAlMAW2f7IsjQ==",
"dev": true,
"requires": {
"@stryker-mutator/api": "4.1.2",
"@stryker-mutator/util": "4.1.2",
"semver": "~6.3.0"
"@stryker-mutator/api": "4.3.1",
"@stryker-mutator/util": "4.3.1",
"semver": "~6.3.0",
"tslib": "~2.0.3"
},
"dependencies": {
"semver": {
@@ -4232,32 +4034,56 @@
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
"dev": true
},
"tslib": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.3.tgz",
"integrity": "sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ==",
"dev": true
}
}
},
"@stryker-mutator/typescript-checker": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/@stryker-mutator/typescript-checker/-/typescript-checker-4.1.2.tgz",
"integrity": "sha512-id9mS08OWj89h2lLqXJPCHAPmjoshnrqO8SO3P9k+b+NZ6J5XZf8mLT/xxWOQd1cct6zFBAItEerHpmMeQKL6Q==",
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/@stryker-mutator/typescript-checker/-/typescript-checker-4.3.1.tgz",
"integrity": "sha512-M0VAd9/TNjXwCrL3n4ofihZJN7w1Kd5eEFm4DDs6xrl21gfFspE2sPsLuD7hxToEXN5mJg8jMYXVCZWwakDmXw==",
"dev": true,
"requires": {
"@stryker-mutator/api": "4.1.2",
"@stryker-mutator/util": "4.1.2",
"@stryker-mutator/api": "4.3.1",
"@stryker-mutator/util": "4.3.1",
"semver": "~7.3.2"
},
"dependencies": {
"lru-cache": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
"dev": true,
"requires": {
"yallist": "^4.0.0"
}
},
"semver": {
"version": "7.3.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz",
"integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==",
"version": "7.3.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.4.tgz",
"integrity": "sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw==",
"dev": true,
"requires": {
"lru-cache": "^6.0.0"
}
},
"yallist": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
"dev": true
}
}
},
"@stryker-mutator/util": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/@stryker-mutator/util/-/util-4.1.2.tgz",
"integrity": "sha512-b0RuhIMPghNoZqP81aAYsqa0cL/eEkaxEdrrUyZwECIss1Ofg5+KU0SNdjw/VJWNvGsh0tE6rEyjFW+gZJ/TSg==",
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/@stryker-mutator/util/-/util-4.3.1.tgz",
"integrity": "sha512-H5XHukO22BLnn0uIl39AUIabTqNTC7rVtrz8Hb1CeGxYF5d5fn5dUGqjUprW9jsxRzt8N3o/j2ysW1ikfB24iA==",
"dev": true,
"requires": {
"lodash.flatmap": "~4.5.0"
@@ -9199,9 +9025,9 @@
}
},
"commander": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-6.2.0.tgz",
"integrity": "sha512-zP4jEKbe8SHzKJYQmq8Y9gYjtO/POJLgIdKgV7B9qNmABVFVc+ctqSX6iXh4mCpJfRBOabiZ2YKPg8ciDw6C+Q==",
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz",
"integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==",
"dev": true
},
"commondir": {
@@ -16554,9 +16380,9 @@
},
"dependencies": {
"debug": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz",
"integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==",
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz",
"integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==",
"dev": true,
"requires": {
"ms": "2.1.2"
@@ -17241,24 +17067,24 @@
"dev": true
},
"mutation-testing-elements": {
"version": "1.4.3",
"resolved": "https://registry.npmjs.org/mutation-testing-elements/-/mutation-testing-elements-1.4.3.tgz",
"integrity": "sha512-TxggiswcnNDYh7MI3yZtoAUZocbLDaz95SKHm2YjddUcgglwvc0xv/1GDQ/6UwweRhAISxXi7HpEUFBkSUJQhg==",
"version": "1.5.2",
"resolved": "https://registry.npmjs.org/mutation-testing-elements/-/mutation-testing-elements-1.5.2.tgz",
"integrity": "sha512-ugngX+MB6tnwFxirDVSFiCQdbGMLCUQ7oPqltk5QJ/pye8aCyuA90C3Gw8klHk4aRL1JR91FEupacR9CgGXC7w==",
"dev": true
},
"mutation-testing-metrics": {
"version": "1.4.3",
"resolved": "https://registry.npmjs.org/mutation-testing-metrics/-/mutation-testing-metrics-1.4.3.tgz",
"integrity": "sha512-b+5upZuuEDyY5oWb/3Jvo8kMxo9VHVP8t7ryZproBbp9bclDbS3c6kFScR6qJPFQ90NK2S7ab/cGxRHMXnNbJQ==",
"version": "1.5.2",
"resolved": "https://registry.npmjs.org/mutation-testing-metrics/-/mutation-testing-metrics-1.5.2.tgz",
"integrity": "sha512-KRMBf1tRNh1snwt+5rZu4Le+dgam+GSX+39WfzJG9k55f/+isRn4hv3dhC4Vl/XdlJ29/Z0dTSe7ZFsWBTABUA==",
"dev": true,
"requires": {
"mutation-testing-report-schema": "^1.4.3"
"mutation-testing-report-schema": "^1.5.2"
}
},
"mutation-testing-report-schema": {
"version": "1.4.3",
"resolved": "https://registry.npmjs.org/mutation-testing-report-schema/-/mutation-testing-report-schema-1.4.3.tgz",
"integrity": "sha512-+TLMYFGFL8MtcbIF0g9VG+OS5qkPPaZtvyT0v/XWrKgrbAhpnJYNsZV9/xMWuO/V8WC02mLksn/HrDnKnIVqXw==",
"version": "1.5.2",
"resolved": "https://registry.npmjs.org/mutation-testing-report-schema/-/mutation-testing-report-schema-1.5.2.tgz",
"integrity": "sha512-ad90c42vMHa0S4ZZ3e5oZOzGAWg4G8JWto9MrmDkrwInf/Dq+Q8FupCOOTqed0V9FTWqv4sl5arRlYEbedW6Ww==",
"dev": true
},
"mute-stream": {
@@ -25074,9 +24900,9 @@
"dev": true
},
"debug": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz",
"integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==",
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz",
"integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==",
"dev": true,
"requires": {
"ms": "2.1.2"
@@ -27307,9 +27133,9 @@
"dev": true
},
"typed-rest-client": {
"version": "1.7.3",
"resolved": "https://registry.npmjs.org/typed-rest-client/-/typed-rest-client-1.7.3.tgz",
"integrity": "sha512-CwTpx/TkRHGZoHkJhBcp4X8K3/WtlzSHVQR0OIFnt10j4tgy4ypgq/SrrgVpA1s6tAL49Q6J3R5C0Cgfh2ddqA==",
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/typed-rest-client/-/typed-rest-client-1.8.0.tgz",
"integrity": "sha512-Nu1MrdH6ECrRW5gHoRAdubgCs4oH6q5/J76jsEC8bVDfvVoVPkigukPalhMHPwb7ZvpsZqPptd5zpt/QdtrdBw==",
"dev": true,
"requires": {
"qs": "^6.9.1",

View File

@@ -6,7 +6,7 @@
"repository": "https://github.com/shlinkio/shlink-web-client",
"license": "MIT",
"scripts": {
"lint": "npm run lint:js && npm run lint:css",
"lint": "npm run lint:css && npm run lint:js",
"lint:js": "eslint --ext .js,.ts,.tsx src test scripts config",
"lint:js:fix": "npm run lint:js -- --fix",
"lint:css": "stylelint src/*.scss src/**/*.scss",
@@ -64,9 +64,9 @@
"@babel/plugin-proposal-nullish-coalescing-operator": "^7.12.1",
"@babel/plugin-proposal-optional-chaining": "^7.12.1",
"@shlinkio/eslint-config-js-coding-standard": "~1.1.0",
"@stryker-mutator/core": "^4.1.2",
"@stryker-mutator/jest-runner": "^4.1.2",
"@stryker-mutator/typescript-checker": "^4.1.2",
"@stryker-mutator/core": "^4.3.1",
"@stryker-mutator/jest-runner": "^4.3.1",
"@stryker-mutator/typescript-checker": "^4.3.1",
"@svgr/webpack": "^5.4.0",
"@types/chart.js": "^2.9.27",
"@types/classnames": "^2.2.11",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 MiB

After

Width:  |  Height:  |  Size: 2.3 MiB

View File

@@ -1,31 +1,12 @@
@import '../utils/mixins/vertical-align';
.domains-dropdown__toggle-btn.domains-dropdown__toggle-btn,
.domains-dropdown__toggle-btn.domains-dropdown__toggle-btn:hover,
.domains-dropdown__toggle-btn.domains-dropdown__toggle-btn:active {
text-align: left;
color: #6c757d;
border-color: #ced4da;
background-color: white;
}
.domains-dropdown__toggle-btn--active.domains-dropdown__toggle-btn--active,
.domains-dropdown__toggle-btn--active.domains-dropdown__toggle-btn--active:hover,
.domains-dropdown__toggle-btn--active.domains-dropdown__toggle-btn--active:active {
color: #495057;
color: #495057 !important;
}
.domains-dropdown__back-btn.domains-dropdown__back-btn,
.domains-dropdown__back-btn.domains-dropdown__back-btn:hover {
border-color: #ced4da;
}
.domains-dropdown__toggle-btn.domains-dropdown__toggle-btn::after {
right: .75rem;
@include vertical-align();
}
.domains-dropdown__menu {
width: 100%;
}

View File

@@ -1,20 +1,10 @@
import { useEffect } from 'react';
import {
Button,
Dropdown,
DropdownItem,
DropdownMenu,
DropdownToggle,
Input,
InputGroup,
InputGroupAddon,
UncontrolledTooltip,
} from 'reactstrap';
import { Button, DropdownItem, Input, InputGroup, InputGroupAddon, UncontrolledTooltip } from 'reactstrap';
import { InputProps } from 'reactstrap/lib/Input';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faUndo } from '@fortawesome/free-solid-svg-icons';
import { isEmpty, pipe } from 'ramda';
import classNames from 'classnames';
import { DropdownBtn } from '../utils/DropdownBtn';
import { useToggle } from '../utils/helpers/hooks';
import { DomainsList } from './reducers/domainsList';
import './DomainSelector.scss';
@@ -31,7 +21,6 @@ interface DomainSelectorConnectProps extends DomainSelectorProps {
export const DomainSelector = ({ listDomains, value, domainsList, onChange }: DomainSelectorConnectProps) => {
const [ inputDisplayed,, showInput, hideInput ] = useToggle();
const [ isDropdownOpen, toggleDropdown ] = useToggle();
const { domains } = domainsList;
const valueIsEmpty = isEmpty(value);
const unselectDomain = () => onChange('');
@@ -63,33 +52,24 @@ export const DomainSelector = ({ listDomains, value, domainsList, onChange }: Do
</InputGroupAddon>
</InputGroup>
) : (
<Dropdown isOpen={isDropdownOpen} toggle={toggleDropdown}>
<DropdownToggle
caret
className={classNames(
'domains-dropdown__toggle-btn btn-block',
{ 'domains-dropdown__toggle-btn--active': !valueIsEmpty },
)}
>
{valueIsEmpty && <>Domain</>}
{!valueIsEmpty && <>Domain: {value}</>}
</DropdownToggle>
<DropdownMenu className="domains-dropdown__menu">
{domains.map(({ domain, isDefault }) => (
<DropdownItem
key={domain}
active={value === domain || isDefault && valueIsEmpty}
onClick={() => onChange(domain)}
>
{domain}
{isDefault && <span className="float-right text-muted">default</span>}
</DropdownItem>
))}
<DropdownItem divider />
<DropdownItem onClick={pipe(unselectDomain, showInput)}>
<i>New domain</i>
<DropdownBtn
text={valueIsEmpty ? 'Domain' : `Domain: ${value}`}
className={!valueIsEmpty ? 'domains-dropdown__toggle-btn--active' : ''}
>
{domains.map(({ domain, isDefault }) => (
<DropdownItem
key={domain}
active={value === domain || isDefault && valueIsEmpty}
onClick={() => onChange(domain)}
>
{domain}
{isDefault && <span className="float-right text-muted">default</span>}
</DropdownItem>
</DropdownMenu>
</Dropdown>
))}
<DropdownItem divider />
<DropdownItem onClick={pipe(unselectDomain, showInput)}>
<i>New domain</i>
</DropdownItem>
</DropdownBtn>
);
};

View File

@@ -1,13 +1,19 @@
import { CsvJson } from 'csvjson';
import { ServerData } from '../data';
const CSV_MIME_TYPE = 'text/csv';
interface CsvFile extends File {
type: 'text/csv' | 'text/comma-separated-values' | 'application/csv';
}
const CSV_MIME_TYPES = [ 'text/csv', 'text/comma-separated-values', 'application/csv' ];
const isCsv = (file?: File | null): file is CsvFile => !!file && CSV_MIME_TYPES.includes(file.type);
export default class ServersImporter {
public constructor(private readonly csvjson: CsvJson, private readonly fileReaderFactory: () => FileReader) {}
public readonly importServersFromFile = async (file?: File | null): Promise<ServerData[]> => {
if (!file || file.type !== CSV_MIME_TYPE) {
if (!isCsv(file)) {
throw new Error('No file provided or file is not a CSV');
}

View File

@@ -1,12 +1,9 @@
import { FC, useEffect, useState } from 'react';
import { Card } from 'reactstrap';
import Paginator from './Paginator';
import { ShortUrlsListProps } from './ShortUrlsList';
const ShortUrls = (SearchBar: FC, ShortUrlsList: FC<ShortUrlsListProps>) => (props: ShortUrlsListProps) => {
const { match, shortUrlsList } = props;
const { match } = props;
const { page = '1', serverId = '' } = match?.params ?? {};
const { pagination } = shortUrlsList?.shortUrls ?? {};
const [ urlsListKey, setUrlsListKey ] = useState(`${serverId}_${page}`);
// Using a key on a component makes react to create a new instance every time the key changes
@@ -18,10 +15,7 @@ const ShortUrls = (SearchBar: FC, ShortUrlsList: FC<ShortUrlsListProps>) => (pro
return (
<>
<div className="form-group"><SearchBar /></div>
<Card body className="pb-1">
<ShortUrlsList {...props} key={urlsListKey} />
<Paginator paginator={pagination} serverId={serverId} />
</Card>
<ShortUrlsList {...props} key={urlsListKey} />
</>
);
};

View File

@@ -3,14 +3,16 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { head, keys, values } from 'ramda';
import { FC, useEffect, useState } from 'react';
import { RouteComponentProps } from 'react-router';
import { Card } from 'reactstrap';
import SortingDropdown from '../utils/SortingDropdown';
import { determineOrderDir, OrderDir } from '../utils/utils';
import { SelectedServer } from '../servers/data';
import { isReachableServer, SelectedServer } from '../servers/data';
import { boundToMercureHub } from '../mercure/helpers/boundToMercureHub';
import { parseQuery } from '../utils/helpers/query';
import { ShortUrlsList as ShortUrlsListState } from './reducers/shortUrlsList';
import { OrderableFields, ShortUrlsListParams, SORTABLE_FIELDS } from './reducers/shortUrlsListParams';
import { ShortUrlsTableProps } from './ShortUrlsTable';
import Paginator from './Paginator';
import './ShortUrlsList.scss';
interface RouteParams {
@@ -40,6 +42,7 @@ const ShortUrlsList = (ShortUrlsTable: FC<ShortUrlsTableProps>) => boundToMercur
orderField: orderBy && (head(keys(orderBy)) as OrderableFields),
orderDir: orderBy && head(values(orderBy)),
});
const { pagination } = shortUrlsList?.shortUrls ?? {};
const refreshList = (extraParams: ShortUrlsListParams) => listShortUrls({ ...shortUrlsListParams, ...extraParams });
const handleOrderBy = (orderField?: OrderableFields, orderDir?: OrderDir) => {
setOrder({ orderField, orderDir });
@@ -75,7 +78,7 @@ const ShortUrlsList = (ShortUrlsTable: FC<ShortUrlsTableProps>) => boundToMercur
return (
<>
<div className="d-block d-md-none mb-3">
<div className="d-block d-lg-none mb-3">
<SortingDropdown
items={SORTABLE_FIELDS}
orderField={order.orderField}
@@ -83,13 +86,16 @@ const ShortUrlsList = (ShortUrlsTable: FC<ShortUrlsTableProps>) => boundToMercur
onChange={handleOrderBy}
/>
</div>
<ShortUrlsTable
orderByColumn={orderByColumn}
renderOrderIcon={renderOrderIcon}
selectedServer={selectedServer}
shortUrlsList={shortUrlsList}
onTagClick={(tag) => refreshList({ tags: [ ...shortUrlsListParams.tags ?? [], tag ] })}
/>
<Card body className="pb-1">
<ShortUrlsTable
orderByColumn={orderByColumn}
renderOrderIcon={renderOrderIcon}
selectedServer={selectedServer}
shortUrlsList={shortUrlsList}
onTagClick={(tag) => refreshList({ tags: [ ...shortUrlsListParams.tags ?? [], tag ] })}
/>
<Paginator paginator={pagination} serverId={isReachableServer(selectedServer) ? selectedServer.id : ''} />
</Card>
</>
);
}, () => 'https://shlink.io/new-visit');

View File

@@ -1,7 +1,7 @@
@import '../utils/base';
.short-urls-table__header {
@media (max-width: $smMax) {
@media (max-width: $responsiveTableBreakpoint) {
display: none;
}
}

View File

@@ -2,7 +2,7 @@
@import '../../utils/mixins/vertical-align';
.short-urls-row {
@media (max-width: $smMax) {
@media (max-width: $responsiveTableBreakpoint) {
display: block;
margin-bottom: 10px;
border-bottom: 1px solid $lightGrey;
@@ -13,7 +13,7 @@
.short-urls-row__cell.short-urls-row__cell {
vertical-align: middle !important;
@media (max-width: $smMax) {
@media (max-width: $responsiveTableBreakpoint) {
display: block;
width: 100%;
position: relative;
@@ -22,7 +22,7 @@
&:before {
content: attr(data-th);
font-weight: bold;
font-weight: 700;
}
&:last-child {
@@ -55,9 +55,10 @@
.short-urls-row__copy-hint {
@include vertical-align(translateX(10px));
box-shadow: 0 3px 15px rgba(0, 0, 0, .25);
@media (max-width: $smMax) {
@media (max-width: $responsiveTableBreakpoint) {
@include vertical-align(translateX(calc(-100% - 20px)));
}
}

View File

@@ -0,0 +1,19 @@
@import '../utils/mixins/vertical-align';
.dropdown-btn__toggle.dropdown-btn__toggle,
.dropdown-btn__toggle.dropdown-btn__toggle:not(:disabled):not(.disabled).active,
.dropdown-btn__toggle.dropdown-btn__toggle:not(:disabled):not(.disabled):active,
.dropdown-btn__toggle.dropdown-btn__toggle:not(:disabled):not(.disabled):focus,
.dropdown-btn__toggle.dropdown-btn__toggle:not(:disabled):not(.disabled):hover,
.show > .dropdown-btn__toggle.dropdown-btn__toggle.dropdown-toggle {
color: #6c757d;
background-color: white;
text-align: left;
border-color: rgba(0, 0, 0, .125);
}
.dropdown-btn__toggle.dropdown-btn__toggle:after {
@include vertical-align();
right: .75rem;
}

22
src/utils/DropdownBtn.tsx Normal file
View File

@@ -0,0 +1,22 @@
import { FC } from 'react';
import { Dropdown, DropdownMenu, DropdownToggle } from 'reactstrap';
import { useToggle } from './helpers/hooks';
import './DropdownBtn.scss';
export interface DropdownBtnProps {
text: string;
disabled?: boolean;
className?: string;
}
export const DropdownBtn: FC<DropdownBtnProps> = ({ text, disabled = false, className = '', children }) => {
const [ isOpen, toggle ] = useToggle();
const toggleClasses = `dropdown-btn__toggle btn-block ${className}`;
return (
<Dropdown isOpen={isOpen} toggle={toggle} disabled={disabled}>
<DropdownToggle caret className={toggleClasses} color="primary">{text}</DropdownToggle>
<DropdownMenu className="w-100">{children}</DropdownMenu>
</Dropdown>
);
};

View File

@@ -28,10 +28,12 @@ export default function SortingDropdown<T extends string = string>(
<UncontrolledDropdown>
<DropdownToggle
caret
color={isButton ? 'secondary' : 'link'}
className={classNames({ 'btn-block': isButton, 'btn-sm p-0': !isButton })}
color={isButton ? 'primary' : 'link'}
className={classNames({ 'dropdown-btn__toggle btn-block': isButton, 'btn-sm p-0': !isButton })}
>
Order by
{!isButton && <>Order by</>}
{isButton && !orderField && <>Order by...</>}
{isButton && orderField && `Order by: "${items[orderField]}" - "${orderDir ?? 'DESC'}"`}
</DropdownToggle>
<DropdownMenu
right={right}

View File

@@ -7,6 +7,7 @@ $mdMax: 991px;
$lgMin: 992px;
$lgMax: 1199px;
$xlgMin: 1200px;
$responsiveTableBreakpoint: $mdMax;
// Colors
$mainColor: #4696e5;

View File

@@ -1,19 +0,0 @@
@import '../../utils/mixins/vertical-align';
.date-range-selector__btn.date-range-selector__btn,
.date-range-selector__btn.date-range-selector__btn:not(:disabled):not(.disabled).active,
.date-range-selector__btn.date-range-selector__btn:not(:disabled):not(.disabled):active,
.date-range-selector__btn.date-range-selector__btn:not(:disabled):not(.disabled):focus,
.date-range-selector__btn.date-range-selector__btn:not(:disabled):not(.disabled):hover,
.show > .date-range-selector__btn.date-range-selector__btn.dropdown-toggle {
color: #6c757d;
background-color: white;
text-align: left;
border-color: rgba(0, 0, 0, .125);
}
.date-range-selector__btn.date-range-selector__btn:after {
@include vertical-align();
right: .75rem;
}

View File

@@ -1,6 +1,6 @@
import { useState } from 'react';
import { Dropdown, DropdownItem, DropdownMenu, DropdownToggle } from 'reactstrap';
import { useToggle } from '../helpers/hooks';
import { DropdownItem } from 'reactstrap';
import { DropdownBtn } from '../DropdownBtn';
import {
DateInterval,
DateRange,
@@ -10,7 +10,6 @@ import {
rangeIsInterval,
} from './types';
import DateRangeRow from './DateRangeRow';
import './DateRangeSelector.scss';
export interface DateRangeSelectorProps {
initialDateRange?: DateInterval | DateRange;
@@ -20,9 +19,8 @@ export interface DateRangeSelectorProps {
}
export const DateRangeSelector = (
{ onDatesChange, initialDateRange, defaultText, disabled = false }: DateRangeSelectorProps,
{ onDatesChange, initialDateRange, defaultText, disabled }: DateRangeSelectorProps,
) => {
const [ isOpen, toggle ] = useToggle();
const [ activeInterval, setActiveInterval ] = useState(
rangeIsInterval(initialDateRange) ? initialDateRange : undefined,
);
@@ -41,35 +39,30 @@ export const DateRangeSelector = (
};
return (
<Dropdown isOpen={isOpen} toggle={toggle} disabled={disabled}>
<DropdownToggle caret className="date-range-selector__btn btn-block" color="primary">
{rangeOrIntervalToString(activeInterval ?? activeDateRange) ?? defaultText}
</DropdownToggle>
<DropdownMenu className="w-100">
<DropdownItem
active={activeInterval === undefined && dateRangeIsEmpty(activeDateRange)}
onClick={updateInterval(undefined)}
>
{defaultText}
</DropdownItem>
<DropdownItem divider />
{([ 'today', 'yesterday', 'last7Days', 'last30Days', 'last90Days', 'last180days', 'last365Days' ] as DateInterval[]).map(
(interval) => (
<DropdownItem key={interval} active={activeInterval === interval} onClick={updateInterval(interval)}>
{rangeOrIntervalToString(interval)}
</DropdownItem>
),
)}
<DropdownItem divider />
<DropdownItem header>Custom:</DropdownItem>
<DropdownItem text>
<DateRangeRow
{...activeDateRange}
onStartDateChange={(startDate) => updateDateRange({ ...activeDateRange, startDate })}
onEndDateChange={(endDate) => updateDateRange({ ...activeDateRange, endDate })}
/>
</DropdownItem>
</DropdownMenu>
</Dropdown>
<DropdownBtn disabled={disabled} text={rangeOrIntervalToString(activeInterval ?? activeDateRange) ?? defaultText}>
<DropdownItem
active={activeInterval === undefined && dateRangeIsEmpty(activeDateRange)}
onClick={updateInterval(undefined)}
>
{defaultText}
</DropdownItem>
<DropdownItem divider />
{([ 'today', 'yesterday', 'last7Days', 'last30Days', 'last90Days', 'last180days', 'last365Days' ] as DateInterval[]).map(
(interval) => (
<DropdownItem key={interval} active={activeInterval === interval} onClick={updateInterval(interval)}>
{rangeOrIntervalToString(interval)}
</DropdownItem>
),
)}
<DropdownItem divider />
<DropdownItem header>Custom:</DropdownItem>
<DropdownItem text>
<DateRangeRow
{...activeDateRange}
onStartDateChange={(startDate) => updateDateRange({ ...activeDateRange, startDate })}
onEndDateChange={(endDate) => updateDateRange({ ...activeDateRange, endDate })}
/>
</DropdownItem>
</DropdownBtn>
);
};

View File

@@ -13,6 +13,10 @@
color: #5d6778;
font-weight: 700;
cursor: pointer;
@media (min-width: $smMin) and (max-width: $mdMax) {
font-size: 89%;
}
}
.visits-stats__nav-link:hover {

View File

@@ -1,6 +1,6 @@
import { isEmpty, propEq, values } from 'ramda';
import { useState, useEffect, useMemo, FC } from 'react';
import { Button, Card, Nav, NavLink, Progress } from 'reactstrap';
import { Button, Card, Nav, NavLink, Progress, Row } from 'reactstrap';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faCalendarAlt, faMapMarkedAlt, faList, faChartPie } from '@fortawesome/free-solid-svg-icons';
import { IconDefinition } from '@fortawesome/fontawesome-common-types';
@@ -61,11 +61,11 @@ const highlightedVisitsToStats = (
let selectedBar: string | undefined;
const initialInterval: DateInterval = 'last30Days';
const VisitsNavLink: FC<VisitsNavLinkProps> = ({ subPath, title, icon, children }) => (
const VisitsNavLink: FC<VisitsNavLinkProps & { to: string }> = ({ subPath, title, icon, to }) => (
<NavLink
tag={RouterNavLink}
className="visits-stats__nav-link"
to={children}
to={to}
isActive={(_: null, { pathname }: Location) => pathname.endsWith(`/visits${subPath}`)}
replace
>
@@ -146,12 +146,12 @@ const VisitsStats: FC<VisitsStatsProps> = ({ children, visitsInfo, getVisits, ca
return (
<>
<Card className="visits-stats__nav p-0 overflow-hidden" body>
<Nav pills justified>
<Nav pills fill>
{Object.entries(sections).map(([ section, props ]) =>
<VisitsNavLink key={section} {...props}>{buildSectionUrl(props.subPath)}</VisitsNavLink>)}
<VisitsNavLink key={section} {...props} to={buildSectionUrl(props.subPath)} />)}
</Nav>
</Card>
<div className="row">
<Row>
<Switch>
<Route exact path={baseUrl}>
<div className="col-12 mt-4">
@@ -233,7 +233,7 @@ const VisitsStats: FC<VisitsStatsProps> = ({ children, visitsInfo, getVisits, ca
<Redirect to={baseUrl} />
</Switch>
</div>
</Row>
</>
);
};

View File

@@ -5,6 +5,7 @@
margin: 1.5rem 0 0;
position: relative;
background-color: white;
overflow-y: hidden;
}
.visits-table__header-cell {

View File

@@ -6,7 +6,7 @@ module.exports = {
tsconfigFile: 'tsconfig.json',
testRunner: 'jest',
reporters: [ 'progress', 'clear-text' ],
coverageAnalysis: 'off',
coverageAnalysis: 'perTest',
jest: {
projectType: 'custom',
config: jestConfig,

View File

@@ -1,9 +1,10 @@
import { shallow, ShallowWrapper } from 'enzyme';
import { Mock } from 'ts-mockery';
import { DropdownItem, DropdownMenu, InputGroup } from 'reactstrap';
import { DropdownItem, InputGroup } from 'reactstrap';
import { DomainSelector } from '../../src/domains/DomainSelector';
import { DomainsList } from '../../src/domains/reducers/domainsList';
import { ShlinkDomain } from '../../src/api/types';
import { DropdownBtn } from '../../src/utils/DropdownBtn';
describe('<DomainSelector />', () => {
let wrapper: ShallowWrapper;
@@ -23,7 +24,7 @@ describe('<DomainSelector />', () => {
it('shows dropdown by default', () => {
const input = wrapper.find(InputGroup);
const dropdown = wrapper.find(DropdownMenu);
const dropdown = wrapper.find(DropdownBtn);
expect(input).toHaveLength(0);
expect(dropdown).toHaveLength(1);
@@ -33,10 +34,10 @@ describe('<DomainSelector />', () => {
it('allows to toggle between dropdown and input', () => {
wrapper.find(DropdownItem).last().simulate('click');
expect(wrapper.find(InputGroup)).toHaveLength(1);
expect(wrapper.find(DropdownMenu)).toHaveLength(0);
expect(wrapper.find(DropdownBtn)).toHaveLength(0);
wrapper.find('.domains-dropdown__back-btn').simulate('click');
expect(wrapper.find(InputGroup)).toHaveLength(0);
expect(wrapper.find(DropdownMenu)).toHaveLength(1);
expect(wrapper.find(DropdownBtn)).toHaveLength(1);
});
});

View File

@@ -29,8 +29,12 @@ describe('ServersImporter', () => {
);
});
it('reads file when a CSV is provided', async () => {
await importer.importServersFromFile(Mock.of<File>({ type: 'text/csv' }));
it.each([
[ 'text/csv' ],
[ 'text/comma-separated-values' ],
[ 'application/csv' ],
])('reads file when a CSV is provided', async (type) => {
await importer.importServersFromFile(Mock.of<File>({ type }));
expect(readAsText).toHaveBeenCalledTimes(1);
expect(toObject).toHaveBeenCalledTimes(1);

View File

@@ -1,7 +1,6 @@
import { shallow, ShallowWrapper } from 'enzyme';
import { Mock } from 'ts-mockery';
import shortUrlsCreator from '../../src/short-urls/ShortUrls';
import Paginator from '../../src/short-urls/Paginator';
import { ShortUrlsListProps } from '../../src/short-urls/ShortUrlsList';
describe('<ShortUrls />', () => {
@@ -18,9 +17,8 @@ describe('<ShortUrls />', () => {
});
afterEach(() => wrapper.unmount());
it('wraps a SearchBar, ShortUrlsList as Paginator', () => {
it('wraps a SearchBar and ShortUrlsList', () => {
expect(wrapper.find(SearchBar)).toHaveLength(1);
expect(wrapper.find(ShortUrlsList)).toHaveLength(1);
expect(wrapper.find(Paginator)).toHaveLength(1);
});
});

View File

@@ -0,0 +1,41 @@
import { shallow, ShallowWrapper } from 'enzyme';
import { DropdownMenu, DropdownToggle } from 'reactstrap';
import { PropsWithChildren } from 'react';
import { DropdownBtn, DropdownBtnProps } from '../../src/utils/DropdownBtn';
describe('<DropdownBtn />', () => {
let wrapper: ShallowWrapper;
const createWrapper = (props: PropsWithChildren<DropdownBtnProps>) => {
wrapper = shallow(<DropdownBtn {...props} />);
return wrapper;
};
afterEach(() => wrapper?.unmount());
it.each([[ 'foo' ], [ 'bar' ], [ 'baz' ]])('displays provided text', (text) => {
const wrapper = createWrapper({ text });
const toggle = wrapper.find(DropdownToggle);
expect(toggle.html()).toContain(text);
});
it.each([[ 'foo' ], [ 'bar' ], [ 'baz' ]])('displays provided children', (children) => {
const wrapper = createWrapper({ text: '', children });
const menu = wrapper.find(DropdownMenu);
expect(menu.html()).toContain(children);
});
it.each([
[ undefined, 'dropdown-btn__toggle btn-block' ],
[ '', 'dropdown-btn__toggle btn-block' ],
[ 'foo', 'dropdown-btn__toggle btn-block foo' ],
[ 'bar', 'dropdown-btn__toggle btn-block bar' ],
])('includes provided classes', (className, expectedClasses) => {
const wrapper = createWrapper({ text: '', className });
const toggle = wrapper.find(DropdownToggle);
expect(toggle.prop('className')?.trim()).toEqual(expectedClasses);
});
});

View File

@@ -1,9 +1,10 @@
import { shallow, ShallowWrapper } from 'enzyme';
import { DropdownItem } from 'reactstrap';
import { DropdownItem, DropdownToggle } from 'reactstrap';
import { identity, values } from 'ramda';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faSortAmountDown as caretDownIcon } from '@fortawesome/free-solid-svg-icons';
import SortingDropdown, { SortingDropdownProps } from '../../src/utils/SortingDropdown';
import { OrderDir } from '../../src/utils/utils';
describe('<SortingDropdown />', () => {
let wrapper: ShallowWrapper;
@@ -73,4 +74,23 @@ describe('<SortingDropdown />', () => {
expect(onChange).toHaveBeenCalledTimes(1);
expect(onChange).toHaveBeenCalledWith('foo', 'DESC');
});
it.each([
[{ isButton: false }, '>Order by<' ],
[{ isButton: true }, '>Order by...<' ],
[
{ isButton: true, orderField: 'foo', orderDir: 'ASC' as OrderDir },
'Order by: &quot;Foo&quot; - &quot;ASC&quot;',
],
[
{ isButton: true, orderField: 'baz', orderDir: 'DESC' as OrderDir },
'Order by: &quot;Hello World&quot; - &quot;DESC&quot;',
],
[{ isButton: true, orderField: 'baz' }, 'Order by: &quot;Hello World&quot; - &quot;DESC&quot;' ],
])('displays expected text in toggle', (props, expectedText) => {
const wrapper = createWrapper(props);
const toggle = wrapper.find(DropdownToggle);
expect(toggle.html()).toContain(expectedText);
});
});