uses pf date and time picker to schedule form

This commit is contained in:
Alex Corey
2021-05-26 15:31:19 -04:00
parent ac5b53b13c
commit 16c6e2d716
15 changed files with 323 additions and 136 deletions

View File

@@ -35,6 +35,8 @@
{ {
"markupOnly": true, "markupOnly": true,
"ignoreAttribute": [ "ignoreAttribute": [
"dateFieldName",
"timeFieldName",
"to", "to",
"streamType", "streamType",
"path", "path",
@@ -85,7 +87,7 @@
"data-cy", "data-cy",
"fieldName" "fieldName"
], ],
"ignore": ["Ansible", "Tower", "JSON", "YAML", "lg"], "ignore": ["Ansible", "Tower", "JSON", "YAML", "lg", "hh:mm AM/PM"],
"ignoreComponent": [ "ignoreComponent": [
"AboutModal", "AboutModal",
"code", "code",

View File

@@ -41,7 +41,6 @@ function ScheduleAdd({
end, end,
frequency, frequency,
interval, interval,
startDateTime,
timezone, timezone,
occurrences, occurrences,
runOn, runOn,
@@ -49,7 +48,6 @@ function ScheduleAdd({
runOnTheMonth, runOnTheMonth,
runOnDayMonth, runOnDayMonth,
runOnDayNumber, runOnDayNumber,
endDateTime,
runOnTheOccurrence, runOnTheOccurrence,
credentials, credentials,
daysOfWeek, daysOfWeek,
@@ -100,6 +98,10 @@ function ScheduleAdd({
}); });
} }
} }
delete requestData.startDate;
delete requestData.startTime;
delete requestData.endDate;
delete requestData.endTime;
const { const {
data: { id: scheduleId }, data: { id: scheduleId },

View File

@@ -82,7 +82,8 @@ describe('<ScheduleAdd />', () => {
frequency: 'none', frequency: 'none',
interval: 1, interval: 1,
name: 'Run once schedule', name: 'Run once schedule',
startDateTime: '2020-03-25T10:00:00', startDate: '2020-03-25',
startTime: '10:00:00',
timezone: 'America/New_York', timezone: 'America/New_York',
}); });
}); });
@@ -103,7 +104,8 @@ describe('<ScheduleAdd />', () => {
interval: 10, interval: 10,
name: 'Run every 10 minutes 10 times', name: 'Run every 10 minutes 10 times',
occurrences: 10, occurrences: 10,
startDateTime: '2020-03-25T10:30:00', startDate: '2020-03-25',
startTime: '10:30:00',
timezone: 'America/New_York', timezone: 'America/New_York',
}); });
}); });
@@ -120,11 +122,13 @@ describe('<ScheduleAdd />', () => {
wrapper.find('Formik').invoke('onSubmit')({ wrapper.find('Formik').invoke('onSubmit')({
description: 'test description', description: 'test description',
end: 'onDate', end: 'onDate',
endDateTime: '2020-03-26T10:45:00', endDate: '2020-03-26',
endTime: '10:45:00',
frequency: 'hour', frequency: 'hour',
interval: 1, interval: 1,
name: 'Run every hour until date', name: 'Run every hour until date',
startDateTime: '2020-03-25T10:45:00', startDate: '2020-03-25',
startTime: '10:45:00',
timezone: 'America/New_York', timezone: 'America/New_York',
}); });
}); });
@@ -144,7 +148,8 @@ describe('<ScheduleAdd />', () => {
frequency: 'day', frequency: 'day',
interval: 1, interval: 1,
name: 'Run daily', name: 'Run daily',
startDateTime: '2020-03-25T10:45:00', startDate: '2020-03-25',
startTime: '10:45:00',
timezone: 'America/New_York', timezone: 'America/New_York',
}); });
}); });
@@ -166,7 +171,8 @@ describe('<ScheduleAdd />', () => {
interval: 1, interval: 1,
name: 'Run weekly on mon/wed/fri', name: 'Run weekly on mon/wed/fri',
occurrences: 1, occurrences: 1,
startDateTime: '2020-03-25T10:45:00', startDate: '2020-03-25',
startTime: '10:45:00',
timezone: 'America/New_York', timezone: 'America/New_York',
}); });
}); });
@@ -188,7 +194,8 @@ describe('<ScheduleAdd />', () => {
occurrences: 1, occurrences: 1,
runOn: 'day', runOn: 'day',
runOnDayNumber: 1, runOnDayNumber: 1,
startDateTime: '2020-04-01T10:45', startTime: '10:45',
startDate: '2020-04-01',
timezone: 'America/New_York', timezone: 'America/New_York',
}); });
}); });
@@ -205,7 +212,8 @@ describe('<ScheduleAdd />', () => {
wrapper.find('Formik').invoke('onSubmit')({ wrapper.find('Formik').invoke('onSubmit')({
description: 'test description', description: 'test description',
end: 'never', end: 'never',
endDateTime: '2020-03-26T11:00:00', endDate: '2020-03-26',
endTime: '11:00:00',
frequency: 'month', frequency: 'month',
interval: 1, interval: 1,
name: 'Run monthly on the last Tuesday', name: 'Run monthly on the last Tuesday',
@@ -213,7 +221,8 @@ describe('<ScheduleAdd />', () => {
runOn: 'the', runOn: 'the',
runOnTheDay: 'tuesday', runOnTheDay: 'tuesday',
runOnTheOccurrence: -1, runOnTheOccurrence: -1,
startDateTime: '2020-03-31T11:00', startDate: '2020-03-31',
startTime: '11:00',
timezone: 'America/New_York', timezone: 'America/New_York',
}); });
}); });
@@ -237,7 +246,8 @@ describe('<ScheduleAdd />', () => {
runOn: 'day', runOn: 'day',
runOnDayMonth: 3, runOnDayMonth: 3,
runOnDayNumber: 1, runOnDayNumber: 1,
startDateTime: '2020-03-01T00:00', startDate: '2020-03-01',
startTime: '00:00',
timezone: 'America/New_York', timezone: 'America/New_York',
}); });
}); });
@@ -262,7 +272,8 @@ describe('<ScheduleAdd />', () => {
runOnTheOccurrence: 2, runOnTheOccurrence: 2,
runOnTheDay: 'friday', runOnTheDay: 'friday',
runOnTheMonth: 4, runOnTheMonth: 4,
startDateTime: '2020-04-10T11:15', startDate: '2020-04-10',
startTime: '11:15',
timezone: 'America/New_York', timezone: 'America/New_York',
}); });
}); });
@@ -287,7 +298,8 @@ describe('<ScheduleAdd />', () => {
runOnTheOccurrence: 1, runOnTheOccurrence: 1,
runOnTheDay: 'weekday', runOnTheDay: 'weekday',
runOnTheMonth: 10, runOnTheMonth: 10,
startDateTime: '2020-04-10T11:15', startDate: '2020-04-10',
startTime: '11:15',
timezone: 'America/New_York', timezone: 'America/New_York',
}); });
}); });
@@ -371,7 +383,8 @@ describe('<ScheduleAdd />', () => {
wrapper.find('Formik').invoke('onSubmit')({ wrapper.find('Formik').invoke('onSubmit')({
name: 'Schedule', name: 'Schedule',
end: 'never', end: 'never',
endDateTime: '2021-01-29T14:15:00', endDate: '2021-01-29',
endTime: '14:15:00',
frequency: 'none', frequency: 'none',
occurrences: 1, occurrences: 1,
runOn: 'day', runOn: 'day',
@@ -386,7 +399,8 @@ describe('<ScheduleAdd />', () => {
{ name: 'cred 1', id: 10 }, { name: 'cred 1', id: 10 },
{ name: 'cred 2', id: 20 }, { name: 'cred 2', id: 20 },
], ],
startDateTime: '2021-01-28T14:15:00', startDate: '2021-01-28',
startTime: '14:15:00',
timezone: 'America/New_York', timezone: 'America/New_York',
}); });
}); });
@@ -457,7 +471,8 @@ describe('<ScheduleAdd />', () => {
frequency: 'none', frequency: 'none',
interval: 1, interval: 1,
name: 'Run once schedule', name: 'Run once schedule',
startDateTime: '2020-03-25T10:00:00', startDate: '2020-03-25',
startTime: '10:00:00',
timezone: 'America/New_York', timezone: 'America/New_York',
}); });
}); });

