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; 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>) => ( const SearchBar = (colorGenerator: ColorGenerator, ForServerVersion: FC<Versions>) => (
{ listShortUrls, shortUrlsListParams }: SearchBarProps, { listShortUrls, shortUrlsListParams }: SearchBarProps,
@ -41,8 +41,8 @@ const SearchBar = (colorGenerator: ColorGenerator, ForServerVersion: FC<Versions
<div className="row"> <div className="row">
<div className="col-lg-8 offset-lg-4 col-xl-6 offset-xl-6"> <div className="col-lg-8 offset-lg-4 col-xl-6 offset-xl-6">
<DateRangeRow <DateRangeRow
startDate={dateOrUndefined(shortUrlsListParams.startDate)} startDate={dateOrNull(shortUrlsListParams.startDate)}
endDate={dateOrUndefined(shortUrlsListParams.endDate)} endDate={dateOrNull(shortUrlsListParams.endDate)}
onStartDateChange={setDate('startDate')} onStartDateChange={setDate('startDate')}
onEndDateChange={setDate('endDate')} onEndDateChange={setDate('endDate')}
/> />

View file

@ -62,9 +62,9 @@ const ShortUrlsList = (ShortUrlsRow: FC<ShortUrlsRowProps>) => ({
orderDir: orderBy && head(values(orderBy)), orderDir: orderBy && head(values(orderBy)),
}); });
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 });
refreshList({ orderBy: { [orderField]: orderDir } }); refreshList({ orderBy: orderField ? { [orderField]: orderDir } : undefined });
}; };
const orderByColumn = (field: OrderableFields) => () => const orderByColumn = (field: OrderableFields) => () =>
handleOrderBy(field, determineOrderDir(field, order.orderField, order.orderDir)); handleOrderBy(field, determineOrderDir(field, order.orderField, order.orderDir));

View file

