Merge pull request #445 from acelaya-forks/feature/moment-js-migration

Feature/moment js migration
This commit is contained in:
Alejandro Celaya 2021-06-25 20:09:12 +02:00 committed by GitHub
commit 741bc21a55
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 202 additions and 164 deletions

View file

@ -18,7 +18,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
* [#430](https://github.com/shlinkio/shlink-web-client/pull/430) Added support to set new and existing short URLs as crawlable, when consuming Shlink >=2.7. * [#430](https://github.com/shlinkio/shlink-web-client/pull/430) Added support to set new and existing short URLs as crawlable, when consuming Shlink >=2.7.
### Changed ### Changed
* *Nothing* * [#337](https://github.com/shlinkio/shlink-web-client/pull/337) Replaced moment.js with date-fns.
### Deprecated ### Deprecated
* *Nothing* * *Nothing*

20
package-lock.json generated
View file

@ -6517,15 +6517,6 @@
"integrity": "sha512-fZQQafSREFyuZcdWFAExYjBiCL7AUCdgsk80iO0q4yihYYdcIiH28CcuPTGFgLOCC8RlW49GSQxdHwZP+I7CNg==", "integrity": "sha512-fZQQafSREFyuZcdWFAExYjBiCL7AUCdgsk80iO0q4yihYYdcIiH28CcuPTGFgLOCC8RlW49GSQxdHwZP+I7CNg==",
"dev": true "dev": true
}, },
"@types/moment": {
"version": "2.13.0",
"resolved": "https://registry.npmjs.org/@types/moment/-/moment-2.13.0.tgz",
"integrity": "sha1-YE69GJvDvDShVIaJQE5hoqSqyJY=",
"dev": true,
"requires": {
"moment": "*"
}
},
"@types/node": { "@types/node": {
"version": "12.7.11", "version": "12.7.11",
"resolved": "https://registry.npmjs.org/@types/node/-/node-12.7.11.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-12.7.11.tgz",
@ -12082,9 +12073,9 @@
} }
}, },
"date-fns": { "date-fns": {
"version": "2.16.1", "version": "2.22.1",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.16.1.tgz", "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.22.1.tgz",
"integrity": "sha512-sAJVKx/FqrLYHAQeN7VpJrPhagZc9R4ImZIWYRFZaaohR3KzmuK88touwsSwSVT8Qcbd4zoDsnGfX4GFB4imyQ==" "integrity": "sha512-yUFPQjrxEmIsMqlHhAhmxkuH769baF21Kk+nZwZGyrMoyLA+LugaQtC0+Tqf9CBUUULWwUJt6Q5ySI3LJDDCGg=="
}, },
"date-format": { "date-format": {
"version": "3.0.0", "version": "3.0.0",
@ -24903,11 +24894,6 @@
"resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz",
"integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA=="
}, },
"react-moment": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/react-moment/-/react-moment-1.0.0.tgz",
"integrity": "sha512-J4iIiwUT4oZcL7cp2U7naQKbQtqvmzGXXBMg/DLj+Pi7n9EW0VhBRx/1aJ1Tp2poCqTCAPoadLEoUIkReGnNNg=="
},
"react-onclickoutside": { "react-onclickoutside": {
"version": "6.10.0", "version": "6.10.0",
"resolved": "https://registry.npmjs.org/react-onclickoutside/-/react-onclickoutside-6.10.0.tgz", "resolved": "https://registry.npmjs.org/react-onclickoutside/-/react-onclickoutside-6.10.0.tgz",

View file

@ -33,9 +33,9 @@
"classnames": "^2.2.6", "classnames": "^2.2.6",
"compare-versions": "^3.6.0", "compare-versions": "^3.6.0",
"csvjson": "^5.1.0", "csvjson": "^5.1.0",
"date-fns": "^2.22.1",
"event-source-polyfill": "^1.0.22", "event-source-polyfill": "^1.0.22",
"leaflet": "^1.7.1", "leaflet": "^1.7.1",
"moment": "^2.29.1",
"promise": "^8.1.0", "promise": "^8.1.0",
"qs": "^6.9.6", "qs": "^6.9.6",
"ramda": "^0.27.1", "ramda": "^0.27.1",
@ -48,7 +48,6 @@
"react-dom": "^17.0.1", "react-dom": "^17.0.1",
"react-external-link": "^1.2.0", "react-external-link": "^1.2.0",
"react-leaflet": "^3.1.0", "react-leaflet": "^3.1.0",
"react-moment": "^1.0.0",
"react-redux": "^7.2.2", "react-redux": "^7.2.2",
"react-router-dom": "^5.2.0", "react-router-dom": "^5.2.0",
"react-swipeable": "^6.0.1", "react-swipeable": "^6.0.1",
@ -78,7 +77,6 @@
"@types/enzyme": "^3.10.8", "@types/enzyme": "^3.10.8",
"@types/jest": "^26.0.20", "@types/jest": "^26.0.20",
"@types/leaflet": "^1.5.23", "@types/leaflet": "^1.5.23",
"@types/moment": "^2.13.0",
"@types/qs": "^6.9.5", "@types/qs": "^6.9.5",
"@types/ramda": "^0.27.38", "@types/ramda": "^0.27.38",
"@types/react": "^17.0.2", "@types/react": "^17.0.2",

View file

@ -1,7 +1,7 @@
import { faTags as tagsIcon } from '@fortawesome/free-solid-svg-icons'; import { faTags as tagsIcon } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { isEmpty, pipe } from 'ramda'; import { isEmpty, pipe } from 'ramda';
import moment from 'moment'; import { parseISO } from 'date-fns';
import SearchField from '../utils/SearchField'; import SearchField from '../utils/SearchField';
import Tag from '../tags/helpers/Tag'; import Tag from '../tags/helpers/Tag';
import { DateRangeSelector } from '../utils/dates/DateRangeSelector'; import { DateRangeSelector } from '../utils/dates/DateRangeSelector';
@ -16,7 +16,7 @@ interface SearchBarProps {
shortUrlsListParams: ShortUrlsListParams; shortUrlsListParams: ShortUrlsListParams;
} }
const dateOrNull = (date?: string) => date ? moment(date) : null; const dateOrNull = (date?: string) => date ? parseISO(date) : null;
const SearchBar = (colorGenerator: ColorGenerator) => ({ listShortUrls, shortUrlsListParams }: SearchBarProps) => { const SearchBar = (colorGenerator: ColorGenerator) => ({ listShortUrls, shortUrlsListParams }: SearchBarProps) => {
const selectedTags = shortUrlsListParams.tags ?? []; const selectedTags = shortUrlsListParams.tags ?? [];

View file

@ -2,8 +2,8 @@ import { FC, useEffect, useState } from 'react';
import { InputType } from 'reactstrap/lib/Input'; import { InputType } from 'reactstrap/lib/Input';
import { Button, FormGroup, Input, Row } from 'reactstrap'; import { Button, FormGroup, Input, Row } from 'reactstrap';
import { isEmpty, pipe, replace, trim } from 'ramda'; import { isEmpty, pipe, replace, trim } from 'ramda';
import m from 'moment';
import classNames from 'classnames'; import classNames from 'classnames';
import { parseISO } from 'date-fns';
import DateInput, { DateInputProps } from '../utils/DateInput'; import DateInput, { DateInputProps } from '../utils/DateInput';
import { import {
supportsCrawlableVisits, supportsCrawlableVisits,
@ -38,6 +38,7 @@ export interface ShortUrlFormProps {
} }
const normalizeTag = pipe(trim, replace(/ /g, '-')); const normalizeTag = pipe(trim, replace(/ /g, '-'));
const toDate = (date?: string | Date): Date | undefined => typeof date === 'string' ? parseISO(date) : date;
export const ShortUrlForm = ( export const ShortUrlForm = (
TagsSelector: FC<TagsSelectorProps>, TagsSelector: FC<TagsSelectorProps>,
@ -74,7 +75,7 @@ export const ShortUrlForm = (
const renderDateInput = (id: DateFields, placeholder: string, props: Partial<DateInputProps> = {}) => ( const renderDateInput = (id: DateFields, placeholder: string, props: Partial<DateInputProps> = {}) => (
<div className="form-group"> <div className="form-group">
<DateInput <DateInput
selected={shortUrlData[id] ? m(shortUrlData[id]) : null} selected={shortUrlData[id] ? toDate(shortUrlData[id] as string | Date) : null}
placeholderText={placeholder} placeholderText={placeholder}
isClearable isClearable
onChange={(date) => setShortUrlData({ ...shortUrlData, [id]: date })} onChange={(date) => setShortUrlData({ ...shortUrlData, [id]: date })}
@ -163,8 +164,8 @@ export const ShortUrlForm = (
<div className={limitAccessCardClasses}> <div className={limitAccessCardClasses}>
<SimpleCard title="Limit access to the short URL"> <SimpleCard title="Limit access to the short URL">
{renderOptionalInput('maxVisits', 'Maximum number of visits allowed', 'number', { min: 1 })} {renderOptionalInput('maxVisits', 'Maximum number of visits allowed', 'number', { min: 1 })}
{renderDateInput('validSince', 'Enabled since...', { maxDate: shortUrlData.validUntil ? m(shortUrlData.validUntil) : undefined })} {renderDateInput('validSince', 'Enabled since...', { maxDate: shortUrlData.validUntil ? toDate(shortUrlData.validUntil) : undefined })}
{renderDateInput('validUntil', 'Enabled until...', { minDate: shortUrlData.validSince ? m(shortUrlData.validSince) : undefined })} {renderDateInput('validUntil', 'Enabled until...', { minDate: shortUrlData.validSince ? toDate(shortUrlData.validSince) : undefined })}
</SimpleCard> </SimpleCard>
</div> </div>
</Row> </Row>

View file

@ -1,12 +1,11 @@
import * as m from 'moment';
import { Nullable, OptionalString } from '../../utils/utils'; import { Nullable, OptionalString } from '../../utils/utils';
export interface EditShortUrlData { export interface EditShortUrlData {
longUrl?: string; longUrl?: string;
tags?: string[]; tags?: string[];
title?: string; title?: string;
validSince?: m.Moment | string | null; validSince?: Date | string | null;
validUntil?: m.Moment | string | null; validUntil?: Date | string | null;
maxVisits?: number | null; maxVisits?: number | null;
validateUrl?: boolean; validateUrl?: boolean;
crawlable?: boolean; crawlable?: boolean;

View file

@ -1,6 +1,5 @@
import { isEmpty } from 'ramda';
import { FC, useEffect, useRef } from 'react'; import { FC, useEffect, useRef } from 'react';
import Moment from 'react-moment'; import { isEmpty } from 'ramda';
import { ExternalLink } from 'react-external-link'; import { ExternalLink } from 'react-external-link';
import ColorGenerator from '../../utils/services/ColorGenerator'; import ColorGenerator from '../../utils/services/ColorGenerator';
import { StateFlagTimeout } from '../../utils/helpers/hooks'; import { StateFlagTimeout } from '../../utils/helpers/hooks';
@ -8,6 +7,7 @@ import Tag from '../../tags/helpers/Tag';
import { SelectedServer } from '../../servers/data'; import { SelectedServer } from '../../servers/data';
import { CopyToClipboardIcon } from '../../utils/CopyToClipboardIcon'; import { CopyToClipboardIcon } from '../../utils/CopyToClipboardIcon';
import { ShortUrl } from '../data'; import { ShortUrl } from '../data';
import { Time } from '../../utils/Time';
import ShortUrlVisitsCount from './ShortUrlVisitsCount'; import ShortUrlVisitsCount from './ShortUrlVisitsCount';
import { ShortUrlsRowMenuProps } from './ShortUrlsRowMenu'; import { ShortUrlsRowMenuProps } from './ShortUrlsRowMenu';
import './ShortUrlsRow.scss'; import './ShortUrlsRow.scss';
@ -53,7 +53,7 @@ const ShortUrlsRow = (
return ( return (
<tr className="short-urls-row"> <tr className="short-urls-row">
<td className="indivisible short-urls-row__cell" data-th="Created at: "> <td className="indivisible short-urls-row__cell" data-th="Created at: ">
<Moment format="YYYY-MM-DD HH:mm">{shortUrl.dateCreated}</Moment> <Time date={shortUrl.dateCreated} />
</td> </td>
<td className="short-urls-row__cell" data-th="Short URL: "> <td className="short-urls-row__cell" data-th="Short URL: ">
<span className="indivisible short-urls-row__cell--relative"> <span className="indivisible short-urls-row__cell--relative">

View file

@ -1,33 +1,12 @@
import { useRef } from 'react'; import { useRef } from 'react';
import { isNil, dissoc } from 'ramda'; import { isNil } from 'ramda';
import DatePicker, { ReactDatePickerProps } from 'react-datepicker'; import DatePicker, { ReactDatePickerProps } from 'react-datepicker';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faCalendarAlt as calendarIcon } from '@fortawesome/free-regular-svg-icons'; import { faCalendarAlt as calendarIcon } from '@fortawesome/free-regular-svg-icons';
import classNames from 'classnames'; import classNames from 'classnames';
import moment from 'moment';
import './DateInput.scss'; import './DateInput.scss';
interface DatePropsInterface { export type DateInputProps = ReactDatePickerProps;
endDate?: moment.Moment | null;
maxDate?: moment.Moment | null;
minDate?: moment.Moment | null;
selected?: moment.Moment | null;
startDate?: moment.Moment | null;
onChange?: (date: moment.Moment | null) => void;
}
export type DateInputProps = DatePropsInterface & Omit<ReactDatePickerProps, keyof DatePropsInterface>;
const transformProps = (props: DateInputProps): ReactDatePickerProps => ({
// @ts-expect-error The DatePicker type definition is wrong. It has a ref prop
...dissoc('ref', props),
endDate: props.endDate?.toDate(),
maxDate: props.maxDate?.toDate(),
minDate: props.minDate?.toDate(),
selected: props.selected?.toDate(),
startDate: props.startDate?.toDate(),
onChange: (date: Date | null) => props.onChange?.(date && moment(date)),
});
const DateInput = (props: DateInputProps) => { const DateInput = (props: DateInputProps) => {
const { className, isClearable, selected } = props; const { className, isClearable, selected } = props;
@ -37,7 +16,7 @@ const DateInput = (props: DateInputProps) => {
return ( return (
<div className="date-input-container"> <div className="date-input-container">
<DatePicker <DatePicker
{...transformProps(props)} {...props}
dateFormat="yyyy-MM-dd" dateFormat="yyyy-MM-dd"
className={classNames('date-input-container__input form-control', className)} className={classNames('date-input-container__input form-control', className)}
// @ts-expect-error The DatePicker type definition is wrong. It has a ref prop // @ts-expect-error The DatePicker type definition is wrong. It has a ref prop

18
src/utils/Time.tsx Normal file
View file

@ -0,0 +1,18 @@
import { parseISO, format as formatDate, getUnixTime, formatDistance } from 'date-fns';
import { isDateObject } from './helpers/date';
export interface DateProps {
date: Date | string;
format?: string;
relative?: boolean;
}
export const Time = ({ date, format = 'yyyy-MM-dd HH:mm', relative = false }: DateProps) => {
const dateObject = isDateObject(date) ? date : parseISO(date);
return (
<time dateTime={`${getUnixTime(dateObject)}000`}>
{relative ? `${formatDistance(new Date(), dateObject)} ago` : formatDate(dateObject, format)}
</time>
);
};

View file

@ -1,10 +1,9 @@
import moment from 'moment';
import DateInput from '../DateInput'; import DateInput from '../DateInput';
import { DateRange } from './types'; import { DateRange } from './types';
interface DateRangeRowProps extends DateRange { interface DateRangeRowProps extends DateRange {
onStartDateChange: (date: moment.Moment | null) => void; onStartDateChange: (date: Date | null) => void;
onEndDateChange: (date: moment.Moment | null) => void; onEndDateChange: (date: Date | null) => void;
disabled?: boolean; disabled?: boolean;
} }

View file

@ -1,10 +1,10 @@
import moment from 'moment'; import { subDays, startOfDay, endOfDay } from 'date-fns';
import { filter, isEmpty } from 'ramda'; import { filter, isEmpty } from 'ramda';
import { formatInternational } from '../../helpers/date'; import { formatInternational } from '../../helpers/date';
export interface DateRange { export interface DateRange {
startDate?: moment.Moment | null; startDate?: Date | null;
endDate?: moment.Moment | null; endDate?: Date | null;
} }
export type DateInterval = 'today' | 'yesterday' | 'last7Days' | 'last30Days' | 'last90Days' | 'last180days' | 'last365Days'; export type DateInterval = 'today' | 'yesterday' | 'last7Days' | 'last30Days' | 'last90Days' | 'last180days' | 'last365Days';
@ -54,6 +54,8 @@ export const rangeOrIntervalToString = (range?: DateRange | DateInterval): strin
return INTERVAL_TO_STRING_MAP[range]; return INTERVAL_TO_STRING_MAP[range];
}; };
const startOfDaysAgo = (daysAgo: number) => startOfDay(subDays(new Date(), daysAgo));
export const intervalToDateRange = (dateInterval?: DateInterval): DateRange => { export const intervalToDateRange = (dateInterval?: DateInterval): DateRange => {
if (!dateInterval) { if (!dateInterval) {
return {}; return {};
@ -61,21 +63,19 @@ export const intervalToDateRange = (dateInterval?: DateInterval): DateRange => {
switch (dateInterval) { switch (dateInterval) {
case 'today': case 'today':
return { startDate: moment().startOf('day'), endDate: moment() }; return { startDate: startOfDay(new Date()), endDate: new Date() };
case 'yesterday': case 'yesterday':
const yesterday = moment().subtract(1, 'day'); // eslint-disable-line no-case-declarations return { startDate: startOfDaysAgo(1), endDate: endOfDay(subDays(new Date(), 1)) };
return { startDate: yesterday.startOf('day'), endDate: yesterday.endOf('day') };
case 'last7Days': case 'last7Days':
return { startDate: moment().subtract(7, 'days').startOf('day'), endDate: moment() }; return { startDate: startOfDaysAgo(7), endDate: new Date() };
case 'last30Days': case 'last30Days':
return { startDate: moment().subtract(30, 'days').startOf('day'), endDate: moment() }; return { startDate: startOfDaysAgo(30), endDate: new Date() };
case 'last90Days': case 'last90Days':
return { startDate: moment().subtract(90, 'days').startOf('day'), endDate: moment() }; return { startDate: startOfDaysAgo(90), endDate: new Date() };
case 'last180days': case 'last180days':
return { startDate: moment().subtract(180, 'days').startOf('day'), endDate: moment() }; return { startDate: startOfDaysAgo(180), endDate: new Date() };
case 'last365Days': case 'last365Days':
return { startDate: moment().subtract(365, 'days').startOf('day'), endDate: moment() }; return { startDate: startOfDaysAgo(365), endDate: new Date() };
} }
return {}; return {};

View file

@ -1,16 +1,23 @@
import * as moment from 'moment'; import { format, formatISO, parse } from 'date-fns';
import { OptionalString } from '../utils'; import { OptionalString } from '../utils';
type MomentOrString = moment.Moment | string; type DateOrString = Date | string;
type NullableDate = MomentOrString | null; type NullableDate = DateOrString | null;
const isMomentObject = (date: MomentOrString): date is moment.Moment => typeof (date as moment.Moment).format === 'function'; export const isDateObject = (date: DateOrString): date is Date => typeof date !== 'string';
const formatDateFromFormat = (date?: NullableDate, format?: string): OptionalString => const formatDateFromFormat = (date?: NullableDate, theFormat?: string): OptionalString => {
!date || !isMomentObject(date) ? date : date.format(format); if (!date || !isDateObject(date)) {
return date;
}
export const formatDate = (format = 'YYYY-MM-DD') => (date?: NullableDate) => formatDateFromFormat(date, format); return theFormat ? format(date, theFormat) : formatISO(date);
};
export const formatDate = (format = 'yyyy-MM-dd') => (date?: NullableDate) => formatDateFromFormat(date, format);
export const formatIsoDate = (date?: NullableDate) => formatDateFromFormat(date, undefined); export const formatIsoDate = (date?: NullableDate) => formatDateFromFormat(date, undefined);
export const formatInternational = formatDate(); export const formatInternational = formatDate();
export const parseDate = (date: string, format: string) => parse(date, format, new Date());

View file

@ -1,7 +1,7 @@
import { UncontrolledTooltip } from 'reactstrap'; import { UncontrolledTooltip } from 'reactstrap';
import Moment from 'react-moment';
import { ExternalLink } from 'react-external-link'; import { ExternalLink } from 'react-external-link';
import { ShortUrlDetail } from '../short-urls/reducers/shortUrlDetail'; import { ShortUrlDetail } from '../short-urls/reducers/shortUrlDetail';
import { Time } from '../utils/Time';
import { ShortUrlVisits } from './reducers/shortUrlVisits'; import { ShortUrlVisits } from './reducers/shortUrlVisits';
import VisitsHeader from './VisitsHeader'; import VisitsHeader from './VisitsHeader';
import './ShortUrlVisitsHeader.scss'; import './ShortUrlVisitsHeader.scss';
@ -22,18 +22,14 @@ const ShortUrlVisitsHeader = ({ shortUrlDetail, shortUrlVisits, goBack }: ShortU
const renderDate = () => !shortUrl ? <small>Loading...</small> : ( const renderDate = () => !shortUrl ? <small>Loading...</small> : (
<span> <span>
<b id="created" className="short-url-visits-header__created-at"> <b id="created" className="short-url-visits-header__created-at">
<Moment fromNow>{shortUrl.dateCreated}</Moment> <Time date={shortUrl.dateCreated} relative />
</b> </b>
<UncontrolledTooltip placement="bottom" target="created"> <UncontrolledTooltip placement="bottom" target="created">
<Moment format="YYYY-MM-DD HH:mm">{shortUrl.dateCreated}</Moment> <Time date={shortUrl.dateCreated} />
</UncontrolledTooltip> </UncontrolledTooltip>
</span> </span>
); );
const visitsStatsTitle = ( const visitsStatsTitle = <>Visits for <ExternalLink href={shortLink} /></>;
<>
Visits for <ExternalLink href={shortLink} />
</>
);
return ( return (
<VisitsHeader title={visitsStatsTitle} goBack={goBack} visits={visits} shortUrl={shortUrl}> <VisitsHeader title={visitsStatsTitle} goBack={goBack} visits={visits} shortUrl={shortUrl}>

View file

@ -1,5 +1,4 @@
import { useEffect, useMemo, useState, useRef } from 'react'; import { useEffect, useMemo, useState, useRef } from 'react';
import Moment from 'react-moment';
import classNames from 'classnames'; import classNames from 'classnames';
import { min, splitEvery } from 'ramda'; import { min, splitEvery } from 'ramda';
import { import {
@ -16,6 +15,7 @@ import { determineOrderDir, OrderDir } from '../utils/utils';
import { prettify } from '../utils/helpers/numbers'; import { prettify } from '../utils/helpers/numbers';
import { supportsBotVisits } from '../utils/helpers/features'; import { supportsBotVisits } from '../utils/helpers/features';
import { SelectedServer } from '../servers/data'; import { SelectedServer } from '../servers/data';
import { Time } from '../utils/Time';
import { NormalizedOrphanVisit, NormalizedVisit } from './types'; import { NormalizedOrphanVisit, NormalizedVisit } from './types';
import './VisitsTable.scss'; import './VisitsTable.scss';
@ -194,9 +194,7 @@ const VisitsTable = ({
)} )}
</td> </td>
)} )}
<td> <td><Time date={visit.date} /></td>
<Moment format="YYYY-MM-DD HH:mm">{visit.date}</Moment>
</td>
<td>{visit.country}</td> <td>{visit.country}</td>
<td>{visit.city}</td> <td>{visit.city}</td>
<td>{visit.browser}</td> <td>{visit.browser}</td>

View file

@ -10,7 +10,17 @@ import {
} from 'reactstrap'; } from 'reactstrap';
import { Line } from 'react-chartjs-2'; import { Line } from 'react-chartjs-2';
import { always, cond, countBy, reverse } from 'ramda'; import { always, cond, countBy, reverse } from 'ramda';
import moment from 'moment'; import {
add,
differenceInDays,
differenceInHours,
differenceInMonths,
differenceInWeeks,
parseISO,
format,
startOfISOWeek,
endOfISOWeek,
} from 'date-fns';
import Chart, { ChartData, ChartDataSets, ChartOptions } from 'chart.js'; import Chart, { ChartData, ChartDataSets, ChartOptions } from 'chart.js';
import { NormalizedVisit, Stats } from '../types'; import { NormalizedVisit, Stats } from '../types';
import { fillTheGaps } from '../../utils/helpers/visits'; import { fillTheGaps } from '../../utils/helpers/visits';
@ -39,46 +49,53 @@ const STEPS_MAP: Record<Step, string> = {
hourly: 'Hour', hourly: 'Hour',
}; };
const STEP_TO_DATE_UNIT_MAP: Record<Step, moment.unitOfTime.Diff> = { const STEP_TO_DURATION_MAP: Record<Step, (amount: number) => Duration> = {
hourly: 'hour', hourly: (hours: number) => ({ hours }),
daily: 'day', daily: (days: number) => ({ days }),
weekly: 'week', weekly: (weeks: number) => ({ weeks }),
monthly: 'month', monthly: (months: number) => ({ months }),
}; };
const STEP_TO_DATE_FORMAT: Record<Step, (date: moment.Moment | string) => string> = { const STEP_TO_DIFF_FUNC_MAP: Record<Step, (dateLeft: Date, dateRight: Date) => number> = {
hourly: (date) => moment(date).format('YYYY-MM-DD HH:00'), hourly: differenceInHours,
daily: (date) => moment(date).format('YYYY-MM-DD'), daily: differenceInDays,
weekly: differenceInWeeks,
monthly: differenceInMonths,
};
const STEP_TO_DATE_FORMAT: Record<Step, (date: Date) => string> = {
hourly: (date) => format(date, 'yyyy-MM-dd HH:00'),
daily: (date) => format(date, 'yyyy-MM-dd'),
weekly(date) { weekly(date) {
const firstWeekDay = moment(date).isoWeekday(1).format('YYYY-MM-DD'); const firstWeekDay = format(startOfISOWeek(date), 'yyyy-MM-dd');
const lastWeekDay = moment(date).isoWeekday(7).format('YYYY-MM-DD'); const lastWeekDay = format(endOfISOWeek(date), 'yyyy-MM-dd');
return `${firstWeekDay} - ${lastWeekDay}`; return `${firstWeekDay} - ${lastWeekDay}`;
}, },
monthly: (date) => moment(date).format('YYYY-MM'), monthly: (date) => format(date, 'yyyy-MM'),
}; };
const determineInitialStep = (oldestVisitDate: string): Step => { const determineInitialStep = (oldestVisitDate: string): Step => {
const now = moment(); const now = new Date();
const oldestDate = moment(oldestVisitDate); const oldestDate = parseISO(oldestVisitDate);
const matcher = cond<never, Step | undefined>([ const matcher = cond<never, Step | undefined>([
[ () => now.diff(oldestDate, 'day') <= 2, always<Step>('hourly') ], // Less than 2 days [ () => differenceInDays(now, oldestDate) <= 2, always<Step>('hourly') ], // Less than 2 days
[ () => now.diff(oldestDate, 'month') <= 1, always<Step>('daily') ], // Between 2 days and 1 month [ () => differenceInMonths(now, oldestDate) <= 1, always<Step>('daily') ], // Between 2 days and 1 month
[ () => now.diff(oldestDate, 'month') <= 6, always<Step>('weekly') ], // Between 1 and 6 months [ () => differenceInMonths(now, oldestDate) <= 6, always<Step>('weekly') ], // Between 1 and 6 months
]); ]);
return matcher() ?? 'monthly'; return matcher() ?? 'monthly';
}; };
const groupVisitsByStep = (step: Step, visits: NormalizedVisit[]): Stats => countBy( const groupVisitsByStep = (step: Step, visits: NormalizedVisit[]): Stats => countBy(
(visit) => STEP_TO_DATE_FORMAT[step](visit.date), (visit) => STEP_TO_DATE_FORMAT[step](parseISO(visit.date)),
visits, visits,
); );
const visitsToDatasetGroups = (step: Step, visits: NormalizedVisit[]) => const visitsToDatasetGroups = (step: Step, visits: NormalizedVisit[]) =>
visits.reduce<Record<string, NormalizedVisit[]>>( visits.reduce<Record<string, NormalizedVisit[]>>(
(acc, visit) => { (acc, visit) => {
const key = STEP_TO_DATE_FORMAT[step](visit.date); const key = STEP_TO_DATE_FORMAT[step](parseISO(visit.date));
acc[key] = acc[key] ?? []; acc[key] = acc[key] ?? [];
acc[key].push(visit); acc[key].push(visit);
@ -89,15 +106,16 @@ const visitsToDatasetGroups = (step: Step, visits: NormalizedVisit[]) =>
); );
const generateLabels = (step: Step, visits: NormalizedVisit[]): string[] => { const generateLabels = (step: Step, visits: NormalizedVisit[]): string[] => {
const unit = STEP_TO_DATE_UNIT_MAP[step]; const diffFunc = STEP_TO_DIFF_FUNC_MAP[step];
const formatter = STEP_TO_DATE_FORMAT[step]; const formatter = STEP_TO_DATE_FORMAT[step];
const newerDate = moment(visits[0].date); const newerDate = parseISO(visits[0].date);
const oldestDate = moment(visits[visits.length - 1].date); const oldestDate = parseISO(visits[visits.length - 1].date);
const size = newerDate.diff(oldestDate, unit); const size = diffFunc(newerDate, oldestDate);
const duration = STEP_TO_DURATION_MAP[step];
return [ return [
formatter(oldestDate), formatter(oldestDate),
...rangeOf(size, () => formatter(oldestDate.add(1, unit))), ...rangeOf(size, (num) => formatter(add(oldestDate, duration(num)))),
]; ];
}; };

View file

@ -1,5 +1,5 @@
import { shallow, ShallowWrapper } from 'enzyme'; import { shallow, ShallowWrapper } from 'enzyme';
import moment from 'moment'; import { formatISO } from 'date-fns';
import { identity } from 'ramda'; import { identity } from 'ramda';
import { Mock } from 'ts-mockery'; import { Mock } from 'ts-mockery';
import { Input } from 'reactstrap'; import { Input } from 'reactstrap';
@ -8,6 +8,7 @@ import DateInput from '../../src/utils/DateInput';
import { ShortUrlData } from '../../src/short-urls/data'; import { ShortUrlData } from '../../src/short-urls/data';
import { ReachableServer, SelectedServer } from '../../src/servers/data'; import { ReachableServer, SelectedServer } from '../../src/servers/data';
import { SimpleCard } from '../../src/utils/SimpleCard'; import { SimpleCard } from '../../src/utils/SimpleCard';
import { parseDate } from '../../src/utils/helpers/date';
describe('<ShortUrlForm />', () => { describe('<ShortUrlForm />', () => {
let wrapper: ShallowWrapper; let wrapper: ShallowWrapper;
@ -34,8 +35,8 @@ describe('<ShortUrlForm />', () => {
it('saves short URL with data set in form controls', () => { it('saves short URL with data set in form controls', () => {
const wrapper = createWrapper(); const wrapper = createWrapper();
const validSince = moment('2017-01-01'); const validSince = parseDate('2017-01-01', 'yyyy-MM-dd');
const validUntil = moment('2017-01-06'); const validUntil = parseDate('2017-01-06', 'yyyy-MM-dd');
wrapper.find(Input).first().simulate('change', { target: { value: 'https://long-domain.com/foo/bar' } }); wrapper.find(Input).first().simulate('change', { target: { value: 'https://long-domain.com/foo/bar' } });
wrapper.find('TagsSelector').simulate('change', [ 'tag_foo', 'tag_bar' ]); wrapper.find('TagsSelector').simulate('change', [ 'tag_foo', 'tag_bar' ]);
@ -53,8 +54,8 @@ describe('<ShortUrlForm />', () => {
tags: [ 'tag_foo', 'tag_bar' ], tags: [ 'tag_foo', 'tag_bar' ],
customSlug: 'my-slug', customSlug: 'my-slug',
domain: 'example.com', domain: 'example.com',
validSince: validSince.format(), validSince: formatISO(validSince),
validUntil: validUntil.format(), validUntil: formatISO(validUntil),
maxVisits: 20, maxVisits: 20,
findIfExists: false, findIfExists: false,
shortCodeLength: 15, shortCodeLength: 15,

View file

@ -1,9 +1,8 @@
import { shallow, ShallowWrapper } from 'enzyme'; import { shallow, ShallowWrapper } from 'enzyme';
import moment from 'moment';
import Moment from 'react-moment';
import { assoc, toString } from 'ramda'; import { assoc, toString } from 'ramda';
import { Mock } from 'ts-mockery'; import { Mock } from 'ts-mockery';
import { ExternalLink } from 'react-external-link'; import { ExternalLink } from 'react-external-link';
import { formatISO } from 'date-fns';
import createShortUrlsRow from '../../../src/short-urls/helpers/ShortUrlsRow'; import createShortUrlsRow from '../../../src/short-urls/helpers/ShortUrlsRow';
import Tag from '../../../src/tags/helpers/Tag'; import Tag from '../../../src/tags/helpers/Tag';
import ColorGenerator from '../../../src/utils/services/ColorGenerator'; import ColorGenerator from '../../../src/utils/services/ColorGenerator';
@ -11,6 +10,8 @@ import { StateFlagTimeout } from '../../../src/utils/helpers/hooks';
import { ShortUrl } from '../../../src/short-urls/data'; import { ShortUrl } from '../../../src/short-urls/data';
import { ReachableServer } from '../../../src/servers/data'; import { ReachableServer } from '../../../src/servers/data';
import { CopyToClipboardIcon } from '../../../src/utils/CopyToClipboardIcon'; import { CopyToClipboardIcon } from '../../../src/utils/CopyToClipboardIcon';
import { Time } from '../../../src/utils/Time';
import { parseDate } from '../../../src/utils/helpers/date';
describe('<ShortUrlsRow />', () => { describe('<ShortUrlsRow />', () => {
let wrapper: ShallowWrapper; let wrapper: ShallowWrapper;
@ -27,7 +28,7 @@ describe('<ShortUrlsRow />', () => {
shortCode: 'abc123', shortCode: 'abc123',
shortUrl: 'http://doma.in/abc123', shortUrl: 'http://doma.in/abc123',
longUrl: 'http://foo.com/bar', longUrl: 'http://foo.com/bar',
dateCreated: moment('2018-05-23 18:30:41').format(), dateCreated: formatISO(parseDate('2018-05-23 18:30:41', 'yyyy-MM-dd HH:mm:ss')),
tags: [ 'nodejs', 'reactjs' ], tags: [ 'nodejs', 'reactjs' ],
visitsCount: 45, visitsCount: 45,
domain: null, domain: null,
@ -62,9 +63,9 @@ describe('<ShortUrlsRow />', () => {
it('renders date in first column', () => { it('renders date in first column', () => {
const col = wrapper.find('td').first(); const col = wrapper.find('td').first();
const moment = col.find(Moment); const date = col.find(Time);
expect(moment.html()).toContain('>2018-05-23 18:30</time>'); expect(date.html()).toContain('>2018-05-23 18:30</time>');
}); });
it('renders short URL in second row', () => { it('renders short URL in second row', () => {

View file

@ -1,6 +1,5 @@
import { shallow, ShallowWrapper } from 'enzyme'; import { shallow, ShallowWrapper } from 'enzyme';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import moment from 'moment';
import { Mock } from 'ts-mockery'; import { Mock } from 'ts-mockery';
import DateInput, { DateInputProps } from '../../src/utils/DateInput'; import DateInput, { DateInputProps } from '../../src/utils/DateInput';
@ -30,7 +29,7 @@ describe('<DateInput />', () => {
}); });
it('does not show calendar icon when input is clearable', () => { it('does not show calendar icon when input is clearable', () => {
wrapped = createComponent({ isClearable: true, selected: moment() }); wrapped = createComponent({ isClearable: true, selected: new Date() });
expect(wrapped.find(FontAwesomeIcon)).toHaveLength(0); expect(wrapped.find(FontAwesomeIcon)).toHaveLength(0);
}); });
}); });

30
test/utils/Time.test.tsx Normal file
View file

@ -0,0 +1,30 @@
import { shallow, ShallowWrapper } from 'enzyme';
import { DateProps, Time } from '../../src/utils/Time';
import { parseDate } from '../../src/utils/helpers/date';
describe('<Time />', () => {
let wrapper: ShallowWrapper;
const createWrapper = (props: DateProps) => {
wrapper = shallow(<Time {...props} />);
return wrapper;
};
afterEach(() => wrapper?.unmount());
it.each([
[{ date: parseDate('2020-05-05', 'yyyy-MM-dd') }, '1588636800000', '2020-05-05 00:00' ],
[{ date: parseDate('2021-03-20', 'yyyy-MM-dd'), format: 'dd/MM/yyyy' }, '1616198400000', '20/03/2021' ],
])('includes expected dateTime and format', (props, expectedDateTime, expectedFormatted) => {
const wrapper = createWrapper(props);
expect(wrapper.prop('dateTime')).toEqual(expectedDateTime);
expect(wrapper.prop('children')).toEqual(expectedFormatted);
});
it('renders relative times when requested', () => {
const wrapper = createWrapper({ date: new Date(), relative: true });
expect(wrapper.prop('children')).toContain(' ago');
});
});

View file

@ -1,6 +1,5 @@
import { shallow, ShallowWrapper } from 'enzyme'; import { shallow, ShallowWrapper } from 'enzyme';
import { DropdownItem } from 'reactstrap'; import { DropdownItem } from 'reactstrap';
import moment from 'moment';
import { Mock } from 'ts-mockery'; import { Mock } from 'ts-mockery';
import { DateRangeSelector, DateRangeSelectorProps } from '../../../src/utils/dates/DateRangeSelector'; import { DateRangeSelector, DateRangeSelectorProps } from '../../../src/utils/dates/DateRangeSelector';
import { DateInterval } from '../../../src/utils/dates/types'; import { DateInterval } from '../../../src/utils/dates/types';
@ -40,7 +39,7 @@ describe('<DateRangeSelector />', () => {
[ 'last90Days' as DateInterval, 0, 1 ], [ 'last90Days' as DateInterval, 0, 1 ],
[ 'last180days' as DateInterval, 0, 1 ], [ 'last180days' as DateInterval, 0, 1 ],
[ 'last365Days' as DateInterval, 0, 1 ], [ 'last365Days' as DateInterval, 0, 1 ],
[{ startDate: moment() }, 0, 0 ], [{ startDate: new Date() }, 0, 0 ],
])('sets proper element as active based on provided date range', ( ])('sets proper element as active based on provided date range', (
initialDateRange, initialDateRange,
expectedActiveItems, expectedActiveItems,

View file

@ -1,4 +1,4 @@
import moment from 'moment'; import { format, subDays } from 'date-fns';
import { import {
DateInterval, DateInterval,
dateRangeIsEmpty, dateRangeIsEmpty,
@ -6,6 +6,7 @@ import {
rangeIsInterval, rangeIsInterval,
rangeOrIntervalToString, rangeOrIntervalToString,
} from '../../../../src/utils/dates/types'; } from '../../../../src/utils/dates/types';
import { parseDate } from '../../../../src/utils/helpers/date';
describe('date-types', () => { describe('date-types', () => {
describe('dateRangeIsEmpty', () => { describe('dateRangeIsEmpty', () => {
@ -20,9 +21,9 @@ describe('date-types', () => {
[{ startDate: undefined, endDate: undefined }, true ], [{ startDate: undefined, endDate: undefined }, true ],
[{ startDate: undefined, endDate: null }, true ], [{ startDate: undefined, endDate: null }, true ],
[{ startDate: null, endDate: undefined }, true ], [{ startDate: null, endDate: undefined }, true ],
[{ startDate: moment() }, false ], [{ startDate: new Date() }, false ],
[{ endDate: moment() }, false ], [{ endDate: new Date() }, false ],
[{ startDate: moment(), endDate: moment() }, false ], [{ startDate: new Date(), endDate: new Date() }, false ],
])('proper result is returned', (dateRange, expectedResult) => { ])('proper result is returned', (dateRange, expectedResult) => {
expect(dateRangeIsEmpty(dateRange)).toEqual(expectedResult); expect(dateRangeIsEmpty(dateRange)).toEqual(expectedResult);
}); });
@ -58,31 +59,36 @@ describe('date-types', () => {
[{ startDate: undefined, endDate: undefined }, undefined ], [{ startDate: undefined, endDate: undefined }, undefined ],
[{ startDate: undefined, endDate: null }, undefined ], [{ startDate: undefined, endDate: null }, undefined ],
[{ startDate: null, endDate: undefined }, undefined ], [{ startDate: null, endDate: undefined }, undefined ],
[{ startDate: moment('2020-01-01') }, 'Since 2020-01-01' ], [{ startDate: parseDate('2020-01-01', 'yyyy-MM-dd') }, 'Since 2020-01-01' ],
[{ endDate: moment('2020-01-01') }, 'Until 2020-01-01' ], [{ endDate: parseDate('2020-01-01', 'yyyy-MM-dd') }, 'Until 2020-01-01' ],
[{ startDate: moment('2020-01-01'), endDate: moment('2021-02-02') }, '2020-01-01 - 2021-02-02' ], [
{ startDate: parseDate('2020-01-01', 'yyyy-MM-dd'), endDate: parseDate('2021-02-02', 'yyyy-MM-dd') },
'2020-01-01 - 2021-02-02',
],
])('proper result is returned', (range, expectedValue) => { ])('proper result is returned', (range, expectedValue) => {
expect(rangeOrIntervalToString(range)).toEqual(expectedValue); expect(rangeOrIntervalToString(range)).toEqual(expectedValue);
}); });
}); });
describe('intervalToDateRange', () => { describe('intervalToDateRange', () => {
const now = () => moment(); const now = () => new Date();
const daysBack = (days: number) => subDays(new Date(), days);
const formatted = (date?: Date | null): string | undefined => !date ? undefined : format(date, 'yyyy-MM-dd');
test.each([ test.each([
[ undefined, undefined, undefined ], [ undefined, undefined, undefined ],
[ 'today' as DateInterval, now(), now() ], [ 'today' as DateInterval, now(), now() ],
[ 'yesterday' as DateInterval, now().subtract(1, 'day'), now().subtract(1, 'day') ], [ 'yesterday' as DateInterval, daysBack(1), daysBack(1) ],
[ 'last7Days' as DateInterval, now().subtract(7, 'day'), now() ], [ 'last7Days' as DateInterval, daysBack(7), now() ],
[ 'last30Days' as DateInterval, now().subtract(30, 'day'), now() ], [ 'last30Days' as DateInterval, daysBack(30), now() ],
[ 'last90Days' as DateInterval, now().subtract(90, 'day'), now() ], [ 'last90Days' as DateInterval, daysBack(90), now() ],
[ 'last180days' as DateInterval, now().subtract(180, 'day'), now() ], [ 'last180days' as DateInterval, daysBack(180), now() ],
[ 'last365Days' as DateInterval, now().subtract(365, 'day'), now() ], [ 'last365Days' as DateInterval, daysBack(365), now() ],
])('proper result is returned', (interval, expectedStartDate, expectedEndDate) => { ])('proper result is returned', (interval, expectedStartDate, expectedEndDate) => {
const { startDate, endDate } = intervalToDateRange(interval); const { startDate, endDate } = intervalToDateRange(interval);
expect(expectedStartDate?.format('YYYY-MM-DD')).toEqual(startDate?.format('YYYY-MM-DD')); expect(formatted(expectedStartDate)).toEqual(formatted(startDate));
expect(expectedEndDate?.format('YYYY-MM-DD')).toEqual(endDate?.format('YYYY-MM-DD')); expect(formatted(expectedEndDate)).toEqual(formatted(endDate));
}); });
}); });
}); });

View file

@ -1,13 +1,13 @@
import moment from 'moment'; import { formatISO } from 'date-fns';
import { formatDate, formatIsoDate } from '../../../src/utils/helpers/date'; import { formatDate, formatIsoDate, parseDate } from '../../../src/utils/helpers/date';
describe('date', () => { describe('date', () => {
describe('formatDate', () => { describe('formatDate', () => {
it.each([ it.each([
[ moment('2020-03-05 10:00:10'), 'DD/MM/YYYY', '05/03/2020' ], [ parseDate('2020-03-05 10:00:10', 'yyyy-MM-dd HH:mm:ss'), 'dd/MM/yyyy', '05/03/2020' ],
[ moment('2020-03-05 10:00:10'), 'YYYY-MM', '2020-03' ], [ parseDate('2020-03-05 10:00:10', 'yyyy-MM-dd HH:mm:ss'), 'yyyy-MM', '2020-03' ],
[ moment('2020-03-05 10:00:10'), undefined, '2020-03-05' ], [ parseDate('2020-03-05 10:00:10', 'yyyy-MM-dd HH:mm:ss'), undefined, '2020-03-05' ],
[ '2020-03-05 10:00:10', 'DD-MM-YYYY', '2020-03-05 10:00:10' ], [ '2020-03-05 10:00:10', 'dd-MM-yyyy', '2020-03-05 10:00:10' ],
[ '2020-03-05 10:00:10', undefined, '2020-03-05 10:00:10' ], [ '2020-03-05 10:00:10', undefined, '2020-03-05 10:00:10' ],
[ undefined, undefined, undefined ], [ undefined, undefined, undefined ],
[ null, undefined, null ], [ null, undefined, null ],
@ -18,7 +18,10 @@ describe('date', () => {
describe('formatIsoDate', () => { describe('formatIsoDate', () => {
it.each([ it.each([
[ moment('2020-03-05 10:00:10'), moment('2020-03-05 10:00:10').format() ], [
parseDate('2020-03-05 10:00:10', 'yyyy-MM-dd HH:mm:ss'),
formatISO(parseDate('2020-03-05 10:00:10', 'yyyy-MM-dd HH:mm:ss')),
],
[ '2020-03-05 10:00:10', '2020-03-05 10:00:10' ], [ '2020-03-05 10:00:10', '2020-03-05 10:00:10' ],
[ 'foo', 'foo' ], [ 'foo', 'foo' ],
[ undefined, undefined ], [ undefined, undefined ],

View file

@ -1,10 +1,10 @@
import { shallow, ShallowWrapper } from 'enzyme'; import { shallow, ShallowWrapper } from 'enzyme';
import Moment from 'react-moment';
import { ExternalLink } from 'react-external-link'; import { ExternalLink } from 'react-external-link';
import { Mock } from 'ts-mockery'; import { Mock } from 'ts-mockery';
import ShortUrlVisitsHeader from '../../src/visits/ShortUrlVisitsHeader'; import ShortUrlVisitsHeader from '../../src/visits/ShortUrlVisitsHeader';
import { ShortUrlDetail } from '../../src/short-urls/reducers/shortUrlDetail'; import { ShortUrlDetail } from '../../src/short-urls/reducers/shortUrlDetail';
import { ShortUrlVisits } from '../../src/visits/reducers/shortUrlVisits'; import { ShortUrlVisits } from '../../src/visits/reducers/shortUrlVisits';
import { Time } from '../../src/utils/Time';
describe('<ShortUrlVisitsHeader />', () => { describe('<ShortUrlVisitsHeader />', () => {
let wrapper: ShallowWrapper; let wrapper: ShallowWrapper;
@ -36,9 +36,9 @@ describe('<ShortUrlVisitsHeader />', () => {
afterEach(() => wrapper.unmount()); afterEach(() => wrapper.unmount());
it('shows when the URL was created', () => { it('shows when the URL was created', () => {
const moment = wrapper.find(Moment).first(); const time = wrapper.find(Time).first();
expect(moment.prop('children')).toEqual(dateCreated); expect(time.prop('date')).toEqual(dateCreated);
}); });
it.each([ it.each([

View file

@ -1,7 +1,7 @@
import { shallow, ShallowWrapper } from 'enzyme'; import { shallow, ShallowWrapper } from 'enzyme';
import { CardHeader, DropdownItem } from 'reactstrap'; import { CardHeader, DropdownItem } from 'reactstrap';
import { Line } from 'react-chartjs-2'; import { Line } from 'react-chartjs-2';
import moment from 'moment'; import { formatISO, subDays, subMonths, subYears } from 'date-fns';
import { Mock } from 'ts-mockery'; import { Mock } from 'ts-mockery';
import LineChartCard from '../../../src/visits/helpers/LineChartCard'; import LineChartCard from '../../../src/visits/helpers/LineChartCard';
import ToggleSwitch from '../../../src/utils/ToggleSwitch'; import ToggleSwitch from '../../../src/utils/ToggleSwitch';
@ -27,12 +27,12 @@ describe('<LineChartCard />', () => {
it.each([ it.each([
[[], 'monthly' ], [[], 'monthly' ],
[[{ date: moment().subtract(1, 'day').format() }], 'hourly' ], [[{ date: formatISO(subDays(new Date(), 1)) }], 'hourly' ],
[[{ date: moment().subtract(3, 'day').format() }], 'daily' ], [[{ date: formatISO(subDays(new Date(), 3)) }], 'daily' ],
[[{ date: moment().subtract(2, 'month').format() }], 'weekly' ], [[{ date: formatISO(subMonths(new Date(), 2)) }], 'weekly' ],
[[{ date: moment().subtract(6, 'month').format() }], 'weekly' ], [[{ date: formatISO(subMonths(new Date(), 6)) }], 'weekly' ],
[[{ date: moment().subtract(7, 'month').format() }], 'monthly' ], [[{ date: formatISO(subMonths(new Date(), 7)) }], 'monthly' ],
[[{ date: moment().subtract(1, 'year').format() }], 'monthly' ], [[{ date: formatISO(subYears(new Date(), 1)) }], 'monthly' ],
])('renders group menu and selects proper grouping item based on visits dates', (visits, expectedActiveItem) => { ])('renders group menu and selects proper grouping item based on visits dates', (visits, expectedActiveItem) => {
const wrapper = createWrapper(visits.map((visit) => Mock.of<NormalizedVisit>(visit))); const wrapper = createWrapper(visits.map((visit) => Mock.of<NormalizedVisit>(visit)));
const items = wrapper.find(DropdownItem); const items = wrapper.find(DropdownItem);
@ -75,8 +75,8 @@ describe('<LineChartCard />', () => {
}); });
it.each([ it.each([
[[ Mock.of<NormalizedVisit>({}) ], [], 1 ], [[ Mock.of<NormalizedVisit>({ date: '2016-04-01' }) ], [], 1 ],
[[ Mock.of<NormalizedVisit>({}) ], [ Mock.of<NormalizedVisit>({}) ], 2 ], [[ Mock.of<NormalizedVisit>({ date: '2016-04-01' }) ], [ Mock.of<NormalizedVisit>({ date: '2016-04-01' }) ], 2 ],
])('renders chart with expected data', (visits, highlightedVisits, expectedLines) => { ])('renders chart with expected data', (visits, highlightedVisits, expectedLines) => {
const wrapper = createWrapper(visits, highlightedVisits); const wrapper = createWrapper(visits, highlightedVisits);
const chart = wrapper.find(Line); const chart = wrapper.find(Line);