Finished migrating all remaining utils to TS

This commit is contained in:
Alejandro Celaya 2020-08-31 18:38:27 +02:00
parent f8ea1ae3d5
commit 16d96efa4a
11 changed files with 95 additions and 105 deletions

View file

@ -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')}
/>

View file

@ -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));

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;
}

View file

@ -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';
}

View file

@ -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();

View file

@ -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();