diff --git a/packages/desktop-client/e2e/page-models/mobile-payees-page.ts b/packages/desktop-client/e2e/page-models/mobile-payees-page.ts index 8e7bf30a2f..6f06bdb5c3 100644 --- a/packages/desktop-client/e2e/page-models/mobile-payees-page.ts +++ b/packages/desktop-client/e2e/page-models/mobile-payees-page.ts @@ -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); diff --git a/packages/desktop-client/e2e/payees.mobile.test.ts b/packages/desktop-client/e2e/payees.mobile.test.ts index 07d4f45245..4c22818d55 100644 --- a/packages/desktop-client/e2e/payees.mobile.test.ts +++ b/packages/desktop-client/e2e/payees.mobile.test.ts @@ -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(); }); diff --git a/packages/desktop-client/e2e/payees.mobile.test.ts-snapshots/Mobile-Payees-clicking-on-a-payee-opens-payee-edit-page-1-chromium-linux.png b/packages/desktop-client/e2e/payees.mobile.test.ts-snapshots/Mobile-Payees-clicking-on-a-payee-opens-payee-edit-page-1-chromium-linux.png new file mode 100644 index 0000000000..1aa091f00d Binary files /dev/null and b/packages/desktop-client/e2e/payees.mobile.test.ts-snapshots/Mobile-Payees-clicking-on-a-payee-opens-payee-edit-page-1-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/payees.mobile.test.ts-snapshots/Mobile-Payees-clicking-on-a-payee-opens-payee-edit-page-2-chromium-linux.png b/packages/desktop-client/e2e/payees.mobile.test.ts-snapshots/Mobile-Payees-clicking-on-a-payee-opens-payee-edit-page-2-chromium-linux.png new file mode 100644 index 0000000000..62b779c501 Binary files /dev/null and b/packages/desktop-client/e2e/payees.mobile.test.ts-snapshots/Mobile-Payees-clicking-on-a-payee-opens-payee-edit-page-2-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/payees.mobile.test.ts-snapshots/Mobile-Payees-clicking-on-a-payee-opens-payee-edit-page-3-chromium-linux.png b/packages/desktop-client/e2e/payees.mobile.test.ts-snapshots/Mobile-Payees-clicking-on-a-payee-opens-payee-edit-page-3-chromium-linux.png new file mode 100644 index 0000000000..f42ba3cf9e Binary files /dev/null and b/packages/desktop-client/e2e/payees.mobile.test.ts-snapshots/Mobile-Payees-clicking-on-a-payee-opens-payee-edit-page-3-chromium-linux.png differ diff --git a/packages/desktop-client/src/components/FinancesApp.tsx b/packages/desktop-client/src/components/FinancesApp.tsx index 33db725208..547887baea 100644 --- a/packages/desktop-client/src/components/FinancesApp.tsx +++ b/packages/desktop-client/src/components/FinancesApp.tsx @@ -270,6 +270,14 @@ export function FinancesApp() { path="/payees" element={} /> + + + + } + /> } diff --git a/packages/desktop-client/src/components/mobile/ActionableGridListItem.tsx b/packages/desktop-client/src/components/mobile/ActionableGridListItem.tsx index e240a37d5e..11ad3e112b 100644 --- a/packages/desktop-client/src/components/mobile/ActionableGridListItem.tsx +++ b/packages/desktop-client/src/components/mobile/ActionableGridListItem.tsx @@ -9,7 +9,7 @@ import { useDrag } from '@use-gesture/react'; import { type WithRequired } from 'loot-core/types/util'; type ActionableGridListItemProps = { - actions?: ReactNode; + actions?: ReactNode | ((params: { close: () => void }) => ReactNode); actionsBackgroundColor?: string; actionsWidth?: number; children?: ReactNode; @@ -131,7 +131,18 @@ export function ActionableGridListItem({ minWidth: actionsWidth, }} > - {actions} + {typeof actions === 'function' + ? actions({ + close: () => { + api.start({ + x: 0, + onRest: () => { + setIsRevealed(false); + }, + }); + }, + }) + : actions} )} diff --git a/packages/desktop-client/src/components/mobile/payees/MobilePayeeEditPage.tsx b/packages/desktop-client/src/components/mobile/payees/MobilePayeeEditPage.tsx new file mode 100644 index 0000000000..3dd06f24f8 --- /dev/null +++ b/packages/desktop-client/src/components/mobile/payees/MobilePayeeEditPage.tsx @@ -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(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 ( + } + /> + } + padding={0} + > + + + Loading payee... + + + + ); + } + + return ( + } + /> + } + footer={ + + + + } + > + + + + + ); +} diff --git a/packages/desktop-client/src/components/mobile/payees/MobilePayeesPage.tsx b/packages/desktop-client/src/components/mobile/payees/MobilePayeesPage.tsx index a2b441f209..df1fed0bbd 100644 --- a/packages/desktop-client/src/components/mobile/payees/MobilePayeesPage.tsx +++ b/packages/desktop-client/src/components/mobile/payees/MobilePayeesPage.tsx @@ -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} /> ); diff --git a/packages/desktop-client/src/components/mobile/payees/PayeesList.tsx b/packages/desktop-client/src/components/mobile/payees/PayeesList.tsx index 14ebe68d78..bd48a02636 100644 --- a/packages/desktop-client/src/components/mobile/payees/PayeesList.tsx +++ b/packages/desktop-client/src/components/mobile/payees/PayeesList.tsx @@ -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)} /> )} diff --git a/packages/desktop-client/src/components/mobile/payees/PayeesListItem.tsx b/packages/desktop-client/src/components/mobile/payees/PayeesListItem.tsx index 2c3ceb6fe2..938cf73f82 100644 --- a/packages/desktop-client/src/components/mobile/payees/PayeesListItem.tsx +++ b/packages/desktop-client/src/components/mobile/payees/PayeesListItem.tsx @@ -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, '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 && ( - + + + + ) } {...props} @@ -112,4 +135,4 @@ export const PayeesListItem = memo(function PayeeListItem({ ); -}); +} diff --git a/packages/desktop-client/src/components/responsive/narrow.ts b/packages/desktop-client/src/components/responsive/narrow.ts index 14f722f2f6..99f0e2a2d9 100644 --- a/packages/desktop-client/src/components/responsive/narrow.ts +++ b/packages/desktop-client/src/components/responsive/narrow.ts @@ -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'; diff --git a/packages/desktop-client/src/components/responsive/wide.ts b/packages/desktop-client/src/components/responsive/wide.ts index d9b8d59f24..2dcd844bb9 100644 --- a/packages/desktop-client/src/components/responsive/wide.ts +++ b/packages/desktop-client/src/components/responsive/wide.ts @@ -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'; diff --git a/upcoming-release-notes/5874.md b/upcoming-release-notes/5874.md new file mode 100644 index 0000000000..2a27238b09 --- /dev/null +++ b/upcoming-release-notes/5874.md @@ -0,0 +1,6 @@ +--- +category: Enhancements +authors: [MatissJanis] +--- + +Mobile payees: add edit payee functionality