Move settings from store to another context

This commit is contained in:
Alejandro Celaya 2023-07-23 18:30:59 +02:00
parent dddbc232c2
commit b3122219be
37 changed files with 293 additions and 263 deletions

View file

@ -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<MenuLayoutProps>(({ selectedServer, sidebarNotPresent, sidebarPresent }) => {
) => withSelectedServer<MenuLayoutProps>(({ selectedServer, sidebarNotPresent, sidebarPresent, settings }) => {
const showContent = isReachableServer(selectedServer);
useEffect(() => {
@ -28,6 +29,7 @@ export const MenuLayout = (
return (
<ShlinkWebComponent
serverVersion={selectedServer.version}
settings={settings}
routesPrefix={`/server/${selectedServer.id}`}
/>
);

View file

@ -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']));

View file

@ -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);

View file

@ -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;
}

View file

@ -7,30 +7,9 @@ import { serversReducer } from '../servers/reducers/servers';
import { settingsReducer } from '../settings/reducers/settings';
export const initReducers = (container: IContainer) => combineReducers<ShlinkState>({
// 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,
});

View file

@ -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;
};

View file

@ -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');

View file

@ -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;

View file

@ -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 })),
},
});

View file

@ -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<ShlinkWebComponentProps> => ({ routesPrefix = '', serverVersion }) => {
store: Store,
): FC<ShlinkWebComponentProps> => ({ 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 (
<FeaturesProvider value={features}>
<FontAwesomeIcon icon={burgerIcon} className={burgerClasses} onClick={toggleSidebar} />
<Provider store={store}>
<SettingsProvider value={settings}>
<FeaturesProvider value={features}>
<FontAwesomeIcon icon={burgerIcon} className={burgerClasses} onClick={toggleSidebar} />
<div {...swipeableProps} className="menu-layout__swipeable">
<div className="menu-layout__swipeable-inner">
<AsideMenu routePrefix={routesPrefix} showOnMobile={sidebarVisible} />
<div className="menu-layout__container" onClick={() => hideSidebar()}>
<div className="container-xl">
<Routes>
<Route index element={<Navigate replace to="overview" />} />
<Route path="/overview" element={<Overview />} />
<Route path="/list-short-urls/:page" element={<ShortUrlsList />} />
<Route path="/create-short-url" element={<CreateShortUrl />} />
<Route path="/short-code/:shortCode/visits/*" element={<ShortUrlVisits />} />
<Route path="/short-code/:shortCode/edit" element={<EditShortUrl />} />
<Route path="/tag/:tag/visits/*" element={<TagVisits />} />
{addDomainVisitsRoute && <Route path="/domain/:domain/visits/*" element={<DomainVisits />} />}
<Route path="/orphan-visits/*" element={<OrphanVisits />} />
{addNonOrphanVisitsRoute && <Route path="/non-orphan-visits/*" element={<NonOrphanVisits />} />}
<Route path="/manage-tags" element={<TagsList />} />
<Route path="/manage-domains" element={<ManageDomains />} />
<Route
path="*"
element={<NotFound to={`${routesPrefix}/list-short-urls/1`}>List short URLs</NotFound>}
/>
</Routes>
<div {...swipeableProps} className="menu-layout__swipeable">
<div className="menu-layout__swipeable-inner">
<AsideMenu routePrefix={routesPrefix} showOnMobile={sidebarVisible} />
<div className="menu-layout__container" onClick={() => hideSidebar()}>
<div className="container-xl">
<Routes>
<Route index element={<Navigate replace to="overview" />} />
<Route path="/overview" element={<Overview />} />
<Route path="/list-short-urls/:page" element={<ShortUrlsList />} />
<Route path="/create-short-url" element={<CreateShortUrl />} />
<Route path="/short-code/:shortCode/visits/*" element={<ShortUrlVisits />} />
<Route path="/short-code/:shortCode/edit" element={<EditShortUrl />} />
<Route path="/tag/:tag/visits/*" element={<TagVisits />} />
{addDomainVisitsRoute && <Route path="/domain/:domain/visits/*" element={<DomainVisits />} />}
<Route path="/orphan-visits/*" element={<OrphanVisits />} />
{addNonOrphanVisitsRoute && <Route path="/non-orphan-visits/*" element={<NonOrphanVisits />} />}
<Route path="/manage-tags" element={<TagsList />} />
<Route path="/manage-domains" element={<ManageDomains />} />
<Route
path="*"
element={<NotFound to={`${routesPrefix}/list-short-urls/1`}>List short URLs</NotFound>}
/>
</Routes>
</div>
</div>
</div>
</div>
</div>
</div>
</FeaturesProvider>
</FeaturesProvider>
</SettingsProvider>
</Provider>
);
};

View file

@ -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<string, Function>;
export type ConnectDecorator = (props: string[] | null, actions?: string[]) => any;
const bottle = new Bottle();
export const { container } = bottle;
const lazyService = <T extends Function, K>(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));

View file

@ -16,5 +16,6 @@ export const provideServices = (bottle: Bottle) => {
'Overview',
'EditShortUrl',
'ManageDomains',
'store',
);
};

View file

@ -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,

View file

@ -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';

View file

@ -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;

View file

