Move some modules from src to shlink-web-component

This commit is contained in:
Alejandro Celaya 2023-07-27 22:23:46 +02:00
parent 0169060de7
commit 275745fd3a
51 changed files with 212 additions and 133 deletions

View file

@ -4,10 +4,10 @@ import classNames from 'classnames';
import type { FC } from 'react'; import type { FC } from 'react';
import { useEffect } from 'react'; import { useEffect } from 'react';
import { Navigate, Route, Routes, useLocation } from 'react-router-dom'; import { Navigate, Route, Routes, useLocation } from 'react-router-dom';
import { AsideMenu } from '../src/common/AsideMenu';
import { NotFound } from '../src/common/NotFound'; import { NotFound } from '../src/common/NotFound';
import { useSwipeable, useToggle } from '../src/utils/helpers/hooks'; import { AsideMenu } from './common/AsideMenu';
import { useFeature } from './utils/features'; import { useFeature } from './utils/features';
import { useSwipeable, useToggle } from './utils/helpers/hooks';
import { useRoutesPrefix } from './utils/routesPrefix'; import { useRoutesPrefix } from './utils/routesPrefix';
export const Main = ( export const Main = (

View file

@ -1,5 +1,5 @@
@import '../utils/base'; @import '../../src/utils/base';
@import '../utils/mixins/vertical-align'; @import '../../src/utils/mixins/vertical-align';
.aside-menu { .aside-menu {
width: $asideMenuWidth; width: $asideMenuWidth;

View file

@ -1,19 +1,17 @@
import type { IContainer } from 'bottlejs'; import type { IContainer } from 'bottlejs';
import Bottle from 'bottlejs'; import Bottle from 'bottlejs';
import { pick } from 'ramda'; import { pick } from 'ramda';
import { connect as reduxConnect } from 'react-redux/es/exports'; import { connect as reduxConnect } from 'react-redux';
import { HttpClient } from '../../src/common/services/HttpClient'; import { HttpClient } from '../../src/common/services/HttpClient';
import { ImageDownloader } from '../../src/common/services/ImageDownloader'; import { ImageDownloader } from '../../src/common/services/ImageDownloader';
import { ReportExporter } from '../../src/common/services/ReportExporter';
import { csvToJson, jsonToCsv } from '../../src/utils/helpers/csvjson'; import { csvToJson, jsonToCsv } from '../../src/utils/helpers/csvjson';
import { useTimeoutToggle } from '../../src/utils/helpers/hooks';
import { ColorGenerator } from '../../src/utils/services/ColorGenerator';
import { LocalStorage } from '../../src/utils/services/LocalStorage';
import { provideServices as provideDomainsServices } from '../domains/services/provideServices'; import { provideServices as provideDomainsServices } from '../domains/services/provideServices';
import { provideServices as provideMercureServices } from '../mercure/services/provideServices'; import { provideServices as provideMercureServices } from '../mercure/services/provideServices';
import { provideServices as provideOverviewServices } from '../overview/services/provideServices'; import { provideServices as provideOverviewServices } from '../overview/services/provideServices';
import { provideServices as provideShortUrlsServices } from '../short-urls/services/provideServices'; import { provideServices as provideShortUrlsServices } from '../short-urls/services/provideServices';
import { provideServices as provideTagsServices } from '../tags/services/provideServices'; import { provideServices as provideTagsServices } from '../tags/services/provideServices';
import { provideServices as provideUtilsServices } from '../utils/services/provideServices';
import { ReportExporter } from '../utils/services/ReportExporter';
import { provideServices as provideVisitsServices } from '../visits/services/provideServices'; import { provideServices as provideVisitsServices } from '../visits/services/provideServices';
import { provideServices as provideWebComponentServices } from './provideServices'; import { provideServices as provideWebComponentServices } from './provideServices';
@ -38,15 +36,16 @@ const connect: ConnectDecorator = (propsFromState: string[] | null, actionServic
actionServiceNames.reduce(mapActionService, {}), actionServiceNames.reduce(mapActionService, {}),
); );
provideWebComponentServices(bottle, connect); provideWebComponentServices(bottle);
provideShortUrlsServices(bottle, connect); provideShortUrlsServices(bottle, connect);
provideTagsServices(bottle, connect); provideTagsServices(bottle, connect);
provideVisitsServices(bottle, connect); provideVisitsServices(bottle, connect);
provideMercureServices(bottle); provideMercureServices(bottle);
provideDomainsServices(bottle, connect); provideDomainsServices(bottle, connect);
provideOverviewServices(bottle, connect); provideOverviewServices(bottle, connect);
provideUtilsServices(bottle);
// TODO Check which of these can be moved to shlink-web-component, and which are needed by the app too // FIXME Check which of these can be moved to shlink-web-component, and which are needed by the app too
bottle.constant('window', window); bottle.constant('window', window);
bottle.constant('console', console); bottle.constant('console', console);
bottle.constant('fetch', window.fetch.bind(window)); bottle.constant('fetch', window.fetch.bind(window));
@ -55,13 +54,5 @@ bottle.service('HttpClient', HttpClient, 'fetch');
bottle.service('ImageDownloader', ImageDownloader, 'HttpClient', 'window'); bottle.service('ImageDownloader', ImageDownloader, 'HttpClient', 'window');
bottle.service('ReportExporter', ReportExporter, 'window', 'jsonToCsv'); bottle.service('ReportExporter', ReportExporter, 'window', 'jsonToCsv');
bottle.constant('localStorage', window.localStorage);
bottle.service('Storage', LocalStorage, 'localStorage');
bottle.service('ColorGenerator', ColorGenerator, 'Storage');
bottle.constant('csvToJson', csvToJson); bottle.constant('csvToJson', csvToJson);
bottle.constant('jsonToCsv', jsonToCsv); bottle.constant('jsonToCsv', jsonToCsv);
bottle.constant('setTimeout', window.setTimeout);
bottle.constant('clearTimeout', window.clearTimeout);
bottle.serviceFactory('useTimeoutToggle', useTimeoutToggle, 'setTimeout', 'clearTimeout');

View file

@ -5,7 +5,7 @@ import { useEffect } from 'react';
import type { InputProps } from 'reactstrap'; import type { InputProps } from 'reactstrap';
import { Button, DropdownItem, Input, InputGroup, UncontrolledTooltip } from 'reactstrap'; import { Button, DropdownItem, Input, InputGroup, UncontrolledTooltip } from 'reactstrap';
import { DropdownBtn } from '../../src/utils/DropdownBtn'; import { DropdownBtn } from '../../src/utils/DropdownBtn';
import { useToggle } from '../../src/utils/helpers/hooks'; import { useToggle } from '../utils/helpers/hooks';
import type { DomainsList } from './reducers/domainsList'; import type { DomainsList } from './reducers/domainsList';
import './DomainSelector.scss'; import './DomainSelector.scss';

View file

@ -3,9 +3,9 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import type { FC } from 'react'; import type { FC } from 'react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { DropdownItem } from 'reactstrap'; import { DropdownItem } from 'reactstrap';
import { useToggle } from '../../../src/utils/helpers/hooks';
import { RowDropdownBtn } from '../../../src/utils/RowDropdownBtn'; import { RowDropdownBtn } from '../../../src/utils/RowDropdownBtn';
import { useFeature } from '../../utils/features'; import { useFeature } from '../../utils/features';
import { useToggle } from '../../utils/helpers/hooks';
import { useRoutesPrefix } from '../../utils/routesPrefix'; import { useRoutesPrefix } from '../../utils/routesPrefix';
import { DEFAULT_DOMAIN } from '../../visits/reducers/domainVisits'; import { DEFAULT_DOMAIN } from '../../visits/reducers/domainVisits';
import type { Domain } from '../data'; import type { Domain } from '../data';

View file

@ -7,9 +7,9 @@ import { useLocation, useParams } from 'react-router-dom';
import { Button, Card } from 'reactstrap'; import { Button, Card } from 'reactstrap';
import { ShlinkApiError } from '../../src/api/ShlinkApiError'; import { ShlinkApiError } from '../../src/api/ShlinkApiError';
import { useGoBack } from '../../src/utils/helpers/hooks'; import { useGoBack } from '../../src/utils/helpers/hooks';
import { parseQuery } from '../../src/utils/helpers/query';
import { Message } from '../../src/utils/Message'; import { Message } from '../../src/utils/Message';
import { Result } from '../../src/utils/Result'; import { Result } from '../../src/utils/Result';
import { parseQuery } from '../utils/helpers/query';
import { useSetting } from '../utils/settings'; import { useSetting } from '../utils/settings';
import type { ShortUrlIdentifier } from './data'; import type { ShortUrlIdentifier } from './data';
import { shortUrlDataFromShortUrl, urlDecodeShortCode } from './helpers'; import { shortUrlDataFromShortUrl, urlDecodeShortCode } from './helpers';

View file

@ -1,7 +1,7 @@
import { faInfoCircle as infoIcon } from '@fortawesome/free-solid-svg-icons'; import { faInfoCircle as infoIcon } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Modal, ModalBody, ModalHeader } from 'reactstrap'; import { Modal, ModalBody, ModalHeader } from 'reactstrap';
import { useToggle } from '../../src/utils/helpers/hooks'; import { useToggle } from '../utils/helpers/hooks';
import './UseExistingIfFoundInfoIcon.scss'; import './UseExistingIfFoundInfoIcon.scss';
const InfoModal = ({ isOpen, toggle }: { isOpen: boolean; toggle: () => void }) => ( const InfoModal = ({ isOpen, toggle }: { isOpen: boolean; toggle: () => void }) => (

View file

@ -1,9 +1,9 @@
import type { FC } from 'react'; import type { FC } from 'react';
import { useCallback } from 'react'; import { useCallback } from 'react';
import type { ReportExporter } from '../../../src/common/services/ReportExporter';
import { ExportBtn } from '../../../src/utils/ExportBtn'; import { ExportBtn } from '../../../src/utils/ExportBtn';
import { useToggle } from '../../../src/utils/helpers/hooks';
import type { ShlinkApiClient } from '../../api-contract'; import type { ShlinkApiClient } from '../../api-contract';
import { useToggle } from '../../utils/helpers/hooks';
import type { ReportExporter } from '../../utils/services/ReportExporter';
import type { ShortUrl } from '../data'; import type { ShortUrl } from '../data';
import { useShortUrlsQuery } from './hooks'; import { useShortUrlsQuery } from './hooks';

View file

@ -4,7 +4,7 @@ import { ExternalLink } from 'react-external-link';
import { CopyToClipboardIcon } from '../../../src/utils/CopyToClipboardIcon'; import { CopyToClipboardIcon } from '../../../src/utils/CopyToClipboardIcon';
import { Time } from '../../../src/utils/dates/Time'; import { Time } from '../../../src/utils/dates/Time';
import type { TimeoutToggle } from '../../../src/utils/helpers/hooks'; import type { TimeoutToggle } from '../../../src/utils/helpers/hooks';
import type { ColorGenerator } from '../../../src/utils/services/ColorGenerator'; import type { ColorGenerator } from '../../utils/services/ColorGenerator';
import { useSetting } from '../../utils/settings'; import { useSetting } from '../../utils/settings';
import type { ShortUrl } from '../data'; import type { ShortUrl } from '../data';
import { useShortUrlsQuery } from './hooks'; import { useShortUrlsQuery } from './hooks';

View file

@ -7,8 +7,8 @@ import {
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import type { FC } from 'react'; import type { FC } from 'react';
import { DropdownItem } from 'reactstrap'; import { DropdownItem } from 'reactstrap';
import { useToggle } from '../../../src/utils/helpers/hooks';
import { RowDropdownBtn } from '../../../src/utils/RowDropdownBtn'; import { RowDropdownBtn } from '../../../src/utils/RowDropdownBtn';
import { useToggle } from '../../utils/helpers/hooks';
import type { ShortUrl, ShortUrlModalProps } from '../data'; import type { ShortUrl, ShortUrlModalProps } from '../data';
import { ShortUrlDetailLink } from './ShortUrlDetailLink'; import { ShortUrlDetailLink } from './ShortUrlDetailLink';

View file

@ -1,7 +1,7 @@
import { isEmpty } from 'ramda'; import { isEmpty } from 'ramda';
import type { FC } from 'react'; import type { FC } from 'react';
import type { ColorGenerator } from '../../../src/utils/services/ColorGenerator';
import { Tag } from '../../tags/helpers/Tag'; import { Tag } from '../../tags/helpers/Tag';
import type { ColorGenerator } from '../../utils/services/ColorGenerator';
interface TagsProps { interface TagsProps {
tags: string[]; tags: string[];

View file

@ -2,10 +2,10 @@ import { isEmpty, pipe } from 'ramda';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { useLocation, useNavigate } from 'react-router-dom'; import { useLocation, useNavigate } from 'react-router-dom';
import { orderToString, stringToOrder } from '../../../src/utils/helpers/ordering'; import { orderToString, stringToOrder } from '../../../src/utils/helpers/ordering';
import { parseQuery, stringifyQuery } from '../../../src/utils/helpers/query';
import type { BooleanString } from '../../../src/utils/utils'; import type { BooleanString } from '../../../src/utils/utils';
import { parseOptionalBooleanToString } from '../../../src/utils/utils'; import { parseOptionalBooleanToString } from '../../../src/utils/utils';
import type { TagsFilteringMode } from '../../api-contract'; import type { TagsFilteringMode } from '../../api-contract';
import { parseQuery, stringifyQuery } from '../../utils/helpers/query';
import { useRoutesPrefix } from '../../utils/routesPrefix'; import { useRoutesPrefix } from '../../utils/routesPrefix';
import type { ShortUrlsOrder, ShortUrlsOrderableFields } from '../data'; import type { ShortUrlsOrder, ShortUrlsOrderableFields } from '../data';

View file

@ -3,10 +3,10 @@ import type { FC } from 'react';
import { useEffect, useRef } from 'react'; import { useEffect, useRef } from 'react';
import { useLocation } from 'react-router-dom'; import { useLocation } from 'react-router-dom';
import { SimplePaginator } from '../../src/common/SimplePaginator'; import { SimplePaginator } from '../../src/common/SimplePaginator';
import { useQueryState } from '../../src/utils/helpers/hooks';
import { parseQuery } from '../../src/utils/helpers/query';
import { SimpleCard } from '../../src/utils/SimpleCard'; import { SimpleCard } from '../../src/utils/SimpleCard';
import { TableOrderIcon } from '../../src/utils/table/TableOrderIcon'; import { TableOrderIcon } from '../../src/utils/table/TableOrderIcon';
import { useQueryState } from '../utils/helpers/hooks';
import { parseQuery } from '../utils/helpers/query';
import type { TagsListChildrenProps, TagsOrder, TagsOrderableFields } from './data/TagsListChildrenProps'; import type { TagsListChildrenProps, TagsOrder, TagsOrderableFields } from './data/TagsListChildrenProps';
import type { TagsTableRowProps } from './TagsTableRow'; import type { TagsTableRowProps } from './TagsTableRow';
import './TagsTable.scss'; import './TagsTable.scss';

View file

@ -3,11 +3,11 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import type { FC } from 'react'; import type { FC } from 'react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { DropdownItem } from 'reactstrap'; import { DropdownItem } from 'reactstrap';
import { useToggle } from '../../src/utils/helpers/hooks';
import { prettify } from '../../src/utils/helpers/numbers'; import { prettify } from '../../src/utils/helpers/numbers';
import { RowDropdownBtn } from '../../src/utils/RowDropdownBtn'; import { RowDropdownBtn } from '../../src/utils/RowDropdownBtn';
import type { ColorGenerator } from '../../src/utils/services/ColorGenerator'; import { useToggle } from '../utils/helpers/hooks';
import { useRoutesPrefix } from '../utils/routesPrefix'; import { useRoutesPrefix } from '../utils/routesPrefix';
import type { ColorGenerator } from '../utils/services/ColorGenerator';
import type { SimplifiedTag, TagModalProps } from './data'; import type { SimplifiedTag, TagModalProps } from './data';
import { TagBullet } from './helpers/TagBullet'; import { TagBullet } from './helpers/TagBullet';

View file

@ -5,10 +5,10 @@ import { useState } from 'react';
import { HexColorPicker } from 'react-colorful'; import { HexColorPicker } from 'react-colorful';
import { Button, Input, InputGroup, Modal, ModalBody, ModalFooter, ModalHeader, Popover } from 'reactstrap'; import { Button, Input, InputGroup, Modal, ModalBody, ModalFooter, ModalHeader, Popover } from 'reactstrap';
import { ShlinkApiError } from '../../../src/api/ShlinkApiError'; import { ShlinkApiError } from '../../../src/api/ShlinkApiError';
import { useToggle } from '../../../src/utils/helpers/hooks';
import { Result } from '../../../src/utils/Result'; import { Result } from '../../../src/utils/Result';
import type { ColorGenerator } from '../../../src/utils/services/ColorGenerator';
import { handleEventPreventingDefault } from '../../../src/utils/utils'; import { handleEventPreventingDefault } from '../../../src/utils/utils';
import { useToggle } from '../../utils/helpers/hooks';
import type { ColorGenerator } from '../../utils/services/ColorGenerator';
import type { TagModalProps } from '../data'; import type { TagModalProps } from '../data';
import type { EditTag, TagEdition } from '../reducers/tagEdit'; import type { EditTag, TagEdition } from '../reducers/tagEdit';
import './EditTagModal.scss'; import './EditTagModal.scss';

View file

@ -1,6 +1,6 @@
import classNames from 'classnames'; import classNames from 'classnames';
import type { FC, MouseEventHandler, PropsWithChildren } from 'react'; import type { FC, MouseEventHandler, PropsWithChildren } from 'react';
import type { ColorGenerator } from '../../../src/utils/services/ColorGenerator'; import type { ColorGenerator } from '../../utils/services/ColorGenerator';
import './Tag.scss'; import './Tag.scss';
type TagProps = PropsWithChildren<{ type TagProps = PropsWithChildren<{

View file

@ -1,4 +1,4 @@
import type { ColorGenerator } from '../../../src/utils/services/ColorGenerator'; import type { ColorGenerator } from '../../utils/services/ColorGenerator';
import './TagBullet.scss'; import './TagBullet.scss';
interface TagBulletProps { interface TagBulletProps {

View file

@ -1,7 +1,7 @@
import { useEffect } from 'react'; import { useEffect } from 'react';
import type { SuggestionComponentProps, TagComponentProps } from 'react-tag-autocomplete'; import type { SuggestionComponentProps, TagComponentProps } from 'react-tag-autocomplete';
import ReactTags from 'react-tag-autocomplete'; import ReactTags from 'react-tag-autocomplete';
import type { ColorGenerator } from '../../../src/utils/services/ColorGenerator'; import type { ColorGenerator } from '../../utils/services/ColorGenerator';
import { useSetting } from '../../utils/settings'; import { useSetting } from '../../utils/settings';
import type { TagsList } from '../reducers/tagsList'; import type { TagsList } from '../reducers/tagsList';
import { Tag } from './Tag'; import { Tag } from './Tag';

View file

@ -1,10 +1,10 @@
import type { PayloadAction } from '@reduxjs/toolkit'; import type { PayloadAction } from '@reduxjs/toolkit';
import { createAction, createSlice } from '@reduxjs/toolkit'; import { createAction, createSlice } from '@reduxjs/toolkit';
import { pick } from 'ramda'; import { pick } from 'ramda';
import type { ColorGenerator } from '../../../src/utils/services/ColorGenerator';
import type { ProblemDetailsError, ShlinkApiClient } from '../../api-contract'; import type { ProblemDetailsError, ShlinkApiClient } from '../../api-contract';
import { parseApiError } from '../../api-contract/utils'; import { parseApiError } from '../../api-contract/utils';
import { createAsyncThunk } from '../../utils/redux'; import { createAsyncThunk } from '../../utils/redux';
import type { ColorGenerator } from '../../utils/services/ColorGenerator';
const REDUCER_PREFIX = 'shlink/tagEdit'; const REDUCER_PREFIX = 'shlink/tagEdit';

View file

@ -0,0 +1,96 @@
import type { DependencyList, EffectCallback } from 'react';
import { useEffect, useRef, useState } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
import { useSwipeable as useReactSwipeable } from 'react-swipeable';
import { v4 as uuid } from 'uuid';
import { parseQuery, stringifyQuery } from './query';
const DEFAULT_DELAY = 2000;
export type TimeoutToggle = (initialValue?: boolean, delay?: number) => [boolean, () => void];
export const useTimeoutToggle = (
setTimeout: (callback: Function, timeout: number) => number,
clearTimeout: (timer: number) => void,
): TimeoutToggle => (initialValue = false, delay = DEFAULT_DELAY) => {
const [flag, setFlag] = useState<boolean>(initialValue);
const timeout = useRef<number | undefined>(undefined);
const callback = () => {
setFlag(!initialValue);
if (timeout.current) {
clearTimeout(timeout.current);
}
timeout.current = setTimeout(() => setFlag(initialValue), delay);
};
return [flag, callback];
};
type ToggleResult = [boolean, () => void, () => void, () => void];
export const useToggle = (initialValue = false): ToggleResult => {
const [flag, setFlag] = useState<boolean>(initialValue);
return [flag, () => setFlag(!flag), () => setFlag(true), () => setFlag(false)];
};
export const useSwipeable = (showSidebar: () => void, hideSidebar: () => void) => {
const swipeMenuIfNoModalExists = (callback: () => void) => (e: any) => {
const swippedOnVisitsTable = (e.event.composedPath() as HTMLElement[]).some(
({ classList }) => classList?.contains('visits-table'),
);
if (swippedOnVisitsTable || document.querySelector('.modal')) {
return;
}
callback();
};
return useReactSwipeable({
delta: 40,
onSwipedLeft: swipeMenuIfNoModalExists(hideSidebar),
onSwipedRight: swipeMenuIfNoModalExists(showSidebar),
});
};
export const useQueryState = <T>(paramName: string, initialState: T): [ T, (newValue: T) => void ] => {
const [value, setValue] = useState(initialState);
const setValueWithLocation = (valueToSet: T) => {
const { location, history } = window;
const query = parseQuery<any>(location.search);
query[paramName] = valueToSet;
history.pushState(null, '', `${location.pathname}?${stringifyQuery(query)}`);
setValue(valueToSet);
};
return [value, setValueWithLocation];
};
export const useEffectExceptFirstTime = (callback: EffectCallback, deps: DependencyList): void => {
const isFirstLoad = useRef(true);
useEffect(() => {
!isFirstLoad.current && callback();
isFirstLoad.current = false;
}, deps);
};
export const useGoBack = () => {
const navigate = useNavigate();
return () => navigate(-1);
};
export const useParsedQuery = <T>(): T => {
const { search } = useLocation();
return parseQuery<T>(search);
};
export const useDomId = (): string => {
const { current: id } = useRef(`dom-${uuid()}`);
return id;
};
export const useElementRef = <T>() => useRef<T | null>(null);

View file

@ -1,6 +1,6 @@
import { isNil } from 'ramda'; import { isNil } from 'ramda';
import { rangeOf } from '../utils'; import type { LocalStorage } from '../../../src/utils/services/LocalStorage';
import type { LocalStorage } from './LocalStorage'; import { rangeOf } from '../../../src/utils/utils';
const HEX_COLOR_LENGTH = 6; const HEX_COLOR_LENGTH = 6;
const HEX_DIGITS = '0123456789ABCDEF'; const HEX_DIGITS = '0123456789ABCDEF';

View file

@ -0,0 +1,14 @@
const PREFIX = 'shlink';
const buildPath = (path: string) => `${PREFIX}.${path}`;
export class LocalStorage {
public constructor(private readonly localStorage: Storage) {}
public readonly get = <T>(key: string): T | undefined => {
const item = this.localStorage.getItem(buildPath(key));
return item ? JSON.parse(item) as T : undefined;
};
public readonly set = (key: string, value: any) => this.localStorage.setItem(buildPath(key), JSON.stringify(value));
}

View file

@ -0,0 +1,30 @@
import type { JsonToCsv } from '../../../src/utils/helpers/csvjson';
import { saveCsv } from '../../../src/utils/helpers/files';
import type { ExportableShortUrl } from '../../short-urls/data';
import type { NormalizedVisit } from '../../visits/types';
export class ReportExporter {
public constructor(private readonly window: Window, private readonly jsonToCsv: JsonToCsv) {
}
public readonly exportVisits = (filename: string, visits: NormalizedVisit[]) => {
if (!visits.length) {
return;
}
this.exportCsv(filename, visits);
};
public readonly exportShortUrls = (shortUrls: ExportableShortUrl[]) => {
if (!shortUrls.length) {
return;
}
this.exportCsv('short_urls.csv', shortUrls);
};
private readonly exportCsv = (filename: string, rows: object[]) => {
const csv = this.jsonToCsv(rows);
saveCsv(this.window, csv, filename);
};
}

View file

@ -0,0 +1,14 @@
import type Bottle from 'bottlejs';
import { useTimeoutToggle } from '../helpers/hooks';
import { ColorGenerator } from './ColorGenerator';
import { LocalStorage } from './LocalStorage';
export function provideServices(bottle: Bottle) {
bottle.constant('localStorage', window.localStorage);
bottle.service('Storage', LocalStorage, 'localStorage');
bottle.service('ColorGenerator', ColorGenerator, 'Storage');
bottle.constant('setTimeout', window.setTimeout);
bottle.constant('clearTimeout', window.clearTimeout);
bottle.serviceFactory('useTimeoutToggle', useTimeoutToggle, 'setTimeout', 'clearTimeout');
}

View file

@ -1,9 +1,9 @@
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import type { ReportExporter } from '../../src/common/services/ReportExporter';
import { useGoBack } from '../../src/utils/helpers/hooks'; import { useGoBack } from '../../src/utils/helpers/hooks';
import type { ShlinkVisitsParams } from '../api-contract'; import type { ShlinkVisitsParams } from '../api-contract';
import { boundToMercureHub } from '../mercure/helpers/boundToMercureHub'; import { boundToMercureHub } from '../mercure/helpers/boundToMercureHub';
import { Topics } from '../mercure/helpers/Topics'; import { Topics } from '../mercure/helpers/Topics';
import type { ReportExporter } from '../utils/services/ReportExporter';
import type { DomainVisits as DomainVisitsState, LoadDomainVisits } from './reducers/domainVisits'; import type { DomainVisits as DomainVisitsState, LoadDomainVisits } from './reducers/domainVisits';
import type { NormalizedVisit } from './types'; import type { NormalizedVisit } from './types';
import { toApiParams } from './types/helpers'; import { toApiParams } from './types/helpers';

View file

@ -1,7 +1,7 @@
import type { ReportExporter } from '../../src/common/services/ReportExporter';
import { useGoBack } from '../../src/utils/helpers/hooks'; import { useGoBack } from '../../src/utils/helpers/hooks';
import { boundToMercureHub } from '../mercure/helpers/boundToMercureHub'; import { boundToMercureHub } from '../mercure/helpers/boundToMercureHub';
import { Topics } from '../mercure/helpers/Topics'; import { Topics } from '../mercure/helpers/Topics';
import type { ReportExporter } from '../utils/services/ReportExporter';
import type { LoadVisits, VisitsInfo } from './reducers/types'; import type { LoadVisits, VisitsInfo } from './reducers/types';
import type { NormalizedVisit, VisitsParams } from './types'; import type { NormalizedVisit, VisitsParams } from './types';
import { toApiParams } from './types/helpers'; import { toApiParams } from './types/helpers';

View file

@ -1,7 +1,7 @@
import type { ReportExporter } from '../../src/common/services/ReportExporter';
import { useGoBack } from '../../src/utils/helpers/hooks'; import { useGoBack } from '../../src/utils/helpers/hooks';
import { boundToMercureHub } from '../mercure/helpers/boundToMercureHub'; import { boundToMercureHub } from '../mercure/helpers/boundToMercureHub';
import { Topics } from '../mercure/helpers/Topics'; import { Topics } from '../mercure/helpers/Topics';
import type { ReportExporter } from '../utils/services/ReportExporter';
import type { LoadOrphanVisits } from './reducers/orphanVisits'; import type { LoadOrphanVisits } from './reducers/orphanVisits';
import type { VisitsInfo } from './reducers/types'; import type { VisitsInfo } from './reducers/types';
import type { NormalizedVisit, VisitsParams } from './types'; import type { NormalizedVisit, VisitsParams } from './types';

View file

@ -1,13 +1,13 @@
import { useEffect } from 'react'; import { useEffect } from 'react';
import { useLocation, useParams } from 'react-router-dom'; import { useLocation, useParams } from 'react-router-dom';
import type { ReportExporter } from '../../src/common/services/ReportExporter';
import { useGoBack } from '../../src/utils/helpers/hooks'; import { useGoBack } from '../../src/utils/helpers/hooks';
import { parseQuery } from '../../src/utils/helpers/query';
import { boundToMercureHub } from '../mercure/helpers/boundToMercureHub'; import { boundToMercureHub } from '../mercure/helpers/boundToMercureHub';
import { Topics } from '../mercure/helpers/Topics'; import { Topics } from '../mercure/helpers/Topics';
import type { ShortUrlIdentifier } from '../short-urls/data'; import type { ShortUrlIdentifier } from '../short-urls/data';
import { urlDecodeShortCode } from '../short-urls/helpers'; import { urlDecodeShortCode } from '../short-urls/helpers';
import type { ShortUrlDetail } from '../short-urls/reducers/shortUrlDetail'; import type { ShortUrlDetail } from '../short-urls/reducers/shortUrlDetail';
import { parseQuery } from '../utils/helpers/query';
import type { ReportExporter } from '../utils/services/ReportExporter';
import type { LoadShortUrlVisits, ShortUrlVisits as ShortUrlVisitsState } from './reducers/shortUrlVisits'; import type { LoadShortUrlVisits, ShortUrlVisits as ShortUrlVisitsState } from './reducers/shortUrlVisits';
import { ShortUrlVisitsHeader } from './ShortUrlVisitsHeader'; import { ShortUrlVisitsHeader } from './ShortUrlVisitsHeader';
import type { NormalizedVisit, VisitsParams } from './types'; import type { NormalizedVisit, VisitsParams } from './types';

View file

@ -1,10 +1,10 @@
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import type { ShlinkVisitsParams } from '../../api/types';
import type { ReportExporter } from '../../src/common/services/ReportExporter';
import { useGoBack } from '../../src/utils/helpers/hooks'; import { useGoBack } from '../../src/utils/helpers/hooks';
import type { ColorGenerator } from '../../src/utils/services/ColorGenerator'; import type { ShlinkVisitsParams } from '../api-contract';
import { boundToMercureHub } from '../mercure/helpers/boundToMercureHub'; import { boundToMercureHub } from '../mercure/helpers/boundToMercureHub';
import { Topics } from '../mercure/helpers/Topics'; import { Topics } from '../mercure/helpers/Topics';
import type { ColorGenerator } from '../utils/services/ColorGenerator';
import type { ReportExporter } from '../utils/services/ReportExporter';
import type { LoadTagVisits, TagVisits as TagVisitsState } from './reducers/tagVisits'; import type { LoadTagVisits, TagVisits as TagVisitsState } from './reducers/tagVisits';
import { TagVisitsHeader } from './TagVisitsHeader'; import { TagVisitsHeader } from './TagVisitsHeader';
import type { NormalizedVisit } from './types'; import type { NormalizedVisit } from './types';

View file

@ -1,5 +1,5 @@
import type { ColorGenerator } from '../../src/utils/services/ColorGenerator';
import { Tag } from '../tags/helpers/Tag'; import { Tag } from '../tags/helpers/Tag';
import type { ColorGenerator } from '../utils/services/ColorGenerator';
import type { TagVisits } from './reducers/tagVisits'; import type { TagVisits } from './reducers/tagVisits';
import { VisitsHeader } from './VisitsHeader'; import { VisitsHeader } from './VisitsHeader';
import './ShortUrlVisitsHeader.scss'; import './ShortUrlVisitsHeader.scss';

View file

@ -25,11 +25,11 @@ import {
} from 'reactstrap'; } from 'reactstrap';
import { pointerOnHover, renderChartLabel } from '../../../src/utils/helpers/charts'; import { pointerOnHover, renderChartLabel } from '../../../src/utils/helpers/charts';
import { STANDARD_DATE_FORMAT } from '../../../src/utils/helpers/date'; import { STANDARD_DATE_FORMAT } from '../../../src/utils/helpers/date';
import { useToggle } from '../../../src/utils/helpers/hooks';
import { prettify } from '../../../src/utils/helpers/numbers'; import { prettify } from '../../../src/utils/helpers/numbers';
import { HIGHLIGHTED_COLOR, MAIN_COLOR } from '../../../src/utils/theme'; import { HIGHLIGHTED_COLOR, MAIN_COLOR } from '../../../src/utils/theme';
import { ToggleSwitch } from '../../../src/utils/ToggleSwitch'; import { ToggleSwitch } from '../../../src/utils/ToggleSwitch';
import { rangeOf } from '../../../src/utils/utils'; import { rangeOf } from '../../../src/utils/utils';
import { useToggle } from '../../utils/helpers/hooks';
import type { NormalizedVisit, Stats } from '../types'; import type { NormalizedVisit, Stats } from '../types';
import { fillTheGaps } from '../utils'; import { fillTheGaps } from '../utils';
import './LineChartCard.scss'; import './LineChartCard.scss';

View file

@ -2,7 +2,7 @@ import { faMapMarkedAlt as mapIcon } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { useState } from 'react'; import { useState } from 'react';
import { Button, Dropdown, DropdownItem, DropdownMenu, UncontrolledTooltip } from 'reactstrap'; import { Button, Dropdown, DropdownItem, DropdownMenu, UncontrolledTooltip } from 'reactstrap';
import { useDomId, useToggle } from '../../../src/utils/helpers/hooks'; import { useDomId, useToggle } from '../../utils/helpers/hooks';
import type { CityStats } from '../types'; import type { CityStats } from '../types';
import { MapModal } from './MapModal'; import { MapModal } from './MapModal';
import './OpenMapModalBtn.scss'; import './OpenMapModalBtn.scss';

View file

@ -5,9 +5,9 @@ import { useLocation, useNavigate } from 'react-router-dom';
import { formatIsoDate } from '../../../src/utils/helpers/date'; import { formatIsoDate } from '../../../src/utils/helpers/date';
import type { DateRange } from '../../../src/utils/helpers/dateIntervals'; import type { DateRange } from '../../../src/utils/helpers/dateIntervals';
import { datesToDateRange } from '../../../src/utils/helpers/dateIntervals'; import { datesToDateRange } from '../../../src/utils/helpers/dateIntervals';
import { parseQuery, stringifyQuery } from '../../../src/utils/helpers/query';
import type { BooleanString } from '../../../src/utils/utils'; import type { BooleanString } from '../../../src/utils/utils';
import { parseBooleanToString } from '../../../src/utils/utils'; import { parseBooleanToString } from '../../../src/utils/utils';
import { parseQuery, stringifyQuery } from '../../utils/helpers/query';
import type { OrphanVisitType, VisitsFilter } from '../types'; import type { OrphanVisitType, VisitsFilter } from '../types';
interface VisitsQuery { interface VisitsQuery {

View file

@ -18,9 +18,9 @@ import type {
ShlinkVisitsParams } from '../../../shlink-web-component/api-contract'; ShlinkVisitsParams } from '../../../shlink-web-component/api-contract';
import { isRegularNotFound, parseApiError } from '../../../shlink-web-component/api-contract/utils'; import { isRegularNotFound, parseApiError } from '../../../shlink-web-component/api-contract/utils';
import type { ShortUrl, ShortUrlData } from '../../../shlink-web-component/short-urls/data'; import type { ShortUrl, ShortUrlData } from '../../../shlink-web-component/short-urls/data';
import { stringifyQuery } from '../../../shlink-web-component/utils/helpers/query';
import type { HttpClient } from '../../common/services/HttpClient'; import type { HttpClient } from '../../common/services/HttpClient';
import { orderToString } from '../../utils/helpers/ordering'; import { orderToString } from '../../utils/helpers/ordering';
import { stringifyQuery } from '../../utils/helpers/query';
import { replaceAuthorityFromUri } from '../../utils/helpers/uri'; import { replaceAuthorityFromUri } from '../../utils/helpers/uri';
import type { OptionalString } from '../../utils/utils'; import type { OptionalString } from '../../utils/utils';

View file

@ -1,29 +0,0 @@
import type { ExportableShortUrl } from '../../../shlink-web-component/short-urls/data';
import type { NormalizedVisit } from '../../../shlink-web-component/visits/types';
import type { JsonToCsv } from '../../utils/helpers/csvjson';
import { saveCsv } from '../../utils/helpers/files';
export class ReportExporter {
public constructor(private readonly window: Window, private readonly jsonToCsv: JsonToCsv) {}
public readonly exportVisits = (filename: string, visits: NormalizedVisit[]) => {
if (!visits.length) {
return;
}
this.exportCsv(filename, visits);
};
public readonly exportShortUrls = (shortUrls: ExportableShortUrl[]) => {
if (!shortUrls.length) {
return;
}
this.exportCsv('short_urls.csv', shortUrls);
};
private readonly exportCsv = (filename: string, rows: object[]) => {
const csv = this.jsonToCsv(rows);
saveCsv(this.window, csv, filename);
};
}

View file

@ -10,7 +10,6 @@ import { ScrollToTop } from '../ScrollToTop';
import { ShlinkVersionsContainer } from '../ShlinkVersionsContainer'; import { ShlinkVersionsContainer } from '../ShlinkVersionsContainer';
import { HttpClient } from './HttpClient'; import { HttpClient } from './HttpClient';
import { ImageDownloader } from './ImageDownloader'; import { ImageDownloader } from './ImageDownloader';
import { ReportExporter } from './ReportExporter';
export const provideServices = (bottle: Bottle, connect: ConnectDecorator) => { export const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
// Services // Services
@ -20,7 +19,6 @@ export const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
bottle.service('HttpClient', HttpClient, 'fetch'); bottle.service('HttpClient', HttpClient, 'fetch');
bottle.service('ImageDownloader', ImageDownloader, 'HttpClient', 'window'); bottle.service('ImageDownloader', ImageDownloader, 'HttpClient', 'window');
bottle.service('ReportExporter', ReportExporter, 'window', 'jsonToCsv');
// Components // Components
bottle.serviceFactory('ScrollToTop', () => ScrollToTop); bottle.serviceFactory('ScrollToTop', () => ScrollToTop);

View file

@ -7,7 +7,7 @@ export const csvToJson = <T>(csvContent: string) => new Promise<T[]>((resolve) =
export type CsvToJson = typeof csvToJson; export type CsvToJson = typeof csvToJson;
const jsonParser = new Parser(); // TODO This accepts options if needed const jsonParser = new Parser(); // This accepts options if needed
export const jsonToCsv = <T>(data: T[]): string => jsonParser.parse(data); export const jsonToCsv = <T>(data: T[]): string => jsonParser.parse(data);

View file

@ -1,9 +1,8 @@
import type { DependencyList, EffectCallback } from 'react'; import type { DependencyList, EffectCallback } from 'react';
import { useEffect, useRef, useState } from 'react'; import { useEffect, useRef, useState } from 'react';
import { useLocation, useNavigate } from 'react-router-dom'; import { useLocation, useNavigate } from 'react-router-dom';
import { useSwipeable as useReactSwipeable } from 'react-swipeable';
import { v4 as uuid } from 'uuid'; import { v4 as uuid } from 'uuid';
import { parseQuery, stringifyQuery } from './query'; import { parseQuery } from '../../../shlink-web-component/utils/helpers/query';
const DEFAULT_DELAY = 2000; const DEFAULT_DELAY = 2000;
@ -35,40 +34,6 @@ export const useToggle = (initialValue = false): ToggleResult => {
return [flag, () => setFlag(!flag), () => setFlag(true), () => setFlag(false)]; return [flag, () => setFlag(!flag), () => setFlag(true), () => setFlag(false)];
}; };
export const useSwipeable = (showSidebar: () => void, hideSidebar: () => void) => {
const swipeMenuIfNoModalExists = (callback: () => void) => (e: any) => {
const swippedOnVisitsTable = (e.event.composedPath() as HTMLElement[]).some(
({ classList }) => classList?.contains('visits-table'),
);
if (swippedOnVisitsTable || document.querySelector('.modal')) {
return;
}
callback();
};
return useReactSwipeable({
delta: 40,
onSwipedLeft: swipeMenuIfNoModalExists(hideSidebar),
onSwipedRight: swipeMenuIfNoModalExists(showSidebar),
});
};
export const useQueryState = <T>(paramName: string, initialState: T): [ T, (newValue: T) => void ] => {
const [value, setValue] = useState(initialState);
const setValueWithLocation = (valueToSet: T) => {
const { location, history } = window;
const query = parseQuery<any>(location.search);
query[paramName] = valueToSet;
history.pushState(null, '', `${location.pathname}?${stringifyQuery(query)}`);
setValue(valueToSet);
};
return [value, setValueWithLocation];
};
export const useEffectExceptFirstTime = (callback: EffectCallback, deps: DependencyList): void => { export const useEffectExceptFirstTime = (callback: EffectCallback, deps: DependencyList): void => {
const isFirstLoad = useRef(true); const isFirstLoad = useRef(true);

View file

@ -1,5 +1,5 @@
import { isEmpty } from 'ramda'; import { isEmpty } from 'ramda';
import { stringifyQuery } from './query'; import { stringifyQuery } from '../../../shlink-web-component/utils/helpers/query';
export type QrCodeFormat = 'svg' | 'png'; export type QrCodeFormat = 'svg' | 'png';

View file

@ -1,7 +1,7 @@
import type Bottle from 'bottlejs'; import type Bottle from 'bottlejs';
import { ColorGenerator } from '../../../shlink-web-component/utils/services/ColorGenerator';
import { csvToJson, jsonToCsv } from '../helpers/csvjson'; import { csvToJson, jsonToCsv } from '../helpers/csvjson';
import { useTimeoutToggle } from '../helpers/hooks'; import { useTimeoutToggle } from '../helpers/hooks';
import { ColorGenerator } from './ColorGenerator';
import { LocalStorage } from './LocalStorage'; import { LocalStorage } from './LocalStorage';
export const provideServices = (bottle: Bottle) => { export const provideServices = (bottle: Bottle) => {

View file

@ -1,7 +1,7 @@
import { render, screen } from '@testing-library/react'; import { render, screen } from '@testing-library/react';
import { fromPartial } from '@total-typescript/shoehorn'; import { fromPartial } from '@total-typescript/shoehorn';
import { MemoryRouter } from 'react-router'; import { MemoryRouter } from 'react-router';
import { AsideMenu } from '../../src/common/AsideMenu'; import { AsideMenu } from '../../shlink-web-component/common/AsideMenu';
describe('<AsideMenu />', () => { describe('<AsideMenu />', () => {
const setUp = () => render( const setUp = () => render(

View file

@ -1,6 +1,6 @@
import type { ExportableShortUrl } from '../../../shlink-web-component/short-urls/data'; import type { ExportableShortUrl } from '../../../shlink-web-component/short-urls/data';
import { ReportExporter } from '../../../shlink-web-component/utils/services/ReportExporter';
import type { NormalizedVisit } from '../../../shlink-web-component/visits/types'; import type { NormalizedVisit } from '../../../shlink-web-component/visits/types';
import { ReportExporter } from '../../../src/common/services/ReportExporter';
import { windowMock } from '../../__mocks__/Window.mock'; import { windowMock } from '../../__mocks__/Window.mock';
describe('ReportExporter', () => { describe('ReportExporter', () => {

View file

@ -3,7 +3,7 @@ import { fromPartial } from '@total-typescript/shoehorn';
import { MemoryRouter } from 'react-router-dom'; import { MemoryRouter } from 'react-router-dom';
import type { ShortUrl } from '../../../shlink-web-component/short-urls/data'; import type { ShortUrl } from '../../../shlink-web-component/short-urls/data';
import { ExportShortUrlsBtn as createExportShortUrlsBtn } from '../../../shlink-web-component/short-urls/helpers/ExportShortUrlsBtn'; import { ExportShortUrlsBtn as createExportShortUrlsBtn } from '../../../shlink-web-component/short-urls/helpers/ExportShortUrlsBtn';
import type { ReportExporter } from '../../../src/common/services/ReportExporter'; import type { ReportExporter } from '../../../shlink-web-component/utils/services/ReportExporter';
import type { NotFoundServer, SelectedServer } from '../../../src/servers/data'; import type { NotFoundServer, SelectedServer } from '../../../src/servers/data';
import { renderWithEvents } from '../../__helpers__/setUpTest'; import { renderWithEvents } from '../../__helpers__/setUpTest';

View file

@ -2,7 +2,7 @@ import { screen } from '@testing-library/react';
import { fromPartial } from '@total-typescript/shoehorn'; import { fromPartial } from '@total-typescript/shoehorn';
import type { ReactNode } from 'react'; import type { ReactNode } from 'react';
import { Tag } from '../../../shlink-web-component/tags/helpers/Tag'; import { Tag } from '../../../shlink-web-component/tags/helpers/Tag';
import type { ColorGenerator } from '../../../src/utils/services/ColorGenerator'; import type { ColorGenerator } from '../../../shlink-web-component/utils/services/ColorGenerator';
import { MAIN_COLOR } from '../../../src/utils/theme'; import { MAIN_COLOR } from '../../../src/utils/theme';
import { renderWithEvents } from '../../__helpers__/setUpTest'; import { renderWithEvents } from '../../__helpers__/setUpTest';

View file

@ -1,8 +1,8 @@
import { fromPartial } from '@total-typescript/shoehorn'; import { fromPartial } from '@total-typescript/shoehorn';
import { editTag as editTagCreator, tagEdited, tagEditReducerCreator } from '../../../shlink-web-component/tags/reducers/tagEdit'; import { editTag as editTagCreator, tagEdited, tagEditReducerCreator } from '../../../shlink-web-component/tags/reducers/tagEdit';
import type { ColorGenerator } from '../../../shlink-web-component/utils/services/ColorGenerator';
import type { ShlinkApiClient } from '../../../src/api/services/ShlinkApiClient'; import type { ShlinkApiClient } from '../../../src/api/services/ShlinkApiClient';
import type { ShlinkState } from '../../../src/container/types'; import type { ShlinkState } from '../../../src/container/types';
import type { ColorGenerator } from '../../../src/utils/services/ColorGenerator';
describe('tagEditReducer', () => { describe('tagEditReducer', () => {
const oldName = 'foo'; const oldName = 'foo';

View file

@ -1,4 +1,4 @@
import { parseQuery, stringifyQuery } from '../../../src/utils/helpers/query'; import { parseQuery, stringifyQuery } from '../../../shlink-web-component/utils/helpers/query';
describe('query', () => { describe('query', () => {
describe('parseQuery', () => { describe('parseQuery', () => {

View file

@ -1,5 +1,5 @@
import { fromPartial } from '@total-typescript/shoehorn'; import { fromPartial } from '@total-typescript/shoehorn';
import { ColorGenerator } from '../../../src/utils/services/ColorGenerator'; import { ColorGenerator } from '../../../shlink-web-component/utils/services/ColorGenerator';
import type { LocalStorage } from '../../../src/utils/services/LocalStorage'; import type { LocalStorage } from '../../../src/utils/services/LocalStorage';
import { MAIN_COLOR } from '../../../src/utils/theme'; import { MAIN_COLOR } from '../../../src/utils/theme';

View file

@ -1,5 +1,5 @@
import { fromPartial } from '@total-typescript/shoehorn'; import { fromPartial } from '@total-typescript/shoehorn';
import type { ColorGenerator } from '../../../../src/utils/services/ColorGenerator'; import type { ColorGenerator } from '../../../../shlink-web-component/utils/services/ColorGenerator';
export const colorGeneratorMock = fromPartial<ColorGenerator>({ export const colorGeneratorMock = fromPartial<ColorGenerator>({
getColorForKey: vi.fn(() => 'red'), getColorForKey: vi.fn(() => 'red'),

View file

@ -1,8 +1,8 @@
import { render, screen } from '@testing-library/react'; import { render, screen } from '@testing-library/react';
import { fromPartial } from '@total-typescript/shoehorn'; import { fromPartial } from '@total-typescript/shoehorn';
import type { ColorGenerator } from '../../shlink-web-component/utils/services/ColorGenerator';
import type { TagVisits } from '../../shlink-web-component/visits/reducers/tagVisits'; import type { TagVisits } from '../../shlink-web-component/visits/reducers/tagVisits';
import { TagVisitsHeader } from '../../shlink-web-component/visits/TagVisitsHeader'; import { TagVisitsHeader } from '../../shlink-web-component/visits/TagVisitsHeader';
import type { ColorGenerator } from '../../src/utils/services/ColorGenerator';
describe('<TagVisitsHeader />', () => { describe('<TagVisitsHeader />', () => {
const tagVisits = fromPartial<TagVisits>({ const tagVisits = fromPartial<TagVisits>({