diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b669a96..7287f36e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,24 @@ 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). -## [0.6.2] - 2024-04.17 +## [0.7.0] - 2024-05-20 +### Added +* Add new `@shlinkio/shlink-web-client/settings` entry point, to expose a component rendering the settings form and all settings-related types. + +### Changed +* Update dependencies + +### Deprecated +* *Nothing* + +### Removed +* *Nothing* + +### Fixed +* *Nothing* + + +## [0.6.2] - 2024-04-17 ### Added * *Nothing* diff --git a/dev/App.tsx b/dev/App.tsx index 568a9147..a1344fb3 100644 --- a/dev/App.tsx +++ b/dev/App.tsx @@ -3,8 +3,8 @@ import { FetchHttpClient } from '@shlinkio/shlink-js-sdk/browser'; import type { FC } from 'react'; import { useEffect, useMemo, useState } from 'react'; import { BrowserRouter, Route, Routes } from 'react-router-dom'; -import type { Settings } from '../src'; import { ShlinkWebComponent } from '../src'; +import type { Settings } from '../src/settings'; import type { SemVer } from '../src/utils/helpers/version'; import { ServerInfoForm } from './server-info/ServerInfoForm'; import type { ServerInfo } from './server-info/useServerInfo'; diff --git a/docker-compose.yml b/docker-compose.yml index 4fd01eb9..4de0d67c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -3,7 +3,7 @@ version: '3' services: shlink_web_component: container_name: shlink_web_component - image: node:20.5-alpine + image: node:22.2-alpine command: /bin/sh -c "cd /shlink-web-component && npm i && npm run dev" volumes: - ./:/shlink-web-component diff --git a/package.json b/package.json index f8d34b3c..bdab00af 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "repository": "https://github.com/shlinkio/shlink-web-component", "license": "MIT", "type": "module", - "main": "./dist/index.umd.cjs", + "main": "./dist/index.cjs", "module": "./dist/index.js", "types": "./dist/index.d.ts", "exports": { @@ -19,6 +19,11 @@ "require": "./dist/api-contract.cjs", "types": "./dist/api-contract.d.ts" }, + "./settings": { + "import": "./dist/settings.js", + "require": "./dist/settings.cjs", + "types": "./dist/settings.d.ts" + }, "./package.json": "./package.json" }, "files": [ diff --git a/src/ShlinkWebComponent.tsx b/src/ShlinkWebComponent.tsx index 963cd88b..19e3b5fd 100644 --- a/src/ShlinkWebComponent.tsx +++ b/src/ShlinkWebComponent.tsx @@ -5,19 +5,19 @@ import { Fragment, useEffect, useMemo, useRef, useState } from 'react'; import { Provider as ReduxStoreProvider } from 'react-redux'; import { BrowserRouter, useInRouterContext } from 'react-router-dom'; import type { ShlinkApiClient } from './api-contract'; +import type { Settings } from './settings'; +import { SettingsProvider } from './settings'; import { FeaturesProvider, useFeatures } from './utils/features'; import type { SemVerOrLatest } from './utils/helpers/version'; import { RoutesPrefixProvider } from './utils/routesPrefix'; import type { TagColorsStorage } from './utils/services/TagColorsStorage'; -import type { Settings } from './utils/settings'; -import { SettingsProvider } from './utils/settings'; type ShlinkWebComponentProps = { serverVersion: SemVerOrLatest; // FIXME Consider making this optional and trying to resolve it if not set apiClient: ShlinkApiClient; tagColorsStorage?: TagColorsStorage; routesPrefix?: string; - settings?: Settings; + settings?: Exclude; createNotFound?: (nonPrefixedHomePath: string) => ReactNode; }; @@ -61,7 +61,7 @@ export const createShlinkWebComponent = ( return !theStore ? <> : ( - + diff --git a/src/index.ts b/src/index.ts index 993ef2a1..6e885cf2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -10,13 +10,4 @@ export const ShlinkWebComponent = createShlinkWebComponent(bottle); export type ShlinkWebComponentType = typeof ShlinkWebComponent; -export type { - RealTimeUpdatesSettings, - ShortUrlCreationSettings, - ShortUrlsListSettings, - VisitsSettings, - TagsSettings, - Settings, -} from './utils/settings'; - export type { TagColorsStorage } from './utils/services/TagColorsStorage'; diff --git a/src/mercure/reducers/mercureInfo.ts b/src/mercure/reducers/mercureInfo.ts index 1a9b2031..99af7475 100644 --- a/src/mercure/reducers/mercureInfo.ts +++ b/src/mercure/reducers/mercureInfo.ts @@ -1,7 +1,7 @@ import { createSlice } from '@reduxjs/toolkit'; import type { ShlinkApiClient, ShlinkMercureInfo } from '../../api-contract'; +import type { Settings } from '../../settings'; import { createAsyncThunk } from '../../utils/redux'; -import type { Settings } from '../../utils/settings'; const REDUCER_PREFIX = 'shlink/mercure'; diff --git a/src/overview/Overview.tsx b/src/overview/Overview.tsx index 48a83bf3..932d20dd 100644 --- a/src/overview/Overview.tsx +++ b/src/overview/Overview.tsx @@ -8,6 +8,7 @@ import { componentFactory, useDependencies } from '../container/utils'; import type { MercureBoundProps } from '../mercure/helpers/boundToMercureHub'; import { boundToMercureHub } from '../mercure/helpers/boundToMercureHub'; import { Topics } from '../mercure/helpers/Topics'; +import { useSetting } from '../settings'; import type { CreateShortUrlProps } from '../short-urls/CreateShortUrl'; import type { ShortUrlsList as ShortUrlsListState } from '../short-urls/reducers/shortUrlsList'; import { ITEMS_IN_OVERVIEW_PAGE } from '../short-urls/reducers/shortUrlsList'; @@ -15,7 +16,6 @@ import type { ShortUrlsTableType } from '../short-urls/ShortUrlsTable'; import type { TagsList } from '../tags/reducers/tagsList'; import { prettify } from '../utils/helpers/numbers'; import { useRoutesPrefix } from '../utils/routesPrefix'; -import { useSetting } from '../utils/settings'; import type { VisitsOverview } from '../visits/reducers/visitsOverview'; import { HighlightCard } from './helpers/HighlightCard'; import { VisitsHighlightCard } from './helpers/VisitsHighlightCard'; diff --git a/src/settings/components/DateIntervalSelector.tsx b/src/settings/components/DateIntervalSelector.tsx new file mode 100644 index 00000000..a809a2c9 --- /dev/null +++ b/src/settings/components/DateIntervalSelector.tsx @@ -0,0 +1,46 @@ +import { DropdownBtn } from '@shlinkio/shlink-frontend-kit'; +import type { FC } from 'react'; +import { DropdownItem } from 'reactstrap'; +import type { VisitsSettings } from '..'; + +export type DateInterval = VisitsSettings['defaultInterval']; + +export interface DateIntervalSelectorProps { + active?: DateInterval; + allText: string; + onChange: (interval: DateInterval) => void; +} + +export const INTERVAL_TO_STRING_MAP: Record, string> = { + today: 'Today', + yesterday: 'Yesterday', + last7Days: 'Last 7 days', + last30Days: 'Last 30 days', + last90Days: 'Last 90 days', + last180Days: 'Last 180 days', + last365Days: 'Last 365 days', +}; + +const intervalToString = (interval: DateInterval | undefined, fallback: string): string => { + if (!interval || interval === 'all') { + return fallback; + } + + return INTERVAL_TO_STRING_MAP[interval]; +}; + +export const DateIntervalSelector: FC = ({ onChange, active, allText }) => ( + + onChange('all')}> + {allText} + + + {Object.entries(INTERVAL_TO_STRING_MAP).map( + ([interval, name]) => ( + onChange(interval as DateInterval)}> + {name} + + ), + )} + +); diff --git a/src/settings/components/FormText.tsx b/src/settings/components/FormText.tsx new file mode 100644 index 00000000..af9d279b --- /dev/null +++ b/src/settings/components/FormText.tsx @@ -0,0 +1,5 @@ +import type { FC, PropsWithChildren } from 'react'; + +export const FormText: FC = ({ children }) => ( + {children} +); diff --git a/src/settings/components/RealTimeUpdatesSettings.tsx b/src/settings/components/RealTimeUpdatesSettings.tsx new file mode 100644 index 00000000..9eae15ff --- /dev/null +++ b/src/settings/components/RealTimeUpdatesSettings.tsx @@ -0,0 +1,57 @@ +import { LabeledFormGroup, SimpleCard, ToggleSwitch } from '@shlinkio/shlink-frontend-kit'; +import { clsx } from 'clsx'; +import { useId } from 'react'; +import { FormGroup, Input } from 'reactstrap'; +import { useSetting } from '..'; +import { FormText } from './FormText'; + +export type RealTimeUpdatesProps = { + toggleRealTimeUpdates: (enabled: boolean) => void; + setRealTimeUpdatesInterval: (interval: number) => void; +}; + +export const RealTimeUpdatesSettings = ( + { toggleRealTimeUpdates, setRealTimeUpdatesInterval }: RealTimeUpdatesProps, +) => { + const { enabled, interval } = useSetting('realTimeUpdates', { enabled: true }); + const inputId = useId(); + + return ( + + + + Enable or disable real-time updates. + + Real-time updates are currently being {enabled ? 'processed' : 'ignored'}. + + + + + setRealTimeUpdatesInterval(Number(target.value))} + /> + {enabled && ( + + {interval ? ( + + Updates will be reflected in the UI + every {interval} minute{interval > 1 && 's'}. + + ) : 'Updates will be reflected in the UI as soon as they happen.'} + + )} + + + ); +}; diff --git a/src/settings/components/ShlinkWebSettings.tsx b/src/settings/components/ShlinkWebSettings.tsx new file mode 100644 index 00000000..817c871d --- /dev/null +++ b/src/settings/components/ShlinkWebSettings.tsx @@ -0,0 +1,100 @@ +import { mergeDeepRight } from '@shlinkio/data-manipulation'; +import { NavPillItem, NavPills } from '@shlinkio/shlink-frontend-kit'; +import type { FC, ReactNode } from 'react'; +import { useCallback } from 'react'; +import { Navigate, Route, Routes } from 'react-router-dom'; +import type { DeepPartial } from '../../utils/types'; +import { SettingsProvider } from '..'; +import type { RealTimeUpdatesSettings, Settings, ShortUrlsListSettings } from '../types'; +import { RealTimeUpdatesSettings as RealTimeUpdates } from './RealTimeUpdatesSettings'; +import { ShortUrlCreationSettings as ShortUrlCreation } from './ShortUrlCreationSettings'; +import { ShortUrlsListSettings as ShortUrlsList } from './ShortUrlsListSettings'; +import { TagsSettings as Tags } from './TagsSettings'; +import { UserInterfaceSettings } from './UserInterfaceSettings'; +import { VisitsSettings as Visits } from './VisitsSettings'; + +export type ShlinkWebSettingsProps = { + settings: Settings; + defaultShortUrlsListOrdering: NonNullable; + updateSettings: (settings: Settings) => void; +}; + +const SettingsSections: FC<{ items: ReactNode[] }> = ({ items }) => ( + <> + {items.map((child, index) =>
{child}
)} + +); + +export const ShlinkWebSettings: FC = ( + { settings, updateSettings, defaultShortUrlsListOrdering }, +) => { + const updatePartialSettings = useCallback( + (partialSettings: DeepPartial) => updateSettings(mergeDeepRight(settings, partialSettings)), + [settings, updateSettings], + ); + const toggleRealTimeUpdates = useCallback( + (enabled: boolean) => updatePartialSettings({ realTimeUpdates: { enabled } }), + [updatePartialSettings], + ); + const setRealTimeUpdatesInterval = useCallback( + (interval: number) => updatePartialSettings({ realTimeUpdates: { interval } as RealTimeUpdatesSettings }), + [updatePartialSettings], + ); + const updateSettingsProp = useCallback( + (prop: Prop, value: Settings[Prop]) => updatePartialSettings({ [prop]: value }), + [updatePartialSettings], + ); + + return ( + + + General + Short URLs + Other items + + + + updateSettingsProp('ui', v)} />, + , + ]} + /> + )} + /> + updateSettingsProp('shortUrlCreation', v)} />, + updateSettingsProp('shortUrlsList', v)} + />, + ]} + /> + )} + /> + updateSettingsProp('tags', v)} />, + updateSettingsProp('visits', v)} />, + ]} + /> + )} + /> + } /> + + + ); +}; diff --git a/src/settings/components/ShortUrlCreationSettings.tsx b/src/settings/components/ShortUrlCreationSettings.tsx new file mode 100644 index 00000000..4d82be45 --- /dev/null +++ b/src/settings/components/ShortUrlCreationSettings.tsx @@ -0,0 +1,74 @@ +import { DropdownBtn, LabeledFormGroup, SimpleCard, ToggleSwitch } from '@shlinkio/shlink-frontend-kit'; +import type { FC, ReactNode } from 'react'; +import { DropdownItem, FormGroup } from 'reactstrap'; +import type { ShortUrlCreationSettings as ShortUrlsSettings } from '..'; +import { useSetting } from '..'; +import { FormText } from './FormText'; + +type TagFilteringMode = NonNullable; + +interface ShortUrlCreationProps { + updateShortUrlCreationSettings: (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 including provided input. + : <>The list of suggested tags will contain those starting with provided input. +); + +export const ShortUrlCreationSettings: FC = ({ updateShortUrlCreationSettings }) => { + const shortUrlCreation = useSetting('shortUrlCreation', { validateUrls: false }); + const changeTagsFilteringMode = (tagFilteringMode: TagFilteringMode) => () => updateShortUrlCreationSettings( + { ...shortUrlCreation ?? { validateUrls: false }, tagFilteringMode }, + ); + + return ( + + + updateShortUrlCreationSettings({ ...shortUrlCreation, validateUrls })} + > + Request validation on long URLs when creating new short URLs.{' '} + This option is ignored by Shlink {'>='}4.0.0 + + The initial state of the Validate URL checkbox will + be {shortUrlCreation.validateUrls ? 'checked' : 'unchecked'}. + + + + + updateShortUrlCreationSettings({ ...shortUrlCreation, forwardQuery })} + > + Make all new short URLs forward their query params to the long URL. + + The initial state of the Forward query params on redirect checkbox will + be {shortUrlCreation.forwardQuery ?? true ? 'checked' : 'unchecked'}. + + + + + + + {tagFilteringModeText('startsWith')} + + + {tagFilteringModeText('includes')} + + + {tagFilteringModeHint(shortUrlCreation.tagFilteringMode)} + + + ); +}; diff --git a/src/settings/components/ShortUrlsListSettings.tsx b/src/settings/components/ShortUrlsListSettings.tsx new file mode 100644 index 00000000..4e1cd32a --- /dev/null +++ b/src/settings/components/ShortUrlsListSettings.tsx @@ -0,0 +1,35 @@ +import { LabeledFormGroup, OrderingDropdown, SimpleCard } from '@shlinkio/shlink-frontend-kit'; +import type { FC } from 'react'; +import type { ShortUrlsListSettings as ShortUrlsSettings } from '..'; +import { useSetting } from '..'; + +export type ShortUrlsListSettingsProps = { + updateShortUrlsListSettings: (settings: ShortUrlsSettings) => void; + defaultOrdering: NonNullable; +}; + +const SHORT_URLS_ORDERABLE_FIELDS = { + dateCreated: 'Created at', + shortCode: 'Short URL', + longUrl: 'Long URL', + title: 'Title', + visits: 'Visits', +}; + +export const ShortUrlsListSettings: FC = ( + { updateShortUrlsListSettings, defaultOrdering }, +) => { + const shortUrlsList = useSetting('shortUrlsList'); + + return ( + + + updateShortUrlsListSettings({ defaultOrdering: { field, dir } })} + /> + + + ); +}; diff --git a/src/settings/components/TagsSettings.tsx b/src/settings/components/TagsSettings.tsx new file mode 100644 index 00000000..b42fee14 --- /dev/null +++ b/src/settings/components/TagsSettings.tsx @@ -0,0 +1,30 @@ +import { LabeledFormGroup, OrderingDropdown, SimpleCard } from '@shlinkio/shlink-frontend-kit'; +import type { FC } from 'react'; +import type { TagsSettings as TagsSettingsOptions } from '..'; +import { useSetting } from '..'; + +export type TagsProps = { + updateTagsSettings: (settings: TagsSettingsOptions) => void; +}; + +const TAGS_ORDERABLE_FIELDS = { + tag: 'Tag', + shortUrls: 'Short URLs', + visits: 'Visits', +}; + +export const TagsSettings: FC = ({ updateTagsSettings }) => { + const tags = useSetting('tags', {}); + + return ( + + + updateTagsSettings({ ...tags, defaultOrdering: { field, dir } })} + /> + + + ); +}; diff --git a/src/settings/components/UserInterfaceSettings.tsx b/src/settings/components/UserInterfaceSettings.tsx new file mode 100644 index 00000000..b42dc762 --- /dev/null +++ b/src/settings/components/UserInterfaceSettings.tsx @@ -0,0 +1,35 @@ +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 { useSetting } from '..'; +import type { UiSettings } from '../types'; + +interface UserInterfaceProps { + updateUiSettings: (settings: UiSettings) => void; + + /* Test seam */ + _matchMedia?: typeof window.matchMedia; +} + +export const UserInterfaceSettings: FC = ({ updateUiSettings, _matchMedia }) => { + const ui = useSetting('ui'); + const currentTheme = useMemo(() => ui?.theme ?? getSystemPreferredTheme(_matchMedia), [ui?.theme, _matchMedia]); + + return ( + + + { + const theme: Theme = useDarkTheme ? 'dark' : 'light'; + updateUiSettings({ ...ui, theme }); + }} + > + Use dark theme. + + + ); +}; diff --git a/src/settings/components/VisitsSettings.tsx b/src/settings/components/VisitsSettings.tsx new file mode 100644 index 00000000..6a3eac95 --- /dev/null +++ b/src/settings/components/VisitsSettings.tsx @@ -0,0 +1,61 @@ +import { LabeledFormGroup, SimpleCard, ToggleSwitch } from '@shlinkio/shlink-frontend-kit'; +import type { FC } from 'react'; +import { useCallback } from 'react'; +import { FormGroup } from 'reactstrap'; +import type { DateInterval, VisitsSettings as VisitsSettingsConfig } from '..'; +import { useSetting } from '..'; +import { DateIntervalSelector } from './DateIntervalSelector'; +import { FormText } from './FormText'; + +export type VisitsProps = { + updateVisitsSettings: (settings: VisitsSettingsConfig) => void; +}; + +const currentDefaultInterval = (visitsSettings?: VisitsSettingsConfig): DateInterval => + visitsSettings?.defaultInterval ?? 'last30Days'; + +export const VisitsSettings: FC = ({ updateVisitsSettings }) => { + const visitsSettings = useSetting('visits'); + const updateSettings = useCallback( + ({ defaultInterval, ...rest }: Partial) => updateVisitsSettings( + { defaultInterval: defaultInterval ?? currentDefaultInterval(visitsSettings), ...rest }, + ), + [updateVisitsSettings, visitsSettings], + ); + + return ( + + + updateSettings({ excludeBots })} + > + Exclude bots wherever possible (this option‘s effect might depend on Shlink server‘s version). + + The visits coming from potential bots will + be {visitsSettings?.excludeBots ? 'excluded' : 'included'}. + + + + + updateSettings({ loadPrevInterval })} + > + Compare visits with previous period. + + When loading visits, previous period {visitsSettings?.loadPrevInterval ? 'will' : 'won\'t'} be + loaded by default. + + + + + updateSettings({ defaultInterval })} + /> + + + ); +}; diff --git a/src/settings/index.ts b/src/settings/index.ts new file mode 100644 index 00000000..9d881547 --- /dev/null +++ b/src/settings/index.ts @@ -0,0 +1,42 @@ +import { createContext, useContext } from 'react'; +import type { Settings } from './types'; + +const defaultSettings: Settings = { + realTimeUpdates: { + enabled: true, + }, + shortUrlCreation: { + validateUrls: false, + }, + visits: { + defaultInterval: 'last30Days', + }, + shortUrlsList: { + defaultOrdering: { + field: 'dateCreated', + dir: 'DESC', + }, + }, +}; + +const SettingsContext = createContext(defaultSettings); + +export const { Provider: SettingsProvider } = SettingsContext; + +export const useSettings = (): Settings => useContext(SettingsContext) ?? defaultSettings; + +export function useSetting(settingName: Setting): Settings[Setting]; +export function useSetting>( + settingName: Setting, + fallbackValue: Default, +): NonNullable; +export function useSetting( + settingName: Setting, + fallbackValue?: Default, +) { + const settings = useSettings(); + return settings[settingName] ?? fallbackValue; +} + +export { ShlinkWebSettings } from './components/ShlinkWebSettings'; +export * from './types'; diff --git a/src/settings/types.ts b/src/settings/types.ts new file mode 100644 index 00000000..f33329fe --- /dev/null +++ b/src/settings/types.ts @@ -0,0 +1,50 @@ +import type { Order, Theme } from '@shlinkio/shlink-frontend-kit'; + +export type DateInterval = 'today' | 'yesterday' | 'last7Days' | 'last30Days' | 'last90Days' | 'last180Days' | 'last365Days' | 'all'; + +/** + * Important! When adding new props in the main Settings interface or any of the nested props, they have to be set as + * optional, as old instances of the app will load partial objects from local storage until it is saved again. + */ + +export type RealTimeUpdatesSettings = { + enabled: boolean; + interval?: number; +}; + +export type TagFilteringMode = 'startsWith' | 'includes'; + +export type ShortUrlCreationSettings = { + tagFilteringMode?: TagFilteringMode; + forwardQuery?: boolean; + + /** @deprecated Shlink 4.0.0 no longer validates URLs */ + validateUrls: boolean; +}; + +export type VisitsSettings = { + defaultInterval: DateInterval; + excludeBots?: boolean; + loadPrevInterval?: boolean; +}; + +export type TagsSettings = { + defaultOrdering?: Order<'tag' | 'shortUrls' | 'visits'>; +}; + +export type ShortUrlsListSettings = { + defaultOrdering?: Order<'dateCreated' | 'shortCode' | 'longUrl' | 'title' | 'visits'>; +}; + +export type UiSettings = { + theme: Theme; +}; + +export type Settings = { + realTimeUpdates?: RealTimeUpdatesSettings; + shortUrlCreation?: ShortUrlCreationSettings; + shortUrlsList?: ShortUrlsListSettings; + visits?: VisitsSettings; + tags?: TagsSettings; + ui?: UiSettings; +}; diff --git a/src/short-urls/CreateShortUrl.tsx b/src/short-urls/CreateShortUrl.tsx index 82516ff8..3c8b0ecf 100644 --- a/src/short-urls/CreateShortUrl.tsx +++ b/src/short-urls/CreateShortUrl.tsx @@ -3,8 +3,8 @@ import { useMemo } from 'react'; import type { ShlinkCreateShortUrlData } from '../api-contract'; import type { FCWithDeps } from '../container/utils'; import { componentFactory, useDependencies } from '../container/utils'; -import type { ShortUrlCreationSettings } from '../utils/settings'; -import { useSetting } from '../utils/settings'; +import type { ShortUrlCreationSettings } from '../settings'; +import { useSetting } from '../settings'; import type { CreateShortUrlResultProps } from './helpers/CreateShortUrlResult'; import type { ShortUrlCreation } from './reducers/shortUrlCreation'; import type { ShortUrlFormProps } from './ShortUrlForm'; diff --git a/src/short-urls/EditShortUrl.tsx b/src/short-urls/EditShortUrl.tsx index 9e2a302c..e4261c36 100644 --- a/src/short-urls/EditShortUrl.tsx +++ b/src/short-urls/EditShortUrl.tsx @@ -7,8 +7,8 @@ import type { ShlinkEditShortUrlData } from '../api-contract'; import { ShlinkApiError } from '../common/ShlinkApiError'; import type { FCWithDeps } from '../container/utils'; import { componentFactory, useDependencies } from '../container/utils'; +import { useSetting } from '../settings'; import { GoBackButton } from '../utils/components/GoBackButton'; -import { useSetting } from '../utils/settings'; import type { ShortUrlIdentifier } from './data'; import { shortUrlDataFromShortUrl } from './helpers'; import { useShortUrlIdentifier } from './helpers/hooks'; diff --git a/src/short-urls/ShortUrlsFilteringBar.tsx b/src/short-urls/ShortUrlsFilteringBar.tsx index b7e7982c..7a7999f8 100644 --- a/src/short-urls/ShortUrlsFilteringBar.tsx +++ b/src/short-urls/ShortUrlsFilteringBar.tsx @@ -8,6 +8,7 @@ import { useCallback, useState } from 'react'; import { Button, InputGroup, Row, UncontrolledTooltip } from 'reactstrap'; import type { FCWithDeps } from '../container/utils'; import { componentFactory, useDependencies } from '../container/utils'; +import { useSetting } from '../settings'; import type { TagsSelectorProps } from '../tags/helpers/TagsSelector'; import type { TagsList } from '../tags/reducers/tagsList'; import { DateRangeSelector } from '../utils/dates/DateRangeSelector'; @@ -15,7 +16,6 @@ import { formatIsoDate } from '../utils/dates/helpers/date'; import type { DateInterval, DateRange } from '../utils/dates/helpers/dateIntervals'; import { datesToDateRange } from '../utils/dates/helpers/dateIntervals'; import { useFeature } from '../utils/features'; -import { useSetting } from '../utils/settings'; import type { ShortUrlsOrder, ShortUrlsOrderableFields } from './data'; import { SHORT_URLS_ORDERABLE_FIELDS } from './data'; import type { ExportShortUrlsBtnProps } from './helpers/ExportShortUrlsBtn'; diff --git a/src/short-urls/ShortUrlsList.tsx b/src/short-urls/ShortUrlsList.tsx index e643cc0e..9e9eea95 100644 --- a/src/short-urls/ShortUrlsList.tsx +++ b/src/short-urls/ShortUrlsList.tsx @@ -11,8 +11,8 @@ import { componentFactory, useDependencies } from '../container/utils'; import type { MercureBoundProps } from '../mercure/helpers/boundToMercureHub'; import { boundToMercureHub } from '../mercure/helpers/boundToMercureHub'; import { Topics } from '../mercure/helpers/Topics'; +import { useSettings } from '../settings'; import { useFeature } from '../utils/features'; -import { useSettings } from '../utils/settings'; import { TableOrderIcon } from '../utils/table/TableOrderIcon'; import { VisitsComparisonCollector } from '../visits/visits-comparison/VisitsComparisonCollector'; import { diff --git a/src/short-urls/helpers/ShortUrlsRow.tsx b/src/short-urls/helpers/ShortUrlsRow.tsx index 0e5112dd..ab9ca09d 100644 --- a/src/short-urls/helpers/ShortUrlsRow.tsx +++ b/src/short-urls/helpers/ShortUrlsRow.tsx @@ -4,10 +4,10 @@ import { ExternalLink } from 'react-external-link'; import type { ShlinkShortUrl } from '../../api-contract'; import type { FCWithDeps } from '../../container/utils'; import { componentFactory, useDependencies } from '../../container/utils'; +import { useSetting } from '../../settings'; import { CopyToClipboardIcon } from '../../utils/components/CopyToClipboardIcon'; import { Time } from '../../utils/dates/Time'; import type { ColorGenerator } from '../../utils/services/ColorGenerator'; -import { useSetting } from '../../utils/settings'; import { useShortUrlsQuery } from './hooks'; import type { ShortUrlsRowMenuType } from './ShortUrlsRowMenu'; import { ShortUrlStatus } from './ShortUrlStatus'; diff --git a/src/short-urls/helpers/index.ts b/src/short-urls/helpers/index.ts index ef1983e6..80fea14b 100644 --- a/src/short-urls/helpers/index.ts +++ b/src/short-urls/helpers/index.ts @@ -1,6 +1,6 @@ import type { ShlinkCreateShortUrlData, ShlinkShortUrl } from '../../api-contract'; +import type { ShortUrlCreationSettings } from '../../settings'; import type { OptionalString } from '../../utils/helpers'; -import type { ShortUrlCreationSettings } from '../../utils/settings'; import { DEFAULT_DOMAIN } from '../../visits/reducers/domainVisits'; import type { ShortUrlIdentifier } from '../data'; diff --git a/src/tags/TagsList.tsx b/src/tags/TagsList.tsx index 1a8d9d03..fd2f25e5 100644 --- a/src/tags/TagsList.tsx +++ b/src/tags/TagsList.tsx @@ -15,7 +15,7 @@ import { componentFactory, useDependencies } from '../container/utils'; import type { MercureBoundProps } from '../mercure/helpers/boundToMercureHub'; import { boundToMercureHub } from '../mercure/helpers/boundToMercureHub'; import { Topics } from '../mercure/helpers/Topics'; -import { useSettings } from '../utils/settings'; +import { useSettings } from '../settings'; import { VisitsComparisonCollector } from '../visits/visits-comparison/VisitsComparisonCollector'; import { useVisitsComparison, VisitsComparisonProvider } from '../visits/visits-comparison/VisitsComparisonContext'; import type { SimplifiedTag } from './data'; diff --git a/src/tags/helpers/TagsSelector.tsx b/src/tags/helpers/TagsSelector.tsx index 16d447c1..99bca2ca 100644 --- a/src/tags/helpers/TagsSelector.tsx +++ b/src/tags/helpers/TagsSelector.tsx @@ -5,8 +5,8 @@ import type { OptionRendererProps, ReactTagsAPI, TagRendererProps, TagSuggestion import { ReactTags } from 'react-tag-autocomplete'; import type { FCWithDeps } from '../../container/utils'; import { componentFactory, useDependencies } from '../../container/utils'; +import { useSetting } from '../../settings'; import type { ColorGenerator } from '../../utils/services/ColorGenerator'; -import { useSetting } from '../../utils/settings'; import { normalizeTag } from './index'; import { Tag } from './Tag'; import { TagBullet } from './TagBullet'; diff --git a/src/utils/dates/helpers/dateIntervals.ts b/src/utils/dates/helpers/dateIntervals.ts index 0f0ad8c6..b023c3c9 100644 --- a/src/utils/dates/helpers/dateIntervals.ts +++ b/src/utils/dates/helpers/dateIntervals.ts @@ -1,7 +1,10 @@ import { differenceInDays, endOfDay, startOfDay, subDays } from 'date-fns'; +import type { DateInterval as SettingsDateInterval } from '../../../settings'; import type { DateOrString } from './date'; import { formatInternational, isBeforeOrEqual, now, parseISO } from './date'; +export type DateInterval = SettingsDateInterval; + export type DateRange = { startDate?: Date | null; endDate?: Date | null; @@ -18,7 +21,7 @@ export type StrictDateRange = { export const ALL = 'all'; -const INTERVAL_TO_STRING_MAP = { +const INTERVAL_TO_STRING_MAP: Record = { today: 'Today', yesterday: 'Yesterday', last7Days: 'Last 7 days', @@ -29,8 +32,6 @@ const INTERVAL_TO_STRING_MAP = { [ALL]: undefined, } as const; -export type DateInterval = keyof typeof INTERVAL_TO_STRING_MAP; - const INTERVALS = Object.keys(INTERVAL_TO_STRING_MAP) as DateInterval[]; export const dateRangeIsEmpty = (dateRange?: DateRange): boolean => !dateRange diff --git a/src/utils/helpers/index.ts b/src/utils/helpers/index.ts index 4ff3e830..183dc2e0 100644 --- a/src/utils/helpers/index.ts +++ b/src/utils/helpers/index.ts @@ -1,6 +1,5 @@ import { range } from '@shlinkio/data-manipulation'; import type { SyntheticEvent } from 'react'; -import type { Mandatory } from '../types'; export type OptionalString = string | null | undefined; @@ -14,7 +13,7 @@ export const rangeOf = (size: number, mappingFn: (value: number) => T, startA export type Empty = null | undefined | '' | never[]; -const isEmpty = (value: Mandatory): boolean => ( +const isEmpty = (value: NonNullable): boolean => ( (Array.isArray(value) && value.length === 0) || (typeof value === 'string' && value === '') || (typeof value === 'object' && Object.keys(value).length === 0) diff --git a/src/utils/settings.ts b/src/utils/settings.ts deleted file mode 100644 index c5ec33fc..00000000 --- a/src/utils/settings.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { createContext, useContext } from 'react'; -import type { ShortUrlsOrder } from '../short-urls/data'; -import type { TagsOrder } from '../tags/data/TagsListChildrenProps'; -import type { DateInterval } from './dates/helpers/dateIntervals'; - -export const DEFAULT_SHORT_URLS_ORDERING: ShortUrlsOrder = { - field: 'dateCreated', - dir: 'DESC', -}; - -/** - * Important! When adding new props in the main Settings interface or any of the nested props, they have to be set as - * optional, as old instances of the app will load partial objects from local storage until it is saved again. - */ - -export type RealTimeUpdatesSettings = { - enabled: boolean; - interval?: number; -}; - -export type TagFilteringMode = 'startsWith' | 'includes'; - -export type ShortUrlCreationSettings = { - tagFilteringMode?: TagFilteringMode; - forwardQuery?: boolean; - - /** @deprecated Shlink 4.0.0 no longer validates URLs */ - validateUrls: boolean; -}; - -export type VisitsSettings = { - defaultInterval: DateInterval; - excludeBots?: boolean; - loadPrevInterval?: boolean; -}; - -export type TagsSettings = { - defaultOrdering?: TagsOrder; -}; - -export type ShortUrlsListSettings = { - defaultOrdering?: ShortUrlsOrder; -}; - -export type Settings = { - realTimeUpdates?: RealTimeUpdatesSettings; - shortUrlCreation?: ShortUrlCreationSettings; - shortUrlsList?: ShortUrlsListSettings; - visits?: VisitsSettings; - tags?: TagsSettings; -}; - -const defaultSettings: Settings = { - realTimeUpdates: { - enabled: true, - }, - shortUrlCreation: { - validateUrls: false, - }, - visits: { - defaultInterval: 'last30Days', - }, - shortUrlsList: { - defaultOrdering: DEFAULT_SHORT_URLS_ORDERING, - }, -}; - -const SettingsContext = createContext(defaultSettings); - -export const SettingsProvider = SettingsContext.Provider; - -export const useSettings = (): Settings => useContext(SettingsContext) ?? defaultSettings; - -export const useSetting = (settingName: T): Settings[T] => { - const settings = useSettings(); - return settings[settingName]; -}; diff --git a/src/utils/types/index.ts b/src/utils/types/index.ts index 81cc8aa1..f8485669 100644 --- a/src/utils/types/index.ts +++ b/src/utils/types/index.ts @@ -11,5 +11,3 @@ export type DeepPartial = { ? DeepPartial : T[P]; }; - -export type Mandatory = Exclude; diff --git a/src/visits/VisitsStats.tsx b/src/visits/VisitsStats.tsx index eaebcf76..79f395bc 100644 --- a/src/visits/VisitsStats.tsx +++ b/src/visits/VisitsStats.tsx @@ -14,12 +14,12 @@ import type { FC, PropsWithChildren } from 'react'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { Navigate, Route, Routes, useLocation } from 'react-router-dom'; import { Button, Row } from 'reactstrap'; +import { useSetting } from '../settings'; import { ExportBtn } from '../utils/components/ExportBtn'; import { DateRangeSelector } from '../utils/dates/DateRangeSelector'; import type { DateInterval, DateRange } from '../utils/dates/helpers/dateIntervals'; import { toDateRange } from '../utils/dates/helpers/dateIntervals'; import { prettify } from '../utils/helpers/numbers'; -import { useSetting } from '../utils/settings'; import { DoughnutChartCard } from './charts/DoughnutChartCard'; import { LineChartCard } from './charts/LineChartCard'; import { SortableBarChartCard } from './charts/SortableBarChartCard'; diff --git a/src/visits/charts/LineChartCard.tsx b/src/visits/charts/LineChartCard.tsx index ba7dc384..5c35efe4 100644 --- a/src/visits/charts/LineChartCard.tsx +++ b/src/visits/charts/LineChartCard.tsx @@ -30,7 +30,7 @@ import { formatInternational } from '../../utils/dates/helpers/date'; import { rangeOf } from '../../utils/helpers'; import { useMaxResolution } from '../../utils/helpers/hooks'; import { prettify } from '../../utils/helpers/numbers'; -import type { Mandatory, MediaMatcher } from '../../utils/types'; +import type { MediaMatcher } from '../../utils/types'; import type { NormalizedVisit, Stats } from '../types'; import { CHART_TOOLTIP_COMMON_PROPS, PREV_COLOR } from './constants'; import { LineChartLegend } from './LineChartLegend'; @@ -153,7 +153,7 @@ export const visitsListColor = (v: VisitsList) => { return v.color; } - const typeColorMap: Record, string> = { + const typeColorMap: Record, string> = { main: MAIN_COLOR, highlighted: HIGHLIGHTED_COLOR, previous: PREV_COLOR, diff --git a/src/visits/visits-comparison/VisitsComparison.tsx b/src/visits/visits-comparison/VisitsComparison.tsx index 7f2fa87e..bf75dbb4 100644 --- a/src/visits/visits-comparison/VisitsComparison.tsx +++ b/src/visits/visits-comparison/VisitsComparison.tsx @@ -1,11 +1,11 @@ import { SimpleCard } from '@shlinkio/shlink-frontend-kit'; import type { FC, ReactNode } from 'react'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { useSetting } from '../../settings'; import { GoBackButton } from '../../utils/components/GoBackButton'; import { DateRangeSelector } from '../../utils/dates/DateRangeSelector'; import type { DateInterval, DateRange } from '../../utils/dates/helpers/dateIntervals'; import { toDateRange } from '../../utils/dates/helpers/dateIntervals'; -import { useSetting } from '../../utils/settings'; import { chartColorForIndex } from '../charts/constants'; import { LineChartCard, type VisitsList } from '../charts/LineChartCard'; import { useVisitsQuery } from '../helpers/hooks'; diff --git a/test/mercure/reducers/mercureInfo.test.ts b/test/mercure/reducers/mercureInfo.test.ts index 630b891d..be55337a 100644 --- a/test/mercure/reducers/mercureInfo.test.ts +++ b/test/mercure/reducers/mercureInfo.test.ts @@ -1,7 +1,7 @@ import { fromPartial } from '@total-typescript/shoehorn'; -import type { Settings } from '../../../src'; import type { ShlinkApiClient } from '../../../src/api-contract'; import { mercureInfoReducerCreator } from '../../../src/mercure/reducers/mercureInfo'; +import type { Settings } from '../../../src/settings'; describe('mercureInfoReducer', () => { const mercureInfo = { diff --git a/test/overview/Overview.test.tsx b/test/overview/Overview.test.tsx index 78d26bf2..e50e4334 100644 --- a/test/overview/Overview.test.tsx +++ b/test/overview/Overview.test.tsx @@ -3,9 +3,9 @@ import { fromPartial } from '@total-typescript/shoehorn'; import { MemoryRouter } from 'react-router-dom'; import type { MercureInfo } from '../../src/mercure/reducers/mercureInfo'; import { OverviewFactory } from '../../src/overview/Overview'; +import { SettingsProvider } from '../../src/settings'; import { prettify } from '../../src/utils/helpers/numbers'; import { RoutesPrefixProvider } from '../../src/utils/routesPrefix'; -import { SettingsProvider } from '../../src/utils/settings'; import { checkAccessibility } from '../__helpers__/accessibility'; import { renderWithEvents } from '../__helpers__/setUpTest'; diff --git a/test/settings/components/RealTimeUpdatesSettings.test.tsx b/test/settings/components/RealTimeUpdatesSettings.test.tsx new file mode 100644 index 00000000..c2b64f0e --- /dev/null +++ b/test/settings/components/RealTimeUpdatesSettings.test.tsx @@ -0,0 +1,88 @@ +import { screen } from '@testing-library/react'; +import { fromPartial } from '@total-typescript/shoehorn'; +import type { RealTimeUpdatesSettings as RealTimeUpdatesSettingsOptions } from '../../../src/settings'; +import { SettingsProvider } from '../../../src/settings'; +import { RealTimeUpdatesSettings } from '../../../src/settings/components/RealTimeUpdatesSettings'; +import { checkAccessibility } from '../../__helpers__/accessibility'; +import { renderWithEvents } from '../../__helpers__/setUpTest'; + +describe('', () => { + const toggleRealTimeUpdates = vi.fn(); + const setRealTimeUpdatesInterval = vi.fn(); + const setUp = (realTimeUpdates: Partial = {}) => renderWithEvents( + + + , + ); + + 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(); + }); +}); diff --git a/test/settings/components/ShlinkWebSettings.test.tsx b/test/settings/components/ShlinkWebSettings.test.tsx new file mode 100644 index 00000000..a515bac0 --- /dev/null +++ b/test/settings/components/ShlinkWebSettings.test.tsx @@ -0,0 +1,47 @@ +import { render, screen } from '@testing-library/react'; +import { createMemoryHistory } from 'history'; +import { Router } from 'react-router-dom'; +import { ShlinkWebSettings } from '../../../src/settings'; +import { checkAccessibility } from '../../__helpers__/accessibility'; + +describe('', () => { + const setUp = (activeRoute = '/') => { + const history = createMemoryHistory(); + history.push(activeRoute); + return render( + + + , + ); + }; + + it('passes a11y checks', () => checkAccessibility(setUp())); + + it.each([ + ['/general', { + visibleComps: ['User interface', 'Real-time updates'], + hiddenComps: ['Short URLs form', 'Short URLs list', 'Tags', 'Visits'], + }], + ['/short-urls', { + visibleComps: ['Short URLs form', 'Short URLs list'], + hiddenComps: ['User interface', 'Real-time updates', 'Tags', 'Visits'], + }], + ['/other-items', { + visibleComps: ['Tags', 'Visits'], + hiddenComps: ['User interface', 'Real-time updates', 'Short URLs form', 'Short URLs list'], + }], + ])('renders expected sections based on route', (activeRoute, { visibleComps, hiddenComps }) => { + setUp(activeRoute); + + visibleComps.forEach((name) => expect(screen.getByRole('heading', { name })).toBeInTheDocument()); + hiddenComps.forEach((name) => expect(screen.queryByRole('heading', { name })).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'); + }); +}); diff --git a/test/settings/components/ShortUrlCreationSettings.test.tsx b/test/settings/components/ShortUrlCreationSettings.test.tsx new file mode 100644 index 00000000..df074c38 --- /dev/null +++ b/test/settings/components/ShortUrlCreationSettings.test.tsx @@ -0,0 +1,115 @@ +import { screen } from '@testing-library/react'; +import { fromPartial } from '@total-typescript/shoehorn'; +import type { ShortUrlCreationSettings as ShortUrlsSettings } from '../../../src/settings'; +import { SettingsProvider } from '../../../src/settings'; +import { ShortUrlCreationSettings } from '../../../src/settings/components/ShortUrlCreationSettings'; +import { checkAccessibility } from '../../__helpers__/accessibility'; +import { renderWithEvents } from '../../__helpers__/setUpTest'; + +describe('', () => { + const setShortUrlCreationSettings = vi.fn(); + const setUp = (shortUrlCreation?: ShortUrlsSettings) => renderWithEvents( + + + , + ); + + 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' }, + )); + }); +}); diff --git a/test/settings/components/ShortUrlsListSettings.test.tsx b/test/settings/components/ShortUrlsListSettings.test.tsx new file mode 100644 index 00000000..17e2c1b7 --- /dev/null +++ b/test/settings/components/ShortUrlsListSettings.test.tsx @@ -0,0 +1,46 @@ +import { screen } from '@testing-library/react'; +import { fromPartial } from '@total-typescript/shoehorn'; +import type { ShortUrlsListSettings as ShortUrlsSettings } from '../../../src/settings'; +import { SettingsProvider } from '../../../src/settings'; +import { ShortUrlsListSettings } from '../../../src/settings/components/ShortUrlsListSettings'; +import { checkAccessibility } from '../../__helpers__/accessibility'; +import { renderWithEvents } from '../../__helpers__/setUpTest'; + +describe('', () => { + const setSettings = vi.fn(); + const setUp = (shortUrlsList?: ShortUrlsSettings) => renderWithEvents( + + + , + ); + + it('passes a11y checks', () => checkAccessibility(setUp())); + + it.each([ + [undefined, 'Order by: Created at - DESC'], + [fromPartial({}), 'Order by: Created at - DESC'], + [fromPartial({ defaultOrdering: {} }), 'Order by...'], + [fromPartial({ defaultOrdering: { field: 'longUrl', dir: 'DESC' } }), 'Order by: Long URL - DESC'], + [fromPartial({ 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 } }); + }); +}); diff --git a/test/settings/components/TagsSettings.test.tsx b/test/settings/components/TagsSettings.test.tsx new file mode 100644 index 00000000..63c71e5f --- /dev/null +++ b/test/settings/components/TagsSettings.test.tsx @@ -0,0 +1,49 @@ +import { screen } from '@testing-library/react'; +import { fromPartial } from '@total-typescript/shoehorn'; +import type { TagsSettings as TagsSettingsOptions } from '../../../src/settings'; +import { SettingsProvider } from '../../../src/settings'; +import { TagsSettings } from '../../../src/settings/components/TagsSettings'; +import { checkAccessibility } from '../../__helpers__/accessibility'; +import { renderWithEvents } from '../../__helpers__/setUpTest'; + +describe('', () => { + const setTagsSettings = vi.fn(); + const setUp = (tags?: TagsSettingsOptions) => renderWithEvents( + + + , + ); + + 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 const }, 'Order by: Tag - DESC'], + [{ defaultOrdering: { field: 'visits', dir: 'ASC' } as const }, '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 } }); + }); +}); diff --git a/test/settings/components/UserInterfaceSettings.test.tsx b/test/settings/components/UserInterfaceSettings.test.tsx new file mode 100644 index 00000000..3322e11c --- /dev/null +++ b/test/settings/components/UserInterfaceSettings.test.tsx @@ -0,0 +1,58 @@ +import { screen } from '@testing-library/react'; +import { fromPartial } from '@total-typescript/shoehorn'; +import type { UiSettings } from '../../../src/settings'; +import { SettingsProvider } from '../../../src/settings'; +import { UserInterfaceSettings } from '../../../src/settings/components/UserInterfaceSettings'; +import { checkAccessibility } from '../../__helpers__/accessibility'; +import { renderWithEvents } from '../../__helpers__/setUpTest'; + +describe('', () => { + const setUiSettings = vi.fn(); + const setUp = (ui?: UiSettings, defaultDarkTheme = false) => renderWithEvents( + + + , + ); + + 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 }); + }); +}); diff --git a/test/settings/components/VisitsSettings.test.tsx b/test/settings/components/VisitsSettings.test.tsx new file mode 100644 index 00000000..f7129d3c --- /dev/null +++ b/test/settings/components/VisitsSettings.test.tsx @@ -0,0 +1,132 @@ +import { screen } from '@testing-library/react'; +import { fromPartial } from '@total-typescript/shoehorn'; +import type { Settings } from '../../../src/settings'; +import { SettingsProvider } from '../../../src/settings'; +import { VisitsSettings } from '../../../src/settings/components/VisitsSettings'; +import { checkAccessibility } from '../../__helpers__/accessibility'; +import { renderWithEvents } from '../../__helpers__/setUpTest'; + +describe('', () => { + const setVisitsSettings = vi.fn(); + const setUp = (settings: Partial = {}) => renderWithEvents( + + + , + ); + + 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({}), 'Last 30 days'], + [fromPartial({ visits: {} }), 'Last 30 days'], + [ + fromPartial({ + visits: { + defaultInterval: 'last7Days', + }, + }), + 'Last 7 days', + ], + [ + fromPartial({ + 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({}), + /The visits coming from potential bots will be included.$/, + /The visits coming from potential bots will be excluded.$/, + ], + [ + fromPartial({ visits: { excludeBots: false } }), + /The visits coming from potential bots will be included.$/, + /The visits coming from potential bots will be excluded.$/, + ], + [ + fromPartial({ 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({}), + /When loading visits, previous period won't be loaded by default.$/, + /When loading visits, previous period will be loaded by default.$/, + ], + [ + fromPartial({ 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({ 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 })); + }); +}); diff --git a/test/settings/components/__snapshots__/UserInterfaceSettings.test.tsx.snap b/test/settings/components/__snapshots__/UserInterfaceSettings.test.tsx.snap new file mode 100644 index 00000000..2c5d0e53 --- /dev/null +++ b/test/settings/components/__snapshots__/UserInterfaceSettings.test.tsx.snap @@ -0,0 +1,55 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[` > shows different icons based on theme 1`] = ` + +`; + +exports[` > shows different icons based on theme 2`] = ` + +`; + +exports[` > shows different icons based on theme 3`] = ` + +`; diff --git a/test/short-urls/CreateShortUrl.test.tsx b/test/short-urls/CreateShortUrl.test.tsx index 222fceec..a0c3fbf6 100644 --- a/test/short-urls/CreateShortUrl.test.tsx +++ b/test/short-urls/CreateShortUrl.test.tsx @@ -1,8 +1,8 @@ import { render, screen } from '@testing-library/react'; import { fromPartial } from '@total-typescript/shoehorn'; +import { SettingsProvider } from '../../src/settings'; import { CreateShortUrlFactory } from '../../src/short-urls/CreateShortUrl'; import type { ShortUrlCreation } from '../../src/short-urls/reducers/shortUrlCreation'; -import { SettingsProvider } from '../../src/utils/settings'; import { checkAccessibility } from '../__helpers__/accessibility'; describe('', () => { diff --git a/test/short-urls/EditShortUrl.test.tsx b/test/short-urls/EditShortUrl.test.tsx index 1febde59..727cb0a9 100644 --- a/test/short-urls/EditShortUrl.test.tsx +++ b/test/short-urls/EditShortUrl.test.tsx @@ -1,11 +1,11 @@ import type { ShlinkShortUrl } from '@shlinkio/shlink-js-sdk/api-contract'; import { render, screen } from '@testing-library/react'; import { fromPartial } from '@total-typescript/shoehorn'; +import { SettingsProvider } from '../../src/settings'; import type { ShortUrlIdentifier } from '../../src/short-urls/data'; import { EditShortUrlFactory } from '../../src/short-urls/EditShortUrl'; import type { ShortUrlEdition } from '../../src/short-urls/reducers/shortUrlEdition'; import type { ShortUrlsDetails } from '../../src/short-urls/reducers/shortUrlsDetails'; -import { SettingsProvider } from '../../src/utils/settings'; import { checkAccessibility } from '../__helpers__/accessibility'; import { MemoryRouterWithParams } from '../__helpers__/MemoryRouterWithParams'; diff --git a/test/short-urls/ShortUrlsFilteringBar.test.tsx b/test/short-urls/ShortUrlsFilteringBar.test.tsx index fbaa834c..d16d242e 100644 --- a/test/short-urls/ShortUrlsFilteringBar.test.tsx +++ b/test/short-urls/ShortUrlsFilteringBar.test.tsx @@ -5,10 +5,10 @@ import { formatISO, parseISO } from 'date-fns'; import type { MemoryHistory } from 'history'; import { createMemoryHistory } from 'history'; import { Router } from 'react-router-dom'; +import { SettingsProvider } from '../../src/settings'; import { ShortUrlsFilteringBarFactory } from '../../src/short-urls/ShortUrlsFilteringBar'; import { FeaturesProvider } from '../../src/utils/features'; import { RoutesPrefixProvider } from '../../src/utils/routesPrefix'; -import { SettingsProvider } from '../../src/utils/settings'; import { checkAccessibility } from '../__helpers__/accessibility'; import { renderWithEvents } from '../__helpers__/setUpTest'; diff --git a/test/short-urls/ShortUrlsList.test.tsx b/test/short-urls/ShortUrlsList.test.tsx index 2e5441bc..a4248d29 100644 --- a/test/short-urls/ShortUrlsList.test.tsx +++ b/test/short-urls/ShortUrlsList.test.tsx @@ -3,14 +3,14 @@ import { fromPartial } from '@total-typescript/shoehorn'; import type { MemoryHistory } from 'history'; import { createMemoryHistory } from 'history'; import { Router } from 'react-router-dom'; -import type { Settings } from '../../src'; import type { MercureBoundProps } from '../../src/mercure/helpers/boundToMercureHub'; +import type { Settings } from '../../src/settings'; +import { SettingsProvider } from '../../src/settings'; import type { ShortUrlsOrder } from '../../src/short-urls/data'; import type { ShortUrlsList as ShortUrlsListModel } from '../../src/short-urls/reducers/shortUrlsList'; import { ShortUrlsListFactory } from '../../src/short-urls/ShortUrlsList'; import type { ShortUrlsTableType } from '../../src/short-urls/ShortUrlsTable'; import { FeaturesProvider } from '../../src/utils/features'; -import { SettingsProvider } from '../../src/utils/settings'; import { checkAccessibility } from '../__helpers__/accessibility'; import { renderWithEvents } from '../__helpers__/setUpTest'; diff --git a/test/short-urls/helpers/ShortUrlsRow.test.tsx b/test/short-urls/helpers/ShortUrlsRow.test.tsx index 6d251fb6..c4dc71db 100644 --- a/test/short-urls/helpers/ShortUrlsRow.test.tsx +++ b/test/short-urls/helpers/ShortUrlsRow.test.tsx @@ -3,11 +3,11 @@ import { screen } from '@testing-library/react'; import { fromPartial } from '@total-typescript/shoehorn'; import { addDays, formatISO, subDays } from 'date-fns'; import { MemoryRouter } from 'react-router-dom'; -import type { Settings } from '../../../src'; import type { ShlinkShortUrl, ShlinkShortUrlMeta } from '../../../src/api-contract'; +import type { Settings } from '../../../src/settings'; +import { SettingsProvider } from '../../../src/settings'; import { ShortUrlsRowFactory } from '../../../src/short-urls/helpers/ShortUrlsRow'; import { now, parseDate } from '../../../src/utils/dates/helpers/date'; -import { SettingsProvider } from '../../../src/utils/settings'; import { checkAccessibility } from '../../__helpers__/accessibility'; import { renderWithEvents } from '../../__helpers__/setUpTest'; import { colorGeneratorMock } from '../../utils/services/__mocks__/ColorGenerator.mock'; diff --git a/test/tags/TagsList.test.tsx b/test/tags/TagsList.test.tsx index 35399a37..75eece19 100644 --- a/test/tags/TagsList.test.tsx +++ b/test/tags/TagsList.test.tsx @@ -1,11 +1,11 @@ import { screen, waitFor } from '@testing-library/react'; import { fromPartial } from '@total-typescript/shoehorn'; import type { MercureBoundProps } from '../../src/mercure/helpers/boundToMercureHub'; +import { SettingsProvider } from '../../src/settings'; import type { TagsList } from '../../src/tags/reducers/tagsList'; import type { TagsListProps } from '../../src/tags/TagsList'; import { TagsListFactory } from '../../src/tags/TagsList'; import type { TagsTableProps } from '../../src/tags/TagsTable'; -import { SettingsProvider } from '../../src/utils/settings'; import { checkAccessibility } from '../__helpers__/accessibility'; import { renderWithEvents } from '../__helpers__/setUpTest'; diff --git a/test/tags/helpers/TagsSelector.test.tsx b/test/tags/helpers/TagsSelector.test.tsx index 713e153c..f177fb65 100644 --- a/test/tags/helpers/TagsSelector.test.tsx +++ b/test/tags/helpers/TagsSelector.test.tsx @@ -1,8 +1,8 @@ import { screen } from '@testing-library/react'; import { fromPartial } from '@total-typescript/shoehorn'; +import type { TagFilteringMode } from '../../../src/settings'; +import { SettingsProvider } from '../../../src/settings'; import { TagsSelectorFactory } from '../../../src/tags/helpers/TagsSelector'; -import type { TagFilteringMode } from '../../../src/utils/settings'; -import { SettingsProvider } from '../../../src/utils/settings'; import { checkAccessibility } from '../../__helpers__/accessibility'; import { renderWithEvents } from '../../__helpers__/setUpTest'; import { colorGeneratorMock } from '../../utils/services/__mocks__/ColorGenerator.mock'; diff --git a/test/visits/DomainVisits.test.tsx b/test/visits/DomainVisits.test.tsx index 276b5e28..0c12c45e 100644 --- a/test/visits/DomainVisits.test.tsx +++ b/test/visits/DomainVisits.test.tsx @@ -2,7 +2,7 @@ import { screen } from '@testing-library/react'; import { fromPartial } from '@total-typescript/shoehorn'; import { formatISO } from 'date-fns'; import type { MercureBoundProps } from '../../src/mercure/helpers/boundToMercureHub'; -import { SettingsProvider } from '../../src/utils/settings'; +import { SettingsProvider } from '../../src/settings'; import { DomainVisitsFactory } from '../../src/visits/DomainVisits'; import type { DomainVisits } from '../../src/visits/reducers/domainVisits'; import { checkAccessibility } from '../__helpers__/accessibility'; diff --git a/test/visits/NonOrphanVisits.test.tsx b/test/visits/NonOrphanVisits.test.tsx index c41e790b..855f4910 100644 --- a/test/visits/NonOrphanVisits.test.tsx +++ b/test/visits/NonOrphanVisits.test.tsx @@ -3,7 +3,7 @@ import { fromPartial } from '@total-typescript/shoehorn'; import { formatISO } from 'date-fns'; import { MemoryRouter } from 'react-router-dom'; import type { MercureBoundProps } from '../../src/mercure/helpers/boundToMercureHub'; -import { SettingsProvider } from '../../src/utils/settings'; +import { SettingsProvider } from '../../src/settings'; import { NonOrphanVisitsFactory } from '../../src/visits/NonOrphanVisits'; import type { VisitsInfo } from '../../src/visits/reducers/types'; import { checkAccessibility } from '../__helpers__/accessibility'; diff --git a/test/visits/OrphanVisits.test.tsx b/test/visits/OrphanVisits.test.tsx index 5dec326c..5684b5d5 100644 --- a/test/visits/OrphanVisits.test.tsx +++ b/test/visits/OrphanVisits.test.tsx @@ -3,8 +3,8 @@ import { fromPartial } from '@total-typescript/shoehorn'; import { formatISO } from 'date-fns'; import { MemoryRouter } from 'react-router-dom'; import type { MercureBoundProps } from '../../src/mercure/helpers/boundToMercureHub'; +import { SettingsProvider } from '../../src/settings'; import { FeaturesProvider } from '../../src/utils/features'; -import { SettingsProvider } from '../../src/utils/settings'; import { OrphanVisitsFactory } from '../../src/visits/OrphanVisits'; import type { VisitsInfo } from '../../src/visits/reducers/types'; import { checkAccessibility } from '../__helpers__/accessibility'; diff --git a/test/visits/ShortUrlVisits.test.tsx b/test/visits/ShortUrlVisits.test.tsx index 6ecd88f2..9f2afc53 100644 --- a/test/visits/ShortUrlVisits.test.tsx +++ b/test/visits/ShortUrlVisits.test.tsx @@ -5,8 +5,8 @@ import { formatISO } from 'date-fns'; import { MemoryRouter } from 'react-router-dom'; import { now } from 'tinybench'; import type { MercureBoundProps } from '../../src/mercure/helpers/boundToMercureHub'; +import { SettingsProvider } from '../../src/settings'; import { FeaturesProvider } from '../../src/utils/features'; -import { SettingsProvider } from '../../src/utils/settings'; import type { ShortUrlVisits as ShortUrlVisitsState } from '../../src/visits/reducers/shortUrlVisits'; import { ShortUrlVisitsFactory } from '../../src/visits/ShortUrlVisits'; import { checkAccessibility } from '../__helpers__/accessibility'; diff --git a/test/visits/TagVisits.test.tsx b/test/visits/TagVisits.test.tsx index c4a55744..b16ad7d0 100644 --- a/test/visits/TagVisits.test.tsx +++ b/test/visits/TagVisits.test.tsx @@ -2,7 +2,7 @@ import { screen } from '@testing-library/react'; import { fromPartial } from '@total-typescript/shoehorn'; import { formatISO } from 'date-fns'; import type { MercureBoundProps } from '../../src/mercure/helpers/boundToMercureHub'; -import { SettingsProvider } from '../../src/utils/settings'; +import { SettingsProvider } from '../../src/settings'; import type { TagVisits as TagVisitsStats } from '../../src/visits/reducers/tagVisits'; import type { TagVisitsProps } from '../../src/visits/TagVisits'; import { TagVisitsFactory } from '../../src/visits/TagVisits'; diff --git a/test/visits/VisitsStats.test.tsx b/test/visits/VisitsStats.test.tsx index 20b71017..c81af30b 100644 --- a/test/visits/VisitsStats.test.tsx +++ b/test/visits/VisitsStats.test.tsx @@ -2,10 +2,10 @@ import { screen, waitFor } from '@testing-library/react'; import { fromPartial } from '@total-typescript/shoehorn'; import { createMemoryHistory } from 'history'; import { Router } from 'react-router-dom'; -import type { Settings } from '../../src'; import type { ShlinkVisit } from '../../src/api-contract'; +import type { Settings } from '../../src/settings'; +import { SettingsProvider } from '../../src/settings'; import { rangeOf } from '../../src/utils/helpers'; -import { SettingsProvider } from '../../src/utils/settings'; import type { VisitsInfo } from '../../src/visits/reducers/types'; import { VisitsStats } from '../../src/visits/VisitsStats'; import { checkAccessibility } from '../__helpers__/accessibility'; diff --git a/vite.config.ts b/vite.config.ts index e1ec7d49..63b7185e 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -13,6 +13,7 @@ export default defineConfig({ entry: { index: resolve(__dirname, 'src/index.ts'), 'api-contract': resolve(__dirname, 'src/api-contract/index.ts'), + 'settings': resolve(__dirname, 'src/settings/index.ts'), }, name: 'shlink-web-component' },