mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2025-01-03 06:47:29 +03:00
Replace local settings UI with the one from shlink-web-component
This commit is contained in:
parent
d4bc9dd62a
commit
202a69bdf5
27 changed files with 88 additions and 1096 deletions
17
CHANGELOG.md
17
CHANGELOG.md
|
@ -4,6 +4,23 @@ All notable changes to this project will be documented in this file.
|
|||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org).
|
||||
|
||||
## [Unreleased]
|
||||
### Added
|
||||
* *Nothing*
|
||||
|
||||
### Changed
|
||||
* Update to `@shlinkio/shlink-web-component` 0.7 and use `ShlinkWebSettings` to replace local settings UI.
|
||||
|
||||
### Deprecated
|
||||
* *Nothing*
|
||||
|
||||
### Removed
|
||||
* *Nothing*
|
||||
|
||||
### Fixed
|
||||
* *Nothing*
|
||||
|
||||
|
||||
## [4.1.2] - 2024-04-17
|
||||
### Added
|
||||
* *Nothing*
|
||||
|
|
34
package-lock.json
generated
34
package-lock.json
generated
|
@ -18,7 +18,7 @@
|
|||
"@shlinkio/data-manipulation": "^1.0.3",
|
||||
"@shlinkio/shlink-frontend-kit": "^0.5.1",
|
||||
"@shlinkio/shlink-js-sdk": "^1.1.0",
|
||||
"@shlinkio/shlink-web-component": "^0.6.2",
|
||||
"@shlinkio/shlink-web-component": "^0.7.0",
|
||||
"bootstrap": "5.2.3",
|
||||
"bottlejs": "^2.0.1",
|
||||
"clsx": "^2.1.1",
|
||||
|
@ -3111,16 +3111,16 @@
|
|||
"integrity": "sha512-KHvFCmRxkK0H0nja66aGzFrjh5VQpC9m2KXnk/Lb7HMpmoWvf8CZqBYyiPFy3Evaa+GnIHjFv7LtqFce+FVFVg=="
|
||||
},
|
||||
"node_modules/@shlinkio/shlink-web-component": {
|
||||
"version": "0.6.2",
|
||||
"resolved": "https://registry.npmjs.org/@shlinkio/shlink-web-component/-/shlink-web-component-0.6.2.tgz",
|
||||
"integrity": "sha512-2R1O0hYmWs11e+4pqn/g3Wk3cbHkhkyE37V73bQghJrmJHKNdBZ7TdsbsWH5eF6RF4UDe1HfG1EJTr+ogqI+mg==",
|
||||
"version": "0.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@shlinkio/shlink-web-component/-/shlink-web-component-0.7.0.tgz",
|
||||
"integrity": "sha512-83QV1uwq7AjQ/SIQldPHBnMq563kMZKDLi+PxM1pSLElCymtfnS4WtC2c2V4K5eStGy9y42+ZTWKYGZljM6q6g==",
|
||||
"dependencies": {
|
||||
"@formkit/drag-and-drop": "^0.0.38",
|
||||
"@json2csv/plainjs": "^7.0.6",
|
||||
"@shlinkio/data-manipulation": "^1.0.3",
|
||||
"bottlejs": "^2.0.1",
|
||||
"bowser": "^2.11.0",
|
||||
"clsx": "^2.1.0",
|
||||
"clsx": "^2.1.1",
|
||||
"compare-versions": "^6.1.0",
|
||||
"date-fns": "^3.6.0",
|
||||
"event-source-polyfill": "^1.0.31",
|
||||
|
@ -3129,7 +3129,7 @@
|
|||
"react-leaflet": "^4.2.1",
|
||||
"react-swipeable": "^7.0.1",
|
||||
"react-tag-autocomplete": "^7.2.0",
|
||||
"recharts": "^2.12.5"
|
||||
"recharts": "^2.12.7"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "^6.4.2",
|
||||
|
@ -8969,9 +8969,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/recharts": {
|
||||
"version": "2.12.5",
|
||||
"resolved": "https://registry.npmjs.org/recharts/-/recharts-2.12.5.tgz",
|
||||
"integrity": "sha512-Cy+BkqrFIYTHJCyKHJEPvbHE2kVQEP6PKbOHJ8ztRGTAhvHuUnCwDaKVb13OwRFZ0QNUk1QvGTDdgWSMbuMtKw==",
|
||||
"version": "2.12.7",
|
||||
"resolved": "https://registry.npmjs.org/recharts/-/recharts-2.12.7.tgz",
|
||||
"integrity": "sha512-hlLJMhPQfv4/3NBSAyq3gzGg4h2v69RJh6KU7b3pXYNNAELs9kEoXOjbkxdXpALqKBoVmVptGfLpxdaVYqjmXQ==",
|
||||
"dependencies": {
|
||||
"clsx": "^2.0.0",
|
||||
"eventemitter3": "^4.0.1",
|
||||
|
@ -13167,16 +13167,16 @@
|
|||
"integrity": "sha512-KHvFCmRxkK0H0nja66aGzFrjh5VQpC9m2KXnk/Lb7HMpmoWvf8CZqBYyiPFy3Evaa+GnIHjFv7LtqFce+FVFVg=="
|
||||
},
|
||||
"@shlinkio/shlink-web-component": {
|
||||
"version": "0.6.2",
|
||||
"resolved": "https://registry.npmjs.org/@shlinkio/shlink-web-component/-/shlink-web-component-0.6.2.tgz",
|
||||
"integrity": "sha512-2R1O0hYmWs11e+4pqn/g3Wk3cbHkhkyE37V73bQghJrmJHKNdBZ7TdsbsWH5eF6RF4UDe1HfG1EJTr+ogqI+mg==",
|
||||
"version": "0.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@shlinkio/shlink-web-component/-/shlink-web-component-0.7.0.tgz",
|
||||
"integrity": "sha512-83QV1uwq7AjQ/SIQldPHBnMq563kMZKDLi+PxM1pSLElCymtfnS4WtC2c2V4K5eStGy9y42+ZTWKYGZljM6q6g==",
|
||||
"requires": {
|
||||
"@formkit/drag-and-drop": "^0.0.38",
|
||||
"@json2csv/plainjs": "^7.0.6",
|
||||
"@shlinkio/data-manipulation": "^1.0.3",
|
||||
"bottlejs": "^2.0.1",
|
||||
"bowser": "^2.11.0",
|
||||
"clsx": "^2.1.0",
|
||||
"clsx": "^2.1.1",
|
||||
"compare-versions": "^6.1.0",
|
||||
"date-fns": "^3.6.0",
|
||||
"event-source-polyfill": "^1.0.31",
|
||||
|
@ -13185,7 +13185,7 @@
|
|||
"react-leaflet": "^4.2.1",
|
||||
"react-swipeable": "^7.0.1",
|
||||
"react-tag-autocomplete": "^7.2.0",
|
||||
"recharts": "^2.12.5"
|
||||
"recharts": "^2.12.7"
|
||||
}
|
||||
},
|
||||
"@shlinkio/stylelint-config-css-coding-standard": {
|
||||
|
@ -17158,9 +17158,9 @@
|
|||
}
|
||||
},
|
||||
"recharts": {
|
||||
"version": "2.12.5",
|
||||
"resolved": "https://registry.npmjs.org/recharts/-/recharts-2.12.5.tgz",
|
||||
"integrity": "sha512-Cy+BkqrFIYTHJCyKHJEPvbHE2kVQEP6PKbOHJ8ztRGTAhvHuUnCwDaKVb13OwRFZ0QNUk1QvGTDdgWSMbuMtKw==",
|
||||
"version": "2.12.7",
|
||||
"resolved": "https://registry.npmjs.org/recharts/-/recharts-2.12.7.tgz",
|
||||
"integrity": "sha512-hlLJMhPQfv4/3NBSAyq3gzGg4h2v69RJh6KU7b3pXYNNAELs9kEoXOjbkxdXpALqKBoVmVptGfLpxdaVYqjmXQ==",
|
||||
"requires": {
|
||||
"clsx": "^2.0.0",
|
||||
"eventemitter3": "^4.0.1",
|
||||
|
|
|
@ -35,7 +35,7 @@
|
|||
"@shlinkio/data-manipulation": "^1.0.3",
|
||||
"@shlinkio/shlink-frontend-kit": "^0.5.1",
|
||||
"@shlinkio/shlink-js-sdk": "^1.1.0",
|
||||
"@shlinkio/shlink-web-component": "^0.6.2",
|
||||
"@shlinkio/shlink-web-component": "^0.7.0",
|
||||
"bootstrap": "5.2.3",
|
||||
"bottlejs": "^2.0.1",
|
||||
"clsx": "^2.1.1",
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { changeThemeInMarkup, getSystemPreferredTheme } from '@shlinkio/shlink-frontend-kit';
|
||||
import type { Settings } from '@shlinkio/shlink-web-component/settings';
|
||||
import { clsx } from 'clsx';
|
||||
import type { FC } from 'react';
|
||||
import { useEffect, useRef } from 'react';
|
||||
|
@ -8,14 +9,13 @@ import { NotFound } from '../common/NotFound';
|
|||
import type { FCWithDeps } from '../container/utils';
|
||||
import { componentFactory, useDependencies } from '../container/utils';
|
||||
import type { ServersMap } from '../servers/data';
|
||||
import type { AppSettings } from '../settings/reducers/settings';
|
||||
import { forceUpdate } from '../utils/helpers/sw';
|
||||
import './App.scss';
|
||||
|
||||
type AppProps = {
|
||||
fetchServers: () => void;
|
||||
servers: ServersMap;
|
||||
settings: AppSettings;
|
||||
settings: Settings;
|
||||
resetAppUpdate: () => void;
|
||||
appUpdated: boolean;
|
||||
};
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import type { FC, PropsWithChildren } from 'react';
|
||||
import './NoMenuLayout.scss';
|
||||
|
||||
export const NoMenuLayout: FC<PropsWithChildren<unknown>> = ({ children }) => (
|
||||
export const NoMenuLayout: FC<PropsWithChildren> = ({ children }) => (
|
||||
<div className="no-menu-wrapper container-xl">{children}</div>
|
||||
);
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import type { Settings, ShlinkWebComponentType, TagColorsStorage } from '@shlinkio/shlink-web-component';
|
||||
import type { ShlinkWebComponentType, TagColorsStorage } from '@shlinkio/shlink-web-component';
|
||||
import type { Settings } from '@shlinkio/shlink-web-component/settings';
|
||||
import type { FC } from 'react';
|
||||
import { memo } from 'react';
|
||||
import type { ShlinkApiClientBuilder } from '../api/services/ShlinkApiClientBuilder';
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import type { Settings } from '@shlinkio/shlink-web-component';
|
||||
import type { Settings } from '@shlinkio/shlink-web-component/settings';
|
||||
import type { SelectedServer, ServersMap } from '../servers/data';
|
||||
|
||||
export interface ShlinkState {
|
||||
|
|
|
@ -1,60 +0,0 @@
|
|||
import { LabeledFormGroup, SimpleCard, ToggleSwitch, useDomId } from '@shlinkio/shlink-frontend-kit';
|
||||
import type { Settings } from '@shlinkio/shlink-web-component';
|
||||
import { clsx } from 'clsx';
|
||||
import { FormGroup, Input } from 'reactstrap';
|
||||
import { FormText } from '../utils/forms/FormText';
|
||||
|
||||
type RealTimeUpdatesProps = {
|
||||
settings: Settings;
|
||||
toggleRealTimeUpdates: (enabled: boolean) => void;
|
||||
setRealTimeUpdatesInterval: (interval: number) => void;
|
||||
};
|
||||
|
||||
const intervalValue = (interval?: number) => (!interval ? '' : `${interval}`);
|
||||
|
||||
export const RealTimeUpdatesSettings = (
|
||||
{ settings, toggleRealTimeUpdates, setRealTimeUpdatesInterval }: RealTimeUpdatesProps,
|
||||
) => {
|
||||
const { realTimeUpdates = { enabled: true } } = settings;
|
||||
const inputId = useDomId();
|
||||
|
||||
return (
|
||||
<SimpleCard title="Real-time updates" className="h-100">
|
||||
<FormGroup>
|
||||
<ToggleSwitch checked={realTimeUpdates.enabled} onChange={toggleRealTimeUpdates}>
|
||||
Enable or disable real-time updates.
|
||||
<FormText>
|
||||
Real-time updates are currently being <b>{realTimeUpdates.enabled ? 'processed' : 'ignored'}</b>.
|
||||
</FormText>
|
||||
</ToggleSwitch>
|
||||
</FormGroup>
|
||||
<LabeledFormGroup
|
||||
noMargin
|
||||
label="Real-time updates frequency (in minutes):"
|
||||
labelClassName={clsx('form-label', { 'text-muted': !realTimeUpdates.enabled })}
|
||||
id={inputId}
|
||||
>
|
||||
<Input
|
||||
type="number"
|
||||
min={0}
|
||||
placeholder="Immediate"
|
||||
disabled={!realTimeUpdates.enabled}
|
||||
value={intervalValue(realTimeUpdates.interval)}
|
||||
id={inputId}
|
||||
onChange={({ target }) => setRealTimeUpdatesInterval(Number(target.value))}
|
||||
/>
|
||||
{realTimeUpdates.enabled && (
|
||||
<FormText>
|
||||
{realTimeUpdates.interval !== undefined && realTimeUpdates.interval > 0 && (
|
||||
<span>
|
||||
Updates will be reflected in the UI
|
||||
every <b>{realTimeUpdates.interval}</b> minute{realTimeUpdates.interval > 1 && 's'}.
|
||||
</span>
|
||||
)}
|
||||
{!realTimeUpdates.interval && 'Updates will be reflected in the UI as soon as they happen.'}
|
||||
</FormText>
|
||||
)}
|
||||
</LabeledFormGroup>
|
||||
</SimpleCard>
|
||||
);
|
||||
};
|
|
@ -1,58 +1,20 @@
|
|||
import { NavPillItem, NavPills } from '@shlinkio/shlink-frontend-kit';
|
||||
import type { FC, ReactNode } from 'react';
|
||||
import { Navigate, Route, Routes } from 'react-router-dom';
|
||||
import type { Settings as AppSettings } from '@shlinkio/shlink-web-component/settings';
|
||||
import { ShlinkWebSettings } from '@shlinkio/shlink-web-component/settings';
|
||||
import type { FC } from 'react';
|
||||
import { NoMenuLayout } from '../common/NoMenuLayout';
|
||||
import type { FCWithDeps } from '../container/utils';
|
||||
import { componentFactory, useDependencies } from '../container/utils';
|
||||
import { DEFAULT_SHORT_URLS_ORDERING } from './reducers/settings';
|
||||
|
||||
type SettingsDeps = {
|
||||
RealTimeUpdatesSettings: FC;
|
||||
ShortUrlCreationSettings: FC;
|
||||
ShortUrlsListSettings: FC;
|
||||
UserInterfaceSettings: FC;
|
||||
VisitsSettings: FC;
|
||||
TagsSettings: FC;
|
||||
export type SettingsProps = {
|
||||
settings: AppSettings;
|
||||
setSettings: (newSettings: AppSettings) => void;
|
||||
};
|
||||
|
||||
const SettingsSections: FC<{ items: ReactNode[] }> = ({ items }) => (
|
||||
<>
|
||||
{items.map((child, index) => <div key={index} className="mb-3">{child}</div>)}
|
||||
</>
|
||||
);
|
||||
|
||||
const Settings: FCWithDeps<{}, SettingsDeps> = () => {
|
||||
const {
|
||||
RealTimeUpdatesSettings: RealTimeUpdates,
|
||||
ShortUrlCreationSettings: ShortUrlCreation,
|
||||
ShortUrlsListSettings: ShortUrlsList,
|
||||
UserInterfaceSettings: UserInterface,
|
||||
VisitsSettings: Visits,
|
||||
TagsSettings: Tags,
|
||||
} = useDependencies(Settings);
|
||||
|
||||
return (
|
||||
export const Settings: FC<SettingsProps> = ({ settings, setSettings }) => (
|
||||
<NoMenuLayout>
|
||||
<NavPills className="mb-3">
|
||||
<NavPillItem to="general">General</NavPillItem>
|
||||
<NavPillItem to="short-urls">Short URLs</NavPillItem>
|
||||
<NavPillItem to="other-items">Other items</NavPillItem>
|
||||
</NavPills>
|
||||
|
||||
<Routes>
|
||||
<Route path="general" element={<SettingsSections items={[<UserInterface />, <RealTimeUpdates />]} />} />
|
||||
<Route path="short-urls" element={<SettingsSections items={[<ShortUrlCreation />, <ShortUrlsList />]} />} />
|
||||
<Route path="other-items" element={<SettingsSections items={[<Tags />, <Visits />]} />} />
|
||||
<Route path="*" element={<Navigate replace to="general" />} />
|
||||
</Routes>
|
||||
<ShlinkWebSettings
|
||||
settings={settings}
|
||||
updateSettings={setSettings}
|
||||
defaultShortUrlsListOrdering={DEFAULT_SHORT_URLS_ORDERING}
|
||||
/>
|
||||
</NoMenuLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export const SettingsFactory = componentFactory(Settings, [
|
||||
'RealTimeUpdatesSettings',
|
||||
'ShortUrlCreationSettings',
|
||||
'ShortUrlsListSettings',
|
||||
'UserInterfaceSettings',
|
||||
'VisitsSettings',
|
||||
'TagsSettings',
|
||||
]);
|
||||
|
|
|
@ -1,75 +0,0 @@
|
|||
import { DropdownBtn, LabeledFormGroup, SimpleCard, ToggleSwitch } from '@shlinkio/shlink-frontend-kit';
|
||||
import type { Settings, ShortUrlCreationSettings as ShortUrlsSettings } from '@shlinkio/shlink-web-component';
|
||||
import type { FC, ReactNode } from 'react';
|
||||
import { DropdownItem, FormGroup } from 'reactstrap';
|
||||
import { FormText } from '../utils/forms/FormText';
|
||||
import type { Defined } from '../utils/types';
|
||||
|
||||
type TagFilteringMode = Defined<ShortUrlsSettings['tagFilteringMode']>;
|
||||
|
||||
interface ShortUrlCreationProps {
|
||||
settings: Settings;
|
||||
setShortUrlCreationSettings: (settings: ShortUrlsSettings) => void;
|
||||
}
|
||||
|
||||
const tagFilteringModeText = (tagFilteringMode: TagFilteringMode | undefined): string =>
|
||||
(tagFilteringMode === 'includes' ? 'Suggest tags including input' : 'Suggest tags starting with input');
|
||||
const tagFilteringModeHint = (tagFilteringMode: TagFilteringMode | undefined): ReactNode => (
|
||||
tagFilteringMode === 'includes'
|
||||
? <>The list of suggested tags will contain those <b>including</b> provided input.</>
|
||||
: <>The list of suggested tags will contain those <b>starting with</b> provided input.</>
|
||||
);
|
||||
|
||||
export const ShortUrlCreationSettings: FC<ShortUrlCreationProps> = ({ settings, setShortUrlCreationSettings }) => {
|
||||
const shortUrlCreation: ShortUrlsSettings = settings.shortUrlCreation ?? { validateUrls: false };
|
||||
const changeTagsFilteringMode = (tagFilteringMode: TagFilteringMode) => () => setShortUrlCreationSettings(
|
||||
{ ...shortUrlCreation ?? { validateUrls: false }, tagFilteringMode },
|
||||
);
|
||||
|
||||
return (
|
||||
<SimpleCard title="Short URLs form" className="h-100">
|
||||
<FormGroup>
|
||||
<ToggleSwitch
|
||||
checked={shortUrlCreation.validateUrls ?? false}
|
||||
onChange={(validateUrls) => setShortUrlCreationSettings({ ...shortUrlCreation, validateUrls })}
|
||||
>
|
||||
Request validation on long URLs when creating new short URLs.{' '}
|
||||
<b>This option is ignored by Shlink {'>='}4.0.0</b>
|
||||
<FormText>
|
||||
The initial state of the <b>Validate URL</b> checkbox will
|
||||
be <b>{shortUrlCreation.validateUrls ? 'checked' : 'unchecked'}</b>.
|
||||
</FormText>
|
||||
</ToggleSwitch>
|
||||
</FormGroup>
|
||||
<FormGroup>
|
||||
<ToggleSwitch
|
||||
checked={shortUrlCreation.forwardQuery ?? true}
|
||||
onChange={(forwardQuery) => setShortUrlCreationSettings({ ...shortUrlCreation, forwardQuery })}
|
||||
>
|
||||
Make all new short URLs forward their query params to the long URL.
|
||||
<FormText>
|
||||
The initial state of the <b>Forward query params on redirect</b> checkbox will
|
||||
be <b>{shortUrlCreation.forwardQuery ?? true ? 'checked' : 'unchecked'}</b>.
|
||||
</FormText>
|
||||
</ToggleSwitch>
|
||||
</FormGroup>
|
||||
<LabeledFormGroup noMargin label="Tag suggestions search mode:">
|
||||
<DropdownBtn text={tagFilteringModeText(shortUrlCreation.tagFilteringMode)}>
|
||||
<DropdownItem
|
||||
active={!shortUrlCreation.tagFilteringMode || shortUrlCreation.tagFilteringMode === 'startsWith'}
|
||||
onClick={changeTagsFilteringMode('startsWith')}
|
||||
>
|
||||
{tagFilteringModeText('startsWith')}
|
||||
</DropdownItem>
|
||||
<DropdownItem
|
||||
active={shortUrlCreation.tagFilteringMode === 'includes'}
|
||||
onClick={changeTagsFilteringMode('includes')}
|
||||
>
|
||||
{tagFilteringModeText('includes')}
|
||||
</DropdownItem>
|
||||
</DropdownBtn>
|
||||
<FormText>{tagFilteringModeHint(shortUrlCreation.tagFilteringMode)}</FormText>
|
||||
</LabeledFormGroup>
|
||||
</SimpleCard>
|
||||
);
|
||||
};
|
|
@ -1,31 +0,0 @@
|
|||
import { LabeledFormGroup, OrderingDropdown, SimpleCard } from '@shlinkio/shlink-frontend-kit';
|
||||
import type { Settings, ShortUrlsListSettings as ShortUrlsSettings } from '@shlinkio/shlink-web-component';
|
||||
import type { FC } from 'react';
|
||||
import { DEFAULT_SHORT_URLS_ORDERING } from './reducers/settings';
|
||||
|
||||
interface ShortUrlsListSettingsProps {
|
||||
settings: Settings;
|
||||
setShortUrlsListSettings: (settings: ShortUrlsSettings) => void;
|
||||
}
|
||||
|
||||
const SHORT_URLS_ORDERABLE_FIELDS = {
|
||||
dateCreated: 'Created at',
|
||||
shortCode: 'Short URL',
|
||||
longUrl: 'Long URL',
|
||||
title: 'Title',
|
||||
visits: 'Visits',
|
||||
};
|
||||
|
||||
export const ShortUrlsListSettings: FC<ShortUrlsListSettingsProps> = (
|
||||
{ settings: { shortUrlsList }, setShortUrlsListSettings },
|
||||
) => (
|
||||
<SimpleCard title="Short URLs list" className="h-100">
|
||||
<LabeledFormGroup noMargin label="Default ordering for short URLs list:">
|
||||
<OrderingDropdown
|
||||
items={SHORT_URLS_ORDERABLE_FIELDS}
|
||||
order={shortUrlsList?.defaultOrdering ?? DEFAULT_SHORT_URLS_ORDERING}
|
||||
onChange={(field, dir) => setShortUrlsListSettings({ defaultOrdering: { field, dir } })}
|
||||
/>
|
||||
</LabeledFormGroup>
|
||||
</SimpleCard>
|
||||
);
|
|
@ -1,29 +0,0 @@
|
|||
import { LabeledFormGroup, OrderingDropdown, SimpleCard } from '@shlinkio/shlink-frontend-kit';
|
||||
import type { Settings, TagsSettings as TagsSettingsOptions } from '@shlinkio/shlink-web-component';
|
||||
import type { FC } from 'react';
|
||||
import type { Defined } from '../utils/types';
|
||||
|
||||
export type TagsOrder = Defined<TagsSettingsOptions['defaultOrdering']>;
|
||||
|
||||
interface TagsProps {
|
||||
settings: Settings;
|
||||
setTagsSettings: (settings: TagsSettingsOptions) => void;
|
||||
}
|
||||
|
||||
const TAGS_ORDERABLE_FIELDS = {
|
||||
tag: 'Tag',
|
||||
shortUrls: 'Short URLs',
|
||||
visits: 'Visits',
|
||||
};
|
||||
|
||||
export const TagsSettings: FC<TagsProps> = ({ settings: { tags }, setTagsSettings }) => (
|
||||
<SimpleCard title="Tags" className="h-100">
|
||||
<LabeledFormGroup noMargin label="Default ordering for tags list:">
|
||||
<OrderingDropdown
|
||||
items={TAGS_ORDERABLE_FIELDS}
|
||||
order={tags?.defaultOrdering ?? {}}
|
||||
onChange={(field, dir) => setTagsSettings({ ...tags, defaultOrdering: { field, dir } })}
|
||||
/>
|
||||
</LabeledFormGroup>
|
||||
</SimpleCard>
|
||||
);
|
|
@ -1,4 +0,0 @@
|
|||
.user-interface__theme-icon {
|
||||
float: right;
|
||||
margin-top: .25rem;
|
||||
}
|
|
@ -1,34 +0,0 @@
|
|||
import { faMoon, faSun } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import type { Theme } from '@shlinkio/shlink-frontend-kit';
|
||||
import { getSystemPreferredTheme, SimpleCard, ToggleSwitch } from '@shlinkio/shlink-frontend-kit';
|
||||
import type { FC } from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import type { AppSettings, UiSettings } from './reducers/settings';
|
||||
import './UserInterfaceSettings.scss';
|
||||
|
||||
interface UserInterfaceProps {
|
||||
settings: AppSettings;
|
||||
setUiSettings: (settings: UiSettings) => void;
|
||||
|
||||
/* Test seam */
|
||||
_matchMedia?: typeof window.matchMedia;
|
||||
}
|
||||
|
||||
export const UserInterfaceSettings: FC<UserInterfaceProps> = ({ settings: { ui }, setUiSettings, _matchMedia }) => {
|
||||
const currentTheme = useMemo(() => ui?.theme ?? getSystemPreferredTheme(_matchMedia), [ui?.theme, _matchMedia]);
|
||||
return (
|
||||
<SimpleCard title="User interface" className="h-100">
|
||||
<FontAwesomeIcon icon={currentTheme === 'dark' ? faMoon : faSun} className="user-interface__theme-icon" />
|
||||
<ToggleSwitch
|
||||
checked={currentTheme === 'dark'}
|
||||
onChange={(useDarkTheme) => {
|
||||
const theme: Theme = useDarkTheme ? 'dark' : 'light';
|
||||
setUiSettings({ ...ui, theme });
|
||||
}}
|
||||
>
|
||||
Use dark theme.
|
||||
</ToggleSwitch>
|
||||
</SimpleCard>
|
||||
);
|
||||
};
|
|
@ -1,60 +0,0 @@
|
|||
import { LabeledFormGroup, SimpleCard, ToggleSwitch } from '@shlinkio/shlink-frontend-kit';
|
||||
import type { Settings, VisitsSettings as VisitsSettingsConfig } from '@shlinkio/shlink-web-component';
|
||||
import type { FC } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
import { FormGroup } from 'reactstrap';
|
||||
import type { DateInterval } from '../utils/dates/DateIntervalSelector';
|
||||
import { DateIntervalSelector } from '../utils/dates/DateIntervalSelector';
|
||||
import { FormText } from '../utils/forms/FormText';
|
||||
|
||||
type VisitsProps = {
|
||||
settings: Settings;
|
||||
setVisitsSettings: (settings: VisitsSettingsConfig) => void;
|
||||
};
|
||||
|
||||
const currentDefaultInterval = (settings: Settings): DateInterval => settings.visits?.defaultInterval ?? 'last30Days';
|
||||
|
||||
export const VisitsSettings: FC<VisitsProps> = ({ settings, setVisitsSettings }) => {
|
||||
const updateSettings = useCallback(
|
||||
({ defaultInterval, ...rest }: Partial<VisitsSettingsConfig>) => setVisitsSettings(
|
||||
{ defaultInterval: defaultInterval ?? currentDefaultInterval(settings), ...rest },
|
||||
),
|
||||
[setVisitsSettings, settings],
|
||||
);
|
||||
|
||||
return (
|
||||
<SimpleCard title="Visits" className="h-100">
|
||||
<FormGroup>
|
||||
<ToggleSwitch
|
||||
checked={!!settings.visits?.excludeBots}
|
||||
onChange={(excludeBots) => updateSettings({ excludeBots })}
|
||||
>
|
||||
Exclude bots wherever possible (this option‘s effect might depend on Shlink server‘s version).
|
||||
<FormText>
|
||||
The visits coming from potential bots will
|
||||
be <b>{settings.visits?.excludeBots ? 'excluded' : 'included'}</b>.
|
||||
</FormText>
|
||||
</ToggleSwitch>
|
||||
</FormGroup>
|
||||
<FormGroup>
|
||||
<ToggleSwitch
|
||||
checked={!!settings.visits?.loadPrevInterval}
|
||||
onChange={(loadPrevInterval) => updateSettings({ loadPrevInterval })}
|
||||
>
|
||||
Compare visits with previous period.
|
||||
<FormText>
|
||||
When loading visits, previous period <b>{settings.visits?.loadPrevInterval ? 'will' : 'won\'t'}</b> be
|
||||
loaded by default.
|
||||
</FormText>
|
||||
</ToggleSwitch>
|
||||
</FormGroup>
|
||||
<LabeledFormGroup noMargin label="Default interval to load on visits sections:">
|
||||
<DateIntervalSelector
|
||||
allText="All visits"
|
||||
active={currentDefaultInterval(settings)}
|
||||
onChange={(defaultInterval) => updateSettings({ defaultInterval })}
|
||||
/>
|
||||
</LabeledFormGroup>
|
||||
</SimpleCard>
|
||||
);
|
||||
};
|
|
@ -1,15 +1,8 @@
|
|||
import type { PayloadAction, PrepareAction } from '@reduxjs/toolkit';
|
||||
import type { PayloadAction } from '@reduxjs/toolkit';
|
||||
import { createSlice } from '@reduxjs/toolkit';
|
||||
import { mergeDeepRight } from '@shlinkio/data-manipulation';
|
||||
import type { Theme } from '@shlinkio/shlink-frontend-kit';
|
||||
import { getSystemPreferredTheme } from '@shlinkio/shlink-frontend-kit';
|
||||
import type {
|
||||
Settings,
|
||||
ShortUrlCreationSettings,
|
||||
ShortUrlsListSettings,
|
||||
TagsSettings,
|
||||
VisitsSettings,
|
||||
} from '@shlinkio/shlink-web-component';
|
||||
import type { Settings, ShortUrlsListSettings } from '@shlinkio/shlink-web-component/settings';
|
||||
import type { Defined } from '../../utils/types';
|
||||
|
||||
type ShortUrlsOrder = Defined<ShortUrlsListSettings['defaultOrdering']>;
|
||||
|
@ -19,15 +12,9 @@ export const DEFAULT_SHORT_URLS_ORDERING: ShortUrlsOrder = {
|
|||
dir: 'DESC',
|
||||
};
|
||||
|
||||
export type UiSettings = {
|
||||
theme: Theme;
|
||||
};
|
||||
type SettingsAction = PayloadAction<Settings>;
|
||||
|
||||
export type AppSettings = Settings & {
|
||||
ui?: UiSettings;
|
||||
};
|
||||
|
||||
const initialState: AppSettings = {
|
||||
const initialState: Settings = {
|
||||
realTimeUpdates: {
|
||||
enabled: true,
|
||||
},
|
||||
|
@ -45,39 +32,14 @@ const initialState: AppSettings = {
|
|||
},
|
||||
};
|
||||
|
||||
type SettingsAction = PayloadAction<AppSettings>;
|
||||
type SettingsPrepareAction = PrepareAction<AppSettings>;
|
||||
|
||||
const commonReducer = (state: AppSettings, { payload }: SettingsAction) => mergeDeepRight(state, payload);
|
||||
const toReducer = (prepare: SettingsPrepareAction) => ({ reducer: commonReducer, prepare });
|
||||
const toPreparedAction: SettingsPrepareAction = (payload: AppSettings) => ({ payload });
|
||||
|
||||
const { reducer, actions } = createSlice({
|
||||
name: 'shlink/settings',
|
||||
initialState,
|
||||
reducers: {
|
||||
toggleRealTimeUpdates: toReducer((enabled: boolean) => toPreparedAction({ realTimeUpdates: { enabled } })),
|
||||
setRealTimeUpdatesInterval: toReducer((interval: number) => toPreparedAction({ realTimeUpdates: { interval } })),
|
||||
setShortUrlCreationSettings: toReducer(
|
||||
(shortUrlCreation: ShortUrlCreationSettings) => toPreparedAction({ shortUrlCreation }),
|
||||
),
|
||||
setShortUrlsListSettings: toReducer(
|
||||
(shortUrlsList: ShortUrlsListSettings) => toPreparedAction({ shortUrlsList }),
|
||||
),
|
||||
setUiSettings: toReducer((ui: UiSettings) => toPreparedAction({ ui })),
|
||||
setVisitsSettings: toReducer((visits: VisitsSettings) => toPreparedAction({ visits })),
|
||||
setTagsSettings: toReducer((tags: TagsSettings) => toPreparedAction({ tags })),
|
||||
setSettings: (state: Settings, { payload }: SettingsAction) => mergeDeepRight(state, payload),
|
||||
},
|
||||
});
|
||||
|
||||
export const {
|
||||
toggleRealTimeUpdates,
|
||||
setRealTimeUpdatesInterval,
|
||||
setShortUrlCreationSettings,
|
||||
setShortUrlsListSettings,
|
||||
setUiSettings,
|
||||
setVisitsSettings,
|
||||
setTagsSettings,
|
||||
} = actions;
|
||||
export const { setSettings } = actions;
|
||||
|
||||
export const settingsReducer = reducer;
|
||||
|
|
|
@ -1,56 +1,15 @@
|
|||
import type Bottle from 'bottlejs';
|
||||
import type { ConnectDecorator } from '../../container/types';
|
||||
import { withoutSelectedServer } from '../../servers/helpers/withoutSelectedServer';
|
||||
import { RealTimeUpdatesSettings } from '../RealTimeUpdatesSettings';
|
||||
import {
|
||||
setRealTimeUpdatesInterval,
|
||||
setShortUrlCreationSettings,
|
||||
setShortUrlsListSettings,
|
||||
setTagsSettings,
|
||||
setUiSettings,
|
||||
setVisitsSettings,
|
||||
toggleRealTimeUpdates,
|
||||
} from '../reducers/settings';
|
||||
import { SettingsFactory } from '../Settings';
|
||||
import { ShortUrlCreationSettings } from '../ShortUrlCreationSettings';
|
||||
import { ShortUrlsListSettings } from '../ShortUrlsListSettings';
|
||||
import { TagsSettings } from '../TagsSettings';
|
||||
import { UserInterfaceSettings } from '../UserInterfaceSettings';
|
||||
import { VisitsSettings } from '../VisitsSettings';
|
||||
import { setSettings } from '../reducers/settings';
|
||||
import { Settings } from '../Settings';
|
||||
|
||||
export const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
|
||||
// Components
|
||||
bottle.factory('Settings', SettingsFactory);
|
||||
bottle.serviceFactory('Settings', () => Settings);
|
||||
bottle.decorator('Settings', withoutSelectedServer);
|
||||
bottle.decorator('Settings', connect(null, ['resetSelectedServer']));
|
||||
|
||||
bottle.serviceFactory('RealTimeUpdatesSettings', () => RealTimeUpdatesSettings);
|
||||
bottle.decorator(
|
||||
'RealTimeUpdatesSettings',
|
||||
connect(['settings'], ['toggleRealTimeUpdates', 'setRealTimeUpdatesInterval']),
|
||||
);
|
||||
|
||||
bottle.serviceFactory('ShortUrlCreationSettings', () => ShortUrlCreationSettings);
|
||||
bottle.decorator('ShortUrlCreationSettings', connect(['settings'], ['setShortUrlCreationSettings']));
|
||||
|
||||
bottle.serviceFactory('UserInterfaceSettings', () => UserInterfaceSettings);
|
||||
bottle.decorator('UserInterfaceSettings', connect(['settings'], ['setUiSettings']));
|
||||
|
||||
bottle.serviceFactory('VisitsSettings', () => VisitsSettings);
|
||||
bottle.decorator('VisitsSettings', connect(['settings'], ['setVisitsSettings']));
|
||||
|
||||
bottle.serviceFactory('TagsSettings', () => TagsSettings);
|
||||
bottle.decorator('TagsSettings', connect(['settings'], ['setTagsSettings']));
|
||||
|
||||
bottle.serviceFactory('ShortUrlsListSettings', () => ShortUrlsListSettings);
|
||||
bottle.decorator('ShortUrlsListSettings', connect(['settings'], ['setShortUrlsListSettings']));
|
||||
bottle.decorator('Settings', connect(['settings'], ['setSettings', 'resetSelectedServer']));
|
||||
|
||||
// Actions
|
||||
bottle.serviceFactory('toggleRealTimeUpdates', () => toggleRealTimeUpdates);
|
||||
bottle.serviceFactory('setRealTimeUpdatesInterval', () => setRealTimeUpdatesInterval);
|
||||
bottle.serviceFactory('setShortUrlCreationSettings', () => setShortUrlCreationSettings);
|
||||
bottle.serviceFactory('setShortUrlsListSettings', () => setShortUrlsListSettings);
|
||||
bottle.serviceFactory('setUiSettings', () => setUiSettings);
|
||||
bottle.serviceFactory('setVisitsSettings', () => setVisitsSettings);
|
||||
bottle.serviceFactory('setTagsSettings', () => setTagsSettings);
|
||||
bottle.serviceFactory('setSettings', () => setSettings);
|
||||
};
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { DropdownBtn } from '@shlinkio/shlink-frontend-kit';
|
||||
import type { VisitsSettings } from '@shlinkio/shlink-web-component';
|
||||
import type { VisitsSettings } from '@shlinkio/shlink-web-component/settings';
|
||||
import type { FC } from 'react';
|
||||
import { DropdownItem } from 'reactstrap';
|
||||
|
||||
|
|
|
@ -1,86 +0,0 @@
|
|||
import type { RealTimeUpdatesSettings as RealTimeUpdatesSettingsOptions } from '@shlinkio/shlink-web-component';
|
||||
import { screen } from '@testing-library/react';
|
||||
import { fromPartial } from '@total-typescript/shoehorn';
|
||||
import { RealTimeUpdatesSettings } from '../../src/settings/RealTimeUpdatesSettings';
|
||||
import { checkAccessibility } from '../__helpers__/accessibility';
|
||||
import { renderWithEvents } from '../__helpers__/setUpTest';
|
||||
|
||||
describe('<RealTimeUpdatesSettings />', () => {
|
||||
const toggleRealTimeUpdates = vi.fn();
|
||||
const setRealTimeUpdatesInterval = vi.fn();
|
||||
const setUp = (realTimeUpdates: Partial<RealTimeUpdatesSettingsOptions> = {}) => renderWithEvents(
|
||||
<RealTimeUpdatesSettings
|
||||
settings={fromPartial({ realTimeUpdates })}
|
||||
toggleRealTimeUpdates={toggleRealTimeUpdates}
|
||||
setRealTimeUpdatesInterval={setRealTimeUpdatesInterval}
|
||||
/>,
|
||||
);
|
||||
|
||||
it('passes a11y checks', () => checkAccessibility(setUp()));
|
||||
|
||||
it('renders enabled real time updates as expected', () => {
|
||||
setUp({ enabled: true });
|
||||
|
||||
expect(screen.getByLabelText(/^Enable or disable real-time updates./)).toBeChecked();
|
||||
expect(screen.getByText(/^Real-time updates are currently being/)).toHaveTextContent('processed');
|
||||
expect(screen.getByText(/^Real-time updates are currently being/)).not.toHaveTextContent('ignored');
|
||||
expect(screen.getByText('Real-time updates frequency (in minutes):')).not.toHaveAttribute(
|
||||
'class',
|
||||
expect.stringContaining('text-muted'),
|
||||
);
|
||||
expect(screen.getByLabelText('Real-time updates frequency (in minutes):')).not.toHaveAttribute('disabled');
|
||||
expect(screen.getByText('Updates will be reflected in the UI as soon as they happen.')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders disabled real time updates as expected', () => {
|
||||
setUp({ enabled: false });
|
||||
|
||||
expect(screen.getByLabelText(/^Enable or disable real-time updates./)).not.toBeChecked();
|
||||
expect(screen.getByText(/^Real-time updates are currently being/)).not.toHaveTextContent('processed');
|
||||
expect(screen.getByText(/^Real-time updates are currently being/)).toHaveTextContent('ignored');
|
||||
expect(screen.getByText('Real-time updates frequency (in minutes):')).toHaveAttribute(
|
||||
'class',
|
||||
expect.stringContaining('text-muted'),
|
||||
);
|
||||
expect(screen.getByLabelText('Real-time updates frequency (in minutes):')).toHaveAttribute('disabled');
|
||||
expect(screen.queryByText('Updates will be reflected in the UI as soon as they happen.')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it.each([
|
||||
[1, 'minute'],
|
||||
[2, 'minutes'],
|
||||
[10, 'minutes'],
|
||||
[100, 'minutes'],
|
||||
])('shows expected children when interval is greater than 0', (interval, minutesWord) => {
|
||||
setUp({ enabled: true, interval });
|
||||
|
||||
expect(screen.getByText(/^Updates will be reflected in the UI every/)).toHaveTextContent(
|
||||
`${interval} ${minutesWord}`,
|
||||
);
|
||||
expect(screen.getByLabelText('Real-time updates frequency (in minutes):')).toHaveValue(interval);
|
||||
expect(screen.queryByText('Updates will be reflected in the UI as soon as they happen.')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it.each([[undefined], [0]])('shows expected children when interval is 0 or undefined', (interval) => {
|
||||
setUp({ enabled: true, interval });
|
||||
|
||||
expect(screen.queryByText(/^Updates will be reflected in the UI every/)).not.toBeInTheDocument();
|
||||
expect(screen.getByText('Updates will be reflected in the UI as soon as they happen.')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('updates real time updates when typing on input', async () => {
|
||||
const { user } = setUp({ enabled: true });
|
||||
|
||||
expect(setRealTimeUpdatesInterval).not.toHaveBeenCalled();
|
||||
await user.type(screen.getByLabelText('Real-time updates frequency (in minutes):'), '5');
|
||||
expect(setRealTimeUpdatesInterval).toHaveBeenCalledWith(5);
|
||||
});
|
||||
|
||||
it('toggles real time updates on switch change', async () => {
|
||||
const { user } = setUp({ enabled: true });
|
||||
|
||||
expect(toggleRealTimeUpdates).not.toHaveBeenCalled();
|
||||
await user.click(screen.getByText(/^Enable or disable real-time updates./));
|
||||
expect(toggleRealTimeUpdates).toHaveBeenCalled();
|
||||
});
|
||||
});
|
|
@ -1,52 +1,14 @@
|
|||
import { render, screen } from '@testing-library/react';
|
||||
import { fromPartial } from '@total-typescript/shoehorn';
|
||||
import { createMemoryHistory } from 'history';
|
||||
import { Router } from 'react-router-dom';
|
||||
import { SettingsFactory } from '../../src/settings/Settings';
|
||||
import { render } from '@testing-library/react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { Settings } from '../../src/settings/Settings';
|
||||
import { checkAccessibility } from '../__helpers__/accessibility';
|
||||
|
||||
describe('<Settings />', () => {
|
||||
const Settings = SettingsFactory(fromPartial({
|
||||
RealTimeUpdatesSettings: () => <span>RealTimeUpdates</span>,
|
||||
ShortUrlCreationSettings: () => <span>ShortUrlCreation</span>,
|
||||
ShortUrlsListSettings: () => <span>ShortUrlsList</span>,
|
||||
UserInterfaceSettings: () => <span>UserInterface</span>,
|
||||
VisitsSettings: () => <span>Visits</span>,
|
||||
TagsSettings: () => <span>Tags</span>,
|
||||
}));
|
||||
const setUp = (activeRoute = '/') => {
|
||||
const history = createMemoryHistory();
|
||||
history.push(activeRoute);
|
||||
return render(<Router location={history.location} navigator={history}><Settings /></Router>);
|
||||
};
|
||||
const setUp = () => render(
|
||||
<MemoryRouter>
|
||||
<Settings settings={{}} setSettings={vi.fn()} />
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
it('passes a11y checks', () => checkAccessibility(setUp()));
|
||||
|
||||
it.each([
|
||||
['/general', {
|
||||
visibleComps: ['UserInterface', 'RealTimeUpdates'],
|
||||
hiddenComps: ['ShortUrlCreation', 'ShortUrlsList', 'Tags', 'Visits'],
|
||||
}],
|
||||
['/short-urls', {
|
||||
visibleComps: ['ShortUrlCreation', 'ShortUrlsList'],
|
||||
hiddenComps: ['UserInterface', 'RealTimeUpdates', 'Tags', 'Visits'],
|
||||
}],
|
||||
['/other-items', {
|
||||
visibleComps: ['Tags', 'Visits'],
|
||||
hiddenComps: ['UserInterface', 'RealTimeUpdates', 'ShortUrlCreation', 'ShortUrlsList'],
|
||||
}],
|
||||
])('renders expected sections based on route', (activeRoute, { visibleComps, hiddenComps }) => {
|
||||
setUp(activeRoute);
|
||||
|
||||
visibleComps.forEach((comp) => expect(screen.getByText(comp)).toBeInTheDocument());
|
||||
hiddenComps.forEach((comp) => expect(screen.queryByText(comp)).not.toBeInTheDocument());
|
||||
});
|
||||
|
||||
it('renders expected menu', () => {
|
||||
setUp();
|
||||
|
||||
expect(screen.getByRole('link', { name: 'General' })).toHaveAttribute('href', '/general');
|
||||
expect(screen.getByRole('link', { name: 'Short URLs' })).toHaveAttribute('href', '/short-urls');
|
||||
expect(screen.getByRole('link', { name: 'Other items' })).toHaveAttribute('href', '/other-items');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,115 +0,0 @@
|
|||
import type { ShortUrlCreationSettings as ShortUrlsSettings } from '@shlinkio/shlink-web-component';
|
||||
import { screen } from '@testing-library/react';
|
||||
import { fromPartial } from '@total-typescript/shoehorn';
|
||||
import { ShortUrlCreationSettings } from '../../src/settings/ShortUrlCreationSettings';
|
||||
import { checkAccessibility } from '../__helpers__/accessibility';
|
||||
import { renderWithEvents } from '../__helpers__/setUpTest';
|
||||
|
||||
describe('<ShortUrlCreationSettings />', () => {
|
||||
const setShortUrlCreationSettings = vi.fn();
|
||||
const setUp = (shortUrlCreation?: ShortUrlsSettings) => renderWithEvents(
|
||||
<ShortUrlCreationSettings
|
||||
settings={fromPartial({ shortUrlCreation })}
|
||||
setShortUrlCreationSettings={setShortUrlCreationSettings}
|
||||
/>,
|
||||
);
|
||||
|
||||
it('passes a11y checks', () => checkAccessibility(setUp()));
|
||||
|
||||
it.each([
|
||||
[{ validateUrls: true }, true],
|
||||
[{ validateUrls: false }, false],
|
||||
[undefined, false],
|
||||
])('URL validation switch has proper initial state', (shortUrlCreation, expectedChecked) => {
|
||||
const matcher = /^Request validation on long URLs when creating new short URLs/;
|
||||
|
||||
setUp(shortUrlCreation);
|
||||
|
||||
const checkbox = screen.getByLabelText(matcher);
|
||||
const label = screen.getByText(matcher);
|
||||
|
||||
if (expectedChecked) {
|
||||
expect(checkbox).toBeChecked();
|
||||
expect(label).toHaveTextContent('Validate URL checkbox will be checked');
|
||||
expect(label).not.toHaveTextContent('Validate URL checkbox will be unchecked');
|
||||
} else {
|
||||
expect(checkbox).not.toBeChecked();
|
||||
expect(label).toHaveTextContent('Validate URL checkbox will be unchecked');
|
||||
expect(label).not.toHaveTextContent('Validate URL checkbox will be checked');
|
||||
}
|
||||
});
|
||||
|
||||
it.each([
|
||||
[{ forwardQuery: true }, true],
|
||||
[{ forwardQuery: false }, false],
|
||||
[{}, true],
|
||||
])('forward query switch is toggled if option is true', (shortUrlCreation, expectedChecked) => {
|
||||
const matcher = /^Make all new short URLs forward their query params to the long URL/;
|
||||
|
||||
setUp({ validateUrls: true, ...shortUrlCreation });
|
||||
|
||||
const checkbox = screen.getByLabelText(matcher);
|
||||
const label = screen.getByText(matcher);
|
||||
|
||||
if (expectedChecked) {
|
||||
expect(checkbox).toBeChecked();
|
||||
expect(label).toHaveTextContent('Forward query params on redirect checkbox will be checked');
|
||||
expect(label).not.toHaveTextContent('Forward query params on redirect checkbox will be unchecked');
|
||||
} else {
|
||||
expect(checkbox).not.toBeChecked();
|
||||
expect(label).toHaveTextContent('Forward query params on redirect checkbox will be unchecked');
|
||||
expect(label).not.toHaveTextContent('Forward query params on redirect checkbox will be checked');
|
||||
}
|
||||
});
|
||||
|
||||
it.each([
|
||||
[{ tagFilteringMode: 'includes' } as ShortUrlsSettings, 'Suggest tags including input', 'including'],
|
||||
[
|
||||
{ tagFilteringMode: 'startsWith' } as ShortUrlsSettings,
|
||||
'Suggest tags starting with input',
|
||||
'starting with',
|
||||
],
|
||||
[undefined, 'Suggest tags starting with input', 'starting with'],
|
||||
])('shows expected texts for tags suggestions', (shortUrlCreation, expectedText, expectedHint) => {
|
||||
setUp(shortUrlCreation);
|
||||
|
||||
expect(screen.getByRole('button', { name: expectedText })).toBeInTheDocument();
|
||||
expect(screen.getByText(/^The list of suggested tags will contain those/)).toHaveTextContent(expectedHint);
|
||||
});
|
||||
|
||||
it.each([[true], [false]])('invokes setShortUrlCreationSettings when URL validation toggle value changes', async (validateUrls) => {
|
||||
const { user } = setUp({ validateUrls });
|
||||
|
||||
expect(setShortUrlCreationSettings).not.toHaveBeenCalled();
|
||||
await user.click(screen.getByLabelText(/^Request validation on long URLs when creating new short URLs/));
|
||||
expect(setShortUrlCreationSettings).toHaveBeenCalledWith({ validateUrls: !validateUrls });
|
||||
});
|
||||
|
||||
it.each([[true], [false]])('invokes setShortUrlCreationSettings when forward query toggle value changes', async (forwardQuery) => {
|
||||
const { user } = setUp({ validateUrls: true, forwardQuery });
|
||||
|
||||
expect(setShortUrlCreationSettings).not.toHaveBeenCalled();
|
||||
await user.click(screen.getByLabelText(/^Make all new short URLs forward their query params to the long URL/));
|
||||
expect(setShortUrlCreationSettings).toHaveBeenCalledWith(expect.objectContaining({ forwardQuery: !forwardQuery }));
|
||||
});
|
||||
|
||||
it('invokes setShortUrlCreationSettings when dropdown value changes', async () => {
|
||||
const { user } = setUp();
|
||||
const clickItem = async (name: string) => {
|
||||
await user.click(screen.getByRole('button', { name: 'Suggest tags starting with input' }));
|
||||
await user.click(await screen.findByRole('menuitem', { name }));
|
||||
};
|
||||
|
||||
expect(setShortUrlCreationSettings).not.toHaveBeenCalled();
|
||||
|
||||
await clickItem('Suggest tags including input');
|
||||
expect(setShortUrlCreationSettings).toHaveBeenCalledWith(expect.objectContaining(
|
||||
{ tagFilteringMode: 'includes' },
|
||||
));
|
||||
|
||||
await clickItem('Suggest tags starting with input');
|
||||
expect(setShortUrlCreationSettings).toHaveBeenCalledWith(expect.objectContaining(
|
||||
{ tagFilteringMode: 'startsWith' },
|
||||
));
|
||||
});
|
||||
});
|
|
@ -1,40 +0,0 @@
|
|||
import type { ShortUrlsListSettings as ShortUrlsSettings } from '@shlinkio/shlink-web-component';
|
||||
import { screen } from '@testing-library/react';
|
||||
import { fromPartial } from '@total-typescript/shoehorn';
|
||||
import { ShortUrlsListSettings } from '../../src/settings/ShortUrlsListSettings';
|
||||
import { checkAccessibility } from '../__helpers__/accessibility';
|
||||
import { renderWithEvents } from '../__helpers__/setUpTest';
|
||||
|
||||
describe('<ShortUrlsListSettings />', () => {
|
||||
const setSettings = vi.fn();
|
||||
const setUp = (shortUrlsList?: ShortUrlsSettings) => renderWithEvents(
|
||||
<ShortUrlsListSettings settings={fromPartial({ shortUrlsList })} setShortUrlsListSettings={setSettings} />,
|
||||
);
|
||||
|
||||
it('passes a11y checks', () => checkAccessibility(setUp()));
|
||||
|
||||
it.each([
|
||||
[undefined, 'Order by: Created at - DESC'],
|
||||
[fromPartial<ShortUrlsSettings>({}), 'Order by: Created at - DESC'],
|
||||
[fromPartial<ShortUrlsSettings>({ defaultOrdering: {} }), 'Order by...'],
|
||||
[fromPartial<ShortUrlsSettings>({ defaultOrdering: { field: 'longUrl', dir: 'DESC' } }), 'Order by: Long URL - DESC'],
|
||||
[fromPartial<ShortUrlsSettings>({ defaultOrdering: { field: 'visits', dir: 'ASC' } }), 'Order by: Visits - ASC'],
|
||||
])('shows expected ordering', (shortUrlsList, expectedOrder) => {
|
||||
setUp(shortUrlsList);
|
||||
expect(screen.getByRole('button')).toHaveTextContent(expectedOrder);
|
||||
});
|
||||
|
||||
it.each([
|
||||
['Clear selection', undefined, undefined],
|
||||
['Long URL', 'longUrl', 'ASC'],
|
||||
['Visits', 'visits', 'ASC'],
|
||||
['Title', 'title', 'ASC'],
|
||||
])('invokes setSettings when ordering changes', async (name, field, dir) => {
|
||||
const { user } = setUp();
|
||||
|
||||
expect(setSettings).not.toHaveBeenCalled();
|
||||
await user.click(screen.getByRole('button'));
|
||||
await user.click(screen.getByRole('menuitem', { name }));
|
||||
expect(setSettings).toHaveBeenCalledWith({ defaultOrdering: { field, dir } });
|
||||
});
|
||||
});
|
|
@ -1,47 +0,0 @@
|
|||
import type { TagsSettings as TagsSettingsOptions } from '@shlinkio/shlink-web-component';
|
||||
import { screen } from '@testing-library/react';
|
||||
import { fromPartial } from '@total-typescript/shoehorn';
|
||||
import type { TagsOrder } from '../../src/settings/TagsSettings';
|
||||
import { TagsSettings } from '../../src/settings/TagsSettings';
|
||||
import { checkAccessibility } from '../__helpers__/accessibility';
|
||||
import { renderWithEvents } from '../__helpers__/setUpTest';
|
||||
|
||||
describe('<TagsSettings />', () => {
|
||||
const setTagsSettings = vi.fn();
|
||||
const setUp = (tags?: TagsSettingsOptions) => renderWithEvents(
|
||||
<TagsSettings settings={fromPartial({ tags })} setTagsSettings={setTagsSettings} />,
|
||||
);
|
||||
|
||||
it('passes a11y checks', () => checkAccessibility(setUp()));
|
||||
|
||||
it('renders expected amount of groups', () => {
|
||||
setUp();
|
||||
|
||||
expect(screen.getByText('Default ordering for tags list:')).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: 'Order by...' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it.each([
|
||||
[undefined, 'Order by...'],
|
||||
[{}, 'Order by...'],
|
||||
[{ defaultOrdering: {} }, 'Order by...'],
|
||||
[{ defaultOrdering: { field: 'tag', dir: 'DESC' } as TagsOrder }, 'Order by: Tag - DESC'],
|
||||
[{ defaultOrdering: { field: 'visits', dir: 'ASC' } as TagsOrder }, 'Order by: Visits - ASC'],
|
||||
])('shows expected ordering', (tags, expectedOrder) => {
|
||||
setUp(tags);
|
||||
expect(screen.getByRole('button', { name: expectedOrder })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it.each([
|
||||
['Tag', 'tag', 'ASC'],
|
||||
['Visits', 'visits', 'ASC'],
|
||||
['Short URLs', 'shortUrls', 'ASC'],
|
||||
])('invokes setTagsSettings when ordering changes', async (name, field, dir) => {
|
||||
const { user } = setUp();
|
||||
|
||||
expect(setTagsSettings).not.toHaveBeenCalled();
|
||||
await user.click(screen.getByText('Order by...'));
|
||||
await user.click(screen.getByRole('menuitem', { name }));
|
||||
expect(setTagsSettings).toHaveBeenCalledWith({ defaultOrdering: { field, dir } });
|
||||
});
|
||||
});
|
|
@ -1,56 +0,0 @@
|
|||
import { screen } from '@testing-library/react';
|
||||
import { fromPartial } from '@total-typescript/shoehorn';
|
||||
import type { UiSettings } from '../../src/settings/reducers/settings';
|
||||
import { UserInterfaceSettings } from '../../src/settings/UserInterfaceSettings';
|
||||
import { checkAccessibility } from '../__helpers__/accessibility';
|
||||
import { renderWithEvents } from '../__helpers__/setUpTest';
|
||||
|
||||
describe('<UserInterfaceSettings />', () => {
|
||||
const setUiSettings = vi.fn();
|
||||
const setUp = (ui?: UiSettings, defaultDarkTheme = false) => renderWithEvents(
|
||||
<UserInterfaceSettings
|
||||
settings={fromPartial({ ui })}
|
||||
setUiSettings={setUiSettings}
|
||||
_matchMedia={vi.fn().mockReturnValue({ matches: defaultDarkTheme })}
|
||||
/>,
|
||||
);
|
||||
|
||||
it('passes a11y checks', () => checkAccessibility(setUp()));
|
||||
|
||||
it.each([
|
||||
[{ theme: 'dark' as const }, true, true],
|
||||
[{ theme: 'dark' as const }, false, true],
|
||||
[{ theme: 'light' as const }, true, false],
|
||||
[{ theme: 'light' as const }, false, false],
|
||||
[undefined, false, false],
|
||||
[undefined, true, true],
|
||||
])('toggles switch if theme is dark', (ui, defaultDarkTheme, expectedChecked) => {
|
||||
setUp(ui, defaultDarkTheme);
|
||||
|
||||
if (expectedChecked) {
|
||||
expect(screen.getByLabelText('Use dark theme.')).toBeChecked();
|
||||
} else {
|
||||
expect(screen.getByLabelText('Use dark theme.')).not.toBeChecked();
|
||||
}
|
||||
});
|
||||
|
||||
it.each([
|
||||
[{ theme: 'dark' as const }],
|
||||
[{ theme: 'light' as const }],
|
||||
[undefined],
|
||||
])('shows different icons based on theme', (ui) => {
|
||||
setUp(ui);
|
||||
expect(screen.getByRole('img', { hidden: true })).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it.each([
|
||||
['light' as const, 'dark' as const],
|
||||
['dark' as const, 'light' as const],
|
||||
])('invokes setUiSettings when theme toggle value changes', async (initialTheme, expectedTheme) => {
|
||||
const { user } = setUp({ theme: initialTheme });
|
||||
|
||||
expect(setUiSettings).not.toHaveBeenCalled();
|
||||
await user.click(screen.getByLabelText('Use dark theme.'));
|
||||
expect(setUiSettings).toHaveBeenCalledWith({ theme: expectedTheme });
|
||||
});
|
||||
});
|
|
@ -1,129 +0,0 @@
|
|||
import type { Settings } from '@shlinkio/shlink-web-component';
|
||||
import { screen } from '@testing-library/react';
|
||||
import { fromPartial } from '@total-typescript/shoehorn';
|
||||
import { VisitsSettings } from '../../src/settings/VisitsSettings';
|
||||
import { checkAccessibility } from '../__helpers__/accessibility';
|
||||
import { renderWithEvents } from '../__helpers__/setUpTest';
|
||||
|
||||
describe('<VisitsSettings />', () => {
|
||||
const setVisitsSettings = vi.fn();
|
||||
const setUp = (settings: Partial<Settings> = {}) => renderWithEvents(
|
||||
<VisitsSettings settings={fromPartial(settings)} setVisitsSettings={setVisitsSettings} />,
|
||||
);
|
||||
|
||||
it('passes a11y checks', () => checkAccessibility(setUp()));
|
||||
|
||||
it('renders expected components', () => {
|
||||
setUp();
|
||||
|
||||
expect(screen.getByRole('heading')).toHaveTextContent('Visits');
|
||||
expect(screen.getByText('Default interval to load on visits sections:')).toBeInTheDocument();
|
||||
expect(screen.getByText(/^Exclude bots wherever possible/)).toBeInTheDocument();
|
||||
expect(screen.getByText('Compare visits with previous period.')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it.each([
|
||||
[fromPartial<Settings>({}), 'Last 30 days'],
|
||||
[fromPartial<Settings>({ visits: {} }), 'Last 30 days'],
|
||||
[
|
||||
fromPartial<Settings>({
|
||||
visits: {
|
||||
defaultInterval: 'last7Days',
|
||||
},
|
||||
}),
|
||||
'Last 7 days',
|
||||
],
|
||||
[
|
||||
fromPartial<Settings>({
|
||||
visits: {
|
||||
defaultInterval: 'today',
|
||||
},
|
||||
}),
|
||||
'Today',
|
||||
],
|
||||
])('sets expected interval as active', (settings, expectedInterval) => {
|
||||
setUp(settings);
|
||||
expect(screen.getByRole('button')).toHaveTextContent(expectedInterval);
|
||||
});
|
||||
|
||||
it('invokes setVisitsSettings when interval changes', async () => {
|
||||
const { user } = setUp();
|
||||
const selectOption = async (name: string) => {
|
||||
await user.click(screen.getByRole('button'));
|
||||
await user.click(screen.getByRole('menuitem', { name }));
|
||||
};
|
||||
|
||||
await selectOption('Last 7 days');
|
||||
await selectOption('Last 180 days');
|
||||
await selectOption('Yesterday');
|
||||
|
||||
expect(setVisitsSettings).toHaveBeenCalledTimes(3);
|
||||
expect(setVisitsSettings).toHaveBeenNthCalledWith(1, { defaultInterval: 'last7Days' });
|
||||
expect(setVisitsSettings).toHaveBeenNthCalledWith(2, { defaultInterval: 'last180Days' });
|
||||
expect(setVisitsSettings).toHaveBeenNthCalledWith(3, { defaultInterval: 'yesterday' });
|
||||
});
|
||||
|
||||
it.each([
|
||||
[
|
||||
fromPartial<Settings>({}),
|
||||
/The visits coming from potential bots will be included.$/,
|
||||
/The visits coming from potential bots will be excluded.$/,
|
||||
],
|
||||
[
|
||||
fromPartial<Settings>({ visits: { excludeBots: false } }),
|
||||
/The visits coming from potential bots will be included.$/,
|
||||
/The visits coming from potential bots will be excluded.$/,
|
||||
],
|
||||
[
|
||||
fromPartial<Settings>({ visits: { excludeBots: true } }),
|
||||
/The visits coming from potential bots will be excluded.$/,
|
||||
/The visits coming from potential bots will be included.$/,
|
||||
],
|
||||
])('displays expected helper text for exclude bots control', (settings, expectedText, notExpectedText) => {
|
||||
setUp(settings);
|
||||
|
||||
const visitsComponent = screen.getByText(/^Exclude bots wherever possible/);
|
||||
|
||||
expect(visitsComponent).toHaveTextContent(expectedText);
|
||||
expect(visitsComponent).not.toHaveTextContent(notExpectedText);
|
||||
});
|
||||
|
||||
it('invokes setVisitsSettings when bot exclusion is toggled', async () => {
|
||||
const { user } = setUp();
|
||||
|
||||
await user.click(screen.getByText(/^Exclude bots wherever possible/));
|
||||
expect(setVisitsSettings).toHaveBeenCalledWith(expect.objectContaining({ excludeBots: true }));
|
||||
});
|
||||
|
||||
it.each([
|
||||
[
|
||||
fromPartial<Settings>({}),
|
||||
/When loading visits, previous period won't be loaded by default.$/,
|
||||
/When loading visits, previous period will be loaded by default.$/,
|
||||
],
|
||||
[
|
||||
fromPartial<Settings>({ visits: { loadPrevInterval: false } }),
|
||||
/When loading visits, previous period won't be loaded by default.$/,
|
||||
/When loading visits, previous period will be loaded by default.$/,
|
||||
],
|
||||
[
|
||||
fromPartial<Settings>({ visits: { loadPrevInterval: true } }),
|
||||
/When loading visits, previous period will be loaded by default.$/,
|
||||
/When loading visits, previous period won't be loaded by default.$/,
|
||||
],
|
||||
])('displays expected helper text for prev interval control', (settings, expectedText, notExpectedText) => {
|
||||
setUp(settings);
|
||||
|
||||
const visitsComponent = screen.getByText('Compare visits with previous period.');
|
||||
|
||||
expect(visitsComponent).toHaveTextContent(expectedText);
|
||||
expect(visitsComponent).not.toHaveTextContent(notExpectedText);
|
||||
});
|
||||
|
||||
it('invokes setVisitsSettings when loading prev visits is toggled', async () => {
|
||||
const { user } = setUp();
|
||||
|
||||
await user.click(screen.getByText('Compare visits with previous period.'));
|
||||
expect(setVisitsSettings).toHaveBeenCalledWith(expect.objectContaining({ loadPrevInterval: true }));
|
||||
});
|
||||
});
|
|
@ -1,55 +0,0 @@
|
|||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`<UserInterfaceSettings /> > shows different icons based on theme 1`] = `
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="svg-inline--fa fa-moon user-interface__theme-icon"
|
||||
data-icon="moon"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
viewBox="0 0 384 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M223.5 32C100 32 0 132.3 0 256S100 480 223.5 480c60.6 0 115.5-24.2 155.8-63.4c5-4.9 6.3-12.5 3.1-18.7s-10.1-9.7-17-8.5c-9.8 1.7-19.8 2.6-30.1 2.6c-96.9 0-175.5-78.8-175.5-176c0-65.8 36-123.1 89.3-153.3c6.1-3.5 9.2-10.5 7.7-17.3s-7.3-11.9-14.3-12.5c-6.3-.5-12.6-.8-19-.8z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
`;
|
||||
|
||||
exports[`<UserInterfaceSettings /> > shows different icons based on theme 2`] = `
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="svg-inline--fa fa-sun user-interface__theme-icon"
|
||||
data-icon="sun"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M361.5 1.2c5 2.1 8.6 6.6 9.6 11.9L391 121l107.9 19.8c5.3 1 9.8 4.6 11.9 9.6s1.5 10.7-1.6 15.2L446.9 256l62.3 90.3c3.1 4.5 3.7 10.2 1.6 15.2s-6.6 8.6-11.9 9.6L391 391 371.1 498.9c-1 5.3-4.6 9.8-9.6 11.9s-10.7 1.5-15.2-1.6L256 446.9l-90.3 62.3c-4.5 3.1-10.2 3.7-15.2 1.6s-8.6-6.6-9.6-11.9L121 391 13.1 371.1c-5.3-1-9.8-4.6-11.9-9.6s-1.5-10.7 1.6-15.2L65.1 256 2.8 165.7c-3.1-4.5-3.7-10.2-1.6-15.2s6.6-8.6 11.9-9.6L121 121 140.9 13.1c1-5.3 4.6-9.8 9.6-11.9s10.7-1.5 15.2 1.6L256 65.1 346.3 2.8c4.5-3.1 10.2-3.7 15.2-1.6zM160 256a96 96 0 1 1 192 0 96 96 0 1 1 -192 0zm224 0a128 128 0 1 0 -256 0 128 128 0 1 0 256 0z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
`;
|
||||
|
||||
exports[`<UserInterfaceSettings /> > shows different icons based on theme 3`] = `
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="svg-inline--fa fa-sun user-interface__theme-icon"
|
||||
data-icon="sun"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M361.5 1.2c5 2.1 8.6 6.6 9.6 11.9L391 121l107.9 19.8c5.3 1 9.8 4.6 11.9 9.6s1.5 10.7-1.6 15.2L446.9 256l62.3 90.3c3.1 4.5 3.7 10.2 1.6 15.2s-6.6 8.6-11.9 9.6L391 391 371.1 498.9c-1 5.3-4.6 9.8-9.6 11.9s-10.7 1.5-15.2-1.6L256 446.9l-90.3 62.3c-4.5 3.1-10.2 3.7-15.2 1.6s-8.6-6.6-9.6-11.9L121 391 13.1 371.1c-5.3-1-9.8-4.6-11.9-9.6s-1.5-10.7 1.6-15.2L65.1 256 2.8 165.7c-3.1-4.5-3.7-10.2-1.6-15.2s6.6-8.6 11.9-9.6L121 121 140.9 13.1c1-5.3 4.6-9.8 9.6-11.9s10.7-1.5 15.2 1.6L256 65.1 346.3 2.8c4.5-3.1 10.2-3.7 15.2-1.6zM160 256a96 96 0 1 1 192 0 96 96 0 1 1 -192 0zm224 0a128 128 0 1 0 -256 0 128 128 0 1 0 256 0z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
`;
|
|
@ -1,75 +1,25 @@
|
|||
import {
|
||||
DEFAULT_SHORT_URLS_ORDERING,
|
||||
setRealTimeUpdatesInterval,
|
||||
setShortUrlCreationSettings,
|
||||
setShortUrlsListSettings,
|
||||
setTagsSettings,
|
||||
settingsReducer,
|
||||
setUiSettings,
|
||||
setVisitsSettings,
|
||||
toggleRealTimeUpdates,
|
||||
} from '../../../src/settings/reducers/settings';
|
||||
import type { Settings } from '@shlinkio/shlink-web-component/settings';
|
||||
import { fromPartial } from '@total-typescript/shoehorn';
|
||||
import { DEFAULT_SHORT_URLS_ORDERING, setSettings, settingsReducer } from '../../../src/settings/reducers/settings';
|
||||
|
||||
describe('settingsReducer', () => {
|
||||
const realTimeUpdates = { enabled: true };
|
||||
const shortUrlCreation = { validateUrls: false };
|
||||
const ui = { theme: 'light' };
|
||||
const visits = { defaultInterval: 'last30Days' };
|
||||
const ui = { theme: 'light' as const };
|
||||
const visits = { defaultInterval: 'last30Days' as const };
|
||||
const shortUrlsList = { defaultOrdering: DEFAULT_SHORT_URLS_ORDERING };
|
||||
const settings = { realTimeUpdates, shortUrlCreation, ui, visits, shortUrlsList };
|
||||
const settings = fromPartial<Settings>({ realTimeUpdates, shortUrlCreation, ui, visits, shortUrlsList });
|
||||
|
||||
describe('reducer', () => {
|
||||
it('returns realTimeUpdates when action is SET_SETTINGS', () => {
|
||||
expect(settingsReducer(undefined, toggleRealTimeUpdates(realTimeUpdates.enabled))).toEqual(settings);
|
||||
it('can update settings', () => {
|
||||
expect(settingsReducer(undefined, setSettings(settings))).toEqual(settings);
|
||||
});
|
||||
});
|
||||
|
||||
describe('toggleRealTimeUpdates', () => {
|
||||
it.each([[true], [false]])('updates settings with provided value and then loads updates again', (enabled) => {
|
||||
const { payload } = toggleRealTimeUpdates(enabled);
|
||||
expect(payload).toEqual({ realTimeUpdates: { enabled } });
|
||||
});
|
||||
});
|
||||
|
||||
describe('setRealTimeUpdatesInterval', () => {
|
||||
it.each([[0], [1], [2], [10]])('updates settings with provided value and then loads updates again', (interval) => {
|
||||
const { payload } = setRealTimeUpdatesInterval(interval);
|
||||
expect(payload).toEqual({ realTimeUpdates: { interval } });
|
||||
});
|
||||
});
|
||||
|
||||
describe('setShortUrlCreationSettings', () => {
|
||||
it('creates action to set shortUrlCreation settings', () => {
|
||||
const { payload } = setShortUrlCreationSettings({ validateUrls: true });
|
||||
expect(payload).toEqual({ shortUrlCreation: { validateUrls: true } });
|
||||
});
|
||||
});
|
||||
|
||||
describe('setUiSettings', () => {
|
||||
it('creates action to set ui settings', () => {
|
||||
const { payload } = setUiSettings({ theme: 'dark' });
|
||||
expect(payload).toEqual({ ui: { theme: 'dark' } });
|
||||
});
|
||||
});
|
||||
|
||||
describe('setVisitsSettings', () => {
|
||||
it('creates action to set visits settings', () => {
|
||||
const { payload } = setVisitsSettings({ defaultInterval: 'last180Days' });
|
||||
expect(payload).toEqual({ visits: { defaultInterval: 'last180Days' } });
|
||||
});
|
||||
});
|
||||
|
||||
describe('setTagsSettings', () => {
|
||||
it('creates action to set tags settings', () => {
|
||||
const { payload } = setTagsSettings({ defaultMode: 'list' });
|
||||
expect(payload).toEqual({ tags: { defaultMode: 'list' } });
|
||||
});
|
||||
});
|
||||
|
||||
describe('setShortUrlsListSettings', () => {
|
||||
it('creates action to set short URLs list settings', () => {
|
||||
const { payload } = setShortUrlsListSettings({ defaultOrdering: DEFAULT_SHORT_URLS_ORDERING });
|
||||
expect(payload).toEqual({ shortUrlsList: { defaultOrdering: DEFAULT_SHORT_URLS_ORDERING } });
|
||||
describe('setSettings', () => {
|
||||
it('creates action to set settings', () => {
|
||||
const { payload } = setSettings(settings);
|
||||
expect(payload).toEqual(settings);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue