diff --git a/src/common/MenuLayout.tsx b/src/common/MenuLayout.tsx index b14f8548..3ba1d654 100644 --- a/src/common/MenuLayout.tsx +++ b/src/common/MenuLayout.tsx @@ -2,18 +2,19 @@ import type { FC } from 'react'; import { useEffect } from 'react'; import { isReachableServer } from '../servers/data'; import { withSelectedServer } from '../servers/helpers/withSelectedServer'; -import type { ShlinkWebComponentType } from '../shlink-web-component'; +import { ShlinkWebComponent } from '../shlink-web-component'; +import type { Settings } from '../shlink-web-component/utils/settings'; import './MenuLayout.scss'; interface MenuLayoutProps { sidebarPresent: Function; sidebarNotPresent: Function; + settings: Settings; } export const MenuLayout = ( ServerError: FC, - ShlinkWebComponent: ShlinkWebComponentType, -) => withSelectedServer(({ selectedServer, sidebarNotPresent, sidebarPresent }) => { +) => withSelectedServer(({ selectedServer, sidebarNotPresent, sidebarPresent, settings }) => { const showContent = isReachableServer(selectedServer); useEffect(() => { @@ -28,6 +29,7 @@ export const MenuLayout = ( return ( ); diff --git a/src/common/services/provideServices.ts b/src/common/services/provideServices.ts index ccd0cf06..f5811e8d 100644 --- a/src/common/services/provideServices.ts +++ b/src/common/services/provideServices.ts @@ -31,8 +31,11 @@ export const provideServices = (bottle: Bottle, connect: ConnectDecorator) => { bottle.decorator('Home', withoutSelectedServer); bottle.decorator('Home', connect(['servers'], ['resetSelectedServer'])); - bottle.serviceFactory('MenuLayout', MenuLayout, 'ServerError', 'ShlinkWebComponent'); - bottle.decorator('MenuLayout', connect(['selectedServer'], ['selectServer', 'sidebarPresent', 'sidebarNotPresent'])); + bottle.serviceFactory('MenuLayout', MenuLayout, 'ServerError'); + bottle.decorator('MenuLayout', connect( + ['selectedServer', 'settings'], + ['selectServer', 'sidebarPresent', 'sidebarNotPresent'], + )); bottle.serviceFactory('ShlinkVersionsContainer', () => ShlinkVersionsContainer); bottle.decorator('ShlinkVersionsContainer', connect(['selectedServer', 'sidebar'])); diff --git a/src/container/index.ts b/src/container/index.ts index ea362314..994b95f4 100644 --- a/src/container/index.ts +++ b/src/container/index.ts @@ -7,12 +7,6 @@ import { provideServices as provideAppServices } from '../app/services/provideSe import { provideServices as provideCommonServices } from '../common/services/provideServices'; import { provideServices as provideServersServices } from '../servers/services/provideServices'; import { provideServices as provideSettingsServices } from '../settings/services/provideServices'; -import { provideServices as provideWebComponentServices } from '../shlink-web-component/container/provideServices'; -import { provideServices as provideDomainsServices } from '../shlink-web-component/domains/services/provideServices'; -import { provideServices as provideMercureServices } from '../shlink-web-component/mercure/services/provideServices'; -import { provideServices as provideShortUrlsServices } from '../shlink-web-component/short-urls/services/provideServices'; -import { provideServices as provideTagsServices } from '../shlink-web-component/tags/services/provideServices'; -import { provideServices as provideVisitsServices } from '../shlink-web-component/visits/services/provideServices'; import { provideServices as provideUtilsServices } from '../utils/services/provideServices'; import type { ConnectDecorator } from './types'; @@ -38,14 +32,6 @@ const connect: ConnectDecorator = (propsFromState: string[] | null, actionServic provideAppServices(bottle, connect); provideCommonServices(bottle, connect); provideApiServices(bottle); -provideShortUrlsServices(bottle, connect); provideServersServices(bottle, connect); -provideTagsServices(bottle, connect); -provideVisitsServices(bottle, connect); provideUtilsServices(bottle); -provideMercureServices(bottle); provideSettingsServices(bottle, connect); -provideDomainsServices(bottle, connect); - -// TODO This should not be needed. -provideWebComponentServices(bottle); diff --git a/src/container/types.ts b/src/container/types.ts index 9d027174..98b28151 100644 --- a/src/container/types.ts +++ b/src/container/types.ts @@ -1,42 +1,11 @@ import type { Sidebar } from '../common/reducers/sidebar'; import type { SelectedServer, ServersMap } from '../servers/data'; import type { Settings } from '../settings/reducers/settings'; -import type { DomainsList } from '../shlink-web-component/domains/reducers/domainsList'; -import type { MercureInfo } from '../shlink-web-component/mercure/reducers/mercureInfo'; -import type { ShortUrlCreation } from '../shlink-web-component/short-urls/reducers/shortUrlCreation'; -import type { ShortUrlDeletion } from '../shlink-web-component/short-urls/reducers/shortUrlDeletion'; -import type { ShortUrlDetail } from '../shlink-web-component/short-urls/reducers/shortUrlDetail'; -import type { ShortUrlEdition } from '../shlink-web-component/short-urls/reducers/shortUrlEdition'; -import type { ShortUrlsList } from '../shlink-web-component/short-urls/reducers/shortUrlsList'; -import type { TagDeletion } from '../shlink-web-component/tags/reducers/tagDelete'; -import type { TagEdition } from '../shlink-web-component/tags/reducers/tagEdit'; -import type { TagsList } from '../shlink-web-component/tags/reducers/tagsList'; -import type { DomainVisits } from '../shlink-web-component/visits/reducers/domainVisits'; -import type { ShortUrlVisits } from '../shlink-web-component/visits/reducers/shortUrlVisits'; -import type { TagVisits } from '../shlink-web-component/visits/reducers/tagVisits'; -import type { VisitsInfo } from '../shlink-web-component/visits/reducers/types'; -import type { VisitsOverview } from '../shlink-web-component/visits/reducers/visitsOverview'; export interface ShlinkState { servers: ServersMap; selectedServer: SelectedServer; - shortUrlsList: ShortUrlsList; - shortUrlCreation: ShortUrlCreation; - shortUrlDeletion: ShortUrlDeletion; - shortUrlEdition: ShortUrlEdition; - shortUrlVisits: ShortUrlVisits; - tagVisits: TagVisits; - domainVisits: DomainVisits; - orphanVisits: VisitsInfo; - nonOrphanVisits: VisitsInfo; - shortUrlDetail: ShortUrlDetail; - tagsList: TagsList; - tagDelete: TagDeletion; - tagEdit: TagEdition; - mercureInfo: MercureInfo; settings: Settings; - domainsList: DomainsList; - visitsOverview: VisitsOverview; appUpdated: boolean; sidebar: Sidebar; } diff --git a/src/reducers/index.ts b/src/reducers/index.ts index 99d2441e..7e3a7c07 100644 --- a/src/reducers/index.ts +++ b/src/reducers/index.ts @@ -7,30 +7,9 @@ import { serversReducer } from '../servers/reducers/servers'; import { settingsReducer } from '../settings/reducers/settings'; export const initReducers = (container: IContainer) => combineReducers({ - // Main shlink-web-client reducers appUpdated: appUpdatesReducer, servers: serversReducer, selectedServer: container.selectedServerReducer, settings: settingsReducer, sidebar: sidebarReducer, - - // TBD - mercureInfo: container.mercureInfoReducer, - - // Nested shlink-web-component reducers - shortUrlsList: container.shortUrlsListReducer, - shortUrlCreation: container.shortUrlCreationReducer, - shortUrlDeletion: container.shortUrlDeletionReducer, - shortUrlEdition: container.shortUrlEditionReducer, - shortUrlDetail: container.shortUrlDetailReducer, - shortUrlVisits: container.shortUrlVisitsReducer, - tagVisits: container.tagVisitsReducer, - domainVisits: container.domainVisitsReducer, - orphanVisits: container.orphanVisitsReducer, - nonOrphanVisits: container.nonOrphanVisitsReducer, - tagsList: container.tagsListReducer, - tagDelete: container.tagDeleteReducer, - tagEdit: container.tagEditReducer, - domainsList: container.domainsListReducer, - visitsOverview: container.visitsOverviewReducer, }); diff --git a/src/servers/reducers/selectedServer.ts b/src/servers/reducers/selectedServer.ts index 6469296b..8833b97f 100644 --- a/src/servers/reducers/selectedServer.ts +++ b/src/servers/reducers/selectedServer.ts @@ -66,12 +66,14 @@ export const selectServerListener = ( ) => { const listener = createListenerMiddleware(); - listener.startListening({ - actionCreator: selectServerThunk.fulfilled, - effect: ({ payload }, { dispatch }) => { - isReachableServer(payload) && dispatch(loadMercureInfo()); - }, - }); + // TODO Find a way for the mercure info to be re-loaded when server changes, without leaking mercure implementation + // details + // listener.startListening({ + // actionCreator: selectServerThunk.fulfilled, + // effect: ({ payload }, { dispatch }) => { + // isReachableServer(payload) && dispatch(loadMercureInfo()); + // }, + // }); return listener; }; diff --git a/src/servers/services/provideServices.ts b/src/servers/services/provideServices.ts index 9b593b07..53d1a8a0 100644 --- a/src/servers/services/provideServices.ts +++ b/src/servers/services/provideServices.ts @@ -1,7 +1,6 @@ import type Bottle from 'bottlejs'; import { prop } from 'ramda'; import type { ConnectDecorator } from '../../container/types'; -import { Overview } from '../../shlink-web-component/overview/Overview'; import { CreateServer } from '../CreateServer'; import { DeleteServerButton } from '../DeleteServerButton'; import { DeleteServerModal } from '../DeleteServerModal'; @@ -63,12 +62,6 @@ export const provideServices = (bottle: Bottle, connect: ConnectDecorator) => { bottle.serviceFactory('ServerError', ServerError, 'DeleteServerButton'); bottle.decorator('ServerError', connect(['servers', 'selectedServer'])); - bottle.serviceFactory('Overview', Overview, 'ShortUrlsTable', 'CreateShortUrl'); - bottle.decorator('Overview', connect( - ['shortUrlsList', 'tagsList', 'selectedServer', 'mercureInfo', 'visitsOverview', 'settings'], - ['listShortUrls', 'listTags', 'createNewVisits', 'loadMercureInfo', 'loadVisitsOverview'], - )); - // Services bottle.constant('fileReaderFactory', () => new FileReader()); bottle.service('ServersImporter', ServersImporter, 'csvToJson', 'fileReaderFactory'); diff --git a/src/settings/VisitsSettings.tsx b/src/settings/VisitsSettings.tsx index 10dcba71..cd23df13 100644 --- a/src/settings/VisitsSettings.tsx +++ b/src/settings/VisitsSettings.tsx @@ -1,12 +1,14 @@ import type { FC } from 'react'; import { FormGroup } from 'reactstrap'; +import type { Settings } from '../shlink-web-component/utils/settings'; import { DateIntervalSelector } from '../utils/dates/DateIntervalSelector'; import { FormText } from '../utils/forms/FormText'; import { LabeledFormGroup } from '../utils/forms/LabeledFormGroup'; import type { DateInterval } from '../utils/helpers/dateIntervals'; import { SimpleCard } from '../utils/SimpleCard'; import { ToggleSwitch } from '../utils/ToggleSwitch'; -import type { Settings, VisitsSettings as VisitsSettingsConfig } from './reducers/settings'; + +type VisitsSettingsConfig = Settings['visits']; interface VisitsProps { settings: Settings; diff --git a/src/settings/reducers/settings.ts b/src/settings/reducers/settings.ts index b0929162..aed418fe 100644 --- a/src/settings/reducers/settings.ts +++ b/src/settings/reducers/settings.ts @@ -2,59 +2,13 @@ import type { PayloadAction, PrepareAction } from '@reduxjs/toolkit'; import { createSlice } from '@reduxjs/toolkit'; import { mergeDeepRight } from 'ramda'; import type { ShortUrlsOrder } from '../../shlink-web-component/short-urls/data'; -import type { TagsOrder } from '../../shlink-web-component/tags/data/TagsListChildrenProps'; -import type { DateInterval } from '../../utils/helpers/dateIntervals'; -import type { Theme } from '../../utils/theme'; +import type { Settings } from '../../shlink-web-component/utils/settings'; 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 interface RealTimeUpdatesSettings { - enabled: boolean; - interval?: number; -} - -export type TagFilteringMode = 'startsWith' | 'includes'; - -export interface ShortUrlCreationSettings { - validateUrls: boolean; - tagFilteringMode?: TagFilteringMode; - forwardQuery?: boolean; -} - -export interface UiSettings { - theme: Theme; -} - -export interface VisitsSettings { - defaultInterval: DateInterval; - excludeBots?: boolean; -} - -export interface TagsSettings { - defaultOrdering?: TagsOrder; -} - -export interface ShortUrlsListSettings { - defaultOrdering?: ShortUrlsOrder; -} - -export interface Settings { - realTimeUpdates: RealTimeUpdatesSettings; - shortUrlCreation?: ShortUrlCreationSettings; - shortUrlsList?: ShortUrlsListSettings; - ui?: UiSettings; - visits?: VisitsSettings; - tags?: TagsSettings; -} - const initialState: Settings = { realTimeUpdates: { enabled: true, @@ -87,12 +41,14 @@ const { reducer, actions } = createSlice({ toggleRealTimeUpdates: toReducer((enabled: boolean) => toPreparedAction({ realTimeUpdates: { enabled } })), setRealTimeUpdatesInterval: toReducer((interval: number) => toPreparedAction({ realTimeUpdates: { interval } })), setShortUrlCreationSettings: toReducer( - (shortUrlCreation: ShortUrlCreationSettings) => toPreparedAction({ shortUrlCreation }), + (shortUrlCreation: Settings['shortUrlCreation']) => 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 })), + setShortUrlsListSettings: toReducer( + (shortUrlsList: Settings['shortUrlsList']) => toPreparedAction({ shortUrlsList }), + ), + setUiSettings: toReducer((ui: Settings['ui']) => toPreparedAction({ ui })), + setVisitsSettings: toReducer((visits: Settings['visits']) => toPreparedAction({ visits })), + setTagsSettings: toReducer((tags: Settings['tags']) => toPreparedAction({ tags })), }, }); diff --git a/src/shlink-web-component/ShlinkWebComponent.tsx b/src/shlink-web-component/ShlinkWebComponent.tsx index 54bb3831..d248accc 100644 --- a/src/shlink-web-component/ShlinkWebComponent.tsx +++ b/src/shlink-web-component/ShlinkWebComponent.tsx @@ -1,18 +1,23 @@ import { faBars as burgerIcon } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import type { Store } from '@reduxjs/toolkit'; import classNames from 'classnames'; import type { FC } from 'react'; import { useEffect } from 'react'; +import { Provider } from 'react-redux'; import { Navigate, Route, Routes, useLocation } from 'react-router-dom'; import { AsideMenu } from '../common/AsideMenu'; import { NotFound } from '../common/NotFound'; import { useSwipeable, useToggle } from '../utils/helpers/hooks'; import type { SemVer } from '../utils/helpers/version'; import { FeaturesProvider, useFeatures } from './utils/features'; +import type { Settings } from './utils/settings'; +import { SettingsProvider } from './utils/settings'; type ShlinkWebComponentProps = { routesPrefix?: string; serverVersion: SemVer; + settings?: Settings; }; export const ShlinkWebComponent = ( @@ -27,7 +32,8 @@ export const ShlinkWebComponent = ( Overview: FC, EditShortUrl: FC, ManageDomains: FC, -): FC => ({ routesPrefix = '', serverVersion }) => { + store: Store, +): FC => ({ routesPrefix = '', serverVersion, settings }) => { const location = useLocation(); const [sidebarVisible, toggleSidebar, showSidebar, hideSidebar] = useToggle(); useEffect(() => hideSidebar(), [location]); @@ -41,37 +47,41 @@ export const ShlinkWebComponent = ( // TODO Check if this is already wrapped by a router, and wrap otherwise return ( - - + + + + -
-
- -
hideSidebar()}> -
- - } /> - } /> - } /> - } /> - } /> - } /> - } /> - {addDomainVisitsRoute && } />} - } /> - {addNonOrphanVisitsRoute && } />} - } /> - } /> - List short URLs} - /> - +
+
+ +
hideSidebar()}> +
+ + } /> + } /> + } /> + } /> + } /> + } /> + } /> + {addDomainVisitsRoute && } />} + } /> + {addNonOrphanVisitsRoute && } />} + } /> + } /> + List short URLs} + /> + +
+
-
-
- + + + ); }; diff --git a/src/shlink-web-component/container/index.ts b/src/shlink-web-component/container/index.ts index a0588031..0d0cf953 100644 --- a/src/shlink-web-component/container/index.ts +++ b/src/shlink-web-component/container/index.ts @@ -1,2 +1,71 @@ -// TODO Create a separated container here -export { container } from '../../container'; +import type { IContainer } from 'bottlejs'; +import Bottle from 'bottlejs'; +import { pick } from 'ramda'; +import { connect as reduxConnect } from 'react-redux/es/exports'; +import { HttpClient } from '../../common/services/HttpClient'; +import { ImageDownloader } from '../../common/services/ImageDownloader'; +import { ReportExporter } from '../../common/services/ReportExporter'; +import { csvToJson, jsonToCsv } from '../../utils/helpers/csvjson'; +import { useTimeoutToggle } from '../../utils/helpers/hooks'; +import { ColorGenerator } from '../../utils/services/ColorGenerator'; +import { LocalStorage } from '../../utils/services/LocalStorage'; +import { provideServices as provideDomainsServices } from '../domains/services/provideServices'; +import { provideServices as provideMercureServices } from '../mercure/services/provideServices'; +import { provideServices as provideOverviewServices } from '../overview/services/provideServices'; +import { provideServices as provideShortUrlsServices } from '../short-urls/services/provideServices'; +import { provideServices as provideTagsServices } from '../tags/services/provideServices'; +import { provideServices as provideVisitsServices } from '../visits/services/provideServices'; +import { provideServices as provideWebComponentServices } from './provideServices'; +import { setUpStore } from './store'; + +type LazyActionMap = Record; + +export type ConnectDecorator = (props: string[] | null, actions?: string[]) => any; + +const bottle = new Bottle(); + +export const { container } = bottle; + +const lazyService = (cont: IContainer, serviceName: string) => + (...args: any[]) => (cont[serviceName] as T)(...args) as K; +const mapActionService = (map: LazyActionMap, actionName: string): LazyActionMap => ({ + ...map, + // Wrap actual action service in a function so that it is lazily created the first time it is called + [actionName]: lazyService(container, actionName), +}); +const connect: ConnectDecorator = (propsFromState: string[] | null, actionServiceNames: string[] = []) => + reduxConnect( + propsFromState ? pick(propsFromState) : null, + actionServiceNames.reduce(mapActionService, {}), + ); + +provideWebComponentServices(bottle); +provideShortUrlsServices(bottle, connect); +provideTagsServices(bottle, connect); +provideVisitsServices(bottle, connect); +provideMercureServices(bottle); +provideDomainsServices(bottle, connect); +provideOverviewServices(bottle, connect); + +// TODO Check which of these can be moved to shlink-web-component, and which are needed by the app too +bottle.constant('window', window); +bottle.constant('console', console); +bottle.constant('fetch', window.fetch.bind(window)); + +bottle.service('HttpClient', HttpClient, 'fetch'); +bottle.service('ImageDownloader', ImageDownloader, 'HttpClient', 'window'); +bottle.service('ReportExporter', ReportExporter, 'window', 'jsonToCsv'); + +bottle.constant('localStorage', window.localStorage); +bottle.service('Storage', LocalStorage, 'localStorage'); +bottle.service('ColorGenerator', ColorGenerator, 'Storage'); + +bottle.constant('csvToJson', csvToJson); +bottle.constant('jsonToCsv', jsonToCsv); + +bottle.constant('setTimeout', window.setTimeout); +bottle.constant('clearTimeout', window.clearTimeout); +bottle.serviceFactory('useTimeoutToggle', useTimeoutToggle, 'setTimeout', 'clearTimeout'); + +// FIXME This has to be last. Find a way to delay the creation, perhaps using some kind of runtime factory +bottle.constant('store', setUpStore(container)); diff --git a/src/shlink-web-component/container/provideServices.ts b/src/shlink-web-component/container/provideServices.ts index 8a42202e..0e54b9c8 100644 --- a/src/shlink-web-component/container/provideServices.ts +++ b/src/shlink-web-component/container/provideServices.ts @@ -16,5 +16,6 @@ export const provideServices = (bottle: Bottle) => { 'Overview', 'EditShortUrl', 'ManageDomains', + 'store', ); }; diff --git a/src/shlink-web-component/container/store.ts b/src/shlink-web-component/container/store.ts index 6e41ece0..4af1309d 100644 --- a/src/shlink-web-component/container/store.ts +++ b/src/shlink-web-component/container/store.ts @@ -6,10 +6,7 @@ const isProduction = process.env.NODE_ENV === 'production'; export const setUpStore = (container: IContainer) => configureStore({ devTools: !isProduction, reducer: combineReducers({ - // TODO Check if this should be here or not mercureInfo: container.mercureInfoReducer, - - // Nested shlink-web-component reducers shortUrlsList: container.shortUrlsListReducer, shortUrlCreation: container.shortUrlCreationReducer, shortUrlDeletion: container.shortUrlDeletionReducer, diff --git a/src/shlink-web-component/domains/services/provideServices.ts b/src/shlink-web-component/domains/services/provideServices.ts index 7322dcb2..30e944d0 100644 --- a/src/shlink-web-component/domains/services/provideServices.ts +++ b/src/shlink-web-component/domains/services/provideServices.ts @@ -1,6 +1,6 @@ import type Bottle from 'bottlejs'; import { prop } from 'ramda'; -import type { ConnectDecorator } from '../../../container/types'; +import type { ConnectDecorator } from '../../container'; import { DomainSelector } from '../DomainSelector'; import { ManageDomains } from '../ManageDomains'; import { editDomainRedirects } from '../reducers/domainRedirects'; diff --git a/src/shlink-web-component/index.ts b/src/shlink-web-component/index.ts index 3c633450..f21ab8aa 100644 --- a/src/shlink-web-component/index.ts +++ b/src/shlink-web-component/index.ts @@ -1,5 +1,4 @@ import { container } from './container'; +import type { ShlinkWebComponentType } from './ShlinkWebComponent'; -export const { ShlinkWebComponent } = container; - -export type { ShlinkWebComponentType } from './ShlinkWebComponent'; +export const ShlinkWebComponent = container.ShlinkWebComponent as ShlinkWebComponentType; diff --git a/src/shlink-web-component/mercure/reducers/mercureInfo.ts b/src/shlink-web-component/mercure/reducers/mercureInfo.ts index feed0cf4..806e387b 100644 --- a/src/shlink-web-component/mercure/reducers/mercureInfo.ts +++ b/src/shlink-web-component/mercure/reducers/mercureInfo.ts @@ -19,14 +19,15 @@ const initialState: MercureInfo = { export const mercureInfoReducerCreator = (buildShlinkApiClient: ShlinkApiClientBuilder) => { const loadMercureInfo = createAsyncThunk( `${REDUCER_PREFIX}/loadMercureInfo`, - (_: void, { getState }): Promise => { - const { settings } = getState(); - if (!settings.realTimeUpdates.enabled) { - throw new Error('Real time updates not enabled'); - } + (_: void, { getState }): Promise => + // TODO Get settings here, where info is only available via hook + // const { settings } = getState(); + // if (!settings.realTimeUpdates.enabled) { + // throw new Error('Real time updates not enabled'); + // } - return buildShlinkApiClient(getState).mercureInfo(); - }, + buildShlinkApiClient(getState).mercureInfo() + , ); const { reducer } = createSlice({ diff --git a/src/shlink-web-component/overview/Overview.tsx b/src/shlink-web-component/overview/Overview.tsx index 7f81c014..9c1d97d7 100644 --- a/src/shlink-web-component/overview/Overview.tsx +++ b/src/shlink-web-component/overview/Overview.tsx @@ -7,7 +7,6 @@ import type { SelectedServer } from '../../servers/data'; import { getServerId } from '../../servers/data'; import { HighlightCard } from '../../servers/helpers/HighlightCard'; import { VisitsHighlightCard } from '../../servers/helpers/VisitsHighlightCard'; -import type { Settings } from '../../settings/reducers/settings'; import { prettify } from '../../utils/helpers/numbers'; import { boundToMercureHub } from '../mercure/helpers/boundToMercureHub'; import { Topics } from '../mercure/helpers/Topics'; @@ -17,6 +16,7 @@ import { ITEMS_IN_OVERVIEW_PAGE } from '../short-urls/reducers/shortUrlsList'; import type { ShortUrlsTableType } from '../short-urls/ShortUrlsTable'; import type { TagsList } from '../tags/reducers/tagsList'; import { useFeature } from '../utils/features'; +import { useSetting } from '../utils/settings'; import type { VisitsOverview } from '../visits/reducers/visitsOverview'; interface OverviewConnectProps { @@ -27,7 +27,6 @@ interface OverviewConnectProps { selectedServer: SelectedServer; visitsOverview: VisitsOverview; loadVisitsOverview: Function; - settings: Settings; } export const Overview = ( @@ -41,7 +40,6 @@ export const Overview = ( selectedServer, loadVisitsOverview, visitsOverview, - settings: { visits }, }: OverviewConnectProps) => { const { loading, shortUrls } = shortUrlsList; const { loading: loadingTags } = tagsList; @@ -49,6 +47,7 @@ export const Overview = ( const serverId = getServerId(selectedServer); const linkToNonOrphanVisits = useFeature('nonOrphanVisits'); const navigate = useNavigate(); + const visits = useSetting('visits'); useEffect(() => { listShortUrls({ itemsPerPage: ITEMS_IN_OVERVIEW_PAGE, orderBy: { field: 'dateCreated', dir: 'DESC' } }); diff --git a/src/shlink-web-component/overview/services/provideServices.ts b/src/shlink-web-component/overview/services/provideServices.ts new file mode 100644 index 00000000..ec423cbc --- /dev/null +++ b/src/shlink-web-component/overview/services/provideServices.ts @@ -0,0 +1,11 @@ +import type Bottle from 'bottlejs'; +import type { ConnectDecorator } from '../../container'; +import { Overview } from '../Overview'; + +export function provideServices(bottle: Bottle, connect: ConnectDecorator) { + bottle.serviceFactory('Overview', Overview, 'ShortUrlsTable', 'CreateShortUrl'); + bottle.decorator('Overview', connect( + ['shortUrlsList', 'tagsList', 'selectedServer', 'mercureInfo', 'visitsOverview'], + ['listShortUrls', 'listTags', 'createNewVisits', 'loadMercureInfo', 'loadVisitsOverview'], + )); +} diff --git a/src/shlink-web-component/short-urls/CreateShortUrl.tsx b/src/shlink-web-component/short-urls/CreateShortUrl.tsx index 10b5aa86..4280f15d 100644 --- a/src/shlink-web-component/short-urls/CreateShortUrl.tsx +++ b/src/shlink-web-component/short-urls/CreateShortUrl.tsx @@ -1,6 +1,7 @@ import type { FC } from 'react'; import { useMemo } from 'react'; -import type { Settings, ShortUrlCreationSettings } from '../../settings/reducers/settings'; +import type { ShortUrlCreationSettings } from '../utils/settings'; +import { useSetting } from '../utils/settings'; import type { ShortUrlData } from './data'; import type { CreateShortUrlResultProps } from './helpers/CreateShortUrlResult'; import type { ShortUrlCreation } from './reducers/shortUrlCreation'; @@ -11,7 +12,6 @@ export interface CreateShortUrlProps { } interface CreateShortUrlConnectProps extends CreateShortUrlProps { - settings: Settings; shortUrlCreation: ShortUrlCreation; createShortUrl: (data: ShortUrlData) => Promise; resetCreateShortUrl: () => void; @@ -40,8 +40,8 @@ export const CreateShortUrl = ( shortUrlCreation, resetCreateShortUrl, basicMode = false, - settings: { shortUrlCreation: shortUrlCreationSettings }, }: CreateShortUrlConnectProps) => { + const shortUrlCreationSettings = useSetting('shortUrlCreation'); const initialState = useMemo(() => getInitialState(shortUrlCreationSettings), [shortUrlCreationSettings]); return ( diff --git a/src/shlink-web-component/short-urls/EditShortUrl.tsx b/src/shlink-web-component/short-urls/EditShortUrl.tsx index f6c448e7..b35de1b6 100644 --- a/src/shlink-web-component/short-urls/EditShortUrl.tsx +++ b/src/shlink-web-component/short-urls/EditShortUrl.tsx @@ -6,11 +6,11 @@ import { ExternalLink } from 'react-external-link'; import { useLocation, useParams } from 'react-router-dom'; import { Button, Card } from 'reactstrap'; import { ShlinkApiError } from '../../api/ShlinkApiError'; -import type { Settings } from '../../settings/reducers/settings'; import { useGoBack } from '../../utils/helpers/hooks'; import { parseQuery } from '../../utils/helpers/query'; import { Message } from '../../utils/Message'; import { Result } from '../../utils/Result'; +import { useSetting } from '../utils/settings'; import type { ShortUrlIdentifier } from './data'; import { shortUrlDataFromShortUrl, urlDecodeShortCode } from './helpers'; import type { ShortUrlDetail } from './reducers/shortUrlDetail'; @@ -18,7 +18,6 @@ import type { EditShortUrl as EditShortUrlInfo, ShortUrlEdition } from './reduce import type { ShortUrlFormProps } from './ShortUrlForm'; interface EditShortUrlConnectProps { - settings: Settings; shortUrlDetail: ShortUrlDetail; shortUrlEdition: ShortUrlEdition; getShortUrlDetail: (shortUrl: ShortUrlIdentifier) => void; @@ -26,7 +25,6 @@ interface EditShortUrlConnectProps { } export const EditShortUrl = (ShortUrlForm: FC) => ({ - settings: { shortUrlCreation: shortUrlCreationSettings }, shortUrlDetail, getShortUrlDetail, shortUrlEdition, @@ -38,6 +36,7 @@ export const EditShortUrl = (ShortUrlForm: FC) => ({ const { loading, error, errorData, shortUrl } = shortUrlDetail; const { saving, saved, error: savingError, errorData: savingErrorData } = shortUrlEdition; const { domain } = parseQuery<{ domain?: string }>(search); + const shortUrlCreationSettings = useSetting('shortUrlCreation'); const initialState = useMemo( () => shortUrlDataFromShortUrl(shortUrl, shortUrlCreationSettings), [shortUrl, shortUrlCreationSettings], diff --git a/src/shlink-web-component/short-urls/ShortUrlsFilteringBar.tsx b/src/shlink-web-component/short-urls/ShortUrlsFilteringBar.tsx index 89e13632..40c1185f 100644 --- a/src/shlink-web-component/short-urls/ShortUrlsFilteringBar.tsx +++ b/src/shlink-web-component/short-urls/ShortUrlsFilteringBar.tsx @@ -4,7 +4,6 @@ import classNames from 'classnames'; import { isEmpty, pipe } from 'ramda'; import type { FC } from 'react'; import { Button, InputGroup, Row, UncontrolledTooltip } from 'reactstrap'; -import type { Settings } from '../../settings/reducers/settings'; import { DateRangeSelector } from '../../utils/dates/DateRangeSelector'; import { formatIsoDate } from '../../utils/helpers/date'; import type { DateRange } from '../../utils/helpers/dateIntervals'; @@ -14,6 +13,7 @@ import { OrderingDropdown } from '../../utils/OrderingDropdown'; import { SearchField } from '../../utils/SearchField'; import type { TagsSelectorProps } from '../tags/helpers/TagsSelector'; 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'; @@ -23,7 +23,6 @@ import './ShortUrlsFilteringBar.scss'; interface ShortUrlsFilteringProps { order: ShortUrlsOrder; - settings: Settings; handleOrderBy: (orderField?: ShortUrlsOrderableFields, orderDir?: OrderDir) => void; className?: string; shortUrlsAmount?: number; @@ -32,7 +31,7 @@ interface ShortUrlsFilteringProps { export const ShortUrlsFilteringBar = ( ExportShortUrlsBtn: FC, TagsSelector: FC, -): FC => ({ className, shortUrlsAmount, order, handleOrderBy, settings }) => { +): FC => ({ className, shortUrlsAmount, order, handleOrderBy }) => { const [filter, toFirstPage] = useShortUrlsQuery(); const { search, @@ -45,6 +44,7 @@ export const ShortUrlsFilteringBar = ( tagsMode = 'any', } = filter; const supportsDisabledFiltering = useFeature('filterDisabledUrls'); + const visitsSettings = useSetting('visits'); const setDates = pipe( ({ startDate: theStartDate, endDate: theEndDate }: DateRange) => ({ @@ -95,7 +95,7 @@ export const ShortUrlsFilteringBar = ( void; - settings: Settings; } export const ShortUrlsList = ( ShortUrlsTable: ShortUrlsTableType, ShortUrlsFilteringBar: ShortUrlsFilteringBarType, -) => boundToMercureHub(({ listShortUrls, shortUrlsList, selectedServer, settings }: ShortUrlsListProps) => { +) => boundToMercureHub(({ listShortUrls, shortUrlsList, selectedServer }: ShortUrlsListProps) => { const serverId = getServerId(selectedServer); const { page } = useParams(); const location = useLocation(); const [filter, toFirstPage] = useShortUrlsQuery(); + const settings = useSettings(); const { tags, search, @@ -104,7 +104,6 @@ export const ShortUrlsList = ( shortUrlsAmount={shortUrlsList.shortUrls?.pagination.totalItems} order={actualOrderBy} handleOrderBy={handleOrderBy} - settings={settings} className="mb-3" /> diff --git a/src/shlink-web-component/short-urls/helpers/ShortUrlsRow.tsx b/src/shlink-web-component/short-urls/helpers/ShortUrlsRow.tsx index 36cbdd75..f783fc83 100644 --- a/src/shlink-web-component/short-urls/helpers/ShortUrlsRow.tsx +++ b/src/shlink-web-component/short-urls/helpers/ShortUrlsRow.tsx @@ -2,11 +2,11 @@ import type { FC } from 'react'; import { useEffect, useRef } from 'react'; import { ExternalLink } from 'react-external-link'; import type { SelectedServer } from '../../../servers/data'; -import type { Settings } from '../../../settings/reducers/settings'; import { CopyToClipboardIcon } from '../../../utils/CopyToClipboardIcon'; import { Time } from '../../../utils/dates/Time'; import type { TimeoutToggle } from '../../../utils/helpers/hooks'; import type { ColorGenerator } from '../../../utils/services/ColorGenerator'; +import { useSetting } from '../../utils/settings'; import type { ShortUrl } from '../data'; import { useShortUrlsQuery } from './hooks'; import type { ShortUrlsRowMenuType } from './ShortUrlsRowMenu'; @@ -21,22 +21,18 @@ interface ShortUrlsRowProps { shortUrl: ShortUrl; } -interface ShortUrlsRowConnectProps extends ShortUrlsRowProps { - settings: Settings; -} - export type ShortUrlsRowType = FC; export const ShortUrlsRow = ( ShortUrlsRowMenu: ShortUrlsRowMenuType, colorGenerator: ColorGenerator, useTimeoutToggle: TimeoutToggle, -) => ({ shortUrl, selectedServer, onTagClick, settings }: ShortUrlsRowConnectProps) => { +) => ({ shortUrl, selectedServer, onTagClick }: ShortUrlsRowProps) => { const [copiedToClipboard, setCopiedToClipboard] = useTimeoutToggle(); const [active, setActive] = useTimeoutToggle(false, 500); const isFirstRun = useRef(true); const [{ excludeBots }] = useShortUrlsQuery(); - const { visits } = settings; + const visits = useSetting('visits'); const doExcludeBots = excludeBots ?? visits?.excludeBots; useEffect(() => { diff --git a/src/shlink-web-component/short-urls/helpers/index.ts b/src/shlink-web-component/short-urls/helpers/index.ts index 5d0b6850..ef4d5f71 100644 --- a/src/shlink-web-component/short-urls/helpers/index.ts +++ b/src/shlink-web-component/short-urls/helpers/index.ts @@ -1,6 +1,6 @@ import { isNil } from 'ramda'; -import type { ShortUrlCreationSettings } from '../../../settings/reducers/settings'; import type { OptionalString } from '../../../utils/utils'; +import type { ShortUrlCreationSettings } from '../../utils/settings'; import { DEFAULT_DOMAIN } from '../../visits/reducers/domainVisits'; import type { ShortUrl, ShortUrlData } from '../data'; diff --git a/src/shlink-web-component/short-urls/services/provideServices.ts b/src/shlink-web-component/short-urls/services/provideServices.ts index 391285cc..d5e76d67 100644 --- a/src/shlink-web-component/short-urls/services/provideServices.ts +++ b/src/shlink-web-component/short-urls/services/provideServices.ts @@ -1,6 +1,6 @@ import type Bottle from 'bottlejs'; import { prop } from 'ramda'; -import type { ConnectDecorator } from '../../../container/types'; +import type { ConnectDecorator } from '../../container'; import { CreateShortUrl } from '../CreateShortUrl'; import { EditShortUrl } from '../EditShortUrl'; import { CreateShortUrlResult } from '../helpers/CreateShortUrlResult'; @@ -23,15 +23,12 @@ export const provideServices = (bottle: Bottle, connect: ConnectDecorator) => { // Components bottle.serviceFactory('ShortUrlsList', ShortUrlsList, 'ShortUrlsTable', 'ShortUrlsFilteringBar'); bottle.decorator('ShortUrlsList', connect( - ['selectedServer', 'mercureInfo', 'shortUrlsList', 'settings'], + ['selectedServer', 'mercureInfo', 'shortUrlsList'], ['listShortUrls', 'createNewVisits', 'loadMercureInfo'], )); bottle.serviceFactory('ShortUrlsTable', ShortUrlsTable, 'ShortUrlsRow'); - bottle.serviceFactory('ShortUrlsRow', ShortUrlsRow, 'ShortUrlsRowMenu', 'ColorGenerator', 'useTimeoutToggle'); - bottle.decorator('ShortUrlsRow', connect(['settings'])); - bottle.serviceFactory('ShortUrlsRowMenu', ShortUrlsRowMenu, 'DeleteShortUrlModal', 'QrCodeModal'); bottle.serviceFactory('CreateShortUrlResult', CreateShortUrlResult, 'useTimeoutToggle'); bottle.serviceFactory('ShortUrlForm', ShortUrlForm, 'TagsSelector', 'DomainSelector'); @@ -39,12 +36,12 @@ export const provideServices = (bottle: Bottle, connect: ConnectDecorator) => { bottle.serviceFactory('CreateShortUrl', CreateShortUrl, 'ShortUrlForm', 'CreateShortUrlResult'); bottle.decorator( 'CreateShortUrl', - connect(['shortUrlCreation', 'settings'], ['createShortUrl', 'resetCreateShortUrl']), + connect(['shortUrlCreation'], ['createShortUrl', 'resetCreateShortUrl']), ); bottle.serviceFactory('EditShortUrl', EditShortUrl, 'ShortUrlForm'); bottle.decorator('EditShortUrl', connect( - ['shortUrlDetail', 'shortUrlEdition', 'settings'], + ['shortUrlDetail', 'shortUrlEdition'], ['getShortUrlDetail', 'editShortUrl'], )); diff --git a/src/shlink-web-component/tags/TagsList.tsx b/src/shlink-web-component/tags/TagsList.tsx index 1ac63ad5..418a91ed 100644 --- a/src/shlink-web-component/tags/TagsList.tsx +++ b/src/shlink-web-component/tags/TagsList.tsx @@ -4,7 +4,6 @@ import { useEffect, useState } from 'react'; import { Row } from 'reactstrap'; import { ShlinkApiError } from '../../api/ShlinkApiError'; import type { SelectedServer } from '../../servers/data'; -import type { Settings } from '../../settings/reducers/settings'; import { determineOrderDir, sortList } from '../../utils/helpers/ordering'; import { Message } from '../../utils/Message'; import { OrderingDropdown } from '../../utils/OrderingDropdown'; @@ -12,6 +11,7 @@ import { Result } from '../../utils/Result'; import { SearchField } from '../../utils/SearchField'; import { boundToMercureHub } from '../mercure/helpers/boundToMercureHub'; import { Topics } from '../mercure/helpers/Topics'; +import { useSettings } from '../utils/settings'; import type { SimplifiedTag } from './data'; import type { TagsOrder, TagsOrderableFields } from './data/TagsListChildrenProps'; import { TAGS_ORDERABLE_FIELDS } from './data/TagsListChildrenProps'; @@ -23,12 +23,12 @@ export interface TagsListProps { forceListTags: Function; tagsList: TagsListState; selectedServer: SelectedServer; - settings: Settings; } export const TagsList = (TagsTable: FC) => boundToMercureHub(( - { filterTags, forceListTags, tagsList, selectedServer, settings }: TagsListProps, + { filterTags, forceListTags, tagsList, selectedServer }: TagsListProps, ) => { + const settings = useSettings(); const [order, setOrder] = useState(settings.tags?.defaultOrdering ?? {}); const resolveSortedTags = pipe( () => tagsList.filteredTags.map((tag): SimplifiedTag => { diff --git a/src/shlink-web-component/tags/helpers/TagsSelector.tsx b/src/shlink-web-component/tags/helpers/TagsSelector.tsx index 917bd803..4ce55d7e 100644 --- a/src/shlink-web-component/tags/helpers/TagsSelector.tsx +++ b/src/shlink-web-component/tags/helpers/TagsSelector.tsx @@ -1,8 +1,8 @@ import { useEffect } from 'react'; import type { SuggestionComponentProps, TagComponentProps } from 'react-tag-autocomplete'; import ReactTags from 'react-tag-autocomplete'; -import type { Settings } from '../../../settings/reducers/settings'; import type { ColorGenerator } from '../../../utils/services/ColorGenerator'; +import { useSetting } from '../../utils/settings'; import type { TagsList } from '../reducers/tagsList'; import { Tag } from './Tag'; import { TagBullet } from './TagBullet'; @@ -17,19 +17,19 @@ export interface TagsSelectorProps { interface TagsSelectorConnectProps extends TagsSelectorProps { listTags: () => void; tagsList: TagsList; - settings: Settings; } const toComponentTag = (tag: string) => ({ id: tag, name: tag }); export const TagsSelector = (colorGenerator: ColorGenerator) => ( - { selectedTags, onChange, placeholder, listTags, tagsList, settings, allowNew = true }: TagsSelectorConnectProps, + { selectedTags, onChange, placeholder, listTags, tagsList, allowNew = true }: TagsSelectorConnectProps, ) => { + const shortUrlCreation = useSetting('shortUrlCreation'); useEffect(() => { listTags(); }, []); - const searchMode = settings.shortUrlCreation?.tagFilteringMode ?? 'startsWith'; + const searchMode = shortUrlCreation?.tagFilteringMode ?? 'startsWith'; const ReactTagsTag = ({ tag, onDelete }: TagComponentProps) => ; const ReactTagsSuggestion = ({ item }: SuggestionComponentProps) => ( diff --git a/src/shlink-web-component/tags/services/provideServices.ts b/src/shlink-web-component/tags/services/provideServices.ts index bd63bdf5..a9b426cc 100644 --- a/src/shlink-web-component/tags/services/provideServices.ts +++ b/src/shlink-web-component/tags/services/provideServices.ts @@ -1,7 +1,7 @@ import type { IContainer } from 'bottlejs'; import type Bottle from 'bottlejs'; import { prop } from 'ramda'; -import type { ConnectDecorator } from '../../../container/types'; +import type { ConnectDecorator } from '../../container'; import { DeleteTagConfirmModal } from '../helpers/DeleteTagConfirmModal'; import { EditTagModal } from '../helpers/EditTagModal'; import { TagsSelector } from '../helpers/TagsSelector'; @@ -15,7 +15,7 @@ import { TagsTableRow } from '../TagsTableRow'; export const provideServices = (bottle: Bottle, connect: ConnectDecorator) => { // Components bottle.serviceFactory('TagsSelector', TagsSelector, 'ColorGenerator'); - bottle.decorator('TagsSelector', connect(['tagsList', 'settings'], ['listTags'])); + bottle.decorator('TagsSelector', connect(['tagsList'], ['listTags'])); bottle.serviceFactory('DeleteTagConfirmModal', () => DeleteTagConfirmModal); bottle.decorator('DeleteTagConfirmModal', connect(['tagDelete'], ['deleteTag', 'tagDeleted'])); @@ -24,13 +24,11 @@ export const provideServices = (bottle: Bottle, connect: ConnectDecorator) => { bottle.decorator('EditTagModal', connect(['tagEdit'], ['editTag', 'tagEdited'])); bottle.serviceFactory('TagsTableRow', TagsTableRow, 'DeleteTagConfirmModal', 'EditTagModal', 'ColorGenerator'); - bottle.decorator('TagsTableRow', connect(['settings'])); - bottle.serviceFactory('TagsTable', TagsTable, 'TagsTableRow'); bottle.serviceFactory('TagsList', TagsList, 'TagsTable'); bottle.decorator('TagsList', connect( - ['tagsList', 'selectedServer', 'mercureInfo', 'settings'], + ['tagsList', 'selectedServer', 'mercureInfo'], ['forceListTags', 'filterTags', 'createNewVisits', 'loadMercureInfo'], )); diff --git a/src/shlink-web-component/utils/settings.ts b/src/shlink-web-component/utils/settings.ts new file mode 100644 index 00000000..432508ac --- /dev/null +++ b/src/shlink-web-component/utils/settings.ts @@ -0,0 +1,83 @@ +import { createContext, useContext } from 'react'; +import type { DateInterval } from '../../utils/helpers/dateIntervals'; +import type { Theme } from '../../utils/theme'; +import type { ShortUrlsOrder } from '../short-urls/data'; +import type { TagsOrder } from '../tags/data/TagsListChildrenProps'; + +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 interface RealTimeUpdatesSettings { + enabled: boolean; + interval?: number; +} + +export type TagFilteringMode = 'startsWith' | 'includes'; + +export interface ShortUrlCreationSettings { + validateUrls: boolean; + tagFilteringMode?: TagFilteringMode; + forwardQuery?: boolean; +} + +export interface UiSettings { + theme: Theme; +} + +export interface VisitsSettings { + defaultInterval: DateInterval; + excludeBots?: boolean; +} + +export interface TagsSettings { + defaultOrdering?: TagsOrder; +} + +export interface ShortUrlsListSettings { + defaultOrdering?: ShortUrlsOrder; +} + +export interface Settings { + realTimeUpdates?: RealTimeUpdatesSettings; + shortUrlCreation?: ShortUrlCreationSettings; + shortUrlsList?: ShortUrlsListSettings; + ui?: UiSettings; + visits?: VisitsSettings; + tags?: TagsSettings; +} + +const defaultSettings: Settings = { + realTimeUpdates: { + enabled: true, + }, + shortUrlCreation: { + validateUrls: false, + }, + ui: { + theme: 'light', + }, + 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/shlink-web-component/visits/DomainVisits.tsx b/src/shlink-web-component/visits/DomainVisits.tsx index 345e2ce5..2c669e64 100644 --- a/src/shlink-web-component/visits/DomainVisits.tsx +++ b/src/shlink-web-component/visits/DomainVisits.tsx @@ -6,12 +6,11 @@ import { boundToMercureHub } from '../mercure/helpers/boundToMercureHub'; import { Topics } from '../mercure/helpers/Topics'; import type { DomainVisits as DomainVisitsState, LoadDomainVisits } from './reducers/domainVisits'; import type { NormalizedVisit } from './types'; -import type { CommonVisitsProps } from './types/CommonVisitsProps'; import { toApiParams } from './types/helpers'; import { VisitsHeader } from './VisitsHeader'; import { VisitsStats } from './VisitsStats'; -export interface DomainVisitsProps extends CommonVisitsProps { +export interface DomainVisitsProps { getDomainVisits: (params: LoadDomainVisits) => void; domainVisits: DomainVisitsState; cancelGetDomainVisits: () => void; @@ -21,7 +20,6 @@ export const DomainVisits = ({ exportVisits }: ReportExporter) => boundToMercure getDomainVisits, domainVisits, cancelGetDomainVisits, - settings, }: DomainVisitsProps) => { const goBack = useGoBack(); const { domain = '' } = useParams(); @@ -35,7 +33,6 @@ export const DomainVisits = ({ exportVisits }: ReportExporter) => boundToMercure getVisits={loadVisits} cancelGetVisits={cancelGetDomainVisits} visitsInfo={domainVisits} - settings={settings} exportCsv={exportCsv} > diff --git a/src/shlink-web-component/visits/NonOrphanVisits.tsx b/src/shlink-web-component/visits/NonOrphanVisits.tsx index aad15a70..9a9ff889 100644 --- a/src/shlink-web-component/visits/NonOrphanVisits.tsx +++ b/src/shlink-web-component/visits/NonOrphanVisits.tsx @@ -4,12 +4,11 @@ import { boundToMercureHub } from '../mercure/helpers/boundToMercureHub'; import { Topics } from '../mercure/helpers/Topics'; import type { LoadVisits, VisitsInfo } from './reducers/types'; import type { NormalizedVisit, VisitsParams } from './types'; -import type { CommonVisitsProps } from './types/CommonVisitsProps'; import { toApiParams } from './types/helpers'; import { VisitsHeader } from './VisitsHeader'; import { VisitsStats } from './VisitsStats'; -export interface NonOrphanVisitsProps extends CommonVisitsProps { +export interface NonOrphanVisitsProps { getNonOrphanVisits: (params: LoadVisits) => void; nonOrphanVisits: VisitsInfo; cancelGetNonOrphanVisits: () => void; @@ -19,7 +18,6 @@ export const NonOrphanVisits = ({ exportVisits }: ReportExporter) => boundToMerc getNonOrphanVisits, nonOrphanVisits, cancelGetNonOrphanVisits, - settings, }: NonOrphanVisitsProps) => { const goBack = useGoBack(); const exportCsv = (visits: NormalizedVisit[]) => exportVisits('non_orphan_visits.csv', visits); @@ -31,7 +29,6 @@ export const NonOrphanVisits = ({ exportVisits }: ReportExporter) => boundToMerc getVisits={loadVisits} cancelGetVisits={cancelGetNonOrphanVisits} visitsInfo={nonOrphanVisits} - settings={settings} exportCsv={exportCsv} > diff --git a/src/shlink-web-component/visits/OrphanVisits.tsx b/src/shlink-web-component/visits/OrphanVisits.tsx index fec22e5a..3d70a032 100644 --- a/src/shlink-web-component/visits/OrphanVisits.tsx +++ b/src/shlink-web-component/visits/OrphanVisits.tsx @@ -5,12 +5,11 @@ import { Topics } from '../mercure/helpers/Topics'; import type { LoadOrphanVisits } from './reducers/orphanVisits'; import type { VisitsInfo } from './reducers/types'; import type { NormalizedVisit, VisitsParams } from './types'; -import type { CommonVisitsProps } from './types/CommonVisitsProps'; import { toApiParams } from './types/helpers'; import { VisitsHeader } from './VisitsHeader'; import { VisitsStats } from './VisitsStats'; -export interface OrphanVisitsProps extends CommonVisitsProps { +export interface OrphanVisitsProps { getOrphanVisits: (params: LoadOrphanVisits) => void; orphanVisits: VisitsInfo; cancelGetOrphanVisits: () => void; @@ -20,7 +19,6 @@ export const OrphanVisits = ({ exportVisits }: ReportExporter) => boundToMercure getOrphanVisits, orphanVisits, cancelGetOrphanVisits, - settings, }: OrphanVisitsProps) => { const goBack = useGoBack(); const exportCsv = (visits: NormalizedVisit[]) => exportVisits('orphan_visits.csv', visits); @@ -33,7 +31,6 @@ export const OrphanVisits = ({ exportVisits }: ReportExporter) => boundToMercure getVisits={loadVisits} cancelGetVisits={cancelGetOrphanVisits} visitsInfo={orphanVisits} - settings={settings} exportCsv={exportCsv} isOrphanVisits > diff --git a/src/shlink-web-component/visits/ShortUrlVisits.tsx b/src/shlink-web-component/visits/ShortUrlVisits.tsx index a1e82aaf..67590c0b 100644 --- a/src/shlink-web-component/visits/ShortUrlVisits.tsx +++ b/src/shlink-web-component/visits/ShortUrlVisits.tsx @@ -11,11 +11,10 @@ import type { ShortUrlDetail } from '../short-urls/reducers/shortUrlDetail'; import type { LoadShortUrlVisits, ShortUrlVisits as ShortUrlVisitsState } from './reducers/shortUrlVisits'; import { ShortUrlVisitsHeader } from './ShortUrlVisitsHeader'; import type { NormalizedVisit, VisitsParams } from './types'; -import type { CommonVisitsProps } from './types/CommonVisitsProps'; import { toApiParams } from './types/helpers'; import { VisitsStats } from './VisitsStats'; -export interface ShortUrlVisitsProps extends CommonVisitsProps { +export interface ShortUrlVisitsProps { getShortUrlVisits: (params: LoadShortUrlVisits) => void; shortUrlVisits: ShortUrlVisitsState; getShortUrlDetail: (shortUrl: ShortUrlIdentifier) => void; @@ -29,7 +28,6 @@ export const ShortUrlVisits = ({ exportVisits }: ReportExporter) => boundToMercu getShortUrlVisits, getShortUrlDetail, cancelGetShortUrlVisits, - settings, }: ShortUrlVisitsProps) => { const { shortCode = '' } = useParams<{ shortCode: string }>(); const { search } = useLocation(); @@ -54,7 +52,6 @@ export const ShortUrlVisits = ({ exportVisits }: ReportExporter) => boundToMercu getVisits={loadVisits} cancelGetVisits={cancelGetShortUrlVisits} visitsInfo={shortUrlVisits} - settings={settings} exportCsv={exportCsv} > diff --git a/src/shlink-web-component/visits/TagVisits.tsx b/src/shlink-web-component/visits/TagVisits.tsx index 311fe5bf..a1839990 100644 --- a/src/shlink-web-component/visits/TagVisits.tsx +++ b/src/shlink-web-component/visits/TagVisits.tsx @@ -8,11 +8,10 @@ import { Topics } from '../mercure/helpers/Topics'; import type { LoadTagVisits, TagVisits as TagVisitsState } from './reducers/tagVisits'; import { TagVisitsHeader } from './TagVisitsHeader'; import type { NormalizedVisit } from './types'; -import type { CommonVisitsProps } from './types/CommonVisitsProps'; import { toApiParams } from './types/helpers'; import { VisitsStats } from './VisitsStats'; -export interface TagVisitsProps extends CommonVisitsProps { +export interface TagVisitsProps { getTagVisits: (params: LoadTagVisits) => void; tagVisits: TagVisitsState; cancelGetTagVisits: () => void; @@ -22,7 +21,6 @@ export const TagVisits = (colorGenerator: ColorGenerator, { exportVisits }: Repo getTagVisits, tagVisits, cancelGetTagVisits, - settings, }: TagVisitsProps) => { const goBack = useGoBack(); const { tag = '' } = useParams(); @@ -35,7 +33,6 @@ export const TagVisits = (colorGenerator: ColorGenerator, { exportVisits }: Repo getVisits={loadVisits} cancelGetVisits={cancelGetTagVisits} visitsInfo={tagVisits} - settings={settings} exportCsv={exportCsv} > diff --git a/src/shlink-web-component/visits/VisitsStats.tsx b/src/shlink-web-component/visits/VisitsStats.tsx index 022e1e77..b0c7bdd1 100644 --- a/src/shlink-web-component/visits/VisitsStats.tsx +++ b/src/shlink-web-component/visits/VisitsStats.tsx @@ -8,7 +8,6 @@ import { useEffect, useMemo, useRef, useState } from 'react'; import { Navigate, Route, Routes, useLocation } from 'react-router-dom'; import { Button, Progress, Row } from 'reactstrap'; import { ShlinkApiError } from '../../api/ShlinkApiError'; -import type { Settings } from '../../settings/reducers/settings'; import { DateRangeSelector } from '../../utils/dates/DateRangeSelector'; import { ExportBtn } from '../../utils/ExportBtn'; import type { DateInterval, DateRange } from '../../utils/helpers/dateIntervals'; @@ -17,6 +16,7 @@ import { prettify } from '../../utils/helpers/numbers'; import { Message } from '../../utils/Message'; import { NavPillItem, NavPills } from '../../utils/NavPills'; import { Result } from '../../utils/Result'; +import { useSetting } from '../utils/settings'; import { DoughnutChartCard } from './charts/DoughnutChartCard'; import { LineChartCard } from './charts/LineChartCard'; import { SortableBarChartCard } from './charts/SortableBarChartCard'; @@ -33,7 +33,6 @@ import { VisitsTable } from './VisitsTable'; export type VisitsStatsProps = PropsWithChildren<{ getVisits: (params: VisitsParams, doIntervalFallback?: boolean) => void; visitsInfo: VisitsInfo; - settings: Settings; cancelGetVisits: () => void; exportCsv: (visits: NormalizedVisit[]) => void; isOrphanVisits?: boolean; @@ -61,12 +60,12 @@ export const VisitsStats: FC = ({ visitsInfo, getVisits, cancelGetVisits, - settings, exportCsv, isOrphanVisits = false, }) => { const { visits, loading, loadingLarge, error, errorData, progress, fallbackInterval } = visitsInfo; const [{ dateRange, visitsFilter }, updateFiltering] = useVisitsQuery(); + const visitsSettings = useSetting('visits'); const setDates = pipe( ({ startDate: theStartDate, endDate: theEndDate }: DateRange) => ({ dateRange: { @@ -77,7 +76,7 @@ export const VisitsStats: FC = ({ updateFiltering, ); const initialInterval = useRef( - dateRange ?? fallbackInterval ?? settings.visits?.defaultInterval ?? 'last30Days', + dateRange ?? fallbackInterval ?? visitsSettings?.defaultInterval ?? 'last30Days', ); const [highlightedVisits, setHighlightedVisits] = useState([]); const [highlightedLabel, setHighlightedLabel] = useState(); @@ -92,7 +91,7 @@ export const VisitsStats: FC = ({ ); const resolvedFilter = useMemo(() => ({ ...visitsFilter, - excludeBots: visitsFilter.excludeBots ?? settings.visits?.excludeBots, + excludeBots: visitsFilter.excludeBots ?? visitsSettings?.excludeBots, }), [visitsFilter]); const mapLocations = values(citiesForMap); @@ -122,7 +121,7 @@ export const VisitsStats: FC = ({ }, [dateRange, visitsFilter]); useEffect(() => { // As soon as the fallback is loaded, if the initial interval used the settings one, we do fall back - if (fallbackInterval && initialInterval.current === (settings.visits?.defaultInterval ?? 'last30Days')) { + if (fallbackInterval && initialInterval.current === (visitsSettings?.defaultInterval ?? 'last30Days')) { initialInterval.current = fallbackInterval; } }, [fallbackInterval]); diff --git a/src/shlink-web-component/visits/services/provideServices.ts b/src/shlink-web-component/visits/services/provideServices.ts index 2da9abe1..53b0b670 100644 --- a/src/shlink-web-component/visits/services/provideServices.ts +++ b/src/shlink-web-component/visits/services/provideServices.ts @@ -1,6 +1,6 @@ import type Bottle from 'bottlejs'; import { prop } from 'ramda'; -import type { ConnectDecorator } from '../../../container/types'; +import type { ConnectDecorator } from '../../container'; import { DomainVisits } from '../DomainVisits'; import { MapModal } from '../helpers/MapModal'; import { NonOrphanVisits } from '../NonOrphanVisits'; @@ -22,31 +22,31 @@ export const provideServices = (bottle: Bottle, connect: ConnectDecorator) => { bottle.serviceFactory('ShortUrlVisits', ShortUrlVisits, 'ReportExporter'); bottle.decorator('ShortUrlVisits', connect( - ['shortUrlVisits', 'shortUrlDetail', 'mercureInfo', 'settings'], + ['shortUrlVisits', 'shortUrlDetail', 'mercureInfo'], ['getShortUrlVisits', 'getShortUrlDetail', 'cancelGetShortUrlVisits', 'createNewVisits', 'loadMercureInfo'], )); bottle.serviceFactory('TagVisits', TagVisits, 'ColorGenerator', 'ReportExporter'); bottle.decorator('TagVisits', connect( - ['tagVisits', 'mercureInfo', 'settings'], + ['tagVisits', 'mercureInfo'], ['getTagVisits', 'cancelGetTagVisits', 'createNewVisits', 'loadMercureInfo'], )); bottle.serviceFactory('DomainVisits', DomainVisits, 'ReportExporter'); bottle.decorator('DomainVisits', connect( - ['domainVisits', 'mercureInfo', 'settings'], + ['domainVisits', 'mercureInfo'], ['getDomainVisits', 'cancelGetDomainVisits', 'createNewVisits', 'loadMercureInfo'], )); bottle.serviceFactory('OrphanVisits', OrphanVisits, 'ReportExporter'); bottle.decorator('OrphanVisits', connect( - ['orphanVisits', 'mercureInfo', 'settings'], + ['orphanVisits', 'mercureInfo'], ['getOrphanVisits', 'cancelGetOrphanVisits', 'createNewVisits', 'loadMercureInfo'], )); bottle.serviceFactory('NonOrphanVisits', NonOrphanVisits, 'ReportExporter'); bottle.decorator('NonOrphanVisits', connect( - ['nonOrphanVisits', 'mercureInfo', 'settings'], + ['nonOrphanVisits', 'mercureInfo'], ['getNonOrphanVisits', 'cancelGetNonOrphanVisits', 'createNewVisits', 'loadMercureInfo'], )); diff --git a/src/shlink-web-component/visits/types/CommonVisitsProps.ts b/src/shlink-web-component/visits/types/CommonVisitsProps.ts deleted file mode 100644 index 1f569aaa..00000000 --- a/src/shlink-web-component/visits/types/CommonVisitsProps.ts +++ /dev/null @@ -1,5 +0,0 @@ -import type { Settings } from '../../../settings/reducers/settings'; - -export interface CommonVisitsProps { - settings: Settings; -}