Refactor DI approach for components

This commit is contained in:
Alejandro Celaya
2023-09-05 09:08:42 +02:00
parent 046f79270a
commit 6926afbac1
30 changed files with 371 additions and 234 deletions

View File

@@ -5,6 +5,8 @@ import { useNavigate } from 'react-router-dom';
import { Button } from 'reactstrap';
import { v4 as uuid } from 'uuid';
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 { useGoBack } from '../utils/helpers/hooks';
import type { ServerData, ServersMap, ServerWithId } from './data';
@@ -14,10 +16,15 @@ import { ServerForm } from './helpers/ServerForm';
const SHOW_IMPORT_MSG_TIME = 4000;
interface CreateServerProps {
type CreateServerProps = {
createServers: (servers: ServerWithId[]) => void;
servers: ServersMap;
}
};
type CreateServerDeps = {
ImportServersBtn: FC<ImportServersBtnProps>;
useTimeoutToggle: TimeoutToggle;
};
const ImportResult = ({ type }: { type: 'error' | 'success' }) => (
<div className="mt-3">
@@ -28,9 +35,8 @@ const ImportResult = ({ type }: { type: 'error' | 'success' }) => (
</div>
);
export const CreateServer = (ImportServersBtn: FC<ImportServersBtnProps>, useTimeoutToggle: TimeoutToggle) => (
{ servers, createServers }: CreateServerProps,
) => {
const CreateServer: FCWithDeps<CreateServerProps, CreateServerDeps> = ({ servers, createServers }) => {
const { ImportServersBtn, useTimeoutToggle } = useDependencies(CreateServer);
const navigate = useNavigate();
const goBack = useGoBack();
const hasServers = !!Object.keys(servers).length;
@@ -79,3 +85,5 @@ export const CreateServer = (ImportServersBtn: FC<ImportServersBtnProps>, useTim
</NoMenuLayout>
);
};
export const CreateServerFactory = componentFactory(CreateServer, ['ImportServersBtn', 'useTimeoutToggle']);

View File

@@ -3,6 +3,8 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { useToggle } from '@shlinkio/shlink-frontend-kit';
import classNames from 'classnames';
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 { DeleteServerModalProps } from './DeleteServerModal';
@@ -12,9 +14,14 @@ export type DeleteServerButtonProps = PropsWithChildren<{
textClassName?: string;
}>;
export const DeleteServerButton = (DeleteServerModal: FC<DeleteServerModalProps>): FC<DeleteServerButtonProps> => (
type DeleteServerButtonDeps = {
DeleteServerModal: FC<DeleteServerModalProps>;
};
const DeleteServerButton: FCWithDeps<DeleteServerButtonProps, DeleteServerButtonDeps> = (
{ server, className, children, textClassName },
) => {
const { DeleteServerModal } = useDependencies(DeleteServerButton);
const [isModalOpen, , showModal, hideModal] = useToggle();
return (
@@ -28,3 +35,5 @@ export const DeleteServerButton = (DeleteServerModal: FC<DeleteServerModalProps>
</>
);
};
export const DeleteServerButtonFactory = componentFactory(DeleteServerButton, ['DeleteServerModal']);

View File

@@ -1,17 +1,24 @@
import type { FC } from 'react';
import { Button } from 'reactstrap';
import { NoMenuLayout } from '../common/NoMenuLayout';
import type { FCWithDeps } from '../container/utils';
import { componentFactory } from '../container/utils';
import { useGoBack, useParsedQuery } from '../utils/helpers/hooks';
import type { ServerData } from './data';
import { isServerWithId } from './data';
import { ServerForm } from './helpers/ServerForm';
import type { WithSelectedServerProps } from './helpers/withSelectedServer';
import { withSelectedServer } from './helpers/withSelectedServer';
interface EditServerProps {
type EditServerProps = WithSelectedServerProps & {
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 },
) => {
const goBack = useGoBack();
@@ -39,4 +46,6 @@ export const EditServer = (ServerError: FC) => withSelectedServer<EditServerProp
</ServerForm>
</NoMenuLayout>
);
}, ServerError);
});
export const EditServerFactory = componentFactory(EditServer, ['ServerError']);

View File

