From 26a947ed315b59cdcc94ab90b6aabb43e0a1f350 Mon Sep 17 00:00:00 2001 From: Alex Corey Date: Tue, 20 Dec 2022 11:46:52 -0500 Subject: [PATCH] Adds functionality to add multiple rrules to a schedule and save the form --- .../Schedule/shared/DateTimePicker.js | 1 - .../Schedule/shared/FrequenciesList.js | 93 +++ .../Schedule/shared/FrequencyDetailSubform.js | 568 ------------------ .../Schedule/shared/FrequencySelect.js | 49 +- .../Schedule/shared/MonthandYearForm.js | 77 +++ .../Schedule/shared/OrdinalDayForm.js | 45 ++ .../Schedule/shared/ScheduleEndForm.js | 67 +++ .../Schedule/shared/ScheduleForm.js | 335 ++++------- .../Schedule/shared/ScheduleFormFields.js | 121 +--- .../Schedule/shared/ScheduleFormWizard.js | 199 ++++++ .../components/Schedule/shared/WeekdayForm.js | 164 +++++ .../Schedule/shared/buildRuleObj.js | 69 +-- .../Schedule/shared/buildRuleSet.js | 34 +- .../Schedule/shared/parseRuleObj.js | 166 +---- .../Schedule/shared/scheduleFormHelpers.js | 232 +++++++ 15 files changed, 1038 insertions(+), 1182 deletions(-) create mode 100644 awx/ui/src/components/Schedule/shared/FrequenciesList.js delete mode 100644 awx/ui/src/components/Schedule/shared/FrequencyDetailSubform.js create mode 100644 awx/ui/src/components/Schedule/shared/MonthandYearForm.js create mode 100644 awx/ui/src/components/Schedule/shared/OrdinalDayForm.js create mode 100644 awx/ui/src/components/Schedule/shared/ScheduleEndForm.js create mode 100644 awx/ui/src/components/Schedule/shared/ScheduleFormWizard.js create mode 100644 awx/ui/src/components/Schedule/shared/WeekdayForm.js create mode 100644 awx/ui/src/components/Schedule/shared/scheduleFormHelpers.js diff --git a/awx/ui/src/components/Schedule/shared/DateTimePicker.js b/awx/ui/src/components/Schedule/shared/DateTimePicker.js index 44d1f10c13..2e7f035c5f 100644 --- a/awx/ui/src/components/Schedule/shared/DateTimePicker.js +++ b/awx/ui/src/components/Schedule/shared/DateTimePicker.js @@ -55,7 +55,6 @@ function DateTimePicker({ dateFieldName, timeFieldName, label }) { onChange={onDateChange} /> ( + + {freq.frequency} + {freq.rrule} + {t`End`} + + + + + ); + return ( + <> + + + + + + + { + setIsShowingRules(isChecked); + }} + /> + + + +
+ {frequencies.value[0].frequency === '' && + frequencies.value.length < 2 ? ( + + ) : ( + + + + {t`Frequency`} + {t`RRule`} + {t`Ending`} + {t`Actions`} + + + {frequencies.value.map((freq, i) => list(freq, i))} + + )} +
+ + ); +} + +export default FrequenciesList; diff --git a/awx/ui/src/components/Schedule/shared/FrequencyDetailSubform.js b/awx/ui/src/components/Schedule/shared/FrequencyDetailSubform.js deleted file mode 100644 index 615d814b8d..0000000000 --- a/awx/ui/src/components/Schedule/shared/FrequencyDetailSubform.js +++ /dev/null @@ -1,568 +0,0 @@ -import 'styled-components/macro'; -import React from 'react'; -import styled from 'styled-components'; -import { useField } from 'formik'; - -import { t, Trans, Plural } from '@lingui/macro'; -import { RRule } from 'rrule'; -import { - Checkbox as _Checkbox, - FormGroup, - Radio, - TextInput, -} from '@patternfly/react-core'; -import { required, requiredPositiveInteger } from 'util/validators'; -import AnsibleSelect from '../../AnsibleSelect'; -import FormField from '../../FormField'; -import DateTimePicker from './DateTimePicker'; - -const RunOnRadio = styled(Radio)` - display: flex; - align-items: center; - - 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; -`; - -const Checkbox = styled(_Checkbox)` - :not(:last-of-type) { - margin-right: 10px; - } -`; - -const FrequencyDetailSubform = ({ frequency, prefix, isException }) => { - const id = prefix.replace('.', '-'); - const [runOnDayMonth] = useField({ - name: `${prefix}.runOnDayMonth`, - }); - const [runOnDayNumber] = useField({ - name: `${prefix}.runOnDayNumber`, - }); - const [runOnTheOccurrence] = useField({ - name: `${prefix}.runOnTheOccurrence`, - }); - const [runOnTheDay] = useField({ - name: `${prefix}.runOnTheDay`, - }); - const [runOnTheMonth] = useField({ - name: `${prefix}.runOnTheMonth`, - }); - const [startDate] = useField(`${prefix}.startDate`); - - const [daysOfWeek, daysOfWeekMeta, daysOfWeekHelpers] = useField({ - name: `${prefix}.daysOfWeek`, - validate: (val) => { - if (frequency === 'week') { - return required(t`Select a value for this field`)(val?.length > 0); - } - return undefined; - }, - }); - const [end, endMeta] = useField({ - name: `${prefix}.end`, - validate: required(t`Select a value for this field`), - }); - const [interval, intervalMeta] = useField({ - name: `${prefix}.interval`, - validate: requiredPositiveInteger(), - }); - const [runOn, runOnMeta] = useField({ - name: `${prefix}.runOn`, - validate: (val) => { - if (frequency === 'month' || frequency === 'year') { - return required(t`Select a value for this field`)(val); - } - return undefined; - }, - }); - - const monthOptions = [ - { - key: 'january', - value: 1, - label: t`January`, - }, - { - key: 'february', - value: 2, - label: t`February`, - }, - { - key: 'march', - value: 3, - label: t`March`, - }, - { - key: 'april', - value: 4, - label: t`April`, - }, - { - key: 'may', - value: 5, - label: t`May`, - }, - { - key: 'june', - value: 6, - label: t`June`, - }, - { - key: 'july', - value: 7, - label: t`July`, - }, - { - key: 'august', - value: 8, - label: t`August`, - }, - { - key: 'september', - value: 9, - label: t`September`, - }, - { - key: 'october', - value: 10, - label: t`October`, - }, - { - key: 'november', - value: 11, - label: t`November`, - }, - { - key: 'december', - value: 12, - label: t`December`, - }, - ]; - - const updateDaysOfWeek = (day, checked) => { - const newDaysOfWeek = daysOfWeek.value ? [...daysOfWeek.value] : []; - daysOfWeekHelpers.setTouched(true); - if (checked) { - newDaysOfWeek.push(day); - daysOfWeekHelpers.setValue(newDaysOfWeek); - } else { - daysOfWeekHelpers.setValue( - newDaysOfWeek.filter((selectedDay) => selectedDay !== day) - ); - } - }; - - const getPeriodLabel = () => { - switch (frequency) { - case 'minute': - return t`Minute`; - case 'hour': - return t`Hour`; - case 'day': - return t`Day`; - case 'week': - return t`Week`; - case 'month': - return t`Month`; - case 'year': - return t`Year`; - default: - throw new Error(t`Frequency did not match an expected value`); - } - }; - - const getRunEveryLabel = () => { - const intervalValue = interval.value; - - switch (frequency) { - case 'minute': - return ; - case 'hour': - return ; - case 'day': - return ; - case 'week': - return ; - case 'month': - return ; - case 'year': - return ; - default: - throw new Error(t`Frequency did not match an expected value`); - } - }; - - return ( - <> -

- {getPeriodLabel()} -

- -
- { - interval.onChange(event); - }} - /> - {getRunEveryLabel()} -
-
- {frequency === 'week' && ( - -
- { - updateDaysOfWeek(RRule.SU, checked); - }} - aria-label={t`Sunday`} - id={`schedule-days-of-week-sun-${id}`} - ouiaId={`schedule-days-of-week-sun-${id}`} - name={`${prefix}.daysOfWeek`} - /> - { - updateDaysOfWeek(RRule.MO, checked); - }} - aria-label={t`Monday`} - id={`schedule-days-of-week-mon-${id}`} - ouiaId={`schedule-days-of-week-mon-${id}`} - name={`${prefix}.daysOfWeek`} - /> - { - updateDaysOfWeek(RRule.TU, checked); - }} - aria-label={t`Tuesday`} - id={`schedule-days-of-week-tue-${id}`} - ouiaId={`schedule-days-of-week-tue-${id}`} - name={`${prefix}.daysOfWeek`} - /> - { - updateDaysOfWeek(RRule.WE, checked); - }} - aria-label={t`Wednesday`} - id={`schedule-days-of-week-wed-${id}`} - ouiaId={`schedule-days-of-week-wed-${id}`} - name={`${prefix}.daysOfWeek`} - /> - { - updateDaysOfWeek(RRule.TH, checked); - }} - aria-label={t`Thursday`} - id={`schedule-days-of-week-thu-${id}`} - ouiaId={`schedule-days-of-week-thu-${id}`} - name={`${prefix}.daysOfWeek`} - /> - { - updateDaysOfWeek(RRule.FR, checked); - }} - aria-label={t`Friday`} - id={`schedule-days-of-week-fri-${id}`} - ouiaId={`schedule-days-of-week-fri-${id}`} - name={`${prefix}.daysOfWeek`} - /> - { - updateDaysOfWeek(RRule.SA, checked); - }} - aria-label={t`Saturday`} - id={`schedule-days-of-week-sat-${id}`} - ouiaId={`schedule-days-of-week-sat-${id}`} - name={`${prefix}.daysOfWeek`} - /> -
-
- )} - {(frequency === 'month' || frequency === 'year') && - !Number.isNaN(new Date(startDate.value)) && ( - - - {frequency === 'month' && ( - - Day - - )} - {frequency === 'year' && ( - - )} - { - runOnDayNumber.onChange(event); - }} - /> - - } - value="day" - isChecked={runOn.value === 'day'} - onChange={(value, event) => { - event.target.value = 'day'; - runOn.onChange(event); - }} - /> - - - The - - - - {frequency === 'year' && ( - <> - - of - - - - )} - - } - value="the" - isChecked={runOn.value === 'the'} - onChange={(value, event) => { - event.target.value = 'the'; - runOn.onChange(event); - }} - /> - - )} - - { - event.target.value = 'never'; - end.onChange(event); - }} - ouiaId={`end-never-radio-button-${id}`} - /> - { - event.target.value = 'after'; - end.onChange(event); - }} - ouiaId={`end-after-radio-button-${id}`} - /> - { - event.target.value = 'onDate'; - end.onChange(event); - }} - ouiaId={`end-on-radio-button-${id}`} - /> - - {end?.value === 'after' && ( - - )} - {end?.value === 'onDate' && ( - - )} - - ); -}; - -export default FrequencyDetailSubform; diff --git a/awx/ui/src/components/Schedule/shared/FrequencySelect.js b/awx/ui/src/components/Schedule/shared/FrequencySelect.js index b76d6357aa..b21011dcfe 100644 --- a/awx/ui/src/components/Schedule/shared/FrequencySelect.js +++ b/awx/ui/src/components/Schedule/shared/FrequencySelect.js @@ -1,30 +1,12 @@ import React, { useState } from 'react'; -import { arrayOf, string } from 'prop-types'; +import { t } from '@lingui/macro'; +import { useField } from 'formik'; +import { RRule } from 'rrule'; import { Select, SelectOption, SelectVariant } from '@patternfly/react-core'; -export default function FrequencySelect({ - id, - value, - onChange, - onBlur, - placeholderText, - children, -}) { +export default function FrequencySelect({ id, onBlur, placeholderText }) { const [isOpen, setIsOpen] = useState(false); - - const onSelect = (event, selectedValue) => { - if (selectedValue === 'none') { - onChange([]); - setIsOpen(false); - return; - } - const index = value.indexOf(selectedValue); - if (index === -1) { - onChange(value.concat(selectedValue)); - } else { - onChange(value.slice(0, index).concat(value.slice(index + 1))); - } - }; + const [frequency, , frequencyHelpers] = useField('freq'); const onToggle = (val) => { if (!val) { @@ -35,21 +17,26 @@ export default function FrequencySelect({ return ( ); } -FrequencySelect.propTypes = { - value: arrayOf(string).isRequired, -}; - export { SelectOption, SelectVariant }; diff --git a/awx/ui/src/components/Schedule/shared/MonthandYearForm.js b/awx/ui/src/components/Schedule/shared/MonthandYearForm.js new file mode 100644 index 0000000000..008c54e71a --- /dev/null +++ b/awx/ui/src/components/Schedule/shared/MonthandYearForm.js @@ -0,0 +1,77 @@ +import React from 'react'; +import { t } from '@lingui/macro'; +import AnsibleSelect from 'components/AnsibleSelect'; +import styled from 'styled-components'; +import { + FormGroup, + Checkbox as _Checkbox, + Grid, + GridItem, +} from '@patternfly/react-core'; +import { useField } from 'formik'; +import { bysetposOptions, monthOptions } from './scheduleFormHelpers'; + +const GroupWrapper = styled(FormGroup)` + && .pf-c-form__group-control { + display: flex; + padding-top: 10px; + } + && .pf-c-form__group-label { + padding-top: 20px; + } +`; +const Checkbox = styled(_Checkbox)` + :not(:last-of-type) { + margin-right: 10px; + } +`; +function MonthandYearForm({ id }) { + const [bySetPos, , bySetPosHelpers] = useField('bysetpos'); + const [byMonth, , byMonthHelpers] = useField('bymonth'); + + return ( + <> + {t`Run on a specific month`}} + > + + {monthOptions.map((month) => ( + + { + if (checked) { + byMonthHelpers.setValue([...byMonth.value, month.value]); + } else { + const removed = byMonth.value.filter( + (i) => i !== month.value + ); + byMonthHelpers.setValue(removed); + } + }} + id={`bymonth-${month.label}`} + ouiaId={`bymonth-${month.label}`} + name="bymonth" + /> + + ))} + + + {t`Run on a specific week day at monthly intervals`}} + > + { + bySetPosHelpers.setValue(v); + }} + /> + + + ); +} +export default MonthandYearForm; diff --git a/awx/ui/src/components/Schedule/shared/OrdinalDayForm.js b/awx/ui/src/components/Schedule/shared/OrdinalDayForm.js new file mode 100644 index 0000000000..1d05f8d38a --- /dev/null +++ b/awx/ui/src/components/Schedule/shared/OrdinalDayForm.js @@ -0,0 +1,45 @@ +import React from 'react'; +import { t } from '@lingui/macro'; +import styled from 'styled-components'; +import { useField } from 'formik'; +import { FormGroup, TextInput } from '@patternfly/react-core'; + +const GroupWrapper = styled(FormGroup)` + && .pf-c-form__group-control { + display: flex; + padding-top: 10px; + } + && .pf-c-form__group-label { + padding-top: 20px; + } +`; + +function OrdinalDayForm() { + const [byMonthDay] = useField('bymonthday'); + const [byYearDay] = useField('byyearday'); + return ( + {t`On a specific number day`}} + name="ordinalDay" + > + { + byMonthDay.onChange(event); + }} + /> + { + byYearDay.onChange(event); + }} + /> + + ); +} + +export default OrdinalDayForm; diff --git a/awx/ui/src/components/Schedule/shared/ScheduleEndForm.js b/awx/ui/src/components/Schedule/shared/ScheduleEndForm.js new file mode 100644 index 0000000000..3ddc48ee10 --- /dev/null +++ b/awx/ui/src/components/Schedule/shared/ScheduleEndForm.js @@ -0,0 +1,67 @@ +import React from 'react'; +import { useField } from 'formik'; +import { t } from '@lingui/macro'; +import { FormGroup, Radio } from '@patternfly/react-core'; +import FormField from 'components/FormField'; +import DateTimePicker from './DateTimePicker'; + +function ScheduleEndForm() { + const [endType, , { setValue }] = useField('endingType'); + const [count] = useField('count'); + return ( + <> + + { + setValue('never'); + }} + /> + { + setValue('after'); + }} + /> + { + setValue('onDate'); + }} + /> + + {endType.value === 'after' && ( + + )} + {endType.value === 'onDate' && ( + + )} + + ); +} + +export default ScheduleEndForm; diff --git a/awx/ui/src/components/Schedule/shared/ScheduleForm.js b/awx/ui/src/components/Schedule/shared/ScheduleForm.js index 43407d2737..feb2834ebd 100644 --- a/awx/ui/src/components/Schedule/shared/ScheduleForm.js +++ b/awx/ui/src/components/Schedule/shared/ScheduleForm.js @@ -18,14 +18,9 @@ import SchedulePromptableFields from './SchedulePromptableFields'; import ScheduleFormFields from './ScheduleFormFields'; import UnsupportedScheduleForm from './UnsupportedScheduleForm'; import parseRuleObj, { UnsupportedRRuleError } from './parseRuleObj'; -import buildRuleObj from './buildRuleObj'; -import buildRuleSet from './buildRuleSet'; - -const NUM_DAYS_PER_FREQUENCY = { - week: 7, - month: 31, - year: 365, -}; +import ScheduleFormWizard from './ScheduleFormWizard'; +import FrequenciesList from './FrequenciesList'; +// import { validateSchedule } from './scheduleFormHelpers'; function ScheduleForm({ hasDaysToKeepField, @@ -40,15 +35,16 @@ function ScheduleForm({ }) { const [isWizardOpen, setIsWizardOpen] = useState(false); const [isSaveDisabled, setIsSaveDisabled] = useState(false); + const [isScheduleWizardOpen, setIsScheduleWizardOpen] = useState(false); const originalLabels = useRef([]); const originalInstanceGroups = useRef([]); let rruleError; const now = DateTime.now(); + const closestQuarterHour = DateTime.fromMillis( Math.ceil(now.ts / 900000) * 900000 ); - const tomorrow = closestQuarterHour.plus({ days: 1 }); const isTemplate = resource.type === 'workflow_job_template' || resource.type === 'job_template'; @@ -283,69 +279,10 @@ function ScheduleForm({ } const [currentDate, time] = dateToInputDateTime(closestQuarterHour.toISO()); - const [tomorrowDate] = dateToInputDateTime(tomorrow.toISO()); - const initialFrequencyOptions = { - minute: { - interval: 1, - end: 'never', - occurrences: 1, - endDate: tomorrowDate, - endTime: time, - }, - hour: { - interval: 1, - end: 'never', - occurrences: 1, - endDate: tomorrowDate, - endTime: time, - }, - day: { - interval: 1, - end: 'never', - occurrences: 1, - endDate: tomorrowDate, - endTime: time, - }, - week: { - interval: 1, - end: 'never', - occurrences: 1, - endDate: tomorrowDate, - endTime: time, - daysOfWeek: [], - }, - month: { - interval: 1, - end: 'never', - occurrences: 1, - endDate: tomorrowDate, - endTime: time, - runOn: 'day', - runOnTheOccurrence: 1, - runOnTheDay: 'sunday', - runOnDayNumber: 1, - }, - year: { - interval: 1, - end: 'never', - occurrences: 1, - endDate: tomorrowDate, - endTime: time, - runOn: 'day', - runOnTheOccurrence: 1, - runOnTheDay: 'sunday', - runOnTheMonth: 1, - runOnDayMonth: 1, - runOnDayNumber: 1, - }, - }; - const initialValues = { description: schedule.description || '', - frequency: [], + frequencies: [], exceptionFrequency: [], - frequencyOptions: initialFrequencyOptions, - exceptionOptions: initialFrequencyOptions, name: schedule.name || '', startDate: currentDate, startTime: time, @@ -367,11 +304,9 @@ function ScheduleForm({ } initialValues.daysToKeep = initialDaysToKeep; } - - let overriddenValues = {}; if (schedule.rrule) { try { - overriddenValues = parseRuleObj(schedule); + parseRuleObj(schedule); } catch (error) { if (error instanceof UnsupportedRRuleError) { return ( @@ -394,89 +329,33 @@ function ScheduleForm({ if (contentLoading) { return ; } - - const validate = (values) => { - const errors = {}; - - values.frequency.forEach((freq) => { - const options = values.frequencyOptions[freq]; - const freqErrors = {}; - - if ( - (freq === 'month' || freq === 'year') && - options.runOn === 'day' && - (options.runOnDayNumber < 1 || options.runOnDayNumber > 31) - ) { - freqErrors.runOn = t`Please select a day number between 1 and 31.`; - } - - if (options.end === 'after' && !options.occurrences) { - freqErrors.occurrences = t`Please enter a number of occurrences.`; - } - - if (options.end === 'onDate') { - if ( - DateTime.fromFormat( - `${values.startDate} ${values.startTime}`, - 'yyyy-LL-dd h:mm a' - ).toMillis() >= - DateTime.fromFormat( - `${options.endDate} ${options.endTime}`, - 'yyyy-LL-dd h:mm a' - ).toMillis() - ) { - freqErrors.endDate = t`Please select an end date/time that comes after the start date/time.`; - } - - if ( - DateTime.fromISO(options.endDate) - .diff(DateTime.fromISO(values.startDate), 'days') - .toObject().days < NUM_DAYS_PER_FREQUENCY[freq] - ) { - const rule = new RRule( - buildRuleObj({ - startDate: values.startDate, - startTime: values.startTime, - frequency: freq, - ...options, - }) - ); - if (rule.all().length === 0) { - errors.startDate = t`Selected date range must have at least 1 schedule occurrence.`; - freqErrors.endDate = t`Selected date range must have at least 1 schedule occurrence.`; - } - } - } - if (Object.keys(freqErrors).length > 0) { - if (!errors.frequencyOptions) { - errors.frequencyOptions = {}; - } - errors.frequencyOptions[freq] = freqErrors; - } - }); - - if (values.exceptionFrequency.length > 0 && !scheduleHasInstances(values)) { - errors.exceptionFrequency = t`This schedule has no occurrences due to the selected exceptions.`; - } - - return errors; - }; - + const frequencies = []; + frequencies.push(parseRuleObj(schedule)); return ( {() => ( { submitSchedule( @@ -488,73 +367,90 @@ function ScheduleForm({ credentials ); }} - validate={validate} + validate={() => {}} > {(formik) => ( -
- - - {isWizardOpen && ( - { - setIsWizardOpen(false); - }} - onSave={() => { - setIsWizardOpen(false); - setIsSaveDisabled(false); - }} - resourceDefaultCredentials={resourceDefaultCredentials} - labels={originalLabels.current} - instanceGroups={originalInstanceGroups.current} + <> + + + - )} - - - - - - {isTemplate && showPromptButton && ( + {isWizardOpen && ( + { + setIsWizardOpen(false); + }} + onSave={() => { + setIsWizardOpen(false); + setIsSaveDisabled(false); + }} + resourceDefaultCredentials={resourceDefaultCredentials} + labels={originalLabels.current} + instanceGroups={originalInstanceGroups.current} + /> + )} + + + + + + + + + + {isTemplate && showPromptButton && ( + + )} + - )} - - - - - + + +
+ + {isScheduleWizardOpen && ( + {}} + setIsOpen={setIsScheduleWizardOpen} + /> + )} + )}
)} @@ -575,24 +471,3 @@ ScheduleForm.defaultProps = { }; export default ScheduleForm; - -function scheduleHasInstances(values) { - let rangeToCheck = 1; - values.frequency.forEach((freq) => { - if (NUM_DAYS_PER_FREQUENCY[freq] > rangeToCheck) { - rangeToCheck = NUM_DAYS_PER_FREQUENCY[freq]; - } - }); - - const ruleSet = buildRuleSet(values, true); - const startDate = DateTime.fromISO(values.startDate); - const endDate = startDate.plus({ days: rangeToCheck }); - const instances = ruleSet.between( - startDate.toJSDate(), - endDate.toJSDate(), - true, - (date, i) => i === 0 - ); - - return instances.length > 0; -} diff --git a/awx/ui/src/components/Schedule/shared/ScheduleFormFields.js b/awx/ui/src/components/Schedule/shared/ScheduleFormFields.js index adf784d302..a1b0e50836 100644 --- a/awx/ui/src/components/Schedule/shared/ScheduleFormFields.js +++ b/awx/ui/src/components/Schedule/shared/ScheduleFormFields.js @@ -1,41 +1,27 @@ import React, { useState } from 'react'; import { useField } from 'formik'; -import { FormGroup, Title } from '@patternfly/react-core'; +import { FormGroup } from '@patternfly/react-core'; import { t } from '@lingui/macro'; -import styled from 'styled-components'; -import 'styled-components/macro'; import FormField from 'components/FormField'; import { required } from 'util/validators'; import { useConfig } from 'contexts/Config'; import Popover from '../../Popover'; import AnsibleSelect from '../../AnsibleSelect'; -import FrequencySelect, { SelectOption } from './FrequencySelect'; import getHelpText from '../../../screens/Template/shared/JobTemplate.helptext'; -import { SubFormLayout, FormColumnLayout } from '../../FormLayout'; -import FrequencyDetailSubform from './FrequencyDetailSubform'; import DateTimePicker from './DateTimePicker'; -import sortFrequencies from './sortFrequencies'; - -const SelectClearOption = styled(SelectOption)` - & > input[type='checkbox'] { - display: none; - } -`; export default function ScheduleFormFields({ hasDaysToKeepField, zoneOptions, zoneLinks, + setTimeZone, }) { const helpText = getHelpText(); const [timezone, timezoneMeta] = useField({ name: 'timezone', validate: required(t`Select a value for this field`), }); - const [frequency, frequencyMeta, frequencyHelper] = useField({ - name: 'frequency', - validate: required(t`Select a value for this field`), - }); + const [timezoneMessage, setTimezoneMessage] = useState(''); const warnLinkedTZ = (event, selectedValue) => { if (zoneLinks[selectedValue]) { @@ -46,6 +32,7 @@ export default function ScheduleFormFields({ setTimezoneMessage(''); } timezone.onChange(event, selectedValue); + setTimeZone(zoneLinks(selectedValue)); }; let timezoneValidatedStatus = 'default'; if (timezoneMeta.touched && timezoneMeta.error) { @@ -55,16 +42,6 @@ export default function ScheduleFormFields({ } const config = useConfig(); - const [exceptionFrequency, exceptionFrequencyMeta, exceptionFrequencyHelper] = - useField({ - name: 'exceptionFrequency', - validate: required(t`Select a value for this field`), - }); - - const updateFrequency = (setFrequency) => (values) => { - setFrequency(values.sort(sortFrequencies)); - }; - return ( <> - - - {t`None (run once)`} - {t`Minute`} - {t`Hour`} - {t`Day`} - {t`Week`} - {t`Month`} - {t`Year`} - - + {hasDaysToKeepField ? ( ) : null} - {frequency.value.length ? ( - - - {t`Frequency Details`} - - {frequency.value.map((val) => ( - - - - ))} - {t`Exceptions`} - - - - {t`None`} - {t`Minute`} - {t`Hour`} - {t`Day`} - {t`Week`} - {t`Month`} - {t`Year`} - - - - {exceptionFrequency.value.map((val) => ( - - - - ))} - - ) : null} ); } diff --git a/awx/ui/src/components/Schedule/shared/ScheduleFormWizard.js b/awx/ui/src/components/Schedule/shared/ScheduleFormWizard.js new file mode 100644 index 0000000000..d8e1077480 --- /dev/null +++ b/awx/ui/src/components/Schedule/shared/ScheduleFormWizard.js @@ -0,0 +1,199 @@ +import React from 'react'; +import { + Button, + FormGroup, + TextInput, + Title, + Wizard, + WizardContextConsumer, + WizardFooter, +} from '@patternfly/react-core'; +import { t } from '@lingui/macro'; +import styled from 'styled-components'; +import { RRule } from 'rrule'; +import { useField, useFormikContext } from 'formik'; +import { DateTime } from 'luxon'; +import { formatDateString } from 'util/dates'; +import FrequencySelect from './FrequencySelect'; +import MonthandYearForm from './MonthandYearForm'; +import OrdinalDayForm from './OrdinalDayForm'; +import WeekdayForm from './WeekdayForm'; +import ScheduleEndForm from './ScheduleEndForm'; +import parseRuleObj from './parseRuleObj'; +import { buildDtStartObj } from './buildRuleObj'; + +const GroupWrapper = styled(FormGroup)` + && .pf-c-form__group-control { + display: flex; + padding-top: 10px; + } + && .pf-c-form__group-label { + padding-top: 20px; + } +`; + +function ScheduleFormWizard({ isOpen, setIsOpen }) { + const { values, resetForm, initialValues } = useFormikContext(); + const [freq, freqMeta] = useField('freq'); + const [{ value: frequenciesValue }] = useField('frequencies'); + const [interval, , intervalHelpers] = useField('interval'); + + const handleSubmit = (goToStepById) => { + const { + name, + description, + endingType, + endTime, + endDate, + timezone, + startDate, + startTime, + frequencies, + ...rest + } = values; + if (endingType === 'onDate') { + const dt = DateTime.fromFormat( + `${endDate} ${endTime}`, + 'yyyy-MM-dd h:mm a', + { + zone: timezone, + } + ); + rest.until = formatDateString(dt, timezone); + + delete rest.count; + } + if (endingType === 'never') delete rest.count; + + const rule = new RRule(rest); + + const start = buildDtStartObj({ + startDate: values.startDate, + startTime: values.startTime, + timezone: values.timezone, + frequency: values.freq, + }); + const newFrequency = parseRuleObj({ + timezone, + frequency: freq.value, + rrule: rule.toString(), + dtstart: start, + }); + if (goToStepById) { + goToStepById(1); + } + + resetForm({ + values: { + ...initialValues, + description: values.description, + name: values.name, + startDate: values.startDate, + startTime: values.startTime, + timezone: values.timezone, + frequencies: frequenciesValue[0].frequency.length + ? [...frequenciesValue, newFrequency] + : [newFrequency], + }, + }); + }; + const CustomFooter = ( + + + {({ activeStep, onNext, onBack, goToStepById }) => ( + <> + {activeStep.id === 2 ? ( + <> + + + + ) : ( + + )} + + + + + )} + + + ); + + return ( + setIsOpen(false)} + isOpen={isOpen} + footer={CustomFooter} + steps={[ + { + key: 'frequency', + name: 'Frequency', + id: 1, + component: ( + <> + {t`Repeat frequency`} + {t`Frequency`}} + > + + + {t`Interval`}}> + intervalHelpers.setValue(v)} + /> + + + + + + ), + }, + { + name: 'End', + key: 'end', + id: 2, + component: , + }, + ]} + /> + ); +} +export default ScheduleFormWizard; diff --git a/awx/ui/src/components/Schedule/shared/WeekdayForm.js b/awx/ui/src/components/Schedule/shared/WeekdayForm.js new file mode 100644 index 0000000000..5ef614cfe2 --- /dev/null +++ b/awx/ui/src/components/Schedule/shared/WeekdayForm.js @@ -0,0 +1,164 @@ +import React, { useState } from 'react'; +import { t } from '@lingui/macro'; +import { + Checkbox as _Checkbox, + FormGroup, + Select, + SelectOption, +} from '@patternfly/react-core'; +import { useField } from 'formik'; +import { RRule } from 'rrule'; +import styled from 'styled-components'; +import { weekdayOptions } from './scheduleFormHelpers'; + +const Checkbox = styled(_Checkbox)` + :not(:last-of-type) { + margin-right: 10px; + } +`; +const GroupWrapper = styled(FormGroup)` + && .pf-c-form__group-control { + display: flex; + padding-top: 10px; + } + && .pf-c-form__group-label { + padding-top: 20px; + } +`; + +function WeekdayForm({ id }) { + const [isOpen, setIsOpen] = useState(false); + const [daysOfWeek, daysOfWeekMeta, daysOfWeekHelpers] = useField('byweekday'); + const [weekStartDay, , weekStartDayHelpers] = useField('wkst'); + const updateDaysOfWeek = (day, checked) => { + const newDaysOfWeek = daysOfWeek.value ? [...daysOfWeek.value] : []; + daysOfWeekHelpers.setTouched(true); + + if (checked) { + newDaysOfWeek.push(day); + daysOfWeekHelpers.setValue(newDaysOfWeek); + } else { + daysOfWeekHelpers.setValue( + newDaysOfWeek.filter((selectedDay) => selectedDay !== day) + ); + } + }; + return ( + <> + {t`Select the first day of the week`}} + > + + + {t`On selected day(s) of the week`}} + > + { + updateDaysOfWeek(RRule.SU, checked); + }} + aria-label={t`Sunday`} + id={`schedule-days-of-week-sun-${id}`} + ouiaId={`schedule-days-of-week-sun-${id}`} + name="daysOfWeek" + /> + { + updateDaysOfWeek(RRule.MO, checked); + }} + aria-label={t`Monday`} + id={`schedule-days-of-week-mon-${id}`} + ouiaId={`schedule-days-of-week-mon-${id}`} + name="daysOfWeek" + /> + { + updateDaysOfWeek(RRule.TU, checked); + }} + aria-label={t`Tuesday`} + id={`schedule-days-of-week-tue-${id}`} + ouiaId={`schedule-days-of-week-tue-${id}`} + name="daysOfWeek" + /> + { + updateDaysOfWeek(RRule.WE, checked); + }} + aria-label={t`Wednesday`} + id={`schedule-days-of-week-wed-${id}`} + ouiaId={`schedule-days-of-week-wed-${id}`} + name="daysOfWeek" + /> + { + updateDaysOfWeek(RRule.TH, checked); + }} + aria-label={t`Thursday`} + id={`schedule-days-of-week-thu-${id}`} + ouiaId={`schedule-days-of-week-thu-${id}`} + name="daysOfWeek" + /> + { + updateDaysOfWeek(RRule.FR, checked); + }} + aria-label={t`Friday`} + id={`schedule-days-of-week-fri-${id}`} + ouiaId={`schedule-days-of-week-fri-${id}`} + name="daysOfWeek" + /> + { + updateDaysOfWeek(RRule.SA, checked); + }} + aria-label={t`Saturday`} + id={`schedule-days-of-week-sat-${id}`} + ouiaId={`schedule-days-of-week-sat-${id}`} + name="daysOfWeek" + /> + + + ); +} +export default WeekdayForm; diff --git a/awx/ui/src/components/Schedule/shared/buildRuleObj.js b/awx/ui/src/components/Schedule/shared/buildRuleObj.js index c631ed959d..96fedbdeb1 100644 --- a/awx/ui/src/components/Schedule/shared/buildRuleObj.js +++ b/awx/ui/src/components/Schedule/shared/buildRuleObj.js @@ -1,7 +1,5 @@ -import { t } from '@lingui/macro'; import { RRule } from 'rrule'; import { DateTime } from 'luxon'; -import { getRRuleDayConstants } from 'util/dates'; window.RRule = RRule; window.DateTime = DateTime; @@ -22,7 +20,7 @@ export function buildDtStartObj(values) { startHour )}${pad(startMinute)}00`; const rruleString = values.timezone - ? `DTSTART;TZID=${values.timezone}:${dateString}` + ? `DTSTART;TZID=${values.timezone}${dateString}` : `DTSTART:${dateString}Z`; const rule = RRule.fromString(rruleString); @@ -38,7 +36,8 @@ function pad(num) { export default function buildRuleObj(values, includeStart) { const ruleObj = { - interval: values.interval, + interval: values.interval || 1, + freq: values.freq, }; if (includeStart) { @@ -49,68 +48,6 @@ export default function buildRuleObj(values, includeStart) { ); } - switch (values.frequency) { - case 'none': - ruleObj.count = 1; - ruleObj.freq = RRule.MINUTELY; - break; - case 'minute': - ruleObj.freq = RRule.MINUTELY; - break; - case 'hour': - ruleObj.freq = RRule.HOURLY; - break; - case 'day': - ruleObj.freq = RRule.DAILY; - break; - case 'week': - ruleObj.freq = RRule.WEEKLY; - ruleObj.byweekday = values.daysOfWeek; - break; - case 'month': - ruleObj.freq = RRule.MONTHLY; - if (values.runOn === 'day') { - ruleObj.bymonthday = values.runOnDayNumber; - } else if (values.runOn === 'the') { - ruleObj.bysetpos = parseInt(values.runOnTheOccurrence, 10); - ruleObj.byweekday = getRRuleDayConstants(values.runOnTheDay); - } - break; - case 'year': - ruleObj.freq = RRule.YEARLY; - 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); - ruleObj.bymonth = parseInt(values.runOnTheMonth, 10); - } - break; - default: - throw new Error(t`Frequency did not match an expected value`); - } - - if (values.frequency !== 'none') { - switch (values.end) { - case 'never': - break; - case 'after': - ruleObj.count = values.occurrences; - break; - case 'onDate': { - ruleObj.until = buildDateTime( - values.endDate, - values.endTime, - values.timezone - ); - break; - } - default: - throw new Error(t`End did not match an expected value (${values.end})`); - } - } - return ruleObj; } diff --git a/awx/ui/src/components/Schedule/shared/buildRuleSet.js b/awx/ui/src/components/Schedule/shared/buildRuleSet.js index 070a0ef663..7a39cdd73d 100644 --- a/awx/ui/src/components/Schedule/shared/buildRuleSet.js +++ b/awx/ui/src/components/Schedule/shared/buildRuleSet.js @@ -1,5 +1,6 @@ import { RRule, RRuleSet } from 'rrule'; import buildRuleObj, { buildDtStartObj } from './buildRuleObj'; +import { FREQUENCIESCONSTANTS } from './scheduleFormHelpers'; window.RRuleSet = RRuleSet; @@ -12,42 +13,31 @@ export default function buildRuleSet(values, useUTCStart) { startDate: values.startDate, startTime: values.startTime, timezone: values.timezone, + frequency: values.freq, }); set.rrule(startRule); } - if (values.frequency.length === 0) { - const rule = buildRuleObj( - { - startDate: values.startDate, - startTime: values.startTime, - timezone: values.timezone, - frequency: 'none', - interval: 1, - }, - useUTCStart - ); - set.rrule(new RRule(rule)); - } - - frequencies.forEach((frequency) => { - if (!values.frequency.includes(frequency)) { + values.frequencies.forEach(({ frequency, rrule }) => { + if (!frequencies.includes(frequency)) { return; } + const rule = buildRuleObj( { startDate: values.startDate, startTime: values.startTime, timezone: values.timezone, - frequency, - ...values.frequencyOptions[frequency], + freq: FREQUENCIESCONSTANTS[frequency], + rrule, }, - useUTCStart + true ); + set.rrule(new RRule(rule)); }); - frequencies.forEach((frequency) => { + values.exceptions?.forEach(({ frequency, rrule }) => { if (!values.exceptionFrequency?.includes(frequency)) { return; } @@ -56,8 +46,8 @@ export default function buildRuleSet(values, useUTCStart) { startDate: values.startDate, startTime: values.startTime, timezone: values.timezone, - frequency, - ...values.exceptionOptions[frequency], + freq: FREQUENCIESCONSTANTS[frequency], + rrule, }, useUTCStart ); diff --git a/awx/ui/src/components/Schedule/shared/parseRuleObj.js b/awx/ui/src/components/Schedule/shared/parseRuleObj.js index a676231193..2244af5b5a 100644 --- a/awx/ui/src/components/Schedule/shared/parseRuleObj.js +++ b/awx/ui/src/components/Schedule/shared/parseRuleObj.js @@ -12,12 +12,14 @@ export class UnsupportedRRuleError extends Error { export default function parseRuleObj(schedule) { let values = { - frequency: [], - frequencyOptions: {}, - exceptionFrequency: [], - exceptionOptions: {}, + frequency: '', + rrules: '', timezone: schedule.timezone, }; + if (Object.values(schedule).length === 0) { + return values; + } + const ruleset = rrulestr(schedule.rrule.replace(' ', '\n'), { forceset: true, }); @@ -40,25 +42,9 @@ export default function parseRuleObj(schedule) { } }); - if (isSingleOccurrence(values)) { - values.frequency = []; - values.frequencyOptions = {}; - } - return values; } -function isSingleOccurrence(values) { - if (values.frequency.length > 1) { - return false; - } - if (values.frequency[0] !== 'minute') { - return false; - } - const options = values.frequencyOptions.minute; - return options.end === 'after' && options.occurrences === 1; -} - function parseDtstart(schedule, values) { // TODO: should this rely on DTSTART in rruleset rather than schedule.dtstart? const [startDate, startTime] = dateToInputDateTime( @@ -81,27 +67,12 @@ const frequencyTypes = { [RRule.YEARLY]: 'year', }; -function parseRrule(rruleString, schedule, values) { - const { frequency, options } = parseRule( - rruleString, - schedule, - values.exceptionFrequency - ); +function parseRrule(rruleString, schedule) { + const { frequency } = parseRule(rruleString, schedule); - if (values.frequencyOptions[frequency]) { - throw new UnsupportedRRuleError( - 'Duplicate exception frequency types not supported' - ); - } + const freq = { frequency, rrule: rruleString }; - return { - ...values, - frequency: [...values.frequency, frequency].sort(sortFrequencies), - frequencyOptions: { - ...values.frequencyOptions, - [frequency]: options, - }, - }; + return freq; } function parseExRule(exruleString, schedule, values) { @@ -129,20 +100,10 @@ function parseExRule(exruleString, schedule, values) { }; } -function parseRule(ruleString, schedule, frequencies) { +function parseRule(ruleString, schedule) { const { - origOptions: { - bymonth, - bymonthday, - bysetpos, - byweekday, - count, - freq, - interval, - until, - }, + origOptions: { count, freq, interval, until, ...rest }, } = RRule.fromString(ruleString); - const now = DateTime.now(); const closestQuarterHour = DateTime.fromMillis( Math.ceil(now.ts / 900000) * 900000 @@ -156,17 +117,17 @@ function parseRule(ruleString, schedule, frequencies) { endTime: time, occurrences: 1, interval: 1, - end: 'never', + endingType: 'never', }; - if (until) { - options.end = 'onDate'; + if (until?.length) { + options.endingType = 'onDate'; const end = DateTime.fromISO(until.toISOString()); const [endDate, endTime] = dateToInputDateTime(end, schedule.timezone); options.endDate = endDate; options.endTime = endTime; } else if (count) { - options.end = 'after'; + options.endingType = 'after'; options.occurrences = count; } @@ -178,101 +139,10 @@ function parseRule(ruleString, schedule, frequencies) { throw new Error(`Unexpected rrule frequency: ${freq}`); } const frequency = frequencyTypes[freq]; - if (frequencies.includes(frequency)) { - throw new Error(`Duplicate frequency types not supported (${frequency})`); - } - - if (freq === RRule.WEEKLY && byweekday) { - options.daysOfWeek = byweekday; - } - - if (freq === RRule.MONTHLY) { - options.runOn = 'day'; - options.runOnTheOccurrence = 1; - options.runOnTheDay = 'sunday'; - options.runOnDayNumber = 1; - - if (bymonthday) { - options.runOnDayNumber = bymonthday; - } - if (bysetpos) { - options.runOn = 'the'; - options.runOnTheOccurrence = bysetpos; - options.runOnTheDay = generateRunOnTheDay(byweekday); - } - } - - if (freq === RRule.YEARLY) { - options.runOn = 'day'; - options.runOnTheOccurrence = 1; - options.runOnTheDay = 'sunday'; - options.runOnTheMonth = 1; - options.runOnDayMonth = 1; - options.runOnDayNumber = 1; - - if (bymonthday) { - options.runOnDayNumber = bymonthday; - options.runOnDayMonth = bymonth; - } - if (bysetpos) { - options.runOn = 'the'; - options.runOnTheOccurrence = bysetpos; - options.runOnTheDay = generateRunOnTheDay(byweekday); - options.runOnTheMonth = bymonth; - } - } return { frequency, - options, + ...options, + ...rest, }; } - -function generateRunOnTheDay(days = []) { - if ( - [ - RRule.MO, - RRule.TU, - RRule.WE, - RRule.TH, - RRule.FR, - RRule.SA, - RRule.SU, - ].every((element) => days.indexOf(element) > -1) - ) { - return 'day'; - } - if ( - [RRule.MO, RRule.TU, RRule.WE, RRule.TH, RRule.FR].every( - (element) => days.indexOf(element) > -1 - ) - ) { - return 'weekday'; - } - if ([RRule.SA, RRule.SU].every((element) => days.indexOf(element) > -1)) { - return 'weekendDay'; - } - if (days.indexOf(RRule.MO) > -1) { - return 'monday'; - } - if (days.indexOf(RRule.TU) > -1) { - return 'tuesday'; - } - if (days.indexOf(RRule.WE) > -1) { - return 'wednesday'; - } - if (days.indexOf(RRule.TH) > -1) { - return 'thursday'; - } - if (days.indexOf(RRule.FR) > -1) { - return 'friday'; - } - if (days.indexOf(RRule.SA) > -1) { - return 'saturday'; - } - if (days.indexOf(RRule.SU) > -1) { - return 'sunday'; - } - - return null; -} diff --git a/awx/ui/src/components/Schedule/shared/scheduleFormHelpers.js b/awx/ui/src/components/Schedule/shared/scheduleFormHelpers.js new file mode 100644 index 0000000000..c81f9b3bc1 --- /dev/null +++ b/awx/ui/src/components/Schedule/shared/scheduleFormHelpers.js @@ -0,0 +1,232 @@ +import { t } from '@lingui/macro'; +import { DateTime } from 'luxon'; +import { RRule } from 'rrule'; +import buildRuleObj from './buildRuleObj'; +import buildRuleSet from './buildRuleSet'; + +// const NUM_DAYS_PER_FREQUENCY = { +// week: 7, +// month: 31, +// year: 365, +// }; +// const validateSchedule = () => +// const errors = {}; + +// values.frequencies.forEach((freq) => { +// const options = values.frequencyOptions[freq]; +// const freqErrors = {}; + +// if ( +// (freq === 'month' || freq === 'year') && +// options.runOn === 'day' && +// (options.runOnDayNumber < 1 || options.runOnDayNumber > 31) +// ) { +// freqErrors.runOn = t`Please select a day number between 1 and 31.`; +// } + +// if (options.end === 'after' && !options.occurrences) { +// freqErrors.occurrences = t`Please enter a number of occurrences.`; +// } + +// if (options.end === 'onDate') { +// if ( +// DateTime.fromFormat( +// `${values.startDate} ${values.startTime}`, +// 'yyyy-LL-dd h:mm a' +// ).toMillis() >= +// DateTime.fromFormat( +// `${options.endDate} ${options.endTime}`, +// 'yyyy-LL-dd h:mm a' +// ).toMillis() +// ) { +// freqErrors.endDate = t`Please select an end date/time that comes after the start date/time.`; +// } + +// if ( +// DateTime.fromISO(options.endDate) +// .diff(DateTime.fromISO(values.startDate), 'days') +// .toObject().days < NUM_DAYS_PER_FREQUENCY[freq] +// ) { +// const rule = new RRule( +// buildRuleObj({ +// startDate: values.startDate, +// startTime: values.startTime, +// frequencies: freq, +// ...options, +// }) +// ); +// if (rule.all().length === 0) { +// errors.startDate = t`Selected date range must have at least 1 schedule occurrence.`; +// freqErrors.endDate = t`Selected date range must have at least 1 schedule occurrence.`; +// } +// } +// } +// if (Object.keys(freqErrors).length > 0) { +// if (!errors.frequencyOptions) { +// errors.frequencyOptions = {}; +// } +// errors.frequencyOptions[freq] = freqErrors; +// } +// }); + +// if (values.exceptionFrequency.length > 0 && !scheduleHasInstances(values)) { +// errors.exceptionFrequency = t`This schedule has no occurrences due to the +// selected exceptions.`; +// } + +// ({}); +// function scheduleHasInstances(values) { +// let rangeToCheck = 1; +// values.frequencies.forEach((freq) => { +// if (NUM_DAYS_PER_FREQUENCY[freq] > rangeToCheck) { +// rangeToCheck = NUM_DAYS_PER_FREQUENCY[freq]; +// } +// }); + +// const ruleSet = buildRuleSet(values, true); +// const startDate = DateTime.fromISO(values.startDate); +// const endDate = startDate.plus({ days: rangeToCheck }); +// const instances = ruleSet.between( +// startDate.toJSDate(), +// endDate.toJSDate(), +// true, +// (date, i) => i === 0 +// ); + +// return instances.length > 0; +// } + +const bysetposOptions = [ + { value: '', key: 'none', label: 'None' }, + { value: 1, key: 'first', label: t`First` }, + { + value: 2, + key: 'second', + label: t`Second`, + }, + { value: 3, key: 'third', label: t`Third` }, + { + value: 4, + key: 'fourth', + label: t`Fourth`, + }, + { value: 5, key: 'fifth', label: t`Fifth` }, + { value: -1, key: 'last', label: t`Last` }, +]; + +const monthOptions = [ + { + key: 'january', + value: 1, + label: t`January`, + }, + { + key: 'february', + value: 2, + label: t`February`, + }, + { + key: 'march', + value: 3, + label: t`March`, + }, + { + key: 'april', + value: 4, + label: t`April`, + }, + { + key: 'may', + value: 5, + label: t`May`, + }, + { + key: 'june', + value: 6, + label: t`June`, + }, + { + key: 'july', + value: 7, + label: t`July`, + }, + { + key: 'august', + value: 8, + label: t`August`, + }, + { + key: 'september', + value: 9, + label: t`September`, + }, + { + key: 'october', + value: 10, + label: t`October`, + }, + { + key: 'november', + value: 11, + label: t`November`, + }, + { + key: 'december', + value: 12, + label: t`December`, + }, +]; + +const weekdayOptions = [ + { + value: RRule.SU, + key: 'sunday', + label: t`Sunday`, + }, + { + value: RRule.MO, + key: 'monday', + label: t`Monday`, + }, + { + value: RRule.TU, + key: 'tuesday', + label: t`Tuesday`, + }, + { + value: RRule.WE, + key: 'wednesday', + label: t`Wednesday`, + }, + { + value: RRule.TH, + key: 'thursday', + label: t`Thursday`, + }, + { + value: RRule.FR, + key: 'friday', + label: t`Friday`, + }, + { + value: RRule.SA, + key: 'saturday', + label: t`Saturday`, + }, +]; + +const FREQUENCIESCONSTANTS = { + minute: RRule.MINUTELY, + hour: RRule.HOURLY, + day: RRule.DAILY, + week: RRule.WEEKLY, + month: RRule.MONTHLY, + year: RRule.YEARLY, +}; +export { + monthOptions, + weekdayOptions, + bysetposOptions, + // validateSchedule, + FREQUENCIESCONSTANTS, +};