diff --git a/awx/ui_next/.eslintrc b/awx/ui_next/.eslintrc index 614af544bf..8634750ebc 100644 --- a/awx/ui_next/.eslintrc +++ b/awx/ui_next/.eslintrc @@ -35,6 +35,8 @@ { "markupOnly": true, "ignoreAttribute": [ + "dateFieldName", + "timeFieldName", "to", "streamType", "path", @@ -85,7 +87,7 @@ "data-cy", "fieldName" ], - "ignore": ["Ansible", "Tower", "JSON", "YAML", "lg"], + "ignore": ["Ansible", "Tower", "JSON", "YAML", "lg", "hh:mm AM/PM"], "ignoreComponent": [ "AboutModal", "code", diff --git a/awx/ui_next/src/components/Schedule/ScheduleAdd/ScheduleAdd.jsx b/awx/ui_next/src/components/Schedule/ScheduleAdd/ScheduleAdd.jsx index 08d8b9fdb1..b52bbeac65 100644 --- a/awx/ui_next/src/components/Schedule/ScheduleAdd/ScheduleAdd.jsx +++ b/awx/ui_next/src/components/Schedule/ScheduleAdd/ScheduleAdd.jsx @@ -41,7 +41,6 @@ function ScheduleAdd({ end, frequency, interval, - startDateTime, timezone, occurrences, runOn, @@ -49,7 +48,6 @@ function ScheduleAdd({ runOnTheMonth, runOnDayMonth, runOnDayNumber, - endDateTime, runOnTheOccurrence, credentials, daysOfWeek, @@ -100,6 +98,10 @@ function ScheduleAdd({ }); } } + delete requestData.startDate; + delete requestData.startTime; + delete requestData.endDate; + delete requestData.endTime; const { data: { id: scheduleId }, diff --git a/awx/ui_next/src/components/Schedule/ScheduleAdd/ScheduleAdd.test.jsx b/awx/ui_next/src/components/Schedule/ScheduleAdd/ScheduleAdd.test.jsx index 01962c8ba8..6d115e00bc 100644 --- a/awx/ui_next/src/components/Schedule/ScheduleAdd/ScheduleAdd.test.jsx +++ b/awx/ui_next/src/components/Schedule/ScheduleAdd/ScheduleAdd.test.jsx @@ -82,7 +82,8 @@ describe('', () => { frequency: 'none', interval: 1, name: 'Run once schedule', - startDateTime: '2020-03-25T10:00:00', + startDate: '2020-03-25', + startTime: '10:00:00', timezone: 'America/New_York', }); }); @@ -103,7 +104,8 @@ describe('', () => { interval: 10, name: 'Run every 10 minutes 10 times', occurrences: 10, - startDateTime: '2020-03-25T10:30:00', + startDate: '2020-03-25', + startTime: '10:30:00', timezone: 'America/New_York', }); }); @@ -120,11 +122,13 @@ describe('', () => { wrapper.find('Formik').invoke('onSubmit')({ description: 'test description', end: 'onDate', - endDateTime: '2020-03-26T10:45:00', + endDate: '2020-03-26', + endTime: '10:45:00', frequency: 'hour', interval: 1, name: 'Run every hour until date', - startDateTime: '2020-03-25T10:45:00', + startDate: '2020-03-25', + startTime: '10:45:00', timezone: 'America/New_York', }); }); @@ -144,7 +148,8 @@ describe('', () => { frequency: 'day', interval: 1, name: 'Run daily', - startDateTime: '2020-03-25T10:45:00', + startDate: '2020-03-25', + startTime: '10:45:00', timezone: 'America/New_York', }); }); @@ -166,7 +171,8 @@ describe('', () => { interval: 1, name: 'Run weekly on mon/wed/fri', occurrences: 1, - startDateTime: '2020-03-25T10:45:00', + startDate: '2020-03-25', + startTime: '10:45:00', timezone: 'America/New_York', }); }); @@ -188,7 +194,8 @@ describe('', () => { occurrences: 1, runOn: 'day', runOnDayNumber: 1, - startDateTime: '2020-04-01T10:45', + startTime: '10:45', + startDate: '2020-04-01', timezone: 'America/New_York', }); }); @@ -205,7 +212,8 @@ describe('', () => { wrapper.find('Formik').invoke('onSubmit')({ description: 'test description', end: 'never', - endDateTime: '2020-03-26T11:00:00', + endDate: '2020-03-26', + endTime: '11:00:00', frequency: 'month', interval: 1, name: 'Run monthly on the last Tuesday', @@ -213,7 +221,8 @@ describe('', () => { runOn: 'the', runOnTheDay: 'tuesday', runOnTheOccurrence: -1, - startDateTime: '2020-03-31T11:00', + startDate: '2020-03-31', + startTime: '11:00', timezone: 'America/New_York', }); }); @@ -237,7 +246,8 @@ describe('', () => { runOn: 'day', runOnDayMonth: 3, runOnDayNumber: 1, - startDateTime: '2020-03-01T00:00', + startDate: '2020-03-01', + startTime: '00:00', timezone: 'America/New_York', }); }); @@ -262,7 +272,8 @@ describe('', () => { runOnTheOccurrence: 2, runOnTheDay: 'friday', runOnTheMonth: 4, - startDateTime: '2020-04-10T11:15', + startDate: '2020-04-10', + startTime: '11:15', timezone: 'America/New_York', }); }); @@ -287,7 +298,8 @@ describe('', () => { runOnTheOccurrence: 1, runOnTheDay: 'weekday', runOnTheMonth: 10, - startDateTime: '2020-04-10T11:15', + startDate: '2020-04-10', + startTime: '11:15', timezone: 'America/New_York', }); }); @@ -371,7 +383,8 @@ describe('', () => { wrapper.find('Formik').invoke('onSubmit')({ name: 'Schedule', end: 'never', - endDateTime: '2021-01-29T14:15:00', + endDate: '2021-01-29', + endTime: '14:15:00', frequency: 'none', occurrences: 1, runOn: 'day', @@ -386,7 +399,8 @@ describe('', () => { { name: 'cred 1', id: 10 }, { name: 'cred 2', id: 20 }, ], - startDateTime: '2021-01-28T14:15:00', + startDate: '2021-01-28', + startTime: '14:15:00', timezone: 'America/New_York', }); }); @@ -457,7 +471,8 @@ describe('', () => { frequency: 'none', interval: 1, name: 'Run once schedule', - startDateTime: '2020-03-25T10:00:00', + startDate: '2020-03-25', + startTime: '10:00:00', timezone: 'America/New_York', }); }); diff --git a/awx/ui_next/src/components/Schedule/ScheduleEdit/ScheduleEdit.jsx b/awx/ui_next/src/components/Schedule/ScheduleEdit/ScheduleEdit.jsx index db69b5734f..b96feadd7d 100644 --- a/awx/ui_next/src/components/Schedule/ScheduleEdit/ScheduleEdit.jsx +++ b/awx/ui_next/src/components/Schedule/ScheduleEdit/ScheduleEdit.jsx @@ -41,7 +41,6 @@ function ScheduleEdit({ end, frequency, interval, - startDateTime, timezone, occurences, runOn, @@ -49,7 +48,6 @@ function ScheduleEdit({ runOnTheMonth, runOnDayMonth, runOnDayNumber, - endDateTime, runOnTheOccurence, daysOfWeek, ...submitValues @@ -98,6 +96,10 @@ function ScheduleEdit({ ...submitValues, rrule: rule.toString().replace(/\n/g, ' '), }; + delete requestData.startDate; + delete requestData.startTime; + delete requestData.endDate; + delete requestData.endTime; if (Object.keys(values).includes('daysToKeep')) { if (!requestData.extra_data) { diff --git a/awx/ui_next/src/components/Schedule/ScheduleEdit/ScheduleEdit.test.jsx b/awx/ui_next/src/components/Schedule/ScheduleEdit/ScheduleEdit.test.jsx index 133b22ed33..faa505734c 100644 --- a/awx/ui_next/src/components/Schedule/ScheduleEdit/ScheduleEdit.test.jsx +++ b/awx/ui_next/src/components/Schedule/ScheduleEdit/ScheduleEdit.test.jsx @@ -16,7 +16,10 @@ import ScheduleEdit from './ScheduleEdit'; jest.mock('../../../api'); let wrapper; - +const now = new Date(); +const closestQuarterHour = new Date(Math.ceil(now.getTime() / 900000) * 900000); +const tomorrow = new Date(closestQuarterHour); +tomorrow.setDate(tomorrow.getDate() + 1); const mockSchedule = { rrule: 'DTSTART;TZID=America/New_York:20200402T144500 RRULE:INTERVAL=1;COUNT=1;FREQ=MINUTELY', @@ -202,7 +205,8 @@ describe('', () => { frequency: 'none', interval: 1, name: 'Run once schedule', - startDateTime: '2020-03-25T10:00:00', + startDate: '2020-03-25', + startTime: '10:00:00', timezone: 'America/New_York', }); }); @@ -223,7 +227,8 @@ describe('', () => { interval: 10, name: 'Run every 10 minutes 10 times', occurrences: 10, - startDateTime: '2020-03-25T10:30:00', + startDate: '2020-03-25', + startTime: '10:30:00', timezone: 'America/New_York', }); }); @@ -241,11 +246,13 @@ describe('', () => { wrapper.find('Formik').invoke('onSubmit')({ description: 'test description', end: 'onDate', - endDateTime: '2020-03-26T10:45:00', + endDate: '2020-03-26', + endTime: '10:45:00', frequency: 'hour', interval: 1, name: 'Run every hour until date', - startDateTime: '2020-03-25T10:45:00', + startDate: '2020-03-25', + startTime: '10:45:00', timezone: 'America/New_York', }); }); @@ -265,7 +272,8 @@ describe('', () => { frequency: 'day', interval: 1, name: 'Run daily', - startDateTime: '2020-03-25T10:45:00', + startDate: '2020-03-25', + startTime: '10:45:00', timezone: 'America/New_York', }); }); @@ -287,7 +295,8 @@ describe('', () => { interval: 1, name: 'Run weekly on mon/wed/fri', occurrences: 1, - startDateTime: '2020-03-25T10:45:00', + startDate: '2020-03-25', + startTime: '10:45:00', timezone: 'America/New_York', }); }); @@ -310,7 +319,8 @@ describe('', () => { occurrences: 1, runOn: 'day', runOnDayNumber: 1, - startDateTime: '2020-04-01T10:45', + startDate: '2020-04-01', + startTime: '10:45', timezone: 'America/New_York', }); }); @@ -336,12 +346,14 @@ describe('', () => { runOn: 'the', runOnTheDay: 'tuesday', runOnTheOccurrence: -1, - startDateTime: '2020-03-31T11:00', + startDate: '2020-03-31', + startTime: '11:00', timezone: 'America/New_York', }); }); expect(SchedulesAPI.update).toHaveBeenCalledWith(27, { description: 'test description', + endDateTime: '2020-03-26T11:00:00', name: 'Run monthly on the last Tuesday', extra_data: {}, occurrences: 1, @@ -362,7 +374,8 @@ describe('', () => { runOn: 'day', runOnDayMonth: 3, runOnDayNumber: 1, - startDateTime: '2020-03-01T00:00', + startTime: '00:00', + startDate: '2020-03-01', timezone: 'America/New_York', }); }); @@ -388,7 +401,8 @@ describe('', () => { runOnTheOccurrence: 2, runOnTheDay: 'friday', runOnTheMonth: 4, - startDateTime: '2020-04-10T11:15', + startTime: '11:15', + startDate: '2020-04-10', timezone: 'America/New_York', }); }); @@ -415,7 +429,8 @@ describe('', () => { runOnTheOccurrence: 1, runOnTheDay: 'weekday', runOnTheMonth: 10, - startDateTime: '2020-04-10T11:15', + startTime: '11:15', + startDate: '2020-04-10', timezone: 'America/New_York', }); }); @@ -526,7 +541,8 @@ describe('', () => { wrapper.find('Formik').invoke('onSubmit')({ name: mockSchedule.name, end: 'never', - endDateTime: '2021-01-29T14:15:00', + endDate: '2021-01-29', + endTime: '14:15:00', frequency: 'none', occurrences: 1, runOn: 'day', @@ -536,7 +552,8 @@ describe('', () => { runOnTheMonth: 1, runOnTheOccurrence: 1, skip_tags: '', - startDateTime: '2021-01-28T14:15:00', + startDate: '2021-01-28', + startTime: '14:15:00', timezone: 'America/New_York', credentials: [ { id: 3, name: 'Credential 3', kind: 'ssh', url: '' }, @@ -622,7 +639,10 @@ describe('', () => { await act(async () => wrapper.find('Button[aria-label="Save"]').prop('onClick')() ); + expect(SchedulesAPI.update).toBeCalledWith(27, { + endDateTime: undefined, + startDateTime: undefined, description: '', extra_data: {}, occurrences: 1, @@ -630,7 +650,7 @@ describe('', () => { name: 'foo', inventory: 702, rrule: - 'DTSTART;TZID=America/New_York:20200402T144500 RRULE:INTERVAL=1;COUNT=1;FREQ=MINUTELY', + 'DTSTART;TZID=America/New_York:20200402T184500 RRULE:INTERVAL=1;COUNT=1;FREQ=MINUTELY', }); }); test('should submit survey with default values properly, without opening prompt wizard', async () => { @@ -728,7 +748,8 @@ describe('', () => { frequency: 'none', interval: 1, name: 'Run once schedule', - startDateTime: '2020-03-25T10:00:00', + startDate: '2020-03-25', + startTime: '10:00:00', timezone: 'America/New_York', }); }); diff --git a/awx/ui_next/src/components/Schedule/shared/DateTimePicker.jsx b/awx/ui_next/src/components/Schedule/shared/DateTimePicker.jsx new file mode 100644 index 0000000000..7f9088a445 --- /dev/null +++ b/awx/ui_next/src/components/Schedule/shared/DateTimePicker.jsx @@ -0,0 +1,67 @@ +import React from 'react'; +import { t } from '@lingui/macro'; +import { useField } from 'formik'; +import { + DatePicker, + isValidDate, + yyyyMMddFormat, + TimePicker, + FormGroup, +} from '@patternfly/react-core'; +import styled from 'styled-components'; +import { required, validateTime, combine } from '../../../util/validators'; + +const DateTimeGroup = styled.span` + display: flex; +`; +function DateTimePicker({ dateFieldName, timeFieldName, label }) { + const [dateField, dateMeta, dateHelpers] = useField({ + name: `${dateFieldName}`, + validate: combine([required(null), isValidDate]), + }); + const [timeField, timeMeta, timeHelpers] = useField({ + name: `${timeFieldName}`, + validate: combine([required(null), validateTime()]), + }); + + const onDateChange = (inputDate, newDate) => { + dateHelpers.setTouched(); + if (isValidDate(newDate) && inputDate === yyyyMMddFormat(newDate)) { + dateHelpers.setValue(new Date(newDate).toISOString().split('T')[0]); + } + }; + + return ( + + + + timeHelpers.setValue(time)} + /> + + + ); +} + +export default DateTimePicker; diff --git a/awx/ui_next/src/components/Schedule/shared/DateTimePicker.test.jsx b/awx/ui_next/src/components/Schedule/shared/DateTimePicker.test.jsx new file mode 100644 index 0000000000..4121b454c1 --- /dev/null +++ b/awx/ui_next/src/components/Schedule/shared/DateTimePicker.test.jsx @@ -0,0 +1,55 @@ +import React from 'react'; +import { act } from 'react-dom/test-utils'; +import { Formik } from 'formik'; + +import { mountWithContexts } from '../../../../testUtils/enzymeHelpers'; +import DateTimePicker from './DateTimePicker'; + +describe('', () => { + let wrapper; + test('should render properly', async () => { + await act(async () => { + wrapper = mountWithContexts( + + + + ); + }); + expect(wrapper.find('DatePicker')).toHaveLength(1); + expect(wrapper.find('DatePicker').prop('value')).toBe('2021-05-26'); + expect(wrapper.find('TimePicker')).toHaveLength(1); + expect(wrapper.find('TimePicker').prop('value')).toBe('2:15 PM'); + }); + test('should update values properly', async () => { + await act(async () => { + wrapper = mountWithContexts( + + + + ); + }); + + await act(async () => { + wrapper.find('DatePicker').prop('onChange')( + '2021-05-29', + new Date('Sat May 29 2021 00:00:00 GMT-0400 (Eastern Daylight Time)') + ); + wrapper.find('TimePicker').prop('onChange')('7:15 PM'); + }); + wrapper.update(); + expect(wrapper.find('DatePicker').prop('value')).toBe('2021-05-29'); + expect(wrapper.find('TimePicker').prop('value')).toBe('7:15 PM'); + }); +}); diff --git a/awx/ui_next/src/components/Schedule/shared/FrequencyDetailSubform.jsx b/awx/ui_next/src/components/Schedule/shared/FrequencyDetailSubform.jsx index 0cde64b22f..47f36379e0 100644 --- a/awx/ui_next/src/components/Schedule/shared/FrequencyDetailSubform.jsx +++ b/awx/ui_next/src/components/Schedule/shared/FrequencyDetailSubform.jsx @@ -14,6 +14,7 @@ import { import AnsibleSelect from '../../AnsibleSelect'; import FormField from '../../FormField'; import { required } from '../../../util/validators'; +import DateTimePicker from './DateTimePicker'; const RunOnRadio = styled(Radio)` label { @@ -74,9 +75,8 @@ const FrequencyDetailSubform = () => { const [runOnTheMonth] = useField({ name: 'runOnTheMonth', }); - const [startDateTime] = useField({ - name: 'startDateTime', - }); + const [startDate] = useField('startDate'); + const [daysOfWeek, daysOfWeekMeta, daysOfWeekHelpers] = useField({ name: 'daysOfWeek', validate: required(t`Select a value for this field`), @@ -93,10 +93,6 @@ const FrequencyDetailSubform = () => { name: 'runOn', validate: required(t`Select a value for this field`), }); - const [endDateTime, endDateTimeMeta] = useField({ - name: 'endDateTime', - validate: required(t`Select a value for this field`), - }); const [frequency] = useField({ name: 'frequency', }); @@ -317,7 +313,7 @@ const FrequencyDetailSubform = () => { )} {(frequency?.value === 'month' || frequency?.value === 'year') && - !isNaN(new Date(startDateTime.value)) && ( + !isNaN(new Date(startDate.value)) && ( { /> )} {end?.value === 'onDate' && ( - - - + )} ); - /* eslint-enable no-restricted-globals */ }; export default FrequencyDetailSubform; diff --git a/awx/ui_next/src/components/Schedule/shared/ScheduleForm.jsx b/awx/ui_next/src/components/Schedule/shared/ScheduleForm.jsx index d986190cd6..510b0c0a83 100644 --- a/awx/ui_next/src/components/Schedule/shared/ScheduleForm.jsx +++ b/awx/ui_next/src/components/Schedule/shared/ScheduleForm.jsx @@ -22,12 +22,13 @@ import { SubFormLayout, FormFullWidthLayout, } from '../../FormLayout'; -import { dateToInputDateTime, formatDateStringUTC } from '../../../util/dates'; +import { dateToInputDateTime } from '../../../util/dates'; import useRequest from '../../../util/useRequest'; import { required } from '../../../util/validators'; import { parseVariableField } from '../../../util/yaml'; import FrequencyDetailSubform from './FrequencyDetailSubform'; import SchedulePromptableFields from './SchedulePromptableFields'; +import DateTimePicker from './DateTimePicker'; const generateRunOnTheDay = (days = []) => { if ( @@ -79,10 +80,6 @@ const generateRunOnTheDay = (days = []) => { }; function ScheduleFormFields({ hasDaysToKeepField, zoneOptions }) { - const [startDateTime, startDateTimeMeta] = useField({ - name: 'startDateTime', - validate: required(t`Select a valid date and time for this field`), - }); const [timezone, timezoneMeta] = useField({ name: 'timezone', validate: required(t`Select a value for this field`), @@ -108,25 +105,11 @@ function ScheduleFormFields({ hasDaysToKeepField, zoneOptions }) { name="description" type="text" /> - - - + new Date(endDateTime) + new Date(startDate) >= new Date(endDate) ) { - errors.endDateTime = t`Please select an end date/time that comes after the start date/time.`; + errors.endDate = t`Please select an end date/time that comes after the start date/time.`; } if ( diff --git a/awx/ui_next/src/components/Schedule/shared/ScheduleForm.test.jsx b/awx/ui_next/src/components/Schedule/shared/ScheduleForm.test.jsx index 5cc4490d9a..4f5aac69c4 100644 --- a/awx/ui_next/src/components/Schedule/shared/ScheduleForm.test.jsx +++ b/awx/ui_next/src/components/Schedule/shared/ScheduleForm.test.jsx @@ -5,6 +5,7 @@ import { mountWithContexts, waitForElement, } from '../../../../testUtils/enzymeHelpers'; +import { dateToInputDateTime } from '../../../util/dates'; import { SchedulesAPI, JobTemplatesAPI, InventoriesAPI } from '../../../api'; import ScheduleForm from './ScheduleForm'; @@ -99,8 +100,12 @@ const nonRRuleValuesMatch = () => { expect(wrapper.find('input#schedule-description').prop('value')).toBe( 'test description' ); - expect(wrapper.find('input#schedule-start-datetime').prop('value')).toBe( - '2020-04-02T14:45:00' + + expect( + wrapper.find('DatePicker[aria-label="Start date"]').prop('value') + ).toBe('2020-04-02'); + expect(wrapper.find('TimePicker[aria-label="Start time"]').prop('time')).toBe( + '6:45 PM' ); expect(wrapper.find('select#schedule-timezone').prop('value')).toBe( 'America/New_York' @@ -472,6 +477,11 @@ describe('', () => { wrapper.unmount(); }); test('initially renders expected fields and values', () => { + const now = new Date(); + const closestQuarterHour = new Date( + Math.ceil(now.getTime() / 900000) * 900000 + ); + const [date, time] = dateToInputDateTime(closestQuarterHour); expect(wrapper.find('ScheduleForm').length).toBe(1); defaultFieldsVisible(); expect(wrapper.find('FormGroup[label="Run every"]').length).toBe(0); @@ -483,9 +493,9 @@ describe('', () => { expect(wrapper.find('input#schedule-name').prop('value')).toBe(''); expect(wrapper.find('input#schedule-description').prop('value')).toBe(''); - expect( - wrapper.find('input#schedule-start-datetime').prop('value') - ).toMatch(/\d\d\d\d-\d\d-\d\dT\d\d:\d\d:\d\d/); + + expect(wrapper.find('DatePicker').prop('value')).toMatch(`${date}`); + expect(wrapper.find('TimePicker').prop('time')).toMatch(`${time}`); expect(wrapper.find('select#schedule-timezone').prop('value')).toBe( 'America/New_York' ); @@ -703,18 +713,18 @@ describe('', () => { expect(wrapper.find('input#end-on-date').prop('checked')).toBe(true); expect(wrapper.find('#schedule-end-datetime-helper').length).toBe(0); await act(async () => { - wrapper.find('input#schedule-end-datetime').simulate('change', { - target: { name: 'endDateTime', value: '2020-03-14T01:45:00' }, - }); + wrapper.find('DatePicker[aria-label="End date"]').prop('onChange')( + '2020-03-14', + new Date('2020-03-14') + ); }); - wrapper.update(); await act(async () => { - wrapper.find('input#schedule-end-datetime').simulate('blur'); + wrapper.find('DatePicker[aria-label="End date"]').simulate('blur'); }); - wrapper.update(); - expect(wrapper.find('#schedule-end-datetime-helper').text()).toBe( + wrapper.update(); + expect(wrapper.find('#schedule-End-datetime-helper').text()).toBe( 'Please select an end date/time that comes after the start date/time.' ); }); @@ -1041,7 +1051,7 @@ describe('', () => { rrule: 'DTSTART;TZID=America/New_York:20200402T144500 RRULE:INTERVAL=1;FREQ=WEEKLY;BYDAY=MO,WE,FR;UNTIL=20210101T050000Z', dtend: '2020-10-30T18:45:00Z', - until: '2021-01-01T00:00:00', + until: '2021-01-01T01:00:00', })} resource={{ id: 23, @@ -1090,9 +1100,12 @@ describe('', () => { expect( wrapper.find('input#schedule-days-of-week-sat').prop('checked') ).toBe(false); - expect(wrapper.find('input#schedule-end-datetime').prop('value')).toBe( - '2021-01-01T00:00:00' - ); + expect( + wrapper.find('DatePicker[aria-label="End date"]').prop('value') + ).toBe('2021-01-01'); + expect( + wrapper.find('TimePicker[aria-label="End time"]').prop('value') + ).toBe('1:00 AM'); }); test('initially renders expected fields and values with existing schedule that runs every month on the last weekday', async () => { await act(async () => { diff --git a/awx/ui_next/src/components/Schedule/shared/buildRuleObj.js b/awx/ui_next/src/components/Schedule/shared/buildRuleObj.js index b2c145a868..9306a5410c 100644 --- a/awx/ui_next/src/components/Schedule/shared/buildRuleObj.js +++ b/awx/ui_next/src/components/Schedule/shared/buildRuleObj.js @@ -2,15 +2,20 @@ import { t } from '@lingui/macro'; import { RRule } from 'rrule'; import { getRRuleDayConstants } from '../../../util/dates'; +const parseTime = time => { + const [hour, minute, ampm] = time.split(/[: ]/); + const timeHour = + ampm === 'PM' && hour !== '12' ? `${parseInt(hour, 10) + 12}` : `${hour}`; + + return [timeHour, minute]; +}; + export default function buildRuleObj(values) { - const [startDate, startTime] = values.startDateTime.split('T'); // Dates are formatted like "YYYY-MM-DD" - const [startYear, startMonth, startDay] = startDate.split('-'); + const [startYear, startMonth, startDay] = values.startDate.split('-'); // Times are formatted like "HH:MM:SS" or "HH:MM" if no seconds // have been specified - const [startHour = 0, startMinute = 0, startSecond = 0] = startTime.split( - ':' - ); + const [startHour, startMinute] = parseTime(values.startTime); const ruleObj = { interval: values.interval, @@ -20,8 +25,7 @@ export default function buildRuleObj(values) { parseInt(startMonth, 10) - 1, startDay, startHour, - startMinute, - startSecond + startMinute ) ), tzid: values.timezone, @@ -77,17 +81,16 @@ export default function buildRuleObj(values) { ruleObj.count = values.occurrences; break; case 'onDate': { - const [endDate, endTime] = values.endDateTime.split('T'); - const [endYear, endMonth, endDay] = endDate.split('-'); - const [endHour = 0, endMinute = 0, endSecond = 0] = endTime.split(':'); + const [endYear, endMonth, endDay] = values.endDate.split('-'); + + const [endHour, endMinute] = parseTime(values.endTime); ruleObj.until = new Date( Date.UTC( endYear, parseInt(endMonth, 10) - 1, endDay, endHour, - endMinute, - endSecond + endMinute ) ); break; diff --git a/awx/ui_next/src/util/dates.jsx b/awx/ui_next/src/util/dates.jsx index 392ae83933..d1162331aa 100644 --- a/awx/ui_next/src/util/dates.jsx +++ b/awx/ui_next/src/util/dates.jsx @@ -44,15 +44,19 @@ export function timeOfDay() { } export function dateToInputDateTime(dateObj) { - // input type="date-time" expects values to be formatted - // like: YYYY-MM-DDTHH-MM-SS - const year = dateObj.getFullYear(); - const month = prependZeros(dateObj.getMonth() + 1); - const day = prependZeros(dateObj.getDate()); - const hour = prependZeros(dateObj.getHours()); - const minute = prependZeros(dateObj.getMinutes()); - const second = prependZeros(dateObj.getSeconds()); - return `${year}-${month}-${day}T${hour}:${minute}:${second}`; + let date = dateObj; + if (typeof dateObj === 'string') { + date = new Date(dateObj); + } + const year = date.getFullYear(); + const month = prependZeros(date.getMonth() + 1); + const day = prependZeros(date.getDate()); + const hour = + date.getHours() > 12 ? parseInt(date.getHours(), 10) - 12 : date.getHours(); + const minute = prependZeros(date.getMinutes()); + const amPmText = date.getHours() > 11 ? 'PM' : 'AM'; + + return [`${year}-${month}-${day}`, `${hour}:${minute} ${amPmText}`]; } export function getRRuleDayConstants(dayString) { diff --git a/awx/ui_next/src/util/dates.test.jsx b/awx/ui_next/src/util/dates.test.jsx index d5dfb559aa..0d0db592bf 100644 --- a/awx/ui_next/src/util/dates.test.jsx +++ b/awx/ui_next/src/util/dates.test.jsx @@ -70,7 +70,7 @@ describe('dateToInputDateTime', () => { test('it returns the expected value', () => { expect( dateToInputDateTime(new Date('2018-01-31T01:14:52.969227Z')) - ).toEqual('2018-01-31T01:14:52'); + ).toEqual(['2018-01-31', '1:14 AM']); }); }); diff --git a/awx/ui_next/src/util/validators.jsx b/awx/ui_next/src/util/validators.jsx index d69b370e34..d590935938 100644 --- a/awx/ui_next/src/util/validators.jsx +++ b/awx/ui_next/src/util/validators.jsx @@ -1,4 +1,5 @@ import { t } from '@lingui/macro'; +import { isValidDate } from '@patternfly/react-core'; export function required(message) { const errorMessage = message || t`This field must not be blank`; @@ -15,6 +16,25 @@ export function required(message) { return undefined; }; } +export function validateTime() { + return value => { + const timeRegex = new RegExp( + `^\\s*(\\d\\d?):([0-5])(\\d)\\s*([AaPp][Mm])?\\s*$` + ); + let message; + const timeComponents = value.split(':'); + + const date = new Date(); + date.setHours(parseInt(timeComponents[0], 10)); + date.setMinutes(parseInt(timeComponents[1], 10)); + + if (!isValidDate(date) || !timeRegex.test(value)) { + message = t`Invalid time format`; + } + + return message; + }; +} export function maxLength(max) { return value => { diff --git a/awx/ui_next/src/util/validators.test.js b/awx/ui_next/src/util/validators.test.js index ab3ac55e84..56dde08e8e 100644 --- a/awx/ui_next/src/util/validators.test.js +++ b/awx/ui_next/src/util/validators.test.js @@ -9,6 +9,7 @@ import { combine, regExp, requiredEmail, + validateTime, } from './validators'; describe('validators', () => { @@ -168,4 +169,13 @@ describe('validators', () => { test('bob has email', () => { expect(requiredEmail()('bob@localhost')).toBeUndefined(); }); + test('validate time validates properly', () => { + expect(validateTime()('12:15 PM')).toBeUndefined(); + expect(validateTime()('1:15 PM')).toBeUndefined(); + expect(validateTime()('01:15 PM')).toBeUndefined(); + expect(validateTime()('12:15')).toBeUndefined(); + expect(validateTime()('12:15: PM')).toEqual('Invalid time format'); + expect(validateTime()('12.15 PM')).toEqual('Invalid time format'); + expect(validateTime()('12;15 PM')).toEqual('Invalid time format'); + }); });