Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Reopened/Feature: Remove moment from datepicker #29984

Merged
merged 6 commits into from
Nov 1, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion src/CONST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,6 @@ const CONST = {
DESKTOP: `${ACTIVE_EXPENSIFY_URL}NewExpensify.dmg`,
},
DATE: {
MOMENT_FORMAT_STRING: 'YYYY-MM-DD',
SQL_DATE_TIME: 'YYYY-MM-DD HH:mm:ss',
FNS_FORMAT_STRING: 'yyyy-MM-dd',
LOCAL_TIME_FORMAT: 'h:mm a',
Expand Down
4 changes: 2 additions & 2 deletions src/components/DatePicker/datepickerPropTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@ const propTypes = {
...fieldPropTypes,

/**
* The datepicker supports any value that `moment` can parse.
* The datepicker supports any value that `new Date()` can parse.
* `onInputChange` would always be called with a Date (or null)
*/
value: PropTypes.oneOfType([PropTypes.instanceOf(Date), PropTypes.string]),

/**
* The datepicker supports any defaultValue that `moment` can parse.
* The datepicker supports any defaultValue that `new Date()` can parse.
* `onInputChange` would always be called with a Date (or null)
*/
defaultValue: PropTypes.oneOfType([PropTypes.instanceOf(Date), PropTypes.string]),
Expand Down
10 changes: 5 additions & 5 deletions src/components/DatePicker/index.android.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import RNDatePicker from '@react-native-community/datetimepicker';
import moment from 'moment';
import {format} from 'date-fns';
import React, {forwardRef, useCallback, useImperativeHandle, useRef, useState} from 'react';
import {Keyboard} from 'react-native';
import TextInput from '@components/TextInput';
Expand All @@ -20,8 +20,7 @@ function DatePicker({value, defaultValue, label, placeholder, errorText, contain
setIsPickerVisible(false);

if (event.type === 'set') {
const asMoment = moment(selectedDate, true);
onInputChange(asMoment.format(CONST.DATE.MOMENT_FORMAT_STRING));
onInputChange(format(selectedDate, CONST.DATE.FNS_FORMAT_STRING));
}
};

Expand All @@ -39,7 +38,8 @@ function DatePicker({value, defaultValue, label, placeholder, errorText, contain
[showPicker],
);

const dateAsText = value || defaultValue ? moment(value || defaultValue).format(CONST.DATE.MOMENT_FORMAT_STRING) : '';
const date = value || defaultValue;
const dateAsText = date ? format(new Date(date), CONST.DATE.FNS_FORMAT_STRING) : '';

return (
<>
Expand All @@ -61,7 +61,7 @@ function DatePicker({value, defaultValue, label, placeholder, errorText, contain
/>
{isPickerVisible && (
<RNDatePicker
value={value || defaultValue ? moment(value || defaultValue).toDate() : new Date()}
value={date ? new Date(date) : new Date()}
mode="date"
onChange={setDate}
maximumDate={maxDate}
Expand Down
10 changes: 5 additions & 5 deletions src/components/DatePicker/index.ios.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import RNDatePicker from '@react-native-community/datetimepicker';
import {format} from 'date-fns';
import isFunction from 'lodash/isFunction';
import moment from 'moment';
import React, {useCallback, useEffect, useRef, useState} from 'react';
import {Button, Keyboard, View} from 'react-native';
import Popover from '@components/Popover';
Expand All @@ -13,8 +13,9 @@ import CONST from '@src/CONST';
import {defaultProps, propTypes} from './datepickerPropTypes';

function DatePicker({value, defaultValue, innerRef, onInputChange, preferredLocale, minDate, maxDate, label, disabled, onBlur, placeholder, containerStyles, errorText}) {
const dateValue = value || defaultValue;
const [isPickerVisible, setIsPickerVisible] = useState(false);
const [selectedDate, setSelectedDate] = useState(moment(value || defaultValue).toDate());
const [selectedDate, setSelectedDate] = useState(dateValue ? new Date(dateValue) : new Date());
const {isKeyboardShown} = useKeyboardState();
const {translate} = useLocalize();
const initialValue = useRef(null);
Expand Down Expand Up @@ -65,8 +66,7 @@ function DatePicker({value, defaultValue, innerRef, onInputChange, preferredLoca
*/
const selectDate = () => {
setIsPickerVisible(false);
const asMoment = moment(selectedDate, true);
onInputChange(asMoment.format(CONST.DATE.MOMENT_FORMAT_STRING));
onInputChange(format(selectedDate, CONST.DATE.FNS_FORMAT_STRING));
};

/**
Expand All @@ -77,7 +77,7 @@ function DatePicker({value, defaultValue, innerRef, onInputChange, preferredLoca
setSelectedDate(date);
};

const dateAsText = value || defaultValue ? moment(value || defaultValue).format(CONST.DATE.MOMENT_FORMAT_STRING) : '';
const dateAsText = dateValue ? format(new Date(dateValue), CONST.DATE.FNS_FORMAT_STRING) : '';

return (
<>
Expand Down
12 changes: 6 additions & 6 deletions src/components/DatePicker/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import moment from 'moment';
import {format, isValid} from 'date-fns';
import React, {useEffect, useRef} from 'react';
import _ from 'underscore';
import TextInput from '@components/TextInput';
Expand All @@ -13,8 +13,8 @@ function DatePicker({maxDate, minDate, onInputChange, innerRef, label, value, pl
useEffect(() => {
// Adds nice native datepicker on web/desktop. Not possible to set this through props
inputRef.current.setAttribute('type', 'date');
inputRef.current.setAttribute('max', moment(maxDate).format(CONST.DATE.MOMENT_FORMAT_STRING));
inputRef.current.setAttribute('min', moment(minDate).format(CONST.DATE.MOMENT_FORMAT_STRING));
inputRef.current.setAttribute('max', format(new Date(maxDate), CONST.DATE.FNS_FORMAT_STRING));
inputRef.current.setAttribute('min', format(new Date(minDate), CONST.DATE.FNS_FORMAT_STRING));
inputRef.current.classList.add('expensify-datepicker');
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
Expand All @@ -29,9 +29,9 @@ function DatePicker({maxDate, minDate, onInputChange, innerRef, label, value, pl
return;
}

const asMoment = moment(text, true);
if (asMoment.isValid()) {
onInputChange(asMoment.format(CONST.DATE.MOMENT_FORMAT_STRING));
const date = new Date(text);
if (isValid(date)) {
onInputChange(format(date, CONST.DATE.FNS_FORMAT_STRING));
}
};

Expand Down
40 changes: 20 additions & 20 deletions src/components/NewDatePicker/CalendarPicker/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {addMonths, endOfMonth, format, getYear, isSameDay, setDate, setYear, startOfDay, subMonths} from 'date-fns';
import Str from 'expensify-common/lib/str';
import moment from 'moment';
import PropTypes from 'prop-types';
import React from 'react';
import {View} from 'react-native';
Expand All @@ -8,6 +8,7 @@ import PressableWithFeedback from '@components/Pressable/PressableWithFeedback';
import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback';
import Text from '@components/Text';
import withLocalize, {withLocalizePropTypes} from '@components/withLocalize';
import DateUtils from '@libs/DateUtils';
import getButtonState from '@libs/getButtonState';
import styles from '@styles/styles';
import * as StyleUtils from '@styles/StyleUtils';
Expand All @@ -34,8 +35,8 @@ const propTypes = {

const defaultProps = {
value: new Date(),
minDate: moment().year(CONST.CALENDAR_PICKER.MIN_YEAR).toDate(),
maxDate: moment().year(CONST.CALENDAR_PICKER.MAX_YEAR).toDate(),
minDate: setYear(new Date(), CONST.CALENDAR_PICKER.MIN_YEAR),
maxDate: setYear(new Date(), CONST.CALENDAR_PICKER.MAX_YEAR),
onSelected: () => {},
};

Expand All @@ -46,16 +47,15 @@ class CalendarPicker extends React.PureComponent {
if (props.minDate >= props.maxDate) {
throw new Error('Minimum date cannot be greater than the maximum date.');
}

let currentDateView = moment(props.value, CONST.DATE.MOMENT_FORMAT_STRING).toDate();
let currentDateView = new Date(props.value);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There was another instance of date being fetched incorrectly on this line.
When props.value type is 'YYYY-MM-DD' and user timezone is behind UTC, the previous date is returned.
parseISO should have been used like others.

if (props.maxDate < currentDateView) {
currentDateView = props.maxDate;
} else if (props.minDate > currentDateView) {
currentDateView = props.minDate;
}

const minYear = moment(this.props.minDate).year();
const maxYear = moment(this.props.maxDate).year();
const minYear = getYear(new Date(this.props.minDate));
const maxYear = getYear(new Date(this.props.maxDate));

this.state = {
currentDateView,
Expand All @@ -79,7 +79,7 @@ class CalendarPicker extends React.PureComponent {

onYearSelected(year) {
this.setState((prev) => {
const newCurrentDateView = moment(prev.currentDateView).set('year', year).toDate();
const newCurrentDateView = setYear(new Date(prev.currentDateView), year);

return {
currentDateView: newCurrentDateView,
Expand All @@ -99,34 +99,34 @@ class CalendarPicker extends React.PureComponent {
onDayPressed(day) {
this.setState(
(prev) => ({
currentDateView: moment(prev.currentDateView).set('date', day).toDate(),
currentDateView: setDate(new Date(prev.currentDateView), day),
}),
() => this.props.onSelected(moment(this.state.currentDateView).format('YYYY-MM-DD')),
() => this.props.onSelected(format(new Date(this.state.currentDateView), CONST.DATE.FNS_FORMAT_STRING)),
);
}

/**
* Handles the user pressing the previous month arrow of the calendar picker.
*/
moveToPrevMonth() {
this.setState((prev) => ({currentDateView: moment(prev.currentDateView).subtract(1, 'months').toDate()}));
this.setState((prev) => ({currentDateView: subMonths(new Date(prev.currentDateView), 1)}));
}

/**
* Handles the user pressing the next month arrow of the calendar picker.
*/
moveToNextMonth() {
this.setState((prev) => ({currentDateView: moment(prev.currentDateView).add(1, 'months').toDate()}));
this.setState((prev) => ({currentDateView: addMonths(new Date(prev.currentDateView), 1)}));
}

render() {
const monthNames = _.map(moment.localeData(this.props.preferredLocale).months(), Str.recapitalize);
const daysOfWeek = _.map(moment.localeData(this.props.preferredLocale).weekdays(), (day) => day.toUpperCase());
const monthNames = _.map(DateUtils.getMonthNames(this.props.preferredLocale), Str.recapitalize);
const daysOfWeek = _.map(DateUtils.getDaysOfWeek(this.props.preferredLocale), (day) => day.toUpperCase());
const currentMonthView = this.state.currentDateView.getMonth();
const currentYearView = this.state.currentDateView.getFullYear();
const calendarDaysMatrix = generateMonthMatrix(currentYearView, currentMonthView);
const hasAvailableDatesNextMonth = moment(this.props.maxDate).endOf('month').endOf('day') >= moment(this.state.currentDateView).add(1, 'months');
const hasAvailableDatesPrevMonth = moment(this.props.minDate).startOf('month').startOf('day') <= moment(this.state.currentDateView).subtract(1, 'months');
const hasAvailableDatesNextMonth = startOfDay(endOfMonth(new Date(this.props.maxDate))) > addMonths(new Date(this.state.currentDateView), 1);
const hasAvailableDatesPrevMonth = startOfDay(new Date(this.props.minDate)) < endOfMonth(subMonths(new Date(this.state.currentDateView), 1));
Comment on lines +128 to +129
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should have the same logic here. This caused a regression #31793


return (
<View>
Expand Down Expand Up @@ -201,11 +201,11 @@ class CalendarPicker extends React.PureComponent {
style={styles.flexRow}
>
{_.map(week, (day, index) => {
const currentDate = moment([currentYearView, currentMonthView, day]);
const isBeforeMinDate = currentDate < moment(this.props.minDate).startOf('day');
const isAfterMaxDate = currentDate > moment(this.props.maxDate).startOf('day');
const currentDate = new Date(currentYearView, currentMonthView, day);
const isBeforeMinDate = currentDate < startOfDay(new Date(this.props.minDate));
const isAfterMaxDate = currentDate > startOfDay(new Date(this.props.maxDate));
const isDisabled = !day || isBeforeMinDate || isAfterMaxDate;
const isSelected = moment(this.props.value).isSame(moment([currentYearView, currentMonthView, day]), 'day');
const isSelected = isSameDay(new Date(this.props.value), new Date(currentYearView, currentMonthView, day));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In moment if we pass a null day we will get an invalid date but this is not the case with Date. This lead to a regression #31793.

moment([2000, 1, null]).format() // 'Invalid date'
new Date(2000, 1, null) // Mon Jan 31 2000 00:00:00 GMT+0100 (Central European Standard Time)


return (
<PressableWithoutFeedback
Expand Down
10 changes: 5 additions & 5 deletions src/components/NewDatePicker/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {setYear} from 'date-fns';
import _ from 'lodash';
import moment from 'moment';
import PropTypes from 'prop-types';
import React, {useEffect, useState} from 'react';
import {View} from 'react-native';
Expand All @@ -14,13 +14,13 @@ import CalendarPicker from './CalendarPicker';

const propTypes = {
/**
* The datepicker supports any value that `moment` can parse.
* The datepicker supports any value that `new Date()` can parse.
* `onInputChange` would always be called with a Date (or null)
*/
value: PropTypes.string,

/**
* The datepicker supports any defaultValue that `moment` can parse.
* The datepicker supports any defaultValue that `new Date()` can parse.
* `onInputChange` would always be called with a Date (or null)
*/
defaultValue: PropTypes.string,
Expand All @@ -39,8 +39,8 @@ const propTypes = {

const datePickerDefaultProps = {
...defaultBaseTextInputPropTypes,
minDate: moment().year(CONST.CALENDAR_PICKER.MIN_YEAR).toDate(),
maxDate: moment().year(CONST.CALENDAR_PICKER.MAX_YEAR).toDate(),
minDate: setYear(new Date(), CONST.CALENDAR_PICKER.MIN_YEAR),
maxDate: setYear(new Date(), CONST.CALENDAR_PICKER.MAX_YEAR),
value: undefined,
};

Expand Down
36 changes: 36 additions & 0 deletions src/libs/DateUtils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import {
addDays,
eachDayOfInterval,
eachMonthOfInterval,
endOfDay,
endOfWeek,
format,
Expand Down Expand Up @@ -255,6 +257,38 @@ function getCurrentTimezone(): Required<Timezone> {
return timezone;
}

/**
* @returns [January, Fabruary, March, April, May, June, July, August, ...]
*/
function getMonthNames(preferredLocale: string): string[] {
if (preferredLocale) {
setLocale(preferredLocale);
}
const fullYear = new Date().getFullYear();
const monthsArray = eachMonthOfInterval({
start: new Date(fullYear, 0, 1), // January 1st of the current year
end: new Date(fullYear, 11, 31), // December 31st of the current year
});

// eslint-disable-next-line rulesdir/prefer-underscore-method
return monthsArray.map((monthDate) => format(monthDate, CONST.DATE.MONTH_FORMAT));
}

/**
* @returns [Monday, Thuesday, Wednesday, ...]
*/
function getDaysOfWeek(preferredLocale: string): string[] {
if (preferredLocale) {
setLocale(preferredLocale);
}
const startOfCurrentWeek = startOfWeek(new Date(), {weekStartsOn: 1}); // Assuming Monday is the start of the week
const endOfCurrentWeek = endOfWeek(new Date(), {weekStartsOn: 1}); // Assuming Monday is the start of the week
const daysOfWeek = eachDayOfInterval({start: startOfCurrentWeek, end: endOfCurrentWeek});

// eslint-disable-next-line rulesdir/prefer-underscore-method
return daysOfWeek.map((date) => format(date, 'eeee'));
}

// Used to throttle updates to the timezone when necessary
let lastUpdatedTimezoneTime = new Date();

Expand Down Expand Up @@ -373,6 +407,8 @@ const DateUtils = {
isToday,
isTomorrow,
isYesterday,
getMonthNames,
getDaysOfWeek,
formatWithUTCTimeZone,
};

Expand Down
11 changes: 2 additions & 9 deletions tests/unit/CalendarPickerTest.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,10 @@
import {fireEvent, render, within} from '@testing-library/react-native';
import {addYears, eachMonthOfInterval, format, subYears} from 'date-fns';
import {addYears, subYears} from 'date-fns';
import CalendarPicker from '../../src/components/NewDatePicker/CalendarPicker';
import CONST from '../../src/CONST';
import DateUtils from '../../src/libs/DateUtils';

DateUtils.setLocale(CONST.LOCALES.EN);
const fullYear = new Date().getFullYear();
const monthsArray = eachMonthOfInterval({
start: new Date(fullYear, 0, 1), // January 1st of the current year
end: new Date(fullYear, 11, 31), // December 31st of the current year
});
// eslint-disable-next-line rulesdir/prefer-underscore-method
const monthNames = monthsArray.map((monthDate) => format(monthDate, CONST.DATE.MONTH_FORMAT));
const monthNames = DateUtils.getMonthNames(CONST.LOCALES.EN);

jest.mock('@react-navigation/native', () => ({
useNavigation: () => ({navigate: jest.fn()}),
Expand Down
Loading