mirror of
https://github.com/actualbudget/actual.git
synced 2026-03-21 15:36:50 -05:00
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:
@@ -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
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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'),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -471,9 +471,6 @@ export type Modal =
|
||||
onSwitchBudgetFile: () => void;
|
||||
};
|
||||
}
|
||||
| {
|
||||
name: 'schedules-page-menu';
|
||||
}
|
||||
| {
|
||||
name: 'envelope-budget-month-menu';
|
||||
options: {
|
||||
|
||||
Reference in New Issue
Block a user