mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2026-02-27 12:16:36 +00:00
Compare commits
152 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a46116d936 | ||
|
|
8fd419dc72 | ||
|
|
18b27dbd0c | ||
|
|
0c17818a24 | ||
|
|
b1749ee2ef | ||
|
|
27b82c56b1 | ||
|
|
f69bda351d | ||
|
|
4a92d0ff11 | ||
|
|
b37a983bde | ||
|
|
97cf3b26b0 | ||
|
|
c490835f9b | ||
|
|
a3ab2c6e1b | ||
|
|
ce5108937d | ||
|
|
9164db181c | ||
|
|
aa14a17ad6 | ||
|
|
d67b8c0530 | ||
|
|
d41c1a2a52 | ||
|
|
3b938251d9 | ||
|
|
b8aa068876 | ||
|
|
5d288de390 | ||
|
|
af851e708b | ||
|
|
72399e7ccd | ||
|
|
1ffd71e81f | ||
|
|
d627de8e83 | ||
|
|
fc4fdb4fc7 | ||
|
|
126537185b | ||
|
|
24de0773d8 | ||
|
|
cc77af6142 | ||
|
|
c16460af82 | ||
|
|
4c7bed90a3 | ||
|
|
491b2f2c07 | ||
|
|
a038f5e618 | ||
|
|
9ba74328ff | ||
|
|
90a643761a | ||
|
|
6236d36372 | ||
|
|
065c908153 | ||
|
|
a14e612a38 | ||
|
|
0b155b1d20 | ||
|
|
9b9cfd0543 | ||
|
|
3d067371d3 | ||
|
|
381eb5a502 | ||
|
|
4a88f30d13 | ||
|
|
bdf181adec | ||
|
|
f97fce873b | ||
|
|
879017ecca | ||
|
|
83150331e5 | ||
|
|
7249ec4968 | ||
|
|
3ac148f7cd | ||
|
|
a73472f7e5 | ||
|
|
08ca59f990 | ||
|
|
d07f7e757e | ||
|
|
cb13e82b9c | ||
|
|
fd7aa570ed | ||
|
|
c00053f6e1 | ||
|
|
2e0e7f361c | ||
|
|
21101d4da8 | ||
|
|
65f739499f | ||
|
|
91ee4a32cd | ||
|
|
498668929f | ||
|
|
935b12763b | ||
|
|
28b15e4a85 | ||
|
|
6af49a9945 | ||
|
|
c80ad70e3b | ||
|
|
1a20065053 | ||
|
|
edef36bae8 | ||
|
|
3cd25dc2df | ||
|
|
43840d7656 | ||
|
|
a1bdb75036 | ||
|
|
ac0107d450 | ||
|
|
d3d2cf72b9 | ||
|
|
eb9ec4ec31 | ||
|
|
3201830b27 | ||
|
|
58ddec6aff | ||
|
|
59fd58b824 | ||
|
|
efa07f0368 | ||
|
|
1dd6a8e2e4 | ||
|
|
bcd3fa8ce4 | ||
|
|
6ff3cf544b | ||
|
|
ab21f923c6 | ||
|
|
b75fd2e03a | ||
|
|
ec7c7d521f | ||
|
|
f9909713d9 | ||
|
|
59087ced8a | ||
|
|
84435714f5 | ||
|
|
6bd628712e | ||
|
|
997f4a6bdc | ||
|
|
b1fec831c5 | ||
|
|
54fe849efd | ||
|
|
8bf1a9d023 | ||
|
|
07cedd0bdb | ||
|
|
44a93ae556 | ||
|
|
72f790b28c | ||
|
|
a63f7e741a | ||
|
|
58f952df8a | ||
|
|
2acd0ec95d | ||
|
|
105254d053 | ||
|
|
e538f2a3bb | ||
|
|
98ea491469 | ||
|
|
10e50efb33 | ||
|
|
d60023f585 | ||
|
|
c0d5feb433 | ||
|
|
2451167296 | ||
|
|
4a70e4ecd3 | ||
|
|
a90c3da7b6 | ||
|
|
53e15b041d | ||
|
|
7669254a0c | ||
|
|
b450e4093e | ||
|
|
a012d6206f | ||
|
|
30f502a51b | ||
|
|
4defeaf017 | ||
|
|
7f35fb0ada | ||
|
|
cd1a926292 | ||
|
|
e46506b264 | ||
|
|
807c5c3fb4 | ||
|
|
64efb1d43d | ||
|
|
49e1f82b03 | ||
|
|
1bd8636c19 | ||
|
|
cfe84e1275 | ||
|
|
5dda4731a0 | ||
|
|
ce830ea6d3 | ||
|
|
b217b70dfe | ||
|
|
ceee26ad25 | ||
|
|
876018390d | ||
|
|
0366f3544b | ||
|
|
b964ba5317 | ||
|
|
494e36c842 | ||
|
|
9c611a5b13 | ||
|
|
357c478640 | ||
|
|
89f830d9bb | ||
|
|
56150e8707 | ||
|
|
1d60db25bd | ||
|
|
2cac1d9fd2 | ||
|
|
e70724f058 | ||
|
|
27a05e55c9 | ||
|
|
0a0de86ecd | ||
|
|
ec025b7d0f | ||
|
|
744cea1f11 | ||
|
|
073617b6d3 | ||
|
|
8d69945e8e | ||
|
|
3f34a1fb87 | ||
|
|
7bbc7250dd | ||
|
|
63433864d3 | ||
|
|
e53f90fc5c | ||
|
|
33adb08105 | ||
|
|
4a610d182c | ||
|
|
8655d9be87 | ||
|
|
9962ddcd36 | ||
|
|
e117429373 | ||
|
|
dd0f5f961c | ||
|
|
f0b5505770 | ||
|
|
c63b4f9f21 | ||
|
|
a8c6e916cf |
3
.github/workflows/ci.yml
vendored
3
.github/workflows/ci.yml
vendored
@@ -11,6 +11,7 @@ jobs:
|
|||||||
ci:
|
ci:
|
||||||
uses: shlinkio/github-actions/.github/workflows/web-app-ci.yml@main
|
uses: shlinkio/github-actions/.github/workflows/web-app-ci.yml@main
|
||||||
with:
|
with:
|
||||||
node-version: 16.13
|
node-version: 16.15
|
||||||
with-mutation-tests: true
|
with-mutation-tests: true
|
||||||
publish-coverage: true
|
publish-coverage: true
|
||||||
|
force-install: true
|
||||||
|
|||||||
4
.github/workflows/deploy-preview.yml
vendored
4
.github/workflows/deploy-preview.yml
vendored
@@ -16,10 +16,10 @@ jobs:
|
|||||||
- name: Use node.js
|
- name: Use node.js
|
||||||
uses: actions/setup-node@v1
|
uses: actions/setup-node@v1
|
||||||
with:
|
with:
|
||||||
node-version: 16.13
|
node-version: 16.15
|
||||||
- name: Build
|
- name: Build
|
||||||
run: |
|
run: |
|
||||||
npm ci && \
|
npm ci --force && \
|
||||||
node ./scripts/set-homepage.js /shlink-web-client/${GITHUB_HEAD_REF#refs/heads/} && \
|
node ./scripts/set-homepage.js /shlink-web-client/${GITHUB_HEAD_REF#refs/heads/} && \
|
||||||
rm src/service-worker.ts && \
|
rm src/service-worker.ts && \
|
||||||
npm run build
|
npm run build
|
||||||
|
|||||||
4
.github/workflows/publish-release.yml
vendored
4
.github/workflows/publish-release.yml
vendored
@@ -14,9 +14,9 @@ jobs:
|
|||||||
- name: Use node.js
|
- name: Use node.js
|
||||||
uses: actions/setup-node@v1
|
uses: actions/setup-node@v1
|
||||||
with:
|
with:
|
||||||
node-version: 16.13
|
node-version: 16.15
|
||||||
- name: Generate release assets
|
- 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
|
- name: Publish release with assets
|
||||||
uses: docker://antonyurchenko/git-release:latest
|
uses: docker://antonyurchenko/git-release:latest
|
||||||
env:
|
env:
|
||||||
|
|||||||
104
CHANGELOG.md
104
CHANGELOG.md
@@ -4,27 +4,63 @@ All notable changes to this project will be documented in this file.
|
|||||||
|
|
||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org).
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org).
|
||||||
|
|
||||||
## [3.7.0] - 2022-05-14
|
## [3.7.2] - 2022-08-07
|
||||||
### Added
|
### 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.
|
* [#671](https://github.com/shlinkio/shlink-web-client/issues/671) Added proper color-scheme in root element based on selected theme.
|
||||||
* [#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.
|
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
* [#616](https://github.com/shlinkio/shlink-web-client/pull/616) Updated to React 18.
|
* [#688](https://github.com/shlinkio/shlink-web-client/issues/688) Finalized migration from enzyme to react-testing-library.
|
||||||
* [#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.
|
|
||||||
|
|
||||||
### Deprecated
|
### Deprecated
|
||||||
* *Nothing*
|
* *Nothing*
|
||||||
|
|
||||||
### Removed
|
### Removed
|
||||||
* [#623](https://github.com/shlinkio/shlink-web-client/pull/623) Dropped support for Shlink older than 2.6.0.
|
* *Nothing*
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
* [#695](https://github.com/shlinkio/shlink-web-client/issues/695) Fixed some warnings in tests.
|
||||||
|
* [#693](https://github.com/shlinkio/shlink-web-client/issues/693) Fixed tags, servers and domains search to make it case-insensitive.
|
||||||
|
* [#694](https://github.com/shlinkio/shlink-web-client/issues/694) Fixed editing and loading visits on short URLs with multi-segment slugs.
|
||||||
|
|
||||||
|
|
||||||
|
## [3.7.1] - 2022-05-25
|
||||||
|
### Added
|
||||||
|
* *Nothing*
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
* [#648](https://github.com/shlinkio/shlink-web-client/issues/648) Migrated some scripts to ESM and updated to chalk 5.
|
||||||
|
|
||||||
|
### Deprecated
|
||||||
|
* *Nothing*
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
* *Nothing*
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
* [#653](https://github.com/shlinkio/shlink-web-client/issues/653) Fixed rendering values greater than 1000 in charts, when the browser has certain locales configured.
|
||||||
|
|
||||||
|
|
||||||
|
## [3.7.0] - 2022-05-14
|
||||||
|
### Added
|
||||||
|
* [#622](https://github.com/shlinkio/shlink-web-client/issues/622) Added support to load domain visits when consuming Shlink 3.1.0 or newer.
|
||||||
|
* [#582](https://github.com/shlinkio/shlink-web-client/issues/582) Improved filtering short URLs by tag.
|
||||||
|
|
||||||
|
Now, a new full tags selector component is available, which allows selecting any of the existing tags and also composes a toggle to filter by "any" tag or "all" tags.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
* [#616](https://github.com/shlinkio/shlink-web-client/issues/616) Updated to React 18.
|
||||||
|
* [#595](https://github.com/shlinkio/shlink-web-client/issues/595) Updated to react-chartjs-2 v4.1.0.
|
||||||
|
* [#594](https://github.com/shlinkio/shlink-web-client/issues/594) Updated to a new coding standard.
|
||||||
|
* [#627](https://github.com/shlinkio/shlink-web-client/issues/627) Updated to Jest 28.
|
||||||
|
* [#603](https://github.com/shlinkio/shlink-web-client/issues/603) Migrated to new and maintained dependencies to parse CSV<->JSON.
|
||||||
|
* [#610](https://github.com/shlinkio/shlink-web-client/issues/610) Migrated to a maintained coding style for CSS.
|
||||||
|
* [#619](https://github.com/shlinkio/shlink-web-client/issues/619) Introduced react testing library, to progressively replace enzyme.
|
||||||
|
|
||||||
|
### Deprecated
|
||||||
|
* *Nothing*
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
* [#623](https://github.com/shlinkio/shlink-web-client/issues/623) Dropped support for Shlink older than 2.6.0.
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
* *Nothing*
|
* *Nothing*
|
||||||
@@ -32,19 +68,19 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|||||||
|
|
||||||
## [3.6.0] - 2022-03-17
|
## [3.6.0] - 2022-03-17
|
||||||
### Added
|
### 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.
|
* [#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/pull/570) Added new section to load non-orphan visits all together when consuming Shlink 3.0.0.
|
* [#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/pull/556) Added support to filter short URLs list by "all" tags 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/pull/549) Allowed to export the list of short URLs as CSV.
|
* [#549](https://github.com/shlinkio/shlink-web-client/issues/549) Allowed to export the list of short URLs as CSV.
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
* [#543](https://github.com/shlinkio/shlink-web-client/pull/543) Redesigned settings section.
|
* [#543](https://github.com/shlinkio/shlink-web-client/issues/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.
|
* [#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/pull/448) Updated to bootstrap v5.
|
* [#448](https://github.com/shlinkio/shlink-web-client/issues/448) Updated to bootstrap v5.
|
||||||
* [#524](https://github.com/shlinkio/shlink-web-client/pull/524) Updated to react-router v6.
|
* [#524](https://github.com/shlinkio/shlink-web-client/issues/524) Updated to react-router v6.
|
||||||
* [#576](https://github.com/shlinkio/shlink-web-client/pull/576) Updated to fontawesome v6.
|
* [#576](https://github.com/shlinkio/shlink-web-client/issues/576) Updated to fontawesome v6.
|
||||||
* [#579](https://github.com/shlinkio/shlink-web-client/pull/579) Replaced react-color with react-colorful.
|
* [#579](https://github.com/shlinkio/shlink-web-client/issues/579) Replaced react-color with react-colorful.
|
||||||
* [#564](https://github.com/shlinkio/shlink-web-client/pull/564) Updated most of the dependencies.
|
* [#564](https://github.com/shlinkio/shlink-web-client/issues/564) Updated most of the dependencies.
|
||||||
|
|
||||||
### Deprecated
|
### Deprecated
|
||||||
* *Nothing*
|
* *Nothing*
|
||||||
@@ -53,7 +89,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|||||||
* *Nothing*
|
* *Nothing*
|
||||||
|
|
||||||
### Fixed
|
### 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
|
## [3.5.1] - 2022-01-08
|
||||||
@@ -77,27 +113,27 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|||||||
|
|
||||||
## [3.5.0] - 2022-01-01
|
## [3.5.0] - 2022-01-01
|
||||||
### Added
|
### 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.
|
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.
|
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.
|
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.
|
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.
|
* [#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/pull/535) Allowed editing default domain redirects when consuming Shlink 2.10 or newer.
|
* [#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/pull/531) Added custom slug field to the basic creation form in the Overview page.
|
* [#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/pull/537) Allowed to customize the ordering for every list in the app that supports it, being currently tags and short URLs.
|
* [#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/pull/542) Added ordering for short URLs to the query, so that it is consistent with the rest of the filtering params.
|
* [#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
|
### Changed
|
||||||
* [#534](https://github.com/shlinkio/shlink-web-client/pull/534) Updated axios.
|
* [#534](https://github.com/shlinkio/shlink-web-client/issues/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
|
* [#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
|
### Deprecated
|
||||||
* *Nothing*
|
* *Nothing*
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
FROM node:16.13-alpine as node
|
FROM node:16.15-alpine as node
|
||||||
COPY . /shlink-web-client
|
COPY . /shlink-web-client
|
||||||
ARG VERSION="latest"
|
ARG VERSION="latest"
|
||||||
ENV VERSION ${VERSION}
|
ENV VERSION ${VERSION}
|
||||||
RUN cd /shlink-web-client && npm ci && NODE_ENV=production npm run build
|
RUN cd /shlink-web-client && npm ci --force && NODE_ENV=production npm run build
|
||||||
|
|
||||||
FROM nginx:1.21-alpine
|
FROM nginx:1.21-alpine
|
||||||
LABEL maintainer="Alejandro Celaya <alejandro@alejandrocelaya.com>"
|
LABEL maintainer="Alejandro Celaya <alejandro@alejandrocelaya.com>"
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
import * as util from 'util';
|
|
||||||
|
|
||||||
global.TextEncoder = util.TextEncoder;
|
|
||||||
global.TextDecoder = util.TextDecoder;
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
import Enzyme from 'enzyme';
|
|
||||||
import Adapter from '@wojtekmaj/enzyme-adapter-react-17';
|
|
||||||
|
|
||||||
Enzyme.configure({ adapter: new Adapter() });
|
|
||||||
@@ -4,4 +4,5 @@ import ResizeObserver from 'resize-observer-polyfill';
|
|||||||
|
|
||||||
(global as any).ResizeObserver = ResizeObserver;
|
(global as any).ResizeObserver = ResizeObserver;
|
||||||
(global as any).scrollTo = () => {};
|
(global as any).scrollTo = () => {};
|
||||||
|
(global as any).prompt = () => {};
|
||||||
(global as any).matchMedia = (media: string) => ({ matches: false, media });
|
(global as any).matchMedia = (media: string) => ({ matches: false, media });
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ version: '3'
|
|||||||
services:
|
services:
|
||||||
shlink_web_client_node:
|
shlink_web_client_node:
|
||||||
container_name: shlink_web_client_node
|
container_name: shlink_web_client_node
|
||||||
image: node:16.13-alpine
|
image: node:16.15-alpine
|
||||||
command: /bin/sh -c "cd /home/shlink/www && npm install && npm run start"
|
command: /bin/sh -c "cd /home/shlink/www && npm install && npm run start"
|
||||||
volumes:
|
volumes:
|
||||||
- ./:/home/shlink/www
|
- ./:/home/shlink/www
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ module.exports = {
|
|||||||
lines: 90,
|
lines: 90,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
setupFiles: ['<rootDir>/config/jest/setupBeforeEnzyme.js', '<rootDir>/config/jest/setupEnzyme.js'],
|
|
||||||
setupFilesAfterEnv: ['<rootDir>/config/jest/setupTests.ts'],
|
setupFilesAfterEnv: ['<rootDir>/config/jest/setupTests.ts'],
|
||||||
testMatch: ['<rootDir>/test/**/*.test.{ts,tsx}'],
|
testMatch: ['<rootDir>/test/**/*.test.{ts,tsx}'],
|
||||||
testEnvironment: 'jsdom',
|
testEnvironment: 'jsdom',
|
||||||
|
|||||||
1051
package-lock.json
generated
1051
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
16
package.json
16
package.json
@@ -13,13 +13,14 @@
|
|||||||
"lint:css:fix": "npm run lint:css -- --fix",
|
"lint:css:fix": "npm run lint:css -- --fix",
|
||||||
"lint:js:fix": "npm run lint:js -- --fix",
|
"lint:js:fix": "npm run lint:js -- --fix",
|
||||||
"start": "DISABLE_ESLINT_PLUGIN=true react-scripts start",
|
"start": "DISABLE_ESLINT_PLUGIN=true react-scripts start",
|
||||||
"build": "DISABLE_ESLINT_PLUGIN=true react-scripts build && node scripts/replace-version.js",
|
"build": "DISABLE_ESLINT_PLUGIN=true react-scripts build && node scripts/replace-version.mjs",
|
||||||
"build:dist": "npm run build && node scripts/create-dist-file.js",
|
"build:dist": "npm run build && node scripts/create-dist-file.mjs",
|
||||||
"build:serve": "serve -p 5000 ./build",
|
"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:coverage": "npm run test -- --coverage --coverageReporters=text --coverageReporters=text-summary",
|
||||||
"test:ci": "npm run test:coverage -- --coverageReporters=clover --ci",
|
"test:ci": "npm run test:coverage -- --coverageReporters=clover --ci",
|
||||||
"test:pretty": "npm run test:coverage -- --coverageReporters=html",
|
"test:pretty": "npm run test:coverage -- --coverageReporters=html",
|
||||||
|
"test:verbose": "npm run test -- --verbose",
|
||||||
"mutate": "./node_modules/.bin/stryker run --concurrency 4 --ignoreStatic"
|
"mutate": "./node_modules/.bin/stryker run --concurrency 4 --ignoreStatic"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -45,14 +46,14 @@
|
|||||||
"react": "^18.1.0",
|
"react": "^18.1.0",
|
||||||
"react-chartjs-2": "^4.1.0",
|
"react-chartjs-2": "^4.1.0",
|
||||||
"react-colorful": "^5.5.1",
|
"react-colorful": "^5.5.1",
|
||||||
"react-copy-to-clipboard": "^5.0.4",
|
"react-copy-to-clipboard": "^5.1.0",
|
||||||
"react-datepicker": "^4.7.0",
|
"react-datepicker": "^4.8.0",
|
||||||
"react-dom": "^18.1.0",
|
"react-dom": "^18.1.0",
|
||||||
"react-external-link": "^2.0.0",
|
"react-external-link": "^2.0.0",
|
||||||
"react-leaflet": "^4.0.0",
|
"react-leaflet": "^4.0.0",
|
||||||
"react-redux": "^8.0.0",
|
"react-redux": "^8.0.0",
|
||||||
"react-router-dom": "^6.3.0",
|
"react-router-dom": "^6.3.0",
|
||||||
"react-swipeable": "^6.2.0",
|
"react-swipeable": "^7.0.0",
|
||||||
"react-tag-autocomplete": "^6.3.0",
|
"react-tag-autocomplete": "^6.3.0",
|
||||||
"reactstrap": "^9.0.1",
|
"reactstrap": "^9.0.1",
|
||||||
"redux": "^4.2.0",
|
"redux": "^4.2.0",
|
||||||
@@ -75,7 +76,6 @@
|
|||||||
"@testing-library/jest-dom": "^5.16.4",
|
"@testing-library/jest-dom": "^5.16.4",
|
||||||
"@testing-library/react": "^13.1.1",
|
"@testing-library/react": "^13.1.1",
|
||||||
"@testing-library/user-event": "^14.1.1",
|
"@testing-library/user-event": "^14.1.1",
|
||||||
"@types/enzyme": "^3.10.11",
|
|
||||||
"@types/jest": "^27.4.1",
|
"@types/jest": "^27.4.1",
|
||||||
"@types/json2csv": "^5.0.3",
|
"@types/json2csv": "^5.0.3",
|
||||||
"@types/leaflet": "^1.7.9",
|
"@types/leaflet": "^1.7.9",
|
||||||
@@ -88,11 +88,9 @@
|
|||||||
"@types/react-dom": "^18.0.3",
|
"@types/react-dom": "^18.0.3",
|
||||||
"@types/react-tag-autocomplete": "^6.1.1",
|
"@types/react-tag-autocomplete": "^6.1.1",
|
||||||
"@types/uuid": "^8.3.4",
|
"@types/uuid": "^8.3.4",
|
||||||
"@wojtekmaj/enzyme-adapter-react-17": "0.6.5",
|
|
||||||
"adm-zip": "^0.5.9",
|
"adm-zip": "^0.5.9",
|
||||||
"babel-jest": "^28.0.3",
|
"babel-jest": "^28.0.3",
|
||||||
"chalk": "^5.0.1",
|
"chalk": "^5.0.1",
|
||||||
"enzyme": "^3.11.0",
|
|
||||||
"eslint": "^8.12.0",
|
"eslint": "^8.12.0",
|
||||||
"identity-obj-proxy": "^3.0.0",
|
"identity-obj-proxy": "^3.0.0",
|
||||||
"jest": "^28.0.3",
|
"jest": "^28.0.3",
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
process.env.BABEL_ENV = 'production';
|
process.env.BABEL_ENV = 'production';
|
||||||
process.env.NODE_ENV = 'production';
|
process.env.NODE_ENV = 'production';
|
||||||
|
|
||||||
const chalk = require('chalk');
|
import chalk from 'chalk';
|
||||||
const AdmZip = require('adm-zip');
|
import AdmZip from 'adm-zip';
|
||||||
const fs = require('fs');
|
import fs from 'fs';
|
||||||
|
|
||||||
function zipDist(version) {
|
function zipDist(version) {
|
||||||
const versionFileName = `./dist/shlink-web-client_${version}_dist.zip`;
|
const versionFileName = `./dist/shlink-web-client_${version}_dist.zip`;
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
const fs = require('fs');
|
import fs from 'fs';
|
||||||
|
|
||||||
function replaceVersionPlaceholder(version) {
|
function replaceVersionPlaceholder(version) {
|
||||||
const staticJsFilesPath = './build/static/js';
|
const staticJsFilesPath = './build/static/js';
|
||||||
@@ -29,7 +29,7 @@ const normalizeOrderByInParams = (params: ShlinkShortUrlsListParams): ShlinkShor
|
|||||||
return { ...rest, orderBy: orderToString(orderBy) };
|
return { ...rest, orderBy: orderToString(orderBy) };
|
||||||
};
|
};
|
||||||
|
|
||||||
export default class ShlinkApiClient {
|
export class ShlinkApiClient {
|
||||||
public constructor(
|
public constructor(
|
||||||
private readonly axios: AxiosInstance,
|
private readonly axios: AxiosInstance,
|
||||||
private readonly baseUrl: string,
|
private readonly baseUrl: string,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { AxiosInstance } from 'axios';
|
|||||||
import { prop } from 'ramda';
|
import { prop } from 'ramda';
|
||||||
import { hasServerData, SelectedServer, ServerWithId } from '../../servers/data';
|
import { hasServerData, SelectedServer, ServerWithId } from '../../servers/data';
|
||||||
import { GetState } from '../../container/types';
|
import { GetState } from '../../container/types';
|
||||||
import ShlinkApiClient from './ShlinkApiClient';
|
import { ShlinkApiClient } from './ShlinkApiClient';
|
||||||
|
|
||||||
const apiClients: Record<string, ShlinkApiClient> = {};
|
const apiClients: Record<string, ShlinkApiClient> = {};
|
||||||
|
|
||||||
@@ -12,7 +12,7 @@ const getSelectedServerFromState = (getState: GetState): SelectedServer => prop(
|
|||||||
|
|
||||||
export type ShlinkApiClientBuilder = (getStateOrSelectedServer: GetState | ServerWithId) => ShlinkApiClient;
|
export type ShlinkApiClientBuilder = (getStateOrSelectedServer: GetState | ServerWithId) => ShlinkApiClient;
|
||||||
|
|
||||||
const buildShlinkApiClient = (axios: AxiosInstance): ShlinkApiClientBuilder => (
|
export const buildShlinkApiClient = (axios: AxiosInstance): ShlinkApiClientBuilder => (
|
||||||
getStateOrSelectedServer: GetState | ServerWithId,
|
getStateOrSelectedServer: GetState | ServerWithId,
|
||||||
) => {
|
) => {
|
||||||
const server = isGetState(getStateOrSelectedServer)
|
const server = isGetState(getStateOrSelectedServer)
|
||||||
@@ -32,5 +32,3 @@ const buildShlinkApiClient = (axios: AxiosInstance): ShlinkApiClientBuilder => (
|
|||||||
|
|
||||||
return apiClients[clientKey];
|
return apiClients[clientKey];
|
||||||
};
|
};
|
||||||
|
|
||||||
export default buildShlinkApiClient;
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import Bottle from 'bottlejs';
|
import Bottle from 'bottlejs';
|
||||||
import buildShlinkApiClient from './ShlinkApiClientBuilder';
|
import { buildShlinkApiClient } from './ShlinkApiClientBuilder';
|
||||||
|
|
||||||
const provideServices = (bottle: Bottle) => {
|
const provideServices = (bottle: Bottle) => {
|
||||||
bottle.serviceFactory('buildShlinkApiClient', buildShlinkApiClient, 'axios');
|
bottle.serviceFactory('buildShlinkApiClient', buildShlinkApiClient, 'axios');
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ const AsideMenuItem: FC<AsideMenuItemProps> = ({ children, to, className, ...res
|
|||||||
</NavLink>
|
</NavLink>
|
||||||
);
|
);
|
||||||
|
|
||||||
const AsideMenu = (DeleteServerButton: FC<DeleteServerButtonProps>) => (
|
export const AsideMenu = (DeleteServerButton: FC<DeleteServerButtonProps>) => (
|
||||||
{ selectedServer, showOnMobile = false }: AsideMenuProps,
|
{ selectedServer, showOnMobile = false }: AsideMenuProps,
|
||||||
) => {
|
) => {
|
||||||
const hasId = isServerWithId(selectedServer);
|
const hasId = isServerWithId(selectedServer);
|
||||||
@@ -89,5 +89,3 @@ const AsideMenu = (DeleteServerButton: FC<DeleteServerButtonProps>) => (
|
|||||||
</aside>
|
</aside>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default AsideMenu;
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { Card, Row } from 'reactstrap';
|
|||||||
import { ExternalLink } from 'react-external-link';
|
import { ExternalLink } from 'react-external-link';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import { faExternalLinkAlt, faPlus } from '@fortawesome/free-solid-svg-icons';
|
import { faExternalLinkAlt, faPlus } from '@fortawesome/free-solid-svg-icons';
|
||||||
import ServersListGroup from '../servers/ServersListGroup';
|
import { ServersListGroup } from '../servers/ServersListGroup';
|
||||||
import { ServersMap } from '../servers/data';
|
import { ServersMap } from '../servers/data';
|
||||||
import { ShlinkLogo } from './img/ShlinkLogo';
|
import { ShlinkLogo } from './img/ShlinkLogo';
|
||||||
import './Home.scss';
|
import './Home.scss';
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ const VersionLink = ({ project, version }: { project: 'shlink' | 'shlink-web-cli
|
|||||||
</ExternalLink>
|
</ExternalLink>
|
||||||
);
|
);
|
||||||
|
|
||||||
const ShlinkVersions = ({ selectedServer, clientVersion = SHLINK_WEB_CLIENT_VERSION }: ShlinkVersionsProps) => {
|
export const ShlinkVersions = ({ selectedServer, clientVersion = SHLINK_WEB_CLIENT_VERSION }: ShlinkVersionsProps) => {
|
||||||
const normalizedClientVersion = normalizeVersion(clientVersion);
|
const normalizedClientVersion = normalizeVersion(clientVersion);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -29,5 +29,3 @@ const ShlinkVersions = ({ selectedServer, clientVersion = SHLINK_WEB_CLIENT_VERS
|
|||||||
</small>
|
</small>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ShlinkVersions;
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { SelectedServer } from '../servers/data';
|
import { SelectedServer } from '../servers/data';
|
||||||
import ShlinkVersions from './ShlinkVersions';
|
import { ShlinkVersions } from './ShlinkVersions';
|
||||||
import { Sidebar } from './reducers/sidebar';
|
import { Sidebar } from './reducers/sidebar';
|
||||||
import './ShlinkVersionsContainer.scss';
|
import './ShlinkVersionsContainer.scss';
|
||||||
|
|
||||||
@@ -9,7 +9,7 @@ export interface ShlinkVersionsContainerProps {
|
|||||||
sidebar: Sidebar;
|
sidebar: Sidebar;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ShlinkVersionsContainer = ({ selectedServer, sidebar }: ShlinkVersionsContainerProps) => {
|
export const ShlinkVersionsContainer = ({ selectedServer, sidebar }: ShlinkVersionsContainerProps) => {
|
||||||
const classes = classNames('text-center', {
|
const classes = classNames('text-center', {
|
||||||
'shlink-versions-container--with-sidebar': sidebar.sidebarPresent,
|
'shlink-versions-container--with-sidebar': sidebar.sidebarPresent,
|
||||||
});
|
});
|
||||||
@@ -20,5 +20,3 @@ const ShlinkVersionsContainer = ({ selectedServer, sidebar }: ShlinkVersionsCont
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ShlinkVersionsContainer;
|
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ import { ScrollToTop } from '../ScrollToTop';
|
|||||||
import { MainHeader } from '../MainHeader';
|
import { MainHeader } from '../MainHeader';
|
||||||
import { Home } from '../Home';
|
import { Home } from '../Home';
|
||||||
import { MenuLayout } from '../MenuLayout';
|
import { MenuLayout } from '../MenuLayout';
|
||||||
import AsideMenu from '../AsideMenu';
|
import { AsideMenu } from '../AsideMenu';
|
||||||
import { ErrorHandler } from '../ErrorHandler';
|
import { ErrorHandler } from '../ErrorHandler';
|
||||||
import ShlinkVersionsContainer from '../ShlinkVersionsContainer';
|
import { ShlinkVersionsContainer } from '../ShlinkVersionsContainer';
|
||||||
import { ConnectDecorator } from '../../container/types';
|
import { ConnectDecorator } from '../../container/types';
|
||||||
import { withoutSelectedServer } from '../../servers/helpers/withoutSelectedServer';
|
import { withoutSelectedServer } from '../../servers/helpers/withoutSelectedServer';
|
||||||
import { sidebarNotPresent, sidebarPresent } from '../reducers/sidebar';
|
import { sidebarNotPresent, sidebarPresent } from '../reducers/sidebar';
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { FC, useEffect } from 'react';
|
import { FC, useEffect } from 'react';
|
||||||
import Message from '../utils/Message';
|
import { Message } from '../utils/Message';
|
||||||
import { Result } from '../utils/Result';
|
import { Result } from '../utils/Result';
|
||||||
import { ShlinkApiError } from '../api/ShlinkApiError';
|
import { ShlinkApiError } from '../api/ShlinkApiError';
|
||||||
import { SimpleCard } from '../utils/SimpleCard';
|
import { SimpleCard } from '../utils/SimpleCard';
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
faCircleNotch as loadingStatusIcon,
|
faCircleNotch as loadingStatusIcon,
|
||||||
} from '@fortawesome/free-solid-svg-icons';
|
} from '@fortawesome/free-solid-svg-icons';
|
||||||
import { MediaMatcher } from '../../utils/types';
|
import { MediaMatcher } from '../../utils/types';
|
||||||
|
import { mutableRefToElementRef } from '../../utils/helpers/components';
|
||||||
import { DomainStatus } from '../data';
|
import { DomainStatus } from '../data';
|
||||||
|
|
||||||
interface DomainStatusIconProps {
|
interface DomainStatusIconProps {
|
||||||
@@ -34,11 +35,7 @@ export const DomainStatusIcon: FC<DomainStatusIconProps> = ({ status, matchMedia
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<span
|
<span ref={mutableRefToElementRef(ref)}>
|
||||||
ref={(el: HTMLSpanElement) => {
|
|
||||||
ref.current = el;
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{status === 'valid'
|
{status === 'valid'
|
||||||
? <FontAwesomeIcon fixedWidth icon={checkIcon} className="text-muted" />
|
? <FontAwesomeIcon fixedWidth icon={checkIcon} className="text-muted" />
|
||||||
: <FontAwesomeIcon fixedWidth icon={invalidIcon} className="text-danger" />}
|
: <FontAwesomeIcon fixedWidth icon={invalidIcon} className="text-danger" />}
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ export const EditDomainRedirectsModal: FC<EditDomainRedirectsModalProps> = (
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal isOpen={isOpen} toggle={toggle} centered>
|
<Modal isOpen={isOpen} toggle={toggle} centered>
|
||||||
<form onSubmit={handleSubmit}>
|
<form name="domainRedirectsModal" onSubmit={handleSubmit}>
|
||||||
<ModalHeader toggle={toggle}>Edit redirects for <b>{domain.domain}</b></ModalHeader>
|
<ModalHeader toggle={toggle}>Edit redirects for <b>{domain.domain}</b></ModalHeader>
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
<FormGroup value={baseUrlRedirect} onChange={setBaseUrlRedirect}>
|
<FormGroup value={baseUrlRedirect} onChange={setBaseUrlRedirect}>
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ export default buildReducer<DomainsList, DomainsCombinedAction>({
|
|||||||
({ ...initialState, domains, filteredDomains: domains, defaultRedirects }),
|
({ ...initialState, domains, filteredDomains: domains, defaultRedirects }),
|
||||||
[FILTER_DOMAINS]: (state, { searchTerm }) => ({
|
[FILTER_DOMAINS]: (state, { searchTerm }) => ({
|
||||||
...state,
|
...state,
|
||||||
filteredDomains: state.domains.filter(({ domain }) => domain.toLowerCase().match(searchTerm)),
|
filteredDomains: state.domains.filter(({ domain }) => domain.toLowerCase().match(searchTerm.toLowerCase())),
|
||||||
}),
|
}),
|
||||||
[EDIT_DOMAIN_REDIRECTS]: (state, { domain, redirects }) => ({
|
[EDIT_DOMAIN_REDIRECTS]: (state, { domain, redirects }) => ({
|
||||||
...state,
|
...state,
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
|
|
||||||
:root {
|
:root {
|
||||||
scroll-behavior: auto;
|
scroll-behavior: auto;
|
||||||
|
color-scheme: var(--color-scheme);
|
||||||
}
|
}
|
||||||
|
|
||||||
html,
|
html,
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { Button } from 'reactstrap';
|
|||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { Result } from '../utils/Result';
|
import { Result } from '../utils/Result';
|
||||||
import { NoMenuLayout } from '../common/NoMenuLayout';
|
import { NoMenuLayout } from '../common/NoMenuLayout';
|
||||||
import { StateFlagTimeout, useGoBack, useToggle } from '../utils/helpers/hooks';
|
import { TimeoutToggle, useGoBack, useToggle } from '../utils/helpers/hooks';
|
||||||
import { ServerForm } from './helpers/ServerForm';
|
import { ServerForm } from './helpers/ServerForm';
|
||||||
import { ImportServersBtnProps } from './helpers/ImportServersBtn';
|
import { ImportServersBtnProps } from './helpers/ImportServersBtn';
|
||||||
import { ServerData, ServersMap, ServerWithId } from './data';
|
import { ServerData, ServersMap, ServerWithId } from './data';
|
||||||
@@ -26,14 +26,14 @@ const ImportResult = ({ type }: { type: 'error' | 'success' }) => (
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
const CreateServer = (ImportServersBtn: FC<ImportServersBtnProps>, useStateFlagTimeout: StateFlagTimeout) => (
|
export const CreateServer = (ImportServersBtn: FC<ImportServersBtnProps>, useTimeoutToggle: TimeoutToggle) => (
|
||||||
{ servers, createServer }: CreateServerProps,
|
{ servers, createServer }: CreateServerProps,
|
||||||
) => {
|
) => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const goBack = useGoBack();
|
const goBack = useGoBack();
|
||||||
const hasServers = !!Object.keys(servers).length;
|
const hasServers = !!Object.keys(servers).length;
|
||||||
const [serversImported, setServersImported] = useStateFlagTimeout(false, SHOW_IMPORT_MSG_TIME);
|
const [serversImported, setServersImported] = useTimeoutToggle(false, SHOW_IMPORT_MSG_TIME);
|
||||||
const [errorImporting, setErrorImporting] = useStateFlagTimeout(false, SHOW_IMPORT_MSG_TIME);
|
const [errorImporting, setErrorImporting] = useTimeoutToggle(false, SHOW_IMPORT_MSG_TIME);
|
||||||
const [isConfirmModalOpen, toggleConfirmModal] = useToggle();
|
const [isConfirmModalOpen, toggleConfirmModal] = useToggle();
|
||||||
const [serverData, setServerData] = useState<ServerData | undefined>();
|
const [serverData, setServerData] = useState<ServerData | undefined>();
|
||||||
const save = () => {
|
const save = () => {
|
||||||
@@ -77,5 +77,3 @@ const CreateServer = (ImportServersBtn: FC<ImportServersBtnProps>, useStateFlagT
|
|||||||
</NoMenuLayout>
|
</NoMenuLayout>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default CreateServer;
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export type DeleteServerButtonProps = PropsWithChildren<{
|
|||||||
textClassName?: string;
|
textClassName?: string;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
const DeleteServerButton = (DeleteServerModal: FC<DeleteServerModalProps>): FC<DeleteServerButtonProps> => (
|
export const DeleteServerButton = (DeleteServerModal: FC<DeleteServerModalProps>): FC<DeleteServerButtonProps> => (
|
||||||
{ server, className, children, textClassName },
|
{ server, className, children, textClassName },
|
||||||
) => {
|
) => {
|
||||||
const [isModalOpen, , showModal, hideModal] = useToggle();
|
const [isModalOpen, , showModal, hideModal] = useToggle();
|
||||||
@@ -27,5 +27,3 @@ const DeleteServerButton = (DeleteServerModal: FC<DeleteServerModalProps>): FC<D
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default DeleteServerButton;
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ interface DeleteServerModalConnectProps extends DeleteServerModalProps {
|
|||||||
deleteServer: (server: ServerWithId) => void;
|
deleteServer: (server: ServerWithId) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const DeleteServerModal: FC<DeleteServerModalConnectProps> = (
|
export const DeleteServerModal: FC<DeleteServerModalConnectProps> = (
|
||||||
{ server, toggle, isOpen, deleteServer, redirectHome = true },
|
{ server, toggle, isOpen, deleteServer, redirectHome = true },
|
||||||
) => {
|
) => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -26,7 +26,7 @@ const DeleteServerModal: FC<DeleteServerModalConnectProps> = (
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal isOpen={isOpen} toggle={toggle} centered>
|
<Modal isOpen={isOpen} toggle={toggle} centered>
|
||||||
<ModalHeader toggle={toggle}><span className="text-danger">Remove server</span></ModalHeader>
|
<ModalHeader toggle={toggle} className="text-danger">Remove server</ModalHeader>
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
<p>Are you sure you want to remove <b>{server ? server.name : ''}</b>?</p>
|
<p>Are you sure you want to remove <b>{server ? server.name : ''}</b>?</p>
|
||||||
<p>
|
<p>
|
||||||
@@ -43,5 +43,3 @@ const DeleteServerModal: FC<DeleteServerModalConnectProps> = (
|
|||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default DeleteServerModal;
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { NoMenuLayout } from '../common/NoMenuLayout';
|
|||||||
import { SimpleCard } from '../utils/SimpleCard';
|
import { SimpleCard } from '../utils/SimpleCard';
|
||||||
import { SearchField } from '../utils/SearchField';
|
import { SearchField } from '../utils/SearchField';
|
||||||
import { Result } from '../utils/Result';
|
import { Result } from '../utils/Result';
|
||||||
import { StateFlagTimeout } from '../utils/helpers/hooks';
|
import { TimeoutToggle } from '../utils/helpers/hooks';
|
||||||
import { ImportServersBtnProps } from './helpers/ImportServersBtn';
|
import { ImportServersBtnProps } from './helpers/ImportServersBtn';
|
||||||
import { ServersMap } from './data';
|
import { ServersMap } from './data';
|
||||||
import { ManageServersRowProps } from './ManageServersRow';
|
import { ManageServersRowProps } from './ManageServersRow';
|
||||||
@@ -22,16 +22,16 @@ const SHOW_IMPORT_MSG_TIME = 4000;
|
|||||||
export const ManageServers = (
|
export const ManageServers = (
|
||||||
serversExporter: ServersExporter,
|
serversExporter: ServersExporter,
|
||||||
ImportServersBtn: FC<ImportServersBtnProps>,
|
ImportServersBtn: FC<ImportServersBtnProps>,
|
||||||
useStateFlagTimeout: StateFlagTimeout,
|
useTimeoutToggle: TimeoutToggle,
|
||||||
ManageServersRow: FC<ManageServersRowProps>,
|
ManageServersRow: FC<ManageServersRowProps>,
|
||||||
): FC<ManageServersProps> => ({ servers }) => {
|
): FC<ManageServersProps> => ({ servers }) => {
|
||||||
const allServers = Object.values(servers);
|
const allServers = Object.values(servers);
|
||||||
const [serversList, setServersList] = useState(allServers);
|
const [serversList, setServersList] = useState(allServers);
|
||||||
const filterServers = (searchTerm: string) => setServersList(
|
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 hasAutoConnect = serversList.some(({ autoConnect }) => !!autoConnect);
|
||||||
const [errorImporting, setErrorImporting] = useStateFlagTimeout(false, SHOW_IMPORT_MSG_TIME);
|
const [errorImporting, setErrorImporting] = useTimeoutToggle(false, SHOW_IMPORT_MSG_TIME);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setServersList(Object.values(servers));
|
setServersList(Object.values(servers));
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ export const ManageServersRowDropdown = (
|
|||||||
<DropdownItem tag={Link} to={`${serverUrl}/edit`}>
|
<DropdownItem tag={Link} to={`${serverUrl}/edit`}>
|
||||||
<FontAwesomeIcon icon={editIcon} fixedWidth /> Edit server
|
<FontAwesomeIcon icon={editIcon} fixedWidth /> Edit server
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
<DropdownItem onClick={() => setAutoConnect(server, !server.autoConnect)}>
|
<DropdownItem onClick={() => setAutoConnect(server, !isAutoConnect)}>
|
||||||
<FontAwesomeIcon icon={autoConnectIcon} fixedWidth /> {isAutoConnect ? 'Do not a' : 'A'}uto-connect
|
<FontAwesomeIcon icon={autoConnectIcon} fixedWidth /> {isAutoConnect ? 'Do not a' : 'A'}uto-connect
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
<DropdownItem divider />
|
<DropdownItem divider />
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export interface ServersDropdownProps {
|
|||||||
selectedServer: SelectedServer;
|
selectedServer: SelectedServer;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ServersDropdown = ({ servers, selectedServer }: ServersDropdownProps) => {
|
export const ServersDropdown = ({ servers, selectedServer }: ServersDropdownProps) => {
|
||||||
const serversList = values(servers);
|
const serversList = values(servers);
|
||||||
|
|
||||||
const renderServers = () => {
|
const renderServers = () => {
|
||||||
@@ -46,5 +46,3 @@ const ServersDropdown = ({ servers, selectedServer }: ServersDropdownProps) => {
|
|||||||
</UncontrolledDropdown>
|
</UncontrolledDropdown>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ServersDropdown;
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ const ServerListItem = ({ id, name }: { id: string; name: string }) => (
|
|||||||
</ListGroupItem>
|
</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>}
|
{children && <h5 className="mb-md-3">{children}</h5>}
|
||||||
{servers.length > 0 && (
|
{servers.length > 0 && (
|
||||||
@@ -31,5 +31,3 @@ const ServersListGroup: FC<ServersListGroupProps> = ({ servers, children, embedd
|
|||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
export default ServersListGroup;
|
|
||||||
|
|||||||
@@ -1,16 +1,15 @@
|
|||||||
import { useRef, RefObject, ChangeEvent, MutableRefObject, useState, useEffect, FC, PropsWithChildren } from 'react';
|
import { useRef, ChangeEvent, useState, useEffect, FC, PropsWithChildren } from 'react';
|
||||||
import { Button, UncontrolledTooltip } from 'reactstrap';
|
import { Button, UncontrolledTooltip } from 'reactstrap';
|
||||||
import { complement, pipe } from 'ramda';
|
import { complement, pipe } from 'ramda';
|
||||||
import { faFileUpload as importIcon } from '@fortawesome/free-solid-svg-icons';
|
import { faFileUpload as importIcon } from '@fortawesome/free-solid-svg-icons';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import { useToggle } from '../../utils/helpers/hooks';
|
import { useToggle } from '../../utils/helpers/hooks';
|
||||||
|
import { mutableRefToElementRef } from '../../utils/helpers/components';
|
||||||
import { ServersImporter } from '../services/ServersImporter';
|
import { ServersImporter } from '../services/ServersImporter';
|
||||||
import { ServerData, ServersMap } from '../data';
|
import { ServerData, ServersMap } from '../data';
|
||||||
import { DuplicatedServersModal } from './DuplicatedServersModal';
|
import { DuplicatedServersModal } from './DuplicatedServersModal';
|
||||||
import './ImportServersBtn.scss';
|
import './ImportServersBtn.scss';
|
||||||
|
|
||||||
type Ref<T> = RefObject<T> | MutableRefObject<T>;
|
|
||||||
|
|
||||||
export type ImportServersBtnProps = PropsWithChildren<{
|
export type ImportServersBtnProps = PropsWithChildren<{
|
||||||
onImport?: () => void;
|
onImport?: () => void;
|
||||||
onImportError?: (error: Error) => void;
|
onImportError?: (error: Error) => void;
|
||||||
@@ -21,23 +20,21 @@ export type ImportServersBtnProps = PropsWithChildren<{
|
|||||||
interface ImportServersBtnConnectProps extends ImportServersBtnProps {
|
interface ImportServersBtnConnectProps extends ImportServersBtnProps {
|
||||||
createServers: (servers: ServerData[]) => void;
|
createServers: (servers: ServerData[]) => void;
|
||||||
servers: ServersMap;
|
servers: ServersMap;
|
||||||
fileRef: Ref<HTMLInputElement>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const serversFiltering = (servers: ServerData[]) =>
|
const serversFiltering = (servers: ServerData[]) =>
|
||||||
({ url, apiKey }: ServerData) => servers.some((server) => server.url === url && server.apiKey === apiKey);
|
({ 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,
|
createServers,
|
||||||
servers,
|
servers,
|
||||||
fileRef,
|
|
||||||
children,
|
children,
|
||||||
onImport = () => {},
|
onImport = () => {},
|
||||||
onImportError = () => {},
|
onImportError = () => {},
|
||||||
tooltipPlacement = 'bottom',
|
tooltipPlacement = 'bottom',
|
||||||
className = '',
|
className = '',
|
||||||
}) => {
|
}) => {
|
||||||
const ref = fileRef ?? useRef<HTMLInputElement>();
|
const ref = useRef<HTMLInputElement>();
|
||||||
const [serversToCreate, setServersToCreate] = useState<ServerData[] | undefined>();
|
const [serversToCreate, setServersToCreate] = useState<ServerData[] | undefined>();
|
||||||
const [duplicatedServers, setDuplicatedServers] = useState<ServerData[]>([]);
|
const [duplicatedServers, setDuplicatedServers] = useState<ServerData[]>([]);
|
||||||
const [isModalOpen,, showModal, hideModal] = useToggle();
|
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>.
|
You can create servers by importing a CSV file with columns <b>name</b>, <b>apiKey</b> and <b>url</b>.
|
||||||
</UncontrolledTooltip>
|
</UncontrolledTooltip>
|
||||||
|
|
||||||
<input type="file" accept="text/csv" className="import-servers-btn__csv-select" ref={ref} onChange={onFile} />
|
<input
|
||||||
|
type="file"
|
||||||
|
accept="text/csv"
|
||||||
|
className="import-servers-btn__csv-select"
|
||||||
|
ref={mutableRefToElementRef(ref)}
|
||||||
|
onChange={onFile}
|
||||||
|
/>
|
||||||
|
|
||||||
<DuplicatedServersModal
|
<DuplicatedServersModal
|
||||||
isOpen={isModalOpen}
|
isOpen={isModalOpen}
|
||||||
@@ -89,5 +92,3 @@ const ImportServersBtn = ({ importServersFromFile }: ServersImporter): FC<Import
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ImportServersBtn;
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { FC } from 'react';
|
import { FC } from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import Message from '../../utils/Message';
|
import { Message } from '../../utils/Message';
|
||||||
import ServersListGroup from '../ServersListGroup';
|
import { ServersListGroup } from '../ServersListGroup';
|
||||||
import { DeleteServerButtonProps } from '../DeleteServerButton';
|
import { DeleteServerButtonProps } from '../DeleteServerButton';
|
||||||
import { isServerWithId, SelectedServer, ServersMap } from '../data';
|
import { isServerWithId, SelectedServer, ServersMap } from '../data';
|
||||||
import { NoMenuLayout } from '../../common/NoMenuLayout';
|
import { NoMenuLayout } from '../../common/NoMenuLayout';
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { FC, useEffect } from 'react';
|
import { FC, useEffect } from 'react';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
import Message from '../../utils/Message';
|
import { Message } from '../../utils/Message';
|
||||||
import { isNotFoundServer, SelectedServer } from '../data';
|
import { isNotFoundServer, SelectedServer } from '../data';
|
||||||
import { NoMenuLayout } from '../../common/NoMenuLayout';
|
import { NoMenuLayout } from '../../common/NoMenuLayout';
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { values } from 'ramda';
|
import { values } from 'ramda';
|
||||||
import LocalStorage from '../../utils/services/LocalStorage';
|
import { LocalStorage } from '../../utils/services/LocalStorage';
|
||||||
import { ServersMap, serverWithIdToServerData } from '../data';
|
import { ServersMap, serverWithIdToServerData } from '../data';
|
||||||
import { saveCsv } from '../../utils/helpers/files';
|
import { saveCsv } from '../../utils/helpers/files';
|
||||||
import { JsonToCsv } from '../../utils/helpers/csvjson';
|
import { JsonToCsv } from '../../utils/helpers/csvjson';
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import Bottle from 'bottlejs';
|
import Bottle from 'bottlejs';
|
||||||
import CreateServer from '../CreateServer';
|
import { CreateServer } from '../CreateServer';
|
||||||
import ServersDropdown from '../ServersDropdown';
|
import { ServersDropdown } from '../ServersDropdown';
|
||||||
import DeleteServerModal from '../DeleteServerModal';
|
import { DeleteServerModal } from '../DeleteServerModal';
|
||||||
import DeleteServerButton from '../DeleteServerButton';
|
import { DeleteServerButton } from '../DeleteServerButton';
|
||||||
import { EditServer } from '../EditServer';
|
import { EditServer } from '../EditServer';
|
||||||
import ImportServersBtn from '../helpers/ImportServersBtn';
|
import { ImportServersBtn } from '../helpers/ImportServersBtn';
|
||||||
import { resetSelectedServer, selectServer } from '../reducers/selectedServer';
|
import { resetSelectedServer, selectServer } from '../reducers/selectedServer';
|
||||||
import { createServer, createServers, deleteServer, editServer, setAutoConnect } from '../reducers/servers';
|
import { createServer, createServers, deleteServer, editServer, setAutoConnect } from '../reducers/servers';
|
||||||
import { fetchServers } from '../reducers/remoteServers';
|
import { fetchServers } from '../reducers/remoteServers';
|
||||||
@@ -25,7 +25,7 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
|
|||||||
ManageServers,
|
ManageServers,
|
||||||
'ServersExporter',
|
'ServersExporter',
|
||||||
'ImportServersBtn',
|
'ImportServersBtn',
|
||||||
'useStateFlagTimeout',
|
'useTimeoutToggle',
|
||||||
'ManageServersRow',
|
'ManageServersRow',
|
||||||
);
|
);
|
||||||
bottle.decorator('ManageServers', withoutSelectedServer);
|
bottle.decorator('ManageServers', withoutSelectedServer);
|
||||||
@@ -36,7 +36,7 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
|
|||||||
bottle.serviceFactory('ManageServersRowDropdown', ManageServersRowDropdown, 'DeleteServerModal');
|
bottle.serviceFactory('ManageServersRowDropdown', ManageServersRowDropdown, 'DeleteServerModal');
|
||||||
bottle.decorator('ManageServersRowDropdown', connect(null, ['setAutoConnect']));
|
bottle.decorator('ManageServersRowDropdown', connect(null, ['setAutoConnect']));
|
||||||
|
|
||||||
bottle.serviceFactory('CreateServer', CreateServer, 'ImportServersBtn', 'useStateFlagTimeout');
|
bottle.serviceFactory('CreateServer', CreateServer, 'ImportServersBtn', 'useTimeoutToggle');
|
||||||
bottle.decorator('CreateServer', withoutSelectedServer);
|
bottle.decorator('CreateServer', withoutSelectedServer);
|
||||||
bottle.decorator('CreateServer', connect(['selectedServer', 'servers'], ['createServer', 'resetSelectedServer']));
|
bottle.decorator('CreateServer', connect(['selectedServer', 'servers'], ['createServer', 'resetSelectedServer']));
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import { FormGroup, Input } from 'reactstrap';
|
import { FormGroup, Input } from 'reactstrap';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import ToggleSwitch from '../utils/ToggleSwitch';
|
import { ToggleSwitch } from '../utils/ToggleSwitch';
|
||||||
import { SimpleCard } from '../utils/SimpleCard';
|
import { SimpleCard } from '../utils/SimpleCard';
|
||||||
import { FormText } from '../utils/forms/FormText';
|
import { FormText } from '../utils/forms/FormText';
|
||||||
import { LabeledFormGroup } from '../utils/forms/LabeledFormGroup';
|
import { LabeledFormGroup } from '../utils/forms/LabeledFormGroup';
|
||||||
import { Settings } from './reducers/settings';
|
import { Settings } from './reducers/settings';
|
||||||
|
import { useDomId } from '../utils/helpers/hooks';
|
||||||
|
|
||||||
interface RealTimeUpdatesProps {
|
interface RealTimeUpdatesProps {
|
||||||
settings: Settings;
|
settings: Settings;
|
||||||
@@ -14,43 +15,48 @@ interface RealTimeUpdatesProps {
|
|||||||
|
|
||||||
const intervalValue = (interval?: number) => (!interval ? '' : `${interval}`);
|
const intervalValue = (interval?: number) => (!interval ? '' : `${interval}`);
|
||||||
|
|
||||||
const RealTimeUpdatesSettings = (
|
export const RealTimeUpdatesSettings = (
|
||||||
{ settings: { realTimeUpdates }, toggleRealTimeUpdates, setRealTimeUpdatesInterval }: RealTimeUpdatesProps,
|
{ settings: { realTimeUpdates }, toggleRealTimeUpdates, setRealTimeUpdatesInterval }: RealTimeUpdatesProps,
|
||||||
) => (
|
) => {
|
||||||
<SimpleCard title="Real-time updates" className="h-100">
|
const inputId = useDomId();
|
||||||
<FormGroup>
|
|
||||||
<ToggleSwitch checked={realTimeUpdates.enabled} onChange={toggleRealTimeUpdates}>
|
|
||||||
Enable or disable real-time updates.
|
|
||||||
<FormText>
|
|
||||||
Real-time updates are currently being <b>{realTimeUpdates.enabled ? 'processed' : 'ignored'}</b>.
|
|
||||||
</FormText>
|
|
||||||
</ToggleSwitch>
|
|
||||||
</FormGroup>
|
|
||||||
<LabeledFormGroup
|
|
||||||
noMargin
|
|
||||||
label="Real-time updates frequency (in minutes):"
|
|
||||||
labelClassName={classNames('form-label', { 'text-muted': !realTimeUpdates.enabled })}
|
|
||||||
>
|
|
||||||
<Input
|
|
||||||
type="number"
|
|
||||||
min={0}
|
|
||||||
placeholder="Immediate"
|
|
||||||
disabled={!realTimeUpdates.enabled}
|
|
||||||
value={intervalValue(realTimeUpdates.interval)}
|
|
||||||
onChange={({ target }) => setRealTimeUpdatesInterval(Number(target.value))}
|
|
||||||
/>
|
|
||||||
{realTimeUpdates.enabled && (
|
|
||||||
<FormText>
|
|
||||||
{realTimeUpdates.interval !== undefined && realTimeUpdates.interval > 0 && (
|
|
||||||
<span>
|
|
||||||
Updates will be reflected in the UI every <b>{realTimeUpdates.interval}</b> minute{realTimeUpdates.interval > 1 && 's'}.
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
{!realTimeUpdates.interval && 'Updates will be reflected in the UI as soon as they happen.'}
|
|
||||||
</FormText>
|
|
||||||
)}
|
|
||||||
</LabeledFormGroup>
|
|
||||||
</SimpleCard>
|
|
||||||
);
|
|
||||||
|
|
||||||
export default RealTimeUpdatesSettings;
|
return (
|
||||||
|
<SimpleCard title="Real-time updates" className="h-100">
|
||||||
|
<FormGroup>
|
||||||
|
<ToggleSwitch checked={realTimeUpdates.enabled} onChange={toggleRealTimeUpdates}>
|
||||||
|
Enable or disable real-time updates.
|
||||||
|
<FormText>
|
||||||
|
Real-time updates are currently being <b>{realTimeUpdates.enabled ? 'processed' : 'ignored'}</b>.
|
||||||
|
</FormText>
|
||||||
|
</ToggleSwitch>
|
||||||
|
</FormGroup>
|
||||||
|
<LabeledFormGroup
|
||||||
|
noMargin
|
||||||
|
label="Real-time updates frequency (in minutes):"
|
||||||
|
labelClassName={classNames('form-label', { 'text-muted': !realTimeUpdates.enabled })}
|
||||||
|
id={inputId}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
min={0}
|
||||||
|
placeholder="Immediate"
|
||||||
|
disabled={!realTimeUpdates.enabled}
|
||||||
|
value={intervalValue(realTimeUpdates.interval)}
|
||||||
|
id={inputId}
|
||||||
|
onChange={({ target }) => setRealTimeUpdatesInterval(Number(target.value))}
|
||||||
|
/>
|
||||||
|
{realTimeUpdates.enabled && (
|
||||||
|
<FormText>
|
||||||
|
{realTimeUpdates.interval !== undefined && realTimeUpdates.interval > 0 && (
|
||||||
|
<span>
|
||||||
|
Updates will be reflected in the UI
|
||||||
|
every <b>{realTimeUpdates.interval}</b> minute{realTimeUpdates.interval > 1 && 's'}.
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{!realTimeUpdates.interval && 'Updates will be reflected in the UI as soon as they happen.'}
|
||||||
|
</FormText>
|
||||||
|
)}
|
||||||
|
</LabeledFormGroup>
|
||||||
|
</SimpleCard>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ const SettingsSections: FC<{ items: ReactNode[] }> = ({ items }) => (
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
const Settings = (
|
export const Settings = (
|
||||||
RealTimeUpdates: FC,
|
RealTimeUpdates: FC,
|
||||||
ShortUrlCreation: FC,
|
ShortUrlCreation: FC,
|
||||||
ShortUrlsList: FC,
|
ShortUrlsList: FC,
|
||||||
@@ -32,5 +32,3 @@ const Settings = (
|
|||||||
</Routes>
|
</Routes>
|
||||||
</NoMenuLayout>
|
</NoMenuLayout>
|
||||||
);
|
);
|
||||||
|
|
||||||
export default Settings;
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { FC, ReactNode } from 'react';
|
import { FC, ReactNode } from 'react';
|
||||||
import { DropdownItem, FormGroup } from 'reactstrap';
|
import { DropdownItem, FormGroup } from 'reactstrap';
|
||||||
import { SimpleCard } from '../utils/SimpleCard';
|
import { SimpleCard } from '../utils/SimpleCard';
|
||||||
import ToggleSwitch from '../utils/ToggleSwitch';
|
import { ToggleSwitch } from '../utils/ToggleSwitch';
|
||||||
import { DropdownBtn } from '../utils/DropdownBtn';
|
import { DropdownBtn } from '../utils/DropdownBtn';
|
||||||
import { FormText } from '../utils/forms/FormText';
|
import { FormText } from '../utils/forms/FormText';
|
||||||
import { LabeledFormGroup } from '../utils/forms/LabeledFormGroup';
|
import { LabeledFormGroup } from '../utils/forms/LabeledFormGroup';
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { FC } from 'react';
|
|||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import { faSun, faMoon } from '@fortawesome/free-solid-svg-icons';
|
import { faSun, faMoon } from '@fortawesome/free-solid-svg-icons';
|
||||||
import { SimpleCard } from '../utils/SimpleCard';
|
import { SimpleCard } from '../utils/SimpleCard';
|
||||||
import ToggleSwitch from '../utils/ToggleSwitch';
|
import { ToggleSwitch } from '../utils/ToggleSwitch';
|
||||||
import { changeThemeInMarkup, Theme } from '../utils/theme';
|
import { changeThemeInMarkup, Theme } from '../utils/theme';
|
||||||
import { Settings, UiSettings } from './reducers/settings';
|
import { Settings, UiSettings } from './reducers/settings';
|
||||||
import './UserInterfaceSettings.scss';
|
import './UserInterfaceSettings.scss';
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import Bottle from 'bottlejs';
|
import Bottle from 'bottlejs';
|
||||||
import RealTimeUpdatesSettings from '../RealTimeUpdatesSettings';
|
import { RealTimeUpdatesSettings } from '../RealTimeUpdatesSettings';
|
||||||
import Settings from '../Settings';
|
import { Settings } from '../Settings';
|
||||||
import {
|
import {
|
||||||
setRealTimeUpdatesInterval,
|
setRealTimeUpdatesInterval,
|
||||||
setShortUrlCreationSettings,
|
setShortUrlCreationSettings,
|
||||||
|
|||||||
@@ -33,7 +33,10 @@ const getInitialState = (settings?: ShortUrlCreationSettings): ShortUrlData => (
|
|||||||
forwardQuery: settings?.forwardQuery ?? true,
|
forwardQuery: settings?.forwardQuery ?? true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const CreateShortUrl = (ShortUrlForm: FC<ShortUrlFormProps>, CreateShortUrlResult: FC<CreateShortUrlResultProps>) => ({
|
export const CreateShortUrl = (
|
||||||
|
ShortUrlForm: FC<ShortUrlFormProps>,
|
||||||
|
CreateShortUrlResult: FC<CreateShortUrlResultProps>,
|
||||||
|
) => ({
|
||||||
createShortUrl,
|
createShortUrl,
|
||||||
shortUrlCreationResult,
|
shortUrlCreationResult,
|
||||||
resetCreateShortUrl,
|
resetCreateShortUrl,
|
||||||
@@ -52,7 +55,6 @@ const CreateShortUrl = (ShortUrlForm: FC<ShortUrlFormProps>, CreateShortUrlResul
|
|||||||
mode={basicMode ? 'create-basic' : 'create'}
|
mode={basicMode ? 'create-basic' : 'create'}
|
||||||
onSave={async (data: ShortUrlData) => {
|
onSave={async (data: ShortUrlData) => {
|
||||||
resetCreateShortUrl();
|
resetCreateShortUrl();
|
||||||
|
|
||||||
return createShortUrl(data);
|
return createShortUrl(data);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -64,5 +66,3 @@ const CreateShortUrl = (ShortUrlForm: FC<ShortUrlFormProps>, CreateShortUrlResul
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default CreateShortUrl;
|
|
||||||
|
|||||||
@@ -5,17 +5,18 @@ import { faArrowLeft } from '@fortawesome/free-solid-svg-icons';
|
|||||||
import { ExternalLink } from 'react-external-link';
|
import { ExternalLink } from 'react-external-link';
|
||||||
import { useLocation, useParams } from 'react-router-dom';
|
import { useLocation, useParams } from 'react-router-dom';
|
||||||
import { SelectedServer } from '../servers/data';
|
import { SelectedServer } from '../servers/data';
|
||||||
import { Settings, ShortUrlCreationSettings } from '../settings/reducers/settings';
|
import { Settings } from '../settings/reducers/settings';
|
||||||
import { OptionalString } from '../utils/utils';
|
import { OptionalString } from '../utils/utils';
|
||||||
import { parseQuery } from '../utils/helpers/query';
|
import { parseQuery } from '../utils/helpers/query';
|
||||||
import Message from '../utils/Message';
|
import { Message } from '../utils/Message';
|
||||||
import { Result } from '../utils/Result';
|
import { Result } from '../utils/Result';
|
||||||
import { ShlinkApiError } from '../api/ShlinkApiError';
|
import { ShlinkApiError } from '../api/ShlinkApiError';
|
||||||
import { useGoBack, useToggle } from '../utils/helpers/hooks';
|
import { useGoBack, useToggle } from '../utils/helpers/hooks';
|
||||||
import { ShortUrlFormProps } from './ShortUrlForm';
|
import { ShortUrlFormProps } from './ShortUrlForm';
|
||||||
import { ShortUrlDetail } from './reducers/shortUrlDetail';
|
import { ShortUrlDetail } from './reducers/shortUrlDetail';
|
||||||
import { EditShortUrlData, ShortUrl, ShortUrlData } from './data';
|
import { EditShortUrlData } from './data';
|
||||||
import { ShortUrlEdition } from './reducers/shortUrlEdition';
|
import { ShortUrlEdition } from './reducers/shortUrlEdition';
|
||||||
|
import { shortUrlDataFromShortUrl, urlDecodeShortCode } from './helpers';
|
||||||
|
|
||||||
interface EditShortUrlConnectProps {
|
interface EditShortUrlConnectProps {
|
||||||
settings: Settings;
|
settings: Settings;
|
||||||
@@ -26,27 +27,6 @@ interface EditShortUrlConnectProps {
|
|||||||
editShortUrl: (shortUrl: string, domain: OptionalString, data: EditShortUrlData) => Promise<void>;
|
editShortUrl: (shortUrl: string, domain: OptionalString, data: EditShortUrlData) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const getInitialState = (shortUrl?: ShortUrl, settings?: ShortUrlCreationSettings): ShortUrlData => {
|
|
||||||
const validateUrl = settings?.validateUrls ?? false;
|
|
||||||
|
|
||||||
if (!shortUrl) {
|
|
||||||
return { longUrl: '', validateUrl };
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
longUrl: shortUrl.longUrl,
|
|
||||||
tags: shortUrl.tags,
|
|
||||||
title: shortUrl.title ?? undefined,
|
|
||||||
domain: shortUrl.domain ?? undefined,
|
|
||||||
validSince: shortUrl.meta.validSince ?? undefined,
|
|
||||||
validUntil: shortUrl.meta.validUntil ?? undefined,
|
|
||||||
maxVisits: shortUrl.meta.maxVisits ?? undefined,
|
|
||||||
crawlable: shortUrl.crawlable,
|
|
||||||
forwardQuery: shortUrl.forwardQuery,
|
|
||||||
validateUrl,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const EditShortUrl = (ShortUrlForm: FC<ShortUrlFormProps>) => ({
|
export const EditShortUrl = (ShortUrlForm: FC<ShortUrlFormProps>) => ({
|
||||||
settings: { shortUrlCreation: shortUrlCreationSettings },
|
settings: { shortUrlCreation: shortUrlCreationSettings },
|
||||||
selectedServer,
|
selectedServer,
|
||||||
@@ -62,13 +42,13 @@ export const EditShortUrl = (ShortUrlForm: FC<ShortUrlFormProps>) => ({
|
|||||||
const { saving, error: savingError, errorData: savingErrorData } = shortUrlEdition;
|
const { saving, error: savingError, errorData: savingErrorData } = shortUrlEdition;
|
||||||
const { domain } = parseQuery<{ domain?: string }>(search);
|
const { domain } = parseQuery<{ domain?: string }>(search);
|
||||||
const initialState = useMemo(
|
const initialState = useMemo(
|
||||||
() => getInitialState(shortUrl, shortUrlCreationSettings),
|
() => shortUrlDataFromShortUrl(shortUrl, shortUrlCreationSettings),
|
||||||
[shortUrl, shortUrlCreationSettings],
|
[shortUrl, shortUrlCreationSettings],
|
||||||
);
|
);
|
||||||
const [savingSucceeded,, isSuccessful, isNotSuccessful] = useToggle();
|
const [savingSucceeded,, isSuccessful, isNotSuccessful] = useToggle();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
params.shortCode && getShortUrlDetail(params.shortCode, domain);
|
params.shortCode && getShortUrlDetail(urlDecodeShortCode(params.shortCode), domain);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ interface PaginatorProps {
|
|||||||
currentQueryString?: string;
|
currentQueryString?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Paginator = ({ paginator, serverId, currentQueryString = '' }: PaginatorProps) => {
|
export const Paginator = ({ paginator, serverId, currentQueryString = '' }: PaginatorProps) => {
|
||||||
const { currentPage = 0, pagesCount = 0 } = paginator ?? {};
|
const { currentPage = 0, pagesCount = 0 } = paginator ?? {};
|
||||||
const urlForPage = (pageNumber: NumberOrEllipsis) =>
|
const urlForPage = (pageNumber: NumberOrEllipsis) =>
|
||||||
`/server/${serverId}/list-short-urls/${pageNumber}${currentQueryString}`;
|
`/server/${serverId}/list-short-urls/${pageNumber}${currentQueryString}`;
|
||||||
@@ -49,5 +49,3 @@ const Paginator = ({ paginator, serverId, currentQueryString = '' }: PaginatorPr
|
|||||||
</Pagination>
|
</Pagination>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Paginator;
|
|
||||||
|
|||||||
@@ -3,11 +3,11 @@ import { InputType } from 'reactstrap/types/lib/Input';
|
|||||||
import { Button, FormGroup, Input, Row } from 'reactstrap';
|
import { Button, FormGroup, Input, Row } from 'reactstrap';
|
||||||
import { cond, isEmpty, pipe, replace, trim, T } from 'ramda';
|
import { cond, isEmpty, pipe, replace, trim, T } from 'ramda';
|
||||||
import { parseISO } from 'date-fns';
|
import { parseISO } from 'date-fns';
|
||||||
import DateInput, { DateInputProps } from '../utils/DateInput';
|
import { DateInput, DateInputProps } from '../utils/DateInput';
|
||||||
import { supportsCrawlableVisits, supportsForwardQuery } from '../utils/helpers/features';
|
import { supportsCrawlableVisits, supportsForwardQuery } from '../utils/helpers/features';
|
||||||
import { SimpleCard } from '../utils/SimpleCard';
|
import { SimpleCard } from '../utils/SimpleCard';
|
||||||
import { handleEventPreventingDefault, hasValue, OptionalString } from '../utils/utils';
|
import { handleEventPreventingDefault, hasValue, OptionalString } from '../utils/utils';
|
||||||
import Checkbox from '../utils/Checkbox';
|
import { Checkbox } from '../utils/Checkbox';
|
||||||
import { SelectedServer } from '../servers/data';
|
import { SelectedServer } from '../servers/data';
|
||||||
import { TagsSelectorProps } from '../tags/helpers/TagsSelector';
|
import { TagsSelectorProps } from '../tags/helpers/TagsSelector';
|
||||||
import { DomainSelectorProps } from '../domains/DomainSelector';
|
import { DomainSelectorProps } from '../domains/DomainSelector';
|
||||||
@@ -118,7 +118,7 @@ export const ShortUrlForm = (
|
|||||||
const showBehaviorCard = showCrawlableControl || showForwardQueryControl;
|
const showBehaviorCard = showCrawlableControl || showForwardQueryControl;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form className="short-url-form" onSubmit={submit}>
|
<form name="shortUrlForm" className="short-url-form" onSubmit={submit}>
|
||||||
{isBasicMode && basicComponents}
|
{isBasicMode && basicComponents}
|
||||||
{!isBasicMode && (
|
{!isBasicMode && (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { ShlinkShortUrlsListParams } from '../api/types';
|
|||||||
import { DEFAULT_SHORT_URLS_ORDERING, Settings } from '../settings/reducers/settings';
|
import { DEFAULT_SHORT_URLS_ORDERING, Settings } from '../settings/reducers/settings';
|
||||||
import { ShortUrlsList as ShortUrlsListState } from './reducers/shortUrlsList';
|
import { ShortUrlsList as ShortUrlsListState } from './reducers/shortUrlsList';
|
||||||
import { ShortUrlsTableProps } from './ShortUrlsTable';
|
import { ShortUrlsTableProps } from './ShortUrlsTable';
|
||||||
import Paginator from './Paginator';
|
import { Paginator } from './Paginator';
|
||||||
import { useShortUrlsQuery } from './helpers/hooks';
|
import { useShortUrlsQuery } from './helpers/hooks';
|
||||||
import { ShortUrlsOrderableFields } from './data';
|
import { ShortUrlsOrderableFields } from './data';
|
||||||
import { ShortUrlsFilteringProps } from './ShortUrlsFilteringBar';
|
import { ShortUrlsFilteringProps } from './ShortUrlsFilteringBar';
|
||||||
@@ -23,7 +23,7 @@ interface ShortUrlsListProps {
|
|||||||
settings: Settings;
|
settings: Settings;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ShortUrlsList = (
|
export const ShortUrlsList = (
|
||||||
ShortUrlsTable: FC<ShortUrlsTableProps>,
|
ShortUrlsTable: FC<ShortUrlsTableProps>,
|
||||||
ShortUrlsFilteringBar: FC<ShortUrlsFilteringProps>,
|
ShortUrlsFilteringBar: FC<ShortUrlsFilteringProps>,
|
||||||
) => boundToMercureHub(({ listShortUrls, shortUrlsList, selectedServer, settings }: ShortUrlsListProps) => {
|
) => boundToMercureHub(({ listShortUrls, shortUrlsList, selectedServer, settings }: ShortUrlsListProps) => {
|
||||||
@@ -83,5 +83,3 @@ const ShortUrlsList = (
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}, () => [Topics.visits]);
|
}, () => [Topics.visits]);
|
||||||
|
|
||||||
export default ShortUrlsList;
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { useEffect } from 'react';
|
|||||||
import CopyToClipboard from 'react-copy-to-clipboard';
|
import CopyToClipboard from 'react-copy-to-clipboard';
|
||||||
import { Tooltip } from 'reactstrap';
|
import { Tooltip } from 'reactstrap';
|
||||||
import { ShortUrlCreation } from '../reducers/shortUrlCreation';
|
import { ShortUrlCreation } from '../reducers/shortUrlCreation';
|
||||||
import { StateFlagTimeout } from '../../utils/helpers/hooks';
|
import { TimeoutToggle } from '../../utils/helpers/hooks';
|
||||||
import { Result } from '../../utils/Result';
|
import { Result } from '../../utils/Result';
|
||||||
import './CreateShortUrlResult.scss';
|
import './CreateShortUrlResult.scss';
|
||||||
import { ShlinkApiError } from '../../api/ShlinkApiError';
|
import { ShlinkApiError } from '../../api/ShlinkApiError';
|
||||||
@@ -16,10 +16,10 @@ export interface CreateShortUrlResultProps extends ShortUrlCreation {
|
|||||||
canBeClosed?: boolean;
|
canBeClosed?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const CreateShortUrlResult = (useStateFlagTimeout: StateFlagTimeout) => (
|
export const CreateShortUrlResult = (useTimeoutToggle: TimeoutToggle) => (
|
||||||
{ error, errorData, result, resetCreateShortUrl, canBeClosed = false }: CreateShortUrlResultProps,
|
{ error, errorData, result, resetCreateShortUrl, canBeClosed = false }: CreateShortUrlResultProps,
|
||||||
) => {
|
) => {
|
||||||
const [showCopyTooltip, setShowCopyTooltip] = useStateFlagTimeout();
|
const [showCopyTooltip, setShowCopyTooltip] = useTimeoutToggle();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
resetCreateShortUrl();
|
resetCreateShortUrl();
|
||||||
@@ -43,7 +43,7 @@ const CreateShortUrlResult = (useStateFlagTimeout: StateFlagTimeout) => (
|
|||||||
return (
|
return (
|
||||||
<Result type="success" className="mt-3">
|
<Result type="success" className="mt-3">
|
||||||
{canBeClosed && <FontAwesomeIcon icon={closeIcon} className="float-end pointer" onClick={resetCreateShortUrl} />}
|
{canBeClosed && <FontAwesomeIcon icon={closeIcon} className="float-end pointer" onClick={resetCreateShortUrl} />}
|
||||||
<b>Great!</b> The short URL is <b>{shortUrl}</b>
|
<span><b>Great!</b> The short URL is <b>{shortUrl}</b></span>
|
||||||
|
|
||||||
<CopyToClipboard text={shortUrl} onCopy={setShowCopyTooltip}>
|
<CopyToClipboard text={shortUrl} onCopy={setShowCopyTooltip}>
|
||||||
<button
|
<button
|
||||||
@@ -61,5 +61,3 @@ const CreateShortUrlResult = (useStateFlagTimeout: StateFlagTimeout) => (
|
|||||||
</Result>
|
</Result>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default CreateShortUrlResult;
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ interface DeleteShortUrlModalConnectProps extends ShortUrlModalProps {
|
|||||||
resetDeleteShortUrl: () => void;
|
resetDeleteShortUrl: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const DeleteShortUrlModal = (
|
export const DeleteShortUrlModal = (
|
||||||
{ shortUrl, toggle, isOpen, shortUrlDeletion, resetDeleteShortUrl, deleteShortUrl }: DeleteShortUrlModalConnectProps,
|
{ shortUrl, toggle, isOpen, shortUrlDeletion, resetDeleteShortUrl, deleteShortUrl }: DeleteShortUrlModalConnectProps,
|
||||||
) => {
|
) => {
|
||||||
const [inputValue, setInputValue] = useState('');
|
const [inputValue, setInputValue] = useState('');
|
||||||
@@ -70,5 +70,3 @@ const DeleteShortUrlModal = (
|
|||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default DeleteShortUrlModal;
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ interface QrCodeModalConnectProps extends ShortUrlModalProps {
|
|||||||
selectedServer: SelectedServer;
|
selectedServer: SelectedServer;
|
||||||
}
|
}
|
||||||
|
|
||||||
const QrCodeModal = (imageDownloader: ImageDownloader) => (
|
export const QrCodeModal = (imageDownloader: ImageDownloader) => (
|
||||||
{ shortUrl: { shortUrl, shortCode }, toggle, isOpen, selectedServer }: QrCodeModalConnectProps,
|
{ shortUrl: { shortUrl, shortCode }, toggle, isOpen, selectedServer }: QrCodeModalConnectProps,
|
||||||
) => {
|
) => {
|
||||||
const [size, setSize] = useState(300);
|
const [size, setSize] = useState(300);
|
||||||
@@ -107,5 +107,3 @@ const QrCodeModal = (imageDownloader: ImageDownloader) => (
|
|||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default QrCodeModal;
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { FC } from 'react';
|
|||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { isServerWithId, SelectedServer, ServerWithId } from '../../servers/data';
|
import { isServerWithId, SelectedServer, ServerWithId } from '../../servers/data';
|
||||||
import { ShortUrl } from '../data';
|
import { ShortUrl } from '../data';
|
||||||
|
import { urlEncodeShortCode } from './index';
|
||||||
|
|
||||||
export type LinkSuffix = 'visits' | 'edit';
|
export type LinkSuffix = 'visits' | 'edit';
|
||||||
|
|
||||||
@@ -13,11 +14,10 @@ export interface ShortUrlDetailLinkProps {
|
|||||||
|
|
||||||
const buildUrl = ({ id }: ServerWithId, { shortCode, domain }: ShortUrl, suffix: LinkSuffix) => {
|
const buildUrl = ({ id }: ServerWithId, { shortCode, domain }: ShortUrl, suffix: LinkSuffix) => {
|
||||||
const query = domain ? `?domain=${domain}` : '';
|
const query = domain ? `?domain=${domain}` : '';
|
||||||
|
return `/server/${id}/short-code/${urlEncodeShortCode(shortCode)}/${suffix}${query}`;
|
||||||
return `/server/${id}/short-code/${shortCode}/${suffix}${query}`;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const ShortUrlDetailLink: FC<ShortUrlDetailLinkProps & Record<string | number, any>> = (
|
export const ShortUrlDetailLink: FC<ShortUrlDetailLinkProps & Record<string | number, any>> = (
|
||||||
{ selectedServer, shortUrl, suffix, children, ...rest },
|
{ selectedServer, shortUrl, suffix, children, ...rest },
|
||||||
) => {
|
) => {
|
||||||
if (!selectedServer || !isServerWithId(selectedServer) || !shortUrl) {
|
if (!selectedServer || !isServerWithId(selectedServer) || !shortUrl) {
|
||||||
@@ -26,5 +26,3 @@ const ShortUrlDetailLink: FC<ShortUrlDetailLinkProps & Record<string | number, a
|
|||||||
|
|
||||||
return <Link to={buildUrl(selectedServer, shortUrl, suffix)} {...rest}>{children}</Link>;
|
return <Link to={buildUrl(selectedServer, shortUrl, suffix)} {...rest}>{children}</Link>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ShortUrlDetailLink;
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { ChangeEvent, FC, PropsWithChildren } from 'react';
|
import { ChangeEvent, FC, PropsWithChildren } from 'react';
|
||||||
import Checkbox from '../../utils/Checkbox';
|
import { Checkbox } from '../../utils/Checkbox';
|
||||||
import { InfoTooltip } from '../../utils/InfoTooltip';
|
import { InfoTooltip } from '../../utils/InfoTooltip';
|
||||||
|
|
||||||
type ShortUrlFormCheckboxGroupProps = PropsWithChildren<{
|
type ShortUrlFormCheckboxGroupProps = PropsWithChildren<{
|
||||||
|
|||||||
@@ -6,8 +6,9 @@ import classNames from 'classnames';
|
|||||||
import { prettify } from '../../utils/helpers/numbers';
|
import { prettify } from '../../utils/helpers/numbers';
|
||||||
import { ShortUrl } from '../data';
|
import { ShortUrl } from '../data';
|
||||||
import { SelectedServer } from '../../servers/data';
|
import { SelectedServer } from '../../servers/data';
|
||||||
import ShortUrlDetailLink from './ShortUrlDetailLink';
|
import { ShortUrlDetailLink } from './ShortUrlDetailLink';
|
||||||
import './ShortUrlVisitsCount.scss';
|
import './ShortUrlVisitsCount.scss';
|
||||||
|
import { mutableRefToElementRef } from '../../utils/helpers/components';
|
||||||
|
|
||||||
interface ShortUrlVisitsCountProps {
|
interface ShortUrlVisitsCountProps {
|
||||||
shortUrl?: ShortUrl | null;
|
shortUrl?: ShortUrl | null;
|
||||||
@@ -16,7 +17,9 @@ interface ShortUrlVisitsCountProps {
|
|||||||
active?: boolean;
|
active?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ShortUrlVisitsCount = ({ visitsCount, shortUrl, selectedServer, active = false }: ShortUrlVisitsCountProps) => {
|
export const ShortUrlVisitsCount = (
|
||||||
|
{ visitsCount, shortUrl, selectedServer, active = false }: ShortUrlVisitsCountProps,
|
||||||
|
) => {
|
||||||
const maxVisits = shortUrl?.meta?.maxVisits;
|
const maxVisits = shortUrl?.meta?.maxVisits;
|
||||||
const visitsLink = (
|
const visitsLink = (
|
||||||
<ShortUrlDetailLink selectedServer={selectedServer} shortUrl={shortUrl} suffix="visits">
|
<ShortUrlDetailLink selectedServer={selectedServer} shortUrl={shortUrl} suffix="visits">
|
||||||
@@ -33,7 +36,7 @@ const ShortUrlVisitsCount = ({ visitsCount, shortUrl, selectedServer, active = f
|
|||||||
}
|
}
|
||||||
|
|
||||||
const prettifiedMaxVisits = prettify(maxVisits);
|
const prettifiedMaxVisits = prettify(maxVisits);
|
||||||
const tooltipRef = useRef<HTMLElement | null>();
|
const tooltipRef = useRef<HTMLElement | undefined>();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -41,9 +44,7 @@ const ShortUrlVisitsCount = ({ visitsCount, shortUrl, selectedServer, active = f
|
|||||||
{visitsLink}
|
{visitsLink}
|
||||||
<small
|
<small
|
||||||
className="short-urls-visits-count__max-visits-control"
|
className="short-urls-visits-count__max-visits-control"
|
||||||
ref={(el) => {
|
ref={mutableRefToElementRef(tooltipRef)}
|
||||||
tooltipRef.current = el;
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{' '}/ {prettifiedMaxVisits}{' '}
|
{' '}/ {prettifiedMaxVisits}{' '}
|
||||||
<sup>
|
<sup>
|
||||||
@@ -57,5 +58,3 @@ const ShortUrlVisitsCount = ({ visitsCount, shortUrl, selectedServer, active = f
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ShortUrlVisitsCount;
|
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import { FC, useEffect, useRef } from 'react';
|
import { FC, useEffect, useRef } from 'react';
|
||||||
import { isEmpty } from 'ramda';
|
import { isEmpty } from 'ramda';
|
||||||
import { ExternalLink } from 'react-external-link';
|
import { ExternalLink } from 'react-external-link';
|
||||||
import ColorGenerator from '../../utils/services/ColorGenerator';
|
import { ColorGenerator } from '../../utils/services/ColorGenerator';
|
||||||
import { StateFlagTimeout } from '../../utils/helpers/hooks';
|
import { TimeoutToggle } from '../../utils/helpers/hooks';
|
||||||
import Tag from '../../tags/helpers/Tag';
|
import { Tag } from '../../tags/helpers/Tag';
|
||||||
import { SelectedServer } from '../../servers/data';
|
import { SelectedServer } from '../../servers/data';
|
||||||
import { CopyToClipboardIcon } from '../../utils/CopyToClipboardIcon';
|
import { CopyToClipboardIcon } from '../../utils/CopyToClipboardIcon';
|
||||||
import { ShortUrl } from '../data';
|
import { ShortUrl } from '../data';
|
||||||
import { Time } from '../../utils/Time';
|
import { Time } from '../../utils/Time';
|
||||||
import ShortUrlVisitsCount from './ShortUrlVisitsCount';
|
import { ShortUrlVisitsCount } from './ShortUrlVisitsCount';
|
||||||
import { ShortUrlsRowMenuProps } from './ShortUrlsRowMenu';
|
import { ShortUrlsRowMenuProps } from './ShortUrlsRowMenu';
|
||||||
import './ShortUrlsRow.scss';
|
import './ShortUrlsRow.scss';
|
||||||
|
|
||||||
@@ -18,13 +18,13 @@ export interface ShortUrlsRowProps {
|
|||||||
shortUrl: ShortUrl;
|
shortUrl: ShortUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ShortUrlsRow = (
|
export const ShortUrlsRow = (
|
||||||
ShortUrlsRowMenu: FC<ShortUrlsRowMenuProps>,
|
ShortUrlsRowMenu: FC<ShortUrlsRowMenuProps>,
|
||||||
colorGenerator: ColorGenerator,
|
colorGenerator: ColorGenerator,
|
||||||
useStateFlagTimeout: StateFlagTimeout,
|
useTimeoutToggle: TimeoutToggle,
|
||||||
) => ({ shortUrl, selectedServer, onTagClick }: ShortUrlsRowProps) => {
|
) => ({ shortUrl, selectedServer, onTagClick }: ShortUrlsRowProps) => {
|
||||||
const [copiedToClipboard, setCopiedToClipboard] = useStateFlagTimeout();
|
const [copiedToClipboard, setCopiedToClipboard] = useTimeoutToggle();
|
||||||
const [active, setActive] = useStateFlagTimeout(false, 500);
|
const [active, setActive] = useTimeoutToggle(false, 500);
|
||||||
const isFirstRun = useRef(true);
|
const isFirstRun = useRef(true);
|
||||||
|
|
||||||
const renderTags = (tags: string[]) => {
|
const renderTags = (tags: string[]) => {
|
||||||
@@ -87,5 +87,3 @@ const ShortUrlsRow = (
|
|||||||
</tr>
|
</tr>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ShortUrlsRow;
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { useToggle } from '../../utils/helpers/hooks';
|
|||||||
import { ShortUrl, ShortUrlModalProps } from '../data';
|
import { ShortUrl, ShortUrlModalProps } from '../data';
|
||||||
import { SelectedServer } from '../../servers/data';
|
import { SelectedServer } from '../../servers/data';
|
||||||
import { DropdownBtnMenu } from '../../utils/DropdownBtnMenu';
|
import { DropdownBtnMenu } from '../../utils/DropdownBtnMenu';
|
||||||
import ShortUrlDetailLink from './ShortUrlDetailLink';
|
import { ShortUrlDetailLink } from './ShortUrlDetailLink';
|
||||||
|
|
||||||
export interface ShortUrlsRowMenuProps {
|
export interface ShortUrlsRowMenuProps {
|
||||||
selectedServer: SelectedServer;
|
selectedServer: SelectedServer;
|
||||||
@@ -19,7 +19,7 @@ export interface ShortUrlsRowMenuProps {
|
|||||||
}
|
}
|
||||||
type ShortUrlModal = FC<ShortUrlModalProps>;
|
type ShortUrlModal = FC<ShortUrlModalProps>;
|
||||||
|
|
||||||
const ShortUrlsRowMenu = (
|
export const ShortUrlsRowMenu = (
|
||||||
DeleteShortUrlModal: ShortUrlModal,
|
DeleteShortUrlModal: ShortUrlModal,
|
||||||
QrCodeModal: ShortUrlModal,
|
QrCodeModal: ShortUrlModal,
|
||||||
) => ({ shortUrl, selectedServer }: ShortUrlsRowMenuProps) => {
|
) => ({ shortUrl, selectedServer }: ShortUrlsRowMenuProps) => {
|
||||||
@@ -51,5 +51,3 @@ const ShortUrlsRowMenu = (
|
|||||||
</DropdownBtnMenu>
|
</DropdownBtnMenu>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ShortUrlsRowMenu;
|
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { isNil } from 'ramda';
|
import { isNil } from 'ramda';
|
||||||
import { ShortUrl } from '../data';
|
import { ShortUrl, ShortUrlData } from '../data';
|
||||||
import { OptionalString } from '../../utils/utils';
|
import { OptionalString } from '../../utils/utils';
|
||||||
import { DEFAULT_DOMAIN } from '../../visits/reducers/domainVisits';
|
import { DEFAULT_DOMAIN } from '../../visits/reducers/domainVisits';
|
||||||
|
import { ShortUrlCreationSettings } from '../../settings/reducers/settings';
|
||||||
|
|
||||||
export const shortUrlMatches = (shortUrl: ShortUrl, shortCode: string, domain: OptionalString): boolean => {
|
export const shortUrlMatches = (shortUrl: ShortUrl, shortCode: string, domain: OptionalString): boolean => {
|
||||||
if (isNil(domain)) {
|
if (isNil(domain)) {
|
||||||
@@ -18,3 +19,30 @@ export const domainMatches = (shortUrl: ShortUrl, domain: string): boolean => {
|
|||||||
|
|
||||||
return shortUrl.domain === domain;
|
return shortUrl.domain === domain;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const shortUrlDataFromShortUrl = (shortUrl?: ShortUrl, settings?: ShortUrlCreationSettings): ShortUrlData => {
|
||||||
|
const validateUrl = settings?.validateUrls ?? false;
|
||||||
|
|
||||||
|
if (!shortUrl) {
|
||||||
|
return { longUrl: '', validateUrl };
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
longUrl: shortUrl.longUrl,
|
||||||
|
tags: shortUrl.tags,
|
||||||
|
title: shortUrl.title ?? undefined,
|
||||||
|
domain: shortUrl.domain ?? undefined,
|
||||||
|
validSince: shortUrl.meta.validSince ?? undefined,
|
||||||
|
validUntil: shortUrl.meta.validUntil ?? undefined,
|
||||||
|
maxVisits: shortUrl.meta.maxVisits ?? undefined,
|
||||||
|
crawlable: shortUrl.crawlable,
|
||||||
|
forwardQuery: shortUrl.forwardQuery,
|
||||||
|
validateUrl,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const MULTI_SEGMENT_SEPARATOR = '__';
|
||||||
|
|
||||||
|
export const urlEncodeShortCode = (shortCode: string): string => shortCode.replaceAll('/', MULTI_SEGMENT_SEPARATOR);
|
||||||
|
|
||||||
|
export const urlDecodeShortCode = (shortCode: string): string => shortCode.replaceAll(MULTI_SEGMENT_SEPARATOR, '/');
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
import Bottle from 'bottlejs';
|
import Bottle from 'bottlejs';
|
||||||
import { ShortUrlsFilteringBar } from '../ShortUrlsFilteringBar';
|
import { ShortUrlsFilteringBar } from '../ShortUrlsFilteringBar';
|
||||||
import ShortUrlsList from '../ShortUrlsList';
|
import { ShortUrlsList } from '../ShortUrlsList';
|
||||||
import ShortUrlsRow from '../helpers/ShortUrlsRow';
|
import { ShortUrlsRow } from '../helpers/ShortUrlsRow';
|
||||||
import ShortUrlsRowMenu from '../helpers/ShortUrlsRowMenu';
|
import { ShortUrlsRowMenu } from '../helpers/ShortUrlsRowMenu';
|
||||||
import CreateShortUrl from '../CreateShortUrl';
|
import { CreateShortUrl } from '../CreateShortUrl';
|
||||||
import DeleteShortUrlModal from '../helpers/DeleteShortUrlModal';
|
import { DeleteShortUrlModal } from '../helpers/DeleteShortUrlModal';
|
||||||
import CreateShortUrlResult from '../helpers/CreateShortUrlResult';
|
import { CreateShortUrlResult } from '../helpers/CreateShortUrlResult';
|
||||||
import { listShortUrls } from '../reducers/shortUrlsList';
|
import { listShortUrls } from '../reducers/shortUrlsList';
|
||||||
import { createShortUrl, resetCreateShortUrl } from '../reducers/shortUrlCreation';
|
import { createShortUrl, resetCreateShortUrl } from '../reducers/shortUrlCreation';
|
||||||
import { deleteShortUrl, resetDeleteShortUrl } from '../reducers/shortUrlDeletion';
|
import { deleteShortUrl, resetDeleteShortUrl } from '../reducers/shortUrlDeletion';
|
||||||
import { editShortUrl } from '../reducers/shortUrlEdition';
|
import { editShortUrl } from '../reducers/shortUrlEdition';
|
||||||
import { ConnectDecorator } from '../../container/types';
|
import { ConnectDecorator } from '../../container/types';
|
||||||
import { ShortUrlsTable } from '../ShortUrlsTable';
|
import { ShortUrlsTable } from '../ShortUrlsTable';
|
||||||
import QrCodeModal from '../helpers/QrCodeModal';
|
import { QrCodeModal } from '../helpers/QrCodeModal';
|
||||||
import { ShortUrlForm } from '../ShortUrlForm';
|
import { ShortUrlForm } from '../ShortUrlForm';
|
||||||
import { EditShortUrl } from '../EditShortUrl';
|
import { EditShortUrl } from '../EditShortUrl';
|
||||||
import { getShortUrlDetail } from '../reducers/shortUrlDetail';
|
import { getShortUrlDetail } from '../reducers/shortUrlDetail';
|
||||||
@@ -27,9 +27,9 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
|
|||||||
));
|
));
|
||||||
|
|
||||||
bottle.serviceFactory('ShortUrlsTable', ShortUrlsTable, 'ShortUrlsRow');
|
bottle.serviceFactory('ShortUrlsTable', ShortUrlsTable, 'ShortUrlsRow');
|
||||||
bottle.serviceFactory('ShortUrlsRow', ShortUrlsRow, 'ShortUrlsRowMenu', 'ColorGenerator', 'useStateFlagTimeout');
|
bottle.serviceFactory('ShortUrlsRow', ShortUrlsRow, 'ShortUrlsRowMenu', 'ColorGenerator', 'useTimeoutToggle');
|
||||||
bottle.serviceFactory('ShortUrlsRowMenu', ShortUrlsRowMenu, 'DeleteShortUrlModal', 'QrCodeModal');
|
bottle.serviceFactory('ShortUrlsRowMenu', ShortUrlsRowMenu, 'DeleteShortUrlModal', 'QrCodeModal');
|
||||||
bottle.serviceFactory('CreateShortUrlResult', CreateShortUrlResult, 'useStateFlagTimeout');
|
bottle.serviceFactory('CreateShortUrlResult', CreateShortUrlResult, 'useTimeoutToggle');
|
||||||
bottle.serviceFactory('ShortUrlForm', ShortUrlForm, 'TagsSelector', 'DomainSelector');
|
bottle.serviceFactory('ShortUrlForm', ShortUrlForm, 'TagsSelector', 'DomainSelector');
|
||||||
|
|
||||||
bottle.serviceFactory('CreateShortUrl', CreateShortUrl, 'ShortUrlForm', 'CreateShortUrlResult');
|
bottle.serviceFactory('CreateShortUrl', CreateShortUrl, 'ShortUrlForm', 'CreateShortUrlResult');
|
||||||
|
|||||||
@@ -5,11 +5,12 @@ import { FC, useEffect, useRef } from 'react';
|
|||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { prettify } from '../utils/helpers/numbers';
|
import { prettify } from '../utils/helpers/numbers';
|
||||||
import { useToggle } from '../utils/helpers/hooks';
|
import { useToggle } from '../utils/helpers/hooks';
|
||||||
import ColorGenerator from '../utils/services/ColorGenerator';
|
import { ColorGenerator } from '../utils/services/ColorGenerator';
|
||||||
import { getServerId, SelectedServer } from '../servers/data';
|
import { getServerId, SelectedServer } from '../servers/data';
|
||||||
import TagBullet from './helpers/TagBullet';
|
import { TagBullet } from './helpers/TagBullet';
|
||||||
import { NormalizedTag, TagModalProps } from './data';
|
import { NormalizedTag, TagModalProps } from './data';
|
||||||
import './TagCard.scss';
|
import './TagCard.scss';
|
||||||
|
import { mutableRefToElementRef } from '../utils/helpers/components';
|
||||||
|
|
||||||
export interface TagCardProps {
|
export interface TagCardProps {
|
||||||
tag: NormalizedTag;
|
tag: NormalizedTag;
|
||||||
@@ -20,7 +21,7 @@ export interface TagCardProps {
|
|||||||
|
|
||||||
const isTruncated = (el: HTMLElement | undefined): boolean => !!el && el.scrollWidth > el.clientWidth;
|
const isTruncated = (el: HTMLElement | undefined): boolean => !!el && el.scrollWidth > el.clientWidth;
|
||||||
|
|
||||||
const TagCard = (
|
export const TagCard = (
|
||||||
DeleteTagConfirmModal: FC<TagModalProps>,
|
DeleteTagConfirmModal: FC<TagModalProps>,
|
||||||
EditTagModal: FC<TagModalProps>,
|
EditTagModal: FC<TagModalProps>,
|
||||||
colorGenerator: ColorGenerator,
|
colorGenerator: ColorGenerator,
|
||||||
@@ -28,7 +29,7 @@ const TagCard = (
|
|||||||
const [isDeleteModalOpen, toggleDelete] = useToggle();
|
const [isDeleteModalOpen, toggleDelete] = useToggle();
|
||||||
const [isEditModalOpen, toggleEdit] = useToggle();
|
const [isEditModalOpen, toggleEdit] = useToggle();
|
||||||
const [hasTitle,, displayTitle] = useToggle();
|
const [hasTitle,, displayTitle] = useToggle();
|
||||||
const titleRef = useRef<HTMLElement>();
|
const titleRef = useRef<HTMLHeadingElement | undefined>();
|
||||||
const serverId = getServerId(selectedServer);
|
const serverId = getServerId(selectedServer);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -40,18 +41,22 @@ const TagCard = (
|
|||||||
return (
|
return (
|
||||||
<Card className="tag-card">
|
<Card className="tag-card">
|
||||||
<CardHeader className="tag-card__header">
|
<CardHeader className="tag-card__header">
|
||||||
<Button color="link" size="sm" className="tag-card__btn tag-card__btn--last" onClick={toggleDelete}>
|
<Button
|
||||||
|
aria-label="Delete tag"
|
||||||
|
color="link"
|
||||||
|
size="sm"
|
||||||
|
className="tag-card__btn tag-card__btn--last"
|
||||||
|
onClick={toggleDelete}
|
||||||
|
>
|
||||||
<FontAwesomeIcon icon={deleteIcon} />
|
<FontAwesomeIcon icon={deleteIcon} />
|
||||||
</Button>
|
</Button>
|
||||||
<Button color="link" size="sm" className="tag-card__btn" onClick={toggleEdit}>
|
<Button aria-label="Edit tag" color="link" size="sm" className="tag-card__btn" onClick={toggleEdit}>
|
||||||
<FontAwesomeIcon icon={editIcon} />
|
<FontAwesomeIcon icon={editIcon} />
|
||||||
</Button>
|
</Button>
|
||||||
<h5
|
<h5
|
||||||
className="tag-card__tag-title text-ellipsis"
|
className="tag-card__tag-title text-ellipsis"
|
||||||
title={hasTitle ? tag.tag : undefined}
|
title={hasTitle ? tag.tag : undefined}
|
||||||
ref={(el) => {
|
ref={mutableRefToElementRef(titleRef)}
|
||||||
titleRef.current = el ?? undefined;
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<TagBullet tag={tag.tag} colorGenerator={colorGenerator} />
|
<TagBullet tag={tag.tag} colorGenerator={colorGenerator} />
|
||||||
<span className="tag-card__tag-name" onClick={toggle}>{tag.tag}</span>
|
<span className="tag-card__tag-name" onClick={toggle}>{tag.tag}</span>
|
||||||
@@ -82,5 +87,3 @@ const TagCard = (
|
|||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default TagCard;
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { FC, useEffect, useState } from 'react';
|
import { FC, useEffect, useState } from 'react';
|
||||||
import { Row } from 'reactstrap';
|
import { Row } from 'reactstrap';
|
||||||
import { pipe } from 'ramda';
|
import { pipe } from 'ramda';
|
||||||
import Message from '../utils/Message';
|
import { Message } from '../utils/Message';
|
||||||
import { SearchField } from '../utils/SearchField';
|
import { SearchField } from '../utils/SearchField';
|
||||||
import { SelectedServer } from '../servers/data';
|
import { SelectedServer } from '../servers/data';
|
||||||
import { boundToMercureHub } from '../mercure/helpers/boundToMercureHub';
|
import { boundToMercureHub } from '../mercure/helpers/boundToMercureHub';
|
||||||
@@ -30,7 +30,7 @@ export interface TagsListProps {
|
|||||||
settings: Settings;
|
settings: Settings;
|
||||||
}
|
}
|
||||||
|
|
||||||
const TagsList = (TagsCards: FC<TagsListChildrenProps>, TagsTable: FC<TagsTableProps>) => boundToMercureHub((
|
export const TagsList = (TagsCards: FC<TagsListChildrenProps>, TagsTable: FC<TagsTableProps>) => boundToMercureHub((
|
||||||
{ filterTags, forceListTags, tagsList, selectedServer, settings }: TagsListProps,
|
{ filterTags, forceListTags, tagsList, selectedServer, settings }: TagsListProps,
|
||||||
) => {
|
) => {
|
||||||
const [mode, setMode] = useState<TagsMode>(settings.tags?.defaultMode ?? 'cards');
|
const [mode, setMode] = useState<TagsMode>(settings.tags?.defaultMode ?? 'cards');
|
||||||
@@ -104,5 +104,3 @@ const TagsList = (TagsCards: FC<TagsListChildrenProps>, TagsTable: FC<TagsTableP
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}, () => [Topics.visits]);
|
}, () => [Topics.visits]);
|
||||||
|
|
||||||
export default TagsList;
|
|
||||||
|
|||||||
@@ -4,11 +4,11 @@ import { DropdownItem } from 'reactstrap';
|
|||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import { faTrash as deleteIcon, faPencilAlt as editIcon } from '@fortawesome/free-solid-svg-icons';
|
import { faTrash as deleteIcon, faPencilAlt as editIcon } from '@fortawesome/free-solid-svg-icons';
|
||||||
import { getServerId, SelectedServer } from '../servers/data';
|
import { getServerId, SelectedServer } from '../servers/data';
|
||||||
import ColorGenerator from '../utils/services/ColorGenerator';
|
import { ColorGenerator } from '../utils/services/ColorGenerator';
|
||||||
import { prettify } from '../utils/helpers/numbers';
|
import { prettify } from '../utils/helpers/numbers';
|
||||||
import { useToggle } from '../utils/helpers/hooks';
|
import { useToggle } from '../utils/helpers/hooks';
|
||||||
import { DropdownBtnMenu } from '../utils/DropdownBtnMenu';
|
import { DropdownBtnMenu } from '../utils/DropdownBtnMenu';
|
||||||
import TagBullet from './helpers/TagBullet';
|
import { TagBullet } from './helpers/TagBullet';
|
||||||
import { NormalizedTag, TagModalProps } from './data';
|
import { NormalizedTag, TagModalProps } from './data';
|
||||||
|
|
||||||
export interface TagsTableRowProps {
|
export interface TagsTableRowProps {
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ interface DeleteTagConfirmModalProps extends TagModalProps {
|
|||||||
tagDelete: TagDeletion;
|
tagDelete: TagDeletion;
|
||||||
}
|
}
|
||||||
|
|
||||||
const DeleteTagConfirmModal = (
|
export const DeleteTagConfirmModal = (
|
||||||
{ tag, toggle, isOpen, deleteTag, tagDelete, tagDeleted }: DeleteTagConfirmModalProps,
|
{ tag, toggle, isOpen, deleteTag, tagDelete, tagDeleted }: DeleteTagConfirmModalProps,
|
||||||
) => {
|
) => {
|
||||||
const { deleting, error, errorData } = tagDelete;
|
const { deleting, error, errorData } = tagDelete;
|
||||||
@@ -22,9 +22,7 @@ const DeleteTagConfirmModal = (
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal toggle={toggle} isOpen={isOpen} centered>
|
<Modal toggle={toggle} isOpen={isOpen} centered>
|
||||||
<ModalHeader toggle={toggle}>
|
<ModalHeader toggle={toggle} className="text-danger">Delete tag</ModalHeader>
|
||||||
<span className="text-danger">Delete tag</span>
|
|
||||||
</ModalHeader>
|
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
Are you sure you want to delete tag <b>{tag}</b>?
|
Are you sure you want to delete tag <b>{tag}</b>?
|
||||||
{error && (
|
{error && (
|
||||||
@@ -42,5 +40,3 @@ const DeleteTagConfirmModal = (
|
|||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default DeleteTagConfirmModal;
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { faPalette as colorIcon } from '@fortawesome/free-solid-svg-icons';
|
|||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import { useToggle } from '../../utils/helpers/hooks';
|
import { useToggle } from '../../utils/helpers/hooks';
|
||||||
import { handleEventPreventingDefault } from '../../utils/utils';
|
import { handleEventPreventingDefault } from '../../utils/utils';
|
||||||
import ColorGenerator from '../../utils/services/ColorGenerator';
|
import { ColorGenerator } from '../../utils/services/ColorGenerator';
|
||||||
import { TagModalProps } from '../data';
|
import { TagModalProps } from '../data';
|
||||||
import { TagEdition } from '../reducers/tagEdit';
|
import { TagEdition } from '../reducers/tagEdit';
|
||||||
import { Result } from '../../utils/Result';
|
import { Result } from '../../utils/Result';
|
||||||
@@ -18,7 +18,7 @@ interface EditTagModalProps extends TagModalProps {
|
|||||||
tagEdited: (oldName: string, newName: string, color: string) => void;
|
tagEdited: (oldName: string, newName: string, color: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const EditTagModal = ({ getColorForKey }: ColorGenerator) => (
|
export const EditTagModal = ({ getColorForKey }: ColorGenerator) => (
|
||||||
{ tag, editTag, toggle, tagEdited, isOpen, tagEdit }: EditTagModalProps,
|
{ tag, editTag, toggle, tagEdited, isOpen, tagEdit }: EditTagModalProps,
|
||||||
) => {
|
) => {
|
||||||
const [newTagName, setNewTagName] = useState(tag);
|
const [newTagName, setNewTagName] = useState(tag);
|
||||||
@@ -34,7 +34,7 @@ const EditTagModal = ({ getColorForKey }: ColorGenerator) => (
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal isOpen={isOpen} toggle={toggle} centered onClosed={hideColorPicker}>
|
<Modal isOpen={isOpen} toggle={toggle} centered onClosed={hideColorPicker}>
|
||||||
<form onSubmit={saveTag}>
|
<form name="editTag" onSubmit={saveTag}>
|
||||||
<ModalHeader toggle={toggle}>Edit tag</ModalHeader>
|
<ModalHeader toggle={toggle}>Edit tag</ModalHeader>
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
<InputGroup>
|
<InputGroup>
|
||||||
@@ -78,5 +78,3 @@ const EditTagModal = ({ getColorForKey }: ColorGenerator) => (
|
|||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default EditTagModal;
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { FC, MouseEventHandler, PropsWithChildren } from 'react';
|
import { FC, MouseEventHandler, PropsWithChildren } from 'react';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import ColorGenerator from '../../utils/services/ColorGenerator';
|
import { ColorGenerator } from '../../utils/services/ColorGenerator';
|
||||||
import './Tag.scss';
|
import './Tag.scss';
|
||||||
|
|
||||||
type TagProps = PropsWithChildren<{
|
type TagProps = PropsWithChildren<{
|
||||||
@@ -12,15 +12,15 @@ type TagProps = PropsWithChildren<{
|
|||||||
onClose?: MouseEventHandler;
|
onClose?: MouseEventHandler;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
const Tag: FC<TagProps> = ({ text, children, clearable, className = '', colorGenerator, onClick, onClose }) => (
|
export const Tag: FC<TagProps> = ({ text, children, clearable, className = '', colorGenerator, onClick, onClose }) => (
|
||||||
<span
|
<span
|
||||||
className={classNames('badge tag', className, { 'tag--light-bg': colorGenerator.isColorLightForKey(text) })}
|
className={classNames('badge tag', className, { 'tag--light-bg': colorGenerator.isColorLightForKey(text) })}
|
||||||
style={{ backgroundColor: colorGenerator.getColorForKey(text), cursor: clearable || !onClick ? 'auto' : 'pointer' }}
|
style={{ backgroundColor: colorGenerator.getColorForKey(text), cursor: clearable || !onClick ? 'auto' : 'pointer' }}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
>
|
>
|
||||||
{children ?? text}
|
{children ?? text}
|
||||||
{clearable && <span className="close tag__close-selected-tag" onClick={onClose}>×</span>}
|
{clearable && (
|
||||||
|
<span aria-label={`Remove ${text}`} className="close tag__close-selected-tag" onClick={onClose}>×</span>
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
|
|
||||||
export default Tag;
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import ColorGenerator from '../../utils/services/ColorGenerator';
|
import { ColorGenerator } from '../../utils/services/ColorGenerator';
|
||||||
import './TagBullet.scss';
|
import './TagBullet.scss';
|
||||||
|
|
||||||
interface TagBulletProps {
|
interface TagBulletProps {
|
||||||
@@ -6,11 +6,9 @@ interface TagBulletProps {
|
|||||||
colorGenerator: ColorGenerator;
|
colorGenerator: ColorGenerator;
|
||||||
}
|
}
|
||||||
|
|
||||||
const TagBullet = ({ tag, colorGenerator }: TagBulletProps) => (
|
export const TagBullet = ({ tag, colorGenerator }: TagBulletProps) => (
|
||||||
<div
|
<div
|
||||||
style={{ backgroundColor: colorGenerator.getColorForKey(tag) }}
|
style={{ backgroundColor: colorGenerator.getColorForKey(tag) }}
|
||||||
className="tag-bullet"
|
className="tag-bullet"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
export default TagBullet;
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import ReactTags, { SuggestionComponentProps, TagComponentProps } from 'react-tag-autocomplete';
|
import ReactTags, { SuggestionComponentProps, TagComponentProps } from 'react-tag-autocomplete';
|
||||||
import ColorGenerator from '../../utils/services/ColorGenerator';
|
import { ColorGenerator } from '../../utils/services/ColorGenerator';
|
||||||
import { Settings } from '../../settings/reducers/settings';
|
import { Settings } from '../../settings/reducers/settings';
|
||||||
import { TagsList } from '../reducers/tagsList';
|
import { TagsList } from '../reducers/tagsList';
|
||||||
import TagBullet from './TagBullet';
|
import { TagBullet } from './TagBullet';
|
||||||
import Tag from './Tag';
|
import { Tag } from './Tag';
|
||||||
|
|
||||||
export interface TagsSelectorProps {
|
export interface TagsSelectorProps {
|
||||||
selectedTags: string[];
|
selectedTags: string[];
|
||||||
@@ -21,7 +21,7 @@ interface TagsSelectorConnectProps extends TagsSelectorProps {
|
|||||||
|
|
||||||
const toComponentTag = (tag: string) => ({ id: tag, name: tag });
|
const toComponentTag = (tag: string) => ({ id: tag, name: tag });
|
||||||
|
|
||||||
const TagsSelector = (colorGenerator: ColorGenerator) => (
|
export const TagsSelector = (colorGenerator: ColorGenerator) => (
|
||||||
{ selectedTags, onChange, placeholder, listTags, tagsList, settings, allowNew = true }: TagsSelectorConnectProps,
|
{ selectedTags, onChange, placeholder, listTags, tagsList, settings, allowNew = true }: TagsSelectorConnectProps,
|
||||||
) => {
|
) => {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -68,5 +68,3 @@ const TagsSelector = (colorGenerator: ColorGenerator) => (
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default TagsSelector;
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { pick } from 'ramda';
|
|||||||
import { Action, Dispatch } from 'redux';
|
import { Action, Dispatch } from 'redux';
|
||||||
import { buildReducer } from '../../utils/helpers/redux';
|
import { buildReducer } from '../../utils/helpers/redux';
|
||||||
import { GetState } from '../../container/types';
|
import { GetState } from '../../container/types';
|
||||||
import ColorGenerator from '../../utils/services/ColorGenerator';
|
import { ColorGenerator } from '../../utils/services/ColorGenerator';
|
||||||
import { ShlinkApiClientBuilder } from '../../api/services/ShlinkApiClientBuilder';
|
import { ShlinkApiClientBuilder } from '../../api/services/ShlinkApiClientBuilder';
|
||||||
import { ProblemDetailsError } from '../../api/types';
|
import { ProblemDetailsError } from '../../api/types';
|
||||||
import { parseApiError } from '../../api/utils';
|
import { parseApiError } from '../../api/utils';
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ export default buildReducer<TagsList, TagsCombinedAction>({
|
|||||||
}),
|
}),
|
||||||
[FILTER_TAGS]: (state, { searchTerm }) => ({
|
[FILTER_TAGS]: (state, { searchTerm }) => ({
|
||||||
...state,
|
...state,
|
||||||
filteredTags: state.tags.filter((tag) => tag.toLowerCase().match(searchTerm)),
|
filteredTags: state.tags.filter((tag) => tag.toLowerCase().match(searchTerm.toLowerCase())),
|
||||||
}),
|
}),
|
||||||
[CREATE_VISITS]: (state, { createdVisits }) => ({
|
[CREATE_VISITS]: (state, { createdVisits }) => ({
|
||||||
...state,
|
...state,
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import Bottle, { IContainer } from 'bottlejs';
|
import Bottle, { IContainer } from 'bottlejs';
|
||||||
import TagsSelector from '../helpers/TagsSelector';
|
import { TagsSelector } from '../helpers/TagsSelector';
|
||||||
import TagCard from '../TagCard';
|
import { TagCard } from '../TagCard';
|
||||||
import DeleteTagConfirmModal from '../helpers/DeleteTagConfirmModal';
|
import { DeleteTagConfirmModal } from '../helpers/DeleteTagConfirmModal';
|
||||||
import EditTagModal from '../helpers/EditTagModal';
|
import { EditTagModal } from '../helpers/EditTagModal';
|
||||||
import TagsList from '../TagsList';
|
import { TagsList } from '../TagsList';
|
||||||
import { filterTags, listTags } from '../reducers/tagsList';
|
import { filterTags, listTags } from '../reducers/tagsList';
|
||||||
import { deleteTag, tagDeleted } from '../reducers/tagDelete';
|
import { deleteTag, tagDeleted } from '../reducers/tagDelete';
|
||||||
import { editTag, tagEdited } from '../reducers/tagEdit';
|
import { editTag, tagEdited } from '../reducers/tagEdit';
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ $darkBorderInputColor: $darkBorderColor;
|
|||||||
$darkTableHighlightColor: $darkBorderColor;
|
$darkTableHighlightColor: $darkBorderColor;
|
||||||
|
|
||||||
html:not([data-theme='dark']) {
|
html:not([data-theme='dark']) {
|
||||||
|
--color-scheme: initial;
|
||||||
--primary-color: #{$lightPrimaryColor};
|
--primary-color: #{$lightPrimaryColor};
|
||||||
--primary-color-alfa: #{$lightPrimaryColorAlfa};
|
--primary-color-alfa: #{$lightPrimaryColorAlfa};
|
||||||
--secondary-color: #{$lightSecondaryColor};
|
--secondary-color: #{$lightSecondaryColor};
|
||||||
@@ -48,6 +49,7 @@ html:not([data-theme='dark']) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
html[data-theme='dark'] {
|
html[data-theme='dark'] {
|
||||||
|
--color-scheme: dark;
|
||||||
--primary-color: #{$darkPrimaryColor};
|
--primary-color: #{$darkPrimaryColor};
|
||||||
--primary-color-alfa: #{$darkPrimaryColorAlfa};
|
--primary-color-alfa: #{$darkPrimaryColorAlfa};
|
||||||
--secondary-color: #{$darkSecondaryColor};
|
--secondary-color: #{$darkSecondaryColor};
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ interface BooleanControlWithTypeProps extends BooleanControlProps {
|
|||||||
type: 'switch' | 'checkbox';
|
type: 'switch' | 'checkbox';
|
||||||
}
|
}
|
||||||
|
|
||||||
const BooleanControl: FC<BooleanControlWithTypeProps> = (
|
export const BooleanControl: FC<BooleanControlWithTypeProps> = (
|
||||||
{ checked = false, onChange = identity, className, children, type, inline = false },
|
{ checked = false, onChange = identity, className, children, type, inline = false },
|
||||||
) => {
|
) => {
|
||||||
const id = useDomId();
|
const id = useDomId();
|
||||||
@@ -32,5 +32,3 @@ const BooleanControl: FC<BooleanControlWithTypeProps> = (
|
|||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default BooleanControl;
|
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
import { FC } from 'react';
|
import { FC } from 'react';
|
||||||
import BooleanControl, { BooleanControlProps } from './BooleanControl';
|
import { BooleanControl, BooleanControlProps } from './BooleanControl';
|
||||||
|
|
||||||
const Checkbox: FC<BooleanControlProps> = (props) => <BooleanControl type="checkbox" {...props} />;
|
export const Checkbox: FC<BooleanControlProps> = (props) => <BooleanControl type="checkbox" {...props} />;
|
||||||
|
|
||||||
export default Checkbox;
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import './DateInput.scss';
|
|||||||
|
|
||||||
export type DateInputProps = ReactDatePickerProps;
|
export type DateInputProps = ReactDatePickerProps;
|
||||||
|
|
||||||
const DateInput = (props: DateInputProps) => {
|
export const DateInput = (props: DateInputProps) => {
|
||||||
const { className, isClearable, selected } = props;
|
const { className, isClearable, selected } = props;
|
||||||
const showCalendarIcon = !isClearable || isNil(selected);
|
const showCalendarIcon = !isClearable || isNil(selected);
|
||||||
const ref = useRef<{ input: HTMLInputElement }>();
|
const ref = useRef<{ input: HTMLInputElement }>();
|
||||||
@@ -32,5 +32,3 @@ const DateInput = (props: DateInputProps) => {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default DateInput;
|
|
||||||
|
|||||||
@@ -3,21 +3,19 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
|||||||
import { faInfoCircle as infoIcon } from '@fortawesome/free-solid-svg-icons';
|
import { faInfoCircle as infoIcon } from '@fortawesome/free-solid-svg-icons';
|
||||||
import { UncontrolledTooltip } from 'reactstrap';
|
import { UncontrolledTooltip } from 'reactstrap';
|
||||||
import { Placement } from '@popperjs/core';
|
import { Placement } from '@popperjs/core';
|
||||||
|
import { mutableRefToElementRef } from './helpers/components';
|
||||||
|
|
||||||
type InfoTooltipProps = PropsWithChildren<{
|
export type InfoTooltipProps = PropsWithChildren<{
|
||||||
className?: string;
|
className?: string;
|
||||||
placement: Placement;
|
placement: Placement;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
export const InfoTooltip: FC<InfoTooltipProps> = ({ className = '', placement, children }) => {
|
export const InfoTooltip: FC<InfoTooltipProps> = ({ className = '', placement, children }) => {
|
||||||
const ref = useRef<HTMLSpanElement | null>();
|
const ref = useRef<HTMLSpanElement | undefined>();
|
||||||
const refCallback = (el: HTMLSpanElement) => {
|
|
||||||
ref.current = el;
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<span className={className} ref={refCallback}>
|
<span className={className} ref={mutableRefToElementRef(ref)}>
|
||||||
<FontAwesomeIcon icon={infoIcon} />
|
<FontAwesomeIcon icon={infoIcon} />
|
||||||
</span>
|
</span>
|
||||||
<UncontrolledTooltip target={(() => ref.current) as any} placement={placement}>{children}</UncontrolledTooltip>
|
<UncontrolledTooltip target={(() => ref.current) as any} placement={placement}>{children}</UncontrolledTooltip>
|
||||||
|
|||||||
@@ -30,7 +30,9 @@ export type MessageProps = PropsWithChildren<{
|
|||||||
type?: MessageType;
|
type?: MessageType;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
const Message: FC<MessageProps> = ({ className, children, loading = false, type = 'default', fullWidth = false }) => {
|
export const Message: FC<MessageProps> = (
|
||||||
|
{ className, children, loading = false, type = 'default', fullWidth = false },
|
||||||
|
) => {
|
||||||
const classes = classNames({
|
const classes = classNames({
|
||||||
'col-md-12': fullWidth,
|
'col-md-12': fullWidth,
|
||||||
'col-md-10 offset-md-1': !fullWidth,
|
'col-md-10 offset-md-1': !fullWidth,
|
||||||
@@ -50,5 +52,3 @@ const Message: FC<MessageProps> = ({ className, children, loading = false, type
|
|||||||
</Row>
|
</Row>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Message;
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ interface PaginationDropdownProps {
|
|||||||
toggleClassName?: string;
|
toggleClassName?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const PaginationDropdown = ({ toggleClassName, ranges, value, setValue }: PaginationDropdownProps) => (
|
export const PaginationDropdown = ({ toggleClassName, ranges, value, setValue }: PaginationDropdownProps) => (
|
||||||
<UncontrolledDropdown>
|
<UncontrolledDropdown>
|
||||||
<DropdownToggle caret color="link" className={toggleClassName}>Paginate</DropdownToggle>
|
<DropdownToggle caret color="link" className={toggleClassName}>Paginate</DropdownToggle>
|
||||||
<DropdownMenu end>
|
<DropdownMenu end>
|
||||||
@@ -23,5 +23,3 @@ const PaginationDropdown = ({ toggleClassName, ranges, value, setValue }: Pagina
|
|||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</UncontrolledDropdown>
|
</UncontrolledDropdown>
|
||||||
);
|
);
|
||||||
|
|
||||||
export default PaginationDropdown;
|
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ export const Result: FC<ResultProps> = ({ children, type, className, small = fal
|
|||||||
<Row className={className}>
|
<Row className={className}>
|
||||||
<div className={classNames({ 'col-md-10 offset-md-1': !small, 'col-12': small })}>
|
<div className={classNames({ 'col-md-10 offset-md-1': !small, 'col-12': small })}>
|
||||||
<SimpleCard
|
<SimpleCard
|
||||||
|
role="document"
|
||||||
className={classNames('text-center', {
|
className={classNames('text-center', {
|
||||||
'bg-main': type === 'success',
|
'bg-main': type === 'success',
|
||||||
'bg-danger': type === 'error',
|
'bg-danger': type === 'error',
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import { parseISO, format as formatDate, getUnixTime, formatDistance } from 'date-fns';
|
import { parseISO, format as formatDate, getUnixTime, formatDistance } from 'date-fns';
|
||||||
import { isDateObject } from './helpers/date';
|
import { isDateObject } from './helpers/date';
|
||||||
|
|
||||||
export interface DateProps {
|
export interface TimeProps {
|
||||||
date: Date | string;
|
date: Date | string;
|
||||||
format?: string;
|
format?: string;
|
||||||
relative?: boolean;
|
relative?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Time = ({ date, format = 'yyyy-MM-dd HH:mm', relative = false }: DateProps) => {
|
export const Time = ({ date, format = 'yyyy-MM-dd HH:mm', relative = false }: TimeProps) => {
|
||||||
const dateObject = isDateObject(date) ? date : parseISO(date);
|
const dateObject = isDateObject(date) ? date : parseISO(date);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
import { FC } from 'react';
|
import { FC } from 'react';
|
||||||
import BooleanControl, { BooleanControlProps } from './BooleanControl';
|
import { BooleanControl, BooleanControlProps } from './BooleanControl';
|
||||||
|
|
||||||
const ToggleSwitch: FC<BooleanControlProps> = (props) => <BooleanControl type="switch" {...props} />;
|
export const ToggleSwitch: FC<BooleanControlProps> = (props) => <BooleanControl type="switch" {...props} />;
|
||||||
|
|
||||||
export default ToggleSwitch;
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { endOfDay } from 'date-fns';
|
import { endOfDay } from 'date-fns';
|
||||||
import DateInput from '../DateInput';
|
import { DateInput } from '../DateInput';
|
||||||
import { DateRange } from './types';
|
import { DateRange } from './types';
|
||||||
|
|
||||||
interface DateRangeRowProps extends DateRange {
|
interface DateRangeRowProps extends DateRange {
|
||||||
@@ -8,7 +8,7 @@ interface DateRangeRowProps extends DateRange {
|
|||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const DateRangeRow = (
|
export const DateRangeRow = (
|
||||||
{ startDate = null, endDate = null, disabled = false, onStartDateChange, onEndDateChange }: DateRangeRowProps,
|
{ startDate = null, endDate = null, disabled = false, onStartDateChange, onEndDateChange }: DateRangeRowProps,
|
||||||
) => (
|
) => (
|
||||||
<div className="row">
|
<div className="row">
|
||||||
@@ -35,5 +35,3 @@ const DateRangeRow = (
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
export default DateRangeRow;
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
rangeIsInterval,
|
rangeIsInterval,
|
||||||
dateRangeIsEmpty,
|
dateRangeIsEmpty,
|
||||||
} from './types';
|
} from './types';
|
||||||
import DateRangeRow from './DateRangeRow';
|
import { DateRangeRow } from './DateRangeRow';
|
||||||
import { DateIntervalDropdownItems } from './DateIntervalDropdownItems';
|
import { DateIntervalDropdownItems } from './DateIntervalDropdownItems';
|
||||||
|
|
||||||
export interface DateRangeSelectorProps {
|
export interface DateRangeSelectorProps {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { FC, PropsWithChildren } from 'react';
|
import { FC, PropsWithChildren } from 'react';
|
||||||
import { InputType } from 'reactstrap/types/lib/Input';
|
import { InputType } from 'reactstrap/types/lib/Input';
|
||||||
import { LabeledFormGroup } from './LabeledFormGroup';
|
import { LabeledFormGroup } from './LabeledFormGroup';
|
||||||
|
import { useDomId } from '../helpers/hooks';
|
||||||
|
|
||||||
export type InputFormGroupProps = PropsWithChildren<{
|
export type InputFormGroupProps = PropsWithChildren<{
|
||||||
value: string;
|
value: string;
|
||||||
@@ -14,15 +15,20 @@ export type InputFormGroupProps = PropsWithChildren<{
|
|||||||
|
|
||||||
export const InputFormGroup: FC<InputFormGroupProps> = (
|
export const InputFormGroup: FC<InputFormGroupProps> = (
|
||||||
{ children, value, onChange, type, required, placeholder, className, labelClassName },
|
{ children, value, onChange, type, required, placeholder, className, labelClassName },
|
||||||
) => (
|
) => {
|
||||||
<LabeledFormGroup label={<>{children}:</>} className={className ?? ''} labelClassName={labelClassName}>
|
const id = useDomId();
|
||||||
<input
|
|
||||||
className="form-control"
|
return (
|
||||||
type={type ?? 'text'}
|
<LabeledFormGroup label={<>{children}:</>} className={className ?? ''} labelClassName={labelClassName} id={id}>
|
||||||
value={value}
|
<input
|
||||||
required={required ?? true}
|
id={id}
|
||||||
placeholder={placeholder}
|
className="form-control"
|
||||||
onChange={(e) => onChange(e.target.value)}
|
type={type ?? 'text'}
|
||||||
/>
|
value={value}
|
||||||
</LabeledFormGroup>
|
required={required ?? true}
|
||||||
);
|
placeholder={placeholder}
|
||||||
|
onChange={(e) => onChange(e.target.value)}
|
||||||
|
/>
|
||||||
|
</LabeledFormGroup>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
@@ -5,14 +5,15 @@ type LabeledFormGroupProps = PropsWithChildren<{
|
|||||||
noMargin?: boolean;
|
noMargin?: boolean;
|
||||||
className?: string;
|
className?: string;
|
||||||
labelClassName?: string;
|
labelClassName?: string;
|
||||||
|
id?: string;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
/* eslint-disable jsx-a11y/label-has-associated-control */
|
/* eslint-disable jsx-a11y/label-has-associated-control */
|
||||||
export const LabeledFormGroup: FC<LabeledFormGroupProps> = (
|
export const LabeledFormGroup: FC<LabeledFormGroupProps> = (
|
||||||
{ children, label, className = '', labelClassName = '', noMargin = false },
|
{ children, label, className = '', labelClassName = '', noMargin = false, id },
|
||||||
) => (
|
) => (
|
||||||
<div className={`${className} ${noMargin ? '' : 'mb-3'}`}>
|
<div className={`${className} ${noMargin ? '' : 'mb-3'}`}>
|
||||||
<label className={`form-label ${labelClassName}`}>{label}</label>
|
<label className={`form-label ${labelClassName}`} htmlFor={id}>{label}</label>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -11,8 +11,6 @@ export const pointerOnHover = ({ native }: ChartEvent, [firstElement]: ActiveEle
|
|||||||
canvas.style.cursor = firstElement ? 'pointer' : 'default';
|
canvas.style.cursor = firstElement ? 'pointer' : 'default';
|
||||||
};
|
};
|
||||||
|
|
||||||
export const renderChartLabel = ({ dataset, formattedValue }: TooltipItem<ChartType>) =>
|
export const renderChartLabel = ({ dataset, raw }: TooltipItem<ChartType>) => `${dataset.label}: ${prettify(`${raw}`)}`;
|
||||||
`${dataset.label}: ${prettify(formattedValue)}`;
|
|
||||||
|
|
||||||
export const renderPieChartLabel = ({ label, formattedValue }: TooltipItem<ChartType>) =>
|
export const renderPieChartLabel = ({ label, raw }: TooltipItem<ChartType>) => `${label}: ${prettify(`${raw}`)}`;
|
||||||
`${label}: ${prettify(formattedValue)}`;
|
|
||||||
|
|||||||
5
src/utils/helpers/components.ts
Normal file
5
src/utils/helpers/components.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import { MutableRefObject, Ref } from 'react';
|
||||||
|
|
||||||
|
export const mutableRefToElementRef = <T>(ref: MutableRefObject<T | undefined>): Ref<T> => (el) => {
|
||||||
|
ref.current = el ?? undefined; // eslint-disable-line no-param-reassign
|
||||||
|
};
|
||||||
@@ -6,12 +6,12 @@ import { parseQuery, stringifyQuery } from './query';
|
|||||||
|
|
||||||
const DEFAULT_DELAY = 2000;
|
const DEFAULT_DELAY = 2000;
|
||||||
|
|
||||||
export type StateFlagTimeout = (initialValue?: boolean, delay?: number) => [ boolean, () => void ];
|
export type TimeoutToggle = (initialValue?: boolean, delay?: number) => [boolean, () => void];
|
||||||
|
|
||||||
export const useStateFlagTimeout = (
|
export const useTimeoutToggle = (
|
||||||
setTimeout: (callback: Function, timeout: number) => number,
|
setTimeout: (callback: Function, timeout: number) => number,
|
||||||
clearTimeout: (timer: number) => void,
|
clearTimeout: (timer: number) => void,
|
||||||
): StateFlagTimeout => (initialValue = false, delay = DEFAULT_DELAY) => {
|
): TimeoutToggle => (initialValue = false, delay = DEFAULT_DELAY) => {
|
||||||
const [flag, setFlag] = useState<boolean>(initialValue);
|
const [flag, setFlag] = useState<boolean>(initialValue);
|
||||||
const timeout = useRef<number | undefined>(undefined);
|
const timeout = useRef<number | undefined>(undefined);
|
||||||
const callback = () => {
|
const callback = () => {
|
||||||
@@ -31,7 +31,6 @@ type ToggleResult = [ boolean, () => void, () => void, () => void ];
|
|||||||
|
|
||||||
export const useToggle = (initialValue = false): ToggleResult => {
|
export const useToggle = (initialValue = false): ToggleResult => {
|
||||||
const [flag, setFlag] = useState<boolean>(initialValue);
|
const [flag, setFlag] = useState<boolean>(initialValue);
|
||||||
|
|
||||||
return [flag, () => setFlag(!flag), () => setFlag(true), () => setFlag(false)];
|
return [flag, () => setFlag(!flag), () => setFlag(true), () => setFlag(false)];
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -80,7 +79,6 @@ export const useEffectExceptFirstTime = (callback: EffectCallback, deps: Depende
|
|||||||
|
|
||||||
export const useGoBack = () => {
|
export const useGoBack = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
return () => navigate(-1);
|
return () => navigate(-1);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { isNil } from 'ramda';
|
import { isNil } from 'ramda';
|
||||||
import { rangeOf } from '../utils';
|
import { rangeOf } from '../utils';
|
||||||
import LocalStorage from './LocalStorage';
|
import { LocalStorage } from './LocalStorage';
|
||||||
|
|
||||||
const HEX_COLOR_LENGTH = 6;
|
const HEX_COLOR_LENGTH = 6;
|
||||||
const HEX_DIGITS = '0123456789ABCDEF';
|
const HEX_DIGITS = '0123456789ABCDEF';
|
||||||
@@ -15,7 +15,7 @@ const hexColorToRgbArray = (colorHex: string): number[] =>
|
|||||||
// HSP by Darel Rex Finley https://alienryderflex.com/hsp.html
|
// HSP by Darel Rex Finley https://alienryderflex.com/hsp.html
|
||||||
const perceivedLightness = (r = 0, g = 0, b = 0) => round(sqrt(0.299 * r ** 2 + 0.587 * g ** 2 + 0.114 * b ** 2));
|
const perceivedLightness = (r = 0, g = 0, b = 0) => round(sqrt(0.299 * r ** 2 + 0.587 * g ** 2 + 0.114 * b ** 2));
|
||||||
|
|
||||||
export default class ColorGenerator {
|
export class ColorGenerator {
|
||||||
private readonly colors: Record<string, string>;
|
private readonly colors: Record<string, string>;
|
||||||
private readonly lights: Record<string, boolean>;
|
private readonly lights: Record<string, boolean>;
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
const PREFIX = 'shlink';
|
const PREFIX = 'shlink';
|
||||||
const buildPath = (path: string) => `${PREFIX}.${path}`;
|
const buildPath = (path: string) => `${PREFIX}.${path}`;
|
||||||
|
|
||||||
export default class LocalStorage {
|
export class LocalStorage {
|
||||||
public constructor(private readonly localStorage: Storage) {}
|
public constructor(private readonly localStorage: Storage) {}
|
||||||
|
|
||||||
public readonly get = <T>(key: string): T | undefined => {
|
public readonly get = <T>(key: string): T | undefined => {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import Bottle from 'bottlejs';
|
import Bottle from 'bottlejs';
|
||||||
import { useStateFlagTimeout } from '../helpers/hooks';
|
import { useTimeoutToggle } from '../helpers/hooks';
|
||||||
import LocalStorage from './LocalStorage';
|
import { LocalStorage } from './LocalStorage';
|
||||||
import ColorGenerator from './ColorGenerator';
|
import { ColorGenerator } from './ColorGenerator';
|
||||||
import { csvToJson, jsonToCsv } from '../helpers/csvjson';
|
import { csvToJson, jsonToCsv } from '../helpers/csvjson';
|
||||||
|
|
||||||
const provideServices = (bottle: Bottle) => {
|
const provideServices = (bottle: Bottle) => {
|
||||||
@@ -14,7 +14,7 @@ const provideServices = (bottle: Bottle) => {
|
|||||||
|
|
||||||
bottle.constant('setTimeout', global.setTimeout);
|
bottle.constant('setTimeout', global.setTimeout);
|
||||||
bottle.constant('clearTimeout', global.clearTimeout);
|
bottle.constant('clearTimeout', global.clearTimeout);
|
||||||
bottle.serviceFactory('useStateFlagTimeout', useStateFlagTimeout, 'setTimeout', 'clearTimeout');
|
bottle.serviceFactory('useTimeoutToggle', useTimeoutToggle, 'setTimeout', 'clearTimeout');
|
||||||
};
|
};
|
||||||
|
|
||||||
export default provideServices;
|
export default provideServices;
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ import { Topics } from '../mercure/helpers/Topics';
|
|||||||
import { useGoBack } from '../utils/helpers/hooks';
|
import { useGoBack } from '../utils/helpers/hooks';
|
||||||
import { toApiParams } from './types/helpers';
|
import { toApiParams } from './types/helpers';
|
||||||
import { NormalizedVisit } from './types';
|
import { NormalizedVisit } from './types';
|
||||||
import VisitsStats from './VisitsStats';
|
import { VisitsStats } from './VisitsStats';
|
||||||
import VisitsHeader from './VisitsHeader';
|
import { VisitsHeader } from './VisitsHeader';
|
||||||
|
|
||||||
export interface DomainVisitsProps extends CommonVisitsProps {
|
export interface DomainVisitsProps extends CommonVisitsProps {
|
||||||
getDomainVisits: (domain: string, query?: ShlinkVisitsParams, doIntervalFallback?: boolean) => void;
|
getDomainVisits: (domain: string, query?: ShlinkVisitsParams, doIntervalFallback?: boolean) => void;
|
||||||
|
|||||||
@@ -3,11 +3,11 @@ import { ShlinkVisitsParams } from '../api/types';
|
|||||||
import { Topics } from '../mercure/helpers/Topics';
|
import { Topics } from '../mercure/helpers/Topics';
|
||||||
import { useGoBack } from '../utils/helpers/hooks';
|
import { useGoBack } from '../utils/helpers/hooks';
|
||||||
import { ReportExporter } from '../common/services/ReportExporter';
|
import { ReportExporter } from '../common/services/ReportExporter';
|
||||||
import VisitsStats from './VisitsStats';
|
import { VisitsStats } from './VisitsStats';
|
||||||
import { NormalizedVisit, VisitsInfo, VisitsParams } from './types';
|
import { NormalizedVisit, VisitsInfo, VisitsParams } from './types';
|
||||||
import { CommonVisitsProps } from './types/CommonVisitsProps';
|
import { CommonVisitsProps } from './types/CommonVisitsProps';
|
||||||
import { toApiParams } from './types/helpers';
|
import { toApiParams } from './types/helpers';
|
||||||
import VisitsHeader from './VisitsHeader';
|
import { VisitsHeader } from './VisitsHeader';
|
||||||
|
|
||||||
export interface NonOrphanVisitsProps extends CommonVisitsProps {
|
export interface NonOrphanVisitsProps extends CommonVisitsProps {
|
||||||
getNonOrphanVisits: (params?: ShlinkVisitsParams, doIntervalFallback?: boolean) => void;
|
getNonOrphanVisits: (params?: ShlinkVisitsParams, doIntervalFallback?: boolean) => void;
|
||||||
|
|||||||
@@ -3,11 +3,11 @@ import { ShlinkVisitsParams } from '../api/types';
|
|||||||
import { Topics } from '../mercure/helpers/Topics';
|
import { Topics } from '../mercure/helpers/Topics';
|
||||||
import { useGoBack } from '../utils/helpers/hooks';
|
import { useGoBack } from '../utils/helpers/hooks';
|
||||||
import { ReportExporter } from '../common/services/ReportExporter';
|
import { ReportExporter } from '../common/services/ReportExporter';
|
||||||
import VisitsStats from './VisitsStats';
|
import { VisitsStats } from './VisitsStats';
|
||||||
import { NormalizedVisit, OrphanVisitType, VisitsInfo, VisitsParams } from './types';
|
import { NormalizedVisit, OrphanVisitType, VisitsInfo, VisitsParams } from './types';
|
||||||
import { CommonVisitsProps } from './types/CommonVisitsProps';
|
import { CommonVisitsProps } from './types/CommonVisitsProps';
|
||||||
import { toApiParams } from './types/helpers';
|
import { toApiParams } from './types/helpers';
|
||||||
import VisitsHeader from './VisitsHeader';
|
import { VisitsHeader } from './VisitsHeader';
|
||||||
|
|
||||||
export interface OrphanVisitsProps extends CommonVisitsProps {
|
export interface OrphanVisitsProps extends CommonVisitsProps {
|
||||||
getOrphanVisits: (
|
getOrphanVisits: (
|
||||||
|
|||||||
@@ -8,11 +8,12 @@ import { ShortUrlDetail } from '../short-urls/reducers/shortUrlDetail';
|
|||||||
import { useGoBack } from '../utils/helpers/hooks';
|
import { useGoBack } from '../utils/helpers/hooks';
|
||||||
import { ReportExporter } from '../common/services/ReportExporter';
|
import { ReportExporter } from '../common/services/ReportExporter';
|
||||||
import { ShortUrlVisits as ShortUrlVisitsState } from './reducers/shortUrlVisits';
|
import { ShortUrlVisits as ShortUrlVisitsState } from './reducers/shortUrlVisits';
|
||||||
import ShortUrlVisitsHeader from './ShortUrlVisitsHeader';
|
import { ShortUrlVisitsHeader } from './ShortUrlVisitsHeader';
|
||||||
import VisitsStats from './VisitsStats';
|
import { VisitsStats } from './VisitsStats';
|
||||||
import { NormalizedVisit, VisitsParams } from './types';
|
import { NormalizedVisit, VisitsParams } from './types';
|
||||||
import { CommonVisitsProps } from './types/CommonVisitsProps';
|
import { CommonVisitsProps } from './types/CommonVisitsProps';
|
||||||
import { toApiParams } from './types/helpers';
|
import { toApiParams } from './types/helpers';
|
||||||
|
import { urlDecodeShortCode } from '../short-urls/helpers';
|
||||||
|
|
||||||
export interface ShortUrlVisitsProps extends CommonVisitsProps {
|
export interface ShortUrlVisitsProps extends CommonVisitsProps {
|
||||||
getShortUrlVisits: (shortCode: string, query?: ShlinkVisitsParams, doIntervalFallback?: boolean) => void;
|
getShortUrlVisits: (shortCode: string, query?: ShlinkVisitsParams, doIntervalFallback?: boolean) => void;
|
||||||
@@ -22,7 +23,7 @@ export interface ShortUrlVisitsProps extends CommonVisitsProps {
|
|||||||
cancelGetShortUrlVisits: () => void;
|
cancelGetShortUrlVisits: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ShortUrlVisits = ({ exportVisits }: ReportExporter) => boundToMercureHub(({
|
export const ShortUrlVisits = ({ exportVisits }: ReportExporter) => boundToMercureHub(({
|
||||||
shortUrlVisits,
|
shortUrlVisits,
|
||||||
shortUrlDetail,
|
shortUrlDetail,
|
||||||
getShortUrlVisits,
|
getShortUrlVisits,
|
||||||
@@ -36,14 +37,14 @@ const ShortUrlVisits = ({ exportVisits }: ReportExporter) => boundToMercureHub((
|
|||||||
const goBack = useGoBack();
|
const goBack = useGoBack();
|
||||||
const { domain } = parseQuery<{ domain?: string }>(search);
|
const { domain } = parseQuery<{ domain?: string }>(search);
|
||||||
const loadVisits = (params: VisitsParams, doIntervalFallback?: boolean) =>
|
const loadVisits = (params: VisitsParams, doIntervalFallback?: boolean) =>
|
||||||
getShortUrlVisits(shortCode, { ...toApiParams(params), domain }, doIntervalFallback);
|
getShortUrlVisits(urlDecodeShortCode(shortCode), { ...toApiParams(params), domain }, doIntervalFallback);
|
||||||
const exportCsv = (visits: NormalizedVisit[]) => exportVisits(
|
const exportCsv = (visits: NormalizedVisit[]) => exportVisits(
|
||||||
`short-url_${shortUrlDetail.shortUrl?.shortUrl.replace(/https?:\/\//g, '')}_visits.csv`,
|
`short-url_${shortUrlDetail.shortUrl?.shortUrl.replace(/https?:\/\//g, '')}_visits.csv`,
|
||||||
visits,
|
visits,
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getShortUrlDetail(shortCode, domain);
|
getShortUrlDetail(urlDecodeShortCode(shortCode), domain);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -59,6 +60,4 @@ const ShortUrlVisits = ({ exportVisits }: ReportExporter) => boundToMercureHub((
|
|||||||
<ShortUrlVisitsHeader shortUrlDetail={shortUrlDetail} shortUrlVisits={shortUrlVisits} goBack={goBack} />
|
<ShortUrlVisitsHeader shortUrlDetail={shortUrlDetail} shortUrlVisits={shortUrlVisits} goBack={goBack} />
|
||||||
</VisitsStats>
|
</VisitsStats>
|
||||||
);
|
);
|
||||||
}, (_, params) => [Topics.shortUrlVisits(params.shortCode)]);
|
}, (_, params) => (params.shortCode ? [Topics.shortUrlVisits(urlDecodeShortCode(params.shortCode))] : []));
|
||||||
|
|
||||||
export default ShortUrlVisits;
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { ExternalLink } from 'react-external-link';
|
|||||||
import { ShortUrlDetail } from '../short-urls/reducers/shortUrlDetail';
|
import { ShortUrlDetail } from '../short-urls/reducers/shortUrlDetail';
|
||||||
import { Time } from '../utils/Time';
|
import { Time } from '../utils/Time';
|
||||||
import { ShortUrlVisits } from './reducers/shortUrlVisits';
|
import { ShortUrlVisits } from './reducers/shortUrlVisits';
|
||||||
import VisitsHeader from './VisitsHeader';
|
import { VisitsHeader } from './VisitsHeader';
|
||||||
import './ShortUrlVisitsHeader.scss';
|
import './ShortUrlVisitsHeader.scss';
|
||||||
|
|
||||||
interface ShortUrlVisitsHeaderProps {
|
interface ShortUrlVisitsHeaderProps {
|
||||||
@@ -12,7 +12,7 @@ interface ShortUrlVisitsHeaderProps {
|
|||||||
goBack: () => void;
|
goBack: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ShortUrlVisitsHeader = ({ shortUrlDetail, shortUrlVisits, goBack }: ShortUrlVisitsHeaderProps) => {
|
export const ShortUrlVisitsHeader = ({ shortUrlDetail, shortUrlVisits, goBack }: ShortUrlVisitsHeaderProps) => {
|
||||||
const { shortUrl, loading } = shortUrlDetail;
|
const { shortUrl, loading } = shortUrlDetail;
|
||||||
const { visits } = shortUrlVisits;
|
const { visits } = shortUrlVisits;
|
||||||
const shortLink = shortUrl?.shortUrl ?? '';
|
const shortLink = shortUrl?.shortUrl ?? '';
|
||||||
@@ -35,7 +35,7 @@ const ShortUrlVisitsHeader = ({ shortUrlDetail, shortUrlVisits, goBack }: ShortU
|
|||||||
<VisitsHeader title={visitsStatsTitle} goBack={goBack} visits={visits} shortUrl={shortUrl}>
|
<VisitsHeader title={visitsStatsTitle} goBack={goBack} visits={visits} shortUrl={shortUrl}>
|
||||||
<hr />
|
<hr />
|
||||||
<div>Created: {renderDate()}</div>
|
<div>Created: {renderDate()}</div>
|
||||||
<div>
|
<div className="long-url-container">
|
||||||
{`${title ? 'Title' : 'Long URL'}: `}
|
{`${title ? 'Title' : 'Long URL'}: `}
|
||||||
{loading && <small>Loading...</small>}
|
{loading && <small>Loading...</small>}
|
||||||
{!loading && <ExternalLink href={longLink}>{title ?? longLink}</ExternalLink>}
|
{!loading && <ExternalLink href={longLink}>{title ?? longLink}</ExternalLink>}
|
||||||
@@ -43,5 +43,3 @@ const ShortUrlVisitsHeader = ({ shortUrlDetail, shortUrlVisits, goBack }: ShortU
|
|||||||
</VisitsHeader>
|
</VisitsHeader>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ShortUrlVisitsHeader;
|
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
import { boundToMercureHub } from '../mercure/helpers/boundToMercureHub';
|
import { boundToMercureHub } from '../mercure/helpers/boundToMercureHub';
|
||||||
import ColorGenerator from '../utils/services/ColorGenerator';
|
import { ColorGenerator } from '../utils/services/ColorGenerator';
|
||||||
import { ShlinkVisitsParams } from '../api/types';
|
import { ShlinkVisitsParams } from '../api/types';
|
||||||
import { Topics } from '../mercure/helpers/Topics';
|
import { Topics } from '../mercure/helpers/Topics';
|
||||||
import { useGoBack } from '../utils/helpers/hooks';
|
import { useGoBack } from '../utils/helpers/hooks';
|
||||||
import { ReportExporter } from '../common/services/ReportExporter';
|
import { ReportExporter } from '../common/services/ReportExporter';
|
||||||
import { TagVisits as TagVisitsState } from './reducers/tagVisits';
|
import { TagVisits as TagVisitsState } from './reducers/tagVisits';
|
||||||
import TagVisitsHeader from './TagVisitsHeader';
|
import { TagVisitsHeader } from './TagVisitsHeader';
|
||||||
import VisitsStats from './VisitsStats';
|
import { VisitsStats } from './VisitsStats';
|
||||||
import { NormalizedVisit } from './types';
|
import { NormalizedVisit } from './types';
|
||||||
import { CommonVisitsProps } from './types/CommonVisitsProps';
|
import { CommonVisitsProps } from './types/CommonVisitsProps';
|
||||||
import { toApiParams } from './types/helpers';
|
import { toApiParams } from './types/helpers';
|
||||||
@@ -18,7 +18,7 @@ export interface TagVisitsProps extends CommonVisitsProps {
|
|||||||
cancelGetTagVisits: () => void;
|
cancelGetTagVisits: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const TagVisits = (colorGenerator: ColorGenerator, { exportVisits }: ReportExporter) => boundToMercureHub(({
|
export const TagVisits = (colorGenerator: ColorGenerator, { exportVisits }: ReportExporter) => boundToMercureHub(({
|
||||||
getTagVisits,
|
getTagVisits,
|
||||||
tagVisits,
|
tagVisits,
|
||||||
cancelGetTagVisits,
|
cancelGetTagVisits,
|
||||||
@@ -44,5 +44,3 @@ const TagVisits = (colorGenerator: ColorGenerator, { exportVisits }: ReportExpor
|
|||||||
</VisitsStats>
|
</VisitsStats>
|
||||||
);
|
);
|
||||||
}, () => [Topics.visits]);
|
}, () => [Topics.visits]);
|
||||||
|
|
||||||
export default TagVisits;
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import Tag from '../tags/helpers/Tag';
|
import { Tag } from '../tags/helpers/Tag';
|
||||||
import ColorGenerator from '../utils/services/ColorGenerator';
|
import { ColorGenerator } from '../utils/services/ColorGenerator';
|
||||||
import VisitsHeader from './VisitsHeader';
|
import { VisitsHeader } from './VisitsHeader';
|
||||||
import { TagVisits } from './reducers/tagVisits';
|
import { TagVisits } from './reducers/tagVisits';
|
||||||
import './ShortUrlVisitsHeader.scss';
|
import './ShortUrlVisitsHeader.scss';
|
||||||
|
|
||||||
@@ -10,9 +10,8 @@ interface TagVisitsHeaderProps {
|
|||||||
colorGenerator: ColorGenerator;
|
colorGenerator: ColorGenerator;
|
||||||
}
|
}
|
||||||
|
|
||||||
const TagVisitsHeader = ({ tagVisits, goBack, colorGenerator }: TagVisitsHeaderProps) => {
|
export const TagVisitsHeader = ({ tagVisits, goBack, colorGenerator }: TagVisitsHeaderProps) => {
|
||||||
const { visits, tag } = tagVisits;
|
const { visits, tag } = tagVisits;
|
||||||
|
|
||||||
const visitsStatsTitle = (
|
const visitsStatsTitle = (
|
||||||
<span className="d-flex align-items-center justify-content-center">
|
<span className="d-flex align-items-center justify-content-center">
|
||||||
<span className="me-2">Visits for</span>
|
<span className="me-2">Visits for</span>
|
||||||
@@ -22,5 +21,3 @@ const TagVisitsHeader = ({ tagVisits, goBack, colorGenerator }: TagVisitsHeaderP
|
|||||||
|
|
||||||
return <VisitsHeader title={visitsStatsTitle} goBack={goBack} visits={visits} />;
|
return <VisitsHeader title={visitsStatsTitle} goBack={goBack} visits={visits} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default TagVisitsHeader;
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { Button, Card } from 'reactstrap';
|
|||||||
import { FC, PropsWithChildren, ReactNode } from 'react';
|
import { FC, PropsWithChildren, ReactNode } from 'react';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import { faArrowLeft } from '@fortawesome/free-solid-svg-icons';
|
import { faArrowLeft } from '@fortawesome/free-solid-svg-icons';
|
||||||
import ShortUrlVisitsCount from '../short-urls/helpers/ShortUrlVisitsCount';
|
import { ShortUrlVisitsCount } from '../short-urls/helpers/ShortUrlVisitsCount';
|
||||||
import { ShortUrl } from '../short-urls/data';
|
import { ShortUrl } from '../short-urls/data';
|
||||||
import { Visit } from './types';
|
import { Visit } from './types';
|
||||||
|
|
||||||
@@ -13,7 +13,7 @@ type VisitsHeaderProps = PropsWithChildren<{
|
|||||||
shortUrl?: ShortUrl;
|
shortUrl?: ShortUrl;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
const VisitsHeader: FC<VisitsHeaderProps> = ({ visits, goBack, shortUrl, children, title }) => (
|
export const VisitsHeader: FC<VisitsHeaderProps> = ({ visits, goBack, shortUrl, children, title }) => (
|
||||||
<header>
|
<header>
|
||||||
<Card body>
|
<Card body>
|
||||||
<h2 className="d-flex justify-content-between align-items-center mb-0">
|
<h2 className="d-flex justify-content-between align-items-center mb-0">
|
||||||
@@ -36,5 +36,3 @@ const VisitsHeader: FC<VisitsHeaderProps> = ({ visits, goBack, shortUrl, childre
|
|||||||
</Card>
|
</Card>
|
||||||
</header>
|
</header>
|
||||||
);
|
);
|
||||||
|
|
||||||
export default VisitsHeader;
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { IconDefinition } from '@fortawesome/fontawesome-common-types';
|
|||||||
import { Route, Routes, Navigate } from 'react-router-dom';
|
import { Route, Routes, Navigate } from 'react-router-dom';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { DateRangeSelector } from '../utils/dates/DateRangeSelector';
|
import { DateRangeSelector } from '../utils/dates/DateRangeSelector';
|
||||||
import Message from '../utils/Message';
|
import { Message } from '../utils/Message';
|
||||||
import { DateInterval, DateRange, intervalToDateRange } from '../utils/dates/types';
|
import { DateInterval, DateRange, intervalToDateRange } from '../utils/dates/types';
|
||||||
import { Result } from '../utils/Result';
|
import { Result } from '../utils/Result';
|
||||||
import { ShlinkApiError } from '../api/ShlinkApiError';
|
import { ShlinkApiError } from '../api/ShlinkApiError';
|
||||||
@@ -17,10 +17,10 @@ import { supportsBotVisits } from '../utils/helpers/features';
|
|||||||
import { prettify } from '../utils/helpers/numbers';
|
import { prettify } from '../utils/helpers/numbers';
|
||||||
import { NavPillItem, NavPills } from '../utils/NavPills';
|
import { NavPillItem, NavPills } from '../utils/NavPills';
|
||||||
import { ExportBtn } from '../utils/ExportBtn';
|
import { ExportBtn } from '../utils/ExportBtn';
|
||||||
import LineChartCard from './charts/LineChartCard';
|
import { LineChartCard } from './charts/LineChartCard';
|
||||||
import VisitsTable from './VisitsTable';
|
import { VisitsTable } from './VisitsTable';
|
||||||
import { NormalizedOrphanVisit, NormalizedVisit, VisitsFilter, VisitsInfo, VisitsParams } from './types';
|
import { NormalizedOrphanVisit, NormalizedVisit, VisitsFilter, VisitsInfo, VisitsParams } from './types';
|
||||||
import OpenMapModalBtn from './helpers/OpenMapModalBtn';
|
import { OpenMapModalBtn } from './helpers/OpenMapModalBtn';
|
||||||
import { normalizeVisits, processStatsFromVisits } from './services/VisitsParser';
|
import { normalizeVisits, processStatsFromVisits } from './services/VisitsParser';
|
||||||
import { VisitsFilterDropdown } from './helpers/VisitsFilterDropdown';
|
import { VisitsFilterDropdown } from './helpers/VisitsFilterDropdown';
|
||||||
import { HighlightableProps, highlightedVisitsToStats } from './types/helpers';
|
import { HighlightableProps, highlightedVisitsToStats } from './types/helpers';
|
||||||
@@ -55,7 +55,7 @@ const sections: Record<Section, VisitsNavLinkProps> = {
|
|||||||
|
|
||||||
let selectedBar: string | undefined;
|
let selectedBar: string | undefined;
|
||||||
|
|
||||||
const VisitsStats: FC<VisitsStatsProps> = ({
|
export const VisitsStats: FC<VisitsStatsProps> = ({
|
||||||
children,
|
children,
|
||||||
visitsInfo,
|
visitsInfo,
|
||||||
getVisits,
|
getVisits,
|
||||||
@@ -139,7 +139,7 @@ const VisitsStats: FC<VisitsStatsProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isEmpty(visits)) {
|
if (isEmpty(visits)) {
|
||||||
return <Message>There are no visits matching current filter :(</Message>;
|
return <Message>There are no visits matching current filter</Message>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -235,7 +235,7 @@ const VisitsStats: FC<VisitsStatsProps> = ({
|
|||||||
stats={cities}
|
stats={cities}
|
||||||
highlightedStats={highlightedVisitsToStats(highlightedVisits, 'city')}
|
highlightedStats={highlightedVisitsToStats(highlightedVisits, 'city')}
|
||||||
highlightedLabel={highlightedLabel}
|
highlightedLabel={highlightedLabel}
|
||||||
extraHeaderContent={(activeCities: string[]) => mapLocations.length > 0 && (
|
extraHeaderContent={(activeCities) => mapLocations.length > 0 && (
|
||||||
<OpenMapModalBtn modalTitle="Cities" locations={mapLocations} activeCities={activeCities} />
|
<OpenMapModalBtn modalTitle="Cities" locations={mapLocations} activeCities={activeCities} />
|
||||||
)}
|
)}
|
||||||
sortingItems={{
|
sortingItems={{
|
||||||
@@ -300,19 +300,19 @@ const VisitsStats: FC<VisitsStatsProps> = ({
|
|||||||
{visits.length > 0 && (
|
{visits.length > 0 && (
|
||||||
<div className="col-lg-5 col-xl-6 mt-3 mt-lg-0">
|
<div className="col-lg-5 col-xl-6 mt-3 mt-lg-0">
|
||||||
<div className="d-flex">
|
<div className="d-flex">
|
||||||
<Button
|
|
||||||
outline
|
|
||||||
disabled={highlightedVisits.length === 0}
|
|
||||||
className="btn-md-block me-2"
|
|
||||||
onClick={() => setSelectedVisits([])}
|
|
||||||
>
|
|
||||||
Clear selection {highlightedVisits.length > 0 && <>({prettify(highlightedVisits.length)})</>}
|
|
||||||
</Button>
|
|
||||||
<ExportBtn
|
<ExportBtn
|
||||||
className="btn-md-block"
|
className="btn-md-block"
|
||||||
amount={normalizedVisits.length}
|
amount={normalizedVisits.length}
|
||||||
onClick={() => exportCsv(normalizedVisits)}
|
onClick={() => exportCsv(normalizedVisits)}
|
||||||
/>
|
/>
|
||||||
|
<Button
|
||||||
|
outline
|
||||||
|
disabled={highlightedVisits.length === 0}
|
||||||
|
className="btn-md-block ms-2"
|
||||||
|
onClick={() => setSelectedVisits([])}
|
||||||
|
>
|
||||||
|
Clear selection {highlightedVisits.length > 0 && <>({prettify(highlightedVisits.length)})</>}
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -325,5 +325,3 @@ const VisitsStats: FC<VisitsStatsProps> = ({
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default VisitsStats;
|
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ const calculateVisits = (allVisits: NormalizedVisit[], searchTerm: string | unde
|
|||||||
return { visitsGroups, total };
|
return { visitsGroups, total };
|
||||||
};
|
};
|
||||||
|
|
||||||
const VisitsTable = ({
|
export const VisitsTable = ({
|
||||||
visits,
|
visits,
|
||||||
selectedVisits = [],
|
selectedVisits = [],
|
||||||
setSelectedVisits,
|
setSelectedVisits,
|
||||||
@@ -222,5 +222,3 @@ const VisitsTable = ({
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default VisitsTable;
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user