Compare commits

..

152 Commits

Author SHA1 Message Date
Alejandro Celaya
a46116d936 Merge pull request #699 from shlinkio/develop
Release 3.7.2
2022-08-07 18:31:24 +02:00
Alejandro Celaya
8fd419dc72 Merge pull request #698 from acelaya-forks/feature/multi-segment-slugs
Feature/multi segment slugs
2022-08-07 18:25:53 +02:00
Alejandro Celaya
18b27dbd0c Updated changelog 2022-08-07 18:21:24 +02:00
Alejandro Celaya
0c17818a24 Added support for short URLs with multi-segment slugs 2022-08-07 18:19:53 +02:00
Alejandro Celaya
b1749ee2ef Merge pull request #697 from acelaya-forks/feature/case-insensitive-search
Feature/case insensitive search
2022-08-07 13:34:23 +02:00
Alejandro Celaya
27b82c56b1 Updated changelog 2022-08-07 13:29:11 +02:00
Alejandro Celaya
f69bda351d Ensured tags, servers and domains search is case insensitive 2022-08-07 13:26:26 +02:00
Alejandro Celaya
4a92d0ff11 Merge pull request #696 from acelaya-forks/feature/fix-tests
Feature/fix tests
2022-08-07 13:14:56 +02:00
Alejandro Celaya
b37a983bde Updated changelog 2022-08-07 13:09:59 +02:00
Alejandro Celaya
97cf3b26b0 Fixed warning in ImportServersBtnTest 2022-08-07 13:07:25 +02:00
Alejandro Celaya
c490835f9b Ensured menu is displayed before asserting in DateRangeSelector test 2022-08-07 12:59:04 +02:00
Alejandro Celaya
a3ab2c6e1b Fixed invalid DOM in ManageServers test 2022-08-03 17:29:07 +02:00
Alejandro Celaya
ce5108937d Merge pull request #691 from shlinkio/dependabot/npm_and_yarn/terser-5.14.2
Bump terser from 5.12.1 to 5.14.2
2022-07-21 04:13:05 +02:00
dependabot[bot]
9164db181c Bump terser from 5.12.1 to 5.14.2
Bumps [terser](https://github.com/terser/terser) from 5.12.1 to 5.14.2.
- [Release notes](https://github.com/terser/terser/releases)
- [Changelog](https://github.com/terser/terser/blob/master/CHANGELOG.md)
- [Commits](https://github.com/terser/terser/commits)

---
updated-dependencies:
- dependency-name: terser
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-07-21 00:07:18 +00:00
Alejandro Celaya
aa14a17ad6 Merge pull request #690 from acelaya-forks/feature/fix-docker-build
Updated react-copy-to-clipboard
2022-07-19 18:48:54 +02:00
Alejandro Celaya
d67b8c0530 Recovered --force on npm ci, as some components still do not officially suport react 18 2022-07-19 18:45:18 +02:00
Alejandro Celaya
d41c1a2a52 Updated react-swipeable 2022-07-19 18:38:29 +02:00
Alejandro Celaya
3b938251d9 Updated react-datepicker 2022-07-19 18:34:48 +02:00
Alejandro Celaya
b8aa068876 Updated react-copy-to-clipboard 2022-07-19 18:28:55 +02:00
Alejandro Celaya
5d288de390 Merge pull request #689 from acelaya-forks/feature/testing-lib
Feature/testing lib
2022-07-17 12:00:07 +02:00
Alejandro Celaya
af851e708b Updated changelog 2022-07-17 11:52:07 +02:00
Alejandro Celaya
72399e7ccd Migrated VisitsTable test to react-testing-library, and removed latest references to enzyme 2022-07-17 11:50:26 +02:00
Alejandro Celaya
1ffd71e81f Migrated VisitsStats test to react testing library 2022-07-17 10:24:34 +02:00
Alejandro Celaya
d627de8e83 Migrated TagsSelector test to react testing library 2022-07-17 10:01:35 +02:00
Alejandro Celaya
fc4fdb4fc7 Migrated DropdownBtnMenu test to react testing library 2022-07-17 09:29:50 +02:00
Alejandro Celaya
126537185b Migrated DateRangeRow test to react testing library 2022-07-17 09:15:36 +02:00
Alejandro Celaya
24de0773d8 Migrated EditTagModal test to react testing library 2022-07-17 09:11:29 +02:00
Alejandro Celaya
cc77af6142 Merge pull request #687 from acelaya-forks/feature/testing-lib
Feature/testing lib
2022-07-17 08:42:52 +02:00
Alejandro Celaya
c16460af82 Migrated TagsTableRow test to react testing library 2022-07-16 10:59:18 +02:00
Alejandro Celaya
4c7bed90a3 Normalized mocks 2022-07-16 10:52:45 +02:00
Alejandro Celaya
491b2f2c07 Migrated TagsTable test to react testing library 2022-07-16 10:43:44 +02:00
Alejandro Celaya
a038f5e618 Migrated ShortUrlsRow test to react testing library 2022-07-16 10:28:14 +02:00
Alejandro Celaya
9ba74328ff Merge pull request #686 from acelaya-forks/feature/testing-lib
Feature/testing lib
2022-07-16 10:01:41 +02:00
Alejandro Celaya
90a643761a Migrated ShortUrlsRowMenu test to react testing library 2022-07-15 22:11:13 +02:00
Alejandro Celaya
6236d36372 Migrated DeleteShortUrlModal test to react testing library 2022-07-15 22:00:30 +02:00
Alejandro Celaya
065c908153 Migrated QrCodeModal test to react testing library 2022-07-15 21:42:16 +02:00
Alejandro Celaya
a14e612a38 Migrated DateRangeSelector test to react testing library 2022-07-14 22:14:42 +02:00
Alejandro Celaya
0b155b1d20 Merge pull request #685 from acelaya-forks/feature/testing-lib
Migrated SortableBarChartCard test to react testing library
2022-07-14 21:55:29 +02:00
Alejandro Celaya
9b9cfd0543 Migrated SortableBarChartCard test to react testing library 2022-07-11 18:26:52 +02:00
Alejandro Celaya
3d067371d3 Merge pull request #684 from acelaya-forks/feature/testing-lib
Feature/testing lib
2022-07-10 20:49:32 +02:00
Alejandro Celaya
381eb5a502 Migrated DropdownBtn test to react testing library 2022-07-10 20:42:58 +02:00
Alejandro Celaya
4a88f30d13 Migrated Message test to react testing library 2022-07-10 20:29:56 +02:00
Alejandro Celaya
bdf181adec Migrated PaginationDropdown test to react testing library 2022-07-10 20:11:01 +02:00
Alejandro Celaya
f97fce873b Migrated ShortUrlDetailLink test to react testing library 2022-07-10 19:58:27 +02:00
Alejandro Celaya
879017ecca Migrated ShortUrlFormCheckboxGroup test to react testing library 2022-07-10 19:51:21 +02:00
Alejandro Celaya
83150331e5 Renamed test helpers folder 2022-07-10 19:44:49 +02:00
Alejandro Celaya
7249ec4968 Merge branch 'feature/testing-lib' into develop 2022-07-09 23:19:18 +02:00
Alejandro Celaya
3ac148f7cd Merge pull request #683 from acelaya-forks/feature/testing-lib
Feature/testing lib
2022-07-09 23:18:49 +02:00
Alejandro Celaya
a73472f7e5 Removed debug line in logs 2022-07-09 23:18:09 +02:00
Alejandro Celaya
08ca59f990 Migrated DeleteTagConfirmModal test to react testing library 2022-07-09 23:13:15 +02:00
Alejandro Celaya
d07f7e757e Moved common test set-up code to helper function 2022-07-09 23:03:21 +02:00
Alejandro Celaya
cb13e82b9c Migrated ShortUrlsList test to react testing library 2022-07-09 22:37:26 +02:00
Alejandro Celaya
fd7aa570ed Merge pull request #681 from acelaya-forks/feature/testing-lib
Feature/testing lib
2022-07-08 15:38:42 +02:00
Alejandro Celaya
c00053f6e1 Migrated DateIntervalSelector test to react testing library 2022-07-08 15:11:51 +02:00
Alejandro Celaya
2e0e7f361c Migrated DateIntervalDropdownItems test to react testing library 2022-07-08 15:03:48 +02:00
Alejandro Celaya
21101d4da8 Migrated Result test to react testing library 2022-07-08 11:24:19 +02:00
Alejandro Celaya
65f739499f Migrated InfoTooltip test to react testing library 2022-07-08 11:03:58 +02:00
Alejandro Celaya
91ee4a32cd Migrated Tag test to react testing library 2022-07-08 10:48:29 +02:00
Alejandro Celaya
498668929f Merge pull request #680 from acelaya-forks/feature/testing-lib
Feature/testing lib
2022-07-07 19:06:01 +02:00
Alejandro Celaya
935b12763b Migrated TagsModeDropdown test to react testing library 2022-07-07 19:01:17 +02:00
Alejandro Celaya
28b15e4a85 Migrated TagsCards test to react testing library 2022-07-07 18:53:28 +02:00
Alejandro Celaya
6af49a9945 Migrated ShortUrlVisitsCount test to react testing library 2022-07-07 18:44:00 +02:00
Alejandro Celaya
c80ad70e3b Migrated ExportShortUrlsBtn test to react testing library 2022-07-06 18:59:30 +02:00
Alejandro Celaya
1a20065053 Migrated CreateShortUrlResult test to react testing library 2022-07-06 18:30:33 +02:00
Alejandro Celaya
edef36bae8 Merge pull request #679 from acelaya-forks/feature/testing-lib
Feature/testing lib
2022-07-05 21:12:04 +02:00
Alejandro Celaya
3cd25dc2df Migrated ShortUrlsTable test to react testing library 2022-07-05 20:46:31 +02:00
Alejandro Celaya
43840d7656 Migrated ShortUrlForm test to react testing library 2022-07-05 20:30:23 +02:00
Alejandro Celaya
a1bdb75036 Merge pull request #678 from acelaya-forks/feature/testing-lib
Migrated VisitsFilterDropdown test to react testing library
2022-07-04 16:36:20 +02:00
Alejandro Celaya
ac0107d450 Migrated VisitsFilterDropdown test to react testing library 2022-06-21 19:23:05 +02:00
Alejandro Celaya
d3d2cf72b9 Merge pull request #673 from acelaya-forks/feature/tests
Feature/tests
2022-06-20 08:06:11 +02:00
Alejandro Celaya
eb9ec4ec31 Migrated QrFormatDropdown test to react testing library 2022-06-13 21:38:39 +02:00
Alejandro Celaya
3201830b27 Migrated QrErrorCorrectionDropdown test to react testing library 2022-06-13 21:32:19 +02:00
Alejandro Celaya
58ddec6aff Merge pull request #672 from acelaya-forks/feature/rtl
Feature/rtl
2022-06-12 20:47:12 +02:00
Alejandro Celaya
59fd58b824 Migrated EditShortUrl test to react testing library 2022-06-12 20:41:40 +02:00
Alejandro Celaya
efa07f0368 Updated changelog 2022-06-12 20:19:48 +02:00
Alejandro Celaya
1dd6a8e2e4 Added proper color-schema to root element based on selected theme 2022-06-12 20:18:31 +02:00
Alejandro Celaya
bcd3fa8ce4 Merge pull request #670 from acelaya-forks/feature/rtl
Feature/rtl
2022-06-11 18:40:40 +02:00
Alejandro Celaya
6ff3cf544b Updated snapshot 2022-06-11 18:12:29 +02:00
Alejandro Celaya
ab21f923c6 Migrated OpenMapModalBtn test to react testing library 2022-06-11 18:10:08 +02:00
Alejandro Celaya
b75fd2e03a Migrated MapModal test to react testing library 2022-06-11 17:38:12 +02:00
Alejandro Celaya
ec7c7d521f Migrated TableOrderIcon test to react testing library 2022-06-11 17:28:47 +02:00
Alejandro Celaya
f9909713d9 Merge pull request #669 from acelaya-forks/feature/rtl
Feature/rtl
2022-06-11 17:12:29 +02:00
Alejandro Celaya
59087ced8a Migrated NavPills test to react testing library 2022-06-11 09:14:51 +02:00
Alejandro Celaya
84435714f5 Migrated ShortUrlVisitsHeader test to react testing library 2022-06-11 08:53:48 +02:00
Alejandro Celaya
6bd628712e Migrated TagVisitsHeader test to react testing library 2022-06-11 08:37:10 +02:00
Alejandro Celaya
997f4a6bdc Merge pull request #668 from acelaya-forks/feature/moar
Feature/moar
2022-06-10 21:42:37 +02:00
Alejandro Celaya
b1fec831c5 Migrated VisitsHeader test to react testing library 2022-06-10 21:35:42 +02:00
Alejandro Celaya
54fe849efd Migrated more tests to react testing lib 2022-06-10 21:31:13 +02:00
Alejandro Celaya
8bf1a9d023 Merge pull request #667 from acelaya-forks/feature/tests-and-more-tests
Feature/tests and more tests
2022-06-10 20:53:24 +02:00
Alejandro Celaya
07cedd0bdb Migrated DateInput test to react testing library 2022-06-10 20:48:53 +02:00
Alejandro Celaya
44a93ae556 Migrated CopyToClipboard test to react testing library 2022-06-10 20:29:42 +02:00
Alejandro Celaya
72f790b28c Merge pull request #666 from acelaya-forks/feature/tests
Migrated Paginator test to react testing library
2022-06-10 20:20:52 +02:00
Alejandro Celaya
a63f7e741a Ensured npm ci is run with --force until we get rid of enzyme 2022-06-10 20:09:38 +02:00
Alejandro Celaya
58f952df8a Migrated Paginator test to react testing library 2022-06-09 22:17:33 +02:00
Alejandro Celaya
2acd0ec95d Merge pull request #665 from acelaya-forks/feature/rtl-why-not
Feature/rtl why not
2022-06-09 07:31:26 +02:00
Alejandro Celaya
105254d053 Migrated CreateShortUrl test to react testing library 2022-06-08 18:28:16 +02:00
Alejandro Celaya
e538f2a3bb Migrated VisitsSettings test to react testing library 2022-06-08 18:24:21 +02:00
Alejandro Celaya
98ea491469 Merge pull request #664 from acelaya-forks/feature/teststs
Feature/teststs
2022-06-07 20:28:59 +02:00
Alejandro Celaya
10e50efb33 Migrated UserInterfaceSettings test to react testing library 2022-06-07 20:22:30 +02:00
Alejandro Celaya
d60023f585 Migrated TagSettings test to react testing library 2022-06-07 20:11:45 +02:00
Alejandro Celaya
c0d5feb433 Added comment 2022-06-06 22:46:53 +02:00
Alejandro Celaya
2451167296 Merge pull request #663 from acelaya-forks/feature/rtl-again
Feature/rtl again
2022-06-06 22:42:28 +02:00
Alejandro Celaya
4a70e4ecd3 Migrated ShortUrlsListSettings test to react testing library 2022-06-06 22:38:07 +02:00
Alejandro Celaya
a90c3da7b6 Migrated RealTimeUpdatesSettings test to react testing library 2022-06-06 22:23:21 +02:00
Alejandro Celaya
53e15b041d Merge pull request #662 from acelaya-forks/feature/tests-all-over
Feature/tests all over
2022-06-06 20:52:14 +02:00
Alejandro Celaya
7669254a0c Created helper function to convert mutable refs from useRef into element refs for the ref prop 2022-06-06 20:46:51 +02:00
Alejandro Celaya
b450e4093e Migrated Settings test to react testing library 2022-06-05 11:16:11 +02:00
Alejandro Celaya
a012d6206f Migrated ImportServersBtn test to react testing library 2022-06-05 11:06:26 +02:00
Alejandro Celaya
30f502a51b Migrated HighlightCard test to react testing library 2022-06-05 10:19:08 +02:00
Alejandro Celaya
4defeaf017 Merge pull request #661 from acelaya-forks/feature/here-we-go-again
Feature/here we go again
2022-06-04 19:18:18 +02:00
Alejandro Celaya
7f35fb0ada Migrated ServerError test to react testing library 2022-06-04 19:13:00 +02:00
Alejandro Celaya
cd1a926292 Migrated ServerForm test to react testing library 2022-06-04 19:02:20 +02:00
Alejandro Celaya
e46506b264 Migrated DuplicatedServersModal test to react testing library 2022-06-04 18:54:34 +02:00
Alejandro Celaya
807c5c3fb4 Merge pull request #659 from acelaya-forks/feature/moar-rtl
Feature/moar rtl
2022-06-04 10:25:17 +02:00
Alejandro Celaya
64efb1d43d Merge pull request #660 from shlinkio/dependabot/npm_and_yarn/eventsource-1.1.1
Bump eventsource from 1.1.0 to 1.1.1
2022-06-04 10:22:55 +02:00
Alejandro Celaya
49e1f82b03 Migrated ManageServersRowDropdown test to react testing library 2022-06-04 10:20:24 +02:00
Alejandro Celaya
1bd8636c19 Migrated ServersListGroup test to react testing library 2022-06-04 09:59:19 +02:00
Alejandro Celaya
cfe84e1275 Migrated ManageServersRow test to react testing library 2022-06-04 09:46:49 +02:00
Alejandro Celaya
5dda4731a0 Migrated ManageServers test to react testing library 2022-06-04 09:34:46 +02:00
dependabot[bot]
ce830ea6d3 Bump eventsource from 1.1.0 to 1.1.1
Bumps [eventsource](https://github.com/EventSource/eventsource) from 1.1.0 to 1.1.1.
- [Release notes](https://github.com/EventSource/eventsource/releases)
- [Changelog](https://github.com/EventSource/eventsource/blob/master/HISTORY.md)
- [Commits](https://github.com/EventSource/eventsource/compare/v1.1.0...v1.1.1)

---
updated-dependencies:
- dependency-name: eventsource
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-06-01 22:45:25 +00:00
Alejandro Celaya
b217b70dfe Migrated DeleteServerModal test to react testing library 2022-05-29 20:45:19 +02:00
Alejandro Celaya
ceee26ad25 Migrated Time test to react testing library 2022-05-29 20:32:34 +02:00
Alejandro Celaya
876018390d Merge pull request #658 from acelaya-forks/feature/rtl
Migrated CreateServer test to react testing library
2022-05-29 16:13:17 +02:00
Alejandro Celaya
0366f3544b Merge pull request #656 from shlinkio/dependabot/npm_and_yarn/async-2.6.4
Bump async from 2.6.3 to 2.6.4
2022-05-29 16:12:31 +02:00
Alejandro Celaya
b964ba5317 Merge pull request #643 from shlinkio/dependabot/npm_and_yarn/ini-1.3.8
Bump ini from 1.3.5 to 1.3.8
2022-05-29 16:12:00 +02:00
Alejandro Celaya
494e36c842 Migrated CreateServer test to react testing library 2022-05-29 12:18:21 +02:00
Alejandro Celaya
9c611a5b13 Merge pull request #657 from acelaya-forks/feature/mar-tests
Feature/mar tests
2022-05-28 12:58:39 +02:00
Alejandro Celaya
357c478640 Migrated LineChartCard test to react testing library 2022-05-28 12:54:33 +02:00
Alejandro Celaya
89f830d9bb Migrated DoughnutChartLegend test to react testing library 2022-05-28 12:33:50 +02:00
Alejandro Celaya
56150e8707 Migrated TagCard test to react testing library 2022-05-28 12:16:17 +02:00
Alejandro Celaya
1d60db25bd Removed all default export except for services and reducers 2022-05-28 11:16:59 +02:00
Alejandro Celaya
2cac1d9fd2 More default exports removals 2022-05-28 10:47:39 +02:00
Alejandro Celaya
e70724f058 Refactored some default exports to regular ones 2022-05-28 10:34:12 +02:00
Alejandro Celaya
27a05e55c9 Migrated TagsList to react testing library 2022-05-27 13:42:30 +02:00
Alejandro Celaya
0a0de86ecd Merge pull request #655 from shlinkio/develop
Release 3.7.1
2022-05-25 20:44:29 +02:00
dependabot[bot]
ec025b7d0f Bump async from 2.6.3 to 2.6.4
Bumps [async](https://github.com/caolan/async) from 2.6.3 to 2.6.4.
- [Release notes](https://github.com/caolan/async/releases)
- [Changelog](https://github.com/caolan/async/blob/v2.6.4/CHANGELOG.md)
- [Commits](https://github.com/caolan/async/compare/v2.6.3...v2.6.4)

---
updated-dependencies:
- dependency-name: async
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-05-25 18:34:47 +00:00
Alejandro Celaya
744cea1f11 Merge pull request #654 from acelaya-forks/feature/fix-nan
Feature/fix nan
2022-05-25 20:32:41 +02:00
Alejandro Celaya
073617b6d3 Fixed rendering of values greater than 1000 2022-05-25 20:26:34 +02:00
Alejandro Celaya
8d69945e8e Switched visits buttons order 2022-05-25 18:26:34 +02:00
Alejandro Celaya
3f34a1fb87 Updated to node 16 2022-05-25 18:13:32 +02:00
Alejandro Celaya
7bbc7250dd Merge pull request #652 from acelaya-forks/feature/and-moar-rtl
Feature/and moar rtl
2022-05-17 22:36:39 +02:00
Alejandro Celaya
63433864d3 Migrated ServersDropdown to react testing library 2022-05-17 22:31:02 +02:00
Alejandro Celaya
e53f90fc5c Migrated EditDomainRedirectsModal to react testing library 2022-05-17 22:18:01 +02:00
Alejandro Celaya
33adb08105 Merge pull request #651 from acelaya-forks/featue/rtl-more
Migrated DeleteServerButton to react testing library
2022-05-16 20:46:53 +02:00
Alejandro Celaya
4a610d182c Migrated DeleteServerButton to react testing library 2022-05-16 20:42:00 +02:00
Alejandro Celaya
8655d9be87 Merge pull request #650 from acelaya-forks/feature/esm-scripts
Feature/esm scripts
2022-05-15 08:42:28 +02:00
Alejandro Celaya
9962ddcd36 Updated changelog 2022-05-15 08:37:18 +02:00
Alejandro Celaya
e117429373 Updated replace-version script to use ESM and updated to node 16.14 2022-05-15 08:35:59 +02:00
Alejandro Celaya
dd0f5f961c Updated create-dist-file script to use ESM 2022-05-15 08:32:36 +02:00
Alejandro Celaya
f0b5505770 Merge pull request #649 from acelaya-forks/feature/downgrade-chalk
Downgraded to chalk 4
2022-05-14 17:32:16 +02:00
Alejandro Celaya
c63b4f9f21 Downgraded to chalk 4 2022-05-14 17:12:50 +02:00
dependabot[bot]
a8c6e916cf Bump ini from 1.3.5 to 1.3.8
Bumps [ini](https://github.com/npm/ini) from 1.3.5 to 1.3.8.
- [Release notes](https://github.com/npm/ini/releases)
- [Changelog](https://github.com/npm/ini/blob/main/CHANGELOG.md)
- [Commits](https://github.com/npm/ini/compare/v1.3.5...v1.3.8)

---
updated-dependencies:
- dependency-name: ini
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-05-11 19:43:12 +00:00
241 changed files with 5438 additions and 4416 deletions

View File

@@ -11,6 +11,7 @@ jobs:
ci:
uses: shlinkio/github-actions/.github/workflows/web-app-ci.yml@main
with:
node-version: 16.13
node-version: 16.15
with-mutation-tests: true
publish-coverage: true
force-install: true

View File

@@ -16,10 +16,10 @@ jobs:
- name: Use node.js
uses: actions/setup-node@v1
with:
node-version: 16.13
node-version: 16.15
- name: Build
run: |
npm ci && \
npm ci --force && \
node ./scripts/set-homepage.js /shlink-web-client/${GITHUB_HEAD_REF#refs/heads/} && \
rm src/service-worker.ts && \
npm run build

View File

@@ -14,9 +14,9 @@ jobs:
- name: Use node.js
uses: actions/setup-node@v1
with:
node-version: 16.13
node-version: 16.15
- name: Generate release assets
run: npm ci && VERSION=${GITHUB_REF#refs/tags/v} npm run build:dist
run: npm ci --force && VERSION=${GITHUB_REF#refs/tags/v} npm run build:dist
- name: Publish release with assets
uses: docker://antonyurchenko/git-release:latest
env:

View File

@@ -4,27 +4,63 @@ 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.7.0] - 2022-05-14
## [3.7.2] - 2022-08-07
### Added
* [#622](https://github.com/shlinkio/shlink-web-client/pull/622) Added support to load domain visits when consuming Shlink 3.1.0 or newer.
* [#582](https://github.com/shlinkio/shlink-web-client/pull/582) Improved filtering short URLs by tag.
Now, a new full tags selector component is available, which allows selecting any of the existing tags and also composes a toggle to filter by "any" tag or "all" tags.
* [#671](https://github.com/shlinkio/shlink-web-client/issues/671) Added proper color-scheme in root element based on selected theme.
### Changed
* [#616](https://github.com/shlinkio/shlink-web-client/pull/616) Updated to React 18.
* [#595](https://github.com/shlinkio/shlink-web-client/pull/595) Updated to react-chartjs-2 v4.1.0.
* [#594](https://github.com/shlinkio/shlink-web-client/pull/594) Updated to a new coding standard.
* [#627](https://github.com/shlinkio/shlink-web-client/pull/627) Updated to Jest 28.
* [#603](https://github.com/shlinkio/shlink-web-client/pull/603) Migrated to new and maintained dependencies to parse CSV<->JSON.
* [#610](https://github.com/shlinkio/shlink-web-client/pull/610) Migrated to a maintained coding style for CSS.
* [#619](https://github.com/shlinkio/shlink-web-client/pull/619) Introduced react testing library, to progressively replace enzyme.
* [#688](https://github.com/shlinkio/shlink-web-client/issues/688) Finalized migration from enzyme to react-testing-library.
### Deprecated
* *Nothing*
### Removed
* [#623](https://github.com/shlinkio/shlink-web-client/pull/623) Dropped support for Shlink older than 2.6.0.
* *Nothing*
### Fixed
* [#695](https://github.com/shlinkio/shlink-web-client/issues/695) Fixed some warnings in tests.
* [#693](https://github.com/shlinkio/shlink-web-client/issues/693) Fixed tags, servers and domains search to make it case-insensitive.
* [#694](https://github.com/shlinkio/shlink-web-client/issues/694) Fixed editing and loading visits on short URLs with multi-segment slugs.
## [3.7.1] - 2022-05-25
### Added
* *Nothing*
### Changed
* [#648](https://github.com/shlinkio/shlink-web-client/issues/648) Migrated some scripts to ESM and updated to chalk 5.
### Deprecated
* *Nothing*
### Removed
* *Nothing*
### Fixed
* [#653](https://github.com/shlinkio/shlink-web-client/issues/653) Fixed rendering values greater than 1000 in charts, when the browser has certain locales configured.
## [3.7.0] - 2022-05-14
### Added
* [#622](https://github.com/shlinkio/shlink-web-client/issues/622) Added support to load domain visits when consuming Shlink 3.1.0 or newer.
* [#582](https://github.com/shlinkio/shlink-web-client/issues/582) Improved filtering short URLs by tag.
Now, a new full tags selector component is available, which allows selecting any of the existing tags and also composes a toggle to filter by "any" tag or "all" tags.
### Changed
* [#616](https://github.com/shlinkio/shlink-web-client/issues/616) Updated to React 18.
* [#595](https://github.com/shlinkio/shlink-web-client/issues/595) Updated to react-chartjs-2 v4.1.0.
* [#594](https://github.com/shlinkio/shlink-web-client/issues/594) Updated to a new coding standard.
* [#627](https://github.com/shlinkio/shlink-web-client/issues/627) Updated to Jest 28.
* [#603](https://github.com/shlinkio/shlink-web-client/issues/603) Migrated to new and maintained dependencies to parse CSV<->JSON.
* [#610](https://github.com/shlinkio/shlink-web-client/issues/610) Migrated to a maintained coding style for CSS.
* [#619](https://github.com/shlinkio/shlink-web-client/issues/619) Introduced react testing library, to progressively replace enzyme.
### Deprecated
* *Nothing*
### Removed
* [#623](https://github.com/shlinkio/shlink-web-client/issues/623) Dropped support for Shlink older than 2.6.0.
### Fixed
* *Nothing*
@@ -32,19 +68,19 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
## [3.6.0] - 2022-03-17
### Added
* [#558](https://github.com/shlinkio/shlink-web-client/pull/558) Added dark text for tags where the generated background is too light, improving its legibility.
* [#570](https://github.com/shlinkio/shlink-web-client/pull/570) Added new section to load non-orphan visits all together when consuming Shlink 3.0.0.
* [#556](https://github.com/shlinkio/shlink-web-client/pull/556) Added support to filter short URLs list by "all" tags when consuming Shlink 3.0.0.
* [#549](https://github.com/shlinkio/shlink-web-client/pull/549) Allowed to export the list of short URLs as CSV.
* [#558](https://github.com/shlinkio/shlink-web-client/issues/558) Added dark text for tags where the generated background is too light, improving its legibility.
* [#570](https://github.com/shlinkio/shlink-web-client/issues/570) Added new section to load non-orphan visits all together when consuming Shlink 3.0.0.
* [#556](https://github.com/shlinkio/shlink-web-client/issues/556) Added support to filter short URLs list by "all" tags when consuming Shlink 3.0.0.
* [#549](https://github.com/shlinkio/shlink-web-client/issues/549) Allowed to export the list of short URLs as CSV.
### Changed
* [#543](https://github.com/shlinkio/shlink-web-client/pull/543) Redesigned settings section.
* [#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.
* [#448](https://github.com/shlinkio/shlink-web-client/pull/448) Updated to bootstrap v5.
* [#524](https://github.com/shlinkio/shlink-web-client/pull/524) Updated to react-router v6.
* [#576](https://github.com/shlinkio/shlink-web-client/pull/576) Updated to fontawesome v6.
* [#579](https://github.com/shlinkio/shlink-web-client/pull/579) Replaced react-color with react-colorful.
* [#564](https://github.com/shlinkio/shlink-web-client/pull/564) Updated most of the dependencies.
* [#543](https://github.com/shlinkio/shlink-web-client/issues/543) Redesigned settings section.
* [#567](https://github.com/shlinkio/shlink-web-client/issues/567) Improved Shlink 3.0.0 compatibility by checking the `INVALID_SHORT_URL_DELETION` error code when deleting short URLs.
* [#448](https://github.com/shlinkio/shlink-web-client/issues/448) Updated to bootstrap v5.
* [#524](https://github.com/shlinkio/shlink-web-client/issues/524) Updated to react-router v6.
* [#576](https://github.com/shlinkio/shlink-web-client/issues/576) Updated to fontawesome v6.
* [#579](https://github.com/shlinkio/shlink-web-client/issues/579) Replaced react-color with react-colorful.
* [#564](https://github.com/shlinkio/shlink-web-client/issues/564) Updated most of the dependencies.
### Deprecated
* *Nothing*
@@ -53,7 +89,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
* *Nothing*
### Fixed
* [#589](https://github.com/shlinkio/shlink-web-client/pull/589) Fixed alignment of shlink versions footer, by basing the logic on the presence of the sidebar instead of selected server.
* [#589](https://github.com/shlinkio/shlink-web-client/issues/589) Fixed alignment of shlink versions footer, by basing the logic on the presence of the sidebar instead of selected server.
## [3.5.1] - 2022-01-08
@@ -77,27 +113,27 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
## [3.5.0] - 2022-01-01
### Added
* [#407](https://github.com/shlinkio/shlink-web-client/pull/407) Improved how visits (short URLs, tags and orphan) are loaded, to avoid ending up in a page with "There are no visits matching current filter".
* [#407](https://github.com/shlinkio/shlink-web-client/issues/407) Improved how visits (short URLs, tags and orphan) are loaded, to avoid ending up in a page with "There are no visits matching current filter".
Now, the app will try to load visits for the configured default interval, and in parallel, it will load the latest visit.
If the resulting list for that interval is empty, it will try to infer the closest interval with visits, based on the latest visit's date, and reload visits for that interval.
* [#547](https://github.com/shlinkio/shlink-web-client/pull/547) Improved domains page, to tell which of the domains are not properly configured.
* [#547](https://github.com/shlinkio/shlink-web-client/issues/547) Improved domains page, to tell which of the domains are not properly configured.
Now, when this section is loaded, it tries to call the `GET /rest/health` endpoint for each one of the domains, and displays a warning icon on each one that failed.
The warning includes a link to the documentation, explaining what are the steps to get it fixed.
* [#506](https://github.com/shlinkio/shlink-web-client/pull/506) Improved how servers are handled, displaying a warning when creating or importing servers that already exist.
* [#535](https://github.com/shlinkio/shlink-web-client/pull/535) Allowed editing default domain redirects when consuming Shlink 2.10 or newer.
* [#531](https://github.com/shlinkio/shlink-web-client/pull/531) Added custom slug field to the basic creation form in the Overview page.
* [#537](https://github.com/shlinkio/shlink-web-client/pull/537) Allowed to customize the ordering for every list in the app that supports it, being currently tags and short URLs.
* [#542](https://github.com/shlinkio/shlink-web-client/pull/542) Added ordering for short URLs to the query, so that it is consistent with the rest of the filtering params.
* [#506](https://github.com/shlinkio/shlink-web-client/issues/506) Improved how servers are handled, displaying a warning when creating or importing servers that already exist.
* [#535](https://github.com/shlinkio/shlink-web-client/issues/535) Allowed editing default domain redirects when consuming Shlink 2.10 or newer.
* [#531](https://github.com/shlinkio/shlink-web-client/issues/531) Added custom slug field to the basic creation form in the Overview page.
* [#537](https://github.com/shlinkio/shlink-web-client/issues/537) Allowed to customize the ordering for every list in the app that supports it, being currently tags and short URLs.
* [#542](https://github.com/shlinkio/shlink-web-client/issues/542) Added ordering for short URLs to the query, so that it is consistent with the rest of the filtering params.
### Changed
* [#534](https://github.com/shlinkio/shlink-web-client/pull/534) Updated axios.
* [#538](https://github.com/shlinkio/shlink-web-client/pull/538) Switched to the `<field>-<dir>` notation in `orderBy` param for short URLs list, in preparation for Shlink v3.0.0
* [#534](https://github.com/shlinkio/shlink-web-client/issues/534) Updated axios.
* [#538](https://github.com/shlinkio/shlink-web-client/issues/538) Switched to the `<field>-<dir>` notation in `orderBy` param for short URLs list, in preparation for Shlink v3.0.0
### Deprecated
* *Nothing*

View File

@@ -1,8 +1,8 @@
FROM node:16.13-alpine as node
FROM node:16.15-alpine as node
COPY . /shlink-web-client
ARG VERSION="latest"
ENV VERSION ${VERSION}
RUN cd /shlink-web-client && npm ci && NODE_ENV=production npm run build
RUN cd /shlink-web-client && npm ci --force && NODE_ENV=production npm run build
FROM nginx:1.21-alpine
LABEL maintainer="Alejandro Celaya <alejandro@alejandrocelaya.com>"

View File

@@ -1,4 +0,0 @@
import * as util from 'util';
global.TextEncoder = util.TextEncoder;
global.TextDecoder = util.TextDecoder;

View File

@@ -1,4 +0,0 @@
import Enzyme from 'enzyme';
import Adapter from '@wojtekmaj/enzyme-adapter-react-17';
Enzyme.configure({ adapter: new Adapter() });

View File

@@ -4,4 +4,5 @@ import ResizeObserver from 'resize-observer-polyfill';
(global as any).ResizeObserver = ResizeObserver;
(global as any).scrollTo = () => {};
(global as any).prompt = () => {};
(global as any).matchMedia = (media: string) => ({ matches: false, media });

View File

@@ -3,7 +3,7 @@ version: '3'
services:
shlink_web_client_node:
container_name: shlink_web_client_node
image: node:16.13-alpine
image: node:16.15-alpine
command: /bin/sh -c "cd /home/shlink/www && npm install && npm run start"
volumes:
- ./:/home/shlink/www

View File

@@ -15,7 +15,6 @@ module.exports = {
lines: 90,
},
},
setupFiles: ['<rootDir>/config/jest/setupBeforeEnzyme.js', '<rootDir>/config/jest/setupEnzyme.js'],
setupFilesAfterEnv: ['<rootDir>/config/jest/setupTests.ts'],
testMatch: ['<rootDir>/test/**/*.test.{ts,tsx}'],
testEnvironment: 'jsdom',

1051
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -13,13 +13,14 @@
"lint:css:fix": "npm run lint:css -- --fix",
"lint:js:fix": "npm run lint:js -- --fix",
"start": "DISABLE_ESLINT_PLUGIN=true react-scripts start",
"build": "DISABLE_ESLINT_PLUGIN=true react-scripts build && node scripts/replace-version.js",
"build:dist": "npm run build && node scripts/create-dist-file.js",
"build": "DISABLE_ESLINT_PLUGIN=true react-scripts build && node scripts/replace-version.mjs",
"build:dist": "npm run build && node scripts/create-dist-file.mjs",
"build:serve": "serve -p 5000 ./build",
"test": "jest --env=jsdom --colors --verbose",
"test": "jest --env=jsdom --colors",
"test:coverage": "npm run test -- --coverage --coverageReporters=text --coverageReporters=text-summary",
"test:ci": "npm run test:coverage -- --coverageReporters=clover --ci",
"test:pretty": "npm run test:coverage -- --coverageReporters=html",
"test:verbose": "npm run test -- --verbose",
"mutate": "./node_modules/.bin/stryker run --concurrency 4 --ignoreStatic"
},
"dependencies": {
@@ -45,14 +46,14 @@
"react": "^18.1.0",
"react-chartjs-2": "^4.1.0",
"react-colorful": "^5.5.1",
"react-copy-to-clipboard": "^5.0.4",
"react-datepicker": "^4.7.0",
"react-copy-to-clipboard": "^5.1.0",
"react-datepicker": "^4.8.0",
"react-dom": "^18.1.0",
"react-external-link": "^2.0.0",
"react-leaflet": "^4.0.0",
"react-redux": "^8.0.0",
"react-router-dom": "^6.3.0",
"react-swipeable": "^6.2.0",
"react-swipeable": "^7.0.0",
"react-tag-autocomplete": "^6.3.0",
"reactstrap": "^9.0.1",
"redux": "^4.2.0",
@@ -75,7 +76,6 @@
"@testing-library/jest-dom": "^5.16.4",
"@testing-library/react": "^13.1.1",
"@testing-library/user-event": "^14.1.1",
"@types/enzyme": "^3.10.11",
"@types/jest": "^27.4.1",
"@types/json2csv": "^5.0.3",
"@types/leaflet": "^1.7.9",
@@ -88,11 +88,9 @@
"@types/react-dom": "^18.0.3",
"@types/react-tag-autocomplete": "^6.1.1",
"@types/uuid": "^8.3.4",
"@wojtekmaj/enzyme-adapter-react-17": "0.6.5",
"adm-zip": "^0.5.9",
"babel-jest": "^28.0.3",
"chalk": "^5.0.1",
"enzyme": "^3.11.0",
"eslint": "^8.12.0",
"identity-obj-proxy": "^3.0.0",
"jest": "^28.0.3",

View File

@@ -2,9 +2,9 @@
process.env.BABEL_ENV = 'production';
process.env.NODE_ENV = 'production';
const chalk = require('chalk');
const AdmZip = require('adm-zip');
const fs = require('fs');
import chalk from 'chalk';
import AdmZip from 'adm-zip';
import fs from 'fs';
function zipDist(version) {
const versionFileName = `./dist/shlink-web-client_${version}_dist.zip`;

View File

@@ -1,4 +1,4 @@
const fs = require('fs');
import fs from 'fs';
function replaceVersionPlaceholder(version) {
const staticJsFilesPath = './build/static/js';

View File

@@ -29,7 +29,7 @@ const normalizeOrderByInParams = (params: ShlinkShortUrlsListParams): ShlinkShor
return { ...rest, orderBy: orderToString(orderBy) };
};
export default class ShlinkApiClient {
export class ShlinkApiClient {
public constructor(
private readonly axios: AxiosInstance,
private readonly baseUrl: string,

View File

@@ -2,7 +2,7 @@ import { AxiosInstance } from 'axios';
import { prop } from 'ramda';
import { hasServerData, SelectedServer, ServerWithId } from '../../servers/data';
import { GetState } from '../../container/types';
import ShlinkApiClient from './ShlinkApiClient';
import { ShlinkApiClient } from './ShlinkApiClient';
const apiClients: Record<string, ShlinkApiClient> = {};
@@ -12,7 +12,7 @@ const getSelectedServerFromState = (getState: GetState): SelectedServer => prop(
export type ShlinkApiClientBuilder = (getStateOrSelectedServer: GetState | ServerWithId) => ShlinkApiClient;
const buildShlinkApiClient = (axios: AxiosInstance): ShlinkApiClientBuilder => (
export const buildShlinkApiClient = (axios: AxiosInstance): ShlinkApiClientBuilder => (
getStateOrSelectedServer: GetState | ServerWithId,
) => {
const server = isGetState(getStateOrSelectedServer)
@@ -32,5 +32,3 @@ const buildShlinkApiClient = (axios: AxiosInstance): ShlinkApiClientBuilder => (
return apiClients[clientKey];
};
export default buildShlinkApiClient;

View File

@@ -1,5 +1,5 @@
import Bottle from 'bottlejs';
import buildShlinkApiClient from './ShlinkApiClientBuilder';
import { buildShlinkApiClient } from './ShlinkApiClientBuilder';
const provideServices = (bottle: Bottle) => {
bottle.serviceFactory('buildShlinkApiClient', buildShlinkApiClient, 'axios');

View File

@@ -34,7 +34,7 @@ const AsideMenuItem: FC<AsideMenuItemProps> = ({ children, to, className, ...res
</NavLink>
);
const AsideMenu = (DeleteServerButton: FC<DeleteServerButtonProps>) => (
export const AsideMenu = (DeleteServerButton: FC<DeleteServerButtonProps>) => (
{ selectedServer, showOnMobile = false }: AsideMenuProps,
) => {
const hasId = isServerWithId(selectedServer);
@@ -89,5 +89,3 @@ const AsideMenu = (DeleteServerButton: FC<DeleteServerButtonProps>) => (
</aside>
);
};
export default AsideMenu;

View File

@@ -5,7 +5,7 @@ import { Card, Row } from 'reactstrap';
import { ExternalLink } from 'react-external-link';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faExternalLinkAlt, faPlus } from '@fortawesome/free-solid-svg-icons';
import ServersListGroup from '../servers/ServersListGroup';
import { ServersListGroup } from '../servers/ServersListGroup';
import { ServersMap } from '../servers/data';
import { ShlinkLogo } from './img/ShlinkLogo';
import './Home.scss';

View File

@@ -17,7 +17,7 @@ const VersionLink = ({ project, version }: { project: 'shlink' | 'shlink-web-cli
</ExternalLink>
);
const ShlinkVersions = ({ selectedServer, clientVersion = SHLINK_WEB_CLIENT_VERSION }: ShlinkVersionsProps) => {
export const ShlinkVersions = ({ selectedServer, clientVersion = SHLINK_WEB_CLIENT_VERSION }: ShlinkVersionsProps) => {
const normalizedClientVersion = normalizeVersion(clientVersion);
return (
@@ -29,5 +29,3 @@ const ShlinkVersions = ({ selectedServer, clientVersion = SHLINK_WEB_CLIENT_VERS
</small>
);
};
export default ShlinkVersions;

View File

@@ -1,6 +1,6 @@
import classNames from 'classnames';
import { SelectedServer } from '../servers/data';
import ShlinkVersions from './ShlinkVersions';
import { ShlinkVersions } from './ShlinkVersions';
import { Sidebar } from './reducers/sidebar';
import './ShlinkVersionsContainer.scss';
@@ -9,7 +9,7 @@ export interface ShlinkVersionsContainerProps {
sidebar: Sidebar;
}
const ShlinkVersionsContainer = ({ selectedServer, sidebar }: ShlinkVersionsContainerProps) => {
export const ShlinkVersionsContainer = ({ selectedServer, sidebar }: ShlinkVersionsContainerProps) => {
const classes = classNames('text-center', {
'shlink-versions-container--with-sidebar': sidebar.sidebarPresent,
});
@@ -20,5 +20,3 @@ const ShlinkVersionsContainer = ({ selectedServer, sidebar }: ShlinkVersionsCont
</div>
);
};
export default ShlinkVersionsContainer;

View File

@@ -4,9 +4,9 @@ import { ScrollToTop } from '../ScrollToTop';
import { MainHeader } from '../MainHeader';
import { Home } from '../Home';
import { MenuLayout } from '../MenuLayout';
import AsideMenu from '../AsideMenu';
import { AsideMenu } from '../AsideMenu';
import { ErrorHandler } from '../ErrorHandler';
import ShlinkVersionsContainer from '../ShlinkVersionsContainer';
import { ShlinkVersionsContainer } from '../ShlinkVersionsContainer';
import { ConnectDecorator } from '../../container/types';
import { withoutSelectedServer } from '../../servers/helpers/withoutSelectedServer';
import { sidebarNotPresent, sidebarPresent } from '../reducers/sidebar';

View File

@@ -1,5 +1,5 @@
import { FC, useEffect } from 'react';
import Message from '../utils/Message';
import { Message } from '../utils/Message';
import { Result } from '../utils/Result';
import { ShlinkApiError } from '../api/ShlinkApiError';
import { SimpleCard } from '../utils/SimpleCard';

View File

@@ -8,6 +8,7 @@ import {
faCircleNotch as loadingStatusIcon,
} from '@fortawesome/free-solid-svg-icons';
import { MediaMatcher } from '../../utils/types';
import { mutableRefToElementRef } from '../../utils/helpers/components';
import { DomainStatus } from '../data';
interface DomainStatusIconProps {
@@ -34,11 +35,7 @@ export const DomainStatusIcon: FC<DomainStatusIconProps> = ({ status, matchMedia
return (
<>
<span
ref={(el: HTMLSpanElement) => {
ref.current = el;
}}
>
<span ref={mutableRefToElementRef(ref)}>
{status === 'valid'
? <FontAwesomeIcon fixedWidth icon={checkIcon} className="text-muted" />
: <FontAwesomeIcon fixedWidth icon={invalidIcon} className="text-danger" />}

View File

@@ -38,7 +38,7 @@ export const EditDomainRedirectsModal: FC<EditDomainRedirectsModalProps> = (
return (
<Modal isOpen={isOpen} toggle={toggle} centered>
<form onSubmit={handleSubmit}>
<form name="domainRedirectsModal" onSubmit={handleSubmit}>
<ModalHeader toggle={toggle}>Edit redirects for <b>{domain.domain}</b></ModalHeader>
<ModalBody>
<FormGroup value={baseUrlRedirect} onChange={setBaseUrlRedirect}>

View File

@@ -65,7 +65,7 @@ export default buildReducer<DomainsList, DomainsCombinedAction>({
({ ...initialState, domains, filteredDomains: domains, defaultRedirects }),
[FILTER_DOMAINS]: (state, { searchTerm }) => ({
...state,
filteredDomains: state.domains.filter(({ domain }) => domain.toLowerCase().match(searchTerm)),
filteredDomains: state.domains.filter(({ domain }) => domain.toLowerCase().match(searchTerm.toLowerCase())),
}),
[EDIT_DOMAIN_REDIRECTS]: (state, { domain, redirects }) => ({
...state,

View File

@@ -13,6 +13,7 @@
:root {
scroll-behavior: auto;
color-scheme: var(--color-scheme);
}
html,

View File

@@ -4,7 +4,7 @@ import { Button } from 'reactstrap';
import { useNavigate } from 'react-router-dom';
import { Result } from '../utils/Result';
import { NoMenuLayout } from '../common/NoMenuLayout';
import { StateFlagTimeout, useGoBack, useToggle } from '../utils/helpers/hooks';
import { TimeoutToggle, useGoBack, useToggle } from '../utils/helpers/hooks';
import { ServerForm } from './helpers/ServerForm';
import { ImportServersBtnProps } from './helpers/ImportServersBtn';
import { ServerData, ServersMap, ServerWithId } from './data';
@@ -26,14 +26,14 @@ const ImportResult = ({ type }: { type: 'error' | 'success' }) => (
</div>
);
const CreateServer = (ImportServersBtn: FC<ImportServersBtnProps>, useStateFlagTimeout: StateFlagTimeout) => (
export const CreateServer = (ImportServersBtn: FC<ImportServersBtnProps>, useTimeoutToggle: TimeoutToggle) => (
{ servers, createServer }: CreateServerProps,
) => {
const navigate = useNavigate();
const goBack = useGoBack();
const hasServers = !!Object.keys(servers).length;
const [serversImported, setServersImported] = useStateFlagTimeout(false, SHOW_IMPORT_MSG_TIME);
const [errorImporting, setErrorImporting] = useStateFlagTimeout(false, SHOW_IMPORT_MSG_TIME);
const [serversImported, setServersImported] = useTimeoutToggle(false, SHOW_IMPORT_MSG_TIME);
const [errorImporting, setErrorImporting] = useTimeoutToggle(false, SHOW_IMPORT_MSG_TIME);
const [isConfirmModalOpen, toggleConfirmModal] = useToggle();
const [serverData, setServerData] = useState<ServerData | undefined>();
const save = () => {
@@ -77,5 +77,3 @@ const CreateServer = (ImportServersBtn: FC<ImportServersBtnProps>, useStateFlagT
</NoMenuLayout>
);
};
export default CreateServer;

View File

@@ -11,7 +11,7 @@ export type DeleteServerButtonProps = PropsWithChildren<{
textClassName?: string;
}>;
const DeleteServerButton = (DeleteServerModal: FC<DeleteServerModalProps>): FC<DeleteServerButtonProps> => (
export const DeleteServerButton = (DeleteServerModal: FC<DeleteServerModalProps>): FC<DeleteServerButtonProps> => (
{ server, className, children, textClassName },
) => {
const [isModalOpen, , showModal, hideModal] = useToggle();
@@ -27,5 +27,3 @@ const DeleteServerButton = (DeleteServerModal: FC<DeleteServerModalProps>): FC<D
</>
);
};
export default DeleteServerButton;

View File

@@ -14,7 +14,7 @@ interface DeleteServerModalConnectProps extends DeleteServerModalProps {
deleteServer: (server: ServerWithId) => void;
}
const DeleteServerModal: FC<DeleteServerModalConnectProps> = (
export const DeleteServerModal: FC<DeleteServerModalConnectProps> = (
{ server, toggle, isOpen, deleteServer, redirectHome = true },
) => {
const navigate = useNavigate();
@@ -26,7 +26,7 @@ const DeleteServerModal: FC<DeleteServerModalConnectProps> = (
return (
<Modal isOpen={isOpen} toggle={toggle} centered>
<ModalHeader toggle={toggle}><span className="text-danger">Remove server</span></ModalHeader>
<ModalHeader toggle={toggle} className="text-danger">Remove server</ModalHeader>
<ModalBody>
<p>Are you sure you want to remove <b>{server ? server.name : ''}</b>?</p>
<p>
@@ -43,5 +43,3 @@ const DeleteServerModal: FC<DeleteServerModalConnectProps> = (
</Modal>
);
};
export default DeleteServerModal;

View File

@@ -7,7 +7,7 @@ import { NoMenuLayout } from '../common/NoMenuLayout';
import { SimpleCard } from '../utils/SimpleCard';
import { SearchField } from '../utils/SearchField';
import { Result } from '../utils/Result';
import { StateFlagTimeout } from '../utils/helpers/hooks';
import { TimeoutToggle } from '../utils/helpers/hooks';
import { ImportServersBtnProps } from './helpers/ImportServersBtn';
import { ServersMap } from './data';
import { ManageServersRowProps } from './ManageServersRow';
@@ -22,16 +22,16 @@ const SHOW_IMPORT_MSG_TIME = 4000;
export const ManageServers = (
serversExporter: ServersExporter,
ImportServersBtn: FC<ImportServersBtnProps>,
useStateFlagTimeout: StateFlagTimeout,
useTimeoutToggle: TimeoutToggle,
ManageServersRow: FC<ManageServersRowProps>,
): FC<ManageServersProps> => ({ servers }) => {
const allServers = Object.values(servers);
const [serversList, setServersList] = useState(allServers);
const filterServers = (searchTerm: string) => setServersList(
allServers.filter(({ name, url }) => `${name} ${url}`.match(searchTerm)),
allServers.filter(({ name, url }) => `${name} ${url}`.toLowerCase().match(searchTerm.toLowerCase())),
);
const hasAutoConnect = serversList.some(({ autoConnect }) => !!autoConnect);
const [errorImporting, setErrorImporting] = useStateFlagTimeout(false, SHOW_IMPORT_MSG_TIME);
const [errorImporting, setErrorImporting] = useTimeoutToggle(false, SHOW_IMPORT_MSG_TIME);
useEffect(() => {
setServersList(Object.values(servers));

View File

@@ -39,7 +39,7 @@ export const ManageServersRowDropdown = (
<DropdownItem tag={Link} to={`${serverUrl}/edit`}>
<FontAwesomeIcon icon={editIcon} fixedWidth /> Edit server
</DropdownItem>
<DropdownItem onClick={() => setAutoConnect(server, !server.autoConnect)}>
<DropdownItem onClick={() => setAutoConnect(server, !isAutoConnect)}>
<FontAwesomeIcon icon={autoConnectIcon} fixedWidth /> {isAutoConnect ? 'Do not a' : 'A'}uto-connect
</DropdownItem>
<DropdownItem divider />

View File

@@ -10,7 +10,7 @@ export interface ServersDropdownProps {
selectedServer: SelectedServer;
}
const ServersDropdown = ({ servers, selectedServer }: ServersDropdownProps) => {
export const ServersDropdown = ({ servers, selectedServer }: ServersDropdownProps) => {
const serversList = values(servers);
const renderServers = () => {
@@ -46,5 +46,3 @@ const ServersDropdown = ({ servers, selectedServer }: ServersDropdownProps) => {
</UncontrolledDropdown>
);
};
export default ServersDropdown;

View File

@@ -19,7 +19,7 @@ const ServerListItem = ({ id, name }: { id: string; name: string }) => (
</ListGroupItem>
);
const ServersListGroup: FC<ServersListGroupProps> = ({ servers, children, embedded = false }) => (
export const ServersListGroup: FC<ServersListGroupProps> = ({ servers, children, embedded = false }) => (
<>
{children && <h5 className="mb-md-3">{children}</h5>}
{servers.length > 0 && (
@@ -31,5 +31,3 @@ const ServersListGroup: FC<ServersListGroupProps> = ({ servers, children, embedd
)}
</>
);
export default ServersListGroup;

View File

@@ -1,16 +1,15 @@
import { useRef, RefObject, ChangeEvent, MutableRefObject, useState, useEffect, FC, PropsWithChildren } from 'react';
import { useRef, ChangeEvent, useState, useEffect, FC, PropsWithChildren } from 'react';
import { Button, UncontrolledTooltip } from 'reactstrap';
import { complement, pipe } from 'ramda';
import { faFileUpload as importIcon } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { useToggle } from '../../utils/helpers/hooks';
import { mutableRefToElementRef } from '../../utils/helpers/components';
import { ServersImporter } from '../services/ServersImporter';
import { ServerData, ServersMap } from '../data';
import { DuplicatedServersModal } from './DuplicatedServersModal';
import './ImportServersBtn.scss';
type Ref<T> = RefObject<T> | MutableRefObject<T>;
export type ImportServersBtnProps = PropsWithChildren<{
onImport?: () => void;
onImportError?: (error: Error) => void;
@@ -21,23 +20,21 @@ export type ImportServersBtnProps = PropsWithChildren<{
interface ImportServersBtnConnectProps extends ImportServersBtnProps {
createServers: (servers: ServerData[]) => void;
servers: ServersMap;
fileRef: Ref<HTMLInputElement>;
}
const serversFiltering = (servers: ServerData[]) =>
({ url, apiKey }: ServerData) => servers.some((server) => server.url === url && server.apiKey === apiKey);
const ImportServersBtn = ({ importServersFromFile }: ServersImporter): FC<ImportServersBtnConnectProps> => ({
export const ImportServersBtn = ({ importServersFromFile }: ServersImporter): FC<ImportServersBtnConnectProps> => ({
createServers,
servers,
fileRef,
children,
onImport = () => {},
onImportError = () => {},
tooltipPlacement = 'bottom',
className = '',
}) => {
const ref = fileRef ?? useRef<HTMLInputElement>();
const ref = useRef<HTMLInputElement>();
const [serversToCreate, setServersToCreate] = useState<ServerData[] | undefined>();
const [duplicatedServers, setDuplicatedServers] = useState<ServerData[]>([]);
const [isModalOpen,, showModal, hideModal] = useToggle();
@@ -78,7 +75,13 @@ const ImportServersBtn = ({ importServersFromFile }: ServersImporter): FC<Import
You can create servers by importing a CSV file with columns <b>name</b>, <b>apiKey</b> and <b>url</b>.
</UncontrolledTooltip>
<input type="file" accept="text/csv" className="import-servers-btn__csv-select" ref={ref} onChange={onFile} />
<input
type="file"
accept="text/csv"
className="import-servers-btn__csv-select"
ref={mutableRefToElementRef(ref)}
onChange={onFile}
/>
<DuplicatedServersModal
isOpen={isModalOpen}
@@ -89,5 +92,3 @@ const ImportServersBtn = ({ importServersFromFile }: ServersImporter): FC<Import
</>
);
};
export default ImportServersBtn;

View File

@@ -1,7 +1,7 @@
import { FC } from 'react';
import { Link } from 'react-router-dom';
import Message from '../../utils/Message';
import ServersListGroup from '../ServersListGroup';
import { Message } from '../../utils/Message';
import { ServersListGroup } from '../ServersListGroup';
import { DeleteServerButtonProps } from '../DeleteServerButton';
import { isServerWithId, SelectedServer, ServersMap } from '../data';
import { NoMenuLayout } from '../../common/NoMenuLayout';

View File

@@ -1,6 +1,6 @@
import { FC, useEffect } from 'react';
import { useParams } from 'react-router-dom';
import Message from '../../utils/Message';
import { Message } from '../../utils/Message';
import { isNotFoundServer, SelectedServer } from '../data';
import { NoMenuLayout } from '../../common/NoMenuLayout';

View File

@@ -1,5 +1,5 @@
import { values } from 'ramda';
import LocalStorage from '../../utils/services/LocalStorage';
import { LocalStorage } from '../../utils/services/LocalStorage';
import { ServersMap, serverWithIdToServerData } from '../data';
import { saveCsv } from '../../utils/helpers/files';
import { JsonToCsv } from '../../utils/helpers/csvjson';

View File

@@ -1,10 +1,10 @@
import Bottle from 'bottlejs';
import CreateServer from '../CreateServer';
import ServersDropdown from '../ServersDropdown';
import DeleteServerModal from '../DeleteServerModal';
import DeleteServerButton from '../DeleteServerButton';
import { CreateServer } from '../CreateServer';
import { ServersDropdown } from '../ServersDropdown';
import { DeleteServerModal } from '../DeleteServerModal';
import { DeleteServerButton } from '../DeleteServerButton';
import { EditServer } from '../EditServer';
import ImportServersBtn from '../helpers/ImportServersBtn';
import { ImportServersBtn } from '../helpers/ImportServersBtn';
import { resetSelectedServer, selectServer } from '../reducers/selectedServer';
import { createServer, createServers, deleteServer, editServer, setAutoConnect } from '../reducers/servers';
import { fetchServers } from '../reducers/remoteServers';
@@ -25,7 +25,7 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
ManageServers,
'ServersExporter',
'ImportServersBtn',
'useStateFlagTimeout',
'useTimeoutToggle',
'ManageServersRow',
);
bottle.decorator('ManageServers', withoutSelectedServer);
@@ -36,7 +36,7 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
bottle.serviceFactory('ManageServersRowDropdown', ManageServersRowDropdown, 'DeleteServerModal');
bottle.decorator('ManageServersRowDropdown', connect(null, ['setAutoConnect']));
bottle.serviceFactory('CreateServer', CreateServer, 'ImportServersBtn', 'useStateFlagTimeout');
bottle.serviceFactory('CreateServer', CreateServer, 'ImportServersBtn', 'useTimeoutToggle');
bottle.decorator('CreateServer', withoutSelectedServer);
bottle.decorator('CreateServer', connect(['selectedServer', 'servers'], ['createServer', 'resetSelectedServer']));

View File

@@ -1,10 +1,11 @@
import { FormGroup, Input } from 'reactstrap';
import classNames from 'classnames';
import ToggleSwitch from '../utils/ToggleSwitch';
import { ToggleSwitch } from '../utils/ToggleSwitch';
import { SimpleCard } from '../utils/SimpleCard';
import { FormText } from '../utils/forms/FormText';
import { LabeledFormGroup } from '../utils/forms/LabeledFormGroup';
import { Settings } from './reducers/settings';
import { useDomId } from '../utils/helpers/hooks';
interface RealTimeUpdatesProps {
settings: Settings;
@@ -14,43 +15,48 @@ interface RealTimeUpdatesProps {
const intervalValue = (interval?: number) => (!interval ? '' : `${interval}`);
const RealTimeUpdatesSettings = (
export const RealTimeUpdatesSettings = (
{ settings: { realTimeUpdates }, toggleRealTimeUpdates, setRealTimeUpdatesInterval }: RealTimeUpdatesProps,
) => (
<SimpleCard title="Real-time updates" className="h-100">
<FormGroup>
<ToggleSwitch checked={realTimeUpdates.enabled} onChange={toggleRealTimeUpdates}>
Enable or disable real-time updates.
<FormText>
Real-time updates are currently being <b>{realTimeUpdates.enabled ? 'processed' : 'ignored'}</b>.
</FormText>
</ToggleSwitch>
</FormGroup>
<LabeledFormGroup
noMargin
label="Real-time updates frequency (in minutes):"
labelClassName={classNames('form-label', { 'text-muted': !realTimeUpdates.enabled })}
>
<Input
type="number"
min={0}
placeholder="Immediate"
disabled={!realTimeUpdates.enabled}
value={intervalValue(realTimeUpdates.interval)}
onChange={({ target }) => setRealTimeUpdatesInterval(Number(target.value))}
/>
{realTimeUpdates.enabled && (
<FormText>
{realTimeUpdates.interval !== undefined && realTimeUpdates.interval > 0 && (
<span>
Updates will be reflected in the UI every <b>{realTimeUpdates.interval}</b> minute{realTimeUpdates.interval > 1 && 's'}.
</span>
)}
{!realTimeUpdates.interval && 'Updates will be reflected in the UI as soon as they happen.'}
</FormText>
)}
</LabeledFormGroup>
</SimpleCard>
);
) => {
const inputId = useDomId();
export default RealTimeUpdatesSettings;
return (
<SimpleCard title="Real-time updates" className="h-100">
<FormGroup>
<ToggleSwitch checked={realTimeUpdates.enabled} onChange={toggleRealTimeUpdates}>
Enable or disable real-time updates.
<FormText>
Real-time updates are currently being <b>{realTimeUpdates.enabled ? 'processed' : 'ignored'}</b>.
</FormText>
</ToggleSwitch>
</FormGroup>
<LabeledFormGroup
noMargin
label="Real-time updates frequency (in minutes):"
labelClassName={classNames('form-label', { 'text-muted': !realTimeUpdates.enabled })}
id={inputId}
>
<Input
type="number"
min={0}
placeholder="Immediate"
disabled={!realTimeUpdates.enabled}
value={intervalValue(realTimeUpdates.interval)}
id={inputId}
onChange={({ target }) => setRealTimeUpdatesInterval(Number(target.value))}
/>
{realTimeUpdates.enabled && (
<FormText>
{realTimeUpdates.interval !== undefined && realTimeUpdates.interval > 0 && (
<span>
Updates will be reflected in the UI
every <b>{realTimeUpdates.interval}</b> minute{realTimeUpdates.interval > 1 && 's'}.
</span>
)}
{!realTimeUpdates.interval && 'Updates will be reflected in the UI as soon as they happen.'}
</FormText>
)}
</LabeledFormGroup>
</SimpleCard>
);
};

View File

@@ -9,7 +9,7 @@ const SettingsSections: FC<{ items: ReactNode[] }> = ({ items }) => (
</>
);
const Settings = (
export const Settings = (
RealTimeUpdates: FC,
ShortUrlCreation: FC,
ShortUrlsList: FC,
@@ -32,5 +32,3 @@ const Settings = (
</Routes>
</NoMenuLayout>
);
export default Settings;

View File

@@ -1,7 +1,7 @@
import { FC, ReactNode } from 'react';
import { DropdownItem, FormGroup } from 'reactstrap';
import { SimpleCard } from '../utils/SimpleCard';
import ToggleSwitch from '../utils/ToggleSwitch';
import { ToggleSwitch } from '../utils/ToggleSwitch';
import { DropdownBtn } from '../utils/DropdownBtn';
import { FormText } from '../utils/forms/FormText';
import { LabeledFormGroup } from '../utils/forms/LabeledFormGroup';

View File

@@ -2,7 +2,7 @@ import { FC } from 'react';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faSun, faMoon } from '@fortawesome/free-solid-svg-icons';
import { SimpleCard } from '../utils/SimpleCard';
import ToggleSwitch from '../utils/ToggleSwitch';
import { ToggleSwitch } from '../utils/ToggleSwitch';
import { changeThemeInMarkup, Theme } from '../utils/theme';
import { Settings, UiSettings } from './reducers/settings';
import './UserInterfaceSettings.scss';

View File

@@ -1,6 +1,6 @@
import Bottle from 'bottlejs';
import RealTimeUpdatesSettings from '../RealTimeUpdatesSettings';
import Settings from '../Settings';
import { RealTimeUpdatesSettings } from '../RealTimeUpdatesSettings';
import { Settings } from '../Settings';
import {
setRealTimeUpdatesInterval,
setShortUrlCreationSettings,

View File

@@ -33,7 +33,10 @@ const getInitialState = (settings?: ShortUrlCreationSettings): ShortUrlData => (
forwardQuery: settings?.forwardQuery ?? true,
});
const CreateShortUrl = (ShortUrlForm: FC<ShortUrlFormProps>, CreateShortUrlResult: FC<CreateShortUrlResultProps>) => ({
export const CreateShortUrl = (
ShortUrlForm: FC<ShortUrlFormProps>,
CreateShortUrlResult: FC<CreateShortUrlResultProps>,
) => ({
createShortUrl,
shortUrlCreationResult,
resetCreateShortUrl,
@@ -52,7 +55,6 @@ const CreateShortUrl = (ShortUrlForm: FC<ShortUrlFormProps>, CreateShortUrlResul
mode={basicMode ? 'create-basic' : 'create'}
onSave={async (data: ShortUrlData) => {
resetCreateShortUrl();
return createShortUrl(data);
}}
/>
@@ -64,5 +66,3 @@ const CreateShortUrl = (ShortUrlForm: FC<ShortUrlFormProps>, CreateShortUrlResul
</>
);
};
export default CreateShortUrl;

View File

@@ -5,17 +5,18 @@ import { faArrowLeft } from '@fortawesome/free-solid-svg-icons';
import { ExternalLink } from 'react-external-link';
import { useLocation, useParams } from 'react-router-dom';
import { SelectedServer } from '../servers/data';
import { Settings, ShortUrlCreationSettings } from '../settings/reducers/settings';
import { Settings } from '../settings/reducers/settings';
import { OptionalString } from '../utils/utils';
import { parseQuery } from '../utils/helpers/query';
import Message from '../utils/Message';
import { Message } from '../utils/Message';
import { Result } from '../utils/Result';
import { ShlinkApiError } from '../api/ShlinkApiError';
import { useGoBack, useToggle } from '../utils/helpers/hooks';
import { ShortUrlFormProps } from './ShortUrlForm';
import { ShortUrlDetail } from './reducers/shortUrlDetail';
import { EditShortUrlData, ShortUrl, ShortUrlData } from './data';
import { EditShortUrlData } from './data';
import { ShortUrlEdition } from './reducers/shortUrlEdition';
import { shortUrlDataFromShortUrl, urlDecodeShortCode } from './helpers';
interface EditShortUrlConnectProps {
settings: Settings;
@@ -26,27 +27,6 @@ interface EditShortUrlConnectProps {
editShortUrl: (shortUrl: string, domain: OptionalString, data: EditShortUrlData) => Promise<void>;
}
const getInitialState = (shortUrl?: ShortUrl, settings?: ShortUrlCreationSettings): ShortUrlData => {
const validateUrl = settings?.validateUrls ?? false;
if (!shortUrl) {
return { longUrl: '', validateUrl };
}
return {
longUrl: shortUrl.longUrl,
tags: shortUrl.tags,
title: shortUrl.title ?? undefined,
domain: shortUrl.domain ?? undefined,
validSince: shortUrl.meta.validSince ?? undefined,
validUntil: shortUrl.meta.validUntil ?? undefined,
maxVisits: shortUrl.meta.maxVisits ?? undefined,
crawlable: shortUrl.crawlable,
forwardQuery: shortUrl.forwardQuery,
validateUrl,
};
};
export const EditShortUrl = (ShortUrlForm: FC<ShortUrlFormProps>) => ({
settings: { shortUrlCreation: shortUrlCreationSettings },
selectedServer,
@@ -62,13 +42,13 @@ export const EditShortUrl = (ShortUrlForm: FC<ShortUrlFormProps>) => ({
const { saving, error: savingError, errorData: savingErrorData } = shortUrlEdition;
const { domain } = parseQuery<{ domain?: string }>(search);
const initialState = useMemo(
() => getInitialState(shortUrl, shortUrlCreationSettings),
() => shortUrlDataFromShortUrl(shortUrl, shortUrlCreationSettings),
[shortUrl, shortUrlCreationSettings],
);
const [savingSucceeded,, isSuccessful, isNotSuccessful] = useToggle();
useEffect(() => {
params.shortCode && getShortUrlDetail(params.shortCode, domain);
params.shortCode && getShortUrlDetail(urlDecodeShortCode(params.shortCode), domain);
}, []);
if (loading) {

View File

@@ -15,7 +15,7 @@ interface PaginatorProps {
currentQueryString?: string;
}
const Paginator = ({ paginator, serverId, currentQueryString = '' }: PaginatorProps) => {
export const Paginator = ({ paginator, serverId, currentQueryString = '' }: PaginatorProps) => {
const { currentPage = 0, pagesCount = 0 } = paginator ?? {};
const urlForPage = (pageNumber: NumberOrEllipsis) =>
`/server/${serverId}/list-short-urls/${pageNumber}${currentQueryString}`;
@@ -49,5 +49,3 @@ const Paginator = ({ paginator, serverId, currentQueryString = '' }: PaginatorPr
</Pagination>
);
};
export default Paginator;

View File

@@ -3,11 +3,11 @@ import { InputType } from 'reactstrap/types/lib/Input';
import { Button, FormGroup, Input, Row } from 'reactstrap';
import { cond, isEmpty, pipe, replace, trim, T } from 'ramda';
import { parseISO } from 'date-fns';
import DateInput, { DateInputProps } from '../utils/DateInput';
import { DateInput, DateInputProps } from '../utils/DateInput';
import { supportsCrawlableVisits, supportsForwardQuery } from '../utils/helpers/features';
import { SimpleCard } from '../utils/SimpleCard';
import { handleEventPreventingDefault, hasValue, OptionalString } from '../utils/utils';
import Checkbox from '../utils/Checkbox';
import { Checkbox } from '../utils/Checkbox';
import { SelectedServer } from '../servers/data';
import { TagsSelectorProps } from '../tags/helpers/TagsSelector';
import { DomainSelectorProps } from '../domains/DomainSelector';
@@ -118,7 +118,7 @@ export const ShortUrlForm = (
const showBehaviorCard = showCrawlableControl || showForwardQueryControl;
return (
<form className="short-url-form" onSubmit={submit}>
<form name="shortUrlForm" className="short-url-form" onSubmit={submit}>
{isBasicMode && basicComponents}
{!isBasicMode && (
<>

View File

@@ -11,7 +11,7 @@ import { ShlinkShortUrlsListParams } from '../api/types';
import { DEFAULT_SHORT_URLS_ORDERING, Settings } from '../settings/reducers/settings';
import { ShortUrlsList as ShortUrlsListState } from './reducers/shortUrlsList';
import { ShortUrlsTableProps } from './ShortUrlsTable';
import Paginator from './Paginator';
import { Paginator } from './Paginator';
import { useShortUrlsQuery } from './helpers/hooks';
import { ShortUrlsOrderableFields } from './data';
import { ShortUrlsFilteringProps } from './ShortUrlsFilteringBar';
@@ -23,7 +23,7 @@ interface ShortUrlsListProps {
settings: Settings;
}
const ShortUrlsList = (
export const ShortUrlsList = (
ShortUrlsTable: FC<ShortUrlsTableProps>,
ShortUrlsFilteringBar: FC<ShortUrlsFilteringProps>,
) => boundToMercureHub(({ listShortUrls, shortUrlsList, selectedServer, settings }: ShortUrlsListProps) => {
@@ -83,5 +83,3 @@ const ShortUrlsList = (
</>
);
}, () => [Topics.visits]);
export default ShortUrlsList;

View File

@@ -6,7 +6,7 @@ import { useEffect } from 'react';
import CopyToClipboard from 'react-copy-to-clipboard';
import { Tooltip } from 'reactstrap';
import { ShortUrlCreation } from '../reducers/shortUrlCreation';
import { StateFlagTimeout } from '../../utils/helpers/hooks';
import { TimeoutToggle } from '../../utils/helpers/hooks';
import { Result } from '../../utils/Result';
import './CreateShortUrlResult.scss';
import { ShlinkApiError } from '../../api/ShlinkApiError';
@@ -16,10 +16,10 @@ export interface CreateShortUrlResultProps extends ShortUrlCreation {
canBeClosed?: boolean;
}
const CreateShortUrlResult = (useStateFlagTimeout: StateFlagTimeout) => (
export const CreateShortUrlResult = (useTimeoutToggle: TimeoutToggle) => (
{ error, errorData, result, resetCreateShortUrl, canBeClosed = false }: CreateShortUrlResultProps,
) => {
const [showCopyTooltip, setShowCopyTooltip] = useStateFlagTimeout();
const [showCopyTooltip, setShowCopyTooltip] = useTimeoutToggle();
useEffect(() => {
resetCreateShortUrl();
@@ -43,7 +43,7 @@ const CreateShortUrlResult = (useStateFlagTimeout: StateFlagTimeout) => (
return (
<Result type="success" className="mt-3">
{canBeClosed && <FontAwesomeIcon icon={closeIcon} className="float-end pointer" onClick={resetCreateShortUrl} />}
<b>Great!</b> The short URL is <b>{shortUrl}</b>
<span><b>Great!</b> The short URL is <b>{shortUrl}</b></span>
<CopyToClipboard text={shortUrl} onCopy={setShowCopyTooltip}>
<button
@@ -61,5 +61,3 @@ const CreateShortUrlResult = (useStateFlagTimeout: StateFlagTimeout) => (
</Result>
);
};
export default CreateShortUrlResult;

View File

@@ -14,7 +14,7 @@ interface DeleteShortUrlModalConnectProps extends ShortUrlModalProps {
resetDeleteShortUrl: () => void;
}
const DeleteShortUrlModal = (
export const DeleteShortUrlModal = (
{ shortUrl, toggle, isOpen, shortUrlDeletion, resetDeleteShortUrl, deleteShortUrl }: DeleteShortUrlModalConnectProps,
) => {
const [inputValue, setInputValue] = useState('');
@@ -70,5 +70,3 @@ const DeleteShortUrlModal = (
</Modal>
);
};
export default DeleteShortUrlModal;

View File

@@ -17,7 +17,7 @@ interface QrCodeModalConnectProps extends ShortUrlModalProps {
selectedServer: SelectedServer;
}
const QrCodeModal = (imageDownloader: ImageDownloader) => (
export const QrCodeModal = (imageDownloader: ImageDownloader) => (
{ shortUrl: { shortUrl, shortCode }, toggle, isOpen, selectedServer }: QrCodeModalConnectProps,
) => {
const [size, setSize] = useState(300);
@@ -107,5 +107,3 @@ const QrCodeModal = (imageDownloader: ImageDownloader) => (
</Modal>
);
};
export default QrCodeModal;

View File

@@ -2,6 +2,7 @@ import { FC } from 'react';
import { Link } from 'react-router-dom';
import { isServerWithId, SelectedServer, ServerWithId } from '../../servers/data';
import { ShortUrl } from '../data';
import { urlEncodeShortCode } from './index';
export type LinkSuffix = 'visits' | 'edit';
@@ -13,11 +14,10 @@ export interface ShortUrlDetailLinkProps {
const buildUrl = ({ id }: ServerWithId, { shortCode, domain }: ShortUrl, suffix: LinkSuffix) => {
const query = domain ? `?domain=${domain}` : '';
return `/server/${id}/short-code/${shortCode}/${suffix}${query}`;
return `/server/${id}/short-code/${urlEncodeShortCode(shortCode)}/${suffix}${query}`;
};
const ShortUrlDetailLink: FC<ShortUrlDetailLinkProps & Record<string | number, any>> = (
export const ShortUrlDetailLink: FC<ShortUrlDetailLinkProps & Record<string | number, any>> = (
{ selectedServer, shortUrl, suffix, children, ...rest },
) => {
if (!selectedServer || !isServerWithId(selectedServer) || !shortUrl) {
@@ -26,5 +26,3 @@ const ShortUrlDetailLink: FC<ShortUrlDetailLinkProps & Record<string | number, a
return <Link to={buildUrl(selectedServer, shortUrl, suffix)} {...rest}>{children}</Link>;
};
export default ShortUrlDetailLink;

View File

@@ -1,5 +1,5 @@
import { ChangeEvent, FC, PropsWithChildren } from 'react';
import Checkbox from '../../utils/Checkbox';
import { Checkbox } from '../../utils/Checkbox';
import { InfoTooltip } from '../../utils/InfoTooltip';
type ShortUrlFormCheckboxGroupProps = PropsWithChildren<{

View File

@@ -6,8 +6,9 @@ import classNames from 'classnames';
import { prettify } from '../../utils/helpers/numbers';
import { ShortUrl } from '../data';
import { SelectedServer } from '../../servers/data';
import ShortUrlDetailLink from './ShortUrlDetailLink';
import { ShortUrlDetailLink } from './ShortUrlDetailLink';
import './ShortUrlVisitsCount.scss';
import { mutableRefToElementRef } from '../../utils/helpers/components';
interface ShortUrlVisitsCountProps {
shortUrl?: ShortUrl | null;
@@ -16,7 +17,9 @@ interface ShortUrlVisitsCountProps {
active?: boolean;
}
const ShortUrlVisitsCount = ({ visitsCount, shortUrl, selectedServer, active = false }: ShortUrlVisitsCountProps) => {
export const ShortUrlVisitsCount = (
{ visitsCount, shortUrl, selectedServer, active = false }: ShortUrlVisitsCountProps,
) => {
const maxVisits = shortUrl?.meta?.maxVisits;
const visitsLink = (
<ShortUrlDetailLink selectedServer={selectedServer} shortUrl={shortUrl} suffix="visits">
@@ -33,7 +36,7 @@ const ShortUrlVisitsCount = ({ visitsCount, shortUrl, selectedServer, active = f
}
const prettifiedMaxVisits = prettify(maxVisits);
const tooltipRef = useRef<HTMLElement | null>();
const tooltipRef = useRef<HTMLElement | undefined>();
return (
<>
@@ -41,9 +44,7 @@ const ShortUrlVisitsCount = ({ visitsCount, shortUrl, selectedServer, active = f
{visitsLink}
<small
className="short-urls-visits-count__max-visits-control"
ref={(el) => {
tooltipRef.current = el;
}}
ref={mutableRefToElementRef(tooltipRef)}
>
{' '}/ {prettifiedMaxVisits}{' '}
<sup>
@@ -57,5 +58,3 @@ const ShortUrlVisitsCount = ({ visitsCount, shortUrl, selectedServer, active = f
</>
);
};
export default ShortUrlVisitsCount;

View File

@@ -1,14 +1,14 @@
import { FC, useEffect, useRef } from 'react';
import { isEmpty } from 'ramda';
import { ExternalLink } from 'react-external-link';
import ColorGenerator from '../../utils/services/ColorGenerator';
import { StateFlagTimeout } from '../../utils/helpers/hooks';
import Tag from '../../tags/helpers/Tag';
import { ColorGenerator } from '../../utils/services/ColorGenerator';
import { TimeoutToggle } from '../../utils/helpers/hooks';
import { Tag } from '../../tags/helpers/Tag';
import { SelectedServer } from '../../servers/data';
import { CopyToClipboardIcon } from '../../utils/CopyToClipboardIcon';
import { ShortUrl } from '../data';
import { Time } from '../../utils/Time';
import ShortUrlVisitsCount from './ShortUrlVisitsCount';
import { ShortUrlVisitsCount } from './ShortUrlVisitsCount';
import { ShortUrlsRowMenuProps } from './ShortUrlsRowMenu';
import './ShortUrlsRow.scss';
@@ -18,13 +18,13 @@ export interface ShortUrlsRowProps {
shortUrl: ShortUrl;
}
const ShortUrlsRow = (
export const ShortUrlsRow = (
ShortUrlsRowMenu: FC<ShortUrlsRowMenuProps>,
colorGenerator: ColorGenerator,
useStateFlagTimeout: StateFlagTimeout,
useTimeoutToggle: TimeoutToggle,
) => ({ shortUrl, selectedServer, onTagClick }: ShortUrlsRowProps) => {
const [copiedToClipboard, setCopiedToClipboard] = useStateFlagTimeout();
const [active, setActive] = useStateFlagTimeout(false, 500);
const [copiedToClipboard, setCopiedToClipboard] = useTimeoutToggle();
const [active, setActive] = useTimeoutToggle(false, 500);
const isFirstRun = useRef(true);
const renderTags = (tags: string[]) => {
@@ -87,5 +87,3 @@ const ShortUrlsRow = (
</tr>
);
};
export default ShortUrlsRow;

View File

@@ -11,7 +11,7 @@ import { useToggle } from '../../utils/helpers/hooks';
import { ShortUrl, ShortUrlModalProps } from '../data';
import { SelectedServer } from '../../servers/data';
import { DropdownBtnMenu } from '../../utils/DropdownBtnMenu';
import ShortUrlDetailLink from './ShortUrlDetailLink';
import { ShortUrlDetailLink } from './ShortUrlDetailLink';
export interface ShortUrlsRowMenuProps {
selectedServer: SelectedServer;
@@ -19,7 +19,7 @@ export interface ShortUrlsRowMenuProps {
}
type ShortUrlModal = FC<ShortUrlModalProps>;
const ShortUrlsRowMenu = (
export const ShortUrlsRowMenu = (
DeleteShortUrlModal: ShortUrlModal,
QrCodeModal: ShortUrlModal,
) => ({ shortUrl, selectedServer }: ShortUrlsRowMenuProps) => {
@@ -51,5 +51,3 @@ const ShortUrlsRowMenu = (
</DropdownBtnMenu>
);
};
export default ShortUrlsRowMenu;

View File

@@ -1,7 +1,8 @@
import { isNil } from 'ramda';
import { ShortUrl } from '../data';
import { ShortUrl, ShortUrlData } from '../data';
import { OptionalString } from '../../utils/utils';
import { DEFAULT_DOMAIN } from '../../visits/reducers/domainVisits';
import { ShortUrlCreationSettings } from '../../settings/reducers/settings';
export const shortUrlMatches = (shortUrl: ShortUrl, shortCode: string, domain: OptionalString): boolean => {
if (isNil(domain)) {
@@ -18,3 +19,30 @@ export const domainMatches = (shortUrl: ShortUrl, domain: string): boolean => {
return shortUrl.domain === domain;
};
export const shortUrlDataFromShortUrl = (shortUrl?: ShortUrl, settings?: ShortUrlCreationSettings): ShortUrlData => {
const validateUrl = settings?.validateUrls ?? false;
if (!shortUrl) {
return { longUrl: '', validateUrl };
}
return {
longUrl: shortUrl.longUrl,
tags: shortUrl.tags,
title: shortUrl.title ?? undefined,
domain: shortUrl.domain ?? undefined,
validSince: shortUrl.meta.validSince ?? undefined,
validUntil: shortUrl.meta.validUntil ?? undefined,
maxVisits: shortUrl.meta.maxVisits ?? undefined,
crawlable: shortUrl.crawlable,
forwardQuery: shortUrl.forwardQuery,
validateUrl,
};
};
const MULTI_SEGMENT_SEPARATOR = '__';
export const urlEncodeShortCode = (shortCode: string): string => shortCode.replaceAll('/', MULTI_SEGMENT_SEPARATOR);
export const urlDecodeShortCode = (shortCode: string): string => shortCode.replaceAll(MULTI_SEGMENT_SEPARATOR, '/');

View File

@@ -1,18 +1,18 @@
import Bottle from 'bottlejs';
import { ShortUrlsFilteringBar } from '../ShortUrlsFilteringBar';
import ShortUrlsList from '../ShortUrlsList';
import ShortUrlsRow from '../helpers/ShortUrlsRow';
import ShortUrlsRowMenu from '../helpers/ShortUrlsRowMenu';
import CreateShortUrl from '../CreateShortUrl';
import DeleteShortUrlModal from '../helpers/DeleteShortUrlModal';
import CreateShortUrlResult from '../helpers/CreateShortUrlResult';
import { ShortUrlsList } from '../ShortUrlsList';
import { ShortUrlsRow } from '../helpers/ShortUrlsRow';
import { ShortUrlsRowMenu } from '../helpers/ShortUrlsRowMenu';
import { CreateShortUrl } from '../CreateShortUrl';
import { DeleteShortUrlModal } from '../helpers/DeleteShortUrlModal';
import { CreateShortUrlResult } from '../helpers/CreateShortUrlResult';
import { listShortUrls } from '../reducers/shortUrlsList';
import { createShortUrl, resetCreateShortUrl } from '../reducers/shortUrlCreation';
import { deleteShortUrl, resetDeleteShortUrl } from '../reducers/shortUrlDeletion';
import { editShortUrl } from '../reducers/shortUrlEdition';
import { ConnectDecorator } from '../../container/types';
import { ShortUrlsTable } from '../ShortUrlsTable';
import QrCodeModal from '../helpers/QrCodeModal';
import { QrCodeModal } from '../helpers/QrCodeModal';
import { ShortUrlForm } from '../ShortUrlForm';
import { EditShortUrl } from '../EditShortUrl';
import { getShortUrlDetail } from '../reducers/shortUrlDetail';
@@ -27,9 +27,9 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
));
bottle.serviceFactory('ShortUrlsTable', ShortUrlsTable, 'ShortUrlsRow');
bottle.serviceFactory('ShortUrlsRow', ShortUrlsRow, 'ShortUrlsRowMenu', 'ColorGenerator', 'useStateFlagTimeout');
bottle.serviceFactory('ShortUrlsRow', ShortUrlsRow, 'ShortUrlsRowMenu', 'ColorGenerator', 'useTimeoutToggle');
bottle.serviceFactory('ShortUrlsRowMenu', ShortUrlsRowMenu, 'DeleteShortUrlModal', 'QrCodeModal');
bottle.serviceFactory('CreateShortUrlResult', CreateShortUrlResult, 'useStateFlagTimeout');
bottle.serviceFactory('CreateShortUrlResult', CreateShortUrlResult, 'useTimeoutToggle');
bottle.serviceFactory('ShortUrlForm', ShortUrlForm, 'TagsSelector', 'DomainSelector');
bottle.serviceFactory('CreateShortUrl', CreateShortUrl, 'ShortUrlForm', 'CreateShortUrlResult');

View File

@@ -5,11 +5,12 @@ import { FC, useEffect, useRef } from 'react';
import { Link } from 'react-router-dom';
import { prettify } from '../utils/helpers/numbers';
import { useToggle } from '../utils/helpers/hooks';
import ColorGenerator from '../utils/services/ColorGenerator';
import { ColorGenerator } from '../utils/services/ColorGenerator';
import { getServerId, SelectedServer } from '../servers/data';
import TagBullet from './helpers/TagBullet';
import { TagBullet } from './helpers/TagBullet';
import { NormalizedTag, TagModalProps } from './data';
import './TagCard.scss';
import { mutableRefToElementRef } from '../utils/helpers/components';
export interface TagCardProps {
tag: NormalizedTag;
@@ -20,7 +21,7 @@ export interface TagCardProps {
const isTruncated = (el: HTMLElement | undefined): boolean => !!el && el.scrollWidth > el.clientWidth;
const TagCard = (
export const TagCard = (
DeleteTagConfirmModal: FC<TagModalProps>,
EditTagModal: FC<TagModalProps>,
colorGenerator: ColorGenerator,
@@ -28,7 +29,7 @@ const TagCard = (
const [isDeleteModalOpen, toggleDelete] = useToggle();
const [isEditModalOpen, toggleEdit] = useToggle();
const [hasTitle,, displayTitle] = useToggle();
const titleRef = useRef<HTMLElement>();
const titleRef = useRef<HTMLHeadingElement | undefined>();
const serverId = getServerId(selectedServer);
useEffect(() => {
@@ -40,18 +41,22 @@ const TagCard = (
return (
<Card className="tag-card">
<CardHeader className="tag-card__header">
<Button color="link" size="sm" className="tag-card__btn tag-card__btn--last" onClick={toggleDelete}>
<Button
aria-label="Delete tag"
color="link"
size="sm"
className="tag-card__btn tag-card__btn--last"
onClick={toggleDelete}
>
<FontAwesomeIcon icon={deleteIcon} />
</Button>
<Button color="link" size="sm" className="tag-card__btn" onClick={toggleEdit}>
<Button aria-label="Edit tag" color="link" size="sm" className="tag-card__btn" onClick={toggleEdit}>
<FontAwesomeIcon icon={editIcon} />
</Button>
<h5
className="tag-card__tag-title text-ellipsis"
title={hasTitle ? tag.tag : undefined}
ref={(el) => {
titleRef.current = el ?? undefined;
}}
ref={mutableRefToElementRef(titleRef)}
>
<TagBullet tag={tag.tag} colorGenerator={colorGenerator} />
<span className="tag-card__tag-name" onClick={toggle}>{tag.tag}</span>
@@ -82,5 +87,3 @@ const TagCard = (
</Card>
);
};
export default TagCard;

View File

@@ -1,7 +1,7 @@
import { FC, useEffect, useState } from 'react';
import { Row } from 'reactstrap';
import { pipe } from 'ramda';
import Message from '../utils/Message';
import { Message } from '../utils/Message';
import { SearchField } from '../utils/SearchField';
import { SelectedServer } from '../servers/data';
import { boundToMercureHub } from '../mercure/helpers/boundToMercureHub';
@@ -30,7 +30,7 @@ export interface TagsListProps {
settings: Settings;
}
const TagsList = (TagsCards: FC<TagsListChildrenProps>, TagsTable: FC<TagsTableProps>) => boundToMercureHub((
export const TagsList = (TagsCards: FC<TagsListChildrenProps>, TagsTable: FC<TagsTableProps>) => boundToMercureHub((
{ filterTags, forceListTags, tagsList, selectedServer, settings }: TagsListProps,
) => {
const [mode, setMode] = useState<TagsMode>(settings.tags?.defaultMode ?? 'cards');
@@ -104,5 +104,3 @@ const TagsList = (TagsCards: FC<TagsListChildrenProps>, TagsTable: FC<TagsTableP
</>
);
}, () => [Topics.visits]);
export default TagsList;

View File

@@ -4,11 +4,11 @@ import { DropdownItem } from 'reactstrap';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faTrash as deleteIcon, faPencilAlt as editIcon } from '@fortawesome/free-solid-svg-icons';
import { getServerId, SelectedServer } from '../servers/data';
import ColorGenerator from '../utils/services/ColorGenerator';
import { ColorGenerator } from '../utils/services/ColorGenerator';
import { prettify } from '../utils/helpers/numbers';
import { useToggle } from '../utils/helpers/hooks';
import { DropdownBtnMenu } from '../utils/DropdownBtnMenu';
import TagBullet from './helpers/TagBullet';
import { TagBullet } from './helpers/TagBullet';
import { NormalizedTag, TagModalProps } from './data';
export interface TagsTableRowProps {

View File

@@ -10,7 +10,7 @@ interface DeleteTagConfirmModalProps extends TagModalProps {
tagDelete: TagDeletion;
}
const DeleteTagConfirmModal = (
export const DeleteTagConfirmModal = (
{ tag, toggle, isOpen, deleteTag, tagDelete, tagDeleted }: DeleteTagConfirmModalProps,
) => {
const { deleting, error, errorData } = tagDelete;
@@ -22,9 +22,7 @@ const DeleteTagConfirmModal = (
return (
<Modal toggle={toggle} isOpen={isOpen} centered>
<ModalHeader toggle={toggle}>
<span className="text-danger">Delete tag</span>
</ModalHeader>
<ModalHeader toggle={toggle} className="text-danger">Delete tag</ModalHeader>
<ModalBody>
Are you sure you want to delete tag <b>{tag}</b>?
{error && (
@@ -42,5 +40,3 @@ const DeleteTagConfirmModal = (
</Modal>
);
};
export default DeleteTagConfirmModal;

View File

@@ -5,7 +5,7 @@ import { faPalette as colorIcon } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { useToggle } from '../../utils/helpers/hooks';
import { handleEventPreventingDefault } from '../../utils/utils';
import ColorGenerator from '../../utils/services/ColorGenerator';
import { ColorGenerator } from '../../utils/services/ColorGenerator';
import { TagModalProps } from '../data';
import { TagEdition } from '../reducers/tagEdit';
import { Result } from '../../utils/Result';
@@ -18,7 +18,7 @@ interface EditTagModalProps extends TagModalProps {
tagEdited: (oldName: string, newName: string, color: string) => void;
}
const EditTagModal = ({ getColorForKey }: ColorGenerator) => (
export const EditTagModal = ({ getColorForKey }: ColorGenerator) => (
{ tag, editTag, toggle, tagEdited, isOpen, tagEdit }: EditTagModalProps,
) => {
const [newTagName, setNewTagName] = useState(tag);
@@ -34,7 +34,7 @@ const EditTagModal = ({ getColorForKey }: ColorGenerator) => (
return (
<Modal isOpen={isOpen} toggle={toggle} centered onClosed={hideColorPicker}>
<form onSubmit={saveTag}>
<form name="editTag" onSubmit={saveTag}>
<ModalHeader toggle={toggle}>Edit tag</ModalHeader>
<ModalBody>
<InputGroup>
@@ -78,5 +78,3 @@ const EditTagModal = ({ getColorForKey }: ColorGenerator) => (
</Modal>
);
};
export default EditTagModal;

View File

@@ -1,6 +1,6 @@
import { FC, MouseEventHandler, PropsWithChildren } from 'react';
import classNames from 'classnames';
import ColorGenerator from '../../utils/services/ColorGenerator';
import { ColorGenerator } from '../../utils/services/ColorGenerator';
import './Tag.scss';
type TagProps = PropsWithChildren<{
@@ -12,15 +12,15 @@ type TagProps = PropsWithChildren<{
onClose?: MouseEventHandler;
}>;
const Tag: FC<TagProps> = ({ text, children, clearable, className = '', colorGenerator, onClick, onClose }) => (
export const Tag: FC<TagProps> = ({ text, children, clearable, className = '', colorGenerator, onClick, onClose }) => (
<span
className={classNames('badge tag', className, { 'tag--light-bg': colorGenerator.isColorLightForKey(text) })}
style={{ backgroundColor: colorGenerator.getColorForKey(text), cursor: clearable || !onClick ? 'auto' : 'pointer' }}
onClick={onClick}
>
{children ?? text}
{clearable && <span className="close tag__close-selected-tag" onClick={onClose}>&times;</span>}
{clearable && (
<span aria-label={`Remove ${text}`} className="close tag__close-selected-tag" onClick={onClose}>&times;</span>
)}
</span>
);
export default Tag;

View File

@@ -1,4 +1,4 @@
import ColorGenerator from '../../utils/services/ColorGenerator';
import { ColorGenerator } from '../../utils/services/ColorGenerator';
import './TagBullet.scss';
interface TagBulletProps {
@@ -6,11 +6,9 @@ interface TagBulletProps {
colorGenerator: ColorGenerator;
}
const TagBullet = ({ tag, colorGenerator }: TagBulletProps) => (
export const TagBullet = ({ tag, colorGenerator }: TagBulletProps) => (
<div
style={{ backgroundColor: colorGenerator.getColorForKey(tag) }}
className="tag-bullet"
/>
);
export default TagBullet;

View File

@@ -1,10 +1,10 @@
import { useEffect } from 'react';
import ReactTags, { SuggestionComponentProps, TagComponentProps } from 'react-tag-autocomplete';
import ColorGenerator from '../../utils/services/ColorGenerator';
import { ColorGenerator } from '../../utils/services/ColorGenerator';
import { Settings } from '../../settings/reducers/settings';
import { TagsList } from '../reducers/tagsList';
import TagBullet from './TagBullet';
import Tag from './Tag';
import { TagBullet } from './TagBullet';
import { Tag } from './Tag';
export interface TagsSelectorProps {
selectedTags: string[];
@@ -21,7 +21,7 @@ interface TagsSelectorConnectProps extends TagsSelectorProps {
const toComponentTag = (tag: string) => ({ id: tag, name: tag });
const TagsSelector = (colorGenerator: ColorGenerator) => (
export const TagsSelector = (colorGenerator: ColorGenerator) => (
{ selectedTags, onChange, placeholder, listTags, tagsList, settings, allowNew = true }: TagsSelectorConnectProps,
) => {
useEffect(() => {
@@ -68,5 +68,3 @@ const TagsSelector = (colorGenerator: ColorGenerator) => (
/>
);
};
export default TagsSelector;

View File

@@ -2,7 +2,7 @@ import { pick } from 'ramda';
import { Action, Dispatch } from 'redux';
import { buildReducer } from '../../utils/helpers/redux';
import { GetState } from '../../container/types';
import ColorGenerator from '../../utils/services/ColorGenerator';
import { ColorGenerator } from '../../utils/services/ColorGenerator';
import { ShlinkApiClientBuilder } from '../../api/services/ShlinkApiClientBuilder';
import { ProblemDetailsError } from '../../api/types';
import { parseApiError } from '../../api/utils';

View File

@@ -96,7 +96,7 @@ export default buildReducer<TagsList, TagsCombinedAction>({
}),
[FILTER_TAGS]: (state, { searchTerm }) => ({
...state,
filteredTags: state.tags.filter((tag) => tag.toLowerCase().match(searchTerm)),
filteredTags: state.tags.filter((tag) => tag.toLowerCase().match(searchTerm.toLowerCase())),
}),
[CREATE_VISITS]: (state, { createdVisits }) => ({
...state,

View File

@@ -1,9 +1,9 @@
import Bottle, { IContainer } from 'bottlejs';
import TagsSelector from '../helpers/TagsSelector';
import TagCard from '../TagCard';
import DeleteTagConfirmModal from '../helpers/DeleteTagConfirmModal';
import EditTagModal from '../helpers/EditTagModal';
import TagsList from '../TagsList';
import { TagsSelector } from '../helpers/TagsSelector';
import { TagCard } from '../TagCard';
import { DeleteTagConfirmModal } from '../helpers/DeleteTagConfirmModal';
import { EditTagModal } from '../helpers/EditTagModal';
import { TagsList } from '../TagsList';
import { filterTags, listTags } from '../reducers/tagsList';
import { deleteTag, tagDeleted } from '../reducers/tagDelete';
import { editTag, tagEdited } from '../reducers/tagEdit';

View File

@@ -31,6 +31,7 @@ $darkBorderInputColor: $darkBorderColor;
$darkTableHighlightColor: $darkBorderColor;
html:not([data-theme='dark']) {
--color-scheme: initial;
--primary-color: #{$lightPrimaryColor};
--primary-color-alfa: #{$lightPrimaryColorAlfa};
--secondary-color: #{$lightSecondaryColor};
@@ -48,6 +49,7 @@ html:not([data-theme='dark']) {
}
html[data-theme='dark'] {
--color-scheme: dark;
--primary-color: #{$darkPrimaryColor};
--primary-color-alfa: #{$darkPrimaryColorAlfa};
--secondary-color: #{$darkSecondaryColor};

View File

@@ -14,7 +14,7 @@ interface BooleanControlWithTypeProps extends BooleanControlProps {
type: 'switch' | 'checkbox';
}
const BooleanControl: FC<BooleanControlWithTypeProps> = (
export const BooleanControl: FC<BooleanControlWithTypeProps> = (
{ checked = false, onChange = identity, className, children, type, inline = false },
) => {
const id = useDomId();
@@ -32,5 +32,3 @@ const BooleanControl: FC<BooleanControlWithTypeProps> = (
</span>
);
};
export default BooleanControl;

View File

@@ -1,6 +1,4 @@
import { FC } from 'react';
import BooleanControl, { BooleanControlProps } from './BooleanControl';
import { BooleanControl, BooleanControlProps } from './BooleanControl';
const Checkbox: FC<BooleanControlProps> = (props) => <BooleanControl type="checkbox" {...props} />;
export default Checkbox;
export const Checkbox: FC<BooleanControlProps> = (props) => <BooleanControl type="checkbox" {...props} />;

View File

@@ -8,7 +8,7 @@ import './DateInput.scss';
export type DateInputProps = ReactDatePickerProps;
const DateInput = (props: DateInputProps) => {
export const DateInput = (props: DateInputProps) => {
const { className, isClearable, selected } = props;
const showCalendarIcon = !isClearable || isNil(selected);
const ref = useRef<{ input: HTMLInputElement }>();
@@ -32,5 +32,3 @@ const DateInput = (props: DateInputProps) => {
</div>
);
};
export default DateInput;

View File

@@ -3,21 +3,19 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faInfoCircle as infoIcon } from '@fortawesome/free-solid-svg-icons';
import { UncontrolledTooltip } from 'reactstrap';
import { Placement } from '@popperjs/core';
import { mutableRefToElementRef } from './helpers/components';
type InfoTooltipProps = PropsWithChildren<{
export type InfoTooltipProps = PropsWithChildren<{
className?: string;
placement: Placement;
}>;
export const InfoTooltip: FC<InfoTooltipProps> = ({ className = '', placement, children }) => {
const ref = useRef<HTMLSpanElement | null>();
const refCallback = (el: HTMLSpanElement) => {
ref.current = el;
};
const ref = useRef<HTMLSpanElement | undefined>();
return (
<>
<span className={className} ref={refCallback}>
<span className={className} ref={mutableRefToElementRef(ref)}>
<FontAwesomeIcon icon={infoIcon} />
</span>
<UncontrolledTooltip target={(() => ref.current) as any} placement={placement}>{children}</UncontrolledTooltip>

View File

@@ -30,7 +30,9 @@ export type MessageProps = PropsWithChildren<{
type?: MessageType;
}>;
const Message: FC<MessageProps> = ({ className, children, loading = false, type = 'default', fullWidth = false }) => {
export const Message: FC<MessageProps> = (
{ className, children, loading = false, type = 'default', fullWidth = false },
) => {
const classes = classNames({
'col-md-12': fullWidth,
'col-md-10 offset-md-1': !fullWidth,
@@ -50,5 +52,3 @@ const Message: FC<MessageProps> = ({ className, children, loading = false, type
</Row>
);
};
export default Message;

View File

@@ -7,7 +7,7 @@ interface PaginationDropdownProps {
toggleClassName?: string;
}
const PaginationDropdown = ({ toggleClassName, ranges, value, setValue }: PaginationDropdownProps) => (
export const PaginationDropdown = ({ toggleClassName, ranges, value, setValue }: PaginationDropdownProps) => (
<UncontrolledDropdown>
<DropdownToggle caret color="link" className={toggleClassName}>Paginate</DropdownToggle>
<DropdownMenu end>
@@ -23,5 +23,3 @@ const PaginationDropdown = ({ toggleClassName, ranges, value, setValue }: Pagina
</DropdownMenu>
</UncontrolledDropdown>
);
export default PaginationDropdown;

View File

@@ -15,6 +15,7 @@ export const Result: FC<ResultProps> = ({ children, type, className, small = fal
<Row className={className}>
<div className={classNames({ 'col-md-10 offset-md-1': !small, 'col-12': small })}>
<SimpleCard
role="document"
className={classNames('text-center', {
'bg-main': type === 'success',
'bg-danger': type === 'error',

View File

@@ -1,13 +1,13 @@
import { parseISO, format as formatDate, getUnixTime, formatDistance } from 'date-fns';
import { isDateObject } from './helpers/date';
export interface DateProps {
export interface TimeProps {
date: Date | string;
format?: string;
relative?: boolean;
}
export const Time = ({ date, format = 'yyyy-MM-dd HH:mm', relative = false }: DateProps) => {
export const Time = ({ date, format = 'yyyy-MM-dd HH:mm', relative = false }: TimeProps) => {
const dateObject = isDateObject(date) ? date : parseISO(date);
return (

View File

@@ -1,6 +1,4 @@
import { FC } from 'react';
import BooleanControl, { BooleanControlProps } from './BooleanControl';
import { BooleanControl, BooleanControlProps } from './BooleanControl';
const ToggleSwitch: FC<BooleanControlProps> = (props) => <BooleanControl type="switch" {...props} />;
export default ToggleSwitch;
export const ToggleSwitch: FC<BooleanControlProps> = (props) => <BooleanControl type="switch" {...props} />;

View File

@@ -1,5 +1,5 @@
import { endOfDay } from 'date-fns';
import DateInput from '../DateInput';
import { DateInput } from '../DateInput';
import { DateRange } from './types';
interface DateRangeRowProps extends DateRange {
@@ -8,7 +8,7 @@ interface DateRangeRowProps extends DateRange {
disabled?: boolean;
}
const DateRangeRow = (
export const DateRangeRow = (
{ startDate = null, endDate = null, disabled = false, onStartDateChange, onEndDateChange }: DateRangeRowProps,
) => (
<div className="row">
@@ -35,5 +35,3 @@ const DateRangeRow = (
</div>
</div>
);
export default DateRangeRow;

View File

@@ -10,7 +10,7 @@ import {
rangeIsInterval,
dateRangeIsEmpty,
} from './types';
import DateRangeRow from './DateRangeRow';
import { DateRangeRow } from './DateRangeRow';
import { DateIntervalDropdownItems } from './DateIntervalDropdownItems';
export interface DateRangeSelectorProps {

View File

@@ -1,6 +1,7 @@
import { FC, PropsWithChildren } from 'react';
import { InputType } from 'reactstrap/types/lib/Input';
import { LabeledFormGroup } from './LabeledFormGroup';
import { useDomId } from '../helpers/hooks';
export type InputFormGroupProps = PropsWithChildren<{
value: string;
@@ -14,15 +15,20 @@ export type InputFormGroupProps = PropsWithChildren<{
export const InputFormGroup: FC<InputFormGroupProps> = (
{ children, value, onChange, type, required, placeholder, className, labelClassName },
) => (
<LabeledFormGroup label={<>{children}:</>} className={className ?? ''} labelClassName={labelClassName}>
<input
className="form-control"
type={type ?? 'text'}
value={value}
required={required ?? true}
placeholder={placeholder}
onChange={(e) => onChange(e.target.value)}
/>
</LabeledFormGroup>
);
) => {
const id = useDomId();
return (
<LabeledFormGroup label={<>{children}:</>} className={className ?? ''} labelClassName={labelClassName} id={id}>
<input
id={id}
className="form-control"
type={type ?? 'text'}
value={value}
required={required ?? true}
placeholder={placeholder}
onChange={(e) => onChange(e.target.value)}
/>
</LabeledFormGroup>
);
};

View File

@@ -5,14 +5,15 @@ type LabeledFormGroupProps = PropsWithChildren<{
noMargin?: boolean;
className?: string;
labelClassName?: string;
id?: string;
}>;
/* eslint-disable jsx-a11y/label-has-associated-control */
export const LabeledFormGroup: FC<LabeledFormGroupProps> = (
{ children, label, className = '', labelClassName = '', noMargin = false },
{ children, label, className = '', labelClassName = '', noMargin = false, id },
) => (
<div className={`${className} ${noMargin ? '' : 'mb-3'}`}>
<label className={`form-label ${labelClassName}`}>{label}</label>
<label className={`form-label ${labelClassName}`} htmlFor={id}>{label}</label>
{children}
</div>
);

View File

@@ -11,8 +11,6 @@ export const pointerOnHover = ({ native }: ChartEvent, [firstElement]: ActiveEle
canvas.style.cursor = firstElement ? 'pointer' : 'default';
};
export const renderChartLabel = ({ dataset, formattedValue }: TooltipItem<ChartType>) =>
`${dataset.label}: ${prettify(formattedValue)}`;
export const renderChartLabel = ({ dataset, raw }: TooltipItem<ChartType>) => `${dataset.label}: ${prettify(`${raw}`)}`;
export const renderPieChartLabel = ({ label, formattedValue }: TooltipItem<ChartType>) =>
`${label}: ${prettify(formattedValue)}`;
export const renderPieChartLabel = ({ label, raw }: TooltipItem<ChartType>) => `${label}: ${prettify(`${raw}`)}`;

View File

@@ -0,0 +1,5 @@
import { MutableRefObject, Ref } from 'react';
export const mutableRefToElementRef = <T>(ref: MutableRefObject<T | undefined>): Ref<T> => (el) => {
ref.current = el ?? undefined; // eslint-disable-line no-param-reassign
};

View File

@@ -6,12 +6,12 @@ import { parseQuery, stringifyQuery } from './query';
const DEFAULT_DELAY = 2000;
export type StateFlagTimeout = (initialValue?: boolean, delay?: number) => [ boolean, () => void ];
export type TimeoutToggle = (initialValue?: boolean, delay?: number) => [boolean, () => void];
export const useStateFlagTimeout = (
export const useTimeoutToggle = (
setTimeout: (callback: Function, timeout: number) => number,
clearTimeout: (timer: number) => void,
): StateFlagTimeout => (initialValue = false, delay = DEFAULT_DELAY) => {
): TimeoutToggle => (initialValue = false, delay = DEFAULT_DELAY) => {
const [flag, setFlag] = useState<boolean>(initialValue);
const timeout = useRef<number | undefined>(undefined);
const callback = () => {
@@ -31,7 +31,6 @@ type ToggleResult = [ boolean, () => void, () => void, () => void ];
export const useToggle = (initialValue = false): ToggleResult => {
const [flag, setFlag] = useState<boolean>(initialValue);
return [flag, () => setFlag(!flag), () => setFlag(true), () => setFlag(false)];
};
@@ -80,7 +79,6 @@ export const useEffectExceptFirstTime = (callback: EffectCallback, deps: Depende
export const useGoBack = () => {
const navigate = useNavigate();
return () => navigate(-1);
};

View File

@@ -1,6 +1,6 @@
import { isNil } from 'ramda';
import { rangeOf } from '../utils';
import LocalStorage from './LocalStorage';
import { LocalStorage } from './LocalStorage';
const HEX_COLOR_LENGTH = 6;
const HEX_DIGITS = '0123456789ABCDEF';
@@ -15,7 +15,7 @@ const hexColorToRgbArray = (colorHex: string): number[] =>
// HSP by Darel Rex Finley https://alienryderflex.com/hsp.html
const perceivedLightness = (r = 0, g = 0, b = 0) => round(sqrt(0.299 * r ** 2 + 0.587 * g ** 2 + 0.114 * b ** 2));
export default class ColorGenerator {
export class ColorGenerator {
private readonly colors: Record<string, string>;
private readonly lights: Record<string, boolean>;

View File

@@ -1,7 +1,7 @@
const PREFIX = 'shlink';
const buildPath = (path: string) => `${PREFIX}.${path}`;
export default class LocalStorage {
export class LocalStorage {
public constructor(private readonly localStorage: Storage) {}
public readonly get = <T>(key: string): T | undefined => {

View File

@@ -1,7 +1,7 @@
import Bottle from 'bottlejs';
import { useStateFlagTimeout } from '../helpers/hooks';
import LocalStorage from './LocalStorage';
import ColorGenerator from './ColorGenerator';
import { useTimeoutToggle } from '../helpers/hooks';
import { LocalStorage } from './LocalStorage';
import { ColorGenerator } from './ColorGenerator';
import { csvToJson, jsonToCsv } from '../helpers/csvjson';
const provideServices = (bottle: Bottle) => {
@@ -14,7 +14,7 @@ const provideServices = (bottle: Bottle) => {
bottle.constant('setTimeout', global.setTimeout);
bottle.constant('clearTimeout', global.clearTimeout);
bottle.serviceFactory('useStateFlagTimeout', useStateFlagTimeout, 'setTimeout', 'clearTimeout');
bottle.serviceFactory('useTimeoutToggle', useTimeoutToggle, 'setTimeout', 'clearTimeout');
};
export default provideServices;

View File

@@ -8,8 +8,8 @@ import { Topics } from '../mercure/helpers/Topics';
import { useGoBack } from '../utils/helpers/hooks';
import { toApiParams } from './types/helpers';
import { NormalizedVisit } from './types';
import VisitsStats from './VisitsStats';
import VisitsHeader from './VisitsHeader';
import { VisitsStats } from './VisitsStats';
import { VisitsHeader } from './VisitsHeader';
export interface DomainVisitsProps extends CommonVisitsProps {
getDomainVisits: (domain: string, query?: ShlinkVisitsParams, doIntervalFallback?: boolean) => void;

View File

@@ -3,11 +3,11 @@ import { ShlinkVisitsParams } from '../api/types';
import { Topics } from '../mercure/helpers/Topics';
import { useGoBack } from '../utils/helpers/hooks';
import { ReportExporter } from '../common/services/ReportExporter';
import VisitsStats from './VisitsStats';
import { VisitsStats } from './VisitsStats';
import { NormalizedVisit, VisitsInfo, VisitsParams } from './types';
import { CommonVisitsProps } from './types/CommonVisitsProps';
import { toApiParams } from './types/helpers';
import VisitsHeader from './VisitsHeader';
import { VisitsHeader } from './VisitsHeader';
export interface NonOrphanVisitsProps extends CommonVisitsProps {
getNonOrphanVisits: (params?: ShlinkVisitsParams, doIntervalFallback?: boolean) => void;

View File

@@ -3,11 +3,11 @@ import { ShlinkVisitsParams } from '../api/types';
import { Topics } from '../mercure/helpers/Topics';
import { useGoBack } from '../utils/helpers/hooks';
import { ReportExporter } from '../common/services/ReportExporter';
import VisitsStats from './VisitsStats';
import { VisitsStats } from './VisitsStats';
import { NormalizedVisit, OrphanVisitType, VisitsInfo, VisitsParams } from './types';
import { CommonVisitsProps } from './types/CommonVisitsProps';
import { toApiParams } from './types/helpers';
import VisitsHeader from './VisitsHeader';
import { VisitsHeader } from './VisitsHeader';
export interface OrphanVisitsProps extends CommonVisitsProps {
getOrphanVisits: (

View File

@@ -8,11 +8,12 @@ import { ShortUrlDetail } from '../short-urls/reducers/shortUrlDetail';
import { useGoBack } from '../utils/helpers/hooks';
import { ReportExporter } from '../common/services/ReportExporter';
import { ShortUrlVisits as ShortUrlVisitsState } from './reducers/shortUrlVisits';
import ShortUrlVisitsHeader from './ShortUrlVisitsHeader';
import VisitsStats from './VisitsStats';
import { ShortUrlVisitsHeader } from './ShortUrlVisitsHeader';
import { VisitsStats } from './VisitsStats';
import { NormalizedVisit, VisitsParams } from './types';
import { CommonVisitsProps } from './types/CommonVisitsProps';
import { toApiParams } from './types/helpers';
import { urlDecodeShortCode } from '../short-urls/helpers';
export interface ShortUrlVisitsProps extends CommonVisitsProps {
getShortUrlVisits: (shortCode: string, query?: ShlinkVisitsParams, doIntervalFallback?: boolean) => void;
@@ -22,7 +23,7 @@ export interface ShortUrlVisitsProps extends CommonVisitsProps {
cancelGetShortUrlVisits: () => void;
}
const ShortUrlVisits = ({ exportVisits }: ReportExporter) => boundToMercureHub(({
export const ShortUrlVisits = ({ exportVisits }: ReportExporter) => boundToMercureHub(({
shortUrlVisits,
shortUrlDetail,
getShortUrlVisits,
@@ -36,14 +37,14 @@ const ShortUrlVisits = ({ exportVisits }: ReportExporter) => boundToMercureHub((
const goBack = useGoBack();
const { domain } = parseQuery<{ domain?: string }>(search);
const loadVisits = (params: VisitsParams, doIntervalFallback?: boolean) =>
getShortUrlVisits(shortCode, { ...toApiParams(params), domain }, doIntervalFallback);
getShortUrlVisits(urlDecodeShortCode(shortCode), { ...toApiParams(params), domain }, doIntervalFallback);
const exportCsv = (visits: NormalizedVisit[]) => exportVisits(
`short-url_${shortUrlDetail.shortUrl?.shortUrl.replace(/https?:\/\//g, '')}_visits.csv`,
visits,
);
useEffect(() => {
getShortUrlDetail(shortCode, domain);
getShortUrlDetail(urlDecodeShortCode(shortCode), domain);
}, []);
return (
@@ -59,6 +60,4 @@ const ShortUrlVisits = ({ exportVisits }: ReportExporter) => boundToMercureHub((
<ShortUrlVisitsHeader shortUrlDetail={shortUrlDetail} shortUrlVisits={shortUrlVisits} goBack={goBack} />
</VisitsStats>
);
}, (_, params) => [Topics.shortUrlVisits(params.shortCode)]);
export default ShortUrlVisits;
}, (_, params) => (params.shortCode ? [Topics.shortUrlVisits(urlDecodeShortCode(params.shortCode))] : []));

View File

@@ -3,7 +3,7 @@ import { ExternalLink } from 'react-external-link';
import { ShortUrlDetail } from '../short-urls/reducers/shortUrlDetail';
import { Time } from '../utils/Time';
import { ShortUrlVisits } from './reducers/shortUrlVisits';
import VisitsHeader from './VisitsHeader';
import { VisitsHeader } from './VisitsHeader';
import './ShortUrlVisitsHeader.scss';
interface ShortUrlVisitsHeaderProps {
@@ -12,7 +12,7 @@ interface ShortUrlVisitsHeaderProps {
goBack: () => void;
}
const ShortUrlVisitsHeader = ({ shortUrlDetail, shortUrlVisits, goBack }: ShortUrlVisitsHeaderProps) => {
export const ShortUrlVisitsHeader = ({ shortUrlDetail, shortUrlVisits, goBack }: ShortUrlVisitsHeaderProps) => {
const { shortUrl, loading } = shortUrlDetail;
const { visits } = shortUrlVisits;
const shortLink = shortUrl?.shortUrl ?? '';
@@ -35,7 +35,7 @@ const ShortUrlVisitsHeader = ({ shortUrlDetail, shortUrlVisits, goBack }: ShortU
<VisitsHeader title={visitsStatsTitle} goBack={goBack} visits={visits} shortUrl={shortUrl}>
<hr />
<div>Created: {renderDate()}</div>
<div>
<div className="long-url-container">
{`${title ? 'Title' : 'Long URL'}: `}
{loading && <small>Loading...</small>}
{!loading && <ExternalLink href={longLink}>{title ?? longLink}</ExternalLink>}
@@ -43,5 +43,3 @@ const ShortUrlVisitsHeader = ({ shortUrlDetail, shortUrlVisits, goBack }: ShortU
</VisitsHeader>
);
};
export default ShortUrlVisitsHeader;

View File

@@ -1,13 +1,13 @@
import { useParams } from 'react-router-dom';
import { boundToMercureHub } from '../mercure/helpers/boundToMercureHub';
import ColorGenerator from '../utils/services/ColorGenerator';
import { ColorGenerator } from '../utils/services/ColorGenerator';
import { ShlinkVisitsParams } from '../api/types';
import { Topics } from '../mercure/helpers/Topics';
import { useGoBack } from '../utils/helpers/hooks';
import { ReportExporter } from '../common/services/ReportExporter';
import { TagVisits as TagVisitsState } from './reducers/tagVisits';
import TagVisitsHeader from './TagVisitsHeader';
import VisitsStats from './VisitsStats';
import { TagVisitsHeader } from './TagVisitsHeader';
import { VisitsStats } from './VisitsStats';
import { NormalizedVisit } from './types';
import { CommonVisitsProps } from './types/CommonVisitsProps';
import { toApiParams } from './types/helpers';
@@ -18,7 +18,7 @@ export interface TagVisitsProps extends CommonVisitsProps {
cancelGetTagVisits: () => void;
}
const TagVisits = (colorGenerator: ColorGenerator, { exportVisits }: ReportExporter) => boundToMercureHub(({
export const TagVisits = (colorGenerator: ColorGenerator, { exportVisits }: ReportExporter) => boundToMercureHub(({
getTagVisits,
tagVisits,
cancelGetTagVisits,
@@ -44,5 +44,3 @@ const TagVisits = (colorGenerator: ColorGenerator, { exportVisits }: ReportExpor
</VisitsStats>
);
}, () => [Topics.visits]);
export default TagVisits;

View File

@@ -1,6 +1,6 @@
import Tag from '../tags/helpers/Tag';
import ColorGenerator from '../utils/services/ColorGenerator';
import VisitsHeader from './VisitsHeader';
import { Tag } from '../tags/helpers/Tag';
import { ColorGenerator } from '../utils/services/ColorGenerator';
import { VisitsHeader } from './VisitsHeader';
import { TagVisits } from './reducers/tagVisits';
import './ShortUrlVisitsHeader.scss';
@@ -10,9 +10,8 @@ interface TagVisitsHeaderProps {
colorGenerator: ColorGenerator;
}
const TagVisitsHeader = ({ tagVisits, goBack, colorGenerator }: TagVisitsHeaderProps) => {
export const TagVisitsHeader = ({ tagVisits, goBack, colorGenerator }: TagVisitsHeaderProps) => {
const { visits, tag } = tagVisits;
const visitsStatsTitle = (
<span className="d-flex align-items-center justify-content-center">
<span className="me-2">Visits for</span>
@@ -22,5 +21,3 @@ const TagVisitsHeader = ({ tagVisits, goBack, colorGenerator }: TagVisitsHeaderP
return <VisitsHeader title={visitsStatsTitle} goBack={goBack} visits={visits} />;
};
export default TagVisitsHeader;

View File

@@ -2,7 +2,7 @@ import { Button, Card } from 'reactstrap';
import { FC, PropsWithChildren, ReactNode } from 'react';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faArrowLeft } from '@fortawesome/free-solid-svg-icons';
import ShortUrlVisitsCount from '../short-urls/helpers/ShortUrlVisitsCount';
import { ShortUrlVisitsCount } from '../short-urls/helpers/ShortUrlVisitsCount';
import { ShortUrl } from '../short-urls/data';
import { Visit } from './types';
@@ -13,7 +13,7 @@ type VisitsHeaderProps = PropsWithChildren<{
shortUrl?: ShortUrl;
}>;
const VisitsHeader: FC<VisitsHeaderProps> = ({ visits, goBack, shortUrl, children, title }) => (
export const VisitsHeader: FC<VisitsHeaderProps> = ({ visits, goBack, shortUrl, children, title }) => (
<header>
<Card body>
<h2 className="d-flex justify-content-between align-items-center mb-0">
@@ -36,5 +36,3 @@ const VisitsHeader: FC<VisitsHeaderProps> = ({ visits, goBack, shortUrl, childre
</Card>
</header>
);
export default VisitsHeader;

View File

@@ -7,7 +7,7 @@ import { IconDefinition } from '@fortawesome/fontawesome-common-types';
import { Route, Routes, Navigate } from 'react-router-dom';
import classNames from 'classnames';
import { DateRangeSelector } from '../utils/dates/DateRangeSelector';
import Message from '../utils/Message';
import { Message } from '../utils/Message';
import { DateInterval, DateRange, intervalToDateRange } from '../utils/dates/types';
import { Result } from '../utils/Result';
import { ShlinkApiError } from '../api/ShlinkApiError';
@@ -17,10 +17,10 @@ import { supportsBotVisits } from '../utils/helpers/features';
import { prettify } from '../utils/helpers/numbers';
import { NavPillItem, NavPills } from '../utils/NavPills';
import { ExportBtn } from '../utils/ExportBtn';
import LineChartCard from './charts/LineChartCard';
import VisitsTable from './VisitsTable';
import { LineChartCard } from './charts/LineChartCard';
import { VisitsTable } from './VisitsTable';
import { NormalizedOrphanVisit, NormalizedVisit, VisitsFilter, VisitsInfo, VisitsParams } from './types';
import OpenMapModalBtn from './helpers/OpenMapModalBtn';
import { OpenMapModalBtn } from './helpers/OpenMapModalBtn';
import { normalizeVisits, processStatsFromVisits } from './services/VisitsParser';
import { VisitsFilterDropdown } from './helpers/VisitsFilterDropdown';
import { HighlightableProps, highlightedVisitsToStats } from './types/helpers';
@@ -55,7 +55,7 @@ const sections: Record<Section, VisitsNavLinkProps> = {
let selectedBar: string | undefined;
const VisitsStats: FC<VisitsStatsProps> = ({
export const VisitsStats: FC<VisitsStatsProps> = ({
children,
visitsInfo,
getVisits,
@@ -139,7 +139,7 @@ const VisitsStats: FC<VisitsStatsProps> = ({
}
if (isEmpty(visits)) {
return <Message>There are no visits matching current filter :(</Message>;
return <Message>There are no visits matching current filter</Message>;
}
return (
@@ -235,7 +235,7 @@ const VisitsStats: FC<VisitsStatsProps> = ({
stats={cities}
highlightedStats={highlightedVisitsToStats(highlightedVisits, 'city')}
highlightedLabel={highlightedLabel}
extraHeaderContent={(activeCities: string[]) => mapLocations.length > 0 && (
extraHeaderContent={(activeCities) => mapLocations.length > 0 && (
<OpenMapModalBtn modalTitle="Cities" locations={mapLocations} activeCities={activeCities} />
)}
sortingItems={{
@@ -300,19 +300,19 @@ const VisitsStats: FC<VisitsStatsProps> = ({
{visits.length > 0 && (
<div className="col-lg-5 col-xl-6 mt-3 mt-lg-0">
<div className="d-flex">
<Button
outline
disabled={highlightedVisits.length === 0}
className="btn-md-block me-2"
onClick={() => setSelectedVisits([])}
>
Clear selection {highlightedVisits.length > 0 && <>({prettify(highlightedVisits.length)})</>}
</Button>
<ExportBtn
className="btn-md-block"
amount={normalizedVisits.length}
onClick={() => exportCsv(normalizedVisits)}
/>
<Button
outline
disabled={highlightedVisits.length === 0}
className="btn-md-block ms-2"
onClick={() => setSelectedVisits([])}
>
Clear selection {highlightedVisits.length > 0 && <>({prettify(highlightedVisits.length)})</>}
</Button>
</div>
</div>
)}
@@ -325,5 +325,3 @@ const VisitsStats: FC<VisitsStatsProps> = ({
</>
);
};
export default VisitsStats;

View File

@@ -45,7 +45,7 @@ const calculateVisits = (allVisits: NormalizedVisit[], searchTerm: string | unde
return { visitsGroups, total };
};
const VisitsTable = ({
export const VisitsTable = ({
visits,
selectedVisits = [],
setSelectedVisits,
@@ -222,5 +222,3 @@ const VisitsTable = ({
</div>
);
};
export default VisitsTable;

Some files were not shown because too many files have changed in this diff Show More