mirror of
https://github.com/actualbudget/actual.git
synced 2026-03-11 12:43:09 -05:00
Add edit payee functionality to mobile page (#5874)
This commit is contained in:
committed by
GitHub
parent
1e6fde571e
commit
3825e65693
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 6.3 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 6.4 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 6.4 KiB |
@@ -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" />}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
6
upcoming-release-notes/5874.md
Normal file
6
upcoming-release-notes/5874.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
category: Enhancements
|
||||
authors: [MatissJanis]
|
||||
---
|
||||
|
||||
Mobile payees: add edit payee functionality
|
||||
Reference in New Issue
Block a user