diff --git a/packages/desktop-client/src/components/select/RecurringSchedulePicker.jsx b/packages/desktop-client/src/components/select/RecurringSchedulePicker.tsx similarity index 79% rename from packages/desktop-client/src/components/select/RecurringSchedulePicker.jsx rename to packages/desktop-client/src/components/select/RecurringSchedulePicker.tsx index 036b875c887..382b3273de1 100644 --- a/packages/desktop-client/src/components/select/RecurringSchedulePicker.jsx +++ b/packages/desktop-client/src/components/select/RecurringSchedulePicker.tsx @@ -1,4 +1,10 @@ -import React, { useEffect, useReducer, useRef, useState } from 'react'; +import React, { + Dispatch, + useEffect, + useReducer, + useRef, + useState, +} from 'react'; import { sendCatch } from 'loot-core/src/platform/client/fetch'; import * as monthUtils from 'loot-core/src/shared/months'; @@ -19,6 +25,8 @@ import { View } from '../common/View'; import { Checkbox } from '../forms'; import { DateSelect } from './DateSelect'; +import { RecurConfig, RecurPattern } from 'loot-core/types/models'; +import { WithRequired } from 'loot-core/types/util'; // ex: There is no 6th Friday of the Month const MAX_DAY_OF_WEEK_INTERVAL = 5; @@ -28,7 +36,7 @@ const FREQUENCY_OPTIONS = [ { id: 'weekly', name: 'Weeks' }, { id: 'monthly', name: 'Months' }, { id: 'yearly', name: 'Years' }, -]; +] as const; const DAY_OF_MONTH_OPTIONS = [...Array(31).keys()].map(day => day + 1); @@ -40,16 +48,16 @@ const DAY_OF_WEEK_OPTIONS = [ { id: 'TH', name: 'Thursday' }, { id: 'FR', name: 'Friday' }, { id: 'SA', name: 'Saturday' }, -]; +] as const; -function parsePatternValue(value) { +function parsePatternValue(value: string | number) { if (value === 'last') { return -1; } return Number(value); } -function parseConfig(config) { +function parseConfig(config: Partial): StateConfig { return { start: monthUtils.currentDay(), interval: 1, @@ -58,13 +66,13 @@ function parseConfig(config) { skipWeekend: false, weekendSolveMode: 'before', endMode: 'never', - endOccurrences: '1', + endOccurrences: 1, endDate: monthUtils.currentDay(), ...config, }; } -function unparseConfig(parsed) { +function unparseConfig(parsed: StateConfig): RecurConfig { return { ...parsed, interval: validInterval(parsed.interval), @@ -72,14 +80,23 @@ function unparseConfig(parsed) { }; } -function createMonthlyRecurrence(startDate) { +function createMonthlyRecurrence(startDate: string) { return { value: parseInt(monthUtils.format(startDate, 'd')), - type: 'day', + type: 'day' as const, }; } -function boundedRecurrence({ field, value, recurrence }) { +function boundedRecurrence({ + field, + value, + recurrence, +}: { + recurrence: RecurPattern; +} & ( + | { field: 'type'; value: RecurPattern['type'] } + | { field: 'value'; value: RecurPattern['value'] } +)) { if ( (field === 'value' && recurrence.type !== 'day' && @@ -93,7 +110,48 @@ function boundedRecurrence({ field, value, recurrence }) { return { [field]: value }; } -function reducer(state, action) { +type StateConfig = Omit< + WithRequired, + 'interval' | 'endOccurrences' +> & { + interval: number | string; + endOccurrences: number | string; +}; + +type ReducerState = { + config: StateConfig; +}; + +type UpdateRecurrenceAction = + | { + type: 'update-recurrence'; + recurrence: RecurPattern; + field: 'type'; + value: RecurPattern['type']; + } + | { + type: 'update-recurrence'; + recurrence: RecurPattern; + field: 'value'; + value: RecurPattern['value']; + }; + +type ChangeFieldAction = { + type: 'change-field'; + field: T; + value: StateConfig[T]; +}; + +type ReducerAction = + | { type: 'replace-config'; config: StateConfig } + | ChangeFieldAction + | UpdateRecurrenceAction + | { type: 'add-recurrence' } + | { type: 'remove-recurrence'; recurrence: any } + | { type: 'set-skip-weekend'; skipWeekend: boolean } + | { type: 'set-weekend-solve'; value: StateConfig['weekendSolveMode'] }; + +function reducer(state: ReducerState, action: ReducerAction): ReducerState { switch (action.type) { case 'replace-config': return { ...state, config: action.config }; @@ -159,7 +217,7 @@ function reducer(state, action) { } } -function SchedulePreview({ previewDates }) { +function SchedulePreview({ previewDates }: { previewDates: Date[] }) { const dateFormat = (useDateFormat() || 'MM/dd/yyyy') .replace('MM', 'M') .replace('dd', 'd'); @@ -198,15 +256,18 @@ function SchedulePreview({ previewDates }) { ); } -function validInterval(interval) { - const intInterval = parseInt(interval); +function validInterval(interval: string | number) { + const intInterval = Number(interval); return Number.isInteger(intInterval) && intInterval > 0 ? intInterval : 1; } -function MonthlyPatterns({ config, dispatch }) { - const updateRecurrence = (recurrence, field, value) => - dispatch({ type: 'update-recurrence', recurrence, field, value }); - +function MonthlyPatterns({ + config, + dispatch, +}: { + config: StateConfig; + dispatch: Dispatch; +}) { return ( {config.patterns.map((recurrence, idx) => ( @@ -221,24 +282,34 @@ function MonthlyPatterns({ config, dispatch }) { options={[ [-1, 'Last'], Menu.line, - ...DAY_OF_MONTH_OPTIONS.map(opt => [opt, opt]), + ...DAY_OF_MONTH_OPTIONS.map(opt => [opt, String(opt)] as const), ]} value={recurrence.value} onChange={value => - updateRecurrence(recurrence, 'value', parsePatternValue(value)) + dispatch({ + type: 'update-recurrence', + recurrence, + field: 'value', + value: parsePatternValue(value), + }) } - disabledKeys={['-']} buttonStyle={{ flex: 1, marginRight: 10 }} />