mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2024-12-23 09:30:31 +03:00
Merge pull request #369 from acelaya-forks/feature/consistent-dropdowns
Feature/consistent dropdowns
This commit is contained in:
commit
3bf64bee1e
14 changed files with 196 additions and 141 deletions
17
CHANGELOG.md
17
CHANGELOG.md
|
@ -4,6 +4,23 @@ All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org).
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org).
|
||||||
|
|
||||||
|
## [Unreleased]
|
||||||
|
### Added
|
||||||
|
* *Nothing*
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
* *Nothing*
|
||||||
|
|
||||||
|
### Deprecated
|
||||||
|
* *Nothing*
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
* *Nothing*
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
* [#364](https://github.com/shlinkio/shlink-web-client/issues/364) Fixed all dropdowns so that they are consistently styled.
|
||||||
|
|
||||||
|
|
||||||
## [3.0.0] - 2020-12-22
|
## [3.0.0] - 2020-12-22
|
||||||
### Added
|
### Added
|
||||||
* [#340](https://github.com/shlinkio/shlink-web-client/issues/340) Added new "overview" page, showing basic information of the active server.
|
* [#340](https://github.com/shlinkio/shlink-web-client/issues/340) Added new "overview" page, showing basic information of the active server.
|
||||||
|
|
|
@ -1,31 +1,12 @@
|
||||||
@import '../utils/mixins/vertical-align';
|
@import '../utils/mixins/vertical-align';
|
||||||
|
|
||||||
.domains-dropdown__toggle-btn.domains-dropdown__toggle-btn,
|
|
||||||
.domains-dropdown__toggle-btn.domains-dropdown__toggle-btn:hover,
|
|
||||||
.domains-dropdown__toggle-btn.domains-dropdown__toggle-btn:active {
|
|
||||||
text-align: left;
|
|
||||||
color: #6c757d;
|
|
||||||
border-color: #ced4da;
|
|
||||||
background-color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.domains-dropdown__toggle-btn--active.domains-dropdown__toggle-btn--active,
|
.domains-dropdown__toggle-btn--active.domains-dropdown__toggle-btn--active,
|
||||||
.domains-dropdown__toggle-btn--active.domains-dropdown__toggle-btn--active:hover,
|
.domains-dropdown__toggle-btn--active.domains-dropdown__toggle-btn--active:hover,
|
||||||
.domains-dropdown__toggle-btn--active.domains-dropdown__toggle-btn--active:active {
|
.domains-dropdown__toggle-btn--active.domains-dropdown__toggle-btn--active:active {
|
||||||
color: #495057;
|
color: #495057 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.domains-dropdown__back-btn.domains-dropdown__back-btn,
|
.domains-dropdown__back-btn.domains-dropdown__back-btn,
|
||||||
.domains-dropdown__back-btn.domains-dropdown__back-btn:hover {
|
.domains-dropdown__back-btn.domains-dropdown__back-btn:hover {
|
||||||
border-color: #ced4da;
|
border-color: #ced4da;
|
||||||
}
|
}
|
||||||
|
|
||||||
.domains-dropdown__toggle-btn.domains-dropdown__toggle-btn::after {
|
|
||||||
right: .75rem;
|
|
||||||
|
|
||||||
@include vertical-align();
|
|
||||||
}
|
|
||||||
|
|
||||||
.domains-dropdown__menu {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,20 +1,10 @@
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import {
|
import { Button, DropdownItem, Input, InputGroup, InputGroupAddon, UncontrolledTooltip } from 'reactstrap';
|
||||||
Button,
|
|
||||||
Dropdown,
|
|
||||||
DropdownItem,
|
|
||||||
DropdownMenu,
|
|
||||||
DropdownToggle,
|
|
||||||
Input,
|
|
||||||
InputGroup,
|
|
||||||
InputGroupAddon,
|
|
||||||
UncontrolledTooltip,
|
|
||||||
} from 'reactstrap';
|
|
||||||
import { InputProps } from 'reactstrap/lib/Input';
|
import { InputProps } from 'reactstrap/lib/Input';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import { faUndo } from '@fortawesome/free-solid-svg-icons';
|
import { faUndo } from '@fortawesome/free-solid-svg-icons';
|
||||||
import { isEmpty, pipe } from 'ramda';
|
import { isEmpty, pipe } from 'ramda';
|
||||||
import classNames from 'classnames';
|
import { DropdownBtn } from '../utils/DropdownBtn';
|
||||||
import { useToggle } from '../utils/helpers/hooks';
|
import { useToggle } from '../utils/helpers/hooks';
|
||||||
import { DomainsList } from './reducers/domainsList';
|
import { DomainsList } from './reducers/domainsList';
|
||||||
import './DomainSelector.scss';
|
import './DomainSelector.scss';
|
||||||
|
@ -31,7 +21,6 @@ interface DomainSelectorConnectProps extends DomainSelectorProps {
|
||||||
|
|
||||||
export const DomainSelector = ({ listDomains, value, domainsList, onChange }: DomainSelectorConnectProps) => {
|
export const DomainSelector = ({ listDomains, value, domainsList, onChange }: DomainSelectorConnectProps) => {
|
||||||
const [ inputDisplayed,, showInput, hideInput ] = useToggle();
|
const [ inputDisplayed,, showInput, hideInput ] = useToggle();
|
||||||
const [ isDropdownOpen, toggleDropdown ] = useToggle();
|
|
||||||
const { domains } = domainsList;
|
const { domains } = domainsList;
|
||||||
const valueIsEmpty = isEmpty(value);
|
const valueIsEmpty = isEmpty(value);
|
||||||
const unselectDomain = () => onChange('');
|
const unselectDomain = () => onChange('');
|
||||||
|
@ -63,33 +52,24 @@ export const DomainSelector = ({ listDomains, value, domainsList, onChange }: Do
|
||||||
</InputGroupAddon>
|
</InputGroupAddon>
|
||||||
</InputGroup>
|
</InputGroup>
|
||||||
) : (
|
) : (
|
||||||
<Dropdown isOpen={isDropdownOpen} toggle={toggleDropdown}>
|
<DropdownBtn
|
||||||
<DropdownToggle
|
text={valueIsEmpty ? 'Domain' : `Domain: ${value}`}
|
||||||
caret
|
className={!valueIsEmpty ? 'domains-dropdown__toggle-btn--active' : ''}
|
||||||
className={classNames(
|
>
|
||||||
'domains-dropdown__toggle-btn btn-block',
|
{domains.map(({ domain, isDefault }) => (
|
||||||
{ 'domains-dropdown__toggle-btn--active': !valueIsEmpty },
|
<DropdownItem
|
||||||
)}
|
key={domain}
|
||||||
>
|
active={value === domain || isDefault && valueIsEmpty}
|
||||||
{valueIsEmpty && <>Domain</>}
|
onClick={() => onChange(domain)}
|
||||||
{!valueIsEmpty && <>Domain: {value}</>}
|
>
|
||||||
</DropdownToggle>
|
{domain}
|
||||||
<DropdownMenu className="domains-dropdown__menu">
|
{isDefault && <span className="float-right text-muted">default</span>}
|
||||||
{domains.map(({ domain, isDefault }) => (
|
|
||||||
<DropdownItem
|
|
||||||
key={domain}
|
|
||||||
active={value === domain || isDefault && valueIsEmpty}
|
|
||||||
onClick={() => onChange(domain)}
|
|
||||||
>
|
|
||||||
{domain}
|
|
||||||
{isDefault && <span className="float-right text-muted">default</span>}
|
|
||||||
</DropdownItem>
|
|
||||||
))}
|
|
||||||
<DropdownItem divider />
|
|
||||||
<DropdownItem onClick={pipe(unselectDomain, showInput)}>
|
|
||||||
<i>New domain</i>
|
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
</DropdownMenu>
|
))}
|
||||||
</Dropdown>
|
<DropdownItem divider />
|
||||||
|
<DropdownItem onClick={pipe(unselectDomain, showInput)}>
|
||||||
|
<i>New domain</i>
|
||||||
|
</DropdownItem>
|
||||||
|
</DropdownBtn>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,12 +1,9 @@
|
||||||
import { FC, useEffect, useState } from 'react';
|
import { FC, useEffect, useState } from 'react';
|
||||||
import { Card } from 'reactstrap';
|
|
||||||
import Paginator from './Paginator';
|
|
||||||
import { ShortUrlsListProps } from './ShortUrlsList';
|
import { ShortUrlsListProps } from './ShortUrlsList';
|
||||||
|
|
||||||
const ShortUrls = (SearchBar: FC, ShortUrlsList: FC<ShortUrlsListProps>) => (props: ShortUrlsListProps) => {
|
const ShortUrls = (SearchBar: FC, ShortUrlsList: FC<ShortUrlsListProps>) => (props: ShortUrlsListProps) => {
|
||||||
const { match, shortUrlsList } = props;
|
const { match } = props;
|
||||||
const { page = '1', serverId = '' } = match?.params ?? {};
|
const { page = '1', serverId = '' } = match?.params ?? {};
|
||||||
const { pagination } = shortUrlsList?.shortUrls ?? {};
|
|
||||||
const [ urlsListKey, setUrlsListKey ] = useState(`${serverId}_${page}`);
|
const [ urlsListKey, setUrlsListKey ] = useState(`${serverId}_${page}`);
|
||||||
|
|
||||||
// Using a key on a component makes react to create a new instance every time the key changes
|
// Using a key on a component makes react to create a new instance every time the key changes
|
||||||
|
@ -18,10 +15,7 @@ const ShortUrls = (SearchBar: FC, ShortUrlsList: FC<ShortUrlsListProps>) => (pro
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="form-group"><SearchBar /></div>
|
<div className="form-group"><SearchBar /></div>
|
||||||
<Card body className="pb-1">
|
<ShortUrlsList {...props} key={urlsListKey} />
|
||||||
<ShortUrlsList {...props} key={urlsListKey} />
|
|
||||||
<Paginator paginator={pagination} serverId={serverId} />
|
|
||||||
</Card>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -3,14 +3,16 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import { head, keys, values } from 'ramda';
|
import { head, keys, values } from 'ramda';
|
||||||
import { FC, useEffect, useState } from 'react';
|
import { FC, useEffect, useState } from 'react';
|
||||||
import { RouteComponentProps } from 'react-router';
|
import { RouteComponentProps } from 'react-router';
|
||||||
|
import { Card } from 'reactstrap';
|
||||||
import SortingDropdown from '../utils/SortingDropdown';
|
import SortingDropdown from '../utils/SortingDropdown';
|
||||||
import { determineOrderDir, OrderDir } from '../utils/utils';
|
import { determineOrderDir, OrderDir } from '../utils/utils';
|
||||||
import { SelectedServer } from '../servers/data';
|
import { isReachableServer, SelectedServer } from '../servers/data';
|
||||||
import { boundToMercureHub } from '../mercure/helpers/boundToMercureHub';
|
import { boundToMercureHub } from '../mercure/helpers/boundToMercureHub';
|
||||||
import { parseQuery } from '../utils/helpers/query';
|
import { parseQuery } from '../utils/helpers/query';
|
||||||
import { ShortUrlsList as ShortUrlsListState } from './reducers/shortUrlsList';
|
import { ShortUrlsList as ShortUrlsListState } from './reducers/shortUrlsList';
|
||||||
import { OrderableFields, ShortUrlsListParams, SORTABLE_FIELDS } from './reducers/shortUrlsListParams';
|
import { OrderableFields, ShortUrlsListParams, SORTABLE_FIELDS } from './reducers/shortUrlsListParams';
|
||||||
import { ShortUrlsTableProps } from './ShortUrlsTable';
|
import { ShortUrlsTableProps } from './ShortUrlsTable';
|
||||||
|
import Paginator from './Paginator';
|
||||||
import './ShortUrlsList.scss';
|
import './ShortUrlsList.scss';
|
||||||
|
|
||||||
interface RouteParams {
|
interface RouteParams {
|
||||||
|
@ -40,6 +42,7 @@ const ShortUrlsList = (ShortUrlsTable: FC<ShortUrlsTableProps>) => boundToMercur
|
||||||
orderField: orderBy && (head(keys(orderBy)) as OrderableFields),
|
orderField: orderBy && (head(keys(orderBy)) as OrderableFields),
|
||||||
orderDir: orderBy && head(values(orderBy)),
|
orderDir: orderBy && head(values(orderBy)),
|
||||||
});
|
});
|
||||||
|
const { pagination } = shortUrlsList?.shortUrls ?? {};
|
||||||
const refreshList = (extraParams: ShortUrlsListParams) => listShortUrls({ ...shortUrlsListParams, ...extraParams });
|
const refreshList = (extraParams: ShortUrlsListParams) => listShortUrls({ ...shortUrlsListParams, ...extraParams });
|
||||||
const handleOrderBy = (orderField?: OrderableFields, orderDir?: OrderDir) => {
|
const handleOrderBy = (orderField?: OrderableFields, orderDir?: OrderDir) => {
|
||||||
setOrder({ orderField, orderDir });
|
setOrder({ orderField, orderDir });
|
||||||
|
@ -83,13 +86,16 @@ const ShortUrlsList = (ShortUrlsTable: FC<ShortUrlsTableProps>) => boundToMercur
|
||||||
onChange={handleOrderBy}
|
onChange={handleOrderBy}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<ShortUrlsTable
|
<Card body className="pb-1">
|
||||||
orderByColumn={orderByColumn}
|
<ShortUrlsTable
|
||||||
renderOrderIcon={renderOrderIcon}
|
orderByColumn={orderByColumn}
|
||||||
selectedServer={selectedServer}
|
renderOrderIcon={renderOrderIcon}
|
||||||
shortUrlsList={shortUrlsList}
|
selectedServer={selectedServer}
|
||||||
onTagClick={(tag) => refreshList({ tags: [ ...shortUrlsListParams.tags ?? [], tag ] })}
|
shortUrlsList={shortUrlsList}
|
||||||
/>
|
onTagClick={(tag) => refreshList({ tags: [ ...shortUrlsListParams.tags ?? [], tag ] })}
|
||||||
|
/>
|
||||||
|
<Paginator paginator={pagination} serverId={isReachableServer(selectedServer) ? selectedServer.id : ''} />
|
||||||
|
</Card>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}, () => 'https://shlink.io/new-visit');
|
}, () => 'https://shlink.io/new-visit');
|
||||||
|
|
19
src/utils/DropdownBtn.scss
Normal file
19
src/utils/DropdownBtn.scss
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
@import '../utils/mixins/vertical-align';
|
||||||
|
|
||||||
|
.dropdown-btn__toggle.dropdown-btn__toggle,
|
||||||
|
.dropdown-btn__toggle.dropdown-btn__toggle:not(:disabled):not(.disabled).active,
|
||||||
|
.dropdown-btn__toggle.dropdown-btn__toggle:not(:disabled):not(.disabled):active,
|
||||||
|
.dropdown-btn__toggle.dropdown-btn__toggle:not(:disabled):not(.disabled):focus,
|
||||||
|
.dropdown-btn__toggle.dropdown-btn__toggle:not(:disabled):not(.disabled):hover,
|
||||||
|
.show > .dropdown-btn__toggle.dropdown-btn__toggle.dropdown-toggle {
|
||||||
|
color: #6c757d;
|
||||||
|
background-color: white;
|
||||||
|
text-align: left;
|
||||||
|
border-color: rgba(0, 0, 0, .125);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-btn__toggle.dropdown-btn__toggle:after {
|
||||||
|
@include vertical-align();
|
||||||
|
|
||||||
|
right: .75rem;
|
||||||
|
}
|
22
src/utils/DropdownBtn.tsx
Normal file
22
src/utils/DropdownBtn.tsx
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
import { FC } from 'react';
|
||||||
|
import { Dropdown, DropdownMenu, DropdownToggle } from 'reactstrap';
|
||||||
|
import { useToggle } from './helpers/hooks';
|
||||||
|
import './DropdownBtn.scss';
|
||||||
|
|
||||||
|
export interface DropdownBtnProps {
|
||||||
|
text: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DropdownBtn: FC<DropdownBtnProps> = ({ text, disabled = false, className = '', children }) => {
|
||||||
|
const [ isOpen, toggle ] = useToggle();
|
||||||
|
const toggleClasses = `dropdown-btn__toggle btn-block ${className}`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dropdown isOpen={isOpen} toggle={toggle} disabled={disabled}>
|
||||||
|
<DropdownToggle caret className={toggleClasses} color="primary">{text}</DropdownToggle>
|
||||||
|
<DropdownMenu className="w-100">{children}</DropdownMenu>
|
||||||
|
</Dropdown>
|
||||||
|
);
|
||||||
|
};
|
|
@ -28,10 +28,12 @@ export default function SortingDropdown<T extends string = string>(
|
||||||
<UncontrolledDropdown>
|
<UncontrolledDropdown>
|
||||||
<DropdownToggle
|
<DropdownToggle
|
||||||
caret
|
caret
|
||||||
color={isButton ? 'secondary' : 'link'}
|
color={isButton ? 'primary' : 'link'}
|
||||||
className={classNames({ 'btn-block': isButton, 'btn-sm p-0': !isButton })}
|
className={classNames({ 'dropdown-btn__toggle btn-block': isButton, 'btn-sm p-0': !isButton })}
|
||||||
>
|
>
|
||||||
Order by
|
{!isButton && <>Order by</>}
|
||||||
|
{isButton && !orderField && <>Order by...</>}
|
||||||
|
{isButton && orderField && `Order by: "${items[orderField]}" - "${orderDir ?? 'DESC'}"`}
|
||||||
</DropdownToggle>
|
</DropdownToggle>
|
||||||
<DropdownMenu
|
<DropdownMenu
|
||||||
right={right}
|
right={right}
|
||||||
|
|
|
@ -1,19 +0,0 @@
|
||||||
@import '../../utils/mixins/vertical-align';
|
|
||||||
|
|
||||||
.date-range-selector__btn.date-range-selector__btn,
|
|
||||||
.date-range-selector__btn.date-range-selector__btn:not(:disabled):not(.disabled).active,
|
|
||||||
.date-range-selector__btn.date-range-selector__btn:not(:disabled):not(.disabled):active,
|
|
||||||
.date-range-selector__btn.date-range-selector__btn:not(:disabled):not(.disabled):focus,
|
|
||||||
.date-range-selector__btn.date-range-selector__btn:not(:disabled):not(.disabled):hover,
|
|
||||||
.show > .date-range-selector__btn.date-range-selector__btn.dropdown-toggle {
|
|
||||||
color: #6c757d;
|
|
||||||
background-color: white;
|
|
||||||
text-align: left;
|
|
||||||
border-color: rgba(0, 0, 0, .125);
|
|
||||||
}
|
|
||||||
|
|
||||||
.date-range-selector__btn.date-range-selector__btn:after {
|
|
||||||
@include vertical-align();
|
|
||||||
|
|
||||||
right: .75rem;
|
|
||||||
}
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { Dropdown, DropdownItem, DropdownMenu, DropdownToggle } from 'reactstrap';
|
import { DropdownItem } from 'reactstrap';
|
||||||
import { useToggle } from '../helpers/hooks';
|
import { DropdownBtn } from '../DropdownBtn';
|
||||||
import {
|
import {
|
||||||
DateInterval,
|
DateInterval,
|
||||||
DateRange,
|
DateRange,
|
||||||
|
@ -10,7 +10,6 @@ import {
|
||||||
rangeIsInterval,
|
rangeIsInterval,
|
||||||
} from './types';
|
} from './types';
|
||||||
import DateRangeRow from './DateRangeRow';
|
import DateRangeRow from './DateRangeRow';
|
||||||
import './DateRangeSelector.scss';
|
|
||||||
|
|
||||||
export interface DateRangeSelectorProps {
|
export interface DateRangeSelectorProps {
|
||||||
initialDateRange?: DateInterval | DateRange;
|
initialDateRange?: DateInterval | DateRange;
|
||||||
|
@ -20,9 +19,8 @@ export interface DateRangeSelectorProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DateRangeSelector = (
|
export const DateRangeSelector = (
|
||||||
{ onDatesChange, initialDateRange, defaultText, disabled = false }: DateRangeSelectorProps,
|
{ onDatesChange, initialDateRange, defaultText, disabled }: DateRangeSelectorProps,
|
||||||
) => {
|
) => {
|
||||||
const [ isOpen, toggle ] = useToggle();
|
|
||||||
const [ activeInterval, setActiveInterval ] = useState(
|
const [ activeInterval, setActiveInterval ] = useState(
|
||||||
rangeIsInterval(initialDateRange) ? initialDateRange : undefined,
|
rangeIsInterval(initialDateRange) ? initialDateRange : undefined,
|
||||||
);
|
);
|
||||||
|
@ -41,35 +39,30 @@ export const DateRangeSelector = (
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dropdown isOpen={isOpen} toggle={toggle} disabled={disabled}>
|
<DropdownBtn disabled={disabled} text={rangeOrIntervalToString(activeInterval ?? activeDateRange) ?? defaultText}>
|
||||||
<DropdownToggle caret className="date-range-selector__btn btn-block" color="primary">
|
<DropdownItem
|
||||||
{rangeOrIntervalToString(activeInterval ?? activeDateRange) ?? defaultText}
|
active={activeInterval === undefined && dateRangeIsEmpty(activeDateRange)}
|
||||||
</DropdownToggle>
|
onClick={updateInterval(undefined)}
|
||||||
<DropdownMenu className="w-100">
|
>
|
||||||
<DropdownItem
|
{defaultText}
|
||||||
active={activeInterval === undefined && dateRangeIsEmpty(activeDateRange)}
|
</DropdownItem>
|
||||||
onClick={updateInterval(undefined)}
|
<DropdownItem divider />
|
||||||
>
|
{([ 'today', 'yesterday', 'last7Days', 'last30Days', 'last90Days', 'last180days', 'last365Days' ] as DateInterval[]).map(
|
||||||
{defaultText}
|
(interval) => (
|
||||||
</DropdownItem>
|
<DropdownItem key={interval} active={activeInterval === interval} onClick={updateInterval(interval)}>
|
||||||
<DropdownItem divider />
|
{rangeOrIntervalToString(interval)}
|
||||||
{([ 'today', 'yesterday', 'last7Days', 'last30Days', 'last90Days', 'last180days', 'last365Days' ] as DateInterval[]).map(
|
</DropdownItem>
|
||||||
(interval) => (
|
),
|
||||||
<DropdownItem key={interval} active={activeInterval === interval} onClick={updateInterval(interval)}>
|
)}
|
||||||
{rangeOrIntervalToString(interval)}
|
<DropdownItem divider />
|
||||||
</DropdownItem>
|
<DropdownItem header>Custom:</DropdownItem>
|
||||||
),
|
<DropdownItem text>
|
||||||
)}
|
<DateRangeRow
|
||||||
<DropdownItem divider />
|
{...activeDateRange}
|
||||||
<DropdownItem header>Custom:</DropdownItem>
|
onStartDateChange={(startDate) => updateDateRange({ ...activeDateRange, startDate })}
|
||||||
<DropdownItem text>
|
onEndDateChange={(endDate) => updateDateRange({ ...activeDateRange, endDate })}
|
||||||
<DateRangeRow
|
/>
|
||||||
{...activeDateRange}
|
</DropdownItem>
|
||||||
onStartDateChange={(startDate) => updateDateRange({ ...activeDateRange, startDate })}
|
</DropdownBtn>
|
||||||
onEndDateChange={(endDate) => updateDateRange({ ...activeDateRange, endDate })}
|
|
||||||
/>
|
|
||||||
</DropdownItem>
|
|
||||||
</DropdownMenu>
|
|
||||||
</Dropdown>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
import { shallow, ShallowWrapper } from 'enzyme';
|
import { shallow, ShallowWrapper } from 'enzyme';
|
||||||
import { Mock } from 'ts-mockery';
|
import { Mock } from 'ts-mockery';
|
||||||
import { DropdownItem, DropdownMenu, InputGroup } from 'reactstrap';
|
import { DropdownItem, InputGroup } from 'reactstrap';
|
||||||
import { DomainSelector } from '../../src/domains/DomainSelector';
|
import { DomainSelector } from '../../src/domains/DomainSelector';
|
||||||
import { DomainsList } from '../../src/domains/reducers/domainsList';
|
import { DomainsList } from '../../src/domains/reducers/domainsList';
|
||||||
import { ShlinkDomain } from '../../src/api/types';
|
import { ShlinkDomain } from '../../src/api/types';
|
||||||
|
import { DropdownBtn } from '../../src/utils/DropdownBtn';
|
||||||
|
|
||||||
describe('<DomainSelector />', () => {
|
describe('<DomainSelector />', () => {
|
||||||
let wrapper: ShallowWrapper;
|
let wrapper: ShallowWrapper;
|
||||||
|
@ -23,7 +24,7 @@ describe('<DomainSelector />', () => {
|
||||||
|
|
||||||
it('shows dropdown by default', () => {
|
it('shows dropdown by default', () => {
|
||||||
const input = wrapper.find(InputGroup);
|
const input = wrapper.find(InputGroup);
|
||||||
const dropdown = wrapper.find(DropdownMenu);
|
const dropdown = wrapper.find(DropdownBtn);
|
||||||
|
|
||||||
expect(input).toHaveLength(0);
|
expect(input).toHaveLength(0);
|
||||||
expect(dropdown).toHaveLength(1);
|
expect(dropdown).toHaveLength(1);
|
||||||
|
@ -33,10 +34,10 @@ describe('<DomainSelector />', () => {
|
||||||
it('allows to toggle between dropdown and input', () => {
|
it('allows to toggle between dropdown and input', () => {
|
||||||
wrapper.find(DropdownItem).last().simulate('click');
|
wrapper.find(DropdownItem).last().simulate('click');
|
||||||
expect(wrapper.find(InputGroup)).toHaveLength(1);
|
expect(wrapper.find(InputGroup)).toHaveLength(1);
|
||||||
expect(wrapper.find(DropdownMenu)).toHaveLength(0);
|
expect(wrapper.find(DropdownBtn)).toHaveLength(0);
|
||||||
|
|
||||||
wrapper.find('.domains-dropdown__back-btn').simulate('click');
|
wrapper.find('.domains-dropdown__back-btn').simulate('click');
|
||||||
expect(wrapper.find(InputGroup)).toHaveLength(0);
|
expect(wrapper.find(InputGroup)).toHaveLength(0);
|
||||||
expect(wrapper.find(DropdownMenu)).toHaveLength(1);
|
expect(wrapper.find(DropdownBtn)).toHaveLength(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import { shallow, ShallowWrapper } from 'enzyme';
|
import { shallow, ShallowWrapper } from 'enzyme';
|
||||||
import { Mock } from 'ts-mockery';
|
import { Mock } from 'ts-mockery';
|
||||||
import shortUrlsCreator from '../../src/short-urls/ShortUrls';
|
import shortUrlsCreator from '../../src/short-urls/ShortUrls';
|
||||||
import Paginator from '../../src/short-urls/Paginator';
|
|
||||||
import { ShortUrlsListProps } from '../../src/short-urls/ShortUrlsList';
|
import { ShortUrlsListProps } from '../../src/short-urls/ShortUrlsList';
|
||||||
|
|
||||||
describe('<ShortUrls />', () => {
|
describe('<ShortUrls />', () => {
|
||||||
|
@ -18,9 +17,8 @@ describe('<ShortUrls />', () => {
|
||||||
});
|
});
|
||||||
afterEach(() => wrapper.unmount());
|
afterEach(() => wrapper.unmount());
|
||||||
|
|
||||||
it('wraps a SearchBar, ShortUrlsList as Paginator', () => {
|
it('wraps a SearchBar and ShortUrlsList', () => {
|
||||||
expect(wrapper.find(SearchBar)).toHaveLength(1);
|
expect(wrapper.find(SearchBar)).toHaveLength(1);
|
||||||
expect(wrapper.find(ShortUrlsList)).toHaveLength(1);
|
expect(wrapper.find(ShortUrlsList)).toHaveLength(1);
|
||||||
expect(wrapper.find(Paginator)).toHaveLength(1);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
41
test/utils/DropdownBtn.test.tsx
Normal file
41
test/utils/DropdownBtn.test.tsx
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
import { shallow, ShallowWrapper } from 'enzyme';
|
||||||
|
import { DropdownMenu, DropdownToggle } from 'reactstrap';
|
||||||
|
import { PropsWithChildren } from 'react';
|
||||||
|
import { DropdownBtn, DropdownBtnProps } from '../../src/utils/DropdownBtn';
|
||||||
|
|
||||||
|
describe('<DropdownBtn />', () => {
|
||||||
|
let wrapper: ShallowWrapper;
|
||||||
|
const createWrapper = (props: PropsWithChildren<DropdownBtnProps>) => {
|
||||||
|
wrapper = shallow(<DropdownBtn {...props} />);
|
||||||
|
|
||||||
|
return wrapper;
|
||||||
|
};
|
||||||
|
|
||||||
|
afterEach(() => wrapper?.unmount());
|
||||||
|
|
||||||
|
it.each([[ 'foo' ], [ 'bar' ], [ 'baz' ]])('displays provided text', (text) => {
|
||||||
|
const wrapper = createWrapper({ text });
|
||||||
|
const toggle = wrapper.find(DropdownToggle);
|
||||||
|
|
||||||
|
expect(toggle.html()).toContain(text);
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([[ 'foo' ], [ 'bar' ], [ 'baz' ]])('displays provided children', (children) => {
|
||||||
|
const wrapper = createWrapper({ text: '', children });
|
||||||
|
const menu = wrapper.find(DropdownMenu);
|
||||||
|
|
||||||
|
expect(menu.html()).toContain(children);
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
[ undefined, 'dropdown-btn__toggle btn-block' ],
|
||||||
|
[ '', 'dropdown-btn__toggle btn-block' ],
|
||||||
|
[ 'foo', 'dropdown-btn__toggle btn-block foo' ],
|
||||||
|
[ 'bar', 'dropdown-btn__toggle btn-block bar' ],
|
||||||
|
])('includes provided classes', (className, expectedClasses) => {
|
||||||
|
const wrapper = createWrapper({ text: '', className });
|
||||||
|
const toggle = wrapper.find(DropdownToggle);
|
||||||
|
|
||||||
|
expect(toggle.prop('className')?.trim()).toEqual(expectedClasses);
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,9 +1,10 @@
|
||||||
import { shallow, ShallowWrapper } from 'enzyme';
|
import { shallow, ShallowWrapper } from 'enzyme';
|
||||||
import { DropdownItem } from 'reactstrap';
|
import { DropdownItem, DropdownToggle } from 'reactstrap';
|
||||||
import { identity, values } from 'ramda';
|
import { identity, values } from 'ramda';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import { faSortAmountDown as caretDownIcon } from '@fortawesome/free-solid-svg-icons';
|
import { faSortAmountDown as caretDownIcon } from '@fortawesome/free-solid-svg-icons';
|
||||||
import SortingDropdown, { SortingDropdownProps } from '../../src/utils/SortingDropdown';
|
import SortingDropdown, { SortingDropdownProps } from '../../src/utils/SortingDropdown';
|
||||||
|
import { OrderDir } from '../../src/utils/utils';
|
||||||
|
|
||||||
describe('<SortingDropdown />', () => {
|
describe('<SortingDropdown />', () => {
|
||||||
let wrapper: ShallowWrapper;
|
let wrapper: ShallowWrapper;
|
||||||
|
@ -73,4 +74,23 @@ describe('<SortingDropdown />', () => {
|
||||||
expect(onChange).toHaveBeenCalledTimes(1);
|
expect(onChange).toHaveBeenCalledTimes(1);
|
||||||
expect(onChange).toHaveBeenCalledWith('foo', 'DESC');
|
expect(onChange).toHaveBeenCalledWith('foo', 'DESC');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
[{ isButton: false }, '>Order by<' ],
|
||||||
|
[{ isButton: true }, '>Order by...<' ],
|
||||||
|
[
|
||||||
|
{ isButton: true, orderField: 'foo', orderDir: 'ASC' as OrderDir },
|
||||||
|
'Order by: "Foo" - "ASC"',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{ isButton: true, orderField: 'baz', orderDir: 'DESC' as OrderDir },
|
||||||
|
'Order by: "Hello World" - "DESC"',
|
||||||
|
],
|
||||||
|
[{ isButton: true, orderField: 'baz' }, 'Order by: "Hello World" - "DESC"' ],
|
||||||
|
])('displays expected text in toggle', (props, expectedText) => {
|
||||||
|
const wrapper = createWrapper(props);
|
||||||
|
const toggle = wrapper.find(DropdownToggle);
|
||||||
|
|
||||||
|
expect(toggle.html()).toContain(expectedText);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in a new issue