Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add option to make a transfer from two selected transactions #2398

Merged
merged 3 commits into from
Mar 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 49 additions & 2 deletions packages/desktop-client/e2e/accounts.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ test.describe('Accounts', () => {
let page;
let navigation;
let configurationPage;
let accountPage;

test.beforeAll(async ({ browser }) => {
page = await browser.newPage();
Expand All @@ -22,7 +23,7 @@ test.describe('Accounts', () => {
});

test('creates a new account and views the initial balance transaction', async () => {
const accountPage = await navigation.createAccount({
accountPage = await navigation.createAccount({
name: 'New Account',
offBudget: false,
balance: 100,
Expand All @@ -38,7 +39,7 @@ test.describe('Accounts', () => {
});

test('closes an account', async () => {
const accountPage = await navigation.goToAccountPage('Roth IRA');
accountPage = await navigation.goToAccountPage('Roth IRA');

await expect(accountPage.accountName).toHaveText('Roth IRA');

Expand All @@ -50,4 +51,50 @@ test.describe('Accounts', () => {
await expect(accountPage.accountName).toHaveText('Closed: Roth IRA');
await expect(page).toMatchThemeScreenshots();
});

test.describe('Budgeted Accounts', () => {
// Reset filters
test.afterEach(async () => {
await accountPage.removeFilter(0);
});

test('creates a transfer from two existing transactions', async () => {
accountPage = await navigation.goToAccountPage('For budget');
await expect(accountPage.accountName).toHaveText('Budgeted Accounts');

await accountPage.filterByNote('Test Acc Transfer');

await accountPage.createSingleTransaction({
account: 'Ally Savings',
payee: '',
notes: 'Test Acc Transfer',
category: 'Food',
debit: '34.56',
});

await accountPage.createSingleTransaction({
account: 'HSBC',
payee: '',
notes: 'Test Acc Transfer',
category: 'Food',
credit: '34.56',
});

await accountPage.selectNthTransaction(0);
await accountPage.selectNthTransaction(1);
await accountPage.clickSelectAction('Make transfer');

let transaction = accountPage.getNthTransaction(0);
await expect(transaction.payee).toHaveText('Ally Savings');
await expect(transaction.category).toHaveText('Transfer');
await expect(transaction.credit).toHaveText('34.56');
await expect(transaction.account).toHaveText('HSBC');

transaction = accountPage.getNthTransaction(1);
await expect(transaction.payee).toHaveText('HSBC');
await expect(transaction.category).toHaveText('Transfer');
await expect(transaction.debit).toHaveText('34.56');
await expect(transaction.account).toHaveText('Ally Savings');
});
});
});
30 changes: 30 additions & 0 deletions packages/desktop-client/e2e/page-models/account-page.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ export class AccountPage {

this.filterButton = this.page.getByRole('button', { name: 'Filter' });
this.filterSelectTooltip = this.page.getByTestId('filters-select-tooltip');

this.selectButton = this.page.getByTestId('transactions-select-button');
this.selectTooltip = this.page.getByTestId('transactions-select-tooltip');
}

/**
Expand Down Expand Up @@ -68,14 +71,21 @@ export class AccountPage {
await this.cancelTransactionButton.click();
}

async selectNthTransaction(index) {
const row = this.transactionTableRow.nth(index);
await row.getByTestId('select').click();
}

/**
* Retrieve the data for the nth-transaction.
* 0-based index
*/
getNthTransaction(index) {
const row = this.transactionTableRow.nth(index);
const account = row.getByTestId('account');

return {
...(account ? { account } : {}),
payee: row.getByTestId('payee'),
notes: row.getByTestId('notes'),
category: row.getByTestId('category'),
Expand All @@ -84,6 +94,11 @@ export class AccountPage {
};
}

async clickSelectAction(action) {
await this.selectButton.click();
await this.selectTooltip.getByRole('button', { name: action }).click();
}

/**
* Open the modal for closing the account.
*/
Expand All @@ -106,6 +121,15 @@ export class AccountPage {
return new FilterTooltip(this.page.getByTestId('filters-menu-tooltip'));
}

/**
* Filter to a specific note
*/
async filterByNote(note) {
const filterTooltip = await this.filterBy('Note');
await this.page.keyboard.type(note);
await filterTooltip.applyButton.click();
}

/**
* Remove the nth filter
*/
Expand All @@ -117,6 +141,12 @@ export class AccountPage {
}

async _fillTransactionFields(transactionRow, transaction) {
if (transaction.account) {
await transactionRow.getByTestId('account').click();
await this.page.keyboard.type(transaction.account);
await this.page.keyboard.press('Tab');
}

if (transaction.payee) {
await transactionRow.getByTestId('payee').click();
await this.page.keyboard.type(transaction.payee);
Expand Down
48 changes: 48 additions & 0 deletions packages/desktop-client/src/components/accounts/Account.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { Navigate, useParams, useLocation, useMatch } from 'react-router-dom';
import { debounce } from 'debounce';
import { bindActionCreators } from 'redux';

import { validForTransfer } from 'loot-core/client/transfer';
import * as actions from 'loot-core/src/client/actions';
import { useFilters } from 'loot-core/src/client/data-hooks/filters';
import {
Expand Down Expand Up @@ -1059,6 +1060,52 @@ class AccountInternal extends PureComponent {
this.props.pushModal('edit-rule', { rule });
};

onSetTransfer = async ids => {
const onConfirmTransfer = async ids => {
this.setState({ workingHard: true });

const payees = await this.props.getPayees();
const { data: transactions } = await runQuery(
q('transactions')
.filter({ id: { $oneof: ids } })
.select('*'),
);
const [fromTrans, toTrans] = transactions;

if (transactions.length === 2 && validForTransfer(fromTrans, toTrans)) {
const fromPayee = payees.find(
p => p.transfer_acct === fromTrans.account,
);
const toPayee = payees.find(p => p.transfer_acct === toTrans.account);

const changes = {
updated: [
{
...fromTrans,
payee: toPayee.id,
transfer_id: toTrans.id,
},
{
...toTrans,
payee: fromPayee.id,
transfer_id: fromTrans.id,
},
],
};

await send('transactions-batch-update', changes);
}

await this.refetchTransactions();
};

await this.checkForReconciledTransactions(
ids,
'batchEditWithReconciled',
onConfirmTransfer,
);
};

onCondOpChange = (value, filters) => {
this.setState({ conditionsOp: value });
this.setState({ filterId: { ...this.state.filterId, status: 'changed' } });
Expand Down Expand Up @@ -1443,6 +1490,7 @@ class AccountInternal extends PureComponent {
onDeleteFilter={this.onDeleteFilter}
onApplyFilter={this.onApplyFilter}
onScheduleAction={this.onScheduleAction}
onSetTransfer={this.onSetTransfer}
/>

<View style={{ flex: 1 }}>
Expand Down
6 changes: 6 additions & 0 deletions packages/desktop-client/src/components/accounts/Header.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ export function AccountHeader({
onCondOpChange,
onDeleteFilter,
onScheduleAction,
onSetTransfer,
}) {
const [menuOpen, setMenuOpen] = useState(false);
const searchInput = useRef(null);
Expand All @@ -94,6 +95,9 @@ export function AccountHeader({
canSync = !!accounts.find(account => !!account.account_id) && isUsingServer;
}

// Only show the ability to make linked transfers on multi-account views.
const showMakeTransfer = !account;

function onToggleSplits() {
if (tableRef.current) {
splitsExpanded.dispatch({
Expand Down Expand Up @@ -276,8 +280,10 @@ export function AccountHeader({
onEdit={onBatchEdit}
onUnlink={onBatchUnlink}
onCreateRule={onCreateRule}
onSetTransfer={onSetTransfer}
onScheduleAction={onScheduleAction}
pushModal={pushModal}
showMakeTransfer={showMakeTransfer}
/>
)}
<Button
Expand Down
2 changes: 2 additions & 0 deletions packages/desktop-client/src/components/table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -801,6 +801,7 @@ export function SelectedItemsButton({ name, keyHandlers, items, onSelect }) {
type="bare"
style={{ color: theme.pageTextPositive }}
onClick={() => setMenuOpen(true)}
data-testid={name + '-select-button'}
>
<SvgExpandArrow
width={8}
Expand All @@ -816,6 +817,7 @@ export function SelectedItemsButton({ name, keyHandlers, items, onSelect }) {
width={200}
style={{ padding: 0, backgroundColor: theme.menuBackground }}
onClose={() => setMenuOpen(false)}
data-testid={name + '-select-tooltip'}
>
<Menu
onMenuSelect={name => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import React, { useMemo } from 'react';

import { validForTransfer } from 'loot-core/src/client/transfer';

import { useSelectedItems } from '../../hooks/useSelected';
import { Menu } from '../common/Menu';
import { SelectedItemsButton } from '../table';
Expand All @@ -14,8 +16,10 @@ export function SelectedTransactionsButton({
onEdit,
onUnlink,
onCreateRule,
onSetTransfer,
onScheduleAction,
pushModal,
showMakeTransfer,
}) {
const selectedItems = useSelectedItems();

Expand Down Expand Up @@ -43,6 +47,23 @@ export function SelectedTransactionsButton({
);
}, [types.preview, selectedItems, getTransaction]);

const canBeTransfer = useMemo(() => {
// only two selected
if (selectedItems.size !== 2) {
return false;
}
const transactions = [...selectedItems];
const fromTrans = getTransaction(transactions[0]);
const toTrans = getTransaction(transactions[1]);

// previously selected transactions aren't always present in current transaction list
if (!fromTrans || !toTrans) {
return false;
}

return validForTransfer(fromTrans, toTrans);
}, [selectedItems, getTransaction]);

return (
<SelectedItemsButton
name="transactions"
Expand Down Expand Up @@ -91,6 +112,15 @@ export function SelectedTransactionsButton({
text: 'Create rule',
},
]),
...(showMakeTransfer
? [
{
name: 'set-transfer',
text: 'Make transfer',
disabled: !canBeTransfer,
},
]
: []),
Menu.line,
{ type: Menu.label, name: 'Edit field' },
{ name: 'date', text: 'Date' },
Expand Down Expand Up @@ -145,6 +175,9 @@ export function SelectedTransactionsButton({
case 'create-rule':
onCreateRule([...selectedItems]);
break;
case 'set-transfer':
onSetTransfer([...selectedItems]);
break;
default:
onEdit(name, [...selectedItems]);
}
Expand Down
Loading
Loading