mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2025-01-03 14:57:22 +03:00
Migrated some common components and their dependencies to TS
This commit is contained in:
parent
a96539129d
commit
f40ad91ea9
25 changed files with 274 additions and 322 deletions
2
shlink-web-client.d.ts
vendored
2
shlink-web-client.d.ts
vendored
|
@ -1,4 +1,6 @@
|
||||||
export declare global {
|
export declare global {
|
||||||
|
declare module '*.png'
|
||||||
|
|
||||||
interface Window {
|
interface Window {
|
||||||
__REDUX_DEVTOOLS_EXTENSION_COMPOSE__?: Function;
|
__REDUX_DEVTOOLS_EXTENSION_COMPOSE__?: Function;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,81 +0,0 @@
|
||||||
import {
|
|
||||||
faList as listIcon,
|
|
||||||
faLink as createIcon,
|
|
||||||
faTags as tagsIcon,
|
|
||||||
faPen as editIcon,
|
|
||||||
} from '@fortawesome/free-solid-svg-icons';
|
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
|
||||||
import React from 'react';
|
|
||||||
import { NavLink } from 'react-router-dom';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import classNames from 'classnames';
|
|
||||||
import { serverType } from '../servers/prop-types';
|
|
||||||
import './AsideMenu.scss';
|
|
||||||
|
|
||||||
const AsideMenuItem = ({ children, to, className, ...rest }) => (
|
|
||||||
<NavLink
|
|
||||||
className={classNames('aside-menu__item', className)}
|
|
||||||
activeClassName="aside-menu__item--selected"
|
|
||||||
to={to}
|
|
||||||
{...rest}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</NavLink>
|
|
||||||
);
|
|
||||||
|
|
||||||
AsideMenuItem.propTypes = {
|
|
||||||
children: PropTypes.node.isRequired,
|
|
||||||
to: PropTypes.string.isRequired,
|
|
||||||
className: PropTypes.string,
|
|
||||||
};
|
|
||||||
|
|
||||||
const propTypes = {
|
|
||||||
selectedServer: serverType,
|
|
||||||
className: PropTypes.string,
|
|
||||||
showOnMobile: PropTypes.bool,
|
|
||||||
};
|
|
||||||
|
|
||||||
const AsideMenu = (DeleteServerButton) => {
|
|
||||||
const AsideMenu = ({ selectedServer, className, showOnMobile }) => {
|
|
||||||
const serverId = selectedServer ? selectedServer.id : '';
|
|
||||||
const asideClass = classNames('aside-menu', className, {
|
|
||||||
'aside-menu--hidden': !showOnMobile,
|
|
||||||
});
|
|
||||||
const shortUrlsIsActive = (match, location) => location.pathname.match('/list-short-urls');
|
|
||||||
const buildPath = (suffix) => `/server/${serverId}${suffix}`;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<aside className={asideClass}>
|
|
||||||
<nav className="nav flex-column aside-menu__nav">
|
|
||||||
<AsideMenuItem to={buildPath('/list-short-urls/1')} isActive={shortUrlsIsActive}>
|
|
||||||
<FontAwesomeIcon icon={listIcon} />
|
|
||||||
<span className="aside-menu__item-text">List short URLs</span>
|
|
||||||
</AsideMenuItem>
|
|
||||||
<AsideMenuItem to={buildPath('/create-short-url')}>
|
|
||||||
<FontAwesomeIcon icon={createIcon} flip="horizontal" />
|
|
||||||
<span className="aside-menu__item-text">Create short URL</span>
|
|
||||||
</AsideMenuItem>
|
|
||||||
<AsideMenuItem to={buildPath('/manage-tags')}>
|
|
||||||
<FontAwesomeIcon icon={tagsIcon} />
|
|
||||||
<span className="aside-menu__item-text">Manage tags</span>
|
|
||||||
</AsideMenuItem>
|
|
||||||
<AsideMenuItem to={buildPath('/edit')} className="aside-menu__item--push">
|
|
||||||
<FontAwesomeIcon icon={editIcon} />
|
|
||||||
<span className="aside-menu__item-text">Edit this server</span>
|
|
||||||
</AsideMenuItem>
|
|
||||||
<DeleteServerButton
|
|
||||||
className="aside-menu__item aside-menu__item--danger"
|
|
||||||
textClassName="aside-menu__item-text"
|
|
||||||
server={selectedServer}
|
|
||||||
/>
|
|
||||||
</nav>
|
|
||||||
</aside>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
AsideMenu.propTypes = propTypes;
|
|
||||||
|
|
||||||
return AsideMenu;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AsideMenu;
|
|
77
src/common/AsideMenu.tsx
Normal file
77
src/common/AsideMenu.tsx
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
import {
|
||||||
|
faList as listIcon,
|
||||||
|
faLink as createIcon,
|
||||||
|
faTags as tagsIcon,
|
||||||
|
faPen as editIcon,
|
||||||
|
} from '@fortawesome/free-solid-svg-icons';
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
|
import React, { FC } from 'react';
|
||||||
|
import { NavLink, NavLinkProps } from 'react-router-dom';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import { Location } from 'history';
|
||||||
|
import { DeleteServerButtonProps } from '../servers/DeleteServerButton';
|
||||||
|
import { ServerWithId } from '../servers/data';
|
||||||
|
import './AsideMenu.scss';
|
||||||
|
|
||||||
|
interface AsideMenuProps {
|
||||||
|
selectedServer: ServerWithId;
|
||||||
|
className?: string;
|
||||||
|
showOnMobile?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AsideMenuItemItemProps extends NavLinkProps {
|
||||||
|
to: string;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const AsideMenuItem: FC<AsideMenuItemItemProps> = ({ children, to, className, ...rest }) => (
|
||||||
|
<NavLink
|
||||||
|
className={classNames('aside-menu__item', className)}
|
||||||
|
activeClassName="aside-menu__item--selected"
|
||||||
|
to={to}
|
||||||
|
{...rest}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</NavLink>
|
||||||
|
);
|
||||||
|
|
||||||
|
const AsideMenu = (DeleteServerButton: FC<DeleteServerButtonProps>) => (
|
||||||
|
{ selectedServer, className, showOnMobile = false }: AsideMenuProps,
|
||||||
|
) => {
|
||||||
|
const serverId = selectedServer ? selectedServer.id : '';
|
||||||
|
const asideClass = classNames('aside-menu', className, {
|
||||||
|
'aside-menu--hidden': !showOnMobile,
|
||||||
|
});
|
||||||
|
const shortUrlsIsActive = (_: null, location: Location) => location.pathname.match('/list-short-urls') !== null;
|
||||||
|
const buildPath = (suffix: string) => `/server/${serverId}${suffix}`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<aside className={asideClass}>
|
||||||
|
<nav className="nav flex-column aside-menu__nav">
|
||||||
|
<AsideMenuItem to={buildPath('/list-short-urls/1')} isActive={shortUrlsIsActive}>
|
||||||
|
<FontAwesomeIcon icon={listIcon} />
|
||||||
|
<span className="aside-menu__item-text">List short URLs</span>
|
||||||
|
</AsideMenuItem>
|
||||||
|
<AsideMenuItem to={buildPath('/create-short-url')}>
|
||||||
|
<FontAwesomeIcon icon={createIcon} flip="horizontal" />
|
||||||
|
<span className="aside-menu__item-text">Create short URL</span>
|
||||||
|
</AsideMenuItem>
|
||||||
|
<AsideMenuItem to={buildPath('/manage-tags')}>
|
||||||
|
<FontAwesomeIcon icon={tagsIcon} />
|
||||||
|
<span className="aside-menu__item-text">Manage tags</span>
|
||||||
|
</AsideMenuItem>
|
||||||
|
<AsideMenuItem to={buildPath('/edit')} className="aside-menu__item--push">
|
||||||
|
<FontAwesomeIcon icon={editIcon} />
|
||||||
|
<span className="aside-menu__item-text">Edit this server</span>
|
||||||
|
</AsideMenuItem>
|
||||||
|
<DeleteServerButton
|
||||||
|
className="aside-menu__item aside-menu__item--danger"
|
||||||
|
textClassName="aside-menu__item-text"
|
||||||
|
server={selectedServer}
|
||||||
|
/>
|
||||||
|
</nav>
|
||||||
|
</aside>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AsideMenu;
|
|
@ -1,16 +1,16 @@
|
||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { isEmpty, values } from 'ramda';
|
import { isEmpty, values } from 'ramda';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import './Home.scss';
|
|
||||||
import ServersListGroup from '../servers/ServersListGroup';
|
import ServersListGroup from '../servers/ServersListGroup';
|
||||||
|
import { ServersMap } from '../servers/reducers/servers';
|
||||||
|
import './Home.scss';
|
||||||
|
|
||||||
const propTypes = {
|
export interface HomeProps {
|
||||||
resetSelectedServer: PropTypes.func,
|
resetSelectedServer: Function;
|
||||||
servers: PropTypes.object,
|
servers: ServersMap;
|
||||||
};
|
}
|
||||||
|
|
||||||
const Home = ({ resetSelectedServer, servers }) => {
|
const Home = ({ resetSelectedServer, servers }: HomeProps) => {
|
||||||
const serversList = values(servers);
|
const serversList = values(servers);
|
||||||
const hasServers = !isEmpty(serversList);
|
const hasServers = !isEmpty(serversList);
|
||||||
|
|
||||||
|
@ -29,6 +29,4 @@ const Home = ({ resetSelectedServer, servers }) => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
Home.propTypes = propTypes;
|
|
||||||
|
|
||||||
export default Home;
|
export default Home;
|
|
@ -1,61 +0,0 @@
|
||||||
import { faPlus as plusIcon, faChevronDown as arrowIcon, faCogs as cogsIcon } from '@fortawesome/free-solid-svg-icons';
|
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
|
||||||
import React, { useEffect } from 'react';
|
|
||||||
import { Link } from 'react-router-dom';
|
|
||||||
import { Collapse, Nav, Navbar, NavbarBrand, NavbarToggler, NavItem, NavLink } from 'reactstrap';
|
|
||||||
import classNames from 'classnames';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { useToggle } from '../utils/helpers/hooks';
|
|
||||||
import shlinkLogo from './shlink-logo-white.png';
|
|
||||||
import './MainHeader.scss';
|
|
||||||
|
|
||||||
const propTypes = {
|
|
||||||
location: PropTypes.object,
|
|
||||||
};
|
|
||||||
|
|
||||||
const MainHeader = (ServersDropdown) => {
|
|
||||||
const MainHeaderComp = ({ location }) => {
|
|
||||||
const [ isOpen, toggleOpen, , close ] = useToggle();
|
|
||||||
const { pathname } = location;
|
|
||||||
|
|
||||||
useEffect(close, [ location ]);
|
|
||||||
|
|
||||||
const createServerPath = '/server/create';
|
|
||||||
const settingsPath = '/settings';
|
|
||||||
const toggleClass = classNames('main-header__toggle-icon', { 'main-header__toggle-icon--opened': isOpen });
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Navbar color="primary" dark fixed="top" className="main-header" expand="md">
|
|
||||||
<NavbarBrand tag={Link} to="/">
|
|
||||||
<img src={shlinkLogo} alt="Shlink" className="main-header__brand-logo" /> Shlink
|
|
||||||
</NavbarBrand>
|
|
||||||
|
|
||||||
<NavbarToggler onClick={toggleOpen}>
|
|
||||||
<FontAwesomeIcon icon={arrowIcon} className={toggleClass} />
|
|
||||||
</NavbarToggler>
|
|
||||||
|
|
||||||
<Collapse navbar isOpen={isOpen}>
|
|
||||||
<Nav navbar className="ml-auto">
|
|
||||||
<NavItem>
|
|
||||||
<NavLink tag={Link} to={settingsPath} active={pathname === settingsPath}>
|
|
||||||
<FontAwesomeIcon icon={cogsIcon} /> Settings
|
|
||||||
</NavLink>
|
|
||||||
</NavItem>
|
|
||||||
<NavItem>
|
|
||||||
<NavLink tag={Link} to={createServerPath} active={pathname === createServerPath}>
|
|
||||||
<FontAwesomeIcon icon={plusIcon} /> Add server
|
|
||||||
</NavLink>
|
|
||||||
</NavItem>
|
|
||||||
<ServersDropdown />
|
|
||||||
</Nav>
|
|
||||||
</Collapse>
|
|
||||||
</Navbar>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
MainHeaderComp.propTypes = propTypes;
|
|
||||||
|
|
||||||
return MainHeaderComp;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default MainHeader;
|
|
51
src/common/MainHeader.tsx
Normal file
51
src/common/MainHeader.tsx
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
import { faPlus as plusIcon, faChevronDown as arrowIcon, faCogs as cogsIcon } from '@fortawesome/free-solid-svg-icons';
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
|
import React, { FC, useEffect } from 'react';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import { Collapse, Nav, Navbar, NavbarBrand, NavbarToggler, NavItem, NavLink } from 'reactstrap';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import { RouteChildrenProps } from 'react-router';
|
||||||
|
import { useToggle } from '../utils/helpers/hooks';
|
||||||
|
import shlinkLogo from './shlink-logo-white.png';
|
||||||
|
import './MainHeader.scss';
|
||||||
|
|
||||||
|
const MainHeader = (ServersDropdown: FC) => ({ location }: RouteChildrenProps) => {
|
||||||
|
const [ isOpen, toggleOpen, , close ] = useToggle();
|
||||||
|
const { pathname } = location;
|
||||||
|
|
||||||
|
useEffect(close, [ location ]);
|
||||||
|
|
||||||
|
const createServerPath = '/server/create';
|
||||||
|
const settingsPath = '/settings';
|
||||||
|
const toggleClass = classNames('main-header__toggle-icon', { 'main-header__toggle-icon--opened': isOpen });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Navbar color="primary" dark fixed="top" className="main-header" expand="md">
|
||||||
|
<NavbarBrand tag={Link} to="/">
|
||||||
|
<img src={shlinkLogo} alt="Shlink" className="main-header__brand-logo" /> Shlink
|
||||||
|
</NavbarBrand>
|
||||||
|
|
||||||
|
<NavbarToggler onClick={toggleOpen}>
|
||||||
|
<FontAwesomeIcon icon={arrowIcon} className={toggleClass} />
|
||||||
|
</NavbarToggler>
|
||||||
|
|
||||||
|
<Collapse navbar isOpen={isOpen}>
|
||||||
|
<Nav navbar className="ml-auto">
|
||||||
|
<NavItem>
|
||||||
|
<NavLink tag={Link} to={settingsPath} active={pathname === settingsPath}>
|
||||||
|
<FontAwesomeIcon icon={cogsIcon} /> Settings
|
||||||
|
</NavLink>
|
||||||
|
</NavItem>
|
||||||
|
<NavItem>
|
||||||
|
<NavLink tag={Link} to={createServerPath} active={pathname === createServerPath}>
|
||||||
|
<FontAwesomeIcon icon={plusIcon} /> Add server
|
||||||
|
</NavLink>
|
||||||
|
</NavItem>
|
||||||
|
<ServersDropdown />
|
||||||
|
</Nav>
|
||||||
|
</Collapse>
|
||||||
|
</Navbar>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MainHeader;
|
|
@ -1,13 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import './NoMenuLayout.scss';
|
|
||||||
|
|
||||||
const propTypes = {
|
|
||||||
children: PropTypes.node,
|
|
||||||
};
|
|
||||||
|
|
||||||
const NoMenuLayout = ({ children }) => <div className="no-menu-wrapper">{children}</div>;
|
|
||||||
|
|
||||||
NoMenuLayout.propTypes = propTypes;
|
|
||||||
|
|
||||||
export default NoMenuLayout;
|
|
6
src/common/NoMenuLayout.tsx
Normal file
6
src/common/NoMenuLayout.tsx
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
import React, { FC } from 'react';
|
||||||
|
import './NoMenuLayout.scss';
|
||||||
|
|
||||||
|
const NoMenuLayout: FC = ({ children }) => <div className="no-menu-wrapper">{children}</div>;
|
||||||
|
|
||||||
|
export default NoMenuLayout;
|
|
@ -1,13 +1,11 @@
|
||||||
import React from 'react';
|
import React, { FC } from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import * as PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
const propTypes = {
|
interface NotFoundProps {
|
||||||
to: PropTypes.string,
|
to?: string;
|
||||||
children: PropTypes.node,
|
}
|
||||||
};
|
|
||||||
|
|
||||||
const NotFound = ({ to = '/', children = 'Home' }) => (
|
const NotFound: FC<NotFoundProps> = ({ to = '/', children = 'Home' }) => (
|
||||||
<div className="home">
|
<div className="home">
|
||||||
<h2>Oops! We could not find requested route.</h2>
|
<h2>Oops! We could not find requested route.</h2>
|
||||||
<p>
|
<p>
|
||||||
|
@ -19,6 +17,4 @@ const NotFound = ({ to = '/', children = 'Home' }) => (
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
NotFound.propTypes = propTypes;
|
|
||||||
|
|
||||||
export default NotFound;
|
export default NotFound;
|
|
@ -1,23 +0,0 @@
|
||||||
import { useEffect } from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
const propTypes = {
|
|
||||||
location: PropTypes.object,
|
|
||||||
children: PropTypes.node,
|
|
||||||
};
|
|
||||||
|
|
||||||
const ScrollToTop = () => {
|
|
||||||
const ScrollToTopComp = ({ location, children }) => {
|
|
||||||
useEffect(() => {
|
|
||||||
scrollTo(0, 0);
|
|
||||||
}, [ location ]);
|
|
||||||
|
|
||||||
return children;
|
|
||||||
};
|
|
||||||
|
|
||||||
ScrollToTopComp.propTypes = propTypes;
|
|
||||||
|
|
||||||
return ScrollToTopComp;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ScrollToTop;
|
|
12
src/common/ScrollToTop.tsx
Normal file
12
src/common/ScrollToTop.tsx
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
import React, { PropsWithChildren, useEffect } from 'react';
|
||||||
|
import { RouteChildrenProps } from 'react-router';
|
||||||
|
|
||||||
|
const ScrollToTop = () => ({ location, children }: PropsWithChildren<RouteChildrenProps>) => {
|
||||||
|
useEffect(() => {
|
||||||
|
scrollTo(0, 0);
|
||||||
|
}, [ location ]);
|
||||||
|
|
||||||
|
return <React.Fragment>{children}</React.Fragment>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ScrollToTop;
|
|
@ -12,7 +12,7 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator, withRouter:
|
||||||
bottle.constant('window', (global as any).window);
|
bottle.constant('window', (global as any).window);
|
||||||
bottle.constant('console', global.console);
|
bottle.constant('console', global.console);
|
||||||
|
|
||||||
bottle.serviceFactory('ScrollToTop', ScrollToTop, 'window');
|
bottle.serviceFactory('ScrollToTop', ScrollToTop);
|
||||||
bottle.decorator('ScrollToTop', withRouter);
|
bottle.decorator('ScrollToTop', withRouter);
|
||||||
|
|
||||||
bottle.serviceFactory('MainHeader', MainHeader, 'ServersDropdown');
|
bottle.serviceFactory('MainHeader', MainHeader, 'ServersDropdown');
|
||||||
|
|
|
@ -1,36 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import { faMinusCircle as deleteIcon } from '@fortawesome/free-solid-svg-icons';
|
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { useToggle } from '../utils/helpers/hooks';
|
|
||||||
import { serverType } from './prop-types';
|
|
||||||
|
|
||||||
const propTypes = {
|
|
||||||
server: serverType,
|
|
||||||
className: PropTypes.string,
|
|
||||||
textClassName: PropTypes.string,
|
|
||||||
children: PropTypes.node,
|
|
||||||
};
|
|
||||||
|
|
||||||
const DeleteServerButton = (DeleteServerModal) => {
|
|
||||||
const DeleteServerButtonComp = ({ server, className, children, textClassName }) => {
|
|
||||||
const [ isModalOpen, , showModal, hideModal ] = useToggle();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<React.Fragment>
|
|
||||||
<span className={className} onClick={showModal}>
|
|
||||||
{!children && <FontAwesomeIcon icon={deleteIcon} />}
|
|
||||||
<span className={textClassName}>{children || 'Remove this server'}</span>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<DeleteServerModal server={server} isOpen={isModalOpen} toggle={hideModal} />
|
|
||||||
</React.Fragment>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
DeleteServerButtonComp.propTypes = propTypes;
|
|
||||||
|
|
||||||
return DeleteServerButtonComp;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default DeleteServerButton;
|
|
31
src/servers/DeleteServerButton.tsx
Normal file
31
src/servers/DeleteServerButton.tsx
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
import React, { FC } from 'react';
|
||||||
|
import { faMinusCircle as deleteIcon } from '@fortawesome/free-solid-svg-icons';
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
|
import { useToggle } from '../utils/helpers/hooks';
|
||||||
|
import { DeleteServerModalProps } from './DeleteServerModal';
|
||||||
|
import { ServerWithId } from './data';
|
||||||
|
|
||||||
|
export interface DeleteServerButtonProps {
|
||||||
|
server: ServerWithId;
|
||||||
|
className?: string;
|
||||||
|
textClassName?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DeleteServerButton = (DeleteServerModal: FC<DeleteServerModalProps>): FC<DeleteServerButtonProps> => (
|
||||||
|
{ server, className, children, textClassName },
|
||||||
|
) => {
|
||||||
|
const [ isModalOpen, , showModal, hideModal ] = useToggle();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<span className={className} onClick={showModal}>
|
||||||
|
{!children && <FontAwesomeIcon icon={deleteIcon} />}
|
||||||
|
<span className={textClassName}>{children ?? 'Remove this server'}</span>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<DeleteServerModal server={server} isOpen={isModalOpen} toggle={hideModal} />
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DeleteServerButton;
|
|
@ -1,19 +1,19 @@
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Modal, ModalBody, ModalFooter, ModalHeader } from 'reactstrap';
|
import { Modal, ModalBody, ModalFooter, ModalHeader } from 'reactstrap';
|
||||||
import { serverType } from './prop-types';
|
import { RouterProps } from 'react-router';
|
||||||
|
import { ServerWithId } from './data';
|
||||||
|
|
||||||
const propTypes = {
|
export interface DeleteServerModalProps {
|
||||||
toggle: PropTypes.func.isRequired,
|
server: ServerWithId;
|
||||||
isOpen: PropTypes.bool.isRequired,
|
toggle: () => void;
|
||||||
server: serverType,
|
isOpen: boolean;
|
||||||
deleteServer: PropTypes.func,
|
}
|
||||||
history: PropTypes.shape({
|
|
||||||
push: PropTypes.func,
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
const DeleteServerModal = ({ server, toggle, isOpen, deleteServer, history }) => {
|
interface DeleteServerModalConnectProps extends DeleteServerModalProps, RouterProps {
|
||||||
|
deleteServer: (server: ServerWithId) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DeleteServerModal = ({ server, toggle, isOpen, deleteServer, history }: DeleteServerModalConnectProps) => {
|
||||||
const closeModal = () => {
|
const closeModal = () => {
|
||||||
deleteServer(server);
|
deleteServer(server);
|
||||||
toggle();
|
toggle();
|
||||||
|
@ -40,6 +40,4 @@ const DeleteServerModal = ({ server, toggle, isOpen, deleteServer, history }) =>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
DeleteServerModal.propTypes = propTypes;
|
|
||||||
|
|
||||||
export default DeleteServerModal;
|
export default DeleteServerModal;
|
|
@ -1,30 +1,23 @@
|
||||||
import React from 'react';
|
import React, { FC } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { ListGroup, ListGroupItem } from 'reactstrap';
|
import { ListGroup, ListGroupItem } from 'reactstrap';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import { faChevronRight as chevronIcon } from '@fortawesome/free-solid-svg-icons';
|
import { faChevronRight as chevronIcon } from '@fortawesome/free-solid-svg-icons';
|
||||||
import { serverType } from './prop-types';
|
|
||||||
import './ServersListGroup.scss';
|
import './ServersListGroup.scss';
|
||||||
|
import { ServerWithId } from './data';
|
||||||
|
|
||||||
const propTypes = {
|
interface ServersListGroup {
|
||||||
servers: PropTypes.arrayOf(serverType).isRequired,
|
servers: ServerWithId[];
|
||||||
children: PropTypes.node.isRequired,
|
}
|
||||||
};
|
|
||||||
|
|
||||||
const ServerListItem = ({ id, name }) => (
|
const ServerListItem = ({ id, name }: { id: string; name: string }) => (
|
||||||
<ListGroupItem tag={Link} to={`/server/${id}/list-short-urls/1`} className="servers-list__server-item">
|
<ListGroupItem tag={Link} to={`/server/${id}/list-short-urls/1`} className="servers-list__server-item">
|
||||||
{name}
|
{name}
|
||||||
<FontAwesomeIcon icon={chevronIcon} className="servers-list__server-item-icon" />
|
<FontAwesomeIcon icon={chevronIcon} className="servers-list__server-item-icon" />
|
||||||
</ListGroupItem>
|
</ListGroupItem>
|
||||||
);
|
);
|
||||||
|
|
||||||
ServerListItem.propTypes = {
|
const ServersListGroup: FC<ServersListGroup> = ({ servers, children }) => (
|
||||||
id: PropTypes.string,
|
|
||||||
name: PropTypes.string,
|
|
||||||
};
|
|
||||||
|
|
||||||
const ServersListGroup = ({ servers, children }) => (
|
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<div className="container">
|
<div className="container">
|
||||||
<h5>{children}</h5>
|
<h5>{children}</h5>
|
||||||
|
@ -37,6 +30,4 @@ const ServersListGroup = ({ servers, children }) => (
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
|
|
||||||
ServersListGroup.propTypes = propTypes;
|
|
||||||
|
|
||||||
export default ServersListGroup;
|
export default ServersListGroup;
|
|
@ -23,7 +23,7 @@ export const useStateFlagTimeout = (
|
||||||
return [ flag, callback ];
|
return [ flag, callback ];
|
||||||
};
|
};
|
||||||
|
|
||||||
type ToggleResult = [ boolean, (flag: boolean) => void, () => void, () => void ];
|
type ToggleResult = [ boolean, () => void, () => void, () => void ];
|
||||||
|
|
||||||
export const useToggle = (initialValue = false): ToggleResult => {
|
export const useToggle = (initialValue = false): ToggleResult => {
|
||||||
const [ flag, setFlag ] = useState<boolean>(initialValue);
|
const [ flag, setFlag ] = useState<boolean>(initialValue);
|
||||||
|
|
|
@ -1,15 +1,17 @@
|
||||||
import { shallow } from 'enzyme';
|
import { shallow, ShallowWrapper } from 'enzyme';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { Mock } from 'ts-mockery';
|
||||||
import asideMenuCreator from '../../src/common/AsideMenu';
|
import asideMenuCreator from '../../src/common/AsideMenu';
|
||||||
|
import { ServerWithId } from '../../src/servers/data';
|
||||||
|
|
||||||
describe('<AsideMenu />', () => {
|
describe('<AsideMenu />', () => {
|
||||||
let wrapped;
|
let wrapped: ShallowWrapper;
|
||||||
const DeleteServerButton = () => '';
|
const DeleteServerButton = () => null;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
const AsideMenu = asideMenuCreator(DeleteServerButton);
|
const AsideMenu = asideMenuCreator(DeleteServerButton);
|
||||||
|
|
||||||
wrapped = shallow(<AsideMenu selectedServer={{ id: 'abc123' }} />);
|
wrapped = shallow(<AsideMenu selectedServer={Mock.of<ServerWithId>({ id: 'abc123' })} />);
|
||||||
});
|
});
|
||||||
afterEach(() => wrapped.unmount());
|
afterEach(() => wrapped.unmount());
|
||||||
|
|
|
@ -1,14 +1,16 @@
|
||||||
import { shallow } from 'enzyme';
|
import { shallow, ShallowWrapper } from 'enzyme';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Home from '../../src/common/Home';
|
import { Mock } from 'ts-mockery';
|
||||||
|
import Home, { HomeProps } from '../../src/common/Home';
|
||||||
|
import { ServerWithId } from '../../src/servers/data';
|
||||||
|
|
||||||
describe('<Home />', () => {
|
describe('<Home />', () => {
|
||||||
let wrapped;
|
let wrapped: ShallowWrapper;
|
||||||
const defaultProps = {
|
const defaultProps = {
|
||||||
resetSelectedServer: jest.fn(),
|
resetSelectedServer: jest.fn(),
|
||||||
servers: {},
|
servers: {},
|
||||||
};
|
};
|
||||||
const createComponent = (props) => {
|
const createComponent = (props: Partial<HomeProps> = {}) => {
|
||||||
const actualProps = { ...defaultProps, ...props };
|
const actualProps = { ...defaultProps, ...props };
|
||||||
|
|
||||||
wrapped = shallow(<Home {...actualProps} />);
|
wrapped = shallow(<Home {...actualProps} />);
|
||||||
|
@ -16,7 +18,7 @@ describe('<Home />', () => {
|
||||||
return wrapped;
|
return wrapped;
|
||||||
};
|
};
|
||||||
|
|
||||||
afterEach(() => wrapped && wrapped.unmount());
|
afterEach(() => wrapped?.unmount());
|
||||||
|
|
||||||
it('shows link to create server when no servers exist', () => {
|
it('shows link to create server when no servers exist', () => {
|
||||||
const wrapped = createComponent();
|
const wrapped = createComponent();
|
||||||
|
@ -26,8 +28,8 @@ describe('<Home />', () => {
|
||||||
|
|
||||||
it('asks to select a server when servers exist', () => {
|
it('asks to select a server when servers exist', () => {
|
||||||
const servers = {
|
const servers = {
|
||||||
1: { name: 'foo', id: '1' },
|
'1a': Mock.of<ServerWithId>({ name: 'foo', id: '1' }),
|
||||||
2: { name: 'bar', id: '2' },
|
'2b': Mock.of<ServerWithId>({ name: 'bar', id: '2' }),
|
||||||
};
|
};
|
||||||
const wrapped = createComponent({ servers });
|
const wrapped = createComponent({ servers });
|
||||||
const span = wrapped.find('span');
|
const span = wrapped.find('span');
|
|
@ -1,10 +1,10 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { shallow } from 'enzyme';
|
import { shallow, ShallowWrapper } from 'enzyme';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import NotFound from '../../src/common/NotFound';
|
import NotFound from '../../src/common/NotFound';
|
||||||
|
|
||||||
describe('<NotFound />', () => {
|
describe('<NotFound />', () => {
|
||||||
let wrapper;
|
let wrapper: ShallowWrapper;
|
||||||
const createWrapper = (props = {}) => {
|
const createWrapper = (props = {}) => {
|
||||||
wrapper = shallow(<NotFound {...props} />);
|
wrapper = shallow(<NotFound {...props} />);
|
||||||
const content = wrapper.text();
|
const content = wrapper.text();
|
||||||
|
@ -12,7 +12,7 @@ describe('<NotFound />', () => {
|
||||||
return { wrapper, content };
|
return { wrapper, content };
|
||||||
};
|
};
|
||||||
|
|
||||||
afterEach(() => wrapper && wrapper.unmount());
|
afterEach(() => wrapper?.unmount());
|
||||||
|
|
||||||
it('shows expected error title', () => {
|
it('shows expected error title', () => {
|
||||||
const { content } = createWrapper();
|
const { content } = createWrapper();
|
|
@ -1,23 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import { shallow } from 'enzyme';
|
|
||||||
import createScrollToTop from '../../src/common/ScrollToTop';
|
|
||||||
|
|
||||||
describe('<ScrollToTop />', () => {
|
|
||||||
let wrapper;
|
|
||||||
const window = {
|
|
||||||
scrollTo: jest.fn(),
|
|
||||||
};
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
const ScrollToTop = createScrollToTop(window);
|
|
||||||
|
|
||||||
wrapper = shallow(<ScrollToTop locaction={{ href: 'foo' }}>Foobar</ScrollToTop>);
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
wrapper.unmount();
|
|
||||||
window.scrollTo.mockReset();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('just renders children', () => expect(wrapper.text()).toEqual('Foobar'));
|
|
||||||
});
|
|
19
test/common/ScrollToTop.test.tsx
Normal file
19
test/common/ScrollToTop.test.tsx
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { shallow, ShallowWrapper } from 'enzyme';
|
||||||
|
import { Mock } from 'ts-mockery';
|
||||||
|
import { RouteChildrenProps } from 'react-router';
|
||||||
|
import createScrollToTop from '../../src/common/ScrollToTop';
|
||||||
|
|
||||||
|
describe('<ScrollToTop />', () => {
|
||||||
|
let wrapper: ShallowWrapper;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
const ScrollToTop = createScrollToTop();
|
||||||
|
|
||||||
|
wrapper = shallow(<ScrollToTop {...Mock.all<RouteChildrenProps>()}>Foobar</ScrollToTop>);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => wrapper.unmount());
|
||||||
|
|
||||||
|
it('just renders children', () => expect(wrapper.text()).toEqual('Foobar'));
|
||||||
|
});
|
|
@ -1,15 +1,17 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { shallow } from 'enzyme';
|
import { shallow, ShallowWrapper } from 'enzyme';
|
||||||
|
import { Mock } from 'ts-mockery';
|
||||||
import deleteServerButtonConstruct from '../../src/servers/DeleteServerButton';
|
import deleteServerButtonConstruct from '../../src/servers/DeleteServerButton';
|
||||||
import DeleteServerModal from '../../src/servers/DeleteServerModal';
|
import { ServerWithId } from '../../src/servers/data';
|
||||||
|
|
||||||
describe('<DeleteServerButton />', () => {
|
describe('<DeleteServerButton />', () => {
|
||||||
let wrapper;
|
let wrapper: ShallowWrapper;
|
||||||
|
const DeleteServerModal = () => null;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
const DeleteServerButton = deleteServerButtonConstruct(DeleteServerModal);
|
const DeleteServerButton = deleteServerButtonConstruct(DeleteServerModal);
|
||||||
|
|
||||||
wrapper = shallow(<DeleteServerButton server={{}} className="button" />);
|
wrapper = shallow(<DeleteServerButton server={Mock.all<ServerWithId>()} className="button" />);
|
||||||
});
|
});
|
||||||
afterEach(() => wrapper.unmount());
|
afterEach(() => wrapper.unmount());
|
||||||
|
|
|
@ -1,31 +1,31 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { shallow } from 'enzyme';
|
import { shallow, ShallowWrapper } from 'enzyme';
|
||||||
import { Modal, ModalBody, ModalFooter, ModalHeader } from 'reactstrap';
|
import { Modal, ModalBody, ModalFooter, ModalHeader } from 'reactstrap';
|
||||||
|
import { History } from 'history';
|
||||||
|
import { Mock } from 'ts-mockery';
|
||||||
import DeleteServerModal from '../../src/servers/DeleteServerModal';
|
import DeleteServerModal from '../../src/servers/DeleteServerModal';
|
||||||
|
import { ServerWithId } from '../../src/servers/data';
|
||||||
|
|
||||||
describe('<DeleteServerModal />', () => {
|
describe('<DeleteServerModal />', () => {
|
||||||
let wrapper;
|
let wrapper: ShallowWrapper;
|
||||||
const deleteServerMock = jest.fn();
|
const deleteServerMock = jest.fn();
|
||||||
const historyMock = { push: jest.fn() };
|
const push = jest.fn();
|
||||||
const toggleMock = jest.fn();
|
const toggleMock = jest.fn();
|
||||||
const serverName = 'the_server_name';
|
const serverName = 'the_server_name';
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
deleteServerMock.mockReset();
|
|
||||||
toggleMock.mockReset();
|
|
||||||
historyMock.push.mockReset();
|
|
||||||
|
|
||||||
wrapper = shallow(
|
wrapper = shallow(
|
||||||
<DeleteServerModal
|
<DeleteServerModal
|
||||||
server={{ name: serverName }}
|
server={Mock.of<ServerWithId>({ name: serverName })}
|
||||||
toggle={toggleMock}
|
toggle={toggleMock}
|
||||||
isOpen={true}
|
isOpen={true}
|
||||||
deleteServer={deleteServerMock}
|
deleteServer={deleteServerMock}
|
||||||
history={historyMock}
|
history={Mock.of<History>({ push })}
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
afterEach(() => wrapper.unmount());
|
afterEach(() => wrapper.unmount());
|
||||||
|
afterEach(jest.clearAllMocks);
|
||||||
|
|
||||||
it('renders a modal window', () => {
|
it('renders a modal window', () => {
|
||||||
expect(wrapper.find(Modal)).toHaveLength(1);
|
expect(wrapper.find(Modal)).toHaveLength(1);
|
||||||
|
@ -49,7 +49,7 @@ describe('<DeleteServerModal />', () => {
|
||||||
|
|
||||||
expect(toggleMock).toHaveBeenCalledTimes(1);
|
expect(toggleMock).toHaveBeenCalledTimes(1);
|
||||||
expect(deleteServerMock).not.toHaveBeenCalled();
|
expect(deleteServerMock).not.toHaveBeenCalled();
|
||||||
expect(historyMock.push).not.toHaveBeenCalled();
|
expect(push).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('deletes server when clicking accept button', () => {
|
it('deletes server when clicking accept button', () => {
|
||||||
|
@ -59,6 +59,6 @@ describe('<DeleteServerModal />', () => {
|
||||||
|
|
||||||
expect(toggleMock).toHaveBeenCalledTimes(1);
|
expect(toggleMock).toHaveBeenCalledTimes(1);
|
||||||
expect(deleteServerMock).toHaveBeenCalledTimes(1);
|
expect(deleteServerMock).toHaveBeenCalledTimes(1);
|
||||||
expect(historyMock.push).toHaveBeenCalledTimes(1);
|
expect(push).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
|
@ -1,17 +1,19 @@
|
||||||
import { shallow } from 'enzyme';
|
import { shallow, ShallowWrapper } from 'enzyme';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { ListGroup } from 'reactstrap';
|
import { ListGroup } from 'reactstrap';
|
||||||
|
import { Mock } from 'ts-mockery';
|
||||||
import ServersListGroup from '../../src/servers/ServersListGroup';
|
import ServersListGroup from '../../src/servers/ServersListGroup';
|
||||||
|
import { ServerWithId } from '../../src/servers/data';
|
||||||
|
|
||||||
describe('<ServersListGroup />', () => {
|
describe('<ServersListGroup />', () => {
|
||||||
let wrapped;
|
let wrapped: ShallowWrapper;
|
||||||
const createComponent = (servers) => {
|
const createComponent = (servers: ServerWithId[]) => {
|
||||||
wrapped = shallow(<ServersListGroup servers={servers}>The list of servers</ServersListGroup>);
|
wrapped = shallow(<ServersListGroup servers={servers}>The list of servers</ServersListGroup>);
|
||||||
|
|
||||||
return wrapped;
|
return wrapped;
|
||||||
};
|
};
|
||||||
|
|
||||||
afterEach(() => wrapped && wrapped.unmount());
|
afterEach(() => wrapped?.unmount());
|
||||||
|
|
||||||
it('Renders title', () => {
|
it('Renders title', () => {
|
||||||
const wrapped = createComponent([]);
|
const wrapped = createComponent([]);
|
||||||
|
@ -23,8 +25,8 @@ describe('<ServersListGroup />', () => {
|
||||||
|
|
||||||
it('shows servers list', () => {
|
it('shows servers list', () => {
|
||||||
const servers = [
|
const servers = [
|
||||||
{ name: 'foo', id: '123' },
|
Mock.of<ServerWithId>({ name: 'foo', id: '123' }),
|
||||||
{ name: 'bar', id: '456' },
|
Mock.of<ServerWithId>({ name: 'bar', id: '456' }),
|
||||||
];
|
];
|
||||||
const wrapped = createComponent(servers);
|
const wrapped = createComponent(servers);
|
||||||
|
|
Loading…
Reference in a new issue