mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2025-12-23 18:50:08 +00:00
Do not inject components into other components
This commit is contained in:
parent
dad3990c23
commit
d10bea50bc
@ -4,33 +4,22 @@ import type { FC } from 'react';
|
|||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { Route, Routes, useLocation } from 'react-router';
|
import { Route, Routes, useLocation } from 'react-router';
|
||||||
import { AppUpdateBanner } from '../common/AppUpdateBanner';
|
import { AppUpdateBanner } from '../common/AppUpdateBanner';
|
||||||
|
import { Home } from '../common/Home';
|
||||||
import { MainHeader } from '../common/MainHeader';
|
import { MainHeader } from '../common/MainHeader';
|
||||||
import { NotFound } from '../common/NotFound';
|
import { NotFound } from '../common/NotFound';
|
||||||
import { ShlinkVersionsContainer } from '../common/ShlinkVersionsContainer';
|
import { ShlinkVersionsContainer } from '../common/ShlinkVersionsContainer';
|
||||||
import type { FCWithDeps } from '../container/utils';
|
import { ShlinkWebComponentContainer } from '../common/ShlinkWebComponentContainer';
|
||||||
import { componentFactory, useDependencies } from '../container/utils';
|
import { CreateServer } from '../servers/CreateServer';
|
||||||
import { EditServer } from '../servers/EditServer';
|
import { EditServer } from '../servers/EditServer';
|
||||||
|
import { ManageServers } from '../servers/ManageServers';
|
||||||
import { useLoadRemoteServers } from '../servers/reducers/remoteServers';
|
import { useLoadRemoteServers } from '../servers/reducers/remoteServers';
|
||||||
import { useSettings } from '../settings/reducers/settings';
|
import { useSettings } from '../settings/reducers/settings';
|
||||||
import { Settings } from '../settings/Settings';
|
import { Settings } from '../settings/Settings';
|
||||||
import { forceUpdate } from '../utils/helpers/sw';
|
import { forceUpdate } from '../utils/helpers/sw';
|
||||||
import { useAppUpdated } from './reducers/appUpdates';
|
import { useAppUpdated } from './reducers/appUpdates';
|
||||||
|
|
||||||
type AppDeps = {
|
export const App: FC = () => {
|
||||||
Home: FC;
|
|
||||||
ShlinkWebComponentContainer: FC;
|
|
||||||
CreateServer: FC;
|
|
||||||
ManageServers: FC;
|
|
||||||
};
|
|
||||||
|
|
||||||
const App: FCWithDeps<any, AppDeps> = () => {
|
|
||||||
const { appUpdated, resetAppUpdate } = useAppUpdated();
|
const { appUpdated, resetAppUpdate } = useAppUpdated();
|
||||||
const {
|
|
||||||
Home,
|
|
||||||
ShlinkWebComponentContainer,
|
|
||||||
CreateServer,
|
|
||||||
ManageServers,
|
|
||||||
} = useDependencies(App);
|
|
||||||
|
|
||||||
useLoadRemoteServers();
|
useLoadRemoteServers();
|
||||||
|
|
||||||
@ -80,10 +69,3 @@ const App: FCWithDeps<any, AppDeps> = () => {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const AppFactory = componentFactory(App, [
|
|
||||||
'Home',
|
|
||||||
'ShlinkWebComponentContainer',
|
|
||||||
'CreateServer',
|
|
||||||
'ManageServers',
|
|
||||||
]);
|
|
||||||
|
|||||||
@ -6,11 +6,12 @@ import type { FC } from 'react';
|
|||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { ExternalLink } from 'react-external-link';
|
import { ExternalLink } from 'react-external-link';
|
||||||
import { useNavigate } from 'react-router';
|
import { useNavigate } from 'react-router';
|
||||||
|
import { withoutSelectedServer } from '../servers/helpers/withoutSelectedServer';
|
||||||
import { useServers } from '../servers/reducers/servers';
|
import { useServers } from '../servers/reducers/servers';
|
||||||
import { ServersListGroup } from '../servers/ServersListGroup';
|
import { ServersListGroup } from '../servers/ServersListGroup';
|
||||||
import { ShlinkLogo } from './img/ShlinkLogo';
|
import { ShlinkLogo } from './img/ShlinkLogo';
|
||||||
|
|
||||||
export const Home: FC = () => {
|
export const Home: FC = withoutSelectedServer(() => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { servers } = useServers();
|
const { servers } = useServers();
|
||||||
const serversList = Object.values(servers);
|
const serversList = Object.values(servers);
|
||||||
@ -66,4 +67,4 @@ export const Home: FC = () => {
|
|||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|||||||
@ -4,10 +4,10 @@ import {
|
|||||||
ShlinkSidebarVisibilityProvider,
|
ShlinkSidebarVisibilityProvider,
|
||||||
ShlinkWebComponent,
|
ShlinkWebComponent,
|
||||||
} from '@shlinkio/shlink-web-component';
|
} from '@shlinkio/shlink-web-component';
|
||||||
|
import type { FC } from 'react';
|
||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
import type { ShlinkApiClientBuilder } from '../api/services/ShlinkApiClientBuilder';
|
import type { ShlinkApiClientBuilder } from '../api/services/ShlinkApiClientBuilder';
|
||||||
import type { FCWithDeps } from '../container/utils';
|
import { withDependencies } from '../container/context';
|
||||||
import { componentFactory, useDependencies } from '../container/utils';
|
|
||||||
import { isReachableServer } from '../servers/data';
|
import { isReachableServer } from '../servers/data';
|
||||||
import { ServerError } from '../servers/helpers/ServerError';
|
import { ServerError } from '../servers/helpers/ServerError';
|
||||||
import { withSelectedServer } from '../servers/helpers/withSelectedServer';
|
import { withSelectedServer } from '../servers/helpers/withSelectedServer';
|
||||||
@ -15,23 +15,21 @@ import { useSelectedServer } from '../servers/reducers/selectedServer';
|
|||||||
import { useSettings } from '../settings/reducers/settings';
|
import { useSettings } from '../settings/reducers/settings';
|
||||||
import { NotFound } from './NotFound';
|
import { NotFound } from './NotFound';
|
||||||
|
|
||||||
type ShlinkWebComponentContainerDeps = {
|
export type ShlinkWebComponentContainerProps = {
|
||||||
TagColorsStorage: TagColorsStorage;
|
TagColorsStorage: TagColorsStorage;
|
||||||
buildShlinkApiClient: ShlinkApiClientBuilder;
|
buildShlinkApiClient: ShlinkApiClientBuilder;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ShlinkWebComponentContainer: FCWithDeps<
|
const ShlinkWebComponentContainerBase: FC<
|
||||||
any,
|
ShlinkWebComponentContainerProps
|
||||||
ShlinkWebComponentContainerDeps
|
|
||||||
// FIXME Using `memo` here to solve a flickering effect in charts.
|
// 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
|
// memo is probably not the right solution. The root cause is the withSelectedServer HOC, but I couldn't fix the
|
||||||
// extra rendering there.
|
// extra rendering there.
|
||||||
// This should be revisited at some point.
|
// This should be revisited at some point.
|
||||||
> = withSelectedServer(memo(() => {
|
> = withSelectedServer(memo(({
|
||||||
const {
|
buildShlinkApiClient,
|
||||||
buildShlinkApiClient,
|
TagColorsStorage: tagColorsStorage,
|
||||||
TagColorsStorage: tagColorsStorage,
|
}) => {
|
||||||
} = useDependencies(ShlinkWebComponentContainer);
|
|
||||||
const { selectedServer } = useSelectedServer();
|
const { selectedServer } = useSelectedServer();
|
||||||
const { settings } = useSettings();
|
const { settings } = useSettings();
|
||||||
|
|
||||||
@ -58,7 +56,7 @@ const ShlinkWebComponentContainer: FCWithDeps<
|
|||||||
);
|
);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const ShlinkWebComponentContainerFactory = componentFactory(ShlinkWebComponentContainer, [
|
export const ShlinkWebComponentContainer = withDependencies(ShlinkWebComponentContainerBase, [
|
||||||
'buildShlinkApiClient',
|
'buildShlinkApiClient',
|
||||||
'TagColorsStorage',
|
'TagColorsStorage',
|
||||||
]);
|
]);
|
||||||
|
|||||||
@ -1,24 +0,0 @@
|
|||||||
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 "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
|
|
||||||
60
src/container/context.tsx
Normal file
60
src/container/context.tsx
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import type { IContainer } from 'bottlejs';
|
||||||
|
import { type ComponentType, createContext, useContext } from 'react';
|
||||||
|
|
||||||
|
const ContainerContext = createContext<IContainer | null>(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 = <T extends unknown[]>(...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<string, unknown>,
|
||||||
|
DependencyName extends string & keyof Props,
|
||||||
|
>(
|
||||||
|
Component: ComponentType<Props>,
|
||||||
|
dependencyNames: DependencyName[],
|
||||||
|
): ComponentType<Omit<Props, DependencyName>> {
|
||||||
|
function Wrapper(props: Omit<Props, DependencyName>) {
|
||||||
|
const container = useContainer('withDependencies');
|
||||||
|
|
||||||
|
// Inject services, unless they have been overridden by props passed from
|
||||||
|
// the parent component.
|
||||||
|
const dependencies: Partial<Record<DependencyName, unknown>> = {};
|
||||||
|
for (const dependency of dependencyNames) {
|
||||||
|
if (!(dependency in props)) {
|
||||||
|
dependencies[dependency] = container[dependency];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const propsWithServices = { ...dependencies, ...props } as Props;
|
||||||
|
return <Component {...propsWithServices} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Wrapper;
|
||||||
|
}
|
||||||
@ -2,13 +2,6 @@ import { useTimeoutToggle } from '@shlinkio/shlink-frontend-kit';
|
|||||||
import { FetchHttpClient } from '@shlinkio/shlink-js-sdk/fetch';
|
import { FetchHttpClient } from '@shlinkio/shlink-js-sdk/fetch';
|
||||||
import Bottle from 'bottlejs';
|
import Bottle from 'bottlejs';
|
||||||
import { buildShlinkApiClient } from '../api/services/ShlinkApiClientBuilder';
|
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 { ServersExporter } from '../servers/services/ServersExporter';
|
||||||
import { ServersImporter } from '../servers/services/ServersImporter';
|
import { ServersImporter } from '../servers/services/ServersImporter';
|
||||||
import { csvToJson, jsonToCsv } from '../utils/helpers/csvjson';
|
import { csvToJson, jsonToCsv } from '../utils/helpers/csvjson';
|
||||||
@ -35,22 +28,5 @@ bottle.serviceFactory('useTimeoutToggle', () => useTimeoutToggle);
|
|||||||
|
|
||||||
bottle.serviceFactory('buildShlinkApiClient', buildShlinkApiClient, 'HttpClient');
|
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('ServersImporter', ServersImporter, 'csvToJson');
|
||||||
bottle.service('ServersExporter', ServersExporter, 'Storage', 'window', 'jsonToCsv');
|
bottle.service('ServersExporter', ServersExporter, 'Storage', 'window', 'jsonToCsv');
|
||||||
|
|||||||
@ -1,27 +0,0 @@
|
|||||||
import type { IContainer } from 'bottlejs';
|
|
||||||
import type { FC } from 'react';
|
|
||||||
import { useMemo } from 'react';
|
|
||||||
|
|
||||||
export type FCWithDeps<Props, Deps> = FC<Props> & Partial<Deps>;
|
|
||||||
|
|
||||||
export function useDependencies<Deps>(obj: Deps): Omit<Required<Deps>, keyof FC> {
|
|
||||||
return useMemo(() => obj as Omit<Required<Deps>, keyof FC>, [obj]);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function componentFactory<Deps, CompType = Omit<Partial<Deps>, keyof FC>>(
|
|
||||||
Component: CompType,
|
|
||||||
deps: ReadonlyArray<keyof CompType>,
|
|
||||||
) {
|
|
||||||
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;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@ -2,6 +2,7 @@ import { createRoot } from 'react-dom/client';
|
|||||||
import { Provider } from 'react-redux';
|
import { Provider } from 'react-redux';
|
||||||
import { BrowserRouter } from 'react-router';
|
import { BrowserRouter } from 'react-router';
|
||||||
import pack from '../package.json';
|
import pack from '../package.json';
|
||||||
|
import { App } from './app/App';
|
||||||
import { appUpdateAvailable } from './app/reducers/appUpdates';
|
import { appUpdateAvailable } from './app/reducers/appUpdates';
|
||||||
import { ErrorHandler } from './common/ErrorHandler';
|
import { ErrorHandler } from './common/ErrorHandler';
|
||||||
import { ScrollToTop } from './common/ScrollToTop';
|
import { ScrollToTop } from './common/ScrollToTop';
|
||||||
@ -12,7 +13,6 @@ import { setUpStore } from './store';
|
|||||||
import './tailwind.css';
|
import './tailwind.css';
|
||||||
|
|
||||||
const store = setUpStore();
|
const store = setUpStore();
|
||||||
const { App } = container;
|
|
||||||
|
|
||||||
createRoot(document.getElementById('root')!).render(
|
createRoot(document.getElementById('root')!).render(
|
||||||
<ContainerProvider value={container}>
|
<ContainerProvider value={container}>
|
||||||
|
|||||||
@ -1,23 +1,22 @@
|
|||||||
import type { ResultProps,TimeoutToggle } from '@shlinkio/shlink-frontend-kit';
|
import type { ResultProps, TimeoutToggle } from '@shlinkio/shlink-frontend-kit';
|
||||||
import { Button, Result,useToggle } from '@shlinkio/shlink-frontend-kit';
|
import { Button, Result, useToggle } from '@shlinkio/shlink-frontend-kit';
|
||||||
import type { FC } from 'react';
|
import type { FC } from 'react';
|
||||||
import { useCallback, useState } from 'react';
|
import { useCallback, useState } from 'react';
|
||||||
import { useNavigate } from 'react-router';
|
import { useNavigate } from 'react-router';
|
||||||
import { NoMenuLayout } from '../common/NoMenuLayout';
|
import { NoMenuLayout } from '../common/NoMenuLayout';
|
||||||
import type { FCWithDeps } from '../container/utils';
|
import { withDependencies } from '../container/context';
|
||||||
import { componentFactory, useDependencies } from '../container/utils';
|
|
||||||
import { useGoBack } from '../utils/helpers/hooks';
|
import { useGoBack } from '../utils/helpers/hooks';
|
||||||
import type { ServerData } from './data';
|
import type { ServerData } from './data';
|
||||||
import { ensureUniqueIds } from './helpers';
|
import { ensureUniqueIds } from './helpers';
|
||||||
import { DuplicatedServersModal } from './helpers/DuplicatedServersModal';
|
import { DuplicatedServersModal } from './helpers/DuplicatedServersModal';
|
||||||
import type { ImportServersBtnProps } from './helpers/ImportServersBtn';
|
import { ImportServersBtn } from './helpers/ImportServersBtn';
|
||||||
import { ServerForm } from './helpers/ServerForm';
|
import { ServerForm } from './helpers/ServerForm';
|
||||||
|
import { withoutSelectedServer } from './helpers/withoutSelectedServer';
|
||||||
import { useServers } from './reducers/servers';
|
import { useServers } from './reducers/servers';
|
||||||
|
|
||||||
const SHOW_IMPORT_MSG_TIME = 4000;
|
const SHOW_IMPORT_MSG_TIME = 4000;
|
||||||
|
|
||||||
type CreateServerDeps = {
|
export type CreateServerProps = {
|
||||||
ImportServersBtn: FC<ImportServersBtnProps>;
|
|
||||||
useTimeoutToggle: TimeoutToggle;
|
useTimeoutToggle: TimeoutToggle;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -30,15 +29,12 @@ const ImportResult = ({ variant }: Pick<ResultProps, 'variant'>) => (
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
const CreateServer: FCWithDeps<any, CreateServerDeps> = () => {
|
const CreateServerBase: FC<CreateServerProps> = withoutSelectedServer(({ useTimeoutToggle }) => {
|
||||||
const { servers, createServers } = useServers();
|
const { servers, createServers } = useServers();
|
||||||
const { ImportServersBtn, useTimeoutToggle } = useDependencies(CreateServer);
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const goBack = useGoBack();
|
const goBack = useGoBack();
|
||||||
const hasServers = !!Object.keys(servers).length;
|
const hasServers = !!Object.keys(servers).length;
|
||||||
// eslint-disable-next-line react-compiler/react-compiler
|
|
||||||
const [serversImported, setServersImported] = useTimeoutToggle({ delay: SHOW_IMPORT_MSG_TIME });
|
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 [errorImporting, setErrorImporting] = useTimeoutToggle({ delay: SHOW_IMPORT_MSG_TIME });
|
||||||
const { flag: isConfirmModalOpen, toggle: toggleConfirmModal } = useToggle();
|
const { flag: isConfirmModalOpen, toggle: toggleConfirmModal } = useToggle();
|
||||||
const [serverData, setServerData] = useState<ServerData>();
|
const [serverData, setServerData] = useState<ServerData>();
|
||||||
@ -83,6 +79,6 @@ const CreateServer: FCWithDeps<any, CreateServerDeps> = () => {
|
|||||||
/>
|
/>
|
||||||
</NoMenuLayout>
|
</NoMenuLayout>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|
||||||
export const CreateServerFactory = componentFactory(CreateServer, ['ImportServersBtn', 'useTimeoutToggle']);
|
export const CreateServer = withDependencies(CreateServerBase, ['useTimeoutToggle']);
|
||||||
|
|||||||
@ -5,27 +5,24 @@ import { Button, Result, SearchInput, SimpleCard, Table } from '@shlinkio/shlink
|
|||||||
import type { FC } from 'react';
|
import type { FC } from 'react';
|
||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
import { NoMenuLayout } from '../common/NoMenuLayout';
|
import { NoMenuLayout } from '../common/NoMenuLayout';
|
||||||
import type { FCWithDeps } from '../container/utils';
|
import { withDependencies } from '../container/context';
|
||||||
import { componentFactory, useDependencies } from '../container/utils';
|
import { ImportServersBtn } from './helpers/ImportServersBtn';
|
||||||
import type { ImportServersBtnProps } from './helpers/ImportServersBtn';
|
import { withoutSelectedServer } from './helpers/withoutSelectedServer';
|
||||||
import { ManageServersRow } from './ManageServersRow';
|
import { ManageServersRow } from './ManageServersRow';
|
||||||
import { useServers } from './reducers/servers';
|
import { useServers } from './reducers/servers';
|
||||||
import type { ServersExporter } from './services/ServersExporter';
|
import type { ServersExporter } from './services/ServersExporter';
|
||||||
|
|
||||||
type ManageServersDeps = {
|
export type ManageServersProps = {
|
||||||
ServersExporter: ServersExporter;
|
ServersExporter: ServersExporter;
|
||||||
ImportServersBtn: FC<ImportServersBtnProps>;
|
|
||||||
useTimeoutToggle: TimeoutToggle;
|
useTimeoutToggle: TimeoutToggle;
|
||||||
};
|
};
|
||||||
|
|
||||||
const SHOW_IMPORT_MSG_TIME = 4000;
|
const SHOW_IMPORT_MSG_TIME = 4000;
|
||||||
|
|
||||||
const ManageServers: FCWithDeps<unknown, ManageServersDeps> = () => {
|
const ManageServersBase: FC<ManageServersProps> = withoutSelectedServer(({
|
||||||
const {
|
ServersExporter: serversExporter,
|
||||||
ServersExporter: serversExporter,
|
useTimeoutToggle,
|
||||||
ImportServersBtn,
|
}) => {
|
||||||
useTimeoutToggle,
|
|
||||||
} = useDependencies(ManageServers);
|
|
||||||
const { servers } = useServers();
|
const { servers } = useServers();
|
||||||
const [searchTerm, setSearchTerm] = useState('');
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
const allServers = useMemo(() => Object.values(servers), [servers]);
|
const allServers = useMemo(() => Object.values(servers), [servers]);
|
||||||
@ -34,7 +31,7 @@ const ManageServers: FCWithDeps<unknown, ManageServersDeps> = () => {
|
|||||||
[allServers, searchTerm],
|
[allServers, searchTerm],
|
||||||
);
|
);
|
||||||
const hasAutoConnect = allServers.some(({ autoConnect }) => !!autoConnect);
|
const hasAutoConnect = allServers.some(({ autoConnect }) => !!autoConnect);
|
||||||
// eslint-disable-next-line react-compiler/react-compiler
|
|
||||||
const [errorImporting, setErrorImporting] = useTimeoutToggle({ delay: SHOW_IMPORT_MSG_TIME });
|
const [errorImporting, setErrorImporting] = useTimeoutToggle({ delay: SHOW_IMPORT_MSG_TIME });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -82,10 +79,6 @@ const ManageServers: FCWithDeps<unknown, ManageServersDeps> = () => {
|
|||||||
)}
|
)}
|
||||||
</NoMenuLayout>
|
</NoMenuLayout>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|
||||||
export const ManageServersFactory = componentFactory(ManageServers, [
|
export const ManageServers = withDependencies(ManageServersBase, ['ServersExporter', 'useTimeoutToggle']);
|
||||||
'ServersExporter',
|
|
||||||
'ImportServersBtn',
|
|
||||||
'useTimeoutToggle',
|
|
||||||
]);
|
|
||||||
|
|||||||
@ -1,10 +1,9 @@
|
|||||||
import { faFileUpload as importIcon } from '@fortawesome/free-solid-svg-icons';
|
import { faFileUpload as importIcon } from '@fortawesome/free-solid-svg-icons';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import { Button, Tooltip, useToggle , useTooltip } from '@shlinkio/shlink-frontend-kit';
|
import { Button, Tooltip, useToggle, useTooltip } from '@shlinkio/shlink-frontend-kit';
|
||||||
import type { ChangeEvent, PropsWithChildren } from 'react';
|
import type { ChangeEvent, FC, PropsWithChildren } from 'react';
|
||||||
import { useCallback, useRef, useState } from 'react';
|
import { useCallback, useRef, useState } from 'react';
|
||||||
import type { FCWithDeps } from '../../container/utils';
|
import { withDependencies } from '../../container/context';
|
||||||
import { componentFactory, useDependencies } from '../../container/utils';
|
|
||||||
import type { ServerData } from '../data';
|
import type { ServerData } from '../data';
|
||||||
import { useServers } from '../reducers/servers';
|
import { useServers } from '../reducers/servers';
|
||||||
import type { ServersImporter } from '../services/ServersImporter';
|
import type { ServersImporter } from '../services/ServersImporter';
|
||||||
@ -16,21 +15,20 @@ export type ImportServersBtnProps = PropsWithChildren<{
|
|||||||
onError?: (error: Error) => void;
|
onError?: (error: Error) => void;
|
||||||
tooltipPlacement?: 'top' | 'bottom';
|
tooltipPlacement?: 'top' | 'bottom';
|
||||||
className?: string;
|
className?: string;
|
||||||
|
|
||||||
|
// Injected
|
||||||
|
ServersImporter: ServersImporter
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
type ImportServersBtnDeps = {
|
const ImportServersBtnBase: FC<ImportServersBtnProps> = ({
|
||||||
ServersImporter: ServersImporter
|
|
||||||
};
|
|
||||||
|
|
||||||
const ImportServersBtn: FCWithDeps<ImportServersBtnProps, ImportServersBtnDeps> = ({
|
|
||||||
children,
|
children,
|
||||||
onImport,
|
onImport,
|
||||||
onError = () => {},
|
onError = () => {},
|
||||||
tooltipPlacement = 'bottom',
|
tooltipPlacement = 'bottom',
|
||||||
className = '',
|
className = '',
|
||||||
|
ServersImporter: serversImporter,
|
||||||
}) => {
|
}) => {
|
||||||
const { createServers, servers } = useServers();
|
const { createServers, servers } = useServers();
|
||||||
const { ServersImporter: serversImporter } = useDependencies(ImportServersBtn);
|
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
const { anchor, tooltip } = useTooltip({ placement: tooltipPlacement });
|
const { anchor, tooltip } = useTooltip({ placement: tooltipPlacement });
|
||||||
const [duplicatedServers, setDuplicatedServers] = useState<ServerData[]>([]);
|
const [duplicatedServers, setDuplicatedServers] = useState<ServerData[]>([]);
|
||||||
@ -106,4 +104,4 @@ const ImportServersBtn: FCWithDeps<ImportServersBtnProps, ImportServersBtnDeps>
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ImportServersBtnFactory = componentFactory(ImportServersBtn, ['ServersImporter']);
|
export const ImportServersBtn = withDependencies(ImportServersBtnBase, ['ServersImporter']);
|
||||||
|
|||||||
@ -2,25 +2,25 @@ import type { HttpClient } from '@shlinkio/shlink-js-sdk';
|
|||||||
import { act, screen } from '@testing-library/react';
|
import { act, screen } from '@testing-library/react';
|
||||||
import { fromPartial } from '@total-typescript/shoehorn';
|
import { fromPartial } from '@total-typescript/shoehorn';
|
||||||
import { MemoryRouter } from 'react-router';
|
import { MemoryRouter } from 'react-router';
|
||||||
import { AppFactory } from '../../src/app/App';
|
import { App } from '../../src/app/App';
|
||||||
import { ContainerProvider } from '../../src/container/context';
|
import { ContainerProvider } from '../../src/container/context';
|
||||||
import type { ServerWithId } from '../../src/servers/data';
|
import type { ServerWithId } from '../../src/servers/data';
|
||||||
import { checkAccessibility } from '../__helpers__/accessibility';
|
import { checkAccessibility } from '../__helpers__/accessibility';
|
||||||
import { renderWithStore } from '../__helpers__/setUpTest';
|
import { renderWithStore } from '../__helpers__/setUpTest';
|
||||||
|
|
||||||
|
vi.mock(import('../../src/common/ShlinkWebComponentContainer'), () => ({
|
||||||
|
ShlinkWebComponentContainer: () => <span>ShlinkWebComponentContainer</span>,
|
||||||
|
}));
|
||||||
|
|
||||||
describe('<App />', () => {
|
describe('<App />', () => {
|
||||||
const App = AppFactory(
|
|
||||||
fromPartial({
|
|
||||||
Home: () => <>Home</>,
|
|
||||||
ShlinkWebComponentContainer: () => <>ShlinkWebComponentContainer</>,
|
|
||||||
CreateServer: () => <>CreateServer</>,
|
|
||||||
ManageServers: () => <>ManageServers</>,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
const setUp = async (activeRoute = '/') => act(() => renderWithStore(
|
const setUp = async (activeRoute = '/') => act(() => renderWithStore(
|
||||||
<MemoryRouter initialEntries={[{ pathname: activeRoute }]}>
|
<MemoryRouter initialEntries={[{ pathname: activeRoute }]}>
|
||||||
<ContainerProvider
|
<ContainerProvider
|
||||||
value={fromPartial({ HttpClient: fromPartial<HttpClient>({}), buildShlinkApiClient: vi.fn() })}
|
value={fromPartial({
|
||||||
|
HttpClient: fromPartial<HttpClient>({}),
|
||||||
|
buildShlinkApiClient: vi.fn(),
|
||||||
|
useTimeoutToggle: vi.fn().mockReturnValue([false, vi.fn()]),
|
||||||
|
})}
|
||||||
>
|
>
|
||||||
<App />
|
<App />
|
||||||
</ContainerProvider>
|
</ContainerProvider>
|
||||||
@ -42,8 +42,8 @@ describe('<App />', () => {
|
|||||||
it.each([
|
it.each([
|
||||||
['/settings/general', 'User interface'],
|
['/settings/general', 'User interface'],
|
||||||
['/settings/short-urls', 'Short URLs form'],
|
['/settings/short-urls', 'Short URLs form'],
|
||||||
['/manage-servers', 'ManageServers'],
|
['/manage-servers', 'Add a server'],
|
||||||
['/server/create', 'CreateServer'],
|
['/server/create', 'Add new server'],
|
||||||
['/server/abc123/edit', 'Edit "abc123 server"'],
|
['/server/abc123/edit', 'Edit "abc123 server"'],
|
||||||
['/server/def456/edit', 'Edit "def456 server"'],
|
['/server/def456/edit', 'Edit "def456 server"'],
|
||||||
['/server/abc123/foo', 'ShlinkWebComponentContainer'],
|
['/server/abc123/foo', 'ShlinkWebComponentContainer'],
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import { screen } from '@testing-library/react';
|
|||||||
import { fromPartial } from '@total-typescript/shoehorn';
|
import { fromPartial } from '@total-typescript/shoehorn';
|
||||||
import { MemoryRouter } from 'react-router';
|
import { MemoryRouter } from 'react-router';
|
||||||
import { Home } from '../../src/common/Home';
|
import { Home } from '../../src/common/Home';
|
||||||
|
import { ContainerProvider } from '../../src/container/context';
|
||||||
import type { ServersMap, ServerWithId } from '../../src/servers/data';
|
import type { ServersMap, ServerWithId } from '../../src/servers/data';
|
||||||
import { checkAccessibility } from '../__helpers__/accessibility';
|
import { checkAccessibility } from '../__helpers__/accessibility';
|
||||||
import { renderWithStore } from '../__helpers__/setUpTest';
|
import { renderWithStore } from '../__helpers__/setUpTest';
|
||||||
@ -9,7 +10,9 @@ import { renderWithStore } from '../__helpers__/setUpTest';
|
|||||||
describe('<Home />', () => {
|
describe('<Home />', () => {
|
||||||
const setUp = (servers: ServersMap = {}) => renderWithStore(
|
const setUp = (servers: ServersMap = {}) => renderWithStore(
|
||||||
<MemoryRouter>
|
<MemoryRouter>
|
||||||
<Home />
|
<ContainerProvider value={fromPartial({ buildShlinkApiClient: vi.fn() })}>
|
||||||
|
<Home />
|
||||||
|
</ContainerProvider>
|
||||||
</MemoryRouter>,
|
</MemoryRouter>,
|
||||||
{
|
{
|
||||||
initialState: { servers },
|
initialState: { servers },
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { screen } from '@testing-library/react';
|
import { screen } from '@testing-library/react';
|
||||||
import { fromPartial } from '@total-typescript/shoehorn';
|
import { fromPartial } from '@total-typescript/shoehorn';
|
||||||
import { MemoryRouter } from 'react-router';
|
import { MemoryRouter } from 'react-router';
|
||||||
import { ShlinkWebComponentContainerFactory } from '../../src/common/ShlinkWebComponentContainer';
|
import { ShlinkWebComponentContainer } from '../../src/common/ShlinkWebComponentContainer';
|
||||||
import { ContainerProvider } from '../../src/container/context';
|
import { ContainerProvider } from '../../src/container/context';
|
||||||
import type { NonReachableServer, NotFoundServer, SelectedServer } from '../../src/servers/data';
|
import type { NonReachableServer, NotFoundServer, SelectedServer } from '../../src/servers/data';
|
||||||
import { checkAccessibility } from '../__helpers__/accessibility';
|
import { checkAccessibility } from '../__helpers__/accessibility';
|
||||||
@ -14,13 +14,12 @@ vi.mock('@shlinkio/shlink-web-component', () => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
describe('<ShlinkWebComponentContainer />', () => {
|
describe('<ShlinkWebComponentContainer />', () => {
|
||||||
const ShlinkWebComponentContainer = ShlinkWebComponentContainerFactory(fromPartial({
|
|
||||||
buildShlinkApiClient: vi.fn().mockReturnValue(fromPartial({})),
|
|
||||||
TagColorsStorage: fromPartial({}),
|
|
||||||
}));
|
|
||||||
const setUp = (selectedServer: SelectedServer) => renderWithStore(
|
const setUp = (selectedServer: SelectedServer) => renderWithStore(
|
||||||
<MemoryRouter>
|
<MemoryRouter>
|
||||||
<ContainerProvider value={fromPartial({ buildShlinkApiClient: vi.fn() })}>
|
<ContainerProvider value={fromPartial({
|
||||||
|
buildShlinkApiClient: vi.fn(),
|
||||||
|
TagColorsStorage: fromPartial({}),
|
||||||
|
})}>
|
||||||
<ShlinkWebComponentContainer />
|
<ShlinkWebComponentContainer />
|
||||||
</ContainerProvider>
|
</ContainerProvider>
|
||||||
</MemoryRouter>,
|
</MemoryRouter>,
|
||||||
|
|||||||
@ -2,7 +2,8 @@ import { fireEvent, screen, waitFor } from '@testing-library/react';
|
|||||||
import { fromPartial } from '@total-typescript/shoehorn';
|
import { fromPartial } from '@total-typescript/shoehorn';
|
||||||
import { createMemoryHistory } from 'history';
|
import { createMemoryHistory } from 'history';
|
||||||
import { Router } from 'react-router';
|
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 type { ServersMap } from '../../src/servers/data';
|
||||||
import { checkAccessibility } from '../__helpers__/accessibility';
|
import { checkAccessibility } from '../__helpers__/accessibility';
|
||||||
import { renderWithStore } from '../__helpers__/setUpTest';
|
import { renderWithStore } from '../__helpers__/setUpTest';
|
||||||
@ -24,17 +25,19 @@ describe('<CreateServer />', () => {
|
|||||||
callCount += 1;
|
callCount += 1;
|
||||||
return result;
|
return result;
|
||||||
});
|
});
|
||||||
const CreateServer = CreateServerFactory(fromPartial({
|
|
||||||
ImportServersBtn: () => <>ImportServersBtn</>,
|
|
||||||
useTimeoutToggle,
|
|
||||||
}));
|
|
||||||
const history = createMemoryHistory({ initialEntries: ['/foo', '/bar'] });
|
const history = createMemoryHistory({ initialEntries: ['/foo', '/bar'] });
|
||||||
|
|
||||||
return {
|
return {
|
||||||
history,
|
history,
|
||||||
...renderWithStore(
|
...renderWithStore(
|
||||||
<Router location={history.location} navigator={history}>
|
<Router location={history.location} navigator={history}>
|
||||||
<CreateServer />
|
<ContainerProvider value={fromPartial({
|
||||||
|
ImportServersBtn: () => <>ImportServersBtn</>,
|
||||||
|
useTimeoutToggle,
|
||||||
|
buildShlinkApiClient: vi.fn(),
|
||||||
|
})}>
|
||||||
|
<CreateServer />
|
||||||
|
</ContainerProvider>
|
||||||
</Router>,
|
</Router>,
|
||||||
{
|
{
|
||||||
initialState: { servers },
|
initialState: { servers },
|
||||||
@ -64,11 +67,6 @@ describe('<CreateServer />', () => {
|
|||||||
expect(screen.getByText('The servers could not be imported. Make sure the format is correct.')).toBeInTheDocument();
|
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 () => {
|
it('creates server data when form is submitted', async () => {
|
||||||
const { user, history, store } = setUp();
|
const { user, history, store } = setUp();
|
||||||
const expectedServerId = 'the_name-the_url.com';
|
const expectedServerId = 'the_name-the_url.com';
|
||||||
|
|||||||
@ -1,8 +1,9 @@
|
|||||||
import { screen, waitFor } from '@testing-library/react';
|
import { screen, waitFor } from '@testing-library/react';
|
||||||
import { fromPartial } from '@total-typescript/shoehorn';
|
import { fromPartial } from '@total-typescript/shoehorn';
|
||||||
import { MemoryRouter } from 'react-router';
|
import { MemoryRouter } from 'react-router';
|
||||||
|
import { ContainerProvider } from '../../src/container/context';
|
||||||
import type { ServersMap, ServerWithId } from '../../src/servers/data';
|
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 type { ServersExporter } from '../../src/servers/services/ServersExporter';
|
||||||
import { checkAccessibility } from '../__helpers__/accessibility';
|
import { checkAccessibility } from '../__helpers__/accessibility';
|
||||||
import { renderWithStore } from '../__helpers__/setUpTest';
|
import { renderWithStore } from '../__helpers__/setUpTest';
|
||||||
@ -11,16 +12,20 @@ describe('<ManageServers />', () => {
|
|||||||
const exportServers = vi.fn();
|
const exportServers = vi.fn();
|
||||||
const serversExporter = fromPartial<ServersExporter>({ exportServers });
|
const serversExporter = fromPartial<ServersExporter>({ exportServers });
|
||||||
const useTimeoutToggle = vi.fn().mockReturnValue([false, vi.fn()]);
|
const useTimeoutToggle = vi.fn().mockReturnValue([false, vi.fn()]);
|
||||||
const ManageServers = ManageServersFactory(fromPartial({
|
|
||||||
ServersExporter: serversExporter,
|
|
||||||
ImportServersBtn: () => <span>ImportServersBtn</span>,
|
|
||||||
useTimeoutToggle,
|
|
||||||
}));
|
|
||||||
const createServerMock = (value: string, autoConnect = false) => fromPartial<ServerWithId>(
|
const createServerMock = (value: string, autoConnect = false) => fromPartial<ServerWithId>(
|
||||||
{ id: value, name: value, url: value, autoConnect },
|
{ id: value, name: value, url: value, autoConnect },
|
||||||
);
|
);
|
||||||
const setUp = (servers: ServersMap = {}) => renderWithStore(
|
const setUp = (servers: ServersMap = {}) => renderWithStore(
|
||||||
<MemoryRouter><ManageServers /></MemoryRouter>,
|
<MemoryRouter>
|
||||||
|
<ContainerProvider value={fromPartial({
|
||||||
|
ServersExporter: serversExporter,
|
||||||
|
ImportServersBtn: () => <span>ImportServersBtn</span>,
|
||||||
|
useTimeoutToggle,
|
||||||
|
buildShlinkApiClient: vi.fn(),
|
||||||
|
})}>
|
||||||
|
<ManageServers />
|
||||||
|
</ContainerProvider>
|
||||||
|
</MemoryRouter>,
|
||||||
{
|
{
|
||||||
initialState: { servers },
|
initialState: { servers },
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
import { screen, waitFor } from '@testing-library/react';
|
import { screen, waitFor } from '@testing-library/react';
|
||||||
import { fromPartial } from '@total-typescript/shoehorn';
|
import { fromPartial } from '@total-typescript/shoehorn';
|
||||||
|
import { ContainerProvider } from '../../../src/container/context';
|
||||||
import type { ServerData, ServersMap, ServerWithId } from '../../../src/servers/data';
|
import type { ServerData, ServersMap, ServerWithId } from '../../../src/servers/data';
|
||||||
import type {
|
import type { ImportServersBtnProps } from '../../../src/servers/helpers/ImportServersBtn';
|
||||||
ImportServersBtnProps } from '../../../src/servers/helpers/ImportServersBtn';
|
import { ImportServersBtn } from '../../../src/servers/helpers/ImportServersBtn';
|
||||||
import { ImportServersBtnFactory } from '../../../src/servers/helpers/ImportServersBtn';
|
|
||||||
import type { ServersImporter } from '../../../src/servers/services/ServersImporter';
|
import type { ServersImporter } from '../../../src/servers/services/ServersImporter';
|
||||||
import { checkAccessibility } from '../../__helpers__/accessibility';
|
import { checkAccessibility } from '../../__helpers__/accessibility';
|
||||||
import { renderWithStore } from '../../__helpers__/setUpTest';
|
import { renderWithStore } from '../../__helpers__/setUpTest';
|
||||||
@ -13,9 +13,10 @@ describe('<ImportServersBtn />', () => {
|
|||||||
const onImportMock = vi.fn();
|
const onImportMock = vi.fn();
|
||||||
const importServersFromFile = vi.fn().mockResolvedValue([]);
|
const importServersFromFile = vi.fn().mockResolvedValue([]);
|
||||||
const serversImporterMock = fromPartial<ServersImporter>({ importServersFromFile });
|
const serversImporterMock = fromPartial<ServersImporter>({ importServersFromFile });
|
||||||
const ImportServersBtn = ImportServersBtnFactory(fromPartial({ ServersImporter: serversImporterMock }));
|
|
||||||
const setUp = (props: Partial<ImportServersBtnProps> = {}, servers: ServersMap = {}) => renderWithStore(
|
const setUp = (props: Partial<ImportServersBtnProps> = {}, servers: ServersMap = {}) => renderWithStore(
|
||||||
<ImportServersBtn {...props} onImport={onImportMock} />,
|
<ContainerProvider value={fromPartial({ ServersImporter: serversImporterMock })}>
|
||||||
|
<ImportServersBtn {...props} onImport={onImportMock} />
|
||||||
|
</ContainerProvider>,
|
||||||
{
|
{
|
||||||
initialState: { servers },
|
initialState: { servers },
|
||||||
},
|
},
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user