Merge pull request #345 from acelaya-forks/feature/restyle

Feature/restyle
This commit is contained in:
Alejandro Celaya 2020-12-12 12:13:52 +01:00 committed by GitHub
commit 32957835b3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
29 changed files with 150 additions and 111 deletions

View file

@ -14,7 +14,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
* [#315](https://github.com/shlinkio/shlink-web-client/issues/315) Now you can tell if you want to validate the long URL when using Shlink >=2.4. * [#315](https://github.com/shlinkio/shlink-web-client/issues/315) Now you can tell if you want to validate the long URL when using Shlink >=2.4.
### Changed ### Changed
* *Nothing* * [#267](https://github.com/shlinkio/shlink-web-client/issues/267) Added some subtle but important improvements on UI/UX.
### Deprecated ### Deprecated
* *Nothing* * *Nothing*

View file

@ -4,7 +4,8 @@
$asideMenuMobileWidth: 280px; $asideMenuMobileWidth: 280px;
.aside-menu { .aside-menu {
background-color: #f7f7f7; background-color: white;
box-shadow: rgba(0, 0, 0, .05) 0 8px 15px;
position: fixed !important; position: fixed !important;
padding-top: 13px; padding-top: 13px;
padding-bottom: 10px; padding-bottom: 10px;

View file

@ -1,4 +1,4 @@
import { faPlus as plusIcon, faChevronDown as arrowIcon, faCogs as cogsIcon } from '@fortawesome/free-solid-svg-icons'; import { faChevronDown as arrowIcon, faCogs as cogsIcon } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { FC, useEffect } from 'react'; import { FC, useEffect } from 'react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
@ -15,7 +15,6 @@ const MainHeader = (ServersDropdown: FC) => ({ location }: RouteComponentProps)
useEffect(close, [ location ]); useEffect(close, [ location ]);
const createServerPath = '/server/create';
const settingsPath = '/settings'; const settingsPath = '/settings';
const toggleClass = classNames('main-header__toggle-icon', { 'main-header__toggle-icon--opened': isOpen }); const toggleClass = classNames('main-header__toggle-icon', { 'main-header__toggle-icon--opened': isOpen });
@ -32,15 +31,10 @@ const MainHeader = (ServersDropdown: FC) => ({ location }: RouteComponentProps)
<Collapse navbar isOpen={isOpen}> <Collapse navbar isOpen={isOpen}>
<Nav navbar className="ml-auto"> <Nav navbar className="ml-auto">
<NavItem> <NavItem>
<NavLink tag={Link} to={settingsPath} active={pathname === settingsPath}> <NavLink tag={Link} to={'/settings'} active={pathname === settingsPath}>
<FontAwesomeIcon icon={cogsIcon} />&nbsp; Settings <FontAwesomeIcon icon={cogsIcon} />&nbsp; Settings
</NavLink> </NavLink>
</NavItem> </NavItem>
<NavItem>
<NavLink tag={Link} to={createServerPath} active={pathname === createServerPath}>
<FontAwesomeIcon icon={plusIcon} />&nbsp; Add server
</NavLink>
</NavItem>
<ServersDropdown /> <ServersDropdown />
</Nav> </Nav>
</Collapse> </Collapse>

View file

@ -1,3 +1,3 @@
.no-menu-wrapper { .no-menu-wrapper {
padding: 40px 20px 20px; padding: 30px 20px 20px;
} }

View file

@ -1,6 +1,6 @@
import { FC } from 'react'; import { FC } from 'react';
import './NoMenuLayout.scss'; import './NoMenuLayout.scss';
const NoMenuLayout: FC = ({ children }) => <div className="no-menu-wrapper">{children}</div>; const NoMenuLayout: FC = ({ children }) => <div className="no-menu-wrapper container-xl">{children}</div>;
export default NoMenuLayout; export default NoMenuLayout;

View file

@ -6,6 +6,7 @@ html,
body, body,
#root { #root {
height: 100%; height: 100%;
background: #f5f6fe;
} }
* { * {
@ -16,6 +17,18 @@ body,
background-color: $mainColor !important; background-color: $mainColor !important;
} }
.card {
box-shadow: 0 .125rem .25rem rgba(0, 0, 0, .075);
}
.card-header {
background-color: white;
}
.card-footer {
background-color: rgba(255, 255, 255, .5);
}
.dropdown-item:not(:disabled) { .dropdown-item:not(:disabled) {
cursor: pointer; cursor: pointer;
} }

View file

@ -44,7 +44,7 @@ const CreateServer = (ImportServersBtn: FC<ImportServersBtnProps>, useStateFlagT
return ( return (
<NoMenuLayout> <NoMenuLayout>
<ServerForm onSubmit={handleSubmit}> <ServerForm title={<h5 className="mb-0">Add new server</h5>} onSubmit={handleSubmit}>
<ImportServersBtn onImport={setServersImported} onImportError={setErrorImporting} /> <ImportServersBtn onImport={setServersImported} onImportError={setErrorImporting} />
<button className="btn btn-outline-primary">Create server</button> <button className="btn btn-outline-primary">Create server</button>
</ServerForm> </ServerForm>

View file

@ -23,7 +23,11 @@ export const EditServer = (ServerError: FC) => withSelectedServer<EditServerProp
return ( return (
<NoMenuLayout> <NoMenuLayout>
<ServerForm initialValues={selectedServer} onSubmit={handleSubmit}> <ServerForm
title={<h5 className="mb-0">Edit "{selectedServer.name}"</h5>}
initialValues={selectedServer}
onSubmit={handleSubmit}
>
<Button outline className="mr-2" onClick={goBack}>Cancel</Button> <Button outline className="mr-2" onClick={goBack}>Cancel</Button>
<Button outline color="primary">Save</Button> <Button outline color="primary">Save</Button>
</ServerForm> </ServerForm>

View file

@ -49,13 +49,13 @@ export const Overview = (
<> <>
<div className="row mb-3"> <div className="row mb-3">
<div className="col-sm-4"> <div className="col-sm-4">
<Card className="overview__card mb-2" body color="light"> <Card className="overview__card mb-2" body>
<CardTitle tag="h5" className="overview__card-title">Visits</CardTitle> <CardTitle tag="h5" className="overview__card-title">Visits</CardTitle>
<CardText tag="h2">{loadingVisits ? 'Loading...' : prettify(visitsCount)}</CardText> <CardText tag="h2">{loadingVisits ? 'Loading...' : prettify(visitsCount)}</CardText>
</Card> </Card>
</div> </div>
<div className="col-sm-4"> <div className="col-sm-4">
<Card className="overview__card mb-2" body color="light"> <Card className="overview__card mb-2" body>
<CardTitle tag="h5" className="overview__card-title">Short URLs</CardTitle> <CardTitle tag="h5" className="overview__card-title">Short URLs</CardTitle>
<CardText tag="h2"> <CardText tag="h2">
{loading ? 'Loading...' : prettify(shortUrls?.pagination.totalItems ?? 0)} {loading ? 'Loading...' : prettify(shortUrls?.pagination.totalItems ?? 0)}
@ -63,7 +63,7 @@ export const Overview = (
</Card> </Card>
</div> </div>
<div className="col-sm-4"> <div className="col-sm-4">
<Card className="overview__card mb-2" body color="light"> <Card className="overview__card mb-2" body>
<CardTitle tag="h5" className="overview__card-title">Tags</CardTitle> <CardTitle tag="h5" className="overview__card-title">Tags</CardTitle>
<CardText tag="h2">{loadingTags ? 'Loading...' : prettify(tagsList.tags.length)}</CardText> <CardText tag="h2">{loadingTags ? 'Loading...' : prettify(tagsList.tags.length)}</CardText>
</Card> </Card>

View file

@ -1,6 +1,8 @@
import { isEmpty, values } from 'ramda'; import { isEmpty, values } from 'ramda';
import { DropdownItem, DropdownMenu, DropdownToggle, UncontrolledDropdown } from 'reactstrap'; import { DropdownItem, DropdownMenu, DropdownToggle, UncontrolledDropdown } from 'reactstrap';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { faPlus as plusIcon, faFileDownload as exportIcon, faServer as serverIcon } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import ServersExporter from './services/ServersExporter'; import ServersExporter from './services/ServersExporter';
import { isServerWithId, SelectedServer, ServersMap } from './data'; import { isServerWithId, SelectedServer, ServersMap } from './data';
@ -11,10 +13,15 @@ export interface ServersDropdownProps {
const ServersDropdown = (serversExporter: ServersExporter) => ({ servers, selectedServer }: ServersDropdownProps) => { const ServersDropdown = (serversExporter: ServersExporter) => ({ servers, selectedServer }: ServersDropdownProps) => {
const serversList = values(servers); const serversList = values(servers);
const createServerItem = (
<DropdownItem tag={Link} to="/server/create">
<FontAwesomeIcon icon={plusIcon} /> <span className="ml-1">Add server</span>
</DropdownItem>
);
const renderServers = () => { const renderServers = () => {
if (isEmpty(serversList)) { if (isEmpty(serversList)) {
return <DropdownItem disabled><i>Add a server first...</i></DropdownItem>; return createServerItem;
} }
return ( return (
@ -30,8 +37,9 @@ const ServersDropdown = (serversExporter: ServersExporter) => ({ servers, select
</DropdownItem> </DropdownItem>
))} ))}
<DropdownItem divider /> <DropdownItem divider />
{createServerItem}
<DropdownItem className="servers-dropdown__export-item" onClick={async () => serversExporter.exportServers()}> <DropdownItem className="servers-dropdown__export-item" onClick={async () => serversExporter.exportServers()}>
Export servers <FontAwesomeIcon icon={exportIcon} /> <span className="ml-1">Export servers</span>
</DropdownItem> </DropdownItem>
</> </>
); );
@ -39,7 +47,9 @@ const ServersDropdown = (serversExporter: ServersExporter) => ({ servers, select
return ( return (
<UncontrolledDropdown nav inNavbar> <UncontrolledDropdown nav inNavbar>
<DropdownToggle nav caret>Servers</DropdownToggle> <DropdownToggle nav caret>
<FontAwesomeIcon icon={serverIcon} /> <span className="ml-1">Servers</span>
</DropdownToggle>
<DropdownMenu right>{renderServers()}</DropdownMenu> <DropdownMenu right>{renderServers()}</DropdownMenu>
</UncontrolledDropdown> </UncontrolledDropdown>
); );

View file

@ -3,6 +3,7 @@
.servers-list__list-group { .servers-list__list-group {
width: 100%; width: 100%;
max-width: 400px; max-width: 400px;
box-shadow: 0 .125rem .25rem rgba(0, 0, 0, .075);
} }
.servers-list__server-item.servers-list__server-item { .servers-list__server-item.servers-list__server-item {

View file

@ -4,6 +4,7 @@ import Message from '../../utils/Message';
import ServersListGroup from '../ServersListGroup'; import ServersListGroup from '../ServersListGroup';
import { DeleteServerButtonProps } from '../DeleteServerButton'; import { DeleteServerButtonProps } from '../DeleteServerButton';
import { isServerWithId, SelectedServer, ServersMap } from '../data'; import { isServerWithId, SelectedServer, ServersMap } from '../data';
import NoMenuLayout from '../../common/NoMenuLayout';
import './ServerError.scss'; import './ServerError.scss';
interface ServerErrorProps { interface ServerErrorProps {
@ -14,9 +15,10 @@ interface ServerErrorProps {
export const ServerError = (DeleteServerButton: FC<DeleteServerButtonProps>): FC<ServerErrorProps> => ( export const ServerError = (DeleteServerButton: FC<DeleteServerButtonProps>): FC<ServerErrorProps> => (
{ servers, selectedServer }, { servers, selectedServer },
) => ( ) => (
<NoMenuLayout>
<div className="server-error__container flex-column"> <div className="server-error__container flex-column">
<div className="row w-100 mb-3 mb-md-5"> <div className="row w-100 mb-3 mb-md-5">
<Message type="error"> <Message type="error" fullWidth noMargin>
{!isServerWithId(selectedServer) && 'Could not find this Shlink server.'} {!isServerWithId(selectedServer) && 'Could not find this Shlink server.'}
{isServerWithId(selectedServer) && ( {isServerWithId(selectedServer) && (
<> <>
@ -42,4 +44,5 @@ export const ServerError = (DeleteServerButton: FC<DeleteServerButtonProps>): FC
</div> </div>
)} )}
</div> </div>
</NoMenuLayout>
); );

View file

@ -0,0 +1,3 @@
.server-form .form-group:last-child {
margin-bottom: 0;
}

View file

@ -1,14 +1,17 @@
import { FC, useEffect, useState } from 'react'; import { FC, ReactNode, useEffect, useState } from 'react';
import { HorizontalFormGroup } from '../../utils/HorizontalFormGroup'; import { FormGroupContainer } from '../../utils/FormGroupContainer';
import { handleEventPreventingDefault } from '../../utils/utils'; import { handleEventPreventingDefault } from '../../utils/utils';
import { ServerData } from '../data'; import { ServerData } from '../data';
import { SimpleCard } from '../../utils/SimpleCard';
import './ServerForm.scss';
interface ServerFormProps { interface ServerFormProps {
onSubmit: (server: ServerData) => void; onSubmit: (server: ServerData) => void;
initialValues?: ServerData; initialValues?: ServerData;
title?: ReactNode;
} }
export const ServerForm: FC<ServerFormProps> = ({ onSubmit, initialValues, children }) => { export const ServerForm: FC<ServerFormProps> = ({ onSubmit, initialValues, children, title }) => {
const [ name, setName ] = useState(''); const [ name, setName ] = useState('');
const [ url, setUrl ] = useState(''); const [ url, setUrl ] = useState('');
const [ apiKey, setApiKey ] = useState(''); const [ apiKey, setApiKey ] = useState('');
@ -21,10 +24,12 @@ export const ServerForm: FC<ServerFormProps> = ({ onSubmit, initialValues, child
}, [ initialValues ]); }, [ initialValues ]);
return ( return (
<form onSubmit={handleSubmit}> <form className="server-form" onSubmit={handleSubmit}>
<HorizontalFormGroup value={name} onChange={setName}>Name</HorizontalFormGroup> <SimpleCard className="mb-4" title={title}>
<HorizontalFormGroup type="url" value={url} onChange={setUrl}>URL</HorizontalFormGroup> <FormGroupContainer value={name} onChange={setName}>Name</FormGroupContainer>
<HorizontalFormGroup value={apiKey} onChange={setApiKey}>API key</HorizontalFormGroup> <FormGroupContainer type="url" value={url} onChange={setUrl}>URL</FormGroupContainer>
<FormGroupContainer value={apiKey} onChange={setApiKey}>API key</FormGroupContainer>
</SimpleCard>
<div className="text-right">{children}</div> <div className="text-right">{children}</div>
</form> </form>

View file

@ -2,6 +2,7 @@ import { FC, useEffect } from 'react';
import { RouteComponentProps } from 'react-router'; import { RouteComponentProps } from 'react-router';
import Message from '../../utils/Message'; import Message from '../../utils/Message';
import { isNotFoundServer, SelectedServer } from '../data'; import { isNotFoundServer, SelectedServer } from '../data';
import NoMenuLayout from '../../common/NoMenuLayout';
interface WithSelectedServerProps extends RouteComponentProps<{ serverId: string }> { interface WithSelectedServerProps extends RouteComponentProps<{ serverId: string }> {
selectServer: (serverId: string) => void; selectServer: (serverId: string) => void;
@ -18,9 +19,9 @@ export function withSelectedServer<T = {}>(WrappedComponent: FC<WithSelectedServ
if (!selectedServer) { if (!selectedServer) {
return ( return (
<div className="row"> <NoMenuLayout>
<Message loading /> <Message loading noMargin />
</div> </NoMenuLayout>
); );
} }

View file

@ -1,7 +1,7 @@
.short-urls-paginator { .short-urls-paginator {
position: sticky; position: sticky;
bottom: 0; bottom: 0;
background-color: rgba(white, .8); background-color: rgba(255, 255, 255, .5);
padding: .75rem 0; padding: .75rem 0;
border-top: 1px solid rgba(black, .125); border-top: 1px solid rgba(black, .125);
} }

View file

@ -1,4 +1,5 @@
import { FC, useEffect, useState } from 'react'; import { FC, useEffect, useState } from 'react';
import { Card } from 'reactstrap';
import Paginator from './Paginator'; import Paginator from './Paginator';
import { ShortUrlsListProps } from './ShortUrlsList'; import { ShortUrlsListProps } from './ShortUrlsList';
@ -17,10 +18,10 @@ const ShortUrls = (SearchBar: FC, ShortUrlsList: FC<ShortUrlsListProps>) => (pro
return ( return (
<> <>
<div className="form-group"><SearchBar /></div> <div className="form-group"><SearchBar /></div>
<div> <Card body className="pb-1">
<ShortUrlsList {...props} key={urlsListKey} /> <ShortUrlsList {...props} key={urlsListKey} />
<Paginator paginator={pagination} serverId={serverId} /> <Paginator paginator={pagination} serverId={serverId} />
</div> </Card>
</> </>
); );
}; };

View file

@ -30,7 +30,7 @@ export const ShortUrlsTable = (ShortUrlsRow: FC<ShortUrlsRowProps>) => ({
const orderableColumnsClasses = classNames('short-urls-table__header-cell', { const orderableColumnsClasses = classNames('short-urls-table__header-cell', {
'short-urls-table__header-cell--with-action': !!orderByColumn, 'short-urls-table__header-cell--with-action': !!orderByColumn,
}); });
const tableClasses = classNames('table table-striped table-hover', className); const tableClasses = classNames('table table-hover', className);
const renderShortUrls = () => { const renderShortUrls = () => {
if (error) { if (error) {

View file

@ -18,7 +18,7 @@ const EditShortUrlModal = ({ isOpen, toggle, shortUrl, shortUrlEdition, editShor
const doEdit = async () => editShortUrl(shortUrl.shortCode, shortUrl.domain, longUrl).then(toggle); const doEdit = async () => editShortUrl(shortUrl.shortCode, shortUrl.domain, longUrl).then(toggle);
return ( return (
<Modal isOpen={isOpen} toggle={toggle} centered> <Modal isOpen={isOpen} toggle={toggle} centered size="lg">
<ModalHeader toggle={toggle}> <ModalHeader toggle={toggle}>
Edit long URL for <ExternalLink href={url} /> Edit long URL for <ExternalLink href={url} />
</ModalHeader> </ModalHeader>

View file

@ -2,10 +2,6 @@
margin-bottom: .5rem; margin-bottom: .5rem;
} }
.tag-card__header.tag-card__header {
background-color: #eeeeee;
}
.tag-card__header.tag-card__header, .tag-card__header.tag-card__header,
.tag-card__body.tag-card__body { .tag-card__body.tag-card__body {
padding: .75rem; padding: .75rem;

View file

@ -0,0 +1,29 @@
import { FC } from 'react';
import { v4 as uuid } from 'uuid';
import { InputType } from 'reactstrap/lib/Input';
interface FormGroupContainer {
value: string;
onChange: (newValue: string) => void;
id?: string;
type?: InputType;
required?: boolean;
}
export const FormGroupContainer: FC<FormGroupContainer> = (
{ children, value, onChange, id = uuid(), type = 'text', required = true },
) => (
<div className="form-group">
<label htmlFor={id} className="create-server__label">
{children}:
</label>
<input
className="form-control"
type={type}
id={id}
value={value}
required={required}
onChange={(e) => onChange(e.target.value)}
/>
</div>
);

View file

@ -1,31 +0,0 @@
import { FC } from 'react';
import { v4 as uuid } from 'uuid';
import { InputType } from 'reactstrap/lib/Input';
interface HorizontalFormGroupProps {
value: string;
onChange: (newValue: string) => void;
id?: string;
type?: InputType;
required?: boolean;
}
export const HorizontalFormGroup: FC<HorizontalFormGroupProps> = (
{ children, value, onChange, id = uuid(), type = 'text', required = true },
) => (
<div className="form-group row">
<label htmlFor={id} className="col-lg-1 col-md-2 col-form-label create-server__label">
{children}:
</label>
<div className="col-lg-11 col-md-10">
<input
className="form-control"
type={type}
id={id}
value={value}
required={required}
onChange={(e) => onChange(e.target.value)}
/>
</div>
</div>
);

View file

@ -26,14 +26,21 @@ const getTextClassForType = (type: MessageType) => {
interface MessageProps { interface MessageProps {
noMargin?: boolean; noMargin?: boolean;
loading?: boolean; loading?: boolean;
fullWidth?: boolean;
type?: MessageType; type?: MessageType;
} }
const Message: FC<MessageProps> = ({ children, loading = false, noMargin = false, type = 'default' }) => { const Message: FC<MessageProps> = (
const cardClasses = classNames('bg-light', getClassForType(type), { 'mt-4': !noMargin }); { children, loading = false, noMargin = false, type = 'default', fullWidth = false },
) => {
const cardClasses = classNames(getClassForType(type), { 'mt-4': !noMargin });
const classes = classNames({
'col-md-12': fullWidth,
'col-md-10 offset-md-1': !fullWidth,
});
return ( return (
<div className="col-md-10 offset-md-1"> <div className={classes}>
<Card className={cardClasses} body> <Card className={cardClasses} body>
<h3 className={classNames('text-center mb-0', getTextClassForType(type))}> <h3 className={classNames('text-center mb-0', getTextClassForType(type))}>
{loading && <FontAwesomeIcon icon={preloader} spin />} {loading && <FontAwesomeIcon icon={preloader} spin />}

View file

@ -1,8 +1,9 @@
import { CardProps } from 'reactstrap/lib/Card'; import { CardProps } from 'reactstrap/lib/Card';
import { Card, CardBody, CardHeader } from 'reactstrap'; import { Card, CardBody, CardHeader } from 'reactstrap';
import { ReactNode } from 'react';
interface SimpleCardProps extends CardProps { interface SimpleCardProps extends Omit<CardProps, 'title'> {
title?: string; title?: ReactNode;
} }
export const SimpleCard = ({ title, children, ...rest }: SimpleCardProps) => ( export const SimpleCard = ({ title, children, ...rest }: SimpleCardProps) => (

View file

@ -15,7 +15,7 @@ interface VisitsHeaderProps {
const VisitsHeader: FC<VisitsHeaderProps> = ({ visits, goBack, shortUrl, children, title }) => ( const VisitsHeader: FC<VisitsHeaderProps> = ({ visits, goBack, shortUrl, children, title }) => (
<header> <header>
<Card className="bg-light" body> <Card body>
<h2 className="d-flex justify-content-between align-items-center mb-0"> <h2 className="d-flex justify-content-between align-items-center mb-0">
<Button color="link" size="lg" className="p-0 mr-3" onClick={goBack}> <Button color="link" size="lg" className="p-0 mr-3" onClick={goBack}>
<FontAwesomeIcon icon={faArrowLeft} /> <FontAwesomeIcon icon={faArrowLeft} />

View file

@ -4,6 +4,7 @@
.visits-table { .visits-table {
margin: 1.5rem 0 0; margin: 1.5rem 0 0;
position: relative; position: relative;
background-color: white;
} }
.visits-table__header-cell { .visits-table__header-cell {

View file

@ -95,7 +95,7 @@ const VisitsTable = ({
}, [ searchTerm ]); }, [ searchTerm ]);
return ( return (
<table className="table table-striped table-bordered table-hover table-sm table-responsive-sm visits-table"> <table className="table table-bordered table-hover table-sm table-responsive-sm visits-table">
<thead className="visits-table__header"> <thead className="visits-table__header">
<tr> <tr>
<th <th

View file

@ -22,8 +22,8 @@ describe('<ServersDropdown />', () => {
}); });
afterEach(() => wrapped.unmount()); afterEach(() => wrapped.unmount());
it('contains the list of servers, the divider and the export button', () => it('contains the list of servers, the divider, the create button and the export button', () =>
expect(wrapped.find(DropdownItem)).toHaveLength(values(servers).length + 2)); expect(wrapped.find(DropdownItem)).toHaveLength(values(servers).length + 3));
it('contains a toggle with proper title', () => it('contains a toggle with proper title', () =>
expect(wrapped.find(DropdownToggle)).toHaveLength(1)); expect(wrapped.find(DropdownToggle)).toHaveLength(1));
@ -35,14 +35,14 @@ describe('<ServersDropdown />', () => {
expect(items.filter('.servers-dropdown__export-item')).toHaveLength(1); expect(items.filter('.servers-dropdown__export-item')).toHaveLength(1);
}); });
it('shows a message when no servers exist yet', () => { it('shows only create link when no servers exist yet', () => {
wrapped = shallow( wrapped = shallow(
<ServersDropdown servers={{}} selectedServer={null} />, <ServersDropdown servers={{}} selectedServer={null} />,
); );
const item = wrapped.find(DropdownItem); const item = wrapped.find(DropdownItem);
expect(item).toHaveLength(1); expect(item).toHaveLength(1);
expect(item.prop('disabled')).toEqual(true); expect(item.prop('to')).toEqual('/server/create');
expect(item.find('i').text()).toEqual('Add a server first...'); expect(item.find('span').text()).toContain('Add server');
}); });
}); });

View file

@ -1,6 +1,6 @@
import { shallow, ShallowWrapper } from 'enzyme'; import { shallow, ShallowWrapper } from 'enzyme';
import { ServerForm } from '../../../src/servers/helpers/ServerForm'; import { ServerForm } from '../../../src/servers/helpers/ServerForm';
import { HorizontalFormGroup } from '../../../src/utils/HorizontalFormGroup'; import { FormGroupContainer } from '../../../src/utils/FormGroupContainer';
describe('<ServerForm />', () => { describe('<ServerForm />', () => {
let wrapper: ShallowWrapper; let wrapper: ShallowWrapper;
@ -14,7 +14,7 @@ describe('<ServerForm />', () => {
afterEach(jest.resetAllMocks); afterEach(jest.resetAllMocks);
it('renders components', () => { it('renders components', () => {
expect(wrapper.find(HorizontalFormGroup)).toHaveLength(3); expect(wrapper.find(FormGroupContainer)).toHaveLength(3);
expect(wrapper.find('span')).toHaveLength(1); expect(wrapper.find('span')).toHaveLength(1);
}); });