mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2025-12-10 14:16:59 -06:00
commit
457458a894
@ -1,5 +1,4 @@
|
|||||||
./.github
|
./.github
|
||||||
./.stryker-tmp
|
|
||||||
./build
|
./build
|
||||||
./coverage
|
./coverage
|
||||||
./node_modules
|
./node_modules
|
||||||
|
|||||||
1
.github/workflows/ci.yml
vendored
1
.github/workflows/ci.yml
vendored
@ -12,6 +12,5 @@ jobs:
|
|||||||
uses: shlinkio/github-actions/.github/workflows/web-app-ci.yml@main
|
uses: shlinkio/github-actions/.github/workflows/web-app-ci.yml@main
|
||||||
with:
|
with:
|
||||||
node-version: 18.12
|
node-version: 18.12
|
||||||
with-mutation-tests: true
|
|
||||||
publish-coverage: true
|
publish-coverage: true
|
||||||
force-install: true
|
force-install: true
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@ -3,7 +3,6 @@
|
|||||||
|
|
||||||
# testing
|
# testing
|
||||||
/coverage
|
/coverage
|
||||||
/.stryker-tmp
|
|
||||||
/reports
|
/reports
|
||||||
|
|
||||||
# production
|
# production
|
||||||
|
|||||||
24
CHANGELOG.md
24
CHANGELOG.md
@ -4,6 +4,30 @@ All notable changes to this project will be documented in this file.
|
|||||||
|
|
||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org).
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org).
|
||||||
|
|
||||||
|
## [3.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
|
## [3.9.1] - 2022-12-31
|
||||||
### Added
|
### Added
|
||||||
* *Nothing*
|
* *Nothing*
|
||||||
|
|||||||
@ -5,7 +5,7 @@
|
|||||||
[](https://github.com/shlinkio/shlink-web-client/releases/latest)
|
[](https://github.com/shlinkio/shlink-web-client/releases/latest)
|
||||||
[](https://hub.docker.com/r/shlinkio/shlink-web-client/)
|
[](https://hub.docker.com/r/shlinkio/shlink-web-client/)
|
||||||
[](https://github.com/shlinkio/shlink-web-client/blob/main/LICENSE)
|
[](https://github.com/shlinkio/shlink-web-client/blob/main/LICENSE)
|
||||||
[](https://twitter.com/shlinkio)
|
[](https://twitter.com/shlinkio)
|
||||||
[](https://fosstodon.org/@shlinkio)
|
[](https://fosstodon.org/@shlinkio)
|
||||||
[](https://slnk.to/donate)
|
[](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",
|
"name": "Main server",
|
||||||
"url": "https://doma.in",
|
"url": "https://s.test",
|
||||||
"apiKey": "09c972b7-506b-49f1-a19a-d729e22e599c"
|
"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 \
|
docker run \
|
||||||
--name shlink-web-client \
|
--name shlink-web-client \
|
||||||
-p 8000:80 \
|
-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 \
|
-e SHLINK_SERVER_API_KEY=6aeb82c6-e275-4538-a747-31f9abfba63c \
|
||||||
shlinkio/shlink-web-client
|
shlinkio/shlink-web-client
|
||||||
```
|
```
|
||||||
|
|||||||
@ -10,4 +10,4 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- "3000:3000"
|
- "3000:3000"
|
||||||
- "56745:56745"
|
- "56745:56745"
|
||||||
- "5000:5000"
|
- "4173:4173"
|
||||||
|
|||||||
@ -10,14 +10,13 @@ module.exports = {
|
|||||||
coverageThreshold: {
|
coverageThreshold: {
|
||||||
global: {
|
global: {
|
||||||
statements: 90,
|
statements: 90,
|
||||||
branches: 80,
|
branches: 85,
|
||||||
functions: 85,
|
functions: 90,
|
||||||
lines: 90,
|
lines: 90,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
setupFilesAfterEnv: ['<rootDir>/config/jest/setupTests.ts'],
|
setupFilesAfterEnv: ['<rootDir>/config/jest/setupTests.ts'],
|
||||||
testMatch: ['<rootDir>/test/**/*.test.{ts,tsx}'],
|
testMatch: ['<rootDir>/test/**/*.test.{ts,tsx}'],
|
||||||
modulePathIgnorePatterns: ['<rootDir>/.stryker-tmp'],
|
|
||||||
testEnvironment: 'jsdom',
|
testEnvironment: 'jsdom',
|
||||||
testEnvironmentOptions: {
|
testEnvironmentOptions: {
|
||||||
url: 'http://localhost',
|
url: 'http://localhost',
|
||||||
@ -28,7 +27,6 @@ module.exports = {
|
|||||||
'^(?!.*\\.(ts|tsx|js|json|scss)$)': '<rootDir>/config/jest/fileTransform.js',
|
'^(?!.*\\.(ts|tsx|js|json|scss)$)': '<rootDir>/config/jest/fileTransform.js',
|
||||||
},
|
},
|
||||||
transformIgnorePatterns: [
|
transformIgnorePatterns: [
|
||||||
'<rootDir>/.stryker-tmp',
|
|
||||||
'node_modules\/(?!(\@react-leaflet|react-leaflet|leaflet|react-chartjs-2|react-colorful)\/)',
|
'node_modules\/(?!(\@react-leaflet|react-leaflet|leaflet|react-chartjs-2|react-colorful)\/)',
|
||||||
'^.+\\.module\\.scss$',
|
'^.+\\.module\\.scss$',
|
||||||
],
|
],
|
||||||
|
|||||||
11254
package-lock.json
generated
11254
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
36
package.json
36
package.json
@ -12,25 +12,26 @@
|
|||||||
"lint:fix": "npm run lint:css:fix && npm run lint:js:fix",
|
"lint:fix": "npm run lint:css:fix && npm run lint:js:fix",
|
||||||
"lint:css:fix": "npm run lint:css -- --fix",
|
"lint:css:fix": "npm run lint:css -- --fix",
|
||||||
"lint:js:fix": "npm run lint:js -- --fix",
|
"lint:js:fix": "npm run lint:js -- --fix",
|
||||||
|
"types": "tsc",
|
||||||
"start": "vite serve --host=0.0.0.0",
|
"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:dist": "npm run build && node scripts/create-dist-file.mjs",
|
||||||
"build:serve": "serve -p 5000 ./build",
|
|
||||||
"test": "jest --env=jsdom --colors",
|
"test": "jest --env=jsdom --colors",
|
||||||
"test:coverage": "npm run test -- --coverage --coverageReporters=text --coverageReporters=text-summary",
|
"test:coverage": "npm run test -- --coverage --coverageReporters=text --coverageReporters=text-summary",
|
||||||
"test:ci": "npm run test:coverage -- --coverageReporters=clover --ci",
|
"test:ci": "npm run test:coverage -- --coverageReporters=clover --ci",
|
||||||
"test:pretty": "npm run test:coverage -- --coverageReporters=html",
|
"test:pretty": "npm run test:coverage -- --coverageReporters=html",
|
||||||
"test:verbose": "npm run test -- --verbose",
|
"test:verbose": "npm run test -- --verbose"
|
||||||
"mutate": "./node_modules/.bin/stryker run --concurrency 4 --ignoreStatic"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/preset-env": "^7.20.2",
|
"@babel/preset-env": "^7.20.2",
|
||||||
"@babel/preset-react": "^7.18.6",
|
"@babel/preset-react": "^7.18.6",
|
||||||
"@babel/preset-typescript": "^7.18.6",
|
"@babel/preset-typescript": "^7.21.0",
|
||||||
"@fortawesome/fontawesome-free": "^6.2.1",
|
"@fortawesome/fontawesome-free": "^6.3.0",
|
||||||
"@fortawesome/fontawesome-svg-core": "^6.2.1",
|
"@fortawesome/fontawesome-svg-core": "^6.3.0",
|
||||||
"@fortawesome/free-regular-svg-icons": "^6.2.1",
|
"@fortawesome/free-brands-svg-icons": "^6.3.0",
|
||||||
"@fortawesome/free-solid-svg-icons": "^6.2.1",
|
"@fortawesome/free-regular-svg-icons": "^6.3.0",
|
||||||
|
"@fortawesome/free-solid-svg-icons": "^6.3.0",
|
||||||
"@fortawesome/react-fontawesome": "^0.2.0",
|
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||||
"@json2csv/plainjs": "^6.1.2",
|
"@json2csv/plainjs": "^6.1.2",
|
||||||
"@reduxjs/toolkit": "^1.9.1",
|
"@reduxjs/toolkit": "^1.9.1",
|
||||||
@ -71,11 +72,8 @@
|
|||||||
"workbox-strategies": "^6.5.4"
|
"workbox-strategies": "^6.5.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"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",
|
"@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/jest-dom": "^5.16.5",
|
||||||
"@testing-library/react": "^13.4.0",
|
"@testing-library/react": "^13.4.0",
|
||||||
"@testing-library/user-event": "^14.4.3",
|
"@testing-library/user-event": "^14.4.3",
|
||||||
@ -91,9 +89,9 @@
|
|||||||
"@types/react-dom": "^18.0.10",
|
"@types/react-dom": "^18.0.10",
|
||||||
"@types/react-tag-autocomplete": "^6.3.0",
|
"@types/react-tag-autocomplete": "^6.3.0",
|
||||||
"@types/uuid": "^8.3.4",
|
"@types/uuid": "^8.3.4",
|
||||||
"@vitejs/plugin-react": "^3.0.0",
|
"@vitejs/plugin-react": "^3.1.0",
|
||||||
"adm-zip": "^0.5.10",
|
"adm-zip": "^0.5.10",
|
||||||
"babel-jest": "^29.3.1",
|
"babel-jest": "^29.5.0",
|
||||||
"chalk": "^5.2.0",
|
"chalk": "^5.2.0",
|
||||||
"eslint": "^8.30.0",
|
"eslint": "^8.30.0",
|
||||||
"identity-obj-proxy": "^3.0.0",
|
"identity-obj-proxy": "^3.0.0",
|
||||||
@ -102,13 +100,11 @@
|
|||||||
"jest-environment-jsdom": "^29.3.1",
|
"jest-environment-jsdom": "^29.3.1",
|
||||||
"resize-observer-polyfill": "^1.5.1",
|
"resize-observer-polyfill": "^1.5.1",
|
||||||
"sass": "^1.57.1",
|
"sass": "^1.57.1",
|
||||||
"serve": "^14.1.2",
|
|
||||||
"stryker-cli": "^1.0.2",
|
|
||||||
"stylelint": "^14.16.0",
|
"stylelint": "^14.16.0",
|
||||||
"ts-mockery": "^1.2.0",
|
"ts-mockery": "^1.2.0",
|
||||||
"typescript": "^4.9.4",
|
"typescript": "^5.0.2",
|
||||||
"vite": "^4.0.3",
|
"vite": "^4.2.0",
|
||||||
"vite-plugin-pwa": "^0.14.0"
|
"vite-plugin-pwa": "^0.14.4"
|
||||||
},
|
},
|
||||||
"browserslist": [
|
"browserslist": [
|
||||||
">0.2%",
|
">0.2%",
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
|
import type { ProblemDetailsError } from './types/errors';
|
||||||
import { isInvalidArgumentError } from './utils';
|
import { isInvalidArgumentError } from './utils';
|
||||||
import { ProblemDetailsError } from './types/errors';
|
|
||||||
|
|
||||||
export interface ShlinkApiErrorProps {
|
export interface ShlinkApiErrorProps {
|
||||||
errorData?: ProblemDetailsError;
|
errorData?: ProblemDetailsError;
|
||||||
|
|||||||
@ -1,26 +1,27 @@
|
|||||||
import { isEmpty, isNil, reject } from 'ramda';
|
import { isEmpty, isNil, reject } from 'ramda';
|
||||||
import { ShortUrl, ShortUrlData } from '../../short-urls/data';
|
import type { HttpClient } from '../../common/services/HttpClient';
|
||||||
import { OptionalString } from '../../utils/utils';
|
import type { ShortUrl, ShortUrlData } from '../../short-urls/data';
|
||||||
import {
|
import { orderToString } from '../../utils/helpers/ordering';
|
||||||
|
import { stringifyQuery } from '../../utils/helpers/query';
|
||||||
|
import type { OptionalString } from '../../utils/utils';
|
||||||
|
import type {
|
||||||
|
ShlinkDomainRedirects,
|
||||||
|
ShlinkDomainsResponse,
|
||||||
|
ShlinkEditDomainRedirects,
|
||||||
ShlinkHealth,
|
ShlinkHealth,
|
||||||
ShlinkMercureInfo,
|
ShlinkMercureInfo,
|
||||||
|
ShlinkShortUrlData,
|
||||||
|
ShlinkShortUrlsListNormalizedParams,
|
||||||
|
ShlinkShortUrlsListParams,
|
||||||
ShlinkShortUrlsResponse,
|
ShlinkShortUrlsResponse,
|
||||||
ShlinkTags,
|
ShlinkTags,
|
||||||
ShlinkTagsResponse,
|
ShlinkTagsResponse,
|
||||||
|
ShlinkTagsStatsResponse,
|
||||||
ShlinkVisits,
|
ShlinkVisits,
|
||||||
ShlinkVisitsParams,
|
|
||||||
ShlinkShortUrlData,
|
|
||||||
ShlinkDomainsResponse,
|
|
||||||
ShlinkVisitsOverview,
|
ShlinkVisitsOverview,
|
||||||
ShlinkEditDomainRedirects,
|
ShlinkVisitsParams,
|
||||||
ShlinkDomainRedirects,
|
|
||||||
ShlinkShortUrlsListParams,
|
|
||||||
ShlinkShortUrlsListNormalizedParams,
|
|
||||||
} from '../types';
|
} from '../types';
|
||||||
import { orderToString } from '../../utils/helpers/ordering';
|
|
||||||
import { isRegularNotFound, parseApiError } from '../utils';
|
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 buildShlinkBaseUrl = (url: string, version: 2 | 3) => `${url}/rest/v${version}`;
|
||||||
const rejectNilProps = reject(isNil);
|
const rejectNilProps = reject(isNil);
|
||||||
@ -90,6 +91,11 @@ export class ShlinkApiClient {
|
|||||||
.then(({ tags }) => tags)
|
.then(({ tags }) => tags)
|
||||||
.then(({ data, stats }) => ({ tags: data, stats }));
|
.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[] }> =>
|
public readonly deleteTags = async (tags: string[]): Promise<{ tags: string[] }> =>
|
||||||
this.performEmptyRequest('/tags', 'DELETE', { tags }).then(() => ({ tags }));
|
this.performEmptyRequest('/tags', 'DELETE', { tags }).then(() => ({ tags }));
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,8 @@
|
|||||||
import { hasServerData, ServerWithId } from '../../servers/data';
|
import type { HttpClient } from '../../common/services/HttpClient';
|
||||||
import { GetState } from '../../container/types';
|
import type { GetState } from '../../container/types';
|
||||||
|
import type { ServerWithId } from '../../servers/data';
|
||||||
|
import { hasServerData } from '../../servers/data';
|
||||||
import { ShlinkApiClient } from './ShlinkApiClient';
|
import { ShlinkApiClient } from './ShlinkApiClient';
|
||||||
import { HttpClient } from '../../common/services/HttpClient';
|
|
||||||
|
|
||||||
const apiClients: Record<string, ShlinkApiClient> = {};
|
const apiClients: Record<string, ShlinkApiClient> = {};
|
||||||
|
|
||||||
|
|||||||
@ -1,8 +1,6 @@
|
|||||||
import Bottle from 'bottlejs';
|
import type Bottle from 'bottlejs';
|
||||||
import { buildShlinkApiClient } from './ShlinkApiClientBuilder';
|
import { buildShlinkApiClient } from './ShlinkApiClientBuilder';
|
||||||
|
|
||||||
const provideServices = (bottle: Bottle) => {
|
export const provideServices = (bottle: Bottle) => {
|
||||||
bottle.serviceFactory('buildShlinkApiClient', buildShlinkApiClient, 'HttpClient');
|
bottle.serviceFactory('buildShlinkApiClient', buildShlinkApiClient, 'HttpClient');
|
||||||
};
|
};
|
||||||
|
|
||||||
export default provideServices;
|
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { Visit } from '../../visits/types';
|
import type { ShortUrl, ShortUrlMeta } from '../../short-urls/data';
|
||||||
import { OptionalString } from '../../utils/utils';
|
import type { Order } from '../../utils/helpers/ordering';
|
||||||
import { ShortUrl, ShortUrlMeta } from '../../short-urls/data';
|
import type { OptionalString } from '../../utils/utils';
|
||||||
import { Order } from '../../utils/helpers/ordering';
|
import type { Visit } from '../../visits/types';
|
||||||
|
|
||||||
export interface ShlinkShortUrlsResponse {
|
export interface ShlinkShortUrlsResponse {
|
||||||
data: ShortUrl[];
|
data: ShortUrl[];
|
||||||
@ -18,9 +18,12 @@ export interface ShlinkHealth {
|
|||||||
version: string;
|
version: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ShlinkTagsStats {
|
export interface ShlinkTagsStats {
|
||||||
tag: string;
|
tag: string;
|
||||||
shortUrlsCount: number;
|
shortUrlsCount: number;
|
||||||
|
visitsSummary?: ShlinkVisitsSummary; // Optional only before Shlink 3.5.0
|
||||||
|
|
||||||
|
/** @deprecated */
|
||||||
visitsCount: number;
|
visitsCount: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -31,22 +34,38 @@ export interface ShlinkTags {
|
|||||||
|
|
||||||
export interface ShlinkTagsResponse {
|
export interface ShlinkTagsResponse {
|
||||||
data: string[];
|
data: string[];
|
||||||
|
/** @deprecated Present only when withStats=true is provided, which is deprecated */
|
||||||
stats: ShlinkTagsStats[];
|
stats: ShlinkTagsStats[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ShlinkTagsStatsResponse {
|
||||||
|
data: ShlinkTagsStats[];
|
||||||
|
}
|
||||||
|
|
||||||
export interface ShlinkPaginator {
|
export interface ShlinkPaginator {
|
||||||
currentPage: number;
|
currentPage: number;
|
||||||
pagesCount: number;
|
pagesCount: number;
|
||||||
totalItems: number;
|
totalItems: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ShlinkVisitsSummary {
|
||||||
|
total: number;
|
||||||
|
nonBots: number;
|
||||||
|
bots: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ShlinkVisits {
|
export interface ShlinkVisits {
|
||||||
data: Visit[];
|
data: Visit[];
|
||||||
pagination: ShlinkPaginator;
|
pagination: ShlinkPaginator;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ShlinkVisitsOverview {
|
export interface ShlinkVisitsOverview {
|
||||||
|
nonOrphanVisits?: ShlinkVisitsSummary; // Optional only before Shlink 3.5.0
|
||||||
|
orphanVisits?: ShlinkVisitsSummary; // Optional only before Shlink 3.5.0
|
||||||
|
|
||||||
|
/** @deprecated */
|
||||||
visitsCount: number;
|
visitsCount: number;
|
||||||
|
/** @deprecated */
|
||||||
orphanVisitsCount: number;
|
orphanVisitsCount: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,10 +1,11 @@
|
|||||||
import {
|
import type {
|
||||||
ErrorTypeV2,
|
|
||||||
ErrorTypeV3,
|
|
||||||
InvalidArgumentError,
|
InvalidArgumentError,
|
||||||
InvalidShortUrlDeletion,
|
InvalidShortUrlDeletion,
|
||||||
ProblemDetailsError,
|
ProblemDetailsError,
|
||||||
RegularNotFound,
|
RegularNotFound } from '../types/errors';
|
||||||
|
import {
|
||||||
|
ErrorTypeV2,
|
||||||
|
ErrorTypeV3,
|
||||||
} from '../types/errors';
|
} from '../types/errors';
|
||||||
|
|
||||||
const isProblemDetails = (e: unknown): e is ProblemDetailsError =>
|
const isProblemDetails = (e: unknown): e is ProblemDetailsError =>
|
||||||
|
|||||||
@ -1,12 +1,13 @@
|
|||||||
import { useEffect, FC } from 'react';
|
|
||||||
import { Route, Routes, useLocation } from 'react-router-dom';
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { NotFound } from '../common/NotFound';
|
import type { FC } from 'react';
|
||||||
import { ServersMap } from '../servers/data';
|
import { useEffect } from 'react';
|
||||||
import { Settings } from '../settings/reducers/settings';
|
import { Route, Routes, useLocation } from 'react-router-dom';
|
||||||
import { changeThemeInMarkup } from '../utils/theme';
|
|
||||||
import { AppUpdateBanner } from '../common/AppUpdateBanner';
|
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 { forceUpdate } from '../utils/helpers/sw';
|
||||||
|
import { changeThemeInMarkup } from '../utils/theme';
|
||||||
import './App.scss';
|
import './App.scss';
|
||||||
|
|
||||||
interface AppProps {
|
interface AppProps {
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
import Bottle from 'bottlejs';
|
import type Bottle from 'bottlejs';
|
||||||
import { appUpdateAvailable, resetAppUpdate } from '../reducers/appUpdates';
|
import type { ConnectDecorator } from '../../container/types';
|
||||||
import { App } from '../App';
|
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
|
// Components
|
||||||
bottle.serviceFactory(
|
bottle.serviceFactory(
|
||||||
'App',
|
'App',
|
||||||
@ -23,5 +23,3 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
|
|||||||
bottle.serviceFactory('appUpdateAvailable', () => appUpdateAvailable);
|
bottle.serviceFactory('appUpdateAvailable', () => appUpdateAvailable);
|
||||||
bottle.serviceFactory('resetAppUpdate', () => resetAppUpdate);
|
bottle.serviceFactory('resetAppUpdate', () => resetAppUpdate);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default provideServices;
|
|
||||||
|
|||||||
@ -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 { 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 { useToggle } from '../utils/helpers/hooks';
|
||||||
|
import { SimpleCard } from '../utils/SimpleCard';
|
||||||
import './AppUpdateBanner.scss';
|
import './AppUpdateBanner.scss';
|
||||||
|
|
||||||
interface AppUpdateBannerProps {
|
interface AppUpdateBannerProps {
|
||||||
|
|||||||
@ -1,17 +1,19 @@
|
|||||||
import {
|
import {
|
||||||
faList as listIcon,
|
|
||||||
faLink as createIcon,
|
|
||||||
faTags as tagsIcon,
|
|
||||||
faPen as editIcon,
|
|
||||||
faHome as overviewIcon,
|
|
||||||
faGlobe as domainsIcon,
|
faGlobe as domainsIcon,
|
||||||
|
faHome as overviewIcon,
|
||||||
|
faLink as createIcon,
|
||||||
|
faList as listIcon,
|
||||||
|
faPen as editIcon,
|
||||||
|
faTags as tagsIcon,
|
||||||
} from '@fortawesome/free-solid-svg-icons';
|
} from '@fortawesome/free-solid-svg-icons';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import { FC } from 'react';
|
|
||||||
import { NavLink, NavLinkProps, useLocation } from 'react-router-dom';
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { DeleteServerButtonProps } from '../servers/DeleteServerButton';
|
import type { FC } from 'react';
|
||||||
import { isServerWithId, SelectedServer } from '../servers/data';
|
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';
|
import './AsideMenu.scss';
|
||||||
|
|
||||||
export interface AsideMenuProps {
|
export interface AsideMenuProps {
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { Component, ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
|
import { Component } from 'react';
|
||||||
import { Button } from 'reactstrap';
|
import { Button } from 'reactstrap';
|
||||||
import { SimpleCard } from '../utils/SimpleCard';
|
import { SimpleCard } from '../utils/SimpleCard';
|
||||||
|
|
||||||
|
|||||||
@ -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 { isEmpty, values } from 'ramda';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
import { ExternalLink } from 'react-external-link';
|
||||||
import { Link, useNavigate } from 'react-router-dom';
|
import { Link, useNavigate } from 'react-router-dom';
|
||||||
import { Card, Row } from 'reactstrap';
|
import { Card, Row } from 'reactstrap';
|
||||||
import { ExternalLink } from 'react-external-link';
|
import type { ServersMap } from '../servers/data';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
|
||||||
import { faExternalLinkAlt, faPlus } from '@fortawesome/free-solid-svg-icons';
|
|
||||||
import { ServersListGroup } from '../servers/ServersListGroup';
|
import { ServersListGroup } from '../servers/ServersListGroup';
|
||||||
import { ServersMap } from '../servers/data';
|
|
||||||
import { ShlinkLogo } from './img/ShlinkLogo';
|
import { ShlinkLogo } from './img/ShlinkLogo';
|
||||||
import './Home.scss';
|
import './Home.scss';
|
||||||
|
|
||||||
|
|||||||
@ -1,9 +1,10 @@
|
|||||||
import { faChevronDown as arrowIcon, faCogs as cogsIcon } from '@fortawesome/free-solid-svg-icons';
|
import { faChevronDown as arrowIcon, faCogs as cogsIcon } from '@fortawesome/free-solid-svg-icons';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
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 { Link, useLocation } from 'react-router-dom';
|
||||||
import { Collapse, Nav, Navbar, NavbarBrand, NavbarToggler, NavItem, NavLink } from 'reactstrap';
|
import { Collapse, Nav, Navbar, NavbarBrand, NavbarToggler, NavItem, NavLink } from 'reactstrap';
|
||||||
import classNames from 'classnames';
|
|
||||||
import { useToggle } from '../utils/helpers/hooks';
|
import { useToggle } from '../utils/helpers/hooks';
|
||||||
import { ShlinkLogo } from './img/ShlinkLogo';
|
import { ShlinkLogo } from './img/ShlinkLogo';
|
||||||
import './MainHeader.scss';
|
import './MainHeader.scss';
|
||||||
|
|||||||
@ -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 { faBars as burgerIcon } from '@fortawesome/free-solid-svg-icons';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { withSelectedServer } from '../servers/helpers/withSelectedServer';
|
import type { FC } from 'react';
|
||||||
import { useSwipeable, useToggle } from '../utils/helpers/hooks';
|
import { useEffect } from 'react';
|
||||||
import { supportsDomainVisits, supportsNonOrphanVisits } from '../utils/helpers/features';
|
import { Navigate, Route, Routes, useLocation } from 'react-router-dom';
|
||||||
import { isReachableServer } from '../servers/data';
|
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 { NotFound } from './NotFound';
|
||||||
import { AsideMenuProps } from './AsideMenu';
|
|
||||||
import './MenuLayout.scss';
|
import './MenuLayout.scss';
|
||||||
|
|
||||||
interface MenuLayoutProps {
|
interface MenuLayoutProps {
|
||||||
@ -45,8 +46,8 @@ export const MenuLayout = (
|
|||||||
return <ServerError />;
|
return <ServerError />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const addNonOrphanVisitsRoute = supportsNonOrphanVisits(selectedServer);
|
const addNonOrphanVisitsRoute = useFeature('nonOrphanVisits', selectedServer);
|
||||||
const addDomainVisitsRoute = supportsDomainVisits(selectedServer);
|
const addDomainVisitsRoute = useFeature('domainVisits', selectedServer);
|
||||||
const burgerClasses = classNames('menu-layout__burger-icon', { 'menu-layout__burger-icon--active': sidebarVisible });
|
const burgerClasses = classNames('menu-layout__burger-icon', { 'menu-layout__burger-icon--active': sidebarVisible });
|
||||||
const swipeableProps = useSwipeable(showSidebar, hideSidebar);
|
const swipeableProps = useSwipeable(showSidebar, hideSidebar);
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { FC, PropsWithChildren } from 'react';
|
import type { FC, PropsWithChildren } from 'react';
|
||||||
import './NoMenuLayout.scss';
|
import './NoMenuLayout.scss';
|
||||||
|
|
||||||
export const NoMenuLayout: FC<PropsWithChildren<unknown>> = ({ children }) => (
|
export const NoMenuLayout: FC<PropsWithChildren<unknown>> = ({ children }) => (
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { FC, PropsWithChildren } from 'react';
|
import type { FC, PropsWithChildren } from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { SimpleCard } from '../utils/SimpleCard';
|
import { SimpleCard } from '../utils/SimpleCard';
|
||||||
|
|
||||||
|
|||||||
@ -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';
|
import { useLocation } from 'react-router-dom';
|
||||||
|
|
||||||
export const ScrollToTop: FC<PropsWithChildren<unknown>> = ({ children }) => {
|
export const ScrollToTop: FC<PropsWithChildren<unknown>> = ({ children }) => {
|
||||||
|
|||||||
@ -1,7 +1,8 @@
|
|||||||
import { pipe } from 'ramda';
|
import { pipe } from 'ramda';
|
||||||
import { ExternalLink } from 'react-external-link';
|
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 { versionToPrintable, versionToSemVer } from '../utils/helpers/version';
|
||||||
import { isReachableServer, SelectedServer } from '../servers/data';
|
|
||||||
|
|
||||||
const SHLINK_WEB_CLIENT_VERSION = '%_VERSION_%';
|
const SHLINK_WEB_CLIENT_VERSION = '%_VERSION_%';
|
||||||
const normalizeVersion = pipe(versionToSemVer(), versionToPrintable);
|
const normalizeVersion = pipe(versionToSemVer(), versionToPrintable);
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import classNames from 'classnames';
|
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 { ShlinkVersions } from './ShlinkVersions';
|
||||||
import { Sidebar } from './reducers/sidebar';
|
|
||||||
import './ShlinkVersionsContainer.scss';
|
import './ShlinkVersionsContainer.scss';
|
||||||
|
|
||||||
export interface ShlinkVersionsContainerProps {
|
export interface ShlinkVersionsContainerProps {
|
||||||
|
|||||||
@ -1,12 +1,13 @@
|
|||||||
import { FC } from 'react';
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
import type { FC } from 'react';
|
||||||
import { Pagination, PaginationItem, PaginationLink } from 'reactstrap';
|
import { Pagination, PaginationItem, PaginationLink } from 'reactstrap';
|
||||||
|
import type {
|
||||||
|
NumberOrEllipsis } from '../utils/helpers/pagination';
|
||||||
import {
|
import {
|
||||||
pageIsEllipsis,
|
|
||||||
keyForPage,
|
keyForPage,
|
||||||
NumberOrEllipsis,
|
pageIsEllipsis,
|
||||||
progressivePagination,
|
|
||||||
prettifyPageNumber,
|
prettifyPageNumber,
|
||||||
|
progressivePagination,
|
||||||
} from '../utils/helpers/pagination';
|
} from '../utils/helpers/pagination';
|
||||||
import './SimplePaginator.scss';
|
import './SimplePaginator.scss';
|
||||||
|
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
.react-tags {
|
.react-tags {
|
||||||
position: relative;
|
position: relative;
|
||||||
padding: 5px 0 0 6px;
|
padding: 5px 0 0 6px;
|
||||||
border-radius: .3rem;
|
border-radius: .5rem;
|
||||||
background-color: var(--primary-color);
|
background-color: var(--primary-color);
|
||||||
border: 1px solid var(--input-border-color);
|
border: 1px solid var(--input-border-color);
|
||||||
transition: border-color .15s ease-in-out, box-shadow .15s ease-in-out;
|
transition: border-color .15s ease-in-out, box-shadow .15s ease-in-out;
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { Fetch } from '../../utils/types';
|
import type { Fetch } from '../../utils/types';
|
||||||
|
|
||||||
const applicationJsonHeader = { 'Content-Type': 'application/json' };
|
const applicationJsonHeader = { 'Content-Type': 'application/json' };
|
||||||
const withJsonContentType = (options?: RequestInit): RequestInit | undefined => {
|
const withJsonContentType = (options?: RequestInit): RequestInit | undefined => {
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { saveUrl } from '../../utils/helpers/files';
|
import { saveUrl } from '../../utils/helpers/files';
|
||||||
import { HttpClient } from './HttpClient';
|
import type { HttpClient } from './HttpClient';
|
||||||
|
|
||||||
export class ImageDownloader {
|
export class ImageDownloader {
|
||||||
public constructor(private readonly httpClient: HttpClient, private readonly window: Window) {}
|
public constructor(private readonly httpClient: HttpClient, private readonly window: Window) {}
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { NormalizedVisit } from '../../visits/types';
|
import type { ExportableShortUrl } from '../../short-urls/data';
|
||||||
import { ExportableShortUrl } from '../../short-urls/data';
|
import type { JsonToCsv } from '../../utils/helpers/csvjson';
|
||||||
import { saveCsv } from '../../utils/helpers/files';
|
import { saveCsv } from '../../utils/helpers/files';
|
||||||
import { JsonToCsv } from '../../utils/helpers/csvjson';
|
import type { NormalizedVisit } from '../../visits/types';
|
||||||
|
|
||||||
export class ReportExporter {
|
export class ReportExporter {
|
||||||
public constructor(private readonly window: Window, private readonly jsonToCsv: JsonToCsv) {}
|
public constructor(private readonly window: Window, private readonly jsonToCsv: JsonToCsv) {}
|
||||||
|
|||||||
@ -1,19 +1,19 @@
|
|||||||
import Bottle from 'bottlejs';
|
import type Bottle from 'bottlejs';
|
||||||
import { ScrollToTop } from '../ScrollToTop';
|
import type { ConnectDecorator } from '../../container/types';
|
||||||
import { MainHeader } from '../MainHeader';
|
import { withoutSelectedServer } from '../../servers/helpers/withoutSelectedServer';
|
||||||
import { Home } from '../Home';
|
|
||||||
import { MenuLayout } from '../MenuLayout';
|
|
||||||
import { AsideMenu } from '../AsideMenu';
|
import { AsideMenu } from '../AsideMenu';
|
||||||
import { ErrorHandler } from '../ErrorHandler';
|
import { ErrorHandler } from '../ErrorHandler';
|
||||||
import { ShlinkVersionsContainer } from '../ShlinkVersionsContainer';
|
import { Home } from '../Home';
|
||||||
import { ConnectDecorator } from '../../container/types';
|
import { MainHeader } from '../MainHeader';
|
||||||
import { withoutSelectedServer } from '../../servers/helpers/withoutSelectedServer';
|
import { MenuLayout } from '../MenuLayout';
|
||||||
import { sidebarNotPresent, sidebarPresent } from '../reducers/sidebar';
|
import { sidebarNotPresent, sidebarPresent } from '../reducers/sidebar';
|
||||||
|
import { ScrollToTop } from '../ScrollToTop';
|
||||||
|
import { ShlinkVersionsContainer } from '../ShlinkVersionsContainer';
|
||||||
|
import { HttpClient } from './HttpClient';
|
||||||
import { ImageDownloader } from './ImageDownloader';
|
import { ImageDownloader } from './ImageDownloader';
|
||||||
import { ReportExporter } from './ReportExporter';
|
import { ReportExporter } from './ReportExporter';
|
||||||
import { HttpClient } from './HttpClient';
|
|
||||||
|
|
||||||
const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
|
export const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
|
||||||
// Services
|
// Services
|
||||||
bottle.constant('window', window);
|
bottle.constant('window', window);
|
||||||
bottle.constant('console', console);
|
bottle.constant('console', console);
|
||||||
@ -62,5 +62,3 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
|
|||||||
bottle.serviceFactory('sidebarPresent', () => sidebarPresent);
|
bottle.serviceFactory('sidebarPresent', () => sidebarPresent);
|
||||||
bottle.serviceFactory('sidebarNotPresent', () => sidebarNotPresent);
|
bottle.serviceFactory('sidebarNotPresent', () => sidebarNotPresent);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default provideServices;
|
|
||||||
|
|||||||
@ -1,18 +1,19 @@
|
|||||||
import Bottle, { IContainer } from 'bottlejs';
|
import type { IContainer } from 'bottlejs';
|
||||||
import { connect as reduxConnect } from 'react-redux';
|
import Bottle from 'bottlejs';
|
||||||
import { pick } from 'ramda';
|
import { pick } from 'ramda';
|
||||||
import provideApiServices from '../api/services/provideServices';
|
import { connect as reduxConnect } from 'react-redux';
|
||||||
import provideCommonServices from '../common/services/provideServices';
|
import { provideServices as provideApiServices } from '../api/services/provideServices';
|
||||||
import provideShortUrlsServices from '../short-urls/services/provideServices';
|
import { provideServices as provideAppServices } from '../app/services/provideServices';
|
||||||
import provideServersServices from '../servers/services/provideServices';
|
import { provideServices as provideCommonServices } from '../common/services/provideServices';
|
||||||
import provideVisitsServices from '../visits/services/provideServices';
|
import { provideServices as provideDomainsServices } from '../domains/services/provideServices';
|
||||||
import provideTagsServices from '../tags/services/provideServices';
|
import { provideServices as provideMercureServices } from '../mercure/services/provideServices';
|
||||||
import provideUtilsServices from '../utils/services/provideServices';
|
import { provideServices as provideServersServices } from '../servers/services/provideServices';
|
||||||
import provideMercureServices from '../mercure/services/provideServices';
|
import { provideServices as provideSettingsServices } from '../settings/services/provideServices';
|
||||||
import provideSettingsServices from '../settings/services/provideServices';
|
import { provideServices as provideShortUrlsServices } from '../short-urls/services/provideServices';
|
||||||
import provideDomainsServices from '../domains/services/provideServices';
|
import { provideServices as provideTagsServices } from '../tags/services/provideServices';
|
||||||
import provideAppServices from '../app/services/provideServices';
|
import { provideServices as provideUtilsServices } from '../utils/services/provideServices';
|
||||||
import { ConnectDecorator } from './types';
|
import { provideServices as provideVisitsServices } from '../visits/services/provideServices';
|
||||||
|
import type { ConnectDecorator } from './types';
|
||||||
|
|
||||||
type LazyActionMap = Record<string, Function>;
|
type LazyActionMap = Record<string, Function>;
|
||||||
|
|
||||||
|
|||||||
@ -1,9 +1,10 @@
|
|||||||
import { IContainer } from 'bottlejs';
|
|
||||||
import { save, load, RLSOptions } from 'redux-localstorage-simple';
|
|
||||||
import { configureStore } from '@reduxjs/toolkit';
|
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 { migrateDeprecatedSettings } from '../settings/helpers';
|
||||||
import { ShlinkState } from './types';
|
import type { ShlinkState } from './types';
|
||||||
|
|
||||||
const isProduction = process.env.NODE_ENV === 'production';
|
const isProduction = process.env.NODE_ENV === 'production';
|
||||||
const localStorageConfig: RLSOptions = {
|
const localStorageConfig: RLSOptions = {
|
||||||
@ -16,7 +17,7 @@ const preloadedState = migrateDeprecatedSettings(load(localStorageConfig) as Shl
|
|||||||
|
|
||||||
export const setUpStore = (container: IContainer) => configureStore({
|
export const setUpStore = (container: IContainer) => configureStore({
|
||||||
devTools: !isProduction,
|
devTools: !isProduction,
|
||||||
reducer: reducer(container),
|
reducer: initReducers(container),
|
||||||
preloadedState,
|
preloadedState,
|
||||||
middleware: (defaultMiddlewaresIncludingReduxThunk) =>
|
middleware: (defaultMiddlewaresIncludingReduxThunk) =>
|
||||||
defaultMiddlewaresIncludingReduxThunk({ immutableCheck: false, serializableCheck: false }) // State is too big for these
|
defaultMiddlewaresIncludingReduxThunk({ immutableCheck: false, serializableCheck: false }) // State is too big for these
|
||||||
|
|||||||
@ -1,21 +1,21 @@
|
|||||||
import { MercureInfo } from '../mercure/reducers/mercureInfo';
|
import type { Sidebar } from '../common/reducers/sidebar';
|
||||||
import { SelectedServer, ServersMap } from '../servers/data';
|
import type { DomainsList } from '../domains/reducers/domainsList';
|
||||||
import { Settings } from '../settings/reducers/settings';
|
import type { MercureInfo } from '../mercure/reducers/mercureInfo';
|
||||||
import { ShortUrlCreation } from '../short-urls/reducers/shortUrlCreation';
|
import type { SelectedServer, ServersMap } from '../servers/data';
|
||||||
import { ShortUrlDeletion } from '../short-urls/reducers/shortUrlDeletion';
|
import type { Settings } from '../settings/reducers/settings';
|
||||||
import { ShortUrlEdition } from '../short-urls/reducers/shortUrlEdition';
|
import type { ShortUrlCreation } from '../short-urls/reducers/shortUrlCreation';
|
||||||
import { ShortUrlsList } from '../short-urls/reducers/shortUrlsList';
|
import type { ShortUrlDeletion } from '../short-urls/reducers/shortUrlDeletion';
|
||||||
import { TagDeletion } from '../tags/reducers/tagDelete';
|
import type { ShortUrlDetail } from '../short-urls/reducers/shortUrlDetail';
|
||||||
import { TagEdition } from '../tags/reducers/tagEdit';
|
import type { ShortUrlEdition } from '../short-urls/reducers/shortUrlEdition';
|
||||||
import { TagsList } from '../tags/reducers/tagsList';
|
import type { ShortUrlsList } from '../short-urls/reducers/shortUrlsList';
|
||||||
import { ShortUrlDetail } from '../short-urls/reducers/shortUrlDetail';
|
import type { TagDeletion } from '../tags/reducers/tagDelete';
|
||||||
import { ShortUrlVisits } from '../visits/reducers/shortUrlVisits';
|
import type { TagEdition } from '../tags/reducers/tagEdit';
|
||||||
import { TagVisits } from '../visits/reducers/tagVisits';
|
import type { TagsList } from '../tags/reducers/tagsList';
|
||||||
import { DomainsList } from '../domains/reducers/domainsList';
|
import type { DomainVisits } from '../visits/reducers/domainVisits';
|
||||||
import { VisitsOverview } from '../visits/reducers/visitsOverview';
|
import type { ShortUrlVisits } from '../visits/reducers/shortUrlVisits';
|
||||||
import { Sidebar } from '../common/reducers/sidebar';
|
import type { TagVisits } from '../visits/reducers/tagVisits';
|
||||||
import { DomainVisits } from '../visits/reducers/domainVisits';
|
import type { VisitsInfo } from '../visits/reducers/types';
|
||||||
import { VisitsInfo } from '../visits/reducers/types';
|
import type { VisitsOverview } from '../visits/reducers/visitsOverview';
|
||||||
|
|
||||||
export interface ShlinkState {
|
export interface ShlinkState {
|
||||||
servers: ServersMap;
|
servers: ServersMap;
|
||||||
|
|||||||
@ -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 { faDotCircle as defaultDomainIcon } from '@fortawesome/free-solid-svg-icons';
|
||||||
import { ShlinkDomainRedirects } from '../api/types';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import { OptionalString } from '../utils/utils';
|
import type { FC } from 'react';
|
||||||
import { SelectedServer } from '../servers/data';
|
import { useEffect } from 'react';
|
||||||
import { Domain } from './data';
|
import { UncontrolledTooltip } from 'reactstrap';
|
||||||
import { DomainStatusIcon } from './helpers/DomainStatusIcon';
|
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 { DomainDropdown } from './helpers/DomainDropdown';
|
||||||
import { EditDomainRedirects } from './reducers/domainRedirects';
|
import { DomainStatusIcon } from './helpers/DomainStatusIcon';
|
||||||
|
import type { EditDomainRedirects } from './reducers/domainRedirects';
|
||||||
|
|
||||||
interface DomainRowProps {
|
interface DomainRowProps {
|
||||||
domain: Domain;
|
domain: Domain;
|
||||||
|
|||||||
@ -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 { faUndo } from '@fortawesome/free-solid-svg-icons';
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import { isEmpty, pipe } from 'ramda';
|
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 { DropdownBtn } from '../utils/DropdownBtn';
|
||||||
import { useToggle } from '../utils/helpers/hooks';
|
import { useToggle } from '../utils/helpers/hooks';
|
||||||
import { DomainsList } from './reducers/domainsList';
|
import type { DomainsList } from './reducers/domainsList';
|
||||||
import './DomainSelector.scss';
|
import './DomainSelector.scss';
|
||||||
|
|
||||||
export interface DomainSelectorProps extends Omit<InputProps, 'onChange'> {
|
export interface DomainSelectorProps extends Omit<InputProps, 'onChange'> {
|
||||||
|
|||||||
@ -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 { Message } from '../utils/Message';
|
||||||
import { Result } from '../utils/Result';
|
import { Result } from '../utils/Result';
|
||||||
import { ShlinkApiError } from '../api/ShlinkApiError';
|
|
||||||
import { SimpleCard } from '../utils/SimpleCard';
|
|
||||||
import { SearchField } from '../utils/SearchField';
|
import { SearchField } from '../utils/SearchField';
|
||||||
import { EditDomainRedirects } from './reducers/domainRedirects';
|
import { SimpleCard } from '../utils/SimpleCard';
|
||||||
import { SelectedServer } from '../servers/data';
|
|
||||||
import { DomainsList } from './reducers/domainsList';
|
|
||||||
import { DomainRow } from './DomainRow';
|
import { DomainRow } from './DomainRow';
|
||||||
|
import type { EditDomainRedirects } from './reducers/domainRedirects';
|
||||||
|
import type { DomainsList } from './reducers/domainsList';
|
||||||
|
|
||||||
interface ManageDomainsProps {
|
interface ManageDomainsProps {
|
||||||
listDomains: Function;
|
listDomains: Function;
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { ShlinkDomain } from '../../api/types';
|
import type { ShlinkDomain } from '../../api/types';
|
||||||
|
|
||||||
export type DomainStatus = 'validating' | 'valid' | 'invalid';
|
export type DomainStatus = 'validating' | 'valid' | 'invalid';
|
||||||
|
|
||||||
|
|||||||
@ -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 { faChartPie as pieChartIcon, faEdit as editIcon } from '@fortawesome/free-solid-svg-icons';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import { useToggle } from '../../utils/helpers/hooks';
|
import 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 { DropdownBtnMenu } from '../../utils/DropdownBtnMenu';
|
||||||
import { EditDomainRedirectsModal } from './EditDomainRedirectsModal';
|
import { useFeature } from '../../utils/helpers/features';
|
||||||
import { Domain } from '../data';
|
import { useToggle } from '../../utils/helpers/hooks';
|
||||||
import { EditDomainRedirects } from '../reducers/domainRedirects';
|
|
||||||
import { supportsDefaultDomainRedirectsEdition, supportsDomainVisits } from '../../utils/helpers/features';
|
|
||||||
import { getServerId, SelectedServer } from '../../servers/data';
|
|
||||||
import { DEFAULT_DOMAIN } from '../../visits/reducers/domainVisits';
|
import { DEFAULT_DOMAIN } from '../../visits/reducers/domainVisits';
|
||||||
|
import type { Domain } from '../data';
|
||||||
|
import type { EditDomainRedirects } from '../reducers/domainRedirects';
|
||||||
|
import { EditDomainRedirectsModal } from './EditDomainRedirectsModal';
|
||||||
|
|
||||||
interface DomainDropdownProps {
|
interface DomainDropdownProps {
|
||||||
domain: Domain;
|
domain: Domain;
|
||||||
@ -22,8 +23,8 @@ export const DomainDropdown: FC<DomainDropdownProps> = ({ domain, editDomainRedi
|
|||||||
const [isOpen, toggle] = useToggle();
|
const [isOpen, toggle] = useToggle();
|
||||||
const [isModalOpen, toggleModal] = useToggle();
|
const [isModalOpen, toggleModal] = useToggle();
|
||||||
const { isDefault } = domain;
|
const { isDefault } = domain;
|
||||||
const canBeEdited = !isDefault || supportsDefaultDomainRedirectsEdition(selectedServer);
|
const canBeEdited = !isDefault || useFeature('defaultDomainRedirectsEdition', selectedServer);
|
||||||
const withVisits = supportsDomainVisits(selectedServer);
|
const withVisits = useFeature('domainVisits', selectedServer);
|
||||||
const serverId = getServerId(selectedServer);
|
const serverId = getServerId(selectedServer);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -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 {
|
import {
|
||||||
faTimes as invalidIcon,
|
|
||||||
faCheck as checkIcon,
|
faCheck as checkIcon,
|
||||||
faCircleNotch as loadingStatusIcon,
|
faCircleNotch as loadingStatusIcon,
|
||||||
|
faTimes as invalidIcon,
|
||||||
} from '@fortawesome/free-solid-svg-icons';
|
} from '@fortawesome/free-solid-svg-icons';
|
||||||
import { MediaMatcher } from '../../utils/types';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import { mutableRefToElementRef } from '../../utils/helpers/components';
|
import type { FC } from 'react';
|
||||||
import { DomainStatus } from '../data';
|
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 {
|
interface DomainStatusIconProps {
|
||||||
status: DomainStatus;
|
status: DomainStatus;
|
||||||
@ -17,7 +18,7 @@ interface DomainStatusIconProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const DomainStatusIcon: FC<DomainStatusIconProps> = ({ status, matchMedia = window.matchMedia }) => {
|
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 matchesMobile = () => matchMedia('(max-width: 991px)').matches;
|
||||||
const [isMobile, setIsMobile] = useState<boolean>(matchesMobile());
|
const [isMobile, setIsMobile] = useState<boolean>(matchesMobile());
|
||||||
|
|
||||||
@ -35,13 +36,13 @@ export const DomainStatusIcon: FC<DomainStatusIconProps> = ({ status, matchMedia
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<span ref={mutableRefToElementRef(ref)}>
|
<span ref={ref}>
|
||||||
{status === 'valid'
|
{status === 'valid'
|
||||||
? <FontAwesomeIcon fixedWidth icon={checkIcon} className="text-muted" />
|
? <FontAwesomeIcon fixedWidth icon={checkIcon} className="text-muted" />
|
||||||
: <FontAwesomeIcon fixedWidth icon={invalidIcon} className="text-danger" />}
|
: <FontAwesomeIcon fixedWidth icon={invalidIcon} className="text-danger" />}
|
||||||
</span>
|
</span>
|
||||||
<UncontrolledTooltip
|
<UncontrolledTooltip
|
||||||
target={(() => ref.current) as any}
|
target={ref}
|
||||||
placement={isMobile ? 'top-start' : 'left'}
|
placement={isMobile ? 'top-start' : 'left'}
|
||||||
autohide={status === 'valid'}
|
autohide={status === 'valid'}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -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 { Button, Modal, ModalBody, ModalFooter, ModalHeader } from 'reactstrap';
|
||||||
import { ShlinkDomain } from '../../api/types';
|
import type { ShlinkDomain } from '../../api/types';
|
||||||
import { InputFormGroup, InputFormGroupProps } from '../../utils/forms/InputFormGroup';
|
import type { InputFormGroupProps } from '../../utils/forms/InputFormGroup';
|
||||||
import { handleEventPreventingDefault, nonEmptyValueOrNull } from '../../utils/utils';
|
import { InputFormGroup } from '../../utils/forms/InputFormGroup';
|
||||||
import { InfoTooltip } from '../../utils/InfoTooltip';
|
import { InfoTooltip } from '../../utils/InfoTooltip';
|
||||||
import { EditDomainRedirects } from '../reducers/domainRedirects';
|
import { handleEventPreventingDefault, nonEmptyValueOrNull } from '../../utils/utils';
|
||||||
|
import type { EditDomainRedirects } from '../reducers/domainRedirects';
|
||||||
|
|
||||||
interface EditDomainRedirectsModalProps {
|
interface EditDomainRedirectsModalProps {
|
||||||
domain: ShlinkDomain;
|
domain: ShlinkDomain;
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
|
import type { ShlinkApiClientBuilder } from '../../api/services/ShlinkApiClientBuilder';
|
||||||
|
import type { ShlinkDomainRedirects } from '../../api/types';
|
||||||
import { createAsyncThunk } from '../../utils/helpers/redux';
|
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';
|
const EDIT_DOMAIN_REDIRECTS = 'shlink/domainRedirects/EDIT_DOMAIN_REDIRECTS';
|
||||||
|
|
||||||
|
|||||||
@ -1,13 +1,14 @@
|
|||||||
import { createSlice, createAction, SliceCaseReducers, AsyncThunk } from '@reduxjs/toolkit';
|
import type { AsyncThunk, SliceCaseReducers } from '@reduxjs/toolkit';
|
||||||
import { createAsyncThunk } from '../../utils/helpers/redux';
|
import { createAction, createSlice } from '@reduxjs/toolkit';
|
||||||
import { ShlinkDomainRedirects } from '../../api/types';
|
import type { ShlinkApiClientBuilder } from '../../api/services/ShlinkApiClientBuilder';
|
||||||
import { ShlinkApiClientBuilder } from '../../api/services/ShlinkApiClientBuilder';
|
import type { ShlinkDomainRedirects } from '../../api/types';
|
||||||
import { Domain, DomainStatus } from '../data';
|
import type { ProblemDetailsError } from '../../api/types/errors';
|
||||||
import { hasServerData } from '../../servers/data';
|
|
||||||
import { replaceAuthorityFromUri } from '../../utils/helpers/uri';
|
|
||||||
import { ProblemDetailsError } from '../../api/types/errors';
|
|
||||||
import { parseApiError } from '../../api/utils';
|
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';
|
const REDUCER_PREFIX = 'shlink/domainsList';
|
||||||
|
|
||||||
|
|||||||
@ -1,12 +1,12 @@
|
|||||||
|
import type Bottle from 'bottlejs';
|
||||||
import { prop } from 'ramda';
|
import { prop } from 'ramda';
|
||||||
import Bottle from 'bottlejs';
|
import type { ConnectDecorator } from '../../container/types';
|
||||||
import { ConnectDecorator } from '../../container/types';
|
|
||||||
import { domainsListReducerCreator } from '../reducers/domainsList';
|
|
||||||
import { DomainSelector } from '../DomainSelector';
|
import { DomainSelector } from '../DomainSelector';
|
||||||
import { ManageDomains } from '../ManageDomains';
|
import { ManageDomains } from '../ManageDomains';
|
||||||
import { editDomainRedirects } from '../reducers/domainRedirects';
|
import { editDomainRedirects } from '../reducers/domainRedirects';
|
||||||
|
import { domainsListReducerCreator } from '../reducers/domainsList';
|
||||||
|
|
||||||
const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
|
export const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
|
||||||
// Components
|
// Components
|
||||||
bottle.serviceFactory('DomainSelector', () => DomainSelector);
|
bottle.serviceFactory('DomainSelector', () => DomainSelector);
|
||||||
bottle.decorator('DomainSelector', connect(['domainsList'], ['listDomains']));
|
bottle.decorator('DomainSelector', connect(['domainsList'], ['listDomains']));
|
||||||
@ -32,5 +32,3 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
|
|||||||
bottle.serviceFactory('editDomainRedirects', editDomainRedirects, 'buildShlinkApiClient');
|
bottle.serviceFactory('editDomainRedirects', editDomainRedirects, 'buildShlinkApiClient');
|
||||||
bottle.serviceFactory('checkDomainHealth', prop('checkDomainHealth'), 'domainsListReducerCreator');
|
bottle.serviceFactory('checkDomainHealth', prop('checkDomainHealth'), 'domainsListReducerCreator');
|
||||||
};
|
};
|
||||||
|
|
||||||
export default provideServices;
|
|
||||||
|
|||||||
@ -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 { createRoot } from 'react-dom/client';
|
||||||
import { Provider } from 'react-redux';
|
import { Provider } from 'react-redux';
|
||||||
import { BrowserRouter } from 'react-router-dom';
|
import { BrowserRouter } from 'react-router-dom';
|
||||||
import pack from '../package.json';
|
import pack from '../package.json';
|
||||||
import { container } from './container';
|
import { container } from './container';
|
||||||
import { setUpStore } from './container/store';
|
import { setUpStore } from './container/store';
|
||||||
import { fixLeafletIcons } from './utils/helpers/leaflet';
|
|
||||||
import { register as registerServiceWorker } from './serviceWorkerRegistration';
|
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 { fixLeafletIcons } from './utils/helpers/leaflet';
|
||||||
import 'react-datepicker/dist/react-datepicker.css';
|
|
||||||
import 'leaflet/dist/leaflet.css';
|
|
||||||
import './index.scss';
|
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
|
// This overwrites icons used for leaflet maps, fixing some issues caused by webpack while processing the CSS
|
||||||
fixLeafletIcons();
|
fixLeafletIcons();
|
||||||
|
|||||||
@ -1,8 +1,9 @@
|
|||||||
import { FC, useEffect } from 'react';
|
|
||||||
import { pipe } from 'ramda';
|
import { pipe } from 'ramda';
|
||||||
|
import type { FC } from 'react';
|
||||||
|
import { useEffect } from 'react';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
import { CreateVisit } from '../../visits/types';
|
import type { CreateVisit } from '../../visits/types';
|
||||||
import { MercureInfo } from '../reducers/mercureInfo';
|
import type { MercureInfo } from '../reducers/mercureInfo';
|
||||||
import { bindToMercureTopic } from './index';
|
import { bindToMercureTopic } from './index';
|
||||||
|
|
||||||
export interface MercureBoundProps {
|
export interface MercureBoundProps {
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { EventSourcePolyfill as EventSource } from 'event-source-polyfill';
|
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
|
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;
|
const { mercureHubUrl, token, loading, error } = mercureInfo;
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { createSlice } from '@reduxjs/toolkit';
|
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 { createAsyncThunk } from '../../utils/helpers/redux';
|
||||||
import { ShlinkMercureInfo } from '../../api/types';
|
|
||||||
import { ShlinkApiClientBuilder } from '../../api/services/ShlinkApiClientBuilder';
|
|
||||||
|
|
||||||
const REDUCER_PREFIX = 'shlink/mercure';
|
const REDUCER_PREFIX = 'shlink/mercure';
|
||||||
|
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
|
import type Bottle from 'bottlejs';
|
||||||
import { prop } from 'ramda';
|
import { prop } from 'ramda';
|
||||||
import Bottle from 'bottlejs';
|
|
||||||
import { mercureInfoReducerCreator } from '../reducers/mercureInfo';
|
import { mercureInfoReducerCreator } from '../reducers/mercureInfo';
|
||||||
|
|
||||||
const provideServices = (bottle: Bottle) => {
|
export const provideServices = (bottle: Bottle) => {
|
||||||
// Reducer
|
// Reducer
|
||||||
bottle.serviceFactory('mercureInfoReducerCreator', mercureInfoReducerCreator, 'buildShlinkApiClient');
|
bottle.serviceFactory('mercureInfoReducerCreator', mercureInfoReducerCreator, 'buildShlinkApiClient');
|
||||||
bottle.serviceFactory('mercureInfoReducer', prop('reducer'), 'mercureInfoReducerCreator');
|
bottle.serviceFactory('mercureInfoReducer', prop('reducer'), 'mercureInfoReducerCreator');
|
||||||
@ -10,5 +10,3 @@ const provideServices = (bottle: Bottle) => {
|
|||||||
// Actions
|
// Actions
|
||||||
bottle.serviceFactory('loadMercureInfo', prop('loadMercureInfo'), 'mercureInfoReducerCreator');
|
bottle.serviceFactory('loadMercureInfo', prop('loadMercureInfo'), 'mercureInfoReducerCreator');
|
||||||
};
|
};
|
||||||
|
|
||||||
export default provideServices;
|
|
||||||
|
|||||||
@ -1,12 +1,12 @@
|
|||||||
import { IContainer } from 'bottlejs';
|
|
||||||
import { combineReducers } from '@reduxjs/toolkit';
|
import { combineReducers } from '@reduxjs/toolkit';
|
||||||
import { serversReducer } from '../servers/reducers/servers';
|
import type { IContainer } from 'bottlejs';
|
||||||
import { settingsReducer } from '../settings/reducers/settings';
|
|
||||||
import { appUpdatesReducer } from '../app/reducers/appUpdates';
|
import { appUpdatesReducer } from '../app/reducers/appUpdates';
|
||||||
import { sidebarReducer } from '../common/reducers/sidebar';
|
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,
|
servers: serversReducer,
|
||||||
selectedServer: container.selectedServerReducer,
|
selectedServer: container.selectedServerReducer,
|
||||||
shortUrlsList: container.shortUrlsListReducer,
|
shortUrlsList: container.shortUrlsListReducer,
|
||||||
|
|||||||
@ -1,14 +1,16 @@
|
|||||||
import { FC, useEffect, useState } from 'react';
|
import type { FC } from 'react';
|
||||||
import { v4 as uuid } from 'uuid';
|
import { useEffect, useState } from 'react';
|
||||||
import { Button } from 'reactstrap';
|
|
||||||
import { useNavigate } from 'react-router-dom';
|
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 { NoMenuLayout } from '../common/NoMenuLayout';
|
||||||
import { TimeoutToggle, useGoBack, useToggle } from '../utils/helpers/hooks';
|
import type { TimeoutToggle } from '../utils/helpers/hooks';
|
||||||
import { ServerForm } from './helpers/ServerForm';
|
import { useGoBack, useToggle } from '../utils/helpers/hooks';
|
||||||
import { ImportServersBtnProps } from './helpers/ImportServersBtn';
|
import { Result } from '../utils/Result';
|
||||||
import { ServerData, ServersMap, ServerWithId } from './data';
|
import type { ServerData, ServersMap, ServerWithId } from './data';
|
||||||
import { DuplicatedServersModal } from './helpers/DuplicatedServersModal';
|
import { DuplicatedServersModal } from './helpers/DuplicatedServersModal';
|
||||||
|
import type { ImportServersBtnProps } from './helpers/ImportServersBtn';
|
||||||
|
import { ServerForm } from './helpers/ServerForm';
|
||||||
|
|
||||||
const SHOW_IMPORT_MSG_TIME = 4000;
|
const SHOW_IMPORT_MSG_TIME = 4000;
|
||||||
|
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
import { FC, PropsWithChildren } from 'react';
|
|
||||||
import { faMinusCircle as deleteIcon } from '@fortawesome/free-solid-svg-icons';
|
import { faMinusCircle as deleteIcon } from '@fortawesome/free-solid-svg-icons';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
|
import type { FC, PropsWithChildren } from 'react';
|
||||||
import { useToggle } from '../utils/helpers/hooks';
|
import { useToggle } from '../utils/helpers/hooks';
|
||||||
import { DeleteServerModalProps } from './DeleteServerModal';
|
import type { ServerWithId } from './data';
|
||||||
import { ServerWithId } from './data';
|
import type { DeleteServerModalProps } from './DeleteServerModal';
|
||||||
|
|
||||||
export type DeleteServerButtonProps = PropsWithChildren<{
|
export type DeleteServerButtonProps = PropsWithChildren<{
|
||||||
server: ServerWithId;
|
server: ServerWithId;
|
||||||
|
|||||||
@ -1,7 +1,8 @@
|
|||||||
import { FC, useRef } from 'react';
|
import type { FC } from 'react';
|
||||||
import { Button, Modal, ModalBody, ModalFooter, ModalHeader } from 'reactstrap';
|
import { useRef } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
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 {
|
export interface DeleteServerModalProps {
|
||||||
server: ServerWithId;
|
server: ServerWithId;
|
||||||
|
|||||||
@ -1,10 +1,11 @@
|
|||||||
import { FC } from 'react';
|
import type { FC } from 'react';
|
||||||
import { Button } from 'reactstrap';
|
import { Button } from 'reactstrap';
|
||||||
import { NoMenuLayout } from '../common/NoMenuLayout';
|
import { NoMenuLayout } from '../common/NoMenuLayout';
|
||||||
import { useGoBack, useParsedQuery } from '../utils/helpers/hooks';
|
import { useGoBack, useParsedQuery } from '../utils/helpers/hooks';
|
||||||
|
import type { ServerData } from './data';
|
||||||
|
import { isServerWithId } from './data';
|
||||||
import { ServerForm } from './helpers/ServerForm';
|
import { ServerForm } from './helpers/ServerForm';
|
||||||
import { withSelectedServer } from './helpers/withSelectedServer';
|
import { withSelectedServer } from './helpers/withSelectedServer';
|
||||||
import { isServerWithId, ServerData } from './data';
|
|
||||||
|
|
||||||
interface EditServerProps {
|
interface EditServerProps {
|
||||||
editServer: (serverId: string, serverData: ServerData) => void;
|
editServer: (serverId: string, serverData: ServerData) => void;
|
||||||
|
|||||||
@ -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 { faFileDownload as exportIcon, faPlus as plusIcon } from '@fortawesome/free-solid-svg-icons';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
|
import type { FC } from 'react';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
import { Button, Row } from 'reactstrap';
|
||||||
import { NoMenuLayout } from '../common/NoMenuLayout';
|
import { NoMenuLayout } from '../common/NoMenuLayout';
|
||||||
import { SimpleCard } from '../utils/SimpleCard';
|
import type { TimeoutToggle } from '../utils/helpers/hooks';
|
||||||
import { SearchField } from '../utils/SearchField';
|
|
||||||
import { Result } from '../utils/Result';
|
import { Result } from '../utils/Result';
|
||||||
import { TimeoutToggle } from '../utils/helpers/hooks';
|
import { SearchField } from '../utils/SearchField';
|
||||||
import { ImportServersBtnProps } from './helpers/ImportServersBtn';
|
import { SimpleCard } from '../utils/SimpleCard';
|
||||||
import { ServersMap } from './data';
|
import type { ServersMap } from './data';
|
||||||
import { ManageServersRowProps } from './ManageServersRow';
|
import type { ImportServersBtnProps } from './helpers/ImportServersBtn';
|
||||||
import ServersExporter from './services/ServersExporter';
|
import type { ManageServersRowProps } from './ManageServersRow';
|
||||||
|
import type { ServersExporter } from './services/ServersExporter';
|
||||||
|
|
||||||
interface ManageServersProps {
|
interface ManageServersProps {
|
||||||
servers: ServersMap;
|
servers: ServersMap;
|
||||||
|
|||||||
@ -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 { faCheck as checkIcon } from '@fortawesome/free-solid-svg-icons';
|
||||||
import { ServerWithId } from './data';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import { ManageServersRowDropdownProps } from './ManageServersRowDropdown';
|
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 {
|
export interface ManageServersRowProps {
|
||||||
server: ServerWithId;
|
server: ServerWithId;
|
||||||
|
|||||||
@ -1,18 +1,18 @@
|
|||||||
import { FC } from 'react';
|
import { faCircle as toggleOnIcon } from '@fortawesome/free-regular-svg-icons';
|
||||||
import { DropdownItem } from 'reactstrap';
|
|
||||||
import { Link } from 'react-router-dom';
|
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
|
||||||
import {
|
import {
|
||||||
faBan as toggleOffIcon,
|
faBan as toggleOffIcon,
|
||||||
faEdit as editIcon,
|
faEdit as editIcon,
|
||||||
faMinusCircle as deleteIcon,
|
faMinusCircle as deleteIcon,
|
||||||
faPlug as connectIcon,
|
faPlug as connectIcon,
|
||||||
} from '@fortawesome/free-solid-svg-icons';
|
} 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 { DropdownBtnMenu } from '../utils/DropdownBtnMenu';
|
||||||
import { useToggle } from '../utils/helpers/hooks';
|
import { useToggle } from '../utils/helpers/hooks';
|
||||||
import { DeleteServerModalProps } from './DeleteServerModal';
|
import type { ServerWithId } from './data';
|
||||||
import { ServerWithId } from './data';
|
import type { DeleteServerModalProps } from './DeleteServerModal';
|
||||||
|
|
||||||
export interface ManageServersRowDropdownProps {
|
export interface ManageServersRowDropdownProps {
|
||||||
server: ServerWithId;
|
server: ServerWithId;
|
||||||
|
|||||||
@ -1,18 +1,23 @@
|
|||||||
import { FC, useEffect } from 'react';
|
import type { FC } from 'react';
|
||||||
import { Card, CardBody, CardHeader, Row } from 'reactstrap';
|
import { useEffect } from 'react';
|
||||||
import { Link, useNavigate } from 'react-router-dom';
|
import { Link, useNavigate } from 'react-router-dom';
|
||||||
import { ITEMS_IN_OVERVIEW_PAGE, ShortUrlsList as ShortUrlsListState } from '../short-urls/reducers/shortUrlsList';
|
import { Card, CardBody, CardHeader, Row } from 'reactstrap';
|
||||||
import { prettify } from '../utils/helpers/numbers';
|
import type { ShlinkShortUrlsListParams } from '../api/types';
|
||||||
import { TagsList } from '../tags/reducers/tagsList';
|
|
||||||
import { ShortUrlsTableType } from '../short-urls/ShortUrlsTable';
|
|
||||||
import { boundToMercureHub } from '../mercure/helpers/boundToMercureHub';
|
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 { Topics } from '../mercure/helpers/Topics';
|
||||||
import { ShlinkShortUrlsListParams } from '../api/types';
|
import type { Settings } from '../settings/reducers/settings';
|
||||||
import { supportsNonOrphanVisits } from '../utils/helpers/features';
|
import type { CreateShortUrlProps } from '../short-urls/CreateShortUrl';
|
||||||
import { getServerId, SelectedServer } from './data';
|
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 { HighlightCard } from './helpers/HighlightCard';
|
||||||
|
import { VisitsHighlightCard } from './helpers/VisitsHighlightCard';
|
||||||
|
|
||||||
interface OverviewConnectProps {
|
interface OverviewConnectProps {
|
||||||
shortUrlsList: ShortUrlsListState;
|
shortUrlsList: ShortUrlsListState;
|
||||||
@ -22,6 +27,7 @@ interface OverviewConnectProps {
|
|||||||
selectedServer: SelectedServer;
|
selectedServer: SelectedServer;
|
||||||
visitsOverview: VisitsOverview;
|
visitsOverview: VisitsOverview;
|
||||||
loadVisitsOverview: Function;
|
loadVisitsOverview: Function;
|
||||||
|
settings: Settings;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Overview = (
|
export const Overview = (
|
||||||
@ -35,12 +41,13 @@ export const Overview = (
|
|||||||
selectedServer,
|
selectedServer,
|
||||||
loadVisitsOverview,
|
loadVisitsOverview,
|
||||||
visitsOverview,
|
visitsOverview,
|
||||||
|
settings: { visits },
|
||||||
}: OverviewConnectProps) => {
|
}: OverviewConnectProps) => {
|
||||||
const { loading, shortUrls } = shortUrlsList;
|
const { loading, shortUrls } = shortUrlsList;
|
||||||
const { loading: loadingTags } = tagsList;
|
const { loading: loadingTags } = tagsList;
|
||||||
const { loading: loadingVisits, visitsCount, orphanVisitsCount } = visitsOverview;
|
const { loading: loadingVisits, nonOrphanVisits, orphanVisits } = visitsOverview;
|
||||||
const serverId = getServerId(selectedServer);
|
const serverId = getServerId(selectedServer);
|
||||||
const linkToNonOrphanVisits = supportsNonOrphanVisits(selectedServer);
|
const linkToNonOrphanVisits = useFeature('nonOrphanVisits', selectedServer);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -53,14 +60,22 @@ export const Overview = (
|
|||||||
<>
|
<>
|
||||||
<Row>
|
<Row>
|
||||||
<div className="col-lg-6 col-xl-3 mb-3">
|
<div className="col-lg-6 col-xl-3 mb-3">
|
||||||
<HighlightCard title="Visits" link={linkToNonOrphanVisits && `/server/${serverId}/non-orphan-visits`}>
|
<VisitsHighlightCard
|
||||||
{loadingVisits ? 'Loading...' : prettify(visitsCount)}
|
title="Visits"
|
||||||
</HighlightCard>
|
link={linkToNonOrphanVisits ? `/server/${serverId}/non-orphan-visits` : undefined}
|
||||||
|
excludeBots={visits?.excludeBots ?? false}
|
||||||
|
loading={loadingVisits}
|
||||||
|
visitsSummary={nonOrphanVisits}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-lg-6 col-xl-3 mb-3">
|
<div className="col-lg-6 col-xl-3 mb-3">
|
||||||
<HighlightCard title="Orphan visits" link={`/server/${serverId}/orphan-visits`}>
|
<VisitsHighlightCard
|
||||||
{loadingVisits ? 'Loading...' : prettify(orphanVisitsCount)}
|
title="Orphan visits"
|
||||||
</HighlightCard>
|
link={`/server/${serverId}/orphan-visits`}
|
||||||
|
excludeBots={visits?.excludeBots ?? false}
|
||||||
|
loading={loadingVisits}
|
||||||
|
visitsSummary={orphanVisits}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-lg-6 col-xl-3 mb-3">
|
<div className="col-lg-6 col-xl-3 mb-3">
|
||||||
<HighlightCard title="Short URLs" link={`/server/${serverId}/list-short-urls/1`}>
|
<HighlightCard title="Short URLs" link={`/server/${serverId}/list-short-urls/1`}>
|
||||||
|
|||||||
@ -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 { faPlus as plusIcon, faServer as serverIcon } from '@fortawesome/free-solid-svg-icons';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
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 {
|
export interface ServersDropdownProps {
|
||||||
servers: ServersMap;
|
servers: ServersMap;
|
||||||
|
|||||||
@ -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 { 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';
|
import './ServersListGroup.scss';
|
||||||
|
|
||||||
type ServersListGroupProps = PropsWithChildren<{
|
type ServersListGroupProps = PropsWithChildren<{
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { omit } from 'ramda';
|
import { omit } from 'ramda';
|
||||||
import { SemVer } from '../../utils/helpers/version';
|
import type { SemVer } from '../../utils/helpers/version';
|
||||||
|
|
||||||
export interface ServerData {
|
export interface ServerData {
|
||||||
name: string;
|
name: string;
|
||||||
|
|||||||
@ -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 { Button, Modal, ModalBody, ModalFooter, ModalHeader } from 'reactstrap';
|
||||||
import { ServerData } from '../data';
|
import type { ServerData } from '../data';
|
||||||
|
|
||||||
interface DuplicatedServersModalProps {
|
interface DuplicatedServersModalProps {
|
||||||
duplicatedServers: ServerData[];
|
duplicatedServers: ServerData[];
|
||||||
|
|||||||
@ -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 { faArrowAltCircleRight as linkIcon } from '@fortawesome/free-regular-svg-icons';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
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';
|
import './HighlightCard.scss';
|
||||||
|
|
||||||
export type HighlightCardProps = PropsWithChildren<{
|
export type HighlightCardProps = PropsWithChildren<{
|
||||||
title: string;
|
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 }) => (
|
export const HighlightCard: FC<HighlightCardProps> = ({ children, title, link, tooltip }) => {
|
||||||
<Card className="highlight-card" body {...buildExtraProps(link)}>
|
const ref = useElementRef<HTMLElement>();
|
||||||
{link && <FontAwesomeIcon size="3x" className="highlight-card__link-icon" icon={linkIcon} />}
|
|
||||||
<CardTitle tag="h5" className="highlight-card__title">{title}</CardTitle>
|
return (
|
||||||
<CardText tag="h2">{children}</CardText>
|
<>
|
||||||
</Card>
|
<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>}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
@ -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 { faFileUpload as importIcon } from '@fortawesome/free-solid-svg-icons';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import { useToggle } from '../../utils/helpers/hooks';
|
import { complement, pipe } from 'ramda';
|
||||||
import { mutableRefToElementRef } from '../../utils/helpers/components';
|
import type { ChangeEvent, FC, PropsWithChildren } from 'react';
|
||||||
import { ServersImporter } from '../services/ServersImporter';
|
import { useEffect, useState } from 'react';
|
||||||
import { ServerData, ServersMap } from '../data';
|
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 { DuplicatedServersModal } from './DuplicatedServersModal';
|
||||||
import './ImportServersBtn.scss';
|
import './ImportServersBtn.scss';
|
||||||
|
|
||||||
@ -34,7 +34,7 @@ export const ImportServersBtn = ({ importServersFromFile }: ServersImporter): FC
|
|||||||
tooltipPlacement = 'bottom',
|
tooltipPlacement = 'bottom',
|
||||||
className = '',
|
className = '',
|
||||||
}) => {
|
}) => {
|
||||||
const ref = useRef<HTMLInputElement>();
|
const ref = useElementRef<HTMLInputElement>();
|
||||||
const [serversToCreate, setServersToCreate] = useState<ServerData[] | undefined>();
|
const [serversToCreate, setServersToCreate] = useState<ServerData[] | undefined>();
|
||||||
const [duplicatedServers, setDuplicatedServers] = useState<ServerData[]>([]);
|
const [duplicatedServers, setDuplicatedServers] = useState<ServerData[]>([]);
|
||||||
const [isModalOpen,, showModal, hideModal] = useToggle();
|
const [isModalOpen,, showModal, hideModal] = useToggle();
|
||||||
@ -79,7 +79,7 @@ export const ImportServersBtn = ({ importServersFromFile }: ServersImporter): FC
|
|||||||
type="file"
|
type="file"
|
||||||
accept="text/csv"
|
accept="text/csv"
|
||||||
className="import-servers-btn__csv-select"
|
className="import-servers-btn__csv-select"
|
||||||
ref={mutableRefToElementRef(ref)}
|
ref={ref}
|
||||||
onChange={onFile}
|
onChange={onFile}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@ -1,10 +1,11 @@
|
|||||||
import { FC } from 'react';
|
import type { FC } from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
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 { 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';
|
import './ServerError.scss';
|
||||||
|
|
||||||
interface ServerErrorProps {
|
interface ServerErrorProps {
|
||||||
|
|||||||
@ -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 { InputFormGroup } from '../../utils/forms/InputFormGroup';
|
||||||
import { handleEventPreventingDefault } from '../../utils/utils';
|
|
||||||
import { ServerData } from '../data';
|
|
||||||
import { SimpleCard } from '../../utils/SimpleCard';
|
import { SimpleCard } from '../../utils/SimpleCard';
|
||||||
|
import { handleEventPreventingDefault } from '../../utils/utils';
|
||||||
|
import type { ServerData } from '../data';
|
||||||
|
|
||||||
type ServerFormProps = PropsWithChildren<{
|
type ServerFormProps = PropsWithChildren<{
|
||||||
onSubmit: (server: ServerData) => void;
|
onSubmit: (server: ServerData) => void;
|
||||||
|
|||||||
26
src/servers/helpers/VisitsHighlightCard.tsx
Normal file
26
src/servers/helpers/VisitsHighlightCard.tsx
Normal 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>
|
||||||
|
);
|
||||||
@ -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 { useParams } from 'react-router-dom';
|
||||||
import { Message } from '../../utils/Message';
|
|
||||||
import { isNotFoundServer, SelectedServer } from '../data';
|
|
||||||
import { NoMenuLayout } from '../../common/NoMenuLayout';
|
import { NoMenuLayout } from '../../common/NoMenuLayout';
|
||||||
|
import { Message } from '../../utils/Message';
|
||||||
|
import type { SelectedServer } from '../data';
|
||||||
|
import { isNotFoundServer } from '../data';
|
||||||
|
|
||||||
interface WithSelectedServerProps {
|
interface WithSelectedServerProps {
|
||||||
selectServer: (serverId: string) => void;
|
selectServer: (serverId: string) => void;
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { FC, useEffect } from 'react';
|
import type { FC } from 'react';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
interface WithoutSelectedServerProps {
|
interface WithoutSelectedServerProps {
|
||||||
resetSelectedServer: Function;
|
resetSelectedServer: Function;
|
||||||
|
|||||||
@ -1,8 +1,9 @@
|
|||||||
import pack from '../../../package.json';
|
import pack from '../../../package.json';
|
||||||
import { hasServerData, ServerData } from '../data';
|
import type { HttpClient } from '../../common/services/HttpClient';
|
||||||
import { createServers } from './servers';
|
|
||||||
import { createAsyncThunk } from '../../utils/helpers/redux';
|
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) : []);
|
const responseToServersList = (data: any): ServerData[] => (Array.isArray(data) ? data.filter(hasServerData) : []);
|
||||||
|
|
||||||
|
|||||||
@ -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 { memoizeWith, pipe } from 'ramda';
|
||||||
import { versionToPrintable, versionToSemVer as toSemVer } from '../../utils/helpers/version';
|
import type { ShlinkApiClientBuilder } from '../../api/services/ShlinkApiClientBuilder';
|
||||||
import { isReachableServer, SelectedServer, ServerWithId } from '../data';
|
import type { ShlinkHealth } from '../../api/types';
|
||||||
import { ShlinkHealth } from '../../api/types';
|
|
||||||
import { createAsyncThunk } from '../../utils/helpers/redux';
|
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';
|
const REDUCER_PREFIX = 'shlink/selectedServer';
|
||||||
|
|
||||||
|
|||||||
@ -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 { assoc, dissoc, fromPairs, map, pipe, reduce, toPairs } from 'ramda';
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
import { ServerData, ServersMap, ServerWithId } from '../data';
|
import type { ServerData, ServersMap, ServerWithId } from '../data';
|
||||||
|
|
||||||
interface EditServer {
|
interface EditServer {
|
||||||
serverId: string;
|
serverId: string;
|
||||||
|
|||||||
@ -1,12 +1,13 @@
|
|||||||
import { values } from 'ramda';
|
import { values } from 'ramda';
|
||||||
import { LocalStorage } from '../../utils/services/LocalStorage';
|
import type { JsonToCsv } from '../../utils/helpers/csvjson';
|
||||||
import { ServersMap, serverWithIdToServerData } from '../data';
|
|
||||||
import { saveCsv } from '../../utils/helpers/files';
|
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';
|
const SERVERS_FILENAME = 'shlink-servers.csv';
|
||||||
|
|
||||||
export default class ServersExporter {
|
export class ServersExporter {
|
||||||
public constructor(
|
public constructor(
|
||||||
private readonly storage: LocalStorage,
|
private readonly storage: LocalStorage,
|
||||||
private readonly window: Window,
|
private readonly window: Window,
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { ServerData } from '../data';
|
import type { CsvToJson } from '../../utils/helpers/csvjson';
|
||||||
import { CsvToJson } from '../../utils/helpers/csvjson';
|
import type { ServerData } from '../data';
|
||||||
|
|
||||||
const validateServer = (server: any): server is ServerData =>
|
const validateServer = (server: any): server is ServerData =>
|
||||||
typeof server.url === 'string' && typeof server.apiKey === 'string' && typeof server.name === 'string';
|
typeof server.url === 'string' && typeof server.apiKey === 'string' && typeof server.name === 'string';
|
||||||
|
|||||||
@ -1,11 +1,18 @@
|
|||||||
|
import type Bottle from 'bottlejs';
|
||||||
import { prop } from 'ramda';
|
import { prop } from 'ramda';
|
||||||
import Bottle from 'bottlejs';
|
import type { ConnectDecorator } from '../../container/types';
|
||||||
import { CreateServer } from '../CreateServer';
|
import { CreateServer } from '../CreateServer';
|
||||||
import { ServersDropdown } from '../ServersDropdown';
|
|
||||||
import { DeleteServerModal } from '../DeleteServerModal';
|
|
||||||
import { DeleteServerButton } from '../DeleteServerButton';
|
import { DeleteServerButton } from '../DeleteServerButton';
|
||||||
|
import { DeleteServerModal } from '../DeleteServerModal';
|
||||||
import { EditServer } from '../EditServer';
|
import { EditServer } from '../EditServer';
|
||||||
import { ImportServersBtn } from '../helpers/ImportServersBtn';
|
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 {
|
import {
|
||||||
resetSelectedServer,
|
resetSelectedServer,
|
||||||
selectedServerReducerCreator,
|
selectedServerReducerCreator,
|
||||||
@ -13,18 +20,11 @@ import {
|
|||||||
selectServerListener,
|
selectServerListener,
|
||||||
} from '../reducers/selectedServer';
|
} from '../reducers/selectedServer';
|
||||||
import { createServers, deleteServer, editServer, setAutoConnect } from '../reducers/servers';
|
import { createServers, deleteServer, editServer, setAutoConnect } from '../reducers/servers';
|
||||||
import { fetchServers } from '../reducers/remoteServers';
|
import { ServersDropdown } from '../ServersDropdown';
|
||||||
import { ServerError } from '../helpers/ServerError';
|
import { ServersExporter } from './ServersExporter';
|
||||||
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 { ServersImporter } from './ServersImporter';
|
import { ServersImporter } from './ServersImporter';
|
||||||
import ServersExporter from './ServersExporter';
|
|
||||||
|
|
||||||
const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
|
export const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
|
||||||
// Components
|
// Components
|
||||||
bottle.serviceFactory(
|
bottle.serviceFactory(
|
||||||
'ManageServers',
|
'ManageServers',
|
||||||
@ -65,7 +65,7 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
|
|||||||
|
|
||||||
bottle.serviceFactory('Overview', Overview, 'ShortUrlsTable', 'CreateShortUrl');
|
bottle.serviceFactory('Overview', Overview, 'ShortUrlsTable', 'CreateShortUrl');
|
||||||
bottle.decorator('Overview', connect(
|
bottle.decorator('Overview', connect(
|
||||||
['shortUrlsList', 'tagsList', 'selectedServer', 'mercureInfo', 'visitsOverview'],
|
['shortUrlsList', 'tagsList', 'selectedServer', 'mercureInfo', 'visitsOverview', 'settings'],
|
||||||
['listShortUrls', 'listTags', 'createNewVisits', 'loadMercureInfo', 'loadVisitsOverview'],
|
['listShortUrls', 'listTags', 'createNewVisits', 'loadMercureInfo', 'loadVisitsOverview'],
|
||||||
));
|
));
|
||||||
|
|
||||||
@ -89,5 +89,3 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
|
|||||||
bottle.serviceFactory('selectedServerReducerCreator', selectedServerReducerCreator, 'selectServer');
|
bottle.serviceFactory('selectedServerReducerCreator', selectedServerReducerCreator, 'selectServer');
|
||||||
bottle.serviceFactory('selectedServerReducer', prop('reducer'), 'selectedServerReducerCreator');
|
bottle.serviceFactory('selectedServerReducer', prop('reducer'), 'selectedServerReducerCreator');
|
||||||
};
|
};
|
||||||
|
|
||||||
export default provideServices;
|
|
||||||
|
|||||||
@ -1,11 +1,11 @@
|
|||||||
import { FormGroup, Input } from 'reactstrap';
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { ToggleSwitch } from '../utils/ToggleSwitch';
|
import { FormGroup, Input } from 'reactstrap';
|
||||||
import { SimpleCard } from '../utils/SimpleCard';
|
|
||||||
import { FormText } from '../utils/forms/FormText';
|
import { FormText } from '../utils/forms/FormText';
|
||||||
import { LabeledFormGroup } from '../utils/forms/LabeledFormGroup';
|
import { LabeledFormGroup } from '../utils/forms/LabeledFormGroup';
|
||||||
import { Settings } from './reducers/settings';
|
|
||||||
import { useDomId } from '../utils/helpers/hooks';
|
import { useDomId } from '../utils/helpers/hooks';
|
||||||
|
import { SimpleCard } from '../utils/SimpleCard';
|
||||||
|
import { ToggleSwitch } from '../utils/ToggleSwitch';
|
||||||
|
import type { Settings } from './reducers/settings';
|
||||||
|
|
||||||
interface RealTimeUpdatesProps {
|
interface RealTimeUpdatesProps {
|
||||||
settings: Settings;
|
settings: Settings;
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { FC, ReactNode } from 'react';
|
import type { FC, ReactNode } from 'react';
|
||||||
import { Navigate, Routes, Route } from 'react-router-dom';
|
import { Navigate, Route, Routes } from 'react-router-dom';
|
||||||
import { NoMenuLayout } from '../common/NoMenuLayout';
|
import { NoMenuLayout } from '../common/NoMenuLayout';
|
||||||
import { NavPillItem, NavPills } from '../utils/NavPills';
|
import { NavPillItem, NavPills } from '../utils/NavPills';
|
||||||
|
|
||||||
|
|||||||
@ -1,11 +1,11 @@
|
|||||||
import { FC, ReactNode } from 'react';
|
import type { FC, ReactNode } from 'react';
|
||||||
import { DropdownItem, FormGroup } from 'reactstrap';
|
import { DropdownItem, FormGroup } from 'reactstrap';
|
||||||
import { SimpleCard } from '../utils/SimpleCard';
|
|
||||||
import { ToggleSwitch } from '../utils/ToggleSwitch';
|
|
||||||
import { DropdownBtn } from '../utils/DropdownBtn';
|
import { DropdownBtn } from '../utils/DropdownBtn';
|
||||||
import { FormText } from '../utils/forms/FormText';
|
import { FormText } from '../utils/forms/FormText';
|
||||||
import { LabeledFormGroup } from '../utils/forms/LabeledFormGroup';
|
import { LabeledFormGroup } from '../utils/forms/LabeledFormGroup';
|
||||||
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 {
|
interface ShortUrlCreationProps {
|
||||||
settings: Settings;
|
settings: Settings;
|
||||||
|
|||||||
@ -1,9 +1,10 @@
|
|||||||
import { FC } from 'react';
|
import type { FC } from 'react';
|
||||||
import { OrderingDropdown } from '../utils/OrderingDropdown';
|
|
||||||
import { SHORT_URLS_ORDERABLE_FIELDS } from '../short-urls/data';
|
import { SHORT_URLS_ORDERABLE_FIELDS } from '../short-urls/data';
|
||||||
import { SimpleCard } from '../utils/SimpleCard';
|
|
||||||
import { LabeledFormGroup } from '../utils/forms/LabeledFormGroup';
|
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 {
|
interface ShortUrlsListSettingsProps {
|
||||||
settings: Settings;
|
settings: Settings;
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
import { FC } from 'react';
|
import type { FC } from 'react';
|
||||||
import { SimpleCard } from '../utils/SimpleCard';
|
|
||||||
import { OrderingDropdown } from '../utils/OrderingDropdown';
|
|
||||||
import { TAGS_ORDERABLE_FIELDS } from '../tags/data/TagsListChildrenProps';
|
import { TAGS_ORDERABLE_FIELDS } from '../tags/data/TagsListChildrenProps';
|
||||||
import { LabeledFormGroup } from '../utils/forms/LabeledFormGroup';
|
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 {
|
interface TagsProps {
|
||||||
settings: Settings;
|
settings: Settings;
|
||||||
|
|||||||
@ -1,10 +1,11 @@
|
|||||||
import { FC } from 'react';
|
import { faMoon, faSun } from '@fortawesome/free-solid-svg-icons';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
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 { SimpleCard } from '../utils/SimpleCard';
|
||||||
|
import type { Theme } from '../utils/theme';
|
||||||
|
import { changeThemeInMarkup } from '../utils/theme';
|
||||||
import { ToggleSwitch } from '../utils/ToggleSwitch';
|
import { ToggleSwitch } from '../utils/ToggleSwitch';
|
||||||
import { changeThemeInMarkup, Theme } from '../utils/theme';
|
import type { Settings, UiSettings } from './reducers/settings';
|
||||||
import { Settings, UiSettings } from './reducers/settings';
|
|
||||||
import './UserInterfaceSettings.scss';
|
import './UserInterfaceSettings.scss';
|
||||||
|
|
||||||
interface UserInterfaceProps {
|
interface UserInterfaceProps {
|
||||||
|
|||||||
@ -1,12 +1,12 @@
|
|||||||
import { FC } from 'react';
|
import type { FC } from 'react';
|
||||||
import { FormGroup } from 'reactstrap';
|
import { FormGroup } from 'reactstrap';
|
||||||
import { SimpleCard } from '../utils/SimpleCard';
|
|
||||||
import { DateIntervalSelector } from '../utils/dates/DateIntervalSelector';
|
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 { 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 {
|
interface VisitsProps {
|
||||||
settings: Settings;
|
settings: Settings;
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { ShlinkState } from '../../container/types';
|
import type { ShlinkState } from '../../container/types';
|
||||||
|
|
||||||
/* eslint-disable no-param-reassign */
|
/* eslint-disable no-param-reassign */
|
||||||
export const migrateDeprecatedSettings = (state: Partial<ShlinkState>): Partial<ShlinkState> => {
|
export const migrateDeprecatedSettings = (state: Partial<ShlinkState>): Partial<ShlinkState> => {
|
||||||
|
|||||||
@ -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 { mergeDeepRight } from 'ramda';
|
||||||
import { Theme } from '../../utils/theme';
|
import type { ShortUrlsOrder } from '../../short-urls/data';
|
||||||
import { DateInterval } from '../../utils/helpers/dateIntervals';
|
import type { TagsOrder } from '../../tags/data/TagsListChildrenProps';
|
||||||
import { TagsOrder } from '../../tags/data/TagsListChildrenProps';
|
import type { DateInterval } from '../../utils/helpers/dateIntervals';
|
||||||
import { ShortUrlsOrder } from '../../short-urls/data';
|
import type { Theme } from '../../utils/theme';
|
||||||
|
|
||||||
export const DEFAULT_SHORT_URLS_ORDERING: ShortUrlsOrder = {
|
export const DEFAULT_SHORT_URLS_ORDERING: ShortUrlsOrder = {
|
||||||
field: 'dateCreated',
|
field: 'dateCreated',
|
||||||
|
|||||||
@ -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 { RealTimeUpdatesSettings } from '../RealTimeUpdatesSettings';
|
||||||
import { Settings } from '../Settings';
|
|
||||||
import {
|
import {
|
||||||
setRealTimeUpdatesInterval,
|
setRealTimeUpdatesInterval,
|
||||||
setShortUrlCreationSettings,
|
setShortUrlCreationSettings,
|
||||||
@ -10,15 +11,14 @@ import {
|
|||||||
setVisitsSettings,
|
setVisitsSettings,
|
||||||
toggleRealTimeUpdates,
|
toggleRealTimeUpdates,
|
||||||
} from '../reducers/settings';
|
} from '../reducers/settings';
|
||||||
import { ConnectDecorator } from '../../container/types';
|
import { Settings } from '../Settings';
|
||||||
import { withoutSelectedServer } from '../../servers/helpers/withoutSelectedServer';
|
|
||||||
import { ShortUrlCreationSettings } from '../ShortUrlCreationSettings';
|
import { ShortUrlCreationSettings } from '../ShortUrlCreationSettings';
|
||||||
|
import { ShortUrlsListSettings } from '../ShortUrlsListSettings';
|
||||||
|
import { TagsSettings } from '../TagsSettings';
|
||||||
import { UserInterfaceSettings } from '../UserInterfaceSettings';
|
import { UserInterfaceSettings } from '../UserInterfaceSettings';
|
||||||
import { VisitsSettings } from '../VisitsSettings';
|
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
|
// Components
|
||||||
bottle.serviceFactory(
|
bottle.serviceFactory(
|
||||||
'Settings',
|
'Settings',
|
||||||
@ -63,5 +63,3 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
|
|||||||
bottle.serviceFactory('setVisitsSettings', () => setVisitsSettings);
|
bottle.serviceFactory('setVisitsSettings', () => setVisitsSettings);
|
||||||
bottle.serviceFactory('setTagsSettings', () => setTagsSettings);
|
bottle.serviceFactory('setTagsSettings', () => setTagsSettings);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default provideServices;
|
|
||||||
|
|||||||
@ -1,10 +1,11 @@
|
|||||||
import { FC, useMemo } from 'react';
|
import type { FC } from 'react';
|
||||||
import { SelectedServer } from '../servers/data';
|
import { useMemo } from 'react';
|
||||||
import { Settings, ShortUrlCreationSettings } from '../settings/reducers/settings';
|
import type { SelectedServer } from '../servers/data';
|
||||||
import { ShortUrlData } from './data';
|
import type { Settings, ShortUrlCreationSettings } from '../settings/reducers/settings';
|
||||||
import { ShortUrlCreation } from './reducers/shortUrlCreation';
|
import type { ShortUrlData } from './data';
|
||||||
import { CreateShortUrlResultProps } from './helpers/CreateShortUrlResult';
|
import type { CreateShortUrlResultProps } from './helpers/CreateShortUrlResult';
|
||||||
import { ShortUrlFormProps } from './ShortUrlForm';
|
import type { ShortUrlCreation } from './reducers/shortUrlCreation';
|
||||||
|
import type { ShortUrlFormProps } from './ShortUrlForm';
|
||||||
|
|
||||||
export interface CreateShortUrlProps {
|
export interface CreateShortUrlProps {
|
||||||
basicMode?: boolean;
|
basicMode?: boolean;
|
||||||
|
|||||||
@ -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 { 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 { ExternalLink } from 'react-external-link';
|
||||||
import { useLocation, useParams } from 'react-router-dom';
|
import { useLocation, useParams } from 'react-router-dom';
|
||||||
import { SelectedServer } from '../servers/data';
|
import { Button, Card } from 'reactstrap';
|
||||||
import { Settings } from '../settings/reducers/settings';
|
import { ShlinkApiError } from '../api/ShlinkApiError';
|
||||||
import { ShortUrlIdentifier } from './data';
|
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 { parseQuery } from '../utils/helpers/query';
|
||||||
import { Message } from '../utils/Message';
|
import { Message } from '../utils/Message';
|
||||||
import { Result } from '../utils/Result';
|
import { Result } from '../utils/Result';
|
||||||
import { ShlinkApiError } from '../api/ShlinkApiError';
|
import type { ShortUrlIdentifier } from './data';
|
||||||
import { useGoBack } from '../utils/helpers/hooks';
|
|
||||||
import { ShortUrlFormProps } from './ShortUrlForm';
|
|
||||||
import { ShortUrlDetail } from './reducers/shortUrlDetail';
|
|
||||||
import { EditShortUrl as EditShortUrlInfo, ShortUrlEdition } from './reducers/shortUrlEdition';
|
|
||||||
import { shortUrlDataFromShortUrl, urlDecodeShortCode } from './helpers';
|
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 {
|
interface EditShortUrlConnectProps {
|
||||||
settings: Settings;
|
settings: Settings;
|
||||||
|
|||||||
@ -1,13 +1,14 @@
|
|||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { Pagination, PaginationItem, PaginationLink } from 'reactstrap';
|
import { Pagination, PaginationItem, PaginationLink } from 'reactstrap';
|
||||||
|
import type { ShlinkPaginator } from '../api/types';
|
||||||
|
import type {
|
||||||
|
NumberOrEllipsis } from '../utils/helpers/pagination';
|
||||||
import {
|
import {
|
||||||
pageIsEllipsis,
|
|
||||||
keyForPage,
|
keyForPage,
|
||||||
progressivePagination,
|
pageIsEllipsis,
|
||||||
prettifyPageNumber,
|
prettifyPageNumber,
|
||||||
NumberOrEllipsis,
|
progressivePagination,
|
||||||
} from '../utils/helpers/pagination';
|
} from '../utils/helpers/pagination';
|
||||||
import { ShlinkPaginator } from '../api/types';
|
|
||||||
|
|
||||||
interface PaginatorProps {
|
interface PaginatorProps {
|
||||||
paginator?: ShlinkPaginator;
|
paginator?: ShlinkPaginator;
|
||||||
|
|||||||
@ -1,20 +1,27 @@
|
|||||||
import { FC, useEffect, useState } from 'react';
|
import type { IconProp } from '@fortawesome/fontawesome-svg-core';
|
||||||
import { InputType } from 'reactstrap/types/lib/Input';
|
import { faAndroid, faApple } from '@fortawesome/free-brands-svg-icons';
|
||||||
import { Button, FormGroup, Input, Row } from 'reactstrap';
|
import { faDesktop } from '@fortawesome/free-solid-svg-icons';
|
||||||
import { cond, isEmpty, pipe, replace, trim, T } from 'ramda';
|
import classNames from 'classnames';
|
||||||
import { parseISO } from 'date-fns';
|
import { parseISO } from 'date-fns';
|
||||||
import { DateTimeInput, DateTimeInputProps } from '../utils/dates/DateTimeInput';
|
import { isEmpty, pipe, replace, trim } from 'ramda';
|
||||||
import { supportsForwardQuery } from '../utils/helpers/features';
|
import type { ChangeEvent, FC } from 'react';
|
||||||
import { SimpleCard } from '../utils/SimpleCard';
|
import { useEffect, useState } from 'react';
|
||||||
import { handleEventPreventingDefault, hasValue, OptionalString } from '../utils/utils';
|
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 { Checkbox } from '../utils/Checkbox';
|
||||||
import { SelectedServer } from '../servers/data';
|
import type { DateTimeInputProps } from '../utils/dates/DateTimeInput';
|
||||||
import { TagsSelectorProps } from '../tags/helpers/TagsSelector';
|
import { DateTimeInput } from '../utils/dates/DateTimeInput';
|
||||||
import { DomainSelectorProps } from '../domains/DomainSelector';
|
|
||||||
import { formatIsoDate } from '../utils/helpers/date';
|
import { formatIsoDate } from '../utils/helpers/date';
|
||||||
import { UseExistingIfFoundInfoIcon } from './UseExistingIfFoundInfoIcon';
|
import { useFeature } from '../utils/helpers/features';
|
||||||
import { ShortUrlData } from './data';
|
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 { ShortUrlFormCheckboxGroup } from './helpers/ShortUrlFormCheckboxGroup';
|
||||||
|
import { UseExistingIfFoundInfoIcon } from './UseExistingIfFoundInfoIcon';
|
||||||
import './ShortUrlForm.scss';
|
import './ShortUrlForm.scss';
|
||||||
|
|
||||||
export type Mode = 'create' | 'create-basic' | 'edit';
|
export type Mode = 'create' | 'create-basic' | 'edit';
|
||||||
@ -38,38 +45,38 @@ export const ShortUrlForm = (
|
|||||||
DomainSelector: FC<DomainSelectorProps>,
|
DomainSelector: FC<DomainSelectorProps>,
|
||||||
): FC<ShortUrlFormProps> => ({ mode, saving, onSave, initialState, selectedServer }) => {
|
): FC<ShortUrlFormProps> => ({ mode, saving, onSave, initialState, selectedServer }) => {
|
||||||
const [shortUrlData, setShortUrlData] = useState(initialState);
|
const [shortUrlData, setShortUrlData] = useState(initialState);
|
||||||
|
const reset = () => setShortUrlData(initialState);
|
||||||
|
const supportsDeviceLongUrls = useFeature('deviceLongUrls', selectedServer);
|
||||||
|
|
||||||
const isEdit = mode === 'edit';
|
const isEdit = mode === 'edit';
|
||||||
const isBasicMode = mode === 'create-basic';
|
const isBasicMode = mode === 'create-basic';
|
||||||
const hadTitleOriginally = hasValue(initialState.title);
|
|
||||||
const changeTags = (tags: string[]) => setShortUrlData({ ...shortUrlData, tags: tags.map(normalizeTag) });
|
const changeTags = (tags: string[]) => setShortUrlData({ ...shortUrlData, tags: tags.map(normalizeTag) });
|
||||||
const reset = () => setShortUrlData(initialState);
|
const setResettableValue = (value: string, initialValue?: any) => {
|
||||||
const resolveNewTitle = (): OptionalString => {
|
if (hasValue(value)) {
|
||||||
const hasNewTitle = hasValue(shortUrlData.title);
|
return value;
|
||||||
const matcher = cond<never, OptionalString>([
|
}
|
||||||
[() => !hasNewTitle && !hadTitleOriginally, () => undefined],
|
|
||||||
[() => !hasNewTitle && hadTitleOriginally, () => null],
|
|
||||||
[T, () => shortUrlData.title],
|
|
||||||
]);
|
|
||||||
|
|
||||||
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({
|
const submit = handleEventPreventingDefault(async () => onSave({
|
||||||
...shortUrlData,
|
...shortUrlData,
|
||||||
validSince: formatIsoDate(shortUrlData.validSince) ?? null,
|
validSince: formatIsoDate(shortUrlData.validSince) ?? null,
|
||||||
validUntil: formatIsoDate(shortUrlData.validUntil) ?? null,
|
validUntil: formatIsoDate(shortUrlData.validUntil) ?? null,
|
||||||
maxVisits: !hasValue(shortUrlData.maxVisits) ? null : Number(shortUrlData.maxVisits),
|
maxVisits: !hasValue(shortUrlData.maxVisits) ? null : Number(shortUrlData.maxVisits),
|
||||||
title: resolveNewTitle(),
|
|
||||||
}).then(() => !isEdit && reset()).catch(() => {}));
|
}).then(() => !isEdit && reset()).catch(() => {}));
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setShortUrlData(initialState);
|
setShortUrlData(initialState);
|
||||||
}, [initialState]);
|
}, [initialState]);
|
||||||
|
|
||||||
|
// TODO Consider extracting these functions to local components
|
||||||
const renderOptionalInput = (
|
const renderOptionalInput = (
|
||||||
id: NonDateFields,
|
id: NonDateFields,
|
||||||
placeholder: string,
|
placeholder: string,
|
||||||
type: InputType = 'text',
|
type: InputType = 'text',
|
||||||
props = {},
|
props: any = {},
|
||||||
fromGroupProps = {},
|
fromGroupProps = {},
|
||||||
) => (
|
) => (
|
||||||
<FormGroup {...fromGroupProps}>
|
<FormGroup {...fromGroupProps}>
|
||||||
@ -78,11 +85,27 @@ export const ShortUrlForm = (
|
|||||||
type={type}
|
type={type}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
value={shortUrlData[id] ?? ''}
|
value={shortUrlData[id] ?? ''}
|
||||||
onChange={(e) => setShortUrlData({ ...shortUrlData, [id]: e.target.value })}
|
onChange={props.onChange ?? ((e) => setShortUrlData({ ...shortUrlData, [id]: e.target.value }))}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</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> = {}) => (
|
const renderDateInput = (id: DateFields, placeholder: string, props: Partial<DateTimeInputProps> = {}) => (
|
||||||
<DateTimeInput
|
<DateTimeInput
|
||||||
selected={shortUrlData[id] ? toDate(shortUrlData[id] as string | Date) : null}
|
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 (
|
return (
|
||||||
<form name="shortUrlForm" className="short-url-form" onSubmit={submit}>
|
<form name="shortUrlForm" className="short-url-form" onSubmit={submit}>
|
||||||
{isBasicMode && basicComponents}
|
{isBasicMode && basicComponents}
|
||||||
{!isBasicMode && (
|
{!isBasicMode && (
|
||||||
<>
|
<>
|
||||||
<SimpleCard title="Main options" className="mb-3">
|
<Row>
|
||||||
{basicComponents}
|
<div
|
||||||
</SimpleCard>
|
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>
|
<Row>
|
||||||
<div className="col-sm-6 mb-3">
|
<div className="col-sm-6 mb-3">
|
||||||
<SimpleCard title="Customize the short URL">
|
<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 && (
|
{!isEdit && (
|
||||||
<>
|
<>
|
||||||
<Row>
|
<Row>
|
||||||
|
|||||||
@ -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 { faTag, faTags } from '@fortawesome/free-solid-svg-icons';
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import classNames from 'classnames';
|
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 { DateRangeSelector } from '../utils/dates/DateRangeSelector';
|
||||||
import { formatIsoDate } from '../utils/helpers/date';
|
import { formatIsoDate } from '../utils/helpers/date';
|
||||||
import { DateRange, datesToDateRange } from '../utils/helpers/dateIntervals';
|
import type { DateRange } from '../utils/helpers/dateIntervals';
|
||||||
import { supportsAllTagsFiltering, supportsFilterDisabledUrls } from '../utils/helpers/features';
|
import { datesToDateRange } from '../utils/helpers/dateIntervals';
|
||||||
import { SelectedServer } from '../servers/data';
|
import { useFeature } from '../utils/helpers/features';
|
||||||
import { OrderDir } from '../utils/helpers/ordering';
|
import type { OrderDir } from '../utils/helpers/ordering';
|
||||||
import { OrderingDropdown } from '../utils/OrderingDropdown';
|
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 { 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 { ShortUrlsFilterDropdown } from './helpers/ShortUrlsFilterDropdown';
|
||||||
import { Settings } from '../settings/reducers/settings';
|
|
||||||
import './ShortUrlsFilteringBar.scss';
|
import './ShortUrlsFilteringBar.scss';
|
||||||
|
|
||||||
interface ShortUrlsFilteringProps {
|
interface ShortUrlsFilteringProps {
|
||||||
@ -44,7 +46,7 @@ export const ShortUrlsFilteringBar = (
|
|||||||
excludePastValidUntil,
|
excludePastValidUntil,
|
||||||
tagsMode = 'any',
|
tagsMode = 'any',
|
||||||
} = filter;
|
} = filter;
|
||||||
const supportsDisabledFiltering = supportsFilterDisabledUrls(selectedServer);
|
const supportsDisabledFiltering = useFeature('filterDisabledUrls', selectedServer);
|
||||||
|
|
||||||
const setDates = pipe(
|
const setDates = pipe(
|
||||||
({ startDate: theStartDate, endDate: theEndDate }: DateRange) => ({
|
({ startDate: theStartDate, endDate: theEndDate }: DateRange) => ({
|
||||||
@ -58,7 +60,7 @@ export const ShortUrlsFilteringBar = (
|
|||||||
(searchTerm) => toFirstPage({ search: searchTerm }),
|
(searchTerm) => toFirstPage({ search: searchTerm }),
|
||||||
);
|
);
|
||||||
const changeTagSelection = (selectedTags: string[]) => toFirstPage({ tags: selectedTags });
|
const changeTagSelection = (selectedTags: string[]) => toFirstPage({ tags: selectedTags });
|
||||||
const canChangeTagsMode = supportsAllTagsFiltering(selectedServer);
|
const canChangeTagsMode = useFeature('allTagsFiltering', selectedServer);
|
||||||
const toggleTagsMode = pipe(
|
const toggleTagsMode = pipe(
|
||||||
() => (tagsMode === 'any' ? 'all' : 'any'),
|
() => (tagsMode === 'any' ? 'all' : 'any'),
|
||||||
(mode) => toFirstPage({ tagsMode: mode }),
|
(mode) => toFirstPage({ tagsMode: mode }),
|
||||||
|
|||||||
@ -1,21 +1,24 @@
|
|||||||
import { pipe } from 'ramda';
|
import { pipe } from 'ramda';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { Card } from 'reactstrap';
|
|
||||||
import { useLocation, useParams } from 'react-router-dom';
|
import { useLocation, useParams } from 'react-router-dom';
|
||||||
import { determineOrderDir, OrderDir } from '../utils/helpers/ordering';
|
import { Card } from 'reactstrap';
|
||||||
import { getServerId, SelectedServer } from '../servers/data';
|
import type { ShlinkShortUrlsListParams, ShlinkShortUrlsOrder } from '../api/types';
|
||||||
import { boundToMercureHub } from '../mercure/helpers/boundToMercureHub';
|
import { boundToMercureHub } from '../mercure/helpers/boundToMercureHub';
|
||||||
import { Topics } from '../mercure/helpers/Topics';
|
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 { TableOrderIcon } from '../utils/table/TableOrderIcon';
|
||||||
import { ShlinkShortUrlsListParams, ShlinkShortUrlsOrder } from '../api/types';
|
import type { ShortUrlsOrder, ShortUrlsOrderableFields } from './data';
|
||||||
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 { useShortUrlsQuery } from './helpers/hooks';
|
import { useShortUrlsQuery } from './helpers/hooks';
|
||||||
import { ShortUrlsOrder, ShortUrlsOrderableFields } from './data';
|
import { Paginator } from './Paginator';
|
||||||
import { ShortUrlsFilteringBarType } from './ShortUrlsFilteringBar';
|
import type { ShortUrlsList as ShortUrlsListState } from './reducers/shortUrlsList';
|
||||||
import { supportsExcludeBotsOnShortUrls } from '../utils/helpers/features';
|
import type { ShortUrlsFilteringBarType } from './ShortUrlsFilteringBar';
|
||||||
|
import type { ShortUrlsTableType } from './ShortUrlsTable';
|
||||||
|
|
||||||
interface ShortUrlsListProps {
|
interface ShortUrlsListProps {
|
||||||
selectedServer: SelectedServer;
|
selectedServer: SelectedServer;
|
||||||
@ -49,6 +52,7 @@ export const ShortUrlsList = (
|
|||||||
);
|
);
|
||||||
const { pagination } = shortUrlsList?.shortUrls ?? {};
|
const { pagination } = shortUrlsList?.shortUrls ?? {};
|
||||||
const doExcludeBots = excludeBots ?? settings.visits?.excludeBots;
|
const doExcludeBots = excludeBots ?? settings.visits?.excludeBots;
|
||||||
|
const supportsExcludingBots = useFeature('excludeBotsOnShortUrls', selectedServer);
|
||||||
const handleOrderBy = (field?: ShortUrlsOrderableFields, dir?: OrderDir) => {
|
const handleOrderBy = (field?: ShortUrlsOrderableFields, dir?: OrderDir) => {
|
||||||
toFirstPage({ orderBy: { field, dir } });
|
toFirstPage({ orderBy: { field, dir } });
|
||||||
setActualOrderBy({ field, dir });
|
setActualOrderBy({ field, dir });
|
||||||
@ -62,7 +66,7 @@ export const ShortUrlsList = (
|
|||||||
(updatedTags) => toFirstPage({ tags: updatedTags }),
|
(updatedTags) => toFirstPage({ tags: updatedTags }),
|
||||||
);
|
);
|
||||||
const parseOrderByForShlink = ({ field, dir }: ShortUrlsOrder): ShlinkShortUrlsOrder => {
|
const parseOrderByForShlink = ({ field, dir }: ShortUrlsOrder): ShlinkShortUrlsOrder => {
|
||||||
if (supportsExcludeBotsOnShortUrls(selectedServer) && doExcludeBots && field === 'visits') {
|
if (supportsExcludingBots && doExcludeBots && field === 'visits') {
|
||||||
return { field: 'nonBotVisits', dir };
|
return { field: 'nonBotVisits', dir };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
import { ReactNode } from 'react';
|
|
||||||
import { isEmpty } from 'ramda';
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { SelectedServer } from '../servers/data';
|
import { isEmpty } from 'ramda';
|
||||||
import { ShortUrlsList as ShortUrlsListState } from './reducers/shortUrlsList';
|
import type { ReactNode } from 'react';
|
||||||
import { ShortUrlsRowType } from './helpers/ShortUrlsRow';
|
import type { SelectedServer } from '../servers/data';
|
||||||
import { ShortUrlsOrderableFields } from './data';
|
import type { ShortUrlsOrderableFields } from './data';
|
||||||
|
import type { ShortUrlsRowType } from './helpers/ShortUrlsRow';
|
||||||
|
import type { ShortUrlsList as ShortUrlsListState } from './reducers/shortUrlsList';
|
||||||
import './ShortUrlsTable.scss';
|
import './ShortUrlsTable.scss';
|
||||||
|
|
||||||
interface ShortUrlsTableProps {
|
interface ShortUrlsTableProps {
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
|
||||||
import { faInfoCircle as infoIcon } from '@fortawesome/free-solid-svg-icons';
|
import { faInfoCircle as infoIcon } from '@fortawesome/free-solid-svg-icons';
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import { Modal, ModalBody, ModalHeader } from 'reactstrap';
|
import { Modal, ModalBody, ModalHeader } from 'reactstrap';
|
||||||
import { useToggle } from '../utils/helpers/hooks';
|
import { useToggle } from '../utils/helpers/hooks';
|
||||||
import './UseExistingIfFoundInfoIcon.scss';
|
import './UseExistingIfFoundInfoIcon.scss';
|
||||||
|
|||||||
@ -1,8 +1,16 @@
|
|||||||
import { Nullable, OptionalString } from '../../utils/utils';
|
import type { ShlinkVisitsSummary } from '../../api/types';
|
||||||
import { Order } from '../../utils/helpers/ordering';
|
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 {
|
export interface EditShortUrlData {
|
||||||
longUrl?: string;
|
longUrl?: string;
|
||||||
|
deviceLongUrls?: DeviceLongUrls;
|
||||||
tags?: string[];
|
tags?: string[];
|
||||||
title?: string | null;
|
title?: string | null;
|
||||||
validSince?: Date | string | null;
|
validSince?: Date | string | null;
|
||||||
@ -30,10 +38,11 @@ export interface ShortUrl {
|
|||||||
shortCode: string;
|
shortCode: string;
|
||||||
shortUrl: string;
|
shortUrl: string;
|
||||||
longUrl: string;
|
longUrl: string;
|
||||||
|
deviceLongUrls?: Required<DeviceLongUrls>, // Optional only before Shlink 3.5.0
|
||||||
dateCreated: string;
|
dateCreated: string;
|
||||||
/** @deprecated */
|
/** @deprecated */
|
||||||
visitsCount: number; // Deprecated since Shlink 3.4.0
|
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>>;
|
meta: Required<Nullable<ShortUrlMeta>>;
|
||||||
tags: string[];
|
tags: string[];
|
||||||
domain: string | null;
|
domain: string | null;
|
||||||
@ -48,12 +57,6 @@ export interface ShortUrlMeta {
|
|||||||
maxVisits?: number;
|
maxVisits?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ShortUrlVisitsSummary {
|
|
||||||
total: number;
|
|
||||||
nonBots: number;
|
|
||||||
bots: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ShortUrlModalProps {
|
export interface ShortUrlModalProps {
|
||||||
shortUrl: ShortUrl;
|
shortUrl: ShortUrl;
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
|
|||||||
@ -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 { faTimes as closeIcon } from '@fortawesome/free-solid-svg-icons';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import CopyToClipboard from 'react-copy-to-clipboard';
|
import CopyToClipboard from 'react-copy-to-clipboard';
|
||||||
import { Tooltip } from 'reactstrap';
|
import { Tooltip } from 'reactstrap';
|
||||||
import { ShortUrlCreation } from '../reducers/shortUrlCreation';
|
|
||||||
import { TimeoutToggle } from '../../utils/helpers/hooks';
|
|
||||||
import { Result } from '../../utils/Result';
|
|
||||||
import './CreateShortUrlResult.scss';
|
|
||||||
import { ShlinkApiError } from '../../api/ShlinkApiError';
|
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 {
|
export interface CreateShortUrlResultProps {
|
||||||
creation: ShortUrlCreation;
|
creation: ShortUrlCreation;
|
||||||
|
|||||||
@ -1,12 +1,12 @@
|
|||||||
|
import { pipe } from 'ramda';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { Modal, ModalBody, ModalFooter, ModalHeader } from 'reactstrap';
|
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 { 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 {
|
interface DeleteShortUrlModalConnectProps extends ShortUrlModalProps {
|
||||||
shortUrlDeletion: ShortUrlDeletion;
|
shortUrlDeletion: ShortUrlDeletion;
|
||||||
|
|||||||
@ -1,10 +1,11 @@
|
|||||||
import { FC } from 'react';
|
import type { FC } 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 { ExportBtn } from '../../utils/ExportBtn';
|
||||||
import { useToggle } from '../../utils/helpers/hooks';
|
import { useToggle } from '../../utils/helpers/hooks';
|
||||||
import { ShlinkApiClientBuilder } from '../../api/services/ShlinkApiClientBuilder';
|
import type { ShortUrl } from '../data';
|
||||||
import { isServerWithId, SelectedServer } from '../../servers/data';
|
|
||||||
import { ShortUrl } from '../data';
|
|
||||||
import { ReportExporter } from '../../common/services/ReportExporter';
|
|
||||||
import { useShortUrlsQuery } from './hooks';
|
import { useShortUrlsQuery } from './hooks';
|
||||||
|
|
||||||
export interface ExportShortUrlsBtnProps {
|
export interface ExportShortUrlsBtnProps {
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user