diff --git a/src/app/App.tsx b/src/app/App.tsx index 5c080613..02bda524 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,29 +27,22 @@ 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); - const location = useLocation(); - const initialServers = useRef(servers); - const isHome = location.pathname === '/'; + useLoadRemoteServers(httpClient); - 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]); + const location = useLocation(); + const isHome = location.pathname === '/'; useEffect(() => { changeThemeInMarkup(settings.ui?.theme ?? getSystemPreferredTheme()); @@ -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 ab975037..9314dca1 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 d7619e4b..163b7c8f 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 b921245e..bb54bfa9 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 7717106d..200f131d 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 3c0cf580..ca8eb9f2 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 }));