fix "show completed schedules" toggle being persisted (#6716)

* make mobile consistent with desktop

* remove old option and modal

* note

* remove some useMemos

* coderabbit
This commit is contained in:
Matt Fiddaman
2026-01-19 17:37:42 +00:00
committed by GitHub
parent cdae09e554
commit 517b1b4a81
7 changed files with 100 additions and 170 deletions

View File

@@ -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 <BudgetPageMenuModal key={key} {...modal.options} />;
case 'schedules-page-menu':
return <SchedulesPageMenuModal key={key} />;
case 'envelope-budget-month-menu':
return (
<SheetNameProvider

View File

@@ -1,7 +1,6 @@
import React, { useCallback, useMemo, useState } from 'react';
import { Trans, useTranslation } from 'react-i18next';
import { Button } from '@actual-app/components/button';
import { styles } from '@actual-app/components/styles';
import { Text } from '@actual-app/components/text';
import { theme } from '@actual-app/components/theme';
@@ -22,12 +21,10 @@ import { MobilePageHeader, Page } from '@desktop-client/components/Page';
import { useAccounts } from '@desktop-client/hooks/useAccounts';
import { useDateFormat } from '@desktop-client/hooks/useDateFormat';
import { useFormat } from '@desktop-client/hooks/useFormat';
import { useLocalPref } from '@desktop-client/hooks/useLocalPref';
import { useNavigate } from '@desktop-client/hooks/useNavigate';
import { usePayees } from '@desktop-client/hooks/usePayees';
import { useSchedules } from '@desktop-client/hooks/useSchedules';
import { useUndo } from '@desktop-client/hooks/useUndo';
import { pushModal } from '@desktop-client/modals/modalsSlice';
import { addNotification } from '@desktop-client/notifications/notificationsSlice';
import { useDispatch } from '@desktop-client/redux';
@@ -37,14 +34,10 @@ export function MobileSchedulesPage() {
const dispatch = useDispatch();
const { showUndoNotification } = useUndo();
const [filter, setFilter] = useState('');
const [showCompleted = false] = useLocalPref('schedules.showCompleted');
const [showCompleted, setShowCompleted] = useState(false);
const format = useFormat();
const dateFormat = useDateFormat() || 'MM/dd/yyyy';
const onOpenSchedulesPageMenu = useCallback(() => {
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={
<MobilePageHeader
title={
<Button
variant="bare"
onPress={onOpenSchedulesPageMenu}
style={{
fontSize: 16,
fontWeight: 500,
}}
>
<Text style={styles.underlinedText}>
<Trans>Schedules</Trans>
</Text>
</Button>
<Text style={{ ...styles.underlinedText, fontSize: 16 }}>
<Trans>Schedules</Trans>
</Text>
}
rightContent={<AddScheduleButton />}
/>
@@ -188,6 +161,9 @@ export function MobileSchedulesPage() {
statuses={statuses}
onSchedulePress={handleSchedulePress}
onScheduleDelete={handleScheduleDelete}
hasCompletedSchedules={hasCompletedSchedules}
showCompleted={showCompleted}
onShowCompleted={() => setShowCompleted(true)}
/>
</Page>
);

View File

@@ -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<ScheduleEntity['id'], ScheduleStatusType>;
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 (
<View
style={{
@@ -45,7 +61,7 @@ export function SchedulesList({
);
}
if (schedules.length === 0) {
if (listItems.length === 0) {
return (
<View
style={{
@@ -82,19 +98,39 @@ export function SchedulesList({
<GridList
aria-label={t('Schedules')}
aria-busy={isLoading || undefined}
items={schedules}
items={listItems}
style={{
paddingBottom: MOBILE_NAV_HEIGHT,
}}
>
{schedule => (
<SchedulesListItem
value={schedule}
status={statuses.get(schedule.id) || 'scheduled'}
onAction={() => onSchedulePress(schedule)}
onDelete={() => onScheduleDelete(schedule)}
/>
)}
{item =>
!('completed' in item) ? (
<ActionableGridListItem
id="show-completed"
value={item}
textValue={showCompletedLabel}
onAction={onShowCompleted}
>
<View style={{ width: '100%', alignItems: 'center' }}>
<Text
style={{
fontStyle: 'italic',
color: theme.pageTextSubdued,
}}
>
<Trans>Show completed schedules</Trans>
</Text>
</View>
</ActionableGridListItem>
) : (
<SchedulesListItem
value={item}
status={statuses.get(item.id) || 'scheduled'}
onAction={() => onSchedulePress(item)}
onDelete={() => onScheduleDelete(item)}
/>
)
}
</GridList>
</Virtualizer>
{isLoading && (

View File

@@ -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 (
<Modal name="schedules-page-menu">
{({ state: { close } }) => (
<>
<ModalHeader
title={t('Schedules')}
rightContent={<ModalCloseButton onPress={close} />}
/>
<SchedulesPageMenu
getItemStyle={() => defaultMenuItemStyle}
onClose={close}
/>
</>
)}
</Modal>
);
}
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 (
<Menu
getItemStyle={getItemStyle}
onMenuSelect={onMenuSelect}
items={[
{
name: 'toggle-completed-schedules',
text: showCompleted
? t('Hide completed schedules')
: t('Show completed schedules'),
},
]}
/>
);
}

View File

@@ -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();

View File

@@ -471,9 +471,6 @@ export type Modal =
onSwitchBudgetFile: () => void;
};
}
| {
name: 'schedules-page-menu';
}
| {
name: 'envelope-budget-month-menu';
options: {