diff --git a/CHANGELOG.md b/CHANGELOG.md
index f84c1824..4ef4cceb 100644
--- a/CHANGELOG.md
+++ b/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).
+## [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
### Added
* [#340](https://github.com/shlinkio/shlink-web-client/issues/340) Added new "overview" page, showing basic information of the active server.
diff --git a/src/domains/DomainSelector.scss b/src/domains/DomainSelector.scss
index c9ab0ba0..89e02433 100644
--- a/src/domains/DomainSelector.scss
+++ b/src/domains/DomainSelector.scss
@@ -1,31 +1,12 @@
@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:hover,
.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:hover {
border-color: #ced4da;
}
-
-.domains-dropdown__toggle-btn.domains-dropdown__toggle-btn::after {
- right: .75rem;
-
- @include vertical-align();
-}
-
-.domains-dropdown__menu {
- width: 100%;
-}
diff --git a/src/domains/DomainSelector.tsx b/src/domains/DomainSelector.tsx
index 510b39f9..c843f409 100644
--- a/src/domains/DomainSelector.tsx
+++ b/src/domains/DomainSelector.tsx
@@ -1,20 +1,10 @@
import { useEffect } from 'react';
-import {
- Button,
- Dropdown,
- DropdownItem,
- DropdownMenu,
- DropdownToggle,
- Input,
- InputGroup,
- InputGroupAddon,
- UncontrolledTooltip,
-} from 'reactstrap';
+import { Button, DropdownItem, Input, InputGroup, InputGroupAddon, UncontrolledTooltip } from 'reactstrap';
import { InputProps } from 'reactstrap/lib/Input';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faUndo } from '@fortawesome/free-solid-svg-icons';
import { isEmpty, pipe } from 'ramda';
-import classNames from 'classnames';
+import { DropdownBtn } from '../utils/DropdownBtn';
import { useToggle } from '../utils/helpers/hooks';
import { DomainsList } from './reducers/domainsList';
import './DomainSelector.scss';
@@ -31,7 +21,6 @@ interface DomainSelectorConnectProps extends DomainSelectorProps {
export const DomainSelector = ({ listDomains, value, domainsList, onChange }: DomainSelectorConnectProps) => {
const [ inputDisplayed,, showInput, hideInput ] = useToggle();
- const [ isDropdownOpen, toggleDropdown ] = useToggle();
const { domains } = domainsList;
const valueIsEmpty = isEmpty(value);
const unselectDomain = () => onChange('');
@@ -63,33 +52,24 @@ export const DomainSelector = ({ listDomains, value, domainsList, onChange }: Do
) : (
-
-
- {valueIsEmpty && <>Domain>}
- {!valueIsEmpty && <>Domain: {value}>}
-
-
- {domains.map(({ domain, isDefault }) => (
- onChange(domain)}
- >
- {domain}
- {isDefault && default}
-
- ))}
-
-
- New domain
+
+ {domains.map(({ domain, isDefault }) => (
+ onChange(domain)}
+ >
+ {domain}
+ {isDefault && default}
-
-
+ ))}
+
+
+ New domain
+
+
);
};
diff --git a/src/short-urls/ShortUrls.tsx b/src/short-urls/ShortUrls.tsx
index d6dea6bc..13814837 100644
--- a/src/short-urls/ShortUrls.tsx
+++ b/src/short-urls/ShortUrls.tsx
@@ -1,12 +1,9 @@
import { FC, useEffect, useState } from 'react';
-import { Card } from 'reactstrap';
-import Paginator from './Paginator';
import { ShortUrlsListProps } from './ShortUrlsList';
const ShortUrls = (SearchBar: FC, ShortUrlsList: FC) => (props: ShortUrlsListProps) => {
- const { match, shortUrlsList } = props;
+ const { match } = props;
const { page = '1', serverId = '' } = match?.params ?? {};
- const { pagination } = shortUrlsList?.shortUrls ?? {};
const [ urlsListKey, setUrlsListKey ] = useState(`${serverId}_${page}`);
// 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) => (pro
return (
<>
-
-
-
-
+
>
);
};
diff --git a/src/short-urls/ShortUrlsList.tsx b/src/short-urls/ShortUrlsList.tsx
index 86397f73..c92ec4c1 100644
--- a/src/short-urls/ShortUrlsList.tsx
+++ b/src/short-urls/ShortUrlsList.tsx
@@ -3,14 +3,16 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { head, keys, values } from 'ramda';
import { FC, useEffect, useState } from 'react';
import { RouteComponentProps } from 'react-router';
+import { Card } from 'reactstrap';
import SortingDropdown from '../utils/SortingDropdown';
import { determineOrderDir, OrderDir } from '../utils/utils';
-import { SelectedServer } from '../servers/data';
+import { isReachableServer, SelectedServer } from '../servers/data';
import { boundToMercureHub } from '../mercure/helpers/boundToMercureHub';
import { parseQuery } from '../utils/helpers/query';
import { ShortUrlsList as ShortUrlsListState } from './reducers/shortUrlsList';
import { OrderableFields, ShortUrlsListParams, SORTABLE_FIELDS } from './reducers/shortUrlsListParams';
import { ShortUrlsTableProps } from './ShortUrlsTable';
+import Paginator from './Paginator';
import './ShortUrlsList.scss';
interface RouteParams {
@@ -40,6 +42,7 @@ const ShortUrlsList = (ShortUrlsTable: FC) => boundToMercur
orderField: orderBy && (head(keys(orderBy)) as OrderableFields),
orderDir: orderBy && head(values(orderBy)),
});
+ const { pagination } = shortUrlsList?.shortUrls ?? {};
const refreshList = (extraParams: ShortUrlsListParams) => listShortUrls({ ...shortUrlsListParams, ...extraParams });
const handleOrderBy = (orderField?: OrderableFields, orderDir?: OrderDir) => {
setOrder({ orderField, orderDir });
@@ -83,13 +86,16 @@ const ShortUrlsList = (ShortUrlsTable: FC) => boundToMercur
onChange={handleOrderBy}
/>
- refreshList({ tags: [ ...shortUrlsListParams.tags ?? [], tag ] })}
- />
+
+ refreshList({ tags: [ ...shortUrlsListParams.tags ?? [], tag ] })}
+ />
+
+
>
);
}, () => 'https://shlink.io/new-visit');
diff --git a/src/utils/DropdownBtn.scss b/src/utils/DropdownBtn.scss
new file mode 100644
index 00000000..6cfae1dc
--- /dev/null
+++ b/src/utils/DropdownBtn.scss
@@ -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;
+}
diff --git a/src/utils/DropdownBtn.tsx b/src/utils/DropdownBtn.tsx
new file mode 100644
index 00000000..d1cf6f6b
--- /dev/null
+++ b/src/utils/DropdownBtn.tsx
@@ -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 = ({ text, disabled = false, className = '', children }) => {
+ const [ isOpen, toggle ] = useToggle();
+ const toggleClasses = `dropdown-btn__toggle btn-block ${className}`;
+
+ return (
+
+ {text}
+ {children}
+
+ );
+};
diff --git a/src/utils/SortingDropdown.tsx b/src/utils/SortingDropdown.tsx
index 76fd1ca4..d9e06ec3 100644
--- a/src/utils/SortingDropdown.tsx
+++ b/src/utils/SortingDropdown.tsx
@@ -28,10 +28,12 @@ export default function SortingDropdown(
- Order by
+ {!isButton && <>Order by>}
+ {isButton && !orderField && <>Order by...>}
+ {isButton && orderField && `Order by: "${items[orderField]}" - "${orderDir ?? 'DESC'}"`}
.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;
-}
diff --git a/src/utils/dates/DateRangeSelector.tsx b/src/utils/dates/DateRangeSelector.tsx
index 7fbdc7bf..1b680f90 100644
--- a/src/utils/dates/DateRangeSelector.tsx
+++ b/src/utils/dates/DateRangeSelector.tsx
@@ -1,6 +1,6 @@
import { useState } from 'react';
-import { Dropdown, DropdownItem, DropdownMenu, DropdownToggle } from 'reactstrap';
-import { useToggle } from '../helpers/hooks';
+import { DropdownItem } from 'reactstrap';
+import { DropdownBtn } from '../DropdownBtn';
import {
DateInterval,
DateRange,
@@ -10,7 +10,6 @@ import {
rangeIsInterval,
} from './types';
import DateRangeRow from './DateRangeRow';
-import './DateRangeSelector.scss';
export interface DateRangeSelectorProps {
initialDateRange?: DateInterval | DateRange;
@@ -20,9 +19,8 @@ export interface DateRangeSelectorProps {
}
export const DateRangeSelector = (
- { onDatesChange, initialDateRange, defaultText, disabled = false }: DateRangeSelectorProps,
+ { onDatesChange, initialDateRange, defaultText, disabled }: DateRangeSelectorProps,
) => {
- const [ isOpen, toggle ] = useToggle();
const [ activeInterval, setActiveInterval ] = useState(
rangeIsInterval(initialDateRange) ? initialDateRange : undefined,
);
@@ -41,35 +39,30 @@ export const DateRangeSelector = (
};
return (
-
-
- {rangeOrIntervalToString(activeInterval ?? activeDateRange) ?? defaultText}
-
-
-
- {defaultText}
-
-
- {([ 'today', 'yesterday', 'last7Days', 'last30Days', 'last90Days', 'last180days', 'last365Days' ] as DateInterval[]).map(
- (interval) => (
-
- {rangeOrIntervalToString(interval)}
-
- ),
- )}
-
- Custom:
-
- updateDateRange({ ...activeDateRange, startDate })}
- onEndDateChange={(endDate) => updateDateRange({ ...activeDateRange, endDate })}
- />
-
-
-
+
+
+ {defaultText}
+
+
+ {([ 'today', 'yesterday', 'last7Days', 'last30Days', 'last90Days', 'last180days', 'last365Days' ] as DateInterval[]).map(
+ (interval) => (
+
+ {rangeOrIntervalToString(interval)}
+
+ ),
+ )}
+
+ Custom:
+
+ updateDateRange({ ...activeDateRange, startDate })}
+ onEndDateChange={(endDate) => updateDateRange({ ...activeDateRange, endDate })}
+ />
+
+
);
};
diff --git a/test/domains/DomainSelector.test.tsx b/test/domains/DomainSelector.test.tsx
index 0c1ab22c..ec9ae9d6 100644
--- a/test/domains/DomainSelector.test.tsx
+++ b/test/domains/DomainSelector.test.tsx
@@ -1,9 +1,10 @@
import { shallow, ShallowWrapper } from 'enzyme';
import { Mock } from 'ts-mockery';
-import { DropdownItem, DropdownMenu, InputGroup } from 'reactstrap';
+import { DropdownItem, InputGroup } from 'reactstrap';
import { DomainSelector } from '../../src/domains/DomainSelector';
import { DomainsList } from '../../src/domains/reducers/domainsList';
import { ShlinkDomain } from '../../src/api/types';
+import { DropdownBtn } from '../../src/utils/DropdownBtn';
describe('', () => {
let wrapper: ShallowWrapper;
@@ -23,7 +24,7 @@ describe('', () => {
it('shows dropdown by default', () => {
const input = wrapper.find(InputGroup);
- const dropdown = wrapper.find(DropdownMenu);
+ const dropdown = wrapper.find(DropdownBtn);
expect(input).toHaveLength(0);
expect(dropdown).toHaveLength(1);
@@ -33,10 +34,10 @@ describe('', () => {
it('allows to toggle between dropdown and input', () => {
wrapper.find(DropdownItem).last().simulate('click');
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');
expect(wrapper.find(InputGroup)).toHaveLength(0);
- expect(wrapper.find(DropdownMenu)).toHaveLength(1);
+ expect(wrapper.find(DropdownBtn)).toHaveLength(1);
});
});
diff --git a/test/short-urls/ShortUrls.test.tsx b/test/short-urls/ShortUrls.test.tsx
index c47ba0ce..f4caed88 100644
--- a/test/short-urls/ShortUrls.test.tsx
+++ b/test/short-urls/ShortUrls.test.tsx
@@ -1,7 +1,6 @@
import { shallow, ShallowWrapper } from 'enzyme';
import { Mock } from 'ts-mockery';
import shortUrlsCreator from '../../src/short-urls/ShortUrls';
-import Paginator from '../../src/short-urls/Paginator';
import { ShortUrlsListProps } from '../../src/short-urls/ShortUrlsList';
describe('', () => {
@@ -18,9 +17,8 @@ describe('', () => {
});
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(ShortUrlsList)).toHaveLength(1);
- expect(wrapper.find(Paginator)).toHaveLength(1);
});
});
diff --git a/test/utils/DropdownBtn.test.tsx b/test/utils/DropdownBtn.test.tsx
new file mode 100644
index 00000000..79722689
--- /dev/null
+++ b/test/utils/DropdownBtn.test.tsx
@@ -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('', () => {
+ let wrapper: ShallowWrapper;
+ const createWrapper = (props: PropsWithChildren) => {
+ wrapper = shallow();
+
+ 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);
+ });
+});
diff --git a/test/utils/SortingDropdown.test.tsx b/test/utils/SortingDropdown.test.tsx
index c327a172..6b3f9682 100644
--- a/test/utils/SortingDropdown.test.tsx
+++ b/test/utils/SortingDropdown.test.tsx
@@ -1,9 +1,10 @@
import { shallow, ShallowWrapper } from 'enzyme';
-import { DropdownItem } from 'reactstrap';
+import { DropdownItem, DropdownToggle } from 'reactstrap';
import { identity, values } from 'ramda';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faSortAmountDown as caretDownIcon } from '@fortawesome/free-solid-svg-icons';
import SortingDropdown, { SortingDropdownProps } from '../../src/utils/SortingDropdown';
+import { OrderDir } from '../../src/utils/utils';
describe('', () => {
let wrapper: ShallowWrapper;
@@ -73,4 +74,23 @@ describe('', () => {
expect(onChange).toHaveBeenCalledTimes(1);
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);
+ });
});