View File

@@ -41,7 +41,6 @@ function ScheduleEdit({
end, end,
frequency, frequency,
interval, interval,
startDateTime,
timezone, timezone,
occurences, occurences,
runOn, runOn,
@@ -49,7 +48,6 @@ function ScheduleEdit({
runOnTheMonth, runOnTheMonth,
runOnDayMonth, runOnDayMonth,
runOnDayNumber, runOnDayNumber,
endDateTime,
runOnTheOccurence, runOnTheOccurence,
daysOfWeek, daysOfWeek,
...submitValues ...submitValues
@@ -98,6 +96,10 @@ function ScheduleEdit({
...submitValues, ...submitValues,
rrule: rule.toString().replace(/\n/g, ' '), 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 (Object.keys(values).includes('daysToKeep')) {
if (!requestData.extra_data) { if (!requestData.extra_data) {

View File

@@ -16,7 +16,10 @@ import ScheduleEdit from './ScheduleEdit';
jest.mock('../../../api'); jest.mock('../../../api');
let wrapper; 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 = { const mockSchedule = {
rrule: rrule:
'DTSTART;TZID=America/New_York:20200402T144500 RRULE:INTERVAL=1;COUNT=1;FREQ=MINUTELY', 'DTSTART;TZID=America/New_York:20200402T144500 RRULE:INTERVAL=1;COUNT=1;FREQ=MINUTELY',
@@ -202,7 +205,8 @@ describe('<ScheduleEdit />', () => {
frequency: 'none', frequency: 'none',
interval: 1, interval: 1,
name: 'Run once schedule', name: 'Run once schedule',
startDateTime: '2020-03-25T10:00:00', startDate: '2020-03-25',
startTime: '10:00:00',
timezone: 'America/New_York', timezone: 'America/New_York',
}); });
}); });
@@ -223,7 +227,8 @@ describe('<ScheduleEdit />', () => {
interval: 10, interval: 10,
name: 'Run every 10 minutes 10 times', name: 'Run every 10 minutes 10 times',
occurrences: 10, occurrences: 10,
startDateTime: '2020-03-25T10:30:00', startDate: '2020-03-25',
startTime: '10:30:00',
timezone: 'America/New_York', timezone: 'America/New_York',
}); });
}); });
@@ -241,11 +246,13 @@ describe('<ScheduleEdit />', () => {
wrapper.find('Formik').invoke('onSubmit')({ wrapper.find('Formik').invoke('onSubmit')({
description: 'test description', description: 'test description',
end: 'onDate', end: 'onDate',
endDateTime: '2020-03-26T10:45:00', endDate: '2020-03-26',
endTime: '10:45:00',
frequency: 'hour', frequency: 'hour',
interval: 1, interval: 1,
name: 'Run every hour until date', name: 'Run every hour until date',
startDateTime: '2020-03-25T10:45:00', startDate: '2020-03-25',
startTime: '10:45:00',
timezone: 'America/New_York', timezone: 'America/New_York',
}); });
}); });
@@ -265,7 +272,8 @@ describe('<ScheduleEdit />', () => {
frequency: 'day', frequency: 'day',
interval: 1, interval: 1,
name: 'Run daily', name: 'Run daily',
startDateTime: '2020-03-25T10:45:00', startDate: '2020-03-25',
startTime: '10:45:00',
timezone: 'America/New_York', timezone: 'America/New_York',
}); });
}); });
@@ -287,7 +295,8 @@ describe('<ScheduleEdit />', () => {
interval: 1, interval: 1,
name: 'Run weekly on mon/wed/fri', name: 'Run weekly on mon/wed/fri',
occurrences: 1, occurrences: 1,
startDateTime: '2020-03-25T10:45:00', startDate: '2020-03-25',
startTime: '10:45:00',
timezone: 'America/New_York', timezone: 'America/New_York',
}); });
}); });
@@ -310,7 +319,8 @@ describe('<ScheduleEdit />', () => {
occurrences: 1, occurrences: 1,
runOn: 'day', runOn: 'day',
runOnDayNumber: 1, runOnDayNumber: 1,
startDateTime: '2020-04-01T10:45', startDate: '2020-04-01',
startTime: '10:45',
timezone: 'America/New_York', timezone: 'America/New_York',
}); });
}); });
@@ -336,12 +346,14 @@ describe('<ScheduleEdit />', () => {
runOn: 'the', runOn: 'the',
runOnTheDay: 'tuesday', runOnTheDay: 'tuesday',
runOnTheOccurrence: -1, runOnTheOccurrence: -1,
startDateTime: '2020-03-31T11:00', startDate: '2020-03-31',
startTime: '11:00',
timezone: 'America/New_York', timezone: 'America/New_York',
}); });
}); });
expect(SchedulesAPI.update).toHaveBeenCalledWith(27, { expect(SchedulesAPI.update).toHaveBeenCalledWith(27, {
description: 'test description', description: 'test description',
endDateTime: '2020-03-26T11:00:00',
name: 'Run monthly on the last Tuesday', name: 'Run monthly on the last Tuesday',
extra_data: {}, extra_data: {},
occurrences: 1, occurrences: 1,
@@ -362,7 +374,8 @@ describe('<ScheduleEdit />', () => {
runOn: 'day', runOn: 'day',
runOnDayMonth: 3, runOnDayMonth: 3,
runOnDayNumber: 1, runOnDayNumber: 1,
startDateTime: '2020-03-01T00:00', startTime: '00:00',
startDate: '2020-03-01',
timezone: 'America/New_York', timezone: 'America/New_York',
}); });
}); });
@@ -388,7 +401,8 @@ describe('<ScheduleEdit />', () => {
runOnTheOccurrence: 2, runOnTheOccurrence: 2,
runOnTheDay: 'friday', runOnTheDay: 'friday',
runOnTheMonth: 4, runOnTheMonth: 4,
startDateTime: '2020-04-10T11:15', startTime: '11:15',
startDate: '2020-04-10',
timezone: 'America/New_York', timezone: 'America/New_York',
}); });
}); });
@@ -415,7 +429,8 @@ describe('<ScheduleEdit />', () => {
runOnTheOccurrence: 1, runOnTheOccurrence: 1,
runOnTheDay: 'weekday', runOnTheDay: 'weekday',
runOnTheMonth: 10, runOnTheMonth: 10,
startDateTime: '2020-04-10T11:15', startTime: '11:15',
startDate: '2020-04-10',
timezone: 'America/New_York', timezone: 'America/New_York',
}); });
}); });
@@ -526,7 +541,8 @@ describe('<ScheduleEdit />', () => {
wrapper.find('Formik').invoke('onSubmit')({ wrapper.find('Formik').invoke('onSubmit')({
name: mockSchedule.name, name: mockSchedule.name,
end: 'never', end: 'never',
endDateTime: '2021-01-29T14:15:00', endDate: '2021-01-29',
endTime: '14:15:00',
frequency: 'none', frequency: 'none',
occurrences: 1, occurrences: 1,
runOn: 'day', runOn: 'day',
@@ -536,7 +552,8 @@ describe('<ScheduleEdit />', () => {
runOnTheMonth: 1, runOnTheMonth: 1,
runOnTheOccurrence: 1, runOnTheOccurrence: 1,
skip_tags: '', skip_tags: '',
startDateTime: '2021-01-28T14:15:00', startDate: '2021-01-28',
startTime: '14:15:00',
timezone: 'America/New_York', timezone: 'America/New_York',
credentials: [ credentials: [
{ id: 3, name: 'Credential 3', kind: 'ssh', url: '' }, { id: 3, name: 'Credential 3', kind: 'ssh', url: '' },
@@ -622,7 +639,10 @@ describe('<ScheduleEdit />', () => {
await act(async () => await act(async () =>
wrapper.find('Button[aria-label="Save"]').prop('onClick')() wrapper.find('Button[aria-label="Save"]').prop('onClick')()
); );
expect(SchedulesAPI.update).toBeCalledWith(27, { expect(SchedulesAPI.update).toBeCalledWith(27, {
endDateTime: undefined,
startDateTime: undefined,
description: '', description: '',
extra_data: {}, extra_data: {},
occurrences: 1, occurrences: 1,
@@ -630,7 +650,7 @@ describe('<ScheduleEdit />', () => {
name: 'foo', name: 'foo',
inventory: 702, inventory: 702,
rrule: 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 () => { test('should submit survey with default values properly, without opening prompt wizard', async () => {
@@ -728,7 +748,8 @@ describe('<ScheduleEdit />', () => {
frequency: 'none', frequency: 'none',
interval: 1, interval: 1,
name: 'Run once schedule', name: 'Run once schedule',
startDateTime: '2020-03-25T10:00:00', startDate: '2020-03-25',
startTime: '10:00:00',
timezone: 'America/New_York', timezone: 'America/New_York',
}); });
}); });

View File

@@ -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 (
<FormGroup
fieldId={`schedule-${label}-datetime`}
helperTextInvalid={dateMeta.error || timeMeta.error}
isRequired
validated={
(!dateMeta.touched || !dateMeta.error) &&
(!timeMeta.touched || !timeMeta.error)
? 'default'
: 'error'
}
label={`${label} date/time`}
>
<DateTimeGroup>
<DatePicker
aria-label={t`${label} date`}
{...dateField}
value={dateField.value.split('T')[0]}
onChange={onDateChange}
/>
<TimePicker
placeholder="hh:mm AM/PM"
stepMinutes={15}
aria-label={t`${label} time`}
time={timeField.value}
{...timeField}
onChange={time => timeHelpers.setValue(time)}
/>
</DateTimeGroup>
</FormGroup>
);
}
export default DateTimePicker;

View File

@@ -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('<DateTimePicker/>', () => {
let wrapper;
test('should render properly', async () => {
await act(async () => {
wrapper = mountWithContexts(
<Formik
initialValues={{ startDate: '2021-05-26', startTime: '2:15 PM' }}
>
<DateTimePicker
dateFieldName="startDate"
timeFieldName="startTime"
label="Start"
/>
</Formik>
);
});
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(
<Formik
initialValues={{ startDate: '2021-05-26', startTime: '2:15 PM' }}
>
<DateTimePicker
dateFieldName="startDate"
timeFieldName="startTime"
label="Start"
/>
</Formik>
);
});
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');
});
});

View File

@@ -14,6 +14,7 @@ import {
import AnsibleSelect from '../../AnsibleSelect'; import AnsibleSelect from '../../AnsibleSelect';
import FormField from '../../FormField'; import FormField from '../../FormField';
import { required } from '../../../util/validators'; import { required } from '../../../util/validators';
import DateTimePicker from './DateTimePicker';
const RunOnRadio = styled(Radio)` const RunOnRadio = styled(Radio)`
label { label {
@@ -74,9 +75,8 @@ const FrequencyDetailSubform = () => {
const [runOnTheMonth] = useField({ const [runOnTheMonth] = useField({
name: 'runOnTheMonth', name: 'runOnTheMonth',
}); });
const [startDateTime] = useField({ const [startDate] = useField('startDate');
name: 'startDateTime',
});
const [daysOfWeek, daysOfWeekMeta, daysOfWeekHelpers] = useField({ const [daysOfWeek, daysOfWeekMeta, daysOfWeekHelpers] = useField({
name: 'daysOfWeek', name: 'daysOfWeek',
validate: required(t`Select a value for this field`), validate: required(t`Select a value for this field`),
@@ -93,10 +93,6 @@ const FrequencyDetailSubform = () => {
name: 'runOn', name: 'runOn',
validate: required(t`Select a value for this field`), 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({ const [frequency] = useField({
name: 'frequency', name: 'frequency',
}); });
@@ -317,7 +313,7 @@ const FrequencyDetailSubform = () => {
</FormGroup> </FormGroup>
)} )}
{(frequency?.value === 'month' || frequency?.value === 'year') && {(frequency?.value === 'month' || frequency?.value === 'year') &&
!isNaN(new Date(startDateTime.value)) && ( !isNaN(new Date(startDate.value)) && (
<FormGroup <FormGroup
name="runOn" name="runOn"
fieldId="schedule-run-on" fieldId="schedule-run-on"
@@ -538,29 +534,14 @@ const FrequencyDetailSubform = () => {
/> />
)} )}
{end?.value === 'onDate' && ( {end?.value === 'onDate' && (
<FormGroup <DateTimePicker
fieldId="schedule-end-datetime" dateFieldName="endDate"
helperTextInvalid={endDateTimeMeta.error} timeFieldName="endTime"
isRequired label={t`End`}
validated={ />
!endDateTimeMeta.touched || !endDateTimeMeta.error
? 'default'
: 'error'
}
label={t`End date/time`}
>
<input
className="pf-c-form-control"
type="datetime-local"
id="schedule-end-datetime"
step="1"
{...endDateTime}
/>
</FormGroup>
)} )}
</> </>
); );
/* eslint-enable no-restricted-globals */
}; };
export default FrequencyDetailSubform; export default FrequencyDetailSubform;

View File

@@ -22,12 +22,13 @@ import {
SubFormLayout, SubFormLayout,
FormFullWidthLayout, FormFullWidthLayout,
} from '../../FormLayout'; } from '../../FormLayout';
import { dateToInputDateTime, formatDateStringUTC } from '../../../util/dates'; import { dateToInputDateTime } from '../../../util/dates';
import useRequest from '../../../util/useRequest'; import useRequest from '../../../util/useRequest';
import { required } from '../../../util/validators'; import { required } from '../../../util/validators';
import { parseVariableField } from '../../../util/yaml'; import { parseVariableField } from '../../../util/yaml';
import FrequencyDetailSubform from './FrequencyDetailSubform'; import FrequencyDetailSubform from './FrequencyDetailSubform';
import SchedulePromptableFields from './SchedulePromptableFields'; import SchedulePromptableFields from './SchedulePromptableFields';
import DateTimePicker from './DateTimePicker';
const generateRunOnTheDay = (days = []) => { const generateRunOnTheDay = (days = []) => {
if ( if (
@@ -79,10 +80,6 @@ const generateRunOnTheDay = (days = []) => {
}; };
function ScheduleFormFields({ hasDaysToKeepField, zoneOptions }) { 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({ const [timezone, timezoneMeta] = useField({
name: 'timezone', name: 'timezone',
validate: required(t`Select a value for this field`), validate: required(t`Select a value for this field`),
@@ -108,25 +105,11 @@ function ScheduleFormFields({ hasDaysToKeepField, zoneOptions }) {
name="description" name="description"
type="text" type="text"
/> />
<FormGroup <DateTimePicker
fieldId="schedule-start-datetime" dateFieldName="startDate"
helperTextInvalid={startDateTimeMeta.error} timeFieldName="startTime"
isRequired label={t`Start`}
validated={ />
!startDateTimeMeta.touched || !startDateTimeMeta.error
? 'default'
: 'error'
}
label={t`Start date/time`}
>
<input
className="pf-c-form-control"
type="datetime-local"
id="schedule-start-datetime"
step="1"
{...startDateTime}
/>
</FormGroup>
<FormGroup <FormGroup
name="timezone" name="timezone"
fieldId="schedule-timezone" fieldId="schedule-timezone"
@@ -394,12 +377,15 @@ function ScheduleForm({
) { ) {
showPromptButton = true; showPromptButton = true;
} }
const [currentDate, time] = dateToInputDateTime(closestQuarterHour);
const [tomorrowDate] = dateToInputDateTime(tomorrow);
const initialValues = { const initialValues = {
daysOfWeek: [], daysOfWeek: [],
description: schedule.description || '', description: schedule.description || '',
end: 'never', end: 'never',
endDateTime: dateToInputDateTime(tomorrow), endDate: tomorrowDate,
endTime: time,
frequency: 'none', frequency: 'none',
interval: 1, interval: 1,
name: schedule.name || '', name: schedule.name || '',
@@ -410,7 +396,8 @@ function ScheduleForm({
runOnTheDay: 'sunday', runOnTheDay: 'sunday',
runOnTheMonth: 1, runOnTheMonth: 1,
runOnTheOccurrence: 1, runOnTheOccurrence: 1,
startDateTime: dateToInputDateTime(closestQuarterHour), startDate: currentDate,
startTime: time,
timezone: schedule.timezone || 'America/New_York', timezone: schedule.timezone || 'America/New_York',
}; };
const submitSchedule = ( const submitSchedule = (
@@ -462,14 +449,19 @@ function ScheduleForm({
} = RRule.fromString(schedule.rrule.replace(' ', '\n')); } = RRule.fromString(schedule.rrule.replace(' ', '\n'));
if (dtstart) { if (dtstart) {
overriddenValues.startDateTime = dateToInputDateTime( const [startDate, startTime] = dateToInputDateTime(schedule.dtstart);
new Date(formatDateStringUTC(dtstart))
); overriddenValues.startDate = startDate;
overriddenValues.startTime = startTime;
} }
if (schedule.until) { if (schedule.until) {
overriddenValues.end = 'onDate'; overriddenValues.end = 'onDate';
overriddenValues.endDateTime = schedule.until;
const [endDate, endTime] = dateToInputDateTime(schedule.until);
overriddenValues.endDate = endDate;
overriddenValues.endTime = endTime;
} else if (count) { } else if (count) {
overriddenValues.end = 'after'; overriddenValues.end = 'after';
overriddenValues.occurrences = count; overriddenValues.occurrences = count;
@@ -553,18 +545,18 @@ function ScheduleForm({
const errors = {}; const errors = {};
const { const {
end, end,
endDateTime, endDate,
frequency, frequency,
runOn, runOn,
runOnDayNumber, runOnDayNumber,
startDateTime, startDate,
} = values; } = values;
if ( if (
end === 'onDate' && end === 'onDate' &&
new Date(startDateTime) > 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 ( if (

View File

@@ -5,6 +5,7 @@ import {
mountWithContexts, mountWithContexts,
waitForElement, waitForElement,
} from '../../../../testUtils/enzymeHelpers'; } from '../../../../testUtils/enzymeHelpers';
import { dateToInputDateTime } from '../../../util/dates';
import { SchedulesAPI, JobTemplatesAPI, InventoriesAPI } from '../../../api'; import { SchedulesAPI, JobTemplatesAPI, InventoriesAPI } from '../../../api';
import ScheduleForm from './ScheduleForm'; import ScheduleForm from './ScheduleForm';
@@ -99,8 +100,12 @@ const nonRRuleValuesMatch = () => {
expect(wrapper.find('input#schedule-description').prop('value')).toBe( expect(wrapper.find('input#schedule-description').prop('value')).toBe(
'test description' '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( expect(wrapper.find('select#schedule-timezone').prop('value')).toBe(
'America/New_York' 'America/New_York'
@@ -472,6 +477,11 @@ describe('<ScheduleForm />', () => {
wrapper.unmount(); wrapper.unmount();
}); });
test('initially renders expected fields and values', () => { 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); expect(wrapper.find('ScheduleForm').length).toBe(1);
defaultFieldsVisible(); defaultFieldsVisible();
expect(wrapper.find('FormGroup[label="Run every"]').length).toBe(0); expect(wrapper.find('FormGroup[label="Run every"]').length).toBe(0);
@@ -483,9 +493,9 @@ describe('<ScheduleForm />', () => {
expect(wrapper.find('input#schedule-name').prop('value')).toBe(''); expect(wrapper.find('input#schedule-name').prop('value')).toBe('');
expect(wrapper.find('input#schedule-description').prop('value')).toBe(''); expect(wrapper.find('input#schedule-description').prop('value')).toBe('');
expect(
wrapper.find('input#schedule-start-datetime').prop('value') expect(wrapper.find('DatePicker').prop('value')).toMatch(`${date}`);
).toMatch(/\d\d\d\d-\d\d-\d\dT\d\d:\d\d:\d\d/); expect(wrapper.find('TimePicker').prop('time')).toMatch(`${time}`);
expect(wrapper.find('select#schedule-timezone').prop('value')).toBe( expect(wrapper.find('select#schedule-timezone').prop('value')).toBe(
'America/New_York' 'America/New_York'
); );
@@ -703,18 +713,18 @@ describe('<ScheduleForm />', () => {
expect(wrapper.find('input#end-on-date').prop('checked')).toBe(true); expect(wrapper.find('input#end-on-date').prop('checked')).toBe(true);
expect(wrapper.find('#schedule-end-datetime-helper').length).toBe(0); expect(wrapper.find('#schedule-end-datetime-helper').length).toBe(0);
await act(async () => { await act(async () => {
wrapper.find('input#schedule-end-datetime').simulate('change', { wrapper.find('DatePicker[aria-label="End date"]').prop('onChange')(
target: { name: 'endDateTime', value: '2020-03-14T01:45:00' }, '2020-03-14',
}); new Date('2020-03-14')
);
}); });
wrapper.update();
await act(async () => { 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.' 'Please select an end date/time that comes after the start date/time.'
); );
}); });
@@ -1041,7 +1051,7 @@ describe('<ScheduleForm />', () => {
rrule: rrule:
'DTSTART;TZID=America/New_York:20200402T144500 RRULE:INTERVAL=1;FREQ=WEEKLY;BYDAY=MO,WE,FR;UNTIL=20210101T050000Z', 'DTSTART;TZID=America/New_York:20200402T144500 RRULE:INTERVAL=1;FREQ=WEEKLY;BYDAY=MO,WE,FR;UNTIL=20210101T050000Z',
dtend: '2020-10-30T18:45:00Z', dtend: '2020-10-30T18:45:00Z',
until: '2021-01-01T00:00:00', until: '2021-01-01T01:00:00',
})} })}
resource={{ resource={{
id: 23, id: 23,
@@ -1090,9 +1100,12 @@ describe('<ScheduleForm />', () => {
expect( expect(
wrapper.find('input#schedule-days-of-week-sat').prop('checked') wrapper.find('input#schedule-days-of-week-sat').prop('checked')
).toBe(false); ).toBe(false);
expect(wrapper.find('input#schedule-end-datetime').prop('value')).toBe( expect(
'2021-01-01T00:00:00' 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 () => { test('initially renders expected fields and values with existing schedule that runs every month on the last weekday', async () => {
await act(async () => { await act(async () => {

View File

@@ -2,15 +2,20 @@ import { t } from '@lingui/macro';
import { RRule } from 'rrule'; import { RRule } from 'rrule';
import { getRRuleDayConstants } from '../../../util/dates'; 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) { export default function buildRuleObj(values) {
const [startDate, startTime] = values.startDateTime.split('T');
// Dates are formatted like "YYYY-MM-DD" // 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 // Times are formatted like "HH:MM:SS" or "HH:MM" if no seconds
// have been specified // have been specified
const [startHour = 0, startMinute = 0, startSecond = 0] = startTime.split( const [startHour, startMinute] = parseTime(values.startTime);
':'
);
const ruleObj = { const ruleObj = {
interval: values.interval, interval: values.interval,
@@ -20,8 +25,7 @@ export default function buildRuleObj(values) {
parseInt(startMonth, 10) - 1, parseInt(startMonth, 10) - 1,
startDay, startDay,
startHour, startHour,
startMinute, startMinute
startSecond
) )
), ),
tzid: values.timezone, tzid: values.timezone,
@@ -77,17 +81,16 @@ export default function buildRuleObj(values) {
ruleObj.count = values.occurrences; ruleObj.count = values.occurrences;
break; break;
case 'onDate': { case 'onDate': {
const [endDate, endTime] = values.endDateTime.split('T'); const [endYear, endMonth, endDay] = values.endDate.split('-');
const [endYear, endMonth, endDay] = endDate.split('-');
const [endHour = 0, endMinute = 0, endSecond = 0] = endTime.split(':'); const [endHour, endMinute] = parseTime(values.endTime);
ruleObj.until = new Date( ruleObj.until = new Date(
Date.UTC( Date.UTC(
endYear, endYear,
parseInt(endMonth, 10) - 1, parseInt(endMonth, 10) - 1,
endDay, endDay,
endHour, endHour,
endMinute, endMinute
endSecond
) )
); );
break; break;

View File

@@ -44,15 +44,19 @@ export function timeOfDay() {
} }
export function dateToInputDateTime(dateObj) { export function dateToInputDateTime(dateObj) {
// input type="date-time" expects values to be formatted let date = dateObj;
// like: YYYY-MM-DDTHH-MM-SS if (typeof dateObj === 'string') {
const year = dateObj.getFullYear(); date = new Date(dateObj);
const month = prependZeros(dateObj.getMonth() + 1); }
const day = prependZeros(dateObj.getDate()); const year = date.getFullYear();
const hour = prependZeros(dateObj.getHours()); const month = prependZeros(date.getMonth() + 1);
const minute = prependZeros(dateObj.getMinutes()); const day = prependZeros(date.getDate());
const second = prependZeros(dateObj.getSeconds()); const hour =
return `${year}-${month}-${day}T${hour}:${minute}:${second}`; 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) { export function getRRuleDayConstants(dayString) {

View File

@@ -70,7 +70,7 @@ describe('dateToInputDateTime', () => {
test('it returns the expected value', () => { test('it returns the expected value', () => {
expect( expect(
dateToInputDateTime(new Date('2018-01-31T01:14:52.969227Z')) dateToInputDateTime(new Date('2018-01-31T01:14:52.969227Z'))
).toEqual('2018-01-31T01:14:52'); ).toEqual(['2018-01-31', '1:14 AM']);
}); });
}); });

View File

@@ -1,4 +1,5 @@
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { isValidDate } from '@patternfly/react-core';
export function required(message) { export function required(message) {
const errorMessage = message || t`This field must not be blank`; const errorMessage = message || t`This field must not be blank`;
@@ -15,6 +16,25 @@ export function required(message) {
return undefined; 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) { export function maxLength(max) {
return value => { return value => {

View File

@@ -9,6 +9,7 @@ import {
combine, combine,
regExp, regExp,
requiredEmail, requiredEmail,
validateTime,
} from './validators'; } from './validators';
describe('validators', () => { describe('validators', () => {
@@ -168,4 +169,13 @@ describe('validators', () => {
test('bob has email', () => { test('bob has email', () => {
expect(requiredEmail()('bob@localhost')).toBeUndefined(); 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');
});
}); });