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');
+ });
});