From c7b23aac9b7bf0e2e6b44eba32bbaa9c9b62e82f Mon Sep 17 00:00:00 2001 From: mabashian Date: Mon, 30 Mar 2020 15:36:48 -0400 Subject: [PATCH] Removes static run on string options and opts for the more dynamic ux pattern already adopted in the old UI --- .../AnsibleSelect/AnsibleSelect.jsx | 14 +- .../Schedule/ScheduleAdd/ScheduleAdd.jsx | 45 +-- .../Schedule/ScheduleAdd/ScheduleAdd.test.jsx | 52 ++- .../shared/FrequencyDetailSubform.jsx | 347 ++++++++++++------ .../Schedule/shared/ScheduleForm.jsx | 26 +- .../Schedule/shared/ScheduleForm.test.jsx | 142 ++----- awx/ui_next/src/util/dates.jsx | 113 ++---- awx/ui_next/src/util/dates.test.jsx | 93 ++--- 8 files changed, 403 insertions(+), 429 deletions(-) diff --git a/awx/ui_next/src/components/AnsibleSelect/AnsibleSelect.jsx b/awx/ui_next/src/components/AnsibleSelect/AnsibleSelect.jsx index cf860ab8f0..bd348998d1 100644 --- a/awx/ui_next/src/components/AnsibleSelect/AnsibleSelect.jsx +++ b/awx/ui_next/src/components/AnsibleSelect/AnsibleSelect.jsx @@ -25,7 +25,16 @@ class AnsibleSelect extends React.Component { } render() { - const { id, data, i18n, isValid, onBlur, value, className } = this.props; + const { + id, + data, + i18n, + isValid, + onBlur, + value, + className, + isDisabled, + } = this.props; return ( {data.map(option => ( {}, className: '', + isDisabled: false, }; AnsibleSelect.propTypes = { @@ -72,6 +83,7 @@ AnsibleSelect.propTypes = { onChange: func.isRequired, value: oneOfType([string, number]).isRequired, className: string, + isDisabled: bool, }; export { AnsibleSelect as _AnsibleSelect }; diff --git a/awx/ui_next/src/components/Schedule/ScheduleAdd/ScheduleAdd.jsx b/awx/ui_next/src/components/Schedule/ScheduleAdd/ScheduleAdd.jsx index c1894ba5bb..23f98cd383 100644 --- a/awx/ui_next/src/components/Schedule/ScheduleAdd/ScheduleAdd.jsx +++ b/awx/ui_next/src/components/Schedule/ScheduleAdd/ScheduleAdd.jsx @@ -6,19 +6,9 @@ import { useHistory, useLocation } from 'react-router-dom'; import { RRule } from 'rrule'; import { Card } from '@patternfly/react-core'; import { CardBody } from '@components/Card'; -import { getWeekNumber } from '@util/dates'; +import { getRRuleDayConstants } from '@util/dates'; import ScheduleForm from '../shared/ScheduleForm'; -const days = { - 0: 'SU', - 1: 'MO', - 2: 'TU', - 3: 'WE', - 4: 'TH', - 5: 'FR', - 6: 'SA', -}; - function ScheduleAdd({ i18n, createSchedule }) { const [formSubmitError, setFormSubmitError] = useState(null); const history = useHistory(); @@ -71,31 +61,22 @@ function ScheduleAdd({ i18n, createSchedule }) { break; case 'month': ruleObj.freq = RRule.MONTHLY; - if (values.runOn === 'number') { - ruleObj.bymonthday = startDay; - } else if (values.runOn === 'day') { - ruleObj.byweekday = - RRule[days[new Date(values.startDateTime).getDay()]]; - ruleObj.bysetpos = getWeekNumber(values.startDateTime); - } else if (values.runOn === 'lastDay') { - ruleObj.byweekday = - RRule[days[new Date(values.startDateTime).getDay()]]; - ruleObj.bysetpos = -1; + if (values.runOn === 'day') { + ruleObj.bymonthday = values.runOnDayNumber; + } else if (values.runOn === 'the') { + ruleObj.bysetpos = parseInt(values.runOnTheOccurrence, 10); + ruleObj.byweekday = getRRuleDayConstants(values.runOnTheDay, i18n); } break; case 'year': ruleObj.freq = RRule.YEARLY; - ruleObj.bymonth = new Date(values.startDateTime).getMonth() + 1; - if (values.runOn === 'number') { - ruleObj.bymonthday = startDay; - } else if (values.runOn === 'day') { - ruleObj.byweekday = - RRule[days[new Date(values.startDateTime).getDay()]]; - ruleObj.bysetpos = getWeekNumber(values.startDateTime); - } else if (values.runOn === 'lastDay') { - ruleObj.byweekday = - RRule[days[new Date(values.startDateTime).getDay()]]; - ruleObj.bysetpos = -1; + if (values.runOn === 'day') { + ruleObj.bymonth = parseInt(values.runOnDayMonth, 10); + ruleObj.bymonthday = values.runOnDayNumber; + } else if (values.runOn === 'the') { + ruleObj.bysetpos = parseInt(values.runOnTheOccurrence, 10); + ruleObj.byweekday = getRRuleDayConstants(values.runOnTheDay, i18n); + ruleObj.bymonth = parseInt(values.runOnTheMonth, 10); } break; default: 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 50f1232c25..8a3376e504 100644 --- a/awx/ui_next/src/components/Schedule/ScheduleAdd/ScheduleAdd.test.jsx +++ b/awx/ui_next/src/components/Schedule/ScheduleAdd/ScheduleAdd.test.jsx @@ -64,7 +64,6 @@ describe('', () => { interval: 10, name: 'Run every 10 minutes 10 times', occurrences: 10, - runOn: 'number', startDateTime: '2020-03-25T10:30:00', timezone: 'America/New_York', }); @@ -85,7 +84,6 @@ describe('', () => { frequency: 'hour', interval: 1, name: 'Run every hour until date', - runOn: 'number', startDateTime: '2020-03-25T10:45:00', timezone: 'America/New_York', }); @@ -105,7 +103,6 @@ describe('', () => { frequency: 'day', interval: 1, name: 'Run daily', - runOn: 'number', startDateTime: '2020-03-25T10:45:00', timezone: 'America/New_York', }); @@ -127,7 +124,6 @@ describe('', () => { interval: 1, name: 'Run weekly on mon/wed/fri', occurrences: 1, - runOn: 'number', startDateTime: '2020-03-25T10:45:00', timezone: 'America/New_York', }); @@ -148,7 +144,8 @@ describe('', () => { interval: 1, name: 'Run on the first day of the month', occurrences: 1, - runOn: 'number', + runOn: 'day', + runOnDayNumber: 1, startDateTime: '2020-04-01T10:45', timezone: 'America/New_York', }); @@ -157,7 +154,7 @@ describe('', () => { description: 'test description', name: 'Run on the first day of the month', rrule: - 'DTSTART;TZID=America/New_York:20200401T104500 RRULE:INTERVAL=1;FREQ=MONTHLY;BYMONTHDAY=01', + 'DTSTART;TZID=America/New_York:20200401T104500 RRULE:INTERVAL=1;FREQ=MONTHLY;BYMONTHDAY=1', }); }); test('Successfully creates a schedule with monthly repeat frequency on the last tuesday of the month', async () => { @@ -170,7 +167,9 @@ describe('', () => { interval: 1, name: 'Run monthly on the last Tuesday', occurrences: 1, - runOn: 'lastDay', + runOn: 'the', + runOnTheDay: 'tuesday', + runOnTheOccurrence: -1, startDateTime: '2020-03-31T11:00', timezone: 'America/New_York', }); @@ -179,7 +178,7 @@ describe('', () => { description: 'test description', name: 'Run monthly on the last Tuesday', rrule: - 'DTSTART;TZID=America/New_York:20200331T110000 RRULE:INTERVAL=1;FREQ=MONTHLY;BYDAY=TU;BYSETPOS=-1', + 'DTSTART;TZID=America/New_York:20200331T110000 RRULE:INTERVAL=1;FREQ=MONTHLY;BYSETPOS=-1;BYDAY=TU', }); }); test('Successfully creates a schedule with yearly repeat frequency on the first day of March', async () => { @@ -191,7 +190,9 @@ describe('', () => { interval: 1, name: 'Yearly on the first day of March', occurrences: 1, - runOn: 'number', + runOn: 'day', + runOnDayMonth: 3, + runOnDayNumber: 1, startDateTime: '2020-03-01T00:00', timezone: 'America/New_York', }); @@ -200,7 +201,7 @@ describe('', () => { description: 'test description', name: 'Yearly on the first day of March', rrule: - 'DTSTART;TZID=America/New_York:20200301T000000 RRULE:INTERVAL=1;FREQ=YEARLY;BYMONTH=3;BYMONTHDAY=01', + 'DTSTART;TZID=America/New_York:20200301T000000 RRULE:INTERVAL=1;FREQ=YEARLY;BYMONTH=3;BYMONTHDAY=1', }); }); test('Successfully creates a schedule with yearly repeat frequency on the second Friday in April', async () => { @@ -212,7 +213,10 @@ describe('', () => { interval: 1, name: 'Yearly on the second Friday in April', occurrences: 1, - runOn: 'day', + runOn: 'the', + runOnTheOccurrence: 2, + runOnTheDay: 'friday', + runOnTheMonth: 4, startDateTime: '2020-04-10T11:15', timezone: 'America/New_York', }); @@ -221,7 +225,31 @@ describe('', () => { description: 'test description', name: 'Yearly on the second Friday in April', rrule: - 'DTSTART;TZID=America/New_York:20200410T111500 RRULE:INTERVAL=1;FREQ=YEARLY;BYMONTH=4;BYDAY=FR;BYSETPOS=2', + 'DTSTART;TZID=America/New_York:20200410T111500 RRULE:INTERVAL=1;FREQ=YEARLY;BYSETPOS=2;BYDAY=FR;BYMONTH=4', + }); + }); + test('Successfully creates a schedule with yearly repeat frequency on the first weekday in October', async () => { + await act(async () => { + wrapper.find('ScheduleForm').invoke('handleSubmit')({ + description: 'test description', + end: 'never', + frequency: 'year', + interval: 1, + name: 'Yearly on the first weekday in October', + occurrences: 1, + runOn: 'the', + runOnTheOccurrence: 1, + runOnTheDay: 'weekday', + runOnTheMonth: 10, + startDateTime: '2020-04-10T11:15', + timezone: 'America/New_York', + }); + }); + expect(createSchedule).toHaveBeenCalledWith({ + description: 'test description', + name: 'Yearly on the first weekday in October', + rrule: + 'DTSTART;TZID=America/New_York:20200410T111500 RRULE:INTERVAL=1;FREQ=YEARLY;BYSETPOS=1;BYDAY=MO,TU,WE,TH,FR;BYMONTH=10', }); }); }); diff --git a/awx/ui_next/src/components/Schedule/shared/FrequencyDetailSubform.jsx b/awx/ui_next/src/components/Schedule/shared/FrequencyDetailSubform.jsx index 24fb8f9bbf..2ca554b8dd 100644 --- a/awx/ui_next/src/components/Schedule/shared/FrequencyDetailSubform.jsx +++ b/awx/ui_next/src/components/Schedule/shared/FrequencyDetailSubform.jsx @@ -1,4 +1,4 @@ -import React, { useEffect } from 'react'; +import React from 'react'; import styled from 'styled-components'; import { useField } from 'formik'; import { withI18n } from '@lingui/react'; @@ -9,16 +9,25 @@ import { Radio, TextInput, } from '@patternfly/react-core'; +import AnsibleSelect from '@components/AnsibleSelect'; import FormField from '@components/FormField'; -import { - getDaysInMonth, - getDayString, - getMonthString, - getWeekString, - getWeekNumber, -} from '@util/dates'; import { required } from '@util/validators'; +const RunOnRadio = styled(Radio)` + label { + display: block; + width: 100%; + } + + :not(:last-of-type) { + margin-bottom: 10px; + } + + select:not(:first-of-type) { + margin-left: 10px; + } +`; + const RunEveryLabel = styled.p` display: flex; align-items: center; @@ -48,6 +57,21 @@ export function requiredPositiveInteger(i18n) { } const FrequencyDetailSubform = ({ i18n }) => { + const [runOnDayMonth] = useField({ + name: 'runOnDayMonth', + }); + const [runOnDayNumber] = useField({ + name: 'runOnDayNumber', + }); + const [runOnTheOccurrence] = useField({ + name: 'runOnTheOccurrence', + }); + const [runOnTheDay] = useField({ + name: 'runOnTheDay', + }); + const [runOnTheMonth] = useField({ + name: 'runOnTheMonth', + }); const [startDateTime] = useField({ name: 'startDateTime', }); @@ -63,7 +87,7 @@ const FrequencyDetailSubform = ({ i18n }) => { name: 'interval', validate: requiredPositiveInteger(i18n), }); - const [runOn, runOnMeta, runOnHelpers] = useField({ + const [runOn, runOnMeta] = useField({ name: 'runOn', validate: required(i18n._(t`Select a value for this field`), i18n), }); @@ -79,20 +103,64 @@ const FrequencyDetailSubform = ({ i18n }) => { validate: requiredPositiveInteger(i18n), }); - useEffect(() => { - // The Last day option disappears if the start date isn't in the - // last week of the month. If that value was selected when this - // happens then we'll clear out the selection and force the user - // to choose between the remaining two. - if ( - (frequency.value === 'month' || frequency.value === 'year') && - runOn.value === 'lastDay' && - getDaysInMonth(startDateTime.value) - 7 >= - new Date(startDateTime.value).getDate() - ) { - runOnHelpers.setValue(''); - } - }, [startDateTime.value, frequency.value, runOn.value, runOnHelpers]); + const monthOptions = [ + { + key: 'january', + value: 1, + label: i18n._(t`January`), + }, + { + key: 'february', + value: 2, + label: i18n._(t`February`), + }, + { + key: 'march', + value: 3, + label: i18n._(t`March`), + }, + { + key: 'april', + value: 4, + label: i18n._(t`April`), + }, + { + key: 'may', + value: 5, + label: i18n._(t`May`), + }, + { + key: 'june', + value: 6, + label: i18n._(t`June`), + }, + { + key: 'july', + value: 7, + label: i18n._(t`July`), + }, + { key: 'august', value: 8, label: i18n._(t`August`) }, + { + key: 'september', + value: 9, + label: i18n._(t`September`), + }, + { + key: 'october', + value: 10, + label: i18n._(t`October`), + }, + { + key: 'november', + value: 11, + label: i18n._(t`November`), + }, + { + key: 'december', + value: 12, + label: i18n._(t`December`), + }, + ]; const updateDaysOfWeek = (day, checked) => { const newDaysOfWeek = [...daysOfWeek.value]; @@ -149,66 +217,6 @@ const FrequencyDetailSubform = ({ i18n }) => { } }; - const generateRunOnNumberLabel = () => { - switch (frequency.value) { - case 'month': - return i18n._( - t`Day ${startDateTime.value.split('T')[0].split('-')[2]}` - ); - case 'year': { - const monthString = getMonthString( - new Date(startDateTime.value).getMonth(), - i18n - ); - return `${monthString} ${new Date(startDateTime.value).getDate()}`; - } - default: - throw new Error(i18n._(t`Frequency did not match an expected value`)); - } - }; - - const generateRunOnDayLabel = () => { - const dayString = getDayString( - new Date(startDateTime.value).getDay(), - i18n - ); - const weekNumber = getWeekNumber(startDateTime.value); - const weekString = getWeekString(weekNumber, i18n); - switch (frequency.value) { - case 'month': - return i18n._(t`The ${weekString} ${dayString}`); - case 'year': { - const monthString = getMonthString( - new Date(startDateTime.value).getMonth(), - i18n - ); - return i18n._(t`The ${weekString} ${dayString} in ${monthString}`); - } - default: - throw new Error(i18n._(t`Frequency did not match an expected value`)); - } - }; - - const generateRunOnLastDayLabel = () => { - const dayString = getDayString( - new Date(startDateTime.value).getDay(), - i18n - ); - switch (frequency.value) { - case 'month': - return i18n._(t`The last ${dayString}`); - case 'year': { - const monthString = getMonthString( - new Date(startDateTime.value).getMonth(), - i18n - ); - return i18n._(t`The last ${dayString} in ${monthString}`); - } - default: - throw new Error(i18n._(t`Frequency did not match an expected value`)); - } - }; - /* eslint-disable no-restricted-globals */ return ( <> @@ -252,7 +260,7 @@ const FrequencyDetailSubform = ({ i18n }) => { updateDaysOfWeek('SU', checked); }} aria-label={i18n._(t`Sunday`)} - id="days-of-week-sun" + id="schedule-days-of-week-sun" name="daysOfWeek" /> { updateDaysOfWeek('MO', checked); }} aria-label={i18n._(t`Monday`)} - id="days-of-week-mon" + id="schedule-days-of-week-mon" name="daysOfWeek" /> { updateDaysOfWeek('TU', checked); }} aria-label={i18n._(t`Tuesday`)} - id="days-of-week-tue" + id="schedule-days-of-week-tue" name="daysOfWeek" /> { updateDaysOfWeek('WE', checked); }} aria-label={i18n._(t`Wednesday`)} - id="days-of-week-wed" + id="schedule-days-of-week-wed" name="daysOfWeek" /> { updateDaysOfWeek('TH', checked); }} aria-label={i18n._(t`Thursday`)} - id="days-of-week-thu" + id="schedule-days-of-week-thu" name="daysOfWeek" /> { updateDaysOfWeek('FR', checked); }} aria-label={i18n._(t`Friday`)} - id="days-of-week-fri" + id="schedule-days-of-week-fri" name="daysOfWeek" /> { updateDaysOfWeek('SA', checked); }} aria-label={i18n._(t`Saturday`)} - id="days-of-week-sat" + id="schedule-days-of-week-sat" name="daysOfWeek" /> @@ -328,21 +336,39 @@ const FrequencyDetailSubform = ({ i18n }) => { isValid={!runOnMeta.touched || !runOnMeta.error} label={i18n._(t`Run on`)} > - { - event.target.value = 'number'; - runOn.onChange(event); - }} - /> - + {frequency?.value === 'month' && ( + + Day + + )} + {frequency?.value === 'year' && ( + + )} + { + runOnDayNumber.onChange(event); + }} + /> + + } value="day" isChecked={runOn.value === 'day'} onChange={(value, event) => { @@ -350,20 +376,105 @@ const FrequencyDetailSubform = ({ i18n }) => { runOn.onChange(event); }} /> - {new Date(startDateTime.value).getDate() > - getDaysInMonth(startDateTime.value) - 7 && ( - { - event.target.value = 'lastDay'; - runOn.onChange(event); - }} - /> - )} + + + The + + + + {frequency?.value === 'year' && ( + + )} + + } + value="the" + isChecked={runOn.value === 'the'} + onChange={(value, event) => { + event.target.value = 'the'; + runOn.onChange(event); + }} + /> )} { const errors = {}; - const { end, endDateTime, startDateTime } = values; + const { + end, + endDateTime, + frequency, + runOn, + runOnDayNumber, + startDateTime, + } = values; if ( end === 'onDate' && @@ -190,6 +202,16 @@ function ScheduleForm({ ); } + if ( + (frequency === 'month' || frequency === 'year') && + runOn === 'day' && + (runOnDayNumber < 1 || runOnDayNumber > 31) + ) { + errors.runOn = i18n._( + t`Please select a day number between 1 and 31` + ); + } + return errors; }} > 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 144182c6c7..2487f9beee 100644 --- a/awx/ui_next/src/components/Schedule/shared/ScheduleForm.test.jsx +++ b/awx/ui_next/src/components/Schedule/shared/ScheduleForm.test.jsx @@ -216,67 +216,17 @@ describe('', () => { expect(wrapper.find('input#end-never').prop('checked')).toBe(true); expect(wrapper.find('input#end-after').prop('checked')).toBe(false); expect(wrapper.find('input#end-on-date').prop('checked')).toBe(false); - }); - test('month run on options displayed correctly as date changes', async () => { - await act(async () => { - wrapper.find('input#schedule-start-datetime').simulate('change', { - target: { value: '2020-03-23T01:45:00', name: 'startDateTime' }, - }); - }); - wrapper.update(); - expect(wrapper.find('input#run-on-number').prop('checked')).toBe(true); - expect(wrapper.find('input#run-on-number + label').text()).toBe('Day 23'); - expect(wrapper.find('input#run-on-day').prop('checked')).toBe(false); - expect(wrapper.find('input#run-on-day + label').text()).toBe( - 'The fourth Monday' + expect(wrapper.find('input#schedule-run-on-day').prop('checked')).toBe( + true ); - expect(wrapper.find('input#run-on-last-day').length).toBe(0); - await act(async () => { - wrapper.find('input#schedule-start-datetime').simulate('change', { - target: { value: '2020-03-27T01:45:00', name: 'startDateTime' }, - }); - }); - wrapper.update(); - expect(wrapper.find('input#run-on-number').prop('checked')).toBe(true); - expect(wrapper.find('input#run-on-number + label').text()).toBe('Day 27'); - expect(wrapper.find('input#run-on-day').prop('checked')).toBe(false); - expect(wrapper.find('input#run-on-day + label').text()).toBe( - 'The fourth Friday' + expect( + wrapper.find('input#schedule-run-on-day-number').prop('value') + ).toBe(1); + expect(wrapper.find('input#schedule-run-on-the').prop('checked')).toBe( + false ); - expect(wrapper.find('input#run-on-last-day').prop('checked')).toBe(false); - expect(wrapper.find('input#run-on-last-day + label').text()).toBe( - 'The last Friday' - ); - }); - test('month run on cleared when last day selected but date changes from one of the last seven days of the month', async () => { - await act(async () => { - wrapper.find('Radio#run-on-last-day').invoke('onChange')('lastDay', { - target: { name: 'runOn' }, - }); - }); - wrapper.update(); - expect(wrapper.find('input#run-on-number').prop('checked')).toBe(false); - expect(wrapper.find('input#run-on-day').prop('checked')).toBe(false); - expect(wrapper.find('input#run-on-last-day').prop('checked')).toBe(true); - await act(async () => { - wrapper.find('input#schedule-start-datetime').simulate('change', { - target: { value: '2020-03-15T01:45:00', name: 'startDateTime' }, - }); - }); - wrapper.update(); - expect(wrapper.find('input#run-on-number').prop('checked')).toBe(false); - expect(wrapper.find('input#run-on-number + label').text()).toBe('Day 15'); - expect(wrapper.find('input#run-on-day').prop('checked')).toBe(false); - expect(wrapper.find('input#run-on-day + label').text()).toBe( - 'The third Sunday' - ); - expect(wrapper.find('input#run-on-last-day').length).toBe(0); - await act(async () => { - wrapper.find('Radio#run-on-number').invoke('onChange')('number', { - target: { name: 'runOn' }, - }); - }); - wrapper.update(); + expect(wrapper.find('select#schedule-run-on-day-month').length).toBe(0); + expect(wrapper.find('select#schedule-run-on-the-month').length).toBe(0); }); test('correct frequency details fields and values shown when frequency changed to year', async () => { const runFrequencySelect = wrapper.find( @@ -300,43 +250,19 @@ describe('', () => { expect(wrapper.find('input#end-never').prop('checked')).toBe(true); expect(wrapper.find('input#end-after').prop('checked')).toBe(false); expect(wrapper.find('input#end-on-date').prop('checked')).toBe(false); + expect(wrapper.find('input#schedule-run-on-day').prop('checked')).toBe( + true + ); + expect( + wrapper.find('input#schedule-run-on-day-number').prop('value') + ).toBe(1); + expect(wrapper.find('input#schedule-run-on-the').prop('checked')).toBe( + false + ); + expect(wrapper.find('select#schedule-run-on-day-month').length).toBe(1); + expect(wrapper.find('select#schedule-run-on-the-month').length).toBe(1); }); - test('year run on options displayed correctly as date changes', async () => { - await act(async () => { - wrapper.find('input#schedule-start-datetime').simulate('change', { - target: { value: '2020-03-23T01:45:00', name: 'startDateTime' }, - }); - }); - wrapper.update(); - expect(wrapper.find('input#run-on-number').prop('checked')).toBe(true); - expect(wrapper.find('input#run-on-number + label').text()).toBe( - 'March 23' - ); - expect(wrapper.find('input#run-on-day').prop('checked')).toBe(false); - expect(wrapper.find('input#run-on-day + label').text()).toBe( - 'The fourth Monday in March' - ); - expect(wrapper.find('input#run-on-last-day').length).toBe(0); - await act(async () => { - wrapper.find('input#schedule-start-datetime').simulate('change', { - target: { value: '2020-03-27T01:45:00', name: 'startDateTime' }, - }); - }); - wrapper.update(); - expect(wrapper.find('input#run-on-number').prop('checked')).toBe(true); - expect(wrapper.find('input#run-on-number + label').text()).toBe( - 'March 27' - ); - expect(wrapper.find('input#run-on-day').prop('checked')).toBe(false); - expect(wrapper.find('input#run-on-day + label').text()).toBe( - 'The fourth Friday in March' - ); - expect(wrapper.find('input#run-on-last-day').prop('checked')).toBe(false); - expect(wrapper.find('input#run-on-last-day + label').text()).toBe( - 'The last Friday in March' - ); - }); - test('occurrences field properly shown when that run on selection is made', async () => { + test('occurrences field properly shown when end after selection is made', async () => { await act(async () => { wrapper.find('Radio#end-after').invoke('onChange')('after', { target: { name: 'end' }, @@ -355,32 +281,6 @@ describe('', () => { }); wrapper.update(); }); - test('year run on cleared when last day selected but date changes from one of the last seven days of the month', async () => { - await act(async () => { - wrapper.find('Radio#run-on-last-day').invoke('onChange')('lastDay', { - target: { name: 'runOn' }, - }); - }); - wrapper.update(); - expect(wrapper.find('input#run-on-number').prop('checked')).toBe(false); - expect(wrapper.find('input#run-on-day').prop('checked')).toBe(false); - expect(wrapper.find('input#run-on-last-day').prop('checked')).toBe(true); - await act(async () => { - wrapper.find('input#schedule-start-datetime').simulate('change', { - target: { value: '2020-03-15T01:45:00', name: 'startDateTime' }, - }); - }); - wrapper.update(); - expect(wrapper.find('input#run-on-number').prop('checked')).toBe(false); - expect(wrapper.find('input#run-on-number + label').text()).toBe( - 'March 15' - ); - expect(wrapper.find('input#run-on-day').prop('checked')).toBe(false); - expect(wrapper.find('input#run-on-day + label').text()).toBe( - 'The third Sunday in March' - ); - expect(wrapper.find('input#run-on-last-day').length).toBe(0); - }); test('error shown when end date/time comes before start date/time', async () => { expect(wrapper.find('input#end-never').prop('checked')).toBe(true); expect(wrapper.find('input#end-after').prop('checked')).toBe(false); diff --git a/awx/ui_next/src/util/dates.jsx b/awx/ui_next/src/util/dates.jsx index b605f79abf..68d9461334 100644 --- a/awx/ui_next/src/util/dates.jsx +++ b/awx/ui_next/src/util/dates.jsx @@ -1,5 +1,6 @@ /* eslint-disable import/prefer-default-export */ import { t } from '@lingui/macro'; +import { RRule } from 'rrule'; import { getLanguage } from './language'; const prependZeros = value => value.toString().padStart(2, 0); @@ -28,87 +29,37 @@ export function dateToInputDateTime(dateObj) { return `${year}-${month}-${day}T${hour}:${minute}:${second}`; } -export function getDaysInMonth(dateString) { - const dateObj = new Date(dateString); - return new Date(dateObj.getFullYear(), dateObj.getMonth() + 1, 0).getDate(); -} - -export function getWeekNumber(dateString) { - const dateObj = new Date(dateString); - const dayOfMonth = dateObj.getDate(); - const dayOfWeek = dateObj.getDay(); - if (dayOfMonth < 8) { - return 1; - } - dateObj.setDate(dayOfMonth - dayOfWeek + 1); - return Math.ceil(dayOfMonth / 7); -} - -export function getDayString(dayIndex, i18n) { - switch (dayIndex) { - case 0: - return i18n._(t`Sunday`); - case 1: - return i18n._(t`Monday`); - case 2: - return i18n._(t`Tuesday`); - case 3: - return i18n._(t`Wednesday`); - case 4: - return i18n._(t`Thursday`); - case 5: - return i18n._(t`Friday`); - case 6: - return i18n._(t`Saturday`); +export function getRRuleDayConstants(dayString, i18n) { + switch (dayString) { + case 'sunday': + return RRule.SU; + case 'monday': + return RRule.MO; + case 'tuesday': + return RRule.TU; + case 'wednesday': + return RRule.WE; + case 'thursday': + return RRule.TH; + case 'friday': + return RRule.FR; + case 'saturday': + return RRule.SA; + case 'day': + return [ + RRule.MO, + RRule.TU, + RRule.WE, + RRule.TH, + RRule.FR, + RRule.SA, + RRule.SU, + ]; + case 'weekday': + return [RRule.MO, RRule.TU, RRule.WE, RRule.TH, RRule.FR]; + case 'weekendDay': + return [RRule.SA, RRule.SU]; default: - throw new Error(i18n._(t`Unrecognized day index`)); - } -} - -export function getWeekString(weekNumber, i18n) { - switch (weekNumber) { - case 1: - return i18n._(t`first`); - case 2: - return i18n._(t`second`); - case 3: - return i18n._(t`third`); - case 4: - return i18n._(t`fourth`); - case 5: - return i18n._(t`fifth`); - default: - throw new Error(i18n._(t`Unrecognized week number`)); - } -} - -export function getMonthString(monthIndex, i18n) { - switch (monthIndex) { - case 0: - return i18n._(t`January`); - case 1: - return i18n._(t`February`); - case 2: - return i18n._(t`March`); - case 3: - return i18n._(t`April`); - case 4: - return i18n._(t`May`); - case 5: - return i18n._(t`June`); - case 6: - return i18n._(t`July`); - case 7: - return i18n._(t`August`); - case 8: - return i18n._(t`September`); - case 9: - return i18n._(t`October`); - case 10: - return i18n._(t`November`); - case 11: - return i18n._(t`December`); - default: - throw new Error(i18n._(t`Unrecognized month index`)); + throw new Error(i18n._(t`Unrecognized day string`)); } } diff --git a/awx/ui_next/src/util/dates.test.jsx b/awx/ui_next/src/util/dates.test.jsx index 4a13701a28..83e6cb066a 100644 --- a/awx/ui_next/src/util/dates.test.jsx +++ b/awx/ui_next/src/util/dates.test.jsx @@ -1,12 +1,9 @@ +import { RRule } from 'rrule'; import { dateToInputDateTime, - getDaysInMonth, - getDayString, - getMonthString, - getWeekNumber, - getWeekString, formatDateString, formatDateStringUTC, + getRRuleDayConstants, secondsToHHMMSS, } from './dates'; @@ -59,63 +56,35 @@ describe('dateToInputDateTime', () => { }); }); -describe('getDaysInMonth', () => { +describe('getRRuleDayConstants', () => { test('it returns the expected value', () => { - expect(getDaysInMonth('2020-02-15T00:00:00Z')).toEqual(29); - expect(getDaysInMonth('2020-03-15T00:00:00Z')).toEqual(31); - expect(getDaysInMonth('2020-04-15T00:00:00Z')).toEqual(30); - }); -}); - -describe('getWeekNumber', () => { - test('it returns the expected value', () => { - expect(getWeekNumber('2020-02-01T00:00:00Z')).toEqual(1); - expect(getWeekNumber('2020-02-08T00:00:00Z')).toEqual(2); - expect(getWeekNumber('2020-02-15T00:00:00Z')).toEqual(3); - expect(getWeekNumber('2020-02-22T00:00:00Z')).toEqual(4); - expect(getWeekNumber('2020-02-29T00:00:00Z')).toEqual(5); - }); -}); - -describe('getDayString', () => { - test('it returns the expected value', () => { - expect(getDayString(0, i18n)).toEqual('Sunday'); - expect(getDayString(1, i18n)).toEqual('Monday'); - expect(getDayString(2, i18n)).toEqual('Tuesday'); - expect(getDayString(3, i18n)).toEqual('Wednesday'); - expect(getDayString(4, i18n)).toEqual('Thursday'); - expect(getDayString(5, i18n)).toEqual('Friday'); - expect(getDayString(6, i18n)).toEqual('Saturday'); - expect(() => getDayString(7, i18n)).toThrow(); - }); -}); - -describe('getWeekString', () => { - test('it returns the expected value', () => { - expect(() => getWeekString(0, i18n)).toThrow(); - expect(getWeekString(1, i18n)).toEqual('first'); - expect(getWeekString(2, i18n)).toEqual('second'); - expect(getWeekString(3, i18n)).toEqual('third'); - expect(getWeekString(4, i18n)).toEqual('fourth'); - expect(getWeekString(5, i18n)).toEqual('fifth'); - expect(() => getWeekString(6, i18n)).toThrow(); - }); -}); - -describe('getMonthString', () => { - test('it returns the expected value', () => { - expect(getMonthString(0, i18n)).toEqual('January'); - expect(getMonthString(1, i18n)).toEqual('February'); - expect(getMonthString(2, i18n)).toEqual('March'); - expect(getMonthString(3, i18n)).toEqual('April'); - expect(getMonthString(4, i18n)).toEqual('May'); - expect(getMonthString(5, i18n)).toEqual('June'); - expect(getMonthString(6, i18n)).toEqual('July'); - expect(getMonthString(7, i18n)).toEqual('August'); - expect(getMonthString(8, i18n)).toEqual('September'); - expect(getMonthString(9, i18n)).toEqual('October'); - expect(getMonthString(10, i18n)).toEqual('November'); - expect(getMonthString(11, i18n)).toEqual('December'); - expect(() => getMonthString(12, i18n)).toThrow(); + expect(getRRuleDayConstants('monday', i18n)).toEqual(RRule.MO); + expect(getRRuleDayConstants('tuesday', i18n)).toEqual(RRule.TU); + expect(getRRuleDayConstants('wednesday', i18n)).toEqual(RRule.WE); + expect(getRRuleDayConstants('thursday', i18n)).toEqual(RRule.TH); + expect(getRRuleDayConstants('friday', i18n)).toEqual(RRule.FR); + expect(getRRuleDayConstants('saturday', i18n)).toEqual(RRule.SA); + expect(getRRuleDayConstants('sunday', i18n)).toEqual(RRule.SU); + expect(getRRuleDayConstants('day', i18n)).toEqual([ + RRule.MO, + RRule.TU, + RRule.WE, + RRule.TH, + RRule.FR, + RRule.SA, + RRule.SU, + ]); + expect(getRRuleDayConstants('weekday', i18n)).toEqual([ + RRule.MO, + RRule.TU, + RRule.WE, + RRule.TH, + RRule.FR, + ]); + expect(getRRuleDayConstants('weekendDay', i18n)).toEqual([ + RRule.SA, + RRule.SU, + ]); + expect(() => getRRuleDayConstants('foobar', i18n)).toThrow(); }); });