mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2024-12-23 09:30:31 +03:00
Merge pull request #890 from acelaya-forks/feature/improve-component-di
Feature/improve component di
This commit is contained in:
commit
b71b14a4fd
36 changed files with 469 additions and 284 deletions
|
@ -6,8 +6,5 @@
|
||||||
"parserOptions": {
|
"parserOptions": {
|
||||||
"project": "./tsconfig.json"
|
"project": "./tsconfig.json"
|
||||||
},
|
},
|
||||||
"ignorePatterns": ["src/service*.ts"],
|
"ignorePatterns": ["src/service*.ts"]
|
||||||
"rules": {
|
|
||||||
"react-hooks/rules-of-hooks": "off"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,29 +5,46 @@ import { useEffect, useRef } from 'react';
|
||||||
import { Route, Routes, useLocation } from 'react-router-dom';
|
import { Route, Routes, useLocation } from 'react-router-dom';
|
||||||
import { AppUpdateBanner } from '../common/AppUpdateBanner';
|
import { AppUpdateBanner } from '../common/AppUpdateBanner';
|
||||||
import { NotFound } from '../common/NotFound';
|
import { NotFound } from '../common/NotFound';
|
||||||
|
import type { FCWithDeps } from '../container/utils';
|
||||||
|
import { componentFactory, useDependencies } from '../container/utils';
|
||||||
import type { ServersMap } from '../servers/data';
|
import type { ServersMap } from '../servers/data';
|
||||||
import type { AppSettings } from '../settings/reducers/settings';
|
import type { AppSettings } from '../settings/reducers/settings';
|
||||||
import { forceUpdate } from '../utils/helpers/sw';
|
import { forceUpdate } from '../utils/helpers/sw';
|
||||||
import './App.scss';
|
import './App.scss';
|
||||||
|
|
||||||
interface AppProps {
|
type AppProps = {
|
||||||
fetchServers: () => void;
|
fetchServers: () => void;
|
||||||
servers: ServersMap;
|
servers: ServersMap;
|
||||||
settings: AppSettings;
|
settings: AppSettings;
|
||||||
resetAppUpdate: () => void;
|
resetAppUpdate: () => void;
|
||||||
appUpdated: boolean;
|
appUpdated: boolean;
|
||||||
}
|
};
|
||||||
|
|
||||||
|
type AppDeps = {
|
||||||
|
MainHeader: FC;
|
||||||
|
Home: FC;
|
||||||
|
ShlinkWebComponentContainer: FC;
|
||||||
|
CreateServer: FC;
|
||||||
|
EditServer: FC;
|
||||||
|
Settings: FC;
|
||||||
|
ManageServers: FC;
|
||||||
|
ShlinkVersionsContainer: FC;
|
||||||
|
};
|
||||||
|
|
||||||
|
const App: FCWithDeps<AppProps, AppDeps> = (
|
||||||
|
{ fetchServers, servers, settings, appUpdated, resetAppUpdate },
|
||||||
|
) => {
|
||||||
|
const {
|
||||||
|
MainHeader,
|
||||||
|
Home,
|
||||||
|
ShlinkWebComponentContainer,
|
||||||
|
CreateServer,
|
||||||
|
EditServer,
|
||||||
|
Settings,
|
||||||
|
ManageServers,
|
||||||
|
ShlinkVersionsContainer,
|
||||||
|
} = useDependencies(App);
|
||||||
|
|
||||||
export const App = (
|
|
||||||
MainHeader: FC,
|
|
||||||
Home: FC,
|
|
||||||
ShlinkWebComponentContainer: FC,
|
|
||||||
CreateServer: FC,
|
|
||||||
EditServer: FC,
|
|
||||||
SettingsComp: FC,
|
|
||||||
ManageServers: FC,
|
|
||||||
ShlinkVersionsContainer: FC,
|
|
||||||
) => ({ fetchServers, servers, settings, appUpdated, resetAppUpdate }: AppProps) => {
|
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const initialServers = useRef(servers);
|
const initialServers = useRef(servers);
|
||||||
const isHome = location.pathname === '/';
|
const isHome = location.pathname === '/';
|
||||||
|
@ -52,7 +69,7 @@ export const App = (
|
||||||
<div className={classNames('shlink-wrapper', { 'd-flex d-md-block align-items-center': isHome })}>
|
<div className={classNames('shlink-wrapper', { 'd-flex d-md-block align-items-center': isHome })}>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route index element={<Home />} />
|
<Route index element={<Home />} />
|
||||||
<Route path="/settings/*" element={<SettingsComp />} />
|
<Route path="/settings/*" element={<Settings />} />
|
||||||
<Route path="/manage-servers" element={<ManageServers />} />
|
<Route path="/manage-servers" element={<ManageServers />} />
|
||||||
<Route path="/server/create" element={<CreateServer />} />
|
<Route path="/server/create" element={<CreateServer />} />
|
||||||
<Route path="/server/:serverId/edit" element={<EditServer />} />
|
<Route path="/server/:serverId/edit" element={<EditServer />} />
|
||||||
|
@ -70,3 +87,14 @@ export const App = (
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const AppFactory = componentFactory(App, [
|
||||||
|
'MainHeader',
|
||||||
|
'Home',
|
||||||
|
'ShlinkWebComponentContainer',
|
||||||
|
'CreateServer',
|
||||||
|
'EditServer',
|
||||||
|
'Settings',
|
||||||
|
'ManageServers',
|
||||||
|
'ShlinkVersionsContainer',
|
||||||
|
]);
|
||||||
|
|
|
@ -1,22 +1,11 @@
|
||||||
import type Bottle from 'bottlejs';
|
import type Bottle from 'bottlejs';
|
||||||
import type { ConnectDecorator } from '../../container/types';
|
import type { ConnectDecorator } from '../../container/types';
|
||||||
import { App } from '../App';
|
import { AppFactory } from '../App';
|
||||||
import { appUpdateAvailable, resetAppUpdate } from '../reducers/appUpdates';
|
import { appUpdateAvailable, resetAppUpdate } from '../reducers/appUpdates';
|
||||||
|
|
||||||
export const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
|
export const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
|
||||||
// Components
|
// Components
|
||||||
bottle.serviceFactory(
|
bottle.factory('App', AppFactory);
|
||||||
'App',
|
|
||||||
App,
|
|
||||||
'MainHeader',
|
|
||||||
'Home',
|
|
||||||
'ShlinkWebComponentContainer',
|
|
||||||
'CreateServer',
|
|
||||||
'EditServer',
|
|
||||||
'Settings',
|
|
||||||
'ManageServers',
|
|
||||||
'ShlinkVersionsContainer',
|
|
||||||
);
|
|
||||||
bottle.decorator('App', connect(['servers', 'settings', 'appUpdated'], ['fetchServers', 'resetAppUpdate']));
|
bottle.decorator('App', connect(['servers', 'settings', 'appUpdated'], ['fetchServers', 'resetAppUpdate']));
|
||||||
|
|
||||||
// Actions
|
// Actions
|
||||||
|
|
|
@ -1,17 +1,19 @@
|
||||||
import { SimpleCard } from '@shlinkio/shlink-frontend-kit';
|
import { SimpleCard } from '@shlinkio/shlink-frontend-kit';
|
||||||
import type { ReactNode } from 'react';
|
import type { PropsWithChildren, ReactNode } from 'react';
|
||||||
import { Component } from 'react';
|
import { Component } from 'react';
|
||||||
import { Button } from 'reactstrap';
|
import { Button } from 'reactstrap';
|
||||||
|
|
||||||
interface ErrorHandlerState {
|
type ErrorHandlerProps = PropsWithChildren<{
|
||||||
hasError: boolean;
|
location?: typeof window.location;
|
||||||
}
|
console?: typeof window.console;
|
||||||
|
}>;
|
||||||
|
|
||||||
export const ErrorHandler = (
|
type ErrorHandlerState = {
|
||||||
{ location }: Window,
|
hasError: boolean;
|
||||||
{ error }: Console,
|
};
|
||||||
) => class extends Component<any, ErrorHandlerState> {
|
|
||||||
public constructor(props: object) {
|
export class ErrorHandler extends Component<ErrorHandlerProps, ErrorHandlerState> {
|
||||||
|
public constructor(props: ErrorHandlerProps) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = { hasError: false };
|
this.state = { hasError: false };
|
||||||
}
|
}
|
||||||
|
@ -21,13 +23,14 @@ export const ErrorHandler = (
|
||||||
}
|
}
|
||||||
|
|
||||||
public componentDidCatch(e: Error): void {
|
public componentDidCatch(e: Error): void {
|
||||||
if (process.env.NODE_ENV !== 'development') {
|
const { console = globalThis.console } = this.props;
|
||||||
error(e);
|
console.error(e);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public render(): ReactNode {
|
public render(): ReactNode {
|
||||||
const { hasError } = this.state;
|
const { hasError } = this.state;
|
||||||
|
const { location = globalThis.location } = this.props;
|
||||||
|
|
||||||
if (hasError) {
|
if (hasError) {
|
||||||
return (
|
return (
|
||||||
<div className="home">
|
<div className="home">
|
||||||
|
@ -44,4 +47,4 @@ export const ErrorHandler = (
|
||||||
const { children } = this.props;
|
const { children } = this.props;
|
||||||
return children;
|
return children;
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
|
@ -6,10 +6,17 @@ import type { FC } from 'react';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { Link, useLocation } from 'react-router-dom';
|
import { Link, useLocation } from 'react-router-dom';
|
||||||
import { Collapse, Nav, Navbar, NavbarBrand, NavbarToggler, NavItem, NavLink } from 'reactstrap';
|
import { Collapse, Nav, Navbar, NavbarBrand, NavbarToggler, NavItem, NavLink } from 'reactstrap';
|
||||||
|
import type { FCWithDeps } from '../container/utils';
|
||||||
|
import { componentFactory, useDependencies } from '../container/utils';
|
||||||
import { ShlinkLogo } from './img/ShlinkLogo';
|
import { ShlinkLogo } from './img/ShlinkLogo';
|
||||||
import './MainHeader.scss';
|
import './MainHeader.scss';
|
||||||
|
|
||||||
export const MainHeader = (ServersDropdown: FC) => () => {
|
type MainHeaderDeps = {
|
||||||
|
ServersDropdown: FC;
|
||||||
|
};
|
||||||
|
|
||||||
|
const MainHeader: FCWithDeps<{}, MainHeaderDeps> = () => {
|
||||||
|
const { ServersDropdown } = useDependencies(MainHeader);
|
||||||
const [isNotCollapsed, toggleCollapse, , collapse] = useToggle();
|
const [isNotCollapsed, toggleCollapse, , collapse] = useToggle();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const { pathname } = location;
|
const { pathname } = location;
|
||||||
|
@ -43,3 +50,5 @@ export const MainHeader = (ServersDropdown: FC) => () => {
|
||||||
</Navbar>
|
</Navbar>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const MainHeaderFactory = componentFactory(MainHeader, ['ServersDropdown']);
|
||||||
|
|
|
@ -2,7 +2,7 @@ import type { FC, PropsWithChildren } from 'react';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { useLocation } from 'react-router-dom';
|
import { useLocation } from 'react-router-dom';
|
||||||
|
|
||||||
export const ScrollToTop: FC<PropsWithChildren<unknown>> = ({ children }) => {
|
export const ScrollToTop: FC<PropsWithChildren> = ({ children }) => {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
|
@ -1,20 +1,34 @@
|
||||||
import type { Settings, ShlinkWebComponentType, TagColorsStorage } from '@shlinkio/shlink-web-component';
|
import type { Settings, ShlinkWebComponentType, TagColorsStorage } from '@shlinkio/shlink-web-component';
|
||||||
import type { FC } from 'react';
|
import type { FC } from 'react';
|
||||||
import type { ShlinkApiClientBuilder } from '../api/services/ShlinkApiClientBuilder';
|
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 { isReachableServer } from '../servers/data';
|
||||||
|
import type { WithSelectedServerProps } from '../servers/helpers/withSelectedServer';
|
||||||
import { withSelectedServer } from '../servers/helpers/withSelectedServer';
|
import { withSelectedServer } from '../servers/helpers/withSelectedServer';
|
||||||
import { NotFound } from './NotFound';
|
import { NotFound } from './NotFound';
|
||||||
|
|
||||||
interface ShlinkWebComponentContainerProps {
|
type ShlinkWebComponentContainerProps = WithSelectedServerProps & {
|
||||||
settings: Settings;
|
settings: Settings;
|
||||||
}
|
};
|
||||||
|
|
||||||
export const ShlinkWebComponentContainer = (
|
type ShlinkWebComponentContainerDeps = {
|
||||||
buildShlinkApiClient: ShlinkApiClientBuilder,
|
buildShlinkApiClient: ShlinkApiClientBuilder,
|
||||||
tagColorsStorage: TagColorsStorage,
|
TagColorsStorage: TagColorsStorage,
|
||||||
ShlinkWebComponent: ShlinkWebComponentType,
|
ShlinkWebComponent: ShlinkWebComponentType,
|
||||||
ServerError: FC,
|
ServerError: FC,
|
||||||
) => withSelectedServer<ShlinkWebComponentContainerProps>(({ selectedServer, settings }) => {
|
};
|
||||||
|
|
||||||
|
const ShlinkWebComponentContainer: FCWithDeps<
|
||||||
|
ShlinkWebComponentContainerProps,
|
||||||
|
ShlinkWebComponentContainerDeps
|
||||||
|
> = withSelectedServer(({ selectedServer, settings }) => {
|
||||||
|
const {
|
||||||
|
buildShlinkApiClient,
|
||||||
|
TagColorsStorage: tagColorsStorage,
|
||||||
|
ShlinkWebComponent,
|
||||||
|
ServerError,
|
||||||
|
} = useDependencies(ShlinkWebComponentContainer);
|
||||||
const selectedServerIsReachable = isReachableServer(selectedServer);
|
const selectedServerIsReachable = isReachableServer(selectedServer);
|
||||||
const routesPrefix = selectedServerIsReachable ? `/server/${selectedServer.id}` : '';
|
const routesPrefix = selectedServerIsReachable ? `/server/${selectedServer.id}` : '';
|
||||||
|
|
||||||
|
@ -34,4 +48,11 @@ export const ShlinkWebComponentContainer = (
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}, ServerError);
|
});
|
||||||
|
|
||||||
|
export const ShlinkWebComponentContainerFactory = componentFactory(ShlinkWebComponentContainer, [
|
||||||
|
'buildShlinkApiClient',
|
||||||
|
'TagColorsStorage',
|
||||||
|
'ShlinkWebComponent',
|
||||||
|
'ServerError',
|
||||||
|
]);
|
||||||
|
|
|
@ -5,10 +5,10 @@ import type { ConnectDecorator } from '../../container/types';
|
||||||
import { withoutSelectedServer } from '../../servers/helpers/withoutSelectedServer';
|
import { withoutSelectedServer } from '../../servers/helpers/withoutSelectedServer';
|
||||||
import { ErrorHandler } from '../ErrorHandler';
|
import { ErrorHandler } from '../ErrorHandler';
|
||||||
import { Home } from '../Home';
|
import { Home } from '../Home';
|
||||||
import { MainHeader } from '../MainHeader';
|
import { MainHeaderFactory } from '../MainHeader';
|
||||||
import { ScrollToTop } from '../ScrollToTop';
|
import { ScrollToTop } from '../ScrollToTop';
|
||||||
import { ShlinkVersionsContainer } from '../ShlinkVersionsContainer';
|
import { ShlinkVersionsContainer } from '../ShlinkVersionsContainer';
|
||||||
import { ShlinkWebComponentContainer } from '../ShlinkWebComponentContainer';
|
import { ShlinkWebComponentContainerFactory } from '../ShlinkWebComponentContainer';
|
||||||
|
|
||||||
export const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
|
export const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
|
||||||
// Services
|
// Services
|
||||||
|
@ -20,25 +20,18 @@ export const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
|
||||||
// Components
|
// Components
|
||||||
bottle.serviceFactory('ScrollToTop', () => ScrollToTop);
|
bottle.serviceFactory('ScrollToTop', () => ScrollToTop);
|
||||||
|
|
||||||
bottle.serviceFactory('MainHeader', MainHeader, 'ServersDropdown');
|
bottle.factory('MainHeader', MainHeaderFactory);
|
||||||
|
|
||||||
bottle.serviceFactory('Home', () => Home);
|
bottle.serviceFactory('Home', () => Home);
|
||||||
bottle.decorator('Home', withoutSelectedServer);
|
bottle.decorator('Home', withoutSelectedServer);
|
||||||
bottle.decorator('Home', connect(['servers'], ['resetSelectedServer']));
|
bottle.decorator('Home', connect(['servers'], ['resetSelectedServer']));
|
||||||
|
|
||||||
bottle.serviceFactory('ShlinkWebComponent', () => ShlinkWebComponent);
|
bottle.serviceFactory('ShlinkWebComponent', () => ShlinkWebComponent);
|
||||||
bottle.serviceFactory(
|
bottle.factory('ShlinkWebComponentContainer', ShlinkWebComponentContainerFactory);
|
||||||
'ShlinkWebComponentContainer',
|
|
||||||
ShlinkWebComponentContainer,
|
|
||||||
'buildShlinkApiClient',
|
|
||||||
'TagColorsStorage',
|
|
||||||
'ShlinkWebComponent',
|
|
||||||
'ServerError',
|
|
||||||
);
|
|
||||||
bottle.decorator('ShlinkWebComponentContainer', connect(['selectedServer', 'settings'], ['selectServer']));
|
bottle.decorator('ShlinkWebComponentContainer', connect(['selectedServer', 'settings'], ['selectServer']));
|
||||||
|
|
||||||
bottle.serviceFactory('ShlinkVersionsContainer', () => ShlinkVersionsContainer);
|
bottle.serviceFactory('ShlinkVersionsContainer', () => ShlinkVersionsContainer);
|
||||||
bottle.decorator('ShlinkVersionsContainer', connect(['selectedServer']));
|
bottle.decorator('ShlinkVersionsContainer', connect(['selectedServer']));
|
||||||
|
|
||||||
bottle.serviceFactory('ErrorHandler', ErrorHandler, 'window', 'console');
|
bottle.serviceFactory('ErrorHandler', () => ErrorHandler);
|
||||||
};
|
};
|
||||||
|
|
29
src/container/utils.ts
Normal file
29
src/container/utils.ts
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
import type { IContainer } from 'bottlejs';
|
||||||
|
import type { FC } from 'react';
|
||||||
|
import { useRef } from 'react';
|
||||||
|
|
||||||
|
export type FCWithDeps<Props, Deps> = FC<Props> & Partial<Deps>;
|
||||||
|
|
||||||
|
export function useDependencies<Deps>(obj: Deps): Omit<Required<Deps>, keyof FC> {
|
||||||
|
const depsRef = useRef(obj as Omit<Required<Deps>, keyof FC>);
|
||||||
|
return depsRef.current;
|
||||||
|
}
|
||||||
|
|
||||||
|
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`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
Component[dep] = resolvedDependency;
|
||||||
|
});
|
||||||
|
|
||||||
|
return Component;
|
||||||
|
};
|
||||||
|
}
|
|
@ -5,6 +5,8 @@ import { useNavigate } from 'react-router-dom';
|
||||||
import { Button } from 'reactstrap';
|
import { Button } from 'reactstrap';
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
import { NoMenuLayout } from '../common/NoMenuLayout';
|
import { NoMenuLayout } from '../common/NoMenuLayout';
|
||||||
|
import type { FCWithDeps } from '../container/utils';
|
||||||
|
import { componentFactory, useDependencies } from '../container/utils';
|
||||||
import type { TimeoutToggle } from '../utils/helpers/hooks';
|
import type { TimeoutToggle } from '../utils/helpers/hooks';
|
||||||
import { useGoBack } from '../utils/helpers/hooks';
|
import { useGoBack } from '../utils/helpers/hooks';
|
||||||
import type { ServerData, ServersMap, ServerWithId } from './data';
|
import type { ServerData, ServersMap, ServerWithId } from './data';
|
||||||
|
@ -14,10 +16,15 @@ import { ServerForm } from './helpers/ServerForm';
|
||||||
|
|
||||||
const SHOW_IMPORT_MSG_TIME = 4000;
|
const SHOW_IMPORT_MSG_TIME = 4000;
|
||||||
|
|
||||||
interface CreateServerProps {
|
type CreateServerProps = {
|
||||||
createServers: (servers: ServerWithId[]) => void;
|
createServers: (servers: ServerWithId[]) => void;
|
||||||
servers: ServersMap;
|
servers: ServersMap;
|
||||||
}
|
};
|
||||||
|
|
||||||
|
type CreateServerDeps = {
|
||||||
|
ImportServersBtn: FC<ImportServersBtnProps>;
|
||||||
|
useTimeoutToggle: TimeoutToggle;
|
||||||
|
};
|
||||||
|
|
||||||
const ImportResult = ({ type }: { type: 'error' | 'success' }) => (
|
const ImportResult = ({ type }: { type: 'error' | 'success' }) => (
|
||||||
<div className="mt-3">
|
<div className="mt-3">
|
||||||
|
@ -28,9 +35,8 @@ const ImportResult = ({ type }: { type: 'error' | 'success' }) => (
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
export const CreateServer = (ImportServersBtn: FC<ImportServersBtnProps>, useTimeoutToggle: TimeoutToggle) => (
|
const CreateServer: FCWithDeps<CreateServerProps, CreateServerDeps> = ({ servers, createServers }) => {
|
||||||
{ servers, createServers }: CreateServerProps,
|
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;
|
||||||
|
@ -79,3 +85,5 @@ export const CreateServer = (ImportServersBtn: FC<ImportServersBtnProps>, useTim
|
||||||
</NoMenuLayout>
|
</NoMenuLayout>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const CreateServerFactory = componentFactory(CreateServer, ['ImportServersBtn', 'useTimeoutToggle']);
|
||||||
|
|
|
@ -3,6 +3,8 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import { useToggle } from '@shlinkio/shlink-frontend-kit';
|
import { useToggle } from '@shlinkio/shlink-frontend-kit';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import type { FC, PropsWithChildren } from 'react';
|
import type { FC, PropsWithChildren } from 'react';
|
||||||
|
import type { FCWithDeps } from '../container/utils';
|
||||||
|
import { componentFactory, useDependencies } from '../container/utils';
|
||||||
import type { ServerWithId } from './data';
|
import type { ServerWithId } from './data';
|
||||||
import type { DeleteServerModalProps } from './DeleteServerModal';
|
import type { DeleteServerModalProps } from './DeleteServerModal';
|
||||||
|
|
||||||
|
@ -12,9 +14,14 @@ export type DeleteServerButtonProps = PropsWithChildren<{
|
||||||
textClassName?: string;
|
textClassName?: string;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
export const DeleteServerButton = (DeleteServerModal: FC<DeleteServerModalProps>): FC<DeleteServerButtonProps> => (
|
type DeleteServerButtonDeps = {
|
||||||
|
DeleteServerModal: FC<DeleteServerModalProps>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const DeleteServerButton: FCWithDeps<DeleteServerButtonProps, DeleteServerButtonDeps> = (
|
||||||
{ server, className, children, textClassName },
|
{ server, className, children, textClassName },
|
||||||
) => {
|
) => {
|
||||||
|
const { DeleteServerModal } = useDependencies(DeleteServerButton);
|
||||||
const [isModalOpen, , showModal, hideModal] = useToggle();
|
const [isModalOpen, , showModal, hideModal] = useToggle();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -28,3 +35,5 @@ export const DeleteServerButton = (DeleteServerModal: FC<DeleteServerModalProps>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const DeleteServerButtonFactory = componentFactory(DeleteServerButton, ['DeleteServerModal']);
|
||||||
|
|
|
@ -1,17 +1,24 @@
|
||||||
import type { FC } from 'react';
|
import type { FC } from 'react';
|
||||||
import { Button } from 'reactstrap';
|
import { Button } from 'reactstrap';
|
||||||
import { NoMenuLayout } from '../common/NoMenuLayout';
|
import { NoMenuLayout } from '../common/NoMenuLayout';
|
||||||
|
import type { FCWithDeps } from '../container/utils';
|
||||||
|
import { componentFactory } from '../container/utils';
|
||||||
import { useGoBack, useParsedQuery } from '../utils/helpers/hooks';
|
import { useGoBack, useParsedQuery } from '../utils/helpers/hooks';
|
||||||
import type { ServerData } from './data';
|
import type { ServerData } from './data';
|
||||||
import { isServerWithId } from './data';
|
import { isServerWithId } from './data';
|
||||||
import { ServerForm } from './helpers/ServerForm';
|
import { ServerForm } from './helpers/ServerForm';
|
||||||
|
import type { WithSelectedServerProps } from './helpers/withSelectedServer';
|
||||||
import { withSelectedServer } from './helpers/withSelectedServer';
|
import { withSelectedServer } from './helpers/withSelectedServer';
|
||||||
|
|
||||||
interface EditServerProps {
|
type EditServerProps = WithSelectedServerProps & {
|
||||||
editServer: (serverId: string, serverData: ServerData) => void;
|
editServer: (serverId: string, serverData: ServerData) => void;
|
||||||
}
|
};
|
||||||
|
|
||||||
export const EditServer = (ServerError: FC) => withSelectedServer<EditServerProps>((
|
type EditServerDeps = {
|
||||||
|
ServerError: FC;
|
||||||
|
};
|
||||||
|
|
||||||
|
const EditServer: FCWithDeps<EditServerProps, EditServerDeps> = withSelectedServer((
|
||||||
{ editServer, selectedServer, selectServer },
|
{ editServer, selectedServer, selectServer },
|
||||||
) => {
|
) => {
|
||||||
const goBack = useGoBack();
|
const goBack = useGoBack();
|
||||||
|
@ -39,4 +46,6 @@ export const EditServer = (ServerError: FC) => withSelectedServer<EditServerProp
|
||||||
</ServerForm>
|
</ServerForm>
|
||||||
</NoMenuLayout>
|
</NoMenuLayout>
|
||||||
);
|
);
|
||||||
}, ServerError);
|
});
|
||||||
|
|
||||||
|
export const EditServerFactory = componentFactory(EditServer, ['ServerError']);
|
||||||
|
|
|
@ -6,24 +6,34 @@ import { useEffect, useState } from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { Button, Row } from 'reactstrap';
|
import { Button, Row } from 'reactstrap';
|
||||||
import { NoMenuLayout } from '../common/NoMenuLayout';
|
import { NoMenuLayout } from '../common/NoMenuLayout';
|
||||||
|
import type { FCWithDeps } from '../container/utils';
|
||||||
|
import { componentFactory, useDependencies } from '../container/utils';
|
||||||
import type { TimeoutToggle } from '../utils/helpers/hooks';
|
import type { TimeoutToggle } from '../utils/helpers/hooks';
|
||||||
import type { ServersMap } from './data';
|
import type { ServersMap } from './data';
|
||||||
import type { ImportServersBtnProps } from './helpers/ImportServersBtn';
|
import type { ImportServersBtnProps } from './helpers/ImportServersBtn';
|
||||||
import type { ManageServersRowProps } from './ManageServersRow';
|
import type { ManageServersRowProps } from './ManageServersRow';
|
||||||
import type { ServersExporter } from './services/ServersExporter';
|
import type { ServersExporter } from './services/ServersExporter';
|
||||||
|
|
||||||
interface ManageServersProps {
|
type ManageServersProps = {
|
||||||
servers: ServersMap;
|
servers: ServersMap;
|
||||||
}
|
};
|
||||||
|
|
||||||
|
type ManageServersDeps = {
|
||||||
|
ServersExporter: ServersExporter;
|
||||||
|
ImportServersBtn: FC<ImportServersBtnProps>;
|
||||||
|
useTimeoutToggle: TimeoutToggle;
|
||||||
|
ManageServersRow: FC<ManageServersRowProps>;
|
||||||
|
};
|
||||||
|
|
||||||
const SHOW_IMPORT_MSG_TIME = 4000;
|
const SHOW_IMPORT_MSG_TIME = 4000;
|
||||||
|
|
||||||
export const ManageServers = (
|
const ManageServers: FCWithDeps<ManageServersProps, ManageServersDeps> = ({ servers }) => {
|
||||||
serversExporter: ServersExporter,
|
const {
|
||||||
ImportServersBtn: FC<ImportServersBtnProps>,
|
ServersExporter: serversExporter,
|
||||||
useTimeoutToggle: TimeoutToggle,
|
ImportServersBtn,
|
||||||
ManageServersRow: FC<ManageServersRowProps>,
|
useTimeoutToggle,
|
||||||
): FC<ManageServersProps> => ({ servers }) => {
|
ManageServersRow,
|
||||||
|
} = useDependencies(ManageServers);
|
||||||
const allServers = Object.values(servers);
|
const allServers = Object.values(servers);
|
||||||
const [serversList, setServersList] = useState(allServers);
|
const [serversList, setServersList] = useState(allServers);
|
||||||
const filterServers = (searchTerm: string) => setServersList(
|
const filterServers = (searchTerm: string) => setServersList(
|
||||||
|
@ -83,3 +93,10 @@ export const ManageServers = (
|
||||||
</NoMenuLayout>
|
</NoMenuLayout>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const ManageServersFactory = componentFactory(ManageServers, [
|
||||||
|
'ServersExporter',
|
||||||
|
'ImportServersBtn',
|
||||||
|
'useTimeoutToggle',
|
||||||
|
'ManageServersRow',
|
||||||
|
]);
|
||||||
|
|
|
@ -3,17 +3,24 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import type { FC } from 'react';
|
import type { FC } from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { UncontrolledTooltip } from 'reactstrap';
|
import { UncontrolledTooltip } from 'reactstrap';
|
||||||
|
import type { FCWithDeps } from '../container/utils';
|
||||||
|
import { componentFactory, useDependencies } from '../container/utils';
|
||||||
import type { ServerWithId } from './data';
|
import type { ServerWithId } from './data';
|
||||||
import type { ManageServersRowDropdownProps } from './ManageServersRowDropdown';
|
import type { ManageServersRowDropdownProps } from './ManageServersRowDropdown';
|
||||||
|
|
||||||
export interface ManageServersRowProps {
|
export type ManageServersRowProps = {
|
||||||
server: ServerWithId;
|
server: ServerWithId;
|
||||||
hasAutoConnect: boolean;
|
hasAutoConnect: boolean;
|
||||||
}
|
};
|
||||||
|
|
||||||
export const ManageServersRow = (
|
type ManageServersRowDeps = {
|
||||||
ManageServersRowDropdown: FC<ManageServersRowDropdownProps>,
|
ManageServersRowDropdown: FC<ManageServersRowDropdownProps>;
|
||||||
): FC<ManageServersRowProps> => ({ server, hasAutoConnect }) => (
|
};
|
||||||
|
|
||||||
|
const ManageServersRow: FCWithDeps<ManageServersRowProps, ManageServersRowDeps> = ({ server, hasAutoConnect }) => {
|
||||||
|
const { ManageServersRowDropdown } = useDependencies(ManageServersRow);
|
||||||
|
|
||||||
|
return (
|
||||||
<tr className="responsive-table__row">
|
<tr className="responsive-table__row">
|
||||||
{hasAutoConnect && (
|
{hasAutoConnect && (
|
||||||
<td className="responsive-table__cell" data-th="Auto-connect">
|
<td className="responsive-table__cell" data-th="Auto-connect">
|
||||||
|
@ -35,4 +42,7 @@ export const ManageServersRow = (
|
||||||
<ManageServersRowDropdown server={server} />
|
<ManageServersRowDropdown server={server} />
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ManageServersRowFactory = componentFactory(ManageServersRow, ['ManageServersRowDropdown']);
|
||||||
|
|
|
@ -10,20 +10,27 @@ import { RowDropdownBtn, useToggle } from '@shlinkio/shlink-frontend-kit';
|
||||||
import type { FC } from 'react';
|
import type { FC } from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { DropdownItem } from 'reactstrap';
|
import { DropdownItem } from 'reactstrap';
|
||||||
|
import type { FCWithDeps } from '../container/utils';
|
||||||
|
import { componentFactory, useDependencies } from '../container/utils';
|
||||||
import type { ServerWithId } from './data';
|
import type { ServerWithId } from './data';
|
||||||
import type { DeleteServerModalProps } from './DeleteServerModal';
|
import type { DeleteServerModalProps } from './DeleteServerModal';
|
||||||
|
|
||||||
export interface ManageServersRowDropdownProps {
|
export type ManageServersRowDropdownProps = {
|
||||||
server: ServerWithId;
|
server: ServerWithId;
|
||||||
}
|
};
|
||||||
|
|
||||||
interface ManageServersRowDropdownConnectProps extends ManageServersRowDropdownProps {
|
type ManageServersRowDropdownConnectProps = ManageServersRowDropdownProps & {
|
||||||
setAutoConnect: (server: ServerWithId, autoConnect: boolean) => void;
|
setAutoConnect: (server: ServerWithId, autoConnect: boolean) => void;
|
||||||
}
|
};
|
||||||
|
|
||||||
export const ManageServersRowDropdown = (
|
type ManageServersRowDropdownDeps = {
|
||||||
DeleteServerModal: FC<DeleteServerModalProps>,
|
DeleteServerModal: FC<DeleteServerModalProps>
|
||||||
): FC<ManageServersRowDropdownConnectProps> => ({ server, setAutoConnect }) => {
|
};
|
||||||
|
|
||||||
|
const ManageServersRowDropdown: FCWithDeps<ManageServersRowDropdownConnectProps, ManageServersRowDropdownDeps> = (
|
||||||
|
{ server, setAutoConnect },
|
||||||
|
) => {
|
||||||
|
const { DeleteServerModal } = useDependencies(ManageServersRowDropdown);
|
||||||
const [isModalOpen,, showModal, hideModal] = useToggle();
|
const [isModalOpen,, showModal, hideModal] = useToggle();
|
||||||
const serverUrl = `/server/${server.id}`;
|
const serverUrl = `/server/${server.id}`;
|
||||||
const { autoConnect: isAutoConnect } = server;
|
const { autoConnect: isAutoConnect } = server;
|
||||||
|
@ -49,3 +56,5 @@ export const ManageServersRowDropdown = (
|
||||||
</RowDropdownBtn>
|
</RowDropdownBtn>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const ManageServersRowDropdownFactory = componentFactory(ManageServersRowDropdown, ['DeleteServerModal']);
|
||||||
|
|
|
@ -2,9 +2,11 @@ import { faFileUpload as importIcon } from '@fortawesome/free-solid-svg-icons';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import { useElementRef, useToggle } from '@shlinkio/shlink-frontend-kit';
|
import { useElementRef, useToggle } from '@shlinkio/shlink-frontend-kit';
|
||||||
import { complement } from 'ramda';
|
import { complement } from 'ramda';
|
||||||
import type { ChangeEvent, FC, PropsWithChildren } from 'react';
|
import type { ChangeEvent, PropsWithChildren } from 'react';
|
||||||
import { useCallback, useRef, useState } from 'react';
|
import { useCallback, useRef, useState } from 'react';
|
||||||
import { Button, UncontrolledTooltip } from 'reactstrap';
|
import { Button, UncontrolledTooltip } from 'reactstrap';
|
||||||
|
import type { FCWithDeps } from '../../container/utils';
|
||||||
|
import { componentFactory, useDependencies } from '../../container/utils';
|
||||||
import type { ServerData, ServersMap } from '../data';
|
import type { ServerData, ServersMap } from '../data';
|
||||||
import type { ServersImporter } from '../services/ServersImporter';
|
import type { ServersImporter } from '../services/ServersImporter';
|
||||||
import { DuplicatedServersModal } from './DuplicatedServersModal';
|
import { DuplicatedServersModal } from './DuplicatedServersModal';
|
||||||
|
@ -16,15 +18,19 @@ export type ImportServersBtnProps = PropsWithChildren<{
|
||||||
className?: string;
|
className?: string;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
interface ImportServersBtnConnectProps extends ImportServersBtnProps {
|
type ImportServersBtnConnectProps = ImportServersBtnProps & {
|
||||||
createServers: (servers: ServerData[]) => void;
|
createServers: (servers: ServerData[]) => void;
|
||||||
servers: ServersMap;
|
servers: ServersMap;
|
||||||
}
|
};
|
||||||
|
|
||||||
|
type ImportServersBtnDeps = {
|
||||||
|
ServersImporter: ServersImporter
|
||||||
|
};
|
||||||
|
|
||||||
const serversFiltering = (servers: ServerData[]) =>
|
const serversFiltering = (servers: ServerData[]) =>
|
||||||
({ url, apiKey }: ServerData) => servers.some((server) => server.url === url && server.apiKey === apiKey);
|
({ url, apiKey }: ServerData) => servers.some((server) => server.url === url && server.apiKey === apiKey);
|
||||||
|
|
||||||
export const ImportServersBtn = (serversImporter: ServersImporter): FC<ImportServersBtnConnectProps> => ({
|
const ImportServersBtn: FCWithDeps<ImportServersBtnConnectProps, ImportServersBtnDeps> = ({
|
||||||
createServers,
|
createServers,
|
||||||
servers,
|
servers,
|
||||||
children,
|
children,
|
||||||
|
@ -33,6 +39,7 @@ export const ImportServersBtn = (serversImporter: ServersImporter): FC<ImportSer
|
||||||
tooltipPlacement = 'bottom',
|
tooltipPlacement = 'bottom',
|
||||||
className = '',
|
className = '',
|
||||||
}) => {
|
}) => {
|
||||||
|
const { ServersImporter: serversImporter } = useDependencies(ImportServersBtn);
|
||||||
const ref = useElementRef<HTMLInputElement>();
|
const ref = useElementRef<HTMLInputElement>();
|
||||||
const [duplicatedServers, setDuplicatedServers] = useState<ServerData[]>([]);
|
const [duplicatedServers, setDuplicatedServers] = useState<ServerData[]>([]);
|
||||||
const [isModalOpen,, showModal, hideModal] = useToggle();
|
const [isModalOpen,, showModal, hideModal] = useToggle();
|
||||||
|
@ -60,7 +67,7 @@ export const ImportServersBtn = (serversImporter: ServersImporter): FC<ImportSer
|
||||||
(target as { value: string | null }).value = null; // eslint-disable-line no-param-reassign
|
(target as { value: string | null }).value = null; // eslint-disable-line no-param-reassign
|
||||||
})
|
})
|
||||||
.catch(onImportError),
|
.catch(onImportError),
|
||||||
[create, onImportError, servers, showModal],
|
[create, onImportError, servers, serversImporter, showModal],
|
||||||
);
|
);
|
||||||
|
|
||||||
const createAllServers = useCallback(() => {
|
const createAllServers = useCallback(() => {
|
||||||
|
@ -92,3 +99,5 @@ export const ImportServersBtn = (serversImporter: ServersImporter): FC<ImportSer
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const ImportServersBtnFactory = componentFactory(ImportServersBtn, ['ServersImporter']);
|
||||||
|
|
|
@ -2,20 +2,27 @@ import { Message } from '@shlinkio/shlink-frontend-kit';
|
||||||
import type { FC } from 'react';
|
import type { FC } from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { NoMenuLayout } from '../../common/NoMenuLayout';
|
import { NoMenuLayout } from '../../common/NoMenuLayout';
|
||||||
|
import type { FCWithDeps } from '../../container/utils';
|
||||||
|
import { componentFactory, useDependencies } from '../../container/utils';
|
||||||
import type { SelectedServer, ServersMap } from '../data';
|
import type { SelectedServer, ServersMap } from '../data';
|
||||||
import { isServerWithId } from '../data';
|
import { isServerWithId } from '../data';
|
||||||
import type { DeleteServerButtonProps } from '../DeleteServerButton';
|
import type { DeleteServerButtonProps } from '../DeleteServerButton';
|
||||||
import { ServersListGroup } from '../ServersListGroup';
|
import { ServersListGroup } from '../ServersListGroup';
|
||||||
import './ServerError.scss';
|
import './ServerError.scss';
|
||||||
|
|
||||||
interface ServerErrorProps {
|
type ServerErrorProps = {
|
||||||
servers: ServersMap;
|
servers: ServersMap;
|
||||||
selectedServer: SelectedServer;
|
selectedServer: SelectedServer;
|
||||||
}
|
};
|
||||||
|
|
||||||
export const ServerError = (DeleteServerButton: FC<DeleteServerButtonProps>): FC<ServerErrorProps> => (
|
type ServerErrorDeps = {
|
||||||
{ servers, selectedServer },
|
DeleteServerButton: FC<DeleteServerButtonProps>;
|
||||||
) => (
|
};
|
||||||
|
|
||||||
|
const ServerError: FCWithDeps<ServerErrorProps, ServerErrorDeps> = ({ servers, selectedServer }) => {
|
||||||
|
const { DeleteServerButton } = useDependencies(ServerError);
|
||||||
|
|
||||||
|
return (
|
||||||
<NoMenuLayout>
|
<NoMenuLayout>
|
||||||
<div className="server-error__container flex-column">
|
<div className="server-error__container flex-column">
|
||||||
<Message className="w-100 mb-3 mb-md-5" type="error" fullWidth>
|
<Message className="w-100 mb-3 mb-md-5" type="error" fullWidth>
|
||||||
|
@ -44,4 +51,7 @@ export const ServerError = (DeleteServerButton: FC<DeleteServerButtonProps>): FC
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</NoMenuLayout>
|
</NoMenuLayout>
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ServerErrorFactory = componentFactory(ServerError, ['DeleteServerButton']);
|
||||||
|
|
|
@ -3,16 +3,25 @@ import type { FC } from 'react';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
import { NoMenuLayout } from '../../common/NoMenuLayout';
|
import { NoMenuLayout } from '../../common/NoMenuLayout';
|
||||||
|
import type { FCWithDeps } from '../../container/utils';
|
||||||
|
import { useDependencies } from '../../container/utils';
|
||||||
import type { SelectedServer } from '../data';
|
import type { SelectedServer } from '../data';
|
||||||
import { isNotFoundServer } from '../data';
|
import { isNotFoundServer } from '../data';
|
||||||
|
|
||||||
interface WithSelectedServerProps {
|
export type WithSelectedServerProps = {
|
||||||
selectServer: (serverId: string) => void;
|
selectServer: (serverId: string) => void;
|
||||||
selectedServer: SelectedServer;
|
selectedServer: SelectedServer;
|
||||||
}
|
};
|
||||||
|
|
||||||
export function withSelectedServer<T = {}>(WrappedComponent: FC<WithSelectedServerProps & T>, ServerError: FC) {
|
type WithSelectedServerPropsDeps = {
|
||||||
return (props: WithSelectedServerProps & T) => {
|
ServerError: FC;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function withSelectedServer<T = {}>(
|
||||||
|
WrappedComponent: FCWithDeps<WithSelectedServerProps & T, WithSelectedServerPropsDeps>,
|
||||||
|
) {
|
||||||
|
const ComponentWrapper: FCWithDeps<WithSelectedServerProps & T, WithSelectedServerPropsDeps> = (props) => {
|
||||||
|
const { ServerError } = useDependencies(ComponentWrapper);
|
||||||
const params = useParams<{ serverId: string }>();
|
const params = useParams<{ serverId: string }>();
|
||||||
const { selectServer, selectedServer } = props;
|
const { selectServer, selectedServer } = props;
|
||||||
|
|
||||||
|
@ -34,4 +43,5 @@ export function withSelectedServer<T = {}>(WrappedComponent: FC<WithSelectedServ
|
||||||
|
|
||||||
return <WrappedComponent {...props} />;
|
return <WrappedComponent {...props} />;
|
||||||
};
|
};
|
||||||
|
return ComponentWrapper;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,16 +1,16 @@
|
||||||
import type Bottle from 'bottlejs';
|
import type Bottle from 'bottlejs';
|
||||||
import { prop } from 'ramda';
|
import { prop } from 'ramda';
|
||||||
import type { ConnectDecorator } from '../../container/types';
|
import type { ConnectDecorator } from '../../container/types';
|
||||||
import { CreateServer } from '../CreateServer';
|
import { CreateServerFactory } from '../CreateServer';
|
||||||
import { DeleteServerButton } from '../DeleteServerButton';
|
import { DeleteServerButtonFactory } from '../DeleteServerButton';
|
||||||
import { DeleteServerModal } from '../DeleteServerModal';
|
import { DeleteServerModal } from '../DeleteServerModal';
|
||||||
import { EditServer } from '../EditServer';
|
import { EditServerFactory } from '../EditServer';
|
||||||
import { ImportServersBtn } from '../helpers/ImportServersBtn';
|
import { ImportServersBtnFactory } from '../helpers/ImportServersBtn';
|
||||||
import { ServerError } from '../helpers/ServerError';
|
import { ServerErrorFactory } from '../helpers/ServerError';
|
||||||
import { withoutSelectedServer } from '../helpers/withoutSelectedServer';
|
import { withoutSelectedServer } from '../helpers/withoutSelectedServer';
|
||||||
import { ManageServers } from '../ManageServers';
|
import { ManageServersFactory } from '../ManageServers';
|
||||||
import { ManageServersRow } from '../ManageServersRow';
|
import { ManageServersRowFactory } from '../ManageServersRow';
|
||||||
import { ManageServersRowDropdown } from '../ManageServersRowDropdown';
|
import { ManageServersRowDropdownFactory } from '../ManageServersRowDropdown';
|
||||||
import { fetchServers } from '../reducers/remoteServers';
|
import { fetchServers } from '../reducers/remoteServers';
|
||||||
import {
|
import {
|
||||||
resetSelectedServer,
|
resetSelectedServer,
|
||||||
|
@ -24,27 +24,20 @@ import { ServersImporter } from './ServersImporter';
|
||||||
|
|
||||||
export const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
|
export const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
|
||||||
// Components
|
// Components
|
||||||
bottle.serviceFactory(
|
bottle.factory('ManageServers', ManageServersFactory);
|
||||||
'ManageServers',
|
|
||||||
ManageServers,
|
|
||||||
'ServersExporter',
|
|
||||||
'ImportServersBtn',
|
|
||||||
'useTimeoutToggle',
|
|
||||||
'ManageServersRow',
|
|
||||||
);
|
|
||||||
bottle.decorator('ManageServers', withoutSelectedServer);
|
bottle.decorator('ManageServers', withoutSelectedServer);
|
||||||
bottle.decorator('ManageServers', connect(['selectedServer', 'servers'], ['resetSelectedServer']));
|
bottle.decorator('ManageServers', connect(['selectedServer', 'servers'], ['resetSelectedServer']));
|
||||||
|
|
||||||
bottle.serviceFactory('ManageServersRow', ManageServersRow, 'ManageServersRowDropdown');
|
bottle.factory('ManageServersRow', ManageServersRowFactory);
|
||||||
|
|
||||||
bottle.serviceFactory('ManageServersRowDropdown', ManageServersRowDropdown, 'DeleteServerModal');
|
bottle.factory('ManageServersRowDropdown', ManageServersRowDropdownFactory);
|
||||||
bottle.decorator('ManageServersRowDropdown', connect(null, ['setAutoConnect']));
|
bottle.decorator('ManageServersRowDropdown', connect(null, ['setAutoConnect']));
|
||||||
|
|
||||||
bottle.serviceFactory('CreateServer', CreateServer, 'ImportServersBtn', 'useTimeoutToggle');
|
bottle.factory('CreateServer', CreateServerFactory);
|
||||||
bottle.decorator('CreateServer', withoutSelectedServer);
|
bottle.decorator('CreateServer', withoutSelectedServer);
|
||||||
bottle.decorator('CreateServer', connect(['selectedServer', 'servers'], ['createServers', 'resetSelectedServer']));
|
bottle.decorator('CreateServer', connect(['selectedServer', 'servers'], ['createServers', 'resetSelectedServer']));
|
||||||
|
|
||||||
bottle.serviceFactory('EditServer', EditServer, 'ServerError');
|
bottle.factory('EditServer', EditServerFactory);
|
||||||
bottle.decorator('EditServer', connect(['selectedServer'], ['editServer', 'selectServer', 'resetSelectedServer']));
|
bottle.decorator('EditServer', connect(['selectedServer'], ['editServer', 'selectServer', 'resetSelectedServer']));
|
||||||
|
|
||||||
bottle.serviceFactory('ServersDropdown', () => ServersDropdown);
|
bottle.serviceFactory('ServersDropdown', () => ServersDropdown);
|
||||||
|
@ -53,12 +46,12 @@ export const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
|
||||||
bottle.serviceFactory('DeleteServerModal', () => DeleteServerModal);
|
bottle.serviceFactory('DeleteServerModal', () => DeleteServerModal);
|
||||||
bottle.decorator('DeleteServerModal', connect(null, ['deleteServer']));
|
bottle.decorator('DeleteServerModal', connect(null, ['deleteServer']));
|
||||||
|
|
||||||
bottle.serviceFactory('DeleteServerButton', DeleteServerButton, 'DeleteServerModal');
|
bottle.factory('DeleteServerButton', DeleteServerButtonFactory);
|
||||||
|
|
||||||
bottle.serviceFactory('ImportServersBtn', ImportServersBtn, 'ServersImporter');
|
bottle.factory('ImportServersBtn', ImportServersBtnFactory);
|
||||||
bottle.decorator('ImportServersBtn', connect(['servers'], ['createServers']));
|
bottle.decorator('ImportServersBtn', connect(['servers'], ['createServers']));
|
||||||
|
|
||||||
bottle.serviceFactory('ServerError', ServerError, 'DeleteServerButton');
|
bottle.factory('ServerError', ServerErrorFactory);
|
||||||
bottle.decorator('ServerError', connect(['servers', 'selectedServer']));
|
bottle.decorator('ServerError', connect(['servers', 'selectedServer']));
|
||||||
|
|
||||||
// Services
|
// Services
|
||||||
|
|
|
@ -2,6 +2,17 @@ import { NavPillItem, NavPills } from '@shlinkio/shlink-frontend-kit';
|
||||||
import type { FC, ReactNode } from 'react';
|
import type { FC, ReactNode } from 'react';
|
||||||
import { Navigate, Route, Routes } from 'react-router-dom';
|
import { Navigate, Route, Routes } from 'react-router-dom';
|
||||||
import { NoMenuLayout } from '../common/NoMenuLayout';
|
import { NoMenuLayout } from '../common/NoMenuLayout';
|
||||||
|
import type { FCWithDeps } from '../container/utils';
|
||||||
|
import { componentFactory, useDependencies } from '../container/utils';
|
||||||
|
|
||||||
|
type SettingsDeps = {
|
||||||
|
RealTimeUpdatesSettings: FC;
|
||||||
|
ShortUrlCreationSettings: FC;
|
||||||
|
ShortUrlsListSettings: FC;
|
||||||
|
UserInterfaceSettings: FC;
|
||||||
|
VisitsSettings: FC;
|
||||||
|
TagsSettings: FC;
|
||||||
|
};
|
||||||
|
|
||||||
const SettingsSections: FC<{ items: ReactNode[] }> = ({ items }) => (
|
const SettingsSections: FC<{ items: ReactNode[] }> = ({ items }) => (
|
||||||
<>
|
<>
|
||||||
|
@ -9,14 +20,17 @@ const SettingsSections: FC<{ items: ReactNode[] }> = ({ items }) => (
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
export const Settings = (
|
const Settings: FCWithDeps<{}, SettingsDeps> = () => {
|
||||||
RealTimeUpdates: FC,
|
const {
|
||||||
ShortUrlCreation: FC,
|
RealTimeUpdatesSettings: RealTimeUpdates,
|
||||||
ShortUrlsList: FC,
|
ShortUrlCreationSettings: ShortUrlCreation,
|
||||||
UserInterface: FC,
|
ShortUrlsListSettings: ShortUrlsList,
|
||||||
Visits: FC,
|
UserInterfaceSettings: UserInterface,
|
||||||
Tags: FC,
|
VisitsSettings: Visits,
|
||||||
) => () => (
|
TagsSettings: Tags,
|
||||||
|
} = useDependencies(Settings);
|
||||||
|
|
||||||
|
return (
|
||||||
<NoMenuLayout>
|
<NoMenuLayout>
|
||||||
<NavPills className="mb-3">
|
<NavPills className="mb-3">
|
||||||
<NavPillItem to="general">General</NavPillItem>
|
<NavPillItem to="general">General</NavPillItem>
|
||||||
|
@ -31,4 +45,14 @@ export const Settings = (
|
||||||
<Route path="*" element={<Navigate replace to="general" />} />
|
<Route path="*" element={<Navigate replace to="general" />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</NoMenuLayout>
|
</NoMenuLayout>
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SettingsFactory = componentFactory(Settings, [
|
||||||
|
'RealTimeUpdatesSettings',
|
||||||
|
'ShortUrlCreationSettings',
|
||||||
|
'ShortUrlsListSettings',
|
||||||
|
'UserInterfaceSettings',
|
||||||
|
'VisitsSettings',
|
||||||
|
'TagsSettings',
|
||||||
|
]);
|
||||||
|
|
|
@ -11,7 +11,7 @@ import {
|
||||||
setVisitsSettings,
|
setVisitsSettings,
|
||||||
toggleRealTimeUpdates,
|
toggleRealTimeUpdates,
|
||||||
} from '../reducers/settings';
|
} from '../reducers/settings';
|
||||||
import { Settings } from '../Settings';
|
import { SettingsFactory } from '../Settings';
|
||||||
import { ShortUrlCreationSettings } from '../ShortUrlCreationSettings';
|
import { ShortUrlCreationSettings } from '../ShortUrlCreationSettings';
|
||||||
import { ShortUrlsListSettings } from '../ShortUrlsListSettings';
|
import { ShortUrlsListSettings } from '../ShortUrlsListSettings';
|
||||||
import { TagsSettings } from '../TagsSettings';
|
import { TagsSettings } from '../TagsSettings';
|
||||||
|
@ -20,16 +20,7 @@ import { VisitsSettings } from '../VisitsSettings';
|
||||||
|
|
||||||
export const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
|
export const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
|
||||||
// Components
|
// Components
|
||||||
bottle.serviceFactory(
|
bottle.factory('Settings', SettingsFactory);
|
||||||
'Settings',
|
|
||||||
Settings,
|
|
||||||
'RealTimeUpdatesSettings',
|
|
||||||
'ShortUrlCreationSettings',
|
|
||||||
'ShortUrlsListSettings',
|
|
||||||
'UserInterfaceSettings',
|
|
||||||
'VisitsSettings',
|
|
||||||
'TagsSettings',
|
|
||||||
);
|
|
||||||
bottle.decorator('Settings', withoutSelectedServer);
|
bottle.decorator('Settings', withoutSelectedServer);
|
||||||
bottle.decorator('Settings', connect(null, ['resetSelectedServer']));
|
bottle.decorator('Settings', connect(null, ['resetSelectedServer']));
|
||||||
|
|
||||||
|
|
|
@ -1,26 +1,31 @@
|
||||||
import { parseQuery } from '@shlinkio/shlink-frontend-kit';
|
import { parseQuery } from '@shlinkio/shlink-frontend-kit';
|
||||||
import { useRef, useState } from 'react';
|
import { useCallback, useRef, useState } from 'react';
|
||||||
import { useLocation, useNavigate } from 'react-router-dom';
|
import { useLocation, useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
const DEFAULT_DELAY = 2000;
|
const DEFAULT_DELAY = 2000;
|
||||||
|
|
||||||
export type TimeoutToggle = (initialValue?: boolean, delay?: number) => [boolean, () => void];
|
export type TimeoutToggle = typeof useTimeoutToggle;
|
||||||
|
|
||||||
export const useTimeoutToggle = (
|
export const useTimeoutToggle = (
|
||||||
setTimeout: (callback: Function, timeout: number) => number,
|
initialValue = false,
|
||||||
clearTimeout: (timer: number) => void,
|
delay = DEFAULT_DELAY,
|
||||||
): TimeoutToggle => (initialValue = false, delay = DEFAULT_DELAY) => {
|
|
||||||
|
// Test seams
|
||||||
|
setTimeout = window.setTimeout,
|
||||||
|
clearTimeout = window.clearTimeout,
|
||||||
|
): [boolean, () => void] => {
|
||||||
const [flag, setFlag] = useState<boolean>(initialValue);
|
const [flag, setFlag] = useState<boolean>(initialValue);
|
||||||
|
const initialValueRef = useRef(initialValue);
|
||||||
const timeout = useRef<number | undefined>(undefined);
|
const timeout = useRef<number | undefined>(undefined);
|
||||||
const callback = () => {
|
const callback = useCallback(() => {
|
||||||
setFlag(!initialValue);
|
setFlag(!initialValueRef.current);
|
||||||
|
|
||||||
if (timeout.current) {
|
if (timeout.current) {
|
||||||
clearTimeout(timeout.current);
|
clearTimeout(timeout.current);
|
||||||
}
|
}
|
||||||
|
|
||||||
timeout.current = setTimeout(() => setFlag(initialValue), delay);
|
timeout.current = setTimeout(() => setFlag(initialValueRef.current), delay);
|
||||||
};
|
}, [clearTimeout, delay, setTimeout]);
|
||||||
|
|
||||||
return [flag, callback];
|
return [flag, callback];
|
||||||
};
|
};
|
||||||
|
|
|
@ -12,7 +12,5 @@ export const provideServices = (bottle: Bottle) => {
|
||||||
bottle.constant('csvToJson', csvToJson);
|
bottle.constant('csvToJson', csvToJson);
|
||||||
bottle.constant('jsonToCsv', jsonToCsv);
|
bottle.constant('jsonToCsv', jsonToCsv);
|
||||||
|
|
||||||
bottle.constant('setTimeout', window.setTimeout);
|
bottle.serviceFactory('useTimeoutToggle', () => useTimeoutToggle);
|
||||||
bottle.constant('clearTimeout', window.clearTimeout);
|
|
||||||
bottle.serviceFactory('useTimeoutToggle', useTimeoutToggle, 'setTimeout', 'clearTimeout');
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -2,18 +2,20 @@ import { render, screen } 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-dom';
|
import { Router } from 'react-router-dom';
|
||||||
import { App as createApp } from '../../src/app/App';
|
import { AppFactory } from '../../src/app/App';
|
||||||
|
|
||||||
describe('<App />', () => {
|
describe('<App />', () => {
|
||||||
const App = createApp(
|
const App = AppFactory(
|
||||||
() => <>MainHeader</>,
|
fromPartial({
|
||||||
() => <>Home</>,
|
MainHeader: () => <>MainHeader</>,
|
||||||
() => <>ShlinkWebComponentContainer</>,
|
Home: () => <>Home</>,
|
||||||
() => <>CreateServer</>,
|
ShlinkWebComponentContainer: () => <>ShlinkWebComponentContainer</>,
|
||||||
() => <>EditServer</>,
|
CreateServer: () => <>CreateServer</>,
|
||||||
() => <>SettingsComp</>,
|
EditServer: () => <>EditServer</>,
|
||||||
() => <>ManageServers</>,
|
Settings: () => <>SettingsComp</>,
|
||||||
() => <>ShlinkVersions</>,
|
ManageServers: () => <>ManageServers</>,
|
||||||
|
ShlinkVersionsContainer: () => <>ShlinkVersions</>,
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
const setUp = (activeRoute = '/') => {
|
const setUp = (activeRoute = '/') => {
|
||||||
const history = createMemoryHistory();
|
const history = createMemoryHistory();
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { render, screen } from '@testing-library/react';
|
import { render, screen } from '@testing-library/react';
|
||||||
import { fromPartial } from '@total-typescript/shoehorn';
|
import { fromPartial } from '@total-typescript/shoehorn';
|
||||||
import { ErrorHandler as createErrorHandler } from '../../src/common/ErrorHandler';
|
import type { PropsWithChildren } from 'react';
|
||||||
|
import { ErrorHandler as BaseErrorHandler } from '../../src/common/ErrorHandler';
|
||||||
import { renderWithEvents } from '../__helpers__/setUpTest';
|
import { renderWithEvents } from '../__helpers__/setUpTest';
|
||||||
|
|
||||||
const ComponentWithError = () => {
|
const ComponentWithError = () => {
|
||||||
|
@ -9,18 +10,16 @@ const ComponentWithError = () => {
|
||||||
|
|
||||||
describe('<ErrorHandler />', () => {
|
describe('<ErrorHandler />', () => {
|
||||||
const reload = vi.fn();
|
const reload = vi.fn();
|
||||||
const window = fromPartial<Window>({
|
const location = fromPartial<Window['location']>({ reload });
|
||||||
location: { reload },
|
|
||||||
});
|
|
||||||
const cons = fromPartial<Console>({ error: vi.fn() });
|
const cons = fromPartial<Console>({ error: vi.fn() });
|
||||||
const ErrorHandler = createErrorHandler(window, cons);
|
const ErrorHandler = (props: PropsWithChildren) => <BaseErrorHandler console={cons} location={location} {...props} />;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.spyOn(console, 'error').mockImplementation(() => {}); // Silence react errors
|
vi.spyOn(console, 'error').mockImplementation(() => {}); // Silence react errors
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders children when no error has occurred', () => {
|
it('renders children when no error has occurred', () => {
|
||||||
render(<ErrorHandler children={<span>Foo</span>} />);
|
render(<ErrorHandler><span>Foo</span></ErrorHandler>);
|
||||||
|
|
||||||
expect(screen.getByText('Foo')).toBeInTheDocument();
|
expect(screen.getByText('Foo')).toBeInTheDocument();
|
||||||
expect(screen.queryByText('Oops! This is awkward :S')).not.toBeInTheDocument();
|
expect(screen.queryByText('Oops! This is awkward :S')).not.toBeInTheDocument();
|
||||||
|
@ -28,14 +27,14 @@ describe('<ErrorHandler />', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders error page when error has occurred', () => {
|
it('renders error page when error has occurred', () => {
|
||||||
render(<ErrorHandler children={<ComponentWithError />} />);
|
render(<ErrorHandler><ComponentWithError /></ErrorHandler>);
|
||||||
|
|
||||||
expect(screen.getByText('Oops! This is awkward :S')).toBeInTheDocument();
|
expect(screen.getByText('Oops! This is awkward :S')).toBeInTheDocument();
|
||||||
expect(screen.getByRole('button')).toBeInTheDocument();
|
expect(screen.getByRole('button')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('reloads page on button click', async () => {
|
it('reloads page on button click', async () => {
|
||||||
const { user } = renderWithEvents(<ErrorHandler children={<ComponentWithError />} />);
|
const { user } = renderWithEvents(<ErrorHandler><ComponentWithError /></ErrorHandler>);
|
||||||
|
|
||||||
expect(reload).not.toHaveBeenCalled();
|
expect(reload).not.toHaveBeenCalled();
|
||||||
await user.click(screen.getByRole('button'));
|
await user.click(screen.getByRole('button'));
|
||||||
|
|
|
@ -1,11 +1,14 @@
|
||||||
import { screen, waitFor } from '@testing-library/react';
|
import { screen, waitFor } from '@testing-library/react';
|
||||||
|
import { fromPartial } from '@total-typescript/shoehorn';
|
||||||
import { createMemoryHistory } from 'history';
|
import { createMemoryHistory } from 'history';
|
||||||
import { Router } from 'react-router-dom';
|
import { Router } from 'react-router-dom';
|
||||||
import { MainHeader as createMainHeader } from '../../src/common/MainHeader';
|
import { MainHeaderFactory } from '../../src/common/MainHeader';
|
||||||
import { renderWithEvents } from '../__helpers__/setUpTest';
|
import { renderWithEvents } from '../__helpers__/setUpTest';
|
||||||
|
|
||||||
describe('<MainHeader />', () => {
|
describe('<MainHeader />', () => {
|
||||||
const MainHeader = createMainHeader(() => <>ServersDropdown</>);
|
const MainHeader = MainHeaderFactory(fromPartial({
|
||||||
|
ServersDropdown: () => <>ServersDropdown</>,
|
||||||
|
}));
|
||||||
const setUp = (pathname = '') => {
|
const setUp = (pathname = '') => {
|
||||||
const history = createMemoryHistory();
|
const history = createMemoryHistory();
|
||||||
history.push(pathname);
|
history.push(pathname);
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { render, screen } from '@testing-library/react';
|
import { render, screen } from '@testing-library/react';
|
||||||
import { fromPartial } from '@total-typescript/shoehorn';
|
import { fromPartial } from '@total-typescript/shoehorn';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
import { ShlinkWebComponentContainer as createContainer } from '../../src/common/ShlinkWebComponentContainer';
|
import { ShlinkWebComponentContainerFactory } from '../../src/common/ShlinkWebComponentContainer';
|
||||||
import type { NonReachableServer, NotFoundServer, SelectedServer } from '../../src/servers/data';
|
import type { NonReachableServer, NotFoundServer, SelectedServer } from '../../src/servers/data';
|
||||||
|
|
||||||
vi.mock('react-router-dom', async () => ({
|
vi.mock('react-router-dom', async () => ({
|
||||||
|
@ -10,12 +10,12 @@ vi.mock('react-router-dom', async () => ({
|
||||||
}));
|
}));
|
||||||
|
|
||||||
describe('<ShlinkWebComponentContainer />', () => {
|
describe('<ShlinkWebComponentContainer />', () => {
|
||||||
const ShlinkWebComponentContainer = createContainer(
|
const ShlinkWebComponentContainer = ShlinkWebComponentContainerFactory(fromPartial({
|
||||||
vi.fn().mockReturnValue(fromPartial({})),
|
buildShlinkApiClient: vi.fn().mockReturnValue(fromPartial({})),
|
||||||
fromPartial({}),
|
TagColorsStorage: fromPartial({}),
|
||||||
() => <>ShlinkWebComponent</>,
|
ShlinkWebComponent: () => <>ShlinkWebComponent</>,
|
||||||
() => <>ServerError</>,
|
ServerError: () => <>ServerError</>,
|
||||||
);
|
}));
|
||||||
const setUp = (selectedServer: SelectedServer) => render(
|
const setUp = (selectedServer: SelectedServer) => render(
|
||||||
<ShlinkWebComponentContainer selectServer={vi.fn()} selectedServer={selectedServer} settings={{}} />,
|
<ShlinkWebComponentContainer selectServer={vi.fn()} selectedServer={selectedServer} settings={{}} />,
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { fireEvent, screen, waitFor } from '@testing-library/react';
|
import { fireEvent, screen, waitFor } from '@testing-library/react';
|
||||||
import { fromPartial } from '@total-typescript/shoehorn';
|
import { fromPartial } from '@total-typescript/shoehorn';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { CreateServer as createCreateServer } from '../../src/servers/CreateServer';
|
import { CreateServerFactory } from '../../src/servers/CreateServer';
|
||||||
import type { ServersMap } from '../../src/servers/data';
|
import type { ServersMap } from '../../src/servers/data';
|
||||||
import { renderWithEvents } from '../__helpers__/setUpTest';
|
import { renderWithEvents } from '../__helpers__/setUpTest';
|
||||||
|
|
||||||
|
@ -31,7 +31,10 @@ describe('<CreateServer />', () => {
|
||||||
callCount += 1;
|
callCount += 1;
|
||||||
return result;
|
return result;
|
||||||
});
|
});
|
||||||
const CreateServer = createCreateServer(() => <>ImportServersBtn</>, useTimeoutToggle);
|
const CreateServer = CreateServerFactory(fromPartial({
|
||||||
|
ImportServersBtn: () => <>ImportServersBtn</>,
|
||||||
|
useTimeoutToggle,
|
||||||
|
}));
|
||||||
|
|
||||||
return renderWithEvents(<CreateServer createServers={createServersMock} servers={servers} />);
|
return renderWithEvents(<CreateServer createServers={createServersMock} servers={servers} />);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,13 +1,14 @@
|
||||||
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 type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
import { DeleteServerButton as createDeleteServerButton } from '../../src/servers/DeleteServerButton';
|
import { DeleteServerButtonFactory } from '../../src/servers/DeleteServerButton';
|
||||||
|
import type { DeleteServerModalProps } from '../../src/servers/DeleteServerModal';
|
||||||
import { renderWithEvents } from '../__helpers__/setUpTest';
|
import { renderWithEvents } from '../__helpers__/setUpTest';
|
||||||
|
|
||||||
describe('<DeleteServerButton />', () => {
|
describe('<DeleteServerButton />', () => {
|
||||||
const DeleteServerButton = createDeleteServerButton(
|
const DeleteServerButton = DeleteServerButtonFactory(fromPartial({
|
||||||
({ isOpen }) => <>DeleteServerModal {isOpen ? '[Open]' : '[Closed]'}</>,
|
DeleteServerModal: ({ isOpen }: DeleteServerModalProps) => <>DeleteServerModal {isOpen ? '[Open]' : '[Closed]'}</>,
|
||||||
);
|
}));
|
||||||
const setUp = (children?: ReactNode) => renderWithEvents(
|
const setUp = (children?: ReactNode) => renderWithEvents(
|
||||||
<DeleteServerButton server={fromPartial({})} textClassName="button">{children}</DeleteServerButton>,
|
<DeleteServerButton server={fromPartial({})} textClassName="button">{children}</DeleteServerButton>,
|
||||||
);
|
);
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { fireEvent, screen } from '@testing-library/react';
|
||||||
import { fromPartial } from '@total-typescript/shoehorn';
|
import { fromPartial } from '@total-typescript/shoehorn';
|
||||||
import { MemoryRouter, useNavigate } from 'react-router-dom';
|
import { MemoryRouter, useNavigate } from 'react-router-dom';
|
||||||
import type { ReachableServer, SelectedServer } from '../../src/servers/data';
|
import type { ReachableServer, SelectedServer } from '../../src/servers/data';
|
||||||
import { EditServer as editServerConstruct } from '../../src/servers/EditServer';
|
import { EditServerFactory } from '../../src/servers/EditServer';
|
||||||
import { renderWithEvents } from '../__helpers__/setUpTest';
|
import { renderWithEvents } from '../__helpers__/setUpTest';
|
||||||
|
|
||||||
vi.mock('react-router-dom', async () => ({
|
vi.mock('react-router-dom', async () => ({
|
||||||
|
@ -20,7 +20,7 @@ describe('<EditServer />', () => {
|
||||||
url: 'the_url',
|
url: 'the_url',
|
||||||
apiKey: 'the_api_key',
|
apiKey: 'the_api_key',
|
||||||
});
|
});
|
||||||
const EditServer = editServerConstruct(ServerError);
|
const EditServer = EditServerFactory(fromPartial({ ServerError }));
|
||||||
const setUp = (selectedServer: SelectedServer = defaultSelectedServer) => renderWithEvents(
|
const setUp = (selectedServer: SelectedServer = defaultSelectedServer) => renderWithEvents(
|
||||||
<MemoryRouter>
|
<MemoryRouter>
|
||||||
<EditServer editServer={editServerMock} selectedServer={selectedServer} selectServer={vi.fn()} />
|
<EditServer editServer={editServerMock} selectedServer={selectedServer} selectServer={vi.fn()} />
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { screen, waitFor } from '@testing-library/react';
|
||||||
import { fromPartial } from '@total-typescript/shoehorn';
|
import { fromPartial } from '@total-typescript/shoehorn';
|
||||||
import { MemoryRouter } from 'react-router-dom';
|
import { MemoryRouter } from 'react-router-dom';
|
||||||
import type { ServersMap, ServerWithId } from '../../src/servers/data';
|
import type { ServersMap, ServerWithId } from '../../src/servers/data';
|
||||||
import { ManageServers as createManageServers } from '../../src/servers/ManageServers';
|
import { ManageServersFactory } from '../../src/servers/ManageServers';
|
||||||
import type { ServersExporter } from '../../src/servers/services/ServersExporter';
|
import type { ServersExporter } from '../../src/servers/services/ServersExporter';
|
||||||
import { renderWithEvents } from '../__helpers__/setUpTest';
|
import { renderWithEvents } from '../__helpers__/setUpTest';
|
||||||
|
|
||||||
|
@ -10,12 +10,14 @@ 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 = createManageServers(
|
const ManageServers = ManageServersFactory(fromPartial({
|
||||||
serversExporter,
|
ServersExporter: serversExporter,
|
||||||
() => <span>ImportServersBtn</span>,
|
ImportServersBtn: () => <span>ImportServersBtn</span>,
|
||||||
useTimeoutToggle,
|
useTimeoutToggle,
|
||||||
({ hasAutoConnect }) => <tr><td>ManageServersRow {hasAutoConnect ? '[YES]' : '[NO]'}</td></tr>,
|
ManageServersRow: ({ hasAutoConnect }: { hasAutoConnect: boolean }) => (
|
||||||
);
|
<tr><td>ManageServersRow {hasAutoConnect ? '[YES]' : '[NO]'}</td></tr>
|
||||||
|
),
|
||||||
|
}));
|
||||||
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 },
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,10 +1,13 @@
|
||||||
import { render, screen } from '@testing-library/react';
|
import { render, screen } from '@testing-library/react';
|
||||||
|
import { fromPartial } from '@total-typescript/shoehorn';
|
||||||
import { MemoryRouter } from 'react-router-dom';
|
import { MemoryRouter } from 'react-router-dom';
|
||||||
import type { ServerWithId } from '../../src/servers/data';
|
import type { ServerWithId } from '../../src/servers/data';
|
||||||
import { ManageServersRow as createManageServersRow } from '../../src/servers/ManageServersRow';
|
import { ManageServersRowFactory } from '../../src/servers/ManageServersRow';
|
||||||
|
|
||||||
describe('<ManageServersRow />', () => {
|
describe('<ManageServersRow />', () => {
|
||||||
const ManageServersRow = createManageServersRow(() => <span>ManageServersRowDropdown</span>);
|
const ManageServersRow = ManageServersRowFactory(fromPartial({
|
||||||
|
ManageServersRowDropdown: () => <span>ManageServersRowDropdown</span>,
|
||||||
|
}));
|
||||||
const server: ServerWithId = {
|
const server: ServerWithId = {
|
||||||
name: 'My server',
|
name: 'My server',
|
||||||
url: 'https://example.com',
|
url: 'https://example.com',
|
||||||
|
|
|
@ -2,13 +2,15 @@ import { screen } from '@testing-library/react';
|
||||||
import { fromPartial } from '@total-typescript/shoehorn';
|
import { fromPartial } from '@total-typescript/shoehorn';
|
||||||
import { MemoryRouter } from 'react-router-dom';
|
import { MemoryRouter } from 'react-router-dom';
|
||||||
import type { ServerWithId } from '../../src/servers/data';
|
import type { ServerWithId } from '../../src/servers/data';
|
||||||
import { ManageServersRowDropdown as createManageServersRowDropdown } from '../../src/servers/ManageServersRowDropdown';
|
import { ManageServersRowDropdownFactory } from '../../src/servers/ManageServersRowDropdown';
|
||||||
import { renderWithEvents } from '../__helpers__/setUpTest';
|
import { renderWithEvents } from '../__helpers__/setUpTest';
|
||||||
|
|
||||||
describe('<ManageServersRowDropdown />', () => {
|
describe('<ManageServersRowDropdown />', () => {
|
||||||
const ManageServersRowDropdown = createManageServersRowDropdown(
|
const ManageServersRowDropdown = ManageServersRowDropdownFactory(fromPartial({
|
||||||
({ isOpen }) => <span>DeleteServerModal {isOpen ? '[OPEN]' : '[CLOSED]'}</span>,
|
DeleteServerModal: ({ isOpen }: { isOpen: boolean }) => (
|
||||||
);
|
<span>DeleteServerModal {isOpen ? '[OPEN]' : '[CLOSED]'}</span>
|
||||||
|
),
|
||||||
|
}));
|
||||||
const setAutoConnect = vi.fn();
|
const setAutoConnect = vi.fn();
|
||||||
const setUp = (autoConnect = false) => {
|
const setUp = (autoConnect = false) => {
|
||||||
const server = fromPartial<ServerWithId>({ id: 'abc123', autoConnect });
|
const server = fromPartial<ServerWithId>({ id: 'abc123', autoConnect });
|
||||||
|
|
|
@ -3,9 +3,7 @@ import { fromPartial } from '@total-typescript/shoehorn';
|
||||||
import type { ServersMap, ServerWithId } from '../../../src/servers/data';
|
import type { ServersMap, ServerWithId } from '../../../src/servers/data';
|
||||||
import type {
|
import type {
|
||||||
ImportServersBtnProps } from '../../../src/servers/helpers/ImportServersBtn';
|
ImportServersBtnProps } from '../../../src/servers/helpers/ImportServersBtn';
|
||||||
import {
|
import { ImportServersBtnFactory } from '../../../src/servers/helpers/ImportServersBtn';
|
||||||
ImportServersBtn as createImportServersBtn,
|
|
||||||
} from '../../../src/servers/helpers/ImportServersBtn';
|
|
||||||
import type { ServersImporter } from '../../../src/servers/services/ServersImporter';
|
import type { ServersImporter } from '../../../src/servers/services/ServersImporter';
|
||||||
import { renderWithEvents } from '../../__helpers__/setUpTest';
|
import { renderWithEvents } from '../../__helpers__/setUpTest';
|
||||||
|
|
||||||
|
@ -14,7 +12,7 @@ describe('<ImportServersBtn />', () => {
|
||||||
const createServersMock = vi.fn();
|
const createServersMock = vi.fn();
|
||||||
const importServersFromFile = vi.fn().mockResolvedValue([]);
|
const importServersFromFile = vi.fn().mockResolvedValue([]);
|
||||||
const serversImporterMock = fromPartial<ServersImporter>({ importServersFromFile });
|
const serversImporterMock = fromPartial<ServersImporter>({ importServersFromFile });
|
||||||
const ImportServersBtn = createImportServersBtn(serversImporterMock);
|
const ImportServersBtn = ImportServersBtnFactory(fromPartial({ ServersImporter: serversImporterMock }));
|
||||||
const setUp = (props: Partial<ImportServersBtnProps> = {}, servers: ServersMap = {}) => renderWithEvents(
|
const setUp = (props: Partial<ImportServersBtnProps> = {}, servers: ServersMap = {}) => renderWithEvents(
|
||||||
<ImportServersBtn
|
<ImportServersBtn
|
||||||
servers={servers}
|
servers={servers}
|
||||||
|
|
|
@ -2,10 +2,10 @@ import { render, screen } from '@testing-library/react';
|
||||||
import { fromPartial } from '@total-typescript/shoehorn';
|
import { fromPartial } from '@total-typescript/shoehorn';
|
||||||
import { MemoryRouter } from 'react-router-dom';
|
import { MemoryRouter } from 'react-router-dom';
|
||||||
import type { NonReachableServer, NotFoundServer } from '../../../src/servers/data';
|
import type { NonReachableServer, NotFoundServer } from '../../../src/servers/data';
|
||||||
import { ServerError as createServerError } from '../../../src/servers/helpers/ServerError';
|
import { ServerErrorFactory } from '../../../src/servers/helpers/ServerError';
|
||||||
|
|
||||||
describe('<ServerError />', () => {
|
describe('<ServerError />', () => {
|
||||||
const ServerError = createServerError(() => null);
|
const ServerError = ServerErrorFactory(fromPartial({ DeleteServerButton: () => null }));
|
||||||
|
|
||||||
it.each([
|
it.each([
|
||||||
[
|
[
|
||||||
|
|
|
@ -1,17 +1,18 @@
|
||||||
import { render, screen } from '@testing-library/react';
|
import { render, screen } from '@testing-library/react';
|
||||||
|
import { fromPartial } from '@total-typescript/shoehorn';
|
||||||
import { createMemoryHistory } from 'history';
|
import { createMemoryHistory } from 'history';
|
||||||
import { Router } from 'react-router-dom';
|
import { Router } from 'react-router-dom';
|
||||||
import { Settings as createSettings } from '../../src/settings/Settings';
|
import { SettingsFactory } from '../../src/settings/Settings';
|
||||||
|
|
||||||
describe('<Settings />', () => {
|
describe('<Settings />', () => {
|
||||||
const Settings = createSettings(
|
const Settings = SettingsFactory(fromPartial({
|
||||||
() => <span>RealTimeUpdates</span>,
|
RealTimeUpdatesSettings: () => <span>RealTimeUpdates</span>,
|
||||||
() => <span>ShortUrlCreation</span>,
|
ShortUrlCreationSettings: () => <span>ShortUrlCreation</span>,
|
||||||
() => <span>ShortUrlsList</span>,
|
ShortUrlsListSettings: () => <span>ShortUrlsList</span>,
|
||||||
() => <span>UserInterface</span>,
|
UserInterfaceSettings: () => <span>UserInterface</span>,
|
||||||
() => <span>Visits</span>,
|
VisitsSettings: () => <span>Visits</span>,
|
||||||
() => <span>Tags</span>,
|
TagsSettings: () => <span>Tags</span>,
|
||||||
);
|
}));
|
||||||
const setUp = (activeRoute = '/') => {
|
const setUp = (activeRoute = '/') => {
|
||||||
const history = createMemoryHistory();
|
const history = createMemoryHistory();
|
||||||
history.push(activeRoute);
|
history.push(activeRoute);
|
||||||
|
|
Loading…
Reference in a new issue