mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2026-03-15 03:53:51 +00:00
Merge pull request #402 from acelaya-forks/feature/visits-default-value
Feature/visits default value
This commit is contained in:
@@ -12,6 +12,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|||||||
* [#177](https://github.com/shlinkio/shlink-web-client/issues/177) Added dark theme.
|
* [#177](https://github.com/shlinkio/shlink-web-client/issues/177) Added dark theme.
|
||||||
* [#387](https://github.com/shlinkio/shlink-web-client/issues/387) Added a section to see orphan visits stats, when consuming Shlink >=2.6.0.
|
* [#387](https://github.com/shlinkio/shlink-web-client/issues/387) Added a section to see orphan visits stats, when consuming Shlink >=2.6.0.
|
||||||
* [#383](https://github.com/shlinkio/shlink-web-client/issues/383) Added title to short URLs list, displayed when consuming Shlink >=2.6.0.
|
* [#383](https://github.com/shlinkio/shlink-web-client/issues/383) Added title to short URLs list, displayed when consuming Shlink >=2.6.0.
|
||||||
|
* [#368](https://github.com/shlinkio/shlink-web-client/issues/368) Added new settings to define the default interval for visits pages.
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
* [#382](https://github.com/shlinkio/shlink-web-client/issues/382) Ensured short URL tags are edited through the `PATCH /short-urls/{shortCode}` endpoint when using Shlink 2.6.0 or higher.
|
* [#382](https://github.com/shlinkio/shlink-web-client/issues/382) Ensured short URL tags are edited through the `PATCH /short-urls/{shortCode}` endpoint when using Shlink 2.6.0 or higher.
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ const intervalValue = (interval?: number) => !interval ? '' : `${interval}`;
|
|||||||
const RealTimeUpdates = (
|
const RealTimeUpdates = (
|
||||||
{ settings: { realTimeUpdates }, toggleRealTimeUpdates, setRealTimeUpdatesInterval }: RealTimeUpdatesProps,
|
{ settings: { realTimeUpdates }, toggleRealTimeUpdates, setRealTimeUpdatesInterval }: RealTimeUpdatesProps,
|
||||||
) => (
|
) => (
|
||||||
<SimpleCard title="Real-time updates">
|
<SimpleCard title="Real-time updates" className="h-100">
|
||||||
<FormGroup>
|
<FormGroup>
|
||||||
<ToggleSwitch checked={realTimeUpdates.enabled} onChange={toggleRealTimeUpdates}>
|
<ToggleSwitch checked={realTimeUpdates.enabled} onChange={toggleRealTimeUpdates}>
|
||||||
Enable or disable real-time updates, when using Shlink v2.2.0 or newer.
|
Enable or disable real-time updates, when using Shlink v2.2.0 or newer.
|
||||||
|
|||||||
@@ -1,22 +1,29 @@
|
|||||||
import { FC } from 'react';
|
import { FC, ReactNode } from 'react';
|
||||||
import { Row } from 'reactstrap';
|
import { Row } from 'reactstrap';
|
||||||
import NoMenuLayout from '../common/NoMenuLayout';
|
import NoMenuLayout from '../common/NoMenuLayout';
|
||||||
|
|
||||||
const Settings = (RealTimeUpdates: FC, ShortUrlCreation: FC, UserInterface: FC) => () => (
|
const SettingsSections: FC<{ items: ReactNode[][] }> = ({ items }) => (
|
||||||
|
<>
|
||||||
|
{items.map((child, index) => (
|
||||||
|
<Row key={index}>
|
||||||
|
{child.map((subChild, subIndex) => (
|
||||||
|
<div key={subIndex} className="col-lg-6 mb-3 mb-md-4">
|
||||||
|
{subChild}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</Row>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
const Settings = (RealTimeUpdates: FC, ShortUrlCreation: FC, UserInterface: FC, Visits: FC) => () => (
|
||||||
<NoMenuLayout>
|
<NoMenuLayout>
|
||||||
<Row>
|
<SettingsSections
|
||||||
<div className="col-lg-6">
|
items={[
|
||||||
<div className="mb-3 mb-md-4">
|
[ <UserInterface />, <ShortUrlCreation /> ], // eslint-disable-line react/jsx-key
|
||||||
<UserInterface />
|
[ <Visits />, <RealTimeUpdates /> ], // eslint-disable-line react/jsx-key
|
||||||
</div>
|
]}
|
||||||
<div className="mb-3 mb-md-4">
|
/>
|
||||||
<ShortUrlCreation />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="col-lg-6">
|
|
||||||
<RealTimeUpdates />
|
|
||||||
</div>
|
|
||||||
</Row>
|
|
||||||
</NoMenuLayout>
|
</NoMenuLayout>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ interface ShortUrlCreationProps {
|
|||||||
export const ShortUrlCreation: FC<ShortUrlCreationProps> = (
|
export const ShortUrlCreation: FC<ShortUrlCreationProps> = (
|
||||||
{ settings: { shortUrlCreation }, setShortUrlCreationSettings },
|
{ settings: { shortUrlCreation }, setShortUrlCreationSettings },
|
||||||
) => (
|
) => (
|
||||||
<SimpleCard title="Short URLs creation">
|
<SimpleCard title="Short URLs creation" className="h-100">
|
||||||
<FormGroup className="mb-0">
|
<FormGroup className="mb-0">
|
||||||
<ToggleSwitch
|
<ToggleSwitch
|
||||||
checked={shortUrlCreation?.validateUrls ?? false}
|
checked={shortUrlCreation?.validateUrls ?? false}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ interface UserInterfaceProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const UserInterface: FC<UserInterfaceProps> = ({ settings: { ui }, setUiSettings }) => (
|
export const UserInterface: FC<UserInterfaceProps> = ({ settings: { ui }, setUiSettings }) => (
|
||||||
<SimpleCard title="User interface">
|
<SimpleCard title="User interface" className="h-100">
|
||||||
<FontAwesomeIcon icon={ui?.theme === 'dark' ? faMoon : faSun} className="user-interface__theme-icon" />
|
<FontAwesomeIcon icon={ui?.theme === 'dark' ? faMoon : faSun} className="user-interface__theme-icon" />
|
||||||
<ToggleSwitch
|
<ToggleSwitch
|
||||||
checked={ui?.theme === 'dark'}
|
checked={ui?.theme === 'dark'}
|
||||||
|
|||||||
22
src/settings/Visits.tsx
Normal file
22
src/settings/Visits.tsx
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { FormGroup } from 'reactstrap';
|
||||||
|
import { FC } from 'react';
|
||||||
|
import { SimpleCard } from '../utils/SimpleCard';
|
||||||
|
import { DateIntervalSelector } from '../utils/dates/DateIntervalSelector';
|
||||||
|
import { Settings, VisitsSettings } from './reducers/settings';
|
||||||
|
|
||||||
|
interface VisitsProps {
|
||||||
|
settings: Settings;
|
||||||
|
setVisitsSettings: (settings: VisitsSettings) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Visits: FC<VisitsProps> = ({ settings, setVisitsSettings }) => (
|
||||||
|
<SimpleCard title="Visits" className="h-100">
|
||||||
|
<FormGroup className="mb-0">
|
||||||
|
<label>Default interval to load on visits sections:</label>
|
||||||
|
<DateIntervalSelector
|
||||||
|
active={settings.visits?.defaultInterval ?? 'last30Days'}
|
||||||
|
onChange={(defaultInterval) => setVisitsSettings({ defaultInterval })}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
</SimpleCard>
|
||||||
|
);
|
||||||
@@ -3,6 +3,7 @@ import { dissoc, mergeDeepRight } from 'ramda';
|
|||||||
import { buildReducer } from '../../utils/helpers/redux';
|
import { buildReducer } from '../../utils/helpers/redux';
|
||||||
import { RecursivePartial } from '../../utils/utils';
|
import { RecursivePartial } from '../../utils/utils';
|
||||||
import { Theme } from '../../utils/theme';
|
import { Theme } from '../../utils/theme';
|
||||||
|
import { DateInterval } from '../../utils/dates/types';
|
||||||
|
|
||||||
export const SET_SETTINGS = 'shlink/realTimeUpdates/SET_SETTINGS';
|
export const SET_SETTINGS = 'shlink/realTimeUpdates/SET_SETTINGS';
|
||||||
|
|
||||||
@@ -24,10 +25,15 @@ export interface UiSettings {
|
|||||||
theme: Theme;
|
theme: Theme;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface VisitsSettings {
|
||||||
|
defaultInterval: DateInterval;
|
||||||
|
}
|
||||||
|
|
||||||
export interface Settings {
|
export interface Settings {
|
||||||
realTimeUpdates: RealTimeUpdatesSettings;
|
realTimeUpdates: RealTimeUpdatesSettings;
|
||||||
shortUrlCreation?: ShortUrlCreationSettings;
|
shortUrlCreation?: ShortUrlCreationSettings;
|
||||||
ui?: UiSettings;
|
ui?: UiSettings;
|
||||||
|
visits?: VisitsSettings;
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialState: Settings = {
|
const initialState: Settings = {
|
||||||
@@ -40,6 +46,9 @@ const initialState: Settings = {
|
|||||||
ui: {
|
ui: {
|
||||||
theme: 'light',
|
theme: 'light',
|
||||||
},
|
},
|
||||||
|
visits: {
|
||||||
|
defaultInterval: 'last30Days',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
type SettingsAction = Action & Settings;
|
type SettingsAction = Action & Settings;
|
||||||
@@ -69,3 +78,8 @@ export const setUiSettings = (settings: UiSettings): PartialSettingsAction => ({
|
|||||||
type: SET_SETTINGS,
|
type: SET_SETTINGS,
|
||||||
ui: settings,
|
ui: settings,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const setVisitsSettings = (settings: VisitsSettings): PartialSettingsAction => ({
|
||||||
|
type: SET_SETTINGS,
|
||||||
|
visits: settings,
|
||||||
|
});
|
||||||
|
|||||||
@@ -5,16 +5,18 @@ import {
|
|||||||
setRealTimeUpdatesInterval,
|
setRealTimeUpdatesInterval,
|
||||||
setShortUrlCreationSettings,
|
setShortUrlCreationSettings,
|
||||||
setUiSettings,
|
setUiSettings,
|
||||||
|
setVisitsSettings,
|
||||||
toggleRealTimeUpdates,
|
toggleRealTimeUpdates,
|
||||||
} from '../reducers/settings';
|
} from '../reducers/settings';
|
||||||
import { ConnectDecorator } from '../../container/types';
|
import { ConnectDecorator } from '../../container/types';
|
||||||
import { withoutSelectedServer } from '../../servers/helpers/withoutSelectedServer';
|
import { withoutSelectedServer } from '../../servers/helpers/withoutSelectedServer';
|
||||||
import { ShortUrlCreation } from '../ShortUrlCreation';
|
import { ShortUrlCreation } from '../ShortUrlCreation';
|
||||||
import { UserInterface } from '../UserInterface';
|
import { UserInterface } from '../UserInterface';
|
||||||
|
import { Visits } from '../Visits';
|
||||||
|
|
||||||
const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
|
const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
|
||||||
// Components
|
// Components
|
||||||
bottle.serviceFactory('Settings', Settings, 'RealTimeUpdates', 'ShortUrlCreation', 'UserInterface');
|
bottle.serviceFactory('Settings', Settings, 'RealTimeUpdates', 'ShortUrlCreation', 'UserInterface', 'Visits');
|
||||||
bottle.decorator('Settings', withoutSelectedServer);
|
bottle.decorator('Settings', withoutSelectedServer);
|
||||||
bottle.decorator('Settings', connect(null, [ 'resetSelectedServer' ]));
|
bottle.decorator('Settings', connect(null, [ 'resetSelectedServer' ]));
|
||||||
|
|
||||||
@@ -30,11 +32,15 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
|
|||||||
bottle.serviceFactory('UserInterface', () => UserInterface);
|
bottle.serviceFactory('UserInterface', () => UserInterface);
|
||||||
bottle.decorator('UserInterface', connect([ 'settings' ], [ 'setUiSettings' ]));
|
bottle.decorator('UserInterface', connect([ 'settings' ], [ 'setUiSettings' ]));
|
||||||
|
|
||||||
|
bottle.serviceFactory('Visits', () => Visits);
|
||||||
|
bottle.decorator('Visits', connect([ 'settings' ], [ 'setVisitsSettings' ]));
|
||||||
|
|
||||||
// Actions
|
// Actions
|
||||||
bottle.serviceFactory('toggleRealTimeUpdates', () => toggleRealTimeUpdates);
|
bottle.serviceFactory('toggleRealTimeUpdates', () => toggleRealTimeUpdates);
|
||||||
bottle.serviceFactory('setRealTimeUpdatesInterval', () => setRealTimeUpdatesInterval);
|
bottle.serviceFactory('setRealTimeUpdatesInterval', () => setRealTimeUpdatesInterval);
|
||||||
bottle.serviceFactory('setShortUrlCreationSettings', () => setShortUrlCreationSettings);
|
bottle.serviceFactory('setShortUrlCreationSettings', () => setShortUrlCreationSettings);
|
||||||
bottle.serviceFactory('setUiSettings', () => setUiSettings);
|
bottle.serviceFactory('setUiSettings', () => setUiSettings);
|
||||||
|
bottle.serviceFactory('setVisitsSettings', () => setVisitsSettings);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default provideServices;
|
export default provideServices;
|
||||||
|
|||||||
20
src/utils/dates/DateIntervalDropdownItems.tsx
Normal file
20
src/utils/dates/DateIntervalDropdownItems.tsx
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { DropdownItem } from 'reactstrap';
|
||||||
|
import { FC } from 'react';
|
||||||
|
import { DATE_INTERVALS, DateInterval, rangeOrIntervalToString } from './types';
|
||||||
|
|
||||||
|
export interface DateIntervalDropdownProps {
|
||||||
|
active?: DateInterval;
|
||||||
|
onChange: (interval: DateInterval) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DateIntervalDropdownItems: FC<DateIntervalDropdownProps> = ({ active, onChange }) => (
|
||||||
|
<>
|
||||||
|
{DATE_INTERVALS.map(
|
||||||
|
(interval) => (
|
||||||
|
<DropdownItem key={interval} active={active === interval} onClick={() => onChange(interval)}>
|
||||||
|
{rangeOrIntervalToString(interval)}
|
||||||
|
</DropdownItem>
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
10
src/utils/dates/DateIntervalSelector.tsx
Normal file
10
src/utils/dates/DateIntervalSelector.tsx
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { FC } from 'react';
|
||||||
|
import { DropdownBtn } from '../DropdownBtn';
|
||||||
|
import { rangeOrIntervalToString } from './types';
|
||||||
|
import { DateIntervalDropdownItems, DateIntervalDropdownProps } from './DateIntervalDropdownItems';
|
||||||
|
|
||||||
|
export const DateIntervalSelector: FC<DateIntervalDropdownProps> = ({ onChange, active }) => (
|
||||||
|
<DropdownBtn text={rangeOrIntervalToString(active) ?? ''}>
|
||||||
|
<DateIntervalDropdownItems active={active} onChange={onChange} />
|
||||||
|
</DropdownBtn>
|
||||||
|
);
|
||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
rangeIsInterval,
|
rangeIsInterval,
|
||||||
} from './types';
|
} from './types';
|
||||||
import DateRangeRow from './DateRangeRow';
|
import DateRangeRow from './DateRangeRow';
|
||||||
|
import { DateIntervalDropdownItems } from './DateIntervalDropdownItems';
|
||||||
|
|
||||||
export interface DateRangeSelectorProps {
|
export interface DateRangeSelectorProps {
|
||||||
initialDateRange?: DateInterval | DateRange;
|
initialDateRange?: DateInterval | DateRange;
|
||||||
@@ -47,13 +48,7 @@ export const DateRangeSelector = (
|
|||||||
{defaultText}
|
{defaultText}
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
<DropdownItem divider />
|
<DropdownItem divider />
|
||||||
{([ 'today', 'yesterday', 'last7Days', 'last30Days', 'last90Days', 'last180days', 'last365Days' ] as DateInterval[]).map(
|
<DateIntervalDropdownItems active={activeInterval} onChange={(interval) => updateInterval(interval)()} />
|
||||||
(interval) => (
|
|
||||||
<DropdownItem key={interval} active={activeInterval === interval} onClick={updateInterval(interval)}>
|
|
||||||
{rangeOrIntervalToString(interval)}
|
|
||||||
</DropdownItem>
|
|
||||||
),
|
|
||||||
)}
|
|
||||||
<DropdownItem divider />
|
<DropdownItem divider />
|
||||||
<DropdownItem header>Custom:</DropdownItem>
|
<DropdownItem header>Custom:</DropdownItem>
|
||||||
<DropdownItem text>
|
<DropdownItem text>
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ const INTERVAL_TO_STRING_MAP: Record<DateInterval, string> = {
|
|||||||
last365Days: 'Last 365 days',
|
last365Days: 'Last 365 days',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const DATE_INTERVALS: DateInterval[] = Object.keys(INTERVAL_TO_STRING_MAP) as DateInterval[];
|
||||||
|
|
||||||
const dateRangeToString = (range?: DateRange): string | undefined => {
|
const dateRangeToString = (range?: DateRange): string | undefined => {
|
||||||
if (!range || dateRangeIsEmpty(range)) {
|
if (!range || dateRangeIsEmpty(range)) {
|
||||||
return undefined;
|
return undefined;
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { RouteComponentProps } from 'react-router';
|
|||||||
import { boundToMercureHub } from '../mercure/helpers/boundToMercureHub';
|
import { boundToMercureHub } from '../mercure/helpers/boundToMercureHub';
|
||||||
import { ShlinkVisitsParams } from '../api/types';
|
import { ShlinkVisitsParams } from '../api/types';
|
||||||
import { Topics } from '../mercure/helpers/Topics';
|
import { Topics } from '../mercure/helpers/Topics';
|
||||||
|
import { Settings } from '../settings/reducers/settings';
|
||||||
import VisitsStats from './VisitsStats';
|
import VisitsStats from './VisitsStats';
|
||||||
import { OrphanVisitsHeader } from './OrphanVisitsHeader';
|
import { OrphanVisitsHeader } from './OrphanVisitsHeader';
|
||||||
import { VisitsInfo } from './types';
|
import { VisitsInfo } from './types';
|
||||||
@@ -10,6 +11,7 @@ export interface OrphanVisitsProps extends RouteComponentProps {
|
|||||||
getOrphanVisits: (params: ShlinkVisitsParams) => void;
|
getOrphanVisits: (params: ShlinkVisitsParams) => void;
|
||||||
orphanVisits: VisitsInfo;
|
orphanVisits: VisitsInfo;
|
||||||
cancelGetOrphanVisits: () => void;
|
cancelGetOrphanVisits: () => void;
|
||||||
|
settings: Settings;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const OrphanVisits = boundToMercureHub(({
|
export const OrphanVisits = boundToMercureHub(({
|
||||||
@@ -18,12 +20,14 @@ export const OrphanVisits = boundToMercureHub(({
|
|||||||
getOrphanVisits,
|
getOrphanVisits,
|
||||||
orphanVisits,
|
orphanVisits,
|
||||||
cancelGetOrphanVisits,
|
cancelGetOrphanVisits,
|
||||||
|
settings,
|
||||||
}: OrphanVisitsProps) => (
|
}: OrphanVisitsProps) => (
|
||||||
<VisitsStats
|
<VisitsStats
|
||||||
getVisits={getOrphanVisits}
|
getVisits={getOrphanVisits}
|
||||||
cancelGetVisits={cancelGetOrphanVisits}
|
cancelGetVisits={cancelGetOrphanVisits}
|
||||||
visitsInfo={orphanVisits}
|
visitsInfo={orphanVisits}
|
||||||
baseUrl={url}
|
baseUrl={url}
|
||||||
|
settings={settings}
|
||||||
>
|
>
|
||||||
<OrphanVisitsHeader orphanVisits={orphanVisits} goBack={goBack} />
|
<OrphanVisitsHeader orphanVisits={orphanVisits} goBack={goBack} />
|
||||||
</VisitsStats>
|
</VisitsStats>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { ShlinkVisitsParams } from '../api/types';
|
|||||||
import { parseQuery } from '../utils/helpers/query';
|
import { parseQuery } from '../utils/helpers/query';
|
||||||
import { Topics } from '../mercure/helpers/Topics';
|
import { Topics } from '../mercure/helpers/Topics';
|
||||||
import { ShortUrlDetail } from '../short-urls/reducers/shortUrlDetail';
|
import { ShortUrlDetail } from '../short-urls/reducers/shortUrlDetail';
|
||||||
|
import { Settings } from '../settings/reducers/settings';
|
||||||
import { ShortUrlVisits as ShortUrlVisitsState } from './reducers/shortUrlVisits';
|
import { ShortUrlVisits as ShortUrlVisitsState } from './reducers/shortUrlVisits';
|
||||||
import ShortUrlVisitsHeader from './ShortUrlVisitsHeader';
|
import ShortUrlVisitsHeader from './ShortUrlVisitsHeader';
|
||||||
import VisitsStats from './VisitsStats';
|
import VisitsStats from './VisitsStats';
|
||||||
@@ -15,6 +16,7 @@ export interface ShortUrlVisitsProps extends RouteComponentProps<{ shortCode: st
|
|||||||
getShortUrlDetail: Function;
|
getShortUrlDetail: Function;
|
||||||
shortUrlDetail: ShortUrlDetail;
|
shortUrlDetail: ShortUrlDetail;
|
||||||
cancelGetShortUrlVisits: () => void;
|
cancelGetShortUrlVisits: () => void;
|
||||||
|
settings: Settings;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ShortUrlVisits = boundToMercureHub(({
|
const ShortUrlVisits = boundToMercureHub(({
|
||||||
@@ -26,6 +28,7 @@ const ShortUrlVisits = boundToMercureHub(({
|
|||||||
getShortUrlVisits,
|
getShortUrlVisits,
|
||||||
getShortUrlDetail,
|
getShortUrlDetail,
|
||||||
cancelGetShortUrlVisits,
|
cancelGetShortUrlVisits,
|
||||||
|
settings,
|
||||||
}: ShortUrlVisitsProps) => {
|
}: ShortUrlVisitsProps) => {
|
||||||
const { shortCode } = params;
|
const { shortCode } = params;
|
||||||
const { domain } = parseQuery<{ domain?: string }>(search);
|
const { domain } = parseQuery<{ domain?: string }>(search);
|
||||||
@@ -42,6 +45,7 @@ const ShortUrlVisits = boundToMercureHub(({
|
|||||||
visitsInfo={shortUrlVisits}
|
visitsInfo={shortUrlVisits}
|
||||||
baseUrl={url}
|
baseUrl={url}
|
||||||
domain={domain}
|
domain={domain}
|
||||||
|
settings={settings}
|
||||||
>
|
>
|
||||||
<ShortUrlVisitsHeader shortUrlDetail={shortUrlDetail} shortUrlVisits={shortUrlVisits} goBack={goBack} />
|
<ShortUrlVisitsHeader shortUrlDetail={shortUrlDetail} shortUrlVisits={shortUrlVisits} goBack={goBack} />
|
||||||
</VisitsStats>
|
</VisitsStats>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { boundToMercureHub } from '../mercure/helpers/boundToMercureHub';
|
|||||||
import ColorGenerator from '../utils/services/ColorGenerator';
|
import ColorGenerator from '../utils/services/ColorGenerator';
|
||||||
import { ShlinkVisitsParams } from '../api/types';
|
import { ShlinkVisitsParams } from '../api/types';
|
||||||
import { Topics } from '../mercure/helpers/Topics';
|
import { Topics } from '../mercure/helpers/Topics';
|
||||||
|
import { Settings } from '../settings/reducers/settings';
|
||||||
import { TagVisits as TagVisitsState } from './reducers/tagVisits';
|
import { TagVisits as TagVisitsState } from './reducers/tagVisits';
|
||||||
import TagVisitsHeader from './TagVisitsHeader';
|
import TagVisitsHeader from './TagVisitsHeader';
|
||||||
import VisitsStats from './VisitsStats';
|
import VisitsStats from './VisitsStats';
|
||||||
@@ -11,6 +12,7 @@ export interface TagVisitsProps extends RouteComponentProps<{ tag: string }> {
|
|||||||
getTagVisits: (tag: string, query: any) => void;
|
getTagVisits: (tag: string, query: any) => void;
|
||||||
tagVisits: TagVisitsState;
|
tagVisits: TagVisitsState;
|
||||||
cancelGetTagVisits: () => void;
|
cancelGetTagVisits: () => void;
|
||||||
|
settings: Settings;
|
||||||
}
|
}
|
||||||
|
|
||||||
const TagVisits = (colorGenerator: ColorGenerator) => boundToMercureHub(({
|
const TagVisits = (colorGenerator: ColorGenerator) => boundToMercureHub(({
|
||||||
@@ -19,12 +21,19 @@ const TagVisits = (colorGenerator: ColorGenerator) => boundToMercureHub(({
|
|||||||
getTagVisits,
|
getTagVisits,
|
||||||
tagVisits,
|
tagVisits,
|
||||||
cancelGetTagVisits,
|
cancelGetTagVisits,
|
||||||
|
settings,
|
||||||
}: TagVisitsProps) => {
|
}: TagVisitsProps) => {
|
||||||
const { tag } = params;
|
const { tag } = params;
|
||||||
const loadVisits = (params: ShlinkVisitsParams) => getTagVisits(tag, params);
|
const loadVisits = (params: ShlinkVisitsParams) => getTagVisits(tag, params);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<VisitsStats getVisits={loadVisits} cancelGetVisits={cancelGetTagVisits} visitsInfo={tagVisits} baseUrl={url}>
|
<VisitsStats
|
||||||
|
getVisits={loadVisits}
|
||||||
|
cancelGetVisits={cancelGetTagVisits}
|
||||||
|
visitsInfo={tagVisits}
|
||||||
|
baseUrl={url}
|
||||||
|
settings={settings}
|
||||||
|
>
|
||||||
<TagVisitsHeader tagVisits={tagVisits} goBack={goBack} colorGenerator={colorGenerator} />
|
<TagVisitsHeader tagVisits={tagVisits} goBack={goBack} colorGenerator={colorGenerator} />
|
||||||
</VisitsStats>
|
</VisitsStats>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { ShlinkVisitsParams } from '../api/types';
|
|||||||
import { DateInterval, DateRange, intervalToDateRange } from '../utils/dates/types';
|
import { DateInterval, DateRange, intervalToDateRange } from '../utils/dates/types';
|
||||||
import { Result } from '../utils/Result';
|
import { Result } from '../utils/Result';
|
||||||
import { ShlinkApiError } from '../api/ShlinkApiError';
|
import { ShlinkApiError } from '../api/ShlinkApiError';
|
||||||
|
import { Settings } from '../settings/reducers/settings';
|
||||||
import SortableBarGraph from './helpers/SortableBarGraph';
|
import SortableBarGraph from './helpers/SortableBarGraph';
|
||||||
import GraphCard from './helpers/GraphCard';
|
import GraphCard from './helpers/GraphCard';
|
||||||
import LineChartCard from './helpers/LineChartCard';
|
import LineChartCard from './helpers/LineChartCard';
|
||||||
@@ -25,6 +26,7 @@ import './VisitsStats.scss';
|
|||||||
export interface VisitsStatsProps {
|
export interface VisitsStatsProps {
|
||||||
getVisits: (params: Partial<ShlinkVisitsParams>) => void;
|
getVisits: (params: Partial<ShlinkVisitsParams>) => void;
|
||||||
visitsInfo: VisitsInfo;
|
visitsInfo: VisitsInfo;
|
||||||
|
settings: Settings;
|
||||||
cancelGetVisits: () => void;
|
cancelGetVisits: () => void;
|
||||||
baseUrl: string;
|
baseUrl: string;
|
||||||
domain?: string;
|
domain?: string;
|
||||||
@@ -59,7 +61,6 @@ const highlightedVisitsToStats = (
|
|||||||
return acc;
|
return acc;
|
||||||
}, {});
|
}, {});
|
||||||
let selectedBar: string | undefined;
|
let selectedBar: string | undefined;
|
||||||
const initialInterval: DateInterval = 'last30Days';
|
|
||||||
|
|
||||||
const VisitsNavLink: FC<VisitsNavLinkProps & { to: string }> = ({ subPath, title, icon, to }) => (
|
const VisitsNavLink: FC<VisitsNavLinkProps & { to: string }> = ({ subPath, title, icon, to }) => (
|
||||||
<NavLink
|
<NavLink
|
||||||
@@ -74,7 +75,10 @@ const VisitsNavLink: FC<VisitsNavLinkProps & { to: string }> = ({ subPath, title
|
|||||||
</NavLink>
|
</NavLink>
|
||||||
);
|
);
|
||||||
|
|
||||||
const VisitsStats: FC<VisitsStatsProps> = ({ children, visitsInfo, getVisits, cancelGetVisits, baseUrl, domain }) => {
|
const VisitsStats: FC<VisitsStatsProps> = (
|
||||||
|
{ children, visitsInfo, getVisits, cancelGetVisits, baseUrl, domain, settings },
|
||||||
|
) => {
|
||||||
|
const initialInterval: DateInterval = settings.visits?.defaultInterval ?? 'last30Days';
|
||||||
const [ dateRange, setDateRange ] = useState<DateRange>(intervalToDateRange(initialInterval));
|
const [ dateRange, setDateRange ] = useState<DateRange>(intervalToDateRange(initialInterval));
|
||||||
const [ highlightedVisits, setHighlightedVisits ] = useState<NormalizedVisit[]>([]);
|
const [ highlightedVisits, setHighlightedVisits ] = useState<NormalizedVisit[]>([]);
|
||||||
const [ highlightedLabel, setHighlightedLabel ] = useState<string | undefined>();
|
const [ highlightedLabel, setHighlightedLabel ] = useState<string | undefined>();
|
||||||
|
|||||||
@@ -18,19 +18,19 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
|
|||||||
|
|
||||||
bottle.serviceFactory('ShortUrlVisits', () => ShortUrlVisits);
|
bottle.serviceFactory('ShortUrlVisits', () => ShortUrlVisits);
|
||||||
bottle.decorator('ShortUrlVisits', connect(
|
bottle.decorator('ShortUrlVisits', connect(
|
||||||
[ 'shortUrlVisits', 'shortUrlDetail', 'mercureInfo' ],
|
[ 'shortUrlVisits', 'shortUrlDetail', 'mercureInfo', 'settings' ],
|
||||||
[ 'getShortUrlVisits', 'getShortUrlDetail', 'cancelGetShortUrlVisits', 'createNewVisits', 'loadMercureInfo' ],
|
[ 'getShortUrlVisits', 'getShortUrlDetail', 'cancelGetShortUrlVisits', 'createNewVisits', 'loadMercureInfo' ],
|
||||||
));
|
));
|
||||||
|
|
||||||
bottle.serviceFactory('TagVisits', TagVisits, 'ColorGenerator');
|
bottle.serviceFactory('TagVisits', TagVisits, 'ColorGenerator');
|
||||||
bottle.decorator('TagVisits', connect(
|
bottle.decorator('TagVisits', connect(
|
||||||
[ 'tagVisits', 'mercureInfo' ],
|
[ 'tagVisits', 'mercureInfo', 'settings' ],
|
||||||
[ 'getTagVisits', 'cancelGetTagVisits', 'createNewVisits', 'loadMercureInfo' ],
|
[ 'getTagVisits', 'cancelGetTagVisits', 'createNewVisits', 'loadMercureInfo' ],
|
||||||
));
|
));
|
||||||
|
|
||||||
bottle.serviceFactory('OrphanVisits', () => OrphanVisits);
|
bottle.serviceFactory('OrphanVisits', () => OrphanVisits);
|
||||||
bottle.decorator('OrphanVisits', connect(
|
bottle.decorator('OrphanVisits', connect(
|
||||||
[ 'orphanVisits', 'mercureInfo' ],
|
[ 'orphanVisits', 'mercureInfo', 'settings' ],
|
||||||
[ 'getOrphanVisits', 'cancelGetOrphanVisits', 'createNewVisits', 'loadMercureInfo' ],
|
[ 'getOrphanVisits', 'cancelGetOrphanVisits', 'createNewVisits', 'loadMercureInfo' ],
|
||||||
));
|
));
|
||||||
|
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ describe('<Overview />', () => {
|
|||||||
|
|
||||||
afterEach(() => wrapper?.unmount());
|
afterEach(() => wrapper?.unmount());
|
||||||
|
|
||||||
test('cards display loading messages when still loading', () => {
|
it('displays loading messages when still loading', () => {
|
||||||
const wrapper = createWrapper(true);
|
const wrapper = createWrapper(true);
|
||||||
const cards = wrapper.find(CardText);
|
const cards = wrapper.find(CardText);
|
||||||
|
|
||||||
@@ -53,7 +53,7 @@ describe('<Overview />', () => {
|
|||||||
cards.forEach((card) => expect(card.html()).toContain('Loading...'));
|
cards.forEach((card) => expect(card.html()).toContain('Loading...'));
|
||||||
});
|
});
|
||||||
|
|
||||||
test('amounts are displayed in cards after finishing loading', () => {
|
it('displays amounts in cards after finishing loading', () => {
|
||||||
const wrapper = createWrapper();
|
const wrapper = createWrapper();
|
||||||
const cards = wrapper.find(CardText);
|
const cards = wrapper.find(CardText);
|
||||||
|
|
||||||
@@ -64,21 +64,21 @@ describe('<Overview />', () => {
|
|||||||
expect(cards.at(3).html()).toContain(prettify(3));
|
expect(cards.at(3).html()).toContain(prettify(3));
|
||||||
});
|
});
|
||||||
|
|
||||||
test('first card displays warning for old shlink versions', () => {
|
it('displays warning in first card for old shlink versions', () => {
|
||||||
const wrapper = createWrapper();
|
const wrapper = createWrapper();
|
||||||
const firstCard = wrapper.find(CardText).first();
|
const firstCard = wrapper.find(CardText).first();
|
||||||
|
|
||||||
expect(firstCard.html()).toContain('Shlink 2.2 is needed');
|
expect(firstCard.html()).toContain('Shlink 2.2 is needed');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('nests complex components', () => {
|
it('nests complex components', () => {
|
||||||
const wrapper = createWrapper();
|
const wrapper = createWrapper();
|
||||||
|
|
||||||
expect(wrapper.find(CreateShortUrl)).toHaveLength(1);
|
expect(wrapper.find(CreateShortUrl)).toHaveLength(1);
|
||||||
expect(wrapper.find(ShortUrlsTable)).toHaveLength(1);
|
expect(wrapper.find(ShortUrlsTable)).toHaveLength(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('links to other sections are displayed', () => {
|
it('displays links to other sections', () => {
|
||||||
const wrapper = createWrapper();
|
const wrapper = createWrapper();
|
||||||
const links = wrapper.find(Link);
|
const links = wrapper.find(Link);
|
||||||
|
|
||||||
|
|||||||
66
test/settings/Visits.test.tsx
Normal file
66
test/settings/Visits.test.tsx
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import { shallow, ShallowWrapper } from 'enzyme';
|
||||||
|
import { Mock } from 'ts-mockery';
|
||||||
|
import { Settings } from '../../src/settings/reducers/settings';
|
||||||
|
import { Visits } from '../../src/settings/Visits';
|
||||||
|
import { SimpleCard } from '../../src/utils/SimpleCard';
|
||||||
|
import { DateIntervalSelector } from '../../src/utils/dates/DateIntervalSelector';
|
||||||
|
|
||||||
|
describe('<Visits />', () => {
|
||||||
|
let wrapper: ShallowWrapper;
|
||||||
|
const setVisitsSettings = jest.fn();
|
||||||
|
const createWrapper = (settings: Partial<Settings> = {}) => {
|
||||||
|
wrapper = shallow(<Visits settings={Mock.of<Settings>(settings)} setVisitsSettings={setVisitsSettings} />);
|
||||||
|
|
||||||
|
return wrapper;
|
||||||
|
};
|
||||||
|
|
||||||
|
afterEach(jest.clearAllMocks);
|
||||||
|
afterEach(() => wrapper?.unmount());
|
||||||
|
|
||||||
|
it('renders expected components', () => {
|
||||||
|
const wrapper = createWrapper();
|
||||||
|
|
||||||
|
expect(wrapper.find(SimpleCard).prop('title')).toEqual('Visits');
|
||||||
|
expect(wrapper.find('label').prop('children')).toEqual('Default interval to load on visits sections:');
|
||||||
|
expect(wrapper.find(DateIntervalSelector)).toHaveLength(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
[ Mock.all<Settings>(), 'last30Days' ],
|
||||||
|
[ Mock.of<Settings>({ visits: {} }), 'last30Days' ],
|
||||||
|
[
|
||||||
|
Mock.of<Settings>({
|
||||||
|
visits: {
|
||||||
|
defaultInterval: 'last7Days',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
'last7Days',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
Mock.of<Settings>({
|
||||||
|
visits: {
|
||||||
|
defaultInterval: 'today',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
'today',
|
||||||
|
],
|
||||||
|
])('sets expected interval as active', (settings, expectedInterval) => {
|
||||||
|
const wrapper = createWrapper(settings);
|
||||||
|
|
||||||
|
expect(wrapper.find(DateIntervalSelector).prop('active')).toEqual(expectedInterval);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('invokes setVisitsSettings when interval changes', () => {
|
||||||
|
const wrapper = createWrapper();
|
||||||
|
const selector = wrapper.find(DateIntervalSelector);
|
||||||
|
|
||||||
|
selector.simulate('change', 'last7Days');
|
||||||
|
selector.simulate('change', 'last180days');
|
||||||
|
selector.simulate('change', 'yesterday');
|
||||||
|
|
||||||
|
expect(setVisitsSettings).toHaveBeenCalledTimes(3);
|
||||||
|
expect(setVisitsSettings).toHaveBeenNthCalledWith(1, { defaultInterval: 'last7Days' });
|
||||||
|
expect(setVisitsSettings).toHaveBeenNthCalledWith(2, { defaultInterval: 'last180days' });
|
||||||
|
expect(setVisitsSettings).toHaveBeenNthCalledWith(3, { defaultInterval: 'yesterday' });
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -4,13 +4,15 @@ import reducer, {
|
|||||||
setRealTimeUpdatesInterval,
|
setRealTimeUpdatesInterval,
|
||||||
setShortUrlCreationSettings,
|
setShortUrlCreationSettings,
|
||||||
setUiSettings,
|
setUiSettings,
|
||||||
|
setVisitsSettings,
|
||||||
} from '../../../src/settings/reducers/settings';
|
} from '../../../src/settings/reducers/settings';
|
||||||
|
|
||||||
describe('settingsReducer', () => {
|
describe('settingsReducer', () => {
|
||||||
const realTimeUpdates = { enabled: true };
|
const realTimeUpdates = { enabled: true };
|
||||||
const shortUrlCreation = { validateUrls: false };
|
const shortUrlCreation = { validateUrls: false };
|
||||||
const ui = { theme: 'light' };
|
const ui = { theme: 'light' };
|
||||||
const settings = { realTimeUpdates, shortUrlCreation, ui };
|
const visits = { defaultInterval: 'last30Days' };
|
||||||
|
const settings = { realTimeUpdates, shortUrlCreation, ui, visits };
|
||||||
|
|
||||||
describe('reducer', () => {
|
describe('reducer', () => {
|
||||||
it('returns realTimeUpdates when action is SET_SETTINGS', () => {
|
it('returns realTimeUpdates when action is SET_SETTINGS', () => {
|
||||||
@@ -49,4 +51,12 @@ describe('settingsReducer', () => {
|
|||||||
expect(result).toEqual({ type: SET_SETTINGS, ui: { theme: 'dark' } });
|
expect(result).toEqual({ type: SET_SETTINGS, ui: { theme: 'dark' } });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('setVisitsSettings', () => {
|
||||||
|
it('creates action to set visits settings', () => {
|
||||||
|
const result = setVisitsSettings({ defaultInterval: 'last180days' });
|
||||||
|
|
||||||
|
expect(result).toEqual({ type: SET_SETTINGS, visits: { defaultInterval: 'last180days' } });
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ describe('<CopyToClipboardIcon />', () => {
|
|||||||
});
|
});
|
||||||
afterEach(() => wrapper?.unmount());
|
afterEach(() => wrapper?.unmount());
|
||||||
|
|
||||||
test('expected components are wrapped', () => {
|
it('wraps expected components', () => {
|
||||||
const copyToClipboard = wrapper.find(CopyToClipboard);
|
const copyToClipboard = wrapper.find(CopyToClipboard);
|
||||||
const icon = wrapper.find(FontAwesomeIcon);
|
const icon = wrapper.find(FontAwesomeIcon);
|
||||||
|
|
||||||
|
|||||||
39
test/utils/dates/DateIntervalDropdownItems.test.tsx
Normal file
39
test/utils/dates/DateIntervalDropdownItems.test.tsx
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import { DropdownItem } from 'reactstrap';
|
||||||
|
import { shallow, ShallowWrapper } from 'enzyme';
|
||||||
|
import { DateIntervalDropdownItems } from '../../../src/utils/dates/DateIntervalDropdownItems';
|
||||||
|
import { DATE_INTERVALS } from '../../../src/utils/dates/types';
|
||||||
|
|
||||||
|
describe('<DateIntervalDropdownItems />', () => {
|
||||||
|
let wrapper: ShallowWrapper;
|
||||||
|
const onChange = jest.fn();
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
wrapper = shallow(<DateIntervalDropdownItems active="last180days" onChange={onChange} />);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(jest.clearAllMocks);
|
||||||
|
afterEach(() => wrapper?.unmount());
|
||||||
|
|
||||||
|
it('renders expected amount of items', () => {
|
||||||
|
const items = wrapper.find(DropdownItem);
|
||||||
|
|
||||||
|
expect(items).toHaveLength(DATE_INTERVALS.length);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets expected item as active', () => {
|
||||||
|
const items = wrapper.find(DropdownItem);
|
||||||
|
const EXPECTED_ACTIVE_INDEX = 5;
|
||||||
|
|
||||||
|
expect.assertions(DATE_INTERVALS.length);
|
||||||
|
items.forEach((item, index) => expect(item.prop('active')).toEqual(index === EXPECTED_ACTIVE_INDEX));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('triggers onChange callback when selecting an element', () => {
|
||||||
|
const items = wrapper.find(DropdownItem);
|
||||||
|
|
||||||
|
items.at(2).simulate('click');
|
||||||
|
items.at(4).simulate('click');
|
||||||
|
items.at(1).simulate('click');
|
||||||
|
expect(onChange).toHaveBeenCalledTimes(3);
|
||||||
|
});
|
||||||
|
});
|
||||||
26
test/utils/dates/DateIntervalSelector.test.tsx
Normal file
26
test/utils/dates/DateIntervalSelector.test.tsx
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { shallow, ShallowWrapper } from 'enzyme';
|
||||||
|
import { DateInterval, rangeOrIntervalToString } from '../../../src/utils/dates/types';
|
||||||
|
import { DateIntervalSelector } from '../../../src/utils/dates/DateIntervalSelector';
|
||||||
|
import { DateIntervalDropdownItems } from '../../../src/utils/dates/DateIntervalDropdownItems';
|
||||||
|
import { DropdownBtn } from '../../../src/utils/DropdownBtn';
|
||||||
|
|
||||||
|
describe('<DateIntervalSelector />', () => {
|
||||||
|
let wrapper: ShallowWrapper;
|
||||||
|
const activeInterval: DateInterval = 'last7Days';
|
||||||
|
const onChange = jest.fn();
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
wrapper = shallow(<DateIntervalSelector active={activeInterval} onChange={onChange} />);
|
||||||
|
});
|
||||||
|
afterEach(() => wrapper?.unmount());
|
||||||
|
|
||||||
|
it('passes props down to nested DateIntervalDropdownItems', () => {
|
||||||
|
const items = wrapper.find(DateIntervalDropdownItems);
|
||||||
|
const dropdown = wrapper.find(DropdownBtn);
|
||||||
|
|
||||||
|
expect(dropdown.prop('text')).toEqual(rangeOrIntervalToString(activeInterval));
|
||||||
|
expect(items).toHaveLength(1);
|
||||||
|
expect(items.prop('onChange')).toEqual(onChange);
|
||||||
|
expect(items.prop('active')).toEqual(activeInterval);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -4,6 +4,7 @@ import moment from 'moment';
|
|||||||
import { Mock } from 'ts-mockery';
|
import { Mock } from 'ts-mockery';
|
||||||
import { DateRangeSelector, DateRangeSelectorProps } from '../../../src/utils/dates/DateRangeSelector';
|
import { DateRangeSelector, DateRangeSelectorProps } from '../../../src/utils/dates/DateRangeSelector';
|
||||||
import { DateInterval } from '../../../src/utils/dates/types';
|
import { DateInterval } from '../../../src/utils/dates/types';
|
||||||
|
import { DateIntervalDropdownItems } from '../../../src/utils/dates/DateIntervalDropdownItems';
|
||||||
|
|
||||||
describe('<DateRangeSelector />', () => {
|
describe('<DateRangeSelector />', () => {
|
||||||
let wrapper: ShallowWrapper;
|
let wrapper: ShallowWrapper;
|
||||||
@@ -17,42 +18,52 @@ describe('<DateRangeSelector />', () => {
|
|||||||
afterEach(jest.clearAllMocks);
|
afterEach(jest.clearAllMocks);
|
||||||
afterEach(() => wrapper?.unmount());
|
afterEach(() => wrapper?.unmount());
|
||||||
|
|
||||||
test('proper amount of items is rendered', () => {
|
it('renders proper amount of items', () => {
|
||||||
const wrapper = createWrapper();
|
const wrapper = createWrapper();
|
||||||
const items = wrapper.find(DropdownItem);
|
const items = wrapper.find(DropdownItem);
|
||||||
|
const dateIntervalItems = wrapper.find(DateIntervalDropdownItems);
|
||||||
|
|
||||||
expect(items).toHaveLength(12);
|
expect(items).toHaveLength(5);
|
||||||
|
expect(dateIntervalItems).toHaveLength(1);
|
||||||
expect(items.filter('[divider]')).toHaveLength(2);
|
expect(items.filter('[divider]')).toHaveLength(2);
|
||||||
expect(items.filter('[header]')).toHaveLength(1);
|
expect(items.filter('[header]')).toHaveLength(1);
|
||||||
expect(items.filter('[text]')).toHaveLength(1);
|
expect(items.filter('[text]')).toHaveLength(1);
|
||||||
expect(items.filter('[active]')).toHaveLength(8);
|
expect(items.filter('[active]')).toHaveLength(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
test.each([
|
it.each([
|
||||||
[ undefined, 0 ],
|
[ undefined, 1, 0 ],
|
||||||
[ 'today' as DateInterval, 1 ],
|
[ 'today' as DateInterval, 0, 1 ],
|
||||||
[ 'yesterday' as DateInterval, 2 ],
|
[ 'yesterday' as DateInterval, 0, 1 ],
|
||||||
[ 'last7Days' as DateInterval, 3 ],
|
[ 'last7Days' as DateInterval, 0, 1 ],
|
||||||
[ 'last30Days' as DateInterval, 4 ],
|
[ 'last30Days' as DateInterval, 0, 1 ],
|
||||||
[ 'last90Days' as DateInterval, 5 ],
|
[ 'last90Days' as DateInterval, 0, 1 ],
|
||||||
[ 'last180days' as DateInterval, 6 ],
|
[ 'last180days' as DateInterval, 0, 1 ],
|
||||||
[ 'last365Days' as DateInterval, 7 ],
|
[ 'last365Days' as DateInterval, 0, 1 ],
|
||||||
[{ startDate: moment() }, 8 ],
|
[{ startDate: moment() }, 0, 0 ],
|
||||||
])('proper element is active based on provided date range', (initialDateRange, expectedActiveIndex) => {
|
])('sets proper element as active based on provided date range', (
|
||||||
|
initialDateRange,
|
||||||
|
expectedActiveItems,
|
||||||
|
expectedActiveIntervalItems,
|
||||||
|
) => {
|
||||||
const wrapper = createWrapper({ initialDateRange });
|
const wrapper = createWrapper({ initialDateRange });
|
||||||
const items = wrapper.find(DropdownItem).filter('[active]');
|
const items = wrapper.find(DropdownItem).filterWhere((item) => item.prop('active') === true);
|
||||||
|
const dateIntervalItems = wrapper.find(DateIntervalDropdownItems).filterWhere(
|
||||||
|
(item) => item.prop('active') !== undefined,
|
||||||
|
);
|
||||||
|
|
||||||
expect.assertions(8);
|
expect(items).toHaveLength(expectedActiveItems);
|
||||||
items.forEach((item, index) => expect(item.prop('active')).toEqual(index === expectedActiveIndex));
|
expect(dateIntervalItems).toHaveLength(expectedActiveIntervalItems);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('selecting an element triggers onDatesChange callback', () => {
|
it('triggers onDatesChange callback when selecting an element', () => {
|
||||||
const wrapper = createWrapper();
|
const wrapper = createWrapper();
|
||||||
const items = wrapper.find(DropdownItem).filter('[active]');
|
const item = wrapper.find(DropdownItem).at(0);
|
||||||
|
const dateIntervalItems = wrapper.find(DateIntervalDropdownItems);
|
||||||
|
|
||||||
items.at(2).simulate('click');
|
item.simulate('click');
|
||||||
items.at(4).simulate('click');
|
item.simulate('click');
|
||||||
items.at(1).simulate('click');
|
dateIntervalItems.simulate('change');
|
||||||
expect(onDatesChange).toHaveBeenCalledTimes(3);
|
expect(onDatesChange).toHaveBeenCalledTimes(3);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { MercureBoundProps } from '../../src/mercure/helpers/boundToMercureHub';
|
|||||||
import { VisitsInfo } from '../../src/visits/types';
|
import { VisitsInfo } from '../../src/visits/types';
|
||||||
import VisitsStats from '../../src/visits/VisitsStats';
|
import VisitsStats from '../../src/visits/VisitsStats';
|
||||||
import { OrphanVisitsHeader } from '../../src/visits/OrphanVisitsHeader';
|
import { OrphanVisitsHeader } from '../../src/visits/OrphanVisitsHeader';
|
||||||
|
import { Settings } from '../../src/settings/reducers/settings';
|
||||||
|
|
||||||
describe('<OrphanVisits />', () => {
|
describe('<OrphanVisits />', () => {
|
||||||
it('wraps visits stats and header', () => {
|
it('wraps visits stats and header', () => {
|
||||||
@@ -24,6 +25,7 @@ describe('<OrphanVisits />', () => {
|
|||||||
history={Mock.of<History>({ goBack })}
|
history={Mock.of<History>({ goBack })}
|
||||||
location={Mock.all<Location>()}
|
location={Mock.all<Location>()}
|
||||||
match={Mock.of<match>({ url: 'the_base_url' })}
|
match={Mock.of<match>({ url: 'the_base_url' })}
|
||||||
|
settings={Mock.all<Settings>()}
|
||||||
/>,
|
/>,
|
||||||
).dive();
|
).dive();
|
||||||
const stats = wrapper.find(VisitsStats);
|
const stats = wrapper.find(VisitsStats);
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { Visit, VisitsInfo } from '../../src/visits/types';
|
|||||||
import LineChartCard from '../../src/visits/helpers/LineChartCard';
|
import LineChartCard from '../../src/visits/helpers/LineChartCard';
|
||||||
import VisitsTable from '../../src/visits/VisitsTable';
|
import VisitsTable from '../../src/visits/VisitsTable';
|
||||||
import { Result } from '../../src/utils/Result';
|
import { Result } from '../../src/utils/Result';
|
||||||
|
import { Settings } from '../../src/settings/reducers/settings';
|
||||||
|
|
||||||
describe('<VisitStats />', () => {
|
describe('<VisitStats />', () => {
|
||||||
const visits = [ Mock.all<Visit>(), Mock.all<Visit>(), Mock.all<Visit>() ];
|
const visits = [ Mock.all<Visit>(), Mock.all<Visit>(), Mock.all<Visit>() ];
|
||||||
@@ -23,6 +24,7 @@ describe('<VisitStats />', () => {
|
|||||||
visitsInfo={Mock.of<VisitsInfo>(visitsInfo)}
|
visitsInfo={Mock.of<VisitsInfo>(visitsInfo)}
|
||||||
cancelGetVisits={() => {}}
|
cancelGetVisits={() => {}}
|
||||||
baseUrl={''}
|
baseUrl={''}
|
||||||
|
settings={Mock.all<Settings>()}
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user