Merge pull request #820 from shlinkio/develop

Release 3.10.0
This commit is contained in:
Alejandro Celaya 2023-03-19 12:00:45 +01:00 committed by GitHub
commit 457458a894
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
352 changed files with 4765 additions and 11421 deletions

View file

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

View file

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

1
.gitignore vendored
View file

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

View file

@ -4,6 +4,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*

View file

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

View file

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

View file

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

11254
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -12,25 +12,26 @@
"lint:fix": "npm run lint:css:fix && npm run lint:js:fix",
"lint:css:fix": "npm run lint:css -- --fix",
"lint:js:fix": "npm run lint:js -- --fix",
"types": "tsc",
"start": "vite serve --host=0.0.0.0",
"build": "tsc --noEmit && vite build && node scripts/replace-version.mjs",
"preview": "vite preview --host=0.0.0.0",
"build": "npm run types && vite build && node scripts/replace-version.mjs",
"build:dist": "npm run build && node scripts/create-dist-file.mjs",
"build:serve": "serve -p 5000 ./build",
"test": "jest --env=jsdom --colors",
"test:coverage": "npm run test -- --coverage --coverageReporters=text --coverageReporters=text-summary",
"test:ci": "npm run test:coverage -- --coverageReporters=clover --ci",
"test:pretty": "npm run test:coverage -- --coverageReporters=html",
"test:verbose": "npm run test -- --verbose",
"mutate": "./node_modules/.bin/stryker run --concurrency 4 --ignoreStatic"
"test:verbose": "npm run test -- --verbose"
},
"dependencies": {
"@babel/preset-env": "^7.20.2",
"@babel/preset-react": "^7.18.6",
"@babel/preset-typescript": "^7.18.6",
"@fortawesome/fontawesome-free": "^6.2.1",
"@fortawesome/fontawesome-svg-core": "^6.2.1",
"@fortawesome/free-regular-svg-icons": "^6.2.1",
"@fortawesome/free-solid-svg-icons": "^6.2.1",
"@babel/preset-typescript": "^7.21.0",
"@fortawesome/fontawesome-free": "^6.3.0",
"@fortawesome/fontawesome-svg-core": "^6.3.0",
"@fortawesome/free-brands-svg-icons": "^6.3.0",
"@fortawesome/free-regular-svg-icons": "^6.3.0",
"@fortawesome/free-solid-svg-icons": "^6.3.0",
"@fortawesome/react-fontawesome": "^0.2.0",
"@json2csv/plainjs": "^6.1.2",
"@reduxjs/toolkit": "^1.9.1",
@ -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%",

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,7 +1,7 @@
import { NormalizedVisit } from '../../visits/types';
import { ExportableShortUrl } from '../../short-urls/data';
import type { ExportableShortUrl } from '../../short-urls/data';
import type { JsonToCsv } from '../../utils/helpers/csvjson';
import { saveCsv } from '../../utils/helpers/files';
import { JsonToCsv } from '../../utils/helpers/csvjson';
import type { NormalizedVisit } from '../../visits/types';
export class ReportExporter {
public constructor(private readonly window: Window, private readonly jsonToCsv: JsonToCsv) {}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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