From 9c1052c10b9e4f761bb387ec77b7855e17f51ba4 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Fri, 14 Nov 2025 10:27:49 +0100 Subject: [PATCH] Replace usage of injected selectedServer with useSelectedServer --- src/common/ShlinkVersionsContainer.tsx | 21 ++++++++--------- src/common/ShlinkWebComponentContainer.tsx | 8 ++++--- src/common/services/provideServices.ts | 3 +-- src/servers/EditServer.tsx | 10 ++++---- src/servers/ServersDropdown.tsx | 7 +++--- src/servers/helpers/ServerError.tsx | 7 +++--- src/servers/helpers/withSelectedServer.tsx | 14 ++++------- src/servers/reducers/selectedServer.ts | 4 +--- src/servers/services/provideServices.ts | 10 ++++---- test/__helpers__/MemoryRouterWithParams.tsx | 23 ------------------- test/common/ShlinkVersionsContainer.test.tsx | 9 +++++--- .../ShlinkWebComponentContainer.test.tsx | 13 ++++++----- test/servers/EditServer.test.tsx | 9 +++++--- test/servers/ServersDropdown.test.tsx | 9 +++++--- test/servers/helpers/ServerError.test.tsx | 10 +++++--- 15 files changed, 71 insertions(+), 86 deletions(-) delete mode 100644 test/__helpers__/MemoryRouterWithParams.tsx diff --git a/src/common/ShlinkVersionsContainer.tsx b/src/common/ShlinkVersionsContainer.tsx index c80eb698..ebba6966 100644 --- a/src/common/ShlinkVersionsContainer.tsx +++ b/src/common/ShlinkVersionsContainer.tsx @@ -1,16 +1,15 @@ import { clsx } from 'clsx'; -import type { SelectedServer } from '../servers/data'; import { isReachableServer } from '../servers/data'; +import { useSelectedServer } from '../servers/reducers/selectedServer'; import { ShlinkVersions } from './ShlinkVersions'; -export type ShlinkVersionsContainerProps = { - selectedServer: SelectedServer; +export const ShlinkVersionsContainer = () => { + const { selectedServer } = useSelectedServer(); + return ( +
+ +
+ ); }; - -export const ShlinkVersionsContainer = ({ selectedServer }: ShlinkVersionsContainerProps) => ( -
- -
-); diff --git a/src/common/ShlinkWebComponentContainer.tsx b/src/common/ShlinkWebComponentContainer.tsx index 5d87bf0a..0484d23c 100644 --- a/src/common/ShlinkWebComponentContainer.tsx +++ b/src/common/ShlinkWebComponentContainer.tsx @@ -9,11 +9,12 @@ import { memo } from 'react'; import type { FCWithDeps } from '../container/utils'; import { componentFactory, useDependencies } from '../container/utils'; import { isReachableServer } from '../servers/data'; -import type { WithSelectedServerProps, WithSelectedServerPropsDeps } from '../servers/helpers/withSelectedServer'; +import type { WithSelectedServerPropsDeps } from '../servers/helpers/withSelectedServer'; import { withSelectedServer } from '../servers/helpers/withSelectedServer'; +import { useSelectedServer } from '../servers/reducers/selectedServer'; import { NotFound } from './NotFound'; -type ShlinkWebComponentContainerProps = WithSelectedServerProps & { +type ShlinkWebComponentContainerProps = { settings: Settings; }; @@ -28,12 +29,13 @@ const ShlinkWebComponentContainer: FCWithDeps< // memo is probably not the right solution. The root cause is the withSelectedServer HOC, but I couldn't fix the // extra rendering there. // This should be revisited at some point. -> = withSelectedServer(memo(({ selectedServer, settings }) => { +> = withSelectedServer(memo(({ settings }) => { const { buildShlinkApiClient, TagColorsStorage: tagColorsStorage, ServerError, } = useDependencies(ShlinkWebComponentContainer); + const { selectedServer } = useSelectedServer(); if (!isReachableServer(selectedServer)) { return ; diff --git a/src/common/services/provideServices.ts b/src/common/services/provideServices.ts index e27e38c5..55a68cf9 100644 --- a/src/common/services/provideServices.ts +++ b/src/common/services/provideServices.ts @@ -26,10 +26,9 @@ export const provideServices = (bottle: Bottle, connect: ConnectDecorator) => { bottle.decorator('Home', connect(['servers'], [])); bottle.factory('ShlinkWebComponentContainer', ShlinkWebComponentContainerFactory); - bottle.decorator('ShlinkWebComponentContainer', connect(['selectedServer', 'settings'], ['selectServer'])); + bottle.decorator('ShlinkWebComponentContainer', connect(['settings'], ['selectServer'])); bottle.serviceFactory('ShlinkVersionsContainer', () => ShlinkVersionsContainer); - bottle.decorator('ShlinkVersionsContainer', connect(['selectedServer'])); bottle.serviceFactory('ErrorHandler', () => ErrorHandler); }; diff --git a/src/servers/EditServer.tsx b/src/servers/EditServer.tsx index 485f18f7..22381053 100644 --- a/src/servers/EditServer.tsx +++ b/src/servers/EditServer.tsx @@ -6,17 +6,17 @@ import { useGoBack } from '../utils/helpers/hooks'; import type { ServerData } from './data'; import { isServerWithId } from './data'; import { ServerForm } from './helpers/ServerForm'; -import type { WithSelectedServerProps, WithSelectedServerPropsDeps } from './helpers/withSelectedServer'; +import type { WithSelectedServerPropsDeps } from './helpers/withSelectedServer'; import { withSelectedServer } from './helpers/withSelectedServer'; +import { useSelectedServer } from './reducers/selectedServer'; -type EditServerProps = WithSelectedServerProps & { +type EditServerProps = { editServer: (serverId: string, serverData: ServerData) => void; }; -const EditServer: FCWithDeps = withSelectedServer(( - { editServer, selectedServer, selectServer }, -) => { +const EditServer: FCWithDeps = withSelectedServer(({ editServer }) => { const { buildShlinkApiClient } = useDependencies(EditServer); + const { selectServer, selectedServer } = useSelectedServer(); const goBack = useGoBack(); const { reconnect } = useParsedQuery<{ reconnect?: 'true' }>(); diff --git a/src/servers/ServersDropdown.tsx b/src/servers/ServersDropdown.tsx index 7fbfa9fa..95147f42 100644 --- a/src/servers/ServersDropdown.tsx +++ b/src/servers/ServersDropdown.tsx @@ -1,16 +1,17 @@ import { faPlus as plusIcon, faServer as serverIcon } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Dropdown, NavBar } from '@shlinkio/shlink-frontend-kit'; -import type { SelectedServer, ServersMap } from './data'; +import type { ServersMap } from './data'; import { getServerId } from './data'; +import { useSelectedServer } from './reducers/selectedServer'; export interface ServersDropdownProps { servers: ServersMap; - selectedServer: SelectedServer; } -export const ServersDropdown = ({ servers, selectedServer }: ServersDropdownProps) => { +export const ServersDropdown = ({ servers }: ServersDropdownProps) => { const serversList = Object.values(servers); + const { selectedServer } = useSelectedServer(); return ( ; }; -const ServerError: FCWithDeps = ({ servers, selectedServer }) => { +const ServerError: FCWithDeps = ({ servers }) => { const { DeleteServerButton } = useDependencies(ServerError); + const { selectedServer } = useSelectedServer(); return ( diff --git a/src/servers/helpers/withSelectedServer.tsx b/src/servers/helpers/withSelectedServer.tsx index 6bd9c43e..1386ff28 100644 --- a/src/servers/helpers/withSelectedServer.tsx +++ b/src/servers/helpers/withSelectedServer.tsx @@ -6,14 +6,8 @@ import type { ShlinkApiClientBuilder } from '../../api/services/ShlinkApiClientB import { NoMenuLayout } from '../../common/NoMenuLayout'; import type { FCWithDeps } from '../../container/utils'; import { useDependencies } from '../../container/utils'; -import type { SelectedServer } from '../data'; import { isNotFoundServer } from '../data'; -import type { SelectServerOptions } from '../reducers/selectedServer'; - -export type WithSelectedServerProps = { - selectServer: (options: SelectServerOptions) => void; - selectedServer: SelectedServer; -}; +import { useSelectedServer } from '../reducers/selectedServer'; export type WithSelectedServerPropsDeps = { ServerError: FC; @@ -21,12 +15,12 @@ export type WithSelectedServerPropsDeps = { }; export function withSelectedServer( - WrappedComponent: FCWithDeps, + WrappedComponent: FCWithDeps, ) { - const ComponentWrapper: FCWithDeps = (props) => { + const ComponentWrapper: FCWithDeps = (props) => { const { ServerError, buildShlinkApiClient } = useDependencies(ComponentWrapper); const params = useParams<{ serverId: string }>(); - const { selectServer, selectedServer } = props; + const { selectServer, selectedServer } = useSelectedServer(); useEffect(() => { if (params.serverId) { diff --git a/src/servers/reducers/selectedServer.ts b/src/servers/reducers/selectedServer.ts index ee562a0c..2a2732a4 100644 --- a/src/servers/reducers/selectedServer.ts +++ b/src/servers/reducers/selectedServer.ts @@ -63,7 +63,7 @@ export const selectServer = createAsyncThunk( }, ); -const { reducer } = createSlice({ +export const { reducer: selectedServerReducer } = createSlice({ name: REDUCER_PREFIX, initialState: initialState as SelectedServer, reducers: {}, @@ -73,8 +73,6 @@ const { reducer } = createSlice({ }, }); -export const selectedServerReducer = reducer; - export const useSelectedServer = () => { const dispatch = useAppDispatch(); const dispatchResetSelectedServer = useCallback(() => dispatch(resetSelectedServer()), [dispatch]); diff --git a/src/servers/services/provideServices.ts b/src/servers/services/provideServices.ts index 79e0ba3c..47e49208 100644 --- a/src/servers/services/provideServices.ts +++ b/src/servers/services/provideServices.ts @@ -21,7 +21,7 @@ export const provideServices = (bottle: Bottle, connect: ConnectDecorator) => { // Components bottle.factory('ManageServers', ManageServersFactory); bottle.decorator('ManageServers', withoutSelectedServer); - bottle.decorator('ManageServers', connect(['selectedServer', 'servers'], [])); + bottle.decorator('ManageServers', connect(['servers'], [])); bottle.factory('ManageServersRow', ManageServersRowFactory); @@ -30,13 +30,13 @@ export const provideServices = (bottle: Bottle, connect: ConnectDecorator) => { bottle.factory('CreateServer', CreateServerFactory); bottle.decorator('CreateServer', withoutSelectedServer); - bottle.decorator('CreateServer', connect(['selectedServer', 'servers'], ['createServers'])); + bottle.decorator('CreateServer', connect(['servers'], ['createServers'])); bottle.factory('EditServer', EditServerFactory); - bottle.decorator('EditServer', connect(['selectedServer'], ['editServer', 'selectServer'])); + bottle.decorator('EditServer', connect([], ['editServer', 'selectServer'])); bottle.serviceFactory('ServersDropdown', () => ServersDropdown); - bottle.decorator('ServersDropdown', connect(['servers', 'selectedServer'])); + bottle.decorator('ServersDropdown', connect(['servers'])); bottle.serviceFactory('DeleteServerModal', () => DeleteServerModal); bottle.decorator('DeleteServerModal', connect(null, ['deleteServer'])); @@ -47,7 +47,7 @@ export const provideServices = (bottle: Bottle, connect: ConnectDecorator) => { bottle.decorator('ImportServersBtn', connect(['servers'], ['createServers'])); bottle.factory('ServerError', ServerErrorFactory); - bottle.decorator('ServerError', connect(['servers', 'selectedServer'])); + bottle.decorator('ServerError', connect(['servers'])); // Services bottle.service('ServersImporter', ServersImporter, 'csvToJson'); diff --git a/test/__helpers__/MemoryRouterWithParams.tsx b/test/__helpers__/MemoryRouterWithParams.tsx deleted file mode 100644 index 76e12f16..00000000 --- a/test/__helpers__/MemoryRouterWithParams.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import type { FC, PropsWithChildren } from 'react'; -import { useMemo } from 'react'; -import { MemoryRouter, Route, Routes } from 'react-router'; - -export type MemoryRouterWithParamsProps = PropsWithChildren<{ - params: Record; -}>; - -/** - * Wrap any component using useParams() with MemoryRouterWithParams, in order to determine wat the hook should return - */ -export const MemoryRouterWithParams: FC = ({ children, params }) => { - const pathname = useMemo(() => `/${Object.values(params).join('/')}`, [params]); - const pathPattern = useMemo(() => `/:${Object.keys(params).join('/:')}`, [params]); - - return ( - - - - - - ); -}; diff --git a/test/common/ShlinkVersionsContainer.test.tsx b/test/common/ShlinkVersionsContainer.test.tsx index 2b092d46..120f769a 100644 --- a/test/common/ShlinkVersionsContainer.test.tsx +++ b/test/common/ShlinkVersionsContainer.test.tsx @@ -1,12 +1,15 @@ -import { render } from '@testing-library/react'; import { fromPartial } from '@total-typescript/shoehorn'; import { ShlinkVersionsContainer } from '../../src/common/ShlinkVersionsContainer'; import type { ReachableServer, SelectedServer } from '../../src/servers/data'; import { checkAccessibility } from '../__helpers__/accessibility'; +import { renderWithStore } from '../__helpers__/setUpTest'; describe('', () => { - const setUp = (selectedServer: SelectedServer = null) => render( - , + const setUp = (selectedServer: SelectedServer = null) => renderWithStore( + , + { + initialState: { selectedServer }, + }, ); it.each([ diff --git a/test/common/ShlinkWebComponentContainer.test.tsx b/test/common/ShlinkWebComponentContainer.test.tsx index d186a588..2237c0fc 100644 --- a/test/common/ShlinkWebComponentContainer.test.tsx +++ b/test/common/ShlinkWebComponentContainer.test.tsx @@ -1,9 +1,9 @@ -import { render, screen } from '@testing-library/react'; +import { screen } from '@testing-library/react'; import { fromPartial } from '@total-typescript/shoehorn'; import { ShlinkWebComponentContainerFactory } from '../../src/common/ShlinkWebComponentContainer'; import type { NonReachableServer, NotFoundServer, SelectedServer } from '../../src/servers/data'; import { checkAccessibility } from '../__helpers__/accessibility'; -import { MemoryRouterWithParams } from '../__helpers__/MemoryRouterWithParams'; +import { renderWithStore } from '../__helpers__/setUpTest'; vi.mock('@shlinkio/shlink-web-component', () => ({ ShlinkSidebarVisibilityProvider: ({ children }: any) => children, @@ -17,10 +17,11 @@ describe('', () => { TagColorsStorage: fromPartial({}), ServerError: () => <>ServerError, })); - const setUp = (selectedServer: SelectedServer) => render( - - - , + const setUp = (selectedServer: SelectedServer) => renderWithStore( + , + { + initialState: { selectedServer }, + }, ); it('passes a11y checks', () => checkAccessibility(setUp(fromPartial({ version: '3.0.0' })))); diff --git a/test/servers/EditServer.test.tsx b/test/servers/EditServer.test.tsx index 144e681f..85011d15 100644 --- a/test/servers/EditServer.test.tsx +++ b/test/servers/EditServer.test.tsx @@ -5,7 +5,7 @@ import { Router } from 'react-router'; import type { ReachableServer, SelectedServer } from '../../src/servers/data'; import { EditServerFactory } from '../../src/servers/EditServer'; import { checkAccessibility } from '../__helpers__/accessibility'; -import { renderWithEvents } from '../__helpers__/setUpTest'; +import { renderWithStore } from '../__helpers__/setUpTest'; describe('', () => { const ServerError = vi.fn(); @@ -21,10 +21,13 @@ describe('', () => { const history = createMemoryHistory({ initialEntries: ['/foo', '/bar'] }); return { history, - ...renderWithEvents( + ...renderWithStore( - + , + { + initialState: { selectedServer }, + }, ), }; }; diff --git a/test/servers/ServersDropdown.test.tsx b/test/servers/ServersDropdown.test.tsx index 410c2443..61574c3d 100644 --- a/test/servers/ServersDropdown.test.tsx +++ b/test/servers/ServersDropdown.test.tsx @@ -4,7 +4,7 @@ import { MemoryRouter } from 'react-router'; import type { ServersMap } from '../../src/servers/data'; import { ServersDropdown } from '../../src/servers/ServersDropdown'; import { checkAccessibility } from '../__helpers__/accessibility'; -import { renderWithEvents } from '../__helpers__/setUpTest'; +import { renderWithStore } from '../__helpers__/setUpTest'; describe('', () => { const fallbackServers: ServersMap = { @@ -12,12 +12,15 @@ describe('', () => { '2b': fromPartial({ name: 'bar', id: '2b' }), '3c': fromPartial({ name: 'baz', id: '3c' }), }; - const setUp = (servers: ServersMap = fallbackServers) => renderWithEvents( + const setUp = (servers: ServersMap = fallbackServers) => renderWithStore(
    - +
, + { + initialState: { selectedServer: null }, + }, ); it('passes a11y checks', async () => { diff --git a/test/servers/helpers/ServerError.test.tsx b/test/servers/helpers/ServerError.test.tsx index ec6d42fb..0fcd7fac 100644 --- a/test/servers/helpers/ServerError.test.tsx +++ b/test/servers/helpers/ServerError.test.tsx @@ -1,16 +1,20 @@ -import { render, screen } from '@testing-library/react'; +import { screen } from '@testing-library/react'; import { fromPartial } from '@total-typescript/shoehorn'; import { MemoryRouter } from 'react-router'; import type { NonReachableServer, NotFoundServer, SelectedServer } from '../../../src/servers/data'; import { ServerErrorFactory } from '../../../src/servers/helpers/ServerError'; import { checkAccessibility } from '../../__helpers__/accessibility'; +import { renderWithStore } from '../../__helpers__/setUpTest'; describe('', () => { const ServerError = ServerErrorFactory(fromPartial({ DeleteServerButton: () => null })); - const setUp = (selectedServer: SelectedServer) => render( + const setUp = (selectedServer: SelectedServer) => renderWithStore( - + , + { + initialState: { selectedServer }, + }, ); it.each([