Compare commits

..

639 Commits

Author SHA1 Message Date
Alejandro Celaya
f5e92c6897 Merge pull request #848 from shlinkio/develop
Release 3.10.2
2023-07-09 10:17:23 +02:00
Alejandro Celaya
f1014a4810 Build docker image for linux/arm64/v8 and linux/amd64 only 2023-07-09 10:09:35 +02:00
Alejandro Celaya
1793424658 Merge pull request #847 from acelaya-forks/feature/docker-build-on-tag
Build docker image only on tags
2023-07-09 09:58:55 +02:00
Alejandro Celaya
c94a5b948e Build docker image only on tags 2023-07-09 09:48:54 +02:00
Alejandro Celaya
107cabcd8b Merge pull request #845 from shlinkio/dependabot/npm_and_yarn/tough-cookie-4.1.3
Bump tough-cookie from 4.1.2 to 4.1.3
2023-07-08 07:50:43 +02:00
Alejandro Celaya
99bc769894 Merge pull request #844 from shlinkio/dependabot/npm_and_yarn/stylelint-15.10.1
Bump stylelint from 14.16.0 to 15.10.1
2023-07-08 07:50:18 +02:00
dependabot[bot]
e8f1964941 Bump tough-cookie from 4.1.2 to 4.1.3
Bumps [tough-cookie](https://github.com/salesforce/tough-cookie) from 4.1.2 to 4.1.3.
- [Release notes](https://github.com/salesforce/tough-cookie/releases)
- [Changelog](https://github.com/salesforce/tough-cookie/blob/master/CHANGELOG.md)
- [Commits](https://github.com/salesforce/tough-cookie/compare/v4.1.2...v4.1.3)

---
updated-dependencies:
- dependency-name: tough-cookie
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-07-08 00:17:47 +00:00
dependabot[bot]
1a491bec1c Bump stylelint from 14.16.0 to 15.10.1
Bumps [stylelint](https://github.com/stylelint/stylelint) from 14.16.0 to 15.10.1.
- [Release notes](https://github.com/stylelint/stylelint/releases)
- [Changelog](https://github.com/stylelint/stylelint/blob/main/CHANGELOG.md)
- [Commits](https://github.com/stylelint/stylelint/compare/14.16.0...15.10.1)

---
updated-dependencies:
- dependency-name: stylelint
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-07-07 22:00:52 +00:00
Alejandro Celaya
4ba63cdbf8 Merge pull request #837 from acelaya-forks/feature/vitest-0.32
Feature/vitest 0.32
2023-06-12 09:47:20 +02:00
Alejandro Celaya
b2c2af3ebb Exclude sw helper from code coverage 2023-06-12 08:59:38 +02:00
Alejandro Celaya
9dca19fcb5 Replace coverage-c8 with coverage-v8 2023-06-12 08:51:37 +02:00
Alejandro Celaya
7c2dab43e1 Update to vitest 0.32 2023-06-09 09:07:20 +02:00
Alejandro Celaya
a8c6d9b034 Merge pull request #836 from shlinkio/dependabot/npm_and_yarn/vite-4.3.9
Bump vite from 4.3.1 to 4.3.9
2023-06-06 05:38:06 +02:00
dependabot[bot]
be0cb20fa2 Bump vite from 4.3.1 to 4.3.9
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 4.3.1 to 4.3.9.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v4.3.9/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-06-06 02:03:33 +00:00
Alejandro Celaya
2ac1025e9f Merge pull request #834 from acelaya-forks/feature/vitest-migration
Feature/vitest migration
2023-05-27 14:21:33 +02:00
Alejandro Celaya
27d79b574e Correct test:watch command so that it actually watches 2023-05-27 14:15:38 +02:00
Alejandro Celaya
1b82f42a33 Remove direct dependency on redux and redux-thunk, as those are now required by @reduxjs/toolkit 2023-05-27 14:10:37 +02:00
Alejandro Celaya
f3f9eac67b Fix comments 2023-05-27 13:12:38 +02:00
Alejandro Celaya
2a86a0e540 Fix remaining type errors in tests 2023-05-27 12:45:12 +02:00
Alejandro Celaya
d14aea708e Fix incorrect types between testing library and vitest 2023-05-27 12:29:03 +02:00
Alejandro Celaya
12a05b422d Fix merge conflicts 2023-05-27 12:04:01 +02:00
Alejandro Celaya
a5abe9dbf2 Update test snapshots 2023-05-27 12:02:13 +02:00
Alejandro Celaya
07fcb4e016 Update tests to use vi instead of jest 2023-05-27 11:57:26 +02:00
Alejandro Celaya
e2cbb2713a Replace jest config with vitest config 2023-05-27 11:36:18 +02:00
Alejandro Celaya
706e00ace0 Merge pull request #833 from acelaya-forks/feature/menus
Feature/menus
2023-05-27 10:57:14 +02:00
Alejandro Celaya
655fbf94c1 Normalize and consolidate dropdown menus 2023-05-27 10:40:07 +02:00
Alejandro Celaya
afc574aceb Fixed block and inline dropdown buttons 2023-05-27 09:40:49 +02:00
Alejandro Celaya
3da2b56426 Update actions dependencies and node 2023-05-27 09:40:10 +02:00
Alejandro Celaya
5f91ad8819 Merge pull request #829 from shlinkio/develop
Release 3.10.1
2023-04-23 15:56:15 +02:00
Alejandro Celaya
131a745514 Add v3.10.1 to changelog 2023-04-23 15:24:48 +02:00
Alejandro Celaya
86349f1ad3 Merge pull request #828 from acelaya-forks/feature/short-url-export
Feature/short url export
2023-04-22 12:28:12 +02:00
Alejandro Celaya
72e4a7b062 Fix incorrect type 2023-04-22 12:09:15 +02:00
Alejandro Celaya
0a0165df45 Update changelog 2023-04-22 12:05:31 +02:00
Alejandro Celaya
992b22fd24 Refactor short URL export so that it is compatible with what Shlink expects 2023-04-21 09:36:51 +02:00
Alejandro Celaya
6fbe6c673b Merge pull request #827 from acelaya-forks/feature/vite-latest
Update vite deps
2023-04-21 08:20:42 +02:00
Alejandro Celaya
ff22e54b59 Update vite deps 2023-04-20 22:57:49 +02:00
Alejandro Celaya
578365ab68 Delete outdated stryker config file 2023-04-20 09:28:39 +02:00
Alejandro Celaya
22905a2efc Merge pull request #825 from acelaya-forks/feature/shoehorn
Introduce shoehorn as a possible replacement for ts-mockery
2023-04-14 09:34:45 +02:00
Alejandro Celaya
26bad75a1a Finish replacing ts-mockery with shoehorn 2023-04-14 09:28:53 +02:00
Alejandro Celaya
04e1950591 Migrate more tests to shoehorn 2023-04-13 22:47:13 +02:00
Alejandro Celaya
340f4b8fb5 Introduce shoehorn as a possible replacement for ts-mockery 2023-04-13 21:48:29 +02:00
Alejandro Celaya
457458a894 Merge pull request #820 from shlinkio/develop
Release 3.10.0
2023-03-19 12:00:45 +01:00
Alejandro Celaya
f6334c3618 Merge pull request #819 from acelaya-forks/feature/tags-non-bot-visits
Feature/tags non bot visits
2023-03-19 11:54:54 +01:00
Alejandro Celaya
cf27de965e Remove redundant type 2023-03-19 11:50:06 +01:00
Alejandro Celaya
43b2926063 Update changelog 2023-03-19 11:47:02 +01:00
Alejandro Celaya
1d6464fefb Take into consideration types of visits when increasing tags visits 2023-03-19 11:44:40 +01:00
Alejandro Celaya
927ab76dbd Increase required coverage 2023-03-19 10:46:52 +01:00
Alejandro Celaya
34cfe2077b Test proper amount of visits is displayed in TagsList 2023-03-19 10:44:09 +01:00
Alejandro Celaya
4ebe23e89f Add logic to dynamically exclude bots visits in tags table 2023-03-19 10:32:11 +01:00
Alejandro Celaya
8fa61a6301 Merge pull request #817 from acelaya-forks/feature/tags-stats
Feature/tags stats
2023-03-18 16:36:06 +01:00
Alejandro Celaya
96c20b36a5 Split tagsList and tagsStats methods in ShlinkApiClient for clarity 2023-03-18 16:32:04 +01:00
Alejandro Celaya
a9af5163c1 Update changelog 2023-03-18 16:27:52 +01:00
Alejandro Celaya
b87b108e53 Use /tags/stats endpoint when the server supports it 2023-03-18 16:26:28 +01:00
Alejandro Celaya
ddaec7c6ac Merge pull request #816 from acelaya-forks/feature/decouple-actions
Feature/decouple actions
2023-03-18 16:07:15 +01:00
Alejandro Celaya
4e8e16f16d Refactor of redux tests to avoid covering RTK implementation details 2023-03-18 16:02:06 +01:00
Alejandro Celaya
9cefdb7977 First refactor of redux tests to avoid covering RTK implementation details 2023-03-18 12:09:38 +01:00
Alejandro Celaya
a6d000714b Merge pull request #815 from acelaya-forks/feature/overview-bots
Feature/overview bots
2023-03-18 11:15:01 +01:00
Alejandro Celaya
54758272be Update changelog 2023-03-18 11:10:50 +01:00
Alejandro Celaya
8e9e2c5b61 Create test for VisitsHighlightCard 2023-03-18 11:10:03 +01:00
Alejandro Celaya
934bf495a0 Extend overview to exclude/include bot visits based on config 2023-03-18 10:55:07 +01:00
Alejandro Celaya
1d8189369c Enhance visits overview reducer to handle bot and non-bots visits amounts 2023-03-18 10:54:14 +01:00
Alejandro Celaya
25aa9b9bd7 Enhance types including potential bots on visits summary endpoint 2023-03-18 10:29:49 +01:00
Alejandro Celaya
a1b879a5b4 Add support for a tooltip on HighlightCard component 2023-03-18 10:17:17 +01:00
Alejandro Celaya
b70724f7d6 Update babel-typescript plugin 2023-03-18 10:08:44 +01:00
Alejandro Celaya
a52f96f8e5 Merge pull request #814 from acelaya-forks/feature/update-ts-vite
Update to latest TypeScript and Vite versions
2023-03-17 08:57:11 +01:00
Alejandro Celaya
970c573a12 Update to latest TypeScript and Vite versions 2023-03-17 08:49:47 +01:00
Alejandro Celaya
46749044e2 Merge pull request #813 from acelaya-forks/feature/device-long-urls
Feature/device long urls
2023-03-14 09:11:54 +01:00
Alejandro Celaya
16d748800c Update copy-to-clipboard icons 2023-03-14 09:06:57 +01:00
Alejandro Celaya
3e698b045a Add IconInput test 2023-03-14 09:02:12 +01:00
Alejandro Celaya
999b21577a Removed duplicated CSS from DateInput 2023-03-14 08:50:53 +01:00
Alejandro Celaya
3be5126e2d Add missing ref to IconInput 2023-03-13 18:18:35 +01:00
Alejandro Celaya
2b14c49c80 Update snapshots 2023-03-13 18:04:09 +01:00
Alejandro Celaya
bace2a10e8 Create component to display input with an icon 2023-03-13 18:02:29 +01:00
Alejandro Celaya
006e6b30b7 Update changelog 2023-03-13 09:06:49 +01:00
Alejandro Celaya
4c5d0321d2 Add support for device-specific long URLs when using Shlink 3.5.0 or newer 2023-03-13 09:05:54 +01:00
Alejandro Celaya
fa69c21fa2 Merge pull request #811 from acelaya-forks/feature/feature-flag-hooks
Convert feature flags into hooks
2023-03-11 10:38:26 +01:00
Alejandro Celaya
95439e5602 Convert feature flags into hooks 2023-03-11 10:33:03 +01:00
Alejandro Celaya
bbd8d8ef4e Fix border radius on tags input 2023-03-10 09:17:07 +01:00
Alejandro Celaya
ef269d565c Merge pull request #810 from acelaya-forks/feature/fallback-bots
Make sure the request to get the latest fallback visit respects bots config
2023-03-10 08:58:56 +01:00
Alejandro Celaya
8acf6dda6e Make sure the request to get the latest fallback visit respects bots config 2023-03-10 08:53:05 +01:00
Alejandro Celaya
d18219dc14 Merge pull request #806 from acelaya-forks/feature/preview
Feature/preview
2023-03-08 09:27:57 +01:00
Alejandro Celaya
3f1718f4c5 Fix merge conflicts 2023-03-08 09:21:05 +01:00
Alejandro Celaya
825a749b45 Replace serve with vite preview to check generated build 2023-03-08 09:18:32 +01:00
Alejandro Celaya
c2eb09e664 Merge pull request #804 from acelaya-forks/feature/update-coding-standard
Feature/update coding standard
2023-02-18 11:44:05 +01:00
Alejandro Celaya
adb670dd0c Update coding standard 2023-02-18 11:40:04 +01:00
Alejandro Celaya
5e9ec071dc Remove default exports 2023-02-18 11:37:49 +01:00
Alejandro Celaya
1f41f8da23 Ordered imports alphabetically 2023-02-18 11:15:35 +01:00
Alejandro Celaya
2a5480da79 Add import type whenever possible 2023-02-18 10:40:37 +01:00
Alejandro Celaya
7add854b40 Merge pull request #803 from acelaya-forks/feature/remove-stryker
Remove stryker and mutation testing
2023-02-17 20:31:39 +01:00
Alejandro Celaya
e639cd0bd2 Update changelog 2023-02-17 20:27:48 +01:00
Alejandro Celaya
3503f1f580 Remove stryker and mutation testing 2023-02-17 20:09:31 +01:00
Alejandro Celaya
853dcbd69a Merge pull request #801 from acelaya-forks/feature/vite-4.1
Update to vite 4.1
2023-02-11 13:22:56 +01:00
Alejandro Celaya
c54fff5472 Update to vite 4.1 2023-02-11 13:16:41 +01:00
Alejandro Celaya
699d3d3eaa Fix twitter badge 2023-01-28 11:18:08 +01:00
Alejandro Celaya
0c91f488f0 Merge pull request #794 from acelaya-forks/feature/domain
Replace references to doma.in with s.test
2023-01-18 08:12:25 +01:00
Alejandro Celaya
d3a644877e Replace references to doma.in with s.test 2023-01-17 22:53:49 +01:00
Alejandro Celaya
aac2832eb7 Merge pull request #791 from acelaya-forks/feature/fix-ref-types
Improved types on element ref objects and their usage
2023-01-10 20:11:36 +01:00
Alejandro Celaya
487c832f5b Improved types on element ref objects and their usage 2023-01-10 20:04:47 +01:00
Alejandro Celaya
98e2e57bb2 Merge pull request #790 from shlinkio/dependabot/npm_and_yarn/json5-1.0.2
Bump json5 from 1.0.1 to 1.0.2
2023-01-08 09:04:30 +01:00
dependabot[bot]
c5170df402 Bump json5 from 1.0.1 to 1.0.2
Bumps [json5](https://github.com/json5/json5) from 1.0.1 to 1.0.2.
- [Release notes](https://github.com/json5/json5/releases)
- [Changelog](https://github.com/json5/json5/blob/main/CHANGELOG.md)
- [Commits](https://github.com/json5/json5/compare/v1.0.1...v1.0.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-01-08 06:27:50 +00:00
Alejandro Celaya
4be38dfd0c Merge pull request #789 from shlinkio/develop
Release 3.9.1
2022-12-31 18:22:38 +01:00
Alejandro Celaya
597f2b69e9 Merge pull request #788 from acelaya-forks/feature/fix-base-path
Fixed wrong base path being used in vite config
2022-12-31 18:21:26 +01:00
Alejandro Celaya
c078a5fb55 Fixed wrong base path being used in vite config 2022-12-31 18:15:47 +01:00
Alejandro Celaya
5db0326350 Merge pull request #786 from shlinkio/develop
Release 3.9.0
2022-12-31 17:07:56 +01:00
Alejandro Celaya
7f6c678eaa Merge pull request #785 from acelaya-forks/feature/server-error-fix
Feature/server error fix
2022-12-31 17:02:12 +01:00
Alejandro Celaya
37ac6cebc1 Fixed selectedServer reucer test 2022-12-31 16:56:22 +01:00
Alejandro Celaya
27099aa7fb Updated changelog 2022-12-31 16:43:34 +01:00
Alejandro Celaya
91f4d09608 Ensured a recconnection happens to selected server when its params are edited 2022-12-31 16:42:04 +01:00
Alejandro Celaya
d34b9b1233 Merge pull request #783 from acelaya-forks/feature/visits-async-thunk
Feature/visits async thunk
2022-12-31 10:47:30 +01:00
Alejandro Celaya
2badd2b743 Fixed warning in tests 2022-12-31 10:47:15 +01:00
Alejandro Celaya
4517f38680 Fixed injection of visits loaders 2022-12-31 10:43:03 +01:00
Alejandro Celaya
84f9727836 Updated changelog 2022-12-31 10:35:58 +01:00
Alejandro Celaya
85452cde23 Enhanced visits async thunk so that it wraps both standard async thunk actions and extra ones 2022-12-31 10:34:29 +01:00
Alejandro Celaya
9b19113262 Merge pull request #780 from acelaya-forks/feature/short-urls-filtering
Feature/short urls filtering
2022-12-29 19:26:44 +01:00
Alejandro Celaya
e1bb091363 Updated changelog 2022-12-29 19:21:11 +01:00
Alejandro Celaya
732d664715 Fixed coding styles 2022-12-29 19:19:46 +01:00
Alejandro Celaya
33498ce903 Added support to filter out disabled short URLs 2022-12-29 19:03:17 +01:00
Alejandro Celaya
c25b74de84 Added test for ShortUrlsFilterDropdown 2022-12-29 10:35:50 +01:00
Alejandro Celaya
1c39e3402b Added function to parse optional boolean to string 2022-12-28 23:00:55 +01:00
Alejandro Celaya
a3bd10bc82 Updated ShlinkApiClient to support more filtering options for short URLs list 2022-12-28 22:59:11 +01:00
Alejandro Celaya
d6d237fc52 Merge pull request #779 from shlinkio/feature/tweak-ts
Added --noEmit to tsc as part of production build
2022-12-26 10:31:31 +01:00
Alejandro Celaya
9b7a169110 Added --noEmit to tsc as part of production build 2022-12-26 10:24:53 +01:00
Alejandro Celaya
2b17a24206 Merge pull request #778 from acelaya-forks/feature/chartjs-4
Feature/chartjs 4
2022-12-25 22:52:49 +01:00
Alejandro Celaya
a0d9bd6f09 Fixed type error in AsideMenu 2022-12-25 22:48:47 +01:00
Alejandro Celaya
cd33abd92d Updated changelog 2022-12-25 22:45:21 +01:00
Alejandro Celaya
c83563c0ea Fixed subpath and tests for chartjs 2022-12-25 22:44:43 +01:00
Alejandro Celaya
79515ac960 Updated production deps 2022-12-25 22:31:28 +01:00
Alejandro Celaya
8e8a5f3fd6 Updated to chartjs 4 2022-12-25 09:53:50 +01:00
Alejandro Celaya
51283cc130 Merge pull request #777 from acelaya-forks/feature/fix-visits-filters
Fixed bots filter getting reset when changing date
2022-12-25 09:49:57 +01:00
Alejandro Celaya
4023c077b3 Fixed bots filter getting reset when changing date 2022-12-25 09:44:47 +01:00
Alejandro Celaya
f3cf21ba08 Merge pull request #776 from acelaya-forks/feature/vite
Feature/vite
2022-12-25 09:30:14 +01:00
Alejandro Celaya
bec7b59abf Updated changelog 2022-12-25 09:20:45 +01:00
Alejandro Celaya
3e0abe329f Fixed preview generation, including service worker 2022-12-25 09:19:26 +01:00
Alejandro Celaya
822fe3db9e Small fixes 2022-12-25 09:13:45 +01:00
Alejandro Celaya
408ec82a10 Migrated serice worker generation to vite-plugin-pwa 2022-12-24 19:17:33 +01:00
Alejandro Celaya
ced3fa00ef Ensured service-worker is bundled separately 2022-12-24 18:20:01 +01:00
Alejandro Celaya
595b3c0450 Updated to node 18 and fixed replace-version script for vite structure 2022-12-24 11:34:39 +01:00
Alejandro Celaya
1d1c8153e7 Updated babel plugins for jest 2022-12-24 10:48:03 +01:00
Alejandro Celaya
52f556eb2e Migrated from react-scripts and webpack to vite 2022-12-24 10:18:26 +01:00
Alejandro Celaya
35fcd20123 Merge pull request #775 from acelaya-forks/feature/shlink-updates
Feature/shlink updates
2022-12-23 21:21:06 +01:00
Alejandro Celaya
e518b94fba Fixed tests 2022-12-23 21:16:17 +01:00
Alejandro Celaya
05553ba18a Updated changelog 2022-12-23 21:09:35 +01:00
Alejandro Celaya
4a9e05cf17 Updated prop to make it required as in Shlink v2.8.0 or newer 2022-12-23 21:08:41 +01:00
Alejandro Celaya
60fc351344 Removed references to feature checks for version 2.7 2022-12-23 21:06:59 +01:00
Alejandro Celaya
815e06809a Removed references to feature checks for version 2.8 2022-12-23 20:42:47 +01:00
Alejandro Celaya
bfcdf703e8 Merge pull request #772 from acelaya-forks/feature/exclude-bots
Feature/exclude bots
2022-12-23 20:29:24 +01:00
Alejandro Celaya
ddb2c1e641 Updated changelog 2022-12-23 20:24:55 +01:00
Alejandro Celaya
e790360de9 Added filtering dropdown to short URLs filtering bar 2022-12-23 20:15:52 +01:00
Alejandro Celaya
b00f6fadf8 Added test for parseBooleanToString 2022-12-23 20:03:27 +01:00
Alejandro Celaya
1d6f4bf5db Take into consideration exclñudeBots from query on short URLs row 2022-12-23 20:00:59 +01:00
Alejandro Celaya
80cea91339 Ensured bots exclusion is selected by default in visits filter dropdown, if it was selected in settings 2022-12-23 10:07:23 +01:00
Alejandro Celaya
5942cd6fcf Ensured proper amount of visits is displayed on short URLs based on config 2022-12-22 18:39:09 +01:00
Alejandro Celaya
2859ba6cd2 Simplified reducer logic to update short URLs when new visits come 2022-12-22 18:30:02 +01:00
Alejandro Celaya
b9285fd600 Enrues proper ordering is sent to shlink when ordering by visits 2022-12-22 18:07:44 +01:00
Alejandro Celaya
901df2b90d Added new setting to exclude bots from visits, wherever possible 2022-12-22 12:25:25 +01:00
Alejandro Celaya
662573d940 Merge pull request #771 from acelaya-forks/feature/highlight-disabled
Feature/highlight disabled
2022-12-19 20:49:46 +01:00
Alejandro Celaya
a8dcd3cac7 Updated changelog 2022-12-19 20:45:02 +01:00
Alejandro Celaya
0ac16a1626 Added test for ShortUrlStatus component 2022-12-19 20:43:55 +01:00
Alejandro Celaya
5162fa2a13 Created Tags test 2022-12-19 20:27:33 +01:00
Alejandro Celaya
73a3d1d50f Extended ShortUrlVisitsCount test to cover different contents of the tooltip 2022-12-19 20:18:50 +01:00
Alejandro Celaya
afc272c4d9 Improved ShortUrlsRow test covering new status icon 2022-12-19 20:00:52 +01:00
Alejandro Celaya
f8bcaed3ad Moved short URL status to the last column in the table 2022-12-19 19:40:04 +01:00
Alejandro Celaya
d1a1b7426e Added ShortUrlStatus concept 2022-12-18 19:26:30 +01:00
Alejandro Celaya
99485cc6d8 Removed conept on short URL tags 2022-12-18 13:29:39 +01:00
Alejandro Celaya
187fee46f4 Added extra info and new label to highlight disabled short URLs 2022-12-18 13:17:49 +01:00
Alejandro Celaya
90837546ab Exported some specific component types and improved spacing in short URLs list 2022-12-18 10:12:34 +01:00
Alejandro Celaya
f4cf4850a3 Merge pull request #769 from acelaya-forks/feature/drop-tags-cards
Feature/drop tags cards
2022-12-17 20:07:51 +01:00
Alejandro Celaya
4ef1e491bc Updated changelog 2022-12-17 20:01:31 +01:00
Alejandro Celaya
170f45d46b Removed references to tagsMode setting 2022-12-17 19:59:55 +01:00
Alejandro Celaya
37caa1ad19 Removed references to tags cards 2022-12-17 19:56:58 +01:00
Alejandro Celaya
0312a0911c Fixed build badge in README 2022-12-17 10:54:18 +01:00
Alejandro Celaya
cc88f7678c Merge pull request #768 from shlinkio/develop
Release 3.8.2
2022-12-17 10:07:37 +01:00
Alejandro Celaya
653b470fec Merge pull request #767 from acelaya-forks/feature/fixes
Feature/fixes
2022-12-17 10:03:19 +01:00
Alejandro Celaya
2603f2f987 Added missing application/json content-type when calling Shlink with payload 2022-12-17 09:57:40 +01:00
Alejandro Celaya
b106b3cd0a Updated changelog 2022-12-17 09:29:34 +01:00
Alejandro Celaya
b2b6b3af18 Fixed visits query being lost when switching between sub-sections 2022-12-17 09:28:42 +01:00
Alejandro Celaya
f911f78c95 Merge pull request #763 from shlinkio/dependabot/npm_and_yarn/express-4.18.2
Bump express from 4.17.1 to 4.18.2
2022-12-14 19:06:22 +01:00
dependabot[bot]
a7560443f3 Bump express from 4.17.1 to 4.18.2
Bumps [express](https://github.com/expressjs/express) from 4.17.1 to 4.18.2.
- [Release notes](https://github.com/expressjs/express/releases)
- [Changelog](https://github.com/expressjs/express/blob/master/History.md)
- [Commits](https://github.com/expressjs/express/compare/4.17.1...4.18.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-12-14 17:59:03 +00:00
Alejandro Celaya
ab757b2f67 Merge pull request #759 from shlinkio/develop
Release 3.8.1
2022-12-06 20:21:39 +01:00
Alejandro Celaya
261cc68624 Merge pull request #758 from acelaya-forks/feature/fix-visits-interval
Feature/fix visits interval
2022-12-06 20:20:22 +01:00
Alejandro Celaya
dc2db3a463 Updated changelog 2022-12-06 20:15:44 +01:00
Alejandro Celaya
ae625e4c8a Fixed all visits interval not working after loading the default configured interval unless switching to another one first 2022-12-06 20:13:43 +01:00
Alejandro Celaya
6f5c5b122f Fixed fallback interval not working on visits pages 2022-12-06 18:09:50 +01:00
Alejandro Celaya
5d712d7d78 Created helper curried function to compare two values 2022-12-05 17:29:59 +01:00
Alejandro Celaya
1654784471 Created now function and refactored intervalToDateRange 2022-12-05 17:18:00 +01:00
Alejandro Celaya
7d83e434e6 Merge pull request #755 from shlinkio/develop
Release 3.8.0
2022-12-03 13:34:37 +01:00
Alejandro Celaya
97a54d44c7 Merge pull request #754 from acelaya-forks/feature/visits-filters
Feature/visits filters
2022-12-03 13:29:05 +01:00
Alejandro Celaya
6694ca6918 Updated changelog 2022-12-03 13:24:16 +01:00
Alejandro Celaya
8e61e94fba Added test to check how visits stats persist filters in query string 2022-12-03 13:22:31 +01:00
Alejandro Celaya
69b1c8039e Ensured stryker-tmp folder is ignored by jest 2022-12-03 12:56:25 +01:00
Alejandro Celaya
5bd89efc09 Added test for toDateRange helper function 2022-12-03 12:45:25 +01:00
Alejandro Celaya
e5185f2099 Renamed file containing date range and date interval utils 2022-12-03 12:20:18 +01:00
Alejandro Celaya
d2ebc880a0 Created new hook to handle visits filtering via query string 2022-12-03 12:15:36 +01:00
Alejandro Celaya
165afa436d Minor refactorings and function extractions 2022-11-26 09:11:46 +01:00
Alejandro Celaya
a3f5095dce Merge pull request #751 from acelaya-forks/feature/fix-max-length
Feature/fix max length
2022-11-25 20:14:53 +01:00
Alejandro Celaya
7bda5769fb Updated changelog 2022-11-25 19:20:52 +01:00
Alejandro Celaya
0bf859d485 Simplified DeleteShortUrlModal so that it only requires writing 'delete' 2022-11-25 19:08:40 +01:00
Alejandro Celaya
b79dced185 Fixed broken short URLs table when creating short URL with too long slug 2022-11-25 18:34:27 +01:00
Alejandro Celaya
e368e618f3 Updated changelog 2022-11-22 20:29:01 +01:00
Alejandro Celaya
bc4c69f207 Merge pull request #747 from acelaya-forks/feature/modal-async-delays
Feature/modal async delays
2022-11-22 20:28:30 +01:00
Alejandro Celaya
32f29a84f7 Used TestModalWrapper in DeleteShortUrlModal test 2022-11-22 20:22:03 +01:00
Alejandro Celaya
f0a6420ba9 Extracted helper to test modals 2022-11-22 20:18:11 +01:00
Alejandro Celaya
cf91637848 Updated changelog 2022-11-22 20:09:00 +01:00
Alejandro Celaya
9bdf55374c Ensured DeleteServerModal is not removed from the DOM before close transition has finished 2022-11-22 20:08:08 +01:00
Alejandro Celaya
d21758c410 Fixed DeleteShortUrlModal being removed from the DOM before CSS transition finished 2022-11-22 19:39:07 +01:00
Alejandro Celaya
bc2c945fee Updated changelog 2022-11-20 13:01:03 +01:00
Alejandro Celaya
db2853880d Merge pull request #746 from acelaya-forks/feature/rename-tag-fix
Feature/rename tag fix
2022-11-20 12:59:10 +01:00
Alejandro Celaya
cb76c89a08 Improved HttpClientTest 2022-11-20 12:54:06 +01:00
Alejandro Celaya
059fa37ca7 Updates ShlinkApiClint to use different methods to fetch, and fixed tests 2022-11-20 12:51:07 +01:00
Alejandro Celaya
54cc99448b Merge pull request #742 from shlinkio/dependabot/npm_and_yarn/minimatch-and-recursive-readdir-and-serve-3.1.2
Bump minimatch, recursive-readdir and serve
2022-11-20 09:46:06 +01:00
Alejandro Celaya
a5dd96805d Merge pull request #743 from shlinkio/dependabot/npm_and_yarn/loader-utils-1.4.2
Bump loader-utils from 1.4.1 to 1.4.2
2022-11-20 09:46:00 +01:00
Alejandro Celaya
1e155af948 Ensured stats for renamed tagged are propagated to new name 2022-11-19 09:31:48 +01:00
Alejandro Celaya
dd9ee044eb Ensured JSON decodedoes not happen for endpoints returning empty body 2022-11-19 09:29:29 +01:00
Alejandro Celaya
bd8ea17c84 Ensured bg-warning elements always use dark text 2022-11-19 09:21:51 +01:00
Alejandro Celaya
0236f5132d Merge pull request #745 from acelaya-forks/feature/http-client
Feature/http client
2022-11-17 21:38:28 +01:00
Alejandro Celaya
b8adf5f274 Created test for HttpClient 2022-11-17 21:30:42 +01:00
Alejandro Celaya
c4bce5ec0a Added badge for Mastodon follow 2022-11-17 19:59:26 +01:00
dependabot[bot]
5bfe7dd128 Bump loader-utils from 1.4.1 to 1.4.2
Bumps [loader-utils](https://github.com/webpack/loader-utils) from 1.4.1 to 1.4.2.
- [Release notes](https://github.com/webpack/loader-utils/releases)
- [Changelog](https://github.com/webpack/loader-utils/blob/v1.4.2/CHANGELOG.md)
- [Commits](https://github.com/webpack/loader-utils/compare/v1.4.1...v1.4.2)

---
updated-dependencies:
- dependency-name: loader-utils
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-11-16 02:51:35 +00:00
Alejandro Celaya
9b3bdebb28 Wrapped logic to perform HTTP requests with fetch into an HttpClient class 2022-11-15 20:31:35 +01:00
Alejandro Celaya
7575387236 Updated changelog 2022-11-15 17:17:55 +01:00
dependabot[bot]
984a99b24e Bump minimatch, recursive-readdir and serve
Bumps [minimatch](https://github.com/isaacs/minimatch) to 3.1.2 and updates ancestor dependencies [minimatch](https://github.com/isaacs/minimatch), [recursive-readdir](https://github.com/jergason/recursive-readdir) and [serve](https://github.com/vercel/serve). These dependencies need to be updated together.


Updates `minimatch` from 3.0.4 to 3.1.2
- [Release notes](https://github.com/isaacs/minimatch/releases)
- [Commits](https://github.com/isaacs/minimatch/compare/v3.0.4...v3.1.2)

Updates `recursive-readdir` from 2.2.2 to 2.2.3
- [Release notes](https://github.com/jergason/recursive-readdir/releases)
- [Changelog](https://github.com/jergason/recursive-readdir/blob/master/CHANGELOG.md)
- [Commits](https://github.com/jergason/recursive-readdir/commits/v2.2.3)

Updates `serve` from 14.0.1 to 14.1.1
- [Release notes](https://github.com/vercel/serve/releases)
- [Commits](https://github.com/vercel/serve/compare/14.0.1...14.1.1)

---
updated-dependencies:
- dependency-name: minimatch
  dependency-type: indirect
- dependency-name: recursive-readdir
  dependency-type: indirect
- dependency-name: serve
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-11-15 16:16:52 +00:00
Alejandro Celaya
a0767417b3 Merge pull request #740 from acelaya-forks/feature/fetch
Feature/fetch
2022-11-15 17:15:43 +01:00
Alejandro Celaya
790c69ba80 Fixed ShlinkApiClient test 2022-11-15 12:19:21 +01:00
Alejandro Celaya
f7ba974d97 Fixed logic to determine if an error is a ProblemDetailError 2022-11-15 12:15:04 +01:00
Alejandro Celaya
1eab5af5c7 Fixed shortUrlDeletion test 2022-11-15 11:50:04 +01:00
Alejandro Celaya
3df0bf79f8 Removed axios from the project 2022-11-15 11:45:28 +01:00
Alejandro Celaya
34aa156d5f Migrated ImageDownloader from axios to fetch 2022-11-15 11:41:05 +01:00
Alejandro Celaya
a88ebc26a9 Merge branch 'develop' into feature/fetch 2022-11-15 11:27:53 +01:00
Alejandro Celaya
d800062159 Extracted helper fetch function and migrated remoteServers redux action from axios to fetch 2022-11-14 23:25:39 +01:00
Alejandro Celaya
e5afe4f767 Migrated ShlinkApiClient from axios to fetch 2022-11-14 23:06:06 +01:00
Alejandro Celaya
ee7a091586 Merge pull request #739 from acelaya-forks/feature/rtk-errors
Changed how errors are serialized by async thunks
2022-11-13 18:36:10 +01:00
Alejandro Celaya
7add83f985 Changed how errors are serialized by async thunks 2022-11-13 18:26:35 +01:00
Alejandro Celaya
16bee43f12 Migrated ShlinkApiClient from axios to fetch 2022-11-13 16:57:16 +01:00
Alejandro Celaya
ba48104c5c Refactored createVisitsReducer so that it expects an object param 2022-11-13 09:59:49 +01:00
Alejandro Celaya
cc620ddf79 Merge pull request #735 from acelaya-forks/feature/visits-rtk
Feature/visits rtk
2022-11-12 20:46:50 +01:00
Alejandro Celaya
6103f6a89b Separated param definition and unpacking for readibility 2022-11-12 20:41:55 +01:00
Alejandro Celaya
4b2c3d2db7 Extracted duplicated code on creating visits reducers to a common helper function 2022-11-12 20:37:04 +01:00
Alejandro Celaya
dac69daf03 Migrated tagVisits reducer to RTK 2022-11-12 20:02:58 +01:00
Alejandro Celaya
3e474a3f2d Migrated shortUrlVisits reducer to RTK 2022-11-12 19:36:12 +01:00
Alejandro Celaya
f81999a4fe Migrated orphanVisits reducer to RTK 2022-11-12 19:15:41 +01:00
Alejandro Celaya
fd80fd65c9 Migrated nonOrphanVisits reducer to RTK 2022-11-12 18:18:16 +01:00
Alejandro Celaya
ab7c52d049 Migrated domainVisits reducer to RTK 2022-11-12 17:51:37 +01:00
Alejandro Celaya
a3cc3d5fc2 Migrated visits-loading actions to payload actions 2022-11-12 10:34:44 +01:00
Alejandro Celaya
a6ed0c811d Updated getShortUrlVisits action so that it expects a signle DTO param 2022-11-12 09:38:24 +01:00
Alejandro Celaya
8e6b9c5afb Updated getOrphanVisits action so that it expects a signle DTO param 2022-11-12 09:32:52 +01:00
Alejandro Celaya
b9efdd69f1 Updated getNonOrphanVisits action so that it expects a signle DTO param 2022-11-12 09:21:23 +01:00
Alejandro Celaya
3b96b89492 Updated getDomainVisits action so that it expects a signle DTO param 2022-11-12 09:18:41 +01:00
Alejandro Celaya
32f7374d92 Migrated progress and fallback visits actions to payload actions 2022-11-12 09:01:43 +01:00
Alejandro Celaya
c6eec8b266 Changed getVisitsWithLoader reducer helper so that it expects an action prefix instead of an action map 2022-11-12 08:49:14 +01:00
Alejandro Celaya
634ae94542 Merge pull request #734 from acelaya-forks/feature/more-rtk
Feature/more rtk
2022-11-11 20:33:05 +01:00
Alejandro Celaya
5095a2c59e Migrated visitsOverview reducer to RTK 2022-11-11 20:23:19 +01:00
Alejandro Celaya
002d2ba8e6 Added tests for selectedServerReducerCreator 2022-11-11 20:01:45 +01:00
Alejandro Celaya
d44fe945d8 Migrated selectedServer reducer to RTK 2022-11-11 19:31:05 +01:00
Alejandro Celaya
6221f9ed05 Migrated selectServer action to RTK and moved loadMercureInfo to an action listener 2022-11-11 19:21:17 +01:00
Alejandro Celaya
2e0e24d87b Migrated fetchServers to RTK 2022-11-11 19:21:17 +01:00
Alejandro Celaya
a1d869900b Merge pull request #733 from acelaya-forks/feature/fix-mercure-integration
Fixed mercure integration
2022-11-11 16:57:40 +01:00
Alejandro Celaya
d90f6c2019 Fixed mercure integration 2022-11-11 16:51:21 +01:00
Alejandro Celaya
ed4c03f154 Merge pull request #732 from acelaya-forks/feature/more-rtk
Feature/more rtk
2022-11-09 19:36:49 +01:00
Alejandro Celaya
7bfccafca8 Migrated shortUrlsList reducer to RTK 2022-11-09 19:13:44 +01:00
Alejandro Celaya
ae49090bad Split short URL edition reducer and async thunk 2022-11-09 18:40:51 +01:00
Alejandro Celaya
979c16eb9c Updated listShortUrls action to use payload 2022-11-09 18:27:05 +01:00
Alejandro Celaya
fe85291772 Changed format on action types and reducer names for those already migrated to RTK 2022-11-09 18:19:07 +01:00
Alejandro Celaya
893c5ace6f Merge pull request #731 from shlinkio/dependabot/npm_and_yarn/loader-utils-1.4.1
Bump loader-utils from 1.4.0 to 1.4.1
2022-11-09 08:31:56 +01:00
Alejandro Celaya
89423737e8 Removed hardcoded action references by improving dependency injection 2022-11-08 22:59:41 +01:00
Alejandro Celaya
f9bfb742da Migrated tagsList reducer to RTK 2022-11-08 22:48:53 +01:00
Alejandro Celaya
b7622b2b38 Migrated filterTags action to use payload 2022-11-08 21:59:17 +01:00
dependabot[bot]
8cfb4cf1e1 Bump loader-utils from 1.4.0 to 1.4.1
Bumps [loader-utils](https://github.com/webpack/loader-utils) from 1.4.0 to 1.4.1.
- [Release notes](https://github.com/webpack/loader-utils/releases)
- [Changelog](https://github.com/webpack/loader-utils/blob/v1.4.1/CHANGELOG.md)
- [Commits](https://github.com/webpack/loader-utils/compare/v1.4.0...v1.4.1)

---
updated-dependencies:
- dependency-name: loader-utils
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-11-08 09:26:38 +00:00
Alejandro Celaya
b9e02cf344 Merge pull request #730 from acelaya-forks/feature/more-rtk
Feature/more rtk
2022-11-07 22:49:33 +01:00
Alejandro Celaya
033df3c3d6 Migrated tagDelete reducer to RTK 2022-11-07 22:41:02 +01:00
Alejandro Celaya
692eaf7dc9 Referenced createNewVisits action directly, instead of its action type 2022-11-07 22:29:15 +01:00
Alejandro Celaya
22b3794154 Migrated tagDeleted action to payload 2022-11-07 22:22:44 +01:00
Alejandro Celaya
dbb08a6ce0 Ensured tags deleted are not removed from list until modal has been hidden 2022-11-07 22:19:44 +01:00
Alejandro Celaya
0571a4a88f Migrated tagEdit reducer to RTK 2022-11-07 22:12:48 +01:00
Alejandro Celaya
648744f440 Migrated tag actions to have a single DTO param 2022-11-07 21:57:01 +01:00
Alejandro Celaya
f8fc1245ca Migrated editTag and tagEdited actions to use payload 2022-11-07 21:45:33 +01:00
Alejandro Celaya
5ecc791b38 Ensured tags list is not updated until the edit modal is closed 2022-11-07 21:32:19 +01:00
Alejandro Celaya
2183b09ffe Merge pull request #728 from acelaya-forks/feature/state-machine-poc
Feature/state machine poc
2022-11-07 19:03:51 +01:00
Alejandro Celaya
085ab521c3 Implemented state machine for short URL creation 2022-11-07 18:55:12 +01:00
Alejandro Celaya
61b274bab9 Replaced inheritance by composition on short URL creation interface 2022-11-07 18:27:41 +01:00
Alejandro Celaya
4ca31fc162 Added flag on short URL creation which tells if the short URL was already saved 2022-11-07 18:24:26 +01:00
Alejandro Celaya
ae1d39bede Merge pull request #725 from acelaya-forks/feature/more-rtk
Feature/more rtk
2022-11-06 19:39:03 +01:00
Alejandro Celaya
f803941fe4 Migrated short URL detail reducer to RTK 2022-11-06 19:32:02 +01:00
Alejandro Celaya
f93bb88d35 Refactored getShortUrlDetail action to have a single DTO param 2022-11-06 19:16:51 +01:00
Alejandro Celaya
ea199dbf8f Removed redundant types 2022-11-06 19:12:41 +01:00
Alejandro Celaya
526d7195bc Updated getShortUrlDetail action to use payload action 2022-11-06 19:06:39 +01:00
Alejandro Celaya
cf4143e4e2 Removed unnecesarry promise references in delete short URL modal 2022-11-06 18:48:47 +01:00
Alejandro Celaya
0f552ae6f4 Removed unnecessary castings to any 2022-11-06 13:21:46 +01:00
Alejandro Celaya
830071278e Migrated shortUrlDeletion reducer to RTK 2022-11-06 13:06:55 +01:00
Alejandro Celaya
d468fb1efe Migrated deleteShortUrl action creator to use PayloadAction and have a single param 2022-11-06 12:46:29 +01:00
Alejandro Celaya
2a268de2cb Migrated editShortUrl reducer to RTK 2022-11-06 12:33:23 +01:00
Alejandro Celaya
77cbb8ebc4 Refactored editShortUrl action to require just one param 2022-11-06 11:59:39 +01:00
Alejandro Celaya
bf84e4a2ed Migrated editShortUrl payload action 2022-11-06 11:53:23 +01:00
Alejandro Celaya
a316366ae9 Migrated shortUrlCreation reducer to RTK 2022-11-06 10:11:44 +01:00
Alejandro Celaya
50823003b4 Migrated settings reducer to RTK 2022-11-06 09:40:23 +01:00
Alejandro Celaya
7c61033bdf Migrated createNewVisits action creator to RTK 2022-11-05 13:05:44 +01:00
Alejandro Celaya
d588d8d9ef Migrated create visit action to use payload 2022-11-05 13:01:00 +01:00
Alejandro Celaya
cd90d3e581 Merge pull request #724 from acelaya-forks/feature/more-rtk
Feature/more rtk
2022-11-05 10:53:12 +01:00
Alejandro Celaya
54407af980 Fixed tests 2022-11-05 10:49:28 +01:00
Alejandro Celaya
a31cdcc9f0 Migrated selectedServer reducer to use payload actions 2022-11-05 10:36:57 +01:00
Alejandro Celaya
10d4419387 Migrated servers reducer to RTK 2022-11-05 10:08:24 +01:00
Alejandro Celaya
6f67f7bbf0 Removed redundant createServer action, leaving just createServers 2022-11-05 09:40:12 +01:00
Alejandro Celaya
90ef41b419 Migrated server list actions to use payload prop 2022-11-05 09:24:12 +01:00
Alejandro Celaya
62ab86aefa Created custom and better typed version of createAsyncThunk 2022-11-05 09:10:30 +01:00
Alejandro Celaya
1dd26fb76f Merge pull request #723 from acelaya-forks/feature/more-rtk
Feature/more rtk
2022-11-04 20:56:30 +01:00
Alejandro Celaya
4a95724425 Migrated mercureInfo reducer to RTK 2022-11-04 20:52:06 +01:00
Alejandro Celaya
f209fa2d58 Migrated sidebar reducer to RTK 2022-11-04 19:45:03 +01:00
Alejandro Celaya
85e2aab4df Migrated appUpdated reducer to RTK 2022-11-04 19:39:15 +01:00
Alejandro Celaya
26c3ea19f4 Merge pull request #722 from acelaya-forks/feature/redux-toolkit-poc
Feature/redux toolkit poc
2022-11-04 19:07:32 +01:00
Alejandro Celaya
a1e2cd7274 Fixed changelog 2022-11-04 18:59:08 +01:00
Alejandro Celaya
6363822ffd Updated to redux/toolkit 1.9 2022-11-04 18:58:21 +01:00
Alejandro Celaya
34f4411aa1 Migrated domainRedirects reducer to redux/toolkit 2022-11-04 18:56:34 +01:00
Alejandro Celaya
b6d08e2203 Updated editDomainRedirects action, to expect a payload DTO instead of multiple args 2022-11-04 17:10:02 +01:00
Alejandro Celaya
4fa6ae493d Removed unnecesary type castings and improved type inference for actions in demainsListReducer 2022-11-04 16:50:03 +01:00
Alejandro Celaya
79645099ba Added explicit import 2022-11-03 20:53:59 +01:00
Alejandro Celaya
18d478e16e Removed unneeded type castings and eslint suppressions in domainsList reducer 2022-11-03 20:51:20 +01:00
Alejandro Celaya
da97b76563 Migrated rest of domainslistreducer-related elements on test to the new ones 2022-11-03 20:29:22 +01:00
Alejandro Celaya
d25dbd5ae6 Replaced domainsList old reducer with new reducer in test 2022-11-03 20:15:28 +01:00
Alejandro Celaya
88e8f3363b Fixed domainsListReducer test so that it works with new payload prop in actions 2022-11-03 19:52:57 +01:00
Alejandro Celaya
24483ec330 Added first redux toolkit based reducer for domains 2022-11-02 20:40:14 +01:00
Alejandro Celaya
15a9fba091 Migrated redux store creation to redux toolkit 2022-11-01 12:52:27 +01:00
Alejandro Celaya
73e2485e09 Merge pull request #720 from acelaya-forks/feature/limit-time
Feature/limit time
2022-10-23 10:58:16 +02:00
Alejandro Celaya
0d94879e49 Updated changelog 2022-10-23 10:51:28 +02:00
Alejandro Celaya
6df12ce194 Moved date-time related utils to the proper folder 2022-10-23 10:49:35 +02:00
Alejandro Celaya
c3b60367f3 Added test covering custom formatting in DateInput 2022-10-23 10:43:01 +02:00
Alejandro Celaya
10d3deff37 Formatted scrollbar in date picker for time component 2022-10-23 10:22:31 +02:00
Alejandro Celaya
3cb79c167e Fixed date picker time styles 2022-10-23 10:13:53 +02:00
Alejandro Celaya
57a17d7e92 Created component for DateTimeInputs 2022-10-18 22:02:09 +02:00
Alejandro Celaya
894934fd08 Merge pull request #716 from acelaya-forks/feature/api3-support
Feature/api3 support
2022-10-12 10:55:16 +02:00
Alejandro Celaya
5a8aae3614 Updated changelog 2022-10-12 10:45:43 +02:00
Alejandro Celaya
3dde1a5b05 Covered short URL deletion when threshold error occurs 2022-10-12 10:43:30 +02:00
Alejandro Celaya
e6c79c19c2 Added support for API v3 error types on different error handlers 2022-10-12 10:35:16 +02:00
Alejandro Celaya
d64abeecdc Use APi v3 by default, and fall back to v2 in case of not found errors 2022-10-12 10:19:54 +02:00
Alejandro Celaya
da6d45a72c Updated to axios 1.1.2 2022-10-11 08:22:20 +02:00
Alejandro Celaya
497a735d80 Merge pull request #714 from acelaya-forks/feature/fix-datepicker-triangle
Feature/fix datepicker triangle
2022-10-08 10:23:06 +02:00
Alejandro Celaya
89f031b338 Updated changelog 2022-10-08 10:18:52 +02:00
Alejandro Celaya
47630dbcd2 Fixed date time picker arrow getting placed on the very edge of the popper 2022-10-08 10:18:08 +02:00
Alejandro Celaya
0d57684565 Merge pull request #712 from acelaya-forks/feature/update-deps
Updated deps
2022-10-05 18:25:20 +02:00
Alejandro Celaya
5880767ac3 Fixed incorrect import 2022-10-05 17:23:41 +02:00
Alejandro Celaya
14c4c29af3 Updated changelog 2022-10-05 17:19:16 +02:00
Alejandro Celaya
9f5614446e Updated bootstrap 2022-10-05 17:17:03 +02:00
Alejandro Celaya
d755e8ffc4 Updated to axios 1.0 2022-10-05 17:12:10 +02:00
Alejandro Celaya
a7a968ab6e Added dependency to history module as react-router-dom no longer depnds on it 2022-10-05 16:35:44 +02:00
Alejandro Celaya
f5757c6081 Fixed incorrectly inferred types 2022-10-04 23:27:11 +02:00
Alejandro Celaya
29fa4fa34d Fixed incorrectly inferred types 2022-10-04 23:24:07 +02:00
Alejandro Celaya
d2de9fb669 Updated dev deps 2022-10-04 23:17:12 +02:00
Alejandro Celaya
e124cd2490 Updated prod deps 2022-10-04 23:09:30 +02:00
Alejandro Celaya
e76c9041b5 Updated @fortawesome/fontawesome-svg-core to v6.2 2022-10-04 23:02:27 +02:00
Alejandro Celaya
755ae23fdb Updated fontawesome deps 2022-10-03 20:06:57 +02:00
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
471 changed files with 23173 additions and 59427 deletions

View File

@@ -1,5 +1,4 @@
./.github
./.stryker-tmp
./build
./coverage
./node_modules

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,6 @@ jobs:
ci:
uses: shlinkio/github-actions/.github/workflows/web-app-ci.yml@main
with:
node-version: 16.13
with-mutation-tests: true
node-version: 20.2
publish-coverage: true
force-install: true

View File

@@ -9,19 +9,18 @@ jobs:
continue-on-error: true
steps:
- name: Checkout code
uses: actions/checkout@v2
uses: actions/checkout@v3
with:
repository: ${{ github.event.pull_request.head.repo.full_name }}
ref: ${{ github.event.pull_request.head.ref }}
- name: Use node.js
uses: actions/setup-node@v1
uses: actions/setup-node@v3
with:
node-version: 16.13
node-version: 20.2
- 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
- name: Deploy preview
uses: shlinkio/deploy-preview-action@v1.0.1

View File

@@ -1,28 +1,15 @@
name: Build docker image
name: Build and publish docker image
on:
push:
branches:
- develop
tags:
- 'v*'
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
platforms: 'linux/arm64/v8,linux/amd64'

View File

@@ -10,13 +10,13 @@ jobs:
runs-on: ubuntu-20.04
steps:
- name: Checkout code
uses: actions/checkout@v2
uses: actions/checkout@v3
- name: Use node.js
uses: actions/setup-node@v1
uses: actions/setup-node@v3
with:
node-version: 16.13
node-version: 20.2
- 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:

1
.gitignore vendored
View File

@@ -3,7 +3,6 @@
# testing
/coverage
/.stryker-tmp
/reports
# production

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,13 @@ 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.10.2] - 2023-07-09
### 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.
* *Nothing*
### 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.
* [#781](https://github.com/shlinkio/shlink-web-client/issues/781) Migrate tests from jest to vitest.
* [#843](https://github.com/shlinkio/shlink-web-client/issues/843) Build docker image only for new tags, making sure it always includes an actual version number.
### Deprecated
* *Nothing*
@@ -27,7 +19,256 @@ 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.
* *Nothing*
## [3.10.1] - 2023-04-23
### Added
* *Nothing*
### Changed
* *Nothing*
### Deprecated
* *Nothing*
### Removed
* *Nothing*
### Fixed
* [#826](https://github.com/shlinkio/shlink-web-client/issues/826) Fix generated short URLs CSV so that it can be used to import on Shlink.
## [3.10.0] - 2023-03-19
### Added
* [#807](https://github.com/shlinkio/shlink-web-client/issues/807) Add support for device-specific long-URLs when creating or editing short URLs.
* [#808](https://github.com/shlinkio/shlink-web-client/issues/808) Respect settings on excluding bots in the overview section, for visits cards.
* [#809](https://github.com/shlinkio/shlink-web-client/issues/809) Respect settings on excluding bots in the tags list.
### Changed
* [#798](https://github.com/shlinkio/shlink-web-client/issues/798) Remove stryker and mutation testing.
* [#800](https://github.com/shlinkio/shlink-web-client/issues/800) Use `/tags/stats` endpoint to load tags stats, when the server supports it.
* Update to Vite 4.2
* Update to TypeScript 5
* Update to coding standard v2.1.0
* Decouple tests from RTK internals.
### Deprecated
* *Nothing*
### Removed
* *Nothing*
### Fixed
* [#799](https://github.com/shlinkio/shlink-web-client/issues/799) Fix fallback visits not taking into account configuration regarding excluding bots.
## [3.9.1] - 2022-12-31
### Added
* *Nothing*
### Changed
* *Nothing*
### Deprecated
* *Nothing*
### Removed
* *Nothing*
### Fixed
* [#787](https://github.com/shlinkio/shlink-web-client/issues/787) Fixed wrong base path set in vite config when homepage is set as empty string.
## [3.9.0] - 2022-12-31
### Added
* [#750](https://github.com/shlinkio/shlink-web-client/issues/750) Added new icon indicators telling if a short URL can be normally visited, it received the max amount of visits, is still not enabled, etc.
* [#764](https://github.com/shlinkio/shlink-web-client/issues/764) Added support to exclude visits from visits on short URLs list when consuming Shlink 3.4.0.
This feature also comes with a new setting to disable visits from bots by default, both on short URLs lists and visits sections.
* [#760](https://github.com/shlinkio/shlink-web-client/issues/760) Added support to exclude short URLs which have reached the maximum amount of visits, or are valid until a date in the past.
### Changed
* [#753](https://github.com/shlinkio/shlink-web-client/issues/753) Migrated from react-scripts/webpack to vite.
* [#770](https://github.com/shlinkio/shlink-web-client/issues/770) Updated to latest dependencies.
* [#741](https://github.com/shlinkio/shlink-web-client/issues/741) Improved `visitsAsyncThunk`, making it wrap pending/fulfilled/rejected actions, as well as custom ones, in a type-safe way.
### Deprecated
* *Nothing*
### Removed
* [#736](https://github.com/shlinkio/shlink-web-client/issues/736) Removed cards mode in tags. Only table mode is supported now.
* [#774](https://github.com/shlinkio/shlink-web-client/issues/774) Dropped support for Shlink older than 2.8.0.
### Fixed
* [#715](https://github.com/shlinkio/shlink-web-client/issues/715) Fixed connection still failing on miss-configured servers, after editing their params to set proper values.
## [3.8.2] - 2022-12-17
### Added
* *Nothing*
### Changed
* *Nothing*
### Deprecated
* *Nothing*
### Removed
* *Nothing*
### Fixed
* [#766](https://github.com/shlinkio/shlink-web-client/issues/766) Fixed visits query being lost when switching between sub-sections.
* [#765](https://github.com/shlinkio/shlink-web-client/issues/765) Added missing `"Content-Type": "application/json"` to requests with payload, making older Shlink versions fail.
## [3.8.1] - 2022-12-06
### Added
* *Nothing*
### Changed
* *Nothing*
### Deprecated
* *Nothing*
### Removed
* *Nothing*
### Fixed
* [#756](https://github.com/shlinkio/shlink-web-client/issues/756) Fixed all visits interval not working unless switching to a different interval first.
* [#757](https://github.com/shlinkio/shlink-web-client/issues/757) Fixed visits fallback interval not working until the visits view has been loaded at least twice.
## [3.8.0] - 2022-12-03
### Added
* [#708](https://github.com/shlinkio/shlink-web-client/issues/708) Added support for API v3.
* [#717](https://github.com/shlinkio/shlink-web-client/issues/717) Allowed to select time in 10 minute intervals when configuring "enabled since" and "enabled until" on short URLs.
* [#748](https://github.com/shlinkio/shlink-web-client/issues/748) Improved visits section to add filters to the query string, allowing to navigate to a specific state or bookmarking filters.
### Changed
* [#713](https://github.com/shlinkio/shlink-web-client/issues/713) Updated dependencies.
* [#620](https://github.com/shlinkio/shlink-web-client/issues/620) Migrated all reducers to redux toolkit.
* [#721](https://github.com/shlinkio/shlink-web-client/issues/721) Migrated from axios to fetch.
### Deprecated
* *Nothing*
### Removed
* *Nothing*
### Fixed
* [#590](https://github.com/shlinkio/shlink-web-client/issues/590) Fixed position of the datepicker triangle.
* [#729](https://github.com/shlinkio/shlink-web-client/issues/729) Fixed wrong stats displayed in tags after renaming.
* [#737](https://github.com/shlinkio/shlink-web-client/issues/737) Fixed incorrect contrast in warning messages when using dark theme.
* [#726](https://github.com/shlinkio/shlink-web-client/issues/726) Fixed delete server and delete short URL modals getting removed from the DOM before finishing close transition.
* [#749](https://github.com/shlinkio/shlink-web-client/issues/749) Fixed broken short URLs table when some short URL has a too long custom slug.
## [3.7.3] - 2022-09-13
### Added
* [#703](https://github.com/shlinkio/shlink-web-client/issues/703) Added support to publish docker image in GHCR.
### Changed
* *Nothing*
### Deprecated
* *Nothing*
### Removed
* *Nothing*
### Fixed
* [#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 +292,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,10 +1,10 @@
FROM node:16.13-alpine as node
FROM node:20.2-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 && npm run build
FROM nginx:1.21-alpine
FROM nginx:1.23-alpine
LABEL maintainer="Alejandro Celaya <alejandro@alejandrocelaya.com>"
RUN rm -r /usr/share/nginx/html && rm /etc/nginx/conf.d/default.conf
COPY config/docker/nginx.conf /etc/nginx/conf.d/default.conf

View File

@@ -1,11 +1,12 @@
# shlink-web-client
[![Build Status](https://img.shields.io/github/workflow/status/shlinkio/shlink-web-client/Continuous%20integration/develop?logo=github&style=flat-square)](https://github.com/shlinkio/shlink-web-client/actions?query=workflow%3A%22Continuous+integration%22)
[![Build Status](https://img.shields.io/github/actions/workflow/status/shlinkio/shlink-web-client/ci.yml?branch=develop&logo=github&style=flat-square)](https://github.com/shlinkio/shlink-web-client/actions/workflows/ci.yml?query=workflow%3A%22Continuous+integration%22)
[![Code Coverage](https://img.shields.io/codecov/c/gh/shlinkio/shlink-web-client/develop?style=flat-square)](https://app.codecov.io/gh/shlinkio/shlink-web-client)
[![GitHub release](https://img.shields.io/github/release/shlinkio/shlink-web-client.svg?style=flat-square)](https://github.com/shlinkio/shlink-web-client/releases/latest)
[![Docker pulls](https://img.shields.io/docker/pulls/shlinkio/shlink-web-client.svg?logo=docker&style=flat-square)](https://hub.docker.com/r/shlinkio/shlink-web-client/)
[![GitHub license](https://img.shields.io/github/license/shlinkio/shlink-web-client.svg?style=flat-square)](https://github.com/shlinkio/shlink-web-client/blob/main/LICENSE)
[![Twitter](https://img.shields.io/twitter/follow/shlinkio?color=blue&label=follow&logo=twitter&style=flat-square)](https://twitter.com/shlinkio)
[![Twitter](https://img.shields.io/badge/follow-shlinkio-blue.svg?style=flat-square&logo=twitter&color=blue)](https://twitter.com/shlinkio)
[![Mastodon](https://img.shields.io/mastodon/follow/109329425426175098?color=%236364ff&domain=https%3A%2F%2Ffosstodon.org&label=follow&logo=mastodon&logoColor=white&style=flat-square)](https://fosstodon.org/@shlinkio)
[![Paypal Donate](https://img.shields.io/badge/Donate-paypal-blue.svg?style=flat-square&logo=paypal&colorA=cccccc)](https://slnk.to/donate)
A ReactJS-based progressive web application for [Shlink](https://shlink.io).
@@ -53,7 +54,7 @@ Those servers can be exported and imported in other browsers, but if for some re
[
{
"name": "Main server",
"url": "https://doma.in",
"url": "https://s.test",
"apiKey": "09c972b7-506b-49f1-a19a-d729e22e599c"
},
{
@@ -84,7 +85,7 @@ If you want to pre-configure a single server, you can provide its config via env
docker run \
--name shlink-web-client \
-p 8000:80 \
-e SHLINK_SERVER_URL=https://doma.in \
-e SHLINK_SERVER_URL=https://s.test \
-e SHLINK_SERVER_API_KEY=6aeb82c6-e275-4538-a747-31f9abfba63c \
shlinkio/shlink-web-client
```

View File

@@ -1,15 +0,0 @@
module.exports = {
presets: [
[
'react-app',
{
runtime: 'automatic',
typescript: true,
},
],
],
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

@@ -1,29 +0,0 @@
const path = require('path');
// This is a custom Jest transformer turning file imports into filenames.
// http://facebook.github.io/jest/docs/en/webpack.html
module.exports = {
process(src, filename) {
const assetFilename = JSON.stringify(path.basename(filename));
if (filename.match(/\.svg$/)) {
return `module.exports = {
__esModule: true,
default: ${assetFilename},
ReactComponent: (props) => ({
$$typeof: Symbol.for('react.element'),
type: 'svg',
ref: null,
key: null,
props: Object.assign({}, props, {
children: ${assetFilename}
})
}),
};`;
}
return `module.exports = ${assetFilename};`;
},
};

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() });

27
config/test/setupTests.ts Normal file
View File

@@ -0,0 +1,27 @@
import 'vitest-canvas-mock';
import 'chart.js/auto';
import type { TestingLibraryMatchers } from '@testing-library/jest-dom/matchers';
import matchers from '@testing-library/jest-dom/matchers';
import { cleanup } from '@testing-library/react';
import ResizeObserver from 'resize-observer-polyfill';
import { afterEach, expect } from 'vitest';
// Workaround for TypeScript error: https://github.com/testing-library/jest-dom/issues/439#issuecomment-1536524120
declare module 'vitest' {
interface Assertion<T = any> extends jest.Matchers<void, T>, TestingLibraryMatchers<T, void> {}
}
// Extends Vitest's expect method with methods from react-testing-library
expect.extend(matchers);
afterEach(() => {
// Clears all mocks after every test
vi.clearAllMocks();
// Run a cleanup after each test case (e.g. clearing jsdom)
cleanup();
});
(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,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,11 +3,11 @@ version: '3'
services:
shlink_web_client_node:
container_name: shlink_web_client_node
image: node:16.13-alpine
command: /bin/sh -c "cd /home/shlink/www && npm install && npm run start"
image: node:20.2-alpine
command: /bin/sh -c "cd /home/shlink/www && npm install --force && npm run start"
volumes:
- ./:/home/shlink/www
ports:
- "3000:3000"
- "56745:56745"
- "5000:5000"
- "4173:4173"

90
index.html Normal file
View File

@@ -0,0 +1,90 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<meta name="theme-color" content="#4696e5">
<!--
manifest.json provides metadata used when your web app is added to the
homescreen on Android. See https://developers.google.com/web/fundamentals/engage-and-retain/web-app-manifest/
-->
<link rel="manifest" href="/manifest.json" crossorigin="use-credentials">
<!-- FavIcon itself -->
<link rel="icon" type="image/x-icon" href="/favicon.ico">
<link rel="icon" type="image/svg+xml" href="/favicon.svg" sizes="any">
<link rel="icon" type="image/png" href="/favicon.png">
<link rel="icon" type="image/gif" href="/favicon.gif">
<!-- Apple Touch -->
<link rel="apple-touch-icon" sizes="16x16" href="/icons/icon-16x16.png">
<link rel="apple-touch-icon" sizes="24x24" href="/icons/icon-24x24.png">
<link rel="apple-touch-icon" sizes="32x32" href="/icons/icon-32x32.png">
<link rel="apple-touch-icon" sizes="40x40" href="/icons/icon-40x40.png">
<link rel="apple-touch-icon" sizes="48x48" href="/icons/icon-48x48.png">
<link rel="apple-touch-icon" sizes="60x60" href="/icons/icon-60x60.png">
<link rel="apple-touch-icon" sizes="64x64" href="/icons/icon-64x64.png">
<link rel="apple-touch-icon" sizes="72x72" href="/icons/icon-72x72.png">
<link rel="apple-touch-icon" sizes="76x76" href="/icons/icon-76x76.png">
<link rel="apple-touch-icon" sizes="96x96" href="/icons/icon-96x96.png">
<link rel="apple-touch-icon" sizes="114x114" href="/icons/icon-114x114.png">
<link rel="apple-touch-icon" sizes="120x120" href="/icons/icon-120x120.png">
<link rel="apple-touch-icon" sizes="128x128" href="/icons/icon-128x128.png">
<link rel="apple-touch-icon" sizes="144x144" href="/icons/icon-144x144.png">
<link rel="apple-touch-icon" sizes="150x150" href="/icons/icon-150x150.png">
<link rel="apple-touch-icon" sizes="152x152" href="/icons/icon-152x152.png">
<link rel="apple-touch-icon" sizes="160x160" href="/icons/icon-160x160.png">
<link rel="apple-touch-icon" sizes="167x167" href="/icons/icon-167x167.png">
<link rel="apple-touch-icon" sizes="180x180" href="/icons/icon-180x180.png">
<link rel="apple-touch-icon" sizes="192x192" href="/icons/icon-192x192.png">
<link rel="apple-touch-icon" sizes="196x196" href="/icons/icon-196x196.png">
<link rel="apple-touch-icon" sizes="228x228" href="/icons/icon-228x228.png">
<link rel="apple-touch-icon" sizes="256x256" href="/icons/icon-256x256.png">
<link rel="apple-touch-icon" sizes="310x310" href="/icons/icon-310x310.png">
<link rel="apple-touch-icon" sizes="384x384" href="/icons/icon-384x384.png">
<link rel="apple-touch-icon" sizes="512x512" href="/icons/icon-512x512.png">
<link rel="apple-touch-icon" sizes="1024x1024" href="/icons/icon-1024x1024.png">
<!-- Normal -->
<link rel="icon" type="image/png" sizes="1024x1024" href="/icons/icon-1024x1024.png">
<link rel="icon" type="image/png" sizes="512x512" href="/icons/icon-512x512.png">
<link rel="icon" type="image/png" sizes="384x384" href="/icons/icon-384x384.png">
<link rel="icon" type="image/png" sizes="310x310" href="/icons/icon-310x310.png">
<link rel="icon" type="image/png" sizes="256x256" href="/icons/icon-256x256.png">
<link rel="icon" type="image/png" sizes="228x228" href="/icons/icon-228x228.png">
<link rel="icon" type="image/png" sizes="196x196" href="/icons/icon-196x196.png">
<link rel="icon" type="image/png" sizes="192x192" href="/icons/icon-192x192.png">
<link rel="icon" type="image/png" sizes="180x180" href="/icons/icon-180x180.png">
<link rel="icon" type="image/png" sizes="167x167" href="/icons/icon-167x167.png">
<link rel="icon" type="image/png" sizes="160x160" href="/icons/icon-160x160.png">
<link rel="icon" type="image/png" sizes="152x152" href="/icons/icon-152x152.png">
<link rel="icon" type="image/png" sizes="150x150" href="/icons/icon-150x150.png">
<link rel="icon" type="image/png" sizes="144x144" href="/icons/icon-144x144.png">
<link rel="icon" type="image/png" sizes="128x128" href="/icons/icon-128x128.png">
<link rel="icon" type="image/png" sizes="120x120" href="/icons/icon-120x120.png">
<link rel="icon" type="image/png" sizes="114x114" href="/icons/icon-114x114.png">
<link rel="icon" type="image/png" sizes="96x96" href="/icons/icon-96x96.png">
<link rel="icon" type="image/png" sizes="76x76" href="/icons/icon-76x76.png">
<link rel="icon" type="image/png" sizes="72x72" href="/icons/icon-72x72.png">
<link rel="icon" type="image/png" sizes="64x64" href="/icons/icon-64x64.png">
<link rel="icon" type="image/png" sizes="60x60" href="/icons/icon-60x60.png">
<link rel="icon" type="image/png" sizes="48x48" href="/icons/icon-48x48.png">
<link rel="icon" type="image/png" sizes="40x40" href="/icons/icon-40x40.png">
<link rel="icon" type="image/png" sizes="32x32" href="/icons/icon-32x32.png">
<link rel="icon" type="image/png" sizes="24x24" href="/icons/icon-24x24.png">
<link rel="icon" type="image/png" sizes="16x16" href="/icons/icon-16x16.png">
<!-- MS -->
<meta name="msapplication-TileImage" content="/icons/icon-144x144.png">
<meta name="msapplication-square70x70logo" content="/icons/icon-70x70.png">
<meta name="msapplication-square144x144logo" content="/icons/icon-144x144.png">
<meta name="msapplication-square150x150logo" content="/icons/icon-150x150.png">
<meta name="msapplication-square310x310logo" content="/icons/icon-310x310.png">
<title>Shlink — The URL shortener</title>
</head>
<body>
<noscript>
You need to enable JavaScript to run this app.
</noscript>
<div id="root"></div>
<script type="module" src="/src/index.tsx"></script>
</body>
</html>

View File

@@ -1,37 +0,0 @@
module.exports = {
coverageDirectory: '<rootDir>/coverage',
collectCoverageFrom: [
'src/**/*.{ts,tsx}',
'!src/*.{ts,tsx}',
'!src/reducers/index.ts',
'!src/**/provideServices.ts',
'!src/container/*.ts',
],
coverageThreshold: {
global: {
statements: 85,
branches: 80,
functions: 80,
lines: 85,
},
},
setupFiles: [ '<rootDir>/config/setupEnzyme.js' ],
testMatch: [ '<rootDir>/test/**/*.test.{ts,tsx}' ],
testEnvironment: 'jsdom',
testURL: 'http://localhost',
transform: {
'^.+\\.(ts|tsx|js)$': '<rootDir>/node_modules/babel-jest',
'^(?!.*\\.(ts|tsx|js|json)$)': '<rootDir>/config/jest/fileTransform.js',
},
transformIgnorePatterns: [
'<rootDir>/.stryker-tmp',
'[/\\\\]node_modules[/\\\\].+\\.(js|jsx|ts|tsx)$',
'^.+\\.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',
},
moduleFileExtensions: [ 'js', 'ts', 'tsx', 'json' ],
};

145
manifest.ts Normal file
View File

@@ -0,0 +1,145 @@
export const manifest = {
short_name: 'Shlink',
name: 'Shlink',
start_url: '/',
display: 'standalone',
theme_color: '#4696e5',
background_color: '#4696e5',
icons: [
{
src: './icons/icon-16x16.png',
type: 'image/png',
sizes: '16x16',
},
{
src: './icons/icon-24x24.png',
type: 'image/png',
sizes: '24x24',
},
{
src: './icons/icon-32x32.png',
type: 'image/png',
sizes: '32x32',
},
{
src: './icons/icon-40x40.png',
type: 'image/png',
sizes: '40x40',
},
{
src: './icons/icon-48x48.png',
type: 'image/png',
sizes: '48x48',
},
{
src: './icons/icon-60x60.png',
type: 'image/png',
sizes: '60x60',
},
{
src: './icons/icon-64x64.png',
type: 'image/png',
sizes: '64x64',
},
{
src: './icons/icon-72x72.png',
type: 'image/png',
sizes: '72x72',
},
{
src: './icons/icon-76x76.png',
type: 'image/png',
sizes: '76x76',
},
{
src: './icons/icon-96x96.png',
type: 'image/png',
sizes: '96x96',
},
{
src: './icons/icon-114x114.png',
type: 'image/png',
sizes: '114x114',
},
{
src: './icons/icon-120x120.png',
type: 'image/png',
sizes: '120x120',
},
{
src: './icons/icon-128x128.png',
type: 'image/png',
sizes: '128x128',
},
{
src: './icons/icon-144x144.png',
type: 'image/png',
sizes: '144x144',
},
{
src: './icons/icon-150x150.png',
type: 'image/png',
sizes: '150x150',
},
{
src: './icons/icon-152x152.png',
type: 'image/png',
sizes: '152x152',
},
{
src: './icons/icon-160x160.png',
type: 'image/png',
sizes: '160x160',
},
{
src: './icons/icon-167x167.png',
type: 'image/png',
sizes: '167x167',
},
{
src: './icons/icon-180x180.png',
type: 'image/png',
sizes: '180x180',
},
{
src: './icons/icon-192x192.png',
type: 'image/png',
sizes: '192x192',
},
{
src: './icons/icon-196x196.png',
type: 'image/png',
sizes: '196x196',
},
{
src: './icons/icon-228x228.png',
type: 'image/png',
sizes: '228x228',
},
{
src: './icons/icon-256x256.png',
type: 'image/png',
sizes: '256x256',
},
{
src: './icons/icon-310x310.png',
type: 'image/png',
sizes: '310x310',
},
{
src: './icons/icon-384x384.png',
type: 'image/png',
sizes: '384x384',
},
{
src: './icons/icon-512x512.png',
type: 'image/png',
sizes: '512x512',
},
{
src: './icons/icon-1024x1024.png',
type: 'image/png',
sizes: '1024x1024',
},
],
};

54383
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -12,137 +12,91 @@
"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",
"test:coverage": "npm run test -- --coverage --coverageReporters=text --coverageReporters=text-summary",
"test:ci": "npm run test:coverage -- --coverageReporters=clover",
"test:pretty": "npm run test:coverage -- --coverageReporters=html",
"mutate": "./node_modules/.bin/stryker run --concurrency 4"
"types": "tsc",
"start": "vite serve --host=0.0.0.0",
"preview": "vite preview --host=0.0.0.0",
"build": "npm run types && vite build && node scripts/replace-version.mjs",
"build:dist": "npm run build && node scripts/create-dist-file.mjs",
"test": "vitest run --run",
"test:watch": "vitest --watch",
"test:ci": "npm run test -- --coverage",
"test:verbose": "npm run test -- --verbose"
},
"dependencies": {
"@fortawesome/fontawesome-free": "^6.0.0",
"@fortawesome/fontawesome-svg-core": "^1.3.0",
"@fortawesome/free-regular-svg-icons": "^6.0.0",
"@fortawesome/free-solid-svg-icons": "^6.0.0",
"@fortawesome/react-fontawesome": "^0.1.17",
"axios": "^0.26.0",
"bootstrap": "^5.1.3",
"bottlejs": "^2.0.0",
"@fortawesome/fontawesome-free": "^6.3.0",
"@fortawesome/fontawesome-svg-core": "^6.3.0",
"@fortawesome/free-brands-svg-icons": "^6.3.0",
"@fortawesome/free-regular-svg-icons": "^6.3.0",
"@fortawesome/free-solid-svg-icons": "^6.3.0",
"@fortawesome/react-fontawesome": "^0.2.0",
"@json2csv/plainjs": "^6.1.2",
"@reduxjs/toolkit": "^1.9.1",
"bootstrap": "^5.2.3",
"bottlejs": "^2.0.1",
"bowser": "^2.11.0",
"chart.js": "^3.7.1",
"classnames": "^2.3.1",
"compare-versions": "^4.1.3",
"csvjson": "^5.1.0",
"date-fns": "^2.28.0",
"event-source-polyfill": "^1.0.25",
"leaflet": "^1.7.1",
"qs": "^6.9.6",
"chart.js": "^4.1.1",
"classnames": "^2.3.2",
"compare-versions": "^5.0.3",
"csvtojson": "^2.0.10",
"date-fns": "^2.29.3",
"event-source-polyfill": "^1.0.31",
"history": "^5.3.0",
"leaflet": "^1.9.3",
"qs": "^6.11.0",
"ramda": "^0.27.2",
"react": "^17.0.2",
"react-chartjs-2": "^3.3.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": "^18.2.0",
"react-chartjs-2": "^5.1.0",
"react-colorful": "^5.6.1",
"react-copy-to-clipboard": "^5.1.0",
"react-datepicker": "^4.8.0",
"react-dom": "^18.2.0",
"react-external-link": "^2.2.0",
"react-leaflet": "^4.2.0",
"react-redux": "^8.0.5",
"react-router-dom": "^6.6.1",
"react-swipeable": "^7.0.0",
"react-tag-autocomplete": "^6.3.0",
"reactstrap": "^9.0.1",
"redux": "^4.1.2",
"redux-localstorage-simple": "^2.4.1",
"redux-thunk": "^2.4.1",
"reactstrap": "^9.1.5",
"redux-localstorage-simple": "^2.5.1",
"uuid": "^8.3.2",
"workbox-core": "^6.5.1",
"workbox-expiration": "^6.5.1",
"workbox-precaching": "^6.5.1",
"workbox-routing": "^6.5.1",
"workbox-strategies": "^6.5.1"
"workbox-core": "^6.5.4",
"workbox-expiration": "^6.5.4",
"workbox-precaching": "^6.5.4",
"workbox-routing": "^6.5.4",
"workbox-strategies": "^6.5.4"
},
"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",
"@types/jest": "^27.4.1",
"@types/leaflet": "^1.7.9",
"@shlinkio/eslint-config-js-coding-standard": "~2.1.0",
"@shlinkio/stylelint-config-css-coding-standard": "~1.0.1",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^14.0.0",
"@testing-library/user-event": "^14.4.3",
"@total-typescript/shoehorn": "^0.1.0",
"@types/json2csv": "^5.0.3",
"@types/leaflet": "^1.9.0",
"@types/qs": "^6.9.7",
"@types/ramda": "0.27.38",
"@types/react": "^17.0.39",
"@types/ramda": "^0.28.15",
"@types/react": "^18.0.26",
"@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-tag-autocomplete": "^6.1.1",
"@types/react-copy-to-clipboard": "^5.0.4",
"@types/react-datepicker": "^4.8.0",
"@types/react-dom": "^18.0.10",
"@types/react-tag-autocomplete": "^6.3.0",
"@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",
"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",
"sass": "^1.49.9",
"sass-loader": "^10.1.0",
"serve": "^12.0.0",
"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",
"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"
"@vitejs/plugin-react": "^4.0.0",
"@vitest/coverage-v8": "^0.32.0",
"adm-zip": "^0.5.10",
"chalk": "^5.2.0",
"eslint": "^8.30.0",
"jsdom": "^22.0.0",
"resize-observer-polyfill": "^1.5.1",
"sass": "^1.57.1",
"stylelint": "^15.10.1",
"typescript": "^5.0.2",
"vite": "^4.3.9",
"vite-plugin-pwa": "^0.14.4",
"vitest": "^0.32.0",
"vitest-canvas-mock": "^0.2.2"
},
"browserslist": [
">0.2%",

View File

@@ -1,109 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<meta name="theme-color" content="#4696e5">
<!--
manifest.json provides metadata used when your web app is added to the
homescreen on Android. See https://developers.google.com/web/fundamentals/engage-and-retain/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" crossorigin="use-credentials">
<!-- FavIcon itself -->
<link rel="icon" type="image/x-icon" href="%PUBLIC_URL%/favicon.ico">
<link rel="icon" type="image/svg+xml" href="%PUBLIC_URL%/favicon.svg" sizes="any">
<link rel="icon" type="image/png" href="%PUBLIC_URL%/favicon.png">
<link rel="icon" type="image/gif" href="%PUBLIC_URL%/favicon.gif">
<!-- Apple Touch -->
<link rel="apple-touch-icon" sizes="16x16" href="%PUBLIC_URL%/icons/icon-16x16.png">
<link rel="apple-touch-icon" sizes="24x24" href="%PUBLIC_URL%/icons/icon-24x24.png">
<link rel="apple-touch-icon" sizes="32x32" href="%PUBLIC_URL%/icons/icon-32x32.png">
<link rel="apple-touch-icon" sizes="40x40" href="%PUBLIC_URL%/icons/icon-40x40.png">
<link rel="apple-touch-icon" sizes="48x48" href="%PUBLIC_URL%/icons/icon-48x48.png">
<link rel="apple-touch-icon" sizes="60x60" href="%PUBLIC_URL%/icons/icon-60x60.png">
<link rel="apple-touch-icon" sizes="64x64" href="%PUBLIC_URL%/icons/icon-64x64.png">
<link rel="apple-touch-icon" sizes="72x72" href="%PUBLIC_URL%/icons/icon-72x72.png">
<link rel="apple-touch-icon" sizes="76x76" href="%PUBLIC_URL%/icons/icon-76x76.png">
<link rel="apple-touch-icon" sizes="96x96" href="%PUBLIC_URL%/icons/icon-96x96.png">
<link rel="apple-touch-icon" sizes="114x114" href="%PUBLIC_URL%/icons/icon-114x114.png">
<link rel="apple-touch-icon" sizes="120x120" href="%PUBLIC_URL%/icons/icon-120x120.png">
<link rel="apple-touch-icon" sizes="128x128" href="%PUBLIC_URL%/icons/icon-128x128.png">
<link rel="apple-touch-icon" sizes="144x144" href="%PUBLIC_URL%/icons/icon-144x144.png">
<link rel="apple-touch-icon" sizes="150x150" href="%PUBLIC_URL%/icons/icon-150x150.png">
<link rel="apple-touch-icon" sizes="152x152" href="%PUBLIC_URL%/icons/icon-152x152.png">
<link rel="apple-touch-icon" sizes="160x160" href="%PUBLIC_URL%/icons/icon-160x160.png">
<link rel="apple-touch-icon" sizes="167x167" href="%PUBLIC_URL%/icons/icon-167x167.png">
<link rel="apple-touch-icon" sizes="180x180" href="%PUBLIC_URL%/icons/icon-180x180.png">
<link rel="apple-touch-icon" sizes="192x192" href="%PUBLIC_URL%/icons/icon-192x192.png">
<link rel="apple-touch-icon" sizes="196x196" href="%PUBLIC_URL%/icons/icon-196x196.png">
<link rel="apple-touch-icon" sizes="228x228" href="%PUBLIC_URL%/icons/icon-228x228.png">
<link rel="apple-touch-icon" sizes="256x256" href="%PUBLIC_URL%/icons/icon-256x256.png">
<link rel="apple-touch-icon" sizes="310x310" href="%PUBLIC_URL%/icons/icon-310x310.png">
<link rel="apple-touch-icon" sizes="384x384" href="%PUBLIC_URL%/icons/icon-384x384.png">
<link rel="apple-touch-icon" sizes="512x512" href="%PUBLIC_URL%/icons/icon-512x512.png">
<link rel="apple-touch-icon" sizes="1024x1024" href="%PUBLIC_URL%/icons/icon-1024x1024.png">
<!-- Normal -->
<link rel="icon" type="image/png" sizes="1024x1024" href="%PUBLIC_URL%/icons/icon-1024x1024.png">
<link rel="icon" type="image/png" sizes="512x512" href="%PUBLIC_URL%/icons/icon-512x512.png">
<link rel="icon" type="image/png" sizes="384x384" href="%PUBLIC_URL%/icons/icon-384x384.png">
<link rel="icon" type="image/png" sizes="310x310" href="%PUBLIC_URL%/icons/icon-310x310.png">
<link rel="icon" type="image/png" sizes="256x256" href="%PUBLIC_URL%/icons/icon-256x256.png">
<link rel="icon" type="image/png" sizes="228x228" href="%PUBLIC_URL%/icons/icon-228x228.png">
<link rel="icon" type="image/png" sizes="196x196" href="%PUBLIC_URL%/icons/icon-196x196.png">
<link rel="icon" type="image/png" sizes="192x192" href="%PUBLIC_URL%/icons/icon-192x192.png">
<link rel="icon" type="image/png" sizes="180x180" href="%PUBLIC_URL%/icons/icon-180x180.png">
<link rel="icon" type="image/png" sizes="167x167" href="%PUBLIC_URL%/icons/icon-167x167.png">
<link rel="icon" type="image/png" sizes="160x160" href="%PUBLIC_URL%/icons/icon-160x160.png">
<link rel="icon" type="image/png" sizes="152x152" href="%PUBLIC_URL%/icons/icon-152x152.png">
<link rel="icon" type="image/png" sizes="150x150" href="%PUBLIC_URL%/icons/icon-150x150.png">
<link rel="icon" type="image/png" sizes="144x144" href="%PUBLIC_URL%/icons/icon-144x144.png">
<link rel="icon" type="image/png" sizes="128x128" href="%PUBLIC_URL%/icons/icon-128x128.png">
<link rel="icon" type="image/png" sizes="120x120" href="%PUBLIC_URL%/icons/icon-120x120.png">
<link rel="icon" type="image/png" sizes="114x114" href="%PUBLIC_URL%/icons/icon-114x114.png">
<link rel="icon" type="image/png" sizes="96x96" href="%PUBLIC_URL%/icons/icon-96x96.png">
<link rel="icon" type="image/png" sizes="76x76" href="%PUBLIC_URL%/icons/icon-76x76.png">
<link rel="icon" type="image/png" sizes="72x72" href="%PUBLIC_URL%/icons/icon-72x72.png">
<link rel="icon" type="image/png" sizes="64x64" href="%PUBLIC_URL%/icons/icon-64x64.png">
<link rel="icon" type="image/png" sizes="60x60" href="%PUBLIC_URL%/icons/icon-60x60.png">
<link rel="icon" type="image/png" sizes="48x48" href="%PUBLIC_URL%/icons/icon-48x48.png">
<link rel="icon" type="image/png" sizes="40x40" href="%PUBLIC_URL%/icons/icon-40x40.png">
<link rel="icon" type="image/png" sizes="32x32" href="%PUBLIC_URL%/icons/icon-32x32.png">
<link rel="icon" type="image/png" sizes="24x24" href="%PUBLIC_URL%/icons/icon-24x24.png">
<link rel="icon" type="image/png" sizes="16x16" href="%PUBLIC_URL%/icons/icon-16x16.png">
<!-- MS -->
<meta name="msapplication-TileImage" content="%PUBLIC_URL%/icons/icon-144x144.png">
<meta name="msapplication-square70x70logo" content="%PUBLIC_URL%/icons/icon-70x70.png">
<meta name="msapplication-square144x144logo" content="%PUBLIC_URL%/icons/icon-144x144.png">
<meta name="msapplication-square150x150logo" content="%PUBLIC_URL%/icons/icon-150x150.png">
<meta name="msapplication-square310x310logo" content="%PUBLIC_URL%/icons/icon-310x310.png">
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>Shlink — The URL shortener</title>
</head>
<body>
<noscript>
You need to enable JavaScript to run this app.
</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>

View File

@@ -1,145 +0,0 @@
{
"short_name": "Shlink",
"name": "Shlink",
"start_url": "/",
"display": "standalone",
"theme_color": "#4696e5",
"background_color": "#4696e5",
"icons": [
{
"src": "./icons/icon-16x16.png",
"type": "image/png",
"sizes": "16x16"
},
{
"src": "./icons/icon-24x24.png",
"type": "image/png",
"sizes": "24x24"
},
{
"src": "./icons/icon-32x32.png",
"type": "image/png",
"sizes": "32x32"
},
{
"src": "./icons/icon-40x40.png",
"type": "image/png",
"sizes": "40x40"
},
{
"src": "./icons/icon-48x48.png",
"type": "image/png",
"sizes": "48x48"
},
{
"src": "./icons/icon-60x60.png",
"type": "image/png",
"sizes": "60x60"
},
{
"src": "./icons/icon-64x64.png",
"type": "image/png",
"sizes": "64x64"
},
{
"src": "./icons/icon-72x72.png",
"type": "image/png",
"sizes": "72x72"
},
{
"src": "./icons/icon-76x76.png",
"type": "image/png",
"sizes": "76x76"
},
{
"src": "./icons/icon-96x96.png",
"type": "image/png",
"sizes": "96x96"
},
{
"src": "./icons/icon-114x114.png",
"type": "image/png",
"sizes": "114x114"
},
{
"src": "./icons/icon-120x120.png",
"type": "image/png",
"sizes": "120x120"
},
{
"src": "./icons/icon-128x128.png",
"type": "image/png",
"sizes": "128x128"
},
{
"src": "./icons/icon-144x144.png",
"type": "image/png",
"sizes": "144x144"
},
{
"src": "./icons/icon-150x150.png",
"type": "image/png",
"sizes": "150x150"
},
{
"src": "./icons/icon-152x152.png",
"type": "image/png",
"sizes": "152x152"
},
{
"src": "./icons/icon-160x160.png",
"type": "image/png",
"sizes": "160x160"
},
{
"src": "./icons/icon-167x167.png",
"type": "image/png",
"sizes": "167x167"
},
{
"src": "./icons/icon-180x180.png",
"type": "image/png",
"sizes": "180x180"
},
{
"src": "./icons/icon-192x192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "./icons/icon-196x196.png",
"type": "image/png",
"sizes": "196x196"
},
{
"src": "./icons/icon-228x228.png",
"type": "image/png",
"sizes": "228x228"
},
{
"src": "./icons/icon-256x256.png",
"type": "image/png",
"sizes": "256x256"
},
{
"src": "./icons/icon-310x310.png",
"type": "image/png",
"sizes": "310x310"
},
{
"src": "./icons/icon-384x384.png",
"type": "image/png",
"sizes": "384x384"
},
{
"src": "./icons/icon-512x512.png",
"type": "image/png",
"sizes": "512x512"
},
{
"src": "./icons/icon-1024x1024.png",
"type": "image/png",
"sizes": "1024x1024"
}
]
}

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,11 +1,11 @@
const fs = require('fs-extra');
import fs from 'fs';
function replaceVersionPlaceholder(version) {
const staticJsFilesPath = './build/static/js';
const staticJsFilesPath = './build/assets';
const versionPlaceholder = '%_VERSION_%';
const isMainFile = (file) => file.startsWith('main.') && file.endsWith('.js');
const [ mainJsFile ] = fs.readdirSync(staticJsFilesPath).filter(isMainFile);
const isMainFile = (file) => file.startsWith('index-') && file.endsWith('.js');
const [mainJsFile] = fs.readdirSync(staticJsFilesPath).filter(isMainFile);
const filePath = `${staticJsFilesPath}/${mainJsFile}`;
const fileContent = fs.readFileSync(filePath, 'utf-8');
const replaced = fileContent.replace(versionPlaceholder, version);

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

@@ -1,3 +1,4 @@
// eslint-disable-next-line max-classes-per-file
declare module 'event-source-polyfill' {
declare class EventSourcePolyfill {
public onmessage?: ({ data }: { data: string }) => void;
@@ -7,10 +8,9 @@ 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 '@json2csv/plainjs' {
export class Parser {
parse: <T>(data: T[]) => string;
}
}

View File

@@ -1,4 +1,4 @@
import { ProblemDetailsError } from './types';
import type { ProblemDetailsError } from './types/errors';
import { isInvalidArgumentError } from './utils';
export interface ShlinkApiErrorProps {
@@ -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

@@ -1,137 +1,149 @@
import { isEmpty, isNil, reject } from 'ramda';
import { AxiosInstance, AxiosResponse, Method } from 'axios';
import { ShortUrl, ShortUrlData } from '../../short-urls/data';
import { OptionalString } from '../../utils/utils';
import {
import type { HttpClient } from '../../common/services/HttpClient';
import type { ShortUrl, ShortUrlData } from '../../short-urls/data';
import { orderToString } from '../../utils/helpers/ordering';
import { stringifyQuery } from '../../utils/helpers/query';
import type { OptionalString } from '../../utils/utils';
import type {
ShlinkDomainRedirects,
ShlinkDomainsResponse,
ShlinkEditDomainRedirects,
ShlinkHealth,
ShlinkMercureInfo,
ShlinkShortUrlData,
ShlinkShortUrlsListNormalizedParams,
ShlinkShortUrlsListParams,
ShlinkShortUrlsResponse,
ShlinkTags,
ShlinkTagsResponse,
ShlinkTagsStatsResponse,
ShlinkVisits,
ShlinkVisitsParams,
ShlinkShortUrlData,
ShlinkDomainsResponse,
ShlinkVisitsOverview,
ShlinkEditDomainRedirects,
ShlinkDomainRedirects,
ShlinkShortUrlsListParams,
ShlinkShortUrlsListNormalizedParams,
ShlinkVisitsParams,
} from '../types';
import { stringifyQuery } from '../../utils/helpers/query';
import { orderToString } from '../../utils/helpers/ordering';
import { isRegularNotFound, parseApiError } from '../utils';
const buildShlinkBaseUrl = (url: string) => url ? `${url}/rest/v2` : '';
const buildShlinkBaseUrl = (url: string, version: 2 | 3) => `${url}/rest/v${version}`;
const rejectNilProps = reject(isNil);
const normalizeOrderByInParams = (params: ShlinkShortUrlsListParams): ShlinkShortUrlsListNormalizedParams => {
const { orderBy = {}, ...rest } = params;
const normalizeListParams = (
{ orderBy = {}, excludeMaxVisitsReached, excludePastValidUntil, ...rest }: ShlinkShortUrlsListParams,
): ShlinkShortUrlsListNormalizedParams => ({
...rest,
excludeMaxVisitsReached: excludeMaxVisitsReached === true ? 'true' : undefined,
excludePastValidUntil: excludePastValidUntil === true ? 'true' : undefined,
orderBy: orderToString(orderBy),
});
return { ...rest, orderBy: orderToString(orderBy) };
};
export class ShlinkApiClient {
private apiVersion: 2 | 3;
export default class ShlinkApiClient {
public constructor(
private readonly axios: AxiosInstance,
private readonly httpClient: HttpClient,
private readonly baseUrl: string,
private readonly apiKey: string,
) {
this.apiVersion = 3;
}
public readonly listShortUrls = async (params: ShlinkShortUrlsListParams = {}): Promise<ShlinkShortUrlsResponse> =>
this.performRequest<{ shortUrls: ShlinkShortUrlsResponse }>('/short-urls', 'GET', normalizeOrderByInParams(params))
.then(({ data }) => data.shortUrls);
this.performRequest<{ shortUrls: ShlinkShortUrlsResponse }>('/short-urls', 'GET', normalizeListParams(params))
.then(({ shortUrls }) => shortUrls);
public readonly createShortUrl = async (options: ShortUrlData): Promise<ShortUrl> => {
const filteredOptions = reject((value) => isEmpty(value) || isNil(value), options as any);
return this.performRequest<ShortUrl>('/short-urls', 'POST', {}, filteredOptions)
.then((resp) => resp.data);
return this.performRequest<ShortUrl>('/short-urls', 'POST', {}, filteredOptions);
};
public readonly getShortUrlVisits = async (shortCode: string, query?: ShlinkVisitsParams): Promise<ShlinkVisits> =>
this.performRequest<{ visits: ShlinkVisits }>(`/short-urls/${shortCode}/visits`, 'GET', query)
.then(({ data }) => data.visits);
.then(({ visits }) => visits);
public readonly getTagVisits = async (tag: string, query?: Omit<ShlinkVisitsParams, 'domain'>): Promise<ShlinkVisits> =>
this.performRequest<{ visits: ShlinkVisits }>(`/tags/${tag}/visits`, 'GET', query)
.then(({ data }) => data.visits);
this.performRequest<{ visits: ShlinkVisits }>(`/tags/${tag}/visits`, 'GET', query).then(({ visits }) => visits);
public readonly getDomainVisits = async (domain: string, query?: Omit<ShlinkVisitsParams, 'domain'>): Promise<ShlinkVisits> =>
this.performRequest<{ visits: ShlinkVisits }>(`/domains/${domain}/visits`, 'GET', query).then(({ visits }) => visits);
public readonly getOrphanVisits = async (query?: Omit<ShlinkVisitsParams, 'domain'>): Promise<ShlinkVisits> =>
this.performRequest<{ visits: ShlinkVisits }>('/visits/orphan', 'GET', query)
.then(({ data }) => data.visits);
this.performRequest<{ visits: ShlinkVisits }>('/visits/orphan', 'GET', query).then(({ visits }) => visits);
public readonly getNonOrphanVisits = async (query?: Omit<ShlinkVisitsParams, 'domain'>): Promise<ShlinkVisits> =>
this.performRequest<{ visits: ShlinkVisits }>('/visits/non-orphan', 'GET', query)
.then(({ data }) => data.visits);
this.performRequest<{ visits: ShlinkVisits }>('/visits/non-orphan', 'GET', query).then(({ visits }) => visits);
public readonly getVisitsOverview = async (): Promise<ShlinkVisitsOverview> =>
this.performRequest<{ visits: ShlinkVisitsOverview }>('/visits', 'GET')
.then(({ data }) => data.visits);
this.performRequest<{ visits: ShlinkVisitsOverview }>('/visits').then(({ visits }) => visits);
public readonly getShortUrl = async (shortCode: string, domain?: OptionalString): Promise<ShortUrl> =>
this.performRequest<ShortUrl>(`/short-urls/${shortCode}`, 'GET', { domain })
.then(({ data }) => data);
this.performRequest<ShortUrl>(`/short-urls/${shortCode}`, 'GET', { domain });
public readonly deleteShortUrl = async (shortCode: string, domain?: OptionalString): Promise<void> =>
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);
this.performEmptyRequest(`/short-urls/${shortCode}`, 'DELETE', { domain });
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);
public readonly listTags = async (): Promise<ShlinkTags> =>
this.performRequest<{ tags: ShlinkTagsResponse }>('/tags', 'GET', { withStats: 'true' })
.then((resp) => resp.data.tags)
.then(({ tags }) => tags)
.then(({ data, stats }) => ({ tags: data, stats }));
public readonly tagsStats = async (): Promise<ShlinkTags> =>
this.performRequest<{ tags: ShlinkTagsStatsResponse }>('/tags/stats', 'GET')
.then(({ tags }) => tags)
.then(({ data }) => ({ tags: data.map(({ tag }) => tag), stats: data }));
public readonly deleteTags = async (tags: string[]): Promise<{ tags: string[] }> =>
this.performRequest('/tags', 'DELETE', { tags })
.then(() => ({ tags }));
this.performEmptyRequest('/tags', 'DELETE', { tags }).then(() => ({ tags }));
public readonly editTag = async (oldName: string, newName: string): Promise<{ oldName: string; newName: string }> =>
this.performRequest('/tags', 'PUT', {}, { oldName, newName })
.then(() => ({ oldName, newName }));
this.performEmptyRequest('/tags', 'PUT', {}, { oldName, newName }).then(() => ({ oldName, newName }));
public readonly health = async (): Promise<ShlinkHealth> =>
this.performRequest<ShlinkHealth>('/health', 'GET')
.then((resp) => resp.data);
public readonly health = async (): Promise<ShlinkHealth> => this.performRequest<ShlinkHealth>('/health', 'GET');
public readonly mercureInfo = async (): Promise<ShlinkMercureInfo> =>
this.performRequest<ShlinkMercureInfo>('/mercure-info', 'GET')
.then((resp) => resp.data);
this.performRequest<ShlinkMercureInfo>('/mercure-info', 'GET');
public readonly listDomains = async (): Promise<ShlinkDomainsResponse> =>
this.performRequest<{ domains: ShlinkDomainsResponse }>('/domains', 'GET').then(({ data }) => data.domains);
this.performRequest<{ domains: ShlinkDomainsResponse }>('/domains').then(({ domains }) => domains);
public readonly editDomainRedirects = async (
domainRedirects: ShlinkEditDomainRedirects,
): Promise<ShlinkDomainRedirects> =>
this.performRequest<ShlinkDomainRedirects>('/domains/redirects', 'PATCH', {}, domainRedirects).then(({ data }) => data);
this.performRequest<ShlinkDomainRedirects>('/domains/redirects', 'PATCH', {}, domainRedirects);
private readonly performRequest = async <T>(url: string, method: Method = 'GET', query = {}, body = {}): Promise<AxiosResponse<T>> =>
this.axios({
private readonly performRequest = async <T>(url: string, method = 'GET', query = {}, body?: object): Promise<T> =>
this.httpClient.fetchJson<T>(...this.toFetchParams(url, method, query, body)).catch(
this.handleFetchError(() => this.httpClient.fetchJson<T>(...this.toFetchParams(url, method, query, body))),
);
private readonly performEmptyRequest = async (url: string, method = 'GET', query = {}, body?: object): Promise<void> =>
this.httpClient.fetchEmpty(...this.toFetchParams(url, method, query, body)).catch(
this.handleFetchError(() => this.httpClient.fetchEmpty(...this.toFetchParams(url, method, query, body))),
);
private readonly toFetchParams = (url: string, method: string, query = {}, body?: object): [string, RequestInit] => {
const normalizedQuery = stringifyQuery(rejectNilProps(query));
const stringifiedQuery = isEmpty(normalizedQuery) ? '' : `?${normalizedQuery}`;
return [`${buildShlinkBaseUrl(this.baseUrl, this.apiVersion)}${url}${stringifiedQuery}`, {
method,
url: `${buildShlinkBaseUrl(this.baseUrl)}${url}`,
body: body && JSON.stringify(body),
headers: { 'X-Api-Key': this.apiKey },
params: rejectNilProps(query),
data: body,
paramsSerializer: stringifyQuery,
});
}];
};
private readonly handleFetchError = (retryFetch: Function) => (e: unknown) => {
if (!isRegularNotFound(parseApiError(e))) {
throw e;
}
// If we capture a not found error, let's assume this Shlink version does not support API v3, so we decrease to
// v2 and retry
this.apiVersion = 2;
return retryFetch();
};
}

View File

@@ -1,36 +1,33 @@
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 type { HttpClient } from '../../common/services/HttpClient';
import type { GetState } from '../../container/types';
import type { ServerWithId } from '../../servers/data';
import { hasServerData } from '../../servers/data';
import { ShlinkApiClient } from './ShlinkApiClient';
const apiClients: Record<string, ShlinkApiClient> = {};
const isGetState = (getStateOrSelectedServer: GetState | ServerWithId): getStateOrSelectedServer is GetState =>
typeof getStateOrSelectedServer === 'function';
const getSelectedServerFromState = (getState: GetState): SelectedServer => prop('selectedServer', getState());
export type ShlinkApiClientBuilder = (getStateOrSelectedServer: GetState | ServerWithId) => ShlinkApiClient;
const buildShlinkApiClient = (axios: AxiosInstance): ShlinkApiClientBuilder => (
getStateOrSelectedServer: GetState | ServerWithId,
) => {
const server = isGetState(getStateOrSelectedServer)
? getSelectedServerFromState(getStateOrSelectedServer)
: getStateOrSelectedServer;
if (!hasServerData(server)) {
const getSelectedServerFromState = (getState: GetState): ServerWithId => {
const { selectedServer } = getState();
if (!hasServerData(selectedServer)) {
throw new Error('There\'s no selected server or it is not found');
}
const { url, apiKey } = server;
return selectedServer;
};
export const buildShlinkApiClient = (httpClient: HttpClient) => (getStateOrSelectedServer: GetState | ServerWithId) => {
const { url, apiKey } = isGetState(getStateOrSelectedServer)
? getSelectedServerFromState(getStateOrSelectedServer)
: getStateOrSelectedServer;
const clientKey = `${url}_${apiKey}`;
if (!apiClients[clientKey]) {
apiClients[clientKey] = new ShlinkApiClient(axios, url, apiKey);
apiClients[clientKey] = new ShlinkApiClient(httpClient, url, apiKey);
}
return apiClients[clientKey];
};
export default buildShlinkApiClient;
export type ShlinkApiClientBuilder = ReturnType<typeof buildShlinkApiClient>;

View File

@@ -1,8 +1,6 @@
import Bottle from 'bottlejs';
import buildShlinkApiClient from './ShlinkApiClientBuilder';
import type Bottle from 'bottlejs';
import { buildShlinkApiClient } from './ShlinkApiClientBuilder';
const provideServices = (bottle: Bottle) => {
bottle.serviceFactory('buildShlinkApiClient', buildShlinkApiClient, 'axios');
export const provideServices = (bottle: Bottle) => {
bottle.serviceFactory('buildShlinkApiClient', buildShlinkApiClient, 'HttpClient');
};
export default provideServices;

View File

@@ -1,6 +0,0 @@
import { Action } from 'redux';
import { ProblemDetailsError } from './index';
export interface ApiErrorAction extends Action<string> {
errorData?: ProblemDetailsError;
}

54
src/api/types/errors.ts Normal file
View File

@@ -0,0 +1,54 @@
export enum ErrorTypeV2 {
INVALID_ARGUMENT = 'INVALID_ARGUMENT',
INVALID_SHORT_URL_DELETION = 'INVALID_SHORT_URL_DELETION',
DOMAIN_NOT_FOUND = 'DOMAIN_NOT_FOUND',
FORBIDDEN_OPERATION = 'FORBIDDEN_OPERATION',
INVALID_URL = 'INVALID_URL',
INVALID_SLUG = 'INVALID_SLUG',
INVALID_SHORTCODE = 'INVALID_SHORTCODE',
TAG_CONFLICT = 'TAG_CONFLICT',
TAG_NOT_FOUND = 'TAG_NOT_FOUND',
MERCURE_NOT_CONFIGURED = 'MERCURE_NOT_CONFIGURED',
INVALID_AUTHORIZATION = 'INVALID_AUTHORIZATION',
INVALID_API_KEY = 'INVALID_API_KEY',
NOT_FOUND = 'NOT_FOUND',
}
export enum ErrorTypeV3 {
INVALID_ARGUMENT = 'https://shlink.io/api/error/invalid-data',
INVALID_SHORT_URL_DELETION = 'https://shlink.io/api/error/invalid-short-url-deletion',
DOMAIN_NOT_FOUND = 'https://shlink.io/api/error/domain-not-found',
FORBIDDEN_OPERATION = 'https://shlink.io/api/error/forbidden-tag-operation',
INVALID_URL = 'https://shlink.io/api/error/invalid-url',
INVALID_SLUG = 'https://shlink.io/api/error/non-unique-slug',
INVALID_SHORTCODE = 'https://shlink.io/api/error/short-url-not-found',
TAG_CONFLICT = 'https://shlink.io/api/error/tag-conflict',
TAG_NOT_FOUND = 'https://shlink.io/api/error/tag-not-found',
MERCURE_NOT_CONFIGURED = 'https://shlink.io/api/error/mercure-not-configured',
INVALID_AUTHORIZATION = 'https://shlink.io/api/error/missing-authentication',
INVALID_API_KEY = 'https://shlink.io/api/error/invalid-api-key',
NOT_FOUND = 'https://shlink.io/api/error/not-found',
}
export interface ProblemDetailsError {
type: string;
detail: string;
title: string;
status: number;
[extraProps: string]: any;
}
export interface InvalidArgumentError extends ProblemDetailsError {
type: ErrorTypeV2.INVALID_ARGUMENT | ErrorTypeV3.INVALID_ARGUMENT;
invalidElements: string[];
}
export interface InvalidShortUrlDeletion extends ProblemDetailsError {
type: 'INVALID_SHORTCODE_DELETION' | ErrorTypeV2.INVALID_SHORT_URL_DELETION | ErrorTypeV3.INVALID_SHORT_URL_DELETION;
threshold: number;
}
export interface RegularNotFound extends ProblemDetailsError {
type: ErrorTypeV2.NOT_FOUND | ErrorTypeV3.NOT_FOUND;
status: 404;
}

View File

@@ -1,6 +1,7 @@
import { Visit } from '../../visits/types';
import { OptionalString } from '../../utils/utils';
import { ShortUrl, ShortUrlMeta, ShortUrlsOrder } from '../../short-urls/data';
import type { ShortUrl, ShortUrlMeta } from '../../short-urls/data';
import type { Order } from '../../utils/helpers/ordering';
import type { OptionalString } from '../../utils/utils';
import type { Visit } from '../../visits/types';
export interface ShlinkShortUrlsResponse {
data: ShortUrl[];
@@ -17,9 +18,12 @@ export interface ShlinkHealth {
version: string;
}
interface ShlinkTagsStats {
export interface ShlinkTagsStats {
tag: string;
shortUrlsCount: number;
visitsSummary?: ShlinkVisitsSummary; // Optional only before Shlink 3.5.0
/** @deprecated */
visitsCount: number;
}
@@ -30,23 +34,39 @@ export interface ShlinkTags {
export interface ShlinkTagsResponse {
data: string[];
/** @deprecated Present only when withStats=true is provided, which is deprecated */
stats: ShlinkTagsStats[];
}
export interface ShlinkTagsStatsResponse {
data: ShlinkTagsStats[];
}
export interface ShlinkPaginator {
currentPage: number;
pagesCount: number;
totalItems: number;
}
export interface ShlinkVisitsSummary {
total: number;
nonBots: number;
bots: number;
}
export interface ShlinkVisits {
data: Visit[];
pagination: ShlinkPaginator;
}
export interface ShlinkVisitsOverview {
nonOrphanVisits?: ShlinkVisitsSummary; // Optional only before Shlink 3.5.0
orphanVisits?: ShlinkVisitsSummary; // Optional only before Shlink 3.5.0
/** @deprecated */
visitsCount: number;
orphanVisitsCount?: number; // Optional only for versions older than 2.6.0
/** @deprecated */
orphanVisitsCount: number;
}
export interface ShlinkVisitsParams {
@@ -88,35 +108,26 @@ export interface ShlinkDomainsResponse {
export type TagsFilteringMode = 'all' | 'any';
type ShlinkShortUrlsOrderableFields = 'dateCreated' | 'shortCode' | 'longUrl' | 'title' | 'visits' | 'nonBotVisits';
export type ShlinkShortUrlsOrder = Order<ShlinkShortUrlsOrderableFields>;
export interface ShlinkShortUrlsListParams {
page?: string;
itemsPerPage?: number;
tags?: string[];
searchTerm?: string;
tags?: string[];
tagsMode?: TagsFilteringMode;
orderBy?: ShlinkShortUrlsOrder;
startDate?: string;
endDate?: string;
orderBy?: ShortUrlsOrder;
tagsMode?: TagsFilteringMode;
excludeMaxVisitsReached?: boolean;
excludePastValidUntil?: boolean;
}
export interface ShlinkShortUrlsListNormalizedParams extends Omit<ShlinkShortUrlsListParams, 'orderBy'> {
export interface ShlinkShortUrlsListNormalizedParams extends
Omit<ShlinkShortUrlsListParams, 'orderBy' | 'excludeMaxVisitsReached' | 'excludePastValidUntil'> {
orderBy?: string;
}
export interface ProblemDetailsError {
type: string;
detail: string;
title: string;
status: number;
[extraProps: string]: any;
}
export interface InvalidArgumentError extends ProblemDetailsError {
type: 'INVALID_ARGUMENT';
invalidElements: string[];
}
export interface InvalidShortUrlDeletion extends ProblemDetailsError {
type: 'INVALID_SHORTCODE_DELETION' | 'INVALID_SHORT_URL_DELETION';
threshold: number;
excludeMaxVisitsReached?: 'true';
excludePastValidUntil?: 'true';
}

View File

@@ -1,10 +1,25 @@
import { AxiosError } from 'axios';
import { InvalidArgumentError, InvalidShortUrlDeletion, ProblemDetailsError } from '../types';
import type {
InvalidArgumentError,
InvalidShortUrlDeletion,
ProblemDetailsError,
RegularNotFound } from '../types/errors';
import {
ErrorTypeV2,
ErrorTypeV3,
} from '../types/errors';
export const parseApiError = (e: AxiosError<ProblemDetailsError>) => e.response?.data;
const isProblemDetails = (e: unknown): e is ProblemDetailsError =>
!!e && typeof e === 'object' && ['type', 'detail', 'title', 'status'].every((prop) => prop in e);
export const parseApiError = (e: unknown): ProblemDetailsError | undefined => (isProblemDetails(e) ? e : undefined);
export const isInvalidArgumentError = (error?: ProblemDetailsError): error is InvalidArgumentError =>
error?.type === 'INVALID_ARGUMENT';
error?.type === ErrorTypeV2.INVALID_ARGUMENT || error?.type === ErrorTypeV3.INVALID_ARGUMENT;
export const isInvalidDeletionError = (error?: ProblemDetailsError): error is InvalidShortUrlDeletion =>
error?.type === 'INVALID_SHORTCODE_DELETION' || error?.type === 'INVALID_SHORT_URL_DELETION';
error?.type === 'INVALID_SHORTCODE_DELETION'
|| error?.type === ErrorTypeV2.INVALID_SHORT_URL_DELETION
|| error?.type === ErrorTypeV3.INVALID_SHORT_URL_DELETION;
export const isRegularNotFound = (error?: ProblemDetailsError): error is RegularNotFound =>
(error?.type === ErrorTypeV2.NOT_FOUND || error?.type === ErrorTypeV3.NOT_FOUND) && error?.status === 404;

View File

@@ -1,12 +1,13 @@
import { useEffect, FC } from 'react';
import { Route, Routes, useLocation } from 'react-router-dom';
import classNames from 'classnames';
import NotFound from '../common/NotFound';
import { ServersMap } from '../servers/data';
import { Settings } from '../settings/reducers/settings';
import { changeThemeInMarkup } from '../utils/theme';
import type { FC } from 'react';
import { useEffect } from 'react';
import { Route, Routes, useLocation } from 'react-router-dom';
import { AppUpdateBanner } from '../common/AppUpdateBanner';
import { NotFound } from '../common/NotFound';
import type { ServersMap } from '../servers/data';
import type { Settings } from '../settings/reducers/settings';
import { forceUpdate } from '../utils/helpers/sw';
import { changeThemeInMarkup } from '../utils/theme';
import './App.scss';
interface AppProps {
@@ -17,13 +18,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 +48,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 +66,3 @@ const App = (
</div>
);
};
export default App;

View File

@@ -1,18 +1,14 @@
import { Action } from 'redux';
import { buildActionCreator, buildReducer } from '../../utils/helpers/redux';
import { createSlice } from '@reduxjs/toolkit';
/* 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 { actions, reducer } = createSlice({
name: 'shlink/appUpdates',
initialState: false,
reducers: {
appUpdateAvailable: () => true,
resetAppUpdate: () => false,
},
});
const initialState = false;
export const { appUpdateAvailable, resetAppUpdate } = actions;
export default buildReducer<boolean, Action<string>>({
[APP_UPDATE_AVAILABLE]: () => true,
[RESET_APP_UPDATE]: () => false,
}, initialState);
export const appUpdateAvailable = buildActionCreator(APP_UPDATE_AVAILABLE);
export const resetAppUpdate = buildActionCreator(RESET_APP_UPDATE);
export const appUpdatesReducer = reducer;

View File

@@ -1,9 +1,9 @@
import Bottle from 'bottlejs';
import type Bottle from 'bottlejs';
import type { ConnectDecorator } from '../../container/types';
import { App } from '../App';
import { appUpdateAvailable, resetAppUpdate } from '../reducers/appUpdates';
import App from '../App';
import { ConnectDecorator } from '../../container/types';
const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
export const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
// Components
bottle.serviceFactory(
'App',
@@ -17,11 +17,9 @@ 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);
bottle.serviceFactory('resetAppUpdate', () => resetAppUpdate);
};
export default provideServices;

View File

@@ -1,9 +1,9 @@
import { FC, MouseEventHandler } from 'react';
import { Alert, Button } from 'reactstrap';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faSyncAlt as reloadIcon } from '@fortawesome/free-solid-svg-icons';
import { SimpleCard } from '../utils/SimpleCard';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import type { FC, MouseEventHandler } from 'react';
import { Alert, Button } from 'reactstrap';
import { useToggle } from '../utils/helpers/hooks';
import { SimpleCard } from '../utils/SimpleCard';
import './AppUpdateBanner.scss';
interface AppUpdateBannerProps {
@@ -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

@@ -1,28 +1,29 @@
import {
faList as listIcon,
faLink as createIcon,
faTags as tagsIcon,
faPen as editIcon,
faHome as overviewIcon,
faGlobe as domainsIcon,
faHome as overviewIcon,
faLink as createIcon,
faList as listIcon,
faPen as editIcon,
faTags as tagsIcon,
} from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { FC } from 'react';
import { NavLink, NavLinkProps, useLocation } from 'react-router-dom';
import classNames from 'classnames';
import { DeleteServerButtonProps } from '../servers/DeleteServerButton';
import { isServerWithId, SelectedServer } from '../servers/data';
import { supportsDomainRedirects } from '../utils/helpers/features';
import type { FC } from 'react';
import type { NavLinkProps } from 'react-router-dom';
import { NavLink, useLocation } from 'react-router-dom';
import type { SelectedServer } from '../servers/data';
import { isServerWithId } from '../servers/data';
import type { DeleteServerButtonProps } from '../servers/DeleteServerButton';
import './AsideMenu.scss';
export interface AsideMenuProps {
selectedServer: SelectedServer;
className?: string;
showOnMobile?: boolean;
}
interface AsideMenuItemProps extends NavLinkProps {
to: string;
className?: string;
}
const AsideMenuItem: FC<AsideMenuItemProps> = ({ children, to, className, ...rest }) => (
@@ -35,13 +36,12 @@ 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);
const serverId = hasId ? selectedServer.id : '';
const { pathname } = useLocation();
const addManageDomainsLink = supportsDomainRedirects(selectedServer);
const asideClass = classNames('aside-menu', {
'aside-menu--hidden': !showOnMobile,
});
@@ -69,12 +69,10 @@ const AsideMenu = (DeleteServerButton: FC<DeleteServerButtonProps>) => (
<FontAwesomeIcon fixedWidth icon={tagsIcon} />
<span className="aside-menu__item-text">Manage tags</span>
</AsideMenuItem>
{addManageDomainsLink && (
<AsideMenuItem to={buildPath('/manage-domains')}>
<FontAwesomeIcon fixedWidth icon={domainsIcon} />
<span className="aside-menu__item-text">Manage domains</span>
</AsideMenuItem>
)}
<AsideMenuItem to={buildPath('/manage-domains')}>
<FontAwesomeIcon fixedWidth icon={domainsIcon} />
<span className="aside-menu__item-text">Manage domains</span>
</AsideMenuItem>
<AsideMenuItem to={buildPath('/edit')} className="aside-menu__item--push">
<FontAwesomeIcon fixedWidth icon={editIcon} />
<span className="aside-menu__item-text">Edit this server</span>
@@ -90,5 +88,3 @@ const AsideMenu = (DeleteServerButton: FC<DeleteServerButtonProps>) => (
</aside>
);
};
export default AsideMenu;

View File

@@ -1,4 +1,5 @@
import { Component, ReactNode } from 'react';
import type { ReactNode } from 'react';
import { Component } from 'react';
import { Button } from 'reactstrap';
import { SimpleCard } from '../utils/SimpleCard';
@@ -6,10 +7,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 +27,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 +41,7 @@ const ErrorHandler = (
);
}
return this.props.children;
const { children } = this.props;
return children;
}
};
export default ErrorHandler;

View File

@@ -1,20 +1,20 @@
import { useEffect } from 'react';
import { faExternalLinkAlt, faPlus } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { isEmpty, values } from 'ramda';
import { useEffect } from 'react';
import { ExternalLink } from 'react-external-link';
import { Link, useNavigate } from 'react-router-dom';
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 { ServersMap } from '../servers/data';
import type { ServersMap } from '../servers/data';
import { ServersListGroup } from '../servers/ServersListGroup';
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

@@ -1,19 +1,20 @@
import { faChevronDown as arrowIcon, faCogs as cogsIcon } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { FC, useEffect } from 'react';
import classNames from 'classnames';
import type { FC } from 'react';
import { useEffect } from 'react';
import { Link, useLocation } from 'react-router-dom';
import { Collapse, Nav, Navbar, NavbarBrand, NavbarToggler, NavItem, NavLink } from 'reactstrap';
import classNames from 'classnames';
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 +42,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

@@ -1,14 +1,15 @@
import { FC, useEffect } from 'react';
import { Navigate, Route, Routes, useLocation } from 'react-router-dom';
import { faBars as burgerIcon } from '@fortawesome/free-solid-svg-icons';
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 type { FC } from 'react';
import { useEffect } from 'react';
import { Navigate, Route, Routes, useLocation } from 'react-router-dom';
import { isReachableServer } from '../servers/data';
import NotFound from './NotFound';
import { AsideMenuProps } from './AsideMenu';
import { withSelectedServer } from '../servers/helpers/withSelectedServer';
import { useFeature } from '../utils/helpers/features';
import { useSwipeable, useToggle } from '../utils/helpers/hooks';
import type { AsideMenuProps } from './AsideMenu';
import { NotFound } from './NotFound';
import './MenuLayout.scss';
interface MenuLayoutProps {
@@ -16,13 +17,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,13 +33,12 @@ 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();
return () => sidebarNotPresent();
}, []);
@@ -45,9 +46,8 @@ const MenuLayout = (
return <ServerError />;
}
const addOrphanVisitsRoute = supportsOrphanVisits(selectedServer);
const addNonOrphanVisitsRoute = supportsNonOrphanVisits(selectedServer);
const addManageDomainsRoute = supportsDomainRedirects(selectedServer);
const addNonOrphanVisitsRoute = useFeature('nonOrphanVisits', selectedServer);
const addDomainVisitsRoute = useFeature('domainVisits', selectedServer);
const burgerClasses = classNames('menu-layout__burger-icon', { 'menu-layout__burger-icon--active': sidebarVisible });
const swipeableProps = useSwipeable(showSidebar, hideSidebar);
@@ -68,10 +68,11 @@ 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 />} />}
<Route path="/manage-domains" element={<ManageDomains />} />
<Route
path="*"
element={<NotFound to={`/server/${selectedServer.id}/list-short-urls/1`}>List short URLs</NotFound>}
@@ -84,5 +85,3 @@ const MenuLayout = (
</>
);
}, ServerError);
export default MenuLayout;

View File

@@ -1,4 +1,6 @@
import { FC } from 'react';
import type { 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 type { 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,13 @@
import { FC, useEffect } from 'react';
import type { FC, PropsWithChildren } from 'react';
import { 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

@@ -1,7 +1,8 @@
import { pipe } from 'ramda';
import { ExternalLink } from 'react-external-link';
import type { SelectedServer } from '../servers/data';
import { isReachableServer } from '../servers/data';
import { versionToPrintable, versionToSemVer } from '../utils/helpers/version';
import { isReachableServer, SelectedServer } from '../servers/data';
const SHLINK_WEB_CLIENT_VERSION = '%_VERSION_%';
const normalizeVersion = pipe(versionToSemVer(), versionToPrintable);
@@ -17,17 +18,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,7 +1,7 @@
import classNames from 'classnames';
import { SelectedServer } from '../servers/data';
import ShlinkVersions from './ShlinkVersions';
import { Sidebar } from './reducers/sidebar';
import type { SelectedServer } from '../servers/data';
import type { Sidebar } from './reducers/sidebar';
import { ShlinkVersions } from './ShlinkVersions';
import './ShlinkVersionsContainer.scss';
export interface ShlinkVersionsContainerProps {
@@ -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

@@ -1,12 +1,13 @@
import { FC } from 'react';
import classNames from 'classnames';
import type { FC } from 'react';
import { Pagination, PaginationItem, PaginationLink } from 'reactstrap';
import type {
NumberOrEllipsis } from '../utils/helpers/pagination';
import {
pageIsEllipsis,
keyForPage,
NumberOrEllipsis,
progressivePagination,
pageIsEllipsis,
prettifyPageNumber,
progressivePagination,
} from '../utils/helpers/pagination';
import './SimplePaginator.scss';
@@ -17,7 +18,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 +38,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 +49,3 @@ const SimplePaginator: FC<SimplePaginatorProps> = ({ pagesCount, currentPage, se
</Pagination>
);
};
export default SimplePaginator;

View File

@@ -3,8 +3,8 @@
.react-tags {
position: relative;
padding: 5px 0 0 6px;
border-radius: .3rem;
background-color: var(--input-color);
border-radius: .5rem;
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,27 +1,22 @@
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 */
import { createSlice } from '@reduxjs/toolkit';
export interface Sidebar {
sidebarPresent: boolean;
}
type SidebarRenderedAction = Action<string>;
type SidebarNotRenderedAction = Action<string>;
const initialState: Sidebar = {
sidebarPresent: false,
};
export default buildReducer<Sidebar, SidebarRenderedAction & SidebarNotRenderedAction>({
[SIDEBAR_PRESENT]: () => ({ sidebarPresent: true }),
[SIDEBAR_NOT_PRESENT]: () => ({ sidebarPresent: false }),
}, initialState);
const { actions, reducer } = createSlice({
name: 'shlink/sidebar',
initialState,
reducers: {
sidebarPresent: () => ({ sidebarPresent: true }),
sidebarNotPresent: () => ({ sidebarPresent: false }),
},
});
export const sidebarPresent = buildActionCreator(SIDEBAR_PRESENT);
export const { sidebarPresent, sidebarNotPresent } = actions;
export const sidebarNotPresent = buildActionCreator(SIDEBAR_NOT_PRESENT);
export const sidebarReducer = reducer;

View File

@@ -0,0 +1,42 @@
import type { Fetch } from '../../utils/types';
const applicationJsonHeader = { 'Content-Type': 'application/json' };
const withJsonContentType = (options?: RequestInit): RequestInit | undefined => {
if (!options?.body) {
return options;
}
return options ? {
...options,
headers: {
...(options.headers ?? {}),
...applicationJsonHeader,
},
} : {
headers: applicationJsonHeader,
};
};
export class HttpClient {
constructor(private readonly fetch: Fetch) {}
public readonly fetchJson = <T>(url: string, options?: RequestInit): Promise<T> =>
this.fetch(url, withJsonContentType(options)).then(async (resp) => {
const json = await resp.json();
if (!resp.ok) {
throw json;
}
return json as T;
});
public readonly fetchEmpty = (url: string, options?: RequestInit): Promise<void> =>
this.fetch(url, withJsonContentType(options)).then(async (resp) => {
if (!resp.ok) {
throw await resp.json();
}
});
public readonly fetchBlob = (url: string): Promise<Blob> => this.fetch(url).then((resp) => resp.blob());
}

View File

@@ -1,11 +1,11 @@
import { AxiosInstance } from 'axios';
import { saveUrl } from '../../utils/helpers/files';
import type { HttpClient } from './HttpClient';
export class ImageDownloader {
public constructor(private readonly axios: AxiosInstance, private readonly window: Window) {}
public constructor(private readonly httpClient: HttpClient, private readonly window: Window) {}
public async saveImage(imgUrl: string, filename: string): Promise<void> {
const { data } = await this.axios.get(imgUrl, { responseType: 'blob' });
const data = await this.httpClient.fetchBlob(imgUrl);
const url = URL.createObjectURL(data);
saveUrl(this.window, url, filename);

View File

@@ -1,13 +1,10 @@
import { CsvJson } from 'csvjson';
import { NormalizedVisit } from '../../visits/types';
import { ExportableShortUrl } from '../../short-urls/data';
import type { ExportableShortUrl } from '../../short-urls/data';
import type { JsonToCsv } from '../../utils/helpers/csvjson';
import { saveCsv } from '../../utils/helpers/files';
import type { NormalizedVisit } from '../../visits/types';
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,8 +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,35 +1,36 @@
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 { ConnectDecorator } from '../../container/types';
import type Bottle from 'bottlejs';
import type { ConnectDecorator } from '../../container/types';
import { withoutSelectedServer } from '../../servers/helpers/withoutSelectedServer';
import { AsideMenu } from '../AsideMenu';
import { ErrorHandler } from '../ErrorHandler';
import { Home } from '../Home';
import { MainHeader } from '../MainHeader';
import { MenuLayout } from '../MenuLayout';
import { sidebarNotPresent, sidebarPresent } from '../reducers/sidebar';
import { ScrollToTop } from '../ScrollToTop';
import { ShlinkVersionsContainer } from '../ShlinkVersionsContainer';
import { HttpClient } from './HttpClient';
import { ImageDownloader } from './ImageDownloader';
import { ReportExporter } from './ReportExporter';
const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
export const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
// Services
bottle.constant('window', (global as any).window);
bottle.constant('console', global.console);
bottle.constant('axios', axios);
bottle.constant('window', window);
bottle.constant('console', console);
bottle.constant('fetch', window.fetch.bind(window));
bottle.service('ImageDownloader', ImageDownloader, 'axios', 'window');
bottle.service('ReportExporter', ReportExporter, 'window', 'csvjson');
bottle.service('HttpClient', HttpClient, 'fetch');
bottle.service('ImageDownloader', ImageDownloader, 'HttpClient', 'window');
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 +41,7 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
'CreateShortUrl',
'ShortUrlVisits',
'TagVisits',
'DomainVisits',
'OrphanVisits',
'NonOrphanVisits',
'ServerError',
@@ -47,12 +49,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');
@@ -60,5 +62,3 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
bottle.serviceFactory('sidebarPresent', () => sidebarPresent);
bottle.serviceFactory('sidebarNotPresent', () => sidebarNotPresent);
};
export default provideServices;

View File

@@ -1,18 +1,19 @@
import Bottle, { IContainer } from 'bottlejs';
import { connect as reduxConnect } from 'react-redux';
import type { IContainer } from 'bottlejs';
import Bottle from 'bottlejs';
import { pick } from 'ramda';
import provideApiServices from '../api/services/provideServices';
import provideCommonServices from '../common/services/provideServices';
import provideShortUrlsServices from '../short-urls/services/provideServices';
import provideServersServices from '../servers/services/provideServices';
import provideVisitsServices from '../visits/services/provideServices';
import provideTagsServices from '../tags/services/provideServices';
import provideUtilsServices from '../utils/services/provideServices';
import provideMercureServices from '../mercure/services/provideServices';
import provideSettingsServices from '../settings/services/provideServices';
import provideDomainsServices from '../domains/services/provideServices';
import provideAppServices from '../app/services/provideServices';
import { ConnectDecorator } from './types';
import { connect as reduxConnect } from 'react-redux';
import { provideServices as provideApiServices } from '../api/services/provideServices';
import { provideServices as provideAppServices } from '../app/services/provideServices';
import { provideServices as provideCommonServices } from '../common/services/provideServices';
import { provideServices as provideDomainsServices } from '../domains/services/provideServices';
import { provideServices as provideMercureServices } from '../mercure/services/provideServices';
import { provideServices as provideServersServices } from '../servers/services/provideServices';
import { provideServices as provideSettingsServices } from '../settings/services/provideServices';
import { provideServices as provideShortUrlsServices } from '../short-urls/services/provideServices';
import { provideServices as provideTagsServices } from '../tags/services/provideServices';
import { provideServices as provideUtilsServices } from '../utils/services/provideServices';
import { provideServices as provideVisitsServices } from '../visits/services/provideServices';
import type { ConnectDecorator } from './types';
type LazyActionMap = Record<string, Function>;
@@ -20,8 +21,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

@@ -1,21 +1,26 @@
import ReduxThunk from 'redux-thunk';
import { applyMiddleware, compose, createStore } from 'redux';
import { save, load, RLSOptions } from 'redux-localstorage-simple';
import reducers from '../reducers';
import { configureStore } from '@reduxjs/toolkit';
import type { IContainer } from 'bottlejs';
import type { RLSOptions } from 'redux-localstorage-simple';
import { load, save } from 'redux-localstorage-simple';
import { initReducers } from '../reducers';
import { migrateDeprecatedSettings } from '../settings/helpers';
import { ShlinkState } from './types';
const isProduction = process.env.NODE_ENV !== 'production';
const composeEnhancers: Function = !isProduction && (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
import type { ShlinkState } from './types';
const isProduction = process.env.NODE_ENV === 'production';
const localStorageConfig: RLSOptions = {
states: [ 'settings', 'servers' ],
states: ['settings', 'servers'],
namespace: 'shlink',
namespaceSeparator: '.',
debounce: 300,
};
const preloadedState = migrateDeprecatedSettings(load(localStorageConfig) as ShlinkState);
export const store = createStore(reducers, preloadedState, composeEnhancers(
applyMiddleware(save(localStorageConfig), ReduxThunk),
));
export const setUpStore = (container: IContainer) => configureStore({
devTools: !isProduction,
reducer: initReducers(container),
preloadedState,
middleware: (defaultMiddlewaresIncludingReduxThunk) =>
defaultMiddlewaresIncludingReduxThunk({ immutableCheck: false, serializableCheck: false }) // State is too big for these
.prepend(container.selectServerListener.middleware)
.concat(save(localStorageConfig)),
});

View File

@@ -1,30 +1,32 @@
import { MercureInfo } from '../mercure/reducers/mercureInfo';
import { SelectedServer, ServersMap } from '../servers/data';
import { Settings } from '../settings/reducers/settings';
import { ShortUrlCreation } from '../short-urls/reducers/shortUrlCreation';
import { ShortUrlDeletion } from '../short-urls/reducers/shortUrlDeletion';
import { ShortUrlEdition } from '../short-urls/reducers/shortUrlEdition';
import { ShortUrlsList } from '../short-urls/reducers/shortUrlsList';
import { TagDeletion } from '../tags/reducers/tagDelete';
import { TagEdition } from '../tags/reducers/tagEdit';
import { TagsList } from '../tags/reducers/tagsList';
import { ShortUrlDetail } from '../short-urls/reducers/shortUrlDetail';
import { ShortUrlVisits } from '../visits/reducers/shortUrlVisits';
import { TagVisits } from '../visits/reducers/tagVisits';
import { DomainsList } from '../domains/reducers/domainsList';
import { VisitsOverview } from '../visits/reducers/visitsOverview';
import { VisitsInfo } from '../visits/types';
import { Sidebar } from '../common/reducers/sidebar';
import type { Sidebar } from '../common/reducers/sidebar';
import type { DomainsList } from '../domains/reducers/domainsList';
import type { MercureInfo } from '../mercure/reducers/mercureInfo';
import type { SelectedServer, ServersMap } from '../servers/data';
import type { Settings } from '../settings/reducers/settings';
import type { ShortUrlCreation } from '../short-urls/reducers/shortUrlCreation';
import type { ShortUrlDeletion } from '../short-urls/reducers/shortUrlDeletion';
import type { ShortUrlDetail } from '../short-urls/reducers/shortUrlDetail';
import type { ShortUrlEdition } from '../short-urls/reducers/shortUrlEdition';
import type { ShortUrlsList } from '../short-urls/reducers/shortUrlsList';
import type { TagDeletion } from '../tags/reducers/tagDelete';
import type { TagEdition } from '../tags/reducers/tagEdit';
import type { TagsList } from '../tags/reducers/tagsList';
import type { DomainVisits } from '../visits/reducers/domainVisits';
import type { ShortUrlVisits } from '../visits/reducers/shortUrlVisits';
import type { TagVisits } from '../visits/reducers/tagVisits';
import type { VisitsInfo } from '../visits/reducers/types';
import type { VisitsOverview } from '../visits/reducers/visitsOverview';
export interface ShlinkState {
servers: ServersMap;
selectedServer: SelectedServer;
shortUrlsList: ShortUrlsList;
shortUrlCreationResult: ShortUrlCreation;
shortUrlCreation: ShortUrlCreation;
shortUrlDeletion: ShortUrlDeletion;
shortUrlEdition: ShortUrlEdition;
shortUrlVisits: ShortUrlVisits;
tagVisits: TagVisits;
domainVisits: DomainVisits;
orphanVisits: VisitsInfo;
nonOrphanVisits: VisitsInfo;
shortUrlDetail: ShortUrlDetail;

View File

@@ -1,24 +1,20 @@
import { FC, useEffect } from 'react';
import { Button, UncontrolledTooltip } from 'reactstrap';
import { faDotCircle as defaultDomainIcon } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import {
faBan as forbiddenIcon,
faDotCircle as defaultDomainIcon,
faEdit as editIcon,
} 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 type { FC } from 'react';
import { useEffect } from 'react';
import { UncontrolledTooltip } from 'reactstrap';
import type { ShlinkDomainRedirects } from '../api/types';
import type { SelectedServer } from '../servers/data';
import type { OptionalString } from '../utils/utils';
import type { Domain } from './data';
import { DomainDropdown } from './helpers/DomainDropdown';
import { DomainStatusIcon } from './helpers/DomainStatusIcon';
import type { EditDomainRedirects } from './reducers/domainRedirects';
interface DomainRowProps {
domain: Domain;
defaultRedirects?: ShlinkDomainRedirects;
editDomainRedirects: (domain: string, redirects: Partial<ShlinkDomainRedirects>) => Promise<void>;
editDomainRedirects: (redirects: EditDomainRedirects) => Promise<void>;
checkDomainHealth: (domain: string) => void;
selectedServer: SelectedServer;
}
@@ -39,9 +35,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 +58,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

@@ -1,11 +1,12 @@
import { useEffect } from 'react';
import { Button, DropdownItem, Input, InputGroup, UncontrolledTooltip, InputProps } from 'reactstrap';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faUndo } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { isEmpty, pipe } from 'ramda';
import { useEffect } from 'react';
import type { InputProps } from 'reactstrap';
import { Button, DropdownItem, Input, InputGroup, UncontrolledTooltip } from 'reactstrap';
import { DropdownBtn } from '../utils/DropdownBtn';
import { useToggle } from '../utils/helpers/hooks';
import { DomainsList } from './reducers/domainsList';
import type { DomainsList } from './reducers/domainsList';
import './DomainSelector.scss';
export interface DomainSelectorProps extends Omit<InputProps, 'onChange'> {
@@ -19,7 +20,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 +41,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 +58,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,24 +1,25 @@
import { FC, useEffect } from 'react';
import Message from '../utils/Message';
import { Result } from '../utils/Result';
import type { FC } from 'react';
import { useEffect } from 'react';
import { ShlinkApiError } from '../api/ShlinkApiError';
import type { SelectedServer } from '../servers/data';
import { Message } from '../utils/Message';
import { Result } from '../utils/Result';
import { SearchField } from '../utils/SearchField';
import { SimpleCard } from '../utils/SimpleCard';
import SearchField from '../utils/SearchField';
import { ShlinkDomainRedirects } from '../api/types';
import { SelectedServer } from '../servers/data';
import { DomainsList } from './reducers/domainsList';
import { DomainRow } from './DomainRow';
import type { EditDomainRedirects } from './reducers/domainRedirects';
import type { DomainsList } from './reducers/domainsList';
interface ManageDomainsProps {
listDomains: Function;
filterDomains: (searchTerm: string) => void;
editDomainRedirects: (domain: string, redirects: Partial<ShlinkDomainRedirects>) => Promise<void>;
editDomainRedirects: (redirects: EditDomainRedirects) => Promise<void>;
checkDomainHealth: (domain: string) => void;
domainsList: DomainsList;
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

@@ -1,4 +1,4 @@
import { ShlinkDomain } from '../../api/types';
import type { ShlinkDomain } from '../../api/types';
export type DomainStatus = 'validating' | 'valid' | 'invalid';

View File

@@ -0,0 +1,51 @@
import { faChartPie as pieChartIcon, faEdit as editIcon } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import type { FC } from 'react';
import { Link } from 'react-router-dom';
import { DropdownItem } from 'reactstrap';
import type { SelectedServer } from '../../servers/data';
import { getServerId } from '../../servers/data';
import { useFeature } from '../../utils/helpers/features';
import { useToggle } from '../../utils/helpers/hooks';
import { RowDropdownBtn } from '../../utils/RowDropdownBtn';
import { DEFAULT_DOMAIN } from '../../visits/reducers/domainVisits';
import type { Domain } from '../data';
import type { EditDomainRedirects } from '../reducers/domainRedirects';
import { EditDomainRedirectsModal } from './EditDomainRedirectsModal';
interface DomainDropdownProps {
domain: Domain;
editDomainRedirects: (redirects: EditDomainRedirects) => Promise<void>;
selectedServer: SelectedServer;
}
export const DomainDropdown: FC<DomainDropdownProps> = ({ domain, editDomainRedirects, selectedServer }) => {
const [isModalOpen, toggleModal] = useToggle();
const { isDefault } = domain;
const canBeEdited = !isDefault || useFeature('defaultDomainRedirectsEdition', selectedServer);
const withVisits = useFeature('domainVisits', selectedServer);
const serverId = getServerId(selectedServer);
return (
<RowDropdownBtn>
{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}
/>
</RowDropdownBtn>
);
};

View File

@@ -1,14 +1,16 @@
import { FC, useEffect, useRef, useState } from 'react';
import { UncontrolledTooltip } from 'reactstrap';
import { ExternalLink } from 'react-external-link';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import {
faTimes as invalidIcon,
faCheck as checkIcon,
faCircleNotch as loadingStatusIcon,
faTimes as invalidIcon,
} from '@fortawesome/free-solid-svg-icons';
import { MediaMatcher } from '../../utils/types';
import { DomainStatus } from '../data';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import type { FC } from 'react';
import { useEffect, useState } from 'react';
import { ExternalLink } from 'react-external-link';
import { UncontrolledTooltip } from 'reactstrap';
import { useElementRef } from '../../utils/helpers/hooks';
import type { MediaMatcher } from '../../utils/types';
import type { DomainStatus } from '../data';
interface DomainStatusIconProps {
status: DomainStatus;
@@ -16,9 +18,9 @@ interface DomainStatusIconProps {
}
export const DomainStatusIcon: FC<DomainStatusIconProps> = ({ status, matchMedia = window.matchMedia }) => {
const ref = useRef<HTMLSpanElement>();
const ref = useElementRef<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,17 +36,13 @@ export const DomainStatusIcon: FC<DomainStatusIconProps> = ({ status, matchMedia
return (
<>
<span
ref={(el: HTMLSpanElement) => {
ref.current = el;
}}
>
<span ref={ref}>
{status === 'valid'
? <FontAwesomeIcon fixedWidth icon={checkIcon} className="text-muted" />
: <FontAwesomeIcon fixedWidth icon={invalidIcon} className="text-danger" />}
</span>
<UncontrolledTooltip
target={(() => ref.current) as any}
target={ref}
placement={isMobile ? 'top-start' : 'left'}
autohide={status === 'valid'}
>

View File

@@ -1,15 +1,18 @@
import { FC, useState } from 'react';
import type { FC } from 'react';
import { useState } from 'react';
import { Button, Modal, ModalBody, ModalFooter, ModalHeader } from 'reactstrap';
import { ShlinkDomain, ShlinkDomainRedirects } from '../../api/types';
import { InputFormGroup, InputFormGroupProps } from '../../utils/forms/InputFormGroup';
import { handleEventPreventingDefault, nonEmptyValueOrNull } from '../../utils/utils';
import type { ShlinkDomain } from '../../api/types';
import type { InputFormGroupProps } from '../../utils/forms/InputFormGroup';
import { InputFormGroup } from '../../utils/forms/InputFormGroup';
import { InfoTooltip } from '../../utils/InfoTooltip';
import { handleEventPreventingDefault, nonEmptyValueOrNull } from '../../utils/utils';
import type { EditDomainRedirects } from '../reducers/domainRedirects';
interface EditDomainRedirectsModalProps {
domain: ShlinkDomain;
isOpen: boolean;
toggle: () => void;
editDomainRedirects: (domain: string, redirects: Partial<ShlinkDomainRedirects>) => Promise<void>;
editDomainRedirects: (redirects: EditDomainRedirects) => Promise<void>;
}
const FormGroup: FC<InputFormGroupProps & { isLast?: boolean }> = ({ isLast, ...rest }) => (
@@ -25,20 +28,23 @@ 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, {
baseUrlRedirect: nonEmptyValueOrNull(baseUrlRedirect),
regular404Redirect: nonEmptyValueOrNull(regular404Redirect),
invalidShortUrlRedirect: nonEmptyValueOrNull(invalidShortUrlRedirect),
const handleSubmit = handleEventPreventingDefault(async () => editDomainRedirects({
domain: domain.domain,
redirects: {
baseUrlRedirect: nonEmptyValueOrNull(baseUrlRedirect),
regular404Redirect: nonEmptyValueOrNull(regular404Redirect),
invalidShortUrlRedirect: nonEmptyValueOrNull(invalidShortUrlRedirect),
},
}).then(toggle));
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

@@ -1,33 +1,22 @@
import { Action, Dispatch } from 'redux';
import { ShlinkApiClientBuilder } from '../../api/services/ShlinkApiClientBuilder';
import { ShlinkDomainRedirects } from '../../api/types';
import { GetState } from '../../container/types';
import { ApiErrorAction } from '../../api/types/actions';
import { parseApiError } from '../../api/utils';
import type { ShlinkApiClientBuilder } from '../../api/services/ShlinkApiClientBuilder';
import type { ShlinkDomainRedirects } from '../../api/types';
import { createAsyncThunk } from '../../utils/helpers/redux';
/* 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 */
const EDIT_DOMAIN_REDIRECTS = 'shlink/domainRedirects/EDIT_DOMAIN_REDIRECTS';
export interface EditDomainRedirectsAction extends Action<string> {
export interface EditDomainRedirects {
domain: string;
redirects: ShlinkDomainRedirects;
}
export const editDomainRedirects = (buildShlinkApiClient: ShlinkApiClientBuilder) => (
domain: string,
domainRedirects: Partial<ShlinkDomainRedirects>,
) => async (dispatch: Dispatch, getState: GetState) => {
dispatch({ type: EDIT_DOMAIN_REDIRECTS_START });
const { editDomainRedirects } = buildShlinkApiClient(getState);
export const editDomainRedirects = (
buildShlinkApiClient: ShlinkApiClientBuilder,
) => createAsyncThunk(
EDIT_DOMAIN_REDIRECTS,
async ({ domain, redirects: providedRedirects }: EditDomainRedirects, { getState }): Promise<EditDomainRedirects> => {
const { editDomainRedirects: shlinkEditDomainRedirects } = buildShlinkApiClient(getState);
const redirects = await shlinkEditDomainRedirects({ domain, ...providedRedirects });
try {
const redirects = await editDomainRedirects({ domain, ...domainRedirects });
dispatch<EditDomainRedirectsAction>({ type: EDIT_DOMAIN_REDIRECTS, domain, redirects });
} catch (e: any) {
dispatch<ApiErrorAction>({ type: EDIT_DOMAIN_REDIRECTS_ERROR, errorData: parseApiError(e) });
}
};
return { domain, redirects };
},
);

View File

@@ -1,22 +1,16 @@
import { Action, Dispatch } from 'redux';
import { ProblemDetailsError, ShlinkDomainRedirects } from '../../api/types';
import { buildReducer } from '../../utils/helpers/redux';
import { ShlinkApiClientBuilder } from '../../api/services/ShlinkApiClientBuilder';
import { GetState } from '../../container/types';
import type { AsyncThunk, SliceCaseReducers } from '@reduxjs/toolkit';
import { createAction, createSlice } from '@reduxjs/toolkit';
import type { ShlinkApiClientBuilder } from '../../api/services/ShlinkApiClientBuilder';
import type { ShlinkDomainRedirects } from '../../api/types';
import type { ProblemDetailsError } from '../../api/types/errors';
import { parseApiError } from '../../api/utils';
import { ApiErrorAction } from '../../api/types/actions';
import { Domain, DomainStatus } from '../data';
import { hasServerData } from '../../servers/data';
import { createAsyncThunk } from '../../utils/helpers/redux';
import { replaceAuthorityFromUri } from '../../utils/helpers/uri';
import { EDIT_DOMAIN_REDIRECTS, EditDomainRedirectsAction } from './domainRedirects';
import type { Domain, DomainStatus } from '../data';
import type { EditDomainRedirects } 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 */
const REDUCER_PREFIX = 'shlink/domainsList';
export interface DomainsList {
domains: Domain[];
@@ -27,16 +21,12 @@ export interface DomainsList {
errorData?: ProblemDetailsError;
}
export interface ListDomainsAction extends Action<string> {
interface ListDomains {
domains: Domain[];
defaultRedirects?: ShlinkDomainRedirects;
}
interface FilterDomainsAction extends Action<string> {
searchTerm: string;
}
interface ValidateDomain extends Action<string> {
interface ValidateDomain {
domain: string;
status: DomainStatus;
}
@@ -48,83 +38,89 @@ const initialState: DomainsList = {
error: false,
};
export type DomainsCombinedAction = ListDomainsAction
& ApiErrorAction
& FilterDomainsAction
& EditDomainRedirectsAction
& ValidateDomain;
export const replaceRedirectsOnDomain = (domain: string, redirects: ShlinkDomainRedirects) =>
(d: Domain): Domain => d.domain !== domain ? d : { ...d, redirects };
export const replaceRedirectsOnDomain = ({ domain, redirects }: EditDomainRedirects) =>
(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 }),
[LIST_DOMAINS_ERROR]: ({ errorData }) => ({ ...initialState, error: true, errorData }),
[LIST_DOMAINS]: (_, { domains, defaultRedirects }) =>
({ ...initialState, domains, filteredDomains: domains, defaultRedirects }),
[FILTER_DOMAINS]: (state, { searchTerm }) => ({
...state,
filteredDomains: state.domains.filter(({ domain }) => domain.toLowerCase().match(searchTerm)),
}),
[EDIT_DOMAIN_REDIRECTS]: (state, { domain, redirects }) => ({
...state,
domains: state.domains.map(replaceRedirectsOnDomain(domain, redirects)),
filteredDomains: state.filteredDomains.map(replaceRedirectsOnDomain(domain, redirects)),
}),
[VALIDATE_DOMAIN]: (state, { domain, status }) => ({
...state,
domains: state.domains.map(replaceStatusOnDomain(domain, status)),
filteredDomains: state.filteredDomains.map(replaceStatusOnDomain(domain, status)),
}),
}, initialState);
export const listDomains = (buildShlinkApiClient: ShlinkApiClientBuilder) => () => async (
dispatch: Dispatch,
getState: GetState,
export const domainsListReducerCreator = (
buildShlinkApiClient: ShlinkApiClientBuilder,
editDomainRedirects: AsyncThunk<EditDomainRedirects, any, any>,
) => {
dispatch({ type: LIST_DOMAINS_START });
const { listDomains } = buildShlinkApiClient(getState);
const listDomains = createAsyncThunk(`${REDUCER_PREFIX}/listDomains`, async (_: void, { getState }): Promise<ListDomains> => {
const { listDomains: shlinkListDomains } = buildShlinkApiClient(getState);
const { data, defaultRedirects } = await shlinkListDomains();
try {
const { domains, defaultRedirects } = await listDomains().then(({ data, defaultRedirects }) => ({
return {
domains: data.map((domain): Domain => ({ ...domain, status: 'validating' })),
defaultRedirects,
}));
};
});
dispatch<ListDomainsAction>({ type: LIST_DOMAINS, domains, defaultRedirects });
} catch (e: any) {
dispatch<ApiErrorAction>({ type: LIST_DOMAINS_ERROR, errorData: parseApiError(e) });
}
};
export const filterDomains = (searchTerm: string): FilterDomainsAction => ({ type: FILTER_DOMAINS, searchTerm });
export const checkDomainHealth = (buildShlinkApiClient: ShlinkApiClientBuilder) => (domain: string) => async (
dispatch: Dispatch,
getState: GetState,
) => {
const { selectedServer } = getState();
if (!hasServerData(selectedServer)) {
dispatch<ValidateDomain>({ type: VALIDATE_DOMAIN, domain, status: 'invalid' });
return;
}
try {
const { url, ...rest } = selectedServer;
const { health } = buildShlinkApiClient({
...rest,
url: replaceAuthorityFromUri(url, domain),
});
const { status } = await health();
dispatch<ValidateDomain>({ type: VALIDATE_DOMAIN, domain, status: status === 'pass' ? 'valid' : 'invalid' });
} catch (e) {
dispatch<ValidateDomain>({ type: VALIDATE_DOMAIN, domain, status: 'invalid' });
}
const checkDomainHealth = createAsyncThunk(
`${REDUCER_PREFIX}/checkDomainHealth`,
async (domain: string, { getState }): Promise<ValidateDomain> => {
const { selectedServer } = getState();
if (!hasServerData(selectedServer)) {
return { domain, status: 'invalid' };
}
try {
const { url, ...rest } = selectedServer;
const { health } = buildShlinkApiClient({
...rest,
url: replaceAuthorityFromUri(url, domain),
});
const { status } = await health();
return { domain, status: status === 'pass' ? 'valid' : 'invalid' };
} catch (e) {
return { domain, status: 'invalid' };
}
},
);
const filterDomains = createAction<string>(`${REDUCER_PREFIX}/filterDomains`);
const { reducer } = createSlice<DomainsList, SliceCaseReducers<DomainsList>>({
name: REDUCER_PREFIX,
initialState,
reducers: {},
extraReducers: (builder) => {
builder.addCase(listDomains.pending, () => ({ ...initialState, loading: true }));
builder.addCase(listDomains.rejected, (_, { error }) => (
{ ...initialState, error: true, errorData: parseApiError(error) }
));
builder.addCase(listDomains.fulfilled, (_, { payload }) => (
{ ...initialState, ...payload, filteredDomains: payload.domains }
));
builder.addCase(checkDomainHealth.fulfilled, ({ domains, filteredDomains, ...rest }, { payload }) => ({
...rest,
domains: domains.map(replaceStatusOnDomain(payload.domain, payload.status)),
filteredDomains: filteredDomains.map(replaceStatusOnDomain(payload.domain, payload.status)),
}));
builder.addCase(filterDomains, (state, { payload }) => ({
...state,
filteredDomains: state.domains.filter(({ domain }) => domain.toLowerCase().match(payload.toLowerCase())),
}));
builder.addCase(editDomainRedirects.fulfilled, (state, { payload }) => ({
...state,
domains: state.domains.map(replaceRedirectsOnDomain(payload)),
filteredDomains: state.filteredDomains.map(replaceRedirectsOnDomain(payload)),
}));
},
});
return {
reducer,
listDomains,
checkDomainHealth,
filterDomains,
};
};

View File

@@ -1,26 +1,34 @@
import Bottle from 'bottlejs';
import { ConnectDecorator } from '../../container/types';
import { checkDomainHealth, filterDomains, listDomains } from '../reducers/domainsList';
import type Bottle from 'bottlejs';
import { prop } from 'ramda';
import type { ConnectDecorator } from '../../container/types';
import { DomainSelector } from '../DomainSelector';
import { ManageDomains } from '../ManageDomains';
import { editDomainRedirects } from '../reducers/domainRedirects';
import { domainsListReducerCreator } from '../reducers/domainsList';
const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
export 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
bottle.serviceFactory('listDomains', listDomains, 'buildShlinkApiClient');
bottle.serviceFactory('filterDomains', () => filterDomains);
bottle.serviceFactory('editDomainRedirects', editDomainRedirects, 'buildShlinkApiClient');
bottle.serviceFactory('checkDomainHealth', checkDomainHealth, 'buildShlinkApiClient');
};
// Reducer
bottle.serviceFactory(
'domainsListReducerCreator',
domainsListReducerCreator,
'buildShlinkApiClient',
'editDomainRedirects',
);
bottle.serviceFactory('domainsListReducer', prop('reducer'), 'domainsListReducerCreator');
export default provideServices;
// Actions
bottle.serviceFactory('listDomains', prop('listDomains'), 'domainsListReducerCreator');
bottle.serviceFactory('filterDomains', prop('filterDomains'), 'domainsListReducerCreator');
bottle.serviceFactory('editDomainRedirects', editDomainRedirects, 'buildShlinkApiClient');
bottle.serviceFactory('checkDomainHealth', prop('checkDomainHealth'), 'domainsListReducerCreator');
};

View File

@@ -3,7 +3,8 @@
@import './utils/base';
@import 'node_modules/bootstrap/scss/bootstrap.scss';
@import './common/react-tag-autocomplete.scss';
@import './theme/theme';
@import './utils/theme/theme';
@import './utils/mixins/text-ellipsis';
@import './utils/table/ResponsiveTable';
@import './utils/StickyCardPaginator';
@@ -13,6 +14,7 @@
:root {
scroll-behavior: auto;
color-scheme: var(--color-scheme);
}
html,
@@ -38,6 +40,10 @@ a:not(.nav-link):not(.navbar-brand):not(.page-link):not(.highlight-card):not(.bt
background-color: $mainColor !important;
}
.bg-warning {
color: $lightTextColor;
}
.card-body,
.card-header,
.list-group-item {
@@ -49,7 +55,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);
}
@@ -217,9 +223,7 @@ hr {
}
.text-ellipsis {
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
@include text-ellipsis();
}
.progress-bar {

View File

@@ -1,23 +1,25 @@
import { render } from 'react-dom';
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 { 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 { setUpStore } from './container/store';
import { register as registerServiceWorker } from './serviceWorkerRegistration';
import 'react-datepicker/dist/react-datepicker.css';
import 'leaflet/dist/leaflet.css';
import { fixLeafletIcons } from './utils/helpers/leaflet';
import './index.scss';
import 'leaflet/dist/leaflet.css';
import 'react-datepicker/dist/react-datepicker.css';
// This overwrites icons used for leaflet maps, fixing some issues caused by webpack while processing the CSS
fixLeafletIcons();
const store = setUpStore(container);
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 +27,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

@@ -1,8 +1,9 @@
import { FC, useEffect } from 'react';
import { pipe } from 'ramda';
import type { FC } from 'react';
import { useEffect } from 'react';
import { useParams } from 'react-router-dom';
import { CreateVisit } from '../../visits/types';
import { MercureInfo } from '../reducers/mercureInfo';
import type { CreateVisit } from '../../visits/types';
import type { MercureInfo } from '../reducers/mercureInfo';
import { bindToMercureTopic } from './index';
export interface MercureBoundProps {
@@ -23,7 +24,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 +33,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

@@ -1,5 +1,5 @@
import { EventSourcePolyfill as EventSource } from 'event-source-polyfill';
import { MercureInfo } from '../reducers/mercureInfo';
import type { MercureInfo } from '../reducers/mercureInfo';
export const bindToMercureTopic = <T>(mercureInfo: MercureInfo, topics: string[], onMessage: (message: T) => void, onTokenExpired: () => void) => { // eslint-disable-line max-len
const { mercureHubUrl, token, loading, error } = mercureInfo;

View File

@@ -1,54 +1,44 @@
import { Action, Dispatch } from 'redux';
import { ShlinkMercureInfo } from '../../api/types';
import { GetState } from '../../container/types';
import { buildReducer } from '../../utils/helpers/redux';
import { ShlinkApiClientBuilder } from '../../api/services/ShlinkApiClientBuilder';
import { createSlice } from '@reduxjs/toolkit';
import type { ShlinkApiClientBuilder } from '../../api/services/ShlinkApiClientBuilder';
import type { ShlinkMercureInfo } from '../../api/types';
import { createAsyncThunk } from '../../utils/helpers/redux';
/* 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 */
const REDUCER_PREFIX = 'shlink/mercure';
export interface MercureInfo {
token?: string;
mercureHubUrl?: string;
export interface MercureInfo extends Partial<ShlinkMercureInfo> {
interval?: number;
loading: boolean;
error: boolean;
}
export type GetMercureInfoAction = Action<string> & ShlinkMercureInfo & { interval?: number };
const initialState: MercureInfo = {
loading: true,
error: false,
};
export default buildReducer<MercureInfo, GetMercureInfoAction>({
[GET_MERCURE_INFO_START]: (state) => ({ ...state, loading: true, error: false }),
[GET_MERCURE_INFO_ERROR]: (state) => ({ ...state, loading: false, error: true }),
[GET_MERCURE_INFO]: (_, action) => ({ ...action, loading: false, error: false }),
}, initialState);
export const mercureInfoReducerCreator = (buildShlinkApiClient: ShlinkApiClientBuilder) => {
const loadMercureInfo = createAsyncThunk(
`${REDUCER_PREFIX}/loadMercureInfo`,
(_: void, { getState }): Promise<ShlinkMercureInfo> => {
const { settings } = getState();
if (!settings.realTimeUpdates.enabled) {
throw new Error('Real time updates not enabled');
}
export const loadMercureInfo = (buildShlinkApiClient: ShlinkApiClientBuilder) =>
() => async (dispatch: Dispatch, getState: GetState) => {
dispatch({ type: GET_MERCURE_INFO_START });
return buildShlinkApiClient(getState).mercureInfo();
},
);
const { settings } = getState();
const { mercureInfo } = buildShlinkApiClient(getState);
const { reducer } = createSlice({
name: REDUCER_PREFIX,
initialState,
reducers: {},
extraReducers: (builder) => {
builder.addCase(loadMercureInfo.pending, (state) => ({ ...state, loading: true, error: false }));
builder.addCase(loadMercureInfo.rejected, (state) => ({ ...state, loading: false, error: true }));
builder.addCase(loadMercureInfo.fulfilled, (_, { payload }) => ({ ...payload, loading: false, error: false }));
},
});
if (!settings.realTimeUpdates.enabled) {
dispatch({ type: GET_MERCURE_INFO_ERROR });
return;
}
try {
const info = await mercureInfo();
dispatch<GetMercureInfoAction>({ type: GET_MERCURE_INFO, interval: settings.realTimeUpdates.interval, ...info });
} catch (e) {
dispatch({ type: GET_MERCURE_INFO_ERROR });
}
};
return { loadMercureInfo, reducer };
};

View File

@@ -1,9 +1,12 @@
import Bottle from 'bottlejs';
import { loadMercureInfo } from '../reducers/mercureInfo';
import type Bottle from 'bottlejs';
import { prop } from 'ramda';
import { mercureInfoReducerCreator } from '../reducers/mercureInfo';
export const provideServices = (bottle: Bottle) => {
// Reducer
bottle.serviceFactory('mercureInfoReducerCreator', mercureInfoReducerCreator, 'buildShlinkApiClient');
bottle.serviceFactory('mercureInfoReducer', prop('reducer'), 'mercureInfoReducerCreator');
const provideServices = (bottle: Bottle) => {
// Actions
bottle.serviceFactory('loadMercureInfo', loadMercureInfo, 'buildShlinkApiClient');
bottle.serviceFactory('loadMercureInfo', prop('loadMercureInfo'), 'mercureInfoReducerCreator');
};
export default provideServices;

View File

@@ -1,45 +1,31 @@
import { combineReducers } from 'redux';
import serversReducer from '../servers/reducers/servers';
import selectedServerReducer from '../servers/reducers/selectedServer';
import shortUrlsListReducer from '../short-urls/reducers/shortUrlsList';
import shortUrlCreationReducer from '../short-urls/reducers/shortUrlCreation';
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 orphanVisitsReducer from '../visits/reducers/orphanVisits';
import nonOrphanVisitsReducer from '../visits/reducers/nonOrphanVisits';
import shortUrlDetailReducer from '../short-urls/reducers/shortUrlDetail';
import tagsListReducer from '../tags/reducers/tagsList';
import tagDeleteReducer from '../tags/reducers/tagDelete';
import tagEditReducer from '../tags/reducers/tagEdit';
import mercureInfoReducer from '../mercure/reducers/mercureInfo';
import settingsReducer from '../settings/reducers/settings';
import domainsListReducer from '../domains/reducers/domainsList';
import visitsOverviewReducer from '../visits/reducers/visitsOverview';
import appUpdatesReducer from '../app/reducers/appUpdates';
import sidebarReducer from '../common/reducers/sidebar';
import { ShlinkState } from '../container/types';
import { combineReducers } from '@reduxjs/toolkit';
import type { IContainer } from 'bottlejs';
import { appUpdatesReducer } from '../app/reducers/appUpdates';
import { sidebarReducer } from '../common/reducers/sidebar';
import type { ShlinkState } from '../container/types';
import { serversReducer } from '../servers/reducers/servers';
import { settingsReducer } from '../settings/reducers/settings';
export default combineReducers<ShlinkState>({
export const initReducers = (container: IContainer) => combineReducers<ShlinkState>({
servers: serversReducer,
selectedServer: selectedServerReducer,
shortUrlsList: shortUrlsListReducer,
shortUrlCreationResult: shortUrlCreationReducer,
shortUrlDeletion: shortUrlDeletionReducer,
shortUrlEdition: shortUrlEditionReducer,
shortUrlVisits: shortUrlVisitsReducer,
tagVisits: tagVisitsReducer,
orphanVisits: orphanVisitsReducer,
nonOrphanVisits: nonOrphanVisitsReducer,
shortUrlDetail: shortUrlDetailReducer,
tagsList: tagsListReducer,
tagDelete: tagDeleteReducer,
tagEdit: tagEditReducer,
mercureInfo: mercureInfoReducer,
selectedServer: container.selectedServerReducer,
shortUrlsList: container.shortUrlsListReducer,
shortUrlCreation: container.shortUrlCreationReducer,
shortUrlDeletion: container.shortUrlDeletionReducer,
shortUrlEdition: container.shortUrlEditionReducer,
shortUrlDetail: container.shortUrlDetailReducer,
shortUrlVisits: container.shortUrlVisitsReducer,
tagVisits: container.tagVisitsReducer,
domainVisits: container.domainVisitsReducer,
orphanVisits: container.orphanVisitsReducer,
nonOrphanVisits: container.nonOrphanVisitsReducer,
tagsList: container.tagsListReducer,
tagDelete: container.tagDeleteReducer,
tagEdit: container.tagEditReducer,
mercureInfo: container.mercureInfoReducer,
settings: settingsReducer,
domainsList: domainsListReducer,
visitsOverview: visitsOverviewReducer,
domainsList: container.domainsListReducer,
visitsOverview: container.visitsOverviewReducer,
appUpdated: appUpdatesReducer,
sidebar: sidebarReducer,
});

View File

@@ -1,19 +1,21 @@
import { FC, useEffect, useState } from 'react';
import { v4 as uuid } from 'uuid';
import { Button } from 'reactstrap';
import type { FC } from 'react';
import { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { Result } from '../utils/Result';
import { Button } from 'reactstrap';
import { v4 as uuid } from 'uuid';
import { NoMenuLayout } from '../common/NoMenuLayout';
import { StateFlagTimeout, useGoBack, useToggle } from '../utils/helpers/hooks';
import { ServerForm } from './helpers/ServerForm';
import { ImportServersBtnProps } from './helpers/ImportServersBtn';
import { ServerData, ServersMap, ServerWithId } from './data';
import type { TimeoutToggle } from '../utils/helpers/hooks';
import { useGoBack, useToggle } from '../utils/helpers/hooks';
import { Result } from '../utils/Result';
import type { ServerData, ServersMap, ServerWithId } from './data';
import { DuplicatedServersModal } from './helpers/DuplicatedServersModal';
import type { ImportServersBtnProps } from './helpers/ImportServersBtn';
import { ServerForm } from './helpers/ServerForm';
const SHOW_IMPORT_MSG_TIME = 4000;
interface CreateServerProps {
createServer: (server: ServerWithId) => void;
createServers: (servers: ServerWithId[]) => void;
servers: ServersMap;
}
@@ -26,16 +28,16 @@ const ImportResult = ({ type }: { type: 'error' | 'success' }) => (
</div>
);
const CreateServer = (ImportServersBtn: FC<ImportServersBtnProps>, useStateFlagTimeout: StateFlagTimeout) => (
{ servers, createServer }: CreateServerProps,
export const CreateServer = (ImportServersBtn: FC<ImportServersBtnProps>, useTimeoutToggle: TimeoutToggle) => (
{ servers, createServers }: 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;
@@ -43,7 +45,7 @@ const CreateServer = (ImportServersBtn: FC<ImportServersBtnProps>, useStateFlagT
const id = uuid();
createServer({ ...serverData, id });
createServers([{ ...serverData, id }]);
navigate(`/server/${id}`);
};
@@ -53,13 +55,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 +72,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 { faMinusCircle as deleteIcon } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import type { FC, PropsWithChildren } from 'react';
import { useToggle } from '../utils/helpers/hooks';
import { DeleteServerModalProps } from './DeleteServerModal';
import { ServerWithId } from './data';
import type { ServerWithId } from './data';
import type { DeleteServerModalProps } from './DeleteServerModal';
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,7 +1,8 @@
import { FC } from 'react';
import { Modal, ModalBody, ModalFooter, ModalHeader } from 'reactstrap';
import type { FC } from 'react';
import { useRef } from 'react';
import { useNavigate } from 'react-router-dom';
import { ServerWithId } from './data';
import { Button, Modal, ModalBody, ModalFooter, ModalHeader } from 'reactstrap';
import type { ServerWithId } from './data';
export interface DeleteServerModalProps {
server: ServerWithId;
@@ -14,19 +15,27 @@ 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();
const closeModal = () => {
deleteServer(server);
const doDelete = useRef<boolean>(false);
const toggleAndDelete = () => {
doDelete.current = true;
toggle();
};
const onClosed = () => {
if (!doDelete.current) {
return;
}
deleteServer(server);
redirectHome && navigate('/');
};
return (
<Modal isOpen={isOpen} toggle={toggle} centered>
<ModalHeader toggle={toggle}><span className="text-danger">Remove server</span></ModalHeader>
<Modal isOpen={isOpen} toggle={toggle} centered onClosed={onClosed}>
<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 +46,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={toggleAndDelete}>Delete</Button>
</ModalFooter>
</Modal>
);
};
export default DeleteServerModal;

View File

@@ -1,17 +1,21 @@
import { FC } from 'react';
import type { FC } from 'react';
import { Button } from 'reactstrap';
import { NoMenuLayout } from '../common/NoMenuLayout';
import { useGoBack } from '../utils/helpers/hooks';
import { useGoBack, useParsedQuery } from '../utils/helpers/hooks';
import type { ServerData } from './data';
import { isServerWithId } from './data';
import { ServerForm } from './helpers/ServerForm';
import { withSelectedServer } from './helpers/withSelectedServer';
import { isServerWithId, ServerData } from './data';
interface EditServerProps {
editServer: (serverId: string, serverData: ServerData) => void;
}
export const EditServer = (ServerError: FC) => withSelectedServer<EditServerProps>(({ editServer, selectedServer }) => {
export const EditServer = (ServerError: FC) => withSelectedServer<EditServerProps>((
{ editServer, selectedServer, selectServer },
) => {
const goBack = useGoBack();
const { reconnect } = useParsedQuery<{ reconnect?: 'true' }>();
if (!isServerWithId(selectedServer)) {
return null;
@@ -19,6 +23,7 @@ export const EditServer = (ServerError: FC) => withSelectedServer<EditServerProp
const handleSubmit = (serverData: ServerData) => {
editServer(selectedServer.id, serverData);
reconnect === 'true' && selectServer(selectedServer.id);
goBack();
};

View File

@@ -1,17 +1,18 @@
import { FC, useEffect, useState } from 'react';
import { Button, Row } from 'reactstrap';
import { faFileDownload as exportIcon, faPlus as plusIcon } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import type { FC } from 'react';
import { useEffect, useState } from 'react';
import { Link } from 'react-router-dom';
import { Button, Row } from 'reactstrap';
import { NoMenuLayout } from '../common/NoMenuLayout';
import { SimpleCard } from '../utils/SimpleCard';
import SearchField from '../utils/SearchField';
import type { TimeoutToggle } from '../utils/helpers/hooks';
import { Result } from '../utils/Result';
import { StateFlagTimeout } from '../utils/helpers/hooks';
import { ImportServersBtnProps } from './helpers/ImportServersBtn';
import { ServersMap } from './data';
import { ManageServersRowProps } from './ManageServersRow';
import ServersExporter from './services/ServersExporter';
import { SearchField } from '../utils/SearchField';
import { SimpleCard } from '../utils/SimpleCard';
import type { ServersMap } from './data';
import type { ImportServersBtnProps } from './helpers/ImportServersBtn';
import type { ManageServersRowProps } from './ManageServersRow';
import type { ServersExporter } from './services/ServersExporter';
interface ManageServersProps {
servers: ServersMap;
@@ -22,20 +23,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 +62,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

@@ -1,10 +1,10 @@
import { FC } from 'react';
import { UncontrolledTooltip } from 'reactstrap';
import { Link } from 'react-router-dom';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faCheck as checkIcon } from '@fortawesome/free-solid-svg-icons';
import { ServerWithId } from './data';
import { ManageServersRowDropdownProps } from './ManageServersRowDropdown';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import type { FC } from 'react';
import { Link } from 'react-router-dom';
import { UncontrolledTooltip } from 'reactstrap';
import type { ServerWithId } from './data';
import type { ManageServersRowDropdownProps } from './ManageServersRowDropdown';
export interface ManageServersRowProps {
server: ServerWithId;

View File

@@ -1,18 +1,18 @@
import { FC } from 'react';
import { DropdownItem } from 'reactstrap';
import { Link } from 'react-router-dom';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faCircle as toggleOnIcon } from '@fortawesome/free-regular-svg-icons';
import {
faBan as toggleOffIcon,
faEdit as editIcon,
faMinusCircle as deleteIcon,
faPlug as connectIcon,
} from '@fortawesome/free-solid-svg-icons';
import { faCircle as toggleOnIcon } from '@fortawesome/free-regular-svg-icons';
import { DropdownBtnMenu } from '../utils/DropdownBtnMenu';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import type { FC } from 'react';
import { Link } from 'react-router-dom';
import { DropdownItem } from 'reactstrap';
import { useToggle } from '../utils/helpers/hooks';
import { DeleteServerModalProps } from './DeleteServerModal';
import { ServerWithId } from './data';
import { RowDropdownBtn } from '../utils/RowDropdownBtn';
import type { ServerWithId } from './data';
import type { DeleteServerModalProps } from './DeleteServerModal';
export interface ManageServersRowDropdownProps {
server: ServerWithId;
@@ -25,21 +25,20 @@ interface ManageServersRowDropdownConnectProps extends ManageServersRowDropdownP
export const ManageServersRowDropdown = (
DeleteServerModal: FC<DeleteServerModalProps>,
): FC<ManageServersRowDropdownConnectProps> => ({ server, setAutoConnect }) => {
const [ isMenuOpen, toggleMenu ] = useToggle();
const [ isModalOpen,, showModal, hideModal ] = useToggle();
const [isModalOpen,, showModal, hideModal] = useToggle();
const serverUrl = `/server/${server.id}`;
const { autoConnect: isAutoConnect } = server;
const autoConnectIcon = isAutoConnect ? toggleOffIcon : toggleOnIcon;
return (
<DropdownBtnMenu isOpen={isMenuOpen} toggle={toggleMenu}>
<RowDropdownBtn minWidth={170}>
<DropdownItem tag={Link} to={serverUrl}>
<FontAwesomeIcon icon={connectIcon} fixedWidth /> Connect
</DropdownItem>
<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 />
@@ -48,6 +47,6 @@ export const ManageServersRowDropdown = (
</DropdownItem>
<DeleteServerModal redirectHome={false} server={server} isOpen={isModalOpen} toggle={hideModal} />
</DropdownBtnMenu>
</RowDropdownBtn>
);
};

View File

@@ -1,19 +1,23 @@
import { FC, useEffect } from 'react';
import { Card, CardBody, CardHeader, Row } from 'reactstrap';
import type { FC } from 'react';
import { useEffect } from 'react';
import { Link, useNavigate } from 'react-router-dom';
import { ITEMS_IN_OVERVIEW_PAGE, ShortUrlsList as ShortUrlsListState } from '../short-urls/reducers/shortUrlsList';
import { prettify } from '../utils/helpers/numbers';
import { TagsList } from '../tags/reducers/tagsList';
import { ShortUrlsTableProps } from '../short-urls/ShortUrlsTable';
import { Card, CardBody, CardHeader, Row } from 'reactstrap';
import type { ShlinkShortUrlsListParams } from '../api/types';
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 { getServerId, SelectedServer } from './data';
import type { Settings } from '../settings/reducers/settings';
import type { CreateShortUrlProps } from '../short-urls/CreateShortUrl';
import type { ShortUrlsList as ShortUrlsListState } from '../short-urls/reducers/shortUrlsList';
import { ITEMS_IN_OVERVIEW_PAGE } from '../short-urls/reducers/shortUrlsList';
import type { ShortUrlsTableType } from '../short-urls/ShortUrlsTable';
import type { TagsList } from '../tags/reducers/tagsList';
import { useFeature } from '../utils/helpers/features';
import { prettify } from '../utils/helpers/numbers';
import type { VisitsOverview } from '../visits/reducers/visitsOverview';
import type { SelectedServer } from './data';
import { getServerId } from './data';
import { HighlightCard } from './helpers/HighlightCard';
import { VisitsHighlightCard } from './helpers/VisitsHighlightCard';
interface OverviewConnectProps {
shortUrlsList: ShortUrlsListState;
@@ -23,12 +27,12 @@ interface OverviewConnectProps {
selectedServer: SelectedServer;
visitsOverview: VisitsOverview;
loadVisitsOverview: Function;
settings: Settings;
}
export const Overview = (
ShortUrlsTable: FC<ShortUrlsTableProps>,
ShortUrlsTable: ShortUrlsTableType,
CreateShortUrl: FC<CreateShortUrlProps>,
ForServerVersion: FC<Versions>,
) => boundToMercureHub(({
shortUrlsList,
listShortUrls,
@@ -37,13 +41,13 @@ export const Overview = (
selectedServer,
loadVisitsOverview,
visitsOverview,
settings: { visits },
}: OverviewConnectProps) => {
const { loading, shortUrls } = shortUrlsList;
const { loading: loadingTags } = tagsList;
const { loading: loadingVisits, visitsCount, orphanVisitsCount } = visitsOverview;
const { loading: loadingVisits, nonOrphanVisits, orphanVisits } = visitsOverview;
const serverId = getServerId(selectedServer);
const linkToOrphanVisits = supportsOrphanVisits(selectedServer);
const linkToNonOrphanVisits = supportsNonOrphanVisits(selectedServer);
const linkToNonOrphanVisits = useFeature('nonOrphanVisits', selectedServer);
const navigate = useNavigate();
useEffect(() => {
@@ -56,19 +60,22 @@ export const Overview = (
<>
<Row>
<div className="col-lg-6 col-xl-3 mb-3">
<HighlightCard title="Visits" link={linkToNonOrphanVisits && `/server/${serverId}/non-orphan-visits`}>
{loadingVisits ? 'Loading...' : prettify(visitsCount)}
</HighlightCard>
<VisitsHighlightCard
title="Visits"
link={linkToNonOrphanVisits ? `/server/${serverId}/non-orphan-visits` : undefined}
excludeBots={visits?.excludeBots ?? false}
loading={loadingVisits}
visitsSummary={nonOrphanVisits}
/>
</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>
<VisitsHighlightCard
title="Orphan visits"
link={`/server/${serverId}/orphan-visits`}
excludeBots={visits?.excludeBots ?? false}
loading={loadingVisits}
visitsSummary={orphanVisits}
/>
</div>
<div className="col-lg-6 col-xl-3 mb-3">
<HighlightCard title="Short URLs" link={`/server/${serverId}/list-short-urls/1`}>
@@ -109,4 +116,4 @@ export const Overview = (
</Card>
</>
);
}, () => [ Topics.visits, Topics.orphanVisits ]);
}, () => [Topics.visits, Topics.orphanVisits]);

View File

@@ -1,16 +1,17 @@
import { isEmpty, values } from 'ramda';
import { DropdownItem, DropdownMenu, DropdownToggle, UncontrolledDropdown } from 'reactstrap';
import { Link } from 'react-router-dom';
import { faPlus as plusIcon, faServer as serverIcon } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { getServerId, SelectedServer, ServersMap } from './data';
import { isEmpty, values } from 'ramda';
import { Link } from 'react-router-dom';
import { DropdownItem, DropdownMenu, DropdownToggle, UncontrolledDropdown } from 'reactstrap';
import type { SelectedServer, ServersMap } from './data';
import { getServerId } from './data';
export interface ServersDropdownProps {
servers: ServersMap;
selectedServer: SelectedServer;
}
const ServersDropdown = ({ servers, selectedServer }: ServersDropdownProps) => {
export const ServersDropdown = ({ servers, selectedServer }: ServersDropdownProps) => {
const serversList = values(servers);
const renderServers = () => {
@@ -46,5 +47,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,16 +1,16 @@
import { FC } from 'react';
import { ListGroup, ListGroupItem } from 'reactstrap';
import { Link } from 'react-router-dom';
import classNames from 'classnames';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faChevronRight as chevronIcon } from '@fortawesome/free-solid-svg-icons';
import { ServerWithId } from './data';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import classNames from 'classnames';
import type { FC, PropsWithChildren } from 'react';
import { Link } from 'react-router-dom';
import { ListGroup, ListGroupItem } from 'reactstrap';
import type { 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

@@ -1,5 +1,5 @@
import { omit } from 'ramda';
import { SemVer } from '../../utils/helpers/version';
import type { SemVer } from '../../utils/helpers/version';
export interface ServerData {
name: string;
@@ -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

@@ -1,6 +1,7 @@
import { FC, Fragment } from 'react';
import type { FC } from 'react';
import { Fragment } from 'react';
import { Button, Modal, ModalBody, ModalFooter, ModalHeader } from 'reactstrap';
import { ServerData } from '../data';
import type { ServerData } from '../data';
interface DuplicatedServersModalProps {
duplicatedServers: ServerData[];
@@ -20,12 +21,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,21 +1,30 @@
import { FC } 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 type { FC, PropsWithChildren, ReactNode } from 'react';
import { Link } from 'react-router-dom';
import { Card, CardText, CardTitle, UncontrolledTooltip } from 'reactstrap';
import { useElementRef } from '../../utils/helpers/hooks';
import './HighlightCard.scss';
export interface HighlightCardProps {
export type HighlightCardProps = PropsWithChildren<{
title: string;
link?: string | false;
}
link?: string;
tooltip?: ReactNode;
}>;
const buildExtraProps = (link?: string | false) => !link ? {} : { tag: Link, to: link };
const buildExtraProps = (link?: string) => (!link ? {} : { tag: Link, to: link });
export const HighlightCard: FC<HighlightCardProps> = ({ children, title, link }) => (
<Card className="highlight-card" body {...buildExtraProps(link)}>
{link && <FontAwesomeIcon size="3x" className="highlight-card__link-icon" icon={linkIcon} />}
<CardTitle tag="h5" className="highlight-card__title">{title}</CardTitle>
<CardText tag="h2">{children}</CardText>
</Card>
);
export const HighlightCard: FC<HighlightCardProps> = ({ children, title, link, tooltip }) => {
const ref = useElementRef<HTMLElement>();
return (
<>
<Card innerRef={ref} className="highlight-card" body {...buildExtraProps(link)}>
{link && <FontAwesomeIcon size="3x" className="highlight-card__link-icon" icon={linkIcon} />}
<CardTitle tag="h5" className="highlight-card__title">{title}</CardTitle>
<CardText tag="h2">{children}</CardText>
</Card>
{tooltip && <UncontrolledTooltip target={ref} placement="bottom">{tooltip}</UncontrolledTooltip>}
</>
);
};

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