diff --git a/src/common/AsideMenu.tsx b/src/common/AsideMenu.tsx index 75647382..6283ff49 100644 --- a/src/common/AsideMenu.tsx +++ b/src/common/AsideMenu.tsx @@ -13,18 +13,18 @@ import { DeleteServerButtonProps } from '../servers/DeleteServerButton'; import { ServerWithId } from '../servers/data'; import './AsideMenu.scss'; -interface AsideMenuProps { +export interface AsideMenuProps { selectedServer: ServerWithId; className?: string; showOnMobile?: boolean; } -interface AsideMenuItemItemProps extends NavLinkProps { +interface AsideMenuItemProps extends NavLinkProps { to: string; className?: string; } -const AsideMenuItem: FC = ({ children, to, className, ...rest }) => ( +const AsideMenuItem: FC = ({ children, to, className, ...rest }) => ( class ErrorHandler extends React.Component { - static propTypes = { - children: PropTypes.node.isRequired, - }; +interface ErrorHandlerState { + hasError: boolean; +} - constructor(props) { +const ErrorHandler = ( + { location }: Window, + { error }: Console, +) => class ErrorHandler extends React.Component { + public constructor(props: object) { super(props); this.state = { hasError: false }; } - static getDerivedStateFromError() { + public static getDerivedStateFromError(): ErrorHandlerState { return { hasError: true }; } - componentDidCatch(e) { + public componentDidCatch(e: Error): void { if (process.env.NODE_ENV !== 'development') { error(e); } } - render() { + public render(): ReactNode | undefined { if (this.state.hasError) { return (
diff --git a/src/common/Home.tsx b/src/common/Home.tsx index 4c5623f3..252e1d62 100644 --- a/src/common/Home.tsx +++ b/src/common/Home.tsx @@ -2,8 +2,8 @@ import React, { useEffect } from 'react'; import { isEmpty, values } from 'ramda'; import { Link } from 'react-router-dom'; import ServersListGroup from '../servers/ServersListGroup'; -import { ServersMap } from '../servers/reducers/servers'; import './Home.scss'; +import { ServersMap } from '../servers/data'; export interface HomeProps { resetSelectedServer: Function; diff --git a/src/common/MenuLayout.js b/src/common/MenuLayout.js deleted file mode 100644 index f890223d..00000000 --- a/src/common/MenuLayout.js +++ /dev/null @@ -1,98 +0,0 @@ -import React, { useEffect } from 'react'; -import { Route, Switch } from 'react-router-dom'; -import { Swipeable } from 'react-swipeable'; -import { faBars as burgerIcon } from '@fortawesome/free-solid-svg-icons'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import classNames from 'classnames'; -import * as PropTypes from 'prop-types'; -import { serverType } from '../servers/prop-types'; -import { withSelectedServer } from '../servers/helpers/withSelectedServer'; -import { useToggle } from '../utils/helpers/hooks'; -import { versionMatch } from '../utils/helpers/version'; -import NotFound from './NotFound'; -import './MenuLayout.scss'; - -const propTypes = { - match: PropTypes.object, - location: PropTypes.object, - selectedServer: serverType, -}; - -const MenuLayout = ( - TagsList, - ShortUrls, - AsideMenu, - CreateShortUrl, - ShortUrlVisits, - TagVisits, - ShlinkVersions, - ServerError, -) => { - const MenuLayoutComp = ({ match, location, selectedServer }) => { - const [ sidebarVisible, toggleSidebar, showSidebar, hideSidebar ] = useToggle(); - const { params: { serverId } } = match; - - useEffect(() => hideSidebar(), [ location ]); - - if (selectedServer.serverNotReachable) { - return ; - } - - const addTagsVisitsRoute = versionMatch(selectedServer.version, { minVersion: '2.2.0' }); - const burgerClasses = classNames('menu-layout__burger-icon', { - 'menu-layout__burger-icon--active': sidebarVisible, - }); - const swipeMenuIfNoModalExists = (callback) => (e) => { - const swippedOnVisitsTable = e.event.path.some( - ({ classList }) => classList && classList.contains('visits-table'), - ); - - if (swippedOnVisitsTable || document.querySelector('.modal')) { - return; - } - - callback(); - }; - - return ( - - - - -
- -
hideSidebar()}> -
- - - - - {addTagsVisitsRoute && } - - List short URLs} - /> - -
- -
- -
-
-
-
-
- ); - }; - - MenuLayoutComp.propTypes = propTypes; - - return withSelectedServer(MenuLayoutComp, ServerError); -}; - -export default MenuLayout; diff --git a/src/common/MenuLayout.tsx b/src/common/MenuLayout.tsx new file mode 100644 index 00000000..79b5a043 --- /dev/null +++ b/src/common/MenuLayout.tsx @@ -0,0 +1,89 @@ +import React, { FC, useEffect } from 'react'; +import { Route, RouteChildrenProps, Switch } from 'react-router-dom'; +import { EventData, Swipeable } from 'react-swipeable'; +import { faBars as burgerIcon } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import classNames from 'classnames'; +import { withSelectedServer } from '../servers/helpers/withSelectedServer'; +import { useToggle } from '../utils/helpers/hooks'; +import { versionMatch } from '../utils/helpers/version'; +import { isReachableServer, SelectedServer } from '../servers/data'; +import NotFound from './NotFound'; +import { AsideMenuProps } from './AsideMenu'; +import './MenuLayout.scss'; + +interface MenuLayoutProps extends RouteChildrenProps { + selectedServer: SelectedServer; +} + +const MenuLayout = ( + TagsList: FC, + ShortUrls: FC, + AsideMenu: FC, + CreateShortUrl: FC, + ShortUrlVisits: FC, + TagVisits: FC, + ShlinkVersions: FC, + ServerError: FC, +) => withSelectedServer(({ location, selectedServer }: MenuLayoutProps) => { + const [ sidebarVisible, toggleSidebar, showSidebar, hideSidebar ] = useToggle(); + + useEffect(() => hideSidebar(), [ location ]); + + if (!isReachableServer(selectedServer)) { + return ; + } + + const addTagsVisitsRoute = versionMatch(selectedServer.version, { minVersion: '2.2.0' }); + const burgerClasses = classNames('menu-layout__burger-icon', { + 'menu-layout__burger-icon--active': sidebarVisible, + }); + const swipeMenuIfNoModalExists = (callback: () => void) => (e: EventData) => { + const swippedOnVisitsTable = (e.event.composedPath() as HTMLElement[]).some( + ({ classList }) => classList?.contains('visits-table'), + ); + + if (swippedOnVisitsTable || document.querySelector('.modal')) { + return; + } + + callback(); + }; + + return ( + + + + +
+ +
hideSidebar()}> +
+ + + + + {addTagsVisitsRoute && } + + List short URLs} + /> + +
+ +
+ +
+
+
+
+
+ ); +}, ServerError); + +export default MenuLayout; diff --git a/src/container/types.ts b/src/container/types.ts index 9afa2de0..8b04e6fb 100644 --- a/src/container/types.ts +++ b/src/container/types.ts @@ -1,6 +1,5 @@ import { MercureInfo } from '../mercure/reducers/mercureInfo'; -import { ServersMap } from '../servers/reducers/servers'; -import { SelectedServer } from '../servers/data'; +import { SelectedServer, ServersMap } from '../servers/data'; import { Settings } from '../settings/reducers/settings'; import { ShortUrlMetaEdition } from '../short-urls/reducers/shortUrlMeta'; import { ShortUrlCreation } from '../short-urls/reducers/shortUrlCreation'; diff --git a/src/servers/data/index.ts b/src/servers/data/index.ts index 3c918cbb..61f86009 100644 --- a/src/servers/data/index.ts +++ b/src/servers/data/index.ts @@ -25,8 +25,13 @@ export type RegularServer = ReachableServer | NonReachableServer; export type SelectedServer = RegularServer | NotFoundServer | null; +export type ServersMap = Record; + export const hasServerData = (server: ServerData | NotFoundServer | null): server is ServerData => !!(server as ServerData)?.url && !!(server as ServerData)?.apiKey; export const isReachableServer = (server: SelectedServer): server is ReachableServer => !!server?.hasOwnProperty('printableVersion'); + +export const isServerWithId = (server: SelectedServer | ServerWithId): server is ServerWithId => + !!server?.hasOwnProperty('id'); diff --git a/src/servers/helpers/ServerError.js b/src/servers/helpers/ServerError.js deleted file mode 100644 index 14983987..00000000 --- a/src/servers/helpers/ServerError.js +++ /dev/null @@ -1,50 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { Link } from 'react-router-dom'; -import Message from '../../utils/Message'; -import ServersListGroup from '../ServersListGroup'; -import { serverType } from '../prop-types'; -import './ServerError.scss'; - -const propTypes = { - servers: PropTypes.object, - selectedServer: serverType, - type: PropTypes.oneOf([ 'not-found', 'not-reachable' ]).isRequired, -}; - -export const ServerError = (DeleteServerButton) => { - const ServerErrorComp = ({ type, servers, selectedServer }) => ( -
-
- - {type === 'not-found' && 'Could not find this Shlink server.'} - {type === 'not-reachable' && ( - -

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. - - - {type === 'not-reachable' && ( -
-
- Alternatively, if you think you may have miss-configured this server, you - can remove it or  - edit it. -
-
- )} -
- ); - - ServerErrorComp.propTypes = propTypes; - - return ServerErrorComp; -}; diff --git a/src/servers/helpers/ServerError.tsx b/src/servers/helpers/ServerError.tsx new file mode 100644 index 00000000..f4d1dc0f --- /dev/null +++ b/src/servers/helpers/ServerError.tsx @@ -0,0 +1,45 @@ +import React, { FC } from 'react'; +import { Link } from 'react-router-dom'; +import Message from '../../utils/Message'; +import ServersListGroup from '../ServersListGroup'; +import { DeleteServerButtonProps } from '../DeleteServerButton'; +import { isServerWithId, SelectedServer, ServersMap } from '../data'; +import './ServerError.scss'; + +interface ServerErrorProps { + servers: ServersMap; + selectedServer: SelectedServer; +} + +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. +
+ )} +
+
+ + + 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. +
+
+ )} +
+); diff --git a/src/servers/helpers/withSelectedServer.js b/src/servers/helpers/withSelectedServer.js index 3c524e61..737f9894 100644 --- a/src/servers/helpers/withSelectedServer.js +++ b/src/servers/helpers/withSelectedServer.js @@ -23,7 +23,7 @@ export const withSelectedServer = (WrappedComponent, ServerError) => { } if (selectedServer.serverNotFound) { - return ; + return ; } return ; diff --git a/src/servers/reducers/servers.ts b/src/servers/reducers/servers.ts index 2312b0e5..057f5531 100644 --- a/src/servers/reducers/servers.ts +++ b/src/servers/reducers/servers.ts @@ -1,7 +1,7 @@ -import { pipe, assoc, map, reduce, dissoc } from 'ramda'; +import { assoc, dissoc, map, pipe, reduce } from 'ramda'; import { v4 as uuid } from 'uuid'; import { Action } from 'redux'; -import { ServerData, ServerWithId } from '../data'; +import { ServerData, ServersMap, ServerWithId } from '../data'; import { buildReducer } from '../../utils/helpers/redux'; /* eslint-disable padding-line-between-statements */ @@ -10,8 +10,6 @@ export const DELETE_SERVER = 'shlink/servers/DELETE_SERVER'; export const CREATE_SERVERS = 'shlink/servers/CREATE_SERVERS'; /* eslint-enable padding-line-between-statements */ -export type ServersMap = Record; - export interface CreateServersAction extends Action { newServers: ServersMap; } diff --git a/src/visits/ShortUrlVisitsHeader.js b/src/visits/ShortUrlVisitsHeader.js index a3b100a3..1d901cbc 100644 --- a/src/visits/ShortUrlVisitsHeader.js +++ b/src/visits/ShortUrlVisitsHeader.js @@ -20,7 +20,7 @@ const ShortUrlVisitsHeader = ({ shortUrlDetail, shortUrlVisits, goBack }) => { const shortLink = shortUrl && shortUrl.shortUrl ? shortUrl.shortUrl : ''; const longLink = shortUrl && shortUrl.longUrl ? shortUrl.longUrl : ''; - const renderDate = () => ( + const renderDate = () => !shortUrl ? Loading... : ( {shortUrl.dateCreated} diff --git a/test/common/ErrorHandler.test.js b/test/common/ErrorHandler.test.tsx similarity index 81% rename from test/common/ErrorHandler.test.js rename to test/common/ErrorHandler.test.tsx index a98437d0..06402637 100644 --- a/test/common/ErrorHandler.test.js +++ b/test/common/ErrorHandler.test.tsx @@ -1,16 +1,17 @@ import React from 'react'; -import { shallow } from 'enzyme'; +import { shallow, ShallowWrapper } from 'enzyme'; import { Button } from 'reactstrap'; +import { Mock } from 'ts-mockery'; import createErrorHandler from '../../src/common/ErrorHandler'; describe('', () => { - const window = { + const window = Mock.of({ location: { reload: jest.fn(), }, - }; - const console = { error: jest.fn() }; - let wrapper; + }); + const console = Mock.of({ error: jest.fn() }); + let wrapper: ShallowWrapper; beforeEach(() => { const ErrorHandler = createErrorHandler(window, console); diff --git a/test/servers/helpers/ServerError.test.js b/test/servers/helpers/ServerError.test.tsx similarity index 70% rename from test/servers/helpers/ServerError.test.js rename to test/servers/helpers/ServerError.test.tsx index 0847afe2..bd31dbf6 100644 --- a/test/servers/helpers/ServerError.test.js +++ b/test/servers/helpers/ServerError.test.tsx @@ -1,18 +1,19 @@ import React from 'react'; -import { shallow } from 'enzyme'; +import { shallow, ShallowWrapper } from 'enzyme'; import { BrowserRouter } from 'react-router-dom'; +import { Mock } from 'ts-mockery'; import { ServerError as createServerError } from '../../../src/servers/helpers/ServerError'; +import { NonReachableServer, NotFoundServer } from '../../../src/servers/data'; describe('', () => { - let wrapper; - const selectedServer = { id: '' }; - const ServerError = createServerError(() => ''); + let wrapper: ShallowWrapper; + const ServerError = createServerError(() => null); - afterEach(() => wrapper && wrapper.unmount()); + afterEach(() => wrapper?.unmount()); it.each([ [ - 'not-found', + Mock.all(), { 'Could not find this Shlink server.': true, 'Oops! Could not connect to this Shlink server.': false, @@ -21,7 +22,7 @@ describe('', () => { }, ], [ - 'not-reachable', + Mock.of({ id: 'abc123' }), { 'Could not find this Shlink server.': false, 'Oops! Could not connect to this Shlink server.': true, @@ -29,10 +30,10 @@ describe('', () => { 'Alternatively, if you think you may have miss-configured this server': true, }, ], - ])('renders expected information for type "%s"', (type, textsToFind) => { + ])('renders expected information based on provided server type', (selectedServer, textsToFind) => { wrapper = shallow( - + , ); const wrapperText = wrapper.html();