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