Updated to airbnb coding styles

This commit is contained in:
Alejandro Celaya 2022-03-26 12:17:42 +01:00
parent 4e9b19afd1
commit a2df486280
239 changed files with 2210 additions and 3549 deletions

View file

@ -1,25 +1,46 @@
{ {
"root": true,
"extends": [ "extends": [
"@shlinkio/js-coding-standard" "airbnb",
"airbnb-typescript",
"plugin:@typescript-eslint/recommended"
], ],
"plugins": ["jest"],
"env": {
"jest/globals": true
},
"parserOptions": { "parserOptions": {
"tsconfigRootDir": ".", "project": "./tsconfig.json"
"createDefaultProgram": true
},
"globals": {
"process": true,
"setImmediate": true
}, },
"ignorePatterns": ["src/service*.ts"], "ignorePatterns": ["src/service*.ts"],
"rules": { "rules": {
"complexity": "off", "object-curly-newline": "off",
"import/named": "off", "implicit-arrow-linebreak": "off",
"@typescript-eslint/no-unnecessary-type-assertion": "off", "no-restricted-globals": "off",
"@typescript-eslint/no-unsafe-return": "off", "default-case": "off",
"@typescript-eslint/no-unsafe-call": "off" "max-len": ["error", { "code": 120, "ignoreComments": true, "ignoreStrings": true }],
"import/no-cycle": "off",
"import/prefer-default-export": "off",
"import/no-extraneous-dependencies": "off",
"react/react-in-jsx-scope": "off",
"react/prop-types": "off",
"react/require-default-props": "off",
"react/function-component-definition": "off",
"react/no-array-index-key": "off",
"react/no-unstable-nested-components": "off",
"react/jsx-one-expression-per-line": "off",
"react/jsx-props-no-spreading": "off",
"react/jsx-no-useless-fragment": "off",
"@typescript-eslint/no-unused-expressions": "off",
"@typescript-eslint/ban-types": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/lines-between-class-members": "off"
},
"overrides": [
{
"files": ["*.test.*"],
"rules": {
"prefer-promise-reject-errors": "off",
"no-param-reassign": "off",
"react/no-children-prop": "off",
"@typescript-eslint/no-shadow": "off"
} }
} }
]
}

3297
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -66,7 +66,6 @@
"workbox-strategies": "^6.5.1" "workbox-strategies": "^6.5.1"
}, },
"devDependencies": { "devDependencies": {
"@shlinkio/eslint-config-js-coding-standard": "~1.2.2",
"@stryker-mutator/core": "^5.6.1", "@stryker-mutator/core": "^5.6.1",
"@stryker-mutator/jest-runner": "^5.6.1", "@stryker-mutator/jest-runner": "^5.6.1",
"@stryker-mutator/typescript-checker": "^5.6.1", "@stryker-mutator/typescript-checker": "^5.6.1",
@ -85,6 +84,8 @@
"@types/react-redux": "^7.1.23", "@types/react-redux": "^7.1.23",
"@types/react-tag-autocomplete": "^6.1.1", "@types/react-tag-autocomplete": "^6.1.1",
"@types/uuid": "^8.3.4", "@types/uuid": "^8.3.4",
"@typescript-eslint/eslint-plugin": "^5.16.0",
"@typescript-eslint/parser": "^5.16.0",
"@wojtekmaj/enzyme-adapter-react-17": "0.6.5", "@wojtekmaj/enzyme-adapter-react-17": "0.6.5",
"adm-zip": "^0.5.9", "adm-zip": "^0.5.9",
"babel-jest": "^27.5.1", "babel-jest": "^27.5.1",
@ -92,6 +93,8 @@
"dart-sass": "^1.25.0", "dart-sass": "^1.25.0",
"enzyme": "^3.11.0", "enzyme": "^3.11.0",
"eslint": "^7.13.0", "eslint": "^7.13.0",
"eslint-config-airbnb": "^19.0.4",
"eslint-config-airbnb-typescript": "^16.1.4",
"identity-obj-proxy": "^3.0.0", "identity-obj-proxy": "^3.0.0",
"jest": "^27.5.1", "jest": "^27.5.1",
"react-scripts": "^5.0.0", "react-scripts": "^5.0.0",

View file

@ -9,8 +9,10 @@ export interface ShlinkApiErrorProps {
export const ShlinkApiError = ({ errorData, fallbackMessage }: ShlinkApiErrorProps) => ( export const ShlinkApiError = ({ errorData, fallbackMessage }: ShlinkApiErrorProps) => (
<> <>
{errorData?.detail ?? fallbackMessage} {errorData?.detail ?? fallbackMessage}
{isInvalidArgumentError(errorData) && {isInvalidArgumentError(errorData) && (
<p className="mb-0">Invalid elements: [{errorData.invalidElements.join(', ')}]</p> <p className="mb-0">
} Invalid elements: [{errorData.invalidElements.join(', ')}]
</p>
)}
</> </>
); );

View file

@ -21,7 +21,7 @@ import {
import { stringifyQuery } from '../../utils/helpers/query'; import { stringifyQuery } from '../../utils/helpers/query';
import { orderToString } from '../../utils/helpers/ordering'; import { orderToString } from '../../utils/helpers/ordering';
const buildShlinkBaseUrl = (url: string) => url ? `${url}/rest/v2` : ''; const buildShlinkBaseUrl = (url: string) => (url ? `${url}/rest/v2` : '');
const rejectNilProps = reject(isNil); const rejectNilProps = reject(isNil);
const normalizeOrderByInParams = (params: ShlinkShortUrlsListParams): ShlinkShortUrlsListNormalizedParams => { const normalizeOrderByInParams = (params: ShlinkShortUrlsListParams): ShlinkShortUrlsListNormalizedParams => {
const { orderBy = {}, ...rest } = params; const { orderBy = {}, ...rest } = params;
@ -91,10 +91,9 @@ export default class ShlinkApiClient {
public readonly updateShortUrl = async ( public readonly updateShortUrl = async (
shortCode: string, shortCode: string,
domain: OptionalString, domain: OptionalString,
data: ShlinkShortUrlData, edit: ShlinkShortUrlData,
): Promise<ShortUrl> => ): Promise<ShortUrl> =>
this.performRequest<ShortUrl>(`/short-urls/${shortCode}`, 'PATCH', { domain }, data) this.performRequest<ShortUrl>(`/short-urls/${shortCode}`, 'PATCH', { domain }, edit).then(({ data }) => data);
.then(({ data }) => data);
public readonly listTags = async (): Promise<ShlinkTags> => public readonly listTags = async (): Promise<ShlinkTags> =>
this.performRequest<{ tags: ShlinkTagsResponse }>('/tags', 'GET', { withStats: 'true' }) this.performRequest<{ tags: ShlinkTagsResponse }>('/tags', 'GET', { withStats: 'true' })

View file

@ -23,7 +23,7 @@ const App = (
MenuLayout: FC, MenuLayout: FC,
CreateServer: FC, CreateServer: FC,
EditServer: FC, EditServer: FC,
Settings: FC, SettingsComp: FC,
ManageServers: FC, ManageServers: FC,
ShlinkVersionsContainer: FC, ShlinkVersionsContainer: FC,
) => ({ fetchServers, servers, settings, appUpdated, resetAppUpdate }: AppProps) => { ) => ({ fetchServers, servers, settings, appUpdated, resetAppUpdate }: AppProps) => {
@ -47,7 +47,7 @@ const App = (
<div className={classNames('shlink-wrapper', { 'd-flex d-md-block align-items-center': isHome })}> <div className={classNames('shlink-wrapper', { 'd-flex d-md-block align-items-center': isHome })}>
<Routes> <Routes>
<Route index element={<Home />} /> <Route index element={<Home />} />
<Route path="/settings/*" element={<Settings />} /> <Route path="/settings/*" element={<SettingsComp />} />
<Route path="/manage-servers" element={<ManageServers />} /> <Route path="/manage-servers" element={<ManageServers />} />
<Route path="/server/create" element={<CreateServer />} /> <Route path="/server/create" element={<CreateServer />} />
<Route path="/server/:serverId/edit" element={<EditServer />} /> <Route path="/server/:serverId/edit" element={<EditServer />} />

View file

@ -1,10 +1,8 @@
import { Action } from 'redux'; import { Action } from 'redux';
import { buildActionCreator, buildReducer } from '../../utils/helpers/redux'; import { buildActionCreator, buildReducer } from '../../utils/helpers/redux';
/* eslint-disable padding-line-between-statements */
export const APP_UPDATE_AVAILABLE = 'shlink/appUpdates/APP_UPDATE_AVAILABLE'; export const APP_UPDATE_AVAILABLE = 'shlink/appUpdates/APP_UPDATE_AVAILABLE';
export const RESET_APP_UPDATE = 'shlink/appUpdates/RESET_APP_UPDATE'; export const RESET_APP_UPDATE = 'shlink/appUpdates/RESET_APP_UPDATE';
/* eslint-enable padding-line-between-statements */
const initialState = false; const initialState = false;

View file

@ -17,7 +17,6 @@ import './AsideMenu.scss';
export interface AsideMenuProps { export interface AsideMenuProps {
selectedServer: SelectedServer; selectedServer: SelectedServer;
className?: string;
showOnMobile?: boolean; showOnMobile?: boolean;
} }

View file

@ -6,7 +6,7 @@ interface ErrorHandlerState {
hasError: boolean; hasError: boolean;
} }
const ErrorHandler = ( const ErrorHandlerCreator = (
{ location }: Window, { location }: Window,
{ error }: Console, { error }: Console,
) => class ErrorHandler extends Component<any, ErrorHandlerState> { ) => class ErrorHandler extends Component<any, ErrorHandlerState> {
@ -26,7 +26,8 @@ const ErrorHandler = (
} }
public render(): ReactNode { public render(): ReactNode {
if (this.state.hasError) { const { hasError } = this.state;
if (hasError) {
return ( return (
<div className="home"> <div className="home">
<SimpleCard className="p-4"> <SimpleCard className="p-4">
@ -39,8 +40,9 @@ const ErrorHandler = (
); );
} }
return this.props.children; const { children } = this.props;
return children;
} }
}; };
export default ErrorHandler; export default ErrorHandlerCreator;

View file

@ -22,7 +22,6 @@ const Home = ({ servers }: HomeProps) => {
useEffect(() => { useEffect(() => {
// Try to redirect to the first server marked as auto-connect // Try to redirect to the first server marked as auto-connect
const autoConnectServer = serversList.find(({ autoConnect }) => autoConnect); const autoConnectServer = serversList.find(({ autoConnect }) => autoConnect);
autoConnectServer && navigate(`/server/${autoConnectServer.id}`); autoConnectServer && navigate(`/server/${autoConnectServer.id}`);
}, []); }, []);

View file

@ -22,9 +22,9 @@ const ShlinkVersions = ({ selectedServer, clientVersion = SHLINK_WEB_CLIENT_VERS
return ( return (
<small className="text-muted"> <small className="text-muted">
{isReachableServer(selectedServer) && {isReachableServer(selectedServer) && (
<>Server: <VersionLink project="shlink" version={selectedServer.printableVersion} /> - </> <>Server: <VersionLink project="shlink" version={selectedServer.printableVersion} /> - </>
} )}
Client: <VersionLink project="shlink-web-client" version={normalizedClientVersion} /> Client: <VersionLink project="shlink-web-client" version={normalizedClientVersion} />
</small> </small>
); );

View file

@ -1,10 +1,8 @@
import { Action } from 'redux'; import { Action } from 'redux';
import { buildActionCreator, buildReducer } from '../../utils/helpers/redux'; import { buildActionCreator, buildReducer } from '../../utils/helpers/redux';
/* eslint-disable padding-line-between-statements */
export const SIDEBAR_PRESENT = 'shlink/common/SIDEBAR_PRESENT'; export const SIDEBAR_PRESENT = 'shlink/common/SIDEBAR_PRESENT';
export const SIDEBAR_NOT_PRESENT = 'shlink/common/SIDEBAR_NOT_PRESENT'; export const SIDEBAR_NOT_PRESENT = 'shlink/common/SIDEBAR_NOT_PRESENT';
/* eslint-enable padding-line-between-statements */
export interface Sidebar { export interface Sidebar {
sidebarPresent: boolean; sidebarPresent: boolean;

View file

@ -20,8 +20,8 @@ const bottle = new Bottle();
export const { container } = bottle; export const { container } = bottle;
const lazyService = <T extends Function, K>(container: IContainer, serviceName: string) => const lazyService = <T extends Function, K>(cont: IContainer, serviceName: string) =>
(...args: any[]) => (container[serviceName] as T)(...args) as K; (...args: any[]) => (cont[serviceName] as T)(...args) as K;
const mapActionService = (map: LazyActionMap, actionName: string): LazyActionMap => ({ const mapActionService = (map: LazyActionMap, actionName: string): LazyActionMap => ({
...map, ...map,
// Wrap actual action service in a function so that it is lazily created the first time it is called // Wrap actual action service in a function so that it is lazily created the first time it is called

View file

@ -6,7 +6,7 @@ import { migrateDeprecatedSettings } from '../settings/helpers';
import { ShlinkState } from './types'; import { ShlinkState } from './types';
const isProduction = process.env.NODE_ENV !== 'production'; const isProduction = process.env.NODE_ENV !== 'production';
const composeEnhancers: Function = !isProduction && (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; const composeEnhancers: Function = !isProduction ? (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ : compose;
const localStorageConfig: RLSOptions = { const localStorageConfig: RLSOptions = {
states: ['settings', 'servers'], states: ['settings', 'servers'],

View file

@ -56,7 +56,7 @@ export const DomainSelector = ({ listDomains, value, domainsList, onChange }: Do
{domains.map(({ domain, isDefault }) => ( {domains.map(({ domain, isDefault }) => (
<DropdownItem <DropdownItem
key={domain} key={domain}
active={value === domain || isDefault && valueIsEmpty} active={(value === domain || isDefault) && valueIsEmpty}
onClick={() => onChange(domain)} onClick={() => onChange(domain)}
> >
{domain} {domain}

View file

@ -5,11 +5,9 @@ import { GetState } from '../../container/types';
import { ApiErrorAction } from '../../api/types/actions'; import { ApiErrorAction } from '../../api/types/actions';
import { parseApiError } from '../../api/utils'; import { parseApiError } from '../../api/utils';
/* eslint-disable padding-line-between-statements */
export const EDIT_DOMAIN_REDIRECTS_START = 'shlink/domainRedirects/EDIT_DOMAIN_REDIRECTS_START'; export const EDIT_DOMAIN_REDIRECTS_START = 'shlink/domainRedirects/EDIT_DOMAIN_REDIRECTS_START';
export const EDIT_DOMAIN_REDIRECTS_ERROR = 'shlink/domainRedirects/EDIT_DOMAIN_REDIRECTS_ERROR'; export const EDIT_DOMAIN_REDIRECTS_ERROR = 'shlink/domainRedirects/EDIT_DOMAIN_REDIRECTS_ERROR';
export const EDIT_DOMAIN_REDIRECTS = 'shlink/domainRedirects/EDIT_DOMAIN_REDIRECTS'; export const EDIT_DOMAIN_REDIRECTS = 'shlink/domainRedirects/EDIT_DOMAIN_REDIRECTS';
/* eslint-enable padding-line-between-statements */
export interface EditDomainRedirectsAction extends Action<string> { export interface EditDomainRedirectsAction extends Action<string> {
domain: string; domain: string;
@ -21,10 +19,10 @@ export const editDomainRedirects = (buildShlinkApiClient: ShlinkApiClientBuilder
domainRedirects: Partial<ShlinkDomainRedirects>, domainRedirects: Partial<ShlinkDomainRedirects>,
) => async (dispatch: Dispatch, getState: GetState) => { ) => async (dispatch: Dispatch, getState: GetState) => {
dispatch({ type: EDIT_DOMAIN_REDIRECTS_START }); dispatch({ type: EDIT_DOMAIN_REDIRECTS_START });
const { editDomainRedirects } = buildShlinkApiClient(getState); const { editDomainRedirects: shlinkEditDomainRedirects } = buildShlinkApiClient(getState);
try { try {
const redirects = await editDomainRedirects({ domain, ...domainRedirects }); const redirects = await shlinkEditDomainRedirects({ domain, ...domainRedirects });
dispatch<EditDomainRedirectsAction>({ type: EDIT_DOMAIN_REDIRECTS, domain, redirects }); dispatch<EditDomainRedirectsAction>({ type: EDIT_DOMAIN_REDIRECTS, domain, redirects });
} catch (e: any) { } catch (e: any) {

View file

@ -10,13 +10,11 @@ import { hasServerData } from '../../servers/data';
import { replaceAuthorityFromUri } from '../../utils/helpers/uri'; import { replaceAuthorityFromUri } from '../../utils/helpers/uri';
import { EDIT_DOMAIN_REDIRECTS, EditDomainRedirectsAction } from './domainRedirects'; import { EDIT_DOMAIN_REDIRECTS, EditDomainRedirectsAction } from './domainRedirects';
/* eslint-disable padding-line-between-statements */
export const LIST_DOMAINS_START = 'shlink/domainsList/LIST_DOMAINS_START'; export const LIST_DOMAINS_START = 'shlink/domainsList/LIST_DOMAINS_START';
export const LIST_DOMAINS_ERROR = 'shlink/domainsList/LIST_DOMAINS_ERROR'; export const LIST_DOMAINS_ERROR = 'shlink/domainsList/LIST_DOMAINS_ERROR';
export const LIST_DOMAINS = 'shlink/domainsList/LIST_DOMAINS'; export const LIST_DOMAINS = 'shlink/domainsList/LIST_DOMAINS';
export const FILTER_DOMAINS = 'shlink/domainsList/FILTER_DOMAINS'; export const FILTER_DOMAINS = 'shlink/domainsList/FILTER_DOMAINS';
export const VALIDATE_DOMAIN = 'shlink/domainsList/VALIDATE_DOMAIN'; export const VALIDATE_DOMAIN = 'shlink/domainsList/VALIDATE_DOMAIN';
/* eslint-enable padding-line-between-statements */
export interface DomainsList { export interface DomainsList {
domains: Domain[]; domains: Domain[];
@ -55,10 +53,10 @@ export type DomainsCombinedAction = ListDomainsAction
& ValidateDomain; & ValidateDomain;
export const replaceRedirectsOnDomain = (domain: string, redirects: ShlinkDomainRedirects) => export const replaceRedirectsOnDomain = (domain: string, redirects: ShlinkDomainRedirects) =>
(d: Domain): Domain => d.domain !== domain ? d : { ...d, redirects }; (d: Domain): Domain => (d.domain !== domain ? d : { ...d, redirects });
export const replaceStatusOnDomain = (domain: string, status: DomainStatus) => export const replaceStatusOnDomain = (domain: string, status: DomainStatus) =>
(d: Domain): Domain => d.domain !== domain ? d : { ...d, status }; (d: Domain): Domain => (d.domain !== domain ? d : { ...d, status });
export default buildReducer<DomainsList, DomainsCombinedAction>({ export default buildReducer<DomainsList, DomainsCombinedAction>({
[LIST_DOMAINS_START]: () => ({ ...initialState, loading: true }), [LIST_DOMAINS_START]: () => ({ ...initialState, loading: true }),
@ -86,15 +84,15 @@ export const listDomains = (buildShlinkApiClient: ShlinkApiClientBuilder) => ()
getState: GetState, getState: GetState,
) => { ) => {
dispatch({ type: LIST_DOMAINS_START }); dispatch({ type: LIST_DOMAINS_START });
const { listDomains } = buildShlinkApiClient(getState); const { listDomains: shlinkListDomains } = buildShlinkApiClient(getState);
try { try {
const { domains, defaultRedirects } = await listDomains().then(({ data, defaultRedirects }) => ({ const resp = await shlinkListDomains().then(({ data, defaultRedirects }) => ({
domains: data.map((domain): Domain => ({ ...domain, status: 'validating' })), domains: data.map((domain): Domain => ({ ...domain, status: 'validating' })),
defaultRedirects, defaultRedirects,
})); }));
dispatch<ListDomainsAction>({ type: LIST_DOMAINS, domains, defaultRedirects }); dispatch<ListDomainsAction>({ type: LIST_DOMAINS, ...resp });
} catch (e: any) { } catch (e: any) {
dispatch<ApiErrorAction>({ type: LIST_DOMAINS_ERROR, errorData: parseApiError(e) }); dispatch<ApiErrorAction>({ type: LIST_DOMAINS_ERROR, errorData: parseApiError(e) });
} }

View file

@ -23,7 +23,7 @@ export function boundToMercureHub<T = {}>(
const params = useParams(); const params = useParams();
useEffect(() => { useEffect(() => {
const onMessage = (visit: CreateVisit) => interval ? pendingUpdates.add(visit) : createNewVisits([ visit ]); const onMessage = (visit: CreateVisit) => (interval ? pendingUpdates.add(visit) : createNewVisits([visit]));
const topics = getTopicsForProps(props, params); const topics = getTopicsForProps(props, params);
const closeEventSource = bindToMercureTopic(mercureInfo, topics, onMessage, loadMercureInfo); const closeEventSource = bindToMercureTopic(mercureInfo, topics, onMessage, loadMercureInfo);

View file

@ -4,11 +4,9 @@ import { GetState } from '../../container/types';
import { buildReducer } from '../../utils/helpers/redux'; import { buildReducer } from '../../utils/helpers/redux';
import { ShlinkApiClientBuilder } from '../../api/services/ShlinkApiClientBuilder'; import { ShlinkApiClientBuilder } from '../../api/services/ShlinkApiClientBuilder';
/* eslint-disable padding-line-between-statements */
export const GET_MERCURE_INFO_START = 'shlink/mercure/GET_MERCURE_INFO_START'; export const GET_MERCURE_INFO_START = 'shlink/mercure/GET_MERCURE_INFO_START';
export const GET_MERCURE_INFO_ERROR = 'shlink/mercure/GET_MERCURE_INFO_ERROR'; export const GET_MERCURE_INFO_ERROR = 'shlink/mercure/GET_MERCURE_INFO_ERROR';
export const GET_MERCURE_INFO = 'shlink/mercure/GET_MERCURE_INFO'; export const GET_MERCURE_INFO = 'shlink/mercure/GET_MERCURE_INFO';
/* eslint-enable padding-line-between-statements */
export interface MercureInfo { export interface MercureInfo {
token?: string; token?: string;

View file

@ -58,8 +58,9 @@ const CreateServer = (ImportServersBtn: FC<ImportServersBtnProps>, useStateFlagT
return ( return (
<NoMenuLayout> <NoMenuLayout>
<ServerForm title={<h5 className="mb-0">Add new server</h5>} onSubmit={setServerData}> <ServerForm title={<h5 className="mb-0">Add new server</h5>} onSubmit={setServerData}>
{!hasServers && {!hasServers && (
<ImportServersBtn tooltipPlacement="top" onImport={setServersImported} onImportError={setErrorImporting} />} <ImportServersBtn tooltipPlacement="top" onImport={setServersImported} onImportError={setErrorImporting} />
)}
{hasServers && <Button outline onClick={goBack}>Cancel</Button>} {hasServers && <Button outline onClick={goBack}>Cancel</Button>}
<Button outline color="primary" className="ms-2">Create server</Button> <Button outline color="primary" className="ms-2">Create server</Button>
</ServerForm> </ServerForm>

View file

@ -1,5 +1,5 @@
import { FC } from 'react'; import { FC } from 'react';
import { Modal, ModalBody, ModalFooter, ModalHeader } from 'reactstrap'; import { Button, Modal, ModalBody, ModalFooter, ModalHeader } from 'reactstrap';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { ServerWithId } from './data'; import { ServerWithId } from './data';
@ -37,8 +37,8 @@ const DeleteServerModal: FC<DeleteServerModalConnectProps> = (
</p> </p>
</ModalBody> </ModalBody>
<ModalFooter> <ModalFooter>
<button className="btn btn-link" onClick={toggle}>Cancel</button> <Button color="link" onClick={toggle}>Cancel</Button>
<button className="btn btn-danger" onClick={() => closeModal()}>Delete</button> <Button color="danger" onClick={() => closeModal()}>Delete</Button>
</ModalFooter> </ModalFooter>
</Modal> </Modal>
); );

View file

@ -61,17 +61,17 @@ export const ManageServers = (
<table className="table table-hover responsive-table mb-0"> <table className="table table-hover responsive-table mb-0">
<thead className="responsive-table__header"> <thead className="responsive-table__header">
<tr> <tr>
{hasAutoConnect && <th style={{ width: '50px' }} />} {hasAutoConnect && <th aria-label="Auto-connect" style={{ width: '50px' }} />}
<th>Name</th> <th>Name</th>
<th>Base URL</th> <th>Base URL</th>
<th /> <th aria-label="Options" />
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{!serversList.length && <tr className="text-center"><td colSpan={4}>No servers found.</td></tr>} {!serversList.length && <tr className="text-center"><td colSpan={4}>No servers found.</td></tr>}
{serversList.map((server) => {serversList.map((server) => (
<ManageServersRow key={server.id} server={server} hasAutoConnect={hasAutoConnect} />) <ManageServersRow key={server.id} server={server} hasAutoConnect={hasAutoConnect} />
} ))}
</tbody> </tbody>
</table> </table>
</SimpleCard> </SimpleCard>

View file

@ -35,15 +35,15 @@ export const hasServerData = (server: SelectedServer | ServerData): server is Se
!!(server as ServerData)?.url && !!(server as ServerData)?.apiKey; !!(server as ServerData)?.url && !!(server as ServerData)?.apiKey;
export const isServerWithId = (server: SelectedServer | ServerWithId): server is ServerWithId => export const isServerWithId = (server: SelectedServer | ServerWithId): server is ServerWithId =>
!!server?.hasOwnProperty('id'); !!(server as ServerWithId)?.id;
export const isReachableServer = (server: SelectedServer): server is ReachableServer => export const isReachableServer = (server: SelectedServer): server is ReachableServer =>
!!server?.hasOwnProperty('version'); !!(server as ReachableServer)?.version;
export const isNotFoundServer = (server: SelectedServer): server is NotFoundServer => export const isNotFoundServer = (server: SelectedServer): server is NotFoundServer =>
!!server?.hasOwnProperty('serverNotFound'); !!(server as NotFoundServer)?.serverNotFound;
export const getServerId = (server: SelectedServer) => isServerWithId(server) ? server.id : ''; export const getServerId = (server: SelectedServer) => (isServerWithId(server) ? server.id : '');
export const serverWithIdToServerData = (server: ServerWithId): ServerData => export const serverWithIdToServerData = (server: ServerWithId): ServerData =>
omit<ServerWithId, 'id' | 'autoConnect'>(['id', 'autoConnect'], server); omit<ServerWithId, 'id' | 'autoConnect'>(['id', 'autoConnect'], server);

View file

@ -20,12 +20,12 @@ export const DuplicatedServersModal: FC<DuplicatedServersModalProps> = (
<ModalBody> <ModalBody>
<p>{hasMultipleServers ? 'The next servers already exist:' : 'There is already a server with:'}</p> <p>{hasMultipleServers ? 'The next servers already exist:' : 'There is already a server with:'}</p>
<ul> <ul>
{duplicatedServers.map(({ url, apiKey }, index) => !hasMultipleServers ? ( {duplicatedServers.map(({ url, apiKey }, index) => (!hasMultipleServers ? (
<Fragment key={index}> <Fragment key={index}>
<li>URL: <b>{url}</b></li> <li>URL: <b>{url}</b></li>
<li>API key: <b>{apiKey}</b></li> <li>API key: <b>{apiKey}</b></li>
</Fragment> </Fragment>
) : <li key={index}><b>{url}</b> - <b>{apiKey}</b></li>)} ) : <li key={index}><b>{url}</b> - <b>{apiKey}</b></li>))}
</ul> </ul>
<span> <span>
{hasMultipleServers ? 'Do you want to ignore duplicated servers' : 'Do you want to save this server anyway'}? {hasMultipleServers ? 'Do you want to ignore duplicated servers' : 'Do you want to save this server anyway'}?

View file

@ -10,7 +10,7 @@ export interface HighlightCardProps {
link?: string | false; link?: string | false;
} }
const buildExtraProps = (link?: string | false) => !link ? {} : { tag: Link, to: link }; const buildExtraProps = (link?: string | false) => (!link ? {} : { tag: Link, to: link });
export const HighlightCard: FC<HighlightCardProps> = ({ children, title, link }) => ( export const HighlightCard: FC<HighlightCardProps> = ({ children, title, link }) => (
<Card className="highlight-card" body {...buildExtraProps(link)}> <Card className="highlight-card" body {...buildExtraProps(link)}>

View file

@ -1,4 +1,4 @@
import { useRef, RefObject, ChangeEvent, MutableRefObject, FC, useState, useEffect } from 'react'; import { useRef, RefObject, ChangeEvent, MutableRefObject, useState, useEffect, FC } from 'react';
import { Button, UncontrolledTooltip } from 'reactstrap'; import { Button, UncontrolledTooltip } from 'reactstrap';
import { complement, pipe } from 'ramda'; import { complement, pipe } from 'ramda';
import { faFileUpload as importIcon } from '@fortawesome/free-solid-svg-icons'; import { faFileUpload as importIcon } from '@fortawesome/free-solid-svg-icons';
@ -52,7 +52,7 @@ const ImportServersBtn = ({ importServersFromFile }: ServersImporter): FC<Import
.then(setServersToCreate) .then(setServersToCreate)
.then(() => { .then(() => {
// Reset input after processing file // Reset input after processing file
(target as { value: string | null }).value = null; (target as { value: string | null }).value = null; // eslint-disable-line no-param-reassign
}) })
.catch(onImportError); .catch(onImportError);
@ -62,10 +62,10 @@ const ImportServersBtn = ({ importServersFromFile }: ServersImporter): FC<Import
} }
const existingServers = Object.values(servers); const existingServers = Object.values(servers);
const duplicatedServers = serversToCreate.filter(serversFiltering(existingServers)); const dupServers = serversToCreate.filter(serversFiltering(existingServers));
const hasDuplicatedServers = !!duplicatedServers.length; const hasDuplicatedServers = !!dupServers.length;
!hasDuplicatedServers ? create(serversToCreate) : setDuplicatedServers(duplicatedServers); !hasDuplicatedServers ? create(serversToCreate) : setDuplicatedServers(dupServers);
hasDuplicatedServers && showModal(); hasDuplicatedServers && showModal();
}, [serversToCreate]); }, [serversToCreate]);

View file

@ -6,8 +6,9 @@ interface WithoutSelectedServerProps {
export function withoutSelectedServer<T = {}>(WrappedComponent: FC<WithoutSelectedServerProps & T>) { export function withoutSelectedServer<T = {}>(WrappedComponent: FC<WithoutSelectedServerProps & T>) {
return (props: WithoutSelectedServerProps & T) => { return (props: WithoutSelectedServerProps & T) => {
const { resetSelectedServer } = props;
useEffect(() => { useEffect(() => {
props.resetSelectedServer(); resetSelectedServer();
}, []); }, []);
return <WrappedComponent {...props} />; return <WrappedComponent {...props} />;

View file

@ -7,7 +7,7 @@ import { createServers } from './servers';
const responseToServersList = pipe( const responseToServersList = pipe(
prop<any, any>('data'), prop<any, any>('data'),
(data: any): ServerData[] => Array.isArray(data) ? data.filter(hasServerData) : [], (data: any): ServerData[] => (Array.isArray(data) ? data.filter(hasServerData) : []),
); );
export const fetchServers = ({ get }: AxiosInstance) => () => async (dispatch: Dispatch) => { export const fetchServers = ({ get }: AxiosInstance) => () => async (dispatch: Dispatch) => {

View file

@ -7,21 +7,19 @@ import { ShlinkHealth } from '../../api/types';
import { buildActionCreator, buildReducer } from '../../utils/helpers/redux'; import { buildActionCreator, buildReducer } from '../../utils/helpers/redux';
import { ShlinkApiClientBuilder } from '../../api/services/ShlinkApiClientBuilder'; import { ShlinkApiClientBuilder } from '../../api/services/ShlinkApiClientBuilder';
/* eslint-disable padding-line-between-statements */
export const SELECT_SERVER = 'shlink/selectedServer/SELECT_SERVER'; export const SELECT_SERVER = 'shlink/selectedServer/SELECT_SERVER';
export const RESET_SELECTED_SERVER = 'shlink/selectedServer/RESET_SELECTED_SERVER'; export const RESET_SELECTED_SERVER = 'shlink/selectedServer/RESET_SELECTED_SERVER';
export const MIN_FALLBACK_VERSION = '1.0.0'; export const MIN_FALLBACK_VERSION = '1.0.0';
export const MAX_FALLBACK_VERSION = '999.999.999'; export const MAX_FALLBACK_VERSION = '999.999.999';
export const LATEST_VERSION_CONSTRAINT = 'latest'; export const LATEST_VERSION_CONSTRAINT = 'latest';
/* eslint-enable padding-line-between-statements */
export interface SelectServerAction extends Action<string> { export interface SelectServerAction extends Action<string> {
selectedServer: SelectedServer; selectedServer: SelectedServer;
} }
const versionToSemVer = pipe( const versionToSemVer = pipe(
(version: string) => version === LATEST_VERSION_CONSTRAINT ? MAX_FALLBACK_VERSION : version, (version: string) => (version === LATEST_VERSION_CONSTRAINT ? MAX_FALLBACK_VERSION : version),
toSemVer(MIN_FALLBACK_VERSION), toSemVer(MIN_FALLBACK_VERSION),
); );

View file

@ -4,12 +4,10 @@ import { Action } from 'redux';
import { ServerData, ServersMap, ServerWithId } from '../data'; import { ServerData, ServersMap, ServerWithId } from '../data';
import { buildReducer } from '../../utils/helpers/redux'; import { buildReducer } from '../../utils/helpers/redux';
/* eslint-disable padding-line-between-statements */
export const EDIT_SERVER = 'shlink/servers/EDIT_SERVER'; export const EDIT_SERVER = 'shlink/servers/EDIT_SERVER';
export const DELETE_SERVER = 'shlink/servers/DELETE_SERVER'; export const DELETE_SERVER = 'shlink/servers/DELETE_SERVER';
export const CREATE_SERVERS = 'shlink/servers/CREATE_SERVERS'; export const CREATE_SERVERS = 'shlink/servers/CREATE_SERVERS';
export const SET_AUTO_CONNECT = 'shlink/servers/SET_AUTO_CONNECT'; export const SET_AUTO_CONNECT = 'shlink/servers/SET_AUTO_CONNECT';
/* eslint-enable padding-line-between-statements */
export interface CreateServersAction extends Action<string> { export interface CreateServersAction extends Action<string> {
newServers: ServersMap; newServers: ServersMap;
@ -37,9 +35,9 @@ const serverWithId = (server: ServerWithId | ServerData): ServerWithId => {
export default buildReducer<ServersMap, CreateServersAction & DeleteServerAction & SetAutoConnectAction>({ export default buildReducer<ServersMap, CreateServersAction & DeleteServerAction & SetAutoConnectAction>({
[CREATE_SERVERS]: (state, { newServers }) => ({ ...state, ...newServers }), [CREATE_SERVERS]: (state, { newServers }) => ({ ...state, ...newServers }),
[DELETE_SERVER]: (state, { serverId }) => dissoc(serverId, state), [DELETE_SERVER]: (state, { serverId }) => dissoc(serverId, state),
[EDIT_SERVER]: (state, { serverId, serverData }: any) => !state[serverId] [EDIT_SERVER]: (state, { serverId, serverData }: any) => (
? state !state[serverId] ? state : assoc(serverId, { ...state[serverId], ...serverData }, state)
: assoc(serverId, { ...state[serverId], ...serverData }, state), ),
[SET_AUTO_CONNECT]: (state, { serverId, autoConnect }) => { [SET_AUTO_CONNECT]: (state, { serverId, autoConnect }) => {
if (!state[serverId]) { if (!state[serverId]) {
return state; return state;

View file

@ -29,8 +29,8 @@ export class ServersImporter {
} }
resolve(servers); resolve(servers);
} catch (e) { } catch (error) {
reject(e); reject(error);
} }
}); });
reader.readAsText(file); reader.readAsText(file);

View file

@ -1,5 +1,4 @@
/// <reference lib="webworker" /> /// <reference lib="webworker" />
/* eslint-disable no-restricted-globals */
// This service worker can be customized! // This service worker can be customized!
// See https://developers.google.com/web/tools/workbox/modules // See https://developers.google.com/web/tools/workbox/modules

View file

@ -12,7 +12,7 @@ interface RealTimeUpdatesProps {
setRealTimeUpdatesInterval: (interval: number) => void; setRealTimeUpdatesInterval: (interval: number) => void;
} }
const intervalValue = (interval?: number) => !interval ? '' : `${interval}`; const intervalValue = (interval?: number) => (!interval ? '' : `${interval}`);
const RealTimeUpdatesSettings = ( const RealTimeUpdatesSettings = (
{ settings: { realTimeUpdates }, toggleRealTimeUpdates, setRealTimeUpdatesInterval }: RealTimeUpdatesProps, { settings: { realTimeUpdates }, toggleRealTimeUpdates, setRealTimeUpdatesInterval }: RealTimeUpdatesProps,

View file

@ -25,9 +25,9 @@ const Settings = (
</NavPills> </NavPills>
<Routes> <Routes>
<Route path="general" element={<SettingsSections items={[ <UserInterface key="one" />, <RealTimeUpdates key="two" /> ]} />} /> <Route path="general" element={<SettingsSections items={[<UserInterface />, <RealTimeUpdates />]} />} />
<Route path="short-urls" element={<SettingsSections items={[ <ShortUrlCreation key="one" />, <ShortUrlsList key="two" /> ]} />} /> <Route path="short-urls" element={<SettingsSections items={[<ShortUrlCreation />, <ShortUrlsList />]} />} />
<Route path="other-items" element={<SettingsSections items={[ <Tags key="one" />, <Visits key="two" /> ]} />} /> <Route path="other-items" element={<SettingsSections items={[<Tags />, <Visits />]} />} />
<Route path="*" element={<Navigate replace to="general" />} /> <Route path="*" element={<Navigate replace to="general" />} />
</Routes> </Routes>
</NoMenuLayout> </NoMenuLayout>

View file

@ -13,11 +13,12 @@ interface ShortUrlCreationProps {
} }
const tagFilteringModeText = (tagFilteringMode: TagFilteringMode | undefined): string => const tagFilteringModeText = (tagFilteringMode: TagFilteringMode | undefined): string =>
tagFilteringMode === 'includes' ? 'Suggest tags including input' : 'Suggest tags starting with input'; (tagFilteringMode === 'includes' ? 'Suggest tags including input' : 'Suggest tags starting with input');
const tagFilteringModeHint = (tagFilteringMode: TagFilteringMode | undefined): ReactNode => const tagFilteringModeHint = (tagFilteringMode: TagFilteringMode | undefined): ReactNode => (
tagFilteringMode === 'includes' tagFilteringMode === 'includes'
? <>The list of suggested tags will contain those <b>including</b> provided input.</> ? <>The list of suggested tags will contain those <b>including</b> provided input.</>
: <>The list of suggested tags will contain those <b>starting with</b> provided input.</>; : <>The list of suggested tags will contain those <b>starting with</b> provided input.</>
);
export const ShortUrlCreationSettings: FC<ShortUrlCreationProps> = ({ settings, setShortUrlCreationSettings }) => { export const ShortUrlCreationSettings: FC<ShortUrlCreationProps> = ({ settings, setShortUrlCreationSettings }) => {
const shortUrlCreation: ShortUrlsSettings = settings.shortUrlCreation ?? { validateUrls: false }; const shortUrlCreation: ShortUrlsSettings = settings.shortUrlCreation ?? { validateUrls: false };

View file

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

View file

@ -32,7 +32,7 @@ export interface ShortUrlFormProps {
} }
const normalizeTag = pipe(trim, replace(/ /g, '-')); const normalizeTag = pipe(trim, replace(/ /g, '-'));
const toDate = (date?: string | Date): Date | undefined => typeof date === 'string' ? parseISO(date) : date; const toDate = (date?: string | Date): Date | undefined => (typeof date === 'string' ? parseISO(date) : date);
const dynamicColClasses = (flag: boolean) => ({ 'col-sm-6': flag, 'col-sm-12': !flag }); const dynamicColClasses = (flag: boolean) => ({ 'col-sm-6': flag, 'col-sm-12': !flag });
export const ShortUrlForm = ( export const ShortUrlForm = (

View file

@ -29,7 +29,7 @@ export interface ShortUrlsFilteringProps {
shortUrlsAmount?: number; shortUrlsAmount?: number;
} }
const dateOrNull = (date?: string) => date ? parseISO(date) : null; const dateOrNull = (date?: string) => (date ? parseISO(date) : null);
const ShortUrlsFilteringBar = ( const ShortUrlsFilteringBar = (
colorGenerator: ColorGenerator, colorGenerator: ColorGenerator,
@ -37,15 +37,15 @@ const ShortUrlsFilteringBar = (
): FC<ShortUrlsFilteringProps> => ({ selectedServer, className, shortUrlsAmount, order, handleOrderBy }) => { ): FC<ShortUrlsFilteringProps> => ({ selectedServer, className, shortUrlsAmount, order, handleOrderBy }) => {
const [{ search, tags, startDate, endDate, tagsMode = 'any' }, toFirstPage] = useShortUrlsQuery(); const [{ search, tags, startDate, endDate, tagsMode = 'any' }, toFirstPage] = useShortUrlsQuery();
const setDates = pipe( const setDates = pipe(
({ startDate, endDate }: DateRange) => ({ ({ startDate: theStartDate, endDate: theEndDate }: DateRange) => ({
startDate: formatIsoDate(startDate) ?? undefined, startDate: formatIsoDate(theStartDate) ?? undefined,
endDate: formatIsoDate(endDate) ?? undefined, endDate: formatIsoDate(theEndDate) ?? undefined,
}), }),
toFirstPage, toFirstPage,
); );
const setSearch = pipe( const setSearch = pipe(
(searchTerm: string) => isEmpty(searchTerm) ? undefined : searchTerm, (searchTerm: string) => (isEmpty(searchTerm) ? undefined : searchTerm),
(search) => toFirstPage({ search }), (searchTerm) => toFirstPage({ search: searchTerm }),
); );
const removeTag = pipe( const removeTag = pipe(
(tag: string) => tags.filter((selectedTag) => selectedTag !== tag), (tag: string) => tags.filter((selectedTag) => selectedTag !== tag),
@ -53,8 +53,8 @@ const ShortUrlsFilteringBar = (
); );
const canChangeTagsMode = supportsAllTagsFiltering(selectedServer); const canChangeTagsMode = supportsAllTagsFiltering(selectedServer);
const toggleTagsMode = pipe( const toggleTagsMode = pipe(
() => tagsMode === 'any' ? 'all' : 'any', () => (tagsMode === 'any' ? 'all' : 'any'),
(tagsMode) => toFirstPage({ tagsMode }), (mode) => toFirstPage({ tagsMode: mode }),
); );
return ( return (

View file

@ -70,11 +70,11 @@ export const ShortUrlsTable = (ShortUrlsRow: FC<ShortUrlsRowProps>) => ({
<th className={orderableColumnsClasses} onClick={orderByColumn?.('shortCode')}> <th className={orderableColumnsClasses} onClick={orderByColumn?.('shortCode')}>
Short URL {renderOrderIcon?.('shortCode')} Short URL {renderOrderIcon?.('shortCode')}
</th> </th>
{!supportsTitle && ( {!supportsTitle ? (
<th className={orderableColumnsClasses} onClick={orderByColumn?.('longUrl')}> <th className={orderableColumnsClasses} onClick={orderByColumn?.('longUrl')}>
Long URL {renderOrderIcon?.('longUrl')} Long URL {renderOrderIcon?.('longUrl')}
</th> </th>
) || ( ) : (
<th className="short-urls-table__header-cell"> <th className="short-urls-table__header-cell">
<span className={actionableFieldClasses} onClick={orderByColumn?.('title')}> <span className={actionableFieldClasses} onClick={orderByColumn?.('title')}>
Title {renderOrderIcon?.('title')} Title {renderOrderIcon?.('title')}

View file

@ -69,8 +69,9 @@ const QrCodeModal = (imageDownloader: ImageDownloader, ForServerVersion: FC<Vers
</FormGroup> </FormGroup>
{capabilities.marginIsSupported && ( {capabilities.marginIsSupported && (
<FormGroup className={`d-grid ${willRenderThreeControls ? 'col-md-4' : 'col-md-6'}`}> <FormGroup className={`d-grid ${willRenderThreeControls ? 'col-md-4' : 'col-md-6'}`}>
<label>Margin: {margin}px</label> <label htmlFor="marginControl">Margin: {margin}px</label>
<input <input
id="marginControl"
type="range" type="range"
className="form-control-range" className="form-control-range"
value={margin} value={margin}
@ -101,7 +102,9 @@ const QrCodeModal = (imageDownloader: ImageDownloader, ForServerVersion: FC<Vers
<Button <Button
block block
color="primary" color="primary"
onClick={async () => imageDownloader.saveImage(qrCodeUrl, `${shortCode}-qr-code.${format}`)} onClick={() => {
imageDownloader.saveImage(qrCodeUrl, `${shortCode}-qr-code.${format}`).catch(() => {});
}}
> >
Download <FontAwesomeIcon icon={downloadIcon} className="ms-1" /> Download <FontAwesomeIcon icon={downloadIcon} className="ms-1" />
</Button> </Button>

View file

@ -8,11 +8,6 @@ import { TagsFilteringMode } from '../../api/types';
type ToFirstPage = (extra: Partial<ShortUrlsFiltering>) => void; type ToFirstPage = (extra: Partial<ShortUrlsFiltering>) => void;
export interface ShortUrlListRouteParams {
page: string;
serverId: string;
}
interface ShortUrlsQueryCommon { interface ShortUrlsQueryCommon {
search?: string; search?: string;
startDate?: string; startDate?: string;
@ -57,7 +52,7 @@ export const useShortUrlsQuery = (): [ShortUrlsFiltering, ToFirstPage] => {
const evolvedQuery = stringifyQuery(normalizedQuery); const evolvedQuery = stringifyQuery(normalizedQuery);
const queryString = isEmpty(evolvedQuery) ? '' : `?${evolvedQuery}`; const queryString = isEmpty(evolvedQuery) ? '' : `?${evolvedQuery}`;
navigate(`/server/${params.serverId}/list-short-urls/1${queryString}`); navigate(`/server/${params.serverId ?? ''}/list-short-urls/1${queryString}`);
}; };
return [query, toFirstPageWithExtra]; return [query, toFirstPageWithExtra];

View file

@ -7,12 +7,10 @@ import { ProblemDetailsError } from '../../api/types';
import { parseApiError } from '../../api/utils'; import { parseApiError } from '../../api/utils';
import { ApiErrorAction } from '../../api/types/actions'; import { ApiErrorAction } from '../../api/types/actions';
/* eslint-disable padding-line-between-statements */
export const CREATE_SHORT_URL_START = 'shlink/createShortUrl/CREATE_SHORT_URL_START'; export const CREATE_SHORT_URL_START = 'shlink/createShortUrl/CREATE_SHORT_URL_START';
export const CREATE_SHORT_URL_ERROR = 'shlink/createShortUrl/CREATE_SHORT_URL_ERROR'; export const CREATE_SHORT_URL_ERROR = 'shlink/createShortUrl/CREATE_SHORT_URL_ERROR';
export const CREATE_SHORT_URL = 'shlink/createShortUrl/CREATE_SHORT_URL'; export const CREATE_SHORT_URL = 'shlink/createShortUrl/CREATE_SHORT_URL';
export const RESET_CREATE_SHORT_URL = 'shlink/createShortUrl/RESET_CREATE_SHORT_URL'; export const RESET_CREATE_SHORT_URL = 'shlink/createShortUrl/RESET_CREATE_SHORT_URL';
/* eslint-enable padding-line-between-statements */
export interface ShortUrlCreation { export interface ShortUrlCreation {
result: ShortUrl | null; result: ShortUrl | null;
@ -43,10 +41,10 @@ export const createShortUrl = (buildShlinkApiClient: ShlinkApiClientBuilder) =>
getState: GetState, getState: GetState,
) => { ) => {
dispatch({ type: CREATE_SHORT_URL_START }); dispatch({ type: CREATE_SHORT_URL_START });
const { createShortUrl } = buildShlinkApiClient(getState); const { createShortUrl: shlinkCreateShortUrl } = buildShlinkApiClient(getState);
try { try {
const result = await createShortUrl(data); const result = await shlinkCreateShortUrl(data);
dispatch<CreateShortUrlAction>({ type: CREATE_SHORT_URL, result }); dispatch<CreateShortUrlAction>({ type: CREATE_SHORT_URL, result });
} catch (e: any) { } catch (e: any) {

View file

@ -6,12 +6,10 @@ import { ShlinkApiClientBuilder } from '../../api/services/ShlinkApiClientBuilde
import { parseApiError } from '../../api/utils'; import { parseApiError } from '../../api/utils';
import { ApiErrorAction } from '../../api/types/actions'; import { ApiErrorAction } from '../../api/types/actions';
/* eslint-disable padding-line-between-statements */
export const DELETE_SHORT_URL_START = 'shlink/deleteShortUrl/DELETE_SHORT_URL_START'; export const DELETE_SHORT_URL_START = 'shlink/deleteShortUrl/DELETE_SHORT_URL_START';
export const DELETE_SHORT_URL_ERROR = 'shlink/deleteShortUrl/DELETE_SHORT_URL_ERROR'; export const DELETE_SHORT_URL_ERROR = 'shlink/deleteShortUrl/DELETE_SHORT_URL_ERROR';
export const SHORT_URL_DELETED = 'shlink/deleteShortUrl/SHORT_URL_DELETED'; export const SHORT_URL_DELETED = 'shlink/deleteShortUrl/SHORT_URL_DELETED';
export const RESET_DELETE_SHORT_URL = 'shlink/deleteShortUrl/RESET_DELETE_SHORT_URL'; export const RESET_DELETE_SHORT_URL = 'shlink/deleteShortUrl/RESET_DELETE_SHORT_URL';
/* eslint-enable padding-line-between-statements */
export interface ShortUrlDeletion { export interface ShortUrlDeletion {
shortCode: string; shortCode: string;
@ -43,10 +41,10 @@ export const deleteShortUrl = (buildShlinkApiClient: ShlinkApiClientBuilder) =>
domain?: string | null, domain?: string | null,
) => async (dispatch: Dispatch, getState: GetState) => { ) => async (dispatch: Dispatch, getState: GetState) => {
dispatch({ type: DELETE_SHORT_URL_START }); dispatch({ type: DELETE_SHORT_URL_START });
const { deleteShortUrl } = buildShlinkApiClient(getState); const { deleteShortUrl: shlinkDeleteShortUrl } = buildShlinkApiClient(getState);
try { try {
await deleteShortUrl(shortCode, domain); await shlinkDeleteShortUrl(shortCode, domain);
dispatch<DeleteShortUrlAction>({ type: SHORT_URL_DELETED, shortCode, domain }); dispatch<DeleteShortUrlAction>({ type: SHORT_URL_DELETED, shortCode, domain });
} catch (e: any) { } catch (e: any) {
dispatch<ApiErrorAction>({ type: DELETE_SHORT_URL_ERROR, errorData: parseApiError(e) }); dispatch<ApiErrorAction>({ type: DELETE_SHORT_URL_ERROR, errorData: parseApiError(e) });

View file

@ -9,11 +9,9 @@ import { ProblemDetailsError } from '../../api/types';
import { parseApiError } from '../../api/utils'; import { parseApiError } from '../../api/utils';
import { ApiErrorAction } from '../../api/types/actions'; import { ApiErrorAction } from '../../api/types/actions';
/* eslint-disable padding-line-between-statements */
export const GET_SHORT_URL_DETAIL_START = 'shlink/shortUrlDetail/GET_SHORT_URL_DETAIL_START'; export const GET_SHORT_URL_DETAIL_START = 'shlink/shortUrlDetail/GET_SHORT_URL_DETAIL_START';
export const GET_SHORT_URL_DETAIL_ERROR = 'shlink/shortUrlDetail/GET_SHORT_URL_DETAIL_ERROR'; export const GET_SHORT_URL_DETAIL_ERROR = 'shlink/shortUrlDetail/GET_SHORT_URL_DETAIL_ERROR';
export const GET_SHORT_URL_DETAIL = 'shlink/shortUrlDetail/GET_SHORT_URL_DETAIL'; export const GET_SHORT_URL_DETAIL = 'shlink/shortUrlDetail/GET_SHORT_URL_DETAIL';
/* eslint-enable padding-line-between-statements */
export interface ShortUrlDetail { export interface ShortUrlDetail {
shortUrl?: ShortUrl; shortUrl?: ShortUrl;
@ -46,7 +44,7 @@ export const getShortUrlDetail = (buildShlinkApiClient: ShlinkApiClientBuilder)
try { try {
const { shortUrlsList } = getState(); const { shortUrlsList } = getState();
const shortUrl = shortUrlsList?.shortUrls?.data.find( const shortUrl = shortUrlsList?.shortUrls?.data.find(
(shortUrl) => shortUrlMatches(shortUrl, shortCode, domain), (url) => shortUrlMatches(url, shortCode, domain),
) ?? await buildShlinkApiClient(getState).getShortUrl(shortCode, domain); ) ?? await buildShlinkApiClient(getState).getShortUrl(shortCode, domain);
dispatch<ShortUrlDetailAction>({ shortUrl, type: GET_SHORT_URL_DETAIL }); dispatch<ShortUrlDetailAction>({ shortUrl, type: GET_SHORT_URL_DETAIL });

View file

@ -9,11 +9,9 @@ import { parseApiError } from '../../api/utils';
import { supportsTagsInPatch } from '../../utils/helpers/features'; import { supportsTagsInPatch } from '../../utils/helpers/features';
import { ApiErrorAction } from '../../api/types/actions'; import { ApiErrorAction } from '../../api/types/actions';
/* eslint-disable padding-line-between-statements */
export const EDIT_SHORT_URL_START = 'shlink/shortUrlEdition/EDIT_SHORT_URL_START'; export const EDIT_SHORT_URL_START = 'shlink/shortUrlEdition/EDIT_SHORT_URL_START';
export const EDIT_SHORT_URL_ERROR = 'shlink/shortUrlEdition/EDIT_SHORT_URL_ERROR'; export const EDIT_SHORT_URL_ERROR = 'shlink/shortUrlEdition/EDIT_SHORT_URL_ERROR';
export const SHORT_URL_EDITED = 'shlink/shortUrlEdition/SHORT_URL_EDITED'; export const SHORT_URL_EDITED = 'shlink/shortUrlEdition/SHORT_URL_EDITED';
/* eslint-enable padding-line-between-statements */
export interface ShortUrlEdition { export interface ShortUrlEdition {
shortUrl?: ShortUrl; shortUrl?: ShortUrl;

View file

@ -10,11 +10,9 @@ import { DeleteShortUrlAction, SHORT_URL_DELETED } from './shortUrlDeletion';
import { CREATE_SHORT_URL, CreateShortUrlAction } from './shortUrlCreation'; import { CREATE_SHORT_URL, CreateShortUrlAction } from './shortUrlCreation';
import { SHORT_URL_EDITED, ShortUrlEditedAction } from './shortUrlEdition'; import { SHORT_URL_EDITED, ShortUrlEditedAction } from './shortUrlEdition';
/* eslint-disable padding-line-between-statements */
export const LIST_SHORT_URLS_START = 'shlink/shortUrlsList/LIST_SHORT_URLS_START'; export const LIST_SHORT_URLS_START = 'shlink/shortUrlsList/LIST_SHORT_URLS_START';
export const LIST_SHORT_URLS_ERROR = 'shlink/shortUrlsList/LIST_SHORT_URLS_ERROR'; export const LIST_SHORT_URLS_ERROR = 'shlink/shortUrlsList/LIST_SHORT_URLS_ERROR';
export const LIST_SHORT_URLS = 'shlink/shortUrlsList/LIST_SHORT_URLS'; export const LIST_SHORT_URLS = 'shlink/shortUrlsList/LIST_SHORT_URLS';
/* eslint-enable padding-line-between-statements */
export const ITEMS_IN_OVERVIEW_PAGE = 5; export const ITEMS_IN_OVERVIEW_PAGE = 5;
@ -46,16 +44,16 @@ export default buildReducer<ShortUrlsList, ListShortUrlsCombinedAction>({
[LIST_SHORT_URLS_ERROR]: () => ({ loading: false, error: true }), [LIST_SHORT_URLS_ERROR]: () => ({ loading: false, error: true }),
[LIST_SHORT_URLS]: (_, { shortUrls }) => ({ loading: false, error: false, shortUrls }), [LIST_SHORT_URLS]: (_, { shortUrls }) => ({ loading: false, error: false, shortUrls }),
[SHORT_URL_DELETED]: pipe( [SHORT_URL_DELETED]: pipe(
(state: ShortUrlsList, { shortCode, domain }: DeleteShortUrlAction) => !state.shortUrls ? state : assocPath( (state: ShortUrlsList, { shortCode, domain }: DeleteShortUrlAction) => (!state.shortUrls ? state : assocPath(
['shortUrls', 'data'], ['shortUrls', 'data'],
reject((shortUrl) => shortUrlMatches(shortUrl, shortCode, domain), state.shortUrls.data), reject((shortUrl) => shortUrlMatches(shortUrl, shortCode, domain), state.shortUrls.data),
state, state,
), )),
(state) => !state.shortUrls ? state : assocPath( (state) => (!state.shortUrls ? state : assocPath(
['shortUrls', 'pagination', 'totalItems'], ['shortUrls', 'pagination', 'totalItems'],
state.shortUrls.pagination.totalItems - 1, state.shortUrls.pagination.totalItems - 1,
state, state,
), )),
), ),
[CREATE_VISITS]: (state, { createdVisits }) => assocPath( [CREATE_VISITS]: (state, { createdVisits }) => assocPath(
['shortUrls', 'data'], ['shortUrls', 'data'],
@ -79,18 +77,18 @@ export default buildReducer<ShortUrlsList, ListShortUrlsCombinedAction>({
// The only place where the list and the creation form coexist is the overview page. // The only place where the list and the creation form coexist is the overview page.
// There we can assume we are displaying page 1, and therefore, we can safely prepend the new short URL. // There we can assume we are displaying page 1, and therefore, we can safely prepend the new short URL.
// We can also remove the items above the amount that is displayed there. // We can also remove the items above the amount that is displayed there.
(state: ShortUrlsList, { result }: CreateShortUrlAction) => !state.shortUrls ? state : assocPath( (state: ShortUrlsList, { result }: CreateShortUrlAction) => (!state.shortUrls ? state : assocPath(
['shortUrls', 'data'], ['shortUrls', 'data'],
[result, ...state.shortUrls.data.slice(0, ITEMS_IN_OVERVIEW_PAGE - 1)], [result, ...state.shortUrls.data.slice(0, ITEMS_IN_OVERVIEW_PAGE - 1)],
state, state,
), )),
(state: ShortUrlsList) => !state.shortUrls ? state : assocPath( (state: ShortUrlsList) => (!state.shortUrls ? state : assocPath(
['shortUrls', 'pagination', 'totalItems'], ['shortUrls', 'pagination', 'totalItems'],
state.shortUrls.pagination.totalItems + 1, state.shortUrls.pagination.totalItems + 1,
state, state,
)),
), ),
), [SHORT_URL_EDITED]: (state, { shortUrl: editedShortUrl }) => (!state.shortUrls ? state : assocPath(
[SHORT_URL_EDITED]: (state, { shortUrl: editedShortUrl }) => !state.shortUrls ? state : assocPath(
['shortUrls', 'data'], ['shortUrls', 'data'],
state.shortUrls.data.map((shortUrl) => { state.shortUrls.data.map((shortUrl) => {
const { shortCode, domain } = editedShortUrl; const { shortCode, domain } = editedShortUrl;
@ -98,17 +96,17 @@ export default buildReducer<ShortUrlsList, ListShortUrlsCombinedAction>({
return shortUrlMatches(shortUrl, shortCode, domain) ? editedShortUrl : shortUrl; return shortUrlMatches(shortUrl, shortCode, domain) ? editedShortUrl : shortUrl;
}), }),
state, state,
), )),
}, initialState); }, initialState);
export const listShortUrls = (buildShlinkApiClient: ShlinkApiClientBuilder) => ( export const listShortUrls = (buildShlinkApiClient: ShlinkApiClientBuilder) => (
params: ShlinkShortUrlsListParams = {}, params: ShlinkShortUrlsListParams = {},
) => async (dispatch: Dispatch, getState: GetState) => { ) => async (dispatch: Dispatch, getState: GetState) => {
dispatch({ type: LIST_SHORT_URLS_START }); dispatch({ type: LIST_SHORT_URLS_START });
const { listShortUrls } = buildShlinkApiClient(getState); const { listShortUrls: shlinkListShortUrls } = buildShlinkApiClient(getState);
try { try {
const shortUrls = await listShortUrls(params); const shortUrls = await shlinkListShortUrls(params);
dispatch<ListShortUrlsAction>({ type: LIST_SHORT_URLS, shortUrls }); dispatch<ListShortUrlsAction>({ type: LIST_SHORT_URLS, shortUrls });
} catch (e) { } catch (e) {

View file

@ -50,9 +50,9 @@ export const TagsTable = (TagsTableRow: FC<TagsTableRowProps>) => (
<th className="tags-table__header-cell text-lg-end" onClick={orderByColumn('visits')}> <th className="tags-table__header-cell text-lg-end" onClick={orderByColumn('visits')}>
Visits <TableOrderIcon currentOrder={currentOrder} field="visits" /> Visits <TableOrderIcon currentOrder={currentOrder} field="visits" />
</th> </th>
<th className="tags-table__header-cell" /> <th aria-label="Options" className="tags-table__header-cell" />
</tr> </tr>
<tr><th colSpan={4} className="p-0 border-top-0" /></tr> <tr><th aria-label="Separator" colSpan={4} className="p-0 border-top-0" /></tr>
</thead> </thead>
<tbody> <tbody>
{currentPage.length === 0 && <tr><td colSpan={4} className="text-center">No results found</td></tr>} {currentPage.length === 0 && <tr><td colSpan={4} className="text-center">No results found</td></tr>}

View file

@ -1,4 +1,4 @@
import { Modal, ModalBody, ModalFooter, ModalHeader } from 'reactstrap'; import { Button, Modal, ModalBody, ModalFooter, ModalHeader } from 'reactstrap';
import { TagDeletion } from '../reducers/tagDelete'; import { TagDeletion } from '../reducers/tagDelete';
import { TagModalProps } from '../data'; import { TagModalProps } from '../data';
import { Result } from '../../utils/Result'; import { Result } from '../../utils/Result';
@ -34,10 +34,10 @@ const DeleteTagConfirmModal = (
)} )}
</ModalBody> </ModalBody>
<ModalFooter> <ModalFooter>
<button className="btn btn-link" onClick={toggle}>Cancel</button> <Button color="link" onClick={toggle}>Cancel</Button>
<button className="btn btn-danger" disabled={deleting} onClick={doDelete}> <Button color="danger" disabled={deleting} onClick={doDelete}>
{deleting ? 'Deleting tag...' : 'Delete tag'} {deleting ? 'Deleting tag...' : 'Delete tag'}
</button> </Button>
</ModalFooter> </ModalFooter>
</Modal> </Modal>
); );

View file

@ -13,7 +13,7 @@ export interface TagsSelectorProps {
} }
interface TagsSelectorConnectProps extends TagsSelectorProps { interface TagsSelectorConnectProps extends TagsSelectorProps {
listTags: Function; listTags: () => void;
tagsList: TagsList; tagsList: TagsList;
settings: Settings; settings: Settings;
} }

View file

@ -6,12 +6,10 @@ import { ProblemDetailsError } from '../../api/types';
import { parseApiError } from '../../api/utils'; import { parseApiError } from '../../api/utils';
import { ApiErrorAction } from '../../api/types/actions'; import { ApiErrorAction } from '../../api/types/actions';
/* eslint-disable padding-line-between-statements */
export const DELETE_TAG_START = 'shlink/deleteTag/DELETE_TAG_START'; export const DELETE_TAG_START = 'shlink/deleteTag/DELETE_TAG_START';
export const DELETE_TAG_ERROR = 'shlink/deleteTag/DELETE_TAG_ERROR'; export const DELETE_TAG_ERROR = 'shlink/deleteTag/DELETE_TAG_ERROR';
export const DELETE_TAG = 'shlink/deleteTag/DELETE_TAG'; export const DELETE_TAG = 'shlink/deleteTag/DELETE_TAG';
export const TAG_DELETED = 'shlink/deleteTag/TAG_DELETED'; export const TAG_DELETED = 'shlink/deleteTag/TAG_DELETED';
/* eslint-enable padding-line-between-statements */
export interface TagDeletion { export interface TagDeletion {
deleting: boolean; deleting: boolean;

View file

@ -8,11 +8,9 @@ import { ProblemDetailsError } from '../../api/types';
import { parseApiError } from '../../api/utils'; import { parseApiError } from '../../api/utils';
import { ApiErrorAction } from '../../api/types/actions'; import { ApiErrorAction } from '../../api/types/actions';
/* eslint-disable padding-line-between-statements */
export const EDIT_TAG_START = 'shlink/editTag/EDIT_TAG_START'; export const EDIT_TAG_START = 'shlink/editTag/EDIT_TAG_START';
export const EDIT_TAG_ERROR = 'shlink/editTag/EDIT_TAG_ERROR'; export const EDIT_TAG_ERROR = 'shlink/editTag/EDIT_TAG_ERROR';
export const EDIT_TAG = 'shlink/editTag/EDIT_TAG'; export const EDIT_TAG = 'shlink/editTag/EDIT_TAG';
/* eslint-enable padding-line-between-statements */
export const TAG_EDITED = 'shlink/editTag/TAG_EDITED'; export const TAG_EDITED = 'shlink/editTag/TAG_EDITED';
@ -53,10 +51,10 @@ export const editTag = (buildShlinkApiClient: ShlinkApiClientBuilder, colorGener
color: string, color: string,
) => async (dispatch: Dispatch, getState: GetState) => { ) => async (dispatch: Dispatch, getState: GetState) => {
dispatch({ type: EDIT_TAG_START }); dispatch({ type: EDIT_TAG_START });
const { editTag } = buildShlinkApiClient(getState); const { editTag: shlinkEditTag } = buildShlinkApiClient(getState);
try { try {
await editTag(oldName, newName); await shlinkEditTag(oldName, newName);
colorGenerator.setColorForKey(newName, color); colorGenerator.setColorForKey(newName, color);
dispatch({ type: EDIT_TAG, oldName, newName }); dispatch({ type: EDIT_TAG, oldName, newName });
} catch (e: any) { } catch (e: any) {

View file

@ -13,12 +13,10 @@ import { CREATE_SHORT_URL, CreateShortUrlAction } from '../../short-urls/reducer
import { DeleteTagAction, TAG_DELETED } from './tagDelete'; import { DeleteTagAction, TAG_DELETED } from './tagDelete';
import { EditTagAction, TAG_EDITED } from './tagEdit'; import { EditTagAction, TAG_EDITED } from './tagEdit';
/* eslint-disable padding-line-between-statements */
export const LIST_TAGS_START = 'shlink/tagsList/LIST_TAGS_START'; export const LIST_TAGS_START = 'shlink/tagsList/LIST_TAGS_START';
export const LIST_TAGS_ERROR = 'shlink/tagsList/LIST_TAGS_ERROR'; export const LIST_TAGS_ERROR = 'shlink/tagsList/LIST_TAGS_ERROR';
export const LIST_TAGS = 'shlink/tagsList/LIST_TAGS'; export const LIST_TAGS = 'shlink/tagsList/LIST_TAGS';
export const FILTER_TAGS = 'shlink/tagsList/FILTER_TAGS'; export const FILTER_TAGS = 'shlink/tagsList/FILTER_TAGS';
/* eslint-enable padding-line-between-statements */
type TagsStatsMap = Record<string, TagStats>; type TagsStatsMap = Record<string, TagStats>;
@ -58,19 +56,19 @@ const initialState = {
type TagIncrease = [string, number]; type TagIncrease = [string, number];
const renameTag = (oldName: string, newName: string) => (tag: string) => tag === oldName ? newName : tag; const renameTag = (oldName: string, newName: string) => (tag: string) => (tag === oldName ? newName : tag);
const rejectTag = (tags: string[], tagToReject: string) => reject((tag) => tag === tagToReject, tags); const rejectTag = (tags: string[], tagToReject: string) => reject((tag) => tag === tagToReject, tags);
const increaseVisitsForTags = (tags: TagIncrease[], stats: TagsStatsMap) => tags.reduce((stats, [ tag, increase ]) => { const increaseVisitsForTags = (tags: TagIncrease[], stats: TagsStatsMap) => tags.reduce((theStats, [tag, increase]) => {
if (!stats[tag]) { if (!theStats[tag]) {
return stats; return theStats;
} }
const tagStats = stats[tag]; const tagStats = theStats[tag];
tagStats.visitsCount = tagStats.visitsCount + increase; tagStats.visitsCount += increase;
stats[tag] = tagStats; theStats[tag] = tagStats; // eslint-disable-line no-param-reassign
return stats; return theStats;
}, { ...stats }); }, { ...stats });
const calculateVisitsPerTag = (createdVisits: CreateVisit[]): TagIncrease[] => Object.entries( const calculateVisitsPerTag = (createdVisits: CreateVisit[]): TagIncrease[] => Object.entries(
createdVisits.reduce<Stats>((acc, { shortUrl }) => { createdVisits.reduce<Stats>((acc, { shortUrl }) => {
@ -123,8 +121,8 @@ export const listTags = (buildShlinkApiClient: ShlinkApiClientBuilder, force = t
dispatch({ type: LIST_TAGS_START }); dispatch({ type: LIST_TAGS_START });
try { try {
const { listTags } = buildShlinkApiClient(getState); const { listTags: shlinkListTags } = buildShlinkApiClient(getState);
const { tags, stats = [] }: ShlinkTags = await listTags(); const { tags, stats = [] }: ShlinkTags = await shlinkListTags();
const processedStats = stats.reduce<TagsStatsMap>((acc, { tag, shortUrlsCount, visitsCount }) => { const processedStats = stats.reduce<TagsStatsMap>((acc, { tag, shortUrlsCount, visitsCount }) => {
acc[tag] = { shortUrlsCount, visitsCount }; acc[tag] = { shortUrlsCount, visitsCount };

View file

@ -5,7 +5,6 @@ import { LabeledFormGroup } from './LabeledFormGroup';
export interface InputFormGroupProps { export interface InputFormGroupProps {
value: string; value: string;
onChange: (newValue: string) => void; onChange: (newValue: string) => void;
id?: string;
type?: InputType; type?: InputType;
required?: boolean; required?: boolean;
placeholder?: string; placeholder?: string;

View file

@ -7,6 +7,7 @@ interface LabeledFormGroupProps {
labelClassName?: string; labelClassName?: string;
} }
/* eslint-disable jsx-a11y/label-has-associated-control */
export const LabeledFormGroup: FC<LabeledFormGroupProps> = ( export const LabeledFormGroup: FC<LabeledFormGroupProps> = (
{ children, label, className = '', labelClassName = '', noMargin = false }, { children, label, className = '', labelClassName = '', noMargin = false },
) => ( ) => (

View file

@ -15,15 +15,15 @@ const formatDateFromFormat = (date?: NullableDate, theFormat?: string): Optional
return theFormat ? format(date, theFormat) : formatISO(date); return theFormat ? format(date, theFormat) : formatISO(date);
}; };
export const formatDate = (format = 'yyyy-MM-dd') => (date?: NullableDate) => formatDateFromFormat(date, format); export const formatDate = (theFormat = 'yyyy-MM-dd') => (date?: NullableDate) => formatDateFromFormat(date, theFormat);
export const formatIsoDate = (date?: NullableDate) => formatDateFromFormat(date, undefined); export const formatIsoDate = (date?: NullableDate) => formatDateFromFormat(date, undefined);
export const formatInternational = formatDate(); export const formatInternational = formatDate();
export const parseDate = (date: string, format: string) => parse(date, format, new Date()); export const parseDate = (date: string, theFormat: string) => parse(date, theFormat, new Date());
export const parseISO = (date: DateOrString): Date => isDateObject(date) ? date : stdParseISO(date); export const parseISO = (date: DateOrString): Date => (isDateObject(date) ? date : stdParseISO(date));
export const isBetween = (date: DateOrString, start?: DateOrString, end?: DateOrString): boolean => { export const isBetween = (date: DateOrString, start?: DateOrString, end?: DateOrString): boolean => {
try { try {

View file

@ -5,27 +5,15 @@ const serverMatchesVersions = (versions: Versions) => (selectedServer: SelectedS
isReachableServer(selectedServer) && versionMatch(selectedServer.version, versions); isReachableServer(selectedServer) && versionMatch(selectedServer.version, versions);
export const supportsQrCodeSizeInQuery = serverMatchesVersions({ minVersion: '2.5.0' }); export const supportsQrCodeSizeInQuery = serverMatchesVersions({ minVersion: '2.5.0' });
export const supportsShortUrlTitle = serverMatchesVersions({ minVersion: '2.6.0' }); export const supportsShortUrlTitle = serverMatchesVersions({ minVersion: '2.6.0' });
export const supportsOrphanVisits = supportsShortUrlTitle; export const supportsOrphanVisits = supportsShortUrlTitle;
export const supportsQrCodeMargin = supportsShortUrlTitle; export const supportsQrCodeMargin = supportsShortUrlTitle;
export const supportsTagsInPatch = supportsShortUrlTitle; export const supportsTagsInPatch = supportsShortUrlTitle;
export const supportsBotVisits = serverMatchesVersions({ minVersion: '2.7.0' }); export const supportsBotVisits = serverMatchesVersions({ minVersion: '2.7.0' });
export const supportsCrawlableVisits = supportsBotVisits; export const supportsCrawlableVisits = supportsBotVisits;
export const supportsQrErrorCorrection = serverMatchesVersions({ minVersion: '2.8.0' }); export const supportsQrErrorCorrection = serverMatchesVersions({ minVersion: '2.8.0' });
export const supportsDomainRedirects = supportsQrErrorCorrection; export const supportsDomainRedirects = supportsQrErrorCorrection;
export const supportsForwardQuery = serverMatchesVersions({ minVersion: '2.9.0' }); export const supportsForwardQuery = serverMatchesVersions({ minVersion: '2.9.0' });
export const supportsDefaultDomainRedirectsEdition = serverMatchesVersions({ minVersion: '2.10.0' }); export const supportsDefaultDomainRedirectsEdition = serverMatchesVersions({ minVersion: '2.10.0' });
export const supportsNonOrphanVisits = serverMatchesVersions({ minVersion: '3.0.0' }); export const supportsNonOrphanVisits = serverMatchesVersions({ minVersion: '3.0.0' });
export const supportsAllTagsFiltering = supportsNonOrphanVisits; export const supportsAllTagsFiltering = supportsNonOrphanVisits;

View file

@ -56,13 +56,13 @@ export const useSwipeable = (showSidebar: () => void, hideSidebar: () => void) =
export const useQueryState = <T>(paramName: string, initialState: T): [ T, (newValue: T) => void ] => { export const useQueryState = <T>(paramName: string, initialState: T): [ T, (newValue: T) => void ] => {
const [value, setValue] = useState(initialState); const [value, setValue] = useState(initialState);
const setValueWithLocation = (value: T) => { const setValueWithLocation = (valueToSet: T) => {
const { location, history } = window; const { location, history } = window;
const query = parseQuery<any>(location.search); const query = parseQuery<any>(location.search);
query[paramName] = value; query[paramName] = valueToSet;
history.pushState(null, '', `${location.pathname}?${stringifyQuery(query)}`); history.pushState(null, '', `${location.pathname}?${stringifyQuery(query)}`);
setValue(value); setValue(valueToSet);
}; };
return [value, setValueWithLocation]; return [value, setValueWithLocation];

View file

@ -4,7 +4,7 @@ import marker from 'leaflet/dist/images/marker-icon.png';
import markerShadow from 'leaflet/dist/images/marker-shadow.png'; import markerShadow from 'leaflet/dist/images/marker-shadow.png';
export const fixLeafletIcons = () => { export const fixLeafletIcons = () => {
delete (L.Icon.Default.prototype as any)._getIconUrl; delete (L.Icon.Default.prototype as any)._getIconUrl; // eslint-disable-line no-underscore-dangle
L.Icon.Default.mergeOptions({ L.Icon.Default.mergeOptions({
iconRetinaUrl: marker2x, iconRetinaUrl: marker2x,

View file

@ -22,20 +22,20 @@ export const determineOrderDir = <T extends string = string>(
return currentOrderDir ? newOrderMap[currentOrderDir] : 'ASC'; return currentOrderDir ? newOrderMap[currentOrderDir] : 'ASC';
}; };
export const sortList = <List>(list: List[], { field, dir }: Order<Partial<keyof List>>) => !field || !dir export const sortList = <List>(list: List[], { field, dir }: Order<Partial<keyof List>>) => (
? list !field || !dir ? list : list.sort((a, b) => {
: list.sort((a, b) => {
const greaterThan = dir === 'ASC' ? 1 : -1; const greaterThan = dir === 'ASC' ? 1 : -1;
const smallerThan = dir === 'ASC' ? -1 : 1; const smallerThan = dir === 'ASC' ? -1 : 1;
return a[field] > b[field] ? greaterThan : smallerThan; return a[field] > b[field] ? greaterThan : smallerThan;
}); })
);
export const orderToString = <T>(order: Order<T>): string | undefined => export const orderToString = <T>(order: Order<T>): string | undefined => (
order.dir ? `${order.field}-${order.dir}` : undefined; order.dir ? `${order.field}-${order.dir}` : undefined
);
export const stringToOrder = <T>(order: string): Order<T> => { export const stringToOrder = <T>(order: string): Order<T> => {
const [field, dir] = order.split('-') as [ T | undefined, OrderDir | undefined ]; const [field, dir] = order.split('-') as [ T | undefined, OrderDir | undefined ];
return { field, dir }; return { field, dir };
}; };

View file

@ -30,7 +30,10 @@ export const progressivePagination = (currentPage: number, pageCount: number): N
export const pageIsEllipsis = (pageNumber: NumberOrEllipsis): pageNumber is Ellipsis => pageNumber === ELLIPSIS; export const pageIsEllipsis = (pageNumber: NumberOrEllipsis): pageNumber is Ellipsis => pageNumber === ELLIPSIS;
export const prettifyPageNumber = (pageNumber: NumberOrEllipsis): string => export const prettifyPageNumber = (pageNumber: NumberOrEllipsis): string => (
pageIsEllipsis(pageNumber) ? pageNumber : prettify(pageNumber); pageIsEllipsis(pageNumber) ? pageNumber : prettify(pageNumber)
);
export const keyForPage = (pageNumber: NumberOrEllipsis, index: number) => !pageIsEllipsis(pageNumber) ? `${pageNumber}` : `${pageNumber}_${index}`; export const keyForPage = (pageNumber: NumberOrEllipsis, index: number) => (
!pageIsEllipsis(pageNumber) ? `${pageNumber}` : `${pageNumber}_${index}`
);

View file

@ -1,9 +1,7 @@
export const forceUpdate = async () => { export const forceUpdate = async () => {
const registrations = await navigator.serviceWorker?.getRegistrations() ?? []; const registrations = await navigator.serviceWorker?.getRegistrations() ?? [];
for (const registration of registrations) { registrations.forEach(({ waiting }) => {
const { waiting } = registration;
waiting?.addEventListener('statechange', (event) => { waiting?.addEventListener('statechange', (event) => {
if ((event.target as any)?.state === 'activated') { if ((event.target as any)?.state === 'activated') {
window.location.reload(); window.location.reload();
@ -12,5 +10,5 @@ export const forceUpdate = async () => {
// The logic that makes skipWaiting to be called when this message is posted is in service-worker.ts // The logic that makes skipWaiting to be called when this message is posted is in service-worker.ts
waiting?.postMessage({ type: 'SKIP_WAITING' }); waiting?.postMessage({ type: 'SKIP_WAITING' });
} });
}; };

View file

@ -34,7 +34,7 @@ const versionIsValidSemVer = memoizeWith(identity, (version: string): version is
} }
}); });
export const versionToPrintable = (version: string) => !versionIsValidSemVer(version) ? version : `v${version}`; export const versionToPrintable = (version: string) => (!versionIsValidSemVer(version) ? version : `v${version}`);
export const versionToSemVer = (defaultValue: SemVer = 'latest') => export const versionToSemVer = (defaultValue: SemVer = 'latest') =>
(version: string): SemVer => versionIsValidSemVer(version) ? version : defaultValue; (version: string): SemVer => (versionIsValidSemVer(version) ? version : defaultValue);

View file

@ -25,6 +25,6 @@ export type RecursivePartial<T> = {
[P in keyof T]?: RecursivePartial<T[P]>; [P in keyof T]?: RecursivePartial<T[P]>;
}; };
export const nonEmptyValueOrNull = <T>(value: T): T | null => isEmpty(value) ? null : value; export const nonEmptyValueOrNull = <T>(value: T): T | null => (isEmpty(value) ? null : value);
export const capitalize = <T extends string>(value: T): string => `${value.charAt(0).toUpperCase()}${value.slice(1)}`; export const capitalize = <T extends string>(value: T): string => `${value.charAt(0).toUpperCase()}${value.slice(1)}`;

View file

@ -19,7 +19,7 @@ const ShortUrlVisitsHeader = ({ shortUrlDetail, shortUrlVisits, goBack }: ShortU
const longLink = shortUrl?.longUrl ?? ''; const longLink = shortUrl?.longUrl ?? '';
const title = shortUrl?.title; const title = shortUrl?.title;
const renderDate = () => !shortUrl ? <small>Loading...</small> : ( const renderDate = () => (!shortUrl ? <small>Loading...</small> : (
<span> <span>
<b id="created" className="short-url-visits-header__created-at"> <b id="created" className="short-url-visits-header__created-at">
<Time date={shortUrl.dateCreated} relative /> <Time date={shortUrl.dateCreated} relative />
@ -28,7 +28,7 @@ const ShortUrlVisitsHeader = ({ shortUrlDetail, shortUrlVisits, goBack }: ShortU
<Time date={shortUrl.dateCreated} /> <Time date={shortUrl.dateCreated} />
</UncontrolledTooltip> </UncontrolledTooltip>
</span> </span>
); ));
const visitsStatsTitle = <>Visits for <ExternalLink href={shortLink} /></>; const visitsStatsTitle = <>Visits for <ExternalLink href={shortLink} /></>;
return ( return (

View file

@ -235,10 +235,9 @@ const VisitsStats: FC<VisitsStatsProps> = ({
stats={cities} stats={cities}
highlightedStats={highlightedVisitsToStats(highlightedVisits, 'city')} highlightedStats={highlightedVisitsToStats(highlightedVisits, 'city')}
highlightedLabel={highlightedLabel} highlightedLabel={highlightedLabel}
extraHeaderContent={(activeCities: string[]) => extraHeaderContent={(activeCities: string[]) => mapLocations.length > 0 && (
mapLocations.length > 0 &&
<OpenMapModalBtn modalTitle="Cities" locations={mapLocations} activeCities={activeCities} /> <OpenMapModalBtn modalTitle="Cities" locations={mapLocations} activeCities={activeCities} />
} )}
sortingItems={{ sortingItems={{
name: 'City name', name: 'City name',
amount: 'Visits amount', amount: 'Visits amount',

View file

@ -16,9 +16,9 @@ export interface HorizontalBarChartProps {
onClick?: (label: string) => void; onClick?: (label: string) => void;
} }
const dropLabelIfHidden = (label: string) => label.startsWith('hidden') ? '' : label; const dropLabelIfHidden = (label: string) => (label.startsWith('hidden') ? '' : label);
const statsAreDefined = (stats: Stats | undefined): stats is Stats => !!stats && Object.keys(stats).length > 0; const statsAreDefined = (stats: Stats | undefined): stats is Stats => !!stats && Object.keys(stats).length > 0;
const determineHeight = (labels: string[]): number | undefined => labels.length > 20 ? labels.length * 10 : undefined; const determineHeight = (labels: string[]): number | undefined => (labels.length > 20 ? labels.length * 10 : undefined);
const generateChartDatasets = ( const generateChartDatasets = (
data: number[], data: number[],

View file

@ -160,7 +160,7 @@ const chartElementAtEvent = (
setSelectedVisits([]); setSelectedVisits([]);
selectedLabel = null; selectedLabel = null;
} else { } else {
setSelectedVisits(labels[index] && datasetsByPoint[labels[index]] || []); setSelectedVisits(labels[index] ? datasetsByPoint[labels[index]] : []);
selectedLabel = labels[index] ?? null; selectedLabel = labels[index] ?? null;
} }
}; };

View file

@ -17,7 +17,7 @@ interface SortableBarChartCardProps extends Omit<HorizontalBarChartProps, 'max'>
extraHeaderContent?: Function; extraHeaderContent?: Function;
} }
const toLowerIfString = (value: any) => type(value) === 'String' ? toLower(value) : value; // eslint-disable-line @typescript-eslint/no-unsafe-return const toLowerIfString = (value: any) => (type(value) === 'String' ? toLower(value) : value);
const pickKeyFromPair = ([key]: StatsRow) => key; const pickKeyFromPair = ([key]: StatsRow) => key;
const pickValueFromPair = ([, value]: StatsRow) => value; const pickValueFromPair = ([, value]: StatsRow) => value;
@ -34,11 +34,11 @@ export const SortableBarChartCard: FC<SortableBarChartCardProps> = ({
const [currentPage, setCurrentPage] = useState(1); const [currentPage, setCurrentPage] = useState(1);
const [itemsPerPage, setItemsPerPage] = useState(50); const [itemsPerPage, setItemsPerPage] = useState(50);
const getSortedPairsForStats = (stats: Stats, sortingItems: Record<string, string>) => { const getSortedPairsForStats = (statsToSort: Stats, sorting: Record<string, string>) => {
const pairs = toPairs(stats); const pairs = toPairs(statsToSort);
const sortedPairs = !order.field ? pairs : sortBy( const sortedPairs = !order.field ? pairs : sortBy(
pipe<StatsRow, string | number, string | number>( pipe<StatsRow, string | number, string | number>(
order.field === Object.keys(sortingItems)[0] ? pickKeyFromPair : pickValueFromPair, order.field === Object.keys(sorting)[0] ? pickKeyFromPair : pickValueFromPair,
toLowerIfString, toLowerIfString,
), ),
pairs, pairs,
@ -60,12 +60,12 @@ export const SortableBarChartCard: FC<SortableBarChartCardProps> = ({
}; };
const renderPagination = (pagesCount: number) => const renderPagination = (pagesCount: number) =>
<SimplePaginator currentPage={currentPage} pagesCount={pagesCount} setCurrentPage={setCurrentPage} />; <SimplePaginator currentPage={currentPage} pagesCount={pagesCount} setCurrentPage={setCurrentPage} />;
const determineStats = (stats: Stats, highlightedStats: Stats | undefined, sortingItems: Record<string, string>) => { const determineStats = (statsToSort: Stats, sorting: Record<string, string>, theHighlightedStats?: Stats) => {
const sortedPairs = getSortedPairsForStats(stats, sortingItems); const sortedPairs = getSortedPairsForStats(statsToSort, sorting);
const sortedKeys = sortedPairs.map(pickKeyFromPair); const sortedKeys = sortedPairs.map(pickKeyFromPair);
// The highlighted stats have to be ordered based on the regular stats, not on its own values // The highlighted stats have to be ordered based on the regular stats, not on its own values
const sortedHighlightedPairs = highlightedStats && toPairs( const sortedHighlightedPairs = theHighlightedStats && toPairs(
{ ...zipObj(sortedKeys, sortedKeys.map(() => 0)), ...highlightedStats }, { ...zipObj(sortedKeys, sortedKeys.map(() => 0)), ...theHighlightedStats },
); );
if (sortedPairs.length <= itemsPerPage) { if (sortedPairs.length <= itemsPerPage) {
@ -88,8 +88,8 @@ export const SortableBarChartCard: FC<SortableBarChartCardProps> = ({
const { currentPageStats, currentPageHighlightedStats, pagination, max } = determineStats( const { currentPageStats, currentPageHighlightedStats, pagination, max } = determineStats(
stats, stats,
highlightedStats && Object.keys(highlightedStats).length > 0 ? highlightedStats : undefined,
sortingItems, sortingItems,
highlightedStats && Object.keys(highlightedStats).length > 0 ? highlightedStats : undefined,
); );
const activeCities = Object.keys(currentPageStats); const activeCities = Object.keys(currentPageStats);
const computeTitle = () => ( const computeTitle = () => (
@ -113,8 +113,8 @@ export const SortableBarChartCard: FC<SortableBarChartCardProps> = ({
toggleClassName="btn-sm p-0 me-3" toggleClassName="btn-sm p-0 me-3"
ranges={[50, 100, 200, 500]} ranges={[50, 100, 200, 500]}
value={itemsPerPage} value={itemsPerPage}
setValue={(itemsPerPage) => { setValue={(value) => {
setItemsPerPage(itemsPerPage); setItemsPerPage(value);
setCurrentPage(1); setCurrentPage(1);
}} }}
/> />

View file

@ -1,7 +1,7 @@
import { useRef, useState } from 'react'; import { useRef, useState } from 'react';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faMapMarkedAlt as mapIcon } from '@fortawesome/free-solid-svg-icons'; import { faMapMarkedAlt as mapIcon } from '@fortawesome/free-solid-svg-icons';
import { Dropdown, DropdownItem, DropdownMenu, UncontrolledTooltip } from 'reactstrap'; import { Button, Dropdown, DropdownItem, DropdownMenu, UncontrolledTooltip } from 'reactstrap';
import { useToggle } from '../../utils/helpers/hooks'; import { useToggle } from '../../utils/helpers/hooks';
import { CityStats } from '../types'; import { CityStats } from '../types';
import MapModal from './MapModal'; import MapModal from './MapModal';
@ -37,9 +37,9 @@ const OpenMapModalBtn = ({ modalTitle, activeCities, locations = [] }: OpenMapMo
return ( return (
<> <>
<button className="btn btn-link open-map-modal-btn__btn" ref={buttonRef as any} onClick={onClick}> <Button color="link" className="open-map-modal-btn__btn" ref={buttonRef as any} onClick={onClick}>
<FontAwesomeIcon icon={mapIcon} /> <FontAwesomeIcon icon={mapIcon} />
</button> </Button>
<UncontrolledTooltip placement="left" target={(() => buttonRef.current) as any}>Show in map</UncontrolledTooltip> <UncontrolledTooltip placement="left" target={(() => buttonRef.current) as any}>Show in map</UncontrolledTooltip>
<Dropdown isOpen={dropdownIsOpened} toggle={toggleDropdown} inNavbar> <Dropdown isOpen={dropdownIsOpened} toggle={toggleDropdown} inNavbar>
<DropdownMenu end> <DropdownMenu end>

View file

@ -11,7 +11,7 @@ const PARALLEL_REQUESTS_COUNT = 4;
const PARALLEL_STARTING_PAGE = 2; const PARALLEL_STARTING_PAGE = 2;
const isLastPage = ({ currentPage, pagesCount }: ShlinkPaginator): boolean => currentPage >= pagesCount; const isLastPage = ({ currentPage, pagesCount }: ShlinkPaginator): boolean => currentPage >= pagesCount;
const calcProgress = (total: number, current: number): number => current * 100 / total; const calcProgress = (total: number, current: number): number => (current * 100) / total;
type VisitsLoader = (page: number, itemsPerPage: number) => Promise<ShlinkVisits>; type VisitsLoader = (page: number, itemsPerPage: number) => Promise<ShlinkVisits>;
type LastVisitLoader = () => Promise<Visit | undefined>; type LastVisitLoader = () => Promise<Visit | undefined>;

View file

@ -14,7 +14,6 @@ import { isBetween } from '../../utils/helpers/date';
import { getVisitsWithLoader, lastVisitLoaderForLoader } from './common'; import { getVisitsWithLoader, lastVisitLoaderForLoader } from './common';
import { CREATE_VISITS, CreateVisitsAction } from './visitCreation'; import { CREATE_VISITS, CreateVisitsAction } from './visitCreation';
/* eslint-disable padding-line-between-statements */
export const GET_NON_ORPHAN_VISITS_START = 'shlink/orphanVisits/GET_NON_ORPHAN_VISITS_START'; export const GET_NON_ORPHAN_VISITS_START = 'shlink/orphanVisits/GET_NON_ORPHAN_VISITS_START';
export const GET_NON_ORPHAN_VISITS_ERROR = 'shlink/orphanVisits/GET_NON_ORPHAN_VISITS_ERROR'; export const GET_NON_ORPHAN_VISITS_ERROR = 'shlink/orphanVisits/GET_NON_ORPHAN_VISITS_ERROR';
export const GET_NON_ORPHAN_VISITS = 'shlink/orphanVisits/GET_NON_ORPHAN_VISITS'; export const GET_NON_ORPHAN_VISITS = 'shlink/orphanVisits/GET_NON_ORPHAN_VISITS';
@ -22,7 +21,6 @@ export const GET_NON_ORPHAN_VISITS_LARGE = 'shlink/orphanVisits/GET_NON_ORPHAN_V
export const GET_NON_ORPHAN_VISITS_CANCEL = 'shlink/orphanVisits/GET_NON_ORPHAN_VISITS_CANCEL'; export const GET_NON_ORPHAN_VISITS_CANCEL = 'shlink/orphanVisits/GET_NON_ORPHAN_VISITS_CANCEL';
export const GET_NON_ORPHAN_VISITS_PROGRESS_CHANGED = 'shlink/orphanVisits/GET_NON_ORPHAN_VISITS_PROGRESS_CHANGED'; export const GET_NON_ORPHAN_VISITS_PROGRESS_CHANGED = 'shlink/orphanVisits/GET_NON_ORPHAN_VISITS_PROGRESS_CHANGED';
export const GET_NON_ORPHAN_VISITS_FALLBACK_TO_INTERVAL = 'shlink/orphanVisits/GET_NON_ORPHAN_VISITS_FALLBACK_TO_INTERVAL'; export const GET_NON_ORPHAN_VISITS_FALLBACK_TO_INTERVAL = 'shlink/orphanVisits/GET_NON_ORPHAN_VISITS_FALLBACK_TO_INTERVAL';
/* eslint-enable padding-line-between-statements */
export interface NonOrphanVisitsAction extends Action<string> { export interface NonOrphanVisitsAction extends Action<string> {
visits: Visit[]; visits: Visit[];
@ -67,10 +65,10 @@ export const getNonOrphanVisits = (buildShlinkApiClient: ShlinkApiClientBuilder)
query: ShlinkVisitsParams = {}, query: ShlinkVisitsParams = {},
doIntervalFallback = false, doIntervalFallback = false,
) => async (dispatch: Dispatch, getState: GetState) => { ) => async (dispatch: Dispatch, getState: GetState) => {
const { getNonOrphanVisits } = buildShlinkApiClient(getState); const { getNonOrphanVisits: shlinkGetNonOrphanVisits } = buildShlinkApiClient(getState);
const visitsLoader = async (page: number, itemsPerPage: number) => const visitsLoader = async (page: number, itemsPerPage: number) =>
getNonOrphanVisits({ ...query, page, itemsPerPage }); shlinkGetNonOrphanVisits({ ...query, page, itemsPerPage });
const lastVisitLoader = lastVisitLoaderForLoader(doIntervalFallback, getNonOrphanVisits); const lastVisitLoader = lastVisitLoaderForLoader(doIntervalFallback, shlinkGetNonOrphanVisits);
const shouldCancel = () => getState().orphanVisits.cancelLoad; const shouldCancel = () => getState().orphanVisits.cancelLoad;
const extraFinishActionData: Partial<NonOrphanVisitsAction> = { query }; const extraFinishActionData: Partial<NonOrphanVisitsAction> = { query };
const actionMap = { const actionMap = {

View file

@ -17,7 +17,6 @@ import { isBetween } from '../../utils/helpers/date';
import { getVisitsWithLoader, lastVisitLoaderForLoader } from './common'; import { getVisitsWithLoader, lastVisitLoaderForLoader } from './common';
import { CREATE_VISITS, CreateVisitsAction } from './visitCreation'; import { CREATE_VISITS, CreateVisitsAction } from './visitCreation';
/* eslint-disable padding-line-between-statements */
export const GET_ORPHAN_VISITS_START = 'shlink/orphanVisits/GET_ORPHAN_VISITS_START'; export const GET_ORPHAN_VISITS_START = 'shlink/orphanVisits/GET_ORPHAN_VISITS_START';
export const GET_ORPHAN_VISITS_ERROR = 'shlink/orphanVisits/GET_ORPHAN_VISITS_ERROR'; export const GET_ORPHAN_VISITS_ERROR = 'shlink/orphanVisits/GET_ORPHAN_VISITS_ERROR';
export const GET_ORPHAN_VISITS = 'shlink/orphanVisits/GET_ORPHAN_VISITS'; export const GET_ORPHAN_VISITS = 'shlink/orphanVisits/GET_ORPHAN_VISITS';
@ -25,7 +24,6 @@ export const GET_ORPHAN_VISITS_LARGE = 'shlink/orphanVisits/GET_ORPHAN_VISITS_LA
export const GET_ORPHAN_VISITS_CANCEL = 'shlink/orphanVisits/GET_ORPHAN_VISITS_CANCEL'; export const GET_ORPHAN_VISITS_CANCEL = 'shlink/orphanVisits/GET_ORPHAN_VISITS_CANCEL';
export const GET_ORPHAN_VISITS_PROGRESS_CHANGED = 'shlink/orphanVisits/GET_ORPHAN_VISITS_PROGRESS_CHANGED'; export const GET_ORPHAN_VISITS_PROGRESS_CHANGED = 'shlink/orphanVisits/GET_ORPHAN_VISITS_PROGRESS_CHANGED';
export const GET_ORPHAN_VISITS_FALLBACK_TO_INTERVAL = 'shlink/orphanVisits/GET_ORPHAN_VISITS_FALLBACK_TO_INTERVAL'; export const GET_ORPHAN_VISITS_FALLBACK_TO_INTERVAL = 'shlink/orphanVisits/GET_ORPHAN_VISITS_FALLBACK_TO_INTERVAL';
/* eslint-enable padding-line-between-statements */
export interface OrphanVisitsAction extends Action<string> { export interface OrphanVisitsAction extends Action<string> {
visits: Visit[]; visits: Visit[];
@ -74,14 +72,14 @@ export const getOrphanVisits = (buildShlinkApiClient: ShlinkApiClientBuilder) =>
orphanVisitsType?: OrphanVisitType, orphanVisitsType?: OrphanVisitType,
doIntervalFallback = false, doIntervalFallback = false,
) => async (dispatch: Dispatch, getState: GetState) => { ) => async (dispatch: Dispatch, getState: GetState) => {
const { getOrphanVisits } = buildShlinkApiClient(getState); const { getOrphanVisits: getVisits } = buildShlinkApiClient(getState);
const visitsLoader = async (page: number, itemsPerPage: number) => getOrphanVisits({ ...query, page, itemsPerPage }) const visitsLoader = async (page: number, itemsPerPage: number) => getVisits({ ...query, page, itemsPerPage })
.then((result) => { .then((result) => {
const visits = result.data.filter((visit) => isOrphanVisit(visit) && matchesType(visit, orphanVisitsType)); const visits = result.data.filter((visit) => isOrphanVisit(visit) && matchesType(visit, orphanVisitsType));
return { ...result, data: visits }; return { ...result, data: visits };
}); });
const lastVisitLoader = lastVisitLoaderForLoader(doIntervalFallback, getOrphanVisits); const lastVisitLoader = lastVisitLoaderForLoader(doIntervalFallback, getVisits);
const shouldCancel = () => getState().orphanVisits.cancelLoad; const shouldCancel = () => getState().orphanVisits.cancelLoad;
const extraFinishActionData: Partial<OrphanVisitsAction> = { query }; const extraFinishActionData: Partial<OrphanVisitsAction> = { query };
const actionMap = { const actionMap = {

View file

@ -11,7 +11,6 @@ import { isBetween } from '../../utils/helpers/date';
import { getVisitsWithLoader, lastVisitLoaderForLoader } from './common'; import { getVisitsWithLoader, lastVisitLoaderForLoader } from './common';
import { CREATE_VISITS, CreateVisitsAction } from './visitCreation'; import { CREATE_VISITS, CreateVisitsAction } from './visitCreation';
/* eslint-disable padding-line-between-statements */
export const GET_SHORT_URL_VISITS_START = 'shlink/shortUrlVisits/GET_SHORT_URL_VISITS_START'; export const GET_SHORT_URL_VISITS_START = 'shlink/shortUrlVisits/GET_SHORT_URL_VISITS_START';
export const GET_SHORT_URL_VISITS_ERROR = 'shlink/shortUrlVisits/GET_SHORT_URL_VISITS_ERROR'; export const GET_SHORT_URL_VISITS_ERROR = 'shlink/shortUrlVisits/GET_SHORT_URL_VISITS_ERROR';
export const GET_SHORT_URL_VISITS = 'shlink/shortUrlVisits/GET_SHORT_URL_VISITS'; export const GET_SHORT_URL_VISITS = 'shlink/shortUrlVisits/GET_SHORT_URL_VISITS';
@ -19,7 +18,6 @@ export const GET_SHORT_URL_VISITS_LARGE = 'shlink/shortUrlVisits/GET_SHORT_URL_V
export const GET_SHORT_URL_VISITS_CANCEL = 'shlink/shortUrlVisits/GET_SHORT_URL_VISITS_CANCEL'; export const GET_SHORT_URL_VISITS_CANCEL = 'shlink/shortUrlVisits/GET_SHORT_URL_VISITS_CANCEL';
export const GET_SHORT_URL_VISITS_PROGRESS_CHANGED = 'shlink/shortUrlVisits/GET_SHORT_URL_VISITS_PROGRESS_CHANGED'; export const GET_SHORT_URL_VISITS_PROGRESS_CHANGED = 'shlink/shortUrlVisits/GET_SHORT_URL_VISITS_PROGRESS_CHANGED';
export const GET_SHORT_URL_VISITS_FALLBACK_TO_INTERVAL = 'shlink/shortUrlVisits/GET_SHORT_URL_VISITS_FALLBACK_TO_INTERVAL'; export const GET_SHORT_URL_VISITS_FALLBACK_TO_INTERVAL = 'shlink/shortUrlVisits/GET_SHORT_URL_VISITS_FALLBACK_TO_INTERVAL';
/* eslint-enable padding-line-between-statements */
export interface ShortUrlVisits extends VisitsInfo, ShortUrlIdentifier {} export interface ShortUrlVisits extends VisitsInfo, ShortUrlIdentifier {}
@ -80,14 +78,14 @@ export const getShortUrlVisits = (buildShlinkApiClient: ShlinkApiClientBuilder)
query: ShlinkVisitsParams = {}, query: ShlinkVisitsParams = {},
doIntervalFallback = false, doIntervalFallback = false,
) => async (dispatch: Dispatch, getState: GetState) => { ) => async (dispatch: Dispatch, getState: GetState) => {
const { getShortUrlVisits } = buildShlinkApiClient(getState); const { getShortUrlVisits: shlinkGetShortUrlVisits } = buildShlinkApiClient(getState);
const visitsLoader = async (page: number, itemsPerPage: number) => getShortUrlVisits( const visitsLoader = async (page: number, itemsPerPage: number) => shlinkGetShortUrlVisits(
shortCode, shortCode,
{ ...query, page, itemsPerPage }, { ...query, page, itemsPerPage },
); );
const lastVisitLoader = lastVisitLoaderForLoader( const lastVisitLoader = lastVisitLoaderForLoader(
doIntervalFallback, doIntervalFallback,
async (params) => getShortUrlVisits(shortCode, { ...params, domain: query.domain }), async (params) => shlinkGetShortUrlVisits(shortCode, { ...params, domain: query.domain }),
); );
const shouldCancel = () => getState().shortUrlVisits.cancelLoad; const shouldCancel = () => getState().shortUrlVisits.cancelLoad;
const extraFinishActionData: Partial<ShortUrlVisitsAction> = { shortCode, query, domain: query.domain }; const extraFinishActionData: Partial<ShortUrlVisitsAction> = { shortCode, query, domain: query.domain };

View file

@ -9,7 +9,6 @@ import { isBetween } from '../../utils/helpers/date';
import { getVisitsWithLoader, lastVisitLoaderForLoader } from './common'; import { getVisitsWithLoader, lastVisitLoaderForLoader } from './common';
import { CREATE_VISITS, CreateVisitsAction } from './visitCreation'; import { CREATE_VISITS, CreateVisitsAction } from './visitCreation';
/* eslint-disable padding-line-between-statements */
export const GET_TAG_VISITS_START = 'shlink/tagVisits/GET_TAG_VISITS_START'; export const GET_TAG_VISITS_START = 'shlink/tagVisits/GET_TAG_VISITS_START';
export const GET_TAG_VISITS_ERROR = 'shlink/tagVisits/GET_TAG_VISITS_ERROR'; export const GET_TAG_VISITS_ERROR = 'shlink/tagVisits/GET_TAG_VISITS_ERROR';
export const GET_TAG_VISITS = 'shlink/tagVisits/GET_TAG_VISITS'; export const GET_TAG_VISITS = 'shlink/tagVisits/GET_TAG_VISITS';
@ -17,7 +16,6 @@ export const GET_TAG_VISITS_LARGE = 'shlink/tagVisits/GET_TAG_VISITS_LARGE';
export const GET_TAG_VISITS_CANCEL = 'shlink/tagVisits/GET_TAG_VISITS_CANCEL'; export const GET_TAG_VISITS_CANCEL = 'shlink/tagVisits/GET_TAG_VISITS_CANCEL';
export const GET_TAG_VISITS_PROGRESS_CHANGED = 'shlink/tagVisits/GET_TAG_VISITS_PROGRESS_CHANGED'; export const GET_TAG_VISITS_PROGRESS_CHANGED = 'shlink/tagVisits/GET_TAG_VISITS_PROGRESS_CHANGED';
export const GET_TAG_VISITS_FALLBACK_TO_INTERVAL = 'shlink/tagVisits/GET_TAG_VISITS_FALLBACK_TO_INTERVAL'; export const GET_TAG_VISITS_FALLBACK_TO_INTERVAL = 'shlink/tagVisits/GET_TAG_VISITS_FALLBACK_TO_INTERVAL';
/* eslint-enable padding-line-between-statements */
export interface TagVisits extends VisitsInfo { export interface TagVisits extends VisitsInfo {
tag: string; tag: string;
@ -69,12 +67,12 @@ export const getTagVisits = (buildShlinkApiClient: ShlinkApiClientBuilder) => (
query: ShlinkVisitsParams = {}, query: ShlinkVisitsParams = {},
doIntervalFallback = false, doIntervalFallback = false,
) => async (dispatch: Dispatch, getState: GetState) => { ) => async (dispatch: Dispatch, getState: GetState) => {
const { getTagVisits } = buildShlinkApiClient(getState); const { getTagVisits: getVisits } = buildShlinkApiClient(getState);
const visitsLoader = async (page: number, itemsPerPage: number) => getTagVisits( const visitsLoader = async (page: number, itemsPerPage: number) => getVisits(
tag, tag,
{ ...query, page, itemsPerPage }, { ...query, page, itemsPerPage },
); );
const lastVisitLoader = lastVisitLoaderForLoader(doIntervalFallback, async (params) => getTagVisits(tag, params)); const lastVisitLoader = lastVisitLoaderForLoader(doIntervalFallback, async (params) => getVisits(tag, params));
const shouldCancel = () => getState().tagVisits.cancelLoad; const shouldCancel = () => getState().tagVisits.cancelLoad;
const extraFinishActionData: Partial<TagVisitsAction> = { tag, query }; const extraFinishActionData: Partial<TagVisitsAction> = { tag, query };
const actionMap = { const actionMap = {

View file

@ -6,11 +6,9 @@ import { buildReducer } from '../../utils/helpers/redux';
import { groupNewVisitsByType } from '../types/helpers'; import { groupNewVisitsByType } from '../types/helpers';
import { CREATE_VISITS, CreateVisitsAction } from './visitCreation'; import { CREATE_VISITS, CreateVisitsAction } from './visitCreation';
/* eslint-disable padding-line-between-statements */
export const GET_OVERVIEW_START = 'shlink/visitsOverview/GET_OVERVIEW_START'; export const GET_OVERVIEW_START = 'shlink/visitsOverview/GET_OVERVIEW_START';
export const GET_OVERVIEW_ERROR = 'shlink/visitsOverview/GET_OVERVIEW_ERROR'; export const GET_OVERVIEW_ERROR = 'shlink/visitsOverview/GET_OVERVIEW_ERROR';
export const GET_OVERVIEW = 'shlink/visitsOverview/GET_OVERVIEW'; export const GET_OVERVIEW = 'shlink/visitsOverview/GET_OVERVIEW';
/* eslint-enable padding-line-between-statements */
export interface VisitsOverview { export interface VisitsOverview {
visitsCount: number; visitsCount: number;

View file

@ -4,6 +4,7 @@ import { hasValue } from '../../utils/utils';
import { CityStats, NormalizedVisit, Stats, Visit, VisitsStats } from '../types'; import { CityStats, NormalizedVisit, Stats, Visit, VisitsStats } from '../types';
import { isNormalizedOrphanVisit, isOrphanVisit } from '../types/helpers'; import { isNormalizedOrphanVisit, isOrphanVisit } from '../types/helpers';
/* eslint-disable no-param-reassign */
const visitHasProperty = (visit: NormalizedVisit, propertyName: keyof NormalizedVisit) => const visitHasProperty = (visit: NormalizedVisit, propertyName: keyof NormalizedVisit) =>
!isNil(visit) && hasValue(visit[propertyName]); !isNil(visit) && hasValue(visit[propertyName]);
@ -49,7 +50,7 @@ const updateCitiesForMapForVisit = (citiesForMapStats: Record<string, CityStats>
latLong: [optionalNumericToNumber(latitude), optionalNumericToNumber(longitude)], latLong: [optionalNumericToNumber(latitude), optionalNumericToNumber(longitude)],
}; };
currentCity.count++; currentCity.count += 1;
citiesForMapStats[city] = currentCity; citiesForMapStats[city] = currentCity;
}; };

View file

@ -3,10 +3,10 @@ import { formatIsoDate } from '../../utils/helpers/date';
import { ShlinkVisitsParams } from '../../api/types'; import { ShlinkVisitsParams } from '../../api/types';
import { CreateVisit, NormalizedOrphanVisit, NormalizedVisit, OrphanVisit, Stats, Visit, VisitsParams } from './index'; import { CreateVisit, NormalizedOrphanVisit, NormalizedVisit, OrphanVisit, Stats, Visit, VisitsParams } from './index';
export const isOrphanVisit = (visit: Visit): visit is OrphanVisit => visit.hasOwnProperty('visitedUrl'); export const isOrphanVisit = (visit: Visit): visit is OrphanVisit => !!(visit as OrphanVisit).visitedUrl;
export const isNormalizedOrphanVisit = (visit: NormalizedVisit): visit is NormalizedOrphanVisit => export const isNormalizedOrphanVisit = (visit: NormalizedVisit): visit is NormalizedOrphanVisit =>
visit.hasOwnProperty('visitedUrl'); !!(visit as NormalizedOrphanVisit).visitedUrl;
export interface GroupedNewVisits { export interface GroupedNewVisits {
orphanVisits: CreateVisit[]; orphanVisits: CreateVisit[];
@ -14,7 +14,7 @@ export interface GroupedNewVisits {
} }
export const groupNewVisitsByType = pipe( export const groupNewVisitsByType = pipe(
groupBy((newVisit: CreateVisit) => isOrphanVisit(newVisit.visit) ? 'orphanVisits' : 'regularVisits'), groupBy((newVisit: CreateVisit) => (isOrphanVisit(newVisit.visit) ? 'orphanVisits' : 'regularVisits')),
// @ts-expect-error Type declaration on groupBy is not correct. It can return undefined props // @ts-expect-error Type declaration on groupBy is not correct. It can return undefined props
(result): GroupedNewVisits => ({ orphanVisits: [], regularVisits: [], ...result }), (result): GroupedNewVisits => ({ orphanVisits: [], regularVisits: [], ...result }),
); );

View file

@ -4,14 +4,14 @@ import { ShlinkApiError, ShlinkApiErrorProps } from '../../src/api/ShlinkApiErro
import { InvalidArgumentError, ProblemDetailsError } from '../../src/api/types'; import { InvalidArgumentError, ProblemDetailsError } from '../../src/api/types';
describe('<ShlinkApiError />', () => { describe('<ShlinkApiError />', () => {
let wrapper: ShallowWrapper; let commonWrapper: ShallowWrapper;
const createWrapper = (props: ShlinkApiErrorProps) => { const createWrapper = (props: ShlinkApiErrorProps) => {
wrapper = shallow(<ShlinkApiError {...props} />); commonWrapper = shallow(<ShlinkApiError {...props} />);
return wrapper; return commonWrapper;
}; };
afterEach(() => wrapper?.unmount()); afterEach(() => commonWrapper?.unmount());
it.each([ it.each([
[undefined, 'the fallback', 'the fallback'], [undefined, 'the fallback', 'the fallback'],

View file

@ -9,7 +9,7 @@ describe('<AppUpdateBanner />', () => {
let wrapper: ShallowWrapper; let wrapper: ShallowWrapper;
beforeEach(() => { beforeEach(() => {
wrapper = shallow(<AppUpdateBanner isOpen={true} toggle={toggle} forceUpdate={forceUpdate} />); wrapper = shallow(<AppUpdateBanner isOpen toggle={toggle} forceUpdate={forceUpdate} />);
}); });
afterEach(jest.clearAllMocks); afterEach(jest.clearAllMocks);

View file

@ -21,7 +21,7 @@ describe('<DeleteServerModal />', () => {
<DeleteServerModal <DeleteServerModal
server={Mock.of<ServerWithId>({ name: serverName })} server={Mock.of<ServerWithId>({ name: serverName })}
toggle={toggleMock} toggle={toggleMock}
isOpen={true} isOpen
deleteServer={deleteServerMock} deleteServer={deleteServerMock}
/>, />,
); );

View file

@ -22,7 +22,7 @@ describe('<QrCodeModal />', () => {
wrapper = shallow( wrapper = shallow(
<QrCodeModal <QrCodeModal
shortUrl={Mock.of<ShortUrl>({ shortUrl })} shortUrl={Mock.of<ShortUrl>({ shortUrl })}
isOpen={true} isOpen
toggle={() => {}} toggle={() => {}}
selectedServer={selectedServer} selectedServer={selectedServer}
/>, />,

View file

@ -16,7 +16,7 @@ describe('<TagCard />', () => {
<TagCard <TagCard
tag={{ tag, visits: 23257, shortUrls: 48 }} tag={{ tag, visits: 23257, shortUrls: 48 }}
selectedServer={Mock.of<ReachableServer>({ id: '1' })} selectedServer={Mock.of<ReachableServer>({ id: '1' })}
displayed={true} displayed
toggle={() => {}} toggle={() => {}}
/>, />,
); );

View file

@ -6,7 +6,7 @@ import { DropdownBtn, DropdownBtnProps } from '../../src/utils/DropdownBtn';
describe('<DropdownBtn />', () => { describe('<DropdownBtn />', () => {
let wrapper: ShallowWrapper; let wrapper: ShallowWrapper;
const createWrapper = (props: PropsWithChildren<DropdownBtnProps>) => { const createWrapper = (props: PropsWithChildren<DropdownBtnProps>) => {
wrapper = shallow(<DropdownBtn children={'foo'} {...props} />); wrapper = shallow(<DropdownBtn children="foo" {...props} />);
return wrapper; return wrapper;
}; };

View file

@ -36,10 +36,10 @@ describe('<Message />', () => {
expect(wrapper.find(FontAwesomeIcon)).toHaveLength(loading ? 1 : 0); expect(wrapper.find(FontAwesomeIcon)).toHaveLength(loading ? 1 : 0);
if (loading) { if (loading) {
expect(wrapper.find('span').text()).toContain(children ? children : 'Loading...'); expect(wrapper.find('span').text()).toContain(children || 'Loading...');
} else { } else {
expect(wrapper.find('span')).toHaveLength(0); expect(wrapper.find('span')).toHaveLength(0);
expect(wrapper.find('h3').text()).toContain(children ? children : ''); expect(wrapper.find('h3').text()).toContain(children || '');
} }
}); });

View file

@ -75,7 +75,7 @@ describe('date-types', () => {
}); });
describe('intervalToDateRange', () => { describe('intervalToDateRange', () => {
const formatted = (date?: Date | null): string | undefined => !date ? undefined : format(date, 'yyyy-MM-dd'); const formatted = (date?: Date | null): string | undefined => (!date ? undefined : format(date, 'yyyy-MM-dd'));
it.each([ it.each([
[undefined, undefined, undefined], [undefined, undefined, undefined],

View file

@ -2,7 +2,7 @@ import { Mock } from 'ts-mockery';
import LocalStorage from '../../../src/utils/services/LocalStorage'; import LocalStorage from '../../../src/utils/services/LocalStorage';
describe('LocalStorage', () => { describe('LocalStorage', () => {
const getItem = jest.fn((key) => key === 'shlink.foo' ? JSON.stringify({ foo: 'bar' }) : null); const getItem = jest.fn((key) => (key === 'shlink.foo' ? JSON.stringify({ foo: 'bar' }) : null));
const setItem = jest.fn(); const setItem = jest.fn();
const localStorageMock = Mock.of<Storage>({ getItem, setItem }); const localStorageMock = Mock.of<Storage>({ getItem, setItem });
let storage: LocalStorage; let storage: LocalStorage;

View file

@ -128,15 +128,25 @@ describe('<VisitsTable />', () => {
country: `Country_${index}`, country: `Country_${index}`,
}))); })));
expect(wrapper.find('tbody').find('tr').at(0).find('td').at(2).text()).toContain('Country_1'); expect(wrapper.find('tbody').find('tr').at(0).find('td')
.at(2)
.text()).toContain('Country_1');
wrapper.find('thead').find('th').at(1).simulate('click'); // Date column ASC wrapper.find('thead').find('th').at(1).simulate('click'); // Date column ASC
expect(wrapper.find('tbody').find('tr').at(0).find('td').at(2).text()).toContain('Country_9'); expect(wrapper.find('tbody').find('tr').at(0).find('td')
.at(2)
.text()).toContain('Country_9');
wrapper.find('thead').find('th').at(6).simulate('click'); // Referer column - ASC wrapper.find('thead').find('th').at(6).simulate('click'); // Referer column - ASC
expect(wrapper.find('tbody').find('tr').at(0).find('td').at(2).text()).toContain('Country_1'); expect(wrapper.find('tbody').find('tr').at(0).find('td')
.at(2)
.text()).toContain('Country_1');
wrapper.find('thead').find('th').at(6).simulate('click'); // Referer column - DESC wrapper.find('thead').find('th').at(6).simulate('click'); // Referer column - DESC
expect(wrapper.find('tbody').find('tr').at(0).find('td').at(2).text()).toContain('Country_9'); expect(wrapper.find('tbody').find('tr').at(0).find('td')
.at(2)
.text()).toContain('Country_9');
wrapper.find('thead').find('th').at(6).simulate('click'); // Referer column - reset wrapper.find('thead').find('th').at(6).simulate('click'); // Referer column - reset
expect(wrapper.find('tbody').find('tr').at(0).find('td').at(2).text()).toContain('Country_1'); expect(wrapper.find('tbody').find('tr').at(0).find('td')
.at(2)
.text()).toContain('Country_1');
}); });
it('filters list when writing in search box', () => { it('filters list when writing in search box', () => {

View file

@ -10,7 +10,7 @@ describe('<VisitsFilterDropdown />', () => {
wrapper = shallow( wrapper = shallow(
<VisitsFilterDropdown <VisitsFilterDropdown
isOrphanVisits={isOrphanVisits} isOrphanVisits={isOrphanVisits}
botsSupported={true} botsSupported
selected={selected} selected={selected}
onChange={onChange} onChange={onChange}
/>, />,

View file

@ -163,14 +163,14 @@ describe('tagVisitsReducer', () => {
beforeEach(jest.clearAllMocks); beforeEach(jest.clearAllMocks);
it('dispatches start and error when promise is rejected', async () => { it('dispatches start and error when promise is rejected', async () => {
const ShlinkApiClient = buildApiClientMock(Promise.reject({})); const shlinkApiClient = buildApiClientMock(Promise.reject(new Error()));
await getTagVisits(() => ShlinkApiClient)('foo')(dispatchMock, getState); await getTagVisits(() => shlinkApiClient)('foo')(dispatchMock, getState);
expect(dispatchMock).toHaveBeenCalledTimes(2); expect(dispatchMock).toHaveBeenCalledTimes(2);
expect(dispatchMock).toHaveBeenNthCalledWith(1, { type: GET_TAG_VISITS_START }); expect(dispatchMock).toHaveBeenNthCalledWith(1, { type: GET_TAG_VISITS_START });
expect(dispatchMock).toHaveBeenNthCalledWith(2, { type: GET_TAG_VISITS_ERROR }); expect(dispatchMock).toHaveBeenNthCalledWith(2, { type: GET_TAG_VISITS_ERROR });
expect(ShlinkApiClient.getTagVisits).toHaveBeenCalledTimes(1); expect(shlinkApiClient.getTagVisits).toHaveBeenCalledTimes(1);
}); });
it.each([ it.each([
@ -178,7 +178,7 @@ describe('tagVisitsReducer', () => {
[{}], [{}],
])('dispatches start and success when promise is resolved', async (query) => { ])('dispatches start and success when promise is resolved', async (query) => {
const visits = visitsMocks; const visits = visitsMocks;
const ShlinkApiClient = buildApiClientMock(Promise.resolve({ const shlinkApiClient = buildApiClientMock(Promise.resolve({
data: visitsMocks, data: visitsMocks,
pagination: { pagination: {
currentPage: 1, currentPage: 1,
@ -187,12 +187,12 @@ describe('tagVisitsReducer', () => {
}, },
})); }));
await getTagVisits(() => ShlinkApiClient)(tag, query)(dispatchMock, getState); await getTagVisits(() => shlinkApiClient)(tag, query)(dispatchMock, getState);
expect(dispatchMock).toHaveBeenCalledTimes(2); expect(dispatchMock).toHaveBeenCalledTimes(2);
expect(dispatchMock).toHaveBeenNthCalledWith(1, { type: GET_TAG_VISITS_START }); expect(dispatchMock).toHaveBeenNthCalledWith(1, { type: GET_TAG_VISITS_START });
expect(dispatchMock).toHaveBeenNthCalledWith(2, { type: GET_TAG_VISITS, visits, tag, query: query ?? {} }); expect(dispatchMock).toHaveBeenNthCalledWith(2, { type: GET_TAG_VISITS, visits, tag, query: query ?? {} });
expect(ShlinkApiClient.getTagVisits).toHaveBeenCalledTimes(1); expect(shlinkApiClient.getTagVisits).toHaveBeenCalledTimes(1);
}); });
it.each([ it.each([

View file

@ -8,9 +8,10 @@ describe('visitCreationReducer', () => {
const shortUrl = Mock.all<ShortUrl>(); const shortUrl = Mock.all<ShortUrl>();
const visit = Mock.all<Visit>(); const visit = Mock.all<Visit>();
it('just returns the action with proper type', () => it('just returns the action with proper type', () => {
expect(createNewVisits([{ shortUrl, visit }])).toEqual( expect(createNewVisits([{ shortUrl, visit }])).toEqual(
{ type: CREATE_VISITS, createdVisits: [{ shortUrl, visit }] }, { type: CREATE_VISITS, createdVisits: [{ shortUrl, visit }] },
)); );
});
}); });
}); });

View file

@ -110,7 +110,7 @@ describe('VisitsParser', () => {
const { referrers } = stats; const { referrers } = stats;
expect(referrers).toEqual({ expect(referrers).toEqual({
'Direct': 2, Direct: 2,
'google.com': 2, 'google.com': 2,
'm.facebook.com': 1, 'm.facebook.com': 1,
}); });
@ -120,9 +120,9 @@ describe('VisitsParser', () => {
const { countries } = stats; const { countries } = stats;
expect(countries).toEqual({ expect(countries).toEqual({
'Spain': 3, Spain: 3,
'United States': 1, 'United States': 1,
'Unknown': 1, Unknown: 1,
}); });
}); });
@ -130,9 +130,9 @@ describe('VisitsParser', () => {
const { cities } = stats; const { cities } = stats;
expect(cities).toEqual({ expect(cities).toEqual({
'Zaragoza': 2, Zaragoza: 2,
'New York': 1, 'New York': 1,
'Unknown': 2, Unknown: 2,
}); });
}); });
@ -144,7 +144,7 @@ describe('VisitsParser', () => {
const newYorkLong = 6758; const newYorkLong = 6758;
expect(citiesForMap).toEqual({ expect(citiesForMap).toEqual({
'Zaragoza': { Zaragoza: {
cityName: 'Zaragoza', cityName: 'Zaragoza',
count: 2, count: 2,
latLong: [zaragozaLat, zaragozaLong], latLong: [zaragozaLat, zaragozaLong],