From c09a85f34051cf79c013fcf4f959d99a1e7a5003 Mon Sep 17 00:00:00 2001 From: Julian Dominguez-Schatz Date: Thu, 14 Dec 2023 06:43:16 -0500 Subject: [PATCH] Add schedule end date/count field (#1899) * Add "end" field with date/count options * Use "end" field to generate schedule * Show "end" field in recurring description * Disable weekend before/after picker when not enabled * Add release notes * Fix failing typechecks * Add some description tests * PR feedback * 'Features', not 'Feature' * Fix goal templates infinite loop * Empty commit to bump ci * Fix bug where schedule templates in the past would apply incorrectly For example, if you had a schedule which started in November 2023 for 1.00, and you applied the schedule in October 2023, then you would end up with a value of 0.50 applied in October. * Fix handling of schedules with an end date This commit also includes a refactor of the skip-weekend logic: rather than referring only to dates with skipped weekends (which requires checking whether the "next date" request worked correctly), we track a "base date" which is the previous value of the schedule according to the rrule, excluding any weekend-skipping. This lets us use `addDays(baseDate, 1)` to get the next occurrence, regardless of the weekend behaviour. Doing things this way ensures that the loop will always make progress. * Only compute skipped weekend if weekend skips were requested * Fix typo in iterate-schedule-occurrences code We should be using `nextBaseDate` to derive the next base date, not `nextDate`; this is because we want the base date to be guaranteed to make progress in each loop iteration, so we can finish in at most 30 iterations without duplicate base dates. * Use const * Revert const -> let for one mutable variable --- .../src/components/common/Select.tsx | 3 + .../src/components/rules/Value.tsx | 2 +- .../schedules/DiscoverSchedules.tsx | 6 +- .../src/components/schedules/EditSchedule.js | 3 + .../select/RecurringSchedulePicker.js | 69 +++- .../src/server/budget/goals/goalsSchedule.ts | 63 ++-- .../loot-core/src/server/schedules/app.ts | 21 +- .../loot-core/src/shared/schedules.test.ts | 302 +++++++++++++----- packages/loot-core/src/shared/schedules.ts | 39 ++- .../loot-core/src/types/models/schedule.d.ts | 3 + upcoming-release-notes/1899.md | 6 + 11 files changed, 391 insertions(+), 126 deletions(-) create mode 100644 upcoming-release-notes/1899.md diff --git a/packages/desktop-client/src/components/common/Select.tsx b/packages/desktop-client/src/components/common/Select.tsx index d60bd41000..eb2be0fbaa 100644 --- a/packages/desktop-client/src/components/common/Select.tsx +++ b/packages/desktop-client/src/components/common/Select.tsx @@ -19,6 +19,7 @@ type SelectProps = { style?: CSSProperties; wrapperStyle?: CSSProperties; line?: number; + disabled?: boolean; disabledKeys?: Value[]; }; @@ -46,6 +47,7 @@ export default function Select({ style, wrapperStyle, line, + disabled, disabledKeys = [], }: SelectProps) { const arrowSize = 7; @@ -55,6 +57,7 @@ export default function Select({ ({ case 'date': if (value) { if (value.frequency) { - return getRecurringDescription(value); + return getRecurringDescription(value, dateFormat); } return formatDate(parseISO(value), dateFormat); } diff --git a/packages/desktop-client/src/components/schedules/DiscoverSchedules.tsx b/packages/desktop-client/src/components/schedules/DiscoverSchedules.tsx index 74a9f2646d..713397ed29 100644 --- a/packages/desktop-client/src/components/schedules/DiscoverSchedules.tsx +++ b/packages/desktop-client/src/components/schedules/DiscoverSchedules.tsx @@ -1,4 +1,5 @@ import React, { useState } from 'react'; +import { useSelector } from 'react-redux'; import q, { runQuery } from 'loot-core/src/client/query-helpers'; import { send } from 'loot-core/src/platform/client/fetch'; @@ -35,11 +36,14 @@ function DiscoverSchedulesTable({ }) { const selectedItems = useSelectedItems(); const dispatchSelected = useSelectedDispatch(); + const dateFormat = useSelector( + state => state.prefs.local.dateFormat || 'MM/dd/yyyy', + ); function renderItem({ item }: { item: DiscoverScheduleEntity }) { const selected = selectedItems.has(item.id); const amountOp = item._conditions.find(c => c.field === 'amount').op; - const recurDescription = getRecurringDescription(item.date); + const recurDescription = getRecurringDescription(item.date, dateFormat); return ( -
- +
+ + updateField('endOccurrences', e.target.value)} + defaultValue={config.endOccurrences || 1} + /> + occurrence{config.endOccurrences === '1' ? '' : 's'} + + )} + {config.endMode === 'on_date' && ( + updateField('endDate', value)} + containerProps={{ style: { width: 100 } }} + dateFormat={dateFormat} + /> + )}
Repeat every updateField('interval', e.target.value)} - onEnter={e => updateField('interval', e.target.value)} + type="number" + min={1} + onChange={e => updateField('interval', e.target.value)} defaultValue={config.interval || 1} />