From 517b1b4a81838cab765e21aa595d4101c87b2c1f Mon Sep 17 00:00:00 2001 From: Matt Fiddaman Date: Mon, 19 Jan 2026 17:37:42 +0000 Subject: [PATCH] fix "show completed schedules" toggle being persisted (#6716) * make mobile consistent with desktop * remove old option and modal * note * remove some useMemos * coderabbit --- .../desktop-client/src/components/Modals.tsx | 4 - .../mobile/schedules/MobileSchedulesPage.tsx | 112 +++++++----------- .../mobile/schedules/SchedulesList.tsx | 60 ++++++++-- .../modals/SchedulesPageMenuModal.tsx | 78 ------------ .../components/schedules/SchedulesTable.tsx | 7 +- .../desktop-client/src/modals/modalsSlice.ts | 3 - upcoming-release-notes/6716.md | 6 + 7 files changed, 100 insertions(+), 170 deletions(-) delete mode 100644 packages/desktop-client/src/components/modals/SchedulesPageMenuModal.tsx create mode 100644 upcoming-release-notes/6716.md diff --git a/packages/desktop-client/src/components/Modals.tsx b/packages/desktop-client/src/components/Modals.tsx index 797084a7c5..de0755b0c0 100644 --- a/packages/desktop-client/src/components/Modals.tsx +++ b/packages/desktop-client/src/components/Modals.tsx @@ -62,7 +62,6 @@ import { PasswordEnableModal } from './modals/PasswordEnableModal'; import { PayeeAutocompleteModal } from './modals/PayeeAutocompleteModal'; import { PluggyAiInitialiseModal } from './modals/PluggyAiInitialiseModal'; import { ScheduledTransactionMenuModal } from './modals/ScheduledTransactionMenuModal'; -import { SchedulesPageMenuModal } from './modals/SchedulesPageMenuModal'; import { SelectLinkedAccountsModal } from './modals/SelectLinkedAccountsModal'; import { SimpleFinInitialiseModal } from './modals/SimpleFinInitialiseModal'; import { TrackingBalanceMenuModal } from './modals/TrackingBalanceMenuModal'; @@ -345,9 +344,6 @@ export function Modals() { case 'budget-page-menu': return ; - case 'schedules-page-menu': - return ; - case 'envelope-budget-month-menu': return ( { - dispatch(pushModal({ modal: { name: 'schedules-page-menu' } })); - }, [dispatch]); - const schedulesQuery = useMemo(() => q('schedules').select('*'), []); const { isLoading: isSchedulesLoading, @@ -55,56 +48,45 @@ export function MobileSchedulesPage() { const payees = usePayees(); const accounts = useAccounts(); - const filteredSchedules = useMemo(() => { - const filterIncludes = (str: string | null | undefined) => - str - ? getNormalisedString(str).includes(getNormalisedString(filter)) || - getNormalisedString(filter).includes(getNormalisedString(str)) - : false; + const filterIncludes = (str: string | null | undefined) => + str + ? getNormalisedString(str).includes(getNormalisedString(filter)) || + getNormalisedString(filter).includes(getNormalisedString(str)) + : false; - const baseSchedules = filter - ? schedules.filter(schedule => { - const payee = payees.find(p => schedule._payee === p.id); - const account = accounts.find(a => schedule._account === a.id); - const amount = getScheduledAmount(schedule._amount); - const amountStr = - (schedule._amountOp === 'isapprox' || - schedule._amountOp === 'isbetween' - ? '~' - : '') + - (amount > 0 ? '+' : '') + - format(Math.abs(amount || 0), 'financial'); - const dateStr = schedule.next_date - ? monthUtilFormat(schedule.next_date, dateFormat) - : null; - const statusLabel = statuses.get(schedule.id); + const baseSchedules = filter + ? schedules.filter(schedule => { + const payee = payees.find(p => schedule._payee === p.id); + const account = accounts.find(a => schedule._account === a.id); + const amount = getScheduledAmount(schedule._amount); + const amountStr = + (schedule._amountOp === 'isapprox' || + schedule._amountOp === 'isbetween' + ? '~' + : '') + + (amount > 0 ? '+' : '') + + format(Math.abs(amount || 0), 'financial'); + const dateStr = schedule.next_date + ? monthUtilFormat(schedule.next_date, dateFormat) + : null; + const statusLabel = statuses.get(schedule.id); - return ( - filterIncludes(schedule.name) || - filterIncludes(payee?.name) || - filterIncludes(account?.name) || - filterIncludes(amountStr) || - filterIncludes(statusLabel) || - filterIncludes(dateStr) - ); - }) - : schedules; - - if (showCompleted) { - return baseSchedules; - } - - return baseSchedules.filter(s => !s.completed); - }, [ - schedules, - filter, - payees, - accounts, - format, - dateFormat, - statuses, - showCompleted, - ]); + return ( + filterIncludes(schedule.name) || + filterIncludes(payee?.name) || + filterIncludes(account?.name) || + filterIncludes(amountStr) || + filterIncludes(statusLabel) || + filterIncludes(dateStr) + ); + }) + : schedules; + const hasCompletedSchedules = baseSchedules.some( + schedule => schedule.completed, + ); + const filteredSchedules = showCompleted + ? baseSchedules + : baseSchedules.filter(schedule => !schedule.completed); const handleSchedulePress = useCallback( (schedule: ScheduleEntity) => { @@ -140,18 +122,9 @@ export function MobileSchedulesPage() { header={ - - Schedules - - + + Schedules + } rightContent={} /> @@ -188,6 +161,9 @@ export function MobileSchedulesPage() { statuses={statuses} onSchedulePress={handleSchedulePress} onScheduleDelete={handleScheduleDelete} + hasCompletedSchedules={hasCompletedSchedules} + showCompleted={showCompleted} + onShowCompleted={() => setShowCompleted(true)} /> ); diff --git a/packages/desktop-client/src/components/mobile/schedules/SchedulesList.tsx b/packages/desktop-client/src/components/mobile/schedules/SchedulesList.tsx index d29f4e9f7f..95792c8aa8 100644 --- a/packages/desktop-client/src/components/mobile/schedules/SchedulesList.tsx +++ b/packages/desktop-client/src/components/mobile/schedules/SchedulesList.tsx @@ -1,5 +1,5 @@ import { GridList, ListLayout, Virtualizer } from 'react-aria-components'; -import { useTranslation } from 'react-i18next'; +import { Trans, useTranslation } from 'react-i18next'; import { AnimatedLoading } from '@actual-app/components/icons/AnimatedLoading'; import { Text } from '@actual-app/components/text'; @@ -10,15 +10,22 @@ import { type ScheduleEntity } from 'loot-core/types/models'; import { SchedulesListItem } from './SchedulesListItem'; +import { ActionableGridListItem } from '@desktop-client/components/mobile/ActionableGridListItem'; import { MOBILE_NAV_HEIGHT } from '@desktop-client/components/mobile/MobileNavTabs'; import { type ScheduleStatusType } from '@desktop-client/hooks/useSchedules'; +type CompletedSchedulesItem = { id: 'show-completed' }; +type SchedulesListEntry = ScheduleEntity | CompletedSchedulesItem; + type SchedulesListProps = { schedules: readonly ScheduleEntity[]; isLoading: boolean; statuses: Map; onSchedulePress: (schedule: ScheduleEntity) => void; onScheduleDelete: (schedule: ScheduleEntity) => void; + hasCompletedSchedules?: boolean; + showCompleted?: boolean; + onShowCompleted?: () => void; }; export function SchedulesList({ @@ -27,10 +34,19 @@ export function SchedulesList({ statuses, onSchedulePress, onScheduleDelete, + hasCompletedSchedules = false, + showCompleted = false, + onShowCompleted, }: SchedulesListProps) { const { t } = useTranslation(); + const shouldShowCompletedItem = + hasCompletedSchedules && !showCompleted && onShowCompleted; + const listItems: readonly SchedulesListEntry[] = shouldShowCompletedItem + ? [...schedules, { id: 'show-completed' }] + : schedules; + const showCompletedLabel = t('Show completed schedules'); - if (isLoading && schedules.length === 0) { + if (isLoading && listItems.length === 0) { return ( - {schedule => ( - onSchedulePress(schedule)} - onDelete={() => onScheduleDelete(schedule)} - /> - )} + {item => + !('completed' in item) ? ( + + + + Show completed schedules + + + + ) : ( + onSchedulePress(item)} + onDelete={() => onScheduleDelete(item)} + /> + ) + } {isLoading && ( diff --git a/packages/desktop-client/src/components/modals/SchedulesPageMenuModal.tsx b/packages/desktop-client/src/components/modals/SchedulesPageMenuModal.tsx deleted file mode 100644 index a9ce1e5154..0000000000 --- a/packages/desktop-client/src/components/modals/SchedulesPageMenuModal.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import { type CSSProperties } from 'react'; -import { useTranslation } from 'react-i18next'; - -import { Menu } from '@actual-app/components/menu'; -import { styles } from '@actual-app/components/styles'; -import { theme } from '@actual-app/components/theme'; - -import { - Modal, - ModalCloseButton, - ModalHeader, -} from '@desktop-client/components/common/Modal'; -import { useLocalPref } from '@desktop-client/hooks/useLocalPref'; - -export function SchedulesPageMenuModal() { - const { t } = useTranslation(); - const defaultMenuItemStyle: CSSProperties = { - ...styles.mobileMenuItem, - color: theme.menuItemText, - borderRadius: 0, - borderTop: `1px solid ${theme.pillBorder}`, - }; - - return ( - - {({ state: { close } }) => ( - <> - } - /> - defaultMenuItemStyle} - onClose={close} - /> - - )} - - ); -} - -type SchedulesPageMenuProps = { - getItemStyle?: () => CSSProperties; - onClose: () => void; -}; - -function SchedulesPageMenu({ getItemStyle, onClose }: SchedulesPageMenuProps) { - const { t } = useTranslation(); - const [showCompleted, setShowCompletedPref] = useLocalPref( - 'schedules.showCompleted', - ); - - const onMenuSelect = (name: string) => { - switch (name) { - case 'toggle-completed-schedules': - setShowCompletedPref(!showCompleted); - break; - default: - throw new Error(`Unrecognized menu item: ${name}`); - } - onClose(); - }; - - return ( - - ); -} diff --git a/packages/desktop-client/src/components/schedules/SchedulesTable.tsx b/packages/desktop-client/src/components/schedules/SchedulesTable.tsx index 877db13bbf..472f36b21e 100644 --- a/packages/desktop-client/src/components/schedules/SchedulesTable.tsx +++ b/packages/desktop-client/src/components/schedules/SchedulesTable.tsx @@ -1,5 +1,5 @@ // @ts-strict-ignore -import React, { useMemo, useRef, type CSSProperties } from 'react'; +import React, { useMemo, useRef, useState, type CSSProperties } from 'react'; import { Trans, useTranslation } from 'react-i18next'; import { Button } from '@actual-app/components/button'; @@ -32,7 +32,6 @@ import { useAccounts } from '@desktop-client/hooks/useAccounts'; import { useContextMenu } from '@desktop-client/hooks/useContextMenu'; import { useDateFormat } from '@desktop-client/hooks/useDateFormat'; import { useFormat } from '@desktop-client/hooks/useFormat'; -import { useLocalPref } from '@desktop-client/hooks/useLocalPref'; import { usePayees } from '@desktop-client/hooks/usePayees'; import { type ScheduleStatuses, @@ -337,9 +336,7 @@ export function SchedulesTable({ const format = useFormat(); const dateFormat = useDateFormat() || 'MM/dd/yyyy'; - const [showCompleted = false, setShowCompleted] = useLocalPref( - 'schedules.showCompleted', - ); + const [showCompleted, setShowCompleted] = useState(false); const payees = usePayees(); const accounts = useAccounts(); diff --git a/packages/desktop-client/src/modals/modalsSlice.ts b/packages/desktop-client/src/modals/modalsSlice.ts index 1017065083..256d544f57 100644 --- a/packages/desktop-client/src/modals/modalsSlice.ts +++ b/packages/desktop-client/src/modals/modalsSlice.ts @@ -471,9 +471,6 @@ export type Modal = onSwitchBudgetFile: () => void; }; } - | { - name: 'schedules-page-menu'; - } | { name: 'envelope-budget-month-menu'; options: { diff --git a/upcoming-release-notes/6716.md b/upcoming-release-notes/6716.md new file mode 100644 index 0000000000..58cd7343bd --- /dev/null +++ b/upcoming-release-notes/6716.md @@ -0,0 +1,6 @@ +--- +category: Bugfixes +authors: [matt-fidd] +--- + +Fix show completed schedules toggle being persisted