diff --git a/app/helpers/form_helper.rb b/app/helpers/form_helper.rb index b072ba3aef8..46333c7427b 100644 --- a/app/helpers/form_helper.rb +++ b/app/helpers/form_helper.rb @@ -398,6 +398,10 @@ def date_local_f(f, attr, options = {}) react_form_input('date', f, attr, options) end + def time_local_f(f, attr, options = {}) + react_form_input('time', f, attr, options) + end + def datetime_local_f(f, attr, options = {}) react_form_input('dateTime', f, attr, options) end diff --git a/webpack/assets/javascripts/react_app/components/common/DateTimePicker/DateComponents/DateConstants.js b/webpack/assets/javascripts/react_app/components/common/DateTimePicker/DateComponents/DateConstants.js deleted file mode 100644 index e2c0897527b..00000000000 --- a/webpack/assets/javascripts/react_app/components/common/DateTimePicker/DateComponents/DateConstants.js +++ /dev/null @@ -1,3 +0,0 @@ -export const YEAR = 'YEAR'; -export const MONTH = 'MONTH'; -export const DAY = 'DAY'; diff --git a/webpack/assets/javascripts/react_app/components/common/DateTimePicker/DateComponents/DateInput.js b/webpack/assets/javascripts/react_app/components/common/DateTimePicker/DateComponents/DateInput.js deleted file mode 100644 index 7c5d6aed346..00000000000 --- a/webpack/assets/javascripts/react_app/components/common/DateTimePicker/DateComponents/DateInput.js +++ /dev/null @@ -1,103 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import classNames from 'classnames'; -import { addMonths } from './helpers'; -import MonthView from './MonthView'; -import YearView from './YearView'; -import DecadeView from './DecadeView'; -import { YEAR, DAY, MONTH } from './DateConstants'; - -class DateInput extends React.Component { - state = { - date: new Date(this.props.date), - typeOfDateInput: this.props.typeOfDateInput, - }; - static getDerivedStateFromProps(props, state) { - if (props.date !== state.date) { - return { - date: props.date, - typeOfDateInput: props.typeOfDateInput, - }; - } - return null; - } - getPrevMonth = () => { - const { date } = this.state; - this.setState({ date: addMonths(date, -1) }); - }; - getNextMonth = () => { - const { date } = this.state; - this.setState({ date: addMonths(date, 1) }); - }; - setSelected = day => { - this.setState({ - date: day, - }); - this.props.setSelected(day); - }; - toggleDateView = (type = null) => { - this.setState({ - typeOfDateInput: type, - }); - }; - getDateViewByType = type => { - const { date, locale, weekStartsOn, setSelected } = this.props; - switch (type) { - case DAY: - return ( - - ); - case YEAR: - return ( - - ); - default: - return ( - - ); - } - }; - render() { - const { className } = this.props; - const { typeOfDateInput } = this.state; - return ( -
- {this.getDateViewByType(typeOfDateInput)} -
- ); - } -} - -DateInput.propTypes = { - date: PropTypes.oneOfType([PropTypes.instanceOf(Date), PropTypes.string]), - setSelected: PropTypes.func, - locale: PropTypes.string, - weekStartsOn: PropTypes.number, - className: PropTypes.string, - typeOfDateInput: PropTypes.string, -}; - -DateInput.defaultProps = { - setSelected: null, - date: new Date(), - locale: 'en-US', - weekStartsOn: 1, - className: '', - typeOfDateInput: MONTH, -}; -export default DateInput; diff --git a/webpack/assets/javascripts/react_app/components/common/DateTimePicker/DateComponents/DateInput.test.js b/webpack/assets/javascripts/react_app/components/common/DateTimePicker/DateComponents/DateInput.test.js deleted file mode 100644 index 2a4a564196e..00000000000 --- a/webpack/assets/javascripts/react_app/components/common/DateTimePicker/DateComponents/DateInput.test.js +++ /dev/null @@ -1,42 +0,0 @@ -import React from 'react'; -import { shallow, mount } from 'enzyme'; -import DateInput from './DateInput'; - -test('DateInput is working properly', () => { - const component = shallow(); - - expect(component.render()).toMatchSnapshot(); -}); - -test('DateInput changes selected on click', () => { - const setSelected = jest.fn(); - const component = mount( - - ); - component - .find('.weekend') - .first() - .simulate('click'); - expect(setSelected).toBeCalledWith(new Date('2019-01-04 14:22:31')); -}); - -test('DateInput toggles view to years', () => { - const component = mount(); - component - .find('.picker-switch') - .first() - .simulate('click'); - expect(component.render()).toMatchSnapshot(); -}); -test('DateInput toggles view to decades', () => { - const component = mount(); - component - .find('.picker-switch') - .first() - .simulate('click'); - component - .find('.picker-switch') - .first() - .simulate('click'); - expect(component.render()).toMatchSnapshot(); -}); diff --git a/webpack/assets/javascripts/react_app/components/common/DateTimePicker/DateComponents/Day.js b/webpack/assets/javascripts/react_app/components/common/DateTimePicker/DateComponents/Day.js deleted file mode 100644 index b0dbdaebc35..00000000000 --- a/webpack/assets/javascripts/react_app/components/common/DateTimePicker/DateComponents/Day.js +++ /dev/null @@ -1,30 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import classNames from 'classnames'; - -const Day = ({ day, setSelected, classNamesArray }) => { - const date = day.getDate(); - return ( - { - setSelected(day); - }} - > - {date} - - ); -}; - -Day.propTypes = { - day: PropTypes.instanceOf(Date).isRequired, - classNamesArray: PropTypes.object, - setSelected: PropTypes.func, -}; - -Day.defaultProps = { - setSelected: null, - classNamesArray: [], -}; -export default Day; diff --git a/webpack/assets/javascripts/react_app/components/common/DateTimePicker/DateComponents/Day.test.js b/webpack/assets/javascripts/react_app/components/common/DateTimePicker/DateComponents/Day.test.js deleted file mode 100644 index ccee6ef4d7d..00000000000 --- a/webpack/assets/javascripts/react_app/components/common/DateTimePicker/DateComponents/Day.test.js +++ /dev/null @@ -1,19 +0,0 @@ -import React from 'react'; -import { shallow } from 'enzyme'; -import Day from './Day'; - -test('Day is working properly', () => { - const day = new Date('2019-01-04 14:22:31'); - const currDate = new Date('2019-01-02 12:22:31'); - const selectedDate = new Date('2019-01-05 18:22:31'); - const component = shallow( - - ); - - expect(component.render()).toMatchSnapshot(); -}); diff --git a/webpack/assets/javascripts/react_app/components/common/DateTimePicker/DateComponents/DecadeView.js b/webpack/assets/javascripts/react_app/components/common/DateTimePicker/DateComponents/DecadeView.js deleted file mode 100644 index ab8c72d4077..00000000000 --- a/webpack/assets/javascripts/react_app/components/common/DateTimePicker/DateComponents/DecadeView.js +++ /dev/null @@ -1,71 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { times } from 'lodash'; -import { addYears } from './helpers'; -import { noop } from '../../../../common/helpers'; -import { DecadeViewHeader } from './DecadeViewHeader'; -import { DecadeViewTable } from './DecadeViewTable'; -import { YEAR } from './DateConstants'; - -class DecadeView extends React.Component { - state = { - date: new Date(this.props.date), - selectedDate: new Date(this.props.date), - }; - getYearArray = () => { - const { date } = this.state; - date.setFullYear(Math.floor(date.getFullYear() / 10) * 10); - return times(12, i => addYears(date, i).getFullYear()); - }; - getPrevDecade = () => { - const { date } = this.state; - this.setState({ date: addYears(date, -10) }); - }; - getNextDecade = () => { - const { date } = this.state; - this.setState({ date: addYears(date, 10) }); - }; - setSelectedYear = year => { - const { setSelected, toggleDateView } = this.props; - const { date } = this.state; - date.setFullYear(year); - setSelected(date); - toggleDateView(YEAR); - }; - - render() { - const { date, selectedDate } = this.state; - const currDecade = Math.floor(date.getFullYear() / 10) * 10; - const selectedYear = selectedDate.getFullYear(); - const yearArray = this.getYearArray(); - return ( -
- - - -
-
- ); - } -} - -DecadeView.propTypes = { - date: PropTypes.oneOfType([PropTypes.instanceOf(Date), PropTypes.string]), - setSelected: PropTypes.func, - toggleDateView: PropTypes.func, -}; - -DecadeView.defaultProps = { - setSelected: noop, - toggleDateView: noop, - date: new Date(), -}; -export default DecadeView; diff --git a/webpack/assets/javascripts/react_app/components/common/DateTimePicker/DateComponents/DecadeView.test.js b/webpack/assets/javascripts/react_app/components/common/DateTimePicker/DateComponents/DecadeView.test.js deleted file mode 100644 index a45612aec9e..00000000000 --- a/webpack/assets/javascripts/react_app/components/common/DateTimePicker/DateComponents/DecadeView.test.js +++ /dev/null @@ -1,47 +0,0 @@ -import React from 'react'; -import { shallow, mount } from 'enzyme'; -import DecadeView from './DecadeView'; - -test('DecadeView is working properly', () => { - const date = new Date('1/1/2020 , 2:22:31 PM'); - const component = shallow(); - - expect(component.render()).toMatchSnapshot(); -}); - -test('Edit year DecadeView', () => { - const date = new Date('2/21/2019 , 2:22:31 PM'); - const setSelected = jest.fn(); - const component = mount(); - expect(component.render()).toMatchSnapshot(); - component - .find('.year') - .first() - .simulate('click'); - expect(setSelected).toBeCalledWith(new Date('2/21/2010, 2:22:31 PM')); -}); - -test('Edit decade DecadeView', () => { - const date = new Date('2/21/2019 , 2:22:31 PM'); - const setSelected = jest.fn(); - const component = mount(); - expect(component.render()).toMatchSnapshot(); - component - .find('.next') - .first() - .simulate('click'); - component - .find('.year') - .first() - .simulate('click'); - expect(setSelected).toBeCalledWith(new Date('2/21/2020 , 2:22:31 PM')); - component - .find('.prev') - .first() - .simulate('click'); - component - .find('.year') - .first() - .simulate('click'); - expect(setSelected).toBeCalledWith(new Date('2/21/2010 , 2:22:31 PM')); -}); diff --git a/webpack/assets/javascripts/react_app/components/common/DateTimePicker/DateComponents/DecadeViewHeader.js b/webpack/assets/javascripts/react_app/components/common/DateTimePicker/DateComponents/DecadeViewHeader.js deleted file mode 100644 index 5b94558ea5c..00000000000 --- a/webpack/assets/javascripts/react_app/components/common/DateTimePicker/DateComponents/DecadeViewHeader.js +++ /dev/null @@ -1,35 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { noop } from '../../../../common/helpers'; - -export const DecadeViewHeader = ({ - currDecade, - getPrevDecade, - getNextDecade, -}) => ( - - - - - - - {`${currDecade}-${currDecade + 11}`} - - - - - - -); - -DecadeViewHeader.propTypes = { - currDecade: PropTypes.number, - getPrevDecade: PropTypes.func, - getNextDecade: PropTypes.func, -}; -DecadeViewHeader.defaultProps = { - currDecade: 20, - getPrevDecade: noop, - getNextDecade: noop, -}; -export default DecadeViewHeader; diff --git a/webpack/assets/javascripts/react_app/components/common/DateTimePicker/DateComponents/DecadeViewHeader.test.js b/webpack/assets/javascripts/react_app/components/common/DateTimePicker/DateComponents/DecadeViewHeader.test.js deleted file mode 100644 index 0fba60bfb15..00000000000 --- a/webpack/assets/javascripts/react_app/components/common/DateTimePicker/DateComponents/DecadeViewHeader.test.js +++ /dev/null @@ -1,9 +0,0 @@ -import React from 'react'; -import { shallow } from 'enzyme'; -import { DecadeViewHeader } from './DecadeViewHeader'; - -test('DecadeViewHeader is working properly', () => { - const component = shallow(); - - expect(component.render()).toMatchSnapshot(); -}); diff --git a/webpack/assets/javascripts/react_app/components/common/DateTimePicker/DateComponents/DecadeViewTable.js b/webpack/assets/javascripts/react_app/components/common/DateTimePicker/DateComponents/DecadeViewTable.js deleted file mode 100644 index d55eb855345..00000000000 --- a/webpack/assets/javascripts/react_app/components/common/DateTimePicker/DateComponents/DecadeViewTable.js +++ /dev/null @@ -1,38 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { noop } from '../../../../common/helpers'; - -export const DecadeViewTable = ({ - yearArray, - selectedYear, - setSelectedYear, -}) => ( - - - - {yearArray.map(year => ( - setSelectedYear(year)} - className={`year ${year === selectedYear ? 'active' : ''}`} - key={year} - > - {year} - - ))} - - - -); - -DecadeViewTable.propTypes = { - yearArray: PropTypes.array, - selectedYear: PropTypes.number, - setSelectedYear: PropTypes.func, -}; -DecadeViewTable.defaultProps = { - yearArray: [], - selectedYear: new Date().getFullYear(), - setSelectedYear: noop, -}; - -export default DecadeViewTable; diff --git a/webpack/assets/javascripts/react_app/components/common/DateTimePicker/DateComponents/DecadeViewTable.test.js b/webpack/assets/javascripts/react_app/components/common/DateTimePicker/DateComponents/DecadeViewTable.test.js deleted file mode 100644 index 74dbd93fa9b..00000000000 --- a/webpack/assets/javascripts/react_app/components/common/DateTimePicker/DateComponents/DecadeViewTable.test.js +++ /dev/null @@ -1,27 +0,0 @@ -import React from 'react'; -import { shallow } from 'enzyme'; -import { DecadeViewTable } from './DecadeViewTable'; - -test('DecadeViewTable is working properly', () => { - const component = shallow( - - ); - - expect(component.render()).toMatchSnapshot(); -}); diff --git a/webpack/assets/javascripts/react_app/components/common/DateTimePicker/DateComponents/Header.js b/webpack/assets/javascripts/react_app/components/common/DateTimePicker/DateComponents/Header.js deleted file mode 100644 index 0e3c614e343..00000000000 --- a/webpack/assets/javascripts/react_app/components/common/DateTimePicker/DateComponents/Header.js +++ /dev/null @@ -1,71 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { Icon } from '@patternfly/react-core'; -import { AngleLeftIcon, AngleRightIcon } from '@patternfly/react-icons'; -import { YEAR } from './DateConstants'; -import { getWeekArray } from './HeaderHelpers'; - -const Header = ({ - getNextMonth, - getPrevMonth, - toggleDateView, - weekStartsOn, - date, - locale, -}) => { - date = new Date(date); - const month = Intl.DateTimeFormat(locale, { - month: 'long', - }).format(date); - const year = date.getFullYear(); - const daysOfTheWeek = getWeekArray(weekStartsOn); - return ( - - - - - - - - toggleDateView(YEAR)} - > - {month} {year} - - - - - - - - - {daysOfTheWeek.map((day, idx) => ( - - {day} - - ))} - - - ); -}; - -Header.propTypes = { - date: PropTypes.oneOfType([PropTypes.instanceOf(Date), PropTypes.string]), - getPrevMonth: PropTypes.func, - getNextMonth: PropTypes.func, - toggleDateView: PropTypes.func, - locale: PropTypes.string, - weekStartsOn: PropTypes.number, -}; - -Header.defaultProps = { - date: new Date(), - getPrevMonth: null, - getNextMonth: null, - toggleDateView: null, - locale: 'en-US', - weekStartsOn: 1, -}; -export default Header; diff --git a/webpack/assets/javascripts/react_app/components/common/DateTimePicker/DateComponents/Header.test.js b/webpack/assets/javascripts/react_app/components/common/DateTimePicker/DateComponents/Header.test.js deleted file mode 100644 index f88d12f8786..00000000000 --- a/webpack/assets/javascripts/react_app/components/common/DateTimePicker/DateComponents/Header.test.js +++ /dev/null @@ -1,17 +0,0 @@ -import React from 'react'; -import { shallow } from 'enzyme'; -import Header from './Header'; - -test('Header is working properly', () => { - const date = new Date('2019-01-04 14:22:31'); - const component = shallow(
); - - expect(component.render()).toMatchSnapshot(); -}); - -test('Header is working properly with different week start', () => { - const date = new Date('2019-01-04 14:22:31'); - const component = shallow(
); - - expect(component.render()).toMatchSnapshot(); -}); diff --git a/webpack/assets/javascripts/react_app/components/common/DateTimePicker/DateComponents/HeaderHelpers.js b/webpack/assets/javascripts/react_app/components/common/DateTimePicker/DateComponents/HeaderHelpers.js deleted file mode 100644 index 8227a4c7683..00000000000 --- a/webpack/assets/javascripts/react_app/components/common/DateTimePicker/DateComponents/HeaderHelpers.js +++ /dev/null @@ -1,16 +0,0 @@ -import { times } from 'lodash'; -import { addDays, getWeekStart } from './helpers'; - -export const getWeekArray = (weekStartsOn, locale) => { - const weekStart = getWeekStart(new Date()); - const dayFormat = - Intl.DateTimeFormat(locale, { weekday: 'short' }).format(weekStart).length > - 3 - ? 'narrow' - : 'short'; - return times(7, i => - Intl.DateTimeFormat(locale, { weekday: dayFormat }) - .format(addDays(weekStart, (i + weekStartsOn) % 7)) - .slice(0, 2) - ); -}; diff --git a/webpack/assets/javascripts/react_app/components/common/DateTimePicker/DateComponents/MonthView.js b/webpack/assets/javascripts/react_app/components/common/DateTimePicker/DateComponents/MonthView.js deleted file mode 100644 index 9ea9330ee63..00000000000 --- a/webpack/assets/javascripts/react_app/components/common/DateTimePicker/DateComponents/MonthView.js +++ /dev/null @@ -1,112 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { chunk, times } from 'lodash'; - -import Day from './Day'; -import { - addDays, - addMonths, - getMonthStart, - isEqualDate, - isWeekend, -} from './helpers'; -import Header from './Header'; - -class MonthView extends React.Component { - state = { - selectedDate: new Date(this.props.date), - date: new Date(this.props.date), - }; - - static getDerivedStateFromProps(props, state) { - const newDate = new Date(props.date); - if (newDate !== new Date(state.date)) { - return { - selectedDate: newDate, - }; - } - return null; - } - - calendarArray = date => { - const { weekStartsOn } = this.props; - const monthStart = getMonthStart(new Date(date)); - const offset = monthStart.getDay() - weekStartsOn; - return chunk( - times(35, i => addDays(monthStart, i - offset)), - 7 - ); - }; - - getPrevMonth = () => { - const { date } = this.state; - this.setState({ date: addMonths(date, -1) }); - }; - getNextMonth = () => { - const { date } = this.state; - this.setState({ date: addMonths(date, 1) }); - }; - setSelected = day => { - this.setState({ - selectedDate: day, - date: day, - }); - this.props.setSelected(day); - }; - - render() { - const { locale, weekStartsOn, toggleDateView } = this.props; - const { date, selectedDate } = this.state; - const calendar = this.calendarArray(date); - return ( -
- -
-
- {calendar.map((el, idx) => ( - - {el.map(day => ( - - ))} - - ))} - -
-
- ); - } -} - -MonthView.propTypes = { - date: PropTypes.oneOfType([PropTypes.instanceOf(Date), PropTypes.string]), - setSelected: PropTypes.func, - toggleDateView: PropTypes.func, - locale: PropTypes.string, - weekStartsOn: PropTypes.number, -}; - -MonthView.defaultProps = { - setSelected: null, - toggleDateView: null, - date: new Date(), - locale: 'en-US', - weekStartsOn: 1, -}; -export default MonthView; diff --git a/webpack/assets/javascripts/react_app/components/common/DateTimePicker/DateComponents/MonthView.test.js b/webpack/assets/javascripts/react_app/components/common/DateTimePicker/DateComponents/MonthView.test.js deleted file mode 100644 index 23703b6dddb..00000000000 --- a/webpack/assets/javascripts/react_app/components/common/DateTimePicker/DateComponents/MonthView.test.js +++ /dev/null @@ -1,9 +0,0 @@ -import React from 'react'; -import { shallow } from 'enzyme'; -import MonthView from './MonthView'; - -test('MonthView is working properly', () => { - const component = shallow(); - - expect(component.render()).toMatchSnapshot(); -}); diff --git a/webpack/assets/javascripts/react_app/components/common/DateTimePicker/DateComponents/TodayButton.js b/webpack/assets/javascripts/react_app/components/common/DateTimePicker/DateComponents/TodayButton.js deleted file mode 100644 index 26344d235bd..00000000000 --- a/webpack/assets/javascripts/react_app/components/common/DateTimePicker/DateComponents/TodayButton.js +++ /dev/null @@ -1,32 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { translate as __ } from '../../../../common/I18n'; - -const TodayButton = ({ setSelected }) => ( - - - - - - -
- -
-); - -TodayButton.propTypes = { - setSelected: PropTypes.func, -}; - -TodayButton.defaultProps = { - setSelected: null, -}; -export default TodayButton; diff --git a/webpack/assets/javascripts/react_app/components/common/DateTimePicker/DateComponents/TodayButton.test.js b/webpack/assets/javascripts/react_app/components/common/DateTimePicker/DateComponents/TodayButton.test.js deleted file mode 100644 index 0d7a4358a93..00000000000 --- a/webpack/assets/javascripts/react_app/components/common/DateTimePicker/DateComponents/TodayButton.test.js +++ /dev/null @@ -1,21 +0,0 @@ -import React from 'react'; -import { shallow } from 'enzyme'; -import TodayButton from './TodayButton'; - -const mockedDate = new Date('2/21/2019 , 3:22:31 PM'); - -global.Date = jest.fn(() => mockedDate); -global.Date.now = jest.fn(() => mockedDate); - -test('TodayButton is working properly', () => { - const component = shallow(); - - expect(component.render()).toMatchSnapshot(); -}); -test('TodayButton Click is setting the date', () => { - const setSelected = jest.fn(); - const component = shallow(); - const date = new Date(); - component.find('.today-button').simulate('click'); - expect(setSelected).toBeCalledWith(date); -}); diff --git a/webpack/assets/javascripts/react_app/components/common/DateTimePicker/DateComponents/YearView.js b/webpack/assets/javascripts/react_app/components/common/DateTimePicker/DateComponents/YearView.js deleted file mode 100644 index efb108c7e5a..00000000000 --- a/webpack/assets/javascripts/react_app/components/common/DateTimePicker/DateComponents/YearView.js +++ /dev/null @@ -1,98 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { times } from 'lodash'; -import classNames from 'classnames'; -import { addMonths, addYears } from './helpers'; -import { noop } from '../../../../common/helpers'; -import { MONTH, DAY } from './DateConstants'; - -class YearView extends React.Component { - state = { - date: new Date(this.props.date), - selectedDate: new Date(this.props.date), - }; - getMonthArray = () => { - const date = new Date('1/1/1'); - return times(12, i => - Intl.DateTimeFormat(this.props.locale, { month: 'short' }).format( - addMonths(date, i) - ) - ); - }; - getPrevYear = () => { - const { date } = this.state; - this.setState({ date: addYears(date, -1) }); - }; - getNextYear = () => { - const { date } = this.state; - this.setState({ date: addYears(date, 1) }); - }; - setSelectedMonth = month => { - const { date } = this.state; - date.setMonth(month); - this.props.setSelected(date); - this.props.toggleDateView(MONTH); - }; - - render() { - const { date, selectedDate } = this.state; - const [currMonth, currYear] = [date.getMonth(), date.getFullYear()]; - const selectedYear = selectedDate.getFullYear(); - const monthArray = this.getMonthArray(); - return ( -
- - - - - - - - - - - - - -
- - this.props.toggleDateView(DAY)} - colSpan="5" - > - {currYear} - - -
- {monthArray.map((month, idx) => ( - this.setSelectedMonth(idx)} - className={classNames('month', { - active: idx === currMonth && selectedYear === currYear, - })} - key={idx} - > - {month} - - ))} -
-
- ); - } -} - -YearView.propTypes = { - date: PropTypes.oneOfType([PropTypes.instanceOf(Date), PropTypes.string]), - setSelected: PropTypes.func, - toggleDateView: PropTypes.func, - locale: PropTypes.string, -}; - -YearView.defaultProps = { - setSelected: noop, - toggleDateView: noop, - date: new Date(), - locale: 'en-US', -}; -export default YearView; diff --git a/webpack/assets/javascripts/react_app/components/common/DateTimePicker/DateComponents/YearView.test.js b/webpack/assets/javascripts/react_app/components/common/DateTimePicker/DateComponents/YearView.test.js deleted file mode 100644 index 92382a3f846..00000000000 --- a/webpack/assets/javascripts/react_app/components/common/DateTimePicker/DateComponents/YearView.test.js +++ /dev/null @@ -1,47 +0,0 @@ -import React from 'react'; -import { shallow } from 'enzyme'; -import YearView from './YearView'; - -test('YearView is working properly', () => { - const date = new Date('2/21/2019 , 2:22:31 PM'); - const component = shallow(); - - expect(component.render()).toMatchSnapshot(); -}); - -test('Edit month YearView', () => { - const date = new Date('2/21/2019 , 2:22:31 PM'); - const setSelected = jest.fn(); - const component = shallow(); - expect(component.render()).toMatchSnapshot(); - component - .find('.month') - .first() - .simulate('click'); - expect(setSelected).toBeCalledWith(new Date('1/21/2019 , 2:22:31 PM')); -}); - -test('Edit year and month YearView', () => { - const date = new Date('2/21/2019 , 2:22:31 PM'); - const setSelected = jest.fn(); - const component = shallow(); - expect(component.render()).toMatchSnapshot(); - component - .find('.next') - .first() - .simulate('click'); - component - .find('.month') - .first() - .simulate('click'); - expect(setSelected).toBeCalledWith(new Date('1/21/2020 , 2:22:31 PM')); - component - .find('.prev') - .first() - .simulate('click'); - component - .find('.month') - .first() - .simulate('click'); - expect(setSelected).toBeCalledWith(new Date('1/21/2019 , 2:22:31 PM')); -}); diff --git a/webpack/assets/javascripts/react_app/components/common/DateTimePicker/DateComponents/__snapshots__/DateInput.test.js.snap b/webpack/assets/javascripts/react_app/components/common/DateTimePicker/DateComponents/__snapshots__/DateInput.test.js.snap deleted file mode 100644 index efe5de5ecc7..00000000000 --- a/webpack/assets/javascripts/react_app/components/common/DateTimePicker/DateComponents/__snapshots__/DateInput.test.js.snap +++ /dev/null @@ -1,550 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`DateInput is working properly 1`] = ` -
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - - - - January 2019 - - - - - - -
- Mo - - Tu - - We - - Th - - Fr - - Sa - - Su -
- 31 - - 1 - - 2 - - 3 - - 4 - - 5 - - 6 -
- 7 - - 8 - - 9 - - 10 - - 11 - - 12 - - 13 -
- 14 - - 15 - - 16 - - 17 - - 18 - - 19 - - 20 -
- 21 - - 22 - - 23 - - 24 - - 25 - - 26 - - 27 -
- 28 - - 29 - - 30 - - 31 - - 1 - - 2 - - 3 -
-
-
-`; - -exports[`DateInput toggles view to decades 1`] = ` -
-
- - - - - - - - - - - - - -
- - - 2010-2021 - - -
- - 2010 - - - 2011 - - - 2012 - - - 2013 - - - 2014 - - - 2015 - - - 2016 - - - 2017 - - - 2018 - - - 2019 - - - 2020 - - - 2021 - -
-
-
-`; - -exports[`DateInput toggles view to years 1`] = ` -
-
- - - - - - - - - - - - - -
- - - 2019 - - -
- - Jan - - - Feb - - - Mar - - - Apr - - - May - - - Jun - - - Jul - - - Aug - - - Sep - - - Oct - - - Nov - - - Dec - -
-
-
-`; diff --git a/webpack/assets/javascripts/react_app/components/common/DateTimePicker/DateComponents/__snapshots__/Day.test.js.snap b/webpack/assets/javascripts/react_app/components/common/DateTimePicker/DateComponents/__snapshots__/Day.test.js.snap deleted file mode 100644 index ba9356b1999..00000000000 --- a/webpack/assets/javascripts/react_app/components/common/DateTimePicker/DateComponents/__snapshots__/Day.test.js.snap +++ /dev/null @@ -1,10 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Day is working properly 1`] = ` - - 4 - -`; diff --git a/webpack/assets/javascripts/react_app/components/common/DateTimePicker/DateComponents/__snapshots__/DecadeView.test.js.snap b/webpack/assets/javascripts/react_app/components/common/DateTimePicker/DateComponents/__snapshots__/DecadeView.test.js.snap deleted file mode 100644 index 217de99f7b2..00000000000 --- a/webpack/assets/javascripts/react_app/components/common/DateTimePicker/DateComponents/__snapshots__/DecadeView.test.js.snap +++ /dev/null @@ -1,313 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`DecadeView is working properly 1`] = ` -
- - - - - - - - - - - - - -
- - - 2020-2031 - - -
- - 2020 - - - 2021 - - - 2022 - - - 2023 - - - 2024 - - - 2025 - - - 2026 - - - 2027 - - - 2028 - - - 2029 - - - 2030 - - - 2031 - -
-
-`; - -exports[`Edit decade DecadeView 1`] = ` -
- - - - - - - - - - - - - -
- - - 2010-2021 - - -
- - 2010 - - - 2011 - - - 2012 - - - 2013 - - - 2014 - - - 2015 - - - 2016 - - - 2017 - - - 2018 - - - 2019 - - - 2020 - - - 2021 - -
-
-`; - -exports[`Edit year DecadeView 1`] = ` -
- - - - - - - - - - - - - -
- - - 2010-2021 - - -
- - 2010 - - - 2011 - - - 2012 - - - 2013 - - - 2014 - - - 2015 - - - 2016 - - - 2017 - - - 2018 - - - 2019 - - - 2020 - - - 2021 - -
-
-`; diff --git a/webpack/assets/javascripts/react_app/components/common/DateTimePicker/DateComponents/__snapshots__/DecadeViewHeader.test.js.snap b/webpack/assets/javascripts/react_app/components/common/DateTimePicker/DateComponents/__snapshots__/DecadeViewHeader.test.js.snap deleted file mode 100644 index 5d5f6f6c704..00000000000 --- a/webpack/assets/javascripts/react_app/components/common/DateTimePicker/DateComponents/__snapshots__/DecadeViewHeader.test.js.snap +++ /dev/null @@ -1,29 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`DecadeViewHeader is working properly 1`] = ` - - - - - - - 2010-2021 - - - - - - -`; diff --git a/webpack/assets/javascripts/react_app/components/common/DateTimePicker/DateComponents/__snapshots__/DecadeViewTable.test.js.snap b/webpack/assets/javascripts/react_app/components/common/DateTimePicker/DateComponents/__snapshots__/DecadeViewTable.test.js.snap deleted file mode 100644 index eb05b08635c..00000000000 --- a/webpack/assets/javascripts/react_app/components/common/DateTimePicker/DateComponents/__snapshots__/DecadeViewTable.test.js.snap +++ /dev/null @@ -1,72 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`DecadeViewTable is working properly 1`] = ` - - - - - 2010 - - - 2011 - - - 2012 - - - 2013 - - - 2014 - - - 2015 - - - 2016 - - - 2017 - - - 2018 - - - 2019 - - - 2020 - - - 2021 - - - - -`; diff --git a/webpack/assets/javascripts/react_app/components/common/DateTimePicker/DateComponents/__snapshots__/Header.test.js.snap b/webpack/assets/javascripts/react_app/components/common/DateTimePicker/DateComponents/__snapshots__/Header.test.js.snap deleted file mode 100644 index 5e96c0d0412..00000000000 --- a/webpack/assets/javascripts/react_app/components/common/DateTimePicker/DateComponents/__snapshots__/Header.test.js.snap +++ /dev/null @@ -1,201 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Header is working properly 1`] = ` - - - - - - - - - - - January 2019 - - - - - - - - - - - - Mo - - - Tu - - - We - - - Th - - - Fr - - - Sa - - - Su - - - -`; - -exports[`Header is working properly with different week start 1`] = ` - - - - - - - - - - - January 2019 - - - - - - - - - - - - Fr - - - Sa - - - Su - - - Mo - - - Tu - - - We - - - Th - - - -`; diff --git a/webpack/assets/javascripts/react_app/components/common/DateTimePicker/DateComponents/__snapshots__/MonthView.test.js.snap b/webpack/assets/javascripts/react_app/components/common/DateTimePicker/DateComponents/__snapshots__/MonthView.test.js.snap deleted file mode 100644 index a1f6dd20c9e..00000000000 --- a/webpack/assets/javascripts/react_app/components/common/DateTimePicker/DateComponents/__snapshots__/MonthView.test.js.snap +++ /dev/null @@ -1,331 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`MonthView is working properly 1`] = ` -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - - - - January 2019 - - - - - - -
- Mo - - Tu - - We - - Th - - Fr - - Sa - - Su -
- 31 - - 1 - - 2 - - 3 - - 4 - - 5 - - 6 -
- 7 - - 8 - - 9 - - 10 - - 11 - - 12 - - 13 -
- 14 - - 15 - - 16 - - 17 - - 18 - - 19 - - 20 -
- 21 - - 22 - - 23 - - 24 - - 25 - - 26 - - 27 -
- 28 - - 29 - - 30 - - 31 - - 1 - - 2 - - 3 -
-
-`; diff --git a/webpack/assets/javascripts/react_app/components/common/DateTimePicker/DateComponents/__snapshots__/TodayButton.test.js.snap b/webpack/assets/javascripts/react_app/components/common/DateTimePicker/DateComponents/__snapshots__/TodayButton.test.js.snap deleted file mode 100644 index cf8b4fc8685..00000000000 --- a/webpack/assets/javascripts/react_app/components/common/DateTimePicker/DateComponents/__snapshots__/TodayButton.test.js.snap +++ /dev/null @@ -1,24 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`TodayButton is working properly 1`] = ` - - - - - - -
- -
-`; diff --git a/webpack/assets/javascripts/react_app/components/common/DateTimePicker/DateComponents/__snapshots__/YearView.test.js.snap b/webpack/assets/javascripts/react_app/components/common/DateTimePicker/DateComponents/__snapshots__/YearView.test.js.snap deleted file mode 100644 index d739c27f103..00000000000 --- a/webpack/assets/javascripts/react_app/components/common/DateTimePicker/DateComponents/__snapshots__/YearView.test.js.snap +++ /dev/null @@ -1,310 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Edit month YearView 1`] = ` -
- - - - - - - - - - - - - -
- - - 2019 - - -
- - Jan - - - Feb - - - Mar - - - Apr - - - May - - - Jun - - - Jul - - - Aug - - - Sep - - - Oct - - - Nov - - - Dec - -
-
-`; - -exports[`Edit year and month YearView 1`] = ` -
- - - - - - - - - - - - - -
- - - 2019 - - -
- - Jan - - - Feb - - - Mar - - - Apr - - - May - - - Jun - - - Jul - - - Aug - - - Sep - - - Oct - - - Nov - - - Dec - -
-
-`; - -exports[`YearView is working properly 1`] = ` -
- - - - - - - - - - - - - -
- - - 2019 - - -
- - Jan - - - Feb - - - Mar - - - Apr - - - May - - - Jun - - - Jul - - - Aug - - - Sep - - - Oct - - - Nov - - - Dec - -
-
-`; diff --git a/webpack/assets/javascripts/react_app/components/common/DateTimePicker/DateComponents/helpers.js b/webpack/assets/javascripts/react_app/components/common/DateTimePicker/DateComponents/helpers.js deleted file mode 100644 index c7a7275cd13..00000000000 --- a/webpack/assets/javascripts/react_app/components/common/DateTimePicker/DateComponents/helpers.js +++ /dev/null @@ -1,42 +0,0 @@ -export const addDays = (date, days) => { - const result = new Date(date); - result.setDate(result.getDate() + days); - return result; -}; - -export const addMonths = (date, months) => { - const result = new Date(date); - result.setMonth(result.getMonth() + months); - return result; -}; - -export const addYears = (date, years) => { - const result = new Date(date); - result.setYear(result.getFullYear() + years); - return result; -}; - -export const isEqualDate = (date1, date2) => - date1.getYear() === date2.getYear() && - date1.getMonth() === date2.getMonth() && - date1.getDate() === date2.getDate(); - -export const isWeekend = date => date.getDay() === 6 || date.getDay() === 5; - -export const getMonthStart = date => { - date.setDate(1); - return date; -}; - -export const getWeekStart = date => addDays(date, (7 - date.getDay()) % 7); - -export const helpers = { - addDays, - addMonths, - isEqualDate, - isWeekend, - getMonthStart, - getWeekStart, -}; - -export default helpers; diff --git a/webpack/assets/javascripts/react_app/components/common/DateTimePicker/DateComponents/helpers.test.js b/webpack/assets/javascripts/react_app/components/common/DateTimePicker/DateComponents/helpers.test.js deleted file mode 100644 index 8ddf9ebec78..00000000000 --- a/webpack/assets/javascripts/react_app/components/common/DateTimePicker/DateComponents/helpers.test.js +++ /dev/null @@ -1,73 +0,0 @@ -import { - addDays, - addMonths, - isEqualDate, - isWeekend, - getMonthStart, -} from './helpers'; - -describe('addDays ', () => { - const date = new Date('2/21/2019 , 2:22:31 PM'); - test('same month', () => { - const newDate = addDays(date, 2); - expect(newDate).toEqual(new Date('2/23/2019 , 2:22:31 PM')); - }); - test('different month', () => { - const newDate = addDays(date, 20); - expect(newDate).toEqual(new Date('3/13/2019 , 2:22:31 PM')); - }); - test('negative amount', () => { - const newDate = addDays(date, -2); - expect(newDate).toEqual(new Date('2/19/2019 , 2:22:31 PM')); - }); -}); - -describe('addMonths ', () => { - const date = new Date('2/21/2019, 2:22:31 PM'); - test('same year', () => { - const newDate = addMonths(date, 2); - expect(newDate).toEqual(new Date('4/21/2019, 2:22:31 PM')); - }); - test('different year', () => { - const newDate = addMonths(date, 13); - expect(newDate).toEqual(new Date('3/21/2020, 2:22:31 PM')); - }); - test('negative amount', () => { - const newDate = addMonths(date, -1); - expect(newDate).toEqual(new Date('1/21/2019, 2:22:31 PM')); - }); -}); - -describe('isEqualDate ', () => { - const date = new Date('2/21/2019 , 2:22:31 PM'); - test('equal', () => { - const date2 = new Date('2/21/2019 , 6:22:31 PM'); - expect(isEqualDate(date, date2)).toBeTruthy(); - }); - test('not equal', () => { - const date2 = new Date('2/22/2019 , 6:22:31 PM'); - expect(isEqualDate(date, date2)).toBeFalsy(); - }); -}); - -describe('isWeekend ', () => { - test('not weekend', () => { - const date = new Date('2/21/2019 , 6:22:31 PM'); - expect(isWeekend(date)).toBeFalsy(); - }); - test('is weekend', () => { - const date = new Date('2/23/2019 , 6:22:31 PM'); - expect(isWeekend(date)).toBeTruthy(); - }); -}); - -describe('getMonthStart ', () => { - test('already strart of the month', () => { - const date = new Date('2/1/2019 , 6:22:31 PM'); - expect(getMonthStart(date)).toEqual(new Date('2/1/2019 , 6:22:31 PM')); - }); - test('muidlle of the month', () => { - const date = new Date('2/23/2019 , 6:22:31 PM'); - expect(getMonthStart(date)).toEqual(new Date('2/1/2019 , 6:22:31 PM')); - }); -}); diff --git a/webpack/assets/javascripts/react_app/components/common/DateTimePicker/DatePicker.js b/webpack/assets/javascripts/react_app/components/common/DateTimePicker/DatePicker.js index 33ad8a23fd3..8351c83b469 100644 --- a/webpack/assets/javascripts/react_app/components/common/DateTimePicker/DatePicker.js +++ b/webpack/assets/javascripts/react_app/components/common/DateTimePicker/DatePicker.js @@ -1,92 +1,58 @@ -import React from 'react'; +import React, { useState } from 'react'; import PropTypes from 'prop-types'; -import { FormControl, InputGroup } from 'patternfly-react'; -import { Icon, Popover } from '@patternfly/react-core'; -import { CalendarAltIcon, TimesIcon } from '@patternfly/react-icons'; -import DateInput from './DateComponents/DateInput'; -import TodayButton from './DateComponents/TodayButton'; -import { formatDate } from '../../../common/helpers'; +import { DatePicker as PfDatePicker, Button } from '@patternfly/react-core'; +import { translate as __ } from '../../../common/I18n'; +import { formatDate } from './dateTimeHelpers'; import './date-time-picker.scss'; -class DatePicker extends React.Component { - get hasDefaultValue() { - const { value } = this.props; - return !!Date.parse(value); - } - - get initialDate() { - const { value } = this.props; - return this.hasDefaultValue ? new Date(value) : new Date(); - } - - state = { - value: this.initialDate, - hiddenValue: !this.hasDefaultValue, - }; - - setSelected = date => { - if (Date.parse(date)) { - const newDate = new Date(date); - this.setState({ value: newDate }); - } - }; - - render() { - const { locale, weekStartsOn, name, id, placement, required } = this.props; - const { value, hiddenValue } = this.state; - const popover = ( -
- -
  • - -
  • -
    - ); - return ( -
    - - this.setSelected(e.target.value)} - /> - this.setState({ hiddenValue: false })} - > - - - - - - - {!required && ( - - this.setState({ hiddenValue: true, value: new Date() }) - } - > - - - - - )} - -
    - ); - } -} +const DatePicker = ({ + value: initialValue, + locale, + weekStartsOn, + name, + id, + placement, + required, +}) => { + const [value, setValue] = useState(formatDate(initialValue)); + return ( +
    + setValue(newValue)} + locale={locale} + weekStart={weekStartsOn} + inputProps={{ name, id }} + popoverProps={{ + position: placement, + className: 'date-picker-popover', + footerContent: ( +
    + + +
    + ), + }} + requiredDateOptions={{ + isRequired: required, + emptyDateText: __('Date is required'), + }} + /> +
    + ); +}; DatePicker.propTypes = { value: PropTypes.oneOfType([PropTypes.instanceOf(Date), PropTypes.string]), diff --git a/webpack/assets/javascripts/react_app/components/common/DateTimePicker/DatePicker.test.js b/webpack/assets/javascripts/react_app/components/common/DateTimePicker/DatePicker.test.js index 971498ad680..4517e02a0b7 100644 --- a/webpack/assets/javascripts/react_app/components/common/DateTimePicker/DatePicker.test.js +++ b/webpack/assets/javascripts/react_app/components/common/DateTimePicker/DatePicker.test.js @@ -1,24 +1,89 @@ import React from 'react'; -import { mount } from 'enzyme'; +import { render, screen, fireEvent, act } from '@testing-library/react'; +import '@testing-library/jest-dom'; import DatePicker from './DatePicker'; describe('DatePicker', () => { - test('renders properly', () => { - const component = mount(); - expect(component.render()).toMatchSnapshot(); + test('renders properly', async () => { + const { container } = render(); + + const wrapper = container.querySelector('.date-picker-pf-wrapper'); + expect(wrapper).toBeInTheDocument(); + const calendarButton = screen.getByLabelText('Toggle date picker'); + expect(calendarButton).toBeInTheDocument(); + + await act(async () => { + fireEvent.click(calendarButton); + }); + const todayButton = screen.getByText('Today'); + expect(todayButton).toBeInTheDocument(); + const clearButton = screen.getByText('Clear'); + expect(clearButton).toBeInTheDocument(); }); test('prefils the value from prop', () => { - const component = mount(); - expect(component.render()).toMatchSnapshot(); + const testDate = new Date('2024-01-15'); + const { container } = render(); + const dateInput = container.querySelector('input'); + + expect(dateInput).toBeInTheDocument(); + expect(dateInput).toHaveValue('2024-01-15'); }); test('edit works', () => { - const component = mount(); - component - .find('input') - .simulate('change', { target: { value: '2/22/2019' } }); - expect(component.state().value).toEqual(new Date('2/22/2019')); - expect(component.render()).toMatchSnapshot(); + const { container } = render(); + + const dateInput = container.querySelector('input'); + + fireEvent.change(dateInput, { target: { value: '2024-12-25' } }); + + expect(dateInput).toHaveValue('2024-12-25'); + }); + + test('clear button clears the value', async () => { + const testDate = new Date('2024-01-15'); + const { container } = render(); + + const dateInput = container.querySelector('input'); + expect(dateInput).toHaveValue('2024-01-15'); + + const calendarButton = screen.getByLabelText('Toggle date picker'); + await act(async () => { + fireEvent.click(calendarButton); + }); + const clearButton = await screen.findByText('Clear'); + fireEvent.click(clearButton); + expect(dateInput).toHaveValue(''); + }); + + test('passes props to PatternFly DatePicker', () => { + const { container } = render( + + ); + + const dateInput = container.querySelector('input[name="test-date"]'); + expect(dateInput).toBeInTheDocument(); + expect(dateInput).toHaveAttribute('name', 'test-date'); + expect(dateInput).toHaveAttribute('id', 'test-date-picker'); + }); + + test('shows error when required field is empty and blurred', () => { + const { container } = render(); + + const dateInput = container.querySelector('input'); + + expect(screen.queryByText('Date is required')).not.toBeInTheDocument(); + fireEvent.change(dateInput, { target: { value: '' } }); + fireEvent.blur(dateInput); + expect(screen.queryByText('Date is required')).toBeInTheDocument(); + + expect(dateInput).toHaveAttribute('aria-invalid', 'true'); }); }); diff --git a/webpack/assets/javascripts/react_app/components/common/DateTimePicker/DateTimePicker.js b/webpack/assets/javascripts/react_app/components/common/DateTimePicker/DateTimePicker.js index 593e63c4fab..94e2a9318dd 100644 --- a/webpack/assets/javascripts/react_app/components/common/DateTimePicker/DateTimePicker.js +++ b/webpack/assets/javascripts/react_app/components/common/DateTimePicker/DateTimePicker.js @@ -1,124 +1,212 @@ -import React from 'react'; +import React, { useState, useEffect, useRef } from 'react'; import PropTypes from 'prop-types'; -import { FormControl, InputGroup } from 'patternfly-react'; -import { OutlinedCalendarAltIcon, TimesIcon } from '@patternfly/react-icons'; -import { Popover, Icon } from '@patternfly/react-core'; -import DateInput from './DateComponents/DateInput'; -import TodayButton from './DateComponents/TodayButton'; -import TimeInput from './TimeComponents/TimeInput'; -import { MONTH } from './DateComponents/DateConstants'; -import { noop, formatDateTime } from '../../../common/helpers'; +import { + CalendarMonth, + InputGroup, + InputGroupItem, + TextInput, + Button, + HelperText, + HelperTextItem, + Popover, +} from '@patternfly/react-core'; +import { OutlinedCalendarAltIcon } from '@patternfly/react-icons'; +import { formatDate, formatTime } from './dateTimeHelpers'; +import { noop } from '../../../common/helpers'; +import TimePicker from './TimePicker'; +import { translate as __ } from '../../../common/I18n'; import './date-time-picker.scss'; -class DateTimePicker extends React.Component { - get hasDefaultValue() { - const { value } = this.props; - return !!Date.parse(value); - } +const DateTimePicker = ({ + value: initialValue, + locale, + weekStartsOn, + name, + id, + placement, + required, + inputProps, + isFutureOnly, + onChange, // additional prop to allow for custom onChange logic +}) => { + const [isCalendarOpen, setIsCalendarOpen] = useState(false); + const [valueDate, setValueDate] = useState(formatDate(initialValue)); + const [valueTime, setValueTime] = useState(formatTime(initialValue)); + const [value, setValue] = useState(`${valueDate} ${valueTime}`); + const [errorText, setErrorText] = useState(null); - get initialDate() { - const { value } = this.props; - return this.hasDefaultValue ? new Date(value) : new Date(); - } + const intervalRef = useRef(null); + const onToggleCalendar = () => { + setIsCalendarOpen(!isCalendarOpen); + }; + const futureErrorMessage = __('Date must be in the future'); + useEffect(() => { + const updateError = () => { + if ((!valueDate || !valueTime || !value) && required) { + setErrorText(__('Date and time are required')); + } else if ( + isFutureOnly && + new Date().setSeconds(0, 0) > new Date(value).setSeconds(0, 0) + ) { + setErrorText(futureErrorMessage); + } else { + setErrorText(null); + onChange(value); + } + }; + if (intervalRef.current) { + clearInterval(intervalRef.current); + intervalRef.current = null; + } + updateError(); + intervalRef.current = setInterval(updateError, 30000); // make sure the error is updated every 30 seconds so isFutureOnly is always up to date + return () => { + if (intervalRef.current) { + clearInterval(intervalRef.current); + intervalRef.current = null; + } + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [valueDate, valueTime, value, required, isFutureOnly]); - state = { - value: this.initialDate, - typeOfDateInput: MONTH, - isTimeTableOpen: false, - hiddenValue: !this.hasDefaultValue, + const onSelectCalendar = (_event, newValueDate) => { + const newDate = formatDate(newValueDate); + setValueDate(newDate); + setIsCalendarOpen(!isCalendarOpen); + setValue(`${newDate} ${valueTime}`); }; - setSelected = date => { - if (Date.parse(date)) { - const newDate = new Date(date); - this.setState({ value: newDate }); - this.props.onChange(newDate); + const onBlur = () => { + const [date, time] = value + .replace(/\s+/g, ' ') + .trim() + .split(' '); + setValueDate(formatDate(date || '')); + setValueTime(formatTime(time || '')); + if (date && time) { + setValue(`${date} ${time}`); + } else { + setValue(''); } - this.setState({ - typeOfDateInput: MONTH, - isTimeTableOpen: false, - }); }; - - clearSelected = () => { - this.setState({ hiddenValue: true, value: new Date() }); - this.props.onChange(undefined); + const onClear = () => { + setValueDate(''); + setValue(''); + }; + const onSelectTime = newTime => { + newTime = formatTime(newTime); + const newDate = formatDate(valueDate); + setValueTime(newTime); + setValue(`${newDate} ${newTime}`); + }; + const onToday = () => { + const todayDate = formatDate(new Date()); + const todayTime = formatTime(new Date()); + setValueDate(todayDate); + setValueTime(todayTime); + setValue(`${todayDate} ${todayTime}`); }; + const futureValidator = date => { + if ( + isFutureOnly && + date.setHours(0, 0, 0, 0) < new Date().setHours(0, 0, 0, 0) + ) { + return false; + } + return true; + }; + const calendar = ( + + ); - render() { - const { - locale, - weekStartsOn, - inputProps, - id, - placement, - name, - required, - } = this.props; - const { value, typeOfDateInput, isTimeTableOpen, hiddenValue } = this.state; - const popover = ( -
    + { + setIsCalendarOpen(false); + }} + hasNoPadding + hasAutoWidth + className="date-picker-popover" + footerContent={ +
    + + +
    + } > - - -
  • - -
  • -
    - ); - return ( -
    - - this.setSelected(e.target.value)} - /> - this.setState({ hiddenValue: false })} - minWidth="500px" - > - - - - - - - {!required && ( - + + { + setValue(newValue); + setIsCalendarOpen(false); + }} + onBlur={onBlur} + isRequired={required} + validated={errorText ? 'error' : 'default'} + placeholder="YYYY-MM-DD HH:MM" + className=" pf-v5-c-form-control" + {...inputProps} + /> + + + + + + + -
    - ); - } -} + + {errorText && ( + + {errorText} + + )} + + ); +}; DateTimePicker.propTypes = { value: PropTypes.oneOfType([PropTypes.instanceOf(Date), PropTypes.string]), @@ -129,6 +217,7 @@ DateTimePicker.propTypes = { placement: PropTypes.string, name: PropTypes.string, required: PropTypes.bool, + isFutureOnly: PropTypes.bool, onChange: PropTypes.func, }; @@ -141,6 +230,7 @@ DateTimePicker.defaultProps = { placement: 'top', name: undefined, required: false, + isFutureOnly: false, onChange: noop, }; export default DateTimePicker; diff --git a/webpack/assets/javascripts/react_app/components/common/DateTimePicker/DateTimePicker.test.js b/webpack/assets/javascripts/react_app/components/common/DateTimePicker/DateTimePicker.test.js index b9a0435edd1..fd27bab36f6 100644 --- a/webpack/assets/javascripts/react_app/components/common/DateTimePicker/DateTimePicker.test.js +++ b/webpack/assets/javascripts/react_app/components/common/DateTimePicker/DateTimePicker.test.js @@ -1,24 +1,268 @@ import React from 'react'; -import { mount } from 'enzyme'; +import { render, screen, fireEvent, act } from '@testing-library/react'; +import '@testing-library/jest-dom'; import DateTimePicker from './DateTimePicker'; describe('DateTimePicker', () => { - test('renders properly', () => { - const component = mount(); - expect(component.render()).toMatchSnapshot(); + test('renders properly', async () => { + const { container } = render(); + + const dateTimeInput = screen.getByLabelText('date and time picker'); + expect(dateTimeInput).toBeInTheDocument(); + + const calendarButton = screen.getByLabelText('Toggle date picker'); + expect(calendarButton).toBeInTheDocument(); + + const wrapper = container.querySelector('.date-time-picker-pf-wrapper'); + expect(wrapper).toBeInTheDocument(); + expect(calendarButton).toBeInTheDocument(); + await act(async () => { + fireEvent.click(calendarButton); + }); + const todayButton = screen.getByText('Today'); + expect(todayButton).toBeInTheDocument(); + const clearButton = screen.getByText('Clear'); + expect(clearButton).toBeInTheDocument(); }); test('prefils the value from prop', () => { - const component = mount(); - expect(component.render()).toMatchSnapshot(); + const testDate = new Date('2024-01-15T14:30:00'); + const { container } = render(); + + const dateTimeInput = container.querySelector( + 'input[aria-label="date and time picker"]' + ); + expect(dateTimeInput).toBeInTheDocument(); + expect(dateTimeInput).toHaveValue('2024-01-15 14:30'); }); test('edit works', () => { - const component = mount(); - component - .find('input') - .simulate('change', { target: { value: '2/22/2019 , 2:22:31 PM' } }); - expect(component.state().value).toEqual(new Date('2/22/2019 , 2:22:31 PM')); - expect(component.render()).toMatchSnapshot(); + const { container } = render(); + + const dateTimeInput = container.querySelector( + 'input[aria-label="date and time picker"]' + ); + fireEvent.change(dateTimeInput, { target: { value: '2024-12-25 16:45' } }); + + expect(dateTimeInput).toHaveValue('2024-12-25 16:45'); + }); + + test('clear button clears the value', async () => { + const testDate = new Date('2024-01-15T14:30:00'); + const { container } = render(); + + const dateTimeInput = container.querySelector( + 'input[aria-label="date and time picker"]' + ); + const calendarButton = screen.getByLabelText('Toggle date picker'); + + expect(dateTimeInput).toHaveValue('2024-01-15 14:30'); + await act(async () => { + fireEvent.click(calendarButton); + }); + const clearButton = screen.getByText('Clear'); + fireEvent.click(clearButton); + expect(dateTimeInput).toHaveValue(''); + }); + + test('calendar toggle button works', async () => { + render(); + + const calendarButton = screen.getByLabelText('Toggle date picker'); + expect(calendarButton).toBeInTheDocument(); + expect(screen.queryByRole('dialog')).toBeNull(); + await act(async () => { + fireEvent.click(calendarButton); + }); + expect(screen.queryByRole('dialog')).toBeVisible(); + }); + + test('shows error when required field is empty', async () => { + const { container } = render(); + + const dateTimeInput = container.querySelector( + 'input[aria-label="date and time picker"]' + ); + const calendarButton = screen.getByLabelText('Toggle date picker'); + + expect( + screen.queryByText('Date and time are required') + ).not.toBeInTheDocument(); + await act(async () => { + fireEvent.click(calendarButton); + }); + const clearButton = screen.getByText('Clear'); + fireEvent.click(clearButton); + expect( + screen.queryByText('Date and time are required') + ).toBeInTheDocument(); + expect(dateTimeInput).toHaveAttribute('aria-invalid', 'true'); + }); + + test('passes props to components', () => { + const { container } = render( + + ); + + const dateTimeInput = container.querySelector( + 'input[name="test-datetime"]' + ); + + expect(dateTimeInput).toBeInTheDocument(); + expect(dateTimeInput).toHaveAttribute('name', 'test-datetime'); + expect(dateTimeInput).toHaveAttribute('id', 'test-datetime-picker'); + expect(dateTimeInput).toHaveAttribute('required', ''); + }); + + test('renders TimePicker component', () => { + const { container } = render(); + + const timePickerInput = container.querySelector( + 'input[aria-label="Time picker"]' + ); + expect(timePickerInput).toBeInTheDocument(); + }); + + test('handles onBlur to format date and time', () => { + const { container } = render(); + + const dateTimeInput = container.querySelector( + 'input[aria-label="date and time picker"]' + ); + + fireEvent.change(dateTimeInput, { target: { value: '2024-01-15 14:30' } }); + expect(dateTimeInput).toHaveValue('2024-01-15 14:30'); + fireEvent.blur(dateTimeInput); + + expect(dateTimeInput).toHaveValue('2024-01-15 14:30'); + }); + + describe('isFutureOnly validation', () => { + test('shows error when past date is entered with isFutureOnly', () => { + const pastDate = new Date(); + pastDate.setFullYear(pastDate.getFullYear() - 1); + const pastDateString = `${pastDate.getFullYear()}-${String( + pastDate.getMonth() + 1 + ).padStart(2, '0')}-${String(pastDate.getDate()).padStart(2, '0')} 14:30`; + + const { container } = render(); + + const dateTimeInput = container.querySelector( + 'input[aria-label="date and time picker"]' + ); + expect( + screen.queryByText('Date must be in the future') + ).not.toBeInTheDocument(); + fireEvent.change(dateTimeInput, { target: { value: pastDateString } }); + fireEvent.blur(dateTimeInput); + + expect( + screen.queryByText('Date must be in the future') + ).toBeInTheDocument(); + expect(dateTimeInput).toHaveAttribute('aria-invalid', 'true'); + }); + + test('does not show error when future date is entered with isFutureOnly', () => { + const futureDate = new Date(); + futureDate.setFullYear(futureDate.getFullYear() + 1); + const futureDateString = `${futureDate.getFullYear()}-${String( + futureDate.getMonth() + 1 + ).padStart(2, '0')}-${String(futureDate.getDate()).padStart( + 2, + '0' + )} 14:30`; + + const { container } = render(); + + const dateTimeInput = container.querySelector( + 'input[aria-label="date and time picker"]' + ); + + fireEvent.change(dateTimeInput, { target: { value: futureDateString } }); + fireEvent.blur(dateTimeInput); + + expect( + screen.queryByText('Date must be in the future') + ).not.toBeInTheDocument(); + expect(dateTimeInput).not.toHaveAttribute('aria-invalid', 'true'); + }); + + test('allows past dates when isFutureOnly is false', () => { + const pastDate = new Date(); + pastDate.setFullYear(pastDate.getFullYear() - 1); + const pastDateString = `${pastDate.getFullYear()}-${String( + pastDate.getMonth() + 1 + ).padStart(2, '0')}-${String(pastDate.getDate()).padStart(2, '0')} 14:30`; + + const { container } = render(); + + const dateTimeInput = container.querySelector( + 'input[aria-label="date and time picker"]' + ); + + fireEvent.change(dateTimeInput, { target: { value: pastDateString } }); + fireEvent.blur(dateTimeInput); + + expect( + screen.queryByText('Date must be in the future') + ).not.toBeInTheDocument(); + }); + + test('shows error when current date with past time is entered with isFutureOnly', () => { + const now = new Date(); + const pastTime = new Date(now); + pastTime.setHours(now.getHours() - 1); + const pastTimeString = `${now.getFullYear()}-${String( + now.getMonth() + 1 + ).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')} ${String( + pastTime.getHours() + ).padStart(2, '0')}:${String(pastTime.getMinutes()).padStart(2, '0')}`; + + const { container } = render(); + + const dateTimeInput = container.querySelector( + 'input[aria-label="date and time picker"]' + ); + + fireEvent.change(dateTimeInput, { target: { value: pastTimeString } }); + fireEvent.blur(dateTimeInput); + + expect( + screen.queryByText('Date must be in the future') + ).toBeInTheDocument(); + expect(dateTimeInput).toHaveAttribute('aria-invalid', 'true'); + }); + + test('does not show error when current date with future time is entered with isFutureOnly', () => { + const now = new Date(); + const futureTime = new Date(now); + futureTime.setHours(now.getHours() + 1); + const futureTimeString = `${now.getFullYear()}-${String( + now.getMonth() + 1 + ).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')} ${String( + futureTime.getHours() + ).padStart(2, '0')}:${String(futureTime.getMinutes()).padStart(2, '0')}`; + + const { container } = render(); + + const dateTimeInput = container.querySelector( + 'input[aria-label="date and time picker"]' + ); + + fireEvent.change(dateTimeInput, { target: { value: futureTimeString } }); + fireEvent.blur(dateTimeInput); + + expect( + screen.queryByText('Date must be in the future') + ).not.toBeInTheDocument(); + expect(dateTimeInput).not.toHaveAttribute('aria-invalid', 'true'); + }); }); }); diff --git a/webpack/assets/javascripts/react_app/components/common/DateTimePicker/TimeComponents/PickTimeClock.js b/webpack/assets/javascripts/react_app/components/common/DateTimePicker/TimeComponents/PickTimeClock.js deleted file mode 100644 index e0bef572ab9..00000000000 --- a/webpack/assets/javascripts/react_app/components/common/DateTimePicker/TimeComponents/PickTimeClock.js +++ /dev/null @@ -1,120 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { noop } from '../../../../common/helpers'; -import { HOUR, MINUTE } from './TimeConstants'; - -class PickTimeClock extends React.Component { - state = { - ampm: this.props.time.getHours() >= 12 ? 'PM' : 'AM', - }; - componentDidUpdate = prevProps => { - const newTime = this.props.time; - if (prevProps.time !== newTime) { - this.setAMPM(newTime); - } - }; - setAMPM = time => { - this.setState({ ampm: time.getHours() >= 12 ? 'PM' : 'AM' }); - }; - setTime = (type, amount) => { - const { time } = this.props; - if (type === HOUR) { - time.setHours(time.getHours() + amount); - } else if (type === MINUTE) { - time.setMinutes(time.getMinutes() + amount); - } - this.props.setSelected(time); - }; - toggleAMPM = () => { - const { time } = this.props; - if (this.state.ampm === 'AM') { - time.setHours(time.getHours() + 12); - this.setState({ ampm: 'PM' }); - } else { - time.setHours(time.getHours() - 12); - this.setState({ ampm: 'AM' }); - } - this.props.setSelected(time); - }; - render() { - const { time, toggleTimeTable } = this.props; - const minutes = time.getMinutes(); - const hours = time.getHours() % 12 || 12; - - return ( -
    - - - - - - - - - - - - - - - - - -
    this.setTime(HOUR, 1)}> - - - - - this.setTime(MINUTE, 1)}> - - - - -
    toggleTimeTable(HOUR)}> - - {`${hours}`.padStart(2, '0')} - - : toggleTimeTable(MINUTE)}> - - {`${minutes}`.padStart(2, '0')} - - - -
    - this.setTime(HOUR, -1)} - > - - - - - this.setTime(MINUTE, -1)} - > - - - -
    -
    - ); - } -} - -PickTimeClock.propTypes = { - time: PropTypes.instanceOf(Date).isRequired, - setSelected: PropTypes.func, - toggleTimeTable: PropTypes.func, -}; -PickTimeClock.defaultProps = { - setSelected: noop, - toggleTimeTable: noop, -}; -export default PickTimeClock; diff --git a/webpack/assets/javascripts/react_app/components/common/DateTimePicker/TimeComponents/PickTimeClock.test.js b/webpack/assets/javascripts/react_app/components/common/DateTimePicker/TimeComponents/PickTimeClock.test.js deleted file mode 100644 index b98ac585b97..00000000000 --- a/webpack/assets/javascripts/react_app/components/common/DateTimePicker/TimeComponents/PickTimeClock.test.js +++ /dev/null @@ -1,71 +0,0 @@ -import React from 'react'; -import { shallow, mount } from 'enzyme'; -import PickTimeClock from './PickTimeClock'; -import { MINUTE, HOUR } from './TimeConstants'; - -test('PickTimeClock is working properly', () => { - const time = new Date('2/21/2019 , 2:22:31 PM'); - const component = shallow(); - - expect(component.render()).toMatchSnapshot(); -}); - -test('Edit minutes of PickTimeClock', () => { - const time = new Date('2/21/2019 , 2:22:31 PM'); - const setSelected = jest.fn(); - const component = mount( - - ); - expect(component.render()).toMatchSnapshot(); - component.find('.increment-min').simulate('click'); - expect(setSelected).toBeCalledWith(new Date('2/21/2019 , 2:23:31 PM')); - component.find('.decrement-min').simulate('click'); - component.find('.decrement-min').simulate('click'); - expect(setSelected).toBeCalledWith(new Date('2/21/2019 , 2:21:31 PM')); -}); - -test('Edit hours of PickTimeClock', () => { - const time = new Date('2/21/2019 , 2:22:31 PM'); - const setSelected = jest.fn(); - const component = mount( - - ); - expect(component.render()).toMatchSnapshot(); - component.find('.increment-hour').simulate('click'); - expect(setSelected).toBeCalledWith(new Date('2/21/2019 , 3:22:31 PM')); - component.find('.decrement-hour').simulate('click'); - component.find('.decrement-hour').simulate('click'); - expect(setSelected).toBeCalledWith(new Date('2/21/2019 , 1:22:31 PM')); -}); - -test('Toggle hours of PickTimeClock', () => { - const time = new Date('2/21/2019 , 12:22:31 PM'); - const component = mount(); - expect(component.state().ampm).toEqual('PM'); - component.find('.ampm-toggle').simulate('click'); - expect(component.state().ampm).toEqual('AM'); - expect(component.render()).toMatchSnapshot(); - component.find('.ampm-toggle').simulate('click'); - expect(component.state().ampm).toEqual('PM'); -}); -test('Toggle TimeTable hour from PickTimeClock', () => { - const time = new Date('2/21/2019 , 12:22:31 PM'); - const toggleTimeTable = jest.fn(); - const component = mount( - - ); - - component.find('.timepicker-hour').simulate('click'); - expect(toggleTimeTable).toBeCalledWith(HOUR); -}); - -test('Toggle TimeTable minute from PickTimeClock', () => { - const time = new Date('2/21/2019 , 12:22:31 PM'); - const toggleTimeTable = jest.fn(); - const component = mount( - - ); - - component.find('.timepicker-minute').simulate('click'); - expect(toggleTimeTable).toBeCalledWith(MINUTE); -}); diff --git a/webpack/assets/javascripts/react_app/components/common/DateTimePicker/TimeComponents/PickTimeTable.js b/webpack/assets/javascripts/react_app/components/common/DateTimePicker/TimeComponents/PickTimeTable.js deleted file mode 100644 index e61d982cb68..00000000000 --- a/webpack/assets/javascripts/react_app/components/common/DateTimePicker/TimeComponents/PickTimeTable.js +++ /dev/null @@ -1,65 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { noop } from '../../../../common/helpers'; -import { HOUR } from './TimeConstants'; - -class PickTimeTable extends React.Component { - setTime = (newTime, type) => { - const { time, setSelected, toggleTimeTable } = this.props; - const hours = time.getHours(); - newTime = parseInt(newTime, 10); - if (type === 'minute') time.setMinutes(newTime); - else if (type === 'hour') { - time.setHours(hours < 12 ? newTime % 12 : (newTime % 12) + 12); - } - setSelected(time); - toggleTimeTable(); - }; - getTimeTable = (array, type) => ( -
    - - - {array.map((row, idx) => ( - - {row.map(hour => ( - - ))} - - ))} - -
    this.setTime(hour, type)} - > - {hour} -
    -
    - ); - render() { - const hoursArray = [ - ['12', '01', '02', '03'], - ['04', '05', '06', '07'], - ['08', '09', '10', '11'], - ]; - const minutesArray = [ - ['00', '05', '10', '15'], - ['20', '25', '30', '35'], - ['40', '45', '50', '55'], - ]; - return this.props.type === HOUR - ? this.getTimeTable(hoursArray, 'hour') - : this.getTimeTable(minutesArray, 'minute'); - } -} -PickTimeTable.propTypes = { - time: PropTypes.instanceOf(Date).isRequired, - setSelected: PropTypes.func, - toggleTimeTable: PropTypes.func, - type: PropTypes.string.isRequired, -}; -PickTimeTable.defaultProps = { - setSelected: noop, - toggleTimeTable: noop, -}; -export default PickTimeTable; diff --git a/webpack/assets/javascripts/react_app/components/common/DateTimePicker/TimeComponents/PickTimeTable.test.js b/webpack/assets/javascripts/react_app/components/common/DateTimePicker/TimeComponents/PickTimeTable.test.js deleted file mode 100644 index 9cbd183db0c..00000000000 --- a/webpack/assets/javascripts/react_app/components/common/DateTimePicker/TimeComponents/PickTimeTable.test.js +++ /dev/null @@ -1,32 +0,0 @@ -import React from 'react'; -import { shallow } from 'enzyme'; -import PickTimeTable from './PickTimeTable'; -import { MINUTE, HOUR } from './TimeConstants'; - -test('PickTimeTable is working properly for Minute', () => { - const time = new Date('2019-01-04 14:22:31'); - const setSelected = jest.fn(); - const component = shallow( - - ); - expect(component.render()).toMatchSnapshot(); - component - .find('.minute') - .first() - .simulate('click'); - expect(setSelected).toBeCalledWith(new Date('2019-01-04 14:00:31')); -}); - -test('PickTimeTable is working properly for Hour', () => { - const time = new Date('2019-01-04 14:22:31'); - const setSelected = jest.fn(); - const component = shallow( - - ); - expect(component.render()).toMatchSnapshot(); - component - .find('.hour') - .first() - .simulate('click'); - expect(setSelected).toBeCalledWith(new Date('2019-01-04 12:22:31')); -}); diff --git a/webpack/assets/javascripts/react_app/components/common/DateTimePicker/TimeComponents/TimeConstants.js b/webpack/assets/javascripts/react_app/components/common/DateTimePicker/TimeComponents/TimeConstants.js deleted file mode 100644 index c3ed6128466..00000000000 --- a/webpack/assets/javascripts/react_app/components/common/DateTimePicker/TimeComponents/TimeConstants.js +++ /dev/null @@ -1,2 +0,0 @@ -export const MINUTE = 'MINUTE'; -export const HOUR = 'HOUR'; diff --git a/webpack/assets/javascripts/react_app/components/common/DateTimePicker/TimeComponents/TimeInput.js b/webpack/assets/javascripts/react_app/components/common/DateTimePicker/TimeComponents/TimeInput.js deleted file mode 100644 index 8e8f9fc3c5e..00000000000 --- a/webpack/assets/javascripts/react_app/components/common/DateTimePicker/TimeComponents/TimeInput.js +++ /dev/null @@ -1,65 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import PickTimeTable from './PickTimeTable'; -import PickTimeClock from './PickTimeClock'; -import { noop } from '../../../../common/helpers'; -import { HOUR } from './TimeConstants'; - -class TimeInput extends React.Component { - state = { - isTimeTableOpen: this.props.isTimeTableOpen, - typeOfTimeInput: HOUR, - }; - componentDidUpdate = prevProps => { - const { time: nextTime, isTimeTableOpen } = this.props; - if (prevProps.time !== nextTime) { - this.setIsTimeTableOpen(isTimeTableOpen); - } - }; - setIsTimeTableOpen = isTimeTableOpen => { - this.setState({ - isTimeTableOpen, - }); - }; - toggleTimeTable = type => { - this.setState({ - typeOfTimeInput: type, - isTimeTableOpen: !this.state.isTimeTableOpen, - }); - }; - render() { - const { time, setSelected } = this.props; - const { typeOfTimeInput, isTimeTableOpen } = this.state; - return ( -
    - {isTimeTableOpen ? ( - - ) : ( - - )} -
    - ); - } -} - -TimeInput.propTypes = { - setSelected: PropTypes.func, - time: PropTypes.oneOfType([PropTypes.instanceOf(Date), PropTypes.string]), - isTimeTableOpen: PropTypes.bool, -}; -TimeInput.defaultProps = { - setSelected: noop, - time: new Date(), - isTimeTableOpen: false, -}; -export default TimeInput; diff --git a/webpack/assets/javascripts/react_app/components/common/DateTimePicker/TimeComponents/TimeInput.test.js b/webpack/assets/javascripts/react_app/components/common/DateTimePicker/TimeComponents/TimeInput.test.js deleted file mode 100644 index 221fe4803e3..00000000000 --- a/webpack/assets/javascripts/react_app/components/common/DateTimePicker/TimeComponents/TimeInput.test.js +++ /dev/null @@ -1,28 +0,0 @@ -import React from 'react'; -import { shallow, mount } from 'enzyme'; -import TimeInput from './TimeInput'; - -test('TimeInput is working properly', () => { - const time = new Date('2019-01-04 14:22:31'); - const component = shallow(); - - expect(component.render()).toMatchSnapshot(); -}); -test('TimeInput toggles view to hours', () => { - const time = new Date('2019-01-04 14:22:31'); - const component = mount(); - component - .find('.timepicker-hour') - .first() - .simulate('click'); - expect(component.render()).toMatchSnapshot(); -}); -test('TimeInput toggles view to minutes', () => { - const time = new Date('2019-01-04 14:22:31'); - const component = mount(); - component - .find('.timepicker-minute') - .first() - .simulate('click'); - expect(component.render()).toMatchSnapshot(); -}); diff --git a/webpack/assets/javascripts/react_app/components/common/DateTimePicker/TimeComponents/__snapshots__/PickTimeClock.test.js.snap b/webpack/assets/javascripts/react_app/components/common/DateTimePicker/TimeComponents/__snapshots__/PickTimeClock.test.js.snap deleted file mode 100644 index b14059a4546..00000000000 --- a/webpack/assets/javascripts/react_app/components/common/DateTimePicker/TimeComponents/__snapshots__/PickTimeClock.test.js.snap +++ /dev/null @@ -1,369 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Edit hours of PickTimeClock 1`] = ` -
    - - - - - - - - - - - - - - - - - -
    - - - - - - - - - -
    - - 02 - - - : - - - 22 - - - -
    - - - - - - - - - -
    -
    -`; - -exports[`Edit minutes of PickTimeClock 1`] = ` -
    - - - - - - - - - - - - - - - - - -
    - - - - - - - - - -
    - - 02 - - - : - - - 22 - - - -
    - - - - - - - - - -
    -
    -`; - -exports[`PickTimeClock is working properly 1`] = ` -
    - - - - - - - - - - - - - - - - - -
    - - - - - - - - - -
    - - 02 - - - : - - - 22 - - - -
    - - - - - - - - - -
    -
    -`; - -exports[`Toggle hours of PickTimeClock 1`] = ` -
    - - - - - - - - - - - - - - - - - -
    - - - - - - - - - -
    - - 12 - - - : - - - 22 - - - -
    - - - - - - - - - -
    -
    -`; diff --git a/webpack/assets/javascripts/react_app/components/common/DateTimePicker/TimeComponents/__snapshots__/PickTimeTable.test.js.snap b/webpack/assets/javascripts/react_app/components/common/DateTimePicker/TimeComponents/__snapshots__/PickTimeTable.test.js.snap deleted file mode 100644 index 8020c36d2df..00000000000 --- a/webpack/assets/javascripts/react_app/components/common/DateTimePicker/TimeComponents/__snapshots__/PickTimeTable.test.js.snap +++ /dev/null @@ -1,159 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`PickTimeTable is working properly for Hour 1`] = ` -
    - - - - - - - - - - - - - - - - - - - - - -
    - 12 - - 01 - - 02 - - 03 -
    - 04 - - 05 - - 06 - - 07 -
    - 08 - - 09 - - 10 - - 11 -
    -
    -`; - -exports[`PickTimeTable is working properly for Minute 1`] = ` -
    - - - - - - - - - - - - - - - - - - - - - -
    - 00 - - 05 - - 10 - - 15 -
    - 20 - - 25 - - 30 - - 35 -
    - 40 - - 45 - - 50 - - 55 -
    -
    -`; diff --git a/webpack/assets/javascripts/react_app/components/common/DateTimePicker/TimeComponents/__snapshots__/TimeInput.test.js.snap b/webpack/assets/javascripts/react_app/components/common/DateTimePicker/TimeComponents/__snapshots__/TimeInput.test.js.snap deleted file mode 100644 index c56331a814d..00000000000 --- a/webpack/assets/javascripts/react_app/components/common/DateTimePicker/TimeComponents/__snapshots__/TimeInput.test.js.snap +++ /dev/null @@ -1,263 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`TimeInput is working properly 1`] = ` -
    -
    - - - - - - - - - - - - - - - - - -
    - - - - - - - - - -
    - - 02 - - - : - - - 22 - - - -
    - - - - - - - - - -
    -
    -
    -`; - -exports[`TimeInput toggles view to hours 1`] = ` -
    -
    - - - - - - - - - - - - - - - - - - - - - -
    - 12 - - 01 - - 02 - - 03 -
    - 04 - - 05 - - 06 - - 07 -
    - 08 - - 09 - - 10 - - 11 -
    -
    -
    -`; - -exports[`TimeInput toggles view to minutes 1`] = ` -
    -
    - - - - - - - - - - - - - - - - - - - - - -
    - 00 - - 05 - - 10 - - 15 -
    - 20 - - 25 - - 30 - - 35 -
    - 40 - - 45 - - 50 - - 55 -
    -
    -
    -`; diff --git a/webpack/assets/javascripts/react_app/components/common/DateTimePicker/TimePicker.js b/webpack/assets/javascripts/react_app/components/common/DateTimePicker/TimePicker.js index 0d343f2a747..bb1870874b7 100644 --- a/webpack/assets/javascripts/react_app/components/common/DateTimePicker/TimePicker.js +++ b/webpack/assets/javascripts/react_app/components/common/DateTimePicker/TimePicker.js @@ -1,88 +1,34 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { FormControl, InputGroup } from 'patternfly-react'; -import { Icon, Popover } from '@patternfly/react-core'; -import { OutlinedClockIcon } from '@patternfly/react-icons'; -import TimeInput from './TimeComponents/TimeInput'; +import { TimePicker as PfTimePicker } from '@patternfly/react-core'; +import { formatTime } from './dateTimeHelpers'; -class TimePicker extends React.Component { - getDateFromTime = time => { - if (Date.parse(time)) { - return new Date(time); - } - return new Date(`1/1/1 ${time}`); - }; - state = { - value: this.getDateFromTime(this.props.value), - }; - formatDate = () => { - const { locale } = this.props; - const { value } = this.state; - const options = { hour: 'numeric', minute: 'numeric' }; - return value.toLocaleString(locale, options); - }; - setSelected = date => { - if (Date.parse(date)) { - const newDate = new Date(date); - this.setState({ value: newDate }); - } else if (Date.parse(`1/1/1 ${date}`)) { - const newDate = new Date(`1/1/1 ${date}`); - this.setState({ value: newDate }); - } - }; - render() { - const { locale } = this.props; - const popover = ( -
    -
      -
    • - - - - -
      -
    • -
    • - -
    • -
    -
    - ); - return ( -
    - - - this.setSelected(e.target.value)} - /> - - - - - - - -
    - ); - } -} +// Note that PF time picker input can not be cleared / set to empty string +const TimePicker = ({ value: initialValue, locale, name, id, onChange }) => ( + { + if (onChange) onChange(value); + }} + locale={locale} + inputProps={{ name, id }} + is24Hour + /> +); TimePicker.propTypes = { value: PropTypes.oneOfType([PropTypes.instanceOf(Date), PropTypes.string]), locale: PropTypes.string, + name: PropTypes.string, + id: PropTypes.string, + onChange: PropTypes.func, }; TimePicker.defaultProps = { - value: new Date(), + value: null, locale: 'en-US', + name: '', + id: '', + onChange: null, }; export default TimePicker; diff --git a/webpack/assets/javascripts/react_app/components/common/DateTimePicker/TimePicker.test.js b/webpack/assets/javascripts/react_app/components/common/DateTimePicker/TimePicker.test.js index 11c1d4aec25..4eeff70e308 100644 --- a/webpack/assets/javascripts/react_app/components/common/DateTimePicker/TimePicker.test.js +++ b/webpack/assets/javascripts/react_app/components/common/DateTimePicker/TimePicker.test.js @@ -1,24 +1,58 @@ import React from 'react'; -import { mount } from 'enzyme'; +import { render, screen, fireEvent } from '@testing-library/react'; +import '@testing-library/jest-dom'; import TimePicker from './TimePicker'; -test('TimePicker is working properly', () => { - const component = mount(); +describe('TimePicker', () => { + test('renders properly', () => { + render(); + + const timeInput = screen.getByLabelText('Time picker'); + expect(timeInput).toBeInTheDocument(); + }); - expect(component.render()).toMatchSnapshot(); -}); + test('prefils the value from prop', () => { + const testTime = '14:30'; + const { container } = render(); + + const timeInput = container.querySelector('input'); + expect(timeInput).toBeInTheDocument(); + expect(timeInput).toHaveValue(testTime); + }); -test('TimePicker is working properly with time only', () => { - const component = mount(); + test('edit works', () => { + const testTime = '14:30'; + const { container } = render(); + const timeInput = container.querySelector('input'); + expect(timeInput).toHaveValue(testTime); + fireEvent.change(timeInput, { target: { value: '16:45' } }); + expect(timeInput).toHaveValue('16:45'); + }); - expect(component.render()).toMatchSnapshot(); -}); + test('calls onChange callback when provided', () => { + const onChange = jest.fn(); + const initialTime = '14:30'; + const { container } = render(); + + const timeInput = container.querySelector('input'); + fireEvent.change(timeInput, { target: { value: '18:00' } }); + + expect(onChange).toHaveBeenCalledWith('18:00'); + }); -test('Edit form of TimePicker', () => { - const component = mount(); - component - .find('input') - .simulate('change', { target: { value: '2:42 PM ' } }); - expect(component.render()).toMatchSnapshot(); - expect(component.state().value).toEqual(new Date('1/1/1 2:42:00 PM ')); + test('passes props to PatternFly TimePicker', () => { + const { container } = render( + + ); + + const timeInput = container.querySelector('input[name="test-time"]'); + + expect(timeInput).toBeInTheDocument(); + expect(timeInput).toHaveAttribute('name', 'test-time'); + expect(timeInput).toHaveAttribute('id', 'test-time-picker'); + }); }); diff --git a/webpack/assets/javascripts/react_app/components/common/DateTimePicker/__snapshots__/DatePicker.test.js.snap b/webpack/assets/javascripts/react_app/components/common/DateTimePicker/__snapshots__/DatePicker.test.js.snap deleted file mode 100644 index cf354d84106..00000000000 --- a/webpack/assets/javascripts/react_app/components/common/DateTimePicker/__snapshots__/DatePicker.test.js.snap +++ /dev/null @@ -1,208 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`DatePicker edit works 1`] = ` -
    - - -
    - - - - - - - -
    - - - - - - - -
    -
    -`; - -exports[`DatePicker prefils the value from prop 1`] = ` -
    - - -
    - - - - - - - -
    - - - - - - - -
    -
    -`; - -exports[`DatePicker renders properly 1`] = ` -
    - - -
    - - - - - - - -
    - - - - - - - -
    -
    -`; diff --git a/webpack/assets/javascripts/react_app/components/common/DateTimePicker/__snapshots__/DateTimePicker.test.js.snap b/webpack/assets/javascripts/react_app/components/common/DateTimePicker/__snapshots__/DateTimePicker.test.js.snap deleted file mode 100644 index 89582eec018..00000000000 --- a/webpack/assets/javascripts/react_app/components/common/DateTimePicker/__snapshots__/DateTimePicker.test.js.snap +++ /dev/null @@ -1,208 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`DateTimePicker edit works 1`] = ` -
    - - -
    - - - - - - - -
    - - - - - - - -
    -
    -`; - -exports[`DateTimePicker prefils the value from prop 1`] = ` -
    - - -
    - - - - - - - -
    - - - - - - - -
    -
    -`; - -exports[`DateTimePicker renders properly 1`] = ` -
    - - -
    - - - - - - - -
    - - - - - - - -
    -
    -`; diff --git a/webpack/assets/javascripts/react_app/components/common/DateTimePicker/__snapshots__/TimePicker.test.js.snap b/webpack/assets/javascripts/react_app/components/common/DateTimePicker/__snapshots__/TimePicker.test.js.snap deleted file mode 100644 index 588018af2bd..00000000000 --- a/webpack/assets/javascripts/react_app/components/common/DateTimePicker/__snapshots__/TimePicker.test.js.snap +++ /dev/null @@ -1,133 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Edit form of TimePicker 1`] = ` -
    -
    - - - - - - - - - - -
    -
    -`; - -exports[`TimePicker is working properly 1`] = ` -
    -
    - - - - - - - - - - -
    -
    -`; - -exports[`TimePicker is working properly with time only 1`] = ` -
    -
    - - - - - - - - - - -
    -
    -`; diff --git a/webpack/assets/javascripts/react_app/components/common/DateTimePicker/date-time-picker.scss b/webpack/assets/javascripts/react_app/components/common/DateTimePicker/date-time-picker.scss index d17f21dc0cd..e2b3e851d20 100644 --- a/webpack/assets/javascripts/react_app/components/common/DateTimePicker/date-time-picker.scss +++ b/webpack/assets/javascripts/react_app/components/common/DateTimePicker/date-time-picker.scss @@ -1,59 +1,8 @@ -.bootstrap-datetimepicker-widget.top { - margin-top: -10px; -} - -.bootstrap-datetimepicker-widget.bottom { - margin-top: 10px !important; -} - -.bootstrap-datetimepicker-widget { - max-width: 20em; - - &.timepicker-sbs { - max-width: 40em; - } - - .arrow { - left: 10% !important; - } - - .popover-content { - color: #292e34; - } - - .clock-btn { - border: 0; - box-shadow: none; - color: #363636; - display: block; - padding-bottom: 4px; - padding-top: 4px; +.date-picker-popover .pf-v5-c-popover__footer { + margin-top: 0; + margin-bottom: 10px; + .date-picker-input-items { + display: flex; + justify-content: space-evenly; } - - table td { - .today-button { - padding: 0 16px 0 0; - border: 0; - background-color: transparent; - - span { - height: 24px; - line-height: 24px; - color: #0088ce; - } - } - } -} - -span.clear-button { - background: none; - border-left: none; - color: #72767b; -} - -input.date-time-input { - border-right: none; -} -.pf-v5-c-popover.date-time-picker-popover { - width: 33%; } diff --git a/webpack/assets/javascripts/react_app/components/common/DateTimePicker/dateTimeHelpers.js b/webpack/assets/javascripts/react_app/components/common/DateTimePicker/dateTimeHelpers.js new file mode 100644 index 00000000000..0664b426af9 --- /dev/null +++ b/webpack/assets/javascripts/react_app/components/common/DateTimePicker/dateTimeHelpers.js @@ -0,0 +1,22 @@ +import { yyyyMMddFormat } from '@patternfly/react-core'; + +export const formatTime = time => { + let now; + if (time) { + now = Number.isNaN(Date.parse(time)) + ? new Date(`01/01/2025 ${time}`) + : new Date(time); + } else { + now = new Date(); + } + return now.toLocaleTimeString([], { + hour: '2-digit', + minute: '2-digit', + hour12: false, + }); +}; + +export const formatDate = date => { + const now = date ? new Date(date) : new Date(); + return yyyyMMddFormat(now); +}; diff --git a/webpack/assets/javascripts/react_app/components/common/forms/__tests__/InputFactory.test.js b/webpack/assets/javascripts/react_app/components/common/forms/__tests__/InputFactory.test.js index af6f8fefdf5..29c8832980a 100644 --- a/webpack/assets/javascripts/react_app/components/common/forms/__tests__/InputFactory.test.js +++ b/webpack/assets/javascripts/react_app/components/common/forms/__tests__/InputFactory.test.js @@ -65,19 +65,19 @@ describe('InputFactory', () => { it('should render DateTimePicker for type="dateTime"', () => { const { container } = render(); - const input = container.querySelector('.date-time-input'); + const input = container.querySelector('.date-time-picker-pf-wrapper'); expect(input).toBeInTheDocument(); }); it('should render DatePicker for type="date"', () => { const { container } = render(); - const input = container.querySelector('.date-input'); + const input = container.querySelector('.date-picker-pf-wrapper'); expect(input).toBeInTheDocument(); }); it('should render TimePicker for type="time"', () => { const { container } = render(); - const input = container.querySelector('.date-time-picker-pf'); + const input = container.querySelector('.time-picker-pf'); expect(input).toBeInTheDocument(); });