From e9951e95a9eaef46ecc4c37458704c400beb2d24 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Fri, 14 Nov 2025 08:24:58 +0100 Subject: [PATCH 01/24] 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 6ee98cadf..c70256793 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 4c5e6cb2b..085f919a2 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 3ae1572bd..f2d738ef5 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 b4f69f5de..a0c986bf8 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 1a88d01df..09f2218a9 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 512ddb4dc..b5d94dbb0 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 4a2d1ab39..2e33a6496 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 28a0a75bb..6a508fe3f 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(); -export const Settings: FC = ({ settings, setSettings }) => ( - - - -); + return ( + + + + ); +}; diff --git a/src/settings/reducers/settings.ts b/src/settings/reducers/settings.ts index 33d9162ca..f71a1dd23 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 246bed83b..000000000 --- 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 d1ffef4d2..fe52bc177 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 7fccbf985..000000000 --- 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 000000000..4a39c271f --- /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 51aef3aea..0a0e099c1 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 8c6020526..36720fe4c 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 a1af73f1e..2d786f023 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( - + , ); From b295240d2870ee4b326921ebb90e13c134789e89 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Fri, 14 Nov 2025 08:50:08 +0100 Subject: [PATCH 02/24] Stop injecting dependencies in selectServer action --- src/common/ShlinkWebComponentContainer.tsx | 8 ++------ src/servers/EditServer.tsx | 16 ++++++---------- src/servers/helpers/withSelectedServer.tsx | 13 ++++++++----- src/servers/reducers/selectedServer.ts | 16 +++++++++------- src/servers/services/provideServices.ts | 2 +- test/servers/reducers/selectedServer.test.ts | 15 +++++++-------- 6 files changed, 33 insertions(+), 37 deletions(-) diff --git a/src/common/ShlinkWebComponentContainer.tsx b/src/common/ShlinkWebComponentContainer.tsx index ab82c7d02..5d87bf0af 100644 --- a/src/common/ShlinkWebComponentContainer.tsx +++ b/src/common/ShlinkWebComponentContainer.tsx @@ -5,13 +5,11 @@ import { ShlinkWebComponent, } from '@shlinkio/shlink-web-component'; import type { Settings } from '@shlinkio/shlink-web-component/settings'; -import type { FC } from 'react'; import { memo } from 'react'; -import type { ShlinkApiClientBuilder } from '../api/services/ShlinkApiClientBuilder'; import type { FCWithDeps } from '../container/utils'; import { componentFactory, useDependencies } from '../container/utils'; import { isReachableServer } from '../servers/data'; -import type { WithSelectedServerProps } from '../servers/helpers/withSelectedServer'; +import type { WithSelectedServerProps, WithSelectedServerPropsDeps } from '../servers/helpers/withSelectedServer'; import { withSelectedServer } from '../servers/helpers/withSelectedServer'; import { NotFound } from './NotFound'; @@ -19,10 +17,8 @@ type ShlinkWebComponentContainerProps = WithSelectedServerProps & { settings: Settings; }; -type ShlinkWebComponentContainerDeps = { - buildShlinkApiClient: ShlinkApiClientBuilder, +type ShlinkWebComponentContainerDeps = WithSelectedServerPropsDeps & { TagColorsStorage: TagColorsStorage, - ServerError: FC, }; const ShlinkWebComponentContainer: FCWithDeps< diff --git a/src/servers/EditServer.tsx b/src/servers/EditServer.tsx index 0c959964c..485f18f7e 100644 --- a/src/servers/EditServer.tsx +++ b/src/servers/EditServer.tsx @@ -1,26 +1,22 @@ -import { Button,useParsedQuery } from '@shlinkio/shlink-frontend-kit'; -import type { FC } from 'react'; +import { Button, useParsedQuery } from '@shlinkio/shlink-frontend-kit'; import { NoMenuLayout } from '../common/NoMenuLayout'; import type { FCWithDeps } from '../container/utils'; -import { componentFactory } from '../container/utils'; +import { componentFactory, useDependencies } from '../container/utils'; import { useGoBack } from '../utils/helpers/hooks'; import type { ServerData } from './data'; import { isServerWithId } from './data'; import { ServerForm } from './helpers/ServerForm'; -import type { WithSelectedServerProps } from './helpers/withSelectedServer'; +import type { WithSelectedServerProps, WithSelectedServerPropsDeps } from './helpers/withSelectedServer'; import { withSelectedServer } from './helpers/withSelectedServer'; type EditServerProps = WithSelectedServerProps & { editServer: (serverId: string, serverData: ServerData) => void; }; -type EditServerDeps = { - ServerError: FC; -}; - -const EditServer: FCWithDeps = withSelectedServer(( +const EditServer: FCWithDeps = withSelectedServer(( { editServer, selectedServer, selectServer }, ) => { + const { buildShlinkApiClient } = useDependencies(EditServer); const goBack = useGoBack(); const { reconnect } = useParsedQuery<{ reconnect?: 'true' }>(); @@ -31,7 +27,7 @@ const EditServer: FCWithDeps = withSelectedServ const handleSubmit = (serverData: ServerData) => { editServer(selectedServer.id, serverData); if (reconnect === 'true') { - selectServer(selectedServer.id); + selectServer({ serverId: selectedServer.id, buildShlinkApiClient }); } goBack(); }; diff --git a/src/servers/helpers/withSelectedServer.tsx b/src/servers/helpers/withSelectedServer.tsx index e1b41ba15..6bd9c43e9 100644 --- a/src/servers/helpers/withSelectedServer.tsx +++ b/src/servers/helpers/withSelectedServer.tsx @@ -2,34 +2,37 @@ import { Message } from '@shlinkio/shlink-frontend-kit'; import type { FC } from 'react'; import { useEffect } from 'react'; import { useParams } from 'react-router'; +import type { ShlinkApiClientBuilder } from '../../api/services/ShlinkApiClientBuilder'; 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: (serverId: string) => void; + selectServer: (options: SelectServerOptions) => void; selectedServer: SelectedServer; }; -type WithSelectedServerPropsDeps = { +export type WithSelectedServerPropsDeps = { ServerError: FC; + buildShlinkApiClient: ShlinkApiClientBuilder; }; export function withSelectedServer( WrappedComponent: FCWithDeps, ) { const ComponentWrapper: FCWithDeps = (props) => { - const { ServerError } = useDependencies(ComponentWrapper); + const { ServerError, buildShlinkApiClient } = useDependencies(ComponentWrapper); const params = useParams<{ serverId: string }>(); const { selectServer, selectedServer } = props; useEffect(() => { if (params.serverId) { - selectServer(params.serverId); + selectServer({ serverId: params.serverId, buildShlinkApiClient }); } - }, [params.serverId, selectServer]); + }, [buildShlinkApiClient, params.serverId, selectServer]); if (!selectedServer) { return ( diff --git a/src/servers/reducers/selectedServer.ts b/src/servers/reducers/selectedServer.ts index b5d94dbb0..9e274fa52 100644 --- a/src/servers/reducers/selectedServer.ts +++ b/src/servers/reducers/selectedServer.ts @@ -29,9 +29,14 @@ const initialState: SelectedServer = null; export const resetSelectedServer = createAction(`${REDUCER_PREFIX}/resetSelectedServer`); -export const selectServer = (buildShlinkApiClient: ShlinkApiClientBuilder) => createAsyncThunk( +export type SelectServerOptions = { + serverId: string; + buildShlinkApiClient: ShlinkApiClientBuilder; +}; + +export const selectServer = createAsyncThunk( `${REDUCER_PREFIX}/selectServer`, - async (serverId: string, { dispatch, getState }): Promise => { + async ({ serverId, buildShlinkApiClient }: SelectServerOptions, { dispatch, getState }): Promise => { dispatch(resetSelectedServer()); const { servers } = getState(); @@ -58,14 +63,11 @@ export const selectServer = (buildShlinkApiClient: ShlinkApiClientBuilder) => cr const { reducer } = createSlice({ name: REDUCER_PREFIX, - initialState, + initialState: initialState as SelectedServer, reducers: {}, extraReducers: (builder) => { builder.addCase(resetSelectedServer, () => initialState); - builder.addCase( - `${REDUCER_PREFIX}/selectServer/fulfilled`, - (_, { payload }: { payload: SelectedServer }) => payload, - ); + builder.addCase(selectServer.fulfilled, (_, { payload }) => payload); }, }); diff --git a/src/servers/services/provideServices.ts b/src/servers/services/provideServices.ts index 2e33a6496..71ed54c92 100644 --- a/src/servers/services/provideServices.ts +++ b/src/servers/services/provideServices.ts @@ -54,7 +54,7 @@ export const provideServices = (bottle: Bottle, connect: ConnectDecorator) => { bottle.service('ServersExporter', ServersExporter, 'Storage', 'window', 'jsonToCsv'); // Actions - bottle.serviceFactory('selectServer', selectServer, 'buildShlinkApiClient', 'loadMercureInfo'); + bottle.serviceFactory('selectServer', () => selectServer, 'buildShlinkApiClient', 'loadMercureInfo'); bottle.serviceFactory('createServers', () => createServers); bottle.serviceFactory('deleteServer', () => deleteServer); bottle.serviceFactory('editServer', () => editServer); diff --git a/test/servers/reducers/selectedServer.test.ts b/test/servers/reducers/selectedServer.test.ts index 36720fe4c..87ffc8e9a 100644 --- a/test/servers/reducers/selectedServer.test.ts +++ b/test/servers/reducers/selectedServer.test.ts @@ -7,14 +7,13 @@ import { MIN_FALLBACK_VERSION, resetSelectedServer, selectedServerReducer as reducer, - selectServer as selectServerCreator, + selectServer, } from '../../../src/servers/reducers/selectedServer'; describe('selectedServerReducer', () => { const dispatch = vi.fn(); const health = vi.fn(); - const buildApiClient = vi.fn().mockReturnValue(fromPartial({ health })); - const selectServer = selectServerCreator(buildApiClient); + const buildShlinkApiClient = vi.fn().mockReturnValue(fromPartial({ health })); describe('reducer', () => { it('returns default when action is RESET_SELECTED_SERVER', () => @@ -22,7 +21,7 @@ describe('selectedServerReducer', () => { it('returns selected server when action is SELECT_SERVER', () => { const payload = fromPartial({ id: 'abc123' }); - expect(reducer(null, selectServer.fulfilled(payload, '', ''))).toEqual(payload); + expect(reducer(null, selectServer.fulfilled(payload, '', { serverId: '', buildShlinkApiClient }))).toEqual(payload); }); }); @@ -49,10 +48,10 @@ describe('selectedServerReducer', () => { health.mockResolvedValue({ version: serverVersion }); - await selectServer(id)(dispatch, getState, {}); + await selectServer({ serverId: id, buildShlinkApiClient })(dispatch, getState, {}); expect(getState).toHaveBeenCalledTimes(1); - expect(buildApiClient).toHaveBeenCalledTimes(1); + expect(buildShlinkApiClient).toHaveBeenCalledTimes(1); expect(dispatch).toHaveBeenCalledTimes(3); // "Pending", "reset" and "fulfilled" expect(dispatch).toHaveBeenLastCalledWith(expect.objectContaining({ payload: expectedSelectedServer })); }); @@ -64,7 +63,7 @@ describe('selectedServerReducer', () => { health.mockRejectedValue({}); - await selectServer(id)(dispatch, getState, {}); + await selectServer({ serverId: id, buildShlinkApiClient })(dispatch, getState, {}); expect(health).toHaveBeenCalled(); expect(dispatch).toHaveBeenLastCalledWith(expect.objectContaining({ payload: expectedSelectedServer })); @@ -75,7 +74,7 @@ describe('selectedServerReducer', () => { const getState = vi.fn(() => fromPartial({ servers: {} })); const expectedSelectedServer: NotFoundServer = { serverNotFound: true }; - await selectServer(id)(dispatch, getState, {}); + await selectServer({ serverId: id, buildShlinkApiClient })(dispatch, getState, {}); expect(getState).toHaveBeenCalled(); expect(health).not.toHaveBeenCalled(); From 7890d0084ac98e5061b885dccf086a5a9e7a5416 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Fri, 14 Nov 2025 09:54:03 +0100 Subject: [PATCH 03/24] Create useSelectedServer hook and use it where reset selected server is done --- src/common/services/provideServices.ts | 2 +- src/container/store.ts | 11 ++++++++++- src/container/types.ts | 6 ++++-- src/servers/helpers/withoutSelectedServer.tsx | 11 ++++------- src/servers/reducers/selectedServer.ts | 18 ++++++++++++++++++ src/servers/services/provideServices.ts | 10 ++++------ 6 files changed, 41 insertions(+), 17 deletions(-) diff --git a/src/common/services/provideServices.ts b/src/common/services/provideServices.ts index 7e3671375..e27e38c54 100644 --- a/src/common/services/provideServices.ts +++ b/src/common/services/provideServices.ts @@ -23,7 +23,7 @@ export const provideServices = (bottle: Bottle, connect: ConnectDecorator) => { bottle.serviceFactory('Home', () => Home); bottle.decorator('Home', withoutSelectedServer); - bottle.decorator('Home', connect(['servers'], ['resetSelectedServer'])); + bottle.decorator('Home', connect(['servers'], [])); bottle.factory('ShlinkWebComponentContainer', ShlinkWebComponentContainerFactory); bottle.decorator('ShlinkWebComponentContainer', connect(['selectedServer', 'settings'], ['selectServer'])); diff --git a/src/container/store.ts b/src/container/store.ts index f2d738ef5..ff71085c0 100644 --- a/src/container/store.ts +++ b/src/container/store.ts @@ -1,11 +1,11 @@ import { configureStore } from '@reduxjs/toolkit'; +import { useDispatch, useSelector } from 'react-redux'; import type { RLSOptions } from 'redux-localstorage-simple'; import { load, save } from 'redux-localstorage-simple'; import { initReducers } from '../reducers'; import { migrateDeprecatedSettings } from '../settings/helpers'; import type { ShlinkState } from './types'; -const isProduction = process.env.NODE_ENV === 'production'; const localStorageConfig: RLSOptions = { states: ['settings', 'servers'], namespace: 'shlink', @@ -14,6 +14,7 @@ const localStorageConfig: RLSOptions = { }; const getStateFromLocalStorage = () => migrateDeprecatedSettings(load(localStorageConfig) as ShlinkState); +const isProduction = process.env.NODE_ENV === 'production'; export const setUpStore = (preloadedState = getStateFromLocalStorage()) => configureStore({ devTools: !isProduction, reducer: initReducers(), @@ -22,3 +23,11 @@ export const setUpStore = (preloadedState = getStateFromLocalStorage()) => confi defaultMiddlewaresIncludingReduxThunk({ immutableCheck: false, serializableCheck: false }) // State is too big for these .concat(save(localStorageConfig)), }); + +export type StoreType = ReturnType; +export type AppDispatch = StoreType['dispatch']; +export type RootState = ReturnType; + +// Typed versions of useDispatch() and useSelector() +export const useAppDispatch = useDispatch.withTypes(); +export const useAppSelector = useSelector.withTypes(); diff --git a/src/container/types.ts b/src/container/types.ts index 487094db6..46a42482a 100644 --- a/src/container/types.ts +++ b/src/container/types.ts @@ -1,13 +1,15 @@ import type { Settings } from '@shlinkio/shlink-web-component/settings'; import type { SelectedServer, ServersMap } from '../servers/data'; -export interface ShlinkState { +/** Deprecated Use RootState */ +export type ShlinkState = { servers: ServersMap; selectedServer: SelectedServer; settings: Settings; appUpdated: boolean; -} +}; export type ConnectDecorator = (props: string[] | null, actions?: string[]) => any; +/** @deprecated */ export type GetState = () => ShlinkState; diff --git a/src/servers/helpers/withoutSelectedServer.tsx b/src/servers/helpers/withoutSelectedServer.tsx index 5f533dc03..34c30a69b 100644 --- a/src/servers/helpers/withoutSelectedServer.tsx +++ b/src/servers/helpers/withoutSelectedServer.tsx @@ -1,13 +1,10 @@ import type { FC } from 'react'; import { useEffect } from 'react'; +import { useSelectedServer } from '../reducers/selectedServer'; -interface WithoutSelectedServerProps { - resetSelectedServer: () => unknown; -} - -export function withoutSelectedServer(WrappedComponent: FC) { - return (props: WithoutSelectedServerProps & T) => { - const { resetSelectedServer } = props; +export function withoutSelectedServer(WrappedComponent: FC) { + return (props: T) => { + const { resetSelectedServer } = useSelectedServer(); useEffect(() => { resetSelectedServer(); }, [resetSelectedServer]); diff --git a/src/servers/reducers/selectedServer.ts b/src/servers/reducers/selectedServer.ts index 9e274fa52..ee562a0ca 100644 --- a/src/servers/reducers/selectedServer.ts +++ b/src/servers/reducers/selectedServer.ts @@ -1,7 +1,9 @@ import { createAction, createSlice } from '@reduxjs/toolkit'; import { memoizeWith } from '@shlinkio/data-manipulation'; import type { ShlinkHealth } from '@shlinkio/shlink-web-component/api-contract'; +import { useCallback } from 'react'; import type { ShlinkApiClientBuilder } from '../../api/services/ShlinkApiClientBuilder'; +import { useAppDispatch, useAppSelector } from '../../container/store'; import { createAsyncThunk } from '../../utils/helpers/redux'; import { versionToPrintable, versionToSemVer as toSemVer } from '../../utils/helpers/version'; import type { SelectedServer, ServerWithId } from '../data'; @@ -72,3 +74,19 @@ const { reducer } = createSlice({ }); export const selectedServerReducer = reducer; + +export const useSelectedServer = () => { + const dispatch = useAppDispatch(); + const dispatchResetSelectedServer = useCallback(() => dispatch(resetSelectedServer()), [dispatch]); + const dispatchSelectServer = useCallback( + (options: SelectServerOptions) => dispatch(selectServer(options)), + [dispatch], + ); + const selectedServer = useAppSelector(({ selectedServer }) => selectedServer); + + return { + selectedServer, + resetSelectedServer: dispatchResetSelectedServer, + selectServer: dispatchSelectServer, + }; +}; diff --git a/src/servers/services/provideServices.ts b/src/servers/services/provideServices.ts index 71ed54c92..79e0ba3cb 100644 --- a/src/servers/services/provideServices.ts +++ b/src/servers/services/provideServices.ts @@ -11,7 +11,7 @@ import { ManageServersFactory } from '../ManageServers'; import { ManageServersRowFactory } from '../ManageServersRow'; import { ManageServersRowDropdownFactory } from '../ManageServersRowDropdown'; import { fetchServers } from '../reducers/remoteServers'; -import { resetSelectedServer, selectServer } from '../reducers/selectedServer'; +import { selectServer } from '../reducers/selectedServer'; import { createServers, deleteServer, editServer, setAutoConnect } from '../reducers/servers'; import { ServersDropdown } from '../ServersDropdown'; import { ServersExporter } from './ServersExporter'; @@ -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'], ['resetSelectedServer'])); + bottle.decorator('ManageServers', connect(['selectedServer', 'servers'], [])); bottle.factory('ManageServersRow', ManageServersRowFactory); @@ -30,10 +30,10 @@ export const provideServices = (bottle: Bottle, connect: ConnectDecorator) => { bottle.factory('CreateServer', CreateServerFactory); bottle.decorator('CreateServer', withoutSelectedServer); - bottle.decorator('CreateServer', connect(['selectedServer', 'servers'], ['createServers', 'resetSelectedServer'])); + bottle.decorator('CreateServer', connect(['selectedServer', 'servers'], ['createServers'])); bottle.factory('EditServer', EditServerFactory); - bottle.decorator('EditServer', connect(['selectedServer'], ['editServer', 'selectServer', 'resetSelectedServer'])); + bottle.decorator('EditServer', connect(['selectedServer'], ['editServer', 'selectServer'])); bottle.serviceFactory('ServersDropdown', () => ServersDropdown); bottle.decorator('ServersDropdown', connect(['servers', 'selectedServer'])); @@ -60,6 +60,4 @@ export const provideServices = (bottle: Bottle, connect: ConnectDecorator) => { bottle.serviceFactory('editServer', () => editServer); bottle.serviceFactory('setAutoConnect', () => setAutoConnect); bottle.serviceFactory('fetchServers', fetchServers, 'HttpClient'); - - bottle.serviceFactory('resetSelectedServer', () => resetSelectedServer); }; From 9c1052c10b9e4f761bb387ec77b7855e17f51ba4 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Fri, 14 Nov 2025 10:27:49 +0100 Subject: [PATCH 04/24] 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 c80eb6984..ebba69667 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 5d87bf0af..0484d23c9 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 e27e38c54..55a68cf9d 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 485f18f7e..223810532 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 7fbfa9fa8..95147f425 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 6bd9c43e9..1386ff286 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 ee562a0ca..2a2732a4f 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 79e0ba3cb..47e49208d 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 76e12f16b..000000000 --- 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 2b092d463..120f769a1 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 d186a5883..2237c0fc9 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 144e681fe..85011d156 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 410c2443c..61574c3d6 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 ec6d42fbf..0fcd7fac0 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([ From 11bbef3aca3286630db48de259e83b0c9a050800 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Fri, 14 Nov 2025 10:34:34 +0100 Subject: [PATCH 05/24] Create dedicated store module --- src/index.tsx | 2 +- src/servers/reducers/selectedServer.ts | 2 +- src/{container/store.ts => store/index.ts} | 4 ++-- src/{reducers/index.ts => store/reducers.ts} | 0 test/__helpers__/setUpTest.tsx | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) rename src/{container/store.ts => store/index.ts} (93%) rename src/{reducers/index.ts => store/reducers.ts} (100%) diff --git a/src/index.tsx b/src/index.tsx index a0c986bf8..4dd136b49 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -3,8 +3,8 @@ import { Provider } from 'react-redux'; import { BrowserRouter } from 'react-router'; import pack from '../package.json'; import { container } from './container'; -import { setUpStore } from './container/store'; import { register as registerServiceWorker } from './serviceWorkerRegistration'; +import { setUpStore } from './store'; import './tailwind.css'; const store = setUpStore(); diff --git a/src/servers/reducers/selectedServer.ts b/src/servers/reducers/selectedServer.ts index 2a2732a4f..5a93b8dff 100644 --- a/src/servers/reducers/selectedServer.ts +++ b/src/servers/reducers/selectedServer.ts @@ -3,7 +3,7 @@ import { memoizeWith } from '@shlinkio/data-manipulation'; import type { ShlinkHealth } from '@shlinkio/shlink-web-component/api-contract'; import { useCallback } from 'react'; import type { ShlinkApiClientBuilder } from '../../api/services/ShlinkApiClientBuilder'; -import { useAppDispatch, useAppSelector } from '../../container/store'; +import { useAppDispatch, useAppSelector } from '../../store'; import { createAsyncThunk } from '../../utils/helpers/redux'; import { versionToPrintable, versionToSemVer as toSemVer } from '../../utils/helpers/version'; import type { SelectedServer, ServerWithId } from '../data'; diff --git a/src/container/store.ts b/src/store/index.ts similarity index 93% rename from src/container/store.ts rename to src/store/index.ts index ff71085c0..65ba8c8c2 100644 --- a/src/container/store.ts +++ b/src/store/index.ts @@ -2,9 +2,9 @@ import { configureStore } from '@reduxjs/toolkit'; import { useDispatch, useSelector } from 'react-redux'; import type { RLSOptions } from 'redux-localstorage-simple'; import { load, save } from 'redux-localstorage-simple'; -import { initReducers } from '../reducers'; +import type { ShlinkState } from '../container/types'; import { migrateDeprecatedSettings } from '../settings/helpers'; -import type { ShlinkState } from './types'; +import { initReducers } from './reducers'; const localStorageConfig: RLSOptions = { states: ['settings', 'servers'], diff --git a/src/reducers/index.ts b/src/store/reducers.ts similarity index 100% rename from src/reducers/index.ts rename to src/store/reducers.ts diff --git a/test/__helpers__/setUpTest.tsx b/test/__helpers__/setUpTest.tsx index 4a39c271f..7e5b91369 100644 --- a/test/__helpers__/setUpTest.tsx +++ b/test/__helpers__/setUpTest.tsx @@ -3,8 +3,8 @@ 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'; +import { setUpStore } from '../../src/store'; export const renderWithEvents = (element: ReactElement, options?: RenderOptions) => ({ user: userEvent.setup(), From 145765e3fa90e9e7b35316547d6d5a05851c8837 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Fri, 14 Nov 2025 10:51:26 +0100 Subject: [PATCH 06/24] Enable immutable and serializable redux checks --- src/store/index.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/store/index.ts b/src/store/index.ts index 65ba8c8c2..f5d425e66 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -2,7 +2,6 @@ import { configureStore } from '@reduxjs/toolkit'; import { useDispatch, useSelector } from 'react-redux'; import type { RLSOptions } from 'redux-localstorage-simple'; import { load, save } from 'redux-localstorage-simple'; -import type { ShlinkState } from '../container/types'; import { migrateDeprecatedSettings } from '../settings/helpers'; import { initReducers } from './reducers'; @@ -12,7 +11,7 @@ const localStorageConfig: RLSOptions = { namespaceSeparator: '.', debounce: 300, }; -const getStateFromLocalStorage = () => migrateDeprecatedSettings(load(localStorageConfig) as ShlinkState); +const getStateFromLocalStorage = () => migrateDeprecatedSettings(load(localStorageConfig)); const isProduction = process.env.NODE_ENV === 'production'; export const setUpStore = (preloadedState = getStateFromLocalStorage()) => configureStore({ @@ -20,8 +19,7 @@ export const setUpStore = (preloadedState = getStateFromLocalStorage()) => confi reducer: initReducers(), preloadedState, middleware: (defaultMiddlewaresIncludingReduxThunk) => - defaultMiddlewaresIncludingReduxThunk({ immutableCheck: false, serializableCheck: false }) // State is too big for these - .concat(save(localStorageConfig)), + defaultMiddlewaresIncludingReduxThunk().concat(save(localStorageConfig)), }); export type StoreType = ReturnType; From ae7aea0e2cc5fc236620b3deb6e3f90ce4fb3ff4 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Fri, 14 Nov 2025 14:21:14 +0100 Subject: [PATCH 07/24] Infer redux types when possible --- src/api/services/ShlinkApiClientBuilder.ts | 2 +- src/container/types.ts | 14 -------------- src/servers/reducers/remoteServers.ts | 2 +- src/servers/reducers/selectedServer.ts | 2 +- src/settings/helpers/index.ts | 10 ++-------- src/settings/reducers/settings.ts | 7 +++---- src/{utils/helpers/redux.ts => store/helpers.ts} | 4 ++-- src/store/index.ts | 3 ++- test/__helpers__/setUpTest.tsx | 4 ++-- test/servers/reducers/selectedServer.test.ts | 4 ++-- test/settings/helpers/index.test.ts | 4 ++-- 11 files changed, 18 insertions(+), 38 deletions(-) rename src/{utils/helpers/redux.ts => store/helpers.ts} (77%) diff --git a/src/api/services/ShlinkApiClientBuilder.ts b/src/api/services/ShlinkApiClientBuilder.ts index 8e64c3eea..b6216a16e 100644 --- a/src/api/services/ShlinkApiClientBuilder.ts +++ b/src/api/services/ShlinkApiClientBuilder.ts @@ -1,8 +1,8 @@ import type { HttpClient } from '@shlinkio/shlink-js-sdk'; import { ShlinkApiClient } from '@shlinkio/shlink-js-sdk'; -import type { GetState } from '../../container/types'; import type { ServerWithId } from '../../servers/data'; import { hasServerData } from '../../servers/data'; +import type { GetState } from '../../store'; const apiClients: Map = new Map(); diff --git a/src/container/types.ts b/src/container/types.ts index 46a42482a..71b20fbd7 100644 --- a/src/container/types.ts +++ b/src/container/types.ts @@ -1,15 +1 @@ -import type { Settings } from '@shlinkio/shlink-web-component/settings'; -import type { SelectedServer, ServersMap } from '../servers/data'; - -/** Deprecated Use RootState */ -export type ShlinkState = { - servers: ServersMap; - selectedServer: SelectedServer; - settings: Settings; - appUpdated: boolean; -}; - export type ConnectDecorator = (props: string[] | null, actions?: string[]) => any; - -/** @deprecated */ -export type GetState = () => ShlinkState; diff --git a/src/servers/reducers/remoteServers.ts b/src/servers/reducers/remoteServers.ts index 37c437d0c..d7619e4b7 100644 --- a/src/servers/reducers/remoteServers.ts +++ b/src/servers/reducers/remoteServers.ts @@ -1,6 +1,6 @@ import type { HttpClient } from '@shlinkio/shlink-js-sdk'; import pack from '../../../package.json'; -import { createAsyncThunk } from '../../utils/helpers/redux'; +import { createAsyncThunk } from '../../store/helpers'; import { hasServerData } from '../data'; import { ensureUniqueIds } from '../helpers'; import { createServers } from './servers'; diff --git a/src/servers/reducers/selectedServer.ts b/src/servers/reducers/selectedServer.ts index 5a93b8dff..5c3a9b333 100644 --- a/src/servers/reducers/selectedServer.ts +++ b/src/servers/reducers/selectedServer.ts @@ -4,7 +4,7 @@ import type { ShlinkHealth } from '@shlinkio/shlink-web-component/api-contract'; import { useCallback } from 'react'; import type { ShlinkApiClientBuilder } from '../../api/services/ShlinkApiClientBuilder'; import { useAppDispatch, useAppSelector } from '../../store'; -import { createAsyncThunk } from '../../utils/helpers/redux'; +import { createAsyncThunk } from '../../store/helpers'; import { versionToPrintable, versionToSemVer as toSemVer } from '../../utils/helpers/version'; import type { SelectedServer, ServerWithId } from '../data'; diff --git a/src/settings/helpers/index.ts b/src/settings/helpers/index.ts index 509df6cd0..53044ad08 100644 --- a/src/settings/helpers/index.ts +++ b/src/settings/helpers/index.ts @@ -1,12 +1,6 @@ -import type { ShlinkState } from '../../container/types'; - -export const migrateDeprecatedSettings = (state: Partial): Partial => { - if (!state.settings) { - return state; - } - +export const migrateDeprecatedSettings = (state: any): any => { // The "last180Days" interval had a typo, with a lowercase d - if (state.settings.visits && (state.settings.visits.defaultInterval as any) === 'last180days') { + if (state.settings?.visits?.defaultInterval === 'last180days') { state.settings.visits.defaultInterval = 'last180Days'; } diff --git a/src/settings/reducers/settings.ts b/src/settings/reducers/settings.ts index f71a1dd23..f05240ec7 100644 --- a/src/settings/reducers/settings.ts +++ b/src/settings/reducers/settings.ts @@ -4,8 +4,7 @@ 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 { useAppDispatch, useAppSelector } from '../../store'; import type { Defined } from '../../utils/types'; type ShortUrlsOrder = Defined; @@ -46,9 +45,9 @@ export const { setSettings } = actions; export const settingsReducer = reducer; export const useSettings = () => { - const dispatch = useDispatch(); + const dispatch = useAppDispatch(); const setSettings = useCallback((settings: Settings) => dispatch(actions.setSettings(settings)), [dispatch]); - const settings = useSelector((state: ShlinkState) => state.settings); + const settings = useAppSelector((state) => state.settings); return { settings, setSettings }; }; diff --git a/src/utils/helpers/redux.ts b/src/store/helpers.ts similarity index 77% rename from src/utils/helpers/redux.ts rename to src/store/helpers.ts index d2766cf39..4304ec760 100644 --- a/src/utils/helpers/redux.ts +++ b/src/store/helpers.ts @@ -1,10 +1,10 @@ import type { AsyncThunkPayloadCreator } from '@reduxjs/toolkit'; import { createAsyncThunk as baseCreateAsyncThunk } from '@reduxjs/toolkit'; -import type { ShlinkState } from '../../container/types'; +import type { RootState } from '.'; export const createAsyncThunk = ( typePrefix: string, - payloadCreator: AsyncThunkPayloadCreator, + payloadCreator: AsyncThunkPayloadCreator, ) => baseCreateAsyncThunk( typePrefix, payloadCreator, diff --git a/src/store/index.ts b/src/store/index.ts index f5d425e66..9a122aebe 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -24,7 +24,8 @@ export const setUpStore = (preloadedState = getStateFromLocalStorage()) => confi export type StoreType = ReturnType; export type AppDispatch = StoreType['dispatch']; -export type RootState = ReturnType; +export type GetState = StoreType['getState']; +export type RootState = ReturnType; // Typed versions of useDispatch() and useSelector() export const useAppDispatch = useDispatch.withTypes(); diff --git a/test/__helpers__/setUpTest.tsx b/test/__helpers__/setUpTest.tsx index 7e5b91369..00f4625db 100644 --- a/test/__helpers__/setUpTest.tsx +++ b/test/__helpers__/setUpTest.tsx @@ -3,7 +3,7 @@ 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 type { ShlinkState } from '../../src/container/types'; +import type { RootState } from '../../src/store'; import { setUpStore } from '../../src/store'; export const renderWithEvents = (element: ReactElement, options?: RenderOptions) => ({ @@ -12,7 +12,7 @@ export const renderWithEvents = (element: ReactElement, options?: RenderOptions) }); export type RenderOptionsWithState = Omit & { - initialState?: Partial; + initialState?: Partial; }; export const renderWithStore = ( diff --git a/test/servers/reducers/selectedServer.test.ts b/test/servers/reducers/selectedServer.test.ts index 87ffc8e9a..a7358f2a2 100644 --- a/test/servers/reducers/selectedServer.test.ts +++ b/test/servers/reducers/selectedServer.test.ts @@ -1,6 +1,5 @@ import type { ShlinkApiClient } from '@shlinkio/shlink-js-sdk'; import { fromPartial } from '@total-typescript/shoehorn'; -import type { ShlinkState } from '../../../src/container/types'; import type { NonReachableServer, NotFoundServer, RegularServer } from '../../../src/servers/data'; import { MAX_FALLBACK_VERSION, @@ -9,6 +8,7 @@ import { selectedServerReducer as reducer, selectServer, } from '../../../src/servers/reducers/selectedServer'; +import type { RootState } from '../../../src/store'; describe('selectedServerReducer', () => { const dispatch = vi.fn(); @@ -71,7 +71,7 @@ describe('selectedServerReducer', () => { it('dispatches error when server is not found', async () => { const id = crypto.randomUUID(); - const getState = vi.fn(() => fromPartial({ servers: {} })); + const getState = vi.fn(() => fromPartial({ servers: {} })); const expectedSelectedServer: NotFoundServer = { serverNotFound: true }; await selectServer({ serverId: id, buildShlinkApiClient })(dispatch, getState, {}); diff --git a/test/settings/helpers/index.test.ts b/test/settings/helpers/index.test.ts index e69fb8995..5fcc9ffc6 100644 --- a/test/settings/helpers/index.test.ts +++ b/test/settings/helpers/index.test.ts @@ -1,6 +1,6 @@ import { fromPartial } from '@total-typescript/shoehorn'; -import type { ShlinkState } from '../../../src/container/types'; import { migrateDeprecatedSettings } from '../../../src/settings/helpers'; +import type { RootState } from '../../../src/store'; describe('settings-helpers', () => { describe('migrateDeprecatedSettings', () => { @@ -9,7 +9,7 @@ describe('settings-helpers', () => { }); it('updates settings as expected', () => { - const state = fromPartial({ + const state = fromPartial({ settings: { visits: { defaultInterval: 'last180days' as any, From a7f2d3224bdd30e7a4f08020d84ae412229f0105 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Fri, 14 Nov 2025 19:23:48 +0100 Subject: [PATCH 08/24] Do not inject servers state or actions --- src/app/App.tsx | 12 +--- src/common/Home.tsx | 10 ++-- src/common/MainHeader.tsx | 12 +--- src/common/ShlinkWebComponentContainer.tsx | 3 +- src/common/services/provideServices.ts | 9 +-- src/container/index.ts | 2 +- src/servers/CreateServer.tsx | 11 ++-- src/servers/DeleteServerButton.tsx | 13 +--- src/servers/DeleteServerModal.tsx | 8 +-- src/servers/EditServer.tsx | 12 ++-- src/servers/ManageServers.tsx | 14 ++--- src/servers/ManageServersRow.tsx | 14 +---- src/servers/ManageServersRowDropdown.tsx | 23 ++------ src/servers/ServersDropdown.tsx | 10 ++-- src/servers/ServersListGroup.tsx | 26 ++++---- .../helpers/DuplicatedServersModal.tsx | 2 +- src/servers/helpers/ImportServersBtn.tsx | 13 ++-- src/servers/helpers/ServerError.tsx | 20 ++----- src/servers/helpers/withSelectedServer.tsx | 5 +- src/servers/reducers/servers.ts | 29 +++++++-- src/servers/services/provideServices.ts | 39 +----------- test/__helpers__/setUpTest.tsx | 13 ++-- test/app/App.test.tsx | 23 ++++---- test/common/Home.test.tsx | 10 +++- test/common/MainHeader.test.tsx | 13 ++-- .../ShlinkWebComponentContainer.test.tsx | 21 ++++--- test/servers/CreateServer.test.tsx | 30 ++++++---- test/servers/DeleteServerButton.test.tsx | 11 +--- test/servers/DeleteServerModal.test.tsx | 32 +++++----- test/servers/EditServer.test.tsx | 34 +++++------ test/servers/ManageServers.test.tsx | 28 ++++----- test/servers/ManageServersRow.test.tsx | 15 ++--- .../servers/ManageServersRowDropdown.test.tsx | 35 +++++------ test/servers/ServersDropdown.test.tsx | 4 +- .../ManageServersRow.test.tsx.snap | 59 +++++++++++++++++-- .../ManageServersRowDropdown.test.tsx.snap | 12 +--- .../servers/helpers/ImportServersBtn.test.tsx | 33 +++++------ test/servers/helpers/ServerError.test.tsx | 7 +-- 38 files changed, 292 insertions(+), 375 deletions(-) diff --git a/src/app/App.tsx b/src/app/App.tsx index c70256793..5c0806137 100644 --- a/src/app/App.tsx +++ b/src/app/App.tsx @@ -5,10 +5,13 @@ import type { FC } from 'react'; import { useEffect, useRef } from 'react'; import { Route, Routes, useLocation } from 'react-router'; import { AppUpdateBanner } from '../common/AppUpdateBanner'; +import { MainHeader } from '../common/MainHeader'; import { NotFound } from '../common/NotFound'; +import { ShlinkVersionsContainer } from '../common/ShlinkVersionsContainer'; import type { FCWithDeps } from '../container/utils'; import { componentFactory, useDependencies } from '../container/utils'; import type { ServersMap } from '../servers/data'; +import { EditServer } from '../servers/EditServer'; import { Settings } from '../settings/Settings'; import { forceUpdate } from '../utils/helpers/sw'; @@ -21,26 +24,20 @@ type AppProps = { }; type AppDeps = { - MainHeader: FC; Home: FC; ShlinkWebComponentContainer: FC; CreateServer: FC; - EditServer: FC; ManageServers: FC; - ShlinkVersionsContainer: FC; }; const App: FCWithDeps = ( { fetchServers, servers, settings, appUpdated, resetAppUpdate }, ) => { const { - MainHeader, Home, ShlinkWebComponentContainer, CreateServer, - EditServer, ManageServers, - ShlinkVersionsContainer, } = useDependencies(App); const location = useLocation(); @@ -99,11 +96,8 @@ const App: FCWithDeps = ( }; export const AppFactory = componentFactory(App, [ - 'MainHeader', 'Home', 'ShlinkWebComponentContainer', 'CreateServer', - 'EditServer', 'ManageServers', - 'ShlinkVersionsContainer', ]); diff --git a/src/common/Home.tsx b/src/common/Home.tsx index e1018489b..94719b090 100644 --- a/src/common/Home.tsx +++ b/src/common/Home.tsx @@ -2,19 +2,17 @@ import { faExternalLinkAlt, faPlus } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Button, Card } from '@shlinkio/shlink-frontend-kit'; import { clsx } from 'clsx'; +import type { FC } from 'react'; import { useEffect } from 'react'; import { ExternalLink } from 'react-external-link'; import { useNavigate } from 'react-router'; -import type { ServersMap } from '../servers/data'; +import { useServers } from '../servers/reducers/servers'; import { ServersListGroup } from '../servers/ServersListGroup'; import { ShlinkLogo } from './img/ShlinkLogo'; -export type HomeProps = { - servers: ServersMap; -}; - -export const Home = ({ servers }: HomeProps) => { +export const Home: FC = () => { const navigate = useNavigate(); + const { servers } = useServers(); const serversList = Object.values(servers); const hasServers = serversList.length > 0; diff --git a/src/common/MainHeader.tsx b/src/common/MainHeader.tsx index 45816f05b..6d5d52c18 100644 --- a/src/common/MainHeader.tsx +++ b/src/common/MainHeader.tsx @@ -3,16 +3,10 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { NavBar } from '@shlinkio/shlink-frontend-kit'; import type { FC } from 'react'; import { Link, useLocation } from 'react-router'; -import type { FCWithDeps } from '../container/utils'; -import { componentFactory, useDependencies } from '../container/utils'; +import { ServersDropdown } from '../servers/ServersDropdown'; import { ShlinkLogo } from './img/ShlinkLogo'; -type MainHeaderDeps = { - ServersDropdown: FC; -}; - -const MainHeader: FCWithDeps = () => { - const { ServersDropdown } = useDependencies(MainHeader); +export const MainHeader: FC = () => { const { pathname } = useLocation(); const settingsPath = '/settings'; @@ -37,5 +31,3 @@ const MainHeader: FCWithDeps = () => { ); }; - -export const MainHeaderFactory = componentFactory(MainHeader, ['ServersDropdown']); diff --git a/src/common/ShlinkWebComponentContainer.tsx b/src/common/ShlinkWebComponentContainer.tsx index 0484d23c9..ff76df254 100644 --- a/src/common/ShlinkWebComponentContainer.tsx +++ b/src/common/ShlinkWebComponentContainer.tsx @@ -9,6 +9,7 @@ import { memo } from 'react'; import type { FCWithDeps } from '../container/utils'; import { componentFactory, useDependencies } from '../container/utils'; import { isReachableServer } from '../servers/data'; +import { ServerError } from '../servers/helpers/ServerError'; import type { WithSelectedServerPropsDeps } from '../servers/helpers/withSelectedServer'; import { withSelectedServer } from '../servers/helpers/withSelectedServer'; import { useSelectedServer } from '../servers/reducers/selectedServer'; @@ -33,7 +34,6 @@ const ShlinkWebComponentContainer: FCWithDeps< const { buildShlinkApiClient, TagColorsStorage: tagColorsStorage, - ServerError, } = useDependencies(ShlinkWebComponentContainer); const { selectedServer } = useSelectedServer(); @@ -63,5 +63,4 @@ const ShlinkWebComponentContainer: FCWithDeps< export const ShlinkWebComponentContainerFactory = componentFactory(ShlinkWebComponentContainer, [ 'buildShlinkApiClient', 'TagColorsStorage', - 'ServerError', ]); diff --git a/src/common/services/provideServices.ts b/src/common/services/provideServices.ts index 55a68cf9d..20026d691 100644 --- a/src/common/services/provideServices.ts +++ b/src/common/services/provideServices.ts @@ -4,9 +4,7 @@ import type { ConnectDecorator } from '../../container/types'; import { withoutSelectedServer } from '../../servers/helpers/withoutSelectedServer'; import { ErrorHandler } from '../ErrorHandler'; import { Home } from '../Home'; -import { MainHeaderFactory } from '../MainHeader'; import { ScrollToTop } from '../ScrollToTop'; -import { ShlinkVersionsContainer } from '../ShlinkVersionsContainer'; import { ShlinkWebComponentContainerFactory } from '../ShlinkWebComponentContainer'; export const provideServices = (bottle: Bottle, connect: ConnectDecorator) => { @@ -19,16 +17,11 @@ export const provideServices = (bottle: Bottle, connect: ConnectDecorator) => { // Components bottle.serviceFactory('ScrollToTop', () => ScrollToTop); - bottle.factory('MainHeader', MainHeaderFactory); - bottle.serviceFactory('Home', () => Home); bottle.decorator('Home', withoutSelectedServer); - bottle.decorator('Home', connect(['servers'], [])); bottle.factory('ShlinkWebComponentContainer', ShlinkWebComponentContainerFactory); - bottle.decorator('ShlinkWebComponentContainer', connect(['settings'], ['selectServer'])); - - bottle.serviceFactory('ShlinkVersionsContainer', () => ShlinkVersionsContainer); + bottle.decorator('ShlinkWebComponentContainer', connect(['settings'], [])); bottle.serviceFactory('ErrorHandler', () => ErrorHandler); }; diff --git a/src/container/index.ts b/src/container/index.ts index 085f919a2..604f04fdb 100644 --- a/src/container/index.ts +++ b/src/container/index.ts @@ -36,5 +36,5 @@ const connect: ConnectDecorator = (propsFromState: string[] | null, actionServic provideAppServices(bottle, connect); provideCommonServices(bottle, connect); provideApiServices(bottle); -provideServersServices(bottle, connect); +provideServersServices(bottle); provideUtilsServices(bottle); diff --git a/src/servers/CreateServer.tsx b/src/servers/CreateServer.tsx index f3ab0cd49..402b0f621 100644 --- a/src/servers/CreateServer.tsx +++ b/src/servers/CreateServer.tsx @@ -7,19 +7,15 @@ import { NoMenuLayout } from '../common/NoMenuLayout'; import type { FCWithDeps } from '../container/utils'; import { componentFactory, useDependencies } from '../container/utils'; import { useGoBack } from '../utils/helpers/hooks'; -import type { ServerData, ServersMap, ServerWithId } from './data'; +import type { ServerData } from './data'; import { ensureUniqueIds } from './helpers'; import { DuplicatedServersModal } from './helpers/DuplicatedServersModal'; import type { ImportServersBtnProps } from './helpers/ImportServersBtn'; import { ServerForm } from './helpers/ServerForm'; +import { useServers } from './reducers/servers'; const SHOW_IMPORT_MSG_TIME = 4000; -type CreateServerProps = { - createServers: (servers: ServerWithId[]) => void; - servers: ServersMap; -}; - type CreateServerDeps = { ImportServersBtn: FC; useTimeoutToggle: TimeoutToggle; @@ -34,7 +30,8 @@ const ImportResult = ({ variant }: Pick) => ( ); -const CreateServer: FCWithDeps = ({ servers, createServers }) => { +const CreateServer: FCWithDeps = () => { + const { servers, createServers } = useServers(); const { ImportServersBtn, useTimeoutToggle } = useDependencies(CreateServer); const navigate = useNavigate(); const goBack = useGoBack(); diff --git a/src/servers/DeleteServerButton.tsx b/src/servers/DeleteServerButton.tsx index b39a8a556..e05858b88 100644 --- a/src/servers/DeleteServerButton.tsx +++ b/src/servers/DeleteServerButton.tsx @@ -2,21 +2,14 @@ import { useToggle } from '@shlinkio/shlink-frontend-kit'; import type { FC, PropsWithChildren } from 'react'; import { useCallback } from 'react'; import { useNavigate } from 'react-router'; -import type { FCWithDeps } from '../container/utils'; -import { componentFactory, useDependencies } from '../container/utils'; import type { ServerWithId } from './data'; -import type { DeleteServerModalProps } from './DeleteServerModal'; +import { DeleteServerModal } from './DeleteServerModal'; export type DeleteServerButtonProps = PropsWithChildren<{ server: ServerWithId; }>; -type DeleteServerButtonDeps = { - DeleteServerModal: FC; -}; - -const DeleteServerButton: FCWithDeps = ({ server, children }) => { - const { DeleteServerModal } = useDependencies(DeleteServerButton); +export const DeleteServerButton: FC = ({ server, children }) => { const { flag: isModalOpen, setToTrue: showModal, setToFalse: hideModal } = useToggle(); const navigate = useNavigate(); const onClose = useCallback((confirmed: boolean) => { @@ -35,5 +28,3 @@ const DeleteServerButton: FCWithDeps ); }; - -export const DeleteServerButtonFactory = componentFactory(DeleteServerButton, ['DeleteServerModal']); diff --git a/src/servers/DeleteServerModal.tsx b/src/servers/DeleteServerModal.tsx index b02187070..ce9c80368 100644 --- a/src/servers/DeleteServerModal.tsx +++ b/src/servers/DeleteServerModal.tsx @@ -3,6 +3,7 @@ import { CardModal } from '@shlinkio/shlink-frontend-kit'; import type { FC } from 'react'; import { useCallback } from 'react'; import type { ServerWithId } from './data'; +import { useServers } from './reducers/servers'; export type DeleteServerModalProps = { server: ServerWithId; @@ -10,11 +11,8 @@ export type DeleteServerModalProps = { open: boolean; }; -type DeleteServerModalConnectProps = DeleteServerModalProps & { - deleteServer: (server: ServerWithId) => void; -}; - -export const DeleteServerModal: FC = ({ server, onClose, open, deleteServer }) => { +export const DeleteServerModal: FC = ({ server, onClose, open }) => { + const { deleteServer } = useServers(); const onClosed = useCallback((exitAction: ExitAction) => { if (exitAction === 'confirm') { deleteServer(server); diff --git a/src/servers/EditServer.tsx b/src/servers/EditServer.tsx index 223810532..74c59a487 100644 --- a/src/servers/EditServer.tsx +++ b/src/servers/EditServer.tsx @@ -1,7 +1,7 @@ import { Button, useParsedQuery } from '@shlinkio/shlink-frontend-kit'; import { NoMenuLayout } from '../common/NoMenuLayout'; import type { FCWithDeps } from '../container/utils'; -import { componentFactory, useDependencies } from '../container/utils'; +import { useDependencies } from '../container/utils'; import { useGoBack } from '../utils/helpers/hooks'; import type { ServerData } from './data'; import { isServerWithId } from './data'; @@ -9,12 +9,10 @@ import { ServerForm } from './helpers/ServerForm'; import type { WithSelectedServerPropsDeps } from './helpers/withSelectedServer'; import { withSelectedServer } from './helpers/withSelectedServer'; import { useSelectedServer } from './reducers/selectedServer'; +import { useServers } from './reducers/servers'; -type EditServerProps = { - editServer: (serverId: string, serverData: ServerData) => void; -}; - -const EditServer: FCWithDeps = withSelectedServer(({ editServer }) => { +export const EditServer: FCWithDeps = withSelectedServer(() => { + const { editServer } = useServers(); const { buildShlinkApiClient } = useDependencies(EditServer); const { selectServer, selectedServer } = useSelectedServer(); const goBack = useGoBack(); @@ -45,5 +43,3 @@ const EditServer: FCWithDeps = wit
); }); - -export const EditServerFactory = componentFactory(EditServer, ['ServerError']); diff --git a/src/servers/ManageServers.tsx b/src/servers/ManageServers.tsx index 9771cbcb2..f07ae658f 100644 --- a/src/servers/ManageServers.tsx +++ b/src/servers/ManageServers.tsx @@ -7,31 +7,26 @@ import { useMemo, useState } from 'react'; import { NoMenuLayout } from '../common/NoMenuLayout'; import type { FCWithDeps } from '../container/utils'; import { componentFactory, useDependencies } from '../container/utils'; -import type { ServersMap } from './data'; import type { ImportServersBtnProps } from './helpers/ImportServersBtn'; -import type { ManageServersRowProps } from './ManageServersRow'; +import { ManageServersRow } from './ManageServersRow'; +import { useServers } from './reducers/servers'; import type { ServersExporter } from './services/ServersExporter'; -type ManageServersProps = { - servers: ServersMap; -}; - type ManageServersDeps = { ServersExporter: ServersExporter; ImportServersBtn: FC; useTimeoutToggle: TimeoutToggle; - ManageServersRow: FC; }; const SHOW_IMPORT_MSG_TIME = 4000; -const ManageServers: FCWithDeps = ({ servers }) => { +const ManageServers: FCWithDeps = () => { const { ServersExporter: serversExporter, ImportServersBtn, useTimeoutToggle, - ManageServersRow, } = useDependencies(ManageServers); + const { servers } = useServers(); const [searchTerm, setSearchTerm] = useState(''); const allServers = useMemo(() => Object.values(servers), [servers]); const filteredServers = useMemo( @@ -93,5 +88,4 @@ export const ManageServersFactory = componentFactory(ManageServers, [ 'ServersExporter', 'ImportServersBtn', 'useTimeoutToggle', - 'ManageServersRow', ]); diff --git a/src/servers/ManageServersRow.tsx b/src/servers/ManageServersRow.tsx index 1181b4f33..f052b7291 100644 --- a/src/servers/ManageServersRow.tsx +++ b/src/servers/ManageServersRow.tsx @@ -3,22 +3,15 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Table, Tooltip, useTooltip } from '@shlinkio/shlink-frontend-kit'; import type { FC } from 'react'; import { Link } from 'react-router'; -import type { FCWithDeps } from '../container/utils'; -import { componentFactory, useDependencies } from '../container/utils'; import type { ServerWithId } from './data'; -import type { ManageServersRowDropdownProps } from './ManageServersRowDropdown'; +import { ManageServersRowDropdown } from './ManageServersRowDropdown'; export type ManageServersRowProps = { server: ServerWithId; hasAutoConnect: boolean; }; -type ManageServersRowDeps = { - ManageServersRowDropdown: FC; -}; - -const ManageServersRow: FCWithDeps = ({ server, hasAutoConnect }) => { - const { ManageServersRowDropdown } = useDependencies(ManageServersRow); +export const ManageServersRow: FC = ({ server, hasAutoConnect }) => { const { anchor, tooltip } = useTooltip(); return ( @@ -31,6 +24,7 @@ const ManageServersRow: FCWithDeps icon={checkIcon} className="text-lm-brand dark:text-dm-brand" {...anchor} + data-testid="auto-connect" /> Auto-connect to this server @@ -47,5 +41,3 @@ const ManageServersRow: FCWithDeps ); }; - -export const ManageServersRowFactory = componentFactory(ManageServersRow, ['ManageServersRowDropdown']); diff --git a/src/servers/ManageServersRowDropdown.tsx b/src/servers/ManageServersRowDropdown.tsx index e09779360..eeaa36e90 100644 --- a/src/servers/ManageServersRowDropdown.tsx +++ b/src/servers/ManageServersRowDropdown.tsx @@ -6,29 +6,18 @@ import { faPlug as connectIcon, } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { RowDropdown,useToggle } from '@shlinkio/shlink-frontend-kit'; +import { RowDropdown, useToggle } from '@shlinkio/shlink-frontend-kit'; import type { FC } from 'react'; -import type { FCWithDeps } from '../container/utils'; -import { componentFactory, useDependencies } from '../container/utils'; import type { ServerWithId } from './data'; -import type { DeleteServerModalProps } from './DeleteServerModal'; +import { DeleteServerModal } from './DeleteServerModal'; +import { useServers } from './reducers/servers'; export type ManageServersRowDropdownProps = { server: ServerWithId; }; -type ManageServersRowDropdownConnectProps = ManageServersRowDropdownProps & { - setAutoConnect: (server: ServerWithId, autoConnect: boolean) => void; -}; - -type ManageServersRowDropdownDeps = { - DeleteServerModal: FC -}; - -const ManageServersRowDropdown: FCWithDeps = ( - { server, setAutoConnect }, -) => { - const { DeleteServerModal } = useDependencies(ManageServersRowDropdown); +export const ManageServersRowDropdown: FC = ({ server }) => { + const { setAutoConnect } = useServers(); const { flag: isModalOpen, setToTrue: showModal, setToFalse: hideModal } = useToggle(); const serverUrl = `/server/${server.id}`; const { autoConnect: isAutoConnect } = server; @@ -56,5 +45,3 @@ const ManageServersRowDropdown: FCWithDeps ); }; - -export const ManageServersRowDropdownFactory = componentFactory(ManageServersRowDropdown, ['DeleteServerModal']); diff --git a/src/servers/ServersDropdown.tsx b/src/servers/ServersDropdown.tsx index 95147f425..1e232b502 100644 --- a/src/servers/ServersDropdown.tsx +++ b/src/servers/ServersDropdown.tsx @@ -1,15 +1,13 @@ 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 { ServersMap } from './data'; +import type { FC } from 'react'; import { getServerId } from './data'; import { useSelectedServer } from './reducers/selectedServer'; +import { useServers } from './reducers/servers'; -export interface ServersDropdownProps { - servers: ServersMap; -} - -export const ServersDropdown = ({ servers }: ServersDropdownProps) => { +export const ServersDropdown: FC = () => { + const { servers } = useServers(); const serversList = Object.values(servers); const { selectedServer } = useSelectedServer(); diff --git a/src/servers/ServersListGroup.tsx b/src/servers/ServersListGroup.tsx index bf991640b..ec779c2b2 100644 --- a/src/servers/ServersListGroup.tsx +++ b/src/servers/ServersListGroup.tsx @@ -26,18 +26,16 @@ const ServerListItem = ({ id, name }: { id: string; name: string }) => ( ); export const ServersListGroup: FC = ({ servers, borderless }) => ( - <> - {servers.length > 0 && ( -
- {servers.map(({ id, name }) => )} -
- )} - + servers.length > 0 && ( +
+ {servers.map(({ id, name }) => )} +
+ ) ); diff --git a/src/servers/helpers/DuplicatedServersModal.tsx b/src/servers/helpers/DuplicatedServersModal.tsx index 07708c855..f1e8f492a 100644 --- a/src/servers/helpers/DuplicatedServersModal.tsx +++ b/src/servers/helpers/DuplicatedServersModal.tsx @@ -26,7 +26,7 @@ export const DuplicatedServersModal: FC = ( cancelText={hasMultipleServers ? 'Ignore duplicates' : 'Discard'} >

{hasMultipleServers ? 'The next servers already exist:' : 'There is already a server with:'}

-
    +
      {duplicatedServers.map(({ url, apiKey }, index) => (!hasMultipleServers ? (
    • URL: {url}
    • diff --git a/src/servers/helpers/ImportServersBtn.tsx b/src/servers/helpers/ImportServersBtn.tsx index 2468f3414..37c4b236e 100644 --- a/src/servers/helpers/ImportServersBtn.tsx +++ b/src/servers/helpers/ImportServersBtn.tsx @@ -5,7 +5,8 @@ import type { ChangeEvent, PropsWithChildren } from 'react'; import { useCallback, useRef, useState } from 'react'; import type { FCWithDeps } from '../../container/utils'; import { componentFactory, useDependencies } from '../../container/utils'; -import type { ServerData, ServersMap, ServerWithId } from '../data'; +import type { ServerData } from '../data'; +import { useServers } from '../reducers/servers'; import type { ServersImporter } from '../services/ServersImporter'; import { DuplicatedServersModal } from './DuplicatedServersModal'; import { dedupServers, ensureUniqueIds } from './index'; @@ -17,24 +18,18 @@ export type ImportServersBtnProps = PropsWithChildren<{ className?: string; }>; -type ImportServersBtnConnectProps = ImportServersBtnProps & { - createServers: (servers: ServerWithId[]) => void; - servers: ServersMap; -}; - type ImportServersBtnDeps = { ServersImporter: ServersImporter }; -const ImportServersBtn: FCWithDeps = ({ - createServers, - servers, +const ImportServersBtn: FCWithDeps = ({ children, onImport, onError = () => {}, tooltipPlacement = 'bottom', className = '', }) => { + const { createServers, servers } = useServers(); const { ServersImporter: serversImporter } = useDependencies(ImportServersBtn); const fileInputRef = useRef(null); const { anchor, tooltip } = useTooltip({ placement: tooltipPlacement }); diff --git a/src/servers/helpers/ServerError.tsx b/src/servers/helpers/ServerError.tsx index ac4acce38..ddbc6cb59 100644 --- a/src/servers/helpers/ServerError.tsx +++ b/src/servers/helpers/ServerError.tsx @@ -2,24 +2,14 @@ import { Card, Message } from '@shlinkio/shlink-frontend-kit'; import type { FC } from 'react'; import { Link } from 'react-router'; import { NoMenuLayout } from '../../common/NoMenuLayout'; -import type { FCWithDeps } from '../../container/utils'; -import { componentFactory, useDependencies } from '../../container/utils'; -import type { ServersMap } from '../data'; import { isServerWithId } from '../data'; -import type { DeleteServerButtonProps } from '../DeleteServerButton'; +import { DeleteServerButton } from '../DeleteServerButton'; import { useSelectedServer } from '../reducers/selectedServer'; +import { useServers } from '../reducers/servers'; import { ServersListGroup } from '../ServersListGroup'; -type ServerErrorProps = { - servers: ServersMap; -}; - -type ServerErrorDeps = { - DeleteServerButton: FC; -}; - -const ServerError: FCWithDeps = ({ servers }) => { - const { DeleteServerButton } = useDependencies(ServerError); +export const ServerError: FC = () => { + const { servers } = useServers(); const { selectedServer } = useSelectedServer(); return ( @@ -55,5 +45,3 @@ const ServerError: FCWithDeps = ({ servers }) ); }; - -export const ServerErrorFactory = componentFactory(ServerError, ['DeleteServerButton']); diff --git a/src/servers/helpers/withSelectedServer.tsx b/src/servers/helpers/withSelectedServer.tsx index 1386ff286..1661f7f58 100644 --- a/src/servers/helpers/withSelectedServer.tsx +++ b/src/servers/helpers/withSelectedServer.tsx @@ -1,5 +1,4 @@ import { Message } from '@shlinkio/shlink-frontend-kit'; -import type { FC } from 'react'; import { useEffect } from 'react'; import { useParams } from 'react-router'; import type { ShlinkApiClientBuilder } from '../../api/services/ShlinkApiClientBuilder'; @@ -8,9 +7,9 @@ import type { FCWithDeps } from '../../container/utils'; import { useDependencies } from '../../container/utils'; import { isNotFoundServer } from '../data'; import { useSelectedServer } from '../reducers/selectedServer'; +import { ServerError } from './ServerError'; export type WithSelectedServerPropsDeps = { - ServerError: FC; buildShlinkApiClient: ShlinkApiClientBuilder; }; @@ -18,7 +17,7 @@ export function withSelectedServer( WrappedComponent: FCWithDeps, ) { const ComponentWrapper: FCWithDeps = (props) => { - const { ServerError, buildShlinkApiClient } = useDependencies(ComponentWrapper); + const { buildShlinkApiClient } = useDependencies(ComponentWrapper); const params = useParams<{ serverId: string }>(); const { selectServer, selectedServer } = useSelectedServer(); diff --git a/src/servers/reducers/servers.ts b/src/servers/reducers/servers.ts index ed02fe681..683959085 100644 --- a/src/servers/reducers/servers.ts +++ b/src/servers/reducers/servers.ts @@ -1,21 +1,23 @@ import type { PayloadAction } from '@reduxjs/toolkit'; import { createSlice } from '@reduxjs/toolkit'; +import { useCallback } from 'react'; +import { useAppDispatch, useAppSelector } from '../../store'; import type { ServerData, ServersMap, ServerWithId } from '../data'; import { serversListToMap } from '../helpers'; -interface EditServer { +type EditServer = { serverId: string; serverData: Partial; -} +}; -interface SetAutoConnect { +type SetAutoConnect = { serverId: string; autoConnect: boolean; -} +}; const initialState: ServersMap = {}; -export const { actions, reducer } = createSlice({ +export const { actions, reducer: serversReducer } = createSlice({ name: 'shlink/servers', initialState, reducers: { @@ -65,4 +67,19 @@ export const { actions, reducer } = createSlice({ export const { editServer, deleteServer, setAutoConnect, createServers } = actions; -export const serversReducer = reducer; +export const useServers = () => { + const dispatch = useAppDispatch(); + const servers = useAppSelector((state) => state.servers); + const editServer = useCallback( + (serverId: string, serverData: Partial) => dispatch(actions.editServer(serverId, serverData)), + [dispatch], + ); + const deleteServer = useCallback((server: ServerWithId) => dispatch(actions.deleteServer(server)), [dispatch]); + const setAutoConnect = useCallback( + (serverData: ServerWithId, autoConnect: boolean) => dispatch(actions.setAutoConnect(serverData, autoConnect)), + [dispatch], + ); + const createServers = useCallback((servers: ServerWithId[]) => dispatch(actions.createServers(servers)), [dispatch]); + + return { servers, editServer, deleteServer, setAutoConnect, createServers }; +}; diff --git a/src/servers/services/provideServices.ts b/src/servers/services/provideServices.ts index 47e49208d..b921245e4 100644 --- a/src/servers/services/provideServices.ts +++ b/src/servers/services/provideServices.ts @@ -1,63 +1,26 @@ import type Bottle from 'bottlejs'; -import type { ConnectDecorator } from '../../container/types'; import { CreateServerFactory } from '../CreateServer'; -import { DeleteServerButtonFactory } from '../DeleteServerButton'; -import { DeleteServerModal } from '../DeleteServerModal'; -import { EditServerFactory } from '../EditServer'; import { ImportServersBtnFactory } from '../helpers/ImportServersBtn'; -import { ServerErrorFactory } from '../helpers/ServerError'; import { withoutSelectedServer } from '../helpers/withoutSelectedServer'; import { ManageServersFactory } from '../ManageServers'; -import { ManageServersRowFactory } from '../ManageServersRow'; -import { ManageServersRowDropdownFactory } from '../ManageServersRowDropdown'; import { fetchServers } from '../reducers/remoteServers'; -import { selectServer } from '../reducers/selectedServer'; -import { createServers, deleteServer, editServer, setAutoConnect } from '../reducers/servers'; -import { ServersDropdown } from '../ServersDropdown'; import { ServersExporter } from './ServersExporter'; import { ServersImporter } from './ServersImporter'; -export const provideServices = (bottle: Bottle, connect: ConnectDecorator) => { +export const provideServices = (bottle: Bottle) => { // Components bottle.factory('ManageServers', ManageServersFactory); bottle.decorator('ManageServers', withoutSelectedServer); - bottle.decorator('ManageServers', connect(['servers'], [])); - - bottle.factory('ManageServersRow', ManageServersRowFactory); - - bottle.factory('ManageServersRowDropdown', ManageServersRowDropdownFactory); - bottle.decorator('ManageServersRowDropdown', connect(null, ['setAutoConnect'])); bottle.factory('CreateServer', CreateServerFactory); bottle.decorator('CreateServer', withoutSelectedServer); - bottle.decorator('CreateServer', connect(['servers'], ['createServers'])); - - bottle.factory('EditServer', EditServerFactory); - bottle.decorator('EditServer', connect([], ['editServer', 'selectServer'])); - - bottle.serviceFactory('ServersDropdown', () => ServersDropdown); - bottle.decorator('ServersDropdown', connect(['servers'])); - - bottle.serviceFactory('DeleteServerModal', () => DeleteServerModal); - bottle.decorator('DeleteServerModal', connect(null, ['deleteServer'])); - - bottle.factory('DeleteServerButton', DeleteServerButtonFactory); bottle.factory('ImportServersBtn', ImportServersBtnFactory); - bottle.decorator('ImportServersBtn', connect(['servers'], ['createServers'])); - - bottle.factory('ServerError', ServerErrorFactory); - bottle.decorator('ServerError', connect(['servers'])); // Services bottle.service('ServersImporter', ServersImporter, 'csvToJson'); bottle.service('ServersExporter', ServersExporter, 'Storage', 'window', 'jsonToCsv'); // Actions - bottle.serviceFactory('selectServer', () => selectServer, 'buildShlinkApiClient', 'loadMercureInfo'); - bottle.serviceFactory('createServers', () => createServers); - bottle.serviceFactory('deleteServer', () => deleteServer); - bottle.serviceFactory('editServer', () => editServer); - bottle.serviceFactory('setAutoConnect', () => setAutoConnect); bottle.serviceFactory('fetchServers', fetchServers, 'HttpClient'); }; diff --git a/test/__helpers__/setUpTest.tsx b/test/__helpers__/setUpTest.tsx index 00f4625db..91763f86b 100644 --- a/test/__helpers__/setUpTest.tsx +++ b/test/__helpers__/setUpTest.tsx @@ -19,10 +19,11 @@ export const renderWithStore = ( element: ReactElement, { initialState = {}, ...options }: RenderOptionsWithState = {}, ) => { - const Wrapper = ({ children }: PropsWithChildren) => ( - - {children} - - ); - return renderWithEvents(element, { ...options, wrapper: Wrapper }); + const store = setUpStore(initialState); + const Wrapper = ({ children }: PropsWithChildren) => {children}; + + return { + store, + ...renderWithEvents(element, { ...options, wrapper: Wrapper }), + }; }; diff --git a/test/app/App.test.tsx b/test/app/App.test.tsx index 0a0e099c1..7717106d5 100644 --- a/test/app/App.test.tsx +++ b/test/app/App.test.tsx @@ -2,19 +2,17 @@ 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 type { ServerWithId } from '../../src/servers/data'; import { checkAccessibility } from '../__helpers__/accessibility'; import { renderWithStore } from '../__helpers__/setUpTest'; describe('', () => { const App = AppFactory( fromPartial({ - MainHeader: () => <>MainHeader, Home: () => <>Home, ShlinkWebComponentContainer: () => <>ShlinkWebComponentContainer, CreateServer: () => <>CreateServer, - EditServer: () => <>EditServer, ManageServers: () => <>ManageServers, - ShlinkVersionsContainer: () => <>ShlinkVersions, }), ); const setUp = async (activeRoute = '/') => act(() => renderWithStore( @@ -27,24 +25,25 @@ describe('', () => { resetAppUpdate={() => {}} /> , + { + initialState: { + servers: { + abc123: fromPartial({ id: 'abc123', name: 'abc123 server' }), + def456: fromPartial({ id: 'def456', name: 'def456 server' }), + }, + }, + }, )); it('passes a11y checks', () => checkAccessibility(setUp())); - it('renders children components', async () => { - await setUp(); - - expect(screen.getByText('MainHeader')).toBeInTheDocument(); - expect(screen.getByText('ShlinkVersions')).toBeInTheDocument(); - }); - it.each([ ['/settings/general', 'User interface'], ['/settings/short-urls', 'Short URLs form'], ['/manage-servers', 'ManageServers'], ['/server/create', 'CreateServer'], - ['/server/abc123/edit', 'EditServer'], - ['/server/def456/edit', 'EditServer'], + ['/server/abc123/edit', 'Edit "abc123 server"'], + ['/server/def456/edit', 'Edit "def456 server"'], ['/server/abc123/foo', 'ShlinkWebComponentContainer'], ['/server/def456/bar', 'ShlinkWebComponentContainer'], ['/other', 'Oops! We could not find requested route.'], diff --git a/test/common/Home.test.tsx b/test/common/Home.test.tsx index 912594d3a..84b36a5f2 100644 --- a/test/common/Home.test.tsx +++ b/test/common/Home.test.tsx @@ -1,15 +1,19 @@ -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 { Home } from '../../src/common/Home'; import type { ServersMap, ServerWithId } from '../../src/servers/data'; import { checkAccessibility } from '../__helpers__/accessibility'; +import { renderWithStore } from '../__helpers__/setUpTest'; describe('', () => { - const setUp = (servers: ServersMap = {}) => render( + const setUp = (servers: ServersMap = {}) => renderWithStore( - + , + { + initialState: { servers }, + }, ); it('passes a11y checks', () => checkAccessibility( diff --git a/test/common/MainHeader.test.tsx b/test/common/MainHeader.test.tsx index 2a1025438..e70f755dd 100644 --- a/test/common/MainHeader.test.tsx +++ b/test/common/MainHeader.test.tsx @@ -1,21 +1,16 @@ import { screen } from '@testing-library/react'; -import { fromPartial } from '@total-typescript/shoehorn'; import { createMemoryHistory } from 'history'; import { Router } from 'react-router'; -import { MainHeaderFactory } from '../../src/common/MainHeader'; +import { MainHeader } from '../../src/common/MainHeader'; import { checkAccessibility } from '../__helpers__/accessibility'; -import { renderWithEvents } from '../__helpers__/setUpTest'; +import { renderWithStore } from '../__helpers__/setUpTest'; describe('', () => { - const MainHeader = MainHeaderFactory(fromPartial({ - // Fake this component as a li[role="menuitem"], as it gets rendered inside a ul[role="menu"] - ServersDropdown: () =>
    • ServersDropdown
    • , - })); const setUp = (pathname = '') => { const history = createMemoryHistory(); history.push(pathname); - return renderWithEvents( + return renderWithStore( , @@ -26,7 +21,7 @@ describe('', () => { it('renders ServersDropdown', () => { setUp(); - expect(screen.getByText('ServersDropdown')).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'Servers' })).toBeInTheDocument(); }); it.each([ diff --git a/test/common/ShlinkWebComponentContainer.test.tsx b/test/common/ShlinkWebComponentContainer.test.tsx index 2237c0fc9..12d429bbe 100644 --- a/test/common/ShlinkWebComponentContainer.test.tsx +++ b/test/common/ShlinkWebComponentContainer.test.tsx @@ -1,5 +1,6 @@ import { screen } from '@testing-library/react'; import { fromPartial } from '@total-typescript/shoehorn'; +import { MemoryRouter } from 'react-router'; import { ShlinkWebComponentContainerFactory } from '../../src/common/ShlinkWebComponentContainer'; import type { NonReachableServer, NotFoundServer, SelectedServer } from '../../src/servers/data'; import { checkAccessibility } from '../__helpers__/accessibility'; @@ -15,12 +16,13 @@ describe('', () => { const ShlinkWebComponentContainer = ShlinkWebComponentContainerFactory(fromPartial({ buildShlinkApiClient: vi.fn().mockReturnValue(fromPartial({})), TagColorsStorage: fromPartial({}), - ServerError: () => <>ServerError, })); const setUp = (selectedServer: SelectedServer) => renderWithStore( - , + + + , { - initialState: { selectedServer }, + initialState: { selectedServer, servers: {} }, }, ); @@ -30,18 +32,20 @@ describe('', () => { setUp(null); expect(screen.getByText('Loading...')).toBeInTheDocument(); - expect(screen.queryByText('ServerError')).not.toBeInTheDocument(); expect(screen.queryByText('ShlinkWebComponent')).not.toBeInTheDocument(); }); it.each([ - [fromPartial({ serverNotFound: true })], - [fromPartial({ serverNotReachable: true })], - ])('shows error for non reachable servers', (selectedServer) => { + [fromPartial({ serverNotFound: true }), 'Could not find this Shlink server.'], + [ + fromPartial({ id: 'foo', serverNotReachable: true }), + /Could not connect to this Shlink server/, + ], + ])('shows error for non reachable servers', (selectedServer, expectedError) => { setUp(selectedServer); expect(screen.queryByText('Loading...')).not.toBeInTheDocument(); - expect(screen.getByText('ServerError')).toBeInTheDocument(); + expect(screen.getByText(expectedError)).toBeInTheDocument(); expect(screen.queryByText('ShlinkWebComponent')).not.toBeInTheDocument(); }); @@ -49,7 +53,6 @@ describe('', () => { setUp(fromPartial({ version: '3.0.0' })); expect(screen.queryByText('Loading...')).not.toBeInTheDocument(); - expect(screen.queryByText('ServerError')).not.toBeInTheDocument(); expect(screen.getByText('ShlinkWebComponent')).toBeInTheDocument(); }); }); diff --git a/test/servers/CreateServer.test.tsx b/test/servers/CreateServer.test.tsx index 204d67486..05375b48b 100644 --- a/test/servers/CreateServer.test.tsx +++ b/test/servers/CreateServer.test.tsx @@ -5,7 +5,7 @@ import { Router } from 'react-router'; import { CreateServerFactory } from '../../src/servers/CreateServer'; import type { ServersMap } from '../../src/servers/data'; import { checkAccessibility } from '../__helpers__/accessibility'; -import { renderWithEvents } from '../__helpers__/setUpTest'; +import { renderWithStore } from '../__helpers__/setUpTest'; type SetUpOptions = { serversImported?: boolean; @@ -14,9 +14,8 @@ type SetUpOptions = { }; describe('', () => { - const createServersMock = vi.fn(); const defaultServers: ServersMap = { - foo: fromPartial({ url: 'https://existing_url.com', apiKey: 'existing_api_key' }), + foo: fromPartial({ url: 'https://existing_url.com', apiKey: 'existing_api_key', id: 'foo' }), }; const setUp = ({ serversImported = false, importFailed = false, servers = defaultServers }: SetUpOptions = {}) => { let callCount = 0; @@ -33,10 +32,13 @@ describe('', () => { return { history, - ...renderWithEvents( + ...renderWithStore( - + , + { + initialState: { servers }, + }, ), }; }; @@ -68,21 +70,23 @@ describe('', () => { }); it('creates server data when form is submitted', async () => { - const { user, history } = setUp(); - - expect(createServersMock).not.toHaveBeenCalled(); + const { user, history, store } = setUp(); + const expectedServerId = 'the_name-the_url.com'; await user.type(screen.getByLabelText(/^Name/), 'the_name'); await user.type(screen.getByLabelText(/^URL/), 'https://the_url.com'); await user.type(screen.getByLabelText(/^API key/), 'the_api_key'); - fireEvent.submit(screen.getByRole('form')); - expect(createServersMock).toHaveBeenCalledWith([expect.objectContaining({ + expect(store.getState().servers[expectedServerId]).not.toBeDefined(); + fireEvent.submit(screen.getByRole('form')); + expect(store.getState().servers[expectedServerId]).toEqual(expect.objectContaining({ + id: expectedServerId, name: 'the_name', url: 'https://the_url.com', apiKey: 'the_api_key', - })]); - expect(history.location.pathname).toEqual(expect.stringMatching(/^\/server\//)); + })); + + expect(history.location.pathname).toEqual(`/server/${expectedServerId}`); expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); }); @@ -92,12 +96,12 @@ describe('', () => { await user.type(screen.getByLabelText(/^Name/), 'the_name'); await user.type(screen.getByLabelText(/^URL/), 'https://existing_url.com'); await user.type(screen.getByLabelText(/^API key/), 'existing_api_key'); + fireEvent.submit(screen.getByRole('form')); await waitFor(() => expect(screen.getByRole('dialog')).toBeInTheDocument()); await user.click(screen.getByRole('button', { name: 'Discard' })); - expect(createServersMock).not.toHaveBeenCalled(); expect(history.location.pathname).toEqual('/foo'); // Goes back to first route from history's initialEntries }); }); diff --git a/test/servers/DeleteServerButton.test.tsx b/test/servers/DeleteServerButton.test.tsx index fce035cb9..c82cb8949 100644 --- a/test/servers/DeleteServerButton.test.tsx +++ b/test/servers/DeleteServerButton.test.tsx @@ -3,19 +3,14 @@ import { fromPartial } from '@total-typescript/shoehorn'; import { createMemoryHistory } from 'history'; import type { ReactNode } from 'react'; import { Router } from 'react-router'; -import { DeleteServerButtonFactory } from '../../src/servers/DeleteServerButton'; -import type { DeleteServerModalProps } from '../../src/servers/DeleteServerModal'; -import { DeleteServerModal } from '../../src/servers/DeleteServerModal'; +import { DeleteServerButton } from '../../src/servers/DeleteServerButton'; import { checkAccessibility } from '../__helpers__/accessibility'; -import { renderWithEvents } from '../__helpers__/setUpTest'; +import { renderWithStore } from '../__helpers__/setUpTest'; describe('', () => { - const DeleteServerButton = DeleteServerButtonFactory(fromPartial({ - DeleteServerModal: (props: DeleteServerModalProps) => , - })); const setUp = (children: ReactNode = 'Remove this server') => { const history = createMemoryHistory({ initialEntries: ['/foo'] }); - const result = renderWithEvents( + const result = renderWithStore( {children} , diff --git a/test/servers/DeleteServerModal.test.tsx b/test/servers/DeleteServerModal.test.tsx index c573b93b6..a377c60fe 100644 --- a/test/servers/DeleteServerModal.test.tsx +++ b/test/servers/DeleteServerModal.test.tsx @@ -1,23 +1,23 @@ import { screen } from '@testing-library/react'; import { fromPartial } from '@total-typescript/shoehorn'; +import type { ServerWithId } from '../../src/servers/data'; import { DeleteServerModal } from '../../src/servers/DeleteServerModal'; import { checkAccessibility } from '../__helpers__/accessibility'; -import { renderWithEvents } from '../__helpers__/setUpTest'; +import { renderWithStore } from '../__helpers__/setUpTest'; import { TestModalWrapper } from '../__helpers__/TestModalWrapper'; describe('', () => { - const deleteServerMock = vi.fn(); const serverName = 'the_server_name'; - const setUp = () => renderWithEvents( + const server = fromPartial({ id: 'foo', name: serverName }); + const setUp = () => renderWithStore( ( - - )} + renderModal={(args) => } />, + { + initialState: { + servers: { foo: server }, + }, + }, ); it('passes a11y checks', () => checkAccessibility(setUp())); @@ -40,19 +40,21 @@ describe('', () => { [() => screen.getByRole('button', { name: 'Cancel' })], [() => screen.getByLabelText('Close dialog')], ])('closes dialog when clicking cancel button', async (getButton) => { - const { user } = setUp(); + const { user, store } = setUp(); expect(screen.getByRole('dialog')).toBeInTheDocument(); await user.click(getButton()); expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); - expect(deleteServerMock).not.toHaveBeenCalled(); + + // No server has been deleted + expect(Object.keys(store.getState().servers)).toHaveLength(1); }); it('deletes server when clicking accept button', async () => { - const { user } = setUp(); + const { user, store } = setUp(); - expect(deleteServerMock).not.toHaveBeenCalled(); + expect(Object.keys(store.getState().servers)).toHaveLength(1); await user.click(screen.getByRole('button', { name: 'Delete' })); - expect(deleteServerMock).toHaveBeenCalledOnce(); + expect(Object.keys(store.getState().servers)).toHaveLength(0); }); }); diff --git a/test/servers/EditServer.test.tsx b/test/servers/EditServer.test.tsx index 85011d156..8c4ed7b47 100644 --- a/test/servers/EditServer.test.tsx +++ b/test/servers/EditServer.test.tsx @@ -1,32 +1,33 @@ -import { fireEvent, screen } from '@testing-library/react'; +import { fireEvent, screen, waitFor } from '@testing-library/react'; import { fromPartial } from '@total-typescript/shoehorn'; import { createMemoryHistory } from 'history'; import { Router } from 'react-router'; import type { ReachableServer, SelectedServer } from '../../src/servers/data'; -import { EditServerFactory } from '../../src/servers/EditServer'; +import { isServerWithId } from '../../src/servers/data'; +import { EditServer } from '../../src/servers/EditServer'; import { checkAccessibility } from '../__helpers__/accessibility'; import { renderWithStore } from '../__helpers__/setUpTest'; describe('', () => { - const ServerError = vi.fn(); - const editServerMock = vi.fn(); const defaultSelectedServer = fromPartial({ id: 'abc123', name: 'the_name', url: 'the_url', apiKey: 'the_api_key', }); - const EditServer = EditServerFactory(fromPartial({ ServerError })); const setUp = (selectedServer: SelectedServer = defaultSelectedServer) => { const history = createMemoryHistory({ initialEntries: ['/foo', '/bar'] }); return { history, ...renderWithStore( - + , { - initialState: { selectedServer }, + initialState: { + selectedServer, + servers: isServerWithId(selectedServer) ? { [selectedServer.id]: selectedServer } : {}, + }, }, ), }; @@ -56,7 +57,7 @@ describe('', () => { }); it('edits server and redirects to it when form is submitted', async () => { - const { user, history } = setUp(); + const { user, history, store } = setUp(); await user.type(screen.getByLabelText(/^Name/), ' edited'); await user.type(screen.getByLabelText(/^URL/), ' edited'); @@ -64,12 +65,10 @@ describe('', () => { // await user.click(screen.getByRole('button', { name: 'Save' })); fireEvent.submit(screen.getByRole('form')); - expect(editServerMock).toHaveBeenCalledWith('abc123', { + expect(store.getState().servers[defaultSelectedServer.id]).toEqual(expect.objectContaining({ name: 'the_name edited', url: 'the_url edited', - apiKey: 'the_api_key', - forwardCredentials: false, - }); + })); // After saving we go back, to the first route from history's initialEntries expect(history.location.pathname).toEqual('/foo'); @@ -78,16 +77,15 @@ describe('', () => { it.each([ { forwardCredentials: true }, { forwardCredentials: false }, - ])('edits advanced options - forward credentials', async (serverPartial) => { - const { user } = setUp({ ...defaultSelectedServer, ...serverPartial }); + ])('edits advanced options - forward credentials', async ({ forwardCredentials }) => { + const { user, store } = setUp({ ...defaultSelectedServer, forwardCredentials }); await user.click(screen.getByText('Advanced options')); await user.click(screen.getByLabelText('Forward credentials to this server on every request.')); - fireEvent.submit(screen.getByRole('form')); - expect(editServerMock).toHaveBeenCalledWith('abc123', expect.objectContaining({ - forwardCredentials: !serverPartial.forwardCredentials, - })); + await waitFor(() => expect(store.getState().servers[defaultSelectedServer.id]).toEqual(expect.objectContaining({ + forwardCredentials: !forwardCredentials, + }))); }); }); diff --git a/test/servers/ManageServers.test.tsx b/test/servers/ManageServers.test.tsx index 2340dd814..c3cebeab8 100644 --- a/test/servers/ManageServers.test.tsx +++ b/test/servers/ManageServers.test.tsx @@ -5,7 +5,7 @@ import type { ServersMap, ServerWithId } from '../../src/servers/data'; import { ManageServersFactory } from '../../src/servers/ManageServers'; import type { ServersExporter } from '../../src/servers/services/ServersExporter'; import { checkAccessibility } from '../__helpers__/accessibility'; -import { renderWithEvents } from '../__helpers__/setUpTest'; +import { renderWithStore } from '../__helpers__/setUpTest'; describe('', () => { const exportServers = vi.fn(); @@ -15,15 +15,15 @@ describe('', () => { ServersExporter: serversExporter, ImportServersBtn: () => ImportServersBtn, useTimeoutToggle, - ManageServersRow: ({ hasAutoConnect }: { hasAutoConnect: boolean }) => ( - ManageServersRow {hasAutoConnect ? '[YES]' : '[NO]'} - ), })); const createServerMock = (value: string, autoConnect = false) => fromPartial( { id: value, name: value, url: value, autoConnect }, ); - const setUp = (servers: ServersMap = {}) => renderWithEvents( - , + const setUp = (servers: ServersMap = {}) => renderWithStore( + , + { + initialState: { servers }, + }, ); it('passes a11y checks', () => checkAccessibility(setUp({ @@ -42,20 +42,22 @@ describe('', () => { await user.clear(screen.getByPlaceholderText('Search...')); await user.type(screen.getByPlaceholderText('Search...'), searchTerm); }; + // Add one for the header row + const expectRows = (amount: number) => expect(screen.getAllByRole('row')).toHaveLength(amount + 1); - expect(screen.getAllByText(/^ManageServersRow/)).toHaveLength(3); + expectRows(3); expect(screen.queryByText('No servers found.')).not.toBeInTheDocument(); await search('foo'); - await waitFor(() => expect(screen.getAllByText(/^ManageServersRow/)).toHaveLength(1)); + await waitFor(() => expectRows(1)); expect(screen.queryByText('No servers found.')).not.toBeInTheDocument(); await search('Ba'); - await waitFor(() => expect(screen.getAllByText(/^ManageServersRow/)).toHaveLength(2)); + await waitFor(() => expectRows(2)); expect(screen.queryByText('No servers found.')).not.toBeInTheDocument(); await search('invalid'); - await waitFor(() => expect(screen.queryByText(/^ManageServersRow/)).not.toBeInTheDocument()); + await waitFor(() => expectRows(1)); expect(screen.getByText('No servers found.')).toBeInTheDocument(); }); @@ -67,11 +69,9 @@ describe('', () => { expect(screen.getAllByRole('columnheader')).toHaveLength(expectedCols); if (server.autoConnect) { - expect(screen.getByText(/\[YES]/)).toBeInTheDocument(); - expect(screen.queryByText(/\[NO]/)).not.toBeInTheDocument(); + expect(screen.getByTestId('auto-connect')).toBeInTheDocument(); } else { - expect(screen.queryByText(/\[YES]/)).not.toBeInTheDocument(); - expect(screen.getByText(/\[NO]/)).toBeInTheDocument(); + expect(screen.queryByTestId('auto-connect')).not.toBeInTheDocument(); } }); diff --git a/test/servers/ManageServersRow.test.tsx b/test/servers/ManageServersRow.test.tsx index 9a97d4050..e625954b8 100644 --- a/test/servers/ManageServersRow.test.tsx +++ b/test/servers/ManageServersRow.test.tsx @@ -1,22 +1,19 @@ import { Table } from '@shlinkio/shlink-frontend-kit'; -import { render, screen } from '@testing-library/react'; -import { fromPartial } from '@total-typescript/shoehorn'; +import { screen } from '@testing-library/react'; import { MemoryRouter } from 'react-router'; import type { ServerWithId } from '../../src/servers/data'; -import { ManageServersRowFactory } from '../../src/servers/ManageServersRow'; +import { ManageServersRow } from '../../src/servers/ManageServersRow'; import { checkAccessibility } from '../__helpers__/accessibility'; +import { renderWithStore } from '../__helpers__/setUpTest'; describe('', () => { - const ManageServersRow = ManageServersRowFactory(fromPartial({ - ManageServersRowDropdown: () => ManageServersRowDropdown, - })); const server: ServerWithId = { name: 'My server', url: 'https://example.com', apiKey: '123', id: 'abc', }; - const setUp = (hasAutoConnect = false, autoConnect = false) => render( + const setUp = (hasAutoConnect = false, autoConnect = false) => renderWithStore( }> @@ -34,9 +31,9 @@ describe('', () => { expect(screen.getAllByRole('cell')).toHaveLength(expectedCols); }); - it('renders a dropdown', () => { + it('renders an options dropdown', () => { setUp(); - expect(screen.getByText('ManageServersRowDropdown')).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'Options' })).toBeInTheDocument(); }); it.each([ diff --git a/test/servers/ManageServersRowDropdown.test.tsx b/test/servers/ManageServersRowDropdown.test.tsx index cb6a96318..a97aeaf6d 100644 --- a/test/servers/ManageServersRowDropdown.test.tsx +++ b/test/servers/ManageServersRowDropdown.test.tsx @@ -3,23 +3,22 @@ import type { UserEvent } from '@testing-library/user-event'; import { fromPartial } from '@total-typescript/shoehorn'; import { MemoryRouter } from 'react-router'; import type { ServerWithId } from '../../src/servers/data'; -import { ManageServersRowDropdownFactory } from '../../src/servers/ManageServersRowDropdown'; +import { ManageServersRowDropdown } from '../../src/servers/ManageServersRowDropdown'; import { checkAccessibility } from '../__helpers__/accessibility'; -import { renderWithEvents } from '../__helpers__/setUpTest'; +import { renderWithStore } from '../__helpers__/setUpTest'; describe('', () => { - const ManageServersRowDropdown = ManageServersRowDropdownFactory(fromPartial({ - DeleteServerModal: ({ open }: { open: boolean }) => ( - DeleteServerModal {open ? '[OPEN]' : '[CLOSED]'} - ), - })); - const setAutoConnect = vi.fn(); const setUp = (autoConnect = false) => { const server = fromPartial({ id: 'abc123', autoConnect }); - return renderWithEvents( + return renderWithStore( - + , + { + initialState: { + servers: { [server.id]: server }, + }, + }, ); }; const toggleDropdown = (user: UserEvent) => user.click(screen.getByRole('button')); @@ -44,26 +43,24 @@ describe('', () => { expect(screen.getByRole('menuitem', { name: 'Edit server' })).toHaveAttribute('href', '/server/abc123/edit'); }); - it('allows toggling auto-connect', async () => { - const { user } = setUp(); + it.each([true, false])('allows toggling auto-connect', async (autoConnect) => { + const { user, store } = setUp(autoConnect); - expect(setAutoConnect).not.toHaveBeenCalled(); await toggleDropdown(user); - await user.click(screen.getByRole('menuitem', { name: 'Auto-connect' })); - expect(setAutoConnect).toHaveBeenCalledWith(expect.objectContaining({ id: 'abc123' }), true); + await user.click(screen.getByRole('menuitem', { name: autoConnect ? 'Do not auto-connect' : 'Auto-connect' })); + + expect(Object.values(store.getState().servers)[0].autoConnect).toEqual(!autoConnect); }); it('renders deletion modal', async () => { const { user } = setUp(); - expect(screen.queryByText('DeleteServerModal [OPEN]')).not.toBeInTheDocument(); - expect(screen.getByText('DeleteServerModal [CLOSED]')).toBeInTheDocument(); + expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); await toggleDropdown(user); await user.click(screen.getByRole('menuitem', { name: 'Remove server' })); - expect(screen.getByText('DeleteServerModal [OPEN]')).toBeInTheDocument(); - expect(screen.queryByText('DeleteServerModal [CLOSED]')).not.toBeInTheDocument(); + expect(screen.getByRole('dialog')).toBeInTheDocument(); }); it.each([[true], [false]])('renders expected size and icon', (autoConnect) => { diff --git a/test/servers/ServersDropdown.test.tsx b/test/servers/ServersDropdown.test.tsx index 61574c3d6..b2668bd5f 100644 --- a/test/servers/ServersDropdown.test.tsx +++ b/test/servers/ServersDropdown.test.tsx @@ -15,11 +15,11 @@ describe('', () => { const setUp = (servers: ServersMap = fallbackServers) => renderWithStore(
        - +
      , { - initialState: { selectedServer: null }, + initialState: { selectedServer: null, servers }, }, ); diff --git a/test/servers/__snapshots__/ManageServersRow.test.tsx.snap b/test/servers/__snapshots__/ManageServersRow.test.tsx.snap index a0169cc22..95412da1a 100644 --- a/test/servers/__snapshots__/ManageServersRow.test.tsx.snap +++ b/test/servers/__snapshots__/ManageServersRow.test.tsx.snap @@ -27,6 +27,7 @@ exports[` > renders auto-connect icon only if server is auto class="svg-inline--fa fa-check text-lm-brand dark:text-dm-brand" data-icon="check" data-prefix="fas" + data-testid="auto-connect" role="img" viewBox="0 0 448 512" > @@ -56,9 +57,32 @@ exports[` > renders auto-connect icon only if server is auto
      @@ -108,9 +132,32 @@ exports[` > renders auto-connect icon only if server is auto diff --git a/test/servers/__snapshots__/ManageServersRowDropdown.test.tsx.snap b/test/servers/__snapshots__/ManageServersRowDropdown.test.tsx.snap index 25f76312f..261c3b77e 100644 --- a/test/servers/__snapshots__/ManageServersRowDropdown.test.tsx.snap +++ b/test/servers/__snapshots__/ManageServersRowDropdown.test.tsx.snap @@ -6,7 +6,7 @@ exports[` > renders expected size and icon 1`] = ` class="relative inline-block" > - - DeleteServerModal - [CLOSED] - `; @@ -41,7 +37,7 @@ exports[` > renders expected size and icon 2`] = ` class="relative inline-block" > - - DeleteServerModal - [CLOSED] - `; diff --git a/test/servers/helpers/ImportServersBtn.test.tsx b/test/servers/helpers/ImportServersBtn.test.tsx index 33c221da2..06be5014f 100644 --- a/test/servers/helpers/ImportServersBtn.test.tsx +++ b/test/servers/helpers/ImportServersBtn.test.tsx @@ -6,22 +6,19 @@ import type { import { ImportServersBtnFactory } from '../../../src/servers/helpers/ImportServersBtn'; import type { ServersImporter } from '../../../src/servers/services/ServersImporter'; import { checkAccessibility } from '../../__helpers__/accessibility'; -import { renderWithEvents } from '../../__helpers__/setUpTest'; +import { renderWithStore } from '../../__helpers__/setUpTest'; describe('', () => { const csvFile = new File([''], 'servers.csv', { type: 'text/csv' }); const onImportMock = vi.fn(); - const createServersMock = vi.fn(); const importServersFromFile = vi.fn().mockResolvedValue([]); const serversImporterMock = fromPartial({ importServersFromFile }); const ImportServersBtn = ImportServersBtnFactory(fromPartial({ ServersImporter: serversImporterMock })); - const setUp = (props: Partial = {}, servers: ServersMap = {}) => renderWithEvents( - , + const setUp = (props: Partial = {}, servers: ServersMap = {}) => renderWithStore( + , + { + initialState: { servers }, + }, ); it('passes a11y checks', () => checkAccessibility(setUp())); @@ -57,11 +54,8 @@ describe('', () => { it('imports servers when file input changes', async () => { const { user } = setUp(); - const input = screen.getByTestId('csv-file-input'); - await user.upload(input, csvFile); - + await user.upload(screen.getByTestId('csv-file-input'), csvFile); expect(importServersFromFile).toHaveBeenCalledTimes(1); - expect(createServersMock).toHaveBeenCalledTimes(1); }); it.each([ @@ -78,26 +72,27 @@ describe('', () => { id: 'existingserver-s.test', }; const newServer: ServerData = { name: 'newServer', url: 'http://s.test/newUrl', apiKey: 'newApiKey' }; - const { user } = setUp({}, { [existingServer.id]: existingServer }); + const { user, store } = setUp({}, { [existingServer.id]: existingServer }); - importServersFromFile.mockResolvedValue([existingServer, newServer]); + importServersFromFile.mockResolvedValue([existingServerData, newServer]); expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); await user.upload(screen.getByTestId('csv-file-input'), csvFile); // Once the file is uploaded, non-duplicated servers are immediately created - expect(createServersMock).toHaveBeenCalledExactlyOnceWith([expect.objectContaining(newServer)]); + const { servers } = store.getState(); + expect(Object.keys(servers)).toHaveLength(2); expect(screen.getByRole('dialog')).toBeInTheDocument(); await user.click(screen.getByRole('button', { name: btnName })); - // If duplicated servers are saved, there's one extra call + // If duplicated servers are saved, there's one extra server creation if (savesDuplicatedServers) { - expect(createServersMock).toHaveBeenLastCalledWith([expect.objectContaining(existingServerData)]); + const { servers } = store.getState(); + expect(Object.keys(servers)).toHaveLength(3); } // On import is called only once, no matter what expect(onImportMock).toHaveBeenCalledOnce(); - expect(createServersMock).toHaveBeenCalledTimes(savesDuplicatedServers ? 2 : 1); }); }); diff --git a/test/servers/helpers/ServerError.test.tsx b/test/servers/helpers/ServerError.test.tsx index 0fcd7fac0..3a3aff9df 100644 --- a/test/servers/helpers/ServerError.test.tsx +++ b/test/servers/helpers/ServerError.test.tsx @@ -2,18 +2,17 @@ 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 { ServerError } 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) => renderWithStore( - + , { - initialState: { selectedServer }, + initialState: { selectedServer, servers: {} }, }, ); From 9e8498b16a3a2a848122e6c162e311f7a407266d Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Fri, 14 Nov 2025 23:20:42 +0100 Subject: [PATCH 09/24] Do not inject remoteServers state or actions --- src/app/App.tsx | 27 +++++++------------ src/app/services/provideServices.ts | 2 +- src/servers/reducers/remoteServers.ts | 29 ++++++++++++++++++--- src/servers/services/provideServices.ts | 4 --- test/app/App.test.tsx | 10 +++---- test/servers/reducers/remoteServers.test.ts | 3 +-- 6 files changed, 41 insertions(+), 34 deletions(-) diff --git a/src/app/App.tsx b/src/app/App.tsx index 5c0806137..02bda524a 100644 --- a/src/app/App.tsx +++ b/src/app/App.tsx @@ -1,8 +1,9 @@ import { changeThemeInMarkup, getSystemPreferredTheme } from '@shlinkio/shlink-frontend-kit'; +import type { HttpClient } from '@shlinkio/shlink-js-sdk'; 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'; +import { useEffect } from 'react'; import { Route, Routes, useLocation } from 'react-router'; import { AppUpdateBanner } from '../common/AppUpdateBanner'; import { MainHeader } from '../common/MainHeader'; @@ -10,14 +11,12 @@ import { NotFound } from '../common/NotFound'; import { ShlinkVersionsContainer } from '../common/ShlinkVersionsContainer'; import type { FCWithDeps } from '../container/utils'; import { componentFactory, useDependencies } from '../container/utils'; -import type { ServersMap } from '../servers/data'; import { EditServer } from '../servers/EditServer'; +import { useLoadRemoteServers } from '../servers/reducers/remoteServers'; import { Settings } from '../settings/Settings'; import { forceUpdate } from '../utils/helpers/sw'; -type AppProps = { - fetchServers: () => void; - servers: ServersMap; +export type AppProps = { settings: AppSettings; resetAppUpdate: () => void; appUpdated: boolean; @@ -28,30 +27,23 @@ type AppDeps = { ShlinkWebComponentContainer: FC; CreateServer: FC; ManageServers: FC; + HttpClient: HttpClient; }; -const App: FCWithDeps = ( - { fetchServers, servers, settings, appUpdated, resetAppUpdate }, -) => { +const App: FCWithDeps = ({ settings, appUpdated, resetAppUpdate }) => { const { Home, ShlinkWebComponentContainer, CreateServer, ManageServers, + HttpClient: httpClient, } = useDependencies(App); + useLoadRemoteServers(httpClient); + const location = useLocation(); - const initialServers = useRef(servers); const isHome = location.pathname === '/'; - useEffect(() => { - // Try to fetch the remote servers if the list is empty during first render. - // We use a ref because we don't care if the servers list becomes empty later. - if (Object.keys(initialServers.current).length === 0) { - fetchServers(); - } - }, [fetchServers]); - useEffect(() => { changeThemeInMarkup(settings.ui?.theme ?? getSystemPreferredTheme()); }, [settings.ui?.theme]); @@ -100,4 +92,5 @@ export const AppFactory = componentFactory(App, [ 'ShlinkWebComponentContainer', 'CreateServer', 'ManageServers', + 'HttpClient', ]); diff --git a/src/app/services/provideServices.ts b/src/app/services/provideServices.ts index ab9750373..9314dca1d 100644 --- a/src/app/services/provideServices.ts +++ b/src/app/services/provideServices.ts @@ -6,7 +6,7 @@ import { appUpdateAvailable, resetAppUpdate } from '../reducers/appUpdates'; export const provideServices = (bottle: Bottle, connect: ConnectDecorator) => { // Components bottle.factory('App', AppFactory); - bottle.decorator('App', connect(['servers', 'settings', 'appUpdated'], ['fetchServers', 'resetAppUpdate'])); + bottle.decorator('App', connect(['settings', 'appUpdated'], ['resetAppUpdate'])); // Actions bottle.serviceFactory('appUpdateAvailable', () => appUpdateAvailable); diff --git a/src/servers/reducers/remoteServers.ts b/src/servers/reducers/remoteServers.ts index d7619e4b7..163b7c8f6 100644 --- a/src/servers/reducers/remoteServers.ts +++ b/src/servers/reducers/remoteServers.ts @@ -1,21 +1,44 @@ import type { HttpClient } from '@shlinkio/shlink-js-sdk'; +import { useCallback, useEffect, useRef } from 'react'; import pack from '../../../package.json'; +import { useAppDispatch } from '../../store'; import { createAsyncThunk } from '../../store/helpers'; import { hasServerData } from '../data'; import { ensureUniqueIds } from '../helpers'; -import { createServers } from './servers'; +import { createServers, useServers } from './servers'; const responseToServersList = (data: any) => ensureUniqueIds( {}, (Array.isArray(data) ? data.filter(hasServerData) : []), ); -export const fetchServers = (httpClient: HttpClient) => createAsyncThunk( +export const fetchServers = createAsyncThunk( 'shlink/remoteServers/fetchServers', - async (_: void, { dispatch }): Promise => { + async (httpClient: HttpClient, { dispatch }): Promise => { const resp = await httpClient.jsonRequest(`${pack.homepage}/servers.json`); const result = responseToServersList(resp); dispatch(createServers(result)); }, ); + +export const useRemoteServers = () => { + const dispatch = useAppDispatch(); + const dispatchFetchServer = useCallback((httpClient: HttpClient) => dispatch(fetchServers(httpClient)), [dispatch]); + + return { fetchServers: dispatchFetchServer }; +}; + +export const useLoadRemoteServers = (httpClient: HttpClient) => { + const { fetchServers } = useRemoteServers(); + const { servers } = useServers(); + const initialServers = useRef(servers); + + useEffect(() => { + // Try to fetch the remote servers if the list is empty during first render. + // We use a ref because we don't care if the servers list becomes empty later. + if (Object.keys(initialServers.current).length === 0) { + fetchServers(httpClient); + } + }, [fetchServers, httpClient]); +}; diff --git a/src/servers/services/provideServices.ts b/src/servers/services/provideServices.ts index b921245e4..bb54bfa95 100644 --- a/src/servers/services/provideServices.ts +++ b/src/servers/services/provideServices.ts @@ -3,7 +3,6 @@ import { CreateServerFactory } from '../CreateServer'; import { ImportServersBtnFactory } from '../helpers/ImportServersBtn'; import { withoutSelectedServer } from '../helpers/withoutSelectedServer'; import { ManageServersFactory } from '../ManageServers'; -import { fetchServers } from '../reducers/remoteServers'; import { ServersExporter } from './ServersExporter'; import { ServersImporter } from './ServersImporter'; @@ -20,7 +19,4 @@ export const provideServices = (bottle: Bottle) => { // Services bottle.service('ServersImporter', ServersImporter, 'csvToJson'); bottle.service('ServersExporter', ServersExporter, 'Storage', 'window', 'jsonToCsv'); - - // Actions - bottle.serviceFactory('fetchServers', fetchServers, 'HttpClient'); }; diff --git a/test/app/App.test.tsx b/test/app/App.test.tsx index 7717106d5..200f131d1 100644 --- a/test/app/App.test.tsx +++ b/test/app/App.test.tsx @@ -1,3 +1,4 @@ +import type { HttpClient } from '@shlinkio/shlink-js-sdk'; import { act, screen } from '@testing-library/react'; import { fromPartial } from '@total-typescript/shoehorn'; import { MemoryRouter } from 'react-router'; @@ -13,17 +14,12 @@ describe('', () => { ShlinkWebComponentContainer: () => <>ShlinkWebComponentContainer, CreateServer: () => <>CreateServer, ManageServers: () => <>ManageServers, + HttpClient: fromPartial({}), }), ); const setUp = async (activeRoute = '/') => act(() => renderWithStore( - {}} - servers={{}} - settings={fromPartial({})} - appUpdated={false} - resetAppUpdate={() => {}} - /> + {}} /> , { initialState: { diff --git a/test/servers/reducers/remoteServers.test.ts b/test/servers/reducers/remoteServers.test.ts index 3c0cf580f..ca8eb9f21 100644 --- a/test/servers/reducers/remoteServers.test.ts +++ b/test/servers/reducers/remoteServers.test.ts @@ -79,9 +79,8 @@ describe('remoteServersReducer', () => { }, ])('tries to fetch servers from remote', async ({ serversArray, expectedNewServers }) => { jsonRequest.mockResolvedValue(serversArray); - const doFetchServers = fetchServers(httpClient); - await doFetchServers()(dispatch, vi.fn(), {}); + await fetchServers(httpClient)(dispatch, vi.fn(), {}); expect(dispatch).toHaveBeenCalledTimes(3); expect(dispatch).toHaveBeenNthCalledWith(2, expect.objectContaining({ payload: expectedNewServers })); From 6094994cfae13dedc43afb126c44f16fc31f7fab Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Fri, 14 Nov 2025 23:29:59 +0100 Subject: [PATCH 10/24] Do not inject settings state or actions --- src/api/services/ShlinkApiClientBuilder.ts | 5 ++--- src/app/App.tsx | 6 +++--- src/app/services/provideServices.ts | 2 +- src/common/ShlinkWebComponentContainer.tsx | 11 ++++------- src/common/services/provideServices.ts | 11 +---------- src/container/index.ts | 2 +- src/index.tsx | 4 +++- test/app/App.test.tsx | 3 ++- test/common/ShlinkWebComponentContainer.test.tsx | 4 ++-- 9 files changed, 19 insertions(+), 29 deletions(-) diff --git a/src/api/services/ShlinkApiClientBuilder.ts b/src/api/services/ShlinkApiClientBuilder.ts index b6216a16e..caf269f80 100644 --- a/src/api/services/ShlinkApiClientBuilder.ts +++ b/src/api/services/ShlinkApiClientBuilder.ts @@ -6,8 +6,6 @@ import type { GetState } from '../../store'; const apiClients: Map = new Map(); -const isGetState = (getStateOrSelectedServer: GetState | ServerWithId): getStateOrSelectedServer is GetState => - typeof getStateOrSelectedServer === 'function'; const getSelectedServerFromState = (getState: GetState): ServerWithId => { const { selectedServer } = getState(); if (!hasServerData(selectedServer)) { @@ -18,7 +16,7 @@ const getSelectedServerFromState = (getState: 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) : getStateOrSelectedServer; const serverKey = `${apiKey}_${baseUrl}_${forwardCredentials ? 'forward' : 'no-forward'}`; @@ -34,6 +32,7 @@ export const buildShlinkApiClient = (httpClient: HttpClient) => (getStateOrSelec { requestCredentials: forwardCredentials ? 'include' : undefined }, ); apiClients.set(serverKey, apiClient); + return apiClient; }; diff --git a/src/app/App.tsx b/src/app/App.tsx index 02bda524a..ede79ee67 100644 --- a/src/app/App.tsx +++ b/src/app/App.tsx @@ -1,6 +1,5 @@ import { changeThemeInMarkup, getSystemPreferredTheme } from '@shlinkio/shlink-frontend-kit'; import type { HttpClient } from '@shlinkio/shlink-js-sdk'; -import type { Settings as AppSettings } from '@shlinkio/shlink-web-component/settings'; import { clsx } from 'clsx'; import type { FC } from 'react'; import { useEffect } from 'react'; @@ -13,11 +12,11 @@ import type { FCWithDeps } from '../container/utils'; import { componentFactory, useDependencies } from '../container/utils'; import { EditServer } from '../servers/EditServer'; import { useLoadRemoteServers } from '../servers/reducers/remoteServers'; +import { useSettings } from '../settings/reducers/settings'; import { Settings } from '../settings/Settings'; import { forceUpdate } from '../utils/helpers/sw'; export type AppProps = { - settings: AppSettings; resetAppUpdate: () => void; appUpdated: boolean; }; @@ -30,7 +29,7 @@ type AppDeps = { HttpClient: HttpClient; }; -const App: FCWithDeps = ({ settings, appUpdated, resetAppUpdate }) => { +const App: FCWithDeps = ({ appUpdated, resetAppUpdate }) => { const { Home, ShlinkWebComponentContainer, @@ -44,6 +43,7 @@ const App: FCWithDeps = ({ settings, appUpdated, resetAppUpda const location = useLocation(); const isHome = location.pathname === '/'; + const { settings } = useSettings(); useEffect(() => { changeThemeInMarkup(settings.ui?.theme ?? getSystemPreferredTheme()); }, [settings.ui?.theme]); diff --git a/src/app/services/provideServices.ts b/src/app/services/provideServices.ts index 9314dca1d..80278b936 100644 --- a/src/app/services/provideServices.ts +++ b/src/app/services/provideServices.ts @@ -6,7 +6,7 @@ import { appUpdateAvailable, resetAppUpdate } from '../reducers/appUpdates'; export const provideServices = (bottle: Bottle, connect: ConnectDecorator) => { // Components bottle.factory('App', AppFactory); - bottle.decorator('App', connect(['settings', 'appUpdated'], ['resetAppUpdate'])); + bottle.decorator('App', connect(['appUpdated'], ['resetAppUpdate'])); // Actions bottle.serviceFactory('appUpdateAvailable', () => appUpdateAvailable); diff --git a/src/common/ShlinkWebComponentContainer.tsx b/src/common/ShlinkWebComponentContainer.tsx index ff76df254..4db6ab9ab 100644 --- a/src/common/ShlinkWebComponentContainer.tsx +++ b/src/common/ShlinkWebComponentContainer.tsx @@ -4,7 +4,6 @@ import { ShlinkSidebarVisibilityProvider, ShlinkWebComponent, } from '@shlinkio/shlink-web-component'; -import type { Settings } from '@shlinkio/shlink-web-component/settings'; import { memo } from 'react'; import type { FCWithDeps } 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 { withSelectedServer } from '../servers/helpers/withSelectedServer'; import { useSelectedServer } from '../servers/reducers/selectedServer'; +import { useSettings } from '../settings/reducers/settings'; import { NotFound } from './NotFound'; -type ShlinkWebComponentContainerProps = { - settings: Settings; -}; - type ShlinkWebComponentContainerDeps = WithSelectedServerPropsDeps & { TagColorsStorage: TagColorsStorage, }; const ShlinkWebComponentContainer: FCWithDeps< - ShlinkWebComponentContainerProps, + any, ShlinkWebComponentContainerDeps // 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 // extra rendering there. // This should be revisited at some point. -> = withSelectedServer(memo(({ settings }) => { +> = withSelectedServer(memo(() => { const { buildShlinkApiClient, TagColorsStorage: tagColorsStorage, } = useDependencies(ShlinkWebComponentContainer); const { selectedServer } = useSelectedServer(); + const { settings } = useSettings(); if (!isReachableServer(selectedServer)) { return ; diff --git a/src/common/services/provideServices.ts b/src/common/services/provideServices.ts index 20026d691..001bbe087 100644 --- a/src/common/services/provideServices.ts +++ b/src/common/services/provideServices.ts @@ -1,27 +1,18 @@ import { FetchHttpClient } from '@shlinkio/shlink-js-sdk/fetch'; import type Bottle from 'bottlejs'; -import type { ConnectDecorator } from '../../container/types'; import { withoutSelectedServer } from '../../servers/helpers/withoutSelectedServer'; -import { ErrorHandler } from '../ErrorHandler'; import { Home } from '../Home'; -import { ScrollToTop } from '../ScrollToTop'; import { ShlinkWebComponentContainerFactory } from '../ShlinkWebComponentContainer'; -export const provideServices = (bottle: Bottle, connect: ConnectDecorator) => { +export const provideServices = (bottle: Bottle) => { // Services bottle.constant('window', window); bottle.constant('console', console); bottle.constant('fetch', window.fetch.bind(window)); bottle.service('HttpClient', FetchHttpClient, 'fetch'); - // Components - bottle.serviceFactory('ScrollToTop', () => ScrollToTop); - bottle.serviceFactory('Home', () => Home); bottle.decorator('Home', withoutSelectedServer); bottle.factory('ShlinkWebComponentContainer', ShlinkWebComponentContainerFactory); - bottle.decorator('ShlinkWebComponentContainer', connect(['settings'], [])); - - bottle.serviceFactory('ErrorHandler', () => ErrorHandler); }; diff --git a/src/container/index.ts b/src/container/index.ts index 604f04fdb..d7fa96064 100644 --- a/src/container/index.ts +++ b/src/container/index.ts @@ -34,7 +34,7 @@ const connect: ConnectDecorator = (propsFromState: string[] | null, actionServic ); provideAppServices(bottle, connect); -provideCommonServices(bottle, connect); +provideCommonServices(bottle); provideApiServices(bottle); provideServersServices(bottle); provideUtilsServices(bottle); diff --git a/src/index.tsx b/src/index.tsx index 4dd136b49..7829245dc 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -2,13 +2,15 @@ import { createRoot } from 'react-dom/client'; import { Provider } from 'react-redux'; import { BrowserRouter } from 'react-router'; import pack from '../package.json'; +import { ErrorHandler } from './common/ErrorHandler'; +import { ScrollToTop } from './common/ScrollToTop'; import { container } from './container'; import { register as registerServiceWorker } from './serviceWorkerRegistration'; import { setUpStore } from './store'; import './tailwind.css'; const store = setUpStore(); -const { App, ScrollToTop, ErrorHandler, appUpdateAvailable } = container; +const { App, appUpdateAvailable } = container; createRoot(document.getElementById('root')!).render( diff --git a/test/app/App.test.tsx b/test/app/App.test.tsx index 200f131d1..ed6cf6e62 100644 --- a/test/app/App.test.tsx +++ b/test/app/App.test.tsx @@ -19,7 +19,7 @@ describe('', () => { ); const setUp = async (activeRoute = '/') => act(() => renderWithStore( - {}} /> + {}} /> , { initialState: { @@ -27,6 +27,7 @@ describe('', () => { abc123: fromPartial({ id: 'abc123', name: 'abc123 server' }), def456: fromPartial({ id: 'def456', name: 'def456 server' }), }, + settings: fromPartial({}), }, }, )); diff --git a/test/common/ShlinkWebComponentContainer.test.tsx b/test/common/ShlinkWebComponentContainer.test.tsx index 12d429bbe..361fa9805 100644 --- a/test/common/ShlinkWebComponentContainer.test.tsx +++ b/test/common/ShlinkWebComponentContainer.test.tsx @@ -19,10 +19,10 @@ describe('', () => { })); const setUp = (selectedServer: SelectedServer) => renderWithStore( - + , { - initialState: { selectedServer, servers: {} }, + initialState: { selectedServer, servers: {}, settings: {} }, }, ); From d5669201f7ef3fc2924bf25a667830c2876912a7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 15 Nov 2025 08:01:37 +0000 Subject: [PATCH 11/24] Bump typescript-eslint from 8.46.3 to 8.46.4 in the eslint group Bumps the eslint group with 1 update: [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint). Updates `typescript-eslint` from 8.46.3 to 8.46.4 - [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases) - [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md) - [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.46.4/packages/typescript-eslint) --- updated-dependencies: - dependency-name: typescript-eslint dependency-version: 8.46.4 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: eslint ... Signed-off-by: dependabot[bot] --- package-lock.json | 244 +++++++++++++++++++++++----------------------- package.json | 2 +- 2 files changed, 123 insertions(+), 123 deletions(-) diff --git a/package-lock.json b/package-lock.json index e12820598..2a4eb38c4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -64,7 +64,7 @@ "playwright": "^1.56.1", "tailwindcss": "^4.1.3", "typescript": "^5.9.3", - "typescript-eslint": "^8.46.3", + "typescript-eslint": "^8.46.4", "vite": "^7.2.2", "vite-plugin-pwa": "^1.1.0", "vitest": "^4.0.3" @@ -4108,16 +4108,16 @@ "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.46.3", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.3.tgz", - "integrity": "sha512-sbaQ27XBUopBkRiuY/P9sWGOWUW4rl8fDoHIUmLpZd8uldsTyB4/Zg6bWTegPoTLnKj9Hqgn3QD6cjPNB32Odw==", + "version": "8.46.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.4.tgz", + "integrity": "sha512-R48VhmTJqplNyDxCyqqVkFSZIx1qX6PzwqgcXn1olLrzxcSBDlOsbtcnQuQhNtnNiJ4Xe5gREI1foajYaYU2Vg==", "dev": true, "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.46.3", - "@typescript-eslint/type-utils": "8.46.3", - "@typescript-eslint/utils": "8.46.3", - "@typescript-eslint/visitor-keys": "8.46.3", + "@typescript-eslint/scope-manager": "8.46.4", + "@typescript-eslint/type-utils": "8.46.4", + "@typescript-eslint/utils": "8.46.4", + "@typescript-eslint/visitor-keys": "8.46.4", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", @@ -4131,7 +4131,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.46.3", + "@typescript-eslint/parser": "^8.46.4", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } @@ -4146,15 +4146,15 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.46.3", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.46.3.tgz", - "integrity": "sha512-6m1I5RmHBGTnUGS113G04DMu3CpSdxCAU/UvtjNWL4Nuf3MW9tQhiJqRlHzChIkhy6kZSAQmc+I1bcGjE3yNKg==", + "version": "8.46.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.46.4.tgz", + "integrity": "sha512-tK3GPFWbirvNgsNKto+UmB/cRtn6TZfyw0D6IKrW55n6Vbs7KJoZtI//kpTKzE/DUmmnAFD8/Ca46s7Obs92/w==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "8.46.3", - "@typescript-eslint/types": "8.46.3", - "@typescript-eslint/typescript-estree": "8.46.3", - "@typescript-eslint/visitor-keys": "8.46.3", + "@typescript-eslint/scope-manager": "8.46.4", + "@typescript-eslint/types": "8.46.4", + "@typescript-eslint/typescript-estree": "8.46.4", + "@typescript-eslint/visitor-keys": "8.46.4", "debug": "^4.3.4" }, "engines": { @@ -4170,13 +4170,13 @@ } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.46.3", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.46.3.tgz", - "integrity": "sha512-Fz8yFXsp2wDFeUElO88S9n4w1I4CWDTXDqDr9gYvZgUpwXQqmZBr9+NTTql5R3J7+hrJZPdpiWaB9VNhAKYLuQ==", + "version": "8.46.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.46.4.tgz", + "integrity": "sha512-nPiRSKuvtTN+no/2N1kt2tUh/HoFzeEgOm9fQ6XQk4/ApGqjx0zFIIaLJ6wooR1HIoozvj2j6vTi/1fgAz7UYQ==", "dev": true, "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.46.3", - "@typescript-eslint/types": "^8.46.3", + "@typescript-eslint/tsconfig-utils": "^8.46.4", + "@typescript-eslint/types": "^8.46.4", "debug": "^4.3.4" }, "engines": { @@ -4191,13 +4191,13 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.46.3", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.46.3.tgz", - "integrity": "sha512-FCi7Y1zgrmxp3DfWfr+3m9ansUUFoy8dkEdeQSgA9gbm8DaHYvZCdkFRQrtKiedFf3Ha6VmoqoAaP68+i+22kg==", + "version": "8.46.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.46.4.tgz", + "integrity": "sha512-tMDbLGXb1wC+McN1M6QeDx7P7c0UWO5z9CXqp7J8E+xGcJuUuevWKxuG8j41FoweS3+L41SkyKKkia16jpX7CA==", "dev": true, "dependencies": { - "@typescript-eslint/types": "8.46.3", - "@typescript-eslint/visitor-keys": "8.46.3" + "@typescript-eslint/types": "8.46.4", + "@typescript-eslint/visitor-keys": "8.46.4" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4208,9 +4208,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.46.3", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.46.3.tgz", - "integrity": "sha512-GLupljMniHNIROP0zE7nCcybptolcH8QZfXOpCfhQDAdwJ/ZTlcaBOYebSOZotpti/3HrHSw7D3PZm75gYFsOA==", + "version": "8.46.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.46.4.tgz", + "integrity": "sha512-+/XqaZPIAk6Cjg7NWgSGe27X4zMGqrFqZ8atJsX3CWxH/jACqWnrWI68h7nHQld0y+k9eTTjb9r+KU4twLoo9A==", "dev": true, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4224,14 +4224,14 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.46.3", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.46.3.tgz", - "integrity": "sha512-ZPCADbr+qfz3aiTTYNNkCbUt+cjNwI/5McyANNrFBpVxPt7GqpEYz5ZfdwuFyGUnJ9FdDXbGODUu6iRCI6XRXw==", + "version": "8.46.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.46.4.tgz", + "integrity": "sha512-V4QC8h3fdT5Wro6vANk6eojqfbv5bpwHuMsBcJUJkqs2z5XnYhJzyz9Y02eUmF9u3PgXEUiOt4w4KHR3P+z0PQ==", "dev": true, "dependencies": { - "@typescript-eslint/types": "8.46.3", - "@typescript-eslint/typescript-estree": "8.46.3", - "@typescript-eslint/utils": "8.46.3", + "@typescript-eslint/types": "8.46.4", + "@typescript-eslint/typescript-estree": "8.46.4", + "@typescript-eslint/utils": "8.46.4", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, @@ -4248,9 +4248,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.46.3", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.46.3.tgz", - "integrity": "sha512-G7Ok9WN/ggW7e/tOf8TQYMaxgID3Iujn231hfi0Pc7ZheztIJVpO44ekY00b7akqc6nZcvregk0Jpah3kep6hA==", + "version": "8.46.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.46.4.tgz", + "integrity": "sha512-USjyxm3gQEePdUwJBFjjGNG18xY9A2grDVGuk7/9AkjIF1L+ZrVnwR5VAU5JXtUnBL/Nwt3H31KlRDaksnM7/w==", "dev": true, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4261,15 +4261,15 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.46.3", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.46.3.tgz", - "integrity": "sha512-f/NvtRjOm80BtNM5OQtlaBdM5BRFUv7gf381j9wygDNL+qOYSNOgtQ/DCndiYi80iIOv76QqaTmp4fa9hwI0OA==", + "version": "8.46.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.46.4.tgz", + "integrity": "sha512-7oV2qEOr1d4NWNmpXLR35LvCfOkTNymY9oyW+lUHkmCno7aOmIf/hMaydnJBUTBMRCOGZh8YjkFOc8dadEoNGA==", "dev": true, "dependencies": { - "@typescript-eslint/project-service": "8.46.3", - "@typescript-eslint/tsconfig-utils": "8.46.3", - "@typescript-eslint/types": "8.46.3", - "@typescript-eslint/visitor-keys": "8.46.3", + "@typescript-eslint/project-service": "8.46.4", + "@typescript-eslint/tsconfig-utils": "8.46.4", + "@typescript-eslint/types": "8.46.4", + "@typescript-eslint/visitor-keys": "8.46.4", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -4313,15 +4313,15 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.46.3", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.46.3.tgz", - "integrity": "sha512-VXw7qmdkucEx9WkmR3ld/u6VhRyKeiF1uxWwCy/iuNfokjJ7VhsgLSOTjsol8BunSw190zABzpwdNsze2Kpo4g==", + "version": "8.46.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.46.4.tgz", + "integrity": "sha512-AbSv11fklGXV6T28dp2Me04Uw90R2iJ30g2bgLz529Koehrmkbs1r7paFqr1vPCZi7hHwYxYtxfyQMRC8QaVSg==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.46.3", - "@typescript-eslint/types": "8.46.3", - "@typescript-eslint/typescript-estree": "8.46.3" + "@typescript-eslint/scope-manager": "8.46.4", + "@typescript-eslint/types": "8.46.4", + "@typescript-eslint/typescript-estree": "8.46.4" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4336,12 +4336,12 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.46.3", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.46.3.tgz", - "integrity": "sha512-uk574k8IU0rOF/AjniX8qbLSGURJVUCeM5e4MIMKBFFi8weeiLrG1fyQejyLXQpRZbU/1BuQasleV/RfHC3hHg==", + "version": "8.46.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.46.4.tgz", + "integrity": "sha512-/++5CYLQqsO9HFGLI7APrxBJYo+5OCMpViuhV8q5/Qa3o5mMrF//eQHks+PXcsAVaLdn817fMuS7zqoXNNZGaw==", "dev": true, "dependencies": { - "@typescript-eslint/types": "8.46.3", + "@typescript-eslint/types": "8.46.4", "eslint-visitor-keys": "^4.2.1" }, "engines": { @@ -10111,15 +10111,15 @@ } }, "node_modules/typescript-eslint": { - "version": "8.46.3", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.46.3.tgz", - "integrity": "sha512-bAfgMavTuGo+8n6/QQDVQz4tZ4f7Soqg53RbrlZQEoAltYop/XR4RAts/I0BrO3TTClTSTFJ0wYbla+P8cEWJA==", + "version": "8.46.4", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.46.4.tgz", + "integrity": "sha512-KALyxkpYV5Ix7UhvjTwJXZv76VWsHG+NjNlt/z+a17SOQSiOcBdUXdbJdyXi7RPxrBFECtFOiPwUJQusJuCqrg==", "dev": true, "dependencies": { - "@typescript-eslint/eslint-plugin": "8.46.3", - "@typescript-eslint/parser": "8.46.3", - "@typescript-eslint/typescript-estree": "8.46.3", - "@typescript-eslint/utils": "8.46.3" + "@typescript-eslint/eslint-plugin": "8.46.4", + "@typescript-eslint/parser": "8.46.4", + "@typescript-eslint/typescript-estree": "8.46.4", + "@typescript-eslint/utils": "8.46.4" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -13600,16 +13600,16 @@ "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==" }, "@typescript-eslint/eslint-plugin": { - "version": "8.46.3", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.3.tgz", - "integrity": "sha512-sbaQ27XBUopBkRiuY/P9sWGOWUW4rl8fDoHIUmLpZd8uldsTyB4/Zg6bWTegPoTLnKj9Hqgn3QD6cjPNB32Odw==", + "version": "8.46.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.4.tgz", + "integrity": "sha512-R48VhmTJqplNyDxCyqqVkFSZIx1qX6PzwqgcXn1olLrzxcSBDlOsbtcnQuQhNtnNiJ4Xe5gREI1foajYaYU2Vg==", "dev": true, "requires": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.46.3", - "@typescript-eslint/type-utils": "8.46.3", - "@typescript-eslint/utils": "8.46.3", - "@typescript-eslint/visitor-keys": "8.46.3", + "@typescript-eslint/scope-manager": "8.46.4", + "@typescript-eslint/type-utils": "8.46.4", + "@typescript-eslint/utils": "8.46.4", + "@typescript-eslint/visitor-keys": "8.46.4", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", @@ -13625,75 +13625,75 @@ } }, "@typescript-eslint/parser": { - "version": "8.46.3", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.46.3.tgz", - "integrity": "sha512-6m1I5RmHBGTnUGS113G04DMu3CpSdxCAU/UvtjNWL4Nuf3MW9tQhiJqRlHzChIkhy6kZSAQmc+I1bcGjE3yNKg==", + "version": "8.46.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.46.4.tgz", + "integrity": "sha512-tK3GPFWbirvNgsNKto+UmB/cRtn6TZfyw0D6IKrW55n6Vbs7KJoZtI//kpTKzE/DUmmnAFD8/Ca46s7Obs92/w==", "dev": true, "requires": { - "@typescript-eslint/scope-manager": "8.46.3", - "@typescript-eslint/types": "8.46.3", - "@typescript-eslint/typescript-estree": "8.46.3", - "@typescript-eslint/visitor-keys": "8.46.3", + "@typescript-eslint/scope-manager": "8.46.4", + "@typescript-eslint/types": "8.46.4", + "@typescript-eslint/typescript-estree": "8.46.4", + "@typescript-eslint/visitor-keys": "8.46.4", "debug": "^4.3.4" } }, "@typescript-eslint/project-service": { - "version": "8.46.3", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.46.3.tgz", - "integrity": "sha512-Fz8yFXsp2wDFeUElO88S9n4w1I4CWDTXDqDr9gYvZgUpwXQqmZBr9+NTTql5R3J7+hrJZPdpiWaB9VNhAKYLuQ==", + "version": "8.46.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.46.4.tgz", + "integrity": "sha512-nPiRSKuvtTN+no/2N1kt2tUh/HoFzeEgOm9fQ6XQk4/ApGqjx0zFIIaLJ6wooR1HIoozvj2j6vTi/1fgAz7UYQ==", "dev": true, "requires": { - "@typescript-eslint/tsconfig-utils": "^8.46.3", - "@typescript-eslint/types": "^8.46.3", + "@typescript-eslint/tsconfig-utils": "^8.46.4", + "@typescript-eslint/types": "^8.46.4", "debug": "^4.3.4" } }, "@typescript-eslint/scope-manager": { - "version": "8.46.3", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.46.3.tgz", - "integrity": "sha512-FCi7Y1zgrmxp3DfWfr+3m9ansUUFoy8dkEdeQSgA9gbm8DaHYvZCdkFRQrtKiedFf3Ha6VmoqoAaP68+i+22kg==", + "version": "8.46.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.46.4.tgz", + "integrity": "sha512-tMDbLGXb1wC+McN1M6QeDx7P7c0UWO5z9CXqp7J8E+xGcJuUuevWKxuG8j41FoweS3+L41SkyKKkia16jpX7CA==", "dev": true, "requires": { - "@typescript-eslint/types": "8.46.3", - "@typescript-eslint/visitor-keys": "8.46.3" + "@typescript-eslint/types": "8.46.4", + "@typescript-eslint/visitor-keys": "8.46.4" } }, "@typescript-eslint/tsconfig-utils": { - "version": "8.46.3", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.46.3.tgz", - "integrity": "sha512-GLupljMniHNIROP0zE7nCcybptolcH8QZfXOpCfhQDAdwJ/ZTlcaBOYebSOZotpti/3HrHSw7D3PZm75gYFsOA==", + "version": "8.46.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.46.4.tgz", + "integrity": "sha512-+/XqaZPIAk6Cjg7NWgSGe27X4zMGqrFqZ8atJsX3CWxH/jACqWnrWI68h7nHQld0y+k9eTTjb9r+KU4twLoo9A==", "dev": true, "requires": {} }, "@typescript-eslint/type-utils": { - "version": "8.46.3", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.46.3.tgz", - "integrity": "sha512-ZPCADbr+qfz3aiTTYNNkCbUt+cjNwI/5McyANNrFBpVxPt7GqpEYz5ZfdwuFyGUnJ9FdDXbGODUu6iRCI6XRXw==", + "version": "8.46.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.46.4.tgz", + "integrity": "sha512-V4QC8h3fdT5Wro6vANk6eojqfbv5bpwHuMsBcJUJkqs2z5XnYhJzyz9Y02eUmF9u3PgXEUiOt4w4KHR3P+z0PQ==", "dev": true, "requires": { - "@typescript-eslint/types": "8.46.3", - "@typescript-eslint/typescript-estree": "8.46.3", - "@typescript-eslint/utils": "8.46.3", + "@typescript-eslint/types": "8.46.4", + "@typescript-eslint/typescript-estree": "8.46.4", + "@typescript-eslint/utils": "8.46.4", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" } }, "@typescript-eslint/types": { - "version": "8.46.3", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.46.3.tgz", - "integrity": "sha512-G7Ok9WN/ggW7e/tOf8TQYMaxgID3Iujn231hfi0Pc7ZheztIJVpO44ekY00b7akqc6nZcvregk0Jpah3kep6hA==", + "version": "8.46.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.46.4.tgz", + "integrity": "sha512-USjyxm3gQEePdUwJBFjjGNG18xY9A2grDVGuk7/9AkjIF1L+ZrVnwR5VAU5JXtUnBL/Nwt3H31KlRDaksnM7/w==", "dev": true }, "@typescript-eslint/typescript-estree": { - "version": "8.46.3", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.46.3.tgz", - "integrity": "sha512-f/NvtRjOm80BtNM5OQtlaBdM5BRFUv7gf381j9wygDNL+qOYSNOgtQ/DCndiYi80iIOv76QqaTmp4fa9hwI0OA==", + "version": "8.46.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.46.4.tgz", + "integrity": "sha512-7oV2qEOr1d4NWNmpXLR35LvCfOkTNymY9oyW+lUHkmCno7aOmIf/hMaydnJBUTBMRCOGZh8YjkFOc8dadEoNGA==", "dev": true, "requires": { - "@typescript-eslint/project-service": "8.46.3", - "@typescript-eslint/tsconfig-utils": "8.46.3", - "@typescript-eslint/types": "8.46.3", - "@typescript-eslint/visitor-keys": "8.46.3", + "@typescript-eslint/project-service": "8.46.4", + "@typescript-eslint/tsconfig-utils": "8.46.4", + "@typescript-eslint/types": "8.46.4", + "@typescript-eslint/visitor-keys": "8.46.4", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -13723,24 +13723,24 @@ } }, "@typescript-eslint/utils": { - "version": "8.46.3", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.46.3.tgz", - "integrity": "sha512-VXw7qmdkucEx9WkmR3ld/u6VhRyKeiF1uxWwCy/iuNfokjJ7VhsgLSOTjsol8BunSw190zABzpwdNsze2Kpo4g==", + "version": "8.46.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.46.4.tgz", + "integrity": "sha512-AbSv11fklGXV6T28dp2Me04Uw90R2iJ30g2bgLz529Koehrmkbs1r7paFqr1vPCZi7hHwYxYtxfyQMRC8QaVSg==", "dev": true, "requires": { "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.46.3", - "@typescript-eslint/types": "8.46.3", - "@typescript-eslint/typescript-estree": "8.46.3" + "@typescript-eslint/scope-manager": "8.46.4", + "@typescript-eslint/types": "8.46.4", + "@typescript-eslint/typescript-estree": "8.46.4" } }, "@typescript-eslint/visitor-keys": { - "version": "8.46.3", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.46.3.tgz", - "integrity": "sha512-uk574k8IU0rOF/AjniX8qbLSGURJVUCeM5e4MIMKBFFi8weeiLrG1fyQejyLXQpRZbU/1BuQasleV/RfHC3hHg==", + "version": "8.46.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.46.4.tgz", + "integrity": "sha512-/++5CYLQqsO9HFGLI7APrxBJYo+5OCMpViuhV8q5/Qa3o5mMrF//eQHks+PXcsAVaLdn817fMuS7zqoXNNZGaw==", "dev": true, "requires": { - "@typescript-eslint/types": "8.46.3", + "@typescript-eslint/types": "8.46.4", "eslint-visitor-keys": "^4.2.1" }, "dependencies": { @@ -17666,15 +17666,15 @@ "dev": true }, "typescript-eslint": { - "version": "8.46.3", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.46.3.tgz", - "integrity": "sha512-bAfgMavTuGo+8n6/QQDVQz4tZ4f7Soqg53RbrlZQEoAltYop/XR4RAts/I0BrO3TTClTSTFJ0wYbla+P8cEWJA==", + "version": "8.46.4", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.46.4.tgz", + "integrity": "sha512-KALyxkpYV5Ix7UhvjTwJXZv76VWsHG+NjNlt/z+a17SOQSiOcBdUXdbJdyXi7RPxrBFECtFOiPwUJQusJuCqrg==", "dev": true, "requires": { - "@typescript-eslint/eslint-plugin": "8.46.3", - "@typescript-eslint/parser": "8.46.3", - "@typescript-eslint/typescript-estree": "8.46.3", - "@typescript-eslint/utils": "8.46.3" + "@typescript-eslint/eslint-plugin": "8.46.4", + "@typescript-eslint/parser": "8.46.4", + "@typescript-eslint/typescript-estree": "8.46.4", + "@typescript-eslint/utils": "8.46.4" } }, "unbox-primitive": { diff --git a/package.json b/package.json index c423d826c..77788bf86 100644 --- a/package.json +++ b/package.json @@ -77,7 +77,7 @@ "playwright": "^1.56.1", "tailwindcss": "^4.1.3", "typescript": "^5.9.3", - "typescript-eslint": "^8.46.3", + "typescript-eslint": "^8.46.4", "vite": "^7.2.2", "vite-plugin-pwa": "^1.1.0", "vitest": "^4.0.3" From 74e3b8fe0b371f84d5873196b150bca6bca0630f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 15 Nov 2025 08:01:52 +0000 Subject: [PATCH 12/24] Bump @shlinkio/shlink-frontend-kit in the shlink group Bumps the shlink group with 1 update: [@shlinkio/shlink-frontend-kit](https://github.com/shlinkio/shlink-frontend-kit). Updates `@shlinkio/shlink-frontend-kit` from 1.3.0 to 1.3.1 - [Release notes](https://github.com/shlinkio/shlink-frontend-kit/releases) - [Changelog](https://github.com/shlinkio/shlink-frontend-kit/blob/main/CHANGELOG.md) - [Commits](https://github.com/shlinkio/shlink-frontend-kit/compare/v1.3.0...v1.3.1) --- updated-dependencies: - dependency-name: "@shlinkio/shlink-frontend-kit" dependency-version: 1.3.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: shlink ... Signed-off-by: dependabot[bot] --- package-lock.json | 15 +++++++-------- package.json | 2 +- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/package-lock.json b/package-lock.json index e12820598..528fcb54c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,7 +16,7 @@ "@json2csv/plainjs": "^7.0.6", "@reduxjs/toolkit": "^2.10.1", "@shlinkio/data-manipulation": "^1.0.4", - "@shlinkio/shlink-frontend-kit": "^1.3.0", + "@shlinkio/shlink-frontend-kit": "^1.3.1", "@shlinkio/shlink-js-sdk": "^3.0.1", "@shlinkio/shlink-web-component": "^0.17.0", "@vitest/browser-playwright": "^4.0.8", @@ -3309,10 +3309,9 @@ } }, "node_modules/@shlinkio/shlink-frontend-kit": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@shlinkio/shlink-frontend-kit/-/shlink-frontend-kit-1.3.0.tgz", - "integrity": "sha512-/ydi82RM/rbSqcMkyGJ4+zz1hAlatBhW8m5YQ1XvXrB7ZrU2VSCJB0VA4XZmsDHNz4WDeuktwaFUo7TxsGutxA==", - "license": "MIT", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@shlinkio/shlink-frontend-kit/-/shlink-frontend-kit-1.3.1.tgz", + "integrity": "sha512-hfKsUJdNYXQv9+ABubvgprnjA28l/lGCXzJ34GMgtXzTnvV5bexAb+CRhHYfRiYkmLyU5or7+rNpBixudHwUjw==", "dependencies": { "@floating-ui/react": "^0.27.16", "@vitest/browser-playwright": "^4.0.8", @@ -13036,9 +13035,9 @@ "requires": {} }, "@shlinkio/shlink-frontend-kit": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@shlinkio/shlink-frontend-kit/-/shlink-frontend-kit-1.3.0.tgz", - "integrity": "sha512-/ydi82RM/rbSqcMkyGJ4+zz1hAlatBhW8m5YQ1XvXrB7ZrU2VSCJB0VA4XZmsDHNz4WDeuktwaFUo7TxsGutxA==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@shlinkio/shlink-frontend-kit/-/shlink-frontend-kit-1.3.1.tgz", + "integrity": "sha512-hfKsUJdNYXQv9+ABubvgprnjA28l/lGCXzJ34GMgtXzTnvV5bexAb+CRhHYfRiYkmLyU5or7+rNpBixudHwUjw==", "requires": { "@floating-ui/react": "^0.27.16", "@vitest/browser-playwright": "^4.0.8", diff --git a/package.json b/package.json index c423d826c..395870a31 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ "@json2csv/plainjs": "^7.0.6", "@reduxjs/toolkit": "^2.10.1", "@shlinkio/data-manipulation": "^1.0.4", - "@shlinkio/shlink-frontend-kit": "^1.3.0", + "@shlinkio/shlink-frontend-kit": "^1.3.1", "@shlinkio/shlink-js-sdk": "^3.0.1", "@shlinkio/shlink-web-component": "^0.17.0", "@vitest/browser-playwright": "^4.0.8", From e0e0b24a6bbf0a922125dcd67e2acec9d1fb59d7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 15 Nov 2025 08:01:55 +0000 Subject: [PATCH 13/24] Bump node from 25.1-alpine to 25.2-alpine Bumps node from 25.1-alpine to 25.2-alpine. --- updated-dependencies: - dependency-name: node dependency-version: 25.2-alpine dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 5a0a7dca6..a4a076f8f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM node:25.1-alpine AS node +FROM node:25.2-alpine AS node COPY . /shlink-web-client ARG VERSION="latest" ENV VERSION=${VERSION} From b5f77de19449a473149be306a850278d3c5b76bb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 15 Nov 2025 08:02:26 +0000 Subject: [PATCH 14/24] Bump @vitejs/plugin-react from 5.1.0 to 5.1.1 in the vite group Bumps the vite group with 1 update: [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/tree/HEAD/packages/plugin-react). Updates `@vitejs/plugin-react` from 5.1.0 to 5.1.1 - [Release notes](https://github.com/vitejs/vite-plugin-react/releases) - [Changelog](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/CHANGELOG.md) - [Commits](https://github.com/vitejs/vite-plugin-react/commits/plugin-react@5.1.1/packages/plugin-react) --- updated-dependencies: - dependency-name: "@vitejs/plugin-react" dependency-version: 5.1.1 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: vite ... Signed-off-by: dependabot[bot] --- package-lock.json | 106 +++++++++++++++++++++++----------------------- package.json | 2 +- 2 files changed, 54 insertions(+), 54 deletions(-) diff --git a/package-lock.json b/package-lock.json index e12820598..312ff31ab 100644 --- a/package-lock.json +++ b/package-lock.json @@ -47,7 +47,7 @@ "@total-typescript/shoehorn": "^0.1.2", "@types/react": "^19.2.2", "@types/react-dom": "^19.2.2", - "@vitejs/plugin-react": "^5.1.0", + "@vitejs/plugin-react": "^5.1.1", "@vitest/browser": "^4.0.3", "@vitest/coverage-v8": "^4.0.8", "adm-zip": "^0.5.16", @@ -230,20 +230,20 @@ } }, "node_modules/@babel/core": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.4.tgz", - "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", + "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "dependencies": { "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.3", + "@babel/generator": "^7.28.5", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.28.3", "@babel/helpers": "^7.28.4", - "@babel/parser": "^7.28.4", + "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", - "@babel/traverse": "^7.28.4", - "@babel/types": "^7.28.4", + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", @@ -269,13 +269,13 @@ } }, "node_modules/@babel/generator": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.3.tgz", - "integrity": "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", + "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", "dev": true, "dependencies": { - "@babel/parser": "^7.28.3", - "@babel/types": "^7.28.2", + "@babel/parser": "^7.28.5", + "@babel/types": "^7.28.5", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" @@ -1745,17 +1745,17 @@ } }, "node_modules/@babel/traverse": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.4.tgz", - "integrity": "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz", + "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", "dev": true, "dependencies": { "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.3", + "@babel/generator": "^7.28.5", "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.28.4", + "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", - "@babel/types": "^7.28.4", + "@babel/types": "^7.28.5", "debug": "^4.3.1" }, "engines": { @@ -2939,9 +2939,9 @@ } }, "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-beta.43", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.43.tgz", - "integrity": "sha512-5Uxg7fQUCmfhax7FJke2+8B6cqgeUJUD9o2uXIKXhD+mG0mL6NObmVoi9wXEU1tY89mZKgAYA6fTbftx3q2ZPQ==", + "version": "1.0.0-beta.47", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.47.tgz", + "integrity": "sha512-8QagwMH3kNCuzD8EWL8R2YPW5e4OrHNSAHRFDdmFqEwEaD/KcNKjVoumo+gP2vW5eKB2UPbM6vTYiGZX0ixLnw==", "dev": true }, "node_modules/@rollup/plugin-node-resolve": { @@ -4365,15 +4365,15 @@ } }, "node_modules/@vitejs/plugin-react": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.0.tgz", - "integrity": "sha512-4LuWrg7EKWgQaMJfnN+wcmbAW+VSsCmqGohftWjuct47bv8uE4n/nPpq4XjJPsxgq00GGG5J8dvBczp8uxScew==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.1.tgz", + "integrity": "sha512-WQfkSw0QbQ5aJ2CHYw23ZGkqnRwqKHD/KYsMeTkZzPT4Jcf0DcBxBtwMJxnu6E7oxw5+JC6ZAiePgh28uJ1HBA==", "dev": true, "dependencies": { - "@babel/core": "^7.28.4", + "@babel/core": "^7.28.5", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", - "@rolldown/pluginutils": "1.0.0-beta.43", + "@rolldown/pluginutils": "1.0.0-beta.47", "@types/babel__core": "^7.20.5", "react-refresh": "^0.18.0" }, @@ -11206,20 +11206,20 @@ "dev": true }, "@babel/core": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.4.tgz", - "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", + "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "requires": { "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.3", + "@babel/generator": "^7.28.5", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.28.3", "@babel/helpers": "^7.28.4", - "@babel/parser": "^7.28.4", + "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", - "@babel/traverse": "^7.28.4", - "@babel/types": "^7.28.4", + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", @@ -11237,13 +11237,13 @@ } }, "@babel/generator": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.3.tgz", - "integrity": "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", + "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", "dev": true, "requires": { - "@babel/parser": "^7.28.3", - "@babel/types": "^7.28.2", + "@babel/parser": "^7.28.5", + "@babel/types": "^7.28.5", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" @@ -12227,17 +12227,17 @@ } }, "@babel/traverse": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.4.tgz", - "integrity": "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz", + "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", "dev": true, "requires": { "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.3", + "@babel/generator": "^7.28.5", "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.28.4", + "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", - "@babel/types": "^7.28.4", + "@babel/types": "^7.28.5", "debug": "^4.3.1" } }, @@ -12849,9 +12849,9 @@ } }, "@rolldown/pluginutils": { - "version": "1.0.0-beta.43", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.43.tgz", - "integrity": "sha512-5Uxg7fQUCmfhax7FJke2+8B6cqgeUJUD9o2uXIKXhD+mG0mL6NObmVoi9wXEU1tY89mZKgAYA6fTbftx3q2ZPQ==", + "version": "1.0.0-beta.47", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.47.tgz", + "integrity": "sha512-8QagwMH3kNCuzD8EWL8R2YPW5e4OrHNSAHRFDdmFqEwEaD/KcNKjVoumo+gP2vW5eKB2UPbM6vTYiGZX0ixLnw==", "dev": true }, "@rollup/plugin-node-resolve": { @@ -13753,15 +13753,15 @@ } }, "@vitejs/plugin-react": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.0.tgz", - "integrity": "sha512-4LuWrg7EKWgQaMJfnN+wcmbAW+VSsCmqGohftWjuct47bv8uE4n/nPpq4XjJPsxgq00GGG5J8dvBczp8uxScew==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.1.tgz", + "integrity": "sha512-WQfkSw0QbQ5aJ2CHYw23ZGkqnRwqKHD/KYsMeTkZzPT4Jcf0DcBxBtwMJxnu6E7oxw5+JC6ZAiePgh28uJ1HBA==", "dev": true, "requires": { - "@babel/core": "^7.28.4", + "@babel/core": "^7.28.5", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", - "@rolldown/pluginutils": "1.0.0-beta.43", + "@rolldown/pluginutils": "1.0.0-beta.47", "@types/babel__core": "^7.20.5", "react-refresh": "^0.18.0" } diff --git a/package.json b/package.json index c423d826c..2dfe7a847 100644 --- a/package.json +++ b/package.json @@ -60,7 +60,7 @@ "@total-typescript/shoehorn": "^0.1.2", "@types/react": "^19.2.2", "@types/react-dom": "^19.2.2", - "@vitejs/plugin-react": "^5.1.0", + "@vitejs/plugin-react": "^5.1.1", "@vitest/browser": "^4.0.3", "@vitest/coverage-v8": "^4.0.8", "adm-zip": "^0.5.16", From 4abaa5b7fb18df89ad0f21f7a644dbfe19999878 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 15 Nov 2025 08:02:53 +0000 Subject: [PATCH 15/24] Bump the vitest group with 4 updates Bumps the vitest group with 4 updates: [@vitest/browser-playwright](https://github.com/vitest-dev/vitest/tree/HEAD/packages/browser-playwright), [@vitest/browser](https://github.com/vitest-dev/vitest/tree/HEAD/packages/browser), [@vitest/coverage-v8](https://github.com/vitest-dev/vitest/tree/HEAD/packages/coverage-v8) and [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest). Updates `@vitest/browser-playwright` from 4.0.8 to 4.0.9 - [Release notes](https://github.com/vitest-dev/vitest/releases) - [Commits](https://github.com/vitest-dev/vitest/commits/v4.0.9/packages/browser-playwright) Updates `@vitest/browser` from 4.0.8 to 4.0.9 - [Release notes](https://github.com/vitest-dev/vitest/releases) - [Commits](https://github.com/vitest-dev/vitest/commits/v4.0.9/packages/browser) Updates `@vitest/coverage-v8` from 4.0.8 to 4.0.9 - [Release notes](https://github.com/vitest-dev/vitest/releases) - [Commits](https://github.com/vitest-dev/vitest/commits/v4.0.9/packages/coverage-v8) Updates `vitest` from 4.0.8 to 4.0.9 - [Release notes](https://github.com/vitest-dev/vitest/releases) - [Commits](https://github.com/vitest-dev/vitest/commits/v4.0.9/packages/vitest) --- updated-dependencies: - dependency-name: "@vitest/browser-playwright" dependency-version: 4.0.9 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: vitest - dependency-name: "@vitest/browser" dependency-version: 4.0.9 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: vitest - dependency-name: "@vitest/coverage-v8" dependency-version: 4.0.9 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: vitest - dependency-name: vitest dependency-version: 4.0.9 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: vitest ... Signed-off-by: dependabot[bot] --- package-lock.json | 240 +++++++++++++++++++++++----------------------- package.json | 4 +- 2 files changed, 122 insertions(+), 122 deletions(-) diff --git a/package-lock.json b/package-lock.json index e12820598..6dfe04596 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,7 +19,7 @@ "@shlinkio/shlink-frontend-kit": "^1.3.0", "@shlinkio/shlink-js-sdk": "^3.0.1", "@shlinkio/shlink-web-component": "^0.17.0", - "@vitest/browser-playwright": "^4.0.8", + "@vitest/browser-playwright": "^4.0.9", "bottlejs": "^2.0.1", "clsx": "^2.1.1", "compare-versions": "^6.1.1", @@ -49,7 +49,7 @@ "@types/react-dom": "^19.2.2", "@vitejs/plugin-react": "^5.1.0", "@vitest/browser": "^4.0.3", - "@vitest/coverage-v8": "^4.0.8", + "@vitest/coverage-v8": "^4.0.9", "adm-zip": "^0.5.16", "axe-core": "^4.11.0", "chalk": "^5.6.2", @@ -4385,12 +4385,12 @@ } }, "node_modules/@vitest/browser": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/@vitest/browser/-/browser-4.0.8.tgz", - "integrity": "sha512-oG6QJAR0d7S5SDnIYZwjxCj/a5fhbp9ZE7GtMgZn+yCUf4CxtqbBV6aXyg0qmn8nbUWT+rGuXL2ZB6qDBUjv/A==", + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@vitest/browser/-/browser-4.0.9.tgz", + "integrity": "sha512-OWN4ZgOIV2+T9cR4qfoajtjZDFoxcLa6qUpgDkviXZFUNkZ7XTVKvL/16X+gz5dtpqdZwXf3m0qIj72Ge/vytw==", "dependencies": { - "@vitest/mocker": "4.0.8", - "@vitest/utils": "4.0.8", + "@vitest/mocker": "4.0.9", + "@vitest/utils": "4.0.9", "magic-string": "^0.30.21", "pixelmatch": "7.1.0", "pngjs": "^7.0.0", @@ -4402,16 +4402,16 @@ "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "vitest": "4.0.8" + "vitest": "4.0.9" } }, "node_modules/@vitest/browser-playwright": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/@vitest/browser-playwright/-/browser-playwright-4.0.8.tgz", - "integrity": "sha512-MUi0msIAPXcA2YAuVMcssrSYP/yylxLt347xyTC6+ODl0c4XQFs0d2AN3Pc3iTa0pxIGmogflUV6eogXpPbJeA==", + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@vitest/browser-playwright/-/browser-playwright-4.0.9.tgz", + "integrity": "sha512-ayr0vCxvJIvodzfUTVzifFMT3bmcMeKzEWoPt7mtgrZsqJhMbYaftifuBZRQeF/glogsVr+jhtIePHw6g+0YRQ==", "dependencies": { - "@vitest/browser": "4.0.8", - "@vitest/mocker": "4.0.8", + "@vitest/browser": "4.0.9", + "@vitest/mocker": "4.0.9", "tinyrainbow": "^3.0.3" }, "funding": { @@ -4419,7 +4419,7 @@ }, "peerDependencies": { "playwright": "*", - "vitest": "4.0.8" + "vitest": "4.0.9" }, "peerDependenciesMeta": { "playwright": { @@ -4428,13 +4428,13 @@ } }, "node_modules/@vitest/coverage-v8": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.0.8.tgz", - "integrity": "sha512-wQgmtW6FtPNn4lWUXi8ZSYLpOIb92j3QCujxX3sQ81NTfQ/ORnE0HtK7Kqf2+7J9jeveMGyGyc4NWc5qy3rC4A==", + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.0.9.tgz", + "integrity": "sha512-70oyhP+Q0HlWBIeGSP74YBw5KSjYhNgSCQjvmuQFciMqnyF36WL2cIkcT7XD85G4JPmBQitEMUsx+XMFv2AzQA==", "dev": true, "dependencies": { "@bcoe/v8-coverage": "^1.0.2", - "@vitest/utils": "4.0.8", + "@vitest/utils": "4.0.9", "ast-v8-to-istanbul": "^0.3.8", "debug": "^4.4.3", "istanbul-lib-coverage": "^3.2.2", @@ -4449,8 +4449,8 @@ "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "@vitest/browser": "4.0.8", - "vitest": "4.0.8" + "@vitest/browser": "4.0.9", + "vitest": "4.0.9" }, "peerDependenciesMeta": { "@vitest/browser": { @@ -4459,14 +4459,14 @@ } }, "node_modules/@vitest/expect": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.8.tgz", - "integrity": "sha512-Rv0eabdP/xjAHQGr8cjBm+NnLHNoL268lMDK85w2aAGLFoVKLd8QGnVon5lLtkXQCoYaNL0wg04EGnyKkkKhPA==", + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.9.tgz", + "integrity": "sha512-C2vyXf5/Jfj1vl4DQYxjib3jzyuswMi/KHHVN2z+H4v16hdJ7jMZ0OGe3uOVIt6LyJsAofDdaJNIFEpQcrSTFw==", "dependencies": { "@standard-schema/spec": "^1.0.0", "@types/chai": "^5.2.2", - "@vitest/spy": "4.0.8", - "@vitest/utils": "4.0.8", + "@vitest/spy": "4.0.9", + "@vitest/utils": "4.0.9", "chai": "^6.2.0", "tinyrainbow": "^3.0.3" }, @@ -4475,11 +4475,11 @@ } }, "node_modules/@vitest/mocker": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.8.tgz", - "integrity": "sha512-9FRM3MZCedXH3+pIh+ME5Up2NBBHDq0wqwhOKkN4VnvCiKbVxddqH9mSGPZeawjd12pCOGnl+lo/ZGHt0/dQSg==", + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.9.tgz", + "integrity": "sha512-PUyaowQFHW+9FKb4dsvvBM4o025rWMlEDXdWRxIOilGaHREYTi5Q2Rt9VCgXgPy/hHZu1LeuXtrA/GdzOatP2g==", "dependencies": { - "@vitest/spy": "4.0.8", + "@vitest/spy": "4.0.9", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, @@ -4508,9 +4508,9 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.8.tgz", - "integrity": "sha512-qRrjdRkINi9DaZHAimV+8ia9Gq6LeGz2CgIEmMLz3sBDYV53EsnLZbJMR1q84z1HZCMsf7s0orDgZn7ScXsZKg==", + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.9.tgz", + "integrity": "sha512-Hor0IBTwEi/uZqB7pvGepyElaM8J75pYjrrqbC8ZYMB9/4n5QA63KC15xhT+sqHpdGWfdnPo96E8lQUxs2YzSQ==", "dependencies": { "tinyrainbow": "^3.0.3" }, @@ -4519,11 +4519,11 @@ } }, "node_modules/@vitest/runner": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.8.tgz", - "integrity": "sha512-mdY8Sf1gsM8hKJUQfiPT3pn1n8RF4QBcJYFslgWh41JTfrK1cbqY8whpGCFzBl45LN028g0njLCYm0d7XxSaQQ==", + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.9.tgz", + "integrity": "sha512-aF77tsXdEvIJRkj9uJZnHtovsVIx22Ambft9HudC+XuG/on1NY/bf5dlDti1N35eJT+QZLb4RF/5dTIG18s98w==", "dependencies": { - "@vitest/utils": "4.0.8", + "@vitest/utils": "4.0.9", "pathe": "^2.0.3" }, "funding": { @@ -4531,11 +4531,11 @@ } }, "node_modules/@vitest/snapshot": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.8.tgz", - "integrity": "sha512-Nar9OTU03KGiubrIOFhcfHg8FYaRaNT+bh5VUlNz8stFhCZPNrJvmZkhsr1jtaYvuefYFwK2Hwrq026u4uPWCw==", + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.9.tgz", + "integrity": "sha512-r1qR4oYstPbnOjg0Vgd3E8ADJbi4ditCzqr+Z9foUrRhIy778BleNyZMeAJ2EjV+r4ASAaDsdciC9ryMy8xMMg==", "dependencies": { - "@vitest/pretty-format": "4.0.8", + "@vitest/pretty-format": "4.0.9", "magic-string": "^0.30.21", "pathe": "^2.0.3" }, @@ -4544,19 +4544,19 @@ } }, "node_modules/@vitest/spy": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.8.tgz", - "integrity": "sha512-nvGVqUunyCgZH7kmo+Ord4WgZ7lN0sOULYXUOYuHr55dvg9YvMz3izfB189Pgp28w0vWFbEEfNc/c3VTrqrXeA==", + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.9.tgz", + "integrity": "sha512-J9Ttsq0hDXmxmT8CUOWUr1cqqAj2FJRGTdyEjSR+NjoOGKEqkEWj+09yC0HhI8t1W6t4Ctqawl1onHgipJve1A==", "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/utils": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.8.tgz", - "integrity": "sha512-pdk2phO5NDvEFfUTxcTP8RFYjVj/kfLSPIN5ebP2Mu9kcIMeAQTbknqcFEyBcC4z2pJlJI9aS5UQjcYfhmKAow==", + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.9.tgz", + "integrity": "sha512-cEol6ygTzY4rUPvNZM19sDf7zGa35IYTm9wfzkHoT/f5jX10IOY7QleWSOh5T0e3I3WVozwK5Asom79qW8DiuQ==", "dependencies": { - "@vitest/pretty-format": "4.0.8", + "@vitest/pretty-format": "4.0.9", "tinyrainbow": "^3.0.3" }, "funding": { @@ -5113,9 +5113,9 @@ "license": "CC-BY-4.0" }, "node_modules/chai": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.0.tgz", - "integrity": "sha512-aUTnJc/JipRzJrNADXVvpVqi6CO0dn3nx4EVPxijri+fj3LUUDyZQOgVeW54Ob3Y1Xh9Iz8f+CgaCl8v0mn9bA==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.1.tgz", + "integrity": "sha512-p4Z49OGG5W/WBCPSS/dH3jQ73kD6tiMmUM+bckNK6Jr5JHMG3k9bg/BvKR8lKmtVBKmOiuVaV2ws8s9oSbwysg==", "engines": { "node": ">=18" } @@ -10430,17 +10430,17 @@ } }, "node_modules/vitest": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.8.tgz", - "integrity": "sha512-urzu3NCEV0Qa0Y2PwvBtRgmNtxhj5t5ULw7cuKhIHh3OrkKTLlut0lnBOv9qe5OvbkMH2g38G7KPDCTpIytBVg==", - "dependencies": { - "@vitest/expect": "4.0.8", - "@vitest/mocker": "4.0.8", - "@vitest/pretty-format": "4.0.8", - "@vitest/runner": "4.0.8", - "@vitest/snapshot": "4.0.8", - "@vitest/spy": "4.0.8", - "@vitest/utils": "4.0.8", + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.9.tgz", + "integrity": "sha512-E0Ja2AX4th+CG33yAFRC+d1wFx2pzU5r6HtG6LiPSE04flaE0qB6YyjSw9ZcpJAtVPfsvZGtJlKWZpuW7EHRxg==", + "dependencies": { + "@vitest/expect": "4.0.9", + "@vitest/mocker": "4.0.9", + "@vitest/pretty-format": "4.0.9", + "@vitest/runner": "4.0.9", + "@vitest/snapshot": "4.0.9", + "@vitest/spy": "4.0.9", + "@vitest/utils": "4.0.9", "debug": "^4.4.3", "es-module-lexer": "^1.7.0", "expect-type": "^1.2.2", @@ -10468,10 +10468,10 @@ "@edge-runtime/vm": "*", "@types/debug": "^4.1.12", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", - "@vitest/browser-playwright": "4.0.8", - "@vitest/browser-preview": "4.0.8", - "@vitest/browser-webdriverio": "4.0.8", - "@vitest/ui": "4.0.8", + "@vitest/browser-playwright": "4.0.9", + "@vitest/browser-preview": "4.0.9", + "@vitest/browser-webdriverio": "4.0.9", + "@vitest/ui": "4.0.9", "happy-dom": "*", "jsdom": "*" }, @@ -13767,12 +13767,12 @@ } }, "@vitest/browser": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/@vitest/browser/-/browser-4.0.8.tgz", - "integrity": "sha512-oG6QJAR0d7S5SDnIYZwjxCj/a5fhbp9ZE7GtMgZn+yCUf4CxtqbBV6aXyg0qmn8nbUWT+rGuXL2ZB6qDBUjv/A==", + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@vitest/browser/-/browser-4.0.9.tgz", + "integrity": "sha512-OWN4ZgOIV2+T9cR4qfoajtjZDFoxcLa6qUpgDkviXZFUNkZ7XTVKvL/16X+gz5dtpqdZwXf3m0qIj72Ge/vytw==", "requires": { - "@vitest/mocker": "4.0.8", - "@vitest/utils": "4.0.8", + "@vitest/mocker": "4.0.9", + "@vitest/utils": "4.0.9", "magic-string": "^0.30.21", "pixelmatch": "7.1.0", "pngjs": "^7.0.0", @@ -13782,23 +13782,23 @@ } }, "@vitest/browser-playwright": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/@vitest/browser-playwright/-/browser-playwright-4.0.8.tgz", - "integrity": "sha512-MUi0msIAPXcA2YAuVMcssrSYP/yylxLt347xyTC6+ODl0c4XQFs0d2AN3Pc3iTa0pxIGmogflUV6eogXpPbJeA==", + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@vitest/browser-playwright/-/browser-playwright-4.0.9.tgz", + "integrity": "sha512-ayr0vCxvJIvodzfUTVzifFMT3bmcMeKzEWoPt7mtgrZsqJhMbYaftifuBZRQeF/glogsVr+jhtIePHw6g+0YRQ==", "requires": { - "@vitest/browser": "4.0.8", - "@vitest/mocker": "4.0.8", + "@vitest/browser": "4.0.9", + "@vitest/mocker": "4.0.9", "tinyrainbow": "^3.0.3" } }, "@vitest/coverage-v8": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.0.8.tgz", - "integrity": "sha512-wQgmtW6FtPNn4lWUXi8ZSYLpOIb92j3QCujxX3sQ81NTfQ/ORnE0HtK7Kqf2+7J9jeveMGyGyc4NWc5qy3rC4A==", + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.0.9.tgz", + "integrity": "sha512-70oyhP+Q0HlWBIeGSP74YBw5KSjYhNgSCQjvmuQFciMqnyF36WL2cIkcT7XD85G4JPmBQitEMUsx+XMFv2AzQA==", "dev": true, "requires": { "@bcoe/v8-coverage": "^1.0.2", - "@vitest/utils": "4.0.8", + "@vitest/utils": "4.0.9", "ast-v8-to-istanbul": "^0.3.8", "debug": "^4.4.3", "istanbul-lib-coverage": "^3.2.2", @@ -13811,24 +13811,24 @@ } }, "@vitest/expect": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.8.tgz", - "integrity": "sha512-Rv0eabdP/xjAHQGr8cjBm+NnLHNoL268lMDK85w2aAGLFoVKLd8QGnVon5lLtkXQCoYaNL0wg04EGnyKkkKhPA==", + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.9.tgz", + "integrity": "sha512-C2vyXf5/Jfj1vl4DQYxjib3jzyuswMi/KHHVN2z+H4v16hdJ7jMZ0OGe3uOVIt6LyJsAofDdaJNIFEpQcrSTFw==", "requires": { "@standard-schema/spec": "^1.0.0", "@types/chai": "^5.2.2", - "@vitest/spy": "4.0.8", - "@vitest/utils": "4.0.8", + "@vitest/spy": "4.0.9", + "@vitest/utils": "4.0.9", "chai": "^6.2.0", "tinyrainbow": "^3.0.3" } }, "@vitest/mocker": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.8.tgz", - "integrity": "sha512-9FRM3MZCedXH3+pIh+ME5Up2NBBHDq0wqwhOKkN4VnvCiKbVxddqH9mSGPZeawjd12pCOGnl+lo/ZGHt0/dQSg==", + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.9.tgz", + "integrity": "sha512-PUyaowQFHW+9FKb4dsvvBM4o025rWMlEDXdWRxIOilGaHREYTi5Q2Rt9VCgXgPy/hHZu1LeuXtrA/GdzOatP2g==", "requires": { - "@vitest/spy": "4.0.8", + "@vitest/spy": "4.0.9", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, @@ -13844,43 +13844,43 @@ } }, "@vitest/pretty-format": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.8.tgz", - "integrity": "sha512-qRrjdRkINi9DaZHAimV+8ia9Gq6LeGz2CgIEmMLz3sBDYV53EsnLZbJMR1q84z1HZCMsf7s0orDgZn7ScXsZKg==", + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.9.tgz", + "integrity": "sha512-Hor0IBTwEi/uZqB7pvGepyElaM8J75pYjrrqbC8ZYMB9/4n5QA63KC15xhT+sqHpdGWfdnPo96E8lQUxs2YzSQ==", "requires": { "tinyrainbow": "^3.0.3" } }, "@vitest/runner": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.8.tgz", - "integrity": "sha512-mdY8Sf1gsM8hKJUQfiPT3pn1n8RF4QBcJYFslgWh41JTfrK1cbqY8whpGCFzBl45LN028g0njLCYm0d7XxSaQQ==", + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.9.tgz", + "integrity": "sha512-aF77tsXdEvIJRkj9uJZnHtovsVIx22Ambft9HudC+XuG/on1NY/bf5dlDti1N35eJT+QZLb4RF/5dTIG18s98w==", "requires": { - "@vitest/utils": "4.0.8", + "@vitest/utils": "4.0.9", "pathe": "^2.0.3" } }, "@vitest/snapshot": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.8.tgz", - "integrity": "sha512-Nar9OTU03KGiubrIOFhcfHg8FYaRaNT+bh5VUlNz8stFhCZPNrJvmZkhsr1jtaYvuefYFwK2Hwrq026u4uPWCw==", + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.9.tgz", + "integrity": "sha512-r1qR4oYstPbnOjg0Vgd3E8ADJbi4ditCzqr+Z9foUrRhIy778BleNyZMeAJ2EjV+r4ASAaDsdciC9ryMy8xMMg==", "requires": { - "@vitest/pretty-format": "4.0.8", + "@vitest/pretty-format": "4.0.9", "magic-string": "^0.30.21", "pathe": "^2.0.3" } }, "@vitest/spy": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.8.tgz", - "integrity": "sha512-nvGVqUunyCgZH7kmo+Ord4WgZ7lN0sOULYXUOYuHr55dvg9YvMz3izfB189Pgp28w0vWFbEEfNc/c3VTrqrXeA==" + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.9.tgz", + "integrity": "sha512-J9Ttsq0hDXmxmT8CUOWUr1cqqAj2FJRGTdyEjSR+NjoOGKEqkEWj+09yC0HhI8t1W6t4Ctqawl1onHgipJve1A==" }, "@vitest/utils": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.8.tgz", - "integrity": "sha512-pdk2phO5NDvEFfUTxcTP8RFYjVj/kfLSPIN5ebP2Mu9kcIMeAQTbknqcFEyBcC4z2pJlJI9aS5UQjcYfhmKAow==", + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.9.tgz", + "integrity": "sha512-cEol6ygTzY4rUPvNZM19sDf7zGa35IYTm9wfzkHoT/f5jX10IOY7QleWSOh5T0e3I3WVozwK5Asom79qW8DiuQ==", "requires": { - "@vitest/pretty-format": "4.0.8", + "@vitest/pretty-format": "4.0.9", "tinyrainbow": "^3.0.3" } }, @@ -14265,9 +14265,9 @@ "dev": true }, "chai": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.0.tgz", - "integrity": "sha512-aUTnJc/JipRzJrNADXVvpVqi6CO0dn3nx4EVPxijri+fj3LUUDyZQOgVeW54Ob3Y1Xh9Iz8f+CgaCl8v0mn9bA==" + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.1.tgz", + "integrity": "sha512-p4Z49OGG5W/WBCPSS/dH3jQ73kD6tiMmUM+bckNK6Jr5JHMG3k9bg/BvKR8lKmtVBKmOiuVaV2ws8s9oSbwysg==" }, "chalk": { "version": "5.6.2", @@ -17832,17 +17832,17 @@ } }, "vitest": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.8.tgz", - "integrity": "sha512-urzu3NCEV0Qa0Y2PwvBtRgmNtxhj5t5ULw7cuKhIHh3OrkKTLlut0lnBOv9qe5OvbkMH2g38G7KPDCTpIytBVg==", - "requires": { - "@vitest/expect": "4.0.8", - "@vitest/mocker": "4.0.8", - "@vitest/pretty-format": "4.0.8", - "@vitest/runner": "4.0.8", - "@vitest/snapshot": "4.0.8", - "@vitest/spy": "4.0.8", - "@vitest/utils": "4.0.8", + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.9.tgz", + "integrity": "sha512-E0Ja2AX4th+CG33yAFRC+d1wFx2pzU5r6HtG6LiPSE04flaE0qB6YyjSw9ZcpJAtVPfsvZGtJlKWZpuW7EHRxg==", + "requires": { + "@vitest/expect": "4.0.9", + "@vitest/mocker": "4.0.9", + "@vitest/pretty-format": "4.0.9", + "@vitest/runner": "4.0.9", + "@vitest/snapshot": "4.0.9", + "@vitest/spy": "4.0.9", + "@vitest/utils": "4.0.9", "debug": "^4.4.3", "es-module-lexer": "^1.7.0", "expect-type": "^1.2.2", diff --git a/package.json b/package.json index c423d826c..f3939d469 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,7 @@ "@shlinkio/shlink-frontend-kit": "^1.3.0", "@shlinkio/shlink-js-sdk": "^3.0.1", "@shlinkio/shlink-web-component": "^0.17.0", - "@vitest/browser-playwright": "^4.0.8", + "@vitest/browser-playwright": "^4.0.9", "bottlejs": "^2.0.1", "clsx": "^2.1.1", "compare-versions": "^6.1.1", @@ -62,7 +62,7 @@ "@types/react-dom": "^19.2.2", "@vitejs/plugin-react": "^5.1.0", "@vitest/browser": "^4.0.3", - "@vitest/coverage-v8": "^4.0.8", + "@vitest/coverage-v8": "^4.0.9", "adm-zip": "^0.5.16", "axe-core": "^4.11.0", "chalk": "^5.6.2", From 8492d47274c5194710152772fea3b97376e2a678 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 15 Nov 2025 08:03:12 +0000 Subject: [PATCH 16/24] Bump react-router from 7.9.5 to 7.9.6 Bumps [react-router](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router) from 7.9.5 to 7.9.6. - [Release notes](https://github.com/remix-run/react-router/releases) - [Changelog](https://github.com/remix-run/react-router/blob/main/packages/react-router/CHANGELOG.md) - [Commits](https://github.com/remix-run/react-router/commits/react-router@7.9.6/packages/react-router) --- updated-dependencies: - dependency-name: react-router dependency-version: 7.9.6 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- package-lock.json | 14 +++++++------- package.json | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index e12820598..baf05502d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,7 +29,7 @@ "react-dom": "^19.2.0", "react-external-link": "^2.6.1", "react-redux": "^9.2.0", - "react-router": "^7.9.5", + "react-router": "^7.9.6", "redux-localstorage-simple": "^2.5.1", "workbox-core": "^7.3.0", "workbox-expiration": "^7.3.0", @@ -8862,9 +8862,9 @@ } }, "node_modules/react-router": { - "version": "7.9.5", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.9.5.tgz", - "integrity": "sha512-JmxqrnBZ6E9hWmf02jzNn9Jm3UqyeimyiwzD69NjxGySG6lIz/1LVPsoTCwN7NBX2XjCEa1LIX5EMz1j2b6u6A==", + "version": "7.9.6", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.9.6.tgz", + "integrity": "sha512-Y1tUp8clYRXpfPITyuifmSoE2vncSME18uVLgaqyxh9H35JWpIfzHo+9y3Fzh5odk/jxPW29IgLgzcdwxGqyNA==", "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" @@ -16798,9 +16798,9 @@ "dev": true }, "react-router": { - "version": "7.9.5", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.9.5.tgz", - "integrity": "sha512-JmxqrnBZ6E9hWmf02jzNn9Jm3UqyeimyiwzD69NjxGySG6lIz/1LVPsoTCwN7NBX2XjCEa1LIX5EMz1j2b6u6A==", + "version": "7.9.6", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.9.6.tgz", + "integrity": "sha512-Y1tUp8clYRXpfPITyuifmSoE2vncSME18uVLgaqyxh9H35JWpIfzHo+9y3Fzh5odk/jxPW29IgLgzcdwxGqyNA==", "requires": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" diff --git a/package.json b/package.json index c423d826c..9ba7ef953 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,7 @@ "react-dom": "^19.2.0", "react-external-link": "^2.6.1", "react-redux": "^9.2.0", - "react-router": "^7.9.5", + "react-router": "^7.9.6", "redux-localstorage-simple": "^2.5.1", "workbox-core": "^7.3.0", "workbox-expiration": "^7.3.0", From af23fa878c73217c05220ccebf718aed3d03c710 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 15 Nov 2025 08:26:48 +0000 Subject: [PATCH 17/24] Bump js-yaml from 4.1.0 to 4.1.1 Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 4.1.0 to 4.1.1. - [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md) - [Commits](https://github.com/nodeca/js-yaml/compare/4.1.0...4.1.1) --- updated-dependencies: - dependency-name: js-yaml dependency-version: 4.1.1 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- package-lock.json | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/package-lock.json b/package-lock.json index 51dd73fb7..a20c792ce 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7655,11 +7655,10 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dev": true, - "license": "MIT", "dependencies": { "argparse": "^2.0.1" }, @@ -16059,9 +16058,9 @@ "dev": true }, "js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dev": true, "requires": { "argparse": "^2.0.1" From fced67d981e6c5cc30f82e134e389f8fdeb6cf21 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 15 Nov 2025 08:27:51 +0000 Subject: [PATCH 18/24] Bump the react group with 2 updates Bumps the react group with 2 updates: [@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react) and [@types/react-dom](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react-dom). Updates `@types/react` from 19.2.2 to 19.2.5 - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react) Updates `@types/react-dom` from 19.2.2 to 19.2.3 - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react-dom) --- updated-dependencies: - dependency-name: "@types/react" dependency-version: 19.2.5 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: react - dependency-name: "@types/react-dom" dependency-version: 19.2.3 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: react ... Signed-off-by: dependabot[bot] --- package-lock.json | 28 ++++++++++++++-------------- package.json | 4 ++-- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2229b91cf..7943d3eb1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -45,8 +45,8 @@ "@testing-library/react": "^16.3.0", "@testing-library/user-event": "^14.6.1", "@total-typescript/shoehorn": "^0.1.2", - "@types/react": "^19.2.2", - "@types/react-dom": "^19.2.2", + "@types/react": "^19.2.5", + "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^5.1.1", "@vitest/browser": "^4.0.3", "@vitest/coverage-v8": "^4.0.9", @@ -4072,18 +4072,18 @@ } }, "node_modules/@types/react": { - "version": "19.2.2", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz", - "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==", + "version": "19.2.5", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.5.tgz", + "integrity": "sha512-keKxkZMqnDicuvFoJbzrhbtdLSPhj/rZThDlKWCDbgXmUg0rEUFtRssDXKYmtXluZlIqiC5VqkCgRwzuyLHKHw==", "devOptional": true, "dependencies": { "csstype": "^3.0.2" } }, "node_modules/@types/react-dom": { - "version": "19.2.2", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.2.tgz", - "integrity": "sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw==", + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "dev": true, "peerDependencies": { "@types/react": "^19.2.0" @@ -13566,18 +13566,18 @@ } }, "@types/react": { - "version": "19.2.2", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz", - "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==", + "version": "19.2.5", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.5.tgz", + "integrity": "sha512-keKxkZMqnDicuvFoJbzrhbtdLSPhj/rZThDlKWCDbgXmUg0rEUFtRssDXKYmtXluZlIqiC5VqkCgRwzuyLHKHw==", "devOptional": true, "requires": { "csstype": "^3.0.2" } }, "@types/react-dom": { - "version": "19.2.2", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.2.tgz", - "integrity": "sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw==", + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "dev": true, "requires": {} }, diff --git a/package.json b/package.json index 8e584f4d1..777fb3556 100644 --- a/package.json +++ b/package.json @@ -58,8 +58,8 @@ "@testing-library/react": "^16.3.0", "@testing-library/user-event": "^14.6.1", "@total-typescript/shoehorn": "^0.1.2", - "@types/react": "^19.2.2", - "@types/react-dom": "^19.2.2", + "@types/react": "^19.2.5", + "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^5.1.1", "@vitest/browser": "^4.0.3", "@vitest/coverage-v8": "^4.0.9", From f301513f5b060fd1517f4bac9d99d37b3c938190 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 15 Nov 2025 10:20:53 +0100 Subject: [PATCH 19/24] Expose container via provider --- src/app/App.tsx | 6 +---- src/common/ShlinkWebComponentContainer.tsx | 7 +++--- src/container/context.ts | 24 +++++++++++++++++++ src/index.tsx | 21 +++++++++------- src/servers/EditServer.tsx | 9 +++---- src/servers/helpers/withSelectedServer.tsx | 19 ++++----------- src/servers/reducers/remoteServers.ts | 10 ++++---- src/servers/reducers/selectedServer.ts | 6 +++-- test/app/App.test.tsx | 8 +++++-- test/common/MainHeader.test.tsx | 6 ++++- test/common/ShlinkVersionsContainer.test.tsx | 5 +++- .../ShlinkWebComponentContainer.test.tsx | 5 +++- test/servers/EditServer.test.tsx | 5 +++- test/servers/ServersDropdown.test.tsx | 9 ++++--- test/servers/helpers/ServerError.test.tsx | 5 +++- 15 files changed, 92 insertions(+), 53 deletions(-) create mode 100644 src/container/context.ts diff --git a/src/app/App.tsx b/src/app/App.tsx index ede79ee67..9bd2d471e 100644 --- a/src/app/App.tsx +++ b/src/app/App.tsx @@ -1,5 +1,4 @@ import { changeThemeInMarkup, getSystemPreferredTheme } from '@shlinkio/shlink-frontend-kit'; -import type { HttpClient } from '@shlinkio/shlink-js-sdk'; import { clsx } from 'clsx'; import type { FC } from 'react'; import { useEffect } from 'react'; @@ -26,7 +25,6 @@ type AppDeps = { ShlinkWebComponentContainer: FC; CreateServer: FC; ManageServers: FC; - HttpClient: HttpClient; }; const App: FCWithDeps = ({ appUpdated, resetAppUpdate }) => { @@ -35,10 +33,9 @@ const App: FCWithDeps = ({ appUpdated, resetAppUpdate }) => { ShlinkWebComponentContainer, CreateServer, ManageServers, - HttpClient: httpClient, } = useDependencies(App); - useLoadRemoteServers(httpClient); + useLoadRemoteServers(); const location = useLocation(); const isHome = location.pathname === '/'; @@ -92,5 +89,4 @@ export const AppFactory = componentFactory(App, [ 'ShlinkWebComponentContainer', 'CreateServer', 'ManageServers', - 'HttpClient', ]); diff --git a/src/common/ShlinkWebComponentContainer.tsx b/src/common/ShlinkWebComponentContainer.tsx index 4db6ab9ab..c343448f0 100644 --- a/src/common/ShlinkWebComponentContainer.tsx +++ b/src/common/ShlinkWebComponentContainer.tsx @@ -5,18 +5,19 @@ import { ShlinkWebComponent, } from '@shlinkio/shlink-web-component'; import { memo } from 'react'; +import type { ShlinkApiClientBuilder } from '../api/services/ShlinkApiClientBuilder'; import type { FCWithDeps } from '../container/utils'; import { componentFactory, useDependencies } from '../container/utils'; import { isReachableServer } from '../servers/data'; import { ServerError } from '../servers/helpers/ServerError'; -import type { WithSelectedServerPropsDeps } from '../servers/helpers/withSelectedServer'; import { withSelectedServer } from '../servers/helpers/withSelectedServer'; import { useSelectedServer } from '../servers/reducers/selectedServer'; import { useSettings } from '../settings/reducers/settings'; import { NotFound } from './NotFound'; -type ShlinkWebComponentContainerDeps = WithSelectedServerPropsDeps & { - TagColorsStorage: TagColorsStorage, +type ShlinkWebComponentContainerDeps = { + TagColorsStorage: TagColorsStorage; + buildShlinkApiClient: ShlinkApiClientBuilder; }; const ShlinkWebComponentContainer: FCWithDeps< diff --git a/src/container/context.ts b/src/container/context.ts new file mode 100644 index 000000000..ceb22874c --- /dev/null +++ b/src/container/context.ts @@ -0,0 +1,24 @@ +import type { IContainer } from 'bottlejs'; +import { createContext, useContext } from 'react'; + +const ContainerContext = createContext(null); + +export const ContainerProvider = ContainerContext.Provider; + +export const useDependencies = (...names: string[]): T => { + const container = useContext(ContainerContext); + if (!container) { + throw new Error('You cannot use "useDependency" outside of a ContainerProvider'); + } + + return names.map((name) => { + const dependency = container[name]; + if (!dependency) { + throw new Error(`Dependency with name "${name}" not found in container`); + } + + return dependency; + }) as T; +}; + +// TODO Create Higher Order Component that can pull dependencies from the container diff --git a/src/index.tsx b/src/index.tsx index 7829245dc..e3509a653 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -5,6 +5,7 @@ import pack from '../package.json'; import { ErrorHandler } from './common/ErrorHandler'; import { ScrollToTop } from './common/ScrollToTop'; import { container } from './container'; +import { ContainerProvider } from './container/context'; import { register as registerServiceWorker } from './serviceWorkerRegistration'; import { setUpStore } from './store'; import './tailwind.css'; @@ -13,15 +14,17 @@ const store = setUpStore(); const { App, appUpdateAvailable } = container; createRoot(document.getElementById('root')!).render( - - - - - - - - - , + + + + + + + + + + + , ); // Learn more about service workers: https://cra.link/PWA diff --git a/src/servers/EditServer.tsx b/src/servers/EditServer.tsx index 74c59a487..707ee7131 100644 --- a/src/servers/EditServer.tsx +++ b/src/servers/EditServer.tsx @@ -1,19 +1,16 @@ import { Button, useParsedQuery } from '@shlinkio/shlink-frontend-kit'; +import type { FC } from 'react'; import { NoMenuLayout } from '../common/NoMenuLayout'; -import type { FCWithDeps } from '../container/utils'; -import { useDependencies } from '../container/utils'; import { useGoBack } from '../utils/helpers/hooks'; import type { ServerData } from './data'; import { isServerWithId } from './data'; import { ServerForm } from './helpers/ServerForm'; -import type { WithSelectedServerPropsDeps } from './helpers/withSelectedServer'; import { withSelectedServer } from './helpers/withSelectedServer'; import { useSelectedServer } from './reducers/selectedServer'; import { useServers } from './reducers/servers'; -export const EditServer: FCWithDeps = withSelectedServer(() => { +export const EditServer: FC = withSelectedServer(() => { const { editServer } = useServers(); - const { buildShlinkApiClient } = useDependencies(EditServer); const { selectServer, selectedServer } = useSelectedServer(); const goBack = useGoBack(); const { reconnect } = useParsedQuery<{ reconnect?: 'true' }>(); @@ -25,7 +22,7 @@ export const EditServer: FCWithDeps = withSele const handleSubmit = (serverData: ServerData) => { editServer(selectedServer.id, serverData); if (reconnect === 'true') { - selectServer({ serverId: selectedServer.id, buildShlinkApiClient }); + selectServer(selectedServer.id); } goBack(); }; diff --git a/src/servers/helpers/withSelectedServer.tsx b/src/servers/helpers/withSelectedServer.tsx index 1661f7f58..15b55a4e6 100644 --- a/src/servers/helpers/withSelectedServer.tsx +++ b/src/servers/helpers/withSelectedServer.tsx @@ -1,31 +1,22 @@ import { Message } from '@shlinkio/shlink-frontend-kit'; +import type { FC } from 'react'; import { useEffect } from 'react'; import { useParams } from 'react-router'; -import type { ShlinkApiClientBuilder } from '../../api/services/ShlinkApiClientBuilder'; import { NoMenuLayout } from '../../common/NoMenuLayout'; -import type { FCWithDeps } from '../../container/utils'; -import { useDependencies } from '../../container/utils'; import { isNotFoundServer } from '../data'; import { useSelectedServer } from '../reducers/selectedServer'; import { ServerError } from './ServerError'; -export type WithSelectedServerPropsDeps = { - buildShlinkApiClient: ShlinkApiClientBuilder; -}; - -export function withSelectedServer( - WrappedComponent: FCWithDeps, -) { - const ComponentWrapper: FCWithDeps = (props) => { - const { buildShlinkApiClient } = useDependencies(ComponentWrapper); +export function withSelectedServer(WrappedComponent: FC) { + const ComponentWrapper: FC = (props) => { const params = useParams<{ serverId: string }>(); const { selectServer, selectedServer } = useSelectedServer(); useEffect(() => { if (params.serverId) { - selectServer({ serverId: params.serverId, buildShlinkApiClient }); + selectServer(params.serverId); } - }, [buildShlinkApiClient, params.serverId, selectServer]); + }, [params.serverId, selectServer]); if (!selectedServer) { return ( diff --git a/src/servers/reducers/remoteServers.ts b/src/servers/reducers/remoteServers.ts index 163b7c8f6..7d6f17ad4 100644 --- a/src/servers/reducers/remoteServers.ts +++ b/src/servers/reducers/remoteServers.ts @@ -1,6 +1,7 @@ import type { HttpClient } from '@shlinkio/shlink-js-sdk'; import { useCallback, useEffect, useRef } from 'react'; import pack from '../../../package.json'; +import { useDependencies } from '../../container/context'; import { useAppDispatch } from '../../store'; import { createAsyncThunk } from '../../store/helpers'; import { hasServerData } from '../data'; @@ -24,12 +25,13 @@ export const fetchServers = createAsyncThunk( export const useRemoteServers = () => { const dispatch = useAppDispatch(); - const dispatchFetchServer = useCallback((httpClient: HttpClient) => dispatch(fetchServers(httpClient)), [dispatch]); + const [httpClient] = useDependencies<[HttpClient]>('HttpClient'); + const dispatchFetchServer = useCallback(() => dispatch(fetchServers(httpClient)), [dispatch, httpClient]); return { fetchServers: dispatchFetchServer }; }; -export const useLoadRemoteServers = (httpClient: HttpClient) => { +export const useLoadRemoteServers = () => { const { fetchServers } = useRemoteServers(); const { servers } = useServers(); const initialServers = useRef(servers); @@ -38,7 +40,7 @@ export const useLoadRemoteServers = (httpClient: HttpClient) => { // Try to fetch the remote servers if the list is empty during first render. // We use a ref because we don't care if the servers list becomes empty later. if (Object.keys(initialServers.current).length === 0) { - fetchServers(httpClient); + fetchServers(); } - }, [fetchServers, httpClient]); + }, [fetchServers]); }; diff --git a/src/servers/reducers/selectedServer.ts b/src/servers/reducers/selectedServer.ts index 5c3a9b333..b99cbcf4b 100644 --- a/src/servers/reducers/selectedServer.ts +++ b/src/servers/reducers/selectedServer.ts @@ -3,6 +3,7 @@ import { memoizeWith } from '@shlinkio/data-manipulation'; import type { ShlinkHealth } from '@shlinkio/shlink-web-component/api-contract'; import { useCallback } from 'react'; import type { ShlinkApiClientBuilder } from '../../api/services/ShlinkApiClientBuilder'; +import { useDependencies } from '../../container/context'; import { useAppDispatch, useAppSelector } from '../../store'; import { createAsyncThunk } from '../../store/helpers'; import { versionToPrintable, versionToSemVer as toSemVer } from '../../utils/helpers/version'; @@ -75,10 +76,11 @@ export const { reducer: selectedServerReducer } = createSlice({ export const useSelectedServer = () => { const dispatch = useAppDispatch(); + const [buildShlinkApiClient] = useDependencies<[ShlinkApiClientBuilder]>('buildShlinkApiClient'); const dispatchResetSelectedServer = useCallback(() => dispatch(resetSelectedServer()), [dispatch]); const dispatchSelectServer = useCallback( - (options: SelectServerOptions) => dispatch(selectServer(options)), - [dispatch], + (serverId: string) => dispatch(selectServer({ serverId, buildShlinkApiClient })), + [buildShlinkApiClient, dispatch], ); const selectedServer = useAppSelector(({ selectedServer }) => selectedServer); diff --git a/test/app/App.test.tsx b/test/app/App.test.tsx index ed6cf6e62..6f291b6ec 100644 --- a/test/app/App.test.tsx +++ b/test/app/App.test.tsx @@ -3,6 +3,7 @@ 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 { ContainerProvider } from '../../src/container/context'; import type { ServerWithId } from '../../src/servers/data'; import { checkAccessibility } from '../__helpers__/accessibility'; import { renderWithStore } from '../__helpers__/setUpTest'; @@ -14,12 +15,15 @@ describe('', () => { ShlinkWebComponentContainer: () => <>ShlinkWebComponentContainer, CreateServer: () => <>CreateServer, ManageServers: () => <>ManageServers, - HttpClient: fromPartial({}), }), ); const setUp = async (activeRoute = '/') => act(() => renderWithStore( - {}} /> + ({}), buildShlinkApiClient: vi.fn() })} + > + {}} /> + , { initialState: { diff --git a/test/common/MainHeader.test.tsx b/test/common/MainHeader.test.tsx index e70f755dd..e9614c6a1 100644 --- a/test/common/MainHeader.test.tsx +++ b/test/common/MainHeader.test.tsx @@ -1,7 +1,9 @@ import { screen } from '@testing-library/react'; +import { fromPartial } from '@total-typescript/shoehorn'; import { createMemoryHistory } from 'history'; import { Router } from 'react-router'; import { MainHeader } from '../../src/common/MainHeader'; +import { ContainerProvider } from '../../src/container/context'; import { checkAccessibility } from '../__helpers__/accessibility'; import { renderWithStore } from '../__helpers__/setUpTest'; @@ -12,7 +14,9 @@ describe('', () => { return renderWithStore( - + + + , ); }; diff --git a/test/common/ShlinkVersionsContainer.test.tsx b/test/common/ShlinkVersionsContainer.test.tsx index 120f769a1..ba9c9dc2e 100644 --- a/test/common/ShlinkVersionsContainer.test.tsx +++ b/test/common/ShlinkVersionsContainer.test.tsx @@ -1,12 +1,15 @@ import { fromPartial } from '@total-typescript/shoehorn'; import { ShlinkVersionsContainer } from '../../src/common/ShlinkVersionsContainer'; +import { ContainerProvider } from '../../src/container/context'; import type { ReachableServer, SelectedServer } from '../../src/servers/data'; import { checkAccessibility } from '../__helpers__/accessibility'; import { renderWithStore } from '../__helpers__/setUpTest'; describe('', () => { const setUp = (selectedServer: SelectedServer = null) => renderWithStore( - , + + + , { initialState: { selectedServer }, }, diff --git a/test/common/ShlinkWebComponentContainer.test.tsx b/test/common/ShlinkWebComponentContainer.test.tsx index 361fa9805..0efca1696 100644 --- a/test/common/ShlinkWebComponentContainer.test.tsx +++ b/test/common/ShlinkWebComponentContainer.test.tsx @@ -2,6 +2,7 @@ import { screen } from '@testing-library/react'; import { fromPartial } from '@total-typescript/shoehorn'; import { MemoryRouter } from 'react-router'; import { ShlinkWebComponentContainerFactory } from '../../src/common/ShlinkWebComponentContainer'; +import { ContainerProvider } from '../../src/container/context'; import type { NonReachableServer, NotFoundServer, SelectedServer } from '../../src/servers/data'; import { checkAccessibility } from '../__helpers__/accessibility'; import { renderWithStore } from '../__helpers__/setUpTest'; @@ -19,7 +20,9 @@ describe('', () => { })); const setUp = (selectedServer: SelectedServer) => renderWithStore( - + + + , { initialState: { selectedServer, servers: {}, settings: {} }, diff --git a/test/servers/EditServer.test.tsx b/test/servers/EditServer.test.tsx index 8c4ed7b47..62b46252c 100644 --- a/test/servers/EditServer.test.tsx +++ b/test/servers/EditServer.test.tsx @@ -2,6 +2,7 @@ import { fireEvent, screen, waitFor } from '@testing-library/react'; import { fromPartial } from '@total-typescript/shoehorn'; import { createMemoryHistory } from 'history'; import { Router } from 'react-router'; +import { ContainerProvider } from '../../src/container/context'; import type { ReachableServer, SelectedServer } from '../../src/servers/data'; import { isServerWithId } from '../../src/servers/data'; import { EditServer } from '../../src/servers/EditServer'; @@ -21,7 +22,9 @@ describe('', () => { history, ...renderWithStore( - + + + , { initialState: { diff --git a/test/servers/ServersDropdown.test.tsx b/test/servers/ServersDropdown.test.tsx index b2668bd5f..d98a3a6ff 100644 --- a/test/servers/ServersDropdown.test.tsx +++ b/test/servers/ServersDropdown.test.tsx @@ -1,6 +1,7 @@ import { screen } from '@testing-library/react'; import { fromPartial } from '@total-typescript/shoehorn'; import { MemoryRouter } from 'react-router'; +import { ContainerProvider } from '../../src/container/context'; import type { ServersMap } from '../../src/servers/data'; import { ServersDropdown } from '../../src/servers/ServersDropdown'; import { checkAccessibility } from '../__helpers__/accessibility'; @@ -14,9 +15,11 @@ describe('', () => { }; const setUp = (servers: ServersMap = fallbackServers) => renderWithStore( -
        - -
      + +
        + +
      +
      , { initialState: { selectedServer: null, servers }, diff --git a/test/servers/helpers/ServerError.test.tsx b/test/servers/helpers/ServerError.test.tsx index 3a3aff9df..95e6693a1 100644 --- a/test/servers/helpers/ServerError.test.tsx +++ b/test/servers/helpers/ServerError.test.tsx @@ -1,6 +1,7 @@ import { screen } from '@testing-library/react'; import { fromPartial } from '@total-typescript/shoehorn'; import { MemoryRouter } from 'react-router'; +import { ContainerProvider } from '../../../src/container/context'; import type { NonReachableServer, NotFoundServer, SelectedServer } from '../../../src/servers/data'; import { ServerError } from '../../../src/servers/helpers/ServerError'; import { checkAccessibility } from '../../__helpers__/accessibility'; @@ -9,7 +10,9 @@ import { renderWithStore } from '../../__helpers__/setUpTest'; describe('', () => { const setUp = (selectedServer: SelectedServer) => renderWithStore( - + + + , { initialState: { selectedServer, servers: {} }, From 373f0dbbbb3f89c91316bc886a1b8ff36b51b1b4 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 15 Nov 2025 10:28:33 +0100 Subject: [PATCH 20/24] Do not inject appupdated state or actions --- src/app/App.tsx | 9 +++------ src/app/reducers/appUpdates.ts | 11 +++++++++++ src/app/services/provideServices.ts | 9 +-------- src/container/index.ts | 26 +------------------------- src/container/types.ts | 1 - src/index.tsx | 3 ++- test/app/App.test.tsx | 3 ++- 7 files changed, 20 insertions(+), 42 deletions(-) delete mode 100644 src/container/types.ts diff --git a/src/app/App.tsx b/src/app/App.tsx index 9bd2d471e..078fef99c 100644 --- a/src/app/App.tsx +++ b/src/app/App.tsx @@ -14,11 +14,7 @@ import { useLoadRemoteServers } from '../servers/reducers/remoteServers'; import { useSettings } from '../settings/reducers/settings'; import { Settings } from '../settings/Settings'; import { forceUpdate } from '../utils/helpers/sw'; - -export type AppProps = { - resetAppUpdate: () => void; - appUpdated: boolean; -}; +import { useAppUpdated } from './reducers/appUpdates'; type AppDeps = { Home: FC; @@ -27,7 +23,8 @@ type AppDeps = { ManageServers: FC; }; -const App: FCWithDeps = ({ appUpdated, resetAppUpdate }) => { +const App: FCWithDeps = () => { + const { appUpdated, resetAppUpdate } = useAppUpdated(); const { Home, ShlinkWebComponentContainer, diff --git a/src/app/reducers/appUpdates.ts b/src/app/reducers/appUpdates.ts index 675b39598..2bf870d8b 100644 --- a/src/app/reducers/appUpdates.ts +++ b/src/app/reducers/appUpdates.ts @@ -1,4 +1,6 @@ import { createSlice } from '@reduxjs/toolkit'; +import { useCallback } from 'react'; +import { useAppDispatch, useAppSelector } from '../../store'; const { actions, reducer } = createSlice({ name: 'shlink/appUpdates', @@ -12,3 +14,12 @@ const { actions, reducer } = createSlice({ export const { appUpdateAvailable, resetAppUpdate } = actions; export const appUpdatesReducer = reducer; + +export const useAppUpdated = () => { + const dispatch = useAppDispatch(); + const appUpdateAvailable = useCallback(() => dispatch(actions.appUpdateAvailable()), [dispatch]); + const resetAppUpdate = useCallback(() => dispatch(actions.resetAppUpdate()), [dispatch]); + const appUpdated = useAppSelector((state) => state.appUpdated); + + return { appUpdated, appUpdateAvailable, resetAppUpdate }; +}; diff --git a/src/app/services/provideServices.ts b/src/app/services/provideServices.ts index 80278b936..0d3e58b67 100644 --- a/src/app/services/provideServices.ts +++ b/src/app/services/provideServices.ts @@ -1,14 +1,7 @@ import type Bottle from 'bottlejs'; -import type { ConnectDecorator } from '../../container/types'; import { AppFactory } from '../App'; -import { appUpdateAvailable, resetAppUpdate } from '../reducers/appUpdates'; -export const provideServices = (bottle: Bottle, connect: ConnectDecorator) => { +export const provideServices = (bottle: Bottle) => { // Components bottle.factory('App', AppFactory); - bottle.decorator('App', connect(['appUpdated'], ['resetAppUpdate'])); - - // Actions - bottle.serviceFactory('appUpdateAvailable', () => appUpdateAvailable); - bottle.serviceFactory('resetAppUpdate', () => resetAppUpdate); }; diff --git a/src/container/index.ts b/src/container/index.ts index d7fa96064..81b5828e4 100644 --- a/src/container/index.ts +++ b/src/container/index.ts @@ -1,39 +1,15 @@ -import type { IContainer } from 'bottlejs'; import Bottle from 'bottlejs'; -import { connect as reduxConnect } from 'react-redux'; import { provideServices as provideApiServices } from '../api/services/provideServices'; 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 provideUtilsServices } from '../utils/services/provideServices'; -import type { ConnectDecorator } from './types'; - -type LazyActionMap = Record unknown>; const bottle = new Bottle(); export const { container } = bottle; -const lazyService = unknown, 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 pickProps = (propsToPick: string[]) => (obj: any) => Object.fromEntries( - propsToPick.map((key) => [key, obj[key]]), -); - -const connect: ConnectDecorator = (propsFromState: string[] | null, actionServiceNames: string[] = []) => - reduxConnect( - propsFromState ? pickProps(propsFromState) : null, - actionServiceNames.reduce(mapActionService, {}), - ); - -provideAppServices(bottle, connect); +provideAppServices(bottle); provideCommonServices(bottle); provideApiServices(bottle); provideServersServices(bottle); diff --git a/src/container/types.ts b/src/container/types.ts deleted file mode 100644 index 71b20fbd7..000000000 --- a/src/container/types.ts +++ /dev/null @@ -1 +0,0 @@ -export type ConnectDecorator = (props: string[] | null, actions?: string[]) => any; diff --git a/src/index.tsx b/src/index.tsx index e3509a653..f7022a694 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -2,6 +2,7 @@ import { createRoot } from 'react-dom/client'; import { Provider } from 'react-redux'; import { BrowserRouter } from 'react-router'; import pack from '../package.json'; +import { appUpdateAvailable } from './app/reducers/appUpdates'; import { ErrorHandler } from './common/ErrorHandler'; import { ScrollToTop } from './common/ScrollToTop'; import { container } from './container'; @@ -11,7 +12,7 @@ import { setUpStore } from './store'; import './tailwind.css'; const store = setUpStore(); -const { App, appUpdateAvailable } = container; +const { App } = container; createRoot(document.getElementById('root')!).render( diff --git a/test/app/App.test.tsx b/test/app/App.test.tsx index 6f291b6ec..694d12a9e 100644 --- a/test/app/App.test.tsx +++ b/test/app/App.test.tsx @@ -22,7 +22,7 @@ describe('', () => { ({}), buildShlinkApiClient: vi.fn() })} > - {}} /> + , { @@ -32,6 +32,7 @@ describe('', () => { def456: fromPartial({ id: 'def456', name: 'def456 server' }), }, settings: fromPartial({}), + appUpdated: false, }, }, )); From 4b655761c6e5c4292dce4146dbfd7555ac16d44e Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 15 Nov 2025 10:33:42 +0100 Subject: [PATCH 21/24] Consolidate all service definitions in one module --- src/api/services/provideServices.ts | 6 --- src/app/services/provideServices.ts | 7 --- src/common/services/provideServices.ts | 18 -------- src/container/index.ts | 60 ++++++++++++++++++++----- src/servers/services/provideServices.ts | 22 --------- src/utils/services/provideServices.ts | 16 ------- 6 files changed, 50 insertions(+), 79 deletions(-) delete mode 100644 src/api/services/provideServices.ts delete mode 100644 src/app/services/provideServices.ts delete mode 100644 src/common/services/provideServices.ts delete mode 100644 src/servers/services/provideServices.ts delete mode 100644 src/utils/services/provideServices.ts diff --git a/src/api/services/provideServices.ts b/src/api/services/provideServices.ts deleted file mode 100644 index a89e111bb..000000000 --- a/src/api/services/provideServices.ts +++ /dev/null @@ -1,6 +0,0 @@ -import type Bottle from 'bottlejs'; -import { buildShlinkApiClient } from './ShlinkApiClientBuilder'; - -export const provideServices = (bottle: Bottle) => { - bottle.serviceFactory('buildShlinkApiClient', buildShlinkApiClient, 'HttpClient'); -}; diff --git a/src/app/services/provideServices.ts b/src/app/services/provideServices.ts deleted file mode 100644 index 0d3e58b67..000000000 --- a/src/app/services/provideServices.ts +++ /dev/null @@ -1,7 +0,0 @@ -import type Bottle from 'bottlejs'; -import { AppFactory } from '../App'; - -export const provideServices = (bottle: Bottle) => { - // Components - bottle.factory('App', AppFactory); -}; diff --git a/src/common/services/provideServices.ts b/src/common/services/provideServices.ts deleted file mode 100644 index 001bbe087..000000000 --- a/src/common/services/provideServices.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { FetchHttpClient } from '@shlinkio/shlink-js-sdk/fetch'; -import type Bottle from 'bottlejs'; -import { withoutSelectedServer } from '../../servers/helpers/withoutSelectedServer'; -import { Home } from '../Home'; -import { ShlinkWebComponentContainerFactory } from '../ShlinkWebComponentContainer'; - -export const provideServices = (bottle: Bottle) => { - // Services - bottle.constant('window', window); - bottle.constant('console', console); - bottle.constant('fetch', window.fetch.bind(window)); - bottle.service('HttpClient', FetchHttpClient, 'fetch'); - - bottle.serviceFactory('Home', () => Home); - bottle.decorator('Home', withoutSelectedServer); - - bottle.factory('ShlinkWebComponentContainer', ShlinkWebComponentContainerFactory); -}; diff --git a/src/container/index.ts b/src/container/index.ts index 81b5828e4..25102bcf9 100644 --- a/src/container/index.ts +++ b/src/container/index.ts @@ -1,16 +1,56 @@ +import { useTimeoutToggle } from '@shlinkio/shlink-frontend-kit'; +import { FetchHttpClient } from '@shlinkio/shlink-js-sdk/fetch'; import Bottle from 'bottlejs'; -import { provideServices as provideApiServices } from '../api/services/provideServices'; -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 provideUtilsServices } from '../utils/services/provideServices'; +import { buildShlinkApiClient } from '../api/services/ShlinkApiClientBuilder'; +import { AppFactory } from '../app/App'; +import { Home } from '../common/Home'; +import { ShlinkWebComponentContainerFactory } from '../common/ShlinkWebComponentContainer'; +import { CreateServerFactory } from '../servers/CreateServer'; +import { ImportServersBtnFactory } from '../servers/helpers/ImportServersBtn'; +import { withoutSelectedServer } from '../servers/helpers/withoutSelectedServer'; +import { ManageServersFactory } from '../servers/ManageServers'; +import { ServersExporter } from '../servers/services/ServersExporter'; +import { ServersImporter } from '../servers/services/ServersImporter'; +import { csvToJson, jsonToCsv } from '../utils/helpers/csvjson'; +import { LocalStorage } from '../utils/services/LocalStorage'; +import { TagColorsStorage } from '../utils/services/TagColorsStorage'; const bottle = new Bottle(); export const { container } = bottle; -provideAppServices(bottle); -provideCommonServices(bottle); -provideApiServices(bottle); -provideServersServices(bottle); -provideUtilsServices(bottle); +bottle.constant('window', window); +bottle.constant('console', console); +bottle.constant('fetch', window.fetch.bind(window)); +bottle.service('HttpClient', FetchHttpClient, 'fetch'); + +bottle.constant('localStorage', window.localStorage); +bottle.service('Storage', LocalStorage, 'localStorage'); +bottle.service('TagColorsStorage', TagColorsStorage, 'Storage'); + +bottle.constant('csvToJson', csvToJson); +bottle.constant('jsonToCsv', jsonToCsv); + +bottle.serviceFactory('useTimeoutToggle', () => useTimeoutToggle); + +bottle.serviceFactory('buildShlinkApiClient', buildShlinkApiClient, 'HttpClient'); + +// Components +bottle.factory('App', AppFactory); + +bottle.serviceFactory('Home', () => Home); +bottle.decorator('Home', withoutSelectedServer); + +bottle.factory('ShlinkWebComponentContainer', ShlinkWebComponentContainerFactory); + +bottle.factory('ManageServers', ManageServersFactory); +bottle.decorator('ManageServers', withoutSelectedServer); + +bottle.factory('CreateServer', CreateServerFactory); +bottle.decorator('CreateServer', withoutSelectedServer); + +bottle.factory('ImportServersBtn', ImportServersBtnFactory); + +// Services +bottle.service('ServersImporter', ServersImporter, 'csvToJson'); +bottle.service('ServersExporter', ServersExporter, 'Storage', 'window', 'jsonToCsv'); diff --git a/src/servers/services/provideServices.ts b/src/servers/services/provideServices.ts deleted file mode 100644 index bb54bfa95..000000000 --- a/src/servers/services/provideServices.ts +++ /dev/null @@ -1,22 +0,0 @@ -import type Bottle from 'bottlejs'; -import { CreateServerFactory } from '../CreateServer'; -import { ImportServersBtnFactory } from '../helpers/ImportServersBtn'; -import { withoutSelectedServer } from '../helpers/withoutSelectedServer'; -import { ManageServersFactory } from '../ManageServers'; -import { ServersExporter } from './ServersExporter'; -import { ServersImporter } from './ServersImporter'; - -export const provideServices = (bottle: Bottle) => { - // Components - bottle.factory('ManageServers', ManageServersFactory); - bottle.decorator('ManageServers', withoutSelectedServer); - - bottle.factory('CreateServer', CreateServerFactory); - bottle.decorator('CreateServer', withoutSelectedServer); - - bottle.factory('ImportServersBtn', ImportServersBtnFactory); - - // Services - bottle.service('ServersImporter', ServersImporter, 'csvToJson'); - bottle.service('ServersExporter', ServersExporter, 'Storage', 'window', 'jsonToCsv'); -}; diff --git a/src/utils/services/provideServices.ts b/src/utils/services/provideServices.ts deleted file mode 100644 index 7c3920efb..000000000 --- a/src/utils/services/provideServices.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { useTimeoutToggle } from '@shlinkio/shlink-frontend-kit'; -import type Bottle from 'bottlejs'; -import { csvToJson, jsonToCsv } from '../helpers/csvjson'; -import { LocalStorage } from './LocalStorage'; -import { TagColorsStorage } from './TagColorsStorage'; - -export const provideServices = (bottle: Bottle) => { - bottle.constant('localStorage', window.localStorage); - bottle.service('Storage', LocalStorage, 'localStorage'); - bottle.service('TagColorsStorage', TagColorsStorage, 'Storage'); - - bottle.constant('csvToJson', csvToJson); - bottle.constant('jsonToCsv', jsonToCsv); - - bottle.serviceFactory('useTimeoutToggle', () => useTimeoutToggle); -}; From b6f1db57ee7ede2af2f7ae99a831b90fbd7a4312 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 15 Nov 2025 10:57:39 +0100 Subject: [PATCH 22/24] Add context test --- src/container/context.ts | 2 +- test/container/context.test.tsx | 39 +++++++++++++++++++++++++++++++++ vite.config.ts | 2 +- 3 files changed, 41 insertions(+), 2 deletions(-) create mode 100644 test/container/context.test.tsx diff --git a/src/container/context.ts b/src/container/context.ts index ceb22874c..f5a47d91f 100644 --- a/src/container/context.ts +++ b/src/container/context.ts @@ -8,7 +8,7 @@ export const ContainerProvider = ContainerContext.Provider; export const useDependencies = (...names: string[]): T => { const container = useContext(ContainerContext); if (!container) { - throw new Error('You cannot use "useDependency" outside of a ContainerProvider'); + throw new Error('You cannot use "useDependencies" outside of a ContainerProvider'); } return names.map((name) => { diff --git a/test/container/context.test.tsx b/test/container/context.test.tsx new file mode 100644 index 000000000..06af2159b --- /dev/null +++ b/test/container/context.test.tsx @@ -0,0 +1,39 @@ +import { render } from '@testing-library/react'; +import { fromPartial } from '@total-typescript/shoehorn'; +import { ContainerProvider, useDependencies } from '../../src/container/context'; + +describe('context', () => { + describe('useDependencies', () => { + let lastDependencies: unknown[]; + + function TestComponent({ name}: { name: string }) { + // eslint-disable-next-line react-compiler/react-compiler + lastDependencies = useDependencies(name); + return null; + } + + it('throws when used outside of ContainerProvider', () => { + expect(() => render()).toThrowError( + 'You cannot use "useDependencies" outside of a ContainerProvider', + ); + }); + + it('throws when requested dependency is not found in container', () => { + expect(() => render( + + + , + )).toThrowError('Dependency with name "foo" not found in container'); + }); + + it('gets dependency from container', () => { + render( + + + , + ); + + expect(lastDependencies).toEqual(['the dependency']); + }); + }); +}); diff --git a/vite.config.ts b/vite.config.ts index 701121423..7266de635 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -65,7 +65,7 @@ export default defineConfig({ thresholds: { statements: 95, branches: 89, // FIXME Increase to 95 again. It dropped after updating to vitest 4 - functions: 95, + functions: 93, lines: 95, }, }, From d10bea50bc56942c78d149f00111e360ce7a20b9 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 15 Nov 2025 11:46:19 +0100 Subject: [PATCH 23/24] Do not inject components into other components --- src/app/App.tsx | 28 ++------- src/common/Home.tsx | 5 +- src/common/ShlinkWebComponentContainer.tsx | 22 ++++--- src/container/context.ts | 24 -------- src/container/context.tsx | 60 +++++++++++++++++++ src/container/index.ts | 24 -------- src/container/utils.ts | 27 --------- src/index.tsx | 2 +- src/servers/CreateServer.tsx | 22 +++---- src/servers/ManageServers.tsx | 29 ++++----- src/servers/helpers/ImportServersBtn.tsx | 18 +++--- test/app/App.test.tsx | 24 ++++---- test/common/Home.test.tsx | 5 +- .../ShlinkWebComponentContainer.test.tsx | 11 ++-- test/servers/CreateServer.test.tsx | 20 +++---- test/servers/ManageServers.test.tsx | 19 +++--- .../servers/helpers/ImportServersBtn.test.tsx | 11 ++-- 17 files changed, 155 insertions(+), 196 deletions(-) delete mode 100644 src/container/context.ts create mode 100644 src/container/context.tsx delete mode 100644 src/container/utils.ts diff --git a/src/app/App.tsx b/src/app/App.tsx index 078fef99c..c295ba6c4 100644 --- a/src/app/App.tsx +++ b/src/app/App.tsx @@ -4,33 +4,22 @@ import type { FC } from 'react'; import { useEffect } from 'react'; import { Route, Routes, useLocation } from 'react-router'; import { AppUpdateBanner } from '../common/AppUpdateBanner'; +import { Home } from '../common/Home'; import { MainHeader } from '../common/MainHeader'; import { NotFound } from '../common/NotFound'; import { ShlinkVersionsContainer } from '../common/ShlinkVersionsContainer'; -import type { FCWithDeps } from '../container/utils'; -import { componentFactory, useDependencies } from '../container/utils'; +import { ShlinkWebComponentContainer } from '../common/ShlinkWebComponentContainer'; +import { CreateServer } from '../servers/CreateServer'; import { EditServer } from '../servers/EditServer'; +import { ManageServers } from '../servers/ManageServers'; import { useLoadRemoteServers } from '../servers/reducers/remoteServers'; import { useSettings } from '../settings/reducers/settings'; import { Settings } from '../settings/Settings'; import { forceUpdate } from '../utils/helpers/sw'; import { useAppUpdated } from './reducers/appUpdates'; -type AppDeps = { - Home: FC; - ShlinkWebComponentContainer: FC; - CreateServer: FC; - ManageServers: FC; -}; - -const App: FCWithDeps = () => { +export const App: FC = () => { const { appUpdated, resetAppUpdate } = useAppUpdated(); - const { - Home, - ShlinkWebComponentContainer, - CreateServer, - ManageServers, - } = useDependencies(App); useLoadRemoteServers(); @@ -80,10 +69,3 @@ const App: FCWithDeps = () => { ); }; - -export const AppFactory = componentFactory(App, [ - 'Home', - 'ShlinkWebComponentContainer', - 'CreateServer', - 'ManageServers', -]); diff --git a/src/common/Home.tsx b/src/common/Home.tsx index 94719b090..ba6288374 100644 --- a/src/common/Home.tsx +++ b/src/common/Home.tsx @@ -6,11 +6,12 @@ import type { FC } from 'react'; import { useEffect } from 'react'; import { ExternalLink } from 'react-external-link'; import { useNavigate } from 'react-router'; +import { withoutSelectedServer } from '../servers/helpers/withoutSelectedServer'; import { useServers } from '../servers/reducers/servers'; import { ServersListGroup } from '../servers/ServersListGroup'; import { ShlinkLogo } from './img/ShlinkLogo'; -export const Home: FC = () => { +export const Home: FC = withoutSelectedServer(() => { const navigate = useNavigate(); const { servers } = useServers(); const serversList = Object.values(servers); @@ -66,4 +67,4 @@ export const Home: FC = () => { ); -}; +}); diff --git a/src/common/ShlinkWebComponentContainer.tsx b/src/common/ShlinkWebComponentContainer.tsx index c343448f0..d07aba3d7 100644 --- a/src/common/ShlinkWebComponentContainer.tsx +++ b/src/common/ShlinkWebComponentContainer.tsx @@ -4,10 +4,10 @@ import { ShlinkSidebarVisibilityProvider, ShlinkWebComponent, } from '@shlinkio/shlink-web-component'; +import type { FC } from 'react'; import { memo } from 'react'; import type { ShlinkApiClientBuilder } from '../api/services/ShlinkApiClientBuilder'; -import type { FCWithDeps } from '../container/utils'; -import { componentFactory, useDependencies } from '../container/utils'; +import { withDependencies } from '../container/context'; import { isReachableServer } from '../servers/data'; import { ServerError } from '../servers/helpers/ServerError'; import { withSelectedServer } from '../servers/helpers/withSelectedServer'; @@ -15,23 +15,21 @@ import { useSelectedServer } from '../servers/reducers/selectedServer'; import { useSettings } from '../settings/reducers/settings'; import { NotFound } from './NotFound'; -type ShlinkWebComponentContainerDeps = { +export type ShlinkWebComponentContainerProps = { TagColorsStorage: TagColorsStorage; buildShlinkApiClient: ShlinkApiClientBuilder; }; -const ShlinkWebComponentContainer: FCWithDeps< - any, - ShlinkWebComponentContainerDeps +const ShlinkWebComponentContainerBase: FC< + ShlinkWebComponentContainerProps // 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 // extra rendering there. // This should be revisited at some point. -> = withSelectedServer(memo(() => { - const { - buildShlinkApiClient, - TagColorsStorage: tagColorsStorage, - } = useDependencies(ShlinkWebComponentContainer); +> = withSelectedServer(memo(({ + buildShlinkApiClient, + TagColorsStorage: tagColorsStorage, +}) => { const { selectedServer } = useSelectedServer(); const { settings } = useSettings(); @@ -58,7 +56,7 @@ const ShlinkWebComponentContainer: FCWithDeps< ); })); -export const ShlinkWebComponentContainerFactory = componentFactory(ShlinkWebComponentContainer, [ +export const ShlinkWebComponentContainer = withDependencies(ShlinkWebComponentContainerBase, [ 'buildShlinkApiClient', 'TagColorsStorage', ]); diff --git a/src/container/context.ts b/src/container/context.ts deleted file mode 100644 index f5a47d91f..000000000 --- a/src/container/context.ts +++ /dev/null @@ -1,24 +0,0 @@ -import type { IContainer } from 'bottlejs'; -import { createContext, useContext } from 'react'; - -const ContainerContext = createContext(null); - -export const ContainerProvider = ContainerContext.Provider; - -export const useDependencies = (...names: string[]): T => { - const container = useContext(ContainerContext); - if (!container) { - throw new Error('You cannot use "useDependencies" outside of a ContainerProvider'); - } - - return names.map((name) => { - const dependency = container[name]; - if (!dependency) { - throw new Error(`Dependency with name "${name}" not found in container`); - } - - return dependency; - }) as T; -}; - -// TODO Create Higher Order Component that can pull dependencies from the container diff --git a/src/container/context.tsx b/src/container/context.tsx new file mode 100644 index 000000000..a11319430 --- /dev/null +++ b/src/container/context.tsx @@ -0,0 +1,60 @@ +import type { IContainer } from 'bottlejs'; +import { type ComponentType, createContext, useContext } from 'react'; + +const ContainerContext = createContext(null); + +export const ContainerProvider = ContainerContext.Provider; + +const useContainer = (wrapperName: string): IContainer => { + const container = useContext(ContainerContext); + if (!container) { + throw new Error(`You cannot use "${wrapperName}" outside of a ContainerProvider`); + } + + return container; +}; + +/** + * Hook used to extract dependencies from the container in other hooks. + */ +export const useDependencies = (...names: string[]): T => { + const container = useContainer('useDependencies'); + + return names.map((name) => { + const dependency = container[name]; + if (!dependency) { + throw new Error(`Dependency with name "${name}" not found in container`); + } + + return dependency; + }) as T; +}; + +/** + * Higher Order Component used to inject services into components as props. + */ +export function withDependencies< + Props extends Record, + DependencyName extends string & keyof Props, +>( + Component: ComponentType, + dependencyNames: DependencyName[], +): ComponentType> { + function Wrapper(props: Omit) { + const container = useContainer('withDependencies'); + + // Inject services, unless they have been overridden by props passed from + // the parent component. + const dependencies: Partial> = {}; + for (const dependency of dependencyNames) { + if (!(dependency in props)) { + dependencies[dependency] = container[dependency]; + } + } + + const propsWithServices = { ...dependencies, ...props } as Props; + return ; + } + + return Wrapper; +} diff --git a/src/container/index.ts b/src/container/index.ts index 25102bcf9..c69ab7766 100644 --- a/src/container/index.ts +++ b/src/container/index.ts @@ -2,13 +2,6 @@ import { useTimeoutToggle } from '@shlinkio/shlink-frontend-kit'; import { FetchHttpClient } from '@shlinkio/shlink-js-sdk/fetch'; import Bottle from 'bottlejs'; import { buildShlinkApiClient } from '../api/services/ShlinkApiClientBuilder'; -import { AppFactory } from '../app/App'; -import { Home } from '../common/Home'; -import { ShlinkWebComponentContainerFactory } from '../common/ShlinkWebComponentContainer'; -import { CreateServerFactory } from '../servers/CreateServer'; -import { ImportServersBtnFactory } from '../servers/helpers/ImportServersBtn'; -import { withoutSelectedServer } from '../servers/helpers/withoutSelectedServer'; -import { ManageServersFactory } from '../servers/ManageServers'; import { ServersExporter } from '../servers/services/ServersExporter'; import { ServersImporter } from '../servers/services/ServersImporter'; import { csvToJson, jsonToCsv } from '../utils/helpers/csvjson'; @@ -35,22 +28,5 @@ bottle.serviceFactory('useTimeoutToggle', () => useTimeoutToggle); bottle.serviceFactory('buildShlinkApiClient', buildShlinkApiClient, 'HttpClient'); -// Components -bottle.factory('App', AppFactory); - -bottle.serviceFactory('Home', () => Home); -bottle.decorator('Home', withoutSelectedServer); - -bottle.factory('ShlinkWebComponentContainer', ShlinkWebComponentContainerFactory); - -bottle.factory('ManageServers', ManageServersFactory); -bottle.decorator('ManageServers', withoutSelectedServer); - -bottle.factory('CreateServer', CreateServerFactory); -bottle.decorator('CreateServer', withoutSelectedServer); - -bottle.factory('ImportServersBtn', ImportServersBtnFactory); - -// Services bottle.service('ServersImporter', ServersImporter, 'csvToJson'); bottle.service('ServersExporter', ServersExporter, 'Storage', 'window', 'jsonToCsv'); diff --git a/src/container/utils.ts b/src/container/utils.ts deleted file mode 100644 index 0da77ccb6..000000000 --- a/src/container/utils.ts +++ /dev/null @@ -1,27 +0,0 @@ -import type { IContainer } from 'bottlejs'; -import type { FC } from 'react'; -import { useMemo } from 'react'; - -export type FCWithDeps = FC & Partial; - -export function useDependencies(obj: Deps): Omit, keyof FC> { - return useMemo(() => obj as Omit, keyof FC>, [obj]); -} - -export function componentFactory, keyof FC>>( - Component: CompType, - deps: ReadonlyArray, -) { - return (container: IContainer, console = globalThis.console) => { - deps.forEach((dep) => { - const resolvedDependency = container[dep as string]; - if (!resolvedDependency && process.env.NODE_ENV !== 'production') { - console.error(`[Debug] Could not find "${dep as string}" dependency in container`); - } - - Component[dep] = resolvedDependency; - }); - - return Component; - }; -} diff --git a/src/index.tsx b/src/index.tsx index f7022a694..e3c853597 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -2,6 +2,7 @@ import { createRoot } from 'react-dom/client'; import { Provider } from 'react-redux'; import { BrowserRouter } from 'react-router'; import pack from '../package.json'; +import { App } from './app/App'; import { appUpdateAvailable } from './app/reducers/appUpdates'; import { ErrorHandler } from './common/ErrorHandler'; import { ScrollToTop } from './common/ScrollToTop'; @@ -12,7 +13,6 @@ import { setUpStore } from './store'; import './tailwind.css'; const store = setUpStore(); -const { App } = container; createRoot(document.getElementById('root')!).render( diff --git a/src/servers/CreateServer.tsx b/src/servers/CreateServer.tsx index 402b0f621..332160ce3 100644 --- a/src/servers/CreateServer.tsx +++ b/src/servers/CreateServer.tsx @@ -1,23 +1,22 @@ -import type { ResultProps,TimeoutToggle } from '@shlinkio/shlink-frontend-kit'; -import { Button, Result,useToggle } from '@shlinkio/shlink-frontend-kit'; +import type { ResultProps, TimeoutToggle } from '@shlinkio/shlink-frontend-kit'; +import { Button, Result, useToggle } from '@shlinkio/shlink-frontend-kit'; import type { FC } from 'react'; import { useCallback, useState } from 'react'; import { useNavigate } from 'react-router'; import { NoMenuLayout } from '../common/NoMenuLayout'; -import type { FCWithDeps } from '../container/utils'; -import { componentFactory, useDependencies } from '../container/utils'; +import { withDependencies } from '../container/context'; import { useGoBack } from '../utils/helpers/hooks'; import type { ServerData } from './data'; import { ensureUniqueIds } from './helpers'; import { DuplicatedServersModal } from './helpers/DuplicatedServersModal'; -import type { ImportServersBtnProps } from './helpers/ImportServersBtn'; +import { ImportServersBtn } from './helpers/ImportServersBtn'; import { ServerForm } from './helpers/ServerForm'; +import { withoutSelectedServer } from './helpers/withoutSelectedServer'; import { useServers } from './reducers/servers'; const SHOW_IMPORT_MSG_TIME = 4000; -type CreateServerDeps = { - ImportServersBtn: FC; +export type CreateServerProps = { useTimeoutToggle: TimeoutToggle; }; @@ -30,15 +29,12 @@ const ImportResult = ({ variant }: Pick) => ( ); -const CreateServer: FCWithDeps = () => { +const CreateServerBase: FC = withoutSelectedServer(({ useTimeoutToggle }) => { const { servers, createServers } = useServers(); - const { ImportServersBtn, useTimeoutToggle } = useDependencies(CreateServer); const navigate = useNavigate(); const goBack = useGoBack(); const hasServers = !!Object.keys(servers).length; - // eslint-disable-next-line react-compiler/react-compiler const [serversImported, setServersImported] = useTimeoutToggle({ delay: SHOW_IMPORT_MSG_TIME }); - // eslint-disable-next-line react-compiler/react-compiler const [errorImporting, setErrorImporting] = useTimeoutToggle({ delay: SHOW_IMPORT_MSG_TIME }); const { flag: isConfirmModalOpen, toggle: toggleConfirmModal } = useToggle(); const [serverData, setServerData] = useState(); @@ -83,6 +79,6 @@ const CreateServer: FCWithDeps = () => { /> ); -}; +}); -export const CreateServerFactory = componentFactory(CreateServer, ['ImportServersBtn', 'useTimeoutToggle']); +export const CreateServer = withDependencies(CreateServerBase, ['useTimeoutToggle']); diff --git a/src/servers/ManageServers.tsx b/src/servers/ManageServers.tsx index f07ae658f..130f405b3 100644 --- a/src/servers/ManageServers.tsx +++ b/src/servers/ManageServers.tsx @@ -5,27 +5,24 @@ import { Button, Result, SearchInput, SimpleCard, Table } from '@shlinkio/shlink import type { FC } from 'react'; import { useMemo, useState } from 'react'; import { NoMenuLayout } from '../common/NoMenuLayout'; -import type { FCWithDeps } from '../container/utils'; -import { componentFactory, useDependencies } from '../container/utils'; -import type { ImportServersBtnProps } from './helpers/ImportServersBtn'; +import { withDependencies } from '../container/context'; +import { ImportServersBtn } from './helpers/ImportServersBtn'; +import { withoutSelectedServer } from './helpers/withoutSelectedServer'; import { ManageServersRow } from './ManageServersRow'; import { useServers } from './reducers/servers'; import type { ServersExporter } from './services/ServersExporter'; -type ManageServersDeps = { +export type ManageServersProps = { ServersExporter: ServersExporter; - ImportServersBtn: FC; useTimeoutToggle: TimeoutToggle; }; const SHOW_IMPORT_MSG_TIME = 4000; -const ManageServers: FCWithDeps = () => { - const { - ServersExporter: serversExporter, - ImportServersBtn, - useTimeoutToggle, - } = useDependencies(ManageServers); +const ManageServersBase: FC = withoutSelectedServer(({ + ServersExporter: serversExporter, + useTimeoutToggle, +}) => { const { servers } = useServers(); const [searchTerm, setSearchTerm] = useState(''); const allServers = useMemo(() => Object.values(servers), [servers]); @@ -34,7 +31,7 @@ const ManageServers: FCWithDeps = () => { [allServers, searchTerm], ); const hasAutoConnect = allServers.some(({ autoConnect }) => !!autoConnect); - // eslint-disable-next-line react-compiler/react-compiler + const [errorImporting, setErrorImporting] = useTimeoutToggle({ delay: SHOW_IMPORT_MSG_TIME }); return ( @@ -82,10 +79,6 @@ const ManageServers: FCWithDeps = () => { )} ); -}; +}); -export const ManageServersFactory = componentFactory(ManageServers, [ - 'ServersExporter', - 'ImportServersBtn', - 'useTimeoutToggle', -]); +export const ManageServers = withDependencies(ManageServersBase, ['ServersExporter', 'useTimeoutToggle']); diff --git a/src/servers/helpers/ImportServersBtn.tsx b/src/servers/helpers/ImportServersBtn.tsx index 37c4b236e..3b774e5ac 100644 --- a/src/servers/helpers/ImportServersBtn.tsx +++ b/src/servers/helpers/ImportServersBtn.tsx @@ -1,10 +1,9 @@ import { faFileUpload as importIcon } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { Button, Tooltip, useToggle , useTooltip } from '@shlinkio/shlink-frontend-kit'; -import type { ChangeEvent, PropsWithChildren } from 'react'; +import { Button, Tooltip, useToggle, useTooltip } from '@shlinkio/shlink-frontend-kit'; +import type { ChangeEvent, FC, PropsWithChildren } from 'react'; import { useCallback, useRef, useState } from 'react'; -import type { FCWithDeps } from '../../container/utils'; -import { componentFactory, useDependencies } from '../../container/utils'; +import { withDependencies } from '../../container/context'; import type { ServerData } from '../data'; import { useServers } from '../reducers/servers'; import type { ServersImporter } from '../services/ServersImporter'; @@ -16,21 +15,20 @@ export type ImportServersBtnProps = PropsWithChildren<{ onError?: (error: Error) => void; tooltipPlacement?: 'top' | 'bottom'; className?: string; -}>; -type ImportServersBtnDeps = { + // Injected ServersImporter: ServersImporter -}; +}>; -const ImportServersBtn: FCWithDeps = ({ +const ImportServersBtnBase: FC = ({ children, onImport, onError = () => {}, tooltipPlacement = 'bottom', className = '', + ServersImporter: serversImporter, }) => { const { createServers, servers } = useServers(); - const { ServersImporter: serversImporter } = useDependencies(ImportServersBtn); const fileInputRef = useRef(null); const { anchor, tooltip } = useTooltip({ placement: tooltipPlacement }); const [duplicatedServers, setDuplicatedServers] = useState([]); @@ -106,4 +104,4 @@ const ImportServersBtn: FCWithDeps ); }; -export const ImportServersBtnFactory = componentFactory(ImportServersBtn, ['ServersImporter']); +export const ImportServersBtn = withDependencies(ImportServersBtnBase, ['ServersImporter']); diff --git a/test/app/App.test.tsx b/test/app/App.test.tsx index 694d12a9e..691f7de76 100644 --- a/test/app/App.test.tsx +++ b/test/app/App.test.tsx @@ -2,25 +2,25 @@ import type { HttpClient } from '@shlinkio/shlink-js-sdk'; 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 { App } from '../../src/app/App'; import { ContainerProvider } from '../../src/container/context'; import type { ServerWithId } from '../../src/servers/data'; import { checkAccessibility } from '../__helpers__/accessibility'; import { renderWithStore } from '../__helpers__/setUpTest'; +vi.mock(import('../../src/common/ShlinkWebComponentContainer'), () => ({ + ShlinkWebComponentContainer: () => ShlinkWebComponentContainer, +})); + describe('', () => { - const App = AppFactory( - fromPartial({ - Home: () => <>Home, - ShlinkWebComponentContainer: () => <>ShlinkWebComponentContainer, - CreateServer: () => <>CreateServer, - ManageServers: () => <>ManageServers, - }), - ); const setUp = async (activeRoute = '/') => act(() => renderWithStore( ({}), buildShlinkApiClient: vi.fn() })} + value={fromPartial({ + HttpClient: fromPartial({}), + buildShlinkApiClient: vi.fn(), + useTimeoutToggle: vi.fn().mockReturnValue([false, vi.fn()]), + })} > @@ -42,8 +42,8 @@ describe('', () => { it.each([ ['/settings/general', 'User interface'], ['/settings/short-urls', 'Short URLs form'], - ['/manage-servers', 'ManageServers'], - ['/server/create', 'CreateServer'], + ['/manage-servers', 'Add a server'], + ['/server/create', 'Add new server'], ['/server/abc123/edit', 'Edit "abc123 server"'], ['/server/def456/edit', 'Edit "def456 server"'], ['/server/abc123/foo', 'ShlinkWebComponentContainer'], diff --git a/test/common/Home.test.tsx b/test/common/Home.test.tsx index 84b36a5f2..e9521d201 100644 --- a/test/common/Home.test.tsx +++ b/test/common/Home.test.tsx @@ -2,6 +2,7 @@ import { screen } from '@testing-library/react'; import { fromPartial } from '@total-typescript/shoehorn'; import { MemoryRouter } from 'react-router'; import { Home } from '../../src/common/Home'; +import { ContainerProvider } from '../../src/container/context'; import type { ServersMap, ServerWithId } from '../../src/servers/data'; import { checkAccessibility } from '../__helpers__/accessibility'; import { renderWithStore } from '../__helpers__/setUpTest'; @@ -9,7 +10,9 @@ import { renderWithStore } from '../__helpers__/setUpTest'; describe('', () => { const setUp = (servers: ServersMap = {}) => renderWithStore( - + + + , { initialState: { servers }, diff --git a/test/common/ShlinkWebComponentContainer.test.tsx b/test/common/ShlinkWebComponentContainer.test.tsx index 0efca1696..fb31f69d0 100644 --- a/test/common/ShlinkWebComponentContainer.test.tsx +++ b/test/common/ShlinkWebComponentContainer.test.tsx @@ -1,7 +1,7 @@ import { screen } from '@testing-library/react'; import { fromPartial } from '@total-typescript/shoehorn'; import { MemoryRouter } from 'react-router'; -import { ShlinkWebComponentContainerFactory } from '../../src/common/ShlinkWebComponentContainer'; +import { ShlinkWebComponentContainer } from '../../src/common/ShlinkWebComponentContainer'; import { ContainerProvider } from '../../src/container/context'; import type { NonReachableServer, NotFoundServer, SelectedServer } from '../../src/servers/data'; import { checkAccessibility } from '../__helpers__/accessibility'; @@ -14,13 +14,12 @@ vi.mock('@shlinkio/shlink-web-component', () => ({ })); describe('', () => { - const ShlinkWebComponentContainer = ShlinkWebComponentContainerFactory(fromPartial({ - buildShlinkApiClient: vi.fn().mockReturnValue(fromPartial({})), - TagColorsStorage: fromPartial({}), - })); const setUp = (selectedServer: SelectedServer) => renderWithStore( - + , diff --git a/test/servers/CreateServer.test.tsx b/test/servers/CreateServer.test.tsx index 05375b48b..2905b783d 100644 --- a/test/servers/CreateServer.test.tsx +++ b/test/servers/CreateServer.test.tsx @@ -2,7 +2,8 @@ import { fireEvent, screen, waitFor } from '@testing-library/react'; import { fromPartial } from '@total-typescript/shoehorn'; import { createMemoryHistory } from 'history'; import { Router } from 'react-router'; -import { CreateServerFactory } from '../../src/servers/CreateServer'; +import { ContainerProvider } from '../../src/container/context'; +import { CreateServer } from '../../src/servers/CreateServer'; import type { ServersMap } from '../../src/servers/data'; import { checkAccessibility } from '../__helpers__/accessibility'; import { renderWithStore } from '../__helpers__/setUpTest'; @@ -24,17 +25,19 @@ describe('', () => { callCount += 1; return result; }); - const CreateServer = CreateServerFactory(fromPartial({ - ImportServersBtn: () => <>ImportServersBtn, - useTimeoutToggle, - })); const history = createMemoryHistory({ initialEntries: ['/foo', '/bar'] }); return { history, ...renderWithStore( - + <>ImportServersBtn, + useTimeoutToggle, + buildShlinkApiClient: vi.fn(), + })}> + + , { initialState: { servers }, @@ -64,11 +67,6 @@ describe('', () => { expect(screen.getByText('The servers could not be imported. Make sure the format is correct.')).toBeInTheDocument(); }); - it('shows import button when no servers exist yet', () => { - setUp({ servers: {} }); - expect(screen.queryByText('ImportServersBtn')).toBeInTheDocument(); - }); - it('creates server data when form is submitted', async () => { const { user, history, store } = setUp(); const expectedServerId = 'the_name-the_url.com'; diff --git a/test/servers/ManageServers.test.tsx b/test/servers/ManageServers.test.tsx index c3cebeab8..f9395816b 100644 --- a/test/servers/ManageServers.test.tsx +++ b/test/servers/ManageServers.test.tsx @@ -1,8 +1,9 @@ import { screen, waitFor } from '@testing-library/react'; import { fromPartial } from '@total-typescript/shoehorn'; import { MemoryRouter } from 'react-router'; +import { ContainerProvider } from '../../src/container/context'; import type { ServersMap, ServerWithId } from '../../src/servers/data'; -import { ManageServersFactory } from '../../src/servers/ManageServers'; +import { ManageServers } from '../../src/servers/ManageServers'; import type { ServersExporter } from '../../src/servers/services/ServersExporter'; import { checkAccessibility } from '../__helpers__/accessibility'; import { renderWithStore } from '../__helpers__/setUpTest'; @@ -11,16 +12,20 @@ describe('', () => { const exportServers = vi.fn(); const serversExporter = fromPartial({ exportServers }); const useTimeoutToggle = vi.fn().mockReturnValue([false, vi.fn()]); - const ManageServers = ManageServersFactory(fromPartial({ - ServersExporter: serversExporter, - ImportServersBtn: () => ImportServersBtn, - useTimeoutToggle, - })); const createServerMock = (value: string, autoConnect = false) => fromPartial( { id: value, name: value, url: value, autoConnect }, ); const setUp = (servers: ServersMap = {}) => renderWithStore( - , + + ImportServersBtn, + useTimeoutToggle, + buildShlinkApiClient: vi.fn(), + })}> + + + , { initialState: { servers }, }, diff --git a/test/servers/helpers/ImportServersBtn.test.tsx b/test/servers/helpers/ImportServersBtn.test.tsx index 06be5014f..aa3a5cdd2 100644 --- a/test/servers/helpers/ImportServersBtn.test.tsx +++ b/test/servers/helpers/ImportServersBtn.test.tsx @@ -1,9 +1,9 @@ import { screen, waitFor } from '@testing-library/react'; import { fromPartial } from '@total-typescript/shoehorn'; +import { ContainerProvider } from '../../../src/container/context'; import type { ServerData, ServersMap, ServerWithId } from '../../../src/servers/data'; -import type { - ImportServersBtnProps } from '../../../src/servers/helpers/ImportServersBtn'; -import { ImportServersBtnFactory } from '../../../src/servers/helpers/ImportServersBtn'; +import type { ImportServersBtnProps } from '../../../src/servers/helpers/ImportServersBtn'; +import { ImportServersBtn } from '../../../src/servers/helpers/ImportServersBtn'; import type { ServersImporter } from '../../../src/servers/services/ServersImporter'; import { checkAccessibility } from '../../__helpers__/accessibility'; import { renderWithStore } from '../../__helpers__/setUpTest'; @@ -13,9 +13,10 @@ describe('', () => { const onImportMock = vi.fn(); const importServersFromFile = vi.fn().mockResolvedValue([]); const serversImporterMock = fromPartial({ importServersFromFile }); - const ImportServersBtn = ImportServersBtnFactory(fromPartial({ ServersImporter: serversImporterMock })); const setUp = (props: Partial = {}, servers: ServersMap = {}) => renderWithStore( - , + + + , { initialState: { servers }, }, From 9080dde565d0a9b071ce4d9cbbae0d7537661991 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 15 Nov 2025 12:10:38 +0100 Subject: [PATCH 24/24] Add v4.6.1 to changelog --- CHANGELOG.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a8dbdf68..11e16c3f4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,24 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org). +## [4.6.1] - 2025-11-15 +### Added +* *Nothing* + +### Changed +* [#802](https://github.com/shlinkio/shlink-web-client/issues/802) Improve dependency injection in components. +* Stop injecting redux state and actions. + +### Deprecated +* *Nothing* + +### Removed +* *Nothing* + +### Fixed +* Fix small UI issues. + + ## [4.6.0] - 2025-11-12 ### Added * [shlink-web-component#839](https://github.com/shlinkio/shlink-web-component/issues/839) Allow filtering short URLs by excluded tags when using Shlink >=4.6.0
      - - ManageServersRowDropdown - +
      + +
      - - ManageServersRowDropdown - +
      + +