@@ -6,24 +6,34 @@ import { useEffect, useState } from 'react';
import { Link } from 'react-router-dom';
import { Button, Row } from 'reactstrap';
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 { ServersMap } from './data';
import type { ImportServersBtnProps } from './helpers/ImportServersBtn';
import type { ManageServersRowProps } from './ManageServersRow';
import type { ServersExporter } from './services/ServersExporter';
interface ManageServersProps {
type ManageServersProps = {
servers: ServersMap;
}
};
type ManageServersDeps = {
ServersExporter: ServersExporter;
ImportServersBtn: FC<ImportServersBtnProps>;
useTimeoutToggle: TimeoutToggle;
ManageServersRow: FC<ManageServersRowProps>;
};
const SHOW_IMPORT_MSG_TIME = 4000;
export const ManageServers = (
serversExporter: ServersExporter,
ImportServersBtn: FC<ImportServersBtnProps>,
useTimeoutToggle: TimeoutToggle,
ManageServersRow: FC<ManageServersRowProps>,
): FC<ManageServersProps> => ({ servers }) => {
const ManageServers: FCWithDeps<ManageServersProps, ManageServersDeps> = ({ servers }) => {
const {
ServersExporter: serversExporter,
ImportServersBtn,
useTimeoutToggle,
ManageServersRow,
} = useDependencies(ManageServers);
const allServers = Object.values(servers);
const [serversList, setServersList] = useState(allServers);
const filterServers = (searchTerm: string) => setServersList(
@@ -83,3 +93,10 @@ export const ManageServers = (
</NoMenuLayout>
);
};
export const ManageServersFactory = componentFactory(ManageServers, [
'ServersExporter',
'ImportServersBtn',
'useTimeoutToggle',
'ManageServersRow',
]);

View File

@@ -3,36 +3,46 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import type { FC } from 'react';
import { Link } from 'react-router-dom';
import { UncontrolledTooltip } from 'reactstrap';
import type { FCWithDeps } from '../container/utils';
import { componentFactory, useDependencies } from '../container/utils';
import type { ServerWithId } from './data';
import type { ManageServersRowDropdownProps } from './ManageServersRowDropdown';
export interface ManageServersRowProps {
export type ManageServersRowProps = {
server: ServerWithId;
hasAutoConnect: boolean;
}
};
export const ManageServersRow = (
ManageServersRowDropdown: FC<ManageServersRowDropdownProps>,
): FC<ManageServersRowProps> => ({ server, hasAutoConnect }) => (
<tr className="responsive-table__row">
{hasAutoConnect && (
<td className="responsive-table__cell" data-th="Auto-connect">
{server.autoConnect && (
<>
<FontAwesomeIcon icon={checkIcon} className="text-primary" id="autoConnectIcon" />
<UncontrolledTooltip target="autoConnectIcon" placement="right">
Auto-connect to this server
</UncontrolledTooltip>
</>
)}
type ManageServersRowDeps = {
ManageServersRowDropdown: FC<ManageServersRowDropdownProps>;
};
const ManageServersRow: FCWithDeps<ManageServersRowProps, ManageServersRowDeps> = ({ server, hasAutoConnect }) => {
const { ManageServersRowDropdown } = useDependencies(ManageServersRow);
return (
<tr className="responsive-table__row">
{hasAutoConnect && (
<td className="responsive-table__cell" data-th="Auto-connect">
{server.autoConnect && (
<>
<FontAwesomeIcon icon={checkIcon} className="text-primary" id="autoConnectIcon" />
<UncontrolledTooltip target="autoConnectIcon" placement="right">
Auto-connect to this server
</UncontrolledTooltip>
</>
)}
</td>
)}
<th className="responsive-table__cell" data-th="Name">
<Link to={`/server/${server.id}`}>{server.name}</Link>
</th>
<td className="responsive-table__cell" data-th="Base URL">{server.url}</td>
<td className="responsive-table__cell text-end">
<ManageServersRowDropdown server={server} />
</td>
)}
<th className="responsive-table__cell" data-th="Name">
<Link to={`/server/${server.id}`}>{server.name}</Link>
</th>
<td className="responsive-table__cell" data-th="Base URL">{server.url}</td>
<td className="responsive-table__cell text-end">
<ManageServersRowDropdown server={server} />
</td>
</tr>
);
</tr>
);
};
export const ManageServersRowFactory = componentFactory(ManageServersRow, ['ManageServersRowDropdown']);

View File

@@ -10,20 +10,27 @@ import { RowDropdownBtn, useToggle } from '@shlinkio/shlink-frontend-kit';
import type { FC } from 'react';
import { Link } from 'react-router-dom';
import { DropdownItem } from 'reactstrap';
import type { FCWithDeps } from '../container/utils';
import { componentFactory, useDependencies } from '../container/utils';
import type { ServerWithId } from './data';
import type { DeleteServerModalProps } from './DeleteServerModal';
export interface ManageServersRowDropdownProps {
export type ManageServersRowDropdownProps = {
server: ServerWithId;
}
};
interface ManageServersRowDropdownConnectProps extends ManageServersRowDropdownProps {
type ManageServersRowDropdownConnectProps = ManageServersRowDropdownProps & {
setAutoConnect: (server: ServerWithId, autoConnect: boolean) => void;
}
};
export const ManageServersRowDropdown = (
DeleteServerModal: FC<DeleteServerModalProps>,
): FC<ManageServersRowDropdownConnectProps> => ({ server, setAutoConnect }) => {
type ManageServersRowDropdownDeps = {
DeleteServerModal: FC<DeleteServerModalProps>
};
const ManageServersRowDropdown: FCWithDeps<ManageServersRowDropdownConnectProps, ManageServersRowDropdownDeps> = (
{ server, setAutoConnect },
) => {
const { DeleteServerModal } = useDependencies(ManageServersRowDropdown);
const [isModalOpen,, showModal, hideModal] = useToggle();
const serverUrl = `/server/${server.id}`;
const { autoConnect: isAutoConnect } = server;
@@ -49,3 +56,5 @@ export const ManageServersRowDropdown = (
</RowDropdownBtn>
);
};
export const ManageServersRowDropdownFactory = componentFactory(ManageServersRowDropdown, ['DeleteServerModal']);

View File

@@ -2,9 +2,11 @@ import { faFileUpload as importIcon } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { useElementRef, useToggle } from '@shlinkio/shlink-frontend-kit';
import { complement } from 'ramda';
import type { ChangeEvent, FC, PropsWithChildren } from 'react';
import type { ChangeEvent, PropsWithChildren } from 'react';
import { useCallback, useRef, useState } from 'react';
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 { ServersImporter } from '../services/ServersImporter';
import { DuplicatedServersModal } from './DuplicatedServersModal';
@@ -16,15 +18,19 @@ export type ImportServersBtnProps = PropsWithChildren<{
className?: string;
}>;
interface ImportServersBtnConnectProps extends ImportServersBtnProps {
type ImportServersBtnConnectProps = ImportServersBtnProps & {
createServers: (servers: ServerData[]) => void;
servers: ServersMap;
}
};
type ImportServersBtnDeps = {
ServersImporter: ServersImporter
};
const serversFiltering = (servers: ServerData[]) =>
({ 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,
servers,
children,
@@ -33,6 +39,7 @@ export const ImportServersBtn = (serversImporter: ServersImporter): FC<ImportSer
tooltipPlacement = 'bottom',
className = '',
}) => {
const { ServersImporter: serversImporter } = useDependencies(ImportServersBtn);
const ref = useElementRef<HTMLInputElement>();
const [duplicatedServers, setDuplicatedServers] = useState<ServerData[]>([]);
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
})
.catch(onImportError),
[create, onImportError, servers, showModal],
[create, onImportError, servers, serversImporter, showModal],
);
const createAllServers = useCallback(() => {
@@ -92,3 +99,5 @@ export const ImportServersBtn = (serversImporter: ServersImporter): FC<ImportSer
</>
);
};
export const ImportServersBtnFactory = componentFactory(ImportServersBtn, ['ServersImporter']);

View File

@@ -2,46 +2,56 @@ import { Message } from '@shlinkio/shlink-frontend-kit';
import type { FC } from 'react';
import { Link } from 'react-router-dom';
import { NoMenuLayout } from '../../common/NoMenuLayout';
import type { FCWithDeps } from '../../container/utils';
import { componentFactory, useDependencies } from '../../container/utils';
import type { SelectedServer, ServersMap } from '../data';
import { isServerWithId } from '../data';
import type { DeleteServerButtonProps } from '../DeleteServerButton';
import { ServersListGroup } from '../ServersListGroup';
import './ServerError.scss';
interface ServerErrorProps {
type ServerErrorProps = {
servers: ServersMap;
selectedServer: SelectedServer;
}
};
type ServerErrorDeps = {
DeleteServerButton: FC<DeleteServerButtonProps>;
};
const ServerError: FCWithDeps<ServerErrorProps, ServerErrorDeps> = ({ servers, selectedServer }) => {
const { DeleteServerButton } = useDependencies(ServerError);
return (
<NoMenuLayout>
<div className="server-error__container flex-column">
<Message className="w-100 mb-3 mb-md-5" type="error" fullWidth>
{!isServerWithId(selectedServer) && 'Could not find this Shlink server.'}
{isServerWithId(selectedServer) && (
<>
<p>Oops! Could not connect to this Shlink server.</p>
Make sure you have internet connection, and the server is properly configured and on-line.
</>
)}
</Message>
<ServersListGroup servers={Object.values(servers)}>
These are the Shlink servers currently configured. Choose one of
them or <Link to="/server/create">add a new one</Link>.
</ServersListGroup>
export const ServerError = (DeleteServerButton: FC<DeleteServerButtonProps>): FC<ServerErrorProps> => (
{ servers, selectedServer },
) => (
<NoMenuLayout>
<div className="server-error__container flex-column">
<Message className="w-100 mb-3 mb-md-5" type="error" fullWidth>
{!isServerWithId(selectedServer) && 'Could not find this Shlink server.'}
{isServerWithId(selectedServer) && (
<>
<p>Oops! Could not connect to this Shlink server.</p>
Make sure you have internet connection, and the server is properly configured and on-line.
</>
<div className="container mt-3 mt-md-5">
<h5>
Alternatively, if you think you may have miss-configured this server, you
can <DeleteServerButton server={selectedServer} className="server-error__delete-btn">remove it</DeleteServerButton> or&nbsp;
<Link to={`/server/${selectedServer.id}/edit?reconnect=true`}>edit it</Link>.
</h5>
</div>
)}
</Message>
</div>
</NoMenuLayout>
);
};
<ServersListGroup servers={Object.values(servers)}>
These are the Shlink servers currently configured. Choose one of
them or <Link to="/server/create">add a new one</Link>.
</ServersListGroup>
{isServerWithId(selectedServer) && (
<div className="container mt-3 mt-md-5">
<h5>
Alternatively, if you think you may have miss-configured this server, you
can <DeleteServerButton server={selectedServer} className="server-error__delete-btn">remove it</DeleteServerButton> or&nbsp;
<Link to={`/server/${selectedServer.id}/edit?reconnect=true`}>edit it</Link>.
</h5>
</div>
)}
</div>
</NoMenuLayout>
);
export const ServerErrorFactory = componentFactory(ServerError, ['DeleteServerButton']);

View File

@@ -3,16 +3,25 @@ import type { FC } from 'react';
import { useEffect } from 'react';
import { useParams } from 'react-router-dom';
import { NoMenuLayout } from '../../common/NoMenuLayout';
import type { FCWithDeps } from '../../container/utils';
import { useDependencies } from '../../container/utils';
import type { SelectedServer } from '../data';
import { isNotFoundServer } from '../data';
interface WithSelectedServerProps {
export type WithSelectedServerProps = {
selectServer: (serverId: string) => void;
selectedServer: SelectedServer;
}
};
export function withSelectedServer<T = {}>(WrappedComponent: FC<WithSelectedServerProps & T>, ServerError: FC) {
return (props: WithSelectedServerProps & T) => {
type WithSelectedServerPropsDeps = {
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 { selectServer, selectedServer } = props;
@@ -34,4 +43,5 @@ export function withSelectedServer<T = {}>(WrappedComponent: FC<WithSelectedServ
return <WrappedComponent {...props} />;
};
return ComponentWrapper;
}

View File

@@ -1,16 +1,16 @@
import type Bottle from 'bottlejs';
import { prop } from 'ramda';
import type { ConnectDecorator } from '../../container/types';
import { CreateServer } from '../CreateServer';
import { DeleteServerButton } from '../DeleteServerButton';
import { CreateServerFactory } from '../CreateServer';
import { DeleteServerButtonFactory } from '../DeleteServerButton';
import { DeleteServerModal } from '../DeleteServerModal';
import { EditServer } from '../EditServer';
import { ImportServersBtn } from '../helpers/ImportServersBtn';
import { ServerError } from '../helpers/ServerError';
import { EditServerFactory } from '../EditServer';
import { ImportServersBtnFactory } from '../helpers/ImportServersBtn';
import { ServerErrorFactory } from '../helpers/ServerError';
import { withoutSelectedServer } from '../helpers/withoutSelectedServer';
import { ManageServers } from '../ManageServers';
import { ManageServersRow } from '../ManageServersRow';
import { ManageServersRowDropdown } from '../ManageServersRowDropdown';
import { ManageServersFactory } from '../ManageServers';
import { ManageServersRowFactory } from '../ManageServersRow';
import { ManageServersRowDropdownFactory } from '../ManageServersRowDropdown';
import { fetchServers } from '../reducers/remoteServers';
import {
resetSelectedServer,
@@ -24,27 +24,20 @@ import { ServersImporter } from './ServersImporter';
export const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
// Components
bottle.serviceFactory(
'ManageServers',
ManageServers,
'ServersExporter',
'ImportServersBtn',
'useTimeoutToggle',
'ManageServersRow',
);
bottle.factory('ManageServers', ManageServersFactory);
bottle.decorator('ManageServers', withoutSelectedServer);
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.serviceFactory('CreateServer', CreateServer, 'ImportServersBtn', 'useTimeoutToggle');
bottle.factory('CreateServer', CreateServerFactory);
bottle.decorator('CreateServer', withoutSelectedServer);
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.serviceFactory('ServersDropdown', () => ServersDropdown);
@@ -53,12 +46,12 @@ export const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
bottle.serviceFactory('DeleteServerModal', () => DeleteServerModal);
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.serviceFactory('ServerError', ServerError, 'DeleteServerButton');
bottle.factory('ServerError', ServerErrorFactory);
bottle.decorator('ServerError', connect(['servers', 'selectedServer']));
// Services