Compare commits

...

291 Commits

Author SHA1 Message Date
Alejandro Celaya
90fde34a45 Merge pull request #711 from shlinkio/develop
Release 3.7.3
2022-09-13 17:48:40 +02:00
Alejandro Celaya
563c60668a Merge pull request #710 from acelaya-forks/feature/fix-loading-large
Feature/fix loading large
2022-09-13 17:45:32 +02:00
Alejandro Celaya
3a657e1e2f Updated changelog 2022-09-13 16:00:27 +02:00
Alejandro Celaya
4466d733b4 Fixed visits not being displayed after a large loading has finished 2022-09-13 15:56:53 +02:00
Alejandro Celaya
dadecdc674 Merge pull request #705 from acelaya-forks/feature/refactor-docker-build
Moved to docker build from reusable workflow
2022-08-19 14:59:09 +02:00
Alejandro Celaya
a2440d3180 Moved to docker build from reusable workflow 2022-08-19 12:48:17 +02:00
Alejandro Celaya
56e6a2a16d Merge pull request #704 from acelaya-forks/feature/ghcr-support
Feature/ghcr support
2022-08-14 17:33:27 +02:00
Alejandro Celaya
c918eaf903 Added GHCR publishing in docker build script 2022-08-14 17:29:01 +02:00
Alejandro Celaya
324eda25e0 Added support to publish docker image in GHCR 2022-08-14 17:27:45 +02:00
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
Alejandro Celaya
6ed19af295 Merge pull request #647 from shlinkio/develop
Release 3.7.0
2022-05-14 16:59:31 +02:00
Alejandro Celaya
46f7a67b14 Tagged v3.7.0 in changelog 2022-05-14 16:51:59 +02:00
Alejandro Celaya
6f2639fd1f Merge pull request #646 from acelaya-forks/feature/short-url-filtering
Feature/short url filtering
2022-05-14 16:48:18 +02:00
Alejandro Celaya
f1568eb43b Updated changelog 2022-05-14 16:43:00 +02:00
Alejandro Celaya
aefc632ed7 Removed some default imports 2022-05-14 16:41:09 +02:00
Alejandro Celaya
bd3555db94 Fixed coding styles 2022-05-14 16:38:25 +02:00
Alejandro Celaya
ed366fa4cc Updated ShortUrlsFilteringBar test 2022-05-14 16:36:45 +02:00
Alejandro Celaya
30aeba0af2 Fixed ordering dropdown to be shorter in short URLs filter 2022-05-14 14:15:21 +02:00
Alejandro Celaya
b4c3bd16b1 Fixed colors and styles in tags selector 2022-05-14 13:05:23 +02:00
Alejandro Celaya
4b97abaf72 Improved tags filtering for short URLs, allowing to select from any existing tag 2022-05-14 12:53:02 +02:00
Alejandro Celaya
e387706a7b Updated to ExternalLink 2.0 2022-05-14 12:51:36 +02:00
Alejandro Celaya
a7cc8786c3 Migrated ExportBtn test to react testing library, and changed icon 2022-05-14 11:15:48 +02:00
Alejandro Celaya
5c0573deb7 Merge pull request #645 from acelaya-forks/feature/moar-rtl-tests
Feature/moar rtl tests
2022-05-13 20:29:59 +02:00
Alejandro Celaya
bc8956db4a Fixed coding styles 2022-05-13 20:25:32 +02:00
Alejandro Celaya
d44be88b3f Fixed ilegal rendering of trinside div in DomainRow test 2022-05-13 20:21:20 +02:00
Alejandro Celaya
64ee9a39cc Migrated HorizontalBarChart test to react testing library 2022-05-13 20:18:40 +02:00
Alejandro Celaya
4999f982e4 Migrated ChartCard test to react testing library 2022-05-13 19:59:30 +02:00
Alejandro Celaya
f3f0dbac19 Added shlinkio css coding styles 2022-05-12 22:44:12 +02:00
Alejandro Celaya
c2818645c4 Merge pull request #644 from acelaya-forks/feature/new-css-style
Feature/new css style
2022-05-11 22:37:02 +02:00
Alejandro Celaya
b0a4aee175 Updated changelog 2022-05-11 22:33:02 +02:00
Alejandro Celaya
67c45f444b Replaced adidas coding styles for CSS with a custom one based on stylelint recommended 2022-05-11 22:32:17 +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
Alejandro Celaya
f3570d0c9d Removed no-longer needed deps 2022-05-11 21:41:19 +02:00
Alejandro Celaya
653be14e77 Merge pull request #628 from shlinkio/dependabot/npm_and_yarn/ejs-3.1.7
Bump ejs from 3.1.6 to 3.1.7
2022-05-11 19:38:16 +02:00
Alejandro Celaya
71c8b596e7 Merge pull request #633 from shlinkio/dependabot/npm_and_yarn/kind-of-6.0.3
Bump kind-of from 6.0.2 to 6.0.3
2022-05-11 19:38:09 +02:00
Alejandro Celaya
ba45b6a7a6 Merge pull request #634 from shlinkio/dependabot/npm_and_yarn/trim-newlines-3.0.1
Bump trim-newlines from 3.0.0 to 3.0.1
2022-05-11 19:38:01 +02:00
Alejandro Celaya
cd311aeaa5 Merge pull request #635 from shlinkio/dependabot/npm_and_yarn/dns-packet-1.3.4
Bump dns-packet from 1.3.1 to 1.3.4
2022-05-11 19:37:53 +02:00
Alejandro Celaya
4f7a6d5c96 Merge pull request #636 from shlinkio/dependabot/npm_and_yarn/ansi-regex-3.0.1
Bump ansi-regex from 3.0.0 to 3.0.1
2022-05-11 19:37:42 +02:00
Alejandro Celaya
a4fb439dd4 Merge pull request #641 from acelaya-forks/feature/user-event
Feature/user event
2022-05-11 19:29:39 +02:00
Alejandro Celaya
3870752bba Replaced firEvent with userEvent as much as possible 2022-05-11 19:18:43 +02:00
Alejandro Celaya
7f059c3f3b Migrated to testing-library/user-event for complex events in tests 2022-05-10 20:54:14 +02:00
Alejandro Celaya
686fe5abbe Merge pull request #640 from acelaya-forks/feature/more-rtl-tests
Feature/more rtl tests
2022-05-09 19:53:46 +02:00
Alejandro Celaya
1f47658c3c Fixed coding styles 2022-05-09 19:47:41 +02:00
Alejandro Celaya
d582a0f9e5 Migrated DomainStatusIcon test to react testing library 2022-05-09 19:45:09 +02:00
Alejandro Celaya
b1d51a4103 Migrated ManageDomains test to react testing library 2022-05-09 19:23:35 +02:00
Alejandro Celaya
fb0adf74f3 Migrated DomainsSelector test to react testing library 2022-05-09 19:03:19 +02:00
Alejandro Celaya
0b16300a70 Migrated DomainRow test to react testing library 2022-05-09 18:38:14 +02:00
Alejandro Celaya
43302ef5a8 Migrated ShlinkLogo test to react testing library 2022-05-06 21:24:16 +02:00
Alejandro Celaya
3846ca293c Migrated SimplePaginator test to react testing library 2022-05-06 21:20:14 +02:00
Alejandro Celaya
00f154ef4e Migrated ShlinkVersionsContainer test to react testing library 2022-05-06 21:01:44 +02:00
Alejandro Celaya
4ea826ed2c Migrated ShlinkVersions test to react testing library 2022-05-06 20:25:48 +02:00
Alejandro Celaya
d327142d00 Migrated ScrollToTop test to react testing library 2022-05-06 20:13:51 +02:00
Alejandro Celaya
37adcb52cf Migrated NotFound test to react testing library 2022-05-06 19:55:25 +02:00
Alejandro Celaya
fcbb9cda12 Migrated HorizontalBarChart test to react testing library + snapshots for the events on the canvas 2022-05-06 19:46:47 +02:00
Alejandro Celaya
116c36febc Merge pull request #639 from acelaya-forks/feature/more-rtl-tests
Feature/more rtl tests
2022-05-06 19:16:07 +02:00
Alejandro Celaya
43a409fb30 Updated to stryker v6 2022-05-06 19:02:03 +02:00
Alejandro Celaya
63f26d0089 Migrated MenuLayout test to react testing library 2022-05-06 18:54:00 +02:00
Alejandro Celaya
218ea09d23 Removed no longer needed module mocking 2022-05-03 20:39:09 +02:00
Alejandro Celaya
a322886710 Migrated MainHeader test to react testing library 2022-05-03 20:36:24 +02:00
Alejandro Celaya
29182ae349 Migrated Home test to react testing library 2022-05-03 20:15:22 +02:00
Alejandro Celaya
bc3bc8dd8a Migrated ErrorHandler test to react testing library 2022-05-03 20:01:40 +02:00
Alejandro Celaya
e128b847be Migrated AsideMenu test to react testing library 2022-05-03 18:03:14 +02:00
Alejandro Celaya
f72251c125 Migrated AppUpdateBanner test to react testing library 2022-05-03 17:49:42 +02:00
Alejandro Celaya
9784cbb3ac Migrated App test to react testing library 2022-05-03 17:36:34 +02:00
Alejandro Celaya
e6f9003fb6 Merge pull request #638 from acelaya-forks/feature/more-rtl-tests
Feature/more rtl tests
2022-05-02 19:39:30 +02:00
Alejandro Celaya
e8a5eadd2b Migrated ShlinkApiError test from enzyme to react testing library 2022-05-02 19:34:33 +02:00
Alejandro Celaya
337bfc47c1 Deleted ForServerVersion component, which is no longer used 2022-05-02 19:28:07 +02:00
Alejandro Celaya
677f1da8df Removed last occurrence of ForServerVersion component, replaced by feature check 2022-05-02 19:24:57 +02:00
Alejandro Celaya
8918b1ac96 Migrated Overview test from enzyme to react testing library 2022-05-02 19:19:47 +02:00
Alejandro Celaya
0de8eb1568 Migrated ForServerVersion test from enzyme to react testing library 2022-05-02 18:58:57 +02:00
Alejandro Celaya
c00aaa9018 Migrated UseExistingIfFoundInfoIcon test from enzyme to react testing library 2022-05-02 18:54:19 +02:00
Alejandro Celaya
e837ee5225 Migrated EditServer test from enzyme to react testing library 2022-05-02 18:47:18 +02:00
dependabot[bot]
fc589a0b29 Bump ejs from 3.1.6 to 3.1.7
Bumps [ejs](https://github.com/mde/ejs) from 3.1.6 to 3.1.7.
- [Release notes](https://github.com/mde/ejs/releases)
- [Changelog](https://github.com/mde/ejs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/mde/ejs/compare/v3.1.6...v3.1.7)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-05-02 11:38:29 +00:00
dependabot[bot]
b0769d1cf8 Bump ansi-regex from 3.0.0 to 3.0.1
Bumps [ansi-regex](https://github.com/chalk/ansi-regex) from 3.0.0 to 3.0.1.
- [Release notes](https://github.com/chalk/ansi-regex/releases)
- [Commits](https://github.com/chalk/ansi-regex/compare/v3.0.0...v3.0.1)

---
updated-dependencies:
- dependency-name: ansi-regex
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-05-02 11:38:13 +00:00
dependabot[bot]
5befd67270 Bump dns-packet from 1.3.1 to 1.3.4
Bumps [dns-packet](https://github.com/mafintosh/dns-packet) from 1.3.1 to 1.3.4.
- [Release notes](https://github.com/mafintosh/dns-packet/releases)
- [Changelog](https://github.com/mafintosh/dns-packet/blob/master/CHANGELOG.md)
- [Commits](https://github.com/mafintosh/dns-packet/compare/v1.3.1...v1.3.4)

---
updated-dependencies:
- dependency-name: dns-packet
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-05-02 11:38:12 +00:00
dependabot[bot]
59bdeb67bc Bump trim-newlines from 3.0.0 to 3.0.1
Bumps [trim-newlines](https://github.com/sindresorhus/trim-newlines) from 3.0.0 to 3.0.1.
- [Release notes](https://github.com/sindresorhus/trim-newlines/releases)
- [Commits](https://github.com/sindresorhus/trim-newlines/commits)

---
updated-dependencies:
- dependency-name: trim-newlines
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-05-02 11:38:10 +00:00
dependabot[bot]
2d1de2cac8 Bump kind-of from 6.0.2 to 6.0.3
Bumps [kind-of](https://github.com/jonschlinkert/kind-of) from 6.0.2 to 6.0.3.
- [Release notes](https://github.com/jonschlinkert/kind-of/releases)
- [Changelog](https://github.com/jonschlinkert/kind-of/blob/master/CHANGELOG.md)
- [Commits](https://github.com/jonschlinkert/kind-of/compare/6.0.2...6.0.3)

---
updated-dependencies:
- dependency-name: kind-of
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-05-02 11:38:09 +00:00
Alejandro Celaya
d3f180f270 Merge pull request #632 from acelaya-forks/feature/jest-28
Feature/jest 28
2022-05-02 13:36:58 +02:00
Alejandro Celaya
5482766d03 Updated changelog 2022-05-02 13:24:44 +02:00
Alejandro Celaya
dfbe43ef02 Added missing configs to fixt tests with Jest 28 2022-05-02 13:22:38 +02:00
Alejandro Celaya
ed7bb20bbb Updated to jest 28 2022-05-02 12:08:47 +02:00
Alejandro Celaya
57a2a03469 Merge pull request #631 from acelaya-forks/feature/react-chartjs-4
Feature/react chartjs 4
2022-05-02 11:57:04 +02:00
Alejandro Celaya
df9c1a88fc Updated changelog 2022-05-02 11:52:16 +02:00
Alejandro Celaya
c8b78d04e2 Fixed jest config to transform react-chartjs-2 lib 2022-05-02 11:51:17 +02:00
Alejandro Celaya
c516969686 Updated to react-chartjs 4 2022-05-02 11:35:05 +02:00
Alejandro Celaya
fd4295ade8 Merge pull request #630 from acelaya-forks/feature/react-testing-lib
Feature/react testing lib
2022-05-02 11:04:12 +02:00
Alejandro Celaya
39d4f8c73d Migrated Checkbox test to rteact testing library 2022-05-02 10:21:18 +02:00
Alejandro Celaya
d9b55f1d95 Updated changelog 2022-05-02 10:03:27 +02:00
Alejandro Celaya
66d8a32f49 Migrated SimpleCard test to react testing library 2022-05-02 10:01:29 +02:00
Alejandro Celaya
8b091a7b23 Migrated ShortUrlCreationSetting test to react testing library 2022-05-02 09:48:49 +02:00
Alejandro Celaya
8edb3dc923 Migrated NonOrphanVisits test to react testing library 2022-05-01 16:52:33 +02:00
Alejandro Celaya
a93d1e821d Merge pull request #629 from acelaya-forks/feature/shlink-2.6
Feature/shlink 2.6
2022-05-01 11:07:42 +02:00
Alejandro Celaya
9c5cad7571 Updated changelog 2022-05-01 11:01:57 +02:00
Alejandro Celaya
b970b38c29 Removed check on title support 2022-05-01 11:00:46 +02:00
Alejandro Celaya
763ef207f1 Removed conditional orphan visits section 2022-05-01 10:50:06 +02:00
Alejandro Celaya
a949ec9e8e Removed checks for QR code features that were not supported on versions older than 2.6 2022-05-01 10:44:12 +02:00
Alejandro Celaya
9518ad9bb4 Dropped support to send tags to the PATCH endpoint 2022-05-01 10:30:51 +02:00
Alejandro Celaya
5a0d67e409 Merge pull request #624 from acelaya-forks/feature/domain-visits
Feature/domain visits
2022-04-30 12:07:24 +02:00
Alejandro Celaya
e6932e7353 Created DomainVisits test 2022-04-30 12:01:50 +02:00
Alejandro Celaya
ddb08f4d2e Removed non-needed components which were just wrapping another component that could directly be used instead 2022-04-30 11:00:19 +02:00
Alejandro Celaya
368f7acd2d Updated react and react-dom to v18.1 2022-04-30 09:24:53 +02:00
Alejandro Celaya
6714f90c37 Fixed linting issues 2022-04-30 09:19:43 +02:00
Alejandro Celaya
bbaa4d0f05 Improved domainVisits reducer covering new visits for different domains and default domain 2022-04-30 09:18:53 +02:00
Alejandro Celaya
3aa990a1b0 Created domainVisits reducer test 2022-04-30 09:11:30 +02:00
Alejandro Celaya
464ee11d0d Added react testing library and add test for DomainDropdown 2022-04-30 09:05:12 +02:00
Alejandro Celaya
f909d38130 Updated changelog 2022-04-24 18:55:34 +02:00
Alejandro Celaya
77e59c886d Added test for ShlinkApiClient.getDomainVisits 2022-04-24 18:54:44 +02:00
Alejandro Celaya
05254326cb Implemented domain visits section 2022-04-24 18:36:25 +02:00
Alejandro Celaya
932dec3bde Added dropdown in domains section, to allow multiple options over domains 2022-04-24 13:10:04 +02:00
Alejandro Celaya
e976a0c716 Merge pull request #621 from acelaya-forks/feature/react-18
Feature/react 18
2022-04-24 11:34:17 +02:00
Alejandro Celaya
71b68562db Fixed types in Overview component test 2022-04-24 11:28:32 +02:00
Alejandro Celaya
460d2d23ce Updated changelog 2022-04-24 11:23:09 +02:00
Alejandro Celaya
6aee08b866 Ensured react-leaflet is transformed with jest, until updated to jest 28 2022-04-24 11:21:01 +02:00
Alejandro Celaya
271b19a4ec Fixed no longer implicit children prop in FunctionalComponent type 2022-04-24 10:39:11 +02:00
Alejandro Celaya
d5b1dc5bff Updated to stable packages with support for react 18 2022-04-23 11:52:39 +02:00
Alejandro Celaya
965e69c525 Fixed reference error 2022-04-03 10:10:10 +02:00
Alejandro Celaya
bd549c8642 Updated to latest react-redux beta 2022-04-03 09:43:22 +02:00
Alejandro Celaya
66edaed3a0 Updated to react 18 2022-04-02 08:58:48 +02:00
Alejandro Celaya
79831322fd Merge pull request #618 from acelaya-forks/feature/latest-error
Fixed call to an invalid function in prod envs
2022-04-02 08:41:07 +02:00
Alejandro Celaya
ed1c5a2197 Fixed call to an invalid function in prod envs 2022-04-02 08:32:54 +02:00
Alejandro Celaya
6d32379b67 Merge pull request #615 from acelaya-forks/feature/csvjson-update
Feature/csvjson update
2022-03-31 20:36:13 +02:00
Alejandro Celaya
56a62ae505 Added tests for new CSV-JSON helper functions 2022-03-31 20:32:39 +02:00
Alejandro Celaya
50072f5997 Updated changelog 2022-03-31 20:20:02 +02:00
Alejandro Celaya
e875e05538 Replaced unmaintained dependency 2022-03-31 20:18:05 +02:00
Alejandro Celaya
e2c8551baf Merge pull request #614 from shlinkio/acelaya-patch-1
Removed comment
2022-03-27 17:45:51 +02:00
Alejandro Celaya
1b0a2811e0 Removed comment 2022-03-27 17:41:29 +02:00
Alejandro Celaya
9cac4d23a7 Merge pull request #611 from acelaya-forks/feature/new-coding-style
Feature/new coding style
2022-03-27 09:48:39 +02:00
Alejandro Celaya
b323ddbd33 Temporarily disabled som a11y linting rules 2022-03-27 09:44:47 +02:00
Alejandro Celaya
98450ebec3 Removed no longer needed eslint disable comments 2022-03-26 13:07:58 +01:00
Alejandro Celaya
28a5166f56 Updated changelog 2022-03-26 12:59:54 +01:00
Alejandro Celaya
4f128b3fe8 Fixed tests 2022-03-26 12:46:32 +01:00
Alejandro Celaya
fd5060b996 Updated to latest eslint 2022-03-26 12:26:46 +01:00
Alejandro Celaya
a2df486280 Updated to airbnb coding styles 2022-03-26 12:17:42 +01:00
Alejandro Celaya
4e9b19afd1 Merge pull request #609 from acelaya-forks/feature/uneject
Feature/uneject
2022-03-25 17:48:04 +01:00
Alejandro Celaya
4d78949b8d Removed test script and used jest directly instead 2022-03-25 17:43:15 +01:00
Alejandro Celaya
13bafdc924 Disabled false positive eslint rule 2022-03-25 17:40:18 +01:00
Alejandro Celaya
ea95e8e7b5 Disabled linting while building 2022-03-25 17:27:41 +01:00
Alejandro Celaya
eaa1a2f2ca Added explicit dependency on webpack 5.70 2022-03-25 17:13:36 +01:00
Alejandro Celaya
9d6121903e Fixed some dependency references 2022-03-25 16:59:37 +01:00
Alejandro Celaya
2795bf050e Dejected project back to react-scripts, where posible 2022-03-24 21:27:33 +01:00
Alejandro Celaya
0e4e430673 Fixed margin in button 2022-03-24 20:23:46 +01:00
391 changed files with 30650 additions and 39213 deletions

View File

@@ -1,24 +1,16 @@
{
"root": true,
"extends": [
"@shlinkio/js-coding-standard"
],
"plugins": ["jest"],
"env": {
"jest/globals": true
},
"parserOptions": {
"tsconfigRootDir": ".",
"createDefaultProgram": true
},
"globals": {
"process": true,
"setImmediate": true
"project": "./tsconfig.json"
},
"ignorePatterns": ["src/service*.ts"],
"rules": {
"complexity": "off",
"@typescript-eslint/no-unnecessary-type-assertion": "off",
"@typescript-eslint/no-unsafe-return": "off",
"@typescript-eslint/no-unsafe-call": "off"
"jsx-a11y/control-has-associated-label": "off",
"jsx-a11y/label-has-associated-control": "off",
"jsx-a11y/click-events-have-key-events": "off",
"jsx-a11y/no-static-element-interactions": "off"
}
}

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

@@ -1,4 +1,4 @@
name: Build docker image
name: Build and publish docker image
on:
push:
@@ -9,20 +9,8 @@ on:
jobs:
build:
runs-on: ubuntu-20.04
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
with:
version: latest
- name: Login to docker hub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
- name: Build the image
run: bash ./scripts/docker/build
uses: shlinkio/github-actions/.github/workflows/docker-build-and-publish.yml@main
secrets: inherit
with:
image-name: shlinkio/shlink-web-client
version-arg-name: VERSION

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

@@ -1,11 +1,5 @@
{
"extends": [
"stylelint-config-adidas",
"stylelint-config-adidas-bem",
"stylelint-config-recommended-scss"
],
"syntax": "scss",
"plugins": [
"stylelint-scss"
"@shlinkio/stylelint-config-css-coding-standard"
]
}

View File

@@ -4,21 +4,12 @@ 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.6.0] - 2022-03-17
## [3.7.3] - 2022-09-13
### 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.
* [#703](https://github.com/shlinkio/shlink-web-client/issues/703) Added support to publish docker image in GHCR.
### 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.
* *Nothing*
### Deprecated
* *Nothing*
@@ -27,7 +18,95 @@ 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.
* [#709](https://github.com/shlinkio/shlink-web-client/issues/709) Fixed visits not being displayed after a large loading has finished.
## [3.7.2] - 2022-08-07
### Added
* [#671](https://github.com/shlinkio/shlink-web-client/issues/671) Added proper color-scheme in root element based on selected theme.
### Changed
* [#688](https://github.com/shlinkio/shlink-web-client/issues/688) Finalized migration from enzyme to react-testing-library.
### Deprecated
* *Nothing*
### Removed
* *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*
## [3.6.0] - 2022-03-17
### Added
* [#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/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*
### Removed
* *Nothing*
### Fixed
* [#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
@@ -51,27 +130,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 && 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

@@ -8,8 +8,4 @@ module.exports = {
},
],
],
plugins: [
'@babel/plugin-proposal-optional-chaining',
'@babel/plugin-proposal-nullish-coalescing-operator',
],
};

View File

@@ -1,101 +0,0 @@
/* eslint-disable @typescript-eslint/prefer-nullish-coalescing, @typescript-eslint/promise-function-async, @typescript-eslint/prefer-optional-chain */
const fs = require('fs');
const path = require('path');
const paths = require('./paths');
// Make sure that including paths.js after env.js will read .env variables.
delete require.cache[require.resolve('./paths')];
const { NODE_ENV } = process.env;
if (!NODE_ENV) {
throw new Error(
'The NODE_ENV environment variable is required but was not specified.',
);
}
// https://github.com/bkeepers/dotenv#what-other-env-files-can-i-use
const dotenvFiles = [
`${paths.dotenv}.${NODE_ENV}.local`,
`${paths.dotenv}.${NODE_ENV}`,
// Don't include `.env.local` for `test` environment
// since normally you expect tests to produce the same
// results for everyone
NODE_ENV !== 'test' && `${paths.dotenv}.local`,
paths.dotenv,
].filter(Boolean);
// Load environment variables from .env* files. Suppress warnings using silent
// if this file is missing. dotenv will never modify any environment variables
// that have already been set. Variable expansion is supported in .env files.
// https://github.com/motdotla/dotenv
// https://github.com/motdotla/dotenv-expand
dotenvFiles.forEach((dotenvFile) => {
if (fs.existsSync(dotenvFile)) {
require('dotenv-expand')(
require('dotenv').config({
path: dotenvFile,
}),
);
}
});
// We support resolving modules according to `NODE_PATH`.
// This lets you use absolute paths in imports inside large monorepos:
// https://github.com/facebook/create-react-app/issues/253.
// It works similar to `NODE_PATH` in Node itself:
// https://nodejs.org/api/modules.html#modules_loading_from_the_global_folders
// Note that unlike in Node, only *relative* paths from `NODE_PATH` are honored.
// Otherwise, we risk importing Node.js core modules into an app instead of Webpack shims.
// https://github.com/facebook/create-react-app/issues/1023#issuecomment-265344421
// We also resolve them to make sure all tools using them work consistently.
const appDirectory = fs.realpathSync(process.cwd());
process.env.NODE_PATH = (process.env.NODE_PATH || '')
.split(path.delimiter)
.filter((folder) => folder && !path.isAbsolute(folder))
.map((folder) => path.resolve(appDirectory, folder))
.join(path.delimiter);
// Grab NODE_ENV and REACT_APP_* environment variables and prepare them to be
// injected into the application via DefinePlugin in Webpack configuration.
const REACT_APP = /^REACT_APP_/i;
function getClientEnvironment(publicUrl) {
const raw = Object.keys(process.env)
.filter((key) => REACT_APP.test(key))
.reduce(
(env, key) => {
env[key] = process.env[key];
return env;
},
{
// Useful for determining whether were running in production mode.
// Most importantly, it switches React into the correct mode.
NODE_ENV: process.env.NODE_ENV || 'development',
// Useful for resolving the correct path to static assets in `public`.
// For example, <img src={process.env.PUBLIC_URL + '/img/logo.png'} />.
// This should only be used as an escape hatch. Normally you would put
// images into the `src` and `import` them in code to get their paths.
PUBLIC_URL: publicUrl,
},
);
// Stringify all values so we can feed into Webpack DefinePlugin
const stringified = {
'process.env': Object.keys(raw).reduce((env, key) => {
env[key] = JSON.stringify(raw[key]);
return env;
}, {}),
};
return { raw, stringified };
}
module.exports = getClientEnvironment;

View File

@@ -0,0 +1,12 @@
// This is a custom Jest transformer turning style imports into empty objects.
// http://facebook.github.io/jest/docs/en/webpack.html
module.exports = {
process() {
return { code: 'module.exports = {};' };
},
getCacheKey() {
// The output is always the same.
return 'cssTransform';
},
};

View File

@@ -24,6 +24,8 @@ module.exports = {
};`;
}
return `module.exports = ${assetFilename};`;
return {
code: `module.exports = ${assetFilename};`
};
},
};

View File

@@ -0,0 +1,8 @@
import '@testing-library/jest-dom';
import 'jest-canvas-mock';
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

@@ -1,89 +0,0 @@
/* eslint-disable @typescript-eslint/prefer-nullish-coalescing, @typescript-eslint/promise-function-async, @typescript-eslint/prefer-optional-chain */
const path = require('path');
const fs = require('fs');
const url = require('url');
// Make sure any symlinks in the project folder are resolved:
// https://github.com/facebook/create-react-app/issues/637
const appDirectory = fs.realpathSync(process.cwd());
const resolveApp = (relativePath) => path.resolve(appDirectory, relativePath);
const envPublicUrl = process.env.PUBLIC_URL;
function ensureSlash(inputPath, needsSlash) {
const hasSlash = inputPath.endsWith('/');
if (hasSlash && !needsSlash) {
return inputPath.substr(0, inputPath.length - 1);
} else if (!hasSlash && needsSlash) {
return `${inputPath}/`;
}
return inputPath;
}
const getPublicUrl = (appPackageJson) =>
envPublicUrl || require(appPackageJson).homepage;
// We use `PUBLIC_URL` environment variable or "homepage" field to infer
// "public path" at which the app is served.
// Webpack needs to know it to put the right <script> hrefs into HTML even in
// single-page apps that may serve index.html for nested URLs like /todos/42.
// We can't use a relative path in HTML because we don't want to load something
// like /todos/42/static/js/bundle.7289d.js. We have to know the root.
function getServedPath(appPackageJson) {
const publicUrl = getPublicUrl(appPackageJson);
const servedUrl =
envPublicUrl || (publicUrl ? url.parse(publicUrl).pathname : '/');
return ensureSlash(servedUrl, true);
}
const moduleFileExtensions = [
'web.mjs',
'mjs',
'web.js',
'js',
'web.ts',
'ts',
'web.tsx',
'tsx',
'json',
'web.jsx',
'jsx',
];
// Resolve file paths in the same order as webpack
const resolveModule = (resolveFn, filePath) => {
const extension = moduleFileExtensions.find((extension) =>
fs.existsSync(resolveFn(`${filePath}.${extension}`)));
if (extension) {
return resolveFn(`${filePath}.${extension}`);
}
return resolveFn(`${filePath}.js`);
};
// config after eject: we're in ./config/
module.exports = {
dotenv: resolveApp('.env'),
appPath: resolveApp('.'),
appBuild: resolveApp('build'),
appPublic: resolveApp('public'),
appHtml: resolveApp('public/index.html'),
appIndexJs: resolveModule(resolveApp, 'src/index'),
appPackageJson: resolveApp('package.json'),
appSrc: resolveApp('src'),
appTsConfig: resolveApp('tsconfig.json'),
yarnLockFile: resolveApp('yarn.lock'),
testsSetup: resolveModule(resolveApp, 'src/setupTests'),
proxySetup: resolveApp('src/setupProxy.js'),
appNodeModules: resolveApp('node_modules'),
publicUrl: getPublicUrl(resolveApp('package.json')),
servedPath: getServedPath(resolveApp('package.json')),
swSrc: resolveModule(resolveApp, 'src/service-worker'),
};
module.exports.moduleFileExtensions = moduleFileExtensions;

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

@@ -1,674 +0,0 @@
const fs = require('fs');
const path = require('path');
const webpack = require('webpack');
const resolve = require('resolve');
const PnpWebpackPlugin = require('pnp-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CaseSensitivePathsPlugin = require('case-sensitive-paths-webpack-plugin');
const InlineChunkHtmlPlugin = require('react-dev-utils/InlineChunkHtmlPlugin');
const TerserPlugin = require('terser-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
const safePostCssParser = require('postcss-safe-parser');
const ManifestPlugin = require('webpack-manifest-plugin');
const InterpolateHtmlPlugin = require('react-dev-utils/InterpolateHtmlPlugin');
const WorkboxWebpackPlugin = require('workbox-webpack-plugin');
const WatchMissingNodeModulesPlugin = require('react-dev-utils/WatchMissingNodeModulesPlugin');
const ModuleScopePlugin = require('react-dev-utils/ModuleScopePlugin');
const getCSSModuleLocalIdent = require('react-dev-utils/getCSSModuleLocalIdent');
const ModuleNotFoundPlugin = require('react-dev-utils/ModuleNotFoundPlugin');
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin-alt');
const typescriptFormatter = require('react-dev-utils/typescriptFormatter');
const getClientEnvironment = require('./env');
const paths = require('./paths');
// Source maps are resource heavy and can cause out of memory issue for large source files.
const shouldUseSourceMap = process.env.GENERATE_SOURCEMAP !== 'false';
// Some apps do not need the benefits of saving a web request, so not inlining the chunk
// makes for a smoother build process.
const shouldInlineRuntimeChunk = process.env.INLINE_RUNTIME_CHUNK !== 'false';
// Check if TypeScript is setup
const useTypeScript = fs.existsSync(paths.appTsConfig);
// Get the path to the uncompiled service worker (if it exists).
const swSrc = paths.swSrc;
// style files regexes
const cssRegex = /\.css$/;
const cssModuleRegex = /\.module\.css$/;
const sassRegex = /\.(scss|sass)$/;
const sassModuleRegex = /\.module\.(scss|sass)$/;
// This is the production and development configuration.
// It is focused on developer experience, fast rebuilds, and a minimal bundle.
/* eslint-disable complexity */
module.exports = (webpackEnv) => {
const isEnvDevelopment = webpackEnv === 'development';
const isEnvProduction = webpackEnv === 'production';
// Webpack uses `publicPath` to determine where the app is being served from.
// It requires a trailing slash, or the file assets will get an incorrect path.
// In development, we always serve from the root. This makes config easier.
const publicPath = isEnvProduction
? paths.servedPath
: isEnvDevelopment && '/';
// Some apps do not use client-side routing with pushState.
// For these, "homepage" can be set to "." to enable relative asset paths.
const shouldUseRelativeAssetPaths = publicPath === './';
// `publicUrl` is just like `publicPath`, but we will provide it to our app
// as %PUBLIC_URL% in `index.html` and `process.env.PUBLIC_URL` in JavaScript.
// Omit trailing slash as %PUBLIC_URL%/xyz looks better than %PUBLIC_URL%xyz.
const publicUrl = isEnvProduction
? publicPath.slice(0, -1)
: isEnvDevelopment && '';
// Get environment variables to inject into our app.
const env = getClientEnvironment(publicUrl);
// common function to get style loaders
const getStyleLoaders = (cssOptions, preProcessor) => {
const loaders = [
isEnvDevelopment && require.resolve('style-loader'),
isEnvProduction && {
loader: MiniCssExtractPlugin.loader,
options: Object.assign(
{},
shouldUseRelativeAssetPaths ? { publicPath: '../../' } : undefined,
),
},
{
loader: require.resolve('css-loader'),
options: cssOptions,
},
{
// Options for PostCSS as we reference these options twice
// Adds vendor prefixing based on your specified browser support in
// package.json
loader: require.resolve('postcss-loader'),
options: {
// Necessary for external CSS imports to work
// https://github.com/facebook/create-react-app/issues/2677
ident: 'postcss',
plugins: () => [
require('postcss-flexbugs-fixes'),
require('postcss-preset-env')({
autoprefixer: {
flexbox: 'no-2009',
},
stage: 3,
}),
],
sourceMap: isEnvProduction && shouldUseSourceMap,
},
},
].filter(Boolean);
if (preProcessor) {
loaders.push({
loader: require.resolve(preProcessor),
options: {
sourceMap: isEnvProduction && shouldUseSourceMap,
},
});
}
return loaders;
};
return {
mode: isEnvProduction ? 'production' : isEnvDevelopment && 'development',
// Stop compilation early in production
bail: isEnvProduction,
devtool: isEnvProduction
? shouldUseSourceMap
? 'source-map'
: false
: isEnvDevelopment && 'cheap-module-source-map',
// These are the "entry points" to our application.
// This means they will be the "root" imports that are included in JS bundle.
entry: [
// Include an alternative client for WebpackDevServer. A client's job is to
// connect to WebpackDevServer by a socket and get notified about changes.
// When you save a file, the client will either apply hot updates (in case
// of CSS changes), or refresh the page (in case of JS changes). When you
// make a syntax error, this client will display a syntax error overlay.
// Note: instead of the default WebpackDevServer client, we use a custom one
// to bring better experience for Create React App users. You can replace
// the line below with these two lines if you prefer the stock client:
// require.resolve('webpack-dev-server/client') + '?/',
// require.resolve('webpack/hot/dev-server'),
isEnvDevelopment &&
require.resolve('react-dev-utils/webpackHotDevClient'),
// Finally, this is your app's code:
paths.appIndexJs,
// We include the app code last so that if there is a runtime error during
// initialization, it doesn't blow up the WebpackDevServer client, and
// changing JS code would still trigger a refresh.
].filter(Boolean),
output: {
// The build folder.
path: isEnvProduction ? paths.appBuild : undefined,
// Add /* filename */ comments to generated require()s in the output.
pathinfo: isEnvDevelopment,
// There will be one main bundle, and one file per asynchronous chunk.
// In development, it does not produce real files.
filename: isEnvProduction
? 'static/js/[name].[chunkhash:8].js'
: isEnvDevelopment && 'static/js/bundle.js',
// There are also additional JS chunk files if you use code splitting.
chunkFilename: isEnvProduction
? 'static/js/[name].[chunkhash:8].chunk.js'
: isEnvDevelopment && 'static/js/[name].chunk.js',
// We inferred the "public path" (such as / or /my-project) from homepage.
// We use "/" in development.
publicPath,
// Point sourcemap entries to original disk location (format as URL on Windows)
devtoolModuleFilenameTemplate: isEnvProduction
? (info) =>
path
.relative(paths.appSrc, info.absoluteResourcePath)
.replace(/\\/g, '/')
: isEnvDevelopment &&
((info) => path.resolve(info.absoluteResourcePath).replace(/\\/g, '/')),
},
optimization: {
minimize: isEnvProduction,
minimizer: [
// This is only used in production mode
new TerserPlugin({
terserOptions: {
parse: {
// we want terser to parse ecma 8 code. However, we don't want it
// to apply any minfication steps that turns valid ecma 5 code
// into invalid ecma 5 code. This is why the 'compress' and 'output'
// sections only apply transformations that are ecma 5 safe
// https://github.com/facebook/create-react-app/pull/4234
ecma: 8,
},
compress: {
ecma: 5,
warnings: false,
// Disabled because of an issue with Uglify breaking seemingly valid code:
// https://github.com/facebook/create-react-app/issues/2376
// Pending further investigation:
// https://github.com/mishoo/UglifyJS2/issues/2011
comparisons: false,
// Disabled because of an issue with Terser breaking valid code:
// https://github.com/facebook/create-react-app/issues/5250
// Pending futher investigation:
// https://github.com/terser-js/terser/issues/120
inline: 2,
},
mangle: {
safari10: true,
},
output: {
ecma: 5,
comments: false,
// Turned on because emoji and regex is not minified properly using default
// https://github.com/facebook/create-react-app/issues/2488
ascii_only: true, // eslint-disable-line @typescript-eslint/camelcase
},
},
// Use multi-process parallel running to improve the build speed
// Default number of concurrent runs: os.cpus().length - 1
parallel: true,
// Enable file caching
cache: true,
sourceMap: shouldUseSourceMap,
}),
// This is only used in production mode
new OptimizeCSSAssetsPlugin({
cssProcessorOptions: {
parser: safePostCssParser,
map: shouldUseSourceMap
? {
// `inline: false` forces the sourcemap to be output into a
// separate file
inline: false,
// `annotation: true` appends the sourceMappingURL to the end of
// the css file, helping the browser find the sourcemap
annotation: true,
}
: false,
},
}),
],
// Automatically split vendor and commons
// https://twitter.com/wSokra/status/969633336732905474
// https://medium.com/webpack/webpack-4-code-splitting-chunk-graph-and-the-splitchunks-optimization-be739a861366
splitChunks: {
chunks: 'all',
name: false,
},
// Keep the runtime chunk separated to enable long term caching
// https://twitter.com/wSokra/status/969679223278505985
runtimeChunk: true,
},
resolve: {
// This allows you to set a fallback for where Webpack should look for modules.
// We placed these paths second because we want `node_modules` to "win"
// if there are any conflicts. This matches Node resolution mechanism.
// https://github.com/facebook/create-react-app/issues/253
modules: [ 'node_modules' ].concat(
// It is guaranteed to exist because we tweak it in `env.js`
process.env.NODE_PATH.split(path.delimiter).filter(Boolean),
),
// These are the reasonable defaults supported by the Node ecosystem.
// We also include JSX as a common component filename extension to support
// some tools, although we do not recommend using it, see:
// https://github.com/facebook/create-react-app/issues/290
// `web` extension prefixes have been added for better support
// for React Native Web.
extensions: paths.moduleFileExtensions
.map((ext) => `.${ext}`)
.filter((ext) => useTypeScript || !ext.includes('ts')),
alias: {
// Support React Native Web
// https://www.smashingmagazine.com/2016/08/a-glimpse-into-the-future-with-react-native-for-web/
'react-native': 'react-native-web',
},
plugins: [
// Adds support for installing with Plug'n'Play, leading to faster installs and adding
// guards against forgotten dependencies and such.
PnpWebpackPlugin,
// Prevents users from importing files from outside of src/ (or node_modules/).
// This often causes confusion because we only process files within src/ with babel.
// To fix this, we prevent you from importing files out of src/ -- if you'd like to,
// please link the files into your node_modules/ and let module-resolution kick in.
// Make sure your source files are compiled, as they will not be processed in any way.
new ModuleScopePlugin(paths.appSrc, [ paths.appPackageJson ]),
],
},
resolveLoader: {
plugins: [
// Also related to Plug'n'Play, but this time it tells Webpack to load its loaders
// from the current package.
PnpWebpackPlugin.moduleLoader(module),
],
},
module: {
strictExportPresence: true,
rules: [
// Disable require.ensure as it's not a standard language feature.
{ parser: { requireEnsure: false } },
// First, run the linter.
// It's important to do this before Babel processes the JS.
{
test: /\.(js|mjs|jsx)$/,
enforce: 'pre',
use: [
{
options: {
formatter: require.resolve('react-dev-utils/eslintFormatter'),
eslintPath: require.resolve('eslint'),
},
loader: require.resolve('eslint-loader'),
},
],
include: paths.appSrc,
},
{
// "oneOf" will traverse all following loaders until one will
// match the requirements. When no loader matches it will fall
// back to the "file" loader at the end of the loader list.
oneOf: [
// "url" loader works like "file" loader except that it embeds assets
// smaller than specified limit in bytes as data URLs to avoid requests.
// A missing `test` is equivalent to a match.
{
test: [ /\.bmp$/, /\.gif$/, /\.jpe?g$/, /\.png$/ ],
loader: require.resolve('url-loader'),
options: {
limit: 10000,
name: 'static/media/[name].[hash:8].[ext]',
},
},
// Process application JS with Babel.
// The preset includes JSX, Flow, TypeScript, and some ESnext features.
{
test: /\.(js|mjs|jsx|ts|tsx)$/,
include: paths.appSrc,
loader: require.resolve('babel-loader'),
options: {
customize: require.resolve(
'babel-preset-react-app/webpack-overrides',
),
plugins: [
[
require.resolve('babel-plugin-named-asset-import'),
{
loaderMap: {
svg: {
ReactComponent:
'@svgr/webpack?-prettier,-svgo![path]',
},
},
},
],
],
// This is a feature of `babel-loader` for webpack (not Babel itself).
// It enables caching results in ./node_modules/.cache/babel-loader/
// directory for faster rebuilds.
cacheDirectory: true,
cacheCompression: isEnvProduction,
compact: isEnvProduction,
},
},
// Process any JS outside of the app with Babel.
// Unlike the application JS, we only compile the standard ES features.
{
test: /\.(js|mjs)$/,
exclude: /@babel(?:\/|\\{1,2})runtime/,
loader: require.resolve('babel-loader'),
options: {
babelrc: false,
configFile: false,
compact: false,
presets: [
[
require.resolve('babel-preset-react-app/dependencies'),
{ helpers: true },
],
],
cacheDirectory: true,
cacheCompression: isEnvProduction,
// If an error happens in a package, it's possible to be
// because it was compiled. Thus, we don't want the browser
// debugger to show the original code. Instead, the code
// being evaluated would be much more helpful.
sourceMaps: false,
},
},
// "postcss" loader applies autoprefixer to our CSS.
// "css" loader resolves paths in CSS and adds assets as dependencies.
// "style" loader turns CSS into JS modules that inject <style> tags.
// In production, we use MiniCSSExtractPlugin to extract that CSS
// to a file, but in development "style" loader enables hot editing
// of CSS.
// By default we support CSS Modules with the extension .module.css
{
test: cssRegex,
exclude: cssModuleRegex,
use: getStyleLoaders({
importLoaders: 1,
sourceMap: isEnvProduction && shouldUseSourceMap,
}),
// Don't consider CSS imports dead code even if the
// containing package claims to have no side effects.
// Remove this when webpack adds a warning or an error for this.
// See https://github.com/webpack/webpack/issues/6571
sideEffects: true,
},
// Adds support for CSS Modules (https://github.com/css-modules/css-modules)
// using the extension .module.css
{
test: cssModuleRegex,
use: getStyleLoaders({
importLoaders: 1,
sourceMap: isEnvProduction && shouldUseSourceMap,
modules: true,
getLocalIdent: getCSSModuleLocalIdent,
}),
},
// Opt-in support for SASS (using .scss or .sass extensions).
// By default we support SASS Modules with the
// extensions .module.scss or .module.sass
{
test: sassRegex,
exclude: sassModuleRegex,
use: getStyleLoaders(
{
importLoaders: 2,
sourceMap: isEnvProduction && shouldUseSourceMap,
},
'sass-loader',
),
// Don't consider CSS imports dead code even if the
// containing package claims to have no side effects.
// Remove this when webpack adds a warning or an error for this.
// See https://github.com/webpack/webpack/issues/6571
sideEffects: true,
},
// Adds support for CSS Modules, but using SASS
// using the extension .module.scss or .module.sass
{
test: sassModuleRegex,
use: getStyleLoaders(
{
importLoaders: 2,
sourceMap: isEnvProduction && shouldUseSourceMap,
modules: true,
getLocalIdent: getCSSModuleLocalIdent,
},
'sass-loader',
),
},
// "file" loader makes sure those assets get served by WebpackDevServer.
// When you `import` an asset, you get its (virtual) filename.
// In production, they would get copied to the `build` folder.
// This loader doesn't use a "test" so it will catch all modules
// that fall through the other loaders.
{
loader: require.resolve('file-loader'),
// Exclude `js` files to keep "css" loader working as it injects
// its runtime that would otherwise be processed through "file" loader.
// Also exclude `html` and `json` extensions so they get processed
// by webpacks internal loaders.
exclude: [ /\.(js|mjs|jsx|ts|tsx)$/, /\.html$/, /\.json$/ ],
options: {
name: 'static/media/[name].[hash:8].[ext]',
},
},
// ** STOP ** Are you adding a new loader?
// Make sure to add the new loader(s) before the "file" loader.
],
},
],
},
plugins: [
// Generates an `index.html` file with the <script> injected.
new HtmlWebpackPlugin(
Object.assign(
{},
{
inject: true,
template: paths.appHtml,
},
isEnvProduction
? {
minify: {
removeComments: true,
collapseWhitespace: true,
removeRedundantAttributes: true,
useShortDoctype: true,
removeEmptyAttributes: true,
removeStyleLinkTypeAttributes: true,
keepClosingSlash: true,
minifyJS: true,
minifyCSS: true,
minifyURLs: true,
},
}
: undefined,
),
),
// Inlines the webpack runtime script. This script is too small to warrant
// a network request.
isEnvProduction &&
shouldInlineRuntimeChunk &&
new InlineChunkHtmlPlugin(HtmlWebpackPlugin, [ /runtime~.+[.]js/ ]),
// Makes some environment variables available in index.html.
// The public URL is available as %PUBLIC_URL% in index.html, e.g.:
// <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
// In production, it will be an empty string unless you specify "homepage"
// in `package.json`, in which case it will be the pathname of that URL.
// In development, this will be an empty string.
new InterpolateHtmlPlugin(HtmlWebpackPlugin, env.raw),
// This gives some necessary context to module not found errors, such as
// the requesting resource.
new ModuleNotFoundPlugin(paths.appPath),
// Makes some environment variables available to the JS code, for example:
// if (process.env.NODE_ENV === 'production') { ... }. See `./env.js`.
// It is absolutely essential that NODE_ENV is set to production
// during a production build.
// Otherwise React will be compiled in the very slow development mode.
new webpack.DefinePlugin(env.stringified),
// This is necessary to emit hot updates (currently CSS only):
isEnvDevelopment && new webpack.HotModuleReplacementPlugin(),
// Watcher doesn't work well if you mistype casing in a path so we use
// a plugin that prints an error when you attempt to do this.
// See https://github.com/facebook/create-react-app/issues/240
isEnvDevelopment && new CaseSensitivePathsPlugin(),
// If you require a missing module and then `npm install` it, you still have
// to restart the development server for Webpack to discover it. This plugin
// makes the discovery automatic so you don't have to restart.
// See https://github.com/facebook/create-react-app/issues/186
isEnvDevelopment &&
new WatchMissingNodeModulesPlugin(paths.appNodeModules),
isEnvProduction &&
new MiniCssExtractPlugin({
// Options similar to the same options in webpackOptions.output
// both options are optional
filename: 'static/css/[name].[contenthash:8].css',
chunkFilename: 'static/css/[name].[contenthash:8].chunk.css',
}),
// Generate a manifest file which contains a mapping of all asset filenames
// to their corresponding output file so that tools can pick it up without
// having to parse `index.html`.
new ManifestPlugin({
fileName: 'asset-manifest.json',
publicPath,
}),
// Moment.js is an extremely popular library that bundles large locale files
// by default due to how Webpack interprets its code. This is a practical
// solution that requires the user to opt into importing specific locales.
// https://github.com/jmblog/how-to-optimize-momentjs-with-webpack
// You can remove this if you don't use Moment.js:
new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/),
// Generate a service worker script that will precache, and keep up to date,
// the HTML & assets that are part of the webpack build.
isEnvProduction && fs.existsSync(swSrc) && new WorkboxWebpackPlugin.InjectManifest({
swSrc,
dontCacheBustURLsMatching: /\.[0-9a-f]{8}\./,
exclude: [ /\.map$/, /asset-manifest\.json$/, /LICENSE/ ],
// Bump up the default maximum size (2mb) that's precached,
// to make lazy-loading failure scenarios less likely.
// See https://github.com/cra-template/pwa/issues/13#issuecomment-722667270
maximumFileSizeToCacheInBytes: 5 * 1024 * 1024,
}),
// TypeScript type checking
useTypeScript &&
new ForkTsCheckerWebpackPlugin({
typescript: resolve.sync('typescript', {
basedir: paths.appNodeModules,
}),
async: false,
checkSyntacticErrors: true,
tsconfig: paths.appTsConfig,
compilerOptions: {
module: 'esnext',
moduleResolution: 'node',
resolveJsonModule: true,
isolatedModules: true,
noEmit: true,
jsx: 'preserve',
},
reportFiles: [
'**',
'!**/*.json',
'!**/__tests__/**',
'!**/?(*.)(spec|test).*',
'!**/src/setupProxy.*',
'!**/src/setupTests.*',
],
watch: paths.appSrc,
silent: true,
formatter: typescriptFormatter,
}),
].filter(Boolean),
// Some libraries import Node modules but don't use them in the browser.
// Tell Webpack to provide empty mocks for them so importing them works.
node: {
dgram: 'empty',
fs: 'empty',
net: 'empty',
tls: 'empty',
child_process: 'empty', // eslint-disable-line @typescript-eslint/camelcase
},
// Turn off performance processing because we utilize
// our own hints via the FileSizeReporter
performance: false,
};
};

View File

@@ -1,116 +0,0 @@
/* eslint-disable @typescript-eslint/prefer-nullish-coalescing */
const fs = require('fs');
const errorOverlayMiddleware = require('react-dev-utils/errorOverlayMiddleware');
const evalSourceMapMiddleware = require('react-dev-utils/evalSourceMapMiddleware');
const noopServiceWorkerMiddleware = require('react-dev-utils/noopServiceWorkerMiddleware');
const ignoredFiles = require('react-dev-utils/ignoredFiles');
const paths = require('./paths');
const protocol = process.env.HTTPS === 'true' ? 'https' : 'http';
const host = process.env.HOST || '0.0.0.0';
module.exports = function(proxy, allowedHost) {
return {
// WebpackDevServer 2.4.3 introduced a security fix that prevents remote
// websites from potentially accessing local content through DNS rebinding:
// https://github.com/webpack/webpack-dev-server/issues/887
// https://medium.com/webpack/webpack-dev-server-middleware-security-issues-1489d950874a
// However, it made several existing use cases such as development in cloud
// environment or subdomains in development significantly more complicated:
// https://github.com/facebook/create-react-app/issues/2271
// https://github.com/facebook/create-react-app/issues/2233
// While we're investigating better solutions, for now we will take a
// compromise. Since our WDS configuration only serves files in the `public`
// folder we won't consider accessing them a vulnerability. However, if you
// use the `proxy` feature, it gets more dangerous because it can expose
// remote code execution vulnerabilities in backends like Django and Rails.
// So we will disable the host check normally, but enable it if you have
// specified the `proxy` setting. Finally, we let you override it if you
// really know what you're doing with a special environment variable.
disableHostCheck:
!proxy || process.env.DANGEROUSLY_DISABLE_HOST_CHECK === 'true',
// Enable gzip compression of generated files.
compress: true,
// Silence WebpackDevServer's own logs since they're generally not useful.
// It will still show compile warnings and errors with this setting.
clientLogLevel: 'none',
// By default WebpackDevServer serves physical files from current directory
// in addition to all the virtual build products that it serves from memory.
// This is confusing because those files wont automatically be available in
// production build folder unless we copy them. However, copying the whole
// project directory is dangerous because we may expose sensitive files.
// Instead, we establish a convention that only files in `public` directory
// get served. Our build script will copy `public` into the `build` folder.
// In `index.html`, you can get URL of `public` folder with %PUBLIC_URL%:
// <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
// In JavaScript code, you can access it with `process.env.PUBLIC_URL`.
// Note that we only recommend to use `public` folder as an escape hatch
// for files like `favicon.ico`, `manifest.json`, and libraries that are
// for some reason broken when imported through Webpack. If you just want to
// use an image, put it in `src` and `import` it from JavaScript instead.
contentBase: paths.appPublic,
// By default files from `contentBase` will not trigger a page reload.
watchContentBase: true,
// Enable hot reloading server. It will provide /sockjs-node/ endpoint
// for the WebpackDevServer client so it can learn when the files were
// updated. The WebpackDevServer client is included as an entry point
// in the Webpack development configuration. Note that only changes
// to CSS are currently hot reloaded. JS changes will refresh the browser.
hot: true,
// It is important to tell WebpackDevServer to use the same "root" path
// as we specified in the config. In development, we always serve from /.
publicPath: '/',
// WebpackDevServer is noisy by default so we emit custom message instead
// by listening to the compiler events with `compiler.hooks[...].tap` calls above.
quiet: true,
// Reportedly, this avoids CPU overload on some systems.
// https://github.com/facebook/create-react-app/issues/293
// src/node_modules is not ignored to support absolute imports
// https://github.com/facebook/create-react-app/issues/1065
watchOptions: {
ignored: ignoredFiles(paths.appSrc),
},
// Enable HTTPS if the HTTPS environment variable is set to 'true'
https: protocol === 'https',
host,
overlay: false,
historyApiFallback: {
// Paths with dots should still use the history fallback.
// See https://github.com/facebook/create-react-app/issues/387.
disableDotRule: true,
},
public: allowedHost,
proxy,
before(app, server) {
if (fs.existsSync(paths.proxySetup)) {
// This registers user provided middleware for proxy reasons
require(paths.proxySetup)(app);
}
// This lets us fetch source contents from webpack for the error overlay
app.use(evalSourceMapMiddleware(server));
// This lets us open files from the runtime error overlay.
app.use(errorOverlayMiddleware());
// This service worker file is effectively a 'no-op' that will reset any
// previous service worker registered for the same host:port combination.
// We do this in development to avoid hitting the production cache if
// it used the same host and port.
// https://github.com/facebook/create-react-app/issues/2272#issuecomment-302832432
app.use(noopServiceWorkerMiddleware(paths.publicUrl));
},
};
};

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

@@ -9,29 +9,32 @@ module.exports = {
],
coverageThreshold: {
global: {
statements: 85,
statements: 90,
branches: 80,
functions: 80,
lines: 85,
functions: 85,
lines: 90,
},
},
setupFiles: [ '<rootDir>/config/setupEnzyme.js' ],
testMatch: [ '<rootDir>/test/**/*.test.{ts,tsx}' ],
setupFilesAfterEnv: ['<rootDir>/config/jest/setupTests.ts'],
testMatch: ['<rootDir>/test/**/*.test.{ts,tsx}'],
testEnvironment: 'jsdom',
testURL: 'http://localhost',
testEnvironmentOptions: {
url: 'http://localhost',
},
transform: {
'^.+\\.(ts|tsx|js)$': '<rootDir>/node_modules/babel-jest',
'^(?!.*\\.(ts|tsx|js|json)$)': '<rootDir>/config/jest/fileTransform.js',
'^.+\\.scss$': '<rootDir>/config/jest/cssTransform.js',
'^(?!.*\\.(ts|tsx|js|json|scss)$)': '<rootDir>/config/jest/fileTransform.js',
},
transformIgnorePatterns: [
'<rootDir>/.stryker-tmp',
'[/\\\\]node_modules[/\\\\].+\\.(js|jsx|ts|tsx)$',
'node_modules\/(?!(\@react-leaflet|react-leaflet|leaflet|react-chartjs-2|react-colorful)\/)',
'^.+\\.module\\.scss$',
],
moduleNameMapper: {
'^.+\\.module\\.scss$': 'identity-obj-proxy',
// Reactstrap module resolution does not work in jest for some reason. Manually mapping it solves the problem
'reactstrap': '<rootDir>/node_modules/reactstrap/dist/reactstrap.umd.js',
'react-chartjs-2': '<rootDir>/node_modules/react-chartjs-2/dist/index.js',
'uuid': '<rootDir>/node_modules/uuid/dist/index.js',
},
moduleFileExtensions: [ 'js', 'ts', 'tsx', 'json' ],
moduleFileExtensions: ['js', 'ts', 'tsx', 'json'],
};

53194
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -12,15 +12,16 @@
"lint:fix": "npm run lint:css:fix && npm run lint:js:fix",
"lint:css:fix": "npm run lint:css -- --fix",
"lint:js:fix": "npm run lint:js -- --fix",
"start": "node scripts/start.js",
"serve:build": "serve ./build",
"build": "node scripts/build.js && node scripts/replace-version.js",
"build:dist": "npm run build && node scripts/create-dist-file.js",
"test": "node scripts/test.js --env=jsdom --colors --verbose",
"start": "DISABLE_ESLINT_PLUGIN=true react-scripts start",
"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",
"test:coverage": "npm run test -- --coverage --coverageReporters=text --coverageReporters=text-summary",
"test:ci": "npm run test:coverage -- --coverageReporters=clover",
"test:ci": "npm run test:coverage -- --coverageReporters=clover --ci",
"test:pretty": "npm run test:coverage -- --coverageReporters=html",
"mutate": "./node_modules/.bin/stryker run --concurrency 4"
"test:verbose": "npm run test -- --verbose",
"mutate": "./node_modules/.bin/stryker run --concurrency 4 --ignoreStatic"
},
"dependencies": {
"@fortawesome/fontawesome-free": "^6.0.0",
@@ -35,28 +36,30 @@
"chart.js": "^3.7.1",
"classnames": "^2.3.1",
"compare-versions": "^4.1.3",
"csvjson": "^5.1.0",
"csvtojson": "^2.0.10",
"date-fns": "^2.28.0",
"event-source-polyfill": "^1.0.25",
"json2csv": "^5.0.7",
"leaflet": "^1.7.1",
"qs": "^6.9.6",
"ramda": "^0.27.2",
"react": "^17.0.2",
"react-chartjs-2": "^3.3.0",
"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-dom": "^17.0.2",
"react-external-link": "^1.2.2",
"react-leaflet": "^3.2.5",
"react-redux": "^7.2.6",
"react-router-dom": "^6.2.2",
"react-swipeable": "^6.2.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": "^7.0.0",
"react-tag-autocomplete": "^6.3.0",
"reactstrap": "^9.0.1",
"redux": "^4.1.2",
"redux": "^4.2.0",
"redux-localstorage-simple": "^2.4.1",
"redux-thunk": "^2.4.1",
"stream": "^0.0.2",
"uuid": "^8.3.2",
"workbox-core": "^6.5.1",
"workbox-expiration": "^6.5.1",
@@ -65,84 +68,43 @@
"workbox-strategies": "^6.5.1"
},
"devDependencies": {
"@babel/core": "^7.17.5",
"@babel/plugin-proposal-nullish-coalescing-operator": "^7.16.7",
"@babel/plugin-proposal-optional-chaining": "^7.16.7",
"@shlinkio/eslint-config-js-coding-standard": "~1.2.2",
"@stryker-mutator/core": "^5.6.1",
"@stryker-mutator/jest-runner": "^5.6.1",
"@stryker-mutator/typescript-checker": "^5.6.1",
"@svgr/webpack": "^5.5.0",
"@types/classnames": "^2.3.1",
"@types/enzyme": "^3.10.11",
"@shlinkio/eslint-config-js-coding-standard": "~2.0.2",
"@shlinkio/stylelint-config-css-coding-standard": "~1.0.1",
"@stryker-mutator/core": "^6.0.2",
"@stryker-mutator/jest-runner": "^6.0.2",
"@stryker-mutator/typescript-checker": "^6.0.2",
"@testing-library/jest-dom": "^5.16.4",
"@testing-library/react": "^13.1.1",
"@testing-library/user-event": "^14.1.1",
"@types/jest": "^27.4.1",
"@types/json2csv": "^5.0.3",
"@types/leaflet": "^1.7.9",
"@types/qs": "^6.9.7",
"@types/ramda": "0.27.38",
"@types/react": "^17.0.39",
"@types/react": "^18.0.8",
"@types/react-color": "^3.0.6",
"@types/react-copy-to-clipboard": "^5.0.2",
"@types/react-datepicker": "^4.3.4",
"@types/react-dom": "^17.0.13",
"@types/react-leaflet": "^2.8.2",
"@types/react-redux": "^7.1.23",
"@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",
"autoprefixer": "^10.4.2",
"babel-jest": "^27.5.1",
"babel-loader": "^8.2.3",
"babel-plugin-named-asset-import": "^0.3.8",
"babel-preset-react-app": "10.0.0",
"babel-runtime": "^6.26.0",
"bfj": "^7.0.2",
"case-sensitive-paths-webpack-plugin": "^2.4.0",
"chalk": "^4.1.2",
"css-loader": "^5.0.1",
"dart-sass": "^1.25.0",
"dotenv": "^8.2.0",
"dotenv-expand": "^5.1.0",
"enzyme": "^3.11.0",
"eslint": "^7.13.0",
"eslint-loader": "^4.0.2",
"file-loader": "^6.2.0",
"fork-ts-checker-webpack-plugin-alt": "^0.4.14",
"fs-extra": "^9.0.1",
"html-webpack-plugin": "^4.5.0",
"babel-jest": "^28.0.3",
"chalk": "^5.0.1",
"eslint": "^8.12.0",
"identity-obj-proxy": "^3.0.0",
"jest": "^27.5.1",
"mini-css-extract-plugin": "^1.3.1",
"object-assign": "^4.1.1",
"optimize-css-assets-webpack-plugin": "^5.0.4",
"pnp-webpack-plugin": "^1.7.0",
"postcss": "^8.4.8",
"postcss-flexbugs-fixes": "^4.2.1",
"postcss-loader": "^3.0.0",
"postcss-preset-env": "^6.7.0",
"postcss-safe-parser": "^5.0.2",
"react-dev-utils": "^11.0.0",
"resolve": "^1.22.0",
"jest": "^28.0.3",
"jest-canvas-mock": "^2.4.0",
"jest-environment-jsdom": "^28.0.2",
"react-scripts": "^5.0.1",
"resize-observer-polyfill": "^1.5.1",
"sass": "^1.49.9",
"sass-loader": "^10.1.0",
"serve": "^12.0.0",
"serve": "^13.0.2",
"stryker-cli": "^1.0.2",
"style-loader": "^2.0.0",
"stylelint": "^13.7.2",
"stylelint-config-adidas": "^1.3.0",
"stylelint-config-adidas-bem": "^1.2.0",
"stylelint-config-recommended-scss": "^4.2.0",
"stylelint-scss": "^3.18.0",
"sw-precache-webpack-plugin": "^1.0.0",
"terser-webpack-plugin": "^4.2.3",
"stylelint": "^14.8.2",
"ts-mockery": "^1.2.0",
"typescript": "^4.6.2",
"url-loader": "^4.1.1",
"webpack": "^4.46.0",
"webpack-dev-server": "^3.11.3",
"webpack-manifest-plugin": "^2.2.0",
"whatwg-fetch": "^3.6.2",
"workbox-webpack-plugin": "^6.5.1"
"webpack": "^5.70.0"
},
"browserslist": [
">0.2%",

View File

@@ -1,182 +0,0 @@
/* eslint-disable no-console, @typescript-eslint/prefer-nullish-coalescing, @typescript-eslint/promise-function-async, @typescript-eslint/prefer-optional-chain */
// Do this as the first thing so that any code reading it knows the right env.
process.env.BABEL_ENV = 'production';
process.env.NODE_ENV = 'production';
// Makes the script crash on unhandled rejections instead of silently
// ignoring them. In the future, promise rejections that are not handled will
// terminate the Node.js process with a non-zero exit code.
process.on('unhandledRejection', (err) => {
throw err;
});
// Ensure environment variables are read.
require('../config/env');
const chalk = require('chalk');
const fs = require('fs-extra');
const webpack = require('webpack');
const bfj = require('bfj');
const checkRequiredFiles = require('react-dev-utils/checkRequiredFiles');
const formatWebpackMessages = require('react-dev-utils/formatWebpackMessages');
const FileSizeReporter = require('react-dev-utils/FileSizeReporter');
const printBuildError = require('react-dev-utils/printBuildError');
const { checkBrowsers } = require('react-dev-utils/browsersHelper');
const paths = require('../config/paths');
const configFactory = require('../config/webpack.config');
const { measureFileSizesBeforeBuild, printFileSizesAfterBuild } = FileSizeReporter;
// These sizes are pretty large. We'll warn for bundles exceeding them.
const WARN_AFTER_BUNDLE_GZIP_SIZE = 512 * 1024; // eslint-disable-line
const WARN_AFTER_CHUNK_GZIP_SIZE = 1024 * 1024; // eslint-disable-line
const isInteractive = process.stdout.isTTY;
// Warn and crash if required files are missing
if (!checkRequiredFiles([ paths.appHtml, paths.appIndexJs ])) {
process.exit(1);
}
// Process CLI arguments
const argvSliceStart = 2;
const argv = process.argv.slice(argvSliceStart);
const writeStatsJson = argv.includes('--stats');
// Generate configuration
const config = configFactory('production');
checkBrowsers(paths.appPath, isInteractive)
.then(() =>
// First, read the current file sizes in build directory.
// This lets us display how much they changed later.
measureFileSizesBeforeBuild(paths.appBuild))
.then((previousFileSizes) => {
// Remove all content but keep the directory so that
// if you're in it, you don't end up in Trash
fs.emptyDirSync(paths.appBuild);
// Merge with the public folder
copyPublicFolder();
// Start the webpack build
return build(previousFileSizes);
})
.then(
({ stats, previousFileSizes, warnings }) => {
if (warnings.length) {
console.log(chalk.yellow('Compiled with warnings.\n'));
console.log(warnings.join('\n\n'));
console.log(
`\nSearch for the ${
chalk.underline(chalk.yellow('keywords'))
} to learn more about each warning.`,
);
console.log(
`To ignore, add ${
chalk.cyan('// eslint-disable-next-line')
} to the line before.\n`,
);
} else {
console.log(chalk.green('Compiled successfully.\n'));
}
console.log('File sizes after gzip:\n');
printFileSizesAfterBuild(
stats,
previousFileSizes,
paths.appBuild,
WARN_AFTER_BUNDLE_GZIP_SIZE,
WARN_AFTER_CHUNK_GZIP_SIZE,
);
console.log();
},
(err) => {
console.log(chalk.red('Failed to compile.\n'));
printBuildError(err);
process.exit(1);
},
)
.catch((err) => {
if (err && err.message) {
console.log(err.message);
}
process.exit(1);
});
// Create the production build and print the deployment instructions.
function build(previousFileSizes) {
console.log('Creating an optimized production build...');
const compiler = webpack(config);
return new Promise((resolve, reject) => {
compiler.run((err, stats) => {
let messages;
if (err) {
if (!err.message) {
return reject(err);
}
messages = formatWebpackMessages({
errors: [ err.message ],
warnings: [],
});
} else {
messages = formatWebpackMessages(
stats.toJson({ all: false, warnings: true, errors: true }),
);
}
if (messages.errors.length) {
// Only keep the first error. Others are often indicative
// of the same problem, but confuse the reader with noise.
if (messages.errors.length > 1) {
messages.errors.length = 1;
}
return reject(new Error(messages.errors.join('\n\n')));
}
if (
process.env.CI &&
(typeof process.env.CI !== 'string' ||
process.env.CI.toLowerCase() !== 'false') &&
messages.warnings.length
) {
console.log(
chalk.yellow(
'\nTreating warnings as errors because process.env.CI = true.\n' +
'Most CI servers set it automatically.\n',
),
);
return reject(new Error(messages.warnings.join('\n\n')));
}
const resolveArgs = {
stats,
previousFileSizes,
warnings: messages.warnings,
};
if (writeStatsJson) {
return bfj // eslint-disable-line promise/no-promise-in-callback
.write(`${paths.appBuild}/bundle-stats.json`, stats.toJson())
.then(() => resolve(resolveArgs))
.catch((error) => reject(new Error(error)));
}
return resolve(resolveArgs);
});
});
}
function copyPublicFolder() {
fs.copySync(paths.appPublic, paths.appBuild, {
dereference: true,
filter: (file) => file !== paths.appHtml,
});
}

View File

@@ -1,12 +1,10 @@
/* eslint-disable no-console, @typescript-eslint/prefer-nullish-coalescing, @typescript-eslint/promise-function-async, @typescript-eslint/prefer-optional-chain */
// Do this as the first thing so that any code reading it knows the right env.
process.env.BABEL_ENV = 'production';
process.env.NODE_ENV = 'production';
const chalk = require('chalk');
const AdmZip = require('adm-zip');
const fs = require('fs-extra');
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,25 +0,0 @@
#!/bin/bash
set -ex
PLATFORMS="linux/arm/v7,linux/arm64/v8,linux/amd64"
DOCKER_IMAGE="shlinkio/shlink-web-client"
if [[ "$GITHUB_REF" == *"develop"* ]]; then
docker buildx build --push \
--platform ${PLATFORMS} \
-t ${DOCKER_IMAGE}:latest .
# If ref is not develop, then this is a tag. Build that docker tag and also "stable"
else
VERSION=${GITHUB_REF#refs/tags/v}
TAGS="-t ${DOCKER_IMAGE}:${VERSION}"
# Push stable tag only if this is not an alpha or beta release
[[ $GITHUB_REF != *"alpha"* && $GITHUB_REF != *"beta"* ]] && TAGS="${TAGS} -t ${DOCKER_IMAGE}:stable"
docker buildx build --push \
--build-arg VERSION=${VERSION} \
--platform ${PLATFORMS} \
${TAGS} .
fi

View File

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

View File

@@ -1,125 +0,0 @@
/* eslint-disable no-console, @typescript-eslint/prefer-nullish-coalescing, @typescript-eslint/promise-function-async, @typescript-eslint/prefer-optional-chain */
// Do this as the first thing so that any code reading it knows the right env.
process.env.BABEL_ENV = 'development';
process.env.NODE_ENV = 'development';
// Makes the script crash on unhandled rejections instead of silently
// ignoring them. In the future, promise rejections that are not handled will
// terminate the Node.js process with a non-zero exit code.
process.on('unhandledRejection', (err) => {
throw err;
});
// Ensure environment variables are read.
require('../config/env');
const fs = require('fs');
const chalk = require('chalk');
const webpack = require('webpack');
const WebpackDevServer = require('webpack-dev-server');
const clearConsole = require('react-dev-utils/clearConsole');
const checkRequiredFiles = require('react-dev-utils/checkRequiredFiles');
const {
choosePort,
createCompiler,
prepareProxy,
prepareUrls,
} = require('react-dev-utils/WebpackDevServerUtils');
const openBrowser = require('react-dev-utils/openBrowser');
const { checkBrowsers } = require('react-dev-utils/browsersHelper');
const paths = require('../config/paths');
const configFactory = require('../config/webpack.config');
const createDevServerConfig = require('../config/webpackDevServer.config');
const useYarn = fs.existsSync(paths.yarnLockFile);
const isInteractive = process.stdout.isTTY;
// Warn and crash if required files are missing
if (!checkRequiredFiles([ paths.appHtml, paths.appIndexJs ])) {
process.exit(1);
}
// Tools like Cloud9 rely on this.
const DEFAULT_PORT = 3000;
const PORT = parseInt(process.env.PORT) || DEFAULT_PORT;
const HOST = process.env.HOST || '0.0.0.0';
if (process.env.HOST) {
console.log(
chalk.cyan(
`Attempting to bind to HOST environment variable: ${chalk.yellow(
chalk.bold(process.env.HOST),
)}`,
),
);
console.log(
'If this was unintentional, check that you haven\'t mistakenly set it in your shell.',
);
console.log(
`Learn more here: ${chalk.yellow('http://bit.ly/CRA-advanced-config')}`,
);
console.log();
}
checkBrowsers(paths.appPath, isInteractive)
.then(() =>
// We attempt to use the default port but if it is busy, we offer the user to
// run on a different port. `choosePort()` Promise resolves to the next free port.
choosePort(HOST, PORT))
.then((port) => {
if (port === null) {
// We have not found a port.
return;
}
const config = configFactory('development');
const protocol = process.env.HTTPS === 'true' ? 'https' : 'http';
const appName = require(paths.appPackageJson).name;
const urls = prepareUrls(protocol, HOST, port);
// Create a webpack compiler that is configured with custom messages.
const compiler = createCompiler({ webpack, config, appName, urls, useYarn });
// Load proxy config
const proxySetting = require(paths.appPackageJson).proxy;
const proxyConfig = prepareProxy(proxySetting, paths.appPublic);
// Serve webpack assets generated by the compiler over a web server.
const serverConfig = createDevServerConfig(
proxyConfig,
urls.lanUrlForConfig,
);
const devServer = new WebpackDevServer(compiler, serverConfig);
// Launch WebpackDevServer.
devServer.listen(port, HOST, (err) => {
if (err) {
return console.log(err);
}
if (isInteractive) {
clearConsole();
}
console.log(chalk.cyan('Starting the development server...\n'));
return openBrowser(urls.localUrlForBrowser);
});
[ 'SIGINT', 'SIGTERM' ].forEach((sig) => {
process.on(sig, () => {
devServer.close();
process.exit();
});
});
})
.catch((err) => {
if (err && err.message) {
console.log(err.message);
}
process.exit(1);
});

View File

@@ -1,23 +0,0 @@
// Do this as the first thing so that any code reading it knows the right env.
process.env.BABEL_ENV = 'test';
process.env.NODE_ENV = 'test';
process.env.PUBLIC_URL = '';
// Makes the script crash on unhandled rejections instead of silently
// ignoring them. In the future, promise rejections that are not handled will
// terminate the Node.js process with a non-zero exit code.
process.on('unhandledRejection', (err) => {
throw err;
});
// Ensure environment variables are read.
require('../config/env');
// Make tests to be matched inside tests folder
const jest = require('jest');
const argumentsToRemove = 2;
const argv = process.argv.slice(argumentsToRemove);
jest.run(argv);

View File

@@ -7,11 +7,4 @@ declare module 'event-source-polyfill' {
}
}
declare module 'csvjson' {
export declare class CsvJson {
public toObject<T>(content: string): T[];
public toCSV<T>(data: T[], options: { headers: 'full' | 'none' | 'relative' | 'key'; wrap?: true }): string;
}
}
declare module '*.png'

View File

@@ -9,8 +9,10 @@ export interface ShlinkApiErrorProps {
export const ShlinkApiError = ({ errorData, fallbackMessage }: ShlinkApiErrorProps) => (
<>
{errorData?.detail ?? fallbackMessage}
{isInvalidArgumentError(errorData) &&
<p className="mb-0">Invalid elements: [{errorData.invalidElements.join(', ')}]</p>
}
{isInvalidArgumentError(errorData) && (
<p className="mb-0">
Invalid elements: [{errorData.invalidElements.join(', ')}]
</p>
)}
</>
);

View File

@@ -21,7 +21,7 @@ import {
import { stringifyQuery } from '../../utils/helpers/query';
import { orderToString } from '../../utils/helpers/ordering';
const buildShlinkBaseUrl = (url: string) => url ? `${url}/rest/v2` : '';
const buildShlinkBaseUrl = (url: string) => (url ? `${url}/rest/v2` : '');
const rejectNilProps = reject(isNil);
const normalizeOrderByInParams = (params: ShlinkShortUrlsListParams): ShlinkShortUrlsListNormalizedParams => {
const { orderBy = {}, ...rest } = params;
@@ -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,
@@ -56,6 +56,10 @@ export default class ShlinkApiClient {
this.performRequest<{ visits: ShlinkVisits }>(`/tags/${tag}/visits`, 'GET', query)
.then(({ data }) => data.visits);
public readonly getDomainVisits = async (domain: string, query?: Omit<ShlinkVisitsParams, 'domain'>): Promise<ShlinkVisits> =>
this.performRequest<{ visits: ShlinkVisits }>(`/domains/${domain}/visits`, 'GET', query)
.then(({ data }) => data.visits);
public readonly getOrphanVisits = async (query?: Omit<ShlinkVisitsParams, 'domain'>): Promise<ShlinkVisits> =>
this.performRequest<{ visits: ShlinkVisits }>('/visits/orphan', 'GET', query)
.then(({ data }) => data.visits);
@@ -76,25 +80,12 @@ export default class ShlinkApiClient {
this.performRequest(`/short-urls/${shortCode}`, 'DELETE', { domain })
.then(() => {});
// eslint-disable-next-line valid-jsdoc
/**
* @deprecated. If using Shlink 2.6.0 or greater, use updateShortUrl instead
*/
public readonly updateShortUrlTags = async (
shortCode: string,
domain: OptionalString,
tags: string[],
): Promise<string[]> =>
this.performRequest<{ tags: string[] }>(`/short-urls/${shortCode}/tags`, 'PUT', { domain }, { tags })
.then(({ data }) => data.tags);
public readonly updateShortUrl = async (
shortCode: string,
domain: OptionalString,
data: ShlinkShortUrlData,
edit: ShlinkShortUrlData,
): Promise<ShortUrl> =>
this.performRequest<ShortUrl>(`/short-urls/${shortCode}`, 'PATCH', { domain }, data)
.then(({ data }) => data);
this.performRequest<ShortUrl>(`/short-urls/${shortCode}`, 'PATCH', { domain }, edit).then(({ data }) => data);
public readonly listTags = async (): Promise<ShlinkTags> =>
this.performRequest<{ tags: ShlinkTagsResponse }>('/tags', 'GET', { withStats: 'true' })

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

@@ -46,7 +46,7 @@ export interface ShlinkVisits {
export interface ShlinkVisitsOverview {
visitsCount: number;
orphanVisitsCount?: number; // Optional only for versions older than 2.6.0
orphanVisitsCount: number;
}
export interface ShlinkVisitsParams {

View File

@@ -1,7 +1,7 @@
import { useEffect, FC } from 'react';
import { Route, Routes, useLocation } from 'react-router-dom';
import classNames from 'classnames';
import NotFound from '../common/NotFound';
import { NotFound } from '../common/NotFound';
import { ServersMap } from '../servers/data';
import { Settings } from '../settings/reducers/settings';
import { changeThemeInMarkup } from '../utils/theme';
@@ -17,13 +17,13 @@ interface AppProps {
appUpdated: boolean;
}
const App = (
export const App = (
MainHeader: FC,
Home: FC,
MenuLayout: FC,
CreateServer: FC,
EditServer: FC,
Settings: FC,
SettingsComp: FC,
ManageServers: FC,
ShlinkVersionsContainer: FC,
) => ({ fetchServers, servers, settings, appUpdated, resetAppUpdate }: AppProps) => {
@@ -47,7 +47,7 @@ const App = (
<div className={classNames('shlink-wrapper', { 'd-flex d-md-block align-items-center': isHome })}>
<Routes>
<Route index element={<Home />} />
<Route path="/settings/*" element={<Settings />} />
<Route path="/settings/*" element={<SettingsComp />} />
<Route path="/manage-servers" element={<ManageServers />} />
<Route path="/server/create" element={<CreateServer />} />
<Route path="/server/:serverId/edit" element={<EditServer />} />
@@ -65,5 +65,3 @@ const App = (
</div>
);
};
export default App;

View File

@@ -1,10 +1,8 @@
import { Action } from 'redux';
import { buildActionCreator, buildReducer } from '../../utils/helpers/redux';
/* eslint-disable padding-line-between-statements */
export const APP_UPDATE_AVAILABLE = 'shlink/appUpdates/APP_UPDATE_AVAILABLE';
export const RESET_APP_UPDATE = 'shlink/appUpdates/RESET_APP_UPDATE';
/* eslint-enable padding-line-between-statements */
const initialState = false;

View File

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

View File

@@ -13,7 +13,7 @@ interface AppUpdateBannerProps {
}
export const AppUpdateBanner: FC<AppUpdateBannerProps> = ({ isOpen, toggle, forceUpdate }) => {
const [ isUpdating,, setUpdating ] = useToggle();
const [isUpdating,, setUpdating] = useToggle();
const update = () => {
setUpdating();
forceUpdate();
@@ -24,7 +24,7 @@ export const AppUpdateBanner: FC<AppUpdateBannerProps> = ({ isOpen, toggle, forc
<h4 className="mb-4">This app has just been updated!</h4>
<p className="mb-0">
Restart it to enjoy the new features.
<Button disabled={isUpdating} className="ms-2" color="secondary" size="sm" onClick={update}>
<Button role="button" disabled={isUpdating} className="ms-2" color="secondary" size="sm" onClick={update}>
{!isUpdating && <>Restart now <FontAwesomeIcon icon={reloadIcon} className="ms-1" /></>}
{isUpdating && <>Restarting...</>}
</Button>

View File

@@ -4,7 +4,7 @@
.aside-menu {
width: $asideMenuWidth;
background-color: var(--primary-color);
box-shadow: rgba(0, 0, 0, .05) 0 8px 15px;
box-shadow: rgb(0 0 0 / .05) 0 8px 15px;
position: fixed !important;
padding-top: 13px;
padding-bottom: 10px;
@@ -23,7 +23,7 @@
@media (max-width: $smMax) {
transition: left 300ms;
top: $headerHeight - 3px;
box-shadow: -10px 0 50px 11px rgba(0, 0, 0, .55);
box-shadow: -10px 0 50px 11px rgb(0 0 0 / .55);
}
}

View File

@@ -17,7 +17,6 @@ import './AsideMenu.scss';
export interface AsideMenuProps {
selectedServer: SelectedServer;
className?: string;
showOnMobile?: boolean;
}
@@ -35,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);
@@ -90,5 +89,3 @@ const AsideMenu = (DeleteServerButton: FC<DeleteServerButtonProps>) => (
</aside>
);
};
export default AsideMenu;

View File

@@ -6,10 +6,10 @@ interface ErrorHandlerState {
hasError: boolean;
}
const ErrorHandler = (
export const ErrorHandler = (
{ location }: Window,
{ error }: Console,
) => class ErrorHandler extends Component<any, ErrorHandlerState> {
) => class extends Component<any, ErrorHandlerState> {
public constructor(props: object) {
super(props);
this.state = { hasError: false };
@@ -26,7 +26,8 @@ const ErrorHandler = (
}
public render(): ReactNode {
if (this.state.hasError) {
const { hasError } = this.state;
if (hasError) {
return (
<div className="home">
<SimpleCard className="p-4">
@@ -39,8 +40,7 @@ const ErrorHandler = (
);
}
return this.props.children;
const { children } = this.props;
return children;
}
};
export default ErrorHandler;

View File

@@ -5,16 +5,16 @@ 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';
export interface HomeProps {
interface HomeProps {
servers: ServersMap;
}
const Home = ({ servers }: HomeProps) => {
export const Home = ({ servers }: HomeProps) => {
const navigate = useNavigate();
const serversList = values(servers);
const hasServers = !isEmpty(serversList);
@@ -22,7 +22,6 @@ const Home = ({ servers }: HomeProps) => {
useEffect(() => {
// Try to redirect to the first server marked as auto-connect
const autoConnectServer = serversList.find(({ autoConnect }) => autoConnect);
autoConnectServer && navigate(`/server/${autoConnectServer.id}`);
}, []);
@@ -66,5 +65,3 @@ const Home = ({ servers }: HomeProps) => {
</div>
);
};
export default Home;

View File

@@ -8,12 +8,12 @@ import { useToggle } from '../utils/helpers/hooks';
import { ShlinkLogo } from './img/ShlinkLogo';
import './MainHeader.scss';
const MainHeader = (ServersDropdown: FC) => () => {
const [ isOpen, toggleOpen, , close ] = useToggle();
export const MainHeader = (ServersDropdown: FC) => () => {
const [isOpen, toggleOpen, , close] = useToggle();
const location = useLocation();
const { pathname } = location;
useEffect(close, [ location ]);
useEffect(close, [location]);
const settingsPath = '/settings';
const toggleClass = classNames('main-header__toggle-icon', { 'main-header__toggle-icon--opened': isOpen });
@@ -41,5 +41,3 @@ const MainHeader = (ServersDropdown: FC) => () => {
</Navbar>
);
};
export default MainHeader;

View File

@@ -16,7 +16,7 @@
z-index: 1035;
font-size: 1.5rem;
cursor: pointer;
color: rgba(255, 255, 255, .5);
color: rgb(255 255 255 / .5);
@media (max-width: $smMax) {
display: inline-block;

View File

@@ -5,9 +5,9 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import classNames from 'classnames';
import { withSelectedServer } from '../servers/helpers/withSelectedServer';
import { useSwipeable, useToggle } from '../utils/helpers/hooks';
import { supportsDomainRedirects, supportsNonOrphanVisits, supportsOrphanVisits } from '../utils/helpers/features';
import { supportsDomainRedirects, supportsDomainVisits, supportsNonOrphanVisits } from '../utils/helpers/features';
import { isReachableServer } from '../servers/data';
import NotFound from './NotFound';
import { NotFound } from './NotFound';
import { AsideMenuProps } from './AsideMenu';
import './MenuLayout.scss';
@@ -16,13 +16,14 @@ interface MenuLayoutProps {
sidebarNotPresent: Function;
}
const MenuLayout = (
export const MenuLayout = (
TagsList: FC,
ShortUrlsList: FC,
AsideMenu: FC<AsideMenuProps>,
CreateShortUrl: FC,
ShortUrlVisits: FC,
TagVisits: FC,
DomainVisits: FC,
OrphanVisits: FC,
NonOrphanVisits: FC,
ServerError: FC,
@@ -31,10 +32,10 @@ const MenuLayout = (
ManageDomains: FC,
) => withSelectedServer<MenuLayoutProps>(({ selectedServer, sidebarNotPresent, sidebarPresent }) => {
const location = useLocation();
const [ sidebarVisible, toggleSidebar, showSidebar, hideSidebar ] = useToggle();
const [sidebarVisible, toggleSidebar, showSidebar, hideSidebar] = useToggle();
const showContent = isReachableServer(selectedServer);
useEffect(() => hideSidebar(), [ location ]);
useEffect(() => hideSidebar(), [location]);
useEffect(() => {
showContent && sidebarPresent();
@@ -45,9 +46,9 @@ const MenuLayout = (
return <ServerError />;
}
const addOrphanVisitsRoute = supportsOrphanVisits(selectedServer);
const addNonOrphanVisitsRoute = supportsNonOrphanVisits(selectedServer);
const addManageDomainsRoute = supportsDomainRedirects(selectedServer);
const addDomainVisitsRoute = supportsDomainVisits(selectedServer);
const burgerClasses = classNames('menu-layout__burger-icon', { 'menu-layout__burger-icon--active': sidebarVisible });
const swipeableProps = useSwipeable(showSidebar, hideSidebar);
@@ -68,7 +69,8 @@ const MenuLayout = (
<Route path="/short-code/:shortCode/visits/*" element={<ShortUrlVisits />} />
<Route path="/short-code/:shortCode/edit" element={<EditShortUrl />} />
<Route path="/tag/:tag/visits/*" element={<TagVisits />} />
{addOrphanVisitsRoute && <Route path="/orphan-visits/*" element={<OrphanVisits />} />}
{addDomainVisitsRoute && <Route path="/domain/:domain/visits/*" element={<DomainVisits />} />}
<Route path="/orphan-visits/*" element={<OrphanVisits />} />
{addNonOrphanVisitsRoute && <Route path="/non-orphan-visits/*" element={<NonOrphanVisits />} />}
<Route path="/manage-tags" element={<TagsList />} />
{addManageDomainsRoute && <Route path="/manage-domains" element={<ManageDomains />} />}
@@ -84,5 +86,3 @@ const MenuLayout = (
</>
);
}, ServerError);
export default MenuLayout;

View File

@@ -1,4 +1,6 @@
import { FC } from 'react';
import { FC, PropsWithChildren } from 'react';
import './NoMenuLayout.scss';
export const NoMenuLayout: FC = ({ children }) => <div className="no-menu-wrapper container-xl">{children}</div>;
export const NoMenuLayout: FC<PropsWithChildren<unknown>> = ({ children }) => (
<div className="no-menu-wrapper container-xl">{children}</div>
);

View File

@@ -1,12 +1,10 @@
import { FC } from 'react';
import { FC, PropsWithChildren } from 'react';
import { Link } from 'react-router-dom';
import { SimpleCard } from '../utils/SimpleCard';
interface NotFoundProps {
to?: string;
}
type NotFoundProps = PropsWithChildren<{ to?: string }>;
const NotFound: FC<NotFoundProps> = ({ to = '/', children = 'Home' }) => (
export const NotFound: FC<NotFoundProps> = ({ to = '/', children = 'Home' }) => (
<div className="home">
<SimpleCard className="p-4">
<h2>Oops! We could not find requested route.</h2>
@@ -19,5 +17,3 @@ const NotFound: FC<NotFoundProps> = ({ to = '/', children = 'Home' }) => (
</SimpleCard>
</div>
);
export default NotFound;

View File

@@ -1,14 +1,12 @@
import { FC, useEffect } from 'react';
import { FC, PropsWithChildren, useEffect } from 'react';
import { useLocation } from 'react-router-dom';
const ScrollToTop = (): FC => ({ children }) => {
export const ScrollToTop: FC<PropsWithChildren<unknown>> = ({ children }) => {
const location = useLocation();
useEffect(() => {
scrollTo(0, 0);
}, [ location ]);
}, [location]);
return <>{children}</>;
};
export default ScrollToTop;

View File

@@ -17,17 +17,15 @@ 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 (
<small className="text-muted">
{isReachableServer(selectedServer) &&
{isReachableServer(selectedServer) && (
<>Server: <VersionLink project="shlink" version={selectedServer.printableVersion} /> - </>
}
)}
Client: <VersionLink project="shlink-web-client" version={normalizedClientVersion} />
</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

@@ -17,7 +17,9 @@ interface SimplePaginatorProps {
centered?: boolean;
}
const SimplePaginator: FC<SimplePaginatorProps> = ({ pagesCount, currentPage, setCurrentPage, centered = true }) => {
export const SimplePaginator: FC<SimplePaginatorProps> = (
{ pagesCount, currentPage, setCurrentPage, centered = true },
) => {
if (pagesCount < 2) {
return null;
}
@@ -35,7 +37,9 @@ const SimplePaginator: FC<SimplePaginatorProps> = ({ pagesCount, currentPage, se
disabled={pageIsEllipsis(pageNumber)}
active={currentPage === pageNumber}
>
<PaginationLink tag="span" onClick={onClick(pageNumber)}>{prettifyPageNumber(pageNumber)}</PaginationLink>
<PaginationLink role="link" tag="span" onClick={onClick(pageNumber)}>
{prettifyPageNumber(pageNumber)}
</PaginationLink>
</PaginationItem>
))}
<PaginationItem disabled={currentPage >= pagesCount}>
@@ -44,5 +48,3 @@ const SimplePaginator: FC<SimplePaginatorProps> = ({ pagesCount, currentPage, se
</Pagination>
);
};
export default SimplePaginator;

View File

@@ -4,7 +4,7 @@
position: relative;
padding: 5px 0 0 6px;
border-radius: .3rem;
background-color: var(--input-color);
background-color: var(--primary-color);
border: 1px solid var(--input-border-color);
transition: border-color .15s ease-in-out, box-shadow .15s ease-in-out;
@@ -16,6 +16,16 @@
cursor: text;
}
.input-group > .react-tags {
flex: 1 1 auto;
width: 1%;
min-width: 0;
}
.card .react-tags {
background-color: var(--input-color);
}
.react-tags.is-focused {
box-shadow: 0 0 0 .2rem rgb(70 150 229 / 25%);
}
@@ -76,7 +86,7 @@
font-size: 1.25rem;
line-height: inherit;
color: var(--input-text-color);
background-color: var(--input-color);
background-color: inherit;
/* prevent autoresize overflowing the container */
max-width: 100%;
@@ -88,6 +98,10 @@
outline: none;
}
.react-tags__search-input::placeholder {
color: #6c757d;
}
.react-tags__search-input::-ms-clear {
display: none;
}
@@ -113,7 +127,7 @@
background: var(--primary-color);
border: 1px solid var(--border-color);
border-radius: .25rem;
box-shadow: 0 2px 6px rgba(0, 0, 0, .2);
box-shadow: 0 2px 6px rgb(0 0 0 / .2);
}
.react-tags__suggestions li {

View File

@@ -1,10 +1,8 @@
import { Action } from 'redux';
import { buildActionCreator, buildReducer } from '../../utils/helpers/redux';
/* eslint-disable padding-line-between-statements */
export const SIDEBAR_PRESENT = 'shlink/common/SIDEBAR_PRESENT';
export const SIDEBAR_NOT_PRESENT = 'shlink/common/SIDEBAR_NOT_PRESENT';
/* eslint-enable padding-line-between-statements */
export interface Sidebar {
sidebarPresent: boolean;

View File

@@ -1,13 +1,10 @@
import { CsvJson } from 'csvjson';
import { NormalizedVisit } from '../../visits/types';
import { ExportableShortUrl } from '../../short-urls/data';
import { saveCsv } from '../../utils/helpers/files';
import { JsonToCsv } from '../../utils/helpers/csvjson';
export class ReportExporter {
public constructor(
private readonly window: Window,
private readonly csvjson: CsvJson,
) {}
public constructor(private readonly window: Window, private readonly jsonToCsv: JsonToCsv) {}
public readonly exportVisits = (filename: string, visits: NormalizedVisit[]) => {
if (!visits.length) {
@@ -26,7 +23,7 @@ export class ReportExporter {
};
private readonly exportCsv = (filename: string, rows: object[]) => {
const csv = this.csvjson.toCSV(rows, { headers: 'key', wrap: true });
const csv = this.jsonToCsv(rows);
saveCsv(this.window, csv, filename);
};

View File

@@ -1,12 +1,12 @@
import axios from 'axios';
import Bottle from 'bottlejs';
import ScrollToTop from '../ScrollToTop';
import MainHeader from '../MainHeader';
import Home from '../Home';
import MenuLayout from '../MenuLayout';
import AsideMenu from '../AsideMenu';
import ErrorHandler from '../ErrorHandler';
import ShlinkVersionsContainer from '../ShlinkVersionsContainer';
import { ScrollToTop } from '../ScrollToTop';
import { MainHeader } from '../MainHeader';
import { Home } from '../Home';
import { MenuLayout } from '../MenuLayout';
import { AsideMenu } from '../AsideMenu';
import { ErrorHandler } from '../ErrorHandler';
import { ShlinkVersionsContainer } from '../ShlinkVersionsContainer';
import { ConnectDecorator } from '../../container/types';
import { withoutSelectedServer } from '../../servers/helpers/withoutSelectedServer';
import { sidebarNotPresent, sidebarPresent } from '../reducers/sidebar';
@@ -20,16 +20,16 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
bottle.constant('axios', axios);
bottle.service('ImageDownloader', ImageDownloader, 'axios', 'window');
bottle.service('ReportExporter', ReportExporter, 'window', 'csvjson');
bottle.service('ReportExporter', ReportExporter, 'window', 'jsonToCsv');
// Components
bottle.serviceFactory('ScrollToTop', ScrollToTop);
bottle.serviceFactory('ScrollToTop', () => ScrollToTop);
bottle.serviceFactory('MainHeader', MainHeader, 'ServersDropdown');
bottle.serviceFactory('Home', () => Home);
bottle.decorator('Home', withoutSelectedServer);
bottle.decorator('Home', connect([ 'servers' ], [ 'resetSelectedServer' ]));
bottle.decorator('Home', connect(['servers'], ['resetSelectedServer']));
bottle.serviceFactory(
'MenuLayout',
@@ -40,6 +40,7 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
'CreateShortUrl',
'ShortUrlVisits',
'TagVisits',
'DomainVisits',
'OrphanVisits',
'NonOrphanVisits',
'ServerError',
@@ -47,12 +48,12 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
'EditShortUrl',
'ManageDomains',
);
bottle.decorator('MenuLayout', connect([ 'selectedServer' ], [ 'selectServer', 'sidebarPresent', 'sidebarNotPresent' ]));
bottle.decorator('MenuLayout', connect(['selectedServer'], ['selectServer', 'sidebarPresent', 'sidebarNotPresent']));
bottle.serviceFactory('AsideMenu', AsideMenu, 'DeleteServerButton');
bottle.serviceFactory('ShlinkVersionsContainer', () => ShlinkVersionsContainer);
bottle.decorator('ShlinkVersionsContainer', connect([ 'selectedServer', 'sidebar' ]));
bottle.decorator('ShlinkVersionsContainer', connect(['selectedServer', 'sidebar']));
bottle.serviceFactory('ErrorHandler', ErrorHandler, 'window', 'console');

View File

@@ -20,8 +20,8 @@ const bottle = new Bottle();
export const { container } = bottle;
const lazyService = <T extends Function, K>(container: IContainer, serviceName: string) =>
(...args: any[]) => (container[serviceName] as T)(...args) as K;
const lazyService = <T extends Function, K>(cont: IContainer, serviceName: string) =>
(...args: any[]) => (cont[serviceName] as T)(...args) as K;
const mapActionService = (map: LazyActionMap, actionName: string): LazyActionMap => ({
...map,
// Wrap actual action service in a function so that it is lazily created the first time it is called

View File

@@ -5,11 +5,12 @@ import reducers from '../reducers';
import { migrateDeprecatedSettings } from '../settings/helpers';
import { ShlinkState } from './types';
const isProduction = process.env.NODE_ENV !== 'production';
const isProduction = process.env.NODE_ENV === 'production';
// eslint-disable-next-line no-mixed-operators
const composeEnhancers: Function = !isProduction && (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const localStorageConfig: RLSOptions = {
states: [ 'settings', 'servers' ],
states: ['settings', 'servers'],
namespace: 'shlink',
namespaceSeparator: '.',
debounce: 300,

View File

@@ -15,6 +15,7 @@ import { DomainsList } from '../domains/reducers/domainsList';
import { VisitsOverview } from '../visits/reducers/visitsOverview';
import { VisitsInfo } from '../visits/types';
import { Sidebar } from '../common/reducers/sidebar';
import { DomainVisits } from '../visits/reducers/domainVisits';
export interface ShlinkState {
servers: ServersMap;
@@ -25,6 +26,7 @@ export interface ShlinkState {
shortUrlEdition: ShortUrlEdition;
shortUrlVisits: ShortUrlVisits;
tagVisits: TagVisits;
domainVisits: DomainVisits;
orphanVisits: VisitsInfo;
nonOrphanVisits: VisitsInfo;
shortUrlDetail: ShortUrlDetail;

View File

@@ -1,19 +1,13 @@
import { FC, useEffect } from 'react';
import { Button, UncontrolledTooltip } from 'reactstrap';
import { UncontrolledTooltip } from 'reactstrap';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import {
faBan as forbiddenIcon,
faDotCircle as defaultDomainIcon,
faEdit as editIcon,
} from '@fortawesome/free-solid-svg-icons';
import { faDotCircle as defaultDomainIcon } from '@fortawesome/free-solid-svg-icons';
import { ShlinkDomainRedirects } from '../api/types';
import { useToggle } from '../utils/helpers/hooks';
import { OptionalString } from '../utils/utils';
import { SelectedServer } from '../servers/data';
import { supportsDefaultDomainRedirectsEdition } from '../utils/helpers/features';
import { EditDomainRedirectsModal } from './helpers/EditDomainRedirectsModal';
import { Domain } from './data';
import { DomainStatusIcon } from './helpers/DomainStatusIcon';
import { DomainDropdown } from './helpers/DomainDropdown';
interface DomainRowProps {
domain: Domain;
@@ -39,9 +33,7 @@ const DefaultDomain: FC = () => (
export const DomainRow: FC<DomainRowProps> = (
{ domain, editDomainRedirects, checkDomainHealth, defaultRedirects, selectedServer },
) => {
const [ isOpen, toggle ] = useToggle();
const { domain: authority, isDefault, redirects, status } = domain;
const canEditDomain = !isDefault || supportsDefaultDomainRedirectsEdition(selectedServer);
useEffect(() => {
checkDomainHealth(domain.domain);
@@ -64,25 +56,8 @@ export const DomainRow: FC<DomainRowProps> = (
<DomainStatusIcon status={status} />
</td>
<td className="responsive-table__cell text-end">
<span id={!canEditDomain ? 'defaultDomainBtn' : undefined}>
<Button outline size="sm" disabled={!canEditDomain} onClick={!canEditDomain ? undefined : toggle}>
<FontAwesomeIcon fixedWidth icon={!canEditDomain ? forbiddenIcon : editIcon} />
</Button>
</span>
{!canEditDomain && (
<UncontrolledTooltip target="defaultDomainBtn" placement="left">
Redirects for default domain cannot be edited here.
<br />
Use config options or env vars directly on the server.
</UncontrolledTooltip>
)}
<DomainDropdown domain={domain} editDomainRedirects={editDomainRedirects} selectedServer={selectedServer} />
</td>
<EditDomainRedirectsModal
domain={domain}
isOpen={isOpen}
toggle={toggle}
editDomainRedirects={editDomainRedirects}
/>
</tr>
);
};

View File

@@ -19,7 +19,7 @@ interface DomainSelectorConnectProps extends DomainSelectorProps {
}
export const DomainSelector = ({ listDomains, value, domainsList, onChange }: DomainSelectorConnectProps) => {
const [ inputDisplayed,, showInput, hideInput ] = useToggle();
const [inputDisplayed,, showInput, hideInput] = useToggle();
const { domains } = domainsList;
const valueIsEmpty = isEmpty(value);
const unselectDomain = () => onChange('');
@@ -40,6 +40,7 @@ export const DomainSelector = ({ listDomains, value, domainsList, onChange }: Do
outline
type="button"
className="domains-dropdown__back-btn"
aria-label="Back to domains list"
onClick={pipe(unselectDomain, hideInput)}
>
<FontAwesomeIcon icon={faUndo} />
@@ -56,7 +57,7 @@ export const DomainSelector = ({ listDomains, value, domainsList, onChange }: Do
{domains.map(({ domain, isDefault }) => (
<DropdownItem
key={domain}
active={value === domain || isDefault && valueIsEmpty}
active={(value === domain || isDefault) && valueIsEmpty}
onClick={() => onChange(domain)}
>
{domain}

View File

@@ -1,9 +1,9 @@
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';
import SearchField from '../utils/SearchField';
import { SearchField } from '../utils/SearchField';
import { ShlinkDomainRedirects } from '../api/types';
import { SelectedServer } from '../servers/data';
import { DomainsList } from './reducers/domainsList';
@@ -18,7 +18,7 @@ interface ManageDomainsProps {
selectedServer: SelectedServer;
}
const headers = [ '', 'Domain', 'Base path redirect', 'Regular 404 redirect', 'Invalid short URL redirect', '', '' ];
const headers = ['', 'Domain', 'Base path redirect', 'Regular 404 redirect', 'Invalid short URL redirect', '', ''];
export const ManageDomains: FC<ManageDomainsProps> = (
{ listDomains, domainsList, filterDomains, editDomainRedirects, checkDomainHealth, selectedServer },

View File

@@ -0,0 +1,51 @@
import { FC } from 'react';
import { DropdownItem } from 'reactstrap';
import { Link } from 'react-router-dom';
import { faChartPie as pieChartIcon, faEdit as editIcon } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { useToggle } from '../../utils/helpers/hooks';
import { DropdownBtnMenu } from '../../utils/DropdownBtnMenu';
import { EditDomainRedirectsModal } from './EditDomainRedirectsModal';
import { Domain } from '../data';
import { ShlinkDomainRedirects } from '../../api/types';
import { supportsDefaultDomainRedirectsEdition, supportsDomainVisits } from '../../utils/helpers/features';
import { getServerId, SelectedServer } from '../../servers/data';
import { DEFAULT_DOMAIN } from '../../visits/reducers/domainVisits';
interface DomainDropdownProps {
domain: Domain;
editDomainRedirects: (domain: string, redirects: Partial<ShlinkDomainRedirects>) => Promise<void>;
selectedServer: SelectedServer;
}
export const DomainDropdown: FC<DomainDropdownProps> = ({ domain, editDomainRedirects, selectedServer }) => {
const [isOpen, toggle] = useToggle();
const [isModalOpen, toggleModal] = useToggle();
const { isDefault } = domain;
const canBeEdited = !isDefault || supportsDefaultDomainRedirectsEdition(selectedServer);
const withVisits = supportsDomainVisits(selectedServer);
const serverId = getServerId(selectedServer);
return (
<DropdownBtnMenu isOpen={isOpen} toggle={toggle}>
{withVisits && (
<DropdownItem
tag={Link}
to={`/server/${serverId}/domain/${domain.domain}${domain.isDefault ? `_${DEFAULT_DOMAIN}` : ''}/visits`}
>
<FontAwesomeIcon icon={pieChartIcon} fixedWidth /> Visit stats
</DropdownItem>
)}
<DropdownItem disabled={!canBeEdited} onClick={!canBeEdited ? undefined : toggleModal}>
<FontAwesomeIcon fixedWidth icon={editIcon} /> Edit redirects
</DropdownItem>
<EditDomainRedirectsModal
domain={domain}
isOpen={isModalOpen}
toggle={toggleModal}
editDomainRedirects={editDomainRedirects}
/>
</DropdownBtnMenu>
);
};

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 {
@@ -18,7 +19,7 @@ interface DomainStatusIconProps {
export const DomainStatusIcon: FC<DomainStatusIconProps> = ({ status, matchMedia = window.matchMedia }) => {
const ref = useRef<HTMLSpanElement>();
const matchesMobile = () => matchMedia('(max-width: 991px)').matches;
const [ isMobile, setIsMobile ] = useState<boolean>(matchesMobile());
const [isMobile, setIsMobile] = useState<boolean>(matchesMobile());
useEffect(() => {
const listener = () => setIsMobile(matchesMobile());
@@ -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

@@ -25,9 +25,9 @@ const FormGroup: FC<InputFormGroupProps & { isLast?: boolean }> = ({ isLast, ...
export const EditDomainRedirectsModal: FC<EditDomainRedirectsModalProps> = (
{ isOpen, toggle, domain, editDomainRedirects },
) => {
const [ baseUrlRedirect, setBaseUrlRedirect ] = useState(domain.redirects?.baseUrlRedirect ?? '');
const [ regular404Redirect, setRegular404Redirect ] = useState(domain.redirects?.regular404Redirect ?? '');
const [ invalidShortUrlRedirect, setInvalidShortUrlRedirect ] = useState(
const [baseUrlRedirect, setBaseUrlRedirect] = useState(domain.redirects?.baseUrlRedirect ?? '');
const [regular404Redirect, setRegular404Redirect] = useState(domain.redirects?.regular404Redirect ?? '');
const [invalidShortUrlRedirect, setInvalidShortUrlRedirect] = useState(
domain.redirects?.invalidShortUrlRedirect ?? '',
);
const handleSubmit = handleEventPreventingDefault(async () => editDomainRedirects(domain.domain, {
@@ -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

@@ -5,11 +5,9 @@ import { GetState } from '../../container/types';
import { ApiErrorAction } from '../../api/types/actions';
import { parseApiError } from '../../api/utils';
/* eslint-disable padding-line-between-statements */
export const EDIT_DOMAIN_REDIRECTS_START = 'shlink/domainRedirects/EDIT_DOMAIN_REDIRECTS_START';
export const EDIT_DOMAIN_REDIRECTS_ERROR = 'shlink/domainRedirects/EDIT_DOMAIN_REDIRECTS_ERROR';
export const EDIT_DOMAIN_REDIRECTS = 'shlink/domainRedirects/EDIT_DOMAIN_REDIRECTS';
/* eslint-enable padding-line-between-statements */
export interface EditDomainRedirectsAction extends Action<string> {
domain: string;
@@ -21,10 +19,10 @@ export const editDomainRedirects = (buildShlinkApiClient: ShlinkApiClientBuilder
domainRedirects: Partial<ShlinkDomainRedirects>,
) => async (dispatch: Dispatch, getState: GetState) => {
dispatch({ type: EDIT_DOMAIN_REDIRECTS_START });
const { editDomainRedirects } = buildShlinkApiClient(getState);
const { editDomainRedirects: shlinkEditDomainRedirects } = buildShlinkApiClient(getState);
try {
const redirects = await editDomainRedirects({ domain, ...domainRedirects });
const redirects = await shlinkEditDomainRedirects({ domain, ...domainRedirects });
dispatch<EditDomainRedirectsAction>({ type: EDIT_DOMAIN_REDIRECTS, domain, redirects });
} catch (e: any) {

View File

@@ -10,13 +10,11 @@ import { hasServerData } from '../../servers/data';
import { replaceAuthorityFromUri } from '../../utils/helpers/uri';
import { EDIT_DOMAIN_REDIRECTS, EditDomainRedirectsAction } from './domainRedirects';
/* eslint-disable padding-line-between-statements */
export const LIST_DOMAINS_START = 'shlink/domainsList/LIST_DOMAINS_START';
export const LIST_DOMAINS_ERROR = 'shlink/domainsList/LIST_DOMAINS_ERROR';
export const LIST_DOMAINS = 'shlink/domainsList/LIST_DOMAINS';
export const FILTER_DOMAINS = 'shlink/domainsList/FILTER_DOMAINS';
export const VALIDATE_DOMAIN = 'shlink/domainsList/VALIDATE_DOMAIN';
/* eslint-enable padding-line-between-statements */
export interface DomainsList {
domains: Domain[];
@@ -55,10 +53,10 @@ export type DomainsCombinedAction = ListDomainsAction
& ValidateDomain;
export const replaceRedirectsOnDomain = (domain: string, redirects: ShlinkDomainRedirects) =>
(d: Domain): Domain => d.domain !== domain ? d : { ...d, redirects };
(d: Domain): Domain => (d.domain !== domain ? d : { ...d, redirects });
export const replaceStatusOnDomain = (domain: string, status: DomainStatus) =>
(d: Domain): Domain => d.domain !== domain ? d : { ...d, status };
(d: Domain): Domain => (d.domain !== domain ? d : { ...d, status });
export default buildReducer<DomainsList, DomainsCombinedAction>({
[LIST_DOMAINS_START]: () => ({ ...initialState, loading: true }),
@@ -67,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,
@@ -86,15 +84,15 @@ export const listDomains = (buildShlinkApiClient: ShlinkApiClientBuilder) => ()
getState: GetState,
) => {
dispatch({ type: LIST_DOMAINS_START });
const { listDomains } = buildShlinkApiClient(getState);
const { listDomains: shlinkListDomains } = buildShlinkApiClient(getState);
try {
const { domains, defaultRedirects } = await listDomains().then(({ data, defaultRedirects }) => ({
const resp = await shlinkListDomains().then(({ data, defaultRedirects }) => ({
domains: data.map((domain): Domain => ({ ...domain, status: 'validating' })),
defaultRedirects,
}));
dispatch<ListDomainsAction>({ type: LIST_DOMAINS, domains, defaultRedirects });
dispatch<ListDomainsAction>({ type: LIST_DOMAINS, ...resp });
} catch (e: any) {
dispatch<ApiErrorAction>({ type: LIST_DOMAINS_ERROR, errorData: parseApiError(e) });
}

View File

@@ -8,12 +8,12 @@ import { editDomainRedirects } from '../reducers/domainRedirects';
const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
// Components
bottle.serviceFactory('DomainSelector', () => DomainSelector);
bottle.decorator('DomainSelector', connect([ 'domainsList' ], [ 'listDomains' ]));
bottle.decorator('DomainSelector', connect(['domainsList'], ['listDomains']));
bottle.serviceFactory('ManageDomains', () => ManageDomains);
bottle.decorator('ManageDomains', connect(
[ 'domainsList', 'selectedServer' ],
[ 'listDomains', 'filterDomains', 'editDomainRedirects', 'checkDomainHealth' ],
['domainsList', 'selectedServer'],
['listDomains', 'filterDomains', 'editDomainRedirects', 'checkDomainHealth'],
));
// Actions

View File

@@ -13,6 +13,7 @@
:root {
scroll-behavior: auto;
color-scheme: var(--color-scheme);
}
html,
@@ -49,7 +50,7 @@ a:not(.nav-link):not(.navbar-brand):not(.page-link):not(.highlight-card):not(.bt
}
.card {
box-shadow: 0 .125rem .25rem rgba(0, 0, 0, .075);
box-shadow: 0 .125rem .25rem rgb(0 0 0 / .075);
background-color: var(--primary-color);
border-color: var(--border-color);
}

View File

@@ -1,11 +1,12 @@
import { render } from 'react-dom';
import { createRoot } from 'react-dom/client';
import { Provider } from 'react-redux';
import { BrowserRouter } from 'react-router-dom';
import { homepage } from '../package.json';
import pack from '../package.json';
import { container } from './container';
import { store } from './container/store';
import { fixLeafletIcons } from './utils/helpers/leaflet';
import { register as registerServiceWorker } from './serviceWorkerRegistration';
import 'chart.js/auto'; // TODO Import specific ones to reduce bundle size https://react-chartjs-2.js.org/docs/migration-to-v4/#tree-shaking
import 'react-datepicker/dist/react-datepicker.css';
import 'leaflet/dist/leaflet.css';
import './index.scss';
@@ -15,9 +16,9 @@ fixLeafletIcons();
const { App, ScrollToTop, ErrorHandler, appUpdateAvailable } = container;
render(
createRoot(document.getElementById('root')!).render( // eslint-disable-line @typescript-eslint/no-non-null-assertion
<Provider store={store}>
<BrowserRouter basename={homepage}>
<BrowserRouter basename={pack.homepage}>
<ErrorHandler>
<ScrollToTop>
<App />
@@ -25,14 +26,11 @@ render(
</ErrorHandler>
</BrowserRouter>
</Provider>,
document.getElementById('root'),
);
// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://cra.link/PWA
registerServiceWorker({
onUpdate() {
store.dispatch(appUpdateAvailable()); // eslint-disable-line @typescript-eslint/no-unsafe-call
store.dispatch(appUpdateAvailable());
},
});

View File

@@ -23,7 +23,7 @@ export function boundToMercureHub<T = {}>(
const params = useParams();
useEffect(() => {
const onMessage = (visit: CreateVisit) => interval ? pendingUpdates.add(visit) : createNewVisits([ visit ]);
const onMessage = (visit: CreateVisit) => (interval ? pendingUpdates.add(visit) : createNewVisits([visit]));
const topics = getTopicsForProps(props, params);
const closeEventSource = bindToMercureTopic(mercureInfo, topics, onMessage, loadMercureInfo);
@@ -32,12 +32,12 @@ export function boundToMercureHub<T = {}>(
}
const timer = setInterval(() => {
createNewVisits([ ...pendingUpdates ]);
createNewVisits([...pendingUpdates]);
pendingUpdates.clear();
}, interval * 1000 * 60);
return pipe(() => clearInterval(timer), () => closeEventSource?.());
}, [ mercureInfo ]);
}, [mercureInfo]);
return <WrappedComponent {...props} />;
};

View File

@@ -4,11 +4,9 @@ import { GetState } from '../../container/types';
import { buildReducer } from '../../utils/helpers/redux';
import { ShlinkApiClientBuilder } from '../../api/services/ShlinkApiClientBuilder';
/* eslint-disable padding-line-between-statements */
export const GET_MERCURE_INFO_START = 'shlink/mercure/GET_MERCURE_INFO_START';
export const GET_MERCURE_INFO_ERROR = 'shlink/mercure/GET_MERCURE_INFO_ERROR';
export const GET_MERCURE_INFO = 'shlink/mercure/GET_MERCURE_INFO';
/* eslint-enable padding-line-between-statements */
export interface MercureInfo {
token?: string;

View File

@@ -7,6 +7,7 @@ import shortUrlDeletionReducer from '../short-urls/reducers/shortUrlDeletion';
import shortUrlEditionReducer from '../short-urls/reducers/shortUrlEdition';
import shortUrlVisitsReducer from '../visits/reducers/shortUrlVisits';
import tagVisitsReducer from '../visits/reducers/tagVisits';
import domainVisitsReducer from '../visits/reducers/domainVisits';
import orphanVisitsReducer from '../visits/reducers/orphanVisits';
import nonOrphanVisitsReducer from '../visits/reducers/nonOrphanVisits';
import shortUrlDetailReducer from '../short-urls/reducers/shortUrlDetail';
@@ -30,6 +31,7 @@ export default combineReducers<ShlinkState>({
shortUrlEdition: shortUrlEditionReducer,
shortUrlVisits: shortUrlVisitsReducer,
tagVisits: tagVisitsReducer,
domainVisits: domainVisitsReducer,
orphanVisits: orphanVisitsReducer,
nonOrphanVisits: nonOrphanVisitsReducer,
shortUrlDetail: shortUrlDetailReducer,

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,16 +26,16 @@ 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 [ isConfirmModalOpen, toggleConfirmModal ] = useToggle();
const [ serverData, setServerData ] = useState<ServerData | undefined>();
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 = () => {
if (!serverData) {
return;
@@ -53,13 +53,14 @@ const CreateServer = (ImportServersBtn: FC<ImportServersBtnProps>, useStateFlagT
);
serverExists ? toggleConfirmModal() : save();
}, [ serverData ]);
}, [serverData]);
return (
<NoMenuLayout>
<ServerForm title={<h5 className="mb-0">Add new server</h5>} onSubmit={setServerData}>
{!hasServers &&
<ImportServersBtn tooltipPlacement="top" onImport={setServersImported} onImportError={setErrorImporting} />}
{!hasServers && (
<ImportServersBtn tooltipPlacement="top" onImport={setServersImported} onImportError={setErrorImporting} />
)}
{hasServers && <Button outline onClick={goBack}>Cancel</Button>}
<Button outline color="primary" className="ms-2">Create server</Button>
</ServerForm>
@@ -69,12 +70,10 @@ const CreateServer = (ImportServersBtn: FC<ImportServersBtnProps>, useStateFlagT
<DuplicatedServersModal
isOpen={isConfirmModalOpen}
duplicatedServers={serverData ? [ serverData ] : []}
duplicatedServers={serverData ? [serverData] : []}
onDiscard={goBack}
onSave={save}
/>
</NoMenuLayout>
);
};
export default CreateServer;

View File

@@ -1,20 +1,20 @@
import { FC } from 'react';
import { FC, PropsWithChildren } from 'react';
import { faMinusCircle as deleteIcon } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { useToggle } from '../utils/helpers/hooks';
import { DeleteServerModalProps } from './DeleteServerModal';
import { ServerWithId } from './data';
export interface DeleteServerButtonProps {
export type DeleteServerButtonProps = PropsWithChildren<{
server: ServerWithId;
className?: string;
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();
const [isModalOpen, , showModal, hideModal] = useToggle();
return (
<>
@@ -27,5 +27,3 @@ const DeleteServerButton = (DeleteServerModal: FC<DeleteServerModalProps>): FC<D
</>
);
};
export default DeleteServerButton;

View File

@@ -1,5 +1,5 @@
import { FC } from 'react';
import { Modal, ModalBody, ModalFooter, ModalHeader } from 'reactstrap';
import { Button, Modal, ModalBody, ModalFooter, ModalHeader } from 'reactstrap';
import { useNavigate } from 'react-router-dom';
import { ServerWithId } from './data';
@@ -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>
@@ -37,11 +37,9 @@ const DeleteServerModal: FC<DeleteServerModalConnectProps> = (
</p>
</ModalBody>
<ModalFooter>
<button className="btn btn-link" onClick={toggle}>Cancel</button>
<button className="btn btn-danger" onClick={() => closeModal()}>Delete</button>
<Button color="link" onClick={toggle}>Cancel</Button>
<Button color="danger" onClick={() => closeModal()}>Delete</Button>
</ModalFooter>
</Modal>
);
};
export default DeleteServerModal;

View File

@@ -5,9 +5,9 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Link } from 'react-router-dom';
import { NoMenuLayout } from '../common/NoMenuLayout';
import { SimpleCard } from '../utils/SimpleCard';
import SearchField from '../utils/SearchField';
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,20 +22,20 @@ 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 [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));
}, [ servers ]);
}, [servers]);
return (
<NoMenuLayout>
@@ -61,17 +61,17 @@ export const ManageServers = (
<table className="table table-hover responsive-table mb-0">
<thead className="responsive-table__header">
<tr>
{hasAutoConnect && <th style={{ width: '50px' }} />}
{hasAutoConnect && <th aria-label="Auto-connect" style={{ width: '50px' }} />}
<th>Name</th>
<th>Base URL</th>
<th />
<th aria-label="Options" />
</tr>
</thead>
<tbody>
{!serversList.length && <tr className="text-center"><td colSpan={4}>No servers found.</td></tr>}
{serversList.map((server) =>
<ManageServersRow key={server.id} server={server} hasAutoConnect={hasAutoConnect} />)
}
{serversList.map((server) => (
<ManageServersRow key={server.id} server={server} hasAutoConnect={hasAutoConnect} />
))}
</tbody>
</table>
</SimpleCard>

View File

@@ -25,8 +25,8 @@ interface ManageServersRowDropdownConnectProps extends ManageServersRowDropdownP
export const ManageServersRowDropdown = (
DeleteServerModal: FC<DeleteServerModalProps>,
): FC<ManageServersRowDropdownConnectProps> => ({ server, setAutoConnect }) => {
const [ isMenuOpen, toggleMenu ] = useToggle();
const [ isModalOpen,, showModal, hideModal ] = useToggle();
const [isMenuOpen, toggleMenu] = useToggle();
const [isModalOpen,, showModal, hideModal] = useToggle();
const serverUrl = `/server/${server.id}`;
const { autoConnect: isAutoConnect } = server;
const autoConnectIcon = isAutoConnect ? toggleOffIcon : toggleOnIcon;
@@ -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

@@ -8,10 +8,9 @@ import { ShortUrlsTableProps } from '../short-urls/ShortUrlsTable';
import { boundToMercureHub } from '../mercure/helpers/boundToMercureHub';
import { CreateShortUrlProps } from '../short-urls/CreateShortUrl';
import { VisitsOverview } from '../visits/reducers/visitsOverview';
import { Versions } from '../utils/helpers/version';
import { Topics } from '../mercure/helpers/Topics';
import { ShlinkShortUrlsListParams } from '../api/types';
import { supportsNonOrphanVisits, supportsOrphanVisits } from '../utils/helpers/features';
import { supportsNonOrphanVisits } from '../utils/helpers/features';
import { getServerId, SelectedServer } from './data';
import { HighlightCard } from './helpers/HighlightCard';
@@ -28,7 +27,6 @@ interface OverviewConnectProps {
export const Overview = (
ShortUrlsTable: FC<ShortUrlsTableProps>,
CreateShortUrl: FC<CreateShortUrlProps>,
ForServerVersion: FC<Versions>,
) => boundToMercureHub(({
shortUrlsList,
listShortUrls,
@@ -42,7 +40,6 @@ export const Overview = (
const { loading: loadingTags } = tagsList;
const { loading: loadingVisits, visitsCount, orphanVisitsCount } = visitsOverview;
const serverId = getServerId(selectedServer);
const linkToOrphanVisits = supportsOrphanVisits(selectedServer);
const linkToNonOrphanVisits = supportsNonOrphanVisits(selectedServer);
const navigate = useNavigate();
@@ -61,13 +58,8 @@ export const Overview = (
</HighlightCard>
</div>
<div className="col-lg-6 col-xl-3 mb-3">
<HighlightCard title="Orphan visits" link={linkToOrphanVisits && `/server/${serverId}/orphan-visits`}>
<ForServerVersion minVersion="2.6.0">
{loadingVisits ? 'Loading...' : prettify(orphanVisitsCount ?? 0)}
</ForServerVersion>
<ForServerVersion maxVersion="2.5.*">
<small className="text-muted"><i>Shlink 2.6 is needed</i></small>
</ForServerVersion>
<HighlightCard title="Orphan visits" link={`/server/${serverId}/orphan-visits`}>
{loadingVisits ? 'Loading...' : prettify(orphanVisitsCount)}
</HighlightCard>
</div>
<div className="col-lg-6 col-xl-3 mb-3">
@@ -109,4 +101,4 @@ export const Overview = (
</Card>
</>
);
}, () => [ Topics.visits, Topics.orphanVisits ]);
}, () => [Topics.visits, Topics.orphanVisits]);

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

@@ -8,7 +8,7 @@
.servers-list__list-group:not(.servers-list__list-group--embedded) {
max-width: 400px;
box-shadow: 0 .125rem .25rem rgba(0, 0, 0, .075);
box-shadow: 0 .125rem .25rem rgb(0 0 0 / .075);
}
.servers-list__server-item.servers-list__server-item {

View File

@@ -1,4 +1,4 @@
import { FC } from 'react';
import { FC, PropsWithChildren } from 'react';
import { ListGroup, ListGroupItem } from 'reactstrap';
import { Link } from 'react-router-dom';
import classNames from 'classnames';
@@ -7,10 +7,10 @@ import { faChevronRight as chevronIcon } from '@fortawesome/free-solid-svg-icons
import { ServerWithId } from './data';
import './ServersListGroup.scss';
interface ServersListGroupProps {
type ServersListGroupProps = PropsWithChildren<{
servers: ServerWithId[];
embedded?: boolean;
}
}>;
const ServerListItem = ({ id, name }: { id: string; name: string }) => (
<ListGroupItem tag={Link} to={`/server/${id}`} className="servers-list__server-item">
@@ -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

@@ -35,15 +35,15 @@ export const hasServerData = (server: SelectedServer | ServerData): server is Se
!!(server as ServerData)?.url && !!(server as ServerData)?.apiKey;
export const isServerWithId = (server: SelectedServer | ServerWithId): server is ServerWithId =>
!!server?.hasOwnProperty('id');
!!(server as ServerWithId)?.id;
export const isReachableServer = (server: SelectedServer): server is ReachableServer =>
!!server?.hasOwnProperty('version');
!!(server as ReachableServer)?.version;
export const isNotFoundServer = (server: SelectedServer): server is NotFoundServer =>
!!server?.hasOwnProperty('serverNotFound');
!!(server as NotFoundServer)?.serverNotFound;
export const getServerId = (server: SelectedServer) => isServerWithId(server) ? server.id : '';
export const getServerId = (server: SelectedServer) => (isServerWithId(server) ? server.id : '');
export const serverWithIdToServerData = (server: ServerWithId): ServerData =>
omit<ServerWithId, 'id' | 'autoConnect'>([ 'id', 'autoConnect' ], server);
omit<ServerWithId, 'id' | 'autoConnect'>(['id', 'autoConnect'], server);

View File

@@ -20,12 +20,12 @@ export const DuplicatedServersModal: FC<DuplicatedServersModalProps> = (
<ModalBody>
<p>{hasMultipleServers ? 'The next servers already exist:' : 'There is already a server with:'}</p>
<ul>
{duplicatedServers.map(({ url, apiKey }, index) => !hasMultipleServers ? (
{duplicatedServers.map(({ url, apiKey }, index) => (!hasMultipleServers ? (
<Fragment key={index}>
<li>URL: <b>{url}</b></li>
<li>API key: <b>{apiKey}</b></li>
</Fragment>
) : <li key={index}><b>{url}</b> - <b>{apiKey}</b></li>)}
) : <li key={index}><b>{url}</b> - <b>{apiKey}</b></li>))}
</ul>
<span>
{hasMultipleServers ? 'Do you want to ignore duplicated servers' : 'Do you want to save this server anyway'}?

View File

@@ -1,24 +0,0 @@
import { FC } from 'react';
import { versionMatch, Versions } from '../../utils/helpers/version';
import { isReachableServer, SelectedServer } from '../data';
interface ForServerVersionProps extends Versions {
selectedServer: SelectedServer;
}
const ForServerVersion: FC<ForServerVersionProps> = ({ minVersion, maxVersion, selectedServer, children }) => {
if (!isReachableServer(selectedServer)) {
return null;
}
const { version } = selectedServer;
const matchesVersion = versionMatch(version, { maxVersion, minVersion });
if (!matchesVersion) {
return null;
}
return <>{children}</>;
};
export default ForServerVersion;

View File

@@ -1,16 +1,16 @@
import { FC } from 'react';
import { FC, PropsWithChildren } from 'react';
import { Card, CardText, CardTitle } from 'reactstrap';
import { Link } from 'react-router-dom';
import { faArrowAltCircleRight as linkIcon } from '@fortawesome/free-regular-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import './HighlightCard.scss';
export interface HighlightCardProps {
export type HighlightCardProps = PropsWithChildren<{
title: string;
link?: string | false;
}
}>;
const buildExtraProps = (link?: string | false) => !link ? {} : { tag: Link, to: link };
const buildExtraProps = (link?: string | false) => (!link ? {} : { tag: Link, to: link });
export const HighlightCard: FC<HighlightCardProps> = ({ children, title, link }) => (
<Card className="highlight-card" body {...buildExtraProps(link)}>

View File

@@ -1,46 +1,43 @@
import { useRef, RefObject, ChangeEvent, MutableRefObject, FC, useState, useEffect } 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 interface ImportServersBtnProps {
export type ImportServersBtnProps = PropsWithChildren<{
onImport?: () => void;
onImportError?: (error: Error) => void;
tooltipPlacement?: 'top' | 'bottom';
className?: string;
}
}>;
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 [ serversToCreate, setServersToCreate ] = useState<ServerData[] | undefined>();
const [ duplicatedServers, setDuplicatedServers ] = useState<ServerData[]>([]);
const [ isModalOpen,, showModal, hideModal ] = useToggle();
const ref = useRef<HTMLInputElement>();
const [serversToCreate, setServersToCreate] = useState<ServerData[] | undefined>();
const [duplicatedServers, setDuplicatedServers] = useState<ServerData[]>([]);
const [isModalOpen,, showModal, hideModal] = useToggle();
const create = pipe(createServers, onImport);
const createAllServers = pipe(() => create(serversToCreate ?? []), hideModal);
const createNonDuplicatedServers = pipe(
@@ -52,7 +49,7 @@ const ImportServersBtn = ({ importServersFromFile }: ServersImporter): FC<Import
.then(setServersToCreate)
.then(() => {
// Reset input after processing file
(target as { value: string | null }).value = null;
(target as { value: string | null }).value = null; // eslint-disable-line no-param-reassign
})
.catch(onImportError);
@@ -62,12 +59,12 @@ const ImportServersBtn = ({ importServersFromFile }: ServersImporter): FC<Import
}
const existingServers = Object.values(servers);
const duplicatedServers = serversToCreate.filter(serversFiltering(existingServers));
const hasDuplicatedServers = !!duplicatedServers.length;
const dupServers = serversToCreate.filter(serversFiltering(existingServers));
const hasDuplicatedServers = !!dupServers.length;
!hasDuplicatedServers ? create(serversToCreate) : setDuplicatedServers(duplicatedServers);
!hasDuplicatedServers ? create(serversToCreate) : setDuplicatedServers(dupServers);
hasDuplicatedServers && showModal();
}, [ serversToCreate ]);
}, [serversToCreate]);
return (
<>
@@ -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,29 +1,29 @@
import { FC, ReactNode, useEffect, useState } from 'react';
import { FC, PropsWithChildren, ReactNode, useEffect, useState } from 'react';
import { InputFormGroup } from '../../utils/forms/InputFormGroup';
import { handleEventPreventingDefault } from '../../utils/utils';
import { ServerData } from '../data';
import { SimpleCard } from '../../utils/SimpleCard';
interface ServerFormProps {
type ServerFormProps = PropsWithChildren<{
onSubmit: (server: ServerData) => void;
initialValues?: ServerData;
title?: ReactNode;
}
}>;
export const ServerForm: FC<ServerFormProps> = ({ onSubmit, initialValues, children, title }) => {
const [ name, setName ] = useState('');
const [ url, setUrl ] = useState('');
const [ apiKey, setApiKey ] = useState('');
const [name, setName] = useState('');
const [url, setUrl] = useState('');
const [apiKey, setApiKey] = useState('');
const handleSubmit = handleEventPreventingDefault(() => onSubmit({ name, url, apiKey }));
useEffect(() => {
initialValues && setName(initialValues.name);
initialValues && setUrl(initialValues.url);
initialValues && setApiKey(initialValues.apiKey);
}, [ initialValues ]);
}, [initialValues]);
return (
<form className="server-form" onSubmit={handleSubmit}>
<form className="server-form" name="serverForm" onSubmit={handleSubmit}>
<SimpleCard className="mb-3" title={title}>
<InputFormGroup value={name} onChange={setName}>Name</InputFormGroup>
<InputFormGroup type="url" value={url} onChange={setUrl}>URL</InputFormGroup>

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';
@@ -16,7 +16,7 @@ export function withSelectedServer<T = {}>(WrappedComponent: FC<WithSelectedServ
useEffect(() => {
params.serverId && selectServer(params.serverId);
}, [ params.serverId ]);
}, [params.serverId]);
if (!selectedServer) {
return (

View File

@@ -6,8 +6,9 @@ interface WithoutSelectedServerProps {
export function withoutSelectedServer<T = {}>(WrappedComponent: FC<WithoutSelectedServerProps & T>) {
return (props: WithoutSelectedServerProps & T) => {
const { resetSelectedServer } = props;
useEffect(() => {
props.resetSelectedServer();
resetSelectedServer();
}, []);
return <WrappedComponent {...props} />;

View File

@@ -1,17 +1,17 @@
import { pipe, prop } from 'ramda';
import { AxiosInstance } from 'axios';
import { Dispatch } from 'redux';
import { homepage } from '../../../package.json';
import pack from '../../../package.json';
import { hasServerData, ServerData } from '../data';
import { createServers } from './servers';
const responseToServersList = pipe(
prop<any, any>('data'),
(data: any): ServerData[] => Array.isArray(data) ? data.filter(hasServerData) : [],
(data: any): ServerData[] => (Array.isArray(data) ? data.filter(hasServerData) : []),
);
export const fetchServers = ({ get }: AxiosInstance) => () => async (dispatch: Dispatch) => {
const resp = await get(`${homepage}/servers.json`);
const resp = await get(`${pack.homepage}/servers.json`);
const remoteList = responseToServersList(resp);
dispatch(createServers(remoteList));

View File

@@ -7,21 +7,19 @@ import { ShlinkHealth } from '../../api/types';
import { buildActionCreator, buildReducer } from '../../utils/helpers/redux';
import { ShlinkApiClientBuilder } from '../../api/services/ShlinkApiClientBuilder';
/* eslint-disable padding-line-between-statements */
export const SELECT_SERVER = 'shlink/selectedServer/SELECT_SERVER';
export const RESET_SELECTED_SERVER = 'shlink/selectedServer/RESET_SELECTED_SERVER';
export const MIN_FALLBACK_VERSION = '1.0.0';
export const MAX_FALLBACK_VERSION = '999.999.999';
export const LATEST_VERSION_CONSTRAINT = 'latest';
/* eslint-enable padding-line-between-statements */
export interface SelectServerAction extends Action<string> {
selectedServer: SelectedServer;
}
const versionToSemVer = pipe(
(version: string) => version === LATEST_VERSION_CONSTRAINT ? MAX_FALLBACK_VERSION : version,
(version: string) => (version === LATEST_VERSION_CONSTRAINT ? MAX_FALLBACK_VERSION : version),
toSemVer(MIN_FALLBACK_VERSION),
);

View File

@@ -4,12 +4,10 @@ import { Action } from 'redux';
import { ServerData, ServersMap, ServerWithId } from '../data';
import { buildReducer } from '../../utils/helpers/redux';
/* eslint-disable padding-line-between-statements */
export const EDIT_SERVER = 'shlink/servers/EDIT_SERVER';
export const DELETE_SERVER = 'shlink/servers/DELETE_SERVER';
export const CREATE_SERVERS = 'shlink/servers/CREATE_SERVERS';
export const SET_AUTO_CONNECT = 'shlink/servers/SET_AUTO_CONNECT';
/* eslint-enable padding-line-between-statements */
export interface CreateServersAction extends Action<string> {
newServers: ServersMap;
@@ -37,9 +35,9 @@ const serverWithId = (server: ServerWithId | ServerData): ServerWithId => {
export default buildReducer<ServersMap, CreateServersAction & DeleteServerAction & SetAutoConnectAction>({
[CREATE_SERVERS]: (state, { newServers }) => ({ ...state, ...newServers }),
[DELETE_SERVER]: (state, { serverId }) => dissoc(serverId, state),
[EDIT_SERVER]: (state, { serverId, serverData }: any) => !state[serverId]
? state
: assoc(serverId, { ...state[serverId], ...serverData }, state),
[EDIT_SERVER]: (state, { serverId, serverData }: any) => (
!state[serverId] ? state : assoc(serverId, { ...state[serverId], ...serverData }, state)
),
[SET_AUTO_CONNECT]: (state, { serverId, autoConnect }) => {
if (!state[serverId]) {
return state;
@@ -50,7 +48,7 @@ export default buildReducer<ServersMap, CreateServersAction & DeleteServerAction
}
return fromPairs(
toPairs(state).map(([ evaluatedServerId, server ]) => [
toPairs(state).map(([evaluatedServerId, server]) => [
evaluatedServerId,
{ ...server, autoConnect: evaluatedServerId === serverId },
]),
@@ -66,7 +64,7 @@ export const createServers = pipe(
(newServers: ServersMap) => ({ type: CREATE_SERVERS, newServers }),
);
export const createServer = (server: ServerWithId) => createServers([ server ]);
export const createServer = (server: ServerWithId) => createServers([server]);
export const editServer = (serverId: string, serverData: Partial<ServerData>) => ({
type: EDIT_SERVER,

View File

@@ -1,8 +1,8 @@
import { values } from 'ramda';
import { CsvJson } from 'csvjson';
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';
const SERVERS_FILENAME = 'shlink-servers.csv';
@@ -10,14 +10,14 @@ export default class ServersExporter {
public constructor(
private readonly storage: LocalStorage,
private readonly window: Window,
private readonly csvjson: CsvJson,
private readonly jsonToCsv: JsonToCsv,
) {}
public readonly exportServers = async () => {
const servers = values(this.storage.get<ServersMap>('servers') ?? {}).map(serverWithIdToServerData);
try {
const csv = this.csvjson.toCSV(servers, { headers: 'key' });
const csv = this.jsonToCsv(servers);
saveCsv(this.window, csv, SERVERS_FILENAME);
} catch (e) {

View File

@@ -1,5 +1,5 @@
import { CsvJson } from 'csvjson';
import { ServerData } from '../data';
import { CsvToJson } from '../../utils/helpers/csvjson';
const validateServer = (server: any): server is ServerData =>
typeof server.url === 'string' && typeof server.apiKey === 'string' && typeof server.name === 'string';
@@ -8,7 +8,7 @@ const validateServers = (servers: any): servers is ServerData[] =>
Array.isArray(servers) && servers.every(validateServer);
export class ServersImporter {
public constructor(private readonly csvJson: CsvJson, private readonly fileReaderFactory: () => FileReader) {}
public constructor(private readonly csvToJson: CsvToJson, private readonly fileReaderFactory: () => FileReader) {}
public readonly importServersFromFile = async (file?: File | null): Promise<ServerData[]> => {
if (!file) {
@@ -18,19 +18,19 @@ export class ServersImporter {
const reader = this.fileReaderFactory();
return new Promise((resolve, reject) => {
reader.addEventListener('loadend', (e: ProgressEvent<FileReader>) => {
reader.addEventListener('loadend', async (e: ProgressEvent<FileReader>) => {
try {
// TODO Read as stream, otherwise, if the file is too big, this will block the browser tab
const content = e.target?.result?.toString() ?? '';
const servers = this.csvJson.toObject(content);
const servers = await this.csvToJson(content);
if (!validateServers(servers)) {
throw new Error('Provided file does not have the right format.');
}
resolve(servers);
} catch (e) {
reject(e);
} catch (error) {
reject(error);
}
});
reader.readAsText(file);

View File

@@ -1,15 +1,13 @@
import csvjson from 'csvjson';
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';
import ForServerVersion from '../helpers/ForServerVersion';
import { ServerError } from '../helpers/ServerError';
import { ConnectDecorator } from '../../container/types';
import { withoutSelectedServer } from '../helpers/withoutSelectedServer';
@@ -27,52 +25,48 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
ManageServers,
'ServersExporter',
'ImportServersBtn',
'useStateFlagTimeout',
'useTimeoutToggle',
'ManageServersRow',
);
bottle.decorator('ManageServers', withoutSelectedServer);
bottle.decorator('ManageServers', connect([ 'selectedServer', 'servers' ], [ 'resetSelectedServer' ]));
bottle.decorator('ManageServers', connect(['selectedServer', 'servers'], ['resetSelectedServer']));
bottle.serviceFactory('ManageServersRow', ManageServersRow, 'ManageServersRowDropdown');
bottle.serviceFactory('ManageServersRowDropdown', ManageServersRowDropdown, 'DeleteServerModal');
bottle.decorator('ManageServersRowDropdown', connect(null, [ 'setAutoConnect' ]));
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' ]));
bottle.decorator('CreateServer', connect(['selectedServer', 'servers'], ['createServer', 'resetSelectedServer']));
bottle.serviceFactory('EditServer', EditServer, 'ServerError');
bottle.decorator('EditServer', connect([ 'selectedServer' ], [ 'editServer', 'selectServer', 'resetSelectedServer' ]));
bottle.decorator('EditServer', connect(['selectedServer'], ['editServer', 'selectServer', 'resetSelectedServer']));
bottle.serviceFactory('ServersDropdown', () => ServersDropdown);
bottle.decorator('ServersDropdown', connect([ 'servers', 'selectedServer' ]));
bottle.decorator('ServersDropdown', connect(['servers', 'selectedServer']));
bottle.serviceFactory('DeleteServerModal', () => DeleteServerModal);
bottle.decorator('DeleteServerModal', connect(null, [ 'deleteServer' ]));
bottle.decorator('DeleteServerModal', connect(null, ['deleteServer']));
bottle.serviceFactory('DeleteServerButton', DeleteServerButton, 'DeleteServerModal');
bottle.serviceFactory('ImportServersBtn', ImportServersBtn, 'ServersImporter');
bottle.decorator('ImportServersBtn', connect([ 'servers' ], [ 'createServers' ]));
bottle.serviceFactory('ForServerVersion', () => ForServerVersion);
bottle.decorator('ForServerVersion', connect([ 'selectedServer' ]));
bottle.decorator('ImportServersBtn', connect(['servers'], ['createServers']));
bottle.serviceFactory('ServerError', ServerError, 'DeleteServerButton');
bottle.decorator('ServerError', connect([ 'servers', 'selectedServer' ]));
bottle.decorator('ServerError', connect(['servers', 'selectedServer']));
bottle.serviceFactory('Overview', Overview, 'ShortUrlsTable', 'CreateShortUrl', 'ForServerVersion');
bottle.serviceFactory('Overview', Overview, 'ShortUrlsTable', 'CreateShortUrl');
bottle.decorator('Overview', connect(
[ 'shortUrlsList', 'tagsList', 'selectedServer', 'mercureInfo', 'visitsOverview' ],
[ 'listShortUrls', 'listTags', 'createNewVisits', 'loadMercureInfo', 'loadVisitsOverview' ],
['shortUrlsList', 'tagsList', 'selectedServer', 'mercureInfo', 'visitsOverview'],
['listShortUrls', 'listTags', 'createNewVisits', 'loadMercureInfo', 'loadVisitsOverview'],
));
// Services
bottle.constant('csvjson', csvjson);
bottle.constant('fileReaderFactory', () => new FileReader());
bottle.service('ServersImporter', ServersImporter, 'csvjson', 'fileReaderFactory');
bottle.service('ServersExporter', ServersExporter, 'Storage', 'window', 'csvjson');
bottle.service('ServersImporter', ServersImporter, 'csvToJson', 'fileReaderFactory');
bottle.service('ServersExporter', ServersExporter, 'Storage', 'window', 'jsonToCsv');
// Actions
bottle.serviceFactory('selectServer', selectServer, 'buildShlinkApiClient', 'loadMercureInfo');

View File

@@ -1,5 +1,4 @@
/// <reference lib="webworker" />
/* eslint-disable no-restricted-globals */
// This service worker can be customized!
// See https://developers.google.com/web/tools/workbox/modules

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;
@@ -12,45 +13,50 @@ interface RealTimeUpdatesProps {
setRealTimeUpdatesInterval: (interval: number) => void;
}
const intervalValue = (interval?: number) => !interval ? '' : `${interval}`;
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,
@@ -25,12 +25,10 @@ const Settings = (
</NavPills>
<Routes>
<Route path="general" element={<SettingsSections items={[ <UserInterface key="one" />, <RealTimeUpdates key="two" /> ]} />} />
<Route path="short-urls" element={<SettingsSections items={[ <ShortUrlCreation key="one" />, <ShortUrlsList key="two" /> ]} />} />
<Route path="other-items" element={<SettingsSections items={[ <Tags key="one" />, <Visits key="two" /> ]} />} />
<Route path="general" element={<SettingsSections items={[<UserInterface />, <RealTimeUpdates />]} />} />
<Route path="short-urls" element={<SettingsSections items={[<ShortUrlCreation />, <ShortUrlsList />]} />} />
<Route path="other-items" element={<SettingsSections items={[<Tags />, <Visits />]} />} />
<Route path="*" element={<Navigate replace to="general" />} />
</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';
@@ -13,11 +13,12 @@ interface ShortUrlCreationProps {
}
const tagFilteringModeText = (tagFilteringMode: TagFilteringMode | undefined): string =>
tagFilteringMode === 'includes' ? 'Suggest tags including input' : 'Suggest tags starting with input';
const tagFilteringModeHint = (tagFilteringMode: TagFilteringMode | undefined): ReactNode =>
(tagFilteringMode === 'includes' ? 'Suggest tags including input' : 'Suggest tags starting with input');
const tagFilteringModeHint = (tagFilteringMode: TagFilteringMode | undefined): ReactNode => (
tagFilteringMode === 'includes'
? <>The list of suggested tags will contain those <b>including</b> provided input.</>
: <>The list of suggested tags will contain those <b>starting with</b> provided input.</>;
: <>The list of suggested tags will contain those <b>starting with</b> provided input.</>
);
export const ShortUrlCreationSettings: FC<ShortUrlCreationProps> = ({ settings, setShortUrlCreationSettings }) => {
const shortUrlCreation: ShortUrlsSettings = settings.shortUrlCreation ?? { validateUrls: false };

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';

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