Do not inject settings state or actions

This commit is contained in:
Alejandro Celaya
2025-11-14 23:29:59 +01:00
parent 9e8498b16a
commit 6094994cfa
9 changed files with 19 additions and 29 deletions

View File

@@ -6,8 +6,6 @@ import type { GetState } from '../../store';
const apiClients: Map<string, ShlinkApiClient> = new Map(); const apiClients: Map<string, ShlinkApiClient> = new Map();
const isGetState = (getStateOrSelectedServer: GetState | ServerWithId): getStateOrSelectedServer is GetState =>
typeof getStateOrSelectedServer === 'function';
const getSelectedServerFromState = (getState: GetState): ServerWithId => { const getSelectedServerFromState = (getState: GetState): ServerWithId => {
const { selectedServer } = getState(); const { selectedServer } = getState();
if (!hasServerData(selectedServer)) { if (!hasServerData(selectedServer)) {
@@ -18,7 +16,7 @@ const getSelectedServerFromState = (getState: GetState): ServerWithId => {
}; };
export const buildShlinkApiClient = (httpClient: HttpClient) => (getStateOrSelectedServer: GetState | ServerWithId) => { export const buildShlinkApiClient = (httpClient: HttpClient) => (getStateOrSelectedServer: GetState | ServerWithId) => {
const { url: baseUrl, apiKey, forwardCredentials } = isGetState(getStateOrSelectedServer) const { url: baseUrl, apiKey, forwardCredentials } = typeof getStateOrSelectedServer === 'function'
? getSelectedServerFromState(getStateOrSelectedServer) ? getSelectedServerFromState(getStateOrSelectedServer)
: getStateOrSelectedServer; : getStateOrSelectedServer;
const serverKey = `${apiKey}_${baseUrl}_${forwardCredentials ? 'forward' : 'no-forward'}`; const serverKey = `${apiKey}_${baseUrl}_${forwardCredentials ? 'forward' : 'no-forward'}`;
@@ -34,6 +32,7 @@ export const buildShlinkApiClient = (httpClient: HttpClient) => (getStateOrSelec
{ requestCredentials: forwardCredentials ? 'include' : undefined }, { requestCredentials: forwardCredentials ? 'include' : undefined },
); );
apiClients.set(serverKey, apiClient); apiClients.set(serverKey, apiClient);
return apiClient; return apiClient;
}; };

View File

@@ -1,6 +1,5 @@
import { changeThemeInMarkup, getSystemPreferredTheme } from '@shlinkio/shlink-frontend-kit'; import { changeThemeInMarkup, getSystemPreferredTheme } from '@shlinkio/shlink-frontend-kit';
import type { HttpClient } from '@shlinkio/shlink-js-sdk'; import type { HttpClient } from '@shlinkio/shlink-js-sdk';
import type { Settings as AppSettings } from '@shlinkio/shlink-web-component/settings';
import { clsx } from 'clsx'; import { clsx } from 'clsx';
import type { FC } from 'react'; import type { FC } from 'react';
import { useEffect } from 'react'; import { useEffect } from 'react';
@@ -13,11 +12,11 @@ import type { FCWithDeps } from '../container/utils';
import { componentFactory, useDependencies } from '../container/utils'; import { componentFactory, useDependencies } from '../container/utils';
import { EditServer } from '../servers/EditServer'; import { EditServer } from '../servers/EditServer';
import { useLoadRemoteServers } from '../servers/reducers/remoteServers'; import { useLoadRemoteServers } from '../servers/reducers/remoteServers';
import { useSettings } from '../settings/reducers/settings';
import { Settings } from '../settings/Settings'; import { Settings } from '../settings/Settings';
import { forceUpdate } from '../utils/helpers/sw'; import { forceUpdate } from '../utils/helpers/sw';
export type AppProps = { export type AppProps = {
settings: AppSettings;
resetAppUpdate: () => void; resetAppUpdate: () => void;
appUpdated: boolean; appUpdated: boolean;
}; };
@@ -30,7 +29,7 @@ type AppDeps = {
HttpClient: HttpClient; HttpClient: HttpClient;
}; };
const App: FCWithDeps<AppProps, AppDeps> = ({ settings, appUpdated, resetAppUpdate }) => { const App: FCWithDeps<AppProps, AppDeps> = ({ appUpdated, resetAppUpdate }) => {
const { const {
Home, Home,
ShlinkWebComponentContainer, ShlinkWebComponentContainer,
@@ -44,6 +43,7 @@ const App: FCWithDeps<AppProps, AppDeps> = ({ settings, appUpdated, resetAppUpda
const location = useLocation(); const location = useLocation();
const isHome = location.pathname === '/'; const isHome = location.pathname === '/';
const { settings } = useSettings();
useEffect(() => { useEffect(() => {
changeThemeInMarkup(settings.ui?.theme ?? getSystemPreferredTheme()); changeThemeInMarkup(settings.ui?.theme ?? getSystemPreferredTheme());
}, [settings.ui?.theme]); }, [settings.ui?.theme]);

View File

@@ -6,7 +6,7 @@ import { appUpdateAvailable, resetAppUpdate } from '../reducers/appUpdates';
export const provideServices = (bottle: Bottle, connect: ConnectDecorator) => { export const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
// Components // Components
bottle.factory('App', AppFactory); bottle.factory('App', AppFactory);
bottle.decorator('App', connect(['settings', 'appUpdated'], ['resetAppUpdate'])); bottle.decorator('App', connect(['appUpdated'], ['resetAppUpdate']));
// Actions // Actions
bottle.serviceFactory('appUpdateAvailable', () => appUpdateAvailable); bottle.serviceFactory('appUpdateAvailable', () => appUpdateAvailable);

View File

@@ -4,7 +4,6 @@ import {
ShlinkSidebarVisibilityProvider, ShlinkSidebarVisibilityProvider,
ShlinkWebComponent, ShlinkWebComponent,
} from '@shlinkio/shlink-web-component'; } from '@shlinkio/shlink-web-component';
import type { Settings } from '@shlinkio/shlink-web-component/settings';
import { memo } from 'react'; import { memo } from 'react';
import type { FCWithDeps } from '../container/utils'; import type { FCWithDeps } from '../container/utils';
import { componentFactory, useDependencies } from '../container/utils'; import { componentFactory, useDependencies } from '../container/utils';
@@ -13,29 +12,27 @@ import { ServerError } from '../servers/helpers/ServerError';
import type { WithSelectedServerPropsDeps } from '../servers/helpers/withSelectedServer'; import type { WithSelectedServerPropsDeps } from '../servers/helpers/withSelectedServer';
import { withSelectedServer } from '../servers/helpers/withSelectedServer'; import { withSelectedServer } from '../servers/helpers/withSelectedServer';
import { useSelectedServer } from '../servers/reducers/selectedServer'; import { useSelectedServer } from '../servers/reducers/selectedServer';
import { useSettings } from '../settings/reducers/settings';
import { NotFound } from './NotFound'; import { NotFound } from './NotFound';
type ShlinkWebComponentContainerProps = {
settings: Settings;
};
type ShlinkWebComponentContainerDeps = WithSelectedServerPropsDeps & { type ShlinkWebComponentContainerDeps = WithSelectedServerPropsDeps & {
TagColorsStorage: TagColorsStorage, TagColorsStorage: TagColorsStorage,
}; };
const ShlinkWebComponentContainer: FCWithDeps< const ShlinkWebComponentContainer: FCWithDeps<
ShlinkWebComponentContainerProps, any,
ShlinkWebComponentContainerDeps ShlinkWebComponentContainerDeps
// FIXME Using `memo` here to solve a flickering effect in charts. // FIXME Using `memo` here to solve a flickering effect in charts.
// memo is probably not the right solution. The root cause is the withSelectedServer HOC, but I couldn't fix the // memo is probably not the right solution. The root cause is the withSelectedServer HOC, but I couldn't fix the
// extra rendering there. // extra rendering there.
// This should be revisited at some point. // This should be revisited at some point.
> = withSelectedServer(memo(({ settings }) => { > = withSelectedServer(memo(() => {
const { const {
buildShlinkApiClient, buildShlinkApiClient,
TagColorsStorage: tagColorsStorage, TagColorsStorage: tagColorsStorage,
} = useDependencies(ShlinkWebComponentContainer); } = useDependencies(ShlinkWebComponentContainer);
const { selectedServer } = useSelectedServer(); const { selectedServer } = useSelectedServer();
const { settings } = useSettings();
if (!isReachableServer(selectedServer)) { if (!isReachableServer(selectedServer)) {
return <ServerError />; return <ServerError />;

View File

@@ -1,27 +1,18 @@
import { FetchHttpClient } from '@shlinkio/shlink-js-sdk/fetch'; import { FetchHttpClient } from '@shlinkio/shlink-js-sdk/fetch';
import type Bottle from 'bottlejs'; import type Bottle from 'bottlejs';
import type { ConnectDecorator } from '../../container/types';
import { withoutSelectedServer } from '../../servers/helpers/withoutSelectedServer'; import { withoutSelectedServer } from '../../servers/helpers/withoutSelectedServer';
import { ErrorHandler } from '../ErrorHandler';
import { Home } from '../Home'; import { Home } from '../Home';
import { ScrollToTop } from '../ScrollToTop';
import { ShlinkWebComponentContainerFactory } from '../ShlinkWebComponentContainer'; import { ShlinkWebComponentContainerFactory } from '../ShlinkWebComponentContainer';
export const provideServices = (bottle: Bottle, connect: ConnectDecorator) => { export const provideServices = (bottle: Bottle) => {
// Services // Services
bottle.constant('window', window); bottle.constant('window', window);
bottle.constant('console', console); bottle.constant('console', console);
bottle.constant('fetch', window.fetch.bind(window)); bottle.constant('fetch', window.fetch.bind(window));
bottle.service('HttpClient', FetchHttpClient, 'fetch'); bottle.service('HttpClient', FetchHttpClient, 'fetch');
// Components
bottle.serviceFactory('ScrollToTop', () => ScrollToTop);
bottle.serviceFactory('Home', () => Home); bottle.serviceFactory('Home', () => Home);
bottle.decorator('Home', withoutSelectedServer); bottle.decorator('Home', withoutSelectedServer);
bottle.factory('ShlinkWebComponentContainer', ShlinkWebComponentContainerFactory); bottle.factory('ShlinkWebComponentContainer', ShlinkWebComponentContainerFactory);
bottle.decorator('ShlinkWebComponentContainer', connect(['settings'], []));
bottle.serviceFactory('ErrorHandler', () => ErrorHandler);
}; };

View File

@@ -34,7 +34,7 @@ const connect: ConnectDecorator = (propsFromState: string[] | null, actionServic
); );
provideAppServices(bottle, connect); provideAppServices(bottle, connect);
provideCommonServices(bottle, connect); provideCommonServices(bottle);
provideApiServices(bottle); provideApiServices(bottle);
provideServersServices(bottle); provideServersServices(bottle);
provideUtilsServices(bottle); provideUtilsServices(bottle);

View File

@@ -2,13 +2,15 @@ import { createRoot } from 'react-dom/client';
import { Provider } from 'react-redux'; import { Provider } from 'react-redux';
import { BrowserRouter } from 'react-router'; import { BrowserRouter } from 'react-router';
import pack from '../package.json'; import pack from '../package.json';
import { ErrorHandler } from './common/ErrorHandler';
import { ScrollToTop } from './common/ScrollToTop';
import { container } from './container'; import { container } from './container';
import { register as registerServiceWorker } from './serviceWorkerRegistration'; import { register as registerServiceWorker } from './serviceWorkerRegistration';
import { setUpStore } from './store'; import { setUpStore } from './store';
import './tailwind.css'; import './tailwind.css';
const store = setUpStore(); const store = setUpStore();
const { App, ScrollToTop, ErrorHandler, appUpdateAvailable } = container; const { App, appUpdateAvailable } = container;
createRoot(document.getElementById('root')!).render( createRoot(document.getElementById('root')!).render(
<Provider store={store}> <Provider store={store}>

View File

@@ -19,7 +19,7 @@ describe('<App />', () => {
); );
const setUp = async (activeRoute = '/') => act(() => renderWithStore( const setUp = async (activeRoute = '/') => act(() => renderWithStore(
<MemoryRouter initialEntries={[{ pathname: activeRoute }]}> <MemoryRouter initialEntries={[{ pathname: activeRoute }]}>
<App settings={fromPartial({})} appUpdated={false} resetAppUpdate={() => {}} /> <App appUpdated={false} resetAppUpdate={() => {}} />
</MemoryRouter>, </MemoryRouter>,
{ {
initialState: { initialState: {
@@ -27,6 +27,7 @@ describe('<App />', () => {
abc123: fromPartial<ServerWithId>({ id: 'abc123', name: 'abc123 server' }), abc123: fromPartial<ServerWithId>({ id: 'abc123', name: 'abc123 server' }),
def456: fromPartial<ServerWithId>({ id: 'def456', name: 'def456 server' }), def456: fromPartial<ServerWithId>({ id: 'def456', name: 'def456 server' }),
}, },
settings: fromPartial({}),
}, },
}, },
)); ));

View File

@@ -19,10 +19,10 @@ describe('<ShlinkWebComponentContainer />', () => {
})); }));
const setUp = (selectedServer: SelectedServer) => renderWithStore( const setUp = (selectedServer: SelectedServer) => renderWithStore(
<MemoryRouter> <MemoryRouter>
<ShlinkWebComponentContainer settings={{}} /> <ShlinkWebComponentContainer />
</MemoryRouter>, </MemoryRouter>,
{ {
initialState: { selectedServer, servers: {} }, initialState: { selectedServer, servers: {}, settings: {} },
}, },
); );