Compare commits

...

452 Commits

Author SHA1 Message Date
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
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
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
428 changed files with 16846 additions and 39925 deletions

View File

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

View File

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

View File

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

View File

@@ -14,9 +14,9 @@ jobs:
- name: Use node.js
uses: actions/setup-node@v1
with:
node-version: 16.15
node-version: 18.12
- 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

@@ -4,12 +4,19 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org).
## [3.7.1] - 2022-05-25
## [3.10.0] - 2023-03-19
### Added
* *Nothing*
* [#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
* [#648](https://github.com/shlinkio/shlink-web-client/pull/648) Migrated some scripts to ESM and updated to chalk 5.
* [#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*
@@ -18,30 +25,186 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
* *Nothing*
### Fixed
* [#653](https://github.com/shlinkio/shlink-web-client/pull/653) Fixed rendering values greater than 1000 in charts, when the browser has certain locales configured.
* [#799](https://github.com/shlinkio/shlink-web-client/issues/799) Fix fallback visits not taking into account configuration regarding excluding bots.
## [3.7.0] - 2022-05-14
## [3.9.1] - 2022-12-31
### Added
* [#622](https://github.com/shlinkio/shlink-web-client/pull/622) Added support to load domain visits when consuming Shlink 3.1.0 or newer.
* [#582](https://github.com/shlinkio/shlink-web-client/pull/582) Improved filtering short URLs by tag.
Now, a new full tags selector component is available, which allows selecting any of the existing tags and also composes a toggle to filter by "any" tag or "all" tags.
* *Nothing*
### Changed
* [#616](https://github.com/shlinkio/shlink-web-client/pull/616) Updated to React 18.
* [#595](https://github.com/shlinkio/shlink-web-client/pull/595) Updated to react-chartjs-2 v4.1.0.
* [#594](https://github.com/shlinkio/shlink-web-client/pull/594) Updated to a new coding standard.
* [#627](https://github.com/shlinkio/shlink-web-client/pull/627) Updated to Jest 28.
* [#603](https://github.com/shlinkio/shlink-web-client/pull/603) Migrated to new and maintained dependencies to parse CSV<->JSON.
* [#610](https://github.com/shlinkio/shlink-web-client/pull/610) Migrated to a maintained coding style for CSS.
* [#619](https://github.com/shlinkio/shlink-web-client/pull/619) Introduced react testing library, to progressively replace enzyme.
* *Nothing*
### Deprecated
* *Nothing*
### Removed
* [#623](https://github.com/shlinkio/shlink-web-client/pull/623) Dropped support for Shlink older than 2.6.0.
* *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*
@@ -49,19 +212,19 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
## [3.6.0] - 2022-03-17
### Added
* [#558](https://github.com/shlinkio/shlink-web-client/pull/558) Added dark text for tags where the generated background is too light, improving its legibility.
* [#570](https://github.com/shlinkio/shlink-web-client/pull/570) Added new section to load non-orphan visits all together when consuming Shlink 3.0.0.
* [#556](https://github.com/shlinkio/shlink-web-client/pull/556) Added support to filter short URLs list by "all" tags when consuming Shlink 3.0.0.
* [#549](https://github.com/shlinkio/shlink-web-client/pull/549) Allowed to export the list of short URLs as CSV.
* [#558](https://github.com/shlinkio/shlink-web-client/issues/558) Added dark text for tags where the generated background is too light, improving its legibility.
* [#570](https://github.com/shlinkio/shlink-web-client/issues/570) Added new section to load non-orphan visits all together when consuming Shlink 3.0.0.
* [#556](https://github.com/shlinkio/shlink-web-client/issues/556) Added support to filter short URLs list by "all" tags when consuming Shlink 3.0.0.
* [#549](https://github.com/shlinkio/shlink-web-client/issues/549) Allowed to export the list of short URLs as CSV.
### Changed
* [#543](https://github.com/shlinkio/shlink-web-client/pull/543) Redesigned settings section.
* [#567](https://github.com/shlinkio/shlink-web-client/pull/567) Improved Shlink 3.0.0 compatibility by checking the `INVALID_SHORT_URL_DELETION` error code when deleting short URLs.
* [#448](https://github.com/shlinkio/shlink-web-client/pull/448) Updated to bootstrap v5.
* [#524](https://github.com/shlinkio/shlink-web-client/pull/524) Updated to react-router v6.
* [#576](https://github.com/shlinkio/shlink-web-client/pull/576) Updated to fontawesome v6.
* [#579](https://github.com/shlinkio/shlink-web-client/pull/579) Replaced react-color with react-colorful.
* [#564](https://github.com/shlinkio/shlink-web-client/pull/564) Updated most of the dependencies.
* [#543](https://github.com/shlinkio/shlink-web-client/issues/543) Redesigned settings section.
* [#567](https://github.com/shlinkio/shlink-web-client/issues/567) Improved Shlink 3.0.0 compatibility by checking the `INVALID_SHORT_URL_DELETION` error code when deleting short URLs.
* [#448](https://github.com/shlinkio/shlink-web-client/issues/448) Updated to bootstrap v5.
* [#524](https://github.com/shlinkio/shlink-web-client/issues/524) Updated to react-router v6.
* [#576](https://github.com/shlinkio/shlink-web-client/issues/576) Updated to fontawesome v6.
* [#579](https://github.com/shlinkio/shlink-web-client/issues/579) Replaced react-color with react-colorful.
* [#564](https://github.com/shlinkio/shlink-web-client/issues/564) Updated most of the dependencies.
### Deprecated
* *Nothing*
@@ -70,7 +233,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
* *Nothing*
### Fixed
* [#589](https://github.com/shlinkio/shlink-web-client/pull/589) Fixed alignment of shlink versions footer, by basing the logic on the presence of the sidebar instead of selected server.
* [#589](https://github.com/shlinkio/shlink-web-client/issues/589) Fixed alignment of shlink versions footer, by basing the logic on the presence of the sidebar instead of selected server.
## [3.5.1] - 2022-01-08
@@ -94,27 +257,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.15-alpine as node
FROM node:18.12-alpine as node
COPY . /shlink-web-client
ARG VERSION="latest"
ENV VERSION ${VERSION}
RUN cd /shlink-web-client && npm ci && NODE_ENV=production npm run build
RUN cd /shlink-web-client && npm ci --force && 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,11 +1,9 @@
module.exports = {
presets: [
[
'react-app',
{
runtime: 'automatic',
typescript: true,
},
],
['@babel/preset-env', {
targets: { esmodules: true }
}],
['@babel/preset-react', { runtime: 'automatic' }],
'@babel/preset-typescript',
],
};

View File

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

View File

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

View File

@@ -1,7 +1,12 @@
import '@testing-library/jest-dom';
import 'jest-canvas-mock';
import 'chart.js/auto';
import ResizeObserver from 'resize-observer-polyfill';
import { setAutoFreeze } from 'immer';
(global as any).ResizeObserver = ResizeObserver;
(global as any).scrollTo = () => {};
(global as any).prompt = () => {};
(global as any).matchMedia = (media: string) => ({ matches: false, media });
setAutoFreeze(false); // TODO Bypassing a bug on jest

View File

@@ -3,11 +3,11 @@ version: '3'
services:
shlink_web_client_node:
container_name: shlink_web_client_node
image: node:16.15-alpine
command: /bin/sh -c "cd /home/shlink/www && npm install && npm run start"
image: node:18.12-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

@@ -10,12 +10,11 @@ module.exports = {
coverageThreshold: {
global: {
statements: 90,
branches: 80,
functions: 85,
branches: 85,
functions: 90,
lines: 90,
},
},
setupFiles: ['<rootDir>/config/jest/setupBeforeEnzyme.js', '<rootDir>/config/jest/setupEnzyme.js'],
setupFilesAfterEnv: ['<rootDir>/config/jest/setupTests.ts'],
testMatch: ['<rootDir>/test/**/*.test.{ts,tsx}'],
testEnvironment: 'jsdom',
@@ -28,7 +27,6 @@ module.exports = {
'^(?!.*\\.(ts|tsx|js|json|scss)$)': '<rootDir>/config/jest/fileTransform.js',
},
transformIgnorePatterns: [
'<rootDir>/.stryker-tmp',
'node_modules\/(?!(\@react-leaflet|react-leaflet|leaflet|react-chartjs-2|react-colorful)\/)',
'^.+\\.module\\.scss$',
],

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',
},
],
};

36597
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -12,101 +12,99 @@
"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": "DISABLE_ESLINT_PLUGIN=true react-scripts start",
"build": "DISABLE_ESLINT_PLUGIN=true react-scripts build && node scripts/replace-version.mjs",
"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",
"build:serve": "serve -p 5000 ./build",
"test": "jest --env=jsdom --colors --verbose",
"test": "jest --env=jsdom --colors",
"test:coverage": "npm run test -- --coverage --coverageReporters=text --coverageReporters=text-summary",
"test:ci": "npm run test:coverage -- --coverageReporters=clover --ci",
"test:pretty": "npm run test:coverage -- --coverageReporters=html",
"mutate": "./node_modules/.bin/stryker run --concurrency 4 --ignoreStatic"
"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",
"@babel/preset-env": "^7.20.2",
"@babel/preset-react": "^7.18.6",
"@babel/preset-typescript": "^7.21.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",
"chart.js": "^4.1.1",
"classnames": "^2.3.2",
"compare-versions": "^5.0.3",
"csvtojson": "^2.0.10",
"date-fns": "^2.28.0",
"event-source-polyfill": "^1.0.25",
"json2csv": "^5.0.7",
"leaflet": "^1.7.1",
"qs": "^6.9.6",
"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": "^18.1.0",
"react-chartjs-2": "^4.1.0",
"react-colorful": "^5.5.1",
"react-copy-to-clipboard": "^5.0.4",
"react-datepicker": "^4.7.0",
"react-dom": "^18.1.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.0.0",
"react-leaflet": "^4.0.0",
"react-redux": "^8.0.0",
"react-router-dom": "^6.3.0",
"react-swipeable": "^6.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",
"reactstrap": "^9.1.5",
"redux": "^4.2.0",
"redux-localstorage-simple": "^2.4.1",
"redux-thunk": "^2.4.1",
"stream": "^0.0.2",
"redux-localstorage-simple": "^2.5.1",
"redux-thunk": "^2.4.2",
"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": {
"@shlinkio/eslint-config-js-coding-standard": "~2.0.2",
"@shlinkio/eslint-config-js-coding-standard": "~2.1.0",
"@shlinkio/stylelint-config-css-coding-standard": "~1.0.1",
"@stryker-mutator/core": "^6.0.2",
"@stryker-mutator/jest-runner": "^6.0.2",
"@stryker-mutator/typescript-checker": "^6.0.2",
"@testing-library/jest-dom": "^5.16.4",
"@testing-library/react": "^13.1.1",
"@testing-library/user-event": "^14.1.1",
"@types/enzyme": "^3.10.11",
"@types/jest": "^27.4.1",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^14.4.3",
"@types/jest": "^29.2.4",
"@types/json2csv": "^5.0.3",
"@types/leaflet": "^1.7.9",
"@types/leaflet": "^1.9.0",
"@types/qs": "^6.9.7",
"@types/ramda": "0.27.38",
"@types/react": "^18.0.8",
"@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": "^18.0.3",
"@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",
"babel-jest": "^28.0.3",
"chalk": "^5.0.1",
"enzyme": "^3.11.0",
"eslint": "^8.12.0",
"@vitejs/plugin-react": "^3.1.0",
"adm-zip": "^0.5.10",
"babel-jest": "^29.5.0",
"chalk": "^5.2.0",
"eslint": "^8.30.0",
"identity-obj-proxy": "^3.0.0",
"jest": "^28.0.3",
"jest": "^29.3.1",
"jest-canvas-mock": "^2.4.0",
"jest-environment-jsdom": "^28.0.2",
"react-scripts": "^5.0.1",
"jest-environment-jsdom": "^29.3.1",
"resize-observer-polyfill": "^1.5.1",
"sass": "^1.49.9",
"serve": "^13.0.2",
"stryker-cli": "^1.0.2",
"stylelint": "^14.8.2",
"sass": "^1.57.1",
"stylelint": "^14.16.0",
"ts-mockery": "^1.2.0",
"typescript": "^4.6.2",
"webpack": "^5.70.0"
"typescript": "^5.0.2",
"vite": "^4.2.0",
"vite-plugin-pwa": "^0.14.4"
},
"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,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 @@
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,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,4 +8,10 @@ declare module 'event-source-polyfill' {
}
}
declare module '@json2csv/plainjs' {
export class Parser {
parse: <T>(data: T[]) => string;
}
}
declare module '*.png'

View File

@@ -1,4 +1,4 @@
import { ProblemDetailsError } from './types';
import type { ProblemDetailsError } from './types/errors';
import { isInvalidArgumentError } from './utils';
export interface ShlinkApiErrorProps {

View File

@@ -1,128 +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(({ data }) => data.visits);
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(() => {});
this.performEmptyRequest(`/short-urls/${shortCode}`, 'DELETE', { domain });
public readonly updateShortUrl = async (
shortCode: string,
domain: OptionalString,
edit: ShlinkShortUrlData,
): Promise<ShortUrl> =>
this.performRequest<ShortUrl>(`/short-urls/${shortCode}`, 'PATCH', { domain }, edit).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,22 +34,38 @@ 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;
/** @deprecated */
orphanVisitsCount: number;
}
@@ -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 {

View File

@@ -1,16 +1,14 @@
import { Action } from 'redux';
import { buildActionCreator, buildReducer } from '../../utils/helpers/redux';
import { createSlice } from '@reduxjs/toolkit';
export const APP_UPDATE_AVAILABLE = 'shlink/appUpdates/APP_UPDATE_AVAILABLE';
export const RESET_APP_UPDATE = 'shlink/appUpdates/RESET_APP_UPDATE';
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 { appUpdateAvailable, resetAppUpdate } from '../reducers/appUpdates';
import type Bottle from 'bottlejs';
import type { ConnectDecorator } from '../../container/types';
import { App } from '../App';
import { ConnectDecorator } from '../../container/types';
import { appUpdateAvailable, resetAppUpdate } from '../reducers/appUpdates';
const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
export const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
// Components
bottle.serviceFactory(
'App',
@@ -23,5 +23,3 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
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 {

View File

@@ -1,18 +1,19 @@
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 {
@@ -22,6 +23,7 @@ export interface AsideMenuProps {
interface AsideMenuItemProps extends NavLinkProps {
to: string;
className?: string;
}
const AsideMenuItem: FC<AsideMenuItemProps> = ({ children, to, className, ...rest }) => (
@@ -34,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,
});
@@ -68,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>
@@ -89,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';

View File

@@ -1,12 +1,12 @@
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';

View File

@@ -1,9 +1,10 @@
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';

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, supportsDomainVisits, supportsNonOrphanVisits } 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 { 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 { AsideMenuProps } from './AsideMenu';
import './MenuLayout.scss';
interface MenuLayoutProps {
@@ -38,7 +39,6 @@ export const MenuLayout = (
useEffect(() => hideSidebar(), [location]);
useEffect(() => {
showContent && sidebarPresent();
return () => sidebarNotPresent();
}, []);
@@ -46,9 +46,8 @@ export const MenuLayout = (
return <ServerError />;
}
const addNonOrphanVisitsRoute = supportsNonOrphanVisits(selectedServer);
const addManageDomainsRoute = supportsDomainRedirects(selectedServer);
const addDomainVisitsRoute = supportsDomainVisits(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);
@@ -73,7 +72,7 @@ export const MenuLayout = (
<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>}

View File

@@ -1,4 +1,4 @@
import { FC, PropsWithChildren } from 'react';
import type { FC, PropsWithChildren } from 'react';
import './NoMenuLayout.scss';
export const NoMenuLayout: FC<PropsWithChildren<unknown>> = ({ children }) => (

View File

@@ -1,4 +1,4 @@
import { FC, PropsWithChildren } from 'react';
import type { FC, PropsWithChildren } from 'react';
import { Link } from 'react-router-dom';
import { SimpleCard } from '../utils/SimpleCard';

View File

@@ -1,4 +1,5 @@
import { FC, PropsWithChildren, useEffect } from 'react';
import type { FC, PropsWithChildren } from 'react';
import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';
export const ScrollToTop: FC<PropsWithChildren<unknown>> = ({ children }) => {

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

View File

@@ -3,7 +3,7 @@
.react-tags {
position: relative;
padding: 5px 0 0 6px;
border-radius: .3rem;
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;

View File

@@ -1,25 +1,22 @@
import { Action } from 'redux';
import { buildActionCreator, buildReducer } from '../../utils/helpers/redux';
export const SIDEBAR_PRESENT = 'shlink/common/SIDEBAR_PRESENT';
export const SIDEBAR_NOT_PRESENT = 'shlink/common/SIDEBAR_NOT_PRESENT';
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,7 +1,7 @@
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 { JsonToCsv } from '../../utils/helpers/csvjson';
import type { NormalizedVisit } from '../../visits/types';
export class ReportExporter {
public constructor(private readonly window: Window, private readonly jsonToCsv: JsonToCsv) {}

View File

@@ -1,25 +1,26 @@
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('HttpClient', HttpClient, 'fetch');
bottle.service('ImageDownloader', ImageDownloader, 'HttpClient', 'window');
bottle.service('ReportExporter', ReportExporter, 'window', 'jsonToCsv');
// Components
@@ -61,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>;

View File

@@ -1,14 +1,12 @@
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';
import type { ShlinkState } from './types';
const isProduction = process.env.NODE_ENV === 'production';
// eslint-disable-next-line no-mixed-operators
const composeEnhancers: Function = !isProduction && (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const localStorageConfig: RLSOptions = {
states: ['settings', 'servers'],
namespace: 'shlink',
@@ -17,6 +15,12 @@ const localStorageConfig: RLSOptions = {
};
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,27 +1,27 @@
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 { DomainVisits } from '../visits/reducers/domainVisits';
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;

View File

@@ -1,18 +1,20 @@
import { FC, useEffect } from 'react';
import { UncontrolledTooltip } from 'reactstrap';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faDotCircle as defaultDomainIcon } from '@fortawesome/free-solid-svg-icons';
import { ShlinkDomainRedirects } from '../api/types';
import { OptionalString } from '../utils/utils';
import { SelectedServer } from '../servers/data';
import { Domain } from './data';
import { DomainStatusIcon } from './helpers/DomainStatusIcon';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
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;
}

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

View File

@@ -1,18 +1,19 @@
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 { SimpleCard } from '../utils/SimpleCard';
import type { SelectedServer } from '../servers/data';
import { Message } from '../utils/Message';
import { Result } from '../utils/Result';
import { SearchField } from '../utils/SearchField';
import { ShlinkDomainRedirects } from '../api/types';
import { SelectedServer } from '../servers/data';
import { DomainsList } from './reducers/domainsList';
import { SimpleCard } from '../utils/SimpleCard';
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;

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

@@ -1,20 +1,21 @@
import { FC } from 'react';
import { DropdownItem } from 'reactstrap';
import { Link } from 'react-router-dom';
import { faChartPie as pieChartIcon, faEdit as editIcon } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { useToggle } from '../../utils/helpers/hooks';
import 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 { DropdownBtnMenu } from '../../utils/DropdownBtnMenu';
import { EditDomainRedirectsModal } from './EditDomainRedirectsModal';
import { Domain } from '../data';
import { ShlinkDomainRedirects } from '../../api/types';
import { supportsDefaultDomainRedirectsEdition, supportsDomainVisits } from '../../utils/helpers/features';
import { getServerId, SelectedServer } from '../../servers/data';
import { useFeature } from '../../utils/helpers/features';
import { useToggle } from '../../utils/helpers/hooks';
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: (domain: string, redirects: Partial<ShlinkDomainRedirects>) => Promise<void>;
editDomainRedirects: (redirects: EditDomainRedirects) => Promise<void>;
selectedServer: SelectedServer;
}
@@ -22,8 +23,8 @@ export const DomainDropdown: FC<DomainDropdownProps> = ({ domain, editDomainRedi
const [isOpen, toggle] = useToggle();
const [isModalOpen, toggleModal] = useToggle();
const { isDefault } = domain;
const canBeEdited = !isDefault || supportsDefaultDomainRedirectsEdition(selectedServer);
const withVisits = supportsDomainVisits(selectedServer);
const canBeEdited = !isDefault || useFeature('defaultDomainRedirectsEdition', selectedServer);
const withVisits = useFeature('domainVisits', selectedServer);
const serverId = getServerId(selectedServer);
return (

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,7 +18,7 @@ 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());
@@ -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 }) => (
@@ -30,10 +33,13 @@ export const EditDomainRedirectsModal: FC<EditDomainRedirectsModalProps> = (
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 (

View File

@@ -1,31 +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';
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';
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: shlinkEditDomainRedirects } = 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 shlinkEditDomainRedirects({ 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,20 +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';
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';
const REDUCER_PREFIX = 'shlink/domainsList';
export interface DomainsList {
domains: Domain[];
@@ -25,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;
}
@@ -46,83 +38,89 @@ const initialState: DomainsList = {
error: false,
};
export type DomainsCombinedAction = ListDomainsAction
& ApiErrorAction
& FilterDomainsAction
& EditDomainRedirectsAction
& ValidateDomain;
export const replaceRedirectsOnDomain = (domain: string, redirects: ShlinkDomainRedirects) =>
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 });
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: shlinkListDomains } = 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 resp = await shlinkListDomains().then(({ data, defaultRedirects }) => ({
return {
domains: data.map((domain): Domain => ({ ...domain, status: 'validating' })),
defaultRedirects,
}));
};
});
dispatch<ListDomainsAction>({ type: LIST_DOMAINS, ...resp });
} 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,11 +1,12 @@
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']));
@@ -16,11 +17,18 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
['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 {
@@ -217,9 +223,7 @@ hr {
}
.text-ellipsis {
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
@include text-ellipsis();
}
.progress-bar {

View File

@@ -1,19 +1,20 @@
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 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 'chart.js/auto'; // TODO Import specific ones to reduce bundle size https://react-chartjs-2.js.org/docs/migration-to-v4/#tree-shaking
import 'react-datepicker/dist/react-datepicker.css';
import 'leaflet/dist/leaflet.css';
import { 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;
createRoot(document.getElementById('root')!).render( // eslint-disable-line @typescript-eslint/no-non-null-assertion

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 {

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,52 +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';
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';
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,47 +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 domainVisitsReducer from '../visits/reducers/domainVisits';
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,
domainVisits: domainVisitsReducer,
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,14 +28,14 @@ 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 [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 = () => {
@@ -43,7 +45,7 @@ const CreateServer = (ImportServersBtn: FC<ImportServersBtnProps>, useStateFlagT
const id = uuid();
createServer({ ...serverData, id });
createServers([{ ...serverData, id }]);
navigate(`/server/${id}`);
};
@@ -77,5 +79,3 @@ const CreateServer = (ImportServersBtn: FC<ImportServersBtnProps>, useStateFlagT
</NoMenuLayout>
);
};
export default CreateServer;

View File

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

View File

@@ -1,7 +1,8 @@
import { FC } from 'react';
import { Button, 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>
@@ -38,10 +47,8 @@ const DeleteServerModal: FC<DeleteServerModalConnectProps> = (
</ModalBody>
<ModalFooter>
<Button color="link" onClick={toggle}>Cancel</Button>
<Button color="danger" onClick={() => closeModal()}>Delete</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,16 +23,16 @@ const SHOW_IMPORT_MSG_TIME = 4000;
export const ManageServers = (
serversExporter: ServersExporter,
ImportServersBtn: FC<ImportServersBtnProps>,
useStateFlagTimeout: StateFlagTimeout,
useTimeoutToggle: TimeoutToggle,
ManageServersRow: FC<ManageServersRowProps>,
): FC<ManageServersProps> => ({ servers }) => {
const allServers = Object.values(servers);
const [serversList, setServersList] = useState(allServers);
const filterServers = (searchTerm: string) => setServersList(
allServers.filter(({ name, url }) => `${name} ${url}`.match(searchTerm)),
allServers.filter(({ name, url }) => `${name} ${url}`.toLowerCase().match(searchTerm.toLowerCase())),
);
const hasAutoConnect = serversList.some(({ autoConnect }) => !!autoConnect);
const [errorImporting, setErrorImporting] = useStateFlagTimeout(false, SHOW_IMPORT_MSG_TIME);
const [errorImporting, setErrorImporting] = useTimeoutToggle(false, SHOW_IMPORT_MSG_TIME);
useEffect(() => {
setServersList(Object.values(servers));

View File

@@ -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 { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import type { FC } from 'react';
import { Link } from 'react-router-dom';
import { DropdownItem } from 'reactstrap';
import { DropdownBtnMenu } from '../utils/DropdownBtnMenu';
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 ManageServersRowDropdownProps {
server: ServerWithId;
@@ -39,7 +39,7 @@ export const ManageServersRowDropdown = (
<DropdownItem tag={Link} to={`${serverUrl}/edit`}>
<FontAwesomeIcon icon={editIcon} fixedWidth /> Edit server
</DropdownItem>
<DropdownItem onClick={() => setAutoConnect(server, !server.autoConnect)}>
<DropdownItem onClick={() => setAutoConnect(server, !isAutoConnect)}>
<FontAwesomeIcon icon={autoConnectIcon} fixedWidth /> {isAutoConnect ? 'Do not a' : 'A'}uto-connect
</DropdownItem>
<DropdownItem divider />

View File

@@ -1,18 +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 { Topics } from '../mercure/helpers/Topics';
import { ShlinkShortUrlsListParams } from '../api/types';
import { supportsNonOrphanVisits } 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;
@@ -22,10 +27,11 @@ interface OverviewConnectProps {
selectedServer: SelectedServer;
visitsOverview: VisitsOverview;
loadVisitsOverview: Function;
settings: Settings;
}
export const Overview = (
ShortUrlsTable: FC<ShortUrlsTableProps>,
ShortUrlsTable: ShortUrlsTableType,
CreateShortUrl: FC<CreateShortUrlProps>,
) => boundToMercureHub(({
shortUrlsList,
@@ -35,12 +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 linkToNonOrphanVisits = supportsNonOrphanVisits(selectedServer);
const linkToNonOrphanVisits = useFeature('nonOrphanVisits', selectedServer);
const navigate = useNavigate();
useEffect(() => {
@@ -53,14 +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={`/server/${serverId}/orphan-visits`}>
{loadingVisits ? 'Loading...' : prettify(orphanVisitsCount)}
</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`}>

View File

@@ -1,9 +1,10 @@
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;

View File

@@ -1,10 +1,10 @@
import { FC, PropsWithChildren } 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';
type ServersListGroupProps = PropsWithChildren<{
@@ -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;

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[];

View File

@@ -1,21 +1,30 @@
import { FC, PropsWithChildren } from 'react';
import { Card, CardText, CardTitle } from 'reactstrap';
import { Link } from 'react-router-dom';
import { faArrowAltCircleRight as linkIcon } from '@fortawesome/free-regular-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import 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 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>}
</>
);
};

View File

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

View File

@@ -1,10 +1,11 @@
import { FC } from 'react';
import type { FC } from 'react';
import { Link } from 'react-router-dom';
import Message from '../../utils/Message';
import ServersListGroup from '../ServersListGroup';
import { DeleteServerButtonProps } from '../DeleteServerButton';
import { isServerWithId, SelectedServer, ServersMap } from '../data';
import { NoMenuLayout } from '../../common/NoMenuLayout';
import { Message } from '../../utils/Message';
import type { SelectedServer, ServersMap } from '../data';
import { isServerWithId } from '../data';
import type { DeleteServerButtonProps } from '../DeleteServerButton';
import { ServersListGroup } from '../ServersListGroup';
import './ServerError.scss';
interface ServerErrorProps {
@@ -37,7 +38,7 @@ export const ServerError = (DeleteServerButton: FC<DeleteServerButtonProps>): FC
<h5>
Alternatively, if you think you may have miss-configured this server, you
can <DeleteServerButton server={selectedServer} className="server-error__delete-btn">remove it</DeleteServerButton> or&nbsp;
<Link to={`/server/${selectedServer.id}/edit`}>edit it</Link>.
<Link to={`/server/${selectedServer.id}/edit?reconnect=true`}>edit it</Link>.
</h5>
</div>
)}

View File

@@ -1,8 +1,9 @@
import { FC, PropsWithChildren, ReactNode, useEffect, useState } from 'react';
import type { FC, PropsWithChildren, ReactNode } from 'react';
import { useEffect, useState } from 'react';
import { InputFormGroup } from '../../utils/forms/InputFormGroup';
import { handleEventPreventingDefault } from '../../utils/utils';
import { ServerData } from '../data';
import { SimpleCard } from '../../utils/SimpleCard';
import { handleEventPreventingDefault } from '../../utils/utils';
import type { ServerData } from '../data';
type ServerFormProps = PropsWithChildren<{
onSubmit: (server: ServerData) => void;

View File

@@ -0,0 +1,26 @@
import type { FC } from 'react';
import { prettify } from '../../utils/helpers/numbers';
import type { PartialVisitsSummary } from '../../visits/reducers/visitsOverview';
import type { HighlightCardProps } from './HighlightCard';
import { HighlightCard } from './HighlightCard';
export type VisitsHighlightCardProps = Omit<HighlightCardProps, 'tooltip' | 'children'> & {
loading: boolean;
excludeBots: boolean;
visitsSummary: PartialVisitsSummary;
};
export const VisitsHighlightCard: FC<VisitsHighlightCardProps> = ({ loading, excludeBots, visitsSummary, ...rest }) => (
<HighlightCard
tooltip={
visitsSummary.bots !== undefined
? <>{excludeBots ? 'Plus' : 'Including'} <b>{prettify(visitsSummary.bots)}</b> potential bot visits</>
: undefined
}
{...rest}
>
{loading ? 'Loading...' : prettify(
excludeBots && visitsSummary.nonBots ? visitsSummary.nonBots : visitsSummary.total,
)}
</HighlightCard>
);

View File

@@ -1,8 +1,10 @@
import { FC, useEffect } from 'react';
import type { FC } from 'react';
import { useEffect } from 'react';
import { useParams } from 'react-router-dom';
import Message from '../../utils/Message';
import { isNotFoundServer, SelectedServer } from '../data';
import { NoMenuLayout } from '../../common/NoMenuLayout';
import { Message } from '../../utils/Message';
import type { SelectedServer } from '../data';
import { isNotFoundServer } from '../data';
interface WithSelectedServerProps {
selectServer: (serverId: string) => void;

View File

@@ -1,4 +1,5 @@
import { FC, useEffect } from 'react';
import type { FC } from 'react';
import { useEffect } from 'react';
interface WithoutSelectedServerProps {
resetSelectedServer: Function;

View File

@@ -1,18 +1,18 @@
import { pipe, prop } from 'ramda';
import { AxiosInstance } from 'axios';
import { Dispatch } from 'redux';
import pack from '../../../package.json';
import { hasServerData, ServerData } from '../data';
import type { HttpClient } from '../../common/services/HttpClient';
import { createAsyncThunk } from '../../utils/helpers/redux';
import type { ServerData } from '../data';
import { hasServerData } from '../data';
import { createServers } from './servers';
const responseToServersList = pipe(
prop<any, any>('data'),
(data: any): ServerData[] => (Array.isArray(data) ? data.filter(hasServerData) : []),
const responseToServersList = (data: any): ServerData[] => (Array.isArray(data) ? data.filter(hasServerData) : []);
export const fetchServers = (httpClient: HttpClient) => createAsyncThunk(
'shlink/remoteServers/fetchServers',
async (_: void, { dispatch }): Promise<void> => {
const resp = await httpClient.fetchJson<any>(`${pack.homepage}/servers.json`);
const result = responseToServersList(resp);
dispatch(createServers(result));
},
);
export const fetchServers = ({ get }: AxiosInstance) => () => async (dispatch: Dispatch) => {
const resp = await get(`${pack.homepage}/servers.json`);
const remoteList = responseToServersList(resp);
dispatch(createServers(remoteList));
};

View File

@@ -1,31 +1,27 @@
import { identity, memoizeWith, pipe } from 'ramda';
import { Action, Dispatch } from 'redux';
import type { PayloadAction } from '@reduxjs/toolkit';
import { createAction, createListenerMiddleware, createSlice } from '@reduxjs/toolkit';
import { memoizeWith, pipe } from 'ramda';
import type { ShlinkApiClientBuilder } from '../../api/services/ShlinkApiClientBuilder';
import type { ShlinkHealth } from '../../api/types';
import { createAsyncThunk } from '../../utils/helpers/redux';
import { versionToPrintable, versionToSemVer as toSemVer } from '../../utils/helpers/version';
import { SelectedServer } from '../data';
import { GetState } from '../../container/types';
import { ShlinkHealth } from '../../api/types';
import { buildActionCreator, buildReducer } from '../../utils/helpers/redux';
import { ShlinkApiClientBuilder } from '../../api/services/ShlinkApiClientBuilder';
import type { SelectedServer, ServerWithId } from '../data';
import { isReachableServer } from '../data';
export const SELECT_SERVER = 'shlink/selectedServer/SELECT_SERVER';
export const RESET_SELECTED_SERVER = 'shlink/selectedServer/RESET_SELECTED_SERVER';
const REDUCER_PREFIX = 'shlink/selectedServer';
export const MIN_FALLBACK_VERSION = '1.0.0';
export const MAX_FALLBACK_VERSION = '999.999.999';
export const LATEST_VERSION_CONSTRAINT = 'latest';
export interface SelectServerAction extends Action<string> {
selectedServer: SelectedServer;
}
const versionToSemVer = pipe(
(version: string) => (version === LATEST_VERSION_CONSTRAINT ? MAX_FALLBACK_VERSION : version),
toSemVer(MIN_FALLBACK_VERSION),
);
const getServerVersion = memoizeWith(
identity,
async (_serverId: string, health: () => Promise<ShlinkHealth>) => health().then(({ version }) => ({
(server: ServerWithId) => `${server.id}_${server.url}_${server.apiKey}`,
async (_server: ServerWithId, health: () => Promise<ShlinkHealth>) => health().then(({ version }) => ({
version: versionToSemVer(version),
printableVersion: versionToPrintable(version),
})),
@@ -33,53 +29,59 @@ const getServerVersion = memoizeWith(
const initialState: SelectedServer = null;
export default buildReducer<SelectedServer, SelectServerAction>({
[RESET_SELECTED_SERVER]: () => initialState,
[SELECT_SERVER]: (_, { selectedServer }) => selectedServer,
}, initialState);
export const resetSelectedServer = createAction<void>(`${REDUCER_PREFIX}/resetSelectedServer`);
export const resetSelectedServer = buildActionCreator(RESET_SELECTED_SERVER);
export const selectServer = (buildShlinkApiClient: ShlinkApiClientBuilder) => createAsyncThunk(
`${REDUCER_PREFIX}/selectServer`,
async (serverId: string, { dispatch, getState }): Promise<SelectedServer> => {
dispatch(resetSelectedServer());
export const selectServer = (
buildShlinkApiClient: ShlinkApiClientBuilder,
loadMercureInfo: () => Action,
) => (
serverId: string,
) => async (
dispatch: Dispatch,
getState: GetState,
) => {
dispatch(resetSelectedServer());
const { servers } = getState();
const selectedServer = servers[serverId];
const { servers } = getState();
const selectedServer = servers[serverId];
if (!selectedServer) {
return { serverNotFound: true };
}
if (!selectedServer) {
dispatch<SelectServerAction>({
type: SELECT_SERVER,
selectedServer: { serverNotFound: true },
});
try {
const { health } = buildShlinkApiClient(selectedServer);
const { version, printableVersion } = await getServerVersion(selectedServer, health);
return;
}
try {
const { health } = buildShlinkApiClient(selectedServer);
const { version, printableVersion } = await getServerVersion(serverId, health);
dispatch<SelectServerAction>({
type: SELECT_SERVER,
selectedServer: {
return {
...selectedServer,
version,
printableVersion,
},
});
dispatch(loadMercureInfo());
} catch (e) {
dispatch<SelectServerAction>({
type: SELECT_SERVER,
selectedServer: { ...selectedServer, serverNotReachable: true },
});
}
};
} catch (e) {
return { ...selectedServer, serverNotReachable: true };
}
},
);
type SelectServerThunk = ReturnType<typeof selectServer>;
export const selectServerListener = (
selectServerThunk: SelectServerThunk,
loadMercureInfo: () => PayloadAction<any>, // TODO Consider setting actual type, if relevant
) => {
const listener = createListenerMiddleware();
listener.startListening({
actionCreator: selectServerThunk.fulfilled,
effect: ({ payload }, { dispatch }) => {
isReachableServer(payload) && dispatch(loadMercureInfo());
},
});
return listener;
};
export const selectedServerReducerCreator = (selectServerThunk: SelectServerThunk) => createSlice({
name: REDUCER_PREFIX,
initialState,
reducers: {},
extraReducers: (builder) => {
builder.addCase(resetSelectedServer, () => initialState);
builder.addCase(selectServerThunk.fulfilled, (_, { payload }) => payload as any);
},
});

View File

@@ -1,23 +1,15 @@
import type { PayloadAction } from '@reduxjs/toolkit';
import { createSlice } from '@reduxjs/toolkit';
import { assoc, dissoc, fromPairs, map, pipe, reduce, toPairs } from 'ramda';
import { v4 as uuid } from 'uuid';
import { Action } from 'redux';
import { ServerData, ServersMap, ServerWithId } from '../data';
import { buildReducer } from '../../utils/helpers/redux';
import type { ServerData, ServersMap, ServerWithId } from '../data';
export const EDIT_SERVER = 'shlink/servers/EDIT_SERVER';
export const DELETE_SERVER = 'shlink/servers/DELETE_SERVER';
export const CREATE_SERVERS = 'shlink/servers/CREATE_SERVERS';
export const SET_AUTO_CONNECT = 'shlink/servers/SET_AUTO_CONNECT';
export interface CreateServersAction extends Action<string> {
newServers: ServersMap;
}
interface DeleteServerAction extends Action<string> {
interface EditServer {
serverId: string;
serverData: Partial<ServerData>;
}
interface SetAutoConnectAction extends Action<string> {
interface SetAutoConnect {
serverId: string;
autoConnect: boolean;
}
@@ -32,50 +24,57 @@ const serverWithId = (server: ServerWithId | ServerData): ServerWithId => {
return assoc('id', uuid(), server);
};
export default buildReducer<ServersMap, CreateServersAction & DeleteServerAction & SetAutoConnectAction>({
[CREATE_SERVERS]: (state, { newServers }) => ({ ...state, ...newServers }),
[DELETE_SERVER]: (state, { serverId }) => dissoc(serverId, state),
[EDIT_SERVER]: (state, { serverId, serverData }: any) => (
!state[serverId] ? state : assoc(serverId, { ...state[serverId], ...serverData }, state)
),
[SET_AUTO_CONNECT]: (state, { serverId, autoConnect }) => {
if (!state[serverId]) {
return state;
}
if (!autoConnect) {
return assoc(serverId, { ...state[serverId], autoConnect }, state);
}
return fromPairs(
toPairs(state).map(([evaluatedServerId, server]) => [
evaluatedServerId,
{ ...server, autoConnect: evaluatedServerId === serverId },
]),
);
},
}, initialState);
const serversListToMap = reduce<ServerWithId, ServersMap>((acc, server) => assoc(server.id, server, acc), {});
export const createServers = pipe(
map(serverWithId),
serversListToMap,
(newServers: ServersMap) => ({ type: CREATE_SERVERS, newServers }),
);
export const { actions, reducer } = createSlice({
name: 'shlink/servers',
initialState,
reducers: {
editServer: {
prepare: (serverId: string, serverData: Partial<ServerData>) => ({
payload: { serverId, serverData },
}),
reducer: (state, { payload }: PayloadAction<EditServer>) => {
const { serverId, serverData } = payload;
return (
!state[serverId] ? state : assoc(serverId, { ...state[serverId], ...serverData }, state)
);
},
},
deleteServer: (state, { payload }) => dissoc(payload.id, state),
setAutoConnect: {
prepare: ({ id: serverId }: ServerWithId, autoConnect: boolean) => ({
payload: { serverId, autoConnect },
}),
reducer: (state, { payload }: PayloadAction<SetAutoConnect>) => {
const { serverId, autoConnect } = payload;
if (!state[serverId]) {
return state;
}
export const createServer = (server: ServerWithId) => createServers([server]);
if (!autoConnect) {
return assoc(serverId, { ...state[serverId], autoConnect }, state);
}
export const editServer = (serverId: string, serverData: Partial<ServerData>) => ({
type: EDIT_SERVER,
serverId,
serverData,
return fromPairs(
toPairs(state).map(([evaluatedServerId, server]) => [
evaluatedServerId,
{ ...server, autoConnect: evaluatedServerId === serverId },
]),
);
},
},
createServers: {
prepare: pipe(
map(serverWithId),
serversListToMap,
(payload: ServersMap) => ({ payload }),
),
reducer: (state, { payload: newServers }: PayloadAction<ServersMap>) => ({ ...state, ...newServers }),
},
},
});
export const deleteServer = ({ id }: ServerWithId): DeleteServerAction => ({ type: DELETE_SERVER, serverId: id });
export const { editServer, deleteServer, setAutoConnect, createServers } = actions;
export const setAutoConnect = ({ id }: ServerWithId, autoConnect: boolean): SetAutoConnectAction => ({
type: SET_AUTO_CONNECT,
serverId: id,
autoConnect,
});
export const serversReducer = reducer;

View File

@@ -1,12 +1,13 @@
import { values } from 'ramda';
import LocalStorage from '../../utils/services/LocalStorage';
import { ServersMap, serverWithIdToServerData } from '../data';
import type { JsonToCsv } from '../../utils/helpers/csvjson';
import { saveCsv } from '../../utils/helpers/files';
import { JsonToCsv } from '../../utils/helpers/csvjson';
import type { LocalStorage } from '../../utils/services/LocalStorage';
import type { ServersMap } from '../data';
import { serverWithIdToServerData } from '../data';
const SERVERS_FILENAME = 'shlink-servers.csv';
export default class ServersExporter {
export class ServersExporter {
public constructor(
private readonly storage: LocalStorage,
private readonly window: Window,

View File

@@ -1,5 +1,5 @@
import { ServerData } from '../data';
import { CsvToJson } from '../../utils/helpers/csvjson';
import type { CsvToJson } from '../../utils/helpers/csvjson';
import type { ServerData } from '../data';
const validateServer = (server: any): server is ServerData =>
typeof server.url === 'string' && typeof server.apiKey === 'string' && typeof server.name === 'string';

View File

@@ -1,31 +1,37 @@
import Bottle from 'bottlejs';
import CreateServer from '../CreateServer';
import { ServersDropdown } from '../ServersDropdown';
import DeleteServerModal from '../DeleteServerModal';
import DeleteServerButton from '../DeleteServerButton';
import type Bottle from 'bottlejs';
import { prop } from 'ramda';
import type { ConnectDecorator } from '../../container/types';
import { CreateServer } from '../CreateServer';
import { DeleteServerButton } from '../DeleteServerButton';
import { DeleteServerModal } from '../DeleteServerModal';
import { EditServer } from '../EditServer';
import ImportServersBtn from '../helpers/ImportServersBtn';
import { resetSelectedServer, selectServer } from '../reducers/selectedServer';
import { createServer, createServers, deleteServer, editServer, setAutoConnect } from '../reducers/servers';
import { fetchServers } from '../reducers/remoteServers';
import { ImportServersBtn } from '../helpers/ImportServersBtn';
import { ServerError } from '../helpers/ServerError';
import { ConnectDecorator } from '../../container/types';
import { withoutSelectedServer } from '../helpers/withoutSelectedServer';
import { Overview } from '../Overview';
import { ManageServers } from '../ManageServers';
import { ManageServersRow } from '../ManageServersRow';
import { ManageServersRowDropdown } from '../ManageServersRowDropdown';
import { Overview } from '../Overview';
import { fetchServers } from '../reducers/remoteServers';
import {
resetSelectedServer,
selectedServerReducerCreator,
selectServer,
selectServerListener,
} from '../reducers/selectedServer';
import { createServers, deleteServer, editServer, setAutoConnect } from '../reducers/servers';
import { ServersDropdown } from '../ServersDropdown';
import { ServersExporter } from './ServersExporter';
import { ServersImporter } from './ServersImporter';
import ServersExporter from './ServersExporter';
const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
export const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
// Components
bottle.serviceFactory(
'ManageServers',
ManageServers,
'ServersExporter',
'ImportServersBtn',
'useStateFlagTimeout',
'useTimeoutToggle',
'ManageServersRow',
);
bottle.decorator('ManageServers', withoutSelectedServer);
@@ -36,9 +42,9 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
bottle.serviceFactory('ManageServersRowDropdown', ManageServersRowDropdown, 'DeleteServerModal');
bottle.decorator('ManageServersRowDropdown', connect(null, ['setAutoConnect']));
bottle.serviceFactory('CreateServer', CreateServer, 'ImportServersBtn', 'useStateFlagTimeout');
bottle.serviceFactory('CreateServer', CreateServer, 'ImportServersBtn', 'useTimeoutToggle');
bottle.decorator('CreateServer', withoutSelectedServer);
bottle.decorator('CreateServer', connect(['selectedServer', 'servers'], ['createServer', 'resetSelectedServer']));
bottle.decorator('CreateServer', connect(['selectedServer', 'servers'], ['createServers', 'resetSelectedServer']));
bottle.serviceFactory('EditServer', EditServer, 'ServerError');
bottle.decorator('EditServer', connect(['selectedServer'], ['editServer', 'selectServer', 'resetSelectedServer']));
@@ -59,7 +65,7 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
bottle.serviceFactory('Overview', Overview, 'ShortUrlsTable', 'CreateShortUrl');
bottle.decorator('Overview', connect(
['shortUrlsList', 'tagsList', 'selectedServer', 'mercureInfo', 'visitsOverview'],
['shortUrlsList', 'tagsList', 'selectedServer', 'mercureInfo', 'visitsOverview', 'settings'],
['listShortUrls', 'listTags', 'createNewVisits', 'loadMercureInfo', 'loadVisitsOverview'],
));
@@ -70,14 +76,16 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
// Actions
bottle.serviceFactory('selectServer', selectServer, 'buildShlinkApiClient', 'loadMercureInfo');
bottle.serviceFactory('createServer', () => createServer);
bottle.serviceFactory('createServers', () => createServers);
bottle.serviceFactory('deleteServer', () => deleteServer);
bottle.serviceFactory('editServer', () => editServer);
bottle.serviceFactory('setAutoConnect', () => setAutoConnect);
bottle.serviceFactory('fetchServers', fetchServers, 'axios');
bottle.serviceFactory('fetchServers', fetchServers, 'HttpClient');
bottle.serviceFactory('resetSelectedServer', () => resetSelectedServer);
};
export default provideServices;
// Reducers
bottle.serviceFactory('selectServerListener', selectServerListener, 'selectServer', 'loadMercureInfo');
bottle.serviceFactory('selectedServerReducerCreator', selectedServerReducerCreator, 'selectServer');
bottle.serviceFactory('selectedServerReducer', prop('reducer'), 'selectedServerReducerCreator');
};

View File

@@ -12,12 +12,13 @@ import { ExpirationPlugin } from 'workbox-expiration';
import { precacheAndRoute, createHandlerBoundToURL } from 'workbox-precaching';
import { registerRoute } from 'workbox-routing';
import { StaleWhileRevalidate } from 'workbox-strategies';
import pack from '../package.json';
declare const self: ServiceWorkerGlobalScope;
clientsClaim();
// Precache all of the assets generated by your build process.
// Precache all the assets generated by your build process.
// Their URLs are injected into the manifest variable below.
// This variable must be present somewhere in your service worker file,
// even if you decide not to use precaching. See https://cra.link/PWA
@@ -49,7 +50,7 @@ registerRoute(
// Return true to signal that we want to use the handler.
return true;
},
createHandlerBoundToURL(process.env.PUBLIC_URL + '/index.html')
createHandlerBoundToURL(`${pack.homepage}/index.html`)
);
// An example runtime caching route for requests that aren't handled by the

View File

@@ -9,6 +9,7 @@
// To learn more about the benefits of this model and instructions on how to
// opt-in, read https://cra.link/PWA
import pack from'../package.json';
const isLocalhost = Boolean(
window.location.hostname === 'localhost' ||
@@ -26,7 +27,7 @@ type Config = {
export function register(config?: Config) {
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
// The URL constructor is available in all browsers that support SW.
const publicUrl = new URL(process.env.PUBLIC_URL ?? '', window.location.href);
const publicUrl = new URL(pack.homepage, window.location.href);
if (publicUrl.origin !== window.location.origin) {
// Our service worker won't work if PUBLIC_URL is on a different origin
// from what our page is served on. This might happen if a CDN is used to
@@ -35,7 +36,7 @@ export function register(config?: Config) {
}
window.addEventListener('load', () => {
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
const swUrl = `${pack.homepage}/service-worker.js`;
if (isLocalhost) {
// This is running on localhost. Let's check if a service worker still exists or not.

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