Add edit payee functionality to mobile page (#5874)

This commit is contained in:
Matiss Janis Aboltins
2025-11-06 20:58:03 +00:00
committed by GitHub
parent 1e6fde571e
commit 3825e65693
14 changed files with 242 additions and 19 deletions

View File

@@ -51,7 +51,7 @@ export class MobilePayeesPage {
}
/**
* Click on a payee to view/edit rules
* Click on a payee to open the edit page
*/
async clickPayee(index: number) {
const payee = this.getNthPayee(index);

View File

@@ -62,7 +62,7 @@ test.describe('Mobile Payees', () => {
await expect(page).toMatchThemeScreenshots();
});
test('clicking on a payee opens rule creation form', async () => {
test('clicking on a payee opens payee edit page', async () => {
await payeesPage.waitForLoadingToComplete();
const payeeCount = await payeesPage.getPayeeCount();
@@ -70,8 +70,16 @@ test.describe('Mobile Payees', () => {
await payeesPage.clickPayee(0);
// Should navigate to rules page for creating a new rule
await expect(page).toHaveURL(/\/rules/);
// Should navigate to payee edit page
await expect(page).toHaveURL(/\/payees\/.+/);
// Check that the edit page elements are visible
await expect(
page.getByRole('heading', { name: 'Edit Payee' }),
).toBeVisible();
await expect(page.getByPlaceholder('Payee name')).toBeVisible();
await expect(page.getByRole('button', { name: 'Save' })).toBeVisible();
await expect(page).toMatchThemeScreenshots();
});

View File

@@ -270,6 +270,14 @@ export function FinancesApp() {
path="/payees"
element={<NarrowAlternate name="Payees" />}
/>
<Route
path="/payees/:id"
element={
<WideNotSupported>
<NarrowAlternate name="PayeeEdit" />
</WideNotSupported>
}
/>
<Route
path="/rules"
element={<NarrowAlternate name="Rules" />}

View File

@@ -9,7 +9,7 @@ import { useDrag } from '@use-gesture/react';
import { type WithRequired } from 'loot-core/types/util';
type ActionableGridListItemProps<T> = {
actions?: ReactNode;
actions?: ReactNode | ((params: { close: () => void }) => ReactNode);
actionsBackgroundColor?: string;
actionsWidth?: number;
children?: ReactNode;
@@ -131,7 +131,18 @@ export function ActionableGridListItem<T extends object>({
minWidth: actionsWidth,
}}
>
{actions}
{typeof actions === 'function'
? actions({
close: () => {
api.start({
x: 0,
onRest: () => {
setIsRevealed(false);
},
});
},
})
: actions}
</div>
)}
</animated.div>

View File

@@ -0,0 +1,152 @@
import React, { useEffect, useState, useCallback } from 'react';
import { useTranslation, Trans } from 'react-i18next';
import { useParams } from 'react-router';
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';
import { View } from '@actual-app/components/view';
import { send } from 'loot-core/platform/client/fetch';
import { type PayeeEntity } from 'loot-core/types/models';
import { MobileBackButton } from '@desktop-client/components/mobile/MobileBackButton';
import { InputField } from '@desktop-client/components/mobile/MobileForms';
import { MobilePageHeader, Page } from '@desktop-client/components/Page';
import { useNavigate } from '@desktop-client/hooks/useNavigate';
import { usePayees } from '@desktop-client/hooks/usePayees';
import { useUndo } from '@desktop-client/hooks/useUndo';
import { addNotification } from '@desktop-client/notifications/notificationsSlice';
import { useDispatch } from '@desktop-client/redux';
export function MobilePayeeEditPage() {
const { t } = useTranslation();
const navigate = useNavigate();
const { id } = useParams<{ id: string }>();
const dispatch = useDispatch();
const { showUndoNotification } = useUndo();
const payees = usePayees();
const [payee, setPayee] = useState<PayeeEntity | null>(null);
const [editedPayeeName, setEditedPayeeName] = useState('');
const [isLoading, setIsLoading] = useState(true);
// Load payee by ID
useEffect(() => {
if (id) {
setIsLoading(true);
const foundPayee = payees.find(p => p.id === id);
if (foundPayee) {
setPayee(foundPayee);
setEditedPayeeName(foundPayee.name);
setIsLoading(false);
} else {
// Payee not found, navigate back to payees list
navigate('/payees');
}
}
}, [id, payees, navigate]);
const handleCancel = useCallback(() => {
navigate(-1);
}, [navigate]);
const handleSave = useCallback(async () => {
if (!payee || !editedPayeeName.trim()) {
return;
}
try {
await send('payees-batch-change', {
updated: [{ id: payee.id, name: editedPayeeName.trim() }],
});
showUndoNotification({
message: t('Payee {{oldName}} renamed to {{newName}}', {
oldName: payee.name,
newName: editedPayeeName.trim(),
}),
});
navigate('/payees');
} catch (error) {
console.error('Failed to update payee:', error);
dispatch(
addNotification({
notification: {
type: 'error',
message: t('Failed to update payee. Please try again.'),
},
}),
);
}
}, [payee, editedPayeeName, dispatch, showUndoNotification, t, navigate]);
// Show loading state while fetching payee
if (isLoading) {
return (
<Page
header={
<MobilePageHeader
title={t('Loading...')}
leftContent={<MobileBackButton onPress={handleCancel} />}
/>
}
padding={0}
>
<View
style={{
flex: 1,
backgroundColor: theme.mobilePageBackground,
justifyContent: 'center',
alignItems: 'center',
}}
>
<Text>
<Trans>Loading payee...</Trans>
</Text>
</View>
</Page>
);
}
return (
<Page
header={
<MobilePageHeader
title={t('Edit Payee')}
leftContent={<MobileBackButton onPress={handleCancel} />}
/>
}
footer={
<View
style={{
paddingLeft: styles.mobileEditingPadding,
paddingRight: styles.mobileEditingPadding,
paddingTop: 10,
paddingBottom: 'calc(10px + env(safe-area-inset-bottom))',
backgroundColor: theme.tableHeaderBackground,
borderTopWidth: 1,
borderColor: theme.tableBorder,
}}
>
<Button
variant="primary"
onPress={handleSave}
isDisabled={!editedPayeeName.trim()}
style={{ height: styles.mobileMinHeight }}
>
<Trans>Save</Trans>
</Button>
</View>
}
>
<View style={{ paddingTop: 20 }}>
<InputField
placeholder={t('Payee name')}
value={editedPayeeName}
onChangeValue={setEditedPayeeName}
/>
</View>
</Page>
);
}

View File

@@ -43,6 +43,13 @@ export function MobilePayeesPage() {
}, []);
const handlePayeePress = useCallback(
(payee: PayeeEntity) => {
navigate(`/payees/${payee.id}`);
},
[navigate],
);
const handlePayeeRuleAction = useCallback(
async (payee: PayeeEntity) => {
// View associated rules for the payee
if ((ruleCounts.get(payee.id) ?? 0) > 0) {
@@ -137,6 +144,7 @@ export function MobilePayeesPage() {
isLoading={isLoading}
onPayeePress={handlePayeePress}
onPayeeDelete={handlePayeeDelete}
onPayeeRuleAction={handlePayeeRuleAction}
/>
</Page>
);

View File

@@ -19,6 +19,7 @@ type PayeesListProps = {
isLoading?: boolean;
onPayeePress: (payee: PayeeEntity) => void;
onPayeeDelete: (payee: PayeeEntity) => void;
onPayeeRuleAction: (payee: PayeeEntity) => void;
};
export function PayeesList({
@@ -28,6 +29,7 @@ export function PayeesList({
isLoading = false,
onPayeePress,
onPayeeDelete,
onPayeeRuleAction,
}: PayeesListProps) {
const { t } = useTranslation();
@@ -90,6 +92,7 @@ export function PayeesList({
isRuleCountLoading={isRuleCountsLoading}
onAction={() => onPayeePress(payee)}
onDelete={() => onPayeeDelete(payee)}
onViewRules={() => onPayeeRuleAction(payee)}
/>
)}
</GridList>

View File

@@ -1,4 +1,4 @@
import React, { memo } from 'react';
import React from 'react';
import { type GridListItemProps } from 'react-aria-components';
import { Trans, useTranslation } from 'react-i18next';
@@ -6,6 +6,7 @@ import { Button } from '@actual-app/components/button';
import { SvgBookmark } from '@actual-app/components/icons/v1';
import { SpaceBetween } from '@actual-app/components/space-between';
import { theme } from '@actual-app/components/theme';
import { View } from '@actual-app/components/view';
import { type PayeeEntity } from 'loot-core/types/models';
import { type WithRequired } from 'loot-core/types/util';
@@ -17,13 +18,15 @@ type PayeesListItemProps = {
ruleCount: number;
isRuleCountLoading?: boolean;
onDelete: () => void;
onViewRules: () => void;
} & WithRequired<GridListItemProps<PayeeEntity>, 'value'>;
export const PayeesListItem = memo(function PayeeListItem({
export function PayeesListItem({
value: payee,
ruleCount,
isRuleCountLoading,
onDelete,
onViewRules,
...props
}: PayeesListItemProps) {
const { t } = useTranslation();
@@ -37,18 +40,38 @@ export const PayeesListItem = memo(function PayeeListItem({
id={payee.id}
value={payee}
textValue={label}
actionsWidth={200}
actions={
!payee.transfer_acct && (
<Button
variant="bare"
onPress={onDelete}
style={{
color: theme.errorText,
width: '100%',
}}
>
<Trans>Delete</Trans>
</Button>
<View style={{ flexDirection: 'row', flex: 1 }}>
<Button
variant="bare"
onPress={onViewRules}
style={{
color: theme.pillText,
backgroundColor: theme.pillBackground,
flex: 1,
borderRadius: 0,
}}
>
{ruleCount > 0 ? (
<Trans>View rules</Trans>
) : (
<Trans>Create rule</Trans>
)}
</Button>
<Button
variant="bare"
onPress={onDelete}
style={{
color: theme.errorText,
flex: 1,
borderRadius: 0,
}}
>
<Trans>Delete</Trans>
</Button>
</View>
)
}
{...props}
@@ -112,4 +135,4 @@ export const PayeesListItem = memo(function PayeeListItem({
</SpaceBetween>
</ActionableGridListItem>
);
});
}

View File

@@ -8,5 +8,7 @@ export { MobileRuleEditPage as RuleEdit } from '../mobile/rules/MobileRuleEditPa
export { CategoryPage as Category } from '../mobile/budget/CategoryPage';
export { MobilePayeesPage as Payees } from '../mobile/payees/MobilePayeesPage';
export { MobilePayeeEditPage as PayeeEdit } from '../mobile/payees/MobilePayeeEditPage';
export { MobileBankSyncPage as BankSync } from '../mobile/banksync/MobileBankSyncPage';
export { MobileBankSyncAccountEditPage as BankSyncAccountEdit } from '../mobile/banksync/MobileBankSyncAccountEditPage';

View File

@@ -10,6 +10,8 @@ export { Account } from '../accounts/Account';
export { ManageRulesPage as Rules } from '../ManageRulesPage';
export { ManageRulesPage as RuleEdit } from '../ManageRulesPage';
export { ManagePayeesPage as Payees } from '../payees/ManagePayeesPage';
export { ManagePayeesPage as PayeeEdit } from '../payees/ManagePayeesPage';
export { BankSync } from '../banksync';
export { UserDirectoryPage } from '../admin/UserDirectory/UserDirectoryPage';

View File

@@ -0,0 +1,6 @@
---
category: Enhancements
authors: [MatissJanis]
---
Mobile payees: add edit payee functionality