Compare commits

...

72 Commits

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

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-01-08 06:27:50 +00:00
356 changed files with 5222 additions and 12072 deletions

View File

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

View File

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

1
.gitignore vendored
View File

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

View File

@@ -4,6 +4,47 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org).
## [3.10.1] - 2023-04-23
### Added
* *Nothing*
### Changed
* *Nothing*
### Deprecated
* *Nothing*
### Removed
* *Nothing*
### Fixed
* [#826](https://github.com/shlinkio/shlink-web-client/issues/826) Fix generated short URLs CSV so that it can be used to import on Shlink.
## [3.10.0] - 2023-03-19
### Added
* [#807](https://github.com/shlinkio/shlink-web-client/issues/807) Add support for device-specific long-URLs when creating or editing short URLs.
* [#808](https://github.com/shlinkio/shlink-web-client/issues/808) Respect settings on excluding bots in the overview section, for visits cards.
* [#809](https://github.com/shlinkio/shlink-web-client/issues/809) Respect settings on excluding bots in the tags list.
### Changed
* [#798](https://github.com/shlinkio/shlink-web-client/issues/798) Remove stryker and mutation testing.
* [#800](https://github.com/shlinkio/shlink-web-client/issues/800) Use `/tags/stats` endpoint to load tags stats, when the server supports it.
* Update to Vite 4.2
* Update to TypeScript 5
* Update to coding standard v2.1.0
* Decouple tests from RTK internals.
### Deprecated
* *Nothing*
### Removed
* *Nothing*
### Fixed
* [#799](https://github.com/shlinkio/shlink-web-client/issues/799) Fix fallback visits not taking into account configuration regarding excluding bots.
## [3.9.1] - 2022-12-31
### Added
* *Nothing*

View File

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

View File

@@ -10,4 +10,4 @@ services:
ports:
- "3000:3000"
- "56745:56745"
- "5000:5000"
- "4173:4173"

View File

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

11198
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -12,25 +12,26 @@
"lint:fix": "npm run lint:css:fix && npm run lint:js:fix",
"lint:css:fix": "npm run lint:css -- --fix",
"lint:js:fix": "npm run lint:js -- --fix",
"types": "tsc",
"start": "vite serve --host=0.0.0.0",
"build": "tsc --noEmit && vite build && node scripts/replace-version.mjs",
"preview": "vite preview --host=0.0.0.0",
"build": "npm run types && vite build && node scripts/replace-version.mjs",
"build:dist": "npm run build && node scripts/create-dist-file.mjs",
"build:serve": "serve -p 5000 ./build",
"test": "jest --env=jsdom --colors",
"test:coverage": "npm run test -- --coverage --coverageReporters=text --coverageReporters=text-summary",
"test:ci": "npm run test:coverage -- --coverageReporters=clover --ci",
"test:pretty": "npm run test:coverage -- --coverageReporters=html",
"test:verbose": "npm run test -- --verbose",
"mutate": "./node_modules/.bin/stryker run --concurrency 4 --ignoreStatic"
"test:verbose": "npm run test -- --verbose"
},
"dependencies": {
"@babel/preset-env": "^7.20.2",
"@babel/preset-react": "^7.18.6",
"@babel/preset-typescript": "^7.18.6",
"@fortawesome/fontawesome-free": "^6.2.1",
"@fortawesome/fontawesome-svg-core": "^6.2.1",
"@fortawesome/free-regular-svg-icons": "^6.2.1",
"@fortawesome/free-solid-svg-icons": "^6.2.1",
"@babel/preset-typescript": "^7.21.0",
"@fortawesome/fontawesome-free": "^6.3.0",
"@fortawesome/fontawesome-svg-core": "^6.3.0",
"@fortawesome/free-brands-svg-icons": "^6.3.0",
"@fortawesome/free-regular-svg-icons": "^6.3.0",
"@fortawesome/free-solid-svg-icons": "^6.3.0",
"@fortawesome/react-fontawesome": "^0.2.0",
"@json2csv/plainjs": "^6.1.2",
"@reduxjs/toolkit": "^1.9.1",
@@ -53,7 +54,7 @@
"react-copy-to-clipboard": "^5.1.0",
"react-datepicker": "^4.8.0",
"react-dom": "^18.2.0",
"react-external-link": "^2.0.0",
"react-external-link": "^2.2.0",
"react-leaflet": "^4.2.0",
"react-redux": "^8.0.5",
"react-router-dom": "^6.6.1",
@@ -71,14 +72,12 @@
"workbox-strategies": "^6.5.4"
},
"devDependencies": {
"@shlinkio/eslint-config-js-coding-standard": "~2.0.2",
"@shlinkio/eslint-config-js-coding-standard": "~2.1.0",
"@shlinkio/stylelint-config-css-coding-standard": "~1.0.1",
"@stryker-mutator/core": "^6.3.1",
"@stryker-mutator/jest-runner": "^6.3.1",
"@stryker-mutator/typescript-checker": "^6.3.1",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^14.4.3",
"@total-typescript/shoehorn": "^0.1.0",
"@types/jest": "^29.2.4",
"@types/json2csv": "^5.0.3",
"@types/leaflet": "^1.9.0",
@@ -91,9 +90,9 @@
"@types/react-dom": "^18.0.10",
"@types/react-tag-autocomplete": "^6.3.0",
"@types/uuid": "^8.3.4",
"@vitejs/plugin-react": "^3.0.0",
"@vitejs/plugin-react": "^4.0.0",
"adm-zip": "^0.5.10",
"babel-jest": "^29.3.1",
"babel-jest": "^29.5.0",
"chalk": "^5.2.0",
"eslint": "^8.30.0",
"identity-obj-proxy": "^3.0.0",
@@ -102,13 +101,10 @@
"jest-environment-jsdom": "^29.3.1",
"resize-observer-polyfill": "^1.5.1",
"sass": "^1.57.1",
"serve": "^14.1.2",
"stryker-cli": "^1.0.2",
"stylelint": "^14.16.0",
"ts-mockery": "^1.2.0",
"typescript": "^4.9.4",
"vite": "^4.0.3",
"vite-plugin-pwa": "^0.14.0"
"typescript": "^5.0.2",
"vite": "^4.3.1",
"vite-plugin-pwa": "^0.14.4"
},
"browserslist": [
">0.2%",

View File

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

View File

@@ -1,26 +1,27 @@
import { isEmpty, isNil, reject } from 'ramda';
import { ShortUrl, ShortUrlData } from '../../short-urls/data';
import { OptionalString } from '../../utils/utils';
import {
import type { HttpClient } from '../../common/services/HttpClient';
import type { ShortUrl, ShortUrlData } from '../../short-urls/data';
import { orderToString } from '../../utils/helpers/ordering';
import { stringifyQuery } from '../../utils/helpers/query';
import type { OptionalString } from '../../utils/utils';
import type {
ShlinkDomainRedirects,
ShlinkDomainsResponse,
ShlinkEditDomainRedirects,
ShlinkHealth,
ShlinkMercureInfo,
ShlinkShortUrlData,
ShlinkShortUrlsListNormalizedParams,
ShlinkShortUrlsListParams,
ShlinkShortUrlsResponse,
ShlinkTags,
ShlinkTagsResponse,
ShlinkTagsStatsResponse,
ShlinkVisits,
ShlinkVisitsParams,
ShlinkShortUrlData,
ShlinkDomainsResponse,
ShlinkVisitsOverview,
ShlinkEditDomainRedirects,
ShlinkDomainRedirects,
ShlinkShortUrlsListParams,
ShlinkShortUrlsListNormalizedParams,
ShlinkVisitsParams,
} from '../types';
import { orderToString } from '../../utils/helpers/ordering';
import { isRegularNotFound, parseApiError } from '../utils';
import { stringifyQuery } from '../../utils/helpers/query';
import { HttpClient } from '../../common/services/HttpClient';
const buildShlinkBaseUrl = (url: string, version: 2 | 3) => `${url}/rest/v${version}`;
const rejectNilProps = reject(isNil);
@@ -90,6 +91,11 @@ export class ShlinkApiClient {
.then(({ tags }) => tags)
.then(({ data, stats }) => ({ tags: data, stats }));
public readonly tagsStats = async (): Promise<ShlinkTags> =>
this.performRequest<{ tags: ShlinkTagsStatsResponse }>('/tags/stats', 'GET')
.then(({ tags }) => tags)
.then(({ data }) => ({ tags: data.map(({ tag }) => tag), stats: data }));
public readonly deleteTags = async (tags: string[]): Promise<{ tags: string[] }> =>
this.performEmptyRequest('/tags', 'DELETE', { tags }).then(() => ({ tags }));

View File

@@ -1,7 +1,8 @@
import { hasServerData, ServerWithId } from '../../servers/data';
import { GetState } from '../../container/types';
import type { HttpClient } from '../../common/services/HttpClient';
import type { GetState } from '../../container/types';
import type { ServerWithId } from '../../servers/data';
import { hasServerData } from '../../servers/data';
import { ShlinkApiClient } from './ShlinkApiClient';
import { HttpClient } from '../../common/services/HttpClient';
const apiClients: Record<string, ShlinkApiClient> = {};

View File

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

View File

@@ -1,7 +1,7 @@
import { Visit } from '../../visits/types';
import { OptionalString } from '../../utils/utils';
import { ShortUrl, ShortUrlMeta } from '../../short-urls/data';
import { Order } from '../../utils/helpers/ordering';
import type { ShortUrl, ShortUrlMeta } from '../../short-urls/data';
import type { Order } from '../../utils/helpers/ordering';
import type { OptionalString } from '../../utils/utils';
import type { Visit } from '../../visits/types';
export interface ShlinkShortUrlsResponse {
data: ShortUrl[];
@@ -18,9 +18,12 @@ export interface ShlinkHealth {
version: string;
}
interface ShlinkTagsStats {
export interface ShlinkTagsStats {
tag: string;
shortUrlsCount: number;
visitsSummary?: ShlinkVisitsSummary; // Optional only before Shlink 3.5.0
/** @deprecated */
visitsCount: number;
}
@@ -31,22 +34,38 @@ export interface ShlinkTags {
export interface ShlinkTagsResponse {
data: string[];
/** @deprecated Present only when withStats=true is provided, which is deprecated */
stats: ShlinkTagsStats[];
}
export interface ShlinkTagsStatsResponse {
data: ShlinkTagsStats[];
}
export interface ShlinkPaginator {
currentPage: number;
pagesCount: number;
totalItems: number;
}
export interface ShlinkVisitsSummary {
total: number;
nonBots: number;
bots: number;
}
export interface ShlinkVisits {
data: Visit[];
pagination: ShlinkPaginator;
}
export interface ShlinkVisitsOverview {
nonOrphanVisits?: ShlinkVisitsSummary; // Optional only before Shlink 3.5.0
orphanVisits?: ShlinkVisitsSummary; // Optional only before Shlink 3.5.0
/** @deprecated */
visitsCount: number;
/** @deprecated */
orphanVisitsCount: number;
}

View File

@@ -1,10 +1,11 @@
import {
ErrorTypeV2,
ErrorTypeV3,
import type {
InvalidArgumentError,
InvalidShortUrlDeletion,
ProblemDetailsError,
RegularNotFound,
RegularNotFound } from '../types/errors';
import {
ErrorTypeV2,
ErrorTypeV3,
} from '../types/errors';
const isProblemDetails = (e: unknown): e is ProblemDetailsError =>

View File

@@ -1,12 +1,13 @@
import { useEffect, FC } from 'react';
import { Route, Routes, useLocation } from 'react-router-dom';
import classNames from 'classnames';
import { NotFound } from '../common/NotFound';
import { ServersMap } from '../servers/data';
import { Settings } from '../settings/reducers/settings';
import { changeThemeInMarkup } from '../utils/theme';
import type { FC } from 'react';
import { useEffect } from 'react';
import { Route, Routes, useLocation } from 'react-router-dom';
import { AppUpdateBanner } from '../common/AppUpdateBanner';
import { NotFound } from '../common/NotFound';
import type { ServersMap } from '../servers/data';
import type { Settings } from '../settings/reducers/settings';
import { forceUpdate } from '../utils/helpers/sw';
import { changeThemeInMarkup } from '../utils/theme';
import './App.scss';
interface AppProps {

View File

@@ -1,9 +1,9 @@
import Bottle from 'bottlejs';
import { appUpdateAvailable, resetAppUpdate } from '../reducers/appUpdates';
import type Bottle from 'bottlejs';
import type { ConnectDecorator } from '../../container/types';
import { App } from '../App';
import { ConnectDecorator } from '../../container/types';
import { appUpdateAvailable, resetAppUpdate } from '../reducers/appUpdates';
const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
export const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
// Components
bottle.serviceFactory(
'App',
@@ -23,5 +23,3 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
bottle.serviceFactory('appUpdateAvailable', () => appUpdateAvailable);
bottle.serviceFactory('resetAppUpdate', () => resetAppUpdate);
};
export default provideServices;

View File

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

View File

@@ -1,17 +1,19 @@
import {
faList as listIcon,
faLink as createIcon,
faTags as tagsIcon,
faPen as editIcon,
faHome as overviewIcon,
faGlobe as domainsIcon,
faHome as overviewIcon,
faLink as createIcon,
faList as listIcon,
faPen as editIcon,
faTags as tagsIcon,
} from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { FC } from 'react';
import { NavLink, NavLinkProps, useLocation } from 'react-router-dom';
import classNames from 'classnames';
import { DeleteServerButtonProps } from '../servers/DeleteServerButton';
import { isServerWithId, SelectedServer } from '../servers/data';
import type { FC } from 'react';
import type { NavLinkProps } from 'react-router-dom';
import { NavLink, useLocation } from 'react-router-dom';
import type { SelectedServer } from '../servers/data';
import { isServerWithId } from '../servers/data';
import type { DeleteServerButtonProps } from '../servers/DeleteServerButton';
import './AsideMenu.scss';
export interface AsideMenuProps {

View File

@@ -1,4 +1,5 @@
import { Component, ReactNode } from 'react';
import type { ReactNode } from 'react';
import { Component } from 'react';
import { Button } from 'reactstrap';
import { SimpleCard } from '../utils/SimpleCard';

View File

@@ -1,12 +1,12 @@
import { useEffect } from 'react';
import { faExternalLinkAlt, faPlus } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { isEmpty, values } from 'ramda';
import { useEffect } from 'react';
import { ExternalLink } from 'react-external-link';
import { Link, useNavigate } from 'react-router-dom';
import { Card, Row } from 'reactstrap';
import { ExternalLink } from 'react-external-link';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faExternalLinkAlt, faPlus } from '@fortawesome/free-solid-svg-icons';
import type { ServersMap } from '../servers/data';
import { ServersListGroup } from '../servers/ServersListGroup';
import { ServersMap } from '../servers/data';
import { ShlinkLogo } from './img/ShlinkLogo';
import './Home.scss';

View File

@@ -1,9 +1,10 @@
import { faChevronDown as arrowIcon, faCogs as cogsIcon } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { FC, useEffect } from 'react';
import classNames from 'classnames';
import type { FC } from 'react';
import { useEffect } from 'react';
import { Link, useLocation } from 'react-router-dom';
import { Collapse, Nav, Navbar, NavbarBrand, NavbarToggler, NavItem, NavLink } from 'reactstrap';
import classNames from 'classnames';
import { useToggle } from '../utils/helpers/hooks';
import { ShlinkLogo } from './img/ShlinkLogo';
import './MainHeader.scss';

View File

@@ -1,14 +1,15 @@
import { FC, useEffect } from 'react';
import { Navigate, Route, Routes, useLocation } from 'react-router-dom';
import { faBars as burgerIcon } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import classNames from 'classnames';
import { withSelectedServer } from '../servers/helpers/withSelectedServer';
import { useSwipeable, useToggle } from '../utils/helpers/hooks';
import { supportsDomainVisits, supportsNonOrphanVisits } from '../utils/helpers/features';
import type { FC } from 'react';
import { useEffect } from 'react';
import { Navigate, Route, Routes, useLocation } from 'react-router-dom';
import { isReachableServer } from '../servers/data';
import { withSelectedServer } from '../servers/helpers/withSelectedServer';
import { useFeature } from '../utils/helpers/features';
import { useSwipeable, useToggle } from '../utils/helpers/hooks';
import type { AsideMenuProps } from './AsideMenu';
import { NotFound } from './NotFound';
import { AsideMenuProps } from './AsideMenu';
import './MenuLayout.scss';
interface MenuLayoutProps {
@@ -45,8 +46,8 @@ export const MenuLayout = (
return <ServerError />;
}
const addNonOrphanVisitsRoute = supportsNonOrphanVisits(selectedServer);
const addDomainVisitsRoute = supportsDomainVisits(selectedServer);
const addNonOrphanVisitsRoute = useFeature('nonOrphanVisits', selectedServer);
const addDomainVisitsRoute = useFeature('domainVisits', selectedServer);
const burgerClasses = classNames('menu-layout__burger-icon', { 'menu-layout__burger-icon--active': sidebarVisible });
const swipeableProps = useSwipeable(showSidebar, hideSidebar);

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,8 @@
import { pipe } from 'ramda';
import { ExternalLink } from 'react-external-link';
import type { SelectedServer } from '../servers/data';
import { isReachableServer } from '../servers/data';
import { versionToPrintable, versionToSemVer } from '../utils/helpers/version';
import { isReachableServer, SelectedServer } from '../servers/data';
const SHLINK_WEB_CLIENT_VERSION = '%_VERSION_%';
const normalizeVersion = pipe(versionToSemVer(), versionToPrintable);

View File

@@ -1,7 +1,7 @@
import classNames from 'classnames';
import { SelectedServer } from '../servers/data';
import type { SelectedServer } from '../servers/data';
import type { Sidebar } from './reducers/sidebar';
import { ShlinkVersions } from './ShlinkVersions';
import { Sidebar } from './reducers/sidebar';
import './ShlinkVersionsContainer.scss';
export interface ShlinkVersionsContainerProps {

View File

@@ -1,12 +1,13 @@
import { FC } from 'react';
import classNames from 'classnames';
import type { FC } from 'react';
import { Pagination, PaginationItem, PaginationLink } from 'reactstrap';
import type {
NumberOrEllipsis } from '../utils/helpers/pagination';
import {
pageIsEllipsis,
keyForPage,
NumberOrEllipsis,
progressivePagination,
pageIsEllipsis,
prettifyPageNumber,
progressivePagination,
} from '../utils/helpers/pagination';
import './SimplePaginator.scss';

View File

@@ -3,7 +3,7 @@
.react-tags {
position: relative;
padding: 5px 0 0 6px;
border-radius: .3rem;
border-radius: .5rem;
background-color: var(--primary-color);
border: 1px solid var(--input-border-color);
transition: border-color .15s ease-in-out, box-shadow .15s ease-in-out;

View File

@@ -1,4 +1,4 @@
import { Fetch } from '../../utils/types';
import type { Fetch } from '../../utils/types';
const applicationJsonHeader = { 'Content-Type': 'application/json' };
const withJsonContentType = (options?: RequestInit): RequestInit | undefined => {

View File

@@ -1,5 +1,5 @@
import { saveUrl } from '../../utils/helpers/files';
import { HttpClient } from './HttpClient';
import type { HttpClient } from './HttpClient';
export class ImageDownloader {
public constructor(private readonly httpClient: HttpClient, private readonly window: Window) {}

View File

@@ -1,7 +1,7 @@
import { NormalizedVisit } from '../../visits/types';
import { ExportableShortUrl } from '../../short-urls/data';
import type { ExportableShortUrl } from '../../short-urls/data';
import type { JsonToCsv } from '../../utils/helpers/csvjson';
import { saveCsv } from '../../utils/helpers/files';
import { JsonToCsv } from '../../utils/helpers/csvjson';
import type { NormalizedVisit } from '../../visits/types';
export class ReportExporter {
public constructor(private readonly window: Window, private readonly jsonToCsv: JsonToCsv) {}
@@ -24,7 +24,6 @@ export class ReportExporter {
private readonly exportCsv = (filename: string, rows: object[]) => {
const csv = this.jsonToCsv(rows);
saveCsv(this.window, csv, filename);
};
}

View File

@@ -1,19 +1,19 @@
import Bottle from 'bottlejs';
import { ScrollToTop } from '../ScrollToTop';
import { MainHeader } from '../MainHeader';
import { Home } from '../Home';
import { MenuLayout } from '../MenuLayout';
import type Bottle from 'bottlejs';
import type { ConnectDecorator } from '../../container/types';
import { withoutSelectedServer } from '../../servers/helpers/withoutSelectedServer';
import { AsideMenu } from '../AsideMenu';
import { ErrorHandler } from '../ErrorHandler';
import { ShlinkVersionsContainer } from '../ShlinkVersionsContainer';
import { ConnectDecorator } from '../../container/types';
import { withoutSelectedServer } from '../../servers/helpers/withoutSelectedServer';
import { Home } from '../Home';
import { MainHeader } from '../MainHeader';
import { MenuLayout } from '../MenuLayout';
import { sidebarNotPresent, sidebarPresent } from '../reducers/sidebar';
import { ScrollToTop } from '../ScrollToTop';
import { ShlinkVersionsContainer } from '../ShlinkVersionsContainer';
import { HttpClient } from './HttpClient';
import { ImageDownloader } from './ImageDownloader';
import { ReportExporter } from './ReportExporter';
import { HttpClient } from './HttpClient';
const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
export const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
// Services
bottle.constant('window', window);
bottle.constant('console', console);
@@ -62,5 +62,3 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
bottle.serviceFactory('sidebarPresent', () => sidebarPresent);
bottle.serviceFactory('sidebarNotPresent', () => sidebarNotPresent);
};
export default provideServices;

View File

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

View File

@@ -1,9 +1,10 @@
import { IContainer } from 'bottlejs';
import { save, load, RLSOptions } from 'redux-localstorage-simple';
import { configureStore } from '@reduxjs/toolkit';
import reducer from '../reducers';
import type { IContainer } from 'bottlejs';
import type { RLSOptions } from 'redux-localstorage-simple';
import { load, save } from 'redux-localstorage-simple';
import { initReducers } from '../reducers';
import { migrateDeprecatedSettings } from '../settings/helpers';
import { ShlinkState } from './types';
import type { ShlinkState } from './types';
const isProduction = process.env.NODE_ENV === 'production';
const localStorageConfig: RLSOptions = {
@@ -16,7 +17,7 @@ const preloadedState = migrateDeprecatedSettings(load(localStorageConfig) as Shl
export const setUpStore = (container: IContainer) => configureStore({
devTools: !isProduction,
reducer: reducer(container),
reducer: initReducers(container),
preloadedState,
middleware: (defaultMiddlewaresIncludingReduxThunk) =>
defaultMiddlewaresIncludingReduxThunk({ immutableCheck: false, serializableCheck: false }) // State is too big for these

View File

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

View File

@@ -1,14 +1,15 @@
import { FC, useEffect } from 'react';
import { UncontrolledTooltip } from 'reactstrap';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faDotCircle as defaultDomainIcon } from '@fortawesome/free-solid-svg-icons';
import { ShlinkDomainRedirects } from '../api/types';
import { OptionalString } from '../utils/utils';
import { SelectedServer } from '../servers/data';
import { Domain } from './data';
import { DomainStatusIcon } from './helpers/DomainStatusIcon';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import type { FC } from 'react';
import { useEffect } from 'react';
import { UncontrolledTooltip } from 'reactstrap';
import type { ShlinkDomainRedirects } from '../api/types';
import type { SelectedServer } from '../servers/data';
import type { OptionalString } from '../utils/utils';
import type { Domain } from './data';
import { DomainDropdown } from './helpers/DomainDropdown';
import { EditDomainRedirects } from './reducers/domainRedirects';
import { DomainStatusIcon } from './helpers/DomainStatusIcon';
import type { EditDomainRedirects } from './reducers/domainRedirects';
interface DomainRowProps {
domain: Domain;

View File

@@ -1,11 +1,12 @@
import { useEffect } from 'react';
import { Button, DropdownItem, Input, InputGroup, UncontrolledTooltip, InputProps } from 'reactstrap';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faUndo } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { isEmpty, pipe } from 'ramda';
import { useEffect } from 'react';
import type { InputProps } from 'reactstrap';
import { Button, DropdownItem, Input, InputGroup, UncontrolledTooltip } from 'reactstrap';
import { DropdownBtn } from '../utils/DropdownBtn';
import { useToggle } from '../utils/helpers/hooks';
import { DomainsList } from './reducers/domainsList';
import type { DomainsList } from './reducers/domainsList';
import './DomainSelector.scss';
export interface DomainSelectorProps extends Omit<InputProps, 'onChange'> {

View File

@@ -1,13 +1,14 @@
import { FC, useEffect } from 'react';
import type { FC } from 'react';
import { useEffect } from 'react';
import { ShlinkApiError } from '../api/ShlinkApiError';
import type { SelectedServer } from '../servers/data';
import { Message } from '../utils/Message';
import { Result } from '../utils/Result';
import { ShlinkApiError } from '../api/ShlinkApiError';
import { SimpleCard } from '../utils/SimpleCard';
import { SearchField } from '../utils/SearchField';
import { EditDomainRedirects } from './reducers/domainRedirects';
import { SelectedServer } from '../servers/data';
import { DomainsList } from './reducers/domainsList';
import { SimpleCard } from '../utils/SimpleCard';
import { DomainRow } from './DomainRow';
import type { EditDomainRedirects } from './reducers/domainRedirects';
import type { DomainsList } from './reducers/domainsList';
interface ManageDomainsProps {
listDomains: Function;

View File

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

View File

@@ -1,16 +1,17 @@
import { FC } from 'react';
import { DropdownItem } from 'reactstrap';
import { Link } from 'react-router-dom';
import { faChartPie as pieChartIcon, faEdit as editIcon } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { useToggle } from '../../utils/helpers/hooks';
import type { FC } from 'react';
import { Link } from 'react-router-dom';
import { DropdownItem } from 'reactstrap';
import type { SelectedServer } from '../../servers/data';
import { getServerId } from '../../servers/data';
import { DropdownBtnMenu } from '../../utils/DropdownBtnMenu';
import { EditDomainRedirectsModal } from './EditDomainRedirectsModal';
import { Domain } from '../data';
import { EditDomainRedirects } from '../reducers/domainRedirects';
import { supportsDefaultDomainRedirectsEdition, supportsDomainVisits } from '../../utils/helpers/features';
import { getServerId, SelectedServer } from '../../servers/data';
import { useFeature } from '../../utils/helpers/features';
import { useToggle } from '../../utils/helpers/hooks';
import { DEFAULT_DOMAIN } from '../../visits/reducers/domainVisits';
import type { Domain } from '../data';
import type { EditDomainRedirects } from '../reducers/domainRedirects';
import { EditDomainRedirectsModal } from './EditDomainRedirectsModal';
interface DomainDropdownProps {
domain: Domain;
@@ -22,8 +23,8 @@ export const DomainDropdown: FC<DomainDropdownProps> = ({ domain, editDomainRedi
const [isOpen, toggle] = useToggle();
const [isModalOpen, toggleModal] = useToggle();
const { isDefault } = domain;
const canBeEdited = !isDefault || supportsDefaultDomainRedirectsEdition(selectedServer);
const withVisits = supportsDomainVisits(selectedServer);
const canBeEdited = !isDefault || useFeature('defaultDomainRedirectsEdition', selectedServer);
const withVisits = useFeature('domainVisits', selectedServer);
const serverId = getServerId(selectedServer);
return (

View File

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

View File

@@ -1,10 +1,12 @@
import { FC, useState } from 'react';
import type { FC } from 'react';
import { useState } from 'react';
import { Button, Modal, ModalBody, ModalFooter, ModalHeader } from 'reactstrap';
import { ShlinkDomain } from '../../api/types';
import { InputFormGroup, InputFormGroupProps } from '../../utils/forms/InputFormGroup';
import { handleEventPreventingDefault, nonEmptyValueOrNull } from '../../utils/utils';
import type { ShlinkDomain } from '../../api/types';
import type { InputFormGroupProps } from '../../utils/forms/InputFormGroup';
import { InputFormGroup } from '../../utils/forms/InputFormGroup';
import { InfoTooltip } from '../../utils/InfoTooltip';
import { EditDomainRedirects } from '../reducers/domainRedirects';
import { handleEventPreventingDefault, nonEmptyValueOrNull } from '../../utils/utils';
import type { EditDomainRedirects } from '../reducers/domainRedirects';
interface EditDomainRedirectsModalProps {
domain: ShlinkDomain;

View File

@@ -1,6 +1,6 @@
import type { ShlinkApiClientBuilder } from '../../api/services/ShlinkApiClientBuilder';
import type { ShlinkDomainRedirects } from '../../api/types';
import { createAsyncThunk } from '../../utils/helpers/redux';
import { ShlinkApiClientBuilder } from '../../api/services/ShlinkApiClientBuilder';
import { ShlinkDomainRedirects } from '../../api/types';
const EDIT_DOMAIN_REDIRECTS = 'shlink/domainRedirects/EDIT_DOMAIN_REDIRECTS';

View File

@@ -1,13 +1,14 @@
import { createSlice, createAction, SliceCaseReducers, AsyncThunk } from '@reduxjs/toolkit';
import { createAsyncThunk } from '../../utils/helpers/redux';
import { ShlinkDomainRedirects } from '../../api/types';
import { ShlinkApiClientBuilder } from '../../api/services/ShlinkApiClientBuilder';
import { Domain, DomainStatus } from '../data';
import { hasServerData } from '../../servers/data';
import { replaceAuthorityFromUri } from '../../utils/helpers/uri';
import { ProblemDetailsError } from '../../api/types/errors';
import type { AsyncThunk, SliceCaseReducers } from '@reduxjs/toolkit';
import { createAction, createSlice } from '@reduxjs/toolkit';
import type { ShlinkApiClientBuilder } from '../../api/services/ShlinkApiClientBuilder';
import type { ShlinkDomainRedirects } from '../../api/types';
import type { ProblemDetailsError } from '../../api/types/errors';
import { parseApiError } from '../../api/utils';
import { EditDomainRedirects } from './domainRedirects';
import { hasServerData } from '../../servers/data';
import { createAsyncThunk } from '../../utils/helpers/redux';
import { replaceAuthorityFromUri } from '../../utils/helpers/uri';
import type { Domain, DomainStatus } from '../data';
import type { EditDomainRedirects } from './domainRedirects';
const REDUCER_PREFIX = 'shlink/domainsList';

View File

@@ -1,12 +1,12 @@
import type Bottle from 'bottlejs';
import { prop } from 'ramda';
import Bottle from 'bottlejs';
import { ConnectDecorator } from '../../container/types';
import { domainsListReducerCreator } from '../reducers/domainsList';
import type { ConnectDecorator } from '../../container/types';
import { DomainSelector } from '../DomainSelector';
import { ManageDomains } from '../ManageDomains';
import { editDomainRedirects } from '../reducers/domainRedirects';
import { domainsListReducerCreator } from '../reducers/domainsList';
const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
export const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
// Components
bottle.serviceFactory('DomainSelector', () => DomainSelector);
bottle.decorator('DomainSelector', connect(['domainsList'], ['listDomains']));
@@ -32,5 +32,3 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
bottle.serviceFactory('editDomainRedirects', editDomainRedirects, 'buildShlinkApiClient');
bottle.serviceFactory('checkDomainHealth', prop('checkDomainHealth'), 'domainsListReducerCreator');
};
export default provideServices;

View File

@@ -1,15 +1,15 @@
import 'chart.js/auto'; // TODO Import specific ones to reduce bundle size https://react-chartjs-2.js.org/docs/migration-to-v4/#tree-shaking
import { createRoot } from 'react-dom/client';
import { Provider } from 'react-redux';
import { BrowserRouter } from 'react-router-dom';
import pack from '../package.json';
import { container } from './container';
import { setUpStore } from './container/store';
import { fixLeafletIcons } from './utils/helpers/leaflet';
import { register as registerServiceWorker } from './serviceWorkerRegistration';
import 'chart.js/auto'; // TODO Import specific ones to reduce bundle size https://react-chartjs-2.js.org/docs/migration-to-v4/#tree-shaking
import 'react-datepicker/dist/react-datepicker.css';
import 'leaflet/dist/leaflet.css';
import { fixLeafletIcons } from './utils/helpers/leaflet';
import './index.scss';
import 'leaflet/dist/leaflet.css';
import 'react-datepicker/dist/react-datepicker.css';
// This overwrites icons used for leaflet maps, fixing some issues caused by webpack while processing the CSS
fixLeafletIcons();

View File

@@ -1,8 +1,9 @@
import { FC, useEffect } from 'react';
import { pipe } from 'ramda';
import type { FC } from 'react';
import { useEffect } from 'react';
import { useParams } from 'react-router-dom';
import { CreateVisit } from '../../visits/types';
import { MercureInfo } from '../reducers/mercureInfo';
import type { CreateVisit } from '../../visits/types';
import type { MercureInfo } from '../reducers/mercureInfo';
import { bindToMercureTopic } from './index';
export interface MercureBoundProps {

View File

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

View File

@@ -1,7 +1,7 @@
import { createSlice } from '@reduxjs/toolkit';
import type { ShlinkApiClientBuilder } from '../../api/services/ShlinkApiClientBuilder';
import type { ShlinkMercureInfo } from '../../api/types';
import { createAsyncThunk } from '../../utils/helpers/redux';
import { ShlinkMercureInfo } from '../../api/types';
import { ShlinkApiClientBuilder } from '../../api/services/ShlinkApiClientBuilder';
const REDUCER_PREFIX = 'shlink/mercure';

View File

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

View File

@@ -1,12 +1,12 @@
import { IContainer } from 'bottlejs';
import { combineReducers } from '@reduxjs/toolkit';
import { serversReducer } from '../servers/reducers/servers';
import { settingsReducer } from '../settings/reducers/settings';
import type { IContainer } from 'bottlejs';
import { appUpdatesReducer } from '../app/reducers/appUpdates';
import { sidebarReducer } from '../common/reducers/sidebar';
import { ShlinkState } from '../container/types';
import type { ShlinkState } from '../container/types';
import { serversReducer } from '../servers/reducers/servers';
import { settingsReducer } from '../settings/reducers/settings';
export default (container: IContainer) => combineReducers<ShlinkState>({
export const initReducers = (container: IContainer) => combineReducers<ShlinkState>({
servers: serversReducer,
selectedServer: container.selectedServerReducer,
shortUrlsList: container.shortUrlsListReducer,

View File

@@ -1,14 +1,16 @@
import { FC, useEffect, useState } from 'react';
import { v4 as uuid } from 'uuid';
import { Button } from 'reactstrap';
import type { FC } from 'react';
import { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { Result } from '../utils/Result';
import { Button } from 'reactstrap';
import { v4 as uuid } from 'uuid';
import { NoMenuLayout } from '../common/NoMenuLayout';
import { TimeoutToggle, useGoBack, useToggle } from '../utils/helpers/hooks';
import { ServerForm } from './helpers/ServerForm';
import { ImportServersBtnProps } from './helpers/ImportServersBtn';
import { ServerData, ServersMap, ServerWithId } from './data';
import type { TimeoutToggle } from '../utils/helpers/hooks';
import { useGoBack, useToggle } from '../utils/helpers/hooks';
import { Result } from '../utils/Result';
import type { ServerData, ServersMap, ServerWithId } from './data';
import { DuplicatedServersModal } from './helpers/DuplicatedServersModal';
import type { ImportServersBtnProps } from './helpers/ImportServersBtn';
import { ServerForm } from './helpers/ServerForm';
const SHOW_IMPORT_MSG_TIME = 4000;

View File

@@ -1,9 +1,9 @@
import { FC, PropsWithChildren } from 'react';
import { faMinusCircle as deleteIcon } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import type { FC, PropsWithChildren } from 'react';
import { useToggle } from '../utils/helpers/hooks';
import { DeleteServerModalProps } from './DeleteServerModal';
import { ServerWithId } from './data';
import type { ServerWithId } from './data';
import type { DeleteServerModalProps } from './DeleteServerModal';
export type DeleteServerButtonProps = PropsWithChildren<{
server: ServerWithId;

View File

@@ -1,7 +1,8 @@
import { FC, useRef } from 'react';
import { Button, Modal, ModalBody, ModalFooter, ModalHeader } from 'reactstrap';
import type { FC } from 'react';
import { useRef } from 'react';
import { useNavigate } from 'react-router-dom';
import { ServerWithId } from './data';
import { Button, Modal, ModalBody, ModalFooter, ModalHeader } from 'reactstrap';
import type { ServerWithId } from './data';
export interface DeleteServerModalProps {
server: ServerWithId;

View File

@@ -1,10 +1,11 @@
import { FC } from 'react';
import type { FC } from 'react';
import { Button } from 'reactstrap';
import { NoMenuLayout } from '../common/NoMenuLayout';
import { useGoBack, useParsedQuery } from '../utils/helpers/hooks';
import type { ServerData } from './data';
import { isServerWithId } from './data';
import { ServerForm } from './helpers/ServerForm';
import { withSelectedServer } from './helpers/withSelectedServer';
import { isServerWithId, ServerData } from './data';
interface EditServerProps {
editServer: (serverId: string, serverData: ServerData) => void;

View File

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

View File

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

View File

@@ -1,18 +1,18 @@
import { FC } from 'react';
import { DropdownItem } from 'reactstrap';
import { Link } from 'react-router-dom';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faCircle as toggleOnIcon } from '@fortawesome/free-regular-svg-icons';
import {
faBan as toggleOffIcon,
faEdit as editIcon,
faMinusCircle as deleteIcon,
faPlug as connectIcon,
} from '@fortawesome/free-solid-svg-icons';
import { faCircle as toggleOnIcon } from '@fortawesome/free-regular-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import type { FC } from 'react';
import { Link } from 'react-router-dom';
import { DropdownItem } from 'reactstrap';
import { DropdownBtnMenu } from '../utils/DropdownBtnMenu';
import { useToggle } from '../utils/helpers/hooks';
import { DeleteServerModalProps } from './DeleteServerModal';
import { ServerWithId } from './data';
import type { ServerWithId } from './data';
import type { DeleteServerModalProps } from './DeleteServerModal';
export interface ManageServersRowDropdownProps {
server: ServerWithId;

View File

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

View File

@@ -1,9 +1,10 @@
import { isEmpty, values } from 'ramda';
import { DropdownItem, DropdownMenu, DropdownToggle, UncontrolledDropdown } from 'reactstrap';
import { Link } from 'react-router-dom';
import { faPlus as plusIcon, faServer as serverIcon } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { getServerId, SelectedServer, ServersMap } from './data';
import { isEmpty, values } from 'ramda';
import { Link } from 'react-router-dom';
import { DropdownItem, DropdownMenu, DropdownToggle, UncontrolledDropdown } from 'reactstrap';
import type { SelectedServer, ServersMap } from './data';
import { getServerId } from './data';
export interface ServersDropdownProps {
servers: ServersMap;

View File

@@ -1,10 +1,10 @@
import { FC, PropsWithChildren } from 'react';
import { ListGroup, ListGroupItem } from 'reactstrap';
import { Link } from 'react-router-dom';
import classNames from 'classnames';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faChevronRight as chevronIcon } from '@fortawesome/free-solid-svg-icons';
import { ServerWithId } from './data';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import classNames from 'classnames';
import type { FC, PropsWithChildren } from 'react';
import { Link } from 'react-router-dom';
import { ListGroup, ListGroupItem } from 'reactstrap';
import type { ServerWithId } from './data';
import './ServersListGroup.scss';
type ServersListGroupProps = PropsWithChildren<{

View File

@@ -1,5 +1,5 @@
import { omit } from 'ramda';
import { SemVer } from '../../utils/helpers/version';
import type { SemVer } from '../../utils/helpers/version';
export interface ServerData {
name: string;

View File

@@ -1,6 +1,7 @@
import { FC, Fragment } from 'react';
import type { FC } from 'react';
import { Fragment } from 'react';
import { Button, Modal, ModalBody, ModalFooter, ModalHeader } from 'reactstrap';
import { ServerData } from '../data';
import type { ServerData } from '../data';
interface DuplicatedServersModalProps {
duplicatedServers: ServerData[];

View File

@@ -1,21 +1,30 @@
import { FC, PropsWithChildren } from 'react';
import { Card, CardText, CardTitle } from 'reactstrap';
import { Link } from 'react-router-dom';
import { faArrowAltCircleRight as linkIcon } from '@fortawesome/free-regular-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import type { FC, PropsWithChildren, ReactNode } from 'react';
import { Link } from 'react-router-dom';
import { Card, CardText, CardTitle, UncontrolledTooltip } from 'reactstrap';
import { useElementRef } from '../../utils/helpers/hooks';
import './HighlightCard.scss';
export type HighlightCardProps = PropsWithChildren<{
title: string;
link?: string | false;
link?: string;
tooltip?: ReactNode;
}>;
const buildExtraProps = (link?: string | false) => (!link ? {} : { tag: Link, to: link });
const buildExtraProps = (link?: string) => (!link ? {} : { tag: Link, to: link });
export const HighlightCard: FC<HighlightCardProps> = ({ children, title, link }) => (
<Card className="highlight-card" body {...buildExtraProps(link)}>
{link && <FontAwesomeIcon size="3x" className="highlight-card__link-icon" icon={linkIcon} />}
<CardTitle tag="h5" className="highlight-card__title">{title}</CardTitle>
<CardText tag="h2">{children}</CardText>
</Card>
);
export const HighlightCard: FC<HighlightCardProps> = ({ children, title, link, tooltip }) => {
const ref = useElementRef<HTMLElement>();
return (
<>
<Card innerRef={ref} className="highlight-card" body {...buildExtraProps(link)}>
{link && <FontAwesomeIcon size="3x" className="highlight-card__link-icon" icon={linkIcon} />}
<CardTitle tag="h5" className="highlight-card__title">{title}</CardTitle>
<CardText tag="h2">{children}</CardText>
</Card>
{tooltip && <UncontrolledTooltip target={ref} placement="bottom">{tooltip}</UncontrolledTooltip>}
</>
);
};

View File

@@ -1,12 +1,12 @@
import { useRef, ChangeEvent, useState, useEffect, FC, PropsWithChildren } from 'react';
import { Button, UncontrolledTooltip } from 'reactstrap';
import { complement, pipe } from 'ramda';
import { faFileUpload as importIcon } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { useToggle } from '../../utils/helpers/hooks';
import { mutableRefToElementRef } from '../../utils/helpers/components';
import { ServersImporter } from '../services/ServersImporter';
import { ServerData, ServersMap } from '../data';
import { complement, pipe } from 'ramda';
import type { ChangeEvent, FC, PropsWithChildren } from 'react';
import { useEffect, useState } from 'react';
import { Button, UncontrolledTooltip } from 'reactstrap';
import { useElementRef, useToggle } from '../../utils/helpers/hooks';
import type { ServerData, ServersMap } from '../data';
import type { ServersImporter } from '../services/ServersImporter';
import { DuplicatedServersModal } from './DuplicatedServersModal';
import './ImportServersBtn.scss';
@@ -34,7 +34,7 @@ export const ImportServersBtn = ({ importServersFromFile }: ServersImporter): FC
tooltipPlacement = 'bottom',
className = '',
}) => {
const ref = useRef<HTMLInputElement>();
const ref = useElementRef<HTMLInputElement>();
const [serversToCreate, setServersToCreate] = useState<ServerData[] | undefined>();
const [duplicatedServers, setDuplicatedServers] = useState<ServerData[]>([]);
const [isModalOpen,, showModal, hideModal] = useToggle();
@@ -79,7 +79,7 @@ export const ImportServersBtn = ({ importServersFromFile }: ServersImporter): FC
type="file"
accept="text/csv"
className="import-servers-btn__csv-select"
ref={mutableRefToElementRef(ref)}
ref={ref}
onChange={onFile}
/>

View File

@@ -1,10 +1,11 @@
import { FC } from 'react';
import type { FC } from 'react';
import { Link } from 'react-router-dom';
import { Message } from '../../utils/Message';
import { ServersListGroup } from '../ServersListGroup';
import { DeleteServerButtonProps } from '../DeleteServerButton';
import { isServerWithId, SelectedServer, ServersMap } from '../data';
import { NoMenuLayout } from '../../common/NoMenuLayout';
import { Message } from '../../utils/Message';
import type { SelectedServer, ServersMap } from '../data';
import { isServerWithId } from '../data';
import type { DeleteServerButtonProps } from '../DeleteServerButton';
import { ServersListGroup } from '../ServersListGroup';
import './ServerError.scss';
interface ServerErrorProps {

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,8 +1,9 @@
import pack from '../../../package.json';
import { hasServerData, ServerData } from '../data';
import { createServers } from './servers';
import type { HttpClient } from '../../common/services/HttpClient';
import { createAsyncThunk } from '../../utils/helpers/redux';
import { HttpClient } from '../../common/services/HttpClient';
import type { ServerData } from '../data';
import { hasServerData } from '../data';
import { createServers } from './servers';
const responseToServersList = (data: any): ServerData[] => (Array.isArray(data) ? data.filter(hasServerData) : []);

View File

@@ -1,10 +1,12 @@
import { createAction, createListenerMiddleware, createSlice, PayloadAction } from '@reduxjs/toolkit';
import type { PayloadAction } from '@reduxjs/toolkit';
import { createAction, createListenerMiddleware, createSlice } from '@reduxjs/toolkit';
import { memoizeWith, pipe } from 'ramda';
import { versionToPrintable, versionToSemVer as toSemVer } from '../../utils/helpers/version';
import { isReachableServer, SelectedServer, ServerWithId } from '../data';
import { ShlinkHealth } from '../../api/types';
import type { ShlinkApiClientBuilder } from '../../api/services/ShlinkApiClientBuilder';
import type { ShlinkHealth } from '../../api/types';
import { createAsyncThunk } from '../../utils/helpers/redux';
import { ShlinkApiClientBuilder } from '../../api/services/ShlinkApiClientBuilder';
import { versionToPrintable, versionToSemVer as toSemVer } from '../../utils/helpers/version';
import type { SelectedServer, ServerWithId } from '../data';
import { isReachableServer } from '../data';
const REDUCER_PREFIX = 'shlink/selectedServer';

View File

@@ -1,7 +1,8 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import type { PayloadAction } from '@reduxjs/toolkit';
import { createSlice } from '@reduxjs/toolkit';
import { assoc, dissoc, fromPairs, map, pipe, reduce, toPairs } from 'ramda';
import { v4 as uuid } from 'uuid';
import { ServerData, ServersMap, ServerWithId } from '../data';
import type { ServerData, ServersMap, ServerWithId } from '../data';
interface EditServer {
serverId: string;

View File

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

View File

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

View File

@@ -1,11 +1,18 @@
import type Bottle from 'bottlejs';
import { prop } from 'ramda';
import Bottle from 'bottlejs';
import type { ConnectDecorator } from '../../container/types';
import { CreateServer } from '../CreateServer';
import { ServersDropdown } from '../ServersDropdown';
import { DeleteServerModal } from '../DeleteServerModal';
import { DeleteServerButton } from '../DeleteServerButton';
import { DeleteServerModal } from '../DeleteServerModal';
import { EditServer } from '../EditServer';
import { ImportServersBtn } from '../helpers/ImportServersBtn';
import { ServerError } from '../helpers/ServerError';
import { withoutSelectedServer } from '../helpers/withoutSelectedServer';
import { ManageServers } from '../ManageServers';
import { ManageServersRow } from '../ManageServersRow';
import { ManageServersRowDropdown } from '../ManageServersRowDropdown';
import { Overview } from '../Overview';
import { fetchServers } from '../reducers/remoteServers';
import {
resetSelectedServer,
selectedServerReducerCreator,
@@ -13,18 +20,11 @@ import {
selectServerListener,
} from '../reducers/selectedServer';
import { createServers, deleteServer, editServer, setAutoConnect } from '../reducers/servers';
import { fetchServers } from '../reducers/remoteServers';
import { ServerError } from '../helpers/ServerError';
import { ConnectDecorator } from '../../container/types';
import { withoutSelectedServer } from '../helpers/withoutSelectedServer';
import { Overview } from '../Overview';
import { ManageServers } from '../ManageServers';
import { ManageServersRow } from '../ManageServersRow';
import { ManageServersRowDropdown } from '../ManageServersRowDropdown';
import { ServersDropdown } from '../ServersDropdown';
import { ServersExporter } from './ServersExporter';
import { ServersImporter } from './ServersImporter';
import ServersExporter from './ServersExporter';
const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
export const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
// Components
bottle.serviceFactory(
'ManageServers',
@@ -65,7 +65,7 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
bottle.serviceFactory('Overview', Overview, 'ShortUrlsTable', 'CreateShortUrl');
bottle.decorator('Overview', connect(
['shortUrlsList', 'tagsList', 'selectedServer', 'mercureInfo', 'visitsOverview'],
['shortUrlsList', 'tagsList', 'selectedServer', 'mercureInfo', 'visitsOverview', 'settings'],
['listShortUrls', 'listTags', 'createNewVisits', 'loadMercureInfo', 'loadVisitsOverview'],
));
@@ -89,5 +89,3 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
bottle.serviceFactory('selectedServerReducerCreator', selectedServerReducerCreator, 'selectServer');
bottle.serviceFactory('selectedServerReducer', prop('reducer'), 'selectedServerReducerCreator');
};
export default provideServices;

View File

@@ -1,11 +1,11 @@
import { FormGroup, Input } from 'reactstrap';
import classNames from 'classnames';
import { ToggleSwitch } from '../utils/ToggleSwitch';
import { SimpleCard } from '../utils/SimpleCard';
import { FormGroup, Input } from 'reactstrap';
import { FormText } from '../utils/forms/FormText';
import { LabeledFormGroup } from '../utils/forms/LabeledFormGroup';
import { Settings } from './reducers/settings';
import { useDomId } from '../utils/helpers/hooks';
import { SimpleCard } from '../utils/SimpleCard';
import { ToggleSwitch } from '../utils/ToggleSwitch';
import type { Settings } from './reducers/settings';
interface RealTimeUpdatesProps {
settings: Settings;

View File

@@ -1,5 +1,5 @@
import { FC, ReactNode } from 'react';
import { Navigate, Routes, Route } from 'react-router-dom';
import type { FC, ReactNode } from 'react';
import { Navigate, Route, Routes } from 'react-router-dom';
import { NoMenuLayout } from '../common/NoMenuLayout';
import { NavPillItem, NavPills } from '../utils/NavPills';

View File

@@ -1,11 +1,11 @@
import { FC, ReactNode } from 'react';
import type { FC, ReactNode } from 'react';
import { DropdownItem, FormGroup } from 'reactstrap';
import { SimpleCard } from '../utils/SimpleCard';
import { ToggleSwitch } from '../utils/ToggleSwitch';
import { DropdownBtn } from '../utils/DropdownBtn';
import { FormText } from '../utils/forms/FormText';
import { LabeledFormGroup } from '../utils/forms/LabeledFormGroup';
import { Settings, ShortUrlCreationSettings as ShortUrlsSettings, TagFilteringMode } from './reducers/settings';
import { SimpleCard } from '../utils/SimpleCard';
import { ToggleSwitch } from '../utils/ToggleSwitch';
import type { Settings, ShortUrlCreationSettings as ShortUrlsSettings, TagFilteringMode } from './reducers/settings';
interface ShortUrlCreationProps {
settings: Settings;

View File

@@ -1,9 +1,10 @@
import { FC } from 'react';
import { OrderingDropdown } from '../utils/OrderingDropdown';
import type { FC } from 'react';
import { SHORT_URLS_ORDERABLE_FIELDS } from '../short-urls/data';
import { SimpleCard } from '../utils/SimpleCard';
import { LabeledFormGroup } from '../utils/forms/LabeledFormGroup';
import { DEFAULT_SHORT_URLS_ORDERING, Settings, ShortUrlsListSettings as ShortUrlsSettings } from './reducers/settings';
import { OrderingDropdown } from '../utils/OrderingDropdown';
import { SimpleCard } from '../utils/SimpleCard';
import type { Settings, ShortUrlsListSettings as ShortUrlsSettings } from './reducers/settings';
import { DEFAULT_SHORT_URLS_ORDERING } from './reducers/settings';
interface ShortUrlsListSettingsProps {
settings: Settings;

View File

@@ -1,9 +1,9 @@
import { FC } from 'react';
import { SimpleCard } from '../utils/SimpleCard';
import { OrderingDropdown } from '../utils/OrderingDropdown';
import type { FC } from 'react';
import { TAGS_ORDERABLE_FIELDS } from '../tags/data/TagsListChildrenProps';
import { LabeledFormGroup } from '../utils/forms/LabeledFormGroup';
import { Settings, TagsSettings as TagsSettingsOptions } from './reducers/settings';
import { OrderingDropdown } from '../utils/OrderingDropdown';
import { SimpleCard } from '../utils/SimpleCard';
import type { Settings, TagsSettings as TagsSettingsOptions } from './reducers/settings';
interface TagsProps {
settings: Settings;

View File

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

View File

@@ -1,12 +1,12 @@
import { FC } from 'react';
import type { FC } from 'react';
import { FormGroup } from 'reactstrap';
import { SimpleCard } from '../utils/SimpleCard';
import { DateIntervalSelector } from '../utils/dates/DateIntervalSelector';
import { LabeledFormGroup } from '../utils/forms/LabeledFormGroup';
import { Settings, VisitsSettings as VisitsSettingsConfig } from './reducers/settings';
import { ToggleSwitch } from '../utils/ToggleSwitch';
import { FormText } from '../utils/forms/FormText';
import { DateInterval } from '../utils/helpers/dateIntervals';
import { LabeledFormGroup } from '../utils/forms/LabeledFormGroup';
import type { DateInterval } from '../utils/helpers/dateIntervals';
import { SimpleCard } from '../utils/SimpleCard';
import { ToggleSwitch } from '../utils/ToggleSwitch';
import type { Settings, VisitsSettings as VisitsSettingsConfig } from './reducers/settings';
interface VisitsProps {
settings: Settings;

View File

@@ -1,4 +1,4 @@
import { ShlinkState } from '../../container/types';
import type { ShlinkState } from '../../container/types';
/* eslint-disable no-param-reassign */
export const migrateDeprecatedSettings = (state: Partial<ShlinkState>): Partial<ShlinkState> => {

View File

@@ -1,9 +1,10 @@
import { createSlice, PayloadAction, PrepareAction } from '@reduxjs/toolkit';
import type { PayloadAction, PrepareAction } from '@reduxjs/toolkit';
import { createSlice } from '@reduxjs/toolkit';
import { mergeDeepRight } from 'ramda';
import { Theme } from '../../utils/theme';
import { DateInterval } from '../../utils/helpers/dateIntervals';
import { TagsOrder } from '../../tags/data/TagsListChildrenProps';
import { ShortUrlsOrder } from '../../short-urls/data';
import type { ShortUrlsOrder } from '../../short-urls/data';
import type { TagsOrder } from '../../tags/data/TagsListChildrenProps';
import type { DateInterval } from '../../utils/helpers/dateIntervals';
import type { Theme } from '../../utils/theme';
export const DEFAULT_SHORT_URLS_ORDERING: ShortUrlsOrder = {
field: 'dateCreated',

View File

@@ -1,6 +1,7 @@
import Bottle from 'bottlejs';
import type Bottle from 'bottlejs';
import type { ConnectDecorator } from '../../container/types';
import { withoutSelectedServer } from '../../servers/helpers/withoutSelectedServer';
import { RealTimeUpdatesSettings } from '../RealTimeUpdatesSettings';
import { Settings } from '../Settings';
import {
setRealTimeUpdatesInterval,
setShortUrlCreationSettings,
@@ -10,15 +11,14 @@ import {
setVisitsSettings,
toggleRealTimeUpdates,
} from '../reducers/settings';
import { ConnectDecorator } from '../../container/types';
import { withoutSelectedServer } from '../../servers/helpers/withoutSelectedServer';
import { Settings } from '../Settings';
import { ShortUrlCreationSettings } from '../ShortUrlCreationSettings';
import { ShortUrlsListSettings } from '../ShortUrlsListSettings';
import { TagsSettings } from '../TagsSettings';
import { UserInterfaceSettings } from '../UserInterfaceSettings';
import { VisitsSettings } from '../VisitsSettings';
import { TagsSettings } from '../TagsSettings';
import { ShortUrlsListSettings } from '../ShortUrlsListSettings';
const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
export const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
// Components
bottle.serviceFactory(
'Settings',
@@ -63,5 +63,3 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
bottle.serviceFactory('setVisitsSettings', () => setVisitsSettings);
bottle.serviceFactory('setTagsSettings', () => setTagsSettings);
};
export default provideServices;

View File

@@ -1,10 +1,11 @@
import { FC, useMemo } from 'react';
import { SelectedServer } from '../servers/data';
import { Settings, ShortUrlCreationSettings } from '../settings/reducers/settings';
import { ShortUrlData } from './data';
import { ShortUrlCreation } from './reducers/shortUrlCreation';
import { CreateShortUrlResultProps } from './helpers/CreateShortUrlResult';
import { ShortUrlFormProps } from './ShortUrlForm';
import type { FC } from 'react';
import { useMemo } from 'react';
import type { SelectedServer } from '../servers/data';
import type { Settings, ShortUrlCreationSettings } from '../settings/reducers/settings';
import type { ShortUrlData } from './data';
import type { CreateShortUrlResultProps } from './helpers/CreateShortUrlResult';
import type { ShortUrlCreation } from './reducers/shortUrlCreation';
import type { ShortUrlFormProps } from './ShortUrlForm';
export interface CreateShortUrlProps {
basicMode?: boolean;

View File

@@ -1,21 +1,22 @@
import { FC, useEffect, useMemo } from 'react';
import { Button, Card } from 'reactstrap';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faArrowLeft } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import type { FC } from 'react';
import { useEffect, useMemo } from 'react';
import { ExternalLink } from 'react-external-link';
import { useLocation, useParams } from 'react-router-dom';
import { SelectedServer } from '../servers/data';
import { Settings } from '../settings/reducers/settings';
import { ShortUrlIdentifier } from './data';
import { Button, Card } from 'reactstrap';
import { ShlinkApiError } from '../api/ShlinkApiError';
import type { SelectedServer } from '../servers/data';
import type { Settings } from '../settings/reducers/settings';
import { useGoBack } from '../utils/helpers/hooks';
import { parseQuery } from '../utils/helpers/query';
import { Message } from '../utils/Message';
import { Result } from '../utils/Result';
import { ShlinkApiError } from '../api/ShlinkApiError';
import { useGoBack } from '../utils/helpers/hooks';
import { ShortUrlFormProps } from './ShortUrlForm';
import { ShortUrlDetail } from './reducers/shortUrlDetail';
import { EditShortUrl as EditShortUrlInfo, ShortUrlEdition } from './reducers/shortUrlEdition';
import type { ShortUrlIdentifier } from './data';
import { shortUrlDataFromShortUrl, urlDecodeShortCode } from './helpers';
import type { ShortUrlDetail } from './reducers/shortUrlDetail';
import type { EditShortUrl as EditShortUrlInfo, ShortUrlEdition } from './reducers/shortUrlEdition';
import type { ShortUrlFormProps } from './ShortUrlForm';
interface EditShortUrlConnectProps {
settings: Settings;

View File

@@ -1,13 +1,14 @@
import { Link } from 'react-router-dom';
import { Pagination, PaginationItem, PaginationLink } from 'reactstrap';
import type { ShlinkPaginator } from '../api/types';
import type {
NumberOrEllipsis } from '../utils/helpers/pagination';
import {
pageIsEllipsis,
keyForPage,
progressivePagination,
pageIsEllipsis,
prettifyPageNumber,
NumberOrEllipsis,
progressivePagination,
} from '../utils/helpers/pagination';
import { ShlinkPaginator } from '../api/types';
interface PaginatorProps {
paginator?: ShlinkPaginator;

View File

@@ -1,20 +1,27 @@
import { FC, useEffect, useState } from 'react';
import { InputType } from 'reactstrap/types/lib/Input';
import { Button, FormGroup, Input, Row } from 'reactstrap';
import { cond, isEmpty, pipe, replace, trim, T } from 'ramda';
import type { IconProp } from '@fortawesome/fontawesome-svg-core';
import { faAndroid, faApple } from '@fortawesome/free-brands-svg-icons';
import { faDesktop } from '@fortawesome/free-solid-svg-icons';
import classNames from 'classnames';
import { parseISO } from 'date-fns';
import { DateTimeInput, DateTimeInputProps } from '../utils/dates/DateTimeInput';
import { supportsForwardQuery } from '../utils/helpers/features';
import { SimpleCard } from '../utils/SimpleCard';
import { handleEventPreventingDefault, hasValue, OptionalString } from '../utils/utils';
import { isEmpty, pipe, replace, trim } from 'ramda';
import type { ChangeEvent, FC } from 'react';
import { useEffect, useState } from 'react';
import { Button, FormGroup, Input, Row } from 'reactstrap';
import type { InputType } from 'reactstrap/types/lib/Input';
import type { DomainSelectorProps } from '../domains/DomainSelector';
import type { SelectedServer } from '../servers/data';
import type { TagsSelectorProps } from '../tags/helpers/TagsSelector';
import { Checkbox } from '../utils/Checkbox';
import { SelectedServer } from '../servers/data';
import { TagsSelectorProps } from '../tags/helpers/TagsSelector';
import { DomainSelectorProps } from '../domains/DomainSelector';
import type { DateTimeInputProps } from '../utils/dates/DateTimeInput';
import { DateTimeInput } from '../utils/dates/DateTimeInput';
import { formatIsoDate } from '../utils/helpers/date';
import { UseExistingIfFoundInfoIcon } from './UseExistingIfFoundInfoIcon';
import { ShortUrlData } from './data';
import { useFeature } from '../utils/helpers/features';
import { IconInput } from '../utils/IconInput';
import { SimpleCard } from '../utils/SimpleCard';
import { handleEventPreventingDefault, hasValue } from '../utils/utils';
import type { DeviceLongUrls, ShortUrlData } from './data';
import { ShortUrlFormCheckboxGroup } from './helpers/ShortUrlFormCheckboxGroup';
import { UseExistingIfFoundInfoIcon } from './UseExistingIfFoundInfoIcon';
import './ShortUrlForm.scss';
export type Mode = 'create' | 'create-basic' | 'edit';
@@ -38,38 +45,38 @@ export const ShortUrlForm = (
DomainSelector: FC<DomainSelectorProps>,
): FC<ShortUrlFormProps> => ({ mode, saving, onSave, initialState, selectedServer }) => {
const [shortUrlData, setShortUrlData] = useState(initialState);
const reset = () => setShortUrlData(initialState);
const supportsDeviceLongUrls = useFeature('deviceLongUrls', selectedServer);
const isEdit = mode === 'edit';
const isBasicMode = mode === 'create-basic';
const hadTitleOriginally = hasValue(initialState.title);
const changeTags = (tags: string[]) => setShortUrlData({ ...shortUrlData, tags: tags.map(normalizeTag) });
const reset = () => setShortUrlData(initialState);
const resolveNewTitle = (): OptionalString => {
const hasNewTitle = hasValue(shortUrlData.title);
const matcher = cond<never, OptionalString>([
[() => !hasNewTitle && !hadTitleOriginally, () => undefined],
[() => !hasNewTitle && hadTitleOriginally, () => null],
[T, () => shortUrlData.title],
]);
const setResettableValue = (value: string, initialValue?: any) => {
if (hasValue(value)) {
return value;
}
return matcher();
// If an initial value was provided for this when the input is "emptied", explicitly set it to null so that the
// value gets removed. Otherwise, set undefined so that it gets ignored.
return hasValue(initialValue) ? null : undefined;
};
const submit = handleEventPreventingDefault(async () => onSave({
...shortUrlData,
validSince: formatIsoDate(shortUrlData.validSince) ?? null,
validUntil: formatIsoDate(shortUrlData.validUntil) ?? null,
maxVisits: !hasValue(shortUrlData.maxVisits) ? null : Number(shortUrlData.maxVisits),
title: resolveNewTitle(),
}).then(() => !isEdit && reset()).catch(() => {}));
useEffect(() => {
setShortUrlData(initialState);
}, [initialState]);
// TODO Consider extracting these functions to local components
const renderOptionalInput = (
id: NonDateFields,
placeholder: string,
type: InputType = 'text',
props = {},
props: any = {},
fromGroupProps = {},
) => (
<FormGroup {...fromGroupProps}>
@@ -78,11 +85,27 @@ export const ShortUrlForm = (
type={type}
placeholder={placeholder}
value={shortUrlData[id] ?? ''}
onChange={(e) => setShortUrlData({ ...shortUrlData, [id]: e.target.value })}
onChange={props.onChange ?? ((e) => setShortUrlData({ ...shortUrlData, [id]: e.target.value }))}
{...props}
/>
</FormGroup>
);
const renderDeviceLongUrlInput = (id: keyof DeviceLongUrls, placeholder: string, icon: IconProp) => (
<IconInput
icon={icon}
id={id}
type="url"
placeholder={placeholder}
value={shortUrlData.deviceLongUrls?.[id] ?? ''}
onChange={(e) => setShortUrlData({
...shortUrlData,
deviceLongUrls: {
...(shortUrlData.deviceLongUrls ?? {}),
[id]: setResettableValue(e.target.value, initialState.deviceLongUrls?.[id]),
},
})}
/>
);
const renderDateInput = (id: DateFields, placeholder: string, props: Partial<DateTimeInputProps> = {}) => (
<DateTimeInput
selected={shortUrlData[id] ? toDate(shortUrlData[id] as string | Date) : null}
@@ -113,21 +136,45 @@ export const ShortUrlForm = (
</>
);
const showForwardQueryControl = supportsForwardQuery(selectedServer);
const showForwardQueryControl = useFeature('forwardQuery', selectedServer);
return (
<form name="shortUrlForm" className="short-url-form" onSubmit={submit}>
{isBasicMode && basicComponents}
{!isBasicMode && (
<>
<SimpleCard title="Main options" className="mb-3">
{basicComponents}
</SimpleCard>
<Row>
<div
className={classNames('mb-3', { 'col-sm-6': supportsDeviceLongUrls, 'col-12': !supportsDeviceLongUrls })}
>
<SimpleCard title="Main options" className="mb-3">
{basicComponents}
</SimpleCard>
</div>
{supportsDeviceLongUrls && (
<div className="col-sm-6 mb-3">
<SimpleCard title="Device-specific long URLs">
<FormGroup>
{renderDeviceLongUrlInput('android', 'Android-specific redirection', faAndroid)}
</FormGroup>
<FormGroup>
{renderDeviceLongUrlInput('ios', 'iOS-specific redirection', faApple)}
</FormGroup>
{renderDeviceLongUrlInput('desktop', 'Desktop-specific redirection', faDesktop)}
</SimpleCard>
</div>
)}
</Row>
<Row>
<div className="col-sm-6 mb-3">
<SimpleCard title="Customize the short URL">
{renderOptionalInput('title', 'Title')}
{renderOptionalInput('title', 'Title', 'text', {
onChange: ({ target }: ChangeEvent<HTMLInputElement>) => setShortUrlData({
...shortUrlData,
title: setResettableValue(target.value, initialState.title),
}),
})}
{!isEdit && (
<>
<Row>

View File

@@ -1,23 +1,25 @@
import { FC } from 'react';
import { isEmpty, pipe } from 'ramda';
import { Button, InputGroup, Row, UncontrolledTooltip } from 'reactstrap';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faTag, faTags } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import classNames from 'classnames';
import { SearchField } from '../utils/SearchField';
import { isEmpty, pipe } from 'ramda';
import type { FC } from 'react';
import { Button, InputGroup, Row, UncontrolledTooltip } from 'reactstrap';
import type { SelectedServer } from '../servers/data';
import type { Settings } from '../settings/reducers/settings';
import type { TagsSelectorProps } from '../tags/helpers/TagsSelector';
import { DateRangeSelector } from '../utils/dates/DateRangeSelector';
import { formatIsoDate } from '../utils/helpers/date';
import { DateRange, datesToDateRange } from '../utils/helpers/dateIntervals';
import { supportsAllTagsFiltering, supportsFilterDisabledUrls } from '../utils/helpers/features';
import { SelectedServer } from '../servers/data';
import { OrderDir } from '../utils/helpers/ordering';
import type { DateRange } from '../utils/helpers/dateIntervals';
import { datesToDateRange } from '../utils/helpers/dateIntervals';
import { useFeature } from '../utils/helpers/features';
import type { OrderDir } from '../utils/helpers/ordering';
import { OrderingDropdown } from '../utils/OrderingDropdown';
import { SearchField } from '../utils/SearchField';
import type { ShortUrlsOrder, ShortUrlsOrderableFields } from './data';
import { SHORT_URLS_ORDERABLE_FIELDS } from './data';
import type { ExportShortUrlsBtnProps } from './helpers/ExportShortUrlsBtn';
import { useShortUrlsQuery } from './helpers/hooks';
import { SHORT_URLS_ORDERABLE_FIELDS, ShortUrlsOrder, ShortUrlsOrderableFields } from './data';
import { ExportShortUrlsBtnProps } from './helpers/ExportShortUrlsBtn';
import { TagsSelectorProps } from '../tags/helpers/TagsSelector';
import { ShortUrlsFilterDropdown } from './helpers/ShortUrlsFilterDropdown';
import { Settings } from '../settings/reducers/settings';
import './ShortUrlsFilteringBar.scss';
interface ShortUrlsFilteringProps {
@@ -44,7 +46,7 @@ export const ShortUrlsFilteringBar = (
excludePastValidUntil,
tagsMode = 'any',
} = filter;
const supportsDisabledFiltering = supportsFilterDisabledUrls(selectedServer);
const supportsDisabledFiltering = useFeature('filterDisabledUrls', selectedServer);
const setDates = pipe(
({ startDate: theStartDate, endDate: theEndDate }: DateRange) => ({
@@ -58,7 +60,7 @@ export const ShortUrlsFilteringBar = (
(searchTerm) => toFirstPage({ search: searchTerm }),
);
const changeTagSelection = (selectedTags: string[]) => toFirstPage({ tags: selectedTags });
const canChangeTagsMode = supportsAllTagsFiltering(selectedServer);
const canChangeTagsMode = useFeature('allTagsFiltering', selectedServer);
const toggleTagsMode = pipe(
() => (tagsMode === 'any' ? 'all' : 'any'),
(mode) => toFirstPage({ tagsMode: mode }),

View File

@@ -1,21 +1,24 @@
import { pipe } from 'ramda';
import { useEffect, useState } from 'react';
import { Card } from 'reactstrap';
import { useLocation, useParams } from 'react-router-dom';
import { determineOrderDir, OrderDir } from '../utils/helpers/ordering';
import { getServerId, SelectedServer } from '../servers/data';
import { Card } from 'reactstrap';
import type { ShlinkShortUrlsListParams, ShlinkShortUrlsOrder } from '../api/types';
import { boundToMercureHub } from '../mercure/helpers/boundToMercureHub';
import { Topics } from '../mercure/helpers/Topics';
import type { SelectedServer } from '../servers/data';
import { getServerId } from '../servers/data';
import type { Settings } from '../settings/reducers/settings';
import { DEFAULT_SHORT_URLS_ORDERING } from '../settings/reducers/settings';
import { useFeature } from '../utils/helpers/features';
import type { OrderDir } from '../utils/helpers/ordering';
import { determineOrderDir } from '../utils/helpers/ordering';
import { TableOrderIcon } from '../utils/table/TableOrderIcon';
import { ShlinkShortUrlsListParams, ShlinkShortUrlsOrder } from '../api/types';
import { DEFAULT_SHORT_URLS_ORDERING, Settings } from '../settings/reducers/settings';
import { ShortUrlsList as ShortUrlsListState } from './reducers/shortUrlsList';
import { ShortUrlsTableType } from './ShortUrlsTable';
import { Paginator } from './Paginator';
import type { ShortUrlsOrder, ShortUrlsOrderableFields } from './data';
import { useShortUrlsQuery } from './helpers/hooks';
import { ShortUrlsOrder, ShortUrlsOrderableFields } from './data';
import { ShortUrlsFilteringBarType } from './ShortUrlsFilteringBar';
import { supportsExcludeBotsOnShortUrls } from '../utils/helpers/features';
import { Paginator } from './Paginator';
import type { ShortUrlsList as ShortUrlsListState } from './reducers/shortUrlsList';
import type { ShortUrlsFilteringBarType } from './ShortUrlsFilteringBar';
import type { ShortUrlsTableType } from './ShortUrlsTable';
interface ShortUrlsListProps {
selectedServer: SelectedServer;
@@ -49,6 +52,7 @@ export const ShortUrlsList = (
);
const { pagination } = shortUrlsList?.shortUrls ?? {};
const doExcludeBots = excludeBots ?? settings.visits?.excludeBots;
const supportsExcludingBots = useFeature('excludeBotsOnShortUrls', selectedServer);
const handleOrderBy = (field?: ShortUrlsOrderableFields, dir?: OrderDir) => {
toFirstPage({ orderBy: { field, dir } });
setActualOrderBy({ field, dir });
@@ -62,7 +66,7 @@ export const ShortUrlsList = (
(updatedTags) => toFirstPage({ tags: updatedTags }),
);
const parseOrderByForShlink = ({ field, dir }: ShortUrlsOrder): ShlinkShortUrlsOrder => {
if (supportsExcludeBotsOnShortUrls(selectedServer) && doExcludeBots && field === 'visits') {
if (supportsExcludingBots && doExcludeBots && field === 'visits') {
return { field: 'nonBotVisits', dir };
}

View File

@@ -1,10 +1,10 @@
import { ReactNode } from 'react';
import { isEmpty } from 'ramda';
import classNames from 'classnames';
import { SelectedServer } from '../servers/data';
import { ShortUrlsList as ShortUrlsListState } from './reducers/shortUrlsList';
import { ShortUrlsRowType } from './helpers/ShortUrlsRow';
import { ShortUrlsOrderableFields } from './data';
import { isEmpty } from 'ramda';
import type { ReactNode } from 'react';
import type { SelectedServer } from '../servers/data';
import type { ShortUrlsOrderableFields } from './data';
import type { ShortUrlsRowType } from './helpers/ShortUrlsRow';
import type { ShortUrlsList as ShortUrlsListState } from './reducers/shortUrlsList';
import './ShortUrlsTable.scss';
interface ShortUrlsTableProps {

View File

@@ -1,5 +1,5 @@
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faInfoCircle as infoIcon } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Modal, ModalBody, ModalHeader } from 'reactstrap';
import { useToggle } from '../utils/helpers/hooks';
import './UseExistingIfFoundInfoIcon.scss';

View File

@@ -1,8 +1,16 @@
import { Nullable, OptionalString } from '../../utils/utils';
import { Order } from '../../utils/helpers/ordering';
import type { ShlinkVisitsSummary } from '../../api/types';
import type { Order } from '../../utils/helpers/ordering';
import type { Nullable, OptionalString } from '../../utils/utils';
export interface DeviceLongUrls {
android?: OptionalString;
ios?: OptionalString;
desktop?: OptionalString;
}
export interface EditShortUrlData {
longUrl?: string;
deviceLongUrls?: DeviceLongUrls;
tags?: string[];
title?: string | null;
validSince?: Date | string | null;
@@ -30,10 +38,11 @@ export interface ShortUrl {
shortCode: string;
shortUrl: string;
longUrl: string;
deviceLongUrls?: Required<DeviceLongUrls>, // Optional only before Shlink 3.5.0
dateCreated: string;
/** @deprecated */
visitsCount: number; // Deprecated since Shlink 3.4.0
visitsSummary?: ShortUrlVisitsSummary; // Optional only before Shlink 3.4.0
visitsSummary?: ShlinkVisitsSummary; // Optional only before Shlink 3.4.0
meta: Required<Nullable<ShortUrlMeta>>;
tags: string[];
domain: string | null;
@@ -48,12 +57,6 @@ export interface ShortUrlMeta {
maxVisits?: number;
}
export interface ShortUrlVisitsSummary {
total: number;
nonBots: number;
bots: number;
}
export interface ShortUrlModalProps {
shortUrl: ShortUrl;
isOpen: boolean;
@@ -76,6 +79,8 @@ export interface ExportableShortUrl {
createdAt: string;
title: string;
shortUrl: string;
domain?: string;
shortCode: string;
longUrl: string;
tags: string;
visits: number;

View File

@@ -1,14 +1,14 @@
import { faCopy as copyIcon } from '@fortawesome/free-regular-svg-icons';
import { faClone as copyIcon } from '@fortawesome/free-regular-svg-icons';
import { faTimes as closeIcon } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { useEffect } from 'react';
import CopyToClipboard from 'react-copy-to-clipboard';
import { Tooltip } from 'reactstrap';
import { ShortUrlCreation } from '../reducers/shortUrlCreation';
import { TimeoutToggle } from '../../utils/helpers/hooks';
import { Result } from '../../utils/Result';
import './CreateShortUrlResult.scss';
import { ShlinkApiError } from '../../api/ShlinkApiError';
import type { TimeoutToggle } from '../../utils/helpers/hooks';
import { Result } from '../../utils/Result';
import type { ShortUrlCreation } from '../reducers/shortUrlCreation';
import './CreateShortUrlResult.scss';
export interface CreateShortUrlResultProps {
creation: ShortUrlCreation;

View File

@@ -1,12 +1,12 @@
import { pipe } from 'ramda';
import { useEffect, useState } from 'react';
import { Modal, ModalBody, ModalFooter, ModalHeader } from 'reactstrap';
import { pipe } from 'ramda';
import { ShortUrlDeletion } from '../reducers/shortUrlDeletion';
import { ShortUrlIdentifier, ShortUrlModalProps } from '../data';
import { handleEventPreventingDefault } from '../../utils/utils';
import { Result } from '../../utils/Result';
import { isInvalidDeletionError } from '../../api/utils';
import { ShlinkApiError } from '../../api/ShlinkApiError';
import { isInvalidDeletionError } from '../../api/utils';
import { Result } from '../../utils/Result';
import { handleEventPreventingDefault } from '../../utils/utils';
import type { ShortUrlIdentifier, ShortUrlModalProps } from '../data';
import type { ShortUrlDeletion } from '../reducers/shortUrlDeletion';
interface DeleteShortUrlModalConnectProps extends ShortUrlModalProps {
shortUrlDeletion: ShortUrlDeletion;

View File

@@ -1,10 +1,12 @@
import { FC } from 'react';
import type { FC } from 'react';
import { useCallback } from 'react';
import type { ShlinkApiClientBuilder } from '../../api/services/ShlinkApiClientBuilder';
import type { ReportExporter } from '../../common/services/ReportExporter';
import type { SelectedServer } from '../../servers/data';
import { isServerWithId } from '../../servers/data';
import { ExportBtn } from '../../utils/ExportBtn';
import { useToggle } from '../../utils/helpers/hooks';
import { ShlinkApiClientBuilder } from '../../api/services/ShlinkApiClientBuilder';
import { isServerWithId, SelectedServer } from '../../servers/data';
import { ShortUrl } from '../data';
import { ReportExporter } from '../../common/services/ReportExporter';
import type { ShortUrl } from '../data';
import { useShortUrlsQuery } from './hooks';
export interface ExportShortUrlsBtnProps {
@@ -23,7 +25,7 @@ export const ExportShortUrlsBtn = (
): FC<ExportShortUrlsBtnConnectProps> => ({ amount = 0, selectedServer }) => {
const [{ tags, search, startDate, endDate, orderBy, tagsMode }] = useShortUrlsQuery();
const [loading,, startLoading, stopLoading] = useToggle();
const exportAllUrls = async () => {
const exportAllUrls = useCallback(async () => {
if (!isServerWithId(selectedServer)) {
return;
}
@@ -46,16 +48,23 @@ export const ExportShortUrlsBtn = (
startLoading();
const shortUrls = await loadAllUrls();
exportShortUrls(shortUrls.map((shortUrl) => ({
createdAt: shortUrl.dateCreated,
shortUrl: shortUrl.shortUrl,
longUrl: shortUrl.longUrl,
title: shortUrl.title ?? '',
tags: shortUrl.tags.join(','),
visits: shortUrl?.visitsSummary?.total ?? shortUrl.visitsCount,
})));
exportShortUrls(shortUrls.map((shortUrl) => {
const { hostname: domain, pathname } = new URL(shortUrl.shortUrl);
const shortCode = pathname.substring(1); // Remove trailing slash
return {
createdAt: shortUrl.dateCreated,
domain,
shortCode,
shortUrl: shortUrl.shortUrl,
longUrl: shortUrl.longUrl,
title: shortUrl.title ?? '',
tags: shortUrl.tags.join('|'),
visits: shortUrl?.visitsSummary?.total ?? shortUrl.visitsCount,
};
}));
stopLoading();
};
}, [selectedServer]);
return <ExportBtn loading={loading} className="btn-md-block" amount={amount} onClick={exportAllUrls} />;
};

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