mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2024-12-23 17:40:23 +03:00
Merge pull request #345 from acelaya-forks/feature/restyle
Feature/restyle
This commit is contained in:
commit
32957835b3
29 changed files with 150 additions and 111 deletions
|
@ -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*
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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} /> Settings
|
<FontAwesomeIcon icon={cogsIcon} /> Settings
|
||||||
</NavLink>
|
</NavLink>
|
||||||
</NavItem>
|
</NavItem>
|
||||||
<NavItem>
|
|
||||||
<NavLink tag={Link} to={createServerPath} active={pathname === createServerPath}>
|
|
||||||
<FontAwesomeIcon icon={plusIcon} /> Add server
|
|
||||||
</NavLink>
|
|
||||||
</NavItem>
|
|
||||||
<ServersDropdown />
|
<ServersDropdown />
|
||||||
</Nav>
|
</Nav>
|
||||||
</Collapse>
|
</Collapse>
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
.no-menu-wrapper {
|
.no-menu-wrapper {
|
||||||
padding: 40px 20px 20px;
|
padding: 30px 20px 20px;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
3
src/servers/helpers/ServerForm.scss
Normal file
3
src/servers/helpers/ServerForm.scss
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
.server-form .form-group:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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;
|
||||||
|
|
29
src/utils/FormGroupContainer.tsx
Normal file
29
src/utils/FormGroupContainer.tsx
Normal 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>
|
||||||
|
);
|
|
@ -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>
|
|
||||||
);
|
|
|
@ -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 />}
|
||||||
|
|
|
@ -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) => (
|
||||||
|
|
|
@ -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} />
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue