From e9951e95a9eaef46ecc4c37458704c400beb2d24 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Fri, 14 Nov 2025 08:24:58 +0100 Subject: [PATCH] Set everything up to use hooks for reduc actions and state --- src/app/App.tsx | 8 +++--- src/container/index.ts | 2 -- src/container/store.ts | 7 +++-- src/index.tsx | 2 +- src/reducers/index.ts | 6 ++--- src/servers/reducers/selectedServer.ts | 11 +++++--- src/servers/services/provideServices.ts | 10 +------ src/settings/Settings.tsx | 28 +++++++++----------- src/settings/reducers/settings.ts | 11 ++++++++ src/settings/services/provideServices.ts | 15 ----------- src/tailwind.css | 2 -- test/__helpers__/setUpTest.ts | 8 ------ test/__helpers__/setUpTest.tsx | 28 ++++++++++++++++++++ test/app/App.test.tsx | 10 +++---- test/servers/reducers/selectedServer.test.ts | 3 +-- test/settings/Settings.test.tsx | 6 ++--- 16 files changed, 79 insertions(+), 78 deletions(-) delete mode 100644 src/settings/services/provideServices.ts delete mode 100644 test/__helpers__/setUpTest.ts create mode 100644 test/__helpers__/setUpTest.tsx diff --git a/src/app/App.tsx b/src/app/App.tsx index 6ee98cad..c7025679 100644 --- a/src/app/App.tsx +++ b/src/app/App.tsx @@ -1,5 +1,5 @@ import { changeThemeInMarkup, getSystemPreferredTheme } from '@shlinkio/shlink-frontend-kit'; -import type { Settings } from '@shlinkio/shlink-web-component/settings'; +import type { Settings as AppSettings } from '@shlinkio/shlink-web-component/settings'; import { clsx } from 'clsx'; import type { FC } from 'react'; import { useEffect, useRef } from 'react'; @@ -9,12 +9,13 @@ import { NotFound } from '../common/NotFound'; import type { FCWithDeps } from '../container/utils'; import { componentFactory, useDependencies } from '../container/utils'; import type { ServersMap } from '../servers/data'; +import { Settings } from '../settings/Settings'; import { forceUpdate } from '../utils/helpers/sw'; type AppProps = { fetchServers: () => void; servers: ServersMap; - settings: Settings; + settings: AppSettings; resetAppUpdate: () => void; appUpdated: boolean; }; @@ -25,7 +26,6 @@ type AppDeps = { ShlinkWebComponentContainer: FC; CreateServer: FC; EditServer: FC; - Settings: FC; ManageServers: FC; ShlinkVersionsContainer: FC; }; @@ -39,7 +39,6 @@ const App: FCWithDeps = ( ShlinkWebComponentContainer, CreateServer, EditServer, - Settings, ManageServers, ShlinkVersionsContainer, } = useDependencies(App); @@ -105,7 +104,6 @@ export const AppFactory = componentFactory(App, [ 'ShlinkWebComponentContainer', 'CreateServer', 'EditServer', - 'Settings', 'ManageServers', 'ShlinkVersionsContainer', ]); diff --git a/src/container/index.ts b/src/container/index.ts index 4c5e6cb2..085f919a 100644 --- a/src/container/index.ts +++ b/src/container/index.ts @@ -5,7 +5,6 @@ import { provideServices as provideApiServices } from '../api/services/provideSe import { provideServices as provideAppServices } from '../app/services/provideServices'; 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 provideUtilsServices } from '../utils/services/provideServices'; import type { ConnectDecorator } from './types'; @@ -39,4 +38,3 @@ provideCommonServices(bottle, connect); provideApiServices(bottle); provideServersServices(bottle, connect); provideUtilsServices(bottle); -provideSettingsServices(bottle, connect); diff --git a/src/container/store.ts b/src/container/store.ts index 3ae1572b..f2d738ef 100644 --- a/src/container/store.ts +++ b/src/container/store.ts @@ -1,5 +1,4 @@ import { configureStore } from '@reduxjs/toolkit'; -import type { IContainer } from 'bottlejs'; import type { RLSOptions } from 'redux-localstorage-simple'; import { load, save } from 'redux-localstorage-simple'; import { initReducers } from '../reducers'; @@ -13,11 +12,11 @@ const localStorageConfig: RLSOptions = { namespaceSeparator: '.', debounce: 300, }; -const preloadedState = migrateDeprecatedSettings(load(localStorageConfig) as ShlinkState); +const getStateFromLocalStorage = () => migrateDeprecatedSettings(load(localStorageConfig) as ShlinkState); -export const setUpStore = (container: IContainer) => configureStore({ +export const setUpStore = (preloadedState = getStateFromLocalStorage()) => configureStore({ devTools: !isProduction, - reducer: initReducers(container), + reducer: initReducers(), preloadedState, middleware: (defaultMiddlewaresIncludingReduxThunk) => defaultMiddlewaresIncludingReduxThunk({ immutableCheck: false, serializableCheck: false }) // State is too big for these diff --git a/src/index.tsx b/src/index.tsx index b4f69f5d..a0c986bf 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -7,7 +7,7 @@ import { setUpStore } from './container/store'; import { register as registerServiceWorker } from './serviceWorkerRegistration'; import './tailwind.css'; -const store = setUpStore(container); +const store = setUpStore(); const { App, ScrollToTop, ErrorHandler, appUpdateAvailable } = container; createRoot(document.getElementById('root')!).render( diff --git a/src/reducers/index.ts b/src/reducers/index.ts index 1a88d01d..09f2218a 100644 --- a/src/reducers/index.ts +++ b/src/reducers/index.ts @@ -1,12 +1,12 @@ import { combineReducers } from '@reduxjs/toolkit'; -import type { IContainer } from 'bottlejs'; import { appUpdatesReducer } from '../app/reducers/appUpdates'; +import { selectedServerReducer } from '../servers/reducers/selectedServer'; import { serversReducer } from '../servers/reducers/servers'; import { settingsReducer } from '../settings/reducers/settings'; -export const initReducers = (container: IContainer) => combineReducers({ +export const initReducers = () => combineReducers({ appUpdated: appUpdatesReducer, servers: serversReducer, - selectedServer: container.selectedServerReducer, + selectedServer: selectedServerReducer, settings: settingsReducer, }); diff --git a/src/servers/reducers/selectedServer.ts b/src/servers/reducers/selectedServer.ts index 512ddb4d..b5d94dbb 100644 --- a/src/servers/reducers/selectedServer.ts +++ b/src/servers/reducers/selectedServer.ts @@ -56,14 +56,17 @@ export const selectServer = (buildShlinkApiClient: ShlinkApiClientBuilder) => cr }, ); -type SelectServerThunk = ReturnType; - -export const selectedServerReducerCreator = (selectServerThunk: SelectServerThunk) => createSlice({ +const { reducer } = createSlice({ name: REDUCER_PREFIX, initialState, reducers: {}, extraReducers: (builder) => { builder.addCase(resetSelectedServer, () => initialState); - builder.addCase(selectServerThunk.fulfilled, (_, { payload }) => payload as any); + builder.addCase( + `${REDUCER_PREFIX}/selectServer/fulfilled`, + (_, { payload }: { payload: SelectedServer }) => payload, + ); }, }); + +export const selectedServerReducer = reducer; diff --git a/src/servers/services/provideServices.ts b/src/servers/services/provideServices.ts index 4a2d1ab3..2e33a649 100644 --- a/src/servers/services/provideServices.ts +++ b/src/servers/services/provideServices.ts @@ -11,11 +11,7 @@ import { ManageServersFactory } from '../ManageServers'; import { ManageServersRowFactory } from '../ManageServersRow'; import { ManageServersRowDropdownFactory } from '../ManageServersRowDropdown'; import { fetchServers } from '../reducers/remoteServers'; -import { - resetSelectedServer, - selectedServerReducerCreator, - selectServer, -} from '../reducers/selectedServer'; +import { resetSelectedServer, selectServer } from '../reducers/selectedServer'; import { createServers, deleteServer, editServer, setAutoConnect } from '../reducers/servers'; import { ServersDropdown } from '../ServersDropdown'; import { ServersExporter } from './ServersExporter'; @@ -66,8 +62,4 @@ export const provideServices = (bottle: Bottle, connect: ConnectDecorator) => { bottle.serviceFactory('fetchServers', fetchServers, 'HttpClient'); bottle.serviceFactory('resetSelectedServer', () => resetSelectedServer); - - // Reducers - bottle.serviceFactory('selectedServerReducerCreator', selectedServerReducerCreator, 'selectServer'); - bottle.serviceFactory('selectedServerReducer', (obj) => obj.reducer, 'selectedServerReducerCreator'); }; diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx index 28a0a75b..6a508fe3 100644 --- a/src/settings/Settings.tsx +++ b/src/settings/Settings.tsx @@ -1,20 +1,18 @@ -import type { Settings as AppSettings } from '@shlinkio/shlink-web-component/settings'; import { ShlinkWebSettings } from '@shlinkio/shlink-web-component/settings'; import type { FC } from 'react'; import { NoMenuLayout } from '../common/NoMenuLayout'; -import { DEFAULT_SHORT_URLS_ORDERING } from './reducers/settings'; +import { DEFAULT_SHORT_URLS_ORDERING, useSettings } from './reducers/settings'; -export type SettingsProps = { - settings: AppSettings; - setSettings: (newSettings: AppSettings) => void; +export const Settings: FC = () => { + const { settings, setSettings } = useSettings(); + + return ( + + + + ); }; - -export const Settings: FC = ({ settings, setSettings }) => ( - - - -); diff --git a/src/settings/reducers/settings.ts b/src/settings/reducers/settings.ts index 33d9162c..f71a1dd2 100644 --- a/src/settings/reducers/settings.ts +++ b/src/settings/reducers/settings.ts @@ -3,6 +3,9 @@ import { createSlice } from '@reduxjs/toolkit'; import { mergeDeepRight } from '@shlinkio/data-manipulation'; import { getSystemPreferredTheme } from '@shlinkio/shlink-frontend-kit'; import type { Settings, ShortUrlsListSettings } from '@shlinkio/shlink-web-component/settings'; +import { useCallback } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import type { ShlinkState } from '../../container/types'; import type { Defined } from '../../utils/types'; type ShortUrlsOrder = Defined; @@ -41,3 +44,11 @@ const { reducer, actions } = createSlice({ export const { setSettings } = actions; export const settingsReducer = reducer; + +export const useSettings = () => { + const dispatch = useDispatch(); + const setSettings = useCallback((settings: Settings) => dispatch(actions.setSettings(settings)), [dispatch]); + const settings = useSelector((state: ShlinkState) => state.settings); + + return { settings, setSettings }; +}; diff --git a/src/settings/services/provideServices.ts b/src/settings/services/provideServices.ts deleted file mode 100644 index 246bed83..00000000 --- a/src/settings/services/provideServices.ts +++ /dev/null @@ -1,15 +0,0 @@ -import type Bottle from 'bottlejs'; -import type { ConnectDecorator } from '../../container/types'; -import { withoutSelectedServer } from '../../servers/helpers/withoutSelectedServer'; -import { setSettings } from '../reducers/settings'; -import { Settings } from '../Settings'; - -export const provideServices = (bottle: Bottle, connect: ConnectDecorator) => { - // Components - bottle.serviceFactory('Settings', () => Settings); - bottle.decorator('Settings', withoutSelectedServer); - bottle.decorator('Settings', connect(['settings'], ['setSettings', 'resetSelectedServer'])); - - // Actions - bottle.serviceFactory('setSettings', () => setSettings); -}; diff --git a/src/tailwind.css b/src/tailwind.css index d1ffef4d..fe52bc17 100644 --- a/src/tailwind.css +++ b/src/tailwind.css @@ -8,7 +8,5 @@ :root { --footer-height: 2.3rem; --footer-margin: .8rem; - /* FIXME Remove this once updated to shlink-web-component 0.15.1 */ - --header-height: 52px; } } diff --git a/test/__helpers__/setUpTest.ts b/test/__helpers__/setUpTest.ts deleted file mode 100644 index 7fccbf98..00000000 --- a/test/__helpers__/setUpTest.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { render } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; -import type { ReactElement } from 'react'; - -export const renderWithEvents = (element: ReactElement) => ({ - user: userEvent.setup(), - ...render(element), -}); diff --git a/test/__helpers__/setUpTest.tsx b/test/__helpers__/setUpTest.tsx new file mode 100644 index 00000000..4a39c271 --- /dev/null +++ b/test/__helpers__/setUpTest.tsx @@ -0,0 +1,28 @@ +import type { RenderOptions } from '@testing-library/react'; +import { render } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import type { PropsWithChildren, ReactElement } from 'react'; +import { Provider } from 'react-redux'; +import { setUpStore } from '../../src/container/store'; +import type { ShlinkState } from '../../src/container/types'; + +export const renderWithEvents = (element: ReactElement, options?: RenderOptions) => ({ + user: userEvent.setup(), + ...render(element, options), +}); + +export type RenderOptionsWithState = Omit & { + initialState?: Partial; +}; + +export const renderWithStore = ( + element: ReactElement, + { initialState = {}, ...options }: RenderOptionsWithState = {}, +) => { + const Wrapper = ({ children }: PropsWithChildren) => ( + + {children} + + ); + return renderWithEvents(element, { ...options, wrapper: Wrapper }); +}; diff --git a/test/app/App.test.tsx b/test/app/App.test.tsx index 51aef3ae..0a0e099c 100644 --- a/test/app/App.test.tsx +++ b/test/app/App.test.tsx @@ -1,8 +1,9 @@ -import { act, render, screen } from '@testing-library/react'; +import { act, screen } from '@testing-library/react'; import { fromPartial } from '@total-typescript/shoehorn'; import { MemoryRouter } from 'react-router'; import { AppFactory } from '../../src/app/App'; import { checkAccessibility } from '../__helpers__/accessibility'; +import { renderWithStore } from '../__helpers__/setUpTest'; describe('', () => { const App = AppFactory( @@ -12,12 +13,11 @@ describe('', () => { ShlinkWebComponentContainer: () => <>ShlinkWebComponentContainer, CreateServer: () => <>CreateServer, EditServer: () => <>EditServer, - Settings: () => <>SettingsComp, ManageServers: () => <>ManageServers, ShlinkVersionsContainer: () => <>ShlinkVersions, }), ); - const setUp = async (activeRoute = '/') => act(() => render( + const setUp = async (activeRoute = '/') => act(() => renderWithStore( {}} @@ -39,8 +39,8 @@ describe('', () => { }); it.each([ - ['/settings/foo', 'SettingsComp'], - ['/settings/bar', 'SettingsComp'], + ['/settings/general', 'User interface'], + ['/settings/short-urls', 'Short URLs form'], ['/manage-servers', 'ManageServers'], ['/server/create', 'CreateServer'], ['/server/abc123/edit', 'EditServer'], diff --git a/test/servers/reducers/selectedServer.test.ts b/test/servers/reducers/selectedServer.test.ts index 8c602052..36720fe4 100644 --- a/test/servers/reducers/selectedServer.test.ts +++ b/test/servers/reducers/selectedServer.test.ts @@ -6,7 +6,7 @@ import { MAX_FALLBACK_VERSION, MIN_FALLBACK_VERSION, resetSelectedServer, - selectedServerReducerCreator, + selectedServerReducer as reducer, selectServer as selectServerCreator, } from '../../../src/servers/reducers/selectedServer'; @@ -15,7 +15,6 @@ describe('selectedServerReducer', () => { const health = vi.fn(); const buildApiClient = vi.fn().mockReturnValue(fromPartial({ health })); const selectServer = selectServerCreator(buildApiClient); - const { reducer } = selectedServerReducerCreator(selectServer); describe('reducer', () => { it('returns default when action is RESET_SELECTED_SERVER', () => diff --git a/test/settings/Settings.test.tsx b/test/settings/Settings.test.tsx index a1af73f1..2d786f02 100644 --- a/test/settings/Settings.test.tsx +++ b/test/settings/Settings.test.tsx @@ -1,12 +1,12 @@ -import { render } from '@testing-library/react'; import { MemoryRouter } from 'react-router'; import { Settings } from '../../src/settings/Settings'; import { checkAccessibility } from '../__helpers__/accessibility'; +import { renderWithStore } from '../__helpers__/setUpTest'; describe('', () => { - const setUp = () => render( + const setUp = () => renderWithStore( - + , );