@ -19,14 +19,15 @@ const initialState: MercureInfo = {
export const mercureInfoReducerCreator = (buildShlinkApiClient: ShlinkApiClientBuilder) => {
const loadMercureInfo = createAsyncThunk(
`${REDUCER_PREFIX}/loadMercureInfo`,
(_: void, { getState }): Promise<ShlinkMercureInfo> => {
const { settings } = getState();
if (!settings.realTimeUpdates.enabled) {
throw new Error('Real time updates not enabled');
}
(_: void, { getState }): Promise<ShlinkMercureInfo> =>
// 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({

View file

@ -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' } });

View file

@ -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'],
));
}

View file

@ -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<void>;
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 (

View file

@ -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<ShortUrlFormProps>) => ({
settings: { shortUrlCreation: shortUrlCreationSettings },
shortUrlDetail,
getShortUrlDetail,
shortUrlEdition,
@ -38,6 +36,7 @@ export const EditShortUrl = (ShortUrlForm: FC<ShortUrlFormProps>) => ({
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],

View file

@ -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<ExportShortUrlsBtnProps>,
TagsSelector: FC<TagsSelectorProps>,
): FC<ShortUrlsFilteringProps> => ({ className, shortUrlsAmount, order, handleOrderBy, settings }) => {
): FC<ShortUrlsFilteringProps> => ({ 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 = (
<ShortUrlsFilterDropdown
className="ms-0 ms-md-2 mt-3 mt-md-0"
selected={{
excludeBots: excludeBots ?? settings.visits?.excludeBots,
excludeBots: excludeBots ?? visitsSettings?.excludeBots,
excludeMaxVisitsReached,
excludePastValidUntil,
}}

View file

@ -5,7 +5,6 @@ import { Card } from 'reactstrap';
import type { ShlinkShortUrlsListParams, ShlinkShortUrlsOrder } from '../../api/types';
import type { SelectedServer } from '../../servers/data';
import { getServerId } from '../../servers/data';
import type { Settings } from '../../settings/reducers/settings';
import { DEFAULT_SHORT_URLS_ORDERING } from '../../settings/reducers/settings';
import type { OrderDir } from '../../utils/helpers/ordering';
import { determineOrderDir } from '../../utils/helpers/ordering';
@ -13,6 +12,7 @@ import { TableOrderIcon } from '../../utils/table/TableOrderIcon';
import { boundToMercureHub } from '../mercure/helpers/boundToMercureHub';
import { Topics } from '../mercure/helpers/Topics';
import { useFeature } from '../utils/features';
import { useSettings } from '../utils/settings';
import type { ShortUrlsOrder, ShortUrlsOrderableFields } from './data';
import { useShortUrlsQuery } from './helpers/hooks';
import { Paginator } from './Paginator';
@ -24,17 +24,17 @@ interface ShortUrlsListProps {
selectedServer: SelectedServer;
shortUrlsList: ShortUrlsListState;
listShortUrls: (params: ShlinkShortUrlsListParams) => 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"
/>
<Card body className="pb-0">

View file

@ -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<ShortUrlsRowProps>;
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(() => {

View file

@ -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';

View file

@ -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'],
));

View file

@ -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<TagsTableProps>) => boundToMercureHub((
{ filterTags, forceListTags, tagsList, selectedServer, settings }: TagsListProps,
{ filterTags, forceListTags, tagsList, selectedServer }: TagsListProps,
) => {
const settings = useSettings();
const [order, setOrder] = useState<TagsOrder>(settings.tags?.defaultOrdering ?? {});
const resolveSortedTags = pipe(
() => tagsList.filteredTags.map((tag): SimplifiedTag => {

View file

@ -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) =>
<Tag colorGenerator={colorGenerator} text={tag.name} clearable className="react-tags__tag" onClose={onDelete} />;
const ReactTagsSuggestion = ({ item }: SuggestionComponentProps) => (

View file

@ -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'],
));

View file

@ -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<Settings | undefined>(defaultSettings);
export const SettingsProvider = SettingsContext.Provider;
export const useSettings = (): Settings => useContext(SettingsContext) ?? defaultSettings;
export const useSetting = <T extends keyof Settings>(settingName: T): Settings[T] => {
const settings = useSettings();
return settings[settingName];
};

View file

@ -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}
>
<VisitsHeader goBack={goBack} visits={domainVisits.visits} title={`"${authority}" visits`} />

View file

@ -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}
>
<VisitsHeader title="Non-orphan visits" goBack={goBack} visits={nonOrphanVisits.visits} />

View file

@ -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
>

View file

@ -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}
>
<ShortUrlVisitsHeader shortUrlDetail={shortUrlDetail} shortUrlVisits={shortUrlVisits} goBack={goBack} />

View file

@ -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}
>
<TagVisitsHeader tagVisits={tagVisits} goBack={goBack} colorGenerator={colorGenerator} />

View file

@ -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<VisitsStatsProps> = ({
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<VisitsStatsProps> = ({
updateFiltering,
);
const initialInterval = useRef<DateRange | DateInterval>(
dateRange ?? fallbackInterval ?? settings.visits?.defaultInterval ?? 'last30Days',
dateRange ?? fallbackInterval ?? visitsSettings?.defaultInterval ?? 'last30Days',
);
const [highlightedVisits, setHighlightedVisits] = useState<NormalizedVisit[]>([]);
const [highlightedLabel, setHighlightedLabel] = useState<string | undefined>();
@ -92,7 +91,7 @@ export const VisitsStats: FC<VisitsStatsProps> = ({
);
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<VisitsStatsProps> = ({
}, [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]);

View file

@ -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'],
));

View file

@ -1,5 +0,0 @@
import type { Settings } from '../../../settings/reducers/settings';
export interface CommonVisitsProps {
settings: Settings;
}