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,
"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",

View File

@ -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 },

View File

@ -82,7 +82,8 @@ describe('<ScheduleAdd />', () => {
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('<ScheduleAdd />', () => {
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('<ScheduleAdd />', () => {
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('<ScheduleAdd />', () => {
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('<ScheduleAdd />', () => {
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('<ScheduleAdd />', () => {
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('<ScheduleAdd />', () => {
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('<ScheduleAdd />', () => {
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('<ScheduleAdd />', () => {
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('<ScheduleAdd />', () => {
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('<ScheduleAdd />', () => {
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('<ScheduleAdd />', () => {
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('<ScheduleAdd />', () => {
{ 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('<ScheduleAdd />', () => {
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',
});
});

View File

@ -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) {

View File

@ -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('<ScheduleEdit />', () => {
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('<ScheduleEdit />', () => {
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('<ScheduleEdit />', () => {
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('<ScheduleEdit />', () => {
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('<ScheduleEdit />', () => {
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('<ScheduleEdit />', () => {
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('<ScheduleEdit />', () => {
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('<ScheduleEdit />', () => {
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('<ScheduleEdit />', () => {
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('<ScheduleEdit />', () => {
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('<ScheduleEdit />', () => {
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('<ScheduleEdit />', () => {
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('<ScheduleEdit />', () => {
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('<ScheduleEdit />', () => {
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('<ScheduleEdit />', () => {
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',
});
});

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 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 = () => {
</FormGroup>
)}
{(frequency?.value === 'month' || frequency?.value === 'year') &&
!isNaN(new Date(startDateTime.value)) && (
!isNaN(new Date(startDate.value)) && (
<FormGroup
name="runOn"
fieldId="schedule-run-on"
@ -538,29 +534,14 @@ const FrequencyDetailSubform = () => {
/>
)}
{end?.value === 'onDate' && (
<FormGroup
fieldId="schedule-end-datetime"
helperTextInvalid={endDateTimeMeta.error}
isRequired
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>
<DateTimePicker
dateFieldName="endDate"
timeFieldName="endTime"
label={t`End`}
/>
)}
</>
);
/* eslint-enable no-restricted-globals */
};
export default FrequencyDetailSubform;

View File

@ -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"
/>
<FormGroup
fieldId="schedule-start-datetime"
helperTextInvalid={startDateTimeMeta.error}
isRequired
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>
<DateTimePicker
dateFieldName="startDate"
timeFieldName="startTime"
label={t`Start`}
/>
<FormGroup
name="timezone"
fieldId="schedule-timezone"
@ -394,12 +377,15 @@ function ScheduleForm({
) {
showPromptButton = true;
}
const [currentDate, time] = dateToInputDateTime(closestQuarterHour);
const [tomorrowDate] = dateToInputDateTime(tomorrow);
const initialValues = {
daysOfWeek: [],
description: schedule.description || '',
end: 'never',
endDateTime: dateToInputDateTime(tomorrow),
endDate: tomorrowDate,
endTime: time,
frequency: 'none',
interval: 1,
name: schedule.name || '',
@ -410,7 +396,8 @@ function ScheduleForm({
runOnTheDay: 'sunday',
runOnTheMonth: 1,
runOnTheOccurrence: 1,
startDateTime: dateToInputDateTime(closestQuarterHour),
startDate: currentDate,
startTime: time,
timezone: schedule.timezone || 'America/New_York',
};
const submitSchedule = (
@ -462,14 +449,19 @@ function ScheduleForm({
} = RRule.fromString(schedule.rrule.replace(' ', '\n'));
if (dtstart) {
overriddenValues.startDateTime = dateToInputDateTime(
new Date(formatDateStringUTC(dtstart))
);
const [startDate, startTime] = dateToInputDateTime(schedule.dtstart);
overriddenValues.startDate = startDate;
overriddenValues.startTime = startTime;
}
if (schedule.until) {
overriddenValues.end = 'onDate';
overriddenValues.endDateTime = schedule.until;
const [endDate, endTime] = dateToInputDateTime(schedule.until);
overriddenValues.endDate = endDate;
overriddenValues.endTime = endTime;
} else if (count) {
overriddenValues.end = 'after';
overriddenValues.occurrences = count;
@ -553,18 +545,18 @@ function ScheduleForm({
const errors = {};
const {
end,
endDateTime,
endDate,
frequency,
runOn,
runOnDayNumber,
startDateTime,
startDate,
} = values;
if (
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 (

View File

@ -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('<ScheduleForm />', () => {
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('<ScheduleForm />', () => {
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('<ScheduleForm />', () => {
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('<ScheduleForm />', () => {
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('<ScheduleForm />', () => {
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 () => {

View File

@ -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;

View File

@ -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) {

View File

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

View File

@ -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 => {

View File

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