mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2025-12-23 18:50:08 +00:00
Expose container via provider
This commit is contained in:
parent
6094994cfa
commit
f301513f5b
@ -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<AppProps, AppDeps> = ({ appUpdated, resetAppUpdate }) => {
|
||||
@ -35,10 +33,9 @@ const App: FCWithDeps<AppProps, AppDeps> = ({ 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',
|
||||
]);
|
||||
|
||||
@ -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<
|
||||
|
||||
24
src/container/context.ts
Normal file
24
src/container/context.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import type { IContainer } from 'bottlejs';
|
||||
import { createContext, useContext } from 'react';
|
||||
|
||||
const ContainerContext = createContext<IContainer | null>(null);
|
||||
|
||||
export const ContainerProvider = ContainerContext.Provider;
|
||||
|
||||
export const useDependencies = <T extends unknown[]>(...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
|
||||
@ -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(
|
||||
<Provider store={store}>
|
||||
<BrowserRouter basename={pack.homepage}>
|
||||
<ErrorHandler>
|
||||
<ScrollToTop>
|
||||
<App />
|
||||
</ScrollToTop>
|
||||
</ErrorHandler>
|
||||
</BrowserRouter>
|
||||
</Provider>,
|
||||
<ContainerProvider value={container}>
|
||||
<Provider store={store}>
|
||||
<BrowserRouter basename={pack.homepage}>
|
||||
<ErrorHandler>
|
||||
<ScrollToTop>
|
||||
<App />
|
||||
</ScrollToTop>
|
||||
</ErrorHandler>
|
||||
</BrowserRouter>
|
||||
</Provider>
|
||||
</ContainerProvider>,
|
||||
);
|
||||
|
||||
// Learn more about service workers: https://cra.link/PWA
|
||||
|
||||
@ -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<any, WithSelectedServerPropsDeps> = 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<any, WithSelectedServerPropsDeps> = withSele
|
||||
const handleSubmit = (serverData: ServerData) => {
|
||||
editServer(selectedServer.id, serverData);
|
||||
if (reconnect === 'true') {
|
||||
selectServer({ serverId: selectedServer.id, buildShlinkApiClient });
|
||||
selectServer(selectedServer.id);
|
||||
}
|
||||
goBack();
|
||||
};
|
||||
|
||||
@ -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<T extends object>(
|
||||
WrappedComponent: FCWithDeps<T, WithSelectedServerPropsDeps>,
|
||||
) {
|
||||
const ComponentWrapper: FCWithDeps<T, WithSelectedServerPropsDeps> = (props) => {
|
||||
const { buildShlinkApiClient } = useDependencies(ComponentWrapper);
|
||||
export function withSelectedServer<T extends object>(WrappedComponent: FC<T>) {
|
||||
const ComponentWrapper: FC<T> = (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 (
|
||||
|
||||
@ -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]);
|
||||
};
|
||||
|
||||
@ -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);
|
||||
|
||||
|
||||
@ -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('<App />', () => {
|
||||
ShlinkWebComponentContainer: () => <>ShlinkWebComponentContainer</>,
|
||||
CreateServer: () => <>CreateServer</>,
|
||||
ManageServers: () => <>ManageServers</>,
|
||||
HttpClient: fromPartial<HttpClient>({}),
|
||||
}),
|
||||
);
|
||||
const setUp = async (activeRoute = '/') => act(() => renderWithStore(
|
||||
<MemoryRouter initialEntries={[{ pathname: activeRoute }]}>
|
||||
<App appUpdated={false} resetAppUpdate={() => {}} />
|
||||
<ContainerProvider
|
||||
value={fromPartial({ HttpClient: fromPartial<HttpClient>({}), buildShlinkApiClient: vi.fn() })}
|
||||
>
|
||||
<App appUpdated={false} resetAppUpdate={() => {}} />
|
||||
</ContainerProvider>
|
||||
</MemoryRouter>,
|
||||
{
|
||||
initialState: {
|
||||
|
||||
@ -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('<MainHeader />', () => {
|
||||
|
||||
return renderWithStore(
|
||||
<Router location={history.location} navigator={history}>
|
||||
<MainHeader />
|
||||
<ContainerProvider value={fromPartial({ buildShlinkApiClient: vi.fn() })}>
|
||||
<MainHeader />
|
||||
</ContainerProvider>
|
||||
</Router>,
|
||||
);
|
||||
};
|
||||
|
||||
@ -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('<ShlinkVersionsContainer />', () => {
|
||||
const setUp = (selectedServer: SelectedServer = null) => renderWithStore(
|
||||
<ShlinkVersionsContainer />,
|
||||
<ContainerProvider value={fromPartial({ buildShlinkApiClient: vi.fn() })}>
|
||||
<ShlinkVersionsContainer />
|
||||
</ContainerProvider>,
|
||||
{
|
||||
initialState: { selectedServer },
|
||||
},
|
||||
|
||||
@ -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('<ShlinkWebComponentContainer />', () => {
|
||||
}));
|
||||
const setUp = (selectedServer: SelectedServer) => renderWithStore(
|
||||
<MemoryRouter>
|
||||
<ShlinkWebComponentContainer />
|
||||
<ContainerProvider value={fromPartial({ buildShlinkApiClient: vi.fn() })}>
|
||||
<ShlinkWebComponentContainer />
|
||||
</ContainerProvider>
|
||||
</MemoryRouter>,
|
||||
{
|
||||
initialState: { selectedServer, servers: {}, settings: {} },
|
||||
|
||||
@ -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('<EditServer />', () => {
|
||||
history,
|
||||
...renderWithStore(
|
||||
<Router location={history.location} navigator={history}>
|
||||
<EditServer />
|
||||
<ContainerProvider value={fromPartial({ buildShlinkApiClient: vi.fn() })}>
|
||||
<EditServer />
|
||||
</ContainerProvider>
|
||||
</Router>,
|
||||
{
|
||||
initialState: {
|
||||
|
||||
@ -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('<ServersDropdown />', () => {
|
||||
};
|
||||
const setUp = (servers: ServersMap = fallbackServers) => renderWithStore(
|
||||
<MemoryRouter>
|
||||
<ul role="menu">
|
||||
<ServersDropdown />
|
||||
</ul>
|
||||
<ContainerProvider value={fromPartial({ buildShlinkApiClient: vi.fn() })}>
|
||||
<ul role="menu">
|
||||
<ServersDropdown />
|
||||
</ul>
|
||||
</ContainerProvider>
|
||||
</MemoryRouter>,
|
||||
{
|
||||
initialState: { selectedServer: null, servers },
|
||||
|
||||
@ -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('<ServerError />', () => {
|
||||
const setUp = (selectedServer: SelectedServer) => renderWithStore(
|
||||
<MemoryRouter>
|
||||
<ServerError />
|
||||
<ContainerProvider value={fromPartial({ buildShlinkApiClient: vi.fn() })}>
|
||||
<ServerError />
|
||||
</ContainerProvider>
|
||||
</MemoryRouter>,
|
||||
{
|
||||
initialState: { selectedServer, servers: {} },
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user