mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2024-12-22 17:10:26 +03:00
Move some modules from src to shlink-web-component
This commit is contained in:
parent
0169060de7
commit
275745fd3a
51 changed files with 212 additions and 133 deletions
|
@ -4,10 +4,10 @@ import classNames from 'classnames';
|
|||
import type { FC } from 'react';
|
||||
import { useEffect } from 'react';
|
||||
import { Navigate, Route, Routes, useLocation } from 'react-router-dom';
|
||||
import { AsideMenu } from '../src/common/AsideMenu';
|
||||
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 { useSwipeable, useToggle } from './utils/helpers/hooks';
|
||||
import { useRoutesPrefix } from './utils/routesPrefix';
|
||||
|
||||
export const Main = (
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
@import '../utils/base';
|
||||
@import '../utils/mixins/vertical-align';
|
||||
@import '../../src/utils/base';
|
||||
@import '../../src/utils/mixins/vertical-align';
|
||||
|
||||
.aside-menu {
|
||||
width: $asideMenuWidth;
|
|
@ -1,19 +1,17 @@
|
|||
import type { IContainer } from 'bottlejs';
|
||||
import Bottle from 'bottlejs';
|
||||
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 { ImageDownloader } from '../../src/common/services/ImageDownloader';
|
||||
import { ReportExporter } from '../../src/common/services/ReportExporter';
|
||||
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 provideMercureServices } from '../mercure/services/provideServices';
|
||||
import { provideServices as provideOverviewServices } from '../overview/services/provideServices';
|
||||
import { provideServices as provideShortUrlsServices } from '../short-urls/services/provideServices';
|
||||
import { provideServices as provideTagsServices } from '../tags/services/provideServices';
|
||||
import { provideServices as provideUtilsServices } from '../utils/services/provideServices';
|
||||
import { ReportExporter } from '../utils/services/ReportExporter';
|
||||
import { provideServices as provideVisitsServices } from '../visits/services/provideServices';
|
||||
import { provideServices as provideWebComponentServices } from './provideServices';
|
||||
|
||||
|
@ -38,15 +36,16 @@ const connect: ConnectDecorator = (propsFromState: string[] | null, actionServic
|
|||
actionServiceNames.reduce(mapActionService, {}),
|
||||
);
|
||||
|
||||
provideWebComponentServices(bottle, connect);
|
||||
provideWebComponentServices(bottle);
|
||||
provideShortUrlsServices(bottle, connect);
|
||||
provideTagsServices(bottle, connect);
|
||||
provideVisitsServices(bottle, connect);
|
||||
provideMercureServices(bottle);
|
||||
provideDomainsServices(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('console', console);
|
||||
bottle.constant('fetch', window.fetch.bind(window));
|
||||
|
@ -55,13 +54,5 @@ bottle.service('HttpClient', HttpClient, 'fetch');
|
|||
bottle.service('ImageDownloader', ImageDownloader, 'HttpClient', 'window');
|
||||
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('jsonToCsv', jsonToCsv);
|
||||
|
||||
bottle.constant('setTimeout', window.setTimeout);
|
||||
bottle.constant('clearTimeout', window.clearTimeout);
|
||||
bottle.serviceFactory('useTimeoutToggle', useTimeoutToggle, 'setTimeout', 'clearTimeout');
|
||||
|
|
|
@ -5,7 +5,7 @@ import { useEffect } from 'react';
|
|||
import type { InputProps } from 'reactstrap';
|
||||
import { Button, DropdownItem, Input, InputGroup, UncontrolledTooltip } from 'reactstrap';
|
||||
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 './DomainSelector.scss';
|
||||
|
||||
|
|
|
@ -3,9 +3,9 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
|||
import type { FC } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { DropdownItem } from 'reactstrap';
|
||||
import { useToggle } from '../../../src/utils/helpers/hooks';
|
||||
import { RowDropdownBtn } from '../../../src/utils/RowDropdownBtn';
|
||||
import { useFeature } from '../../utils/features';
|
||||
import { useToggle } from '../../utils/helpers/hooks';
|
||||
import { useRoutesPrefix } from '../../utils/routesPrefix';
|
||||
import { DEFAULT_DOMAIN } from '../../visits/reducers/domainVisits';
|
||||
import type { Domain } from '../data';
|
||||
|
|
|
@ -7,9 +7,9 @@ import { useLocation, useParams } from 'react-router-dom';
|
|||
import { Button, Card } from 'reactstrap';
|
||||
import { ShlinkApiError } from '../../src/api/ShlinkApiError';
|
||||
import { useGoBack } from '../../src/utils/helpers/hooks';
|
||||
import { parseQuery } from '../../src/utils/helpers/query';
|
||||
import { Message } from '../../src/utils/Message';
|
||||
import { Result } from '../../src/utils/Result';
|
||||
import { parseQuery } from '../utils/helpers/query';
|
||||
import { useSetting } from '../utils/settings';
|
||||
import type { ShortUrlIdentifier } from './data';
|
||||
import { shortUrlDataFromShortUrl, urlDecodeShortCode } from './helpers';
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { faInfoCircle as infoIcon } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { Modal, ModalBody, ModalHeader } from 'reactstrap';
|
||||
import { useToggle } from '../../src/utils/helpers/hooks';
|
||||
import { useToggle } from '../utils/helpers/hooks';
|
||||
import './UseExistingIfFoundInfoIcon.scss';
|
||||
|
||||
const InfoModal = ({ isOpen, toggle }: { isOpen: boolean; toggle: () => void }) => (
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import type { FC } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
import type { ReportExporter } from '../../../src/common/services/ReportExporter';
|
||||
import { ExportBtn } from '../../../src/utils/ExportBtn';
|
||||
import { useToggle } from '../../../src/utils/helpers/hooks';
|
||||
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 { useShortUrlsQuery } from './hooks';
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ import { ExternalLink } from 'react-external-link';
|
|||
import { CopyToClipboardIcon } from '../../../src/utils/CopyToClipboardIcon';
|
||||
import { Time } from '../../../src/utils/dates/Time';
|
||||
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 type { ShortUrl } from '../data';
|
||||
import { useShortUrlsQuery } from './hooks';
|
||||
|
|
|
@ -7,8 +7,8 @@ import {
|
|||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import type { FC } from 'react';
|
||||
import { DropdownItem } from 'reactstrap';
|
||||
import { useToggle } from '../../../src/utils/helpers/hooks';
|
||||
import { RowDropdownBtn } from '../../../src/utils/RowDropdownBtn';
|
||||
import { useToggle } from '../../utils/helpers/hooks';
|
||||
import type { ShortUrl, ShortUrlModalProps } from '../data';
|
||||
import { ShortUrlDetailLink } from './ShortUrlDetailLink';
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { isEmpty } from 'ramda';
|
||||
import type { FC } from 'react';
|
||||
import type { ColorGenerator } from '../../../src/utils/services/ColorGenerator';
|
||||
import { Tag } from '../../tags/helpers/Tag';
|
||||
import type { ColorGenerator } from '../../utils/services/ColorGenerator';
|
||||
|
||||
interface TagsProps {
|
||||
tags: string[];
|
||||
|
|
|
@ -2,10 +2,10 @@ import { isEmpty, pipe } from 'ramda';
|
|||
import { useMemo } from 'react';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import { orderToString, stringToOrder } from '../../../src/utils/helpers/ordering';
|
||||
import { parseQuery, stringifyQuery } from '../../../src/utils/helpers/query';
|
||||
import type { BooleanString } from '../../../src/utils/utils';
|
||||
import { parseOptionalBooleanToString } from '../../../src/utils/utils';
|
||||
import type { TagsFilteringMode } from '../../api-contract';
|
||||
import { parseQuery, stringifyQuery } from '../../utils/helpers/query';
|
||||
import { useRoutesPrefix } from '../../utils/routesPrefix';
|
||||
import type { ShortUrlsOrder, ShortUrlsOrderableFields } from '../data';
|
||||
|
||||
|
|
|
@ -3,10 +3,10 @@ import type { FC } from 'react';
|
|||
import { useEffect, useRef } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
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 { 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 { TagsTableRowProps } from './TagsTableRow';
|
||||
import './TagsTable.scss';
|
||||
|
|
|
@ -3,11 +3,11 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
|||
import type { FC } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { DropdownItem } from 'reactstrap';
|
||||
import { useToggle } from '../../src/utils/helpers/hooks';
|
||||
import { prettify } from '../../src/utils/helpers/numbers';
|
||||
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 type { ColorGenerator } from '../utils/services/ColorGenerator';
|
||||
import type { SimplifiedTag, TagModalProps } from './data';
|
||||
import { TagBullet } from './helpers/TagBullet';
|
||||
|
||||
|
|
|
@ -5,10 +5,10 @@ import { useState } from 'react';
|
|||
import { HexColorPicker } from 'react-colorful';
|
||||
import { Button, Input, InputGroup, Modal, ModalBody, ModalFooter, ModalHeader, Popover } from 'reactstrap';
|
||||
import { ShlinkApiError } from '../../../src/api/ShlinkApiError';
|
||||
import { useToggle } from '../../../src/utils/helpers/hooks';
|
||||
import { Result } from '../../../src/utils/Result';
|
||||
import type { ColorGenerator } from '../../../src/utils/services/ColorGenerator';
|
||||
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 { EditTag, TagEdition } from '../reducers/tagEdit';
|
||||
import './EditTagModal.scss';
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import classNames from 'classnames';
|
||||
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';
|
||||
|
||||
type TagProps = PropsWithChildren<{
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import type { ColorGenerator } from '../../../src/utils/services/ColorGenerator';
|
||||
import type { ColorGenerator } from '../../utils/services/ColorGenerator';
|
||||
import './TagBullet.scss';
|
||||
|
||||
interface TagBulletProps {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { useEffect } from 'react';
|
||||
import type { SuggestionComponentProps, TagComponentProps } 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 type { TagsList } from '../reducers/tagsList';
|
||||
import { Tag } from './Tag';
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import type { PayloadAction } from '@reduxjs/toolkit';
|
||||
import { createAction, createSlice } from '@reduxjs/toolkit';
|
||||
import { pick } from 'ramda';
|
||||
import type { ColorGenerator } from '../../../src/utils/services/ColorGenerator';
|
||||
import type { ProblemDetailsError, ShlinkApiClient } from '../../api-contract';
|
||||
import { parseApiError } from '../../api-contract/utils';
|
||||
import { createAsyncThunk } from '../../utils/redux';
|
||||
import type { ColorGenerator } from '../../utils/services/ColorGenerator';
|
||||
|
||||
const REDUCER_PREFIX = 'shlink/tagEdit';
|
||||
|
||||
|
|
96
shlink-web-component/utils/helpers/hooks.ts
Normal file
96
shlink-web-component/utils/helpers/hooks.ts
Normal 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);
|
|
@ -1,6 +1,6 @@
|
|||
import { isNil } from 'ramda';
|
||||
import { rangeOf } from '../utils';
|
||||
import type { LocalStorage } from './LocalStorage';
|
||||
import type { LocalStorage } from '../../../src/utils/services/LocalStorage';
|
||||
import { rangeOf } from '../../../src/utils/utils';
|
||||
|
||||
const HEX_COLOR_LENGTH = 6;
|
||||
const HEX_DIGITS = '0123456789ABCDEF';
|
14
shlink-web-component/utils/services/LocalStorage.ts
Normal file
14
shlink-web-component/utils/services/LocalStorage.ts
Normal 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));
|
||||
}
|
30
shlink-web-component/utils/services/ReportExporter.ts
Normal file
30
shlink-web-component/utils/services/ReportExporter.ts
Normal 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);
|
||||
};
|
||||
}
|
14
shlink-web-component/utils/services/provideServices.ts
Normal file
14
shlink-web-component/utils/services/provideServices.ts
Normal 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');
|
||||
}
|
|
@ -1,9 +1,9 @@
|
|||
import { useParams } from 'react-router-dom';
|
||||
import type { ReportExporter } from '../../src/common/services/ReportExporter';
|
||||
import { useGoBack } from '../../src/utils/helpers/hooks';
|
||||
import type { ShlinkVisitsParams } from '../api-contract';
|
||||
import { boundToMercureHub } from '../mercure/helpers/boundToMercureHub';
|
||||
import { Topics } from '../mercure/helpers/Topics';
|
||||
import type { ReportExporter } from '../utils/services/ReportExporter';
|
||||
import type { DomainVisits as DomainVisitsState, LoadDomainVisits } from './reducers/domainVisits';
|
||||
import type { NormalizedVisit } from './types';
|
||||
import { toApiParams } from './types/helpers';
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import type { ReportExporter } from '../../src/common/services/ReportExporter';
|
||||
import { useGoBack } from '../../src/utils/helpers/hooks';
|
||||
import { boundToMercureHub } from '../mercure/helpers/boundToMercureHub';
|
||||
import { Topics } from '../mercure/helpers/Topics';
|
||||
import type { ReportExporter } from '../utils/services/ReportExporter';
|
||||
import type { LoadVisits, VisitsInfo } from './reducers/types';
|
||||
import type { NormalizedVisit, VisitsParams } from './types';
|
||||
import { toApiParams } from './types/helpers';
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import type { ReportExporter } from '../../src/common/services/ReportExporter';
|
||||
import { useGoBack } from '../../src/utils/helpers/hooks';
|
||||
import { boundToMercureHub } from '../mercure/helpers/boundToMercureHub';
|
||||
import { Topics } from '../mercure/helpers/Topics';
|
||||
import type { ReportExporter } from '../utils/services/ReportExporter';
|
||||
import type { LoadOrphanVisits } from './reducers/orphanVisits';
|
||||
import type { VisitsInfo } from './reducers/types';
|
||||
import type { NormalizedVisit, VisitsParams } from './types';
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
import { useEffect } from 'react';
|
||||
import { useLocation, useParams } from 'react-router-dom';
|
||||
import type { ReportExporter } from '../../src/common/services/ReportExporter';
|
||||
import { useGoBack } from '../../src/utils/helpers/hooks';
|
||||
import { parseQuery } from '../../src/utils/helpers/query';
|
||||
import { boundToMercureHub } from '../mercure/helpers/boundToMercureHub';
|
||||
import { Topics } from '../mercure/helpers/Topics';
|
||||
import type { ShortUrlIdentifier } from '../short-urls/data';
|
||||
import { urlDecodeShortCode } from '../short-urls/helpers';
|
||||
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 { ShortUrlVisitsHeader } from './ShortUrlVisitsHeader';
|
||||
import type { NormalizedVisit, VisitsParams } from './types';
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
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 type { ColorGenerator } from '../../src/utils/services/ColorGenerator';
|
||||
import type { ShlinkVisitsParams } from '../api-contract';
|
||||
import { boundToMercureHub } from '../mercure/helpers/boundToMercureHub';
|
||||
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 { TagVisitsHeader } from './TagVisitsHeader';
|
||||
import type { NormalizedVisit } from './types';
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import type { ColorGenerator } from '../../src/utils/services/ColorGenerator';
|
||||
import { Tag } from '../tags/helpers/Tag';
|
||||
import type { ColorGenerator } from '../utils/services/ColorGenerator';
|
||||
import type { TagVisits } from './reducers/tagVisits';
|
||||
import { VisitsHeader } from './VisitsHeader';
|
||||
import './ShortUrlVisitsHeader.scss';
|
||||
|
|
|
@ -25,11 +25,11 @@ import {
|
|||
} from 'reactstrap';
|
||||
import { pointerOnHover, renderChartLabel } from '../../../src/utils/helpers/charts';
|
||||
import { STANDARD_DATE_FORMAT } from '../../../src/utils/helpers/date';
|
||||
import { useToggle } from '../../../src/utils/helpers/hooks';
|
||||
import { prettify } from '../../../src/utils/helpers/numbers';
|
||||
import { HIGHLIGHTED_COLOR, MAIN_COLOR } from '../../../src/utils/theme';
|
||||
import { ToggleSwitch } from '../../../src/utils/ToggleSwitch';
|
||||
import { rangeOf } from '../../../src/utils/utils';
|
||||
import { useToggle } from '../../utils/helpers/hooks';
|
||||
import type { NormalizedVisit, Stats } from '../types';
|
||||
import { fillTheGaps } from '../utils';
|
||||
import './LineChartCard.scss';
|
||||
|
|
|
@ -2,7 +2,7 @@ import { faMapMarkedAlt as mapIcon } from '@fortawesome/free-solid-svg-icons';
|
|||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { useState } from 'react';
|
||||
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 { MapModal } from './MapModal';
|
||||
import './OpenMapModalBtn.scss';
|
||||
|
|
|
@ -5,9 +5,9 @@ import { useLocation, useNavigate } from 'react-router-dom';
|
|||
import { formatIsoDate } from '../../../src/utils/helpers/date';
|
||||
import type { DateRange } 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 { parseBooleanToString } from '../../../src/utils/utils';
|
||||
import { parseQuery, stringifyQuery } from '../../utils/helpers/query';
|
||||
import type { OrphanVisitType, VisitsFilter } from '../types';
|
||||
|
||||
interface VisitsQuery {
|
||||
|
|
|
@ -18,9 +18,9 @@ import type {
|
|||
ShlinkVisitsParams } from '../../../shlink-web-component/api-contract';
|
||||
import { isRegularNotFound, parseApiError } from '../../../shlink-web-component/api-contract/utils';
|
||||
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 { orderToString } from '../../utils/helpers/ordering';
|
||||
import { stringifyQuery } from '../../utils/helpers/query';
|
||||
import { replaceAuthorityFromUri } from '../../utils/helpers/uri';
|
||||
import type { OptionalString } from '../../utils/utils';
|
||||
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
}
|
|
@ -10,7 +10,6 @@ import { ScrollToTop } from '../ScrollToTop';
|
|||
import { ShlinkVersionsContainer } from '../ShlinkVersionsContainer';
|
||||
import { HttpClient } from './HttpClient';
|
||||
import { ImageDownloader } from './ImageDownloader';
|
||||
import { ReportExporter } from './ReportExporter';
|
||||
|
||||
export const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
|
||||
// Services
|
||||
|
@ -20,7 +19,6 @@ export const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
|
|||
|
||||
bottle.service('HttpClient', HttpClient, 'fetch');
|
||||
bottle.service('ImageDownloader', ImageDownloader, 'HttpClient', 'window');
|
||||
bottle.service('ReportExporter', ReportExporter, 'window', 'jsonToCsv');
|
||||
|
||||
// Components
|
||||
bottle.serviceFactory('ScrollToTop', () => ScrollToTop);
|
||||
|
|
|
@ -7,7 +7,7 @@ export const csvToJson = <T>(csvContent: string) => new Promise<T[]>((resolve) =
|
|||
|
||||
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);
|
||||
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
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';
|
||||
import { parseQuery } from '../../../shlink-web-component/utils/helpers/query';
|
||||
|
||||
const DEFAULT_DELAY = 2000;
|
||||
|
||||
|
@ -35,40 +34,6 @@ export const useToggle = (initialValue = false): ToggleResult => {
|
|||
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);
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { isEmpty } from 'ramda';
|
||||
import { stringifyQuery } from './query';
|
||||
import { stringifyQuery } from '../../../shlink-web-component/utils/helpers/query';
|
||||
|
||||
export type QrCodeFormat = 'svg' | 'png';
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import type Bottle from 'bottlejs';
|
||||
import { ColorGenerator } from '../../../shlink-web-component/utils/services/ColorGenerator';
|
||||
import { csvToJson, jsonToCsv } from '../helpers/csvjson';
|
||||
import { useTimeoutToggle } from '../helpers/hooks';
|
||||
import { ColorGenerator } from './ColorGenerator';
|
||||
import { LocalStorage } from './LocalStorage';
|
||||
|
||||
export const provideServices = (bottle: Bottle) => {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { render, screen } from '@testing-library/react';
|
||||
import { fromPartial } from '@total-typescript/shoehorn';
|
||||
import { MemoryRouter } from 'react-router';
|
||||
import { AsideMenu } from '../../src/common/AsideMenu';
|
||||
import { AsideMenu } from '../../shlink-web-component/common/AsideMenu';
|
||||
|
||||
describe('<AsideMenu />', () => {
|
||||
const setUp = () => render(
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
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 { ReportExporter } from '../../../src/common/services/ReportExporter';
|
||||
import { windowMock } from '../../__mocks__/Window.mock';
|
||||
|
||||
describe('ReportExporter', () => {
|
||||
|
|
|
@ -3,7 +3,7 @@ import { fromPartial } from '@total-typescript/shoehorn';
|
|||
import { MemoryRouter } from 'react-router-dom';
|
||||
import type { ShortUrl } from '../../../shlink-web-component/short-urls/data';
|
||||
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 { renderWithEvents } from '../../__helpers__/setUpTest';
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@ import { screen } from '@testing-library/react';
|
|||
import { fromPartial } from '@total-typescript/shoehorn';
|
||||
import type { ReactNode } from 'react';
|
||||
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 { renderWithEvents } from '../../__helpers__/setUpTest';
|
||||
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { fromPartial } from '@total-typescript/shoehorn';
|
||||
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 { ShlinkState } from '../../../src/container/types';
|
||||
import type { ColorGenerator } from '../../../src/utils/services/ColorGenerator';
|
||||
|
||||
describe('tagEditReducer', () => {
|
||||
const oldName = 'foo';
|
||||
|
|
|
@ -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('parseQuery', () => {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
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 { MAIN_COLOR } from '../../../src/utils/theme';
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
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>({
|
||||
getColorForKey: vi.fn(() => 'red'),
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { render, screen } from '@testing-library/react';
|
||||
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 { TagVisitsHeader } from '../../shlink-web-component/visits/TagVisitsHeader';
|
||||
import type { ColorGenerator } from '../../src/utils/services/ColorGenerator';
|
||||
|
||||
describe('<TagVisitsHeader />', () => {
|
||||
const tagVisits = fromPartial<TagVisits>({
|
||||
|
|
Loading…
Reference in a new issue