From 6926afbac14672202c7c8a657aa9f3fbe69781b5 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Tue, 5 Sep 2023 09:08:42 +0200 Subject: [PATCH] Refactor DI approach for components --- src/app/App.tsx | 4 +- src/common/ErrorHandler.tsx | 29 ++++---- src/common/MainHeader.tsx | 11 ++- src/common/ScrollToTop.tsx | 2 +- src/common/ShlinkWebComponentContainer.tsx | 33 +++++++-- src/common/services/provideServices.ts | 17 ++--- src/servers/CreateServer.tsx | 18 +++-- src/servers/DeleteServerButton.tsx | 11 ++- src/servers/EditServer.tsx | 17 +++-- src/servers/ManageServers.tsx | 33 ++++++--- src/servers/ManageServersRow.tsx | 62 +++++++++------- src/servers/ManageServersRowDropdown.tsx | 23 ++++-- src/servers/helpers/ImportServersBtn.tsx | 19 +++-- src/servers/helpers/ServerError.tsx | 72 +++++++++++-------- src/servers/helpers/withSelectedServer.tsx | 18 +++-- src/servers/services/provideServices.ts | 39 +++++----- src/settings/Settings.tsx | 68 ++++++++++++------ src/settings/services/provideServices.ts | 13 +--- test/common/ErrorHandler.test.tsx | 15 ++-- test/common/MainHeader.test.tsx | 7 +- .../ShlinkWebComponentContainer.test.tsx | 14 ++-- test/servers/CreateServer.test.tsx | 7 +- test/servers/DeleteServerButton.test.tsx | 9 +-- test/servers/EditServer.test.tsx | 4 +- test/servers/ManageServers.test.tsx | 14 ++-- test/servers/ManageServersRow.test.tsx | 7 +- .../servers/ManageServersRowDropdown.test.tsx | 10 +-- .../servers/helpers/ImportServersBtn.test.tsx | 6 +- test/servers/helpers/ServerError.test.tsx | 4 +- test/settings/Settings.test.tsx | 19 ++--- 30 files changed, 371 insertions(+), 234 deletions(-) diff --git a/src/app/App.tsx b/src/app/App.tsx index dfb425ae..540e70ee 100644 --- a/src/app/App.tsx +++ b/src/app/App.tsx @@ -20,7 +20,7 @@ type AppProps = { appUpdated: boolean; }; -type AppDependencies = { +type AppDeps = { MainHeader: FC; Home: FC; ShlinkWebComponentContainer: FC; @@ -31,7 +31,7 @@ type AppDependencies = { ShlinkVersionsContainer: FC; }; -const App: FCWithDeps = ( +const App: FCWithDeps = ( { fetchServers, servers, settings, appUpdated, resetAppUpdate }, ) => { const { diff --git a/src/common/ErrorHandler.tsx b/src/common/ErrorHandler.tsx index 49f30e4d..a18de8c1 100644 --- a/src/common/ErrorHandler.tsx +++ b/src/common/ErrorHandler.tsx @@ -1,17 +1,19 @@ import { SimpleCard } from '@shlinkio/shlink-frontend-kit'; -import type { ReactNode } from 'react'; +import type { PropsWithChildren, ReactNode } from 'react'; import { Component } from 'react'; import { Button } from 'reactstrap'; -interface ErrorHandlerState { - hasError: boolean; -} +type ErrorHandlerProps = PropsWithChildren<{ + location?: typeof window.location; + console?: typeof window.console; +}>; -export const ErrorHandler = ( - { location }: Window, - { error }: Console, -) => class extends Component { - public constructor(props: object) { +type ErrorHandlerState = { + hasError: boolean; +}; + +export class ErrorHandler extends Component { + public constructor(props: ErrorHandlerProps) { super(props); this.state = { hasError: false }; } @@ -21,13 +23,14 @@ export const ErrorHandler = ( } public componentDidCatch(e: Error): void { - if (process.env.NODE_ENV !== 'development') { - error(e); - } + const { console = globalThis.console } = this.props; + console.error(e); } public render(): ReactNode { const { hasError } = this.state; + const { location = globalThis.location } = this.props; + if (hasError) { return (
@@ -44,4 +47,4 @@ export const ErrorHandler = ( const { children } = this.props; return children; } -}; +} diff --git a/src/common/MainHeader.tsx b/src/common/MainHeader.tsx index 750ba744..7473df97 100644 --- a/src/common/MainHeader.tsx +++ b/src/common/MainHeader.tsx @@ -6,10 +6,17 @@ import type { FC } from 'react'; import { useEffect } from 'react'; import { Link, useLocation } from 'react-router-dom'; 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 './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 location = useLocation(); const { pathname } = location; @@ -43,3 +50,5 @@ export const MainHeader = (ServersDropdown: FC) => () => { ); }; + +export const MainHeaderFactory = componentFactory(MainHeader, ['ServersDropdown']); diff --git a/src/common/ScrollToTop.tsx b/src/common/ScrollToTop.tsx index 33f4bb3a..d110f193 100644 --- a/src/common/ScrollToTop.tsx +++ b/src/common/ScrollToTop.tsx @@ -2,7 +2,7 @@ import type { FC, PropsWithChildren } from 'react'; import { useEffect } from 'react'; import { useLocation } from 'react-router-dom'; -export const ScrollToTop: FC> = ({ children }) => { +export const ScrollToTop: FC = ({ children }) => { const location = useLocation(); useEffect(() => { diff --git a/src/common/ShlinkWebComponentContainer.tsx b/src/common/ShlinkWebComponentContainer.tsx index 7060a793..41a3047b 100644 --- a/src/common/ShlinkWebComponentContainer.tsx +++ b/src/common/ShlinkWebComponentContainer.tsx @@ -1,20 +1,34 @@ import type { Settings, ShlinkWebComponentType, TagColorsStorage } from '@shlinkio/shlink-web-component'; import type { FC } from 'react'; import type { ShlinkApiClientBuilder } from '../api/services/ShlinkApiClientBuilder'; +import type { FCWithDeps } from '../container/utils'; +import { componentFactory, useDependencies } from '../container/utils'; import { isReachableServer } from '../servers/data'; +import type { WithSelectedServerProps } from '../servers/helpers/withSelectedServer'; import { withSelectedServer } from '../servers/helpers/withSelectedServer'; import { NotFound } from './NotFound'; -interface ShlinkWebComponentContainerProps { +type ShlinkWebComponentContainerProps = WithSelectedServerProps & { settings: Settings; -} +}; -export const ShlinkWebComponentContainer = ( +type ShlinkWebComponentContainerDeps = { buildShlinkApiClient: ShlinkApiClientBuilder, - tagColorsStorage: TagColorsStorage, + TagColorsStorage: TagColorsStorage, ShlinkWebComponent: ShlinkWebComponentType, ServerError: FC, -) => withSelectedServer(({ selectedServer, settings }) => { +}; + +const ShlinkWebComponentContainer: FCWithDeps< +ShlinkWebComponentContainerProps, +ShlinkWebComponentContainerDeps +> = withSelectedServer(({ selectedServer, settings }) => { + const { + buildShlinkApiClient, + TagColorsStorage: tagColorsStorage, + ShlinkWebComponent, + ServerError, + } = useDependencies(ShlinkWebComponentContainer); const selectedServerIsReachable = isReachableServer(selectedServer); const routesPrefix = selectedServerIsReachable ? `/server/${selectedServer.id}` : ''; @@ -34,4 +48,11 @@ export const ShlinkWebComponentContainer = ( )} /> ); -}, ServerError); +}); + +export const ShlinkWebComponentContainerFactory = componentFactory(ShlinkWebComponentContainer, [ + 'buildShlinkApiClient', + 'TagColorsStorage', + 'ShlinkWebComponent', + 'ServerError', +]); diff --git a/src/common/services/provideServices.ts b/src/common/services/provideServices.ts index 309c7751..cf233f8d 100644 --- a/src/common/services/provideServices.ts +++ b/src/common/services/provideServices.ts @@ -5,10 +5,10 @@ import type { ConnectDecorator } from '../../container/types'; import { withoutSelectedServer } from '../../servers/helpers/withoutSelectedServer'; import { ErrorHandler } from '../ErrorHandler'; import { Home } from '../Home'; -import { MainHeader } from '../MainHeader'; +import { MainHeaderFactory } from '../MainHeader'; import { ScrollToTop } from '../ScrollToTop'; import { ShlinkVersionsContainer } from '../ShlinkVersionsContainer'; -import { ShlinkWebComponentContainer } from '../ShlinkWebComponentContainer'; +import { ShlinkWebComponentContainerFactory } from '../ShlinkWebComponentContainer'; export const provideServices = (bottle: Bottle, connect: ConnectDecorator) => { // Services @@ -20,25 +20,18 @@ export const provideServices = (bottle: Bottle, connect: ConnectDecorator) => { // Components bottle.serviceFactory('ScrollToTop', () => ScrollToTop); - bottle.serviceFactory('MainHeader', MainHeader, 'ServersDropdown'); + bottle.factory('MainHeader', MainHeaderFactory); bottle.serviceFactory('Home', () => Home); bottle.decorator('Home', withoutSelectedServer); bottle.decorator('Home', connect(['servers'], ['resetSelectedServer'])); bottle.serviceFactory('ShlinkWebComponent', () => ShlinkWebComponent); - bottle.serviceFactory( - 'ShlinkWebComponentContainer', - ShlinkWebComponentContainer, - 'buildShlinkApiClient', - 'TagColorsStorage', - 'ShlinkWebComponent', - 'ServerError', - ); + bottle.factory('ShlinkWebComponentContainer', ShlinkWebComponentContainerFactory); bottle.decorator('ShlinkWebComponentContainer', connect(['selectedServer', 'settings'], ['selectServer'])); bottle.serviceFactory('ShlinkVersionsContainer', () => ShlinkVersionsContainer); bottle.decorator('ShlinkVersionsContainer', connect(['selectedServer'])); - bottle.serviceFactory('ErrorHandler', ErrorHandler, 'window', 'console'); + bottle.serviceFactory('ErrorHandler', () => ErrorHandler); }; diff --git a/src/servers/CreateServer.tsx b/src/servers/CreateServer.tsx index 428672e9..6d6787d8 100644 --- a/src/servers/CreateServer.tsx +++ b/src/servers/CreateServer.tsx @@ -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; + useTimeoutToggle: TimeoutToggle; +}; const ImportResult = ({ type }: { type: 'error' | 'success' }) => (
@@ -28,9 +35,8 @@ const ImportResult = ({ type }: { type: 'error' | 'success' }) => (
); -export const CreateServer = (ImportServersBtn: FC, useTimeoutToggle: TimeoutToggle) => ( - { servers, createServers }: CreateServerProps, -) => { +const CreateServer: FCWithDeps = ({ 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, useTim ); }; + +export const CreateServerFactory = componentFactory(CreateServer, ['ImportServersBtn', 'useTimeoutToggle']); diff --git a/src/servers/DeleteServerButton.tsx b/src/servers/DeleteServerButton.tsx index 9955aaa0..45970977 100644 --- a/src/servers/DeleteServerButton.tsx +++ b/src/servers/DeleteServerButton.tsx @@ -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): FC => ( +type DeleteServerButtonDeps = { + DeleteServerModal: FC; +}; + +const DeleteServerButton: FCWithDeps = ( { server, className, children, textClassName }, ) => { + const { DeleteServerModal } = useDependencies(DeleteServerButton); const [isModalOpen, , showModal, hideModal] = useToggle(); return ( @@ -28,3 +35,5 @@ export const DeleteServerButton = (DeleteServerModal: FC ); }; + +export const DeleteServerButtonFactory = componentFactory(DeleteServerButton, ['DeleteServerModal']); diff --git a/src/servers/EditServer.tsx b/src/servers/EditServer.tsx index b24a961e..0d61ec4a 100644 --- a/src/servers/EditServer.tsx +++ b/src/servers/EditServer.tsx @@ -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(( +type EditServerDeps = { + ServerError: FC; +}; + +const EditServer: FCWithDeps = withSelectedServer(( { editServer, selectedServer, selectServer }, ) => { const goBack = useGoBack(); @@ -39,4 +46,6 @@ export const EditServer = (ServerError: FC) => withSelectedServer ); -}, ServerError); +}); + +export const EditServerFactory = componentFactory(EditServer, ['ServerError']); diff --git a/src/servers/ManageServers.tsx b/src/servers/ManageServers.tsx index 5e687c0c..ff24d552 100644 --- a/src/servers/ManageServers.tsx +++ b/src/servers/ManageServers.tsx @@ -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; + useTimeoutToggle: TimeoutToggle; + ManageServersRow: FC; +}; const SHOW_IMPORT_MSG_TIME = 4000; -export const ManageServers = ( - serversExporter: ServersExporter, - ImportServersBtn: FC, - useTimeoutToggle: TimeoutToggle, - ManageServersRow: FC, -): FC => ({ servers }) => { +const ManageServers: FCWithDeps = ({ 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 = ( ); }; + +export const ManageServersFactory = componentFactory(ManageServers, [ + 'ServersExporter', + 'ImportServersBtn', + 'useTimeoutToggle', + 'ManageServersRow', +]); diff --git a/src/servers/ManageServersRow.tsx b/src/servers/ManageServersRow.tsx index 89291a48..0caad30c 100644 --- a/src/servers/ManageServersRow.tsx +++ b/src/servers/ManageServersRow.tsx @@ -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, -): FC => ({ server, hasAutoConnect }) => ( - - {hasAutoConnect && ( - - {server.autoConnect && ( - <> - - - Auto-connect to this server - - - )} +type ManageServersRowDeps = { + ManageServersRowDropdown: FC; +}; + +const ManageServersRow: FCWithDeps = ({ server, hasAutoConnect }) => { + const { ManageServersRowDropdown } = useDependencies(ManageServersRow); + + return ( + + {hasAutoConnect && ( + + {server.autoConnect && ( + <> + + + Auto-connect to this server + + + )} + + )} + + {server.name} + + {server.url} + + - )} - - {server.name} - - {server.url} - - - - -); + + ); +}; + +export const ManageServersRowFactory = componentFactory(ManageServersRow, ['ManageServersRowDropdown']); diff --git a/src/servers/ManageServersRowDropdown.tsx b/src/servers/ManageServersRowDropdown.tsx index 94cb42ff..3b106045 100644 --- a/src/servers/ManageServersRowDropdown.tsx +++ b/src/servers/ManageServersRowDropdown.tsx @@ -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, -): FC => ({ server, setAutoConnect }) => { +type ManageServersRowDropdownDeps = { + DeleteServerModal: FC +}; + +const ManageServersRowDropdown: FCWithDeps = ( + { 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 = ( ); }; + +export const ManageServersRowDropdownFactory = componentFactory(ManageServersRowDropdown, ['DeleteServerModal']); diff --git a/src/servers/helpers/ImportServersBtn.tsx b/src/servers/helpers/ImportServersBtn.tsx index 9856bd9e..132c8520 100644 --- a/src/servers/helpers/ImportServersBtn.tsx +++ b/src/servers/helpers/ImportServersBtn.tsx @@ -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 => ({ +const ImportServersBtn: FCWithDeps = ({ createServers, servers, children, @@ -33,6 +39,7 @@ export const ImportServersBtn = (serversImporter: ServersImporter): FC { + const { ServersImporter: serversImporter } = useDependencies(ImportServersBtn); const ref = useElementRef(); const [duplicatedServers, setDuplicatedServers] = useState([]); const [isModalOpen,, showModal, hideModal] = useToggle(); @@ -60,7 +67,7 @@ export const ImportServersBtn = (serversImporter: ServersImporter): FC { @@ -92,3 +99,5 @@ export const ImportServersBtn = (serversImporter: ServersImporter): FC ); }; + +export const ImportServersBtnFactory = componentFactory(ImportServersBtn, ['ServersImporter']); diff --git a/src/servers/helpers/ServerError.tsx b/src/servers/helpers/ServerError.tsx index 3e0dc594..6b80fe45 100644 --- a/src/servers/helpers/ServerError.tsx +++ b/src/servers/helpers/ServerError.tsx @@ -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; +}; + +const ServerError: FCWithDeps = ({ servers, selectedServer }) => { + const { DeleteServerButton } = useDependencies(ServerError); + + return ( + +
+ + {!isServerWithId(selectedServer) && 'Could not find this Shlink server.'} + {isServerWithId(selectedServer) && ( + <> +

Oops! Could not connect to this Shlink server.

+ Make sure you have internet connection, and the server is properly configured and on-line. + + )} +
+ + + These are the Shlink servers currently configured. Choose one of + them or add a new one. + -export const ServerError = (DeleteServerButton: FC): FC => ( - { servers, selectedServer }, -) => ( - -
- - {!isServerWithId(selectedServer) && 'Could not find this Shlink server.'} {isServerWithId(selectedServer) && ( - <> -

Oops! Could not connect to this Shlink server.

- Make sure you have internet connection, and the server is properly configured and on-line. - +
+
+ Alternatively, if you think you may have miss-configured this server, you + can remove it or  + edit it. +
+
)} -
+
+
+ ); +}; - - These are the Shlink servers currently configured. Choose one of - them or add a new one. - - - {isServerWithId(selectedServer) && ( -
-
- Alternatively, if you think you may have miss-configured this server, you - can remove it or  - edit it. -
-
- )} -
-
-); +export const ServerErrorFactory = componentFactory(ServerError, ['DeleteServerButton']); diff --git a/src/servers/helpers/withSelectedServer.tsx b/src/servers/helpers/withSelectedServer.tsx index 61804290..a2158f3a 100644 --- a/src/servers/helpers/withSelectedServer.tsx +++ b/src/servers/helpers/withSelectedServer.tsx @@ -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(WrappedComponent: FC, ServerError: FC) { - return (props: WithSelectedServerProps & T) => { +type WithSelectedServerPropsDeps = { + ServerError: FC; +}; + +export function withSelectedServer( + WrappedComponent: FCWithDeps, +) { + const ComponentWrapper: FCWithDeps = (props) => { + const { ServerError } = useDependencies(ComponentWrapper); const params = useParams<{ serverId: string }>(); const { selectServer, selectedServer } = props; @@ -34,4 +43,5 @@ export function withSelectedServer(WrappedComponent: FC; }; + return ComponentWrapper; } diff --git a/src/servers/services/provideServices.ts b/src/servers/services/provideServices.ts index cee6552b..8c332d55 100644 --- a/src/servers/services/provideServices.ts +++ b/src/servers/services/provideServices.ts @@ -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 diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx index 81fb3468..3d16a4ae 100644 --- a/src/settings/Settings.tsx +++ b/src/settings/Settings.tsx @@ -2,6 +2,17 @@ import { NavPillItem, NavPills } from '@shlinkio/shlink-frontend-kit'; import type { FC, ReactNode } from 'react'; import { Navigate, Route, Routes } from 'react-router-dom'; 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 }) => ( <> @@ -9,26 +20,39 @@ const SettingsSections: FC<{ items: ReactNode[] }> = ({ items }) => ( ); -export const Settings = ( - RealTimeUpdates: FC, - ShortUrlCreation: FC, - ShortUrlsList: FC, - UserInterface: FC, - Visits: FC, - Tags: FC, -) => () => ( - - - General - Short URLs - Other items - +const Settings: FCWithDeps<{}, SettingsDeps> = () => { + const { + RealTimeUpdatesSettings: RealTimeUpdates, + ShortUrlCreationSettings: ShortUrlCreation, + ShortUrlsListSettings: ShortUrlsList, + UserInterfaceSettings: UserInterface, + VisitsSettings: Visits, + TagsSettings: Tags, + } = useDependencies(Settings); - - , ]} />} /> - , ]} />} /> - , ]} />} /> - } /> - - -); + return ( + + + General + Short URLs + Other items + + + + , ]} />} /> + , ]} />} /> + , ]} />} /> + } /> + + + ); +}; + +export const SettingsFactory = componentFactory(Settings, [ + 'RealTimeUpdatesSettings', + 'ShortUrlCreationSettings', + 'ShortUrlsListSettings', + 'UserInterfaceSettings', + 'VisitsSettings', + 'TagsSettings', +]); diff --git a/src/settings/services/provideServices.ts b/src/settings/services/provideServices.ts index 8f9dc9e4..4a09cb0d 100644 --- a/src/settings/services/provideServices.ts +++ b/src/settings/services/provideServices.ts @@ -11,7 +11,7 @@ import { setVisitsSettings, toggleRealTimeUpdates, } from '../reducers/settings'; -import { Settings } from '../Settings'; +import { SettingsFactory } from '../Settings'; import { ShortUrlCreationSettings } from '../ShortUrlCreationSettings'; import { ShortUrlsListSettings } from '../ShortUrlsListSettings'; import { TagsSettings } from '../TagsSettings'; @@ -20,16 +20,7 @@ import { VisitsSettings } from '../VisitsSettings'; export const provideServices = (bottle: Bottle, connect: ConnectDecorator) => { // Components - bottle.serviceFactory( - 'Settings', - Settings, - 'RealTimeUpdatesSettings', - 'ShortUrlCreationSettings', - 'ShortUrlsListSettings', - 'UserInterfaceSettings', - 'VisitsSettings', - 'TagsSettings', - ); + bottle.factory('Settings', SettingsFactory); bottle.decorator('Settings', withoutSelectedServer); bottle.decorator('Settings', connect(null, ['resetSelectedServer'])); diff --git a/test/common/ErrorHandler.test.tsx b/test/common/ErrorHandler.test.tsx index 4f4e8ef8..f95e48c9 100644 --- a/test/common/ErrorHandler.test.tsx +++ b/test/common/ErrorHandler.test.tsx @@ -1,6 +1,7 @@ import { render, screen } from '@testing-library/react'; 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'; const ComponentWithError = () => { @@ -9,18 +10,16 @@ const ComponentWithError = () => { describe('', () => { const reload = vi.fn(); - const window = fromPartial({ - location: { reload }, - }); + const location = fromPartial({ reload }); const cons = fromPartial({ error: vi.fn() }); - const ErrorHandler = createErrorHandler(window, cons); + const ErrorHandler = (props: PropsWithChildren) => ; beforeEach(() => { vi.spyOn(console, 'error').mockImplementation(() => {}); // Silence react errors }); it('renders children when no error has occurred', () => { - render(Foo} />); + render(Foo); expect(screen.getByText('Foo')).toBeInTheDocument(); expect(screen.queryByText('Oops! This is awkward :S')).not.toBeInTheDocument(); @@ -28,14 +27,14 @@ describe('', () => { }); it('renders error page when error has occurred', () => { - render(} />); + render(); expect(screen.getByText('Oops! This is awkward :S')).toBeInTheDocument(); expect(screen.getByRole('button')).toBeInTheDocument(); }); it('reloads page on button click', async () => { - const { user } = renderWithEvents(} />); + const { user } = renderWithEvents(); expect(reload).not.toHaveBeenCalled(); await user.click(screen.getByRole('button')); diff --git a/test/common/MainHeader.test.tsx b/test/common/MainHeader.test.tsx index 678df1f8..f38fc2ee 100644 --- a/test/common/MainHeader.test.tsx +++ b/test/common/MainHeader.test.tsx @@ -1,11 +1,14 @@ import { screen, waitFor } from '@testing-library/react'; +import { fromPartial } from '@total-typescript/shoehorn'; import { createMemoryHistory } from 'history'; 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'; describe('', () => { - const MainHeader = createMainHeader(() => <>ServersDropdown); + const MainHeader = MainHeaderFactory(fromPartial({ + ServersDropdown: () => <>ServersDropdown, + })); const setUp = (pathname = '') => { const history = createMemoryHistory(); history.push(pathname); diff --git a/test/common/ShlinkWebComponentContainer.test.tsx b/test/common/ShlinkWebComponentContainer.test.tsx index 54bf6411..71438dbb 100644 --- a/test/common/ShlinkWebComponentContainer.test.tsx +++ b/test/common/ShlinkWebComponentContainer.test.tsx @@ -1,7 +1,7 @@ import { render, screen } from '@testing-library/react'; import { fromPartial } from '@total-typescript/shoehorn'; 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'; vi.mock('react-router-dom', async () => ({ @@ -10,12 +10,12 @@ vi.mock('react-router-dom', async () => ({ })); describe('', () => { - const ShlinkWebComponentContainer = createContainer( - vi.fn().mockReturnValue(fromPartial({})), - fromPartial({}), - () => <>ShlinkWebComponent, - () => <>ServerError, - ); + const ShlinkWebComponentContainer = ShlinkWebComponentContainerFactory(fromPartial({ + buildShlinkApiClient: vi.fn().mockReturnValue(fromPartial({})), + TagColorsStorage: fromPartial({}), + ShlinkWebComponent: () => <>ShlinkWebComponent, + ServerError: () => <>ServerError, + })); const setUp = (selectedServer: SelectedServer) => render( , ); diff --git a/test/servers/CreateServer.test.tsx b/test/servers/CreateServer.test.tsx index 0d0f5e21..70693636 100644 --- a/test/servers/CreateServer.test.tsx +++ b/test/servers/CreateServer.test.tsx @@ -1,7 +1,7 @@ import { fireEvent, screen, waitFor } from '@testing-library/react'; import { fromPartial } from '@total-typescript/shoehorn'; 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 { renderWithEvents } from '../__helpers__/setUpTest'; @@ -31,7 +31,10 @@ describe('', () => { callCount += 1; return result; }); - const CreateServer = createCreateServer(() => <>ImportServersBtn, useTimeoutToggle); + const CreateServer = CreateServerFactory(fromPartial({ + ImportServersBtn: () => <>ImportServersBtn, + useTimeoutToggle, + })); return renderWithEvents(); }; diff --git a/test/servers/DeleteServerButton.test.tsx b/test/servers/DeleteServerButton.test.tsx index 2b3e69f9..a9e188e7 100644 --- a/test/servers/DeleteServerButton.test.tsx +++ b/test/servers/DeleteServerButton.test.tsx @@ -1,13 +1,14 @@ import { screen, waitFor } from '@testing-library/react'; import { fromPartial } from '@total-typescript/shoehorn'; 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'; describe('', () => { - const DeleteServerButton = createDeleteServerButton( - ({ isOpen }) => <>DeleteServerModal {isOpen ? '[Open]' : '[Closed]'}, - ); + const DeleteServerButton = DeleteServerButtonFactory(fromPartial({ + DeleteServerModal: ({ isOpen }: DeleteServerModalProps) => <>DeleteServerModal {isOpen ? '[Open]' : '[Closed]'}, + })); const setUp = (children?: ReactNode) => renderWithEvents( {children}, ); diff --git a/test/servers/EditServer.test.tsx b/test/servers/EditServer.test.tsx index 44ceeec5..f3c379c7 100644 --- a/test/servers/EditServer.test.tsx +++ b/test/servers/EditServer.test.tsx @@ -2,7 +2,7 @@ import { fireEvent, screen } from '@testing-library/react'; import { fromPartial } from '@total-typescript/shoehorn'; import { MemoryRouter, useNavigate } from 'react-router-dom'; 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'; vi.mock('react-router-dom', async () => ({ @@ -20,7 +20,7 @@ describe('', () => { url: 'the_url', apiKey: 'the_api_key', }); - const EditServer = editServerConstruct(ServerError); + const EditServer = EditServerFactory(fromPartial({ ServerError })); const setUp = (selectedServer: SelectedServer = defaultSelectedServer) => renderWithEvents( diff --git a/test/servers/ManageServers.test.tsx b/test/servers/ManageServers.test.tsx index 3a29c79d..7a87f49f 100644 --- a/test/servers/ManageServers.test.tsx +++ b/test/servers/ManageServers.test.tsx @@ -2,7 +2,7 @@ import { screen, waitFor } from '@testing-library/react'; import { fromPartial } from '@total-typescript/shoehorn'; import { MemoryRouter } from 'react-router-dom'; 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 { renderWithEvents } from '../__helpers__/setUpTest'; @@ -10,12 +10,14 @@ describe('', () => { const exportServers = vi.fn(); const serversExporter = fromPartial({ exportServers }); const useTimeoutToggle = vi.fn().mockReturnValue([false, vi.fn()]); - const ManageServers = createManageServers( - serversExporter, - () => ImportServersBtn, + const ManageServers = ManageServersFactory(fromPartial({ + ServersExporter: serversExporter, + ImportServersBtn: () => ImportServersBtn, useTimeoutToggle, - ({ hasAutoConnect }) => ManageServersRow {hasAutoConnect ? '[YES]' : '[NO]'}, - ); + ManageServersRow: ({ hasAutoConnect }: { hasAutoConnect: boolean }) => ( + ManageServersRow {hasAutoConnect ? '[YES]' : '[NO]'} + ), + })); const createServerMock = (value: string, autoConnect = false) => fromPartial( { id: value, name: value, url: value, autoConnect }, ); diff --git a/test/servers/ManageServersRow.test.tsx b/test/servers/ManageServersRow.test.tsx index 25a27d65..e9160c4e 100644 --- a/test/servers/ManageServersRow.test.tsx +++ b/test/servers/ManageServersRow.test.tsx @@ -1,10 +1,13 @@ import { render, screen } from '@testing-library/react'; +import { fromPartial } from '@total-typescript/shoehorn'; import { MemoryRouter } from 'react-router-dom'; import type { ServerWithId } from '../../src/servers/data'; -import { ManageServersRow as createManageServersRow } from '../../src/servers/ManageServersRow'; +import { ManageServersRowFactory } from '../../src/servers/ManageServersRow'; describe('', () => { - const ManageServersRow = createManageServersRow(() => ManageServersRowDropdown); + const ManageServersRow = ManageServersRowFactory(fromPartial({ + ManageServersRowDropdown: () => ManageServersRowDropdown, + })); const server: ServerWithId = { name: 'My server', url: 'https://example.com', diff --git a/test/servers/ManageServersRowDropdown.test.tsx b/test/servers/ManageServersRowDropdown.test.tsx index 6825cb5e..e9aa93b1 100644 --- a/test/servers/ManageServersRowDropdown.test.tsx +++ b/test/servers/ManageServersRowDropdown.test.tsx @@ -2,13 +2,15 @@ import { screen } from '@testing-library/react'; import { fromPartial } from '@total-typescript/shoehorn'; import { MemoryRouter } from 'react-router-dom'; 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'; describe('', () => { - const ManageServersRowDropdown = createManageServersRowDropdown( - ({ isOpen }) => DeleteServerModal {isOpen ? '[OPEN]' : '[CLOSED]'}, - ); + const ManageServersRowDropdown = ManageServersRowDropdownFactory(fromPartial({ + DeleteServerModal: ({ isOpen }: { isOpen: boolean }) => ( + DeleteServerModal {isOpen ? '[OPEN]' : '[CLOSED]'} + ), + })); const setAutoConnect = vi.fn(); const setUp = (autoConnect = false) => { const server = fromPartial({ id: 'abc123', autoConnect }); diff --git a/test/servers/helpers/ImportServersBtn.test.tsx b/test/servers/helpers/ImportServersBtn.test.tsx index 44f0c08a..22f9643c 100644 --- a/test/servers/helpers/ImportServersBtn.test.tsx +++ b/test/servers/helpers/ImportServersBtn.test.tsx @@ -3,9 +3,7 @@ import { fromPartial } from '@total-typescript/shoehorn'; import type { ServersMap, ServerWithId } from '../../../src/servers/data'; import type { ImportServersBtnProps } from '../../../src/servers/helpers/ImportServersBtn'; -import { - ImportServersBtn as createImportServersBtn, -} from '../../../src/servers/helpers/ImportServersBtn'; +import { ImportServersBtnFactory } from '../../../src/servers/helpers/ImportServersBtn'; import type { ServersImporter } from '../../../src/servers/services/ServersImporter'; import { renderWithEvents } from '../../__helpers__/setUpTest'; @@ -14,7 +12,7 @@ describe('', () => { const createServersMock = vi.fn(); const importServersFromFile = vi.fn().mockResolvedValue([]); const serversImporterMock = fromPartial({ importServersFromFile }); - const ImportServersBtn = createImportServersBtn(serversImporterMock); + const ImportServersBtn = ImportServersBtnFactory(fromPartial({ ServersImporter: serversImporterMock })); const setUp = (props: Partial = {}, servers: ServersMap = {}) => renderWithEvents( ', () => { - const ServerError = createServerError(() => null); + const ServerError = ServerErrorFactory(fromPartial({ DeleteServerButton: () => null })); it.each([ [ diff --git a/test/settings/Settings.test.tsx b/test/settings/Settings.test.tsx index bef4d26b..5c2321d0 100644 --- a/test/settings/Settings.test.tsx +++ b/test/settings/Settings.test.tsx @@ -1,17 +1,18 @@ import { render, screen } from '@testing-library/react'; +import { fromPartial } from '@total-typescript/shoehorn'; import { createMemoryHistory } from 'history'; import { Router } from 'react-router-dom'; -import { Settings as createSettings } from '../../src/settings/Settings'; +import { SettingsFactory } from '../../src/settings/Settings'; describe('', () => { - const Settings = createSettings( - () => RealTimeUpdates, - () => ShortUrlCreation, - () => ShortUrlsList, - () => UserInterface, - () => Visits, - () => Tags, - ); + const Settings = SettingsFactory(fromPartial({ + RealTimeUpdatesSettings: () => RealTimeUpdates, + ShortUrlCreationSettings: () => ShortUrlCreation, + ShortUrlsListSettings: () => ShortUrlsList, + UserInterfaceSettings: () => UserInterface, + VisitsSettings: () => Visits, + TagsSettings: () => Tags, + })); const setUp = (activeRoute = '/') => { const history = createMemoryHistory(); history.push(activeRoute);