@ -1,25 +1,26 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import moment from 'moment';
import DateInput from './DateInput'; import DateInput from './DateInput';
import './DateRangeRow.scss'; import './DateRangeRow.scss';
const dateType = PropTypes.oneOfType([ PropTypes.string, PropTypes.object ]); interface DateRangeRowProps {
const propTypes = { startDate?: moment.Moment | null;
startDate: dateType, endDate?: moment.Moment | null;
endDate: dateType, onStartDateChange: (date: moment.Moment | null) => void;
onStartDateChange: PropTypes.func.isRequired, onEndDateChange: (date: moment.Moment | null) => void;
onEndDateChange: PropTypes.func.isRequired, disabled?: boolean;
disabled: PropTypes.bool, }
};
const DateRangeRow = ({ startDate, endDate, onStartDateChange, onEndDateChange, disabled = false }) => ( const DateRangeRow = (
{ startDate = null, endDate = null, disabled = false, onStartDateChange, onEndDateChange }: DateRangeRowProps,
) => (
<div className="row"> <div className="row">
<div className="col-md-6"> <div className="col-md-6">
<DateInput <DateInput
selected={startDate} selected={startDate}
placeholderText="Since" placeholderText="Since"
isClearable isClearable
maxDate={endDate} maxDate={endDate ?? undefined}
disabled={disabled} disabled={disabled}
onChange={onStartDateChange} onChange={onStartDateChange}
/> />
@ -30,7 +31,7 @@ const DateRangeRow = ({ startDate, endDate, onStartDateChange, onEndDateChange,
selected={endDate} selected={endDate}
placeholderText="Until" placeholderText="Until"
isClearable isClearable
minDate={startDate} minDate={startDate ?? undefined}
disabled={disabled} disabled={disabled}
onChange={onEndDateChange} onChange={onEndDateChange}
/> />
@ -38,6 +39,4 @@ const DateRangeRow = ({ startDate, endDate, onStartDateChange, onEndDateChange,
</div> </div>
); );
DateRangeRow.propTypes = propTypes;
export default DateRangeRow; 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 { v4 as uuid } from 'uuid';
import PropTypes from 'prop-types'; import { InputType } from 'reactstrap/lib/Input';
const propTypes = { interface HorizontalFormGroupProps {
children: PropTypes.node.isRequired, value: string;
value: PropTypes.string.isRequired, onChange: (newValue: string) => void;
onChange: PropTypes.func.isRequired, id?: string;
id: PropTypes.string, type?: InputType;
type: PropTypes.string, required?: boolean;
required: PropTypes.bool, }
};
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"> <div className="form-group row">
<label htmlFor={id} className="col-lg-1 col-md-2 col-form-label create-server__label"> <label htmlFor={id} className="col-lg-1 col-md-2 col-form-label create-server__label">
{children}: {children}:
@ -28,5 +29,3 @@ export const HorizontalFormGroup = ({ children, value, onChange, id = uuid(), ty
</div> </div>
</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 { Card } from 'reactstrap';
import classNames from 'classnames'; import classNames from 'classnames';
import PropTypes from 'prop-types';
import { faCircleNotch as preloader } from '@fortawesome/free-solid-svg-icons'; import { faCircleNotch as preloader } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
const getClassForType = (type) => { type MessageType = 'default' | 'error';
const map = {
const getClassForType = (type: MessageType) => {
const map: Record<MessageType, string> = {
error: 'border-danger', error: 'border-danger',
default: '',
}; };
return map[type] || ''; return map[type];
}; };
const getTextClassForType = (type) => { const getTextClassForType = (type: MessageType) => {
const map = { const map: Record<MessageType, string> = {
error: 'text-danger', error: 'text-danger',
default: 'text-muted',
}; };
return map[type] || 'text-muted'; return map[type];
}; };
const propTypes = { interface MessageProps {
noMargin: PropTypes.bool, noMargin?: boolean;
loading: PropTypes.bool, loading?: boolean;
children: PropTypes.node, type?: MessageType;
type: PropTypes.oneOf([ 'default', 'error' ]), }
};
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 }); const cardClasses = classNames('bg-light', getClassForType(type), { 'mt-4': !noMargin });
return ( return (
@ -35,7 +37,7 @@ const Message = ({ children, loading = false, noMargin = false, type = 'default'
<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 />}
{loading && <span className="ml-2">{children || 'Loading...'}</span>} {loading && <span className="ml-2">{children ?? 'Loading...'}</span>}
{!loading && children} {!loading && children}
</h3> </h3>
</Card> </Card>
@ -43,6 +45,4 @@ const Message = ({ children, loading = false, noMargin = false, type = 'default'
); );
}; };
Message.propTypes = propTypes;
export default Message; export default Message;

View file

@ -1,15 +1,14 @@
import React from 'react'; import React from 'react';
import { DropdownItem, DropdownMenu, DropdownToggle, UncontrolledDropdown } from 'reactstrap'; import { DropdownItem, DropdownMenu, DropdownToggle, UncontrolledDropdown } from 'reactstrap';
import * as PropTypes from 'prop-types';
const propTypes = { interface PaginationDropdownProps {
toggleClassName: PropTypes.string, ranges: number[];
ranges: PropTypes.arrayOf(PropTypes.number).isRequired, value: number;
value: PropTypes.number.isRequired, setValue: (newValue: number) => void;
setValue: PropTypes.func.isRequired, toggleClassName?: string;
}; }
const PaginationDropdown = ({ toggleClassName, ranges, value, setValue }) => ( const PaginationDropdown = ({ toggleClassName, ranges, value, setValue }: PaginationDropdownProps) => (
<UncontrolledDropdown> <UncontrolledDropdown>
<DropdownToggle caret color="link" className={toggleClassName}> <DropdownToggle caret color="link" className={toggleClassName}>
Paginate Paginate
@ -28,6 +27,4 @@ const PaginationDropdown = ({ toggleClassName, ranges, value, setValue }) => (
</UncontrolledDropdown> </UncontrolledDropdown>
); );
PaginationDropdown.propTypes = propTypes;
export default PaginationDropdown; export default PaginationDropdown;

View file

@ -1,29 +1,30 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faSearch as searchIcon } from '@fortawesome/free-solid-svg-icons'; import { faSearch as searchIcon } from '@fortawesome/free-solid-svg-icons';
import PropTypes from 'prop-types';
import classNames from 'classnames'; import classNames from 'classnames';
import './SearchField.scss'; import './SearchField.scss';
const DEFAULT_SEARCH_INTERVAL = 500; const DEFAULT_SEARCH_INTERVAL = 500;
let timer; let timer: NodeJS.Timeout | null;
const propTypes = { interface SearchField {
onChange: PropTypes.func.isRequired, onChange: (value: string) => void;
className: PropTypes.string, className?: string;
placeholder: PropTypes.string, placeholder?: string;
large: PropTypes.bool, large?: boolean;
noBorder: PropTypes.bool, 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 [ searchTerm, setSearchTerm ] = useState('');
const resetTimer = () => { const resetTimer = () => {
clearTimeout(timer); timer && clearTimeout(timer);
timer = null; timer = null;
}; };
const searchTermChanged = (newSearchTerm, timeout = DEFAULT_SEARCH_INTERVAL) => { const searchTermChanged = (newSearchTerm: string, timeout = DEFAULT_SEARCH_INTERVAL) => {
setSearchTerm(newSearchTerm); setSearchTerm(newSearchTerm);
resetTimer(); resetTimer();
@ -59,6 +60,4 @@ const SearchField = ({ onChange, className, placeholder = 'Search...', large = t
); );
}; };
SearchField.propTypes = propTypes;
export default SearchField; export default SearchField;

View file

@ -1,28 +1,25 @@
import React from 'react'; import React from 'react';
import { UncontrolledDropdown, DropdownToggle, DropdownMenu, DropdownItem } from 'reactstrap'; import { UncontrolledDropdown, DropdownToggle, DropdownMenu, DropdownItem } from 'reactstrap';
import { toPairs } from 'ramda'; import { toPairs } from 'ramda';
import PropTypes from 'prop-types';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faSortAmountUp as sortAscIcon, faSortAmountDown as sortDescIcon } from '@fortawesome/free-solid-svg-icons'; import { faSortAmountUp as sortAscIcon, faSortAmountDown as sortDescIcon } from '@fortawesome/free-solid-svg-icons';
import classNames from 'classnames'; import classNames from 'classnames';
import { determineOrderDir } from './utils'; import { determineOrderDir, OrderDir } from './utils';
import './SortingDropdown.scss'; import './SortingDropdown.scss';
const propTypes = { export interface SortingDropdownProps<T extends string = string> {
items: PropTypes.object.isRequired, items: Record<T, string>;
orderField: PropTypes.string, orderField?: T;
orderDir: PropTypes.oneOf([ 'ASC', 'DESC' ]), orderDir?: OrderDir;
onChange: PropTypes.func.isRequired, onChange: (orderField?: T, orderDir?: OrderDir) => void;
isButton: PropTypes.bool, isButton?: boolean;
right: PropTypes.bool, right?: boolean;
}; }
const defaultProps = {
isButton: true,
right: false,
};
const SortingDropdown = ({ items, orderField, orderDir, onChange, isButton, right }) => { export default function SortingDropdown<T extends string = string>(
const handleItemClick = (fieldKey) => () => { { items, orderField, orderDir, onChange, isButton = true, right = false }: SortingDropdownProps<T>,
) {
const handleItemClick = (fieldKey: T) => () => {
const newOrderDir = determineOrderDir(fieldKey, orderField, orderDir); const newOrderDir = determineOrderDir(fieldKey, orderField, orderDir);
onChange(newOrderDir ? fieldKey : undefined, newOrderDir); 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 })} className={classNames('sorting-dropdown__menu', { 'sorting-dropdown__menu--link': !isButton })}
> >
{toPairs(items).map(([ fieldKey, fieldValue ]) => ( {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} {fieldValue}
{orderField === fieldKey && ( {orderField === fieldKey && (
<FontAwesomeIcon <FontAwesomeIcon
@ -59,9 +56,4 @@ const SortingDropdown = ({ items, orderField, orderDir, onChange, isButton, righ
</DropdownMenu> </DropdownMenu>
</UncontrolledDropdown> </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 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) { if (currentField !== newField) {
return 'ASC'; return 'ASC';
} }

View file

@ -1,10 +1,10 @@
import React from 'react'; import React from 'react';
import { shallow } from 'enzyme'; import { shallow, ShallowWrapper } from 'enzyme';
import DateRangeRow from '../../src/utils/DateRangeRow'; import DateRangeRow from '../../src/utils/DateRangeRow';
import DateInput from '../../src/utils/DateInput'; import DateInput from '../../src/utils/DateInput';
describe('<DateRangeRow />', () => { describe('<DateRangeRow />', () => {
let wrapper; let wrapper: ShallowWrapper;
const onEndDateChange = jest.fn(); const onEndDateChange = jest.fn();
const onStartDateChange = jest.fn(); const onStartDateChange = jest.fn();

View file

@ -1,25 +1,25 @@
import React from 'react'; import React from 'react';
import { shallow } from 'enzyme'; import { shallow, ShallowWrapper } from 'enzyme';
import { DropdownItem } from 'reactstrap'; import { DropdownItem } 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 from '../../src/utils/SortingDropdown'; import SortingDropdown, { SortingDropdownProps } from '../../src/utils/SortingDropdown';
describe('<SortingDropdown />', () => { describe('<SortingDropdown />', () => {
let wrapper; let wrapper: ShallowWrapper;
const items = { const items = {
foo: 'Foo', foo: 'Foo',
bar: 'Bar', bar: 'Bar',
baz: 'Hello World', baz: 'Hello World',
}; };
const createWrapper = (props) => { const createWrapper = (props: Partial<SortingDropdownProps> = {}) => {
wrapper = shallow(<SortingDropdown items={items} onChange={identity} {...props} />); wrapper = shallow(<SortingDropdown items={items} onChange={identity} {...props} />);
return wrapper; return wrapper;
}; };
afterEach(() => wrapper && wrapper.unmount()); afterEach(() => wrapper?.unmount());
it('properly renders provided list of items', () => { it('properly renders provided list of items', () => {
const wrapper = createWrapper(); const wrapper = createWrapper();