mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2025-03-28 08:39:13 +03:00
Merge pull request #396 from acelaya-forks/feature/updated-deps
Feature/updated deps
This commit is contained in:
commit
fddba80b08
38 changed files with 3926 additions and 2317 deletions
16
.eslintrc
16
.eslintrc
|
@ -15,16 +15,10 @@
|
||||||
"setImmediate": true
|
"setImmediate": true
|
||||||
},
|
},
|
||||||
"rules": {
|
"rules": {
|
||||||
"max-len": ["error", {
|
"no-nonoctal-decimal-escape": "off",
|
||||||
"code": 120,
|
"@typescript-eslint/no-unsafe-assignment": "off",
|
||||||
"ignoreStrings": true,
|
"@typescript-eslint/no-unsafe-member-access": "off",
|
||||||
"ignoreTemplateLiterals": true,
|
"@typescript-eslint/ban-types": "off",
|
||||||
"ignoreComments": true
|
"@typescript-eslint/naming-convention": "off"
|
||||||
}],
|
|
||||||
"no-mixed-operators": "off",
|
|
||||||
"object-shorthand": "off",
|
|
||||||
"react/display-name": "off",
|
|
||||||
"react/react-in-jsx-scope": "off",
|
|
||||||
"@typescript-eslint/require-array-sort-compare": "off"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
1
.github/workflows/ci.yml
vendored
1
.github/workflows/ci.yml
vendored
|
@ -8,7 +8,6 @@ on:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
lint:
|
lint:
|
||||||
continue-on-error: true
|
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-20.04
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
|
|
|
@ -22,7 +22,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
* *Nothing*
|
* *Nothing*
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
* *Nothing*
|
* [#335](https://github.com/shlinkio/shlink-web-client/issues/335) Fixed linting errors.
|
||||||
|
|
||||||
|
|
||||||
## [3.0.1] - 2020-12-30
|
## [3.0.1] - 2020-12-30
|
||||||
|
|
5963
package-lock.json
generated
5963
package-lock.json
generated
File diff suppressed because it is too large
Load diff
75
package.json
75
package.json
|
@ -7,7 +7,7 @@
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"lint": "npm run lint:css && npm run lint:js",
|
"lint": "npm run lint:css && npm run lint:js",
|
||||||
"lint:js": "eslint --ext .js,.ts,.tsx src test scripts config",
|
"lint:js": "eslint --ext .js,.ts,.tsx src test",
|
||||||
"lint:js:fix": "npm run lint:js -- --fix",
|
"lint:js:fix": "npm run lint:js -- --fix",
|
||||||
"lint:css": "stylelint src/*.scss src/**/*.scss",
|
"lint:css": "stylelint src/*.scss src/**/*.scss",
|
||||||
"lint:css:fix": "npm run lint:css -- --fix",
|
"lint:css:fix": "npm run lint:css -- --fix",
|
||||||
|
@ -20,12 +20,12 @@
|
||||||
"mutate": "./node_modules/.bin/stryker run --concurrency 4"
|
"mutate": "./node_modules/.bin/stryker run --concurrency 4"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fortawesome/fontawesome-free": "^5.15.1",
|
"@fortawesome/fontawesome-free": "^5.15.2",
|
||||||
"@fortawesome/fontawesome-svg-core": "^1.2.32",
|
"@fortawesome/fontawesome-svg-core": "^1.2.34",
|
||||||
"@fortawesome/free-regular-svg-icons": "^5.15.1",
|
"@fortawesome/free-regular-svg-icons": "^5.15.2",
|
||||||
"@fortawesome/free-solid-svg-icons": "^5.15.1",
|
"@fortawesome/free-solid-svg-icons": "^5.15.2",
|
||||||
"@fortawesome/react-fontawesome": "^0.1.12",
|
"@fortawesome/react-fontawesome": "^0.1.14",
|
||||||
"axios": "^0.21.0",
|
"axios": "^0.21.1",
|
||||||
"bootstrap": "^4.6.0",
|
"bootstrap": "^4.6.0",
|
||||||
"bottlejs": "^2.0.0",
|
"bottlejs": "^2.0.0",
|
||||||
"bowser": "^2.11.0",
|
"bowser": "^2.11.0",
|
||||||
|
@ -33,61 +33,60 @@
|
||||||
"classnames": "^2.2.6",
|
"classnames": "^2.2.6",
|
||||||
"compare-versions": "^3.6.0",
|
"compare-versions": "^3.6.0",
|
||||||
"csvjson": "^5.1.0",
|
"csvjson": "^5.1.0",
|
||||||
"event-source-polyfill": "^1.0.21",
|
"event-source-polyfill": "^1.0.22",
|
||||||
"leaflet": "^1.7.1",
|
"leaflet": "^1.7.1",
|
||||||
"moment": "^2.29.1",
|
"moment": "^2.29.1",
|
||||||
"promise": "^8.1.0",
|
"promise": "^8.1.0",
|
||||||
"qs": "^6.9.4",
|
"qs": "^6.9.6",
|
||||||
"ramda": "^0.27.1",
|
"ramda": "^0.27.1",
|
||||||
"react": "^17.0.1",
|
"react": "^17.0.1",
|
||||||
"react-autosuggest": "^10.0.3",
|
"react-autosuggest": "^10.1.0",
|
||||||
"react-chartjs-2": "^2.11.1",
|
"react-chartjs-2": "^2.11.1",
|
||||||
"react-color": "^2.19.3",
|
"react-color": "^2.19.3",
|
||||||
"react-copy-to-clipboard": "^5.0.2",
|
"react-copy-to-clipboard": "^5.0.2",
|
||||||
"react-datepicker": "^3.3.0",
|
"react-datepicker": "^3.6.0",
|
||||||
"react-dom": "^17.0.1",
|
"react-dom": "^17.0.1",
|
||||||
"react-external-link": "^1.2.0",
|
"react-external-link": "^1.2.0",
|
||||||
"react-leaflet": "^3.0.2",
|
"react-leaflet": "^3.1.0",
|
||||||
"react-moment": "^1.0.0",
|
"react-moment": "^1.0.0",
|
||||||
"react-redux": "^7.2.2",
|
"react-redux": "^7.2.2",
|
||||||
"react-router-dom": "^5.2.0",
|
"react-router-dom": "^5.2.0",
|
||||||
"react-swipeable": "^6.0.0",
|
"react-swipeable": "^6.0.1",
|
||||||
"react-tagsinput": "^3.19.0",
|
"react-tagsinput": "^3.19.0",
|
||||||
"reactstrap": "^8.7.1",
|
"reactstrap": "^8.9.0",
|
||||||
"redux": "^4.0.5",
|
"redux": "^4.0.5",
|
||||||
"redux-localstorage-simple": "^2.3.1",
|
"redux-localstorage-simple": "^2.4.0",
|
||||||
"redux-thunk": "^2.3.0",
|
"redux-thunk": "^2.3.0",
|
||||||
"uuid": "^8.3.1"
|
"uuid": "^8.3.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.12.3",
|
"@babel/core": "^7.13.8",
|
||||||
"@babel/plugin-proposal-nullish-coalescing-operator": "^7.12.1",
|
"@babel/plugin-proposal-nullish-coalescing-operator": "^7.13.8",
|
||||||
"@babel/plugin-proposal-optional-chaining": "^7.12.1",
|
"@babel/plugin-proposal-optional-chaining": "^7.13.8",
|
||||||
"@shlinkio/eslint-config-js-coding-standard": "~1.1.0",
|
"@shlinkio/eslint-config-js-coding-standard": "~1.2.1",
|
||||||
"@stryker-mutator/core": "^4.3.1",
|
"@stryker-mutator/core": "^4.4.1",
|
||||||
"@stryker-mutator/jest-runner": "^4.3.1",
|
"@stryker-mutator/jest-runner": "^4.4.1",
|
||||||
"@stryker-mutator/typescript-checker": "^4.3.1",
|
"@stryker-mutator/typescript-checker": "^4.4.1",
|
||||||
"@svgr/webpack": "^5.4.0",
|
"@svgr/webpack": "^5.5.0",
|
||||||
"@types/chart.js": "^2.9.27",
|
"@types/chart.js": "^2.9.31",
|
||||||
"@types/classnames": "^2.2.11",
|
"@types/classnames": "^2.2.11",
|
||||||
"@types/enzyme": "^3.10.8",
|
"@types/enzyme": "^3.10.8",
|
||||||
"@types/jest": "^26.0.15",
|
"@types/jest": "^26.0.20",
|
||||||
"@types/leaflet": "^1.5.19",
|
"@types/leaflet": "^1.5.23",
|
||||||
"@types/moment": "^2.13.0",
|
"@types/moment": "^2.13.0",
|
||||||
"@types/qs": "^6.9.5",
|
"@types/qs": "^6.9.5",
|
||||||
"@types/ramda": "^0.27.32",
|
"@types/ramda": "^0.27.38",
|
||||||
"@types/react": "^16.9.56",
|
"@types/react": "^17.0.2",
|
||||||
"@types/react-autosuggest": "^10.0.1",
|
"@types/react-autosuggest": "^10.1.2",
|
||||||
"@types/react-color": "^3.0.4",
|
"@types/react-color": "^3.0.4",
|
||||||
"@types/react-copy-to-clipboard": "^4.3.0",
|
"@types/react-copy-to-clipboard": "^5.0.0",
|
||||||
"@types/react-datepicker": "^3.1.1",
|
"@types/react-datepicker": "^3.1.5",
|
||||||
"@types/react-dom": "^16.9.9",
|
"@types/react-dom": "^17.0.1",
|
||||||
"@types/react-leaflet": "^2.5.2",
|
"@types/react-leaflet": "^2.5.2",
|
||||||
"@types/react-redux": "^7.1.11",
|
"@types/react-redux": "^7.1.16",
|
||||||
"@types/react-router-dom": "^5.1.6",
|
"@types/react-router-dom": "^5.1.7",
|
||||||
"@types/react-tagsinput": "^3.19.7",
|
"@types/react-tagsinput": "^3.19.7",
|
||||||
"@types/uuid": "^8.3.0",
|
"@types/uuid": "^8.3.0",
|
||||||
"@typescript-eslint/parser": "^4.7.0",
|
|
||||||
"@wojtekmaj/enzyme-adapter-react-17": "^0.3.1",
|
"@wojtekmaj/enzyme-adapter-react-17": "^0.3.1",
|
||||||
"adm-zip": "^0.4.16",
|
"adm-zip": "^0.4.16",
|
||||||
"autoprefixer": "^10.0.2",
|
"autoprefixer": "^10.0.2",
|
||||||
|
@ -140,9 +139,9 @@
|
||||||
"stylelint-scss": "^3.18.0",
|
"stylelint-scss": "^3.18.0",
|
||||||
"sw-precache-webpack-plugin": "^1.0.0",
|
"sw-precache-webpack-plugin": "^1.0.0",
|
||||||
"terser-webpack-plugin": "^4.2.3",
|
"terser-webpack-plugin": "^4.2.3",
|
||||||
"ts-jest": "^26.4.4",
|
"ts-jest": "^26.5.2",
|
||||||
"ts-mockery": "^1.2.0",
|
"ts-mockery": "^1.2.0",
|
||||||
"typescript": "^4.0.5",
|
"typescript": "^4.2.2",
|
||||||
"url-loader": "^4.1.1",
|
"url-loader": "^4.1.1",
|
||||||
"webpack": "^4.44.2",
|
"webpack": "^4.44.2",
|
||||||
"webpack-dev-server": "^3.11.0",
|
"webpack-dev-server": "^3.11.0",
|
||||||
|
|
7
shlink-web-client.d.ts
vendored
7
shlink-web-client.d.ts
vendored
|
@ -1,5 +1,10 @@
|
||||||
declare module 'event-source-polyfill' {
|
declare module 'event-source-polyfill' {
|
||||||
export const EventSourcePolyfill: any;
|
declare class EventSourcePolyfill {
|
||||||
|
public onmessage?: ({ data }: { data: string }) => void;
|
||||||
|
public onerror?: ({ status }: { status: number }) => void;
|
||||||
|
public close: () => void;
|
||||||
|
public constructor(hubUrl: URL, options?: any);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
declare module 'csvjson' {
|
declare module 'csvjson' {
|
||||||
|
|
|
@ -20,7 +20,8 @@ type LazyActionMap = Record<string, Function>;
|
||||||
const bottle = new Bottle();
|
const bottle = new Bottle();
|
||||||
const { container } = bottle;
|
const { container } = bottle;
|
||||||
|
|
||||||
const lazyService = (container: IContainer, serviceName: string) => (...args: any[]) => container[serviceName](...args);
|
const lazyService = <T extends Function, K>(container: IContainer, serviceName: string) =>
|
||||||
|
(...args: any[]) => (container[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
|
||||||
|
|
|
@ -6,7 +6,7 @@ import { bindToMercureTopic } from './index';
|
||||||
|
|
||||||
export interface MercureBoundProps {
|
export interface MercureBoundProps {
|
||||||
createNewVisits: (createdVisits: CreateVisit[]) => void;
|
createNewVisits: (createdVisits: CreateVisit[]) => void;
|
||||||
loadMercureInfo: Function;
|
loadMercureInfo: () => void;
|
||||||
mercureInfo: MercureInfo;
|
mercureInfo: MercureInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { EventSourcePolyfill as EventSource } from 'event-source-polyfill';
|
import { EventSourcePolyfill as EventSource } from 'event-source-polyfill';
|
||||||
import { MercureInfo } from '../reducers/mercureInfo';
|
import { MercureInfo } from '../reducers/mercureInfo';
|
||||||
|
|
||||||
export const bindToMercureTopic = <T>(mercureInfo: MercureInfo, topics: string[], onMessage: (message: T) => void, onTokenExpired: Function) => { // eslint-disable-line max-len
|
export const bindToMercureTopic = <T>(mercureInfo: MercureInfo, topics: string[], onMessage: (message: T) => void, onTokenExpired: () => void) => { // eslint-disable-line max-len
|
||||||
const { mercureHubUrl, token, loading, error } = mercureInfo;
|
const { mercureHubUrl, token, loading, error } = mercureInfo;
|
||||||
|
|
||||||
if (loading || error || !mercureHubUrl) {
|
if (loading || error || !mercureHubUrl) {
|
||||||
|
@ -11,7 +11,7 @@ export const bindToMercureTopic = <T>(mercureInfo: MercureInfo, topics: string[]
|
||||||
const onEventSourceMessage = ({ data }: { data: string }) => onMessage(JSON.parse(data) as T);
|
const onEventSourceMessage = ({ data }: { data: string }) => onMessage(JSON.parse(data) as T);
|
||||||
const onEventSourceError = ({ status }: { status: number }) => status === 401 && onTokenExpired();
|
const onEventSourceError = ({ status }: { status: number }) => status === 401 && onTokenExpired();
|
||||||
|
|
||||||
const subscriptions: EventSource[] = topics.map((topic) => {
|
const subscriptions = topics.map((topic) => {
|
||||||
const hubUrl = new URL(mercureHubUrl);
|
const hubUrl = new URL(mercureHubUrl);
|
||||||
|
|
||||||
hubUrl.searchParams.append('topic', topic);
|
hubUrl.searchParams.append('topic', topic);
|
||||||
|
|
|
@ -7,7 +7,7 @@ import { faChevronRight as chevronIcon } from '@fortawesome/free-solid-svg-icons
|
||||||
import { ServerWithId } from './data';
|
import { ServerWithId } from './data';
|
||||||
import './ServersListGroup.scss';
|
import './ServersListGroup.scss';
|
||||||
|
|
||||||
interface ServersListGroup {
|
interface ServersListGroupProps {
|
||||||
servers: ServerWithId[];
|
servers: ServerWithId[];
|
||||||
embedded?: boolean;
|
embedded?: boolean;
|
||||||
}
|
}
|
||||||
|
@ -19,7 +19,7 @@ const ServerListItem = ({ id, name }: { id: string; name: string }) => (
|
||||||
</ListGroupItem>
|
</ListGroupItem>
|
||||||
);
|
);
|
||||||
|
|
||||||
const ServersListGroup: FC<ServersListGroup> = ({ servers, children, embedded = false }) => (
|
const ServersListGroup: FC<ServersListGroupProps> = ({ servers, children, embedded = false }) => (
|
||||||
<>
|
<>
|
||||||
{children && <h5 className="mb-md-3">{children}</h5>}
|
{children && <h5 className="mb-md-3">{children}</h5>}
|
||||||
{servers.length > 0 && (
|
{servers.length > 0 && (
|
||||||
|
|
|
@ -35,7 +35,7 @@ export default class ServersExporter {
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public readonly exportServers = async () => {
|
public readonly exportServers = async () => {
|
||||||
const servers = values(this.storage.get<ServersMap>('servers') || {}).map(dissoc('id'));
|
const servers = values(this.storage.get<ServersMap>('servers') ?? {}).map(dissoc('id'));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const csv = this.csvjson.toCSV(servers, {
|
const csv = this.csvjson.toCSV(servers, {
|
||||||
|
|
|
@ -7,12 +7,12 @@ import { prettify } from '../../utils/helpers/numbers';
|
||||||
import VisitStatsLink, { VisitStatsLinkProps } from './VisitStatsLink';
|
import VisitStatsLink, { VisitStatsLinkProps } from './VisitStatsLink';
|
||||||
import './ShortUrlVisitsCount.scss';
|
import './ShortUrlVisitsCount.scss';
|
||||||
|
|
||||||
export interface ShortUrlVisitsCount extends VisitStatsLinkProps {
|
interface ShortUrlVisitsCountProps extends VisitStatsLinkProps {
|
||||||
visitsCount: number;
|
visitsCount: number;
|
||||||
active?: boolean;
|
active?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ShortUrlVisitsCount = ({ visitsCount, shortUrl, selectedServer, active = false }: ShortUrlVisitsCount) => {
|
const ShortUrlVisitsCount = ({ visitsCount, shortUrl, selectedServer, active = false }: ShortUrlVisitsCountProps) => {
|
||||||
const maxVisits = shortUrl?.meta?.maxVisits;
|
const maxVisits = shortUrl?.meta?.maxVisits;
|
||||||
const visitsLink = (
|
const visitsLink = (
|
||||||
<VisitStatsLink selectedServer={selectedServer} shortUrl={shortUrl}>
|
<VisitStatsLink selectedServer={selectedServer} shortUrl={shortUrl}>
|
||||||
|
|
|
@ -6,9 +6,9 @@ import { SelectedServer } from '../servers/data';
|
||||||
import { boundToMercureHub } from '../mercure/helpers/boundToMercureHub';
|
import { boundToMercureHub } from '../mercure/helpers/boundToMercureHub';
|
||||||
import { Result } from '../utils/Result';
|
import { Result } from '../utils/Result';
|
||||||
import { ShlinkApiError } from '../api/ShlinkApiError';
|
import { ShlinkApiError } from '../api/ShlinkApiError';
|
||||||
|
import { Topics } from '../mercure/helpers/Topics';
|
||||||
import { TagsList as TagsListState } from './reducers/tagsList';
|
import { TagsList as TagsListState } from './reducers/tagsList';
|
||||||
import { TagCardProps } from './TagCard';
|
import { TagCardProps } from './TagCard';
|
||||||
import { Topics } from '../mercure/helpers/Topics';
|
|
||||||
|
|
||||||
const { ceil } = Math;
|
const { ceil } = Math;
|
||||||
const TAGS_GROUPS_AMOUNT = 4;
|
const TAGS_GROUPS_AMOUNT = 4;
|
||||||
|
|
|
@ -5,11 +5,11 @@ import { buildReducer } from '../../utils/helpers/redux';
|
||||||
import { ProblemDetailsError, ShlinkTags } from '../../api/types';
|
import { ProblemDetailsError, ShlinkTags } from '../../api/types';
|
||||||
import { GetState } from '../../container/types';
|
import { GetState } from '../../container/types';
|
||||||
import { ShlinkApiClientBuilder } from '../../api/services/ShlinkApiClientBuilder';
|
import { ShlinkApiClientBuilder } from '../../api/services/ShlinkApiClientBuilder';
|
||||||
import { TagStats } from '../data';
|
|
||||||
import { CreateVisit, Stats } from '../../visits/types';
|
import { CreateVisit, Stats } from '../../visits/types';
|
||||||
|
import { parseApiError } from '../../api/utils';
|
||||||
|
import { TagStats } from '../data';
|
||||||
import { DeleteTagAction, TAG_DELETED } from './tagDelete';
|
import { DeleteTagAction, TAG_DELETED } from './tagDelete';
|
||||||
import { EditTagAction, TAG_EDITED } from './tagEdit';
|
import { EditTagAction, TAG_EDITED } from './tagEdit';
|
||||||
import { parseApiError } from '../../api/utils';
|
|
||||||
|
|
||||||
/* eslint-disable padding-line-between-statements */
|
/* 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';
|
||||||
|
@ -34,7 +34,6 @@ interface ListTagsAction extends Action<string> {
|
||||||
stats: TagsStatsMap;
|
stats: TagsStatsMap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
interface ListTagsFailedAction extends Action<string> {
|
interface ListTagsFailedAction extends Action<string> {
|
||||||
errorData?: ProblemDetailsError;
|
errorData?: ProblemDetailsError;
|
||||||
}
|
}
|
||||||
|
@ -44,11 +43,11 @@ interface FilterTagsAction extends Action<string> {
|
||||||
}
|
}
|
||||||
|
|
||||||
type ListTagsCombinedAction = ListTagsAction
|
type ListTagsCombinedAction = ListTagsAction
|
||||||
& DeleteTagAction
|
& DeleteTagAction
|
||||||
& CreateVisitsAction
|
& CreateVisitsAction
|
||||||
& EditTagAction
|
& EditTagAction
|
||||||
& FilterTagsAction
|
& FilterTagsAction
|
||||||
& ListTagsFailedAction;
|
& ListTagsFailedAction;
|
||||||
|
|
||||||
const initialState = {
|
const initialState = {
|
||||||
tags: [],
|
tags: [],
|
||||||
|
@ -75,13 +74,13 @@ const increaseVisitsForTags = (tags: TagIncrease[], stats: TagsStatsMap) => tags
|
||||||
return stats;
|
return stats;
|
||||||
}, { ...stats });
|
}, { ...stats });
|
||||||
const calculateVisitsPerTag = (createdVisits: CreateVisit[]): TagIncrease[] => Object.entries(
|
const calculateVisitsPerTag = (createdVisits: CreateVisit[]): TagIncrease[] => Object.entries(
|
||||||
createdVisits.reduce((acc, { shortUrl }) => {
|
createdVisits.reduce<Stats>((acc, { shortUrl }) => {
|
||||||
shortUrl?.tags.forEach((tag) => {
|
shortUrl?.tags.forEach((tag) => {
|
||||||
acc[tag] = (acc[tag] || 0) + 1;
|
acc[tag] = (acc[tag] || 0) + 1;
|
||||||
});
|
});
|
||||||
|
|
||||||
return acc;
|
return acc;
|
||||||
}, {} as Stats),
|
}, {}),
|
||||||
);
|
);
|
||||||
|
|
||||||
export default buildReducer<TagsList, ListTagsCombinedAction>({
|
export default buildReducer<TagsList, ListTagsCombinedAction>({
|
||||||
|
|
|
@ -19,6 +19,7 @@ interface DatePropsInterface {
|
||||||
export type DateInputProps = DatePropsInterface & Omit<ReactDatePickerProps, keyof DatePropsInterface>;
|
export type DateInputProps = DatePropsInterface & Omit<ReactDatePickerProps, keyof DatePropsInterface>;
|
||||||
|
|
||||||
const transformProps = (props: DateInputProps): ReactDatePickerProps => ({
|
const transformProps = (props: DateInputProps): ReactDatePickerProps => ({
|
||||||
|
// @ts-expect-error The DatePicker type definition is wrong. It has a ref prop
|
||||||
...dissoc('ref', props),
|
...dissoc('ref', props),
|
||||||
endDate: props.endDate?.toDate(),
|
endDate: props.endDate?.toDate(),
|
||||||
maxDate: props.maxDate?.toDate(),
|
maxDate: props.maxDate?.toDate(),
|
||||||
|
@ -39,7 +40,7 @@ const DateInput = (props: DateInputProps) => {
|
||||||
{...transformProps(props)}
|
{...transformProps(props)}
|
||||||
dateFormat="yyyy-MM-dd"
|
dateFormat="yyyy-MM-dd"
|
||||||
className={classNames('date-input-container__input form-control', className)}
|
className={classNames('date-input-container__input form-control', className)}
|
||||||
// @ts-expect-error
|
// @ts-expect-error The DatePicker type definition is wrong. It has a ref prop
|
||||||
ref={ref}
|
ref={ref}
|
||||||
/>
|
/>
|
||||||
{showCalendarIcon && (
|
{showCalendarIcon && (
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { FC } from 'react';
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
import { InputType } from 'reactstrap/lib/Input';
|
import { InputType } from 'reactstrap/lib/Input';
|
||||||
|
|
||||||
interface FormGroupContainer {
|
interface FormGroupContainerProps {
|
||||||
value: string;
|
value: string;
|
||||||
onChange: (newValue: string) => void;
|
onChange: (newValue: string) => void;
|
||||||
id?: string;
|
id?: string;
|
||||||
|
@ -10,7 +10,7 @@ interface FormGroupContainer {
|
||||||
required?: boolean;
|
required?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const FormGroupContainer: FC<FormGroupContainer> = (
|
export const FormGroupContainer: FC<FormGroupContainerProps> = (
|
||||||
{ children, value, onChange, id = uuid(), type = 'text', required = true },
|
{ children, value, onChange, id = uuid(), type = 'text', required = true },
|
||||||
) => (
|
) => (
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
|
|
|
@ -7,7 +7,7 @@ import './SearchField.scss';
|
||||||
const DEFAULT_SEARCH_INTERVAL = 500;
|
const DEFAULT_SEARCH_INTERVAL = 500;
|
||||||
let timer: NodeJS.Timeout | null;
|
let timer: NodeJS.Timeout | null;
|
||||||
|
|
||||||
interface SearchField {
|
interface SearchFieldProps {
|
||||||
onChange: (value: string) => void;
|
onChange: (value: string) => void;
|
||||||
className?: string;
|
className?: string;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
|
@ -16,7 +16,7 @@ interface SearchField {
|
||||||
}
|
}
|
||||||
|
|
||||||
const SearchField = (
|
const SearchField = (
|
||||||
{ onChange, className, placeholder = 'Search...', large = true, noBorder = false }: SearchField,
|
{ onChange, className, placeholder = 'Search...', large = true, noBorder = false }: SearchFieldProps,
|
||||||
) => {
|
) => {
|
||||||
const [ searchTerm, setSearchTerm ] = useState('');
|
const [ searchTerm, setSearchTerm ] = useState('');
|
||||||
|
|
||||||
|
|
|
@ -61,7 +61,7 @@ export const intervalToDateRange = (dateInterval?: DateInterval): DateRange => {
|
||||||
case 'today':
|
case 'today':
|
||||||
return { startDate: moment().startOf('day'), endDate: moment() };
|
return { startDate: moment().startOf('day'), endDate: moment() };
|
||||||
case 'yesterday':
|
case 'yesterday':
|
||||||
const yesterday = moment().subtract(1, 'day');
|
const yesterday = moment().subtract(1, 'day'); // eslint-disable-line no-case-declarations
|
||||||
|
|
||||||
return { startDate: yesterday.startOf('day'), endDate: yesterday.endOf('day') };
|
return { startDate: yesterday.startOf('day'), endDate: yesterday.endOf('day') };
|
||||||
case 'last7Days':
|
case 'last7Days':
|
||||||
|
|
|
@ -26,5 +26,5 @@ export const renderDoughnutChartLabel = (
|
||||||
&& datasets?.[datasetIndex]?.data?.[index]
|
&& datasets?.[datasetIndex]?.data?.[index]
|
||||||
|| '';
|
|| '';
|
||||||
|
|
||||||
return `${datasetLabel}: ${prettify(Number(value))}`;
|
return `${datasetLabel}: ${prettify(Number(value))}`; // eslint-disable-line @typescript-eslint/no-base-to-string
|
||||||
};
|
};
|
||||||
|
|
|
@ -34,7 +34,7 @@ export const useToggle = (initialValue = false): ToggleResult => {
|
||||||
|
|
||||||
export const useSwipeable = (showSidebar: () => void, hideSidebar: () => void) => {
|
export const useSwipeable = (showSidebar: () => void, hideSidebar: () => void) => {
|
||||||
const swipeMenuIfNoModalExists = (callback: () => void) => (e: any) => {
|
const swipeMenuIfNoModalExists = (callback: () => void) => (e: any) => {
|
||||||
const swippedOnVisitsTable = (e.event.composedPath() as HTMLElement[]).some(
|
const swippedOnVisitsTable = (e.event.composedPath() as HTMLElement[]).some( // eslint-disable-line @typescript-eslint/no-unsafe-call
|
||||||
({ classList }) => classList?.contains('visits-table'),
|
({ classList }) => classList?.contains('visits-table'),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,7 @@ export default class ColorGenerator {
|
||||||
private readonly colors: Record<string, string>;
|
private readonly colors: Record<string, string>;
|
||||||
|
|
||||||
public constructor(private readonly storage: LocalStorage) {
|
public constructor(private readonly storage: LocalStorage) {
|
||||||
this.colors = this.storage.get<Record<string, string>>('colors') || {};
|
this.colors = this.storage.get<Record<string, string>>('colors') ?? {};
|
||||||
}
|
}
|
||||||
|
|
||||||
public readonly getColorForKey = (key: string) => {
|
public readonly getColorForKey = (key: string) => {
|
||||||
|
|
|
@ -4,10 +4,10 @@ const buildPath = (path: string) => `${PREFIX}.${path}`;
|
||||||
export default class LocalStorage {
|
export default class LocalStorage {
|
||||||
public constructor(private readonly localStorage: Storage) {}
|
public constructor(private readonly localStorage: Storage) {}
|
||||||
|
|
||||||
public readonly get = <T>(key: string): T => {
|
public readonly get = <T>(key: string): T | undefined => {
|
||||||
const item = this.localStorage.getItem(buildPath(key));
|
const item = this.localStorage.getItem(buildPath(key));
|
||||||
|
|
||||||
return item ? JSON.parse(item) : undefined;
|
return item ? JSON.parse(item) as T : undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
public readonly set = (key: string, value: any) => this.localStorage.setItem(buildPath(key), JSON.stringify(value));
|
public readonly set = (key: string, value: any) => this.localStorage.setItem(buildPath(key), JSON.stringify(value));
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
import VisitsHeader from './VisitsHeader';
|
import VisitsHeader from './VisitsHeader';
|
||||||
import './ShortUrlVisitsHeader.scss';
|
|
||||||
import { VisitsInfo } from './types';
|
import { VisitsInfo } from './types';
|
||||||
|
import './ShortUrlVisitsHeader.scss';
|
||||||
|
|
||||||
interface OrphanVisitsHeader {
|
interface OrphanVisitsHeaderProps {
|
||||||
orphanVisits: VisitsInfo;
|
orphanVisits: VisitsInfo;
|
||||||
goBack: () => void;
|
goBack: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const OrphanVisitsHeader = ({ orphanVisits, goBack }: OrphanVisitsHeader) => {
|
export const OrphanVisitsHeader = ({ orphanVisits, goBack }: OrphanVisitsHeaderProps) => {
|
||||||
const { visits } = orphanVisits;
|
const { visits } = orphanVisits;
|
||||||
|
|
||||||
return <VisitsHeader title="Orphan visits" goBack={goBack} visits={visits} />;
|
return <VisitsHeader title="Orphan visits" goBack={goBack} visits={visits} />;
|
||||||
|
|
|
@ -4,13 +4,13 @@ import VisitsHeader from './VisitsHeader';
|
||||||
import { TagVisits } from './reducers/tagVisits';
|
import { TagVisits } from './reducers/tagVisits';
|
||||||
import './ShortUrlVisitsHeader.scss';
|
import './ShortUrlVisitsHeader.scss';
|
||||||
|
|
||||||
interface TagVisitsHeader {
|
interface TagVisitsHeaderProps {
|
||||||
tagVisits: TagVisits;
|
tagVisits: TagVisits;
|
||||||
goBack: () => void;
|
goBack: () => void;
|
||||||
colorGenerator: ColorGenerator;
|
colorGenerator: ColorGenerator;
|
||||||
}
|
}
|
||||||
|
|
||||||
const TagVisitsHeader = ({ tagVisits, goBack, colorGenerator }: TagVisitsHeader) => {
|
const TagVisitsHeader = ({ tagVisits, goBack, colorGenerator }: TagVisitsHeaderProps) => {
|
||||||
const { visits, tag } = tagVisits;
|
const { visits, tag } = tagVisits;
|
||||||
|
|
||||||
const visitsStatsTitle = (
|
const visitsStatsTitle = (
|
||||||
|
|
|
@ -55,7 +55,7 @@ const generateGraphData = (
|
||||||
'#DCDCDC',
|
'#DCDCDC',
|
||||||
'#463730',
|
'#463730',
|
||||||
],
|
],
|
||||||
borderColor: isBarChart ? MAIN_COLOR : (isDarkThemeEnabled() ? PRIMARY_DARK_COLOR : PRIMARY_LIGHT_COLOR),
|
borderColor: isBarChart ? MAIN_COLOR : isDarkThemeEnabled() ? PRIMARY_DARK_COLOR : PRIMARY_LIGHT_COLOR,
|
||||||
borderWidth: 2,
|
borderWidth: 2,
|
||||||
},
|
},
|
||||||
highlightedData && {
|
highlightedData && {
|
||||||
|
@ -127,7 +127,7 @@ const DefaultChart = (
|
||||||
}, { ...stats }),
|
}, { ...stats }),
|
||||||
);
|
);
|
||||||
const highlightedData = statsAreDefined(highlightedStats) ? fillTheGaps(highlightedStats, labels) : undefined;
|
const highlightedData = statsAreDefined(highlightedStats) ? fillTheGaps(highlightedStats, labels) : undefined;
|
||||||
const [ chartRef, setChartRef ] = useState<HorizontalBar | Doughnut | undefined>()
|
const [ chartRef, setChartRef ] = useState<HorizontalBar | Doughnut | undefined>();
|
||||||
|
|
||||||
const options: ChartOptions = {
|
const options: ChartOptions = {
|
||||||
legend: { display: false },
|
legend: { display: false },
|
||||||
|
@ -137,7 +137,6 @@ const DefaultChart = (
|
||||||
{
|
{
|
||||||
ticks: {
|
ticks: {
|
||||||
beginAtZero: true,
|
beginAtZero: true,
|
||||||
// @ts-expect-error
|
|
||||||
precision: 0,
|
precision: 0,
|
||||||
callback: prettify,
|
callback: prettify,
|
||||||
max,
|
max,
|
||||||
|
|
|
@ -81,17 +81,18 @@ const groupVisitsByStep = (step: Step, visits: NormalizedVisit[]): Stats => visi
|
||||||
{},
|
{},
|
||||||
);
|
);
|
||||||
|
|
||||||
const visitsToDatasetGroups = (step: Step, visits: NormalizedVisit[]) => visits.reduce(
|
const visitsToDatasetGroups = (step: Step, visits: NormalizedVisit[]) =>
|
||||||
(acc, visit) => {
|
visits.reduce<Record<string, NormalizedVisit[]>>(
|
||||||
const key = STEP_TO_DATE_FORMAT[step](visit.date);
|
(acc, visit) => {
|
||||||
|
const key = STEP_TO_DATE_FORMAT[step](visit.date);
|
||||||
|
|
||||||
acc[key] = acc[key] ?? [];
|
acc[key] = acc[key] ?? [];
|
||||||
acc[key].push(visit);
|
acc[key].push(visit);
|
||||||
|
|
||||||
return acc;
|
return acc;
|
||||||
},
|
},
|
||||||
{} as Record<string, NormalizedVisit[]>,
|
{},
|
||||||
);
|
);
|
||||||
|
|
||||||
const generateLabels = (step: Step, visits: NormalizedVisit[]): string[] => {
|
const generateLabels = (step: Step, visits: NormalizedVisit[]): string[] => {
|
||||||
const unit = STEP_TO_DATE_UNIT_MAP[step];
|
const unit = STEP_TO_DATE_UNIT_MAP[step];
|
||||||
|
@ -186,7 +187,6 @@ const LineChartCard = (
|
||||||
{
|
{
|
||||||
ticks: {
|
ticks: {
|
||||||
beginAtZero: true,
|
beginAtZero: true,
|
||||||
// @ts-expect-error
|
|
||||||
precision: 0,
|
precision: 0,
|
||||||
callback: prettify,
|
callback: prettify,
|
||||||
},
|
},
|
||||||
|
|
|
@ -9,7 +9,7 @@ import { Stats, StatsRow } from '../types';
|
||||||
import GraphCard from './GraphCard';
|
import GraphCard from './GraphCard';
|
||||||
import { DefaultChartProps } from './DefaultChart';
|
import { DefaultChartProps } from './DefaultChart';
|
||||||
|
|
||||||
const toLowerIfString = (value: any) => type(value) === 'String' ? toLower(value) : value;
|
const toLowerIfString = (value: any) => type(value) === 'String' ? toLower(value) : value; // eslint-disable-line @typescript-eslint/no-unsafe-return
|
||||||
const pickKeyFromPair = ([ key ]: StatsRow) => key;
|
const pickKeyFromPair = ([ key ]: StatsRow) => key;
|
||||||
const pickValueFromPair = ([ , value ]: StatsRow) => value;
|
const pickValueFromPair = ([ , value ]: StatsRow) => value;
|
||||||
|
|
||||||
|
|
|
@ -10,5 +10,6 @@ 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
|
||||||
(result): GroupedNewVisits => ({ orphanVisits: [], regularVisits: [], ...result }),
|
(result): GroupedNewVisits => ({ orphanVisits: [], regularVisits: [], ...result }),
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { AxiosInstance, AxiosRequestConfig } from 'axios';
|
import { AxiosInstance, AxiosRequestConfig } from 'axios';
|
||||||
|
import { Mock } from 'ts-mockery';
|
||||||
import ShlinkApiClient from '../../../src/api/services/ShlinkApiClient';
|
import ShlinkApiClient from '../../../src/api/services/ShlinkApiClient';
|
||||||
import { OptionalString } from '../../../src/utils/utils';
|
import { OptionalString } from '../../../src/utils/utils';
|
||||||
import { Mock } from 'ts-mockery';
|
|
||||||
import { ShlinkDomain, ShlinkVisitsOverview } from '../../../src/api/types';
|
import { ShlinkDomain, ShlinkVisitsOverview } from '../../../src/api/types';
|
||||||
import { ShortUrl } from '../../../src/short-urls/data';
|
import { ShortUrl } from '../../../src/short-urls/data';
|
||||||
import { Visit } from '../../../src/visits/types';
|
import { Visit } from '../../../src/visits/types';
|
||||||
|
@ -49,7 +49,7 @@ describe('ShlinkApiClient', () => {
|
||||||
const { createShortUrl } = new ShlinkApiClient(axiosSpy, '', '');
|
const { createShortUrl } = new ShlinkApiClient(axiosSpy, '', '');
|
||||||
|
|
||||||
await createShortUrl(
|
await createShortUrl(
|
||||||
// @ts-expect-error
|
// @ts-expect-error in case maxVisits is null, it needs to be ignored as if it was undefined
|
||||||
{ longUrl: 'bar', customSlug: undefined, maxVisits: null },
|
{ longUrl: 'bar', customSlug: undefined, maxVisits: null },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -145,7 +145,7 @@ describe('ShlinkApiClient', () => {
|
||||||
maxVisits: 50,
|
maxVisits: 50,
|
||||||
validSince: '2025-01-01T10:00:00+01:00',
|
validSince: '2025-01-01T10:00:00+01:00',
|
||||||
};
|
};
|
||||||
const expectedResp = Mock.of<ShortUrl>()
|
const expectedResp = Mock.of<ShortUrl>();
|
||||||
const axiosSpy = createAxiosMock({ data: expectedResp });
|
const axiosSpy = createAxiosMock({ data: expectedResp });
|
||||||
const { updateShortUrlMeta } = new ShlinkApiClient(axiosSpy, '', '');
|
const { updateShortUrlMeta } = new ShlinkApiClient(axiosSpy, '', '');
|
||||||
|
|
||||||
|
@ -259,7 +259,7 @@ describe('ShlinkApiClient', () => {
|
||||||
|
|
||||||
describe('listDomains', () => {
|
describe('listDomains', () => {
|
||||||
it('returns domains', async () => {
|
it('returns domains', async () => {
|
||||||
const expectedData = [Mock.all<ShlinkDomain>(), Mock.all<ShlinkDomain>()];
|
const expectedData = [ Mock.all<ShlinkDomain>(), Mock.all<ShlinkDomain>() ];
|
||||||
const resp = {
|
const resp = {
|
||||||
domains: { data: expectedData },
|
domains: { data: expectedData },
|
||||||
};
|
};
|
||||||
|
|
|
@ -46,7 +46,7 @@ describe('ShlinkApiClientBuilder', () => {
|
||||||
const apiKey = 'apiKey';
|
const apiKey = 'apiKey';
|
||||||
const apiClient = buildShlinkApiClient(axiosMock)(server({ url, apiKey }));
|
const apiClient = buildShlinkApiClient(axiosMock)(server({ url, apiKey }));
|
||||||
|
|
||||||
expect(apiClient['baseUrl']).toEqual(url); // eslint-disable-line dot-notation
|
expect(apiClient['baseUrl']).toEqual(url); // eslint-disable-line @typescript-eslint/dot-notation
|
||||||
expect(apiClient['apiKey']).toEqual(apiKey); // eslint-disable-line dot-notation
|
expect(apiClient['apiKey']).toEqual(apiKey); // eslint-disable-line @typescript-eslint/dot-notation
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { EventSourcePolyfill as EventSource } from 'event-source-polyfill';
|
import { EventSourcePolyfill } from 'event-source-polyfill';
|
||||||
import { Mock } from 'ts-mockery';
|
import { Mock } from 'ts-mockery';
|
||||||
import { identity } from 'ramda';
|
import { identity } from 'ramda';
|
||||||
import { bindToMercureTopic } from '../../../src/mercure/helpers';
|
import { bindToMercureTopic } from '../../../src/mercure/helpers';
|
||||||
|
@ -20,9 +20,9 @@ describe('helpers', () => {
|
||||||
[ Mock.of<MercureInfo>({ loading: false, error: false, mercureHubUrl: undefined }) ],
|
[ Mock.of<MercureInfo>({ loading: false, error: false, mercureHubUrl: undefined }) ],
|
||||||
[ Mock.of<MercureInfo>({ loading: true, error: true, mercureHubUrl: undefined }) ],
|
[ Mock.of<MercureInfo>({ loading: true, error: true, mercureHubUrl: undefined }) ],
|
||||||
])('does not bind an EventSource when loading, error or no hub URL', (mercureInfo) => {
|
])('does not bind an EventSource when loading, error or no hub URL', (mercureInfo) => {
|
||||||
bindToMercureTopic(mercureInfo, [ '' ], identity, identity);
|
bindToMercureTopic(mercureInfo, [ '' ], identity, () => {});
|
||||||
|
|
||||||
expect(EventSource).not.toHaveBeenCalled();
|
expect(EventSourcePolyfill).not.toHaveBeenCalled();
|
||||||
expect(onMessage).not.toHaveBeenCalled();
|
expect(onMessage).not.toHaveBeenCalled();
|
||||||
expect(onTokenExpired).not.toHaveBeenCalled();
|
expect(onTokenExpired).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
@ -42,16 +42,16 @@ describe('helpers', () => {
|
||||||
token,
|
token,
|
||||||
}, [ topic ], onMessage, onTokenExpired);
|
}, [ topic ], onMessage, onTokenExpired);
|
||||||
|
|
||||||
expect(EventSource).toHaveBeenCalledWith(hubUrl, {
|
expect(EventSourcePolyfill).toHaveBeenCalledWith(hubUrl, {
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${token}`,
|
Authorization: `Bearer ${token}`,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const [ es ] = EventSource.mock.instances;
|
const [ es ] = (EventSourcePolyfill as any).mock.instances as EventSourcePolyfill[];
|
||||||
|
|
||||||
es.onmessage({ data: '{"foo": "bar"}' });
|
es.onmessage?.({ data: '{"foo": "bar"}' });
|
||||||
es.onerror({ status: 401 });
|
es.onerror?.({ status: 401 });
|
||||||
expect(onMessage).toHaveBeenCalledWith({ foo: 'bar' });
|
expect(onMessage).toHaveBeenCalledWith({ foo: 'bar' });
|
||||||
expect(onTokenExpired).toHaveBeenCalled();
|
expect(onTokenExpired).toHaveBeenCalled();
|
||||||
|
|
||||||
|
|
|
@ -9,16 +9,15 @@ describe('ServersExporter', () => {
|
||||||
click: jest.fn(),
|
click: jest.fn(),
|
||||||
style: {},
|
style: {},
|
||||||
});
|
});
|
||||||
|
const appendChild = jest.fn();
|
||||||
|
const removeChild = jest.fn();
|
||||||
const createWindowMock = (isIe10 = true) => Mock.of<Window>({
|
const createWindowMock = (isIe10 = true) => Mock.of<Window>({
|
||||||
navigator: {
|
navigator: {
|
||||||
msSaveBlob: isIe10 ? jest.fn() : undefined,
|
msSaveBlob: isIe10 ? jest.fn() : undefined,
|
||||||
},
|
},
|
||||||
document: {
|
document: {
|
||||||
createElement: jest.fn(() => createLinkMock()),
|
createElement: jest.fn(() => createLinkMock()),
|
||||||
body: {
|
body: { appendChild, removeChild },
|
||||||
appendChild: jest.fn(),
|
|
||||||
removeChild: jest.fn(),
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const storageMock = Mock.of<LocalStorage>({
|
const storageMock = Mock.of<LocalStorage>({
|
||||||
|
@ -33,15 +32,14 @@ describe('ServersExporter', () => {
|
||||||
},
|
},
|
||||||
})),
|
})),
|
||||||
});
|
});
|
||||||
const createCsvjsonMock = (throwError = false) => Mock.of<CsvJson>({
|
const erroneousToCsv = jest.fn(() => {
|
||||||
toCSV: jest.fn(() => {
|
throw new Error('');
|
||||||
if (throwError) {
|
|
||||||
throw new Error('');
|
|
||||||
}
|
|
||||||
|
|
||||||
return '';
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
|
const createCsvjsonMock = (throwError = false) => Mock.of<CsvJson>({
|
||||||
|
toCSV: throwError ? erroneousToCsv : jest.fn(() => ''),
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(jest.clearAllMocks);
|
||||||
|
|
||||||
describe('exportServers', () => {
|
describe('exportServers', () => {
|
||||||
let originalConsole: Console;
|
let originalConsole: Console;
|
||||||
|
@ -61,12 +59,11 @@ describe('ServersExporter', () => {
|
||||||
it('logs an error if something fails', () => {
|
it('logs an error if something fails', () => {
|
||||||
const csvjsonMock = createCsvjsonMock(true);
|
const csvjsonMock = createCsvjsonMock(true);
|
||||||
const exporter = new ServersExporter(storageMock, createWindowMock(), csvjsonMock);
|
const exporter = new ServersExporter(storageMock, createWindowMock(), csvjsonMock);
|
||||||
const { toCSV } = csvjsonMock;
|
|
||||||
|
|
||||||
exporter.exportServers();
|
exporter.exportServers();
|
||||||
|
|
||||||
expect(error).toHaveBeenCalledTimes(1);
|
expect(error).toHaveBeenCalledTimes(1);
|
||||||
expect(toCSV).toHaveBeenCalledTimes(1);
|
expect(erroneousToCsv).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('makes use of msSaveBlob API when available', () => {
|
it('makes use of msSaveBlob API when available', () => {
|
||||||
|
@ -84,8 +81,7 @@ describe('ServersExporter', () => {
|
||||||
it('makes use of download link API when available', () => {
|
it('makes use of download link API when available', () => {
|
||||||
const windowMock = createWindowMock(false);
|
const windowMock = createWindowMock(false);
|
||||||
const exporter = new ServersExporter(storageMock, windowMock, createCsvjsonMock());
|
const exporter = new ServersExporter(storageMock, windowMock, createCsvjsonMock());
|
||||||
const { document: { createElement, body } } = windowMock;
|
const { document: { createElement } } = windowMock;
|
||||||
const { appendChild, removeChild } = body;
|
|
||||||
|
|
||||||
exporter.exportServers();
|
exporter.exportServers();
|
||||||
|
|
||||||
|
|
|
@ -10,7 +10,9 @@ describe('ServersImporter', () => {
|
||||||
const readAsText = jest.fn();
|
const readAsText = jest.fn();
|
||||||
const fileReaderMock = Mock.of<FileReader>({
|
const fileReaderMock = Mock.of<FileReader>({
|
||||||
readAsText,
|
readAsText,
|
||||||
addEventListener: (_eventName: string, listener: Function) => listener({ target: { result: '' } }),
|
addEventListener: (_eventName: string, listener: (e: ProgressEvent<FileReader>) => void) => listener(
|
||||||
|
Mock.of<ProgressEvent<FileReader>>({ target: { result: '' } }),
|
||||||
|
),
|
||||||
});
|
});
|
||||||
const importer = new ServersImporter(csvjsonMock, () => fileReaderMock);
|
const importer = new ServersImporter(csvjsonMock, () => fileReaderMock);
|
||||||
|
|
||||||
|
|
|
@ -69,7 +69,7 @@ describe('<EditTagsModal />', () => {
|
||||||
[ undefined ],
|
[ undefined ],
|
||||||
[ null ],
|
[ null ],
|
||||||
[ 'example.com' ],
|
[ 'example.com' ],
|
||||||
// @ts-expect-error
|
// @ts-expect-error Type declaration is not correct, which makes "done" function not being properly detected
|
||||||
])('saves tags when save button is clicked', (domain: OptionalString, done: jest.DoneCallback) => {
|
])('saves tags when save button is clicked', (domain: OptionalString, done: jest.DoneCallback) => {
|
||||||
const wrapper = createWrapper({
|
const wrapper = createWrapper({
|
||||||
shortCode,
|
shortCode,
|
||||||
|
|
|
@ -60,7 +60,7 @@ describe('<ShortUrlsRowMenu />', () => {
|
||||||
const wrapper = createWrapper();
|
const wrapper = createWrapper();
|
||||||
|
|
||||||
expect(wrapper.find(modalComponent).prop('isOpen')).toEqual(false);
|
expect(wrapper.find(modalComponent).prop('isOpen')).toEqual(false);
|
||||||
wrapper.find(modalComponent).prop('toggle')();
|
(wrapper.find(modalComponent).prop('toggle') as Function)();
|
||||||
expect(wrapper.find(modalComponent).prop('isOpen')).toEqual(true);
|
expect(wrapper.find(modalComponent).prop('isOpen')).toEqual(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -25,7 +25,7 @@ describe('<DefaultChart />', () => {
|
||||||
|
|
||||||
const { labels, datasets } = doughnut.prop('data') as any;
|
const { labels, datasets } = doughnut.prop('data') as any;
|
||||||
const [{ title, data, backgroundColor, borderColor }] = datasets;
|
const [{ title, data, backgroundColor, borderColor }] = datasets;
|
||||||
const { legend, legendCallback, scales } = doughnut.prop('options') ?? {};
|
const { legend, scales, ...options } = doughnut.prop('options') ?? {};
|
||||||
|
|
||||||
expect(title).toEqual('The chart');
|
expect(title).toEqual('The chart');
|
||||||
expect(labels).toEqual(keys(stats));
|
expect(labels).toEqual(keys(stats));
|
||||||
|
@ -46,7 +46,7 @@ describe('<DefaultChart />', () => {
|
||||||
]);
|
]);
|
||||||
expect(borderColor).toEqual('white');
|
expect(borderColor).toEqual('white');
|
||||||
expect(legend).toEqual({ display: false });
|
expect(legend).toEqual({ display: false });
|
||||||
expect(typeof legendCallback).toEqual('function');
|
expect(typeof options.legendCallback).toEqual('function');
|
||||||
expect(scales).toBeUndefined();
|
expect(scales).toBeUndefined();
|
||||||
expect(cols).toHaveLength(2);
|
expect(cols).toHaveLength(2);
|
||||||
});
|
});
|
||||||
|
@ -61,12 +61,12 @@ describe('<DefaultChart />', () => {
|
||||||
expect(horizontal).toHaveLength(1);
|
expect(horizontal).toHaveLength(1);
|
||||||
|
|
||||||
const { datasets: [{ backgroundColor, borderColor }] } = horizontal.prop('data') as any;
|
const { datasets: [{ backgroundColor, borderColor }] } = horizontal.prop('data') as any;
|
||||||
const { legend, legendCallback, scales } = horizontal.prop('options') ?? {};
|
const { legend, scales, ...options } = horizontal.prop('options') ?? {};
|
||||||
|
|
||||||
expect(backgroundColor).toEqual(MAIN_COLOR_ALPHA);
|
expect(backgroundColor).toEqual(MAIN_COLOR_ALPHA);
|
||||||
expect(borderColor).toEqual(MAIN_COLOR);
|
expect(borderColor).toEqual(MAIN_COLOR);
|
||||||
expect(legend).toEqual({ display: false });
|
expect(legend).toEqual({ display: false });
|
||||||
expect(legendCallback).toEqual(false);
|
expect(typeof options.legendCallback).toEqual('boolean');
|
||||||
expect(scales).toEqual({
|
expect(scales).toEqual({
|
||||||
xAxes: [
|
xAxes: [
|
||||||
{
|
{
|
||||||
|
|
|
@ -91,14 +91,14 @@ describe('visitsOverviewReducer', () => {
|
||||||
|
|
||||||
it('dispatches start and success when promise is resolved', async () => {
|
it('dispatches start and success when promise is resolved', async () => {
|
||||||
const resolvedOverview = Mock.of<ShlinkVisitsOverview>({ visitsCount: 50 });
|
const resolvedOverview = Mock.of<ShlinkVisitsOverview>({ visitsCount: 50 });
|
||||||
const ShlinkApiClient = buildApiClientMock(Promise.resolve(resolvedOverview));
|
const shlinkApiClient = buildApiClientMock(Promise.resolve(resolvedOverview));
|
||||||
|
|
||||||
await loadVisitsOverview(() => ShlinkApiClient)()(dispatchMock, getState);
|
await loadVisitsOverview(() => shlinkApiClient)()(dispatchMock, getState);
|
||||||
|
|
||||||
expect(dispatchMock).toHaveBeenCalledTimes(2);
|
expect(dispatchMock).toHaveBeenCalledTimes(2);
|
||||||
expect(dispatchMock).toHaveBeenNthCalledWith(1, { type: GET_OVERVIEW_START });
|
expect(dispatchMock).toHaveBeenNthCalledWith(1, { type: GET_OVERVIEW_START });
|
||||||
expect(dispatchMock).toHaveBeenNthCalledWith(2, { type: GET_OVERVIEW, visitsCount: 50 });
|
expect(dispatchMock).toHaveBeenNthCalledWith(2, { type: GET_OVERVIEW, visitsCount: 50 });
|
||||||
expect(ShlinkApiClient.getVisitsOverview).toHaveBeenCalledTimes(1);
|
expect(shlinkApiClient.getVisitsOverview).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
|
import { Mock } from 'ts-mockery';
|
||||||
import { GroupedNewVisits, groupNewVisitsByType } from '../../../src/visits/types/helpers';
|
import { GroupedNewVisits, groupNewVisitsByType } from '../../../src/visits/types/helpers';
|
||||||
import { CreateVisit, OrphanVisit, Visit } from '../../../src/visits/types';
|
import { CreateVisit, OrphanVisit, Visit } from '../../../src/visits/types';
|
||||||
import { Mock } from 'ts-mockery';
|
|
||||||
|
|
||||||
describe('visitsTypeHelpers', () => {
|
describe('visitsTypeHelpers', () => {
|
||||||
describe('groupNewVisitsByType', () => {
|
describe('groupNewVisitsByType', () => {
|
||||||
|
|
Loading…
Add table
Reference in a new issue