mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2024-12-23 01:20:24 +03:00
Finished migrating all remaining utils to TS
This commit is contained in:
parent
f8ea1ae3d5
commit
16d96efa4a
11 changed files with 95 additions and 105 deletions
|
@ -17,7 +17,7 @@ interface SearchBarProps {
|
|||
shortUrlsListParams: ShortUrlsListParams;
|
||||
}
|
||||
|
||||
const dateOrUndefined = (date?: string) => date ? moment(date) : undefined;
|
||||
const dateOrNull = (date?: string) => date ? moment(date) : null;
|
||||
|
||||
const SearchBar = (colorGenerator: ColorGenerator, ForServerVersion: FC<Versions>) => (
|
||||
{ listShortUrls, shortUrlsListParams }: SearchBarProps,
|
||||
|
@ -41,8 +41,8 @@ const SearchBar = (colorGenerator: ColorGenerator, ForServerVersion: FC<Versions
|
|||
<div className="row">
|
||||
<div className="col-lg-8 offset-lg-4 col-xl-6 offset-xl-6">
|
||||
<DateRangeRow
|
||||
startDate={dateOrUndefined(shortUrlsListParams.startDate)}
|
||||
endDate={dateOrUndefined(shortUrlsListParams.endDate)}
|
||||
startDate={dateOrNull(shortUrlsListParams.startDate)}
|
||||
endDate={dateOrNull(shortUrlsListParams.endDate)}
|
||||
onStartDateChange={setDate('startDate')}
|
||||
onEndDateChange={setDate('endDate')}
|
||||
/>
|
||||
|
|
|
@ -62,9 +62,9 @@ const ShortUrlsList = (ShortUrlsRow: FC<ShortUrlsRowProps>) => ({
|
|||
orderDir: orderBy && head(values(orderBy)),
|
||||
});
|
||||
const refreshList = (extraParams: ShortUrlsListParams) => listShortUrls({ ...shortUrlsListParams, ...extraParams });
|
||||
const handleOrderBy = (orderField: OrderableFields, orderDir: OrderDir) => {
|
||||
const handleOrderBy = (orderField?: OrderableFields, orderDir?: OrderDir) => {
|
||||
setOrder({ orderField, orderDir });
|
||||
refreshList({ orderBy: { [orderField]: orderDir } });
|
||||
refreshList({ orderBy: orderField ? { [orderField]: orderDir } : undefined });
|
||||
};
|
||||
const orderByColumn = (field: OrderableFields) => () =>
|
||||
handleOrderBy(field, determineOrderDir(field, order.orderField, order.orderDir));
|
||||
|
|
|
@ -1,25 +1,26 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import moment from 'moment';
|
||||
import DateInput from './DateInput';
|
||||
import './DateRangeRow.scss';
|
||||
|
||||
const dateType = PropTypes.oneOfType([ PropTypes.string, PropTypes.object ]);
|
||||
const propTypes = {
|
||||
startDate: dateType,
|
||||
endDate: dateType,
|
||||
onStartDateChange: PropTypes.func.isRequired,
|
||||
onEndDateChange: PropTypes.func.isRequired,
|
||||
disabled: PropTypes.bool,
|
||||
};
|
||||
interface DateRangeRowProps {
|
||||
startDate?: moment.Moment | null;
|
||||
endDate?: moment.Moment | null;
|
||||
onStartDateChange: (date: moment.Moment | null) => void;
|
||||
onEndDateChange: (date: moment.Moment | null) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const DateRangeRow = ({ startDate, endDate, onStartDateChange, onEndDateChange, disabled = false }) => (
|
||||
const DateRangeRow = (
|
||||
{ startDate = null, endDate = null, disabled = false, onStartDateChange, onEndDateChange }: DateRangeRowProps,
|
||||
) => (
|
||||
<div className="row">
|
||||
<div className="col-md-6">
|
||||
<DateInput
|
||||
selected={startDate}
|
||||
placeholderText="Since"
|
||||
isClearable
|
||||
maxDate={endDate}
|
||||
maxDate={endDate ?? undefined}
|
||||
disabled={disabled}
|
||||
onChange={onStartDateChange}
|
||||
/>
|
||||
|
@ -30,7 +31,7 @@ const DateRangeRow = ({ startDate, endDate, onStartDateChange, onEndDateChange,
|
|||
selected={endDate}
|
||||
placeholderText="Until"
|
||||
isClearable
|
||||
minDate={startDate}
|
||||
minDate={startDate ?? undefined}
|
||||
disabled={disabled}
|
||||
onChange={onEndDateChange}
|
||||
/>
|
||||
|
@ -38,6 +39,4 @@ const DateRangeRow = ({ startDate, endDate, onStartDateChange, onEndDateChange,
|
|||
</div>
|
||||
);
|
||||
|
||||
DateRangeRow.propTypes = propTypes;
|
||||
|
||||
export default DateRangeRow;
|
|
@ -1,17 +1,18 @@
|
|||
import React from 'react';
|
||||
import React, { FC } from 'react';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import PropTypes from 'prop-types';
|
||||
import { InputType } from 'reactstrap/lib/Input';
|
||||
|
||||
const propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
value: PropTypes.string.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
id: PropTypes.string,
|
||||
type: PropTypes.string,
|
||||
required: PropTypes.bool,
|
||||
};
|
||||
interface HorizontalFormGroupProps {
|
||||
value: string;
|
||||
onChange: (newValue: string) => void;
|
||||
id?: string;
|
||||
type?: InputType;
|
||||
required?: boolean;
|
||||
}
|
||||
|
||||
export const HorizontalFormGroup = ({ children, value, onChange, id = uuid(), type = 'text', required = true }) => (
|
||||
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}:
|
||||
|
@ -28,5 +29,3 @@ export const HorizontalFormGroup = ({ children, value, onChange, id = uuid(), ty
|
|||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
HorizontalFormGroup.propTypes = propTypes;
|
|
@ -1,33 +1,35 @@
|
|||
import React from 'react';
|
||||
import React, { FC } from 'react';
|
||||
import { Card } from 'reactstrap';
|
||||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import { faCircleNotch as preloader } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
|
||||
const getClassForType = (type) => {
|
||||
const map = {
|
||||
type MessageType = 'default' | 'error';
|
||||
|
||||
const getClassForType = (type: MessageType) => {
|
||||
const map: Record<MessageType, string> = {
|
||||
error: 'border-danger',
|
||||
default: '',
|
||||
};
|
||||
|
||||
return map[type] || '';
|
||||
return map[type];
|
||||
};
|
||||
const getTextClassForType = (type) => {
|
||||
const map = {
|
||||
const getTextClassForType = (type: MessageType) => {
|
||||
const map: Record<MessageType, string> = {
|
||||
error: 'text-danger',
|
||||
default: 'text-muted',
|
||||
};
|
||||
|
||||
return map[type] || 'text-muted';
|
||||
return map[type];
|
||||
};
|
||||
|
||||
const propTypes = {
|
||||
noMargin: PropTypes.bool,
|
||||
loading: PropTypes.bool,
|
||||
children: PropTypes.node,
|
||||
type: PropTypes.oneOf([ 'default', 'error' ]),
|
||||
};
|
||||
interface MessageProps {
|
||||
noMargin?: boolean;
|
||||
loading?: boolean;
|
||||
type?: MessageType;
|
||||
}
|
||||
|
||||
const Message = ({ children, loading = false, noMargin = false, type = 'default' }) => {
|
||||
const Message: FC<MessageProps> = ({ children, loading = false, noMargin = false, type = 'default' }) => {
|
||||
const cardClasses = classNames('bg-light', getClassForType(type), { 'mt-4': !noMargin });
|
||||
|
||||
return (
|
||||
|
@ -35,7 +37,7 @@ const Message = ({ children, loading = false, noMargin = false, type = 'default'
|
|||
<Card className={cardClasses} body>
|
||||
<h3 className={classNames('text-center mb-0', getTextClassForType(type))}>
|
||||
{loading && <FontAwesomeIcon icon={preloader} spin />}
|
||||
{loading && <span className="ml-2">{children || 'Loading...'}</span>}
|
||||
{loading && <span className="ml-2">{children ?? 'Loading...'}</span>}
|
||||
{!loading && children}
|
||||
</h3>
|
||||
</Card>
|
||||
|
@ -43,6 +45,4 @@ const Message = ({ children, loading = false, noMargin = false, type = 'default'
|
|||
);
|
||||
};
|
||||
|
||||
Message.propTypes = propTypes;
|
||||
|
||||
export default Message;
|
|
@ -1,15 +1,14 @@
|
|||
import React from 'react';
|
||||
import { DropdownItem, DropdownMenu, DropdownToggle, UncontrolledDropdown } from 'reactstrap';
|
||||
import * as PropTypes from 'prop-types';
|
||||
|
||||
const propTypes = {
|
||||
toggleClassName: PropTypes.string,
|
||||
ranges: PropTypes.arrayOf(PropTypes.number).isRequired,
|
||||
value: PropTypes.number.isRequired,
|
||||
setValue: PropTypes.func.isRequired,
|
||||
};
|
||||
interface PaginationDropdownProps {
|
||||
ranges: number[];
|
||||
value: number;
|
||||
setValue: (newValue: number) => void;
|
||||
toggleClassName?: string;
|
||||
}
|
||||
|
||||
const PaginationDropdown = ({ toggleClassName, ranges, value, setValue }) => (
|
||||
const PaginationDropdown = ({ toggleClassName, ranges, value, setValue }: PaginationDropdownProps) => (
|
||||
<UncontrolledDropdown>
|
||||
<DropdownToggle caret color="link" className={toggleClassName}>
|
||||
Paginate
|
||||
|
@ -28,6 +27,4 @@ const PaginationDropdown = ({ toggleClassName, ranges, value, setValue }) => (
|
|||
</UncontrolledDropdown>
|
||||
);
|
||||
|
||||
PaginationDropdown.propTypes = propTypes;
|
||||
|
||||
export default PaginationDropdown;
|
|
@ -1,29 +1,30 @@
|
|||
import React, { useState } from 'react';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faSearch as searchIcon } from '@fortawesome/free-solid-svg-icons';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import './SearchField.scss';
|
||||
|
||||
const DEFAULT_SEARCH_INTERVAL = 500;
|
||||
let timer;
|
||||
let timer: NodeJS.Timeout | null;
|
||||
|
||||
const propTypes = {
|
||||
onChange: PropTypes.func.isRequired,
|
||||
className: PropTypes.string,
|
||||
placeholder: PropTypes.string,
|
||||
large: PropTypes.bool,
|
||||
noBorder: PropTypes.bool,
|
||||
};
|
||||
interface SearchField {
|
||||
onChange: (value: string) => void;
|
||||
className?: string;
|
||||
placeholder?: string;
|
||||
large?: boolean;
|
||||
noBorder?: boolean;
|
||||
}
|
||||
|
||||
const SearchField = ({ onChange, className, placeholder = 'Search...', large = true, noBorder = false }) => {
|
||||
const SearchField = (
|
||||
{ onChange, className, placeholder = 'Search...', large = true, noBorder = false }: SearchField,
|
||||
) => {
|
||||
const [ searchTerm, setSearchTerm ] = useState('');
|
||||
|
||||
const resetTimer = () => {
|
||||
clearTimeout(timer);
|
||||
timer && clearTimeout(timer);
|
||||
timer = null;
|
||||
};
|
||||
const searchTermChanged = (newSearchTerm, timeout = DEFAULT_SEARCH_INTERVAL) => {
|
||||
const searchTermChanged = (newSearchTerm: string, timeout = DEFAULT_SEARCH_INTERVAL) => {
|
||||
setSearchTerm(newSearchTerm);
|
||||
|
||||
resetTimer();
|
||||
|
@ -59,6 +60,4 @@ const SearchField = ({ onChange, className, placeholder = 'Search...', large = t
|
|||
);
|
||||
};
|
||||
|
||||
SearchField.propTypes = propTypes;
|
||||
|
||||
export default SearchField;
|
|
@ -1,28 +1,25 @@
|
|||
import React from 'react';
|
||||
import { UncontrolledDropdown, DropdownToggle, DropdownMenu, DropdownItem } from 'reactstrap';
|
||||
import { toPairs } from 'ramda';
|
||||
import PropTypes from 'prop-types';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faSortAmountUp as sortAscIcon, faSortAmountDown as sortDescIcon } from '@fortawesome/free-solid-svg-icons';
|
||||
import classNames from 'classnames';
|
||||
import { determineOrderDir } from './utils';
|
||||
import { determineOrderDir, OrderDir } from './utils';
|
||||
import './SortingDropdown.scss';
|
||||
|
||||
const propTypes = {
|
||||
items: PropTypes.object.isRequired,
|
||||
orderField: PropTypes.string,
|
||||
orderDir: PropTypes.oneOf([ 'ASC', 'DESC' ]),
|
||||
onChange: PropTypes.func.isRequired,
|
||||
isButton: PropTypes.bool,
|
||||
right: PropTypes.bool,
|
||||
};
|
||||
const defaultProps = {
|
||||
isButton: true,
|
||||
right: false,
|
||||
};
|
||||
export interface SortingDropdownProps<T extends string = string> {
|
||||
items: Record<T, string>;
|
||||
orderField?: T;
|
||||
orderDir?: OrderDir;
|
||||
onChange: (orderField?: T, orderDir?: OrderDir) => void;
|
||||
isButton?: boolean;
|
||||
right?: boolean;
|
||||
}
|
||||
|
||||
const SortingDropdown = ({ items, orderField, orderDir, onChange, isButton, right }) => {
|
||||
const handleItemClick = (fieldKey) => () => {
|
||||
export default function SortingDropdown<T extends string = string>(
|
||||
{ items, orderField, orderDir, onChange, isButton = true, right = false }: SortingDropdownProps<T>,
|
||||
) {
|
||||
const handleItemClick = (fieldKey: T) => () => {
|
||||
const newOrderDir = determineOrderDir(fieldKey, orderField, orderDir);
|
||||
|
||||
onChange(newOrderDir ? fieldKey : undefined, newOrderDir);
|
||||
|
@ -42,7 +39,7 @@ const SortingDropdown = ({ items, orderField, orderDir, onChange, isButton, righ
|
|||
className={classNames('sorting-dropdown__menu', { 'sorting-dropdown__menu--link': !isButton })}
|
||||
>
|
||||
{toPairs(items).map(([ fieldKey, fieldValue ]) => (
|
||||
<DropdownItem key={fieldKey} active={orderField === fieldKey} onClick={handleItemClick(fieldKey)}>
|
||||
<DropdownItem key={fieldKey} active={orderField === fieldKey} onClick={handleItemClick(fieldKey as T)}>
|
||||
{fieldValue}
|
||||
{orderField === fieldKey && (
|
||||
<FontAwesomeIcon
|
||||
|
@ -59,9 +56,4 @@ const SortingDropdown = ({ items, orderField, orderDir, onChange, isButton, righ
|
|||
</DropdownMenu>
|
||||
</UncontrolledDropdown>
|
||||
);
|
||||
};
|
||||
|
||||
SortingDropdown.propTypes = propTypes;
|
||||
SortingDropdown.defaultProps = defaultProps;
|
||||
|
||||
export default SortingDropdown;
|
||||
}
|
|
@ -3,7 +3,11 @@ import { SyntheticEvent } from 'react';
|
|||
|
||||
export type OrderDir = 'ASC' | 'DESC' | undefined;
|
||||
|
||||
export const determineOrderDir = (currentField: string, newField?: string, currentOrderDir?: OrderDir): OrderDir => {
|
||||
export const determineOrderDir = <T extends string = string>(
|
||||
currentField: T,
|
||||
newField?: T,
|
||||
currentOrderDir?: OrderDir,
|
||||
): OrderDir => {
|
||||
if (currentField !== newField) {
|
||||
return 'ASC';
|
||||
}
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { shallow, ShallowWrapper } from 'enzyme';
|
||||
import DateRangeRow from '../../src/utils/DateRangeRow';
|
||||
import DateInput from '../../src/utils/DateInput';
|
||||
|
||||
describe('<DateRangeRow />', () => {
|
||||
let wrapper;
|
||||
let wrapper: ShallowWrapper;
|
||||
const onEndDateChange = jest.fn();
|
||||
const onStartDateChange = jest.fn();
|
||||
|
|
@ -1,25 +1,25 @@
|
|||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { shallow, ShallowWrapper } from 'enzyme';
|
||||
import { DropdownItem } 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 from '../../src/utils/SortingDropdown';
|
||||
import SortingDropdown, { SortingDropdownProps } from '../../src/utils/SortingDropdown';
|
||||
|
||||
describe('<SortingDropdown />', () => {
|
||||
let wrapper;
|
||||
let wrapper: ShallowWrapper;
|
||||
const items = {
|
||||
foo: 'Foo',
|
||||
bar: 'Bar',
|
||||
baz: 'Hello World',
|
||||
};
|
||||
const createWrapper = (props) => {
|
||||
const createWrapper = (props: Partial<SortingDropdownProps> = {}) => {
|
||||
wrapper = shallow(<SortingDropdown items={items} onChange={identity} {...props} />);
|
||||
|
||||
return wrapper;
|
||||
};
|
||||
|
||||
afterEach(() => wrapper && wrapper.unmount());
|
||||
afterEach(() => wrapper?.unmount());
|
||||
|
||||
it('properly renders provided list of items', () => {
|
||||
const wrapper = createWrapper();
|
Loading…
Reference in a new issue