Compare commits

..

28 Commits

Author SHA1 Message Date
Leandro Menezes
83cd364c5f Changed to remove sessionStorage 2025-06-15 13:56:29 -03:00
Crazypkr1099
b61b1758d6 Merge branch 'master' into scrollToLocationBudget 2024-06-20 21:53:37 -04:00
Crazypkr
dbc434c84e Renamed scrollToPosition to setScrollPosition 2024-06-20 21:50:53 -04:00
Crazypkr
b1ea639e11 Merge branch 'scrollToLocationBudget' of https://github.com/Crazypkr1099/actual into scrollToLocationBudget 2024-06-20 21:47:07 -04:00
Crazypkr
bc6098fbb3 Remove intersectionboundary 2024-06-20 21:47:04 -04:00
Crazypkr1099
15869eca61 Merge branch 'master' into scrollToLocationBudget 2024-06-19 21:59:42 -04:00
Crazypkr1099
c5c098ea0c Merge branch 'master' into scrollToLocationBudget 2024-06-18 18:38:02 -04:00
Crazypkr1099
f6b88cc1ba Merge branch 'master' into scrollToLocationBudget 2024-06-18 16:03:47 -04:00
Crazypkr1099
ca1d067921 Merge branch 'master' into scrollToLocationBudget 2024-06-18 13:55:14 -04:00
Crazypkr1099
18e55800e4 Merge branch 'master' into scrollToLocationBudget 2024-06-18 12:36:52 -04:00
Crazypkr1099
18314acd25 Merge branch 'master' into scrollToLocationBudget 2024-06-14 18:44:32 -04:00
Crazypkr1099
042058ec7b Merge branch 'scrollToLocationBudget' of https://github.com/Crazypkr1099/actual into scrollToLocationBudget 2024-06-12 22:43:01 -04:00
Crazypkr1099
9580be7bc4 Mobile Support 2024-06-12 22:42:58 -04:00
Crazypkr1099
569b995278 Update 2859.md 2024-06-12 19:12:39 -04:00
Crazypkr1099
9590a93e9f Merge branch 'master' into scrollToLocationBudget 2024-06-12 19:12:01 -04:00
Crazypkr1099
c20ebd9dbd Added rollover and reset when leaving budget page 2024-06-12 19:10:50 -04:00
Crazypkr
112f066b8b Fix typescript mistake and changed to sessionStorage 2024-06-09 07:53:45 -04:00
Crazypkr
cf6825a541 bug fixes 2024-06-08 12:27:37 -04:00
Crazypkr
9ec0bdec33 fixing more issues 2024-06-08 12:10:31 -04:00
Crazypkr
4c57596117 linting fix 2024-06-08 12:04:26 -04:00
Crazypkr1099
9e7ebb405f Merge branch 'master' into scrollToLocationBudget 2024-06-08 11:57:25 -04:00
Crazypkr
7ba3a37ead Merge branch 'scrollToLocationBudget' of https://github.com/Crazypkr1099/actual into scrollToLocationBudget 2024-06-08 11:55:13 -04:00
Crazypkr
201e1dab54 Revert "Delete packages/desktop-client/e2e directory"
This reverts commit 2476e45735.
2024-06-08 11:55:09 -04:00
Crazypkr
ab124105c2 Revert "Delete packages/desktop-client/e2e directory"
This reverts commit bbec585305.
2024-06-08 11:53:54 -04:00
Crazypkr1099
129b2c3061 Create 2859.md 2024-06-08 11:39:30 -04:00
Crazypkr
9d6b574708 Add ability to scroll to current position in table when leaving budgetable 2024-06-08 11:34:57 -04:00
Crazypkr1099
bbec585305 Delete packages/desktop-client/e2e directory 2024-06-06 23:51:10 -04:00
Crazypkr1099
2476e45735 Delete packages/desktop-client/e2e directory 2024-06-06 23:45:55 -04:00
216 changed files with 2555 additions and 3610 deletions

View File

@@ -21,6 +21,8 @@ on:
- edited
- review_requested
- review_request_removed
pull_request_review:
types: [submitted, edited, dismissed]
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}

1
.gitignore vendored
View File

@@ -21,7 +21,6 @@ packages/api/dist
packages/api/@types
packages/crdt/dist
packages/desktop-electron/client-build
packages/desktop-electron/build
packages/desktop-electron/.electron-symbols
packages/desktop-electron/dist
packages/desktop-electron/loot-core

View File

@@ -1,6 +1,6 @@
{
"name": "@actual-app/api",
"version": "6.8.2",
"version": "6.8.1",
"license": "MIT",
"description": "An API for Actual",
"engines": {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 32 KiB

View File

@@ -44,7 +44,7 @@ export class RulesPage {
.first()
.click();
await this.page
.getByRole('button', { exact: true, name: data.conditionsOp })
.getByRole('option', { exact: true, name: data.conditionsOp })
.click();
}
@@ -97,13 +97,13 @@ export class RulesPage {
if (field) {
await row.getByRole('button').first().click();
await this.page
.getByRole('button', { exact: true, name: field })
.getByRole('option', { exact: true, name: field })
.click();
}
if (op) {
await row.getByRole('button', { name: 'is' }).click();
await this.page.getByRole('button', { name: op, exact: true }).click();
await this.page.getByRole('option', { name: op, exact: true }).click();
}
if (value) {

View File

@@ -84,10 +84,6 @@ export class SchedulesPage {
if (data.amount) {
await this.page.getByLabel('Amount').fill(String(data.amount));
// For some readon, the input field does not trigger the change event on tests
// but it works on the browser. We can revisit this once migration to
// react aria components is complete.
await this.page.keyboard.press('Enter');
}
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 81 KiB

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 81 KiB

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 79 KiB

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

After

Width:  |  Height:  |  Size: 77 KiB

View File

@@ -89,7 +89,7 @@ test.describe('Rules', () => {
splitActions: [
[
{
field: 'a fixed percent of the remainder',
field: 'a fixed percent',
value: '90',
},
{
@@ -120,7 +120,7 @@ test.describe('Rules', () => {
});
const transaction = accountPage.getNthTransaction(0);
await expect(transaction.payee).toHaveText('Split');
await expect(transaction.payee).toHaveText('Ikea');
await expect(transaction.notes).toHaveText('food / entertainment');
await expect(transaction.category).toHaveText('Split');
await expect(transaction.debit).toHaveText('100.00');

Binary file not shown.

Before

Width:  |  Height:  |  Size: 83 KiB

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 83 KiB

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 82 KiB

After

Width:  |  Height:  |  Size: 83 KiB

View File

@@ -120,7 +120,7 @@ test.describe('Transactions', () => {
]);
const firstTransaction = accountPage.getNthTransaction(0);
await expect(firstTransaction.payee).toHaveText('Split');
await expect(firstTransaction.payee).toHaveText('Krogger');
await expect(firstTransaction.notes).toHaveText('Notes');
await expect(firstTransaction.category).toHaveText('Split');
await expect(firstTransaction.debit).toHaveText('333.33');

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 108 KiB

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 108 KiB

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 108 KiB

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.1 KiB

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.3 KiB

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 58 KiB

Submodule packages/desktop-client/locale added at a0e122296a

View File

@@ -1,6 +1,6 @@
{
"name": "@actual-app/web",
"version": "24.7.0",
"version": "24.6.0",
"license": "MIT",
"files": [
"build"
@@ -8,6 +8,7 @@
"devDependencies": {
"@juggle/resize-observer": "^3.4.0",
"@playwright/test": "1.41.1",
"@reach/listbox": "^0.18.0",
"@react-aria/focus": "^3.16.0",
"@react-aria/listbox": "^3.11.3",
"@react-aria/utils": "^3.23.0",
@@ -48,7 +49,7 @@
"pikaday": "1.8.2",
"promise-retry": "^2.0.1",
"react": "18.2.0",
"react-aria-components": "^1.2.1",
"react-aria-components": "^1.1.1",
"react-dnd": "^16.0.1",
"react-dnd-html5-backend": "^16.0.1",
"react-dom": "18.2.0",

View File

@@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
<path d="M1.857 7.612H.98a.98.98 0 0 0-.98.98v2.224c0 .449.184.857.51 1.163.612.551 1.816 1.49 3.735 2.368a.571.571 0 0 1 .306.346l.939 3.286a.484.484 0 0 0 .469.347h1.47a.479.479 0 0 0 .469-.367l.47-1.817a.262.262 0 0 1 .326-.183c.571.122 1.183.163 1.795.163.613 0 1.225-.061 1.796-.163.143-.02.286.06.327.183l.47 1.817a.502.502 0 0 0 .468.367h1.47a.484.484 0 0 0 .47-.347l1.224-4.245a.487.487 0 0 1 .122-.224c.919-.98 1.53-2.163 1.735-3.47h.49c.53 0 .959-.448.938-.979-.02-.51-.47-.918-.98-.918h-.448C18.06 4.673 14.632 2 10.489 2c-1.775 0-3.428.49-4.755 1.326-.591-.408-1.469-.734-2.693-.632-.49.04-.715.612-.388.959.408.429.796 1 .877 1.735L1.857 7.612Zm3.122.98a.862.862 0 0 1-.857-.858c0-.469.388-.857.857-.857.47 0 .858.388.858.857 0 .47-.388.858-.858.858Z" />
</svg>

Before

Width:  |  Height:  |  Size: 838 B

View File

@@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
<path d="M19 18h-1v-8c0-.6-.4-1-1-1s-1 .4-1 1v8h-3V1c0-.6-.4-1-1-1s-1 .4-1 1v17H8V7c0-.6-.4-1-1-1s-1 .4-1 1v11H3V3c0-.6-.4-1-1-1s-1 .4-1 1v15c-.6 0-1 .4-1 1s.4 1 1 1h18c.6 0 1-.4 1-1s-.4-1-1-1z" />
</svg>

Before

Width:  |  Height:  |  Size: 268 B

View File

@@ -1,4 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M23 11.5a1.5 1.5 0 0 1-1.5 1.5h-20a1.5 1.5 0 0 1 0-3h20a1.5 1.5 0 0 1 1.5 1.5Z" />
<path d="M11.5 23a1.5 1.5 0 0 1-1.5-1.5v-20a1.5 1.5 0 0 1 3 0v20a1.5 1.5 0 0 1-1.5 1.5Z" />
</svg>

Before

Width:  |  Height:  |  Size: 256 B

View File

@@ -28,44 +28,6 @@
"purpose": "maskable"
}
],
"shortcuts": [
{
"name": "Add Transaction",
"short_name": "Add Transaction",
"description": "Add a new transaction",
"url": "/transactions/new",
"icons": [
{
"src": "/shortcut-transaction.svg",
"sizes": "150x150"
}
]
},
{
"name": "Accounts",
"short_name": "Accounts",
"description": "View all accounts",
"url": "/accounts",
"icons": [
{
"src": "/shortcut-accounts.svg",
"sizes": "150x150"
}
]
},
{
"name": "Reports",
"short_name": "Reports",
"description": "View reports",
"url": "/reports",
"icons": [
{
"src": "/shortcut-reports.svg",
"sizes": "150x150"
}
]
}
],
"screenshots": [
{
"src": "/screenshot_wide.png",
@@ -81,7 +43,7 @@
"type": "image/png",
"sizes": "350x600"
}
],
],
"theme_color": "#8812E1",
"background_color": "#ffffff",
"display": "standalone",

View File

@@ -173,7 +173,6 @@ export function Modals() {
<ConfirmTransactionEdit
key={name}
modalProps={modalProps}
onCancel={options.onCancel}
onConfirm={options.onConfirm}
confirmReason={options.confirmReason}
/>
@@ -402,6 +401,7 @@ export function Modals() {
actions={actions}
transactionIds={options?.transactionIds}
getTransaction={options?.getTransaction}
pushModal={options?.pushModal}
/>
);

View File

@@ -1,11 +1,12 @@
import React, { PureComponent, createRef, useMemo } from 'react';
import { useSelector } from 'react-redux';
import { Navigate, useParams, useLocation } from 'react-router-dom';
import { useSelector, useDispatch } from 'react-redux';
import { Navigate, useParams, useLocation, useMatch } from 'react-router-dom';
import { debounce } from 'debounce';
import { v4 as uuidv4 } from 'uuid';
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 { SchedulesProvider } from 'loot-core/src/client/data-hooks/schedules';
import * as queries from 'loot-core/src/client/queries';
@@ -21,13 +22,10 @@ import {
realizeTempTransactions,
ungroupTransaction,
ungroupTransactions,
makeChild,
makeAsNonChildTransactions,
} from 'loot-core/src/shared/transactions';
import { applyChanges, groupById } from 'loot-core/src/shared/util';
import { useAccounts } from '../../hooks/useAccounts';
import { useActions } from '../../hooks/useActions';
import { useCategories } from '../../hooks/useCategories';
import { useDateFormat } from '../../hooks/useDateFormat';
import { useFailedAccounts } from '../../hooks/useFailedAccounts';
@@ -40,7 +38,7 @@ import {
useSplitsExpanded,
} from '../../hooks/useSplitsExpanded';
import { styles, theme } from '../../style';
import { Button } from '../common/Button2';
import { Button } from '../common/Button';
import { Text } from '../common/Text';
import { View } from '../common/View';
import { TransactionList } from '../transactions/TransactionList';
@@ -73,7 +71,7 @@ function EmptyMessage({ onAdd }) {
manage it locally yourself.
</Text>
<Button variant="primary" style={{ marginTop: 20 }} onPress={onAdd}>
<Button type="primary" style={{ marginTop: 20 }} onClick={onAdd}>
Add account
</Button>
@@ -181,9 +179,7 @@ class AccountInternal extends PureComponent {
this.state = {
search: '',
filterConditions: props.filterConditions || [],
filterId: [],
filterConditionsOp: 'and',
filters: props.conditions || [],
loading: true,
workingHard: false,
reconcileAmount: null,
@@ -196,8 +192,9 @@ class AccountInternal extends PureComponent {
editingName: false,
isAdding: false,
latestDate: null,
filterId: [],
conditionsOp: 'and',
sort: [],
filteredAmount: null,
};
}
@@ -259,7 +256,7 @@ class AccountInternal extends PureComponent {
// Important that any async work happens last so that the
// listeners are set up synchronously
await this.props.initiallyLoadPayees();
await this.fetchTransactions(this.state.filterConditions);
await this.fetchTransactions(this.state.filters);
// If there is a pending undo, apply it immediately (this happens
// when an undo changes the location to this page)
@@ -288,7 +285,7 @@ class AccountInternal extends PureComponent {
//Resest sort/filter/search on account change
if (this.props.accountId !== prevProps.accountId) {
this.setState({ sort: [], search: '', filterConditions: [] });
this.setState({ sort: [], search: '', filters: [] });
}
}
@@ -316,10 +313,10 @@ class AccountInternal extends PureComponent {
this.paged?.run();
};
fetchTransactions = filterConditions => {
fetchTransactions = filters => {
const query = this.makeRootQuery();
this.rootQuery = this.currentQuery = query;
if (filterConditions) this.applyFilters(filterConditions);
if (filters) this.applyFilters(filters);
else this.updateQuery(query);
if (this.props.accountId) {
@@ -374,7 +371,6 @@ class AccountInternal extends PureComponent {
balances: this.state.showBalances
? await this.calculateBalances()
: null,
filteredAmount: await this.getFilteredAmount(),
},
() => {
if (firstLoad) {
@@ -422,10 +418,7 @@ class AccountInternal extends PureComponent {
onSearchDone = debounce(() => {
if (this.state.search === '') {
this.updateQuery(
this.currentQuery,
this.state.filterConditions.length > 0,
);
this.updateQuery(this.currentQuery, this.state.filters.length > 0);
} else {
this.updateQuery(
queries.makeTransactionSearchQuery(
@@ -518,7 +511,7 @@ class AccountInternal extends PureComponent {
return (
account &&
this.state.search === '' &&
this.state.filterConditions.length === 0 &&
this.state.filters.length === 0 &&
(this.state.sort.length === 0 ||
(this.state.sort.field === 'date' &&
this.state.sort.ascDesc === 'desc'))
@@ -606,7 +599,7 @@ class AccountInternal extends PureComponent {
{
transactions: [],
transactionCount: 0,
filterConditions: [],
filters: [],
search: '',
sort: [],
showBalances: true,
@@ -619,9 +612,9 @@ class AccountInternal extends PureComponent {
break;
case 'remove-sorting': {
this.setState({ sort: [] }, () => {
const filterConditions = this.state.filterConditions;
if (filterConditions.length > 0) {
this.applyFilters([...filterConditions]);
const filters = this.state.filters;
if (filters.length > 0) {
this.applyFilters([...filters]);
} else {
this.fetchTransactions();
}
@@ -644,12 +637,12 @@ class AccountInternal extends PureComponent {
if (this.state.showReconciled) {
this.props.savePrefs({ ['hide-reconciled-' + accountId]: true });
this.setState({ showReconciled: false }, () =>
this.fetchTransactions(this.state.filterConditions),
this.fetchTransactions(this.state.filters),
);
} else {
this.props.savePrefs({ ['hide-reconciled-' + accountId]: false });
this.setState({ showReconciled: true }, () =>
this.fetchTransactions(this.state.filterConditions),
this.fetchTransactions(this.state.filters),
);
}
break;
@@ -687,11 +680,24 @@ class AccountInternal extends PureComponent {
};
}
getFilteredAmount = async () => {
const { data: amount } = await runQuery(
this.paged.getQuery().calculate({ $sum: '$amount' }),
getFilteredAmount = async (filters, conditionsOpKey) => {
const filter = queries.getAccountFilter(this.props.accountId);
let query = q('transactions').filter({
[conditionsOpKey]: [...filters],
});
if (filter) {
query = query.filter(filter);
}
const filteredQuery = await runQuery(
query.select([{ amount: { $sum: '$amount' } }]),
);
return amount;
const filteredAmount = filteredQuery.data.reduce(
(a, v) => (a = a + v.amount),
0,
);
return filteredAmount;
};
isNew = id => {
@@ -811,7 +817,7 @@ class AccountInternal extends PureComponent {
onShowTransactions = async ids => {
this.onApplyFilter({
customName: 'Selected transactions',
queryFilter: { id: { $oneof: ids } },
filter: { id: { $oneof: ids } },
});
};
@@ -1052,114 +1058,6 @@ class AccountInternal extends PureComponent {
);
};
onMakeAsSplitTransaction = async ids => {
this.setState({ workingHard: true });
const { data: transactions } = await runQuery(
q('transactions')
.filter({ id: { $oneof: ids } })
.select('*')
.options({ splits: 'none' }),
);
if (!transactions || transactions.length === 0) {
return;
}
const [firstTransaction] = transactions;
const parentTransaction = {
id: uuidv4(),
is_parent: true,
cleared: transactions.every(t => !!t.cleared),
date: firstTransaction.date,
account: firstTransaction.account,
amount: transactions
.map(t => t.amount)
.reduce((total, amount) => total + amount, 0),
};
const childTransactions = transactions.map(t =>
makeChild(parentTransaction, t),
);
await send('transactions-batch-update', {
added: [parentTransaction],
updated: childTransactions,
});
this.refetchTransactions();
};
onMakeAsNonSplitTransactions = async ids => {
this.setState({ workingHard: true });
const { data: groupedTransactions } = await runQuery(
q('transactions')
.filter({ id: { $oneof: ids } })
.select('*')
.options({ splits: 'grouped' }),
);
let changes = {
updated: [],
deleted: [],
};
const groupedTransactionsToUpdate = groupedTransactions.filter(
t => t.is_parent,
);
for (const groupedTransaction of groupedTransactionsToUpdate) {
const transactions = ungroupTransaction(groupedTransaction);
const [parentTransaction, ...childTransactions] = transactions;
if (ids.includes(parentTransaction.id)) {
// Unsplit all child transactions.
const diff = makeAsNonChildTransactions(
childTransactions,
transactions,
);
changes = {
updated: [...changes.updated, ...diff.updated],
deleted: [...changes.deleted, ...diff.deleted],
};
// Already processed the child transactions above, no need to process them below.
continue;
}
// Unsplit selected child transactions.
const selectedChildTransactions = childTransactions.filter(t =>
ids.includes(t.id),
);
if (selectedChildTransactions.length === 0) {
continue;
}
const diff = makeAsNonChildTransactions(
selectedChildTransactions,
transactions,
);
changes = {
updated: [...changes.updated, ...diff.updated],
deleted: [...changes.deleted, ...diff.deleted],
};
}
await send('transactions-batch-update', changes);
this.refetchTransactions();
const transactionsToSelect = changes.updated.map(t => t.id);
this.dispatchSelected({
type: 'select-all',
ids: transactionsToSelect,
});
};
checkForReconciledTransactions = async (ids, confirmReason, onConfirm) => {
const { data } = await runQuery(
q('transactions')
@@ -1311,10 +1209,10 @@ class AccountInternal extends PureComponent {
);
};
onConditionsOpChange = (value, conditions) => {
this.setState({ filterConditionsOp: value });
onCondOpChange = (value, filters) => {
this.setState({ conditionsOp: value });
this.setState({ filterId: { ...this.state.filterId, status: 'changed' } });
this.applyFilters([...conditions]);
this.applyFilters([...filters]);
if (this.state.search !== '') {
this.onSearch(this.state.search);
}
@@ -1322,14 +1220,14 @@ class AccountInternal extends PureComponent {
onReloadSavedFilter = (savedFilter, item) => {
if (item === 'reload') {
const [savedFilter] = this.props.savedFilters.filter(
const [getFilter] = this.props.filtersList.filter(
f => f.id === this.state.filterId.id,
);
this.setState({ filterConditionsOp: savedFilter.conditionsOp });
this.applyFilters([...savedFilter.conditions]);
this.setState({ conditionsOp: getFilter.conditionsOp });
this.applyFilters([...getFilter.conditions]);
} else {
if (savedFilter.status) {
this.setState({ filterConditionsOp: savedFilter.conditionsOp });
this.setState({ conditionsOp: savedFilter.conditionsOp });
this.applyFilters([...savedFilter.conditions]);
}
}
@@ -1337,7 +1235,7 @@ class AccountInternal extends PureComponent {
};
onClearFilters = () => {
this.setState({ filterConditionsOp: 'and' });
this.setState({ conditionsOp: 'and' });
this.setState({ filterId: [] });
this.applyFilters([]);
if (this.state.search !== '') {
@@ -1345,11 +1243,9 @@ class AccountInternal extends PureComponent {
}
};
onUpdateFilter = (oldCondition, updatedCondition) => {
onUpdateFilter = (oldFilter, updatedFilter) => {
this.applyFilters(
this.state.filterConditions.map(c =>
c === oldCondition ? updatedCondition : c,
),
this.state.filters.map(f => (f === oldFilter ? updatedFilter : f)),
);
this.setState({
filterId: {
@@ -1362,11 +1258,11 @@ class AccountInternal extends PureComponent {
}
};
onDeleteFilter = condition => {
this.applyFilters(this.state.filterConditions.filter(c => c !== condition));
if (this.state.filterConditions.length === 1) {
onDeleteFilter = filter => {
this.applyFilters(this.state.filters.filter(f => f !== filter));
if (this.state.filters.length === 1) {
this.setState({ filterId: [] });
this.setState({ filterConditionsOp: 'and' });
this.setState({ conditionsOp: 'and' });
} else {
this.setState({
filterId: {
@@ -1380,31 +1276,23 @@ class AccountInternal extends PureComponent {
}
};
onApplyFilter = async conditionOrSavedFilter => {
let filterConditions = this.state.filterConditions;
if (conditionOrSavedFilter.customName) {
filterConditions = filterConditions.filter(
c => c.customName !== conditionOrSavedFilter.customName,
);
onApplyFilter = async cond => {
let filters = this.state.filters;
if (cond.customName) {
filters = filters.filter(f => f.customName !== cond.customName);
}
if (conditionOrSavedFilter.conditions) {
// A saved filter was passed in.
const savedFilter = conditionOrSavedFilter;
this.setState({
filterId: { ...savedFilter, status: 'saved' },
});
this.setState({ filterConditionsOp: savedFilter.conditionsOp });
this.applyFilters([...savedFilter.conditions]);
if (cond.conditions) {
this.setState({ filterId: { ...cond, status: 'saved' } });
this.setState({ conditionsOp: cond.conditionsOp });
this.applyFilters([...cond.conditions]);
} else {
// A condition was passed in.
const condition = conditionOrSavedFilter;
this.setState({
filterId: {
...this.state.filterId,
status: this.state.filterId && 'changed',
},
});
this.applyFilters([...filterConditions, condition]);
this.applyFilters([...filters, cond]);
}
if (this.state.search !== '') {
this.onSearch(this.state.search);
@@ -1432,35 +1320,30 @@ class AccountInternal extends PureComponent {
applyFilters = async conditions => {
if (conditions.length > 0) {
const customQueryFilters = conditions
const customFilters = conditions
.filter(cond => !!cond.customName)
.map(f => f.queryFilter);
const { filters: queryFilters } = await send(
'make-filters-from-conditions',
{
conditions: conditions.filter(cond => !cond.customName),
},
.map(f => f.filter);
const { filters } = await send('make-filters-from-conditions', {
conditions: conditions.filter(cond => !cond.customName),
});
const conditionsOpKey = this.state.conditionsOp === 'or' ? '$or' : '$and';
this.filteredAmount = await this.getFilteredAmount(
filters,
conditionsOpKey,
);
const conditionsOpKey =
this.state.filterConditionsOp === 'or' ? '$or' : '$and';
this.currentQuery = this.rootQuery.filter({
[conditionsOpKey]: [...queryFilters, ...customQueryFilters],
[conditionsOpKey]: [...filters, ...customFilters],
});
this.setState(
{
filterConditions: conditions,
},
() => {
this.updateQuery(this.currentQuery, true);
},
);
this.setState({ filters: conditions }, () => {
this.updateQuery(this.currentQuery, true);
});
} else {
this.setState(
{
transactions: [],
transactionCount: 0,
filterConditions: conditions,
filters: conditions,
},
() => {
this.fetchTransactions();
@@ -1474,8 +1357,8 @@ class AccountInternal extends PureComponent {
};
applySort = (field, ascDesc, prevField, prevAscDesc) => {
const filterConditions = this.state.filterConditions;
const isFiltered = filterConditions.length > 0;
const filters = this.state.filters;
const isFiltered = filters.length > 0;
const sortField = getField(!field ? this.state.sort.field : field);
const sortAscDesc = !ascDesc ? this.state.sort.ascDesc : ascDesc;
const sortPrevField = getField(
@@ -1542,7 +1425,7 @@ class AccountInternal extends PureComponent {
// called directly from UI by sorting a column.
// active filters need to be applied before sorting
case isFiltered:
this.applyFilters([...filterConditions]);
this.applyFilters([...filters]);
sortCurrentQuery(this, sortField, sortAscDesc);
break;
@@ -1604,6 +1487,7 @@ class AccountInternal extends PureComponent {
addNotification,
accountsSyncing,
failedAccounts,
pushModal,
replaceModal,
showExtraBalances,
accountId,
@@ -1621,7 +1505,6 @@ class AccountInternal extends PureComponent {
balances,
showCleared,
showReconciled,
filteredAmount,
} = this.state;
const account = accounts.find(account => account.id === accountId);
@@ -1665,13 +1548,14 @@ class AccountInternal extends PureComponent {
>
<View style={styles.page}>
<AccountHeader
filteredAmount={this.filteredAmount}
tableRef={this.table}
editingName={editingName}
isNameEditable={isNameEditable}
workingHard={workingHard}
account={account}
filterId={filterId}
savedFilters={this.props.savedFilters}
filtersList={this.props.filtersList}
location={this.props.location}
accountName={accountName}
accountsSyncing={accountsSyncing}
@@ -1685,13 +1569,11 @@ class AccountInternal extends PureComponent {
showEmptyMessage={showEmptyMessage}
balanceQuery={balanceQuery}
canCalculateBalance={this.canCalculateBalance}
filteredAmount={filteredAmount}
isFiltered={transactionsFiltered}
isSorted={this.state.sort.length !== 0}
reconcileAmount={reconcileAmount}
search={this.state.search}
filterConditions={this.state.filterConditions}
filterConditionsOp={this.state.filterConditionsOp}
filters={this.state.filters}
conditionsOp={this.state.conditionsOp}
savePrefs={this.props.savePrefs}
pushModal={this.props.pushModal}
onSearch={this.onSearch}
@@ -1716,13 +1598,11 @@ class AccountInternal extends PureComponent {
onUpdateFilter={this.onUpdateFilter}
onClearFilters={this.onClearFilters}
onReloadSavedFilter={this.onReloadSavedFilter}
onConditionsOpChange={this.onConditionsOpChange}
onCondOpChange={this.onCondOpChange}
onDeleteFilter={this.onDeleteFilter}
onApplyFilter={this.onApplyFilter}
onScheduleAction={this.onScheduleAction}
onSetTransfer={this.onSetTransfer}
onMakeAsSplitTransaction={this.onMakeAsSplitTransaction}
onMakeAsNonSplitTransactions={this.onMakeAsNonSplitTransactions}
/>
<View style={{ flex: 1 }}>
@@ -1751,7 +1631,9 @@ class AccountInternal extends PureComponent {
isAdding={this.state.isAdding}
isNew={this.isNew}
isMatched={this.isMatched}
isFiltered={transactionsFiltered}
isFiltered={
this.state.search !== '' || this.state.filters.length > 0
}
dateFormat={dateFormat}
hideFraction={hideFraction}
addNotification={addNotification}
@@ -1771,6 +1653,7 @@ class AccountInternal extends PureComponent {
</View>
) : null
}
pushModal={pushModal}
onSort={this.onSort}
sortField={this.state.sort.field}
ascDesc={this.state.sort.ascDesc}
@@ -1786,7 +1669,6 @@ class AccountInternal extends PureComponent {
this.setState({ isAdding: false })
}
onCreatePayee={this.onCreatePayee}
onApplyFilter={this.onApplyFilter}
/>
</View>
</View>
@@ -1799,11 +1681,13 @@ class AccountInternal extends PureComponent {
function AccountHack(props) {
const { dispatch: splitsExpandedDispatch } = useSplitsExpanded();
const match = useMatch(props.location.pathname);
return (
<AccountInternal
splitsExpandedDispatch={splitsExpandedDispatch}
{...props}
match={match}
splitsExpandedDispatch={splitsExpandedDispatch}
/>
);
}
@@ -1832,10 +1716,36 @@ export function Account() {
const modalShowing = useSelector(state => state.modals.modalStack.length > 0);
const accountsSyncing = useSelector(state => state.account.accountsSyncing);
const lastUndoState = useSelector(state => state.app.lastUndoState);
const filterConditions = location?.state?.filterConditions || [];
const conditions =
location.state && location.state.conditions
? location.state.conditions
: [];
const savedFiters = useFilters();
const actionCreators = useActions();
const state = {
newTransactions,
matchedTransactions,
accounts,
failedAccounts,
dateFormat,
hideFraction,
expandSplits,
showBalances,
showCleared: !hideCleared,
showReconciled: !hideReconciled,
showExtraBalances,
payees,
modalShowing,
accountsSyncing,
lastUndoState,
conditions,
};
const dispatch = useDispatch();
const filtersList = useFilters();
const actionCreators = useMemo(
() => bindActionCreators(actions, dispatch),
[dispatch],
);
const transform = useMemo(() => {
const filterByAccount = queries.getAccountFilter(params.id, '_account');
@@ -1864,31 +1774,17 @@ export function Account() {
return (
<SchedulesProvider transform={transform}>
<SplitsExpandedProvider
initialMode={expandSplits ? 'collapse' : 'expand'}
initialMode={state.expandSplits ? 'collapse' : 'expand'}
>
<AccountHack
newTransactions={newTransactions}
matchedTransactions={matchedTransactions}
accounts={accounts}
failedAccounts={failedAccounts}
dateFormat={dateFormat}
hideFraction={hideFraction}
expandSplits={expandSplits}
showBalances={showBalances}
showCleared={!hideCleared}
showReconciled={!hideReconciled}
showExtraBalances={showExtraBalances}
payees={payees}
modalShowing={modalShowing}
accountsSyncing={accountsSyncing}
lastUndoState={lastUndoState}
filterConditions={filterConditions}
{...state}
categoryGroups={categoryGroups}
{...actionCreators}
modalShowing={state.modalShowing}
accountId={params.id}
categoryId={location?.state?.categoryId}
location={location}
savedFilters={savedFiters}
filtersList={filtersList}
/>
</SplitsExpandedProvider>
</SchedulesProvider>

View File

@@ -7,7 +7,7 @@ import { useAccounts } from '../../hooks/useAccounts';
import { useActions } from '../../hooks/useActions';
import { SvgExclamationOutline } from '../../icons/v1';
import { theme } from '../../style';
import { Button } from '../common/Button2';
import { Button } from '../common/Button';
import { Link } from '../common/Link';
import { Popover } from '../common/Popover';
import { View } from '../common/View';
@@ -94,7 +94,7 @@ export function AccountSyncCheck() {
<View>
<Button
ref={triggerRef}
variant="bare"
type="bare"
style={{
flexDirection: 'row',
alignItems: 'center',
@@ -103,7 +103,7 @@ export function AccountSyncCheck() {
padding: '4px 8px',
borderRadius: 4,
}}
onPress={() => setOpen(true)}
onClick={() => setOpen(true)}
>
<SvgExclamationOutline
style={{ width: 14, height: 14, marginRight: 5 }}
@@ -129,17 +129,13 @@ export function AccountSyncCheck() {
<View style={{ justifyContent: 'flex-end', flexDirection: 'row' }}>
{showAuth ? (
<>
<Button onPress={unlink}>Unlink</Button>
<Button
variant="primary"
onPress={reauth}
style={{ marginLeft: 5 }}
>
<Button onClick={unlink}>Unlink</Button>
<Button type="primary" onClick={reauth} style={{ marginLeft: 5 }}>
Reauthorize
</Button>
</>
) : (
<Button onPress={unlink}>Unlink account</Button>
<Button onClick={unlink}>Unlink account</Button>
)}
</View>
</Popover>

View File

@@ -1,6 +1,4 @@
import React, { useRef } from 'react';
import { useHover } from 'usehooks-ts';
import React from 'react';
import { isPreviewId } from 'loot-core/shared/transactions';
import { useCachedSchedules } from 'loot-core/src/client/data-hooks/schedules';
@@ -10,7 +8,7 @@ import { getScheduledAmount } from 'loot-core/src/shared/schedules';
import { useSelectedItems } from '../../hooks/useSelected';
import { SvgArrowButtonRight1 } from '../../icons/v2';
import { theme } from '../../style';
import { Button } from '../common/Button2';
import { Button } from '../common/Button';
import { Text } from '../common/Text';
import { View } from '../common/View';
import { PrivacyFilter } from '../PrivacyFilter';
@@ -139,12 +137,10 @@ export function Balances({
showExtraBalances,
onToggleExtraBalances,
account,
isFiltered,
filteredItems,
filteredAmount,
}) {
const selectedItems = useSelectedItems();
const buttonRef = useRef(null);
const isButtonHovered = useHover(buttonRef);
return (
<View
@@ -156,11 +152,14 @@ export function Balances({
}}
>
<Button
ref={buttonRef}
data-testid="account-balance"
variant="bare"
onPress={onToggleExtraBalances}
type="bare"
onClick={onToggleExtraBalances}
style={{
'& svg': {
opacity: selectedItems.size > 0 || showExtraBalances ? 1 : 0,
},
'&:hover svg': { opacity: 1 },
paddingTop: 1,
paddingBottom: 1,
}}
@@ -189,10 +188,6 @@ export function Balances({
marginLeft: 10,
color: theme.pillText,
transform: showExtraBalances ? 'rotateZ(180deg)' : 'rotateZ(0)',
opacity:
isButtonHovered || selectedItems.size > 0 || showExtraBalances
? 1
: 0,
}}
/>
</Button>
@@ -201,7 +196,9 @@ export function Balances({
{selectedItems.size > 0 && (
<SelectedBalance selectedItems={selectedItems} account={account} />
)}
{isFiltered && <FilteredBalance filteredAmount={filteredAmount} />}
{filteredItems.length > 0 && (
<FilteredBalance filteredAmount={filteredAmount} />
)}
</View>
);
}

View File

@@ -14,7 +14,7 @@ import {
} from '../../icons/v2';
import { theme, styles } from '../../style';
import { AnimatedRefresh } from '../AnimatedRefresh';
import { Button } from '../common/Button2';
import { Button } from '../common/Button';
import { InitialFocus } from '../common/InitialFocus';
import { Input } from '../common/Input';
import { Menu } from '../common/Menu';
@@ -26,12 +26,13 @@ import { View } from '../common/View';
import { FilterButton } from '../filters/FiltersMenu';
import { FiltersStack } from '../filters/FiltersStack';
import { NotesButton } from '../NotesButton';
import { SelectedTransactionsButton } from '../transactions/SelectedTransactionsButton';
import { SelectedTransactionsButton } from '../transactions/SelectedTransactions';
import { Balances } from './Balance';
import { ReconcilingMessage, ReconcileMenu } from './Reconcile';
export function AccountHeader({
filteredAmount,
tableRef,
editingName,
isNameEditable,
@@ -39,7 +40,7 @@ export function AccountHeader({
accountName,
account,
filterId,
savedFilters,
filtersList,
accountsSyncing,
failedAccounts,
accounts,
@@ -52,12 +53,10 @@ export function AccountHeader({
balanceQuery,
reconcileAmount,
canCalculateBalance,
isFiltered,
filteredAmount,
isSorted,
search,
filterConditions,
filterConditionsOp,
filters,
conditionsOp,
pushModal,
onSearch,
onAddTransaction,
@@ -80,12 +79,10 @@ export function AccountHeader({
onUpdateFilter,
onClearFilters,
onReloadSavedFilter,
onConditionsOpChange,
onCondOpChange,
onDeleteFilter,
onScheduleAction,
onSetTransfer,
onMakeAsSplitTransaction,
onMakeAsNonSplitTransactions,
}) {
const [menuOpen, setMenuOpen] = useState(false);
const searchInput = useRef(null);
@@ -214,10 +211,10 @@ export function AccountHeader({
/>
)}
<Button
variant="bare"
type="bare"
aria-label="Edit account name"
className="hover-visible"
onPress={() => onExposeName(true)}
onClick={() => onExposeName(true)}
>
<SvgPencil1
style={{
@@ -246,7 +243,7 @@ export function AccountHeader({
showExtraBalances={showExtraBalances}
onToggleExtraBalances={onToggleExtraBalances}
account={account}
isFiltered={isFiltered}
filteredItems={filters}
filteredAmount={filteredAmount}
/>
@@ -258,9 +255,9 @@ export function AccountHeader({
>
{((account && !account.closed) || canSync) && (
<Button
variant="bare"
onPress={canSync ? onSync : onImport}
isDisabled={canSync && isServerOffline}
type="bare"
onClick={canSync ? onSync : onImport}
disabled={canSync && isServerOffline}
>
{canSync ? (
<>
@@ -289,7 +286,7 @@ export function AccountHeader({
</Button>
)}
{!showEmptyMessage && (
<Button variant="bare" onPress={onAddTransaction}>
<Button type="bare" onClick={onAddTransaction}>
<SvgAdd width={10} height={10} style={{ marginRight: 3 }} /> Add
New
</Button>
@@ -321,34 +318,24 @@ export function AccountHeader({
onScheduleAction={onScheduleAction}
pushModal={pushModal}
showMakeTransfer={showMakeTransfer}
onMakeAsSplitTransaction={onMakeAsSplitTransaction}
onMakeAsNonSplitTransactions={onMakeAsNonSplitTransactions}
/>
)}
<Button
variant="bare"
aria-label={
type="bare"
disabled={search !== '' || filters.length > 0}
style={{ padding: 6, marginLeft: 10 }}
onClick={onToggleSplits}
title={
splitsExpanded.state.mode === 'collapse'
? 'Collapse split transactions'
: 'Expand split transactions'
}
isDisabled={search !== '' || filterConditions.length > 0}
style={{ padding: 6, marginLeft: 10 }}
onPress={onToggleSplits}
>
<View
title={
splitsExpanded.state.mode === 'collapse'
? 'Collapse split transactions'
: 'Expand split transactions'
}
>
{splitsExpanded.state.mode === 'collapse' ? (
<SvgArrowsShrink3 style={{ width: 14, height: 14 }} />
) : (
<SvgArrowsExpand3 style={{ width: 14, height: 14 }} />
)}
</View>
{splitsExpanded.state.mode === 'collapse' ? (
<SvgArrowsShrink3 style={{ width: 14, height: 14 }} />
) : (
<SvgArrowsExpand3 style={{ width: 14, height: 14 }} />
)}
</Button>
{account ? (
<View>
@@ -404,17 +391,17 @@ export function AccountHeader({
)}
</Stack>
{filterConditions?.length > 0 && (
{filters && filters.length > 0 && (
<FiltersStack
conditions={filterConditions}
conditionsOp={filterConditionsOp}
filters={filters}
conditionsOp={conditionsOp}
onUpdateFilter={onUpdateFilter}
onDeleteFilter={onDeleteFilter}
onClearFilters={onClearFilters}
onReloadSavedFilter={onReloadSavedFilter}
filterId={filterId}
savedFilters={savedFilters}
onConditionsOpChange={onConditionsOpChange}
filtersList={filtersList}
onCondOpChange={onCondOpChange}
/>
)}
</View>

View File

@@ -1,11 +1,11 @@
import React, { useState } from 'react';
import React from 'react';
import * as queries from 'loot-core/src/client/queries';
import { currencyToInteger } from 'loot-core/src/shared/util';
import { SvgCheckCircle1 } from '../../icons/v2';
import { styles, theme } from '../../style';
import { Button } from '../common/Button2';
import { Button } from '../common/Button';
import { InitialFocus } from '../common/InitialFocus';
import { Input } from '../common/Input';
import { Text } from '../common/Text';
@@ -78,13 +78,13 @@ export function ReconcilingMessage({
</View>
)}
<View style={{ marginLeft: 15 }}>
<Button variant="primary" onPress={onDone}>
<Button type="primary" onClick={onDone}>
Done Reconciling
</Button>
</View>
{targetDiff !== 0 && (
<View style={{ marginLeft: 15 }}>
<Button onPress={() => onCreateTransaction(targetDiff)}>
<Button onClick={() => onCreateTransaction(targetDiff)}>
Create Reconciliation Transaction
</Button>
</View>
@@ -102,20 +102,17 @@ export function ReconcileMenu({ account, onReconcile, onClose }) {
query: balanceQuery.query.filter({ cleared: true }),
});
const format = useFormat();
const [inputValue, setInputValue] = useState(null);
const [inputFocused, setInputFocused] = useState(false);
function onSubmit() {
if (inputValue === '') {
setInputFocused(true);
return;
function onSubmit(e) {
e.preventDefault();
const input = e.target.elements[0];
const amount = currencyToInteger(input.value);
if (amount != null) {
onReconcile(amount == null ? clearedBalance : amount);
onClose();
} else {
input.select();
}
const amount =
inputValue != null ? currencyToInteger(inputValue) : clearedBalance;
onReconcile(amount);
onClose();
}
return (
@@ -124,20 +121,17 @@ export function ReconcileMenu({ account, onReconcile, onClose }) {
Enter the current balance of your bank account that you want to
reconcile with:
</Text>
{clearedBalance != null && (
<InitialFocus>
<Input
defaultValue={format(clearedBalance, 'financial')}
onChangeValue={setInputValue}
style={{ margin: '7px 0' }}
focused={inputFocused}
onEnter={onSubmit}
/>
</InitialFocus>
)}
<Button variant="primary" onPress={onSubmit}>
Reconcile
</Button>
<form onSubmit={onSubmit}>
{clearedBalance != null && (
<InitialFocus>
<Input
defaultValue={format(clearedBalance, 'financial')}
style={{ margin: '7px 0' }}
/>
</InitialFocus>
)}
<Button type="primary">Reconcile</Button>
</form>
</View>
);
}

View File

@@ -3,6 +3,7 @@ import React, { type ComponentPropsWithoutRef } from 'react';
import { useFeatureFlag } from '../../hooks/useFeatureFlag';
import { SvgArrowThinRight } from '../../icons/v1';
import { useResponsive } from '../../ResponsiveProvider';
import { type CSSProperties } from '../../style';
import { View } from '../common/View';
import { type Binding } from '../spreadsheet';
@@ -11,51 +12,24 @@ import { useSheetValue } from '../spreadsheet/useSheetValue';
import { makeBalanceAmountStyle } from './util';
type CarryoverIndicatorProps = {
style?: CSSProperties;
};
type BalanceWithCarryoverProps = Omit<
ComponentPropsWithoutRef<typeof CellValue>,
'binding'
> & {
carryover: Binding;
balance: Binding;
goal: Binding;
budgeted: Binding;
goal?: Binding;
budgeted?: Binding;
disabled?: boolean;
carryoverIndicator?: ({ style }: CarryoverIndicatorProps) => JSX.Element;
carryoverStyle?: CSSProperties;
};
export function DefaultCarryoverIndicator({ style }: CarryoverIndicatorProps) {
return (
<View
style={{
marginLeft: 2,
position: 'absolute',
right: '-4px',
alignSelf: 'center',
top: 0,
bottom: 0,
...style,
}}
>
<SvgArrowThinRight
width={style?.width || 7}
height={style?.height || 7}
style={style}
/>
</View>
);
}
export function BalanceWithCarryover({
carryover,
balance,
goal,
budgeted,
disabled,
carryoverIndicator = DefaultCarryoverIndicator,
carryoverStyle,
...props
}: BalanceWithCarryoverProps) {
const carryoverValue = useSheetValue(carryover);
@@ -63,21 +37,11 @@ export function BalanceWithCarryover({
const goalValue = useSheetValue(goal);
const budgetedValue = useSheetValue(budgeted);
const isGoalTemplatesEnabled = useFeatureFlag('goalTemplatesEnabled');
const valueStyle = makeBalanceAmountStyle(
balanceValue,
isGoalTemplatesEnabled ? goalValue : null,
budgetedValue,
);
const { isNarrowWidth } = useResponsive();
return (
<span
style={{
alignItems: 'center',
display: 'inline-flex',
justifyContent: 'right',
maxWidth: '100%',
}}
>
<>
<CellValue
{...props}
binding={balance}
@@ -90,8 +54,6 @@ export function BalanceWithCarryover({
)
}
style={{
overflow: 'hidden',
textOverflow: 'ellipsis',
textAlign: 'right',
...(!disabled && {
cursor: 'pointer',
@@ -99,7 +61,30 @@ export function BalanceWithCarryover({
...props.style,
}}
/>
{carryoverValue && carryoverIndicator({ style: valueStyle })}
</span>
{carryoverValue && (
<View
style={{
alignSelf: 'center',
marginLeft: 2,
position: 'absolute',
right: isNarrowWidth ? '-8px' : '-4px',
top: 0,
bottom: 0,
justifyContent: 'center',
...carryoverStyle,
}}
>
<SvgArrowThinRight
width={carryoverStyle?.width || 7}
height={carryoverStyle?.height || 7}
style={makeBalanceAmountStyle(
balanceValue,
goalValue,
budgetedValue,
)}
/>
</View>
)}
</>
);
}

View File

@@ -37,7 +37,6 @@ export const BudgetCategories = memo(
function onCollapse(value) {
setCollapsedGroupIdsPref(value);
}
const [isAddingGroup, setIsAddingGroup] = useState(false);
const [newCategoryForGroup, setNewCategoryForGroup] = useState(null);
const items = useMemo(() => {

View File

@@ -1,4 +1,5 @@
import React, { useState } from 'react';
import React, { useRef, useState, useEffect } from 'react';
import { useLocation } from 'react-router-dom';
import { useCategories } from '../../hooks/useCategories';
import { useLocalPref } from '../../hooks/useLocalPref';
@@ -29,12 +30,38 @@ export function BudgetTable(props) {
onBudgetAction,
} = props;
const budgetCategoriesRef = useRef();
const scrollableDivRef = useRef();
const location = useLocation();
const { grouped: categoryGroups } = useCategories();
const [collapsedGroupIds = [], setCollapsedGroupIdsPref] =
useLocalPref('budget.collapsed');
const [showHiddenCategories, setShowHiddenCategoriesPef] = useLocalPref(
'budget.showHiddenCategories',
);
const getCurrentScrollPosition = () => {
return scrollableDivRef.current?.scrollTop || 0;
};
const onShowActivityWithScroll = (categoryId, month) => {
const scrollPosition = getCurrentScrollPosition();
onShowActivity(categoryId, month, scrollPosition);
};
useEffect(() => {
const savedScrollPosition = location.state?.scrollPosition;
if (savedScrollPosition && scrollableDivRef.current) {
// Use requestAnimationFrame to ensure the DOM is ready
requestAnimationFrame(() => {
if (scrollableDivRef.current) {
scrollableDivRef.current.scrollTop = savedScrollPosition;
}
});
}
}, [location.state?.scrollPosition]);
const [editing, setEditing] = useState(null);
const onEditMonth = (id, month) => {
@@ -201,6 +228,7 @@ export function BudgetTable(props) {
collapseAllCategories={collapseAllCategories}
/>
<View
id="scrollableDiv"
style={{
overflowY: 'scroll',
overflowAnchor: 'none',
@@ -208,6 +236,7 @@ export function BudgetTable(props) {
paddingLeft: 5,
paddingRight: 5,
}}
innerRef={scrollableDivRef}
>
<View
style={{
@@ -228,7 +257,7 @@ export function BudgetTable(props) {
onReorderCategory={_onReorderCategory}
onReorderGroup={_onReorderGroup}
onBudgetAction={onBudgetAction}
onShowActivity={onShowActivity}
onShowActivity={onShowActivityWithScroll}
/>
</View>
</View>

View File

@@ -276,8 +276,8 @@ function BudgetInner(props: BudgetInnerProps) {
dispatch(applyBudgetAction(month, type, args));
};
const onShowActivity = (categoryId, month) => {
const filterConditions = [
const onShowActivity = (categoryId, month, scrollPosition) => {
const conditions = [
{ field: 'category', op: 'is', value: categoryId, type: 'id' },
{
field: 'date',
@@ -287,10 +287,16 @@ function BudgetInner(props: BudgetInnerProps) {
type: 'date',
},
];
navigate('/budget', {
replace: true,
state: { scrollPosition }
});
navigate('/accounts', {
state: {
goBack: true,
filterConditions,
conditions,
categoryId,
},
});

View File

@@ -302,7 +302,9 @@ export const CategoryMonth = memo(function CategoryMonth({
<Field name="spent" width="flex" style={{ textAlign: 'right' }}>
<span
data-testid="category-month-spent"
onClick={() => onShowActivity(category.id, month)}
onClick={() => {
onShowActivity(category.id, month);
}}
>
<CellValue
binding={reportBudget.catSumAmount(category.id)}
@@ -321,6 +323,7 @@ export const CategoryMonth = memo(function CategoryMonth({
{!category.is_income && (
<Field
name="balance"
truncate={false}
width="flex"
style={{ paddingRight: styles.monthRightPadding, textAlign: 'right' }}
>

View File

@@ -295,7 +295,9 @@ export const ExpenseCategoryMonth = memo(function ExpenseCategoryMonth({
<Field name="spent" width="flex" style={{ textAlign: 'right' }}>
<span
data-testid="category-month-spent"
onClick={() => onShowActivity(category.id, month)}
onClick={() => {
onShowActivity(category.id, month);
}}
>
<CellValue
binding={rolloverBudget.catSumAmount(category.id)}
@@ -310,6 +312,7 @@ export const ExpenseCategoryMonth = memo(function ExpenseCategoryMonth({
</Field>
<Field
name="balance"
truncate={false}
width="flex"
style={{ paddingRight: styles.monthRightPadding, textAlign: 'right' }}
>

View File

@@ -1,229 +0,0 @@
import React, { forwardRef, type ComponentPropsWithoutRef } from 'react';
import {
Button as ReactAriaButton,
type ButtonProps as ReactAriaButtonProps,
} from 'react-aria-components';
import { AnimatedLoading } from '../../icons/AnimatedLoading';
import { type CSSProperties, styles, theme } from '../../style';
import { View } from './View';
const backgroundColor: {
[key in ButtonVariant | `${ButtonVariant}Disabled`]?: string;
} = {
normal: theme.buttonNormalBackground,
normalDisabled: theme.buttonNormalDisabledBackground,
primary: theme.buttonPrimaryBackground,
primaryDisabled: theme.buttonPrimaryDisabledBackground,
bare: theme.buttonBareBackground,
bareDisabled: theme.buttonBareDisabledBackground,
menu: theme.buttonMenuBackground,
menuSelected: theme.buttonMenuSelectedBackground,
};
const backgroundColorHover: Record<
ButtonVariant | `${ButtonVariant}Disabled`,
CSSProperties['backgroundColor']
> = {
normal: theme.buttonNormalBackgroundHover,
primary: theme.buttonPrimaryBackgroundHover,
bare: theme.buttonBareBackgroundHover,
menu: theme.buttonMenuBackgroundHover,
menuSelected: theme.buttonMenuSelectedBackgroundHover,
normalDisabled: 'transparent',
primaryDisabled: 'transparent',
bareDisabled: 'transparent',
menuDisabled: 'transparent',
menuSelectedDisabled: 'transparent',
};
const borderColor: {
[key in
| ButtonVariant
| `${ButtonVariant}Disabled`]?: CSSProperties['borderColor'];
} = {
normal: theme.buttonNormalBorder,
normalDisabled: theme.buttonNormalDisabledBorder,
primary: theme.buttonPrimaryBorder,
primaryDisabled: theme.buttonPrimaryDisabledBorder,
menu: theme.buttonMenuBorder,
menuSelected: theme.buttonMenuSelectedBorder,
};
const textColor: {
[key in ButtonVariant | `${ButtonVariant}Disabled`]?: CSSProperties['color'];
} = {
normal: theme.buttonNormalText,
normalDisabled: theme.buttonNormalDisabledText,
primary: theme.buttonPrimaryText,
primaryDisabled: theme.buttonPrimaryDisabledText,
bare: theme.buttonBareText,
bareDisabled: theme.buttonBareDisabledText,
menu: theme.buttonMenuText,
menuSelected: theme.buttonMenuSelectedText,
};
const textColorHover: {
[key in ButtonVariant]?: string;
} = {
normal: theme.buttonNormalTextHover,
primary: theme.buttonPrimaryTextHover,
bare: theme.buttonBareTextHover,
menu: theme.buttonMenuTextHover,
menuSelected: theme.buttonMenuSelectedTextHover,
};
const _getBorder = (
variant: ButtonVariant,
variantWithDisabled: keyof typeof borderColor,
): string => {
switch (variant) {
case 'bare':
return 'none';
default:
return '1px solid ' + borderColor[variantWithDisabled];
}
};
const _getPadding = (variant: ButtonVariant): string => {
switch (variant) {
case 'bare':
return '5px';
default:
return '5px 10px';
}
};
const _getActiveStyles = (
variant: ButtonVariant,
bounce: boolean,
): CSSProperties => {
switch (variant) {
case 'bare':
return { backgroundColor: theme.buttonBareBackgroundActive };
default:
return {
transform: bounce ? 'translateY(1px)' : undefined,
boxShadow: `0 1px 4px 0 ${
variant === 'primary'
? theme.buttonPrimaryShadow
: theme.buttonNormalShadow
}`,
transition: 'none',
};
}
};
type ButtonProps = ComponentPropsWithoutRef<typeof ReactAriaButton> & {
variant?: ButtonVariant;
bounce?: boolean;
};
type ButtonVariant = 'normal' | 'primary' | 'bare' | 'menu' | 'menuSelected';
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
(props, ref) => {
const {
children,
variant = 'normal',
bounce = true,
style,
isDisabled,
...restProps
} = props;
const variantWithDisabled: ButtonVariant | `${ButtonVariant}Disabled` =
isDisabled ? `${variant}Disabled` : variant;
const hoveredStyle = {
...(variant !== 'bare' && styles.shadow),
backgroundColor: backgroundColorHover[variant],
color: textColorHover[variant],
};
const pressedStyle = {
..._getActiveStyles(variant, bounce),
};
const buttonStyle: ComponentPropsWithoutRef<
typeof Button
>['style'] = props => ({
...props.defaultStyle,
alignItems: 'center',
justifyContent: 'center',
flexShrink: 0,
padding: _getPadding(variant),
margin: 0,
overflow: 'hidden',
display: 'flex',
borderRadius: 4,
backgroundColor: backgroundColor[variantWithDisabled],
border: _getBorder(variant, variantWithDisabled),
color: textColor[variantWithDisabled],
transition: 'box-shadow .25s',
WebkitAppRegion: 'no-drag',
...styles.smallText,
...(props.isHovered && !isDisabled ? hoveredStyle : {}),
...(props.isPressed && !isDisabled ? pressedStyle : {}),
...(typeof style === 'function' ? style(props) : style),
});
return (
<ReactAriaButton ref={ref} style={buttonStyle} {...restProps}>
{children}
</ReactAriaButton>
);
},
);
Button.displayName = 'Button';
type ButtonWithLoadingProps = ButtonProps & {
isLoading?: boolean;
};
export const ButtonWithLoading = forwardRef<
HTMLButtonElement,
ButtonWithLoadingProps
>((props, ref) => {
const { isLoading, children, ...buttonProps } = props;
return (
<Button
{...buttonProps}
ref={ref}
style={{ position: 'relative', ...buttonProps.style }}
>
{renderProps => (
<>
{isLoading && (
<View
style={{
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
alignItems: 'center',
justifyContent: 'center',
}}
>
<AnimatedLoading style={{ width: 20, height: 20 }} />
</View>
)}
<View
style={{
opacity: isLoading ? 0 : 1,
flexDirection: 'row',
alignItems: 'center',
}}
>
{typeof children === 'function' ? children(renderProps) : children}
</View>
</>
)}
</Button>
);
});
ButtonWithLoading.displayName = 'ButtonWithLoading';

View File

@@ -23,6 +23,14 @@ export const Popover = ({
...style,
})}`}
shouldCloseOnInteractOutside={element => {
// Disable closing the popover when a reach listbox is clicked (Select component)
if (
element.getAttribute('data-reach-listbox-list') !== null ||
element.getAttribute('data-reach-listbox-option') !== null
) {
return false;
}
if (shouldCloseOnInteractOutside) {
return shouldCloseOnInteractOutside(element);
}

View File

@@ -1,28 +1,26 @@
import { useRef, useState } from 'react';
import {
ListboxInput,
ListboxButton,
ListboxPopover,
ListboxList,
ListboxOption,
} from '@reach/listbox';
import { css } from 'glamor';
import { SvgExpandArrow } from '../../icons/v0';
import { type CSSProperties } from '../../style';
import { Button } from './Button';
import { Menu } from './Menu';
import { Popover } from './Popover';
import { View } from './View';
function isValueOption<Value extends string>(
option: [Value, string] | typeof Menu.line,
): option is [Value, string] {
return option !== Menu.line;
}
import { theme, styles, type CSSProperties } from '../../style';
type SelectProps<Value extends string> = {
bare?: boolean;
options: Array<[Value, string] | typeof Menu.line>;
options: Array<[Value, string]>;
value: Value;
defaultLabel?: string;
onChange?: (newValue: Value) => void;
style?: CSSProperties;
wrapperStyle?: CSSProperties;
line?: number;
disabled?: boolean;
disabledKeys?: Value[];
buttonStyle?: CSSProperties;
};
/**
@@ -31,6 +29,7 @@ type SelectProps<Value extends string> = {
* @param {string} [defaultLabel] - The label to display when the selected value is not in the options.
* @param {function} [onChange] - A callback function invoked when the selected value changes.
* @param {CSSProperties} [style] - Custom styles to apply to the selected button.
* @param {CSSProperties} [wrapperStyle] - Custom style to apply to the select wrapper.
* @param {string[]} [disabledKeys] - An array of option values to disable.
*
* @example
@@ -38,96 +37,128 @@ type SelectProps<Value extends string> = {
* // <Select options={[['1', 'Option 1'], ['2', 'Option 2']]} value="1" onChange={handleOnChange} />
* // <Select options={[['1', 'Option 1'], ['2', 'Option 2']]} value="3" defaultLabel="Select an option" onChange={handleOnChange} />
*/
export function Select<Value extends string>({
bare,
options,
value,
defaultLabel = '',
onChange,
disabled = false,
style,
wrapperStyle,
line,
disabled,
disabledKeys = [],
buttonStyle = {},
}: SelectProps<Value>) {
const targetOption = options
.filter(isValueOption)
.find(option => option[0] === value);
const triggerRef = useRef(null);
const [isOpen, setIsOpen] = useState(false);
const arrowSize = 7;
const minHeight = style?.minHeight ? style.minHeight : '18px';
const targetOption = options.filter(option => option[0] === value);
return (
<>
<Button
ref={triggerRef}
type={bare ? 'bare' : 'normal'}
disabled={disabled}
onClick={() => {
setIsOpen(true);
}}
style={buttonStyle}
hoveredStyle={{
backgroundColor: bare ? 'transparent' : undefined,
...buttonStyle,
}}
>
<View
style={{
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
gap: 5,
width: '100%',
}}
>
<span
style={{
textAlign: 'left',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
width: 'calc(100% - 7px)',
}}
>
{targetOption ? targetOption[1] : defaultLabel}
</span>
<ListboxInput
value={value}
onChange={onChange}
disabled={disabled}
style={{
color: bare ? 'inherit' : theme.formInputText,
backgroundColor: bare ? 'transparent' : theme.cardBackground,
borderRadius: styles.menuBorderRadius,
border: bare ? 'none' : '1px solid ' + theme.formInputBorder,
lineHeight: '1em',
...wrapperStyle,
}}
>
<ListboxButton
className={`${css([
{ borderWidth: 0, padding: 5, borderRadius: 4 },
style,
])}`}
arrow={
<SvgExpandArrow
style={{
width: 7,
height: 7,
width: arrowSize,
height: arrowSize,
color: 'inherit',
}}
/>
</View>
</Button>
<Popover
triggerRef={triggerRef}
placement="bottom start"
isOpen={isOpen}
onOpenChange={() => setIsOpen(false)}
}
>
<Menu
onMenuSelect={item => {
onChange?.(item);
setIsOpen(false);
<span
style={{
display: 'flex',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
maxWidth: `calc(100% - ${arrowSize + 5}px)`,
alignItems: 'center',
minHeight,
}}
items={options.map(item =>
item === Menu.line
? Menu.line
: {
name: item[0],
text: item[1],
disabled: disabledKeys.includes(item[0]),
},
)}
getItemStyle={option => {
if (targetOption && targetOption[0] === option.name) {
return { fontWeight: 'bold' };
}
return {};
}}
/>
</Popover>
</>
>
{targetOption.length !== 0 ? targetOption[0][1] : defaultLabel}
</span>
</ListboxButton>
<ListboxPopover
style={{
zIndex: 100000,
outline: 0,
borderRadius: styles.menuBorderRadius,
backgroundColor: theme.menuBackground,
color: theme.menuItemText,
boxShadow: styles.cardShadow,
border: '1px solid ' + theme.menuBorder,
}}
className={`${css({
'[data-reach-listbox-option]': {
background: theme.menuItemBackground,
color: theme.menuItemText,
},
'[data-reach-listbox-option][data-current-nav]': {
background: theme.menuItemBackgroundHover,
color: theme.menuItemTextHover,
},
})}`}
>
{!line ? (
<ListboxList style={{ maxHeight: 250, overflowY: 'auto' }}>
{options.map(([value, label]) => (
<ListboxOption
key={value}
value={value}
disabled={disabledKeys.includes(value)}
>
{label}
</ListboxOption>
))}
</ListboxList>
) : (
<ListboxList style={{ maxHeight: 250, overflowY: 'auto' }}>
{options.slice(0, line).map(([value, label]) => (
<ListboxOption
key={value}
value={value}
disabled={disabledKeys.includes(value)}
>
{label}
</ListboxOption>
))}
<div
style={{
padding: '2px',
marginTop: 5,
borderTop: '1px solid ' + theme.menuBorder,
}}
/>
{options.slice(line, options.length).map(([value, label]) => (
<ListboxOption
key={value}
value={value}
disabled={disabledKeys.includes(value)}
>
{label}
</ListboxOption>
))}
</ListboxList>
)}
</ListboxPopover>
</ListboxInput>
);
}

View File

@@ -4,29 +4,26 @@ import { type RuleConditionEntity } from 'loot-core/src/types/models';
import { View } from '../common/View';
import { ConditionsOpMenu } from './ConditionsOpMenu';
import { CondOpMenu } from './CondOpMenu';
import { FilterExpression } from './FilterExpression';
type AppliedFiltersProps = {
conditions: RuleConditionEntity[];
filters: RuleConditionEntity[];
onUpdate: (
filter: RuleConditionEntity,
newFilter: RuleConditionEntity,
) => void;
onDelete: (filter: RuleConditionEntity) => void;
conditionsOp: string;
onConditionsOpChange: (
value: string,
conditions: RuleConditionEntity[],
) => void;
onCondOpChange: (value: string, filters: RuleConditionEntity[]) => void;
};
export function AppliedFilters({
conditions,
filters,
onUpdate,
onDelete,
conditionsOp,
onConditionsOpChange,
onCondOpChange,
}: AppliedFiltersProps) {
return (
<View
@@ -36,12 +33,12 @@ export function AppliedFilters({
flexWrap: 'wrap',
}}
>
<ConditionsOpMenu
<CondOpMenu
conditionsOp={conditionsOp}
onChange={onConditionsOpChange}
conditions={conditions}
onCondOpChange={onCondOpChange}
filters={filters}
/>
{conditions.map((filter: RuleConditionEntity, i: number) => (
{filters.map((filter: RuleConditionEntity, i: number) => (
<FilterExpression
key={i}
customName={filter.customName}

View File

@@ -7,16 +7,16 @@ import { Text } from '../common/Text';
import { View } from '../common/View';
import { FieldSelect } from '../modals/EditRule';
export function ConditionsOpMenu({
export function CondOpMenu({
conditionsOp,
onChange,
conditions,
onCondOpChange,
filters,
}: {
conditionsOp: string;
onChange: (value: string, conditions: RuleConditionEntity[]) => void;
conditions: RuleConditionEntity[];
onCondOpChange: (value: string, filters: RuleConditionEntity[]) => void;
filters: RuleConditionEntity[];
}) {
return conditions.length > 1 ? (
return filters.length > 1 ? (
<Text style={{ color: theme.pageText, marginTop: 11, marginRight: 5 }}>
<FieldSelect
style={{ display: 'inline-flex' }}
@@ -25,7 +25,9 @@ export function ConditionsOpMenu({
['or', 'any'],
]}
value={conditionsOp}
onChange={(name: string, value: string) => onChange(value, conditions)}
onChange={(name: string, value: string) =>
onCondOpChange(value, filters)
}
/>
of:
</Text>

View File

@@ -61,6 +61,7 @@ export function FilterExpression({
type="bare"
disabled={customName != null}
onClick={() => setEditing(true)}
style={{ marginRight: -7 }}
>
<div style={{ paddingBlock: 1, paddingLeft: 5, paddingRight: 2 }}>
{customName ? (
@@ -75,11 +76,7 @@ export function FilterExpression({
value={value}
field={field}
inline={true}
valueIsRaw={
op === 'contains' ||
op === 'matches' ||
op === 'doesNotContain'
}
valueIsRaw={op === 'contains' || op === 'doesNotContain'}
/>
</>
)}
@@ -90,7 +87,8 @@ export function FilterExpression({
style={{
width: 8,
height: 8,
margin: 4,
margin: 5,
marginLeft: 3,
}}
/>
</Button>

View File

@@ -87,6 +87,7 @@ function ConfigureField({
<Stack direction="row" align="flex-start">
{field === 'amount' || field === 'date' ? (
<Select
bare
options={
field === 'amount'
? [
@@ -110,6 +111,7 @@ function ConfigureField({
dispatch({ type: 'set-op', op: 'is' });
}
}}
style={{ borderWidth: 1 }}
/>
) : (
titleFirst(mapField(field))
@@ -197,8 +199,7 @@ function ConfigureField({
field={field}
subfield={subfield}
type={
type === 'id' &&
(op === 'contains' || op === 'matches' || op === 'doesNotContain')
type === 'id' && (op === 'contains' || op === 'doesNotContain')
? 'string'
: type
}

View File

@@ -1,6 +1,5 @@
import React from 'react';
import { type TransactionFilterEntity } from 'loot-core/types/models';
import { type RuleConditionEntity } from 'loot-core/types/models/rule';
import { Stack } from '../common/Stack';
@@ -13,17 +12,17 @@ import {
} from './SavedFilterMenuButton';
export function FiltersStack({
conditions,
filters,
conditionsOp,
onUpdateFilter,
onDeleteFilter,
onClearFilters,
onReloadSavedFilter,
filterId,
savedFilters,
onConditionsOpChange,
filtersList,
onCondOpChange,
}: {
conditions: RuleConditionEntity[];
filters: RuleConditionEntity[];
conditionsOp: string;
onUpdateFilter: (
filter: RuleConditionEntity,
@@ -33,8 +32,8 @@ export function FiltersStack({
onClearFilters: () => void;
onReloadSavedFilter: (savedFilter: SavedFilter, value?: string) => void;
filterId: SavedFilter;
savedFilters: TransactionFilterEntity[];
onConditionsOpChange: () => void;
filtersList: RuleConditionEntity[];
onCondOpChange: () => void;
}) {
return (
<View>
@@ -45,20 +44,20 @@ export function FiltersStack({
align="flex-start"
>
<AppliedFilters
conditions={conditions}
filters={filters}
conditionsOp={conditionsOp}
onConditionsOpChange={onConditionsOpChange}
onCondOpChange={onCondOpChange}
onUpdate={onUpdateFilter}
onDelete={onDeleteFilter}
/>
<View style={{ flex: 1 }} />
<SavedFilterMenuButton
conditions={conditions}
filters={filters}
conditionsOp={conditionsOp}
filterId={filterId}
onClearFilters={onClearFilters}
onReloadSavedFilter={onReloadSavedFilter}
savedFilters={savedFilters}
filtersList={filtersList}
/>
</Stack>
</View>

View File

@@ -1,7 +1,6 @@
import React, { useRef, useState } from 'react';
import { send, sendCatch } from 'loot-core/src/platform/client/fetch';
import { type TransactionFilterEntity } from 'loot-core/types/models';
import { type RuleConditionEntity } from 'loot-core/types/models/rule';
import { SvgExpandArrow } from '../../icons/v0';
@@ -22,19 +21,19 @@ export type SavedFilter = {
};
export function SavedFilterMenuButton({
conditions,
filters,
conditionsOp,
filterId,
onClearFilters,
onReloadSavedFilter,
savedFilters,
filtersList,
}: {
conditions: RuleConditionEntity[];
filters: RuleConditionEntity[];
conditionsOp: string;
filterId: SavedFilter;
onClearFilters: () => void;
onReloadSavedFilter: (savedFilter: SavedFilter, value?: string) => void;
savedFilters: TransactionFilterEntity[];
filtersList: RuleConditionEntity[];
}) {
const [nameOpen, setNameOpen] = useState(false);
const [adding, setAdding] = useState(false);
@@ -65,7 +64,7 @@ export function SavedFilterMenuButton({
setAdding(false);
setMenuOpen(false);
savedFilter = {
conditions,
conditions: filters,
conditionsOp,
id: filterId.id,
name: filterId.name,
@@ -73,7 +72,7 @@ export function SavedFilterMenuButton({
};
const response = await sendCatch('filter-update', {
state: savedFilter,
filters: [...savedFilters],
filters: [...filtersList],
});
if (response.error) {
@@ -109,7 +108,7 @@ export function SavedFilterMenuButton({
async function onAddUpdate() {
if (adding) {
const newSavedFilter = {
conditions,
conditions: filters,
conditionsOp,
name,
status: 'saved',
@@ -117,7 +116,7 @@ export function SavedFilterMenuButton({
const response = await sendCatch('filter-create', {
state: newSavedFilter,
filters: [...savedFilters],
filters: [...filtersList],
});
if (response.error) {
@@ -143,7 +142,7 @@ export function SavedFilterMenuButton({
const response = await sendCatch('filter-update', {
state: updatedFilter,
filters: [...savedFilters],
filters: [...filtersList],
});
if (response.error) {
@@ -158,7 +157,7 @@ export function SavedFilterMenuButton({
return (
<View>
{conditions.length > 0 && (
{filters.length > 0 && (
<Button
ref={triggerRef}
type="bare"

View File

@@ -12,7 +12,6 @@ export function updateFilterReducer(
if (
(type === 'id' || type === 'string') &&
(action.op === 'contains' ||
action.op === 'matches' ||
action.op === 'is' ||
action.op === 'doesNotContain' ||
action.op === 'isNot')

View File

@@ -1,5 +1,6 @@
import React, { memo, useRef } from 'react';
import React, { memo, useRef, useEffect } from 'react';
import { useDispatch } from 'react-redux';
import { useLocation } from 'react-router-dom';
import { AutoTextSize } from 'auto-text-size';
import memoizeOne from 'memoize-one';
@@ -8,7 +9,6 @@ import { collapseModals, pushModal } from 'loot-core/client/actions';
import { rolloverBudget, reportBudget } from 'loot-core/src/client/queries';
import * as monthUtils from 'loot-core/src/shared/months';
import { useFeatureFlag } from '../../../hooks/useFeatureFlag';
import { useLocalPref } from '../../../hooks/useLocalPref';
import { useNavigate } from '../../../hooks/useNavigate';
import { SvgLogo } from '../../../icons/logo';
@@ -16,14 +16,13 @@ import { SvgExpandArrow } from '../../../icons/v0';
import {
SvgArrowThinLeft,
SvgArrowThinRight,
SvgArrowThickRight,
SvgCheveronRight,
} from '../../../icons/v1';
import { SvgViewShow } from '../../../icons/v2';
import { useResponsive } from '../../../ResponsiveProvider';
import { theme, styles } from '../../../style';
import { BalanceWithCarryover } from '../../budget/BalanceWithCarryover';
import { makeAmountGrey, makeBalanceAmountStyle } from '../../budget/util';
import { makeAmountFullStyle, makeAmountGrey } from '../../budget/util';
import { Button } from '../../common/Button';
import { Card } from '../../common/Card';
import { Label } from '../../common/Label';
@@ -330,15 +329,11 @@ const ExpenseCategory = memo(function ExpenseCategory({
onBudgetAction,
show3Cols,
showBudgetedCol,
setScrollPosition,
onShowActivityWithScroll,
}) {
const opacity = blank ? 0 : 1;
const isGoalTemplatesEnabled = useFeatureFlag('goalTemplatesEnabled');
const goalTemp = useSheetValue(goal);
const goalValue = isGoalTemplatesEnabled ? goalTemp : null;
const budgetedTemp = useSheetValue(budgeted);
const budgetedValue = isGoalTemplatesEnabled ? budgetedTemp : null;
const [budgetType = 'rollover'] = useLocalPref('budgetType');
const dispatch = useDispatch();
@@ -404,10 +399,6 @@ const ExpenseCategory = memo(function ExpenseCategory({
const listItemRef = useRef();
const format = useFormat();
const navigate = useNavigate();
const onShowActivity = () => {
navigate(`/categories/${category.id}?month=${month}`);
};
const sidebarColumnWidth = getColumnWidth({ show3Cols, isSidebar: true });
const columnWidth = getColumnWidth({ show3Cols });
@@ -525,7 +516,9 @@ const ExpenseCategory = memo(function ExpenseCategory({
binding={spent}
getStyle={makeAmountGrey}
type="financial"
onClick={onShowActivity}
onClick={() => {
onShowActivityWithScroll(category.id, month);
}}
formatter={value => (
<Button
type="bare"
@@ -582,11 +575,9 @@ const ExpenseCategory = memo(function ExpenseCategory({
mode="oneline"
style={{
maxWidth: columnWidth,
...makeBalanceAmountStyle(
value,
goalValue,
budgetedValue,
),
...makeAmountFullStyle(value, {
zeroColor: theme.pillTextSubdued,
}),
textAlign: 'right',
fontSize: 12,
}}
@@ -595,23 +586,6 @@ const ExpenseCategory = memo(function ExpenseCategory({
</AutoTextSize>
</Button>
)}
carryoverIndicator={({ style }) => (
<View
style={{
position: 'absolute',
right: '-3px',
top: '-5px',
borderRadius: '50%',
backgroundColor: style?.color ?? theme.pillText,
}}
>
<SvgArrowThickRight
width={11}
height={11}
style={{ color: theme.pillBackgroundLight }}
/>
</View>
)}
/>
</span>
</View>
@@ -1262,6 +1236,8 @@ const ExpenseGroup = memo(function ExpenseGroup({
showHiddenCategories,
collapsed,
onToggleCollapse,
setScrollPosition,
onShowActivityWithScroll,
}) {
function editable(content) {
if (!editMode) {
@@ -1374,6 +1350,8 @@ const ExpenseGroup = memo(function ExpenseGroup({
month={month}
// onReorder={onReorderCategory}
onBudgetAction={onBudgetAction}
setScrollPosition={setScrollPosition}
onShowActivityWithScroll={onShowActivityWithScroll}
/>
);
})}
@@ -1485,6 +1463,8 @@ function BudgetGroups({
showBudgetedCol,
show3Cols,
showHiddenCategories,
setScrollPosition,
onShowActivityWithScroll,
}) {
const separateGroups = memoizeOne(groups => {
return {
@@ -1534,6 +1514,8 @@ function BudgetGroups({
showHiddenCategories={showHiddenCategories}
collapsed={collapsedGroupIds.includes(group.id)}
onToggleCollapse={onToggleCollapse}
setScrollPosition={setScrollPosition}
onShowActivityWithScroll={onShowActivityWithScroll}
/>
);
})}
@@ -1584,6 +1566,8 @@ export function BudgetTable({
}) {
const { width } = useResponsive();
const show3Cols = width >= 360;
const location = useLocation();
const navigate = useNavigate();
// let editMode = false; // neuter editMode -- sorry, not rewriting drag-n-drop right now
@@ -1591,6 +1575,44 @@ export function BudgetTable({
'mobile.showSpentColumn',
);
const getCurrentScrollPosition = () => {
const scrollableDiv = document.getElementById('scrollableDiv');
return scrollableDiv?.scrollTop || 0;
};
const setScrollPosition = () => {
const scrollPosition = getCurrentScrollPosition();
navigate('/budget', {
replace: true,
state: { scrollPosition }
});
};
const onShowActivityWithScroll = (categoryId, month) => {
const scrollPosition = getCurrentScrollPosition();
navigate('/budget', {
replace: true,
state: { scrollPosition }
});
navigate(`/categories/${categoryId}?month=${month}`);
};
useEffect(() => {
const savedScrollPosition = location.state?.scrollPosition;
if (savedScrollPosition) {
const scrollableDiv = document.getElementById('scrollableDiv');
if (scrollableDiv) {
requestAnimationFrame(() => {
scrollableDiv.scrollTop = savedScrollPosition;
});
}
}
}, [location.state?.scrollPosition]);
function toggleSpentColumn() {
setShowSpentColumnPref(!showSpentColumn);
}
@@ -1650,9 +1672,12 @@ export function BudgetTable({
<PullToRefresh onRefresh={onRefresh}>
<View
data-testid="budget-table"
id="scrollableDiv"
style={{
backgroundColor: theme.pageBackground,
paddingBottom: MOBILE_NAV_HEIGHT,
overflowY: 'auto',
height: '100%',
}}
>
<BudgetGroups
@@ -1674,6 +1699,8 @@ export function BudgetTable({
onReorderCategory={onReorderCategory}
onReorderGroup={onReorderGroup}
onBudgetAction={onBudgetAction}
setScrollPosition={setScrollPosition}
onShowActivityWithScroll={onShowActivityWithScroll}
/>
</View>
</PullToRefresh>

View File

@@ -118,19 +118,14 @@ export const Transaction = memo(function Transaction({
}}
>
<View style={{ flex: 1 }}>
<View
style={{
flexDirection: 'row',
alignItems: 'center',
color: textStyle.color || theme.tableText,
}}
>
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
{schedule && (
<SvgArrowsSynchronize
style={{
width: 12,
height: 12,
marginRight: 5,
color: textStyle.color || theme.menuItemText,
}}
/>
)}
@@ -140,18 +135,13 @@ export const Transaction = memo(function Transaction({
...textStyle,
fontSize: 14,
fontWeight: added ? '600' : '400',
...(!isPreview &&
prettyDescription === '' && {
color: theme.tableTextSubdued,
}),
...(!isPreview &&
(prettyDescription === '' ||
prettyDescription === 'Split') && {
fontStyle: 'italic',
}),
...(prettyDescription === '' && {
color: theme.tableTextLight,
fontStyle: 'italic',
}),
}}
>
{prettyDescription || 'No payee'}
{prettyDescription || 'Empty'}
</TextOneLine>
</View>
{isPreview ? (
@@ -180,7 +170,7 @@ export const Transaction = memo(function Transaction({
height: 11,
color: cleared
? theme.noticeTextLight
: theme.tableTextSubdued,
: theme.pageTextSubdued,
marginRight: 5,
}}
/>

View File

@@ -69,11 +69,9 @@ function getFieldName(transactionId, field) {
}
export function getDescriptionPretty(transaction, payee, transferAcct) {
const { amount, is_parent: isParent } = transaction;
const { amount } = transaction;
if (isParent) {
return 'Split';
} else if (transferAcct) {
if (transferAcct) {
return `Transfer ${amount > 0 ? 'from' : 'to'} ${transferAcct.name}`;
} else if (payee) {
return payee.name;
@@ -364,7 +362,7 @@ const ChildTransactionEdit = forwardRef(
<View>
<FieldLabel title="Category" />
<TapField
textStyle={{
style={{
...((isOffBudget || isBudgetTransfer(transaction)) && {
fontStyle: 'italic',
color: theme.pageTextSubdued,
@@ -492,9 +490,6 @@ const TransactionEditInner = memo(function TransactionEditInner({
};
const getPrettyPayee = trans => {
if (trans && trans.is_parent) {
return 'Split';
}
const transPayee = trans && getPayee(trans);
const transTransferAcct = trans && getTransferAcct(trans);
return getDescriptionPretty(trans, transPayee, transTransferAcct);
@@ -768,17 +763,11 @@ const TransactionEditInner = memo(function TransactionEditInner({
<View>
<FieldLabel title="Payee" />
<TapField
textStyle={{
...(transaction.is_parent && {
fontStyle: 'italic',
fontWeight: 300,
}),
}}
value={getPrettyPayee(transaction)}
disabled={
editingField &&
editingField !== getFieldName(transaction.id, 'payee')
}
value={getPrettyPayee(transaction)}
onClick={() => onEditField(transaction.id, 'payee')}
data-testid="payee-field"
/>

View File

@@ -9,14 +9,12 @@ import { type CommonModalProps } from '../Modals';
type ConfirmTransactionEditProps = {
modalProps: Partial<CommonModalProps>;
onCancel?: () => void;
onConfirm: () => void;
confirmReason: string;
};
export function ConfirmTransactionEdit({
modalProps,
onCancel,
onConfirm,
confirmReason,
}: ConfirmTransactionEditProps) {
@@ -73,13 +71,7 @@ export function ConfirmTransactionEdit({
justifyContent: 'flex-end',
}}
>
<Button
style={{ marginRight: 10 }}
onClick={() => {
modalProps.onClose();
onCancel();
}}
>
<Button style={{ marginRight: 10 }} onClick={modalProps.onClose}>
Cancel
</Button>
<Button

View File

@@ -1,10 +1,4 @@
import React, {
useState,
useEffect,
useRef,
useCallback,
useMemo,
} from 'react';
import React, { useState, useEffect, useRef, useCallback } from 'react';
import { useDispatch } from 'react-redux';
import { v4 as uuid } from 'uuid';
@@ -41,7 +35,6 @@ import { SvgDelete, SvgAdd, SvgSubtract } from '../../icons/v0';
import { SvgInformationOutline } from '../../icons/v1';
import { styles, theme } from '../../style';
import { Button } from '../common/Button';
import { Menu } from '../common/Menu';
import { Modal } from '../common/Modal';
import { Select } from '../common/Select';
import { Stack } from '../common/Stack';
@@ -89,13 +82,12 @@ function getTransactionFields(conditions, actions) {
export function FieldSelect({ fields, style, value, onChange }) {
return (
<View style={style}>
<View style={{ color: theme.pageTextPositive, ...style }}>
<Select
bare
options={fields}
value={value}
onChange={value => onChange('field', value)}
buttonStyle={{ color: theme.pageTextPositive }}
/>
</View>
);
@@ -105,36 +97,31 @@ export function OpSelect({
ops,
type,
style,
wrapperStyle,
value,
formatOp = friendlyOp,
onChange,
}) {
const opOptions = useMemo(() => {
const options = ops
// We don't support the `contains`, `doesNotContain`, `matches` operators
// for the id type rules yet
// TODO: Add matches op support for payees, accounts, categories.
.filter(op =>
type === 'id'
? !['contains', 'matches', 'doesNotContain'].includes(op)
: true,
)
.map(op => [op, formatOp(op, type)]);
if (type === 'string' || type === 'id') {
options.splice(options.length / 2, 0, Menu.line);
}
return options;
}, [ops, type]);
let line;
// We don't support the `contains` operator for the id type for
// rules yet
if (type === 'id') {
ops = ops.filter(op => op !== 'contains' && op !== 'doesNotContain');
line = ops.length / 2;
}
if (type === 'string') {
line = ops.length / 2;
}
return (
<Select
bare
options={opOptions}
options={ops.map(op => [op, formatOp(op, type)])}
value={value}
onChange={value => onChange('op', value)}
buttonStyle={style}
line={line}
style={{ minHeight: '1px', ...style }}
wrapperStyle={wrapperStyle}
/>
);
}

View File

@@ -487,8 +487,9 @@ function SelectField({
]),
]}
value={value === null ? 'choose-field' : value}
onChange={onChange}
buttonStyle={style}
style={{ width: '100%' }}
wrapperStyle={style}
onChange={value => onChange(value)}
/>
);
}
@@ -519,7 +520,8 @@ function DateFormatSelect({
f.label.replace(/ /g, delimiter),
])}
value={parseDateFormat || ''}
onChange={onChange}
onChange={value => onChange(value)}
style={{ width: '100%' }}
/>
</View>
);

View File

@@ -1,7 +1,6 @@
import React, { type ComponentPropsWithoutRef } from 'react';
import { useAccounts } from '../../hooks/useAccounts';
import { useNavigate } from '../../hooks/useNavigate';
import { usePayees } from '../../hooks/usePayees';
import { useResponsive } from '../../ResponsiveProvider';
import { theme } from '../../style';
@@ -22,7 +21,6 @@ export function PayeeAutocompleteModal({
}: PayeeAutocompleteModalProps) {
const payees = usePayees() || [];
const accounts = useAccounts() || [];
const navigate = useNavigate();
const _onClose = () => {
modalProps.onClose();
@@ -34,8 +32,6 @@ export function PayeeAutocompleteModal({
containerProps: { style: { height: isNarrowWidth ? '90vh' : 275 } },
};
const onManagePayees = () => navigate('/payees');
return (
<Modal
title={
@@ -60,19 +56,20 @@ export function PayeeAutocompleteModal({
/>
)}
>
<PayeeAutocomplete
payees={payees}
accounts={accounts}
focused={true}
embedded={true}
closeOnBlur={false}
onClose={_onClose}
onManagePayees={onManagePayees}
showManagePayees={!isNarrowWidth}
showMakeTransfer={!isNarrowWidth}
{...defaultAutocompleteProps}
{...autocompleteProps}
/>
{() => (
<PayeeAutocomplete
payees={payees}
accounts={accounts}
focused={true}
embedded={true}
closeOnBlur={false}
onClose={_onClose}
showManagePayees={false}
showMakeTransfer={!isNarrowWidth}
{...defaultAutocompleteProps}
{...autocompleteProps}
/>
)}
</Modal>
);
}

View File

@@ -4,10 +4,7 @@ import { reportBudget } from 'loot-core/client/queries';
import { useCategory } from '../../hooks/useCategory';
import { type CSSProperties, theme, styles } from '../../style';
import {
BalanceWithCarryover,
DefaultCarryoverIndicator,
} from '../budget/BalanceWithCarryover';
import { BalanceWithCarryover } from '../budget/BalanceWithCarryover';
import { BalanceMenu } from '../budget/report/BalanceMenu';
import { Modal, ModalTitle } from '../common/Modal';
import { Text } from '../common/Text';
@@ -66,21 +63,11 @@ export function ReportBalanceMenuModal({
textAlign: 'center',
...styles.veryLargeText,
}}
carryoverStyle={{ right: -20, width: 15, height: 15 }}
carryover={reportBudget.catCarryover(categoryId)}
balance={reportBudget.catBalance(categoryId)}
goal={reportBudget.catGoal(categoryId)}
budgeted={reportBudget.catBudgeted(categoryId)}
carryoverIndicator={({ style }) =>
DefaultCarryoverIndicator({
style: {
width: 15,
height: 15,
display: 'inline-flex',
position: 'relative',
...style,
},
})
}
/>
</View>
<BalanceMenu

View File

@@ -4,10 +4,7 @@ import { rolloverBudget } from 'loot-core/client/queries';
import { useCategory } from '../../hooks/useCategory';
import { type CSSProperties, theme, styles } from '../../style';
import {
BalanceWithCarryover,
DefaultCarryoverIndicator,
} from '../budget/BalanceWithCarryover';
import { BalanceWithCarryover } from '../budget/BalanceWithCarryover';
import { BalanceMenu } from '../budget/rollover/BalanceMenu';
import { Modal, ModalTitle } from '../common/Modal';
import { Text } from '../common/Text';
@@ -68,21 +65,11 @@ export function RolloverBalanceMenuModal({
textAlign: 'center',
...styles.veryLargeText,
}}
carryoverStyle={{ right: -20, width: 15, height: 15 }}
carryover={rolloverBudget.catCarryover(categoryId)}
balance={rolloverBudget.catBalance(categoryId)}
goal={rolloverBudget.catGoal(categoryId)}
budgeted={rolloverBudget.catBudgeted(categoryId)}
carryoverIndicator={({ style }) =>
DefaultCarryoverIndicator({
style: {
width: 15,
height: 15,
display: 'inline-flex',
position: 'relative',
...style,
},
})
}
/>
</View>
<BalanceMenu

View File

@@ -22,7 +22,7 @@ import {
import { useStableCallback } from '../../hooks/useStableCallback';
import { SvgExpandArrow } from '../../icons/v0';
import { theme } from '../../style';
import { Button } from '../common/Button2';
import { Button } from '../common/Button';
import { Popover } from '../common/Popover';
import { Search } from '../common/Search';
import { View } from '../common/View';
@@ -236,10 +236,10 @@ export const ManagePayees = forwardRef(
<View style={{ flexShrink: 0 }}>
<Button
ref={triggerRef}
variant="bare"
type="bare"
style={{ marginRight: 10 }}
isDisabled={buttonsDisabled}
onPress={() => setMenuOpen(true)}
disabled={buttonsDisabled}
onClick={() => setMenuOpen(true)}
>
{buttonsDisabled
? 'No payees selected'
@@ -273,9 +273,9 @@ export const ManagePayees = forwardRef(
{(orphanedOnly ||
(orphanedPayees && orphanedPayees.length > 0)) && (
<Button
variant="bare"
type="bare"
style={{ marginRight: 10 }}
onPress={() => {
onClick={() => {
setOrphanedOnly(!orphanedOnly);
applyFilter(filter);
tableNavigator.onEdit(null);

View File

@@ -27,7 +27,7 @@ export function Header({
onApply,
onUpdateFilter,
onDeleteFilter,
onConditionsOpChange,
onCondOpChange,
headerPrefixItems,
children,
}) {
@@ -151,11 +151,11 @@ export function Header({
align="flex-start"
>
<AppliedFilters
conditions={filters}
filters={filters}
onUpdate={onUpdateFilter}
onDelete={onDeleteFilter}
conditionsOp={conditionsOp}
onConditionsOpChange={onConditionsOpChange}
onCondOpChange={onCondOpChange}
/>
</View>
)}

View File

@@ -35,8 +35,6 @@ const balanceTypeOptions = [
{ description: 'Payment', format: 'totalDebts' as const },
{ description: 'Deposit', format: 'totalAssets' as const },
{ description: 'Net', format: 'totalTotals' as const },
{ description: 'Net Payment', format: 'netDebts' as const },
{ description: 'Net Deposit', format: 'netAssets' as const },
];
const groupByOptions = [

View File

@@ -1,4 +1,4 @@
import React, { useMemo, useRef, useState, type ComponentProps } from 'react';
import React, { useRef, useState } from 'react';
import * as monthUtils from 'loot-core/src/shared/months';
import { type CategoryEntity } from 'loot-core/types/models/category';
@@ -131,19 +131,6 @@ export function ReportSidebar({
setBalanceType(cond);
};
const rangeOptions = useMemo(() => {
const options: ComponentProps<typeof Select>['options'] =
ReportOptions.dateRange
.filter(f => f[customReportItems.interval as keyof dateRangeProps])
.map(option => [option.description, option.description]);
// Append separator if necessary
if (dateRangeLine > 0) {
options.splice(dateRangeLine, 0, Menu.line);
}
return options;
}, [customReportItems, dateRangeLine]);
return (
<View
style={{
@@ -432,8 +419,15 @@ export function ReportSidebar({
</Text>
<Select
value={customReportItems.dateRange}
onChange={onSelectRange}
options={rangeOptions}
onChange={e => {
onSelectRange(e);
}}
options={ReportOptions.dateRange
.filter(
f => f[customReportItems.interval as keyof dateRangeProps],
)
.map(option => [option.description, option.description])}
line={dateRangeLine > 0 ? dateRangeLine : undefined}
/>
</View>
) : (

View File

@@ -6,10 +6,7 @@ import {
integerToCurrency,
amountToInteger,
} from 'loot-core/src/shared/util';
import {
type balanceTypeOpType,
type DataEntity,
} from 'loot-core/src/types/models/reports';
import { type DataEntity } from 'loot-core/src/types/models/reports';
import { theme, styles } from '../../style';
import { Text } from '../common/Text';
@@ -22,7 +19,7 @@ type ReportSummaryProps = {
startDate: string;
endDate: string;
data: DataEntity;
balanceTypeOp: balanceTypeOpType;
balanceTypeOp: 'totalDebts' | 'totalAssets' | 'totalTotals';
interval: string;
intervalsCount: number;
};
@@ -36,13 +33,9 @@ export function ReportSummary({
intervalsCount,
}: ReportSummaryProps) {
const net =
balanceTypeOp === 'netAssets'
? 'DEPOSIT'
: balanceTypeOp === 'netDebts'
? 'PAYMENT'
: Math.abs(data.totalDebts) > Math.abs(data.totalAssets)
? 'PAYMENT'
: 'DEPOSIT';
Math.abs(data.totalDebts) > Math.abs(data.totalAssets)
? 'PAYMENT'
: 'DEPOSIT';
const average = amountToInteger(data[balanceTypeOp]) / intervalsCount;
return (
<View

View File

@@ -63,7 +63,7 @@ const totalGraphOptions: graphOptions[] = [
description: 'BarGraph',
disabledSplit: [],
defaultSplit: 'Category',
disabledType: [],
disabledType: ['Net'],
defaultType: 'Payment',
},
{
@@ -88,7 +88,7 @@ const timeGraphOptions: graphOptions[] = [
description: 'TableGraph',
disabledSplit: ['Interval'],
defaultSplit: 'Category',
disabledType: ['Net Payment', 'Net Deposit'],
disabledType: [],
defaultType: 'Payment',
disableLegend: true,
disableLabel: true,
@@ -97,14 +97,14 @@ const timeGraphOptions: graphOptions[] = [
description: 'StackedBarGraph',
disabledSplit: ['Interval'],
defaultSplit: 'Category',
disabledType: [],
disabledType: ['Net'],
defaultType: 'Payment',
},
{
description: 'LineGraph',
disabledSplit: ['Interval'],
defaultSplit: 'Category',
disabledType: [],
disabledType: ['Net'],
defaultType: 'Payment',
disableLegend: false,
disableLabel: true,

View File

@@ -16,10 +16,7 @@ import {
amountToCurrency,
amountToCurrencyNoDecimal,
} from 'loot-core/src/shared/util';
import {
type balanceTypeOpType,
type DataEntity,
} from 'loot-core/src/types/models/reports';
import { type DataEntity } from 'loot-core/src/types/models/reports';
import { usePrivacyMode } from '../../../hooks/usePrivacyMode';
import { useResponsive } from '../../../ResponsiveProvider';
@@ -36,8 +33,6 @@ type PayloadItem = {
date: string;
totalAssets: number | string;
totalDebts: number | string;
netAssets: number | string;
netDebts: number | string;
totalTotals: number | string;
};
};
@@ -45,7 +40,7 @@ type PayloadItem = {
type CustomTooltipProps = {
active?: boolean;
payload?: PayloadItem[];
balanceTypeOp: balanceTypeOpType;
balanceTypeOp: 'totalAssets' | 'totalTotals' | 'totalDebts';
};
const CustomTooltip = ({
@@ -79,22 +74,10 @@ const CustomTooltip = ({
)}
{['totalDebts', 'totalTotals'].includes(balanceTypeOp) && (
<AlignedText
left="Debts:"
left="Debt:"
right={amountToCurrency(payload[0].payload.totalDebts)}
/>
)}
{['netAssets'].includes(balanceTypeOp) && (
<AlignedText
left="Net Assets:"
right={amountToCurrency(payload[0].payload.netAssets)}
/>
)}
{['netDebts'].includes(balanceTypeOp) && (
<AlignedText
left="Net Debts:"
right={amountToCurrency(payload[0].payload.netDebts)}
/>
)}
{['totalTotals'].includes(balanceTypeOp) && (
<AlignedText
left="Net:"
@@ -149,7 +132,7 @@ const customLabel = ({
type AreaGraphProps = {
style?: CSSProperties;
data: DataEntity;
balanceTypeOp: balanceTypeOpType;
balanceTypeOp: 'totalAssets' | 'totalTotals' | 'totalDebts';
compact?: boolean;
viewLabels: boolean;
};

View File

@@ -19,10 +19,7 @@ import {
amountToCurrency,
amountToCurrencyNoDecimal,
} from 'loot-core/src/shared/util';
import {
type balanceTypeOpType,
type DataEntity,
} from 'loot-core/src/types/models/reports';
import { type DataEntity } from 'loot-core/src/types/models/reports';
import { type RuleConditionEntity } from 'loot-core/types/models/rule';
import { useAccounts } from '../../../hooks/useAccounts';
@@ -53,8 +50,6 @@ type PayloadItem = {
name: string;
totalAssets: number | string;
totalDebts: number | string;
netAssets: number | string;
netDebts: number | string;
totalTotals: number | string;
networth: number | string;
totalChange: number | string;
@@ -65,7 +60,7 @@ type PayloadItem = {
type CustomTooltipProps = {
active?: boolean;
payload?: PayloadItem[];
balanceTypeOp?: balanceTypeOpType;
balanceTypeOp?: 'totalAssets' | 'totalDebts' | 'totalTotals';
yAxis?: string;
};
@@ -101,22 +96,10 @@ const CustomTooltip = ({
)}
{['totalDebts', 'totalTotals'].includes(balanceTypeOp) && (
<AlignedText
left="Debts:"
left="Debt:"
right={amountToCurrency(payload[0].payload.totalDebts)}
/>
)}
{['netAssets'].includes(balanceTypeOp) && (
<AlignedText
left="Net Assets:"
right={amountToCurrency(payload[0].payload.netAssets)}
/>
)}
{['netDebts'].includes(balanceTypeOp) && (
<AlignedText
left="Net Debts:"
right={amountToCurrency(payload[0].payload.netDebts)}
/>
)}
{['totalTotals'].includes(balanceTypeOp) && (
<AlignedText
left="Net:"
@@ -154,7 +137,7 @@ type BarGraphProps = {
data: DataEntity;
filters: RuleConditionEntity[];
groupBy: string;
balanceTypeOp: balanceTypeOpType;
balanceTypeOp: 'totalAssets' | 'totalDebts' | 'totalTotals';
compact?: boolean;
viewLabels: boolean;
showHiddenCategories?: boolean;
@@ -184,15 +167,11 @@ export function BarGraph({
const labelsMargin = viewLabels ? 30 : 0;
const getVal = obj => {
if (balanceTypeOp === 'totalTotals' && groupBy === 'Interval') {
if (balanceTypeOp === 'totalDebts') {
return -1 * obj.totalDebts;
} else {
return obj.totalAssets;
}
if (['totalDebts', 'netDebts'].includes(balanceTypeOp)) {
return -1 * obj[balanceTypeOp];
}
return obj[balanceTypeOp];
};
const longestLabelLength = data[splitData]

View File

@@ -4,10 +4,7 @@ import React, { useState } from 'react';
import { PieChart, Pie, Cell, Sector, ResponsiveContainer } from 'recharts';
import { amountToCurrency } from 'loot-core/src/shared/util';
import {
type balanceTypeOpType,
type DataEntity,
} from 'loot-core/src/types/models/reports';
import { type DataEntity } from 'loot-core/src/types/models/reports';
import { type RuleConditionEntity } from 'loot-core/types/models/rule';
import { useAccounts } from '../../../hooks/useAccounts';
@@ -184,7 +181,7 @@ type DonutGraphProps = {
data: DataEntity;
filters: RuleConditionEntity[];
groupBy: string;
balanceTypeOp: balanceTypeOpType;
balanceTypeOp: 'totalAssets' | 'totalDebts' | 'totalTotals';
compact?: boolean;
viewLabels: boolean;
showHiddenCategories?: boolean;
@@ -212,7 +209,7 @@ export function DonutGraph({
const [pointer, setPointer] = useState('');
const getVal = obj => {
if (['totalDebts', 'netDebts'].includes(balanceTypeOp)) {
if (balanceTypeOp === 'totalDebts') {
return -1 * obj[balanceTypeOp];
} else {
return obj[balanceTypeOp];

View File

@@ -16,10 +16,7 @@ import {
amountToCurrency,
amountToCurrencyNoDecimal,
} from 'loot-core/src/shared/util';
import {
type balanceTypeOpType,
type DataEntity,
} from 'loot-core/types/models/reports';
import { type DataEntity } from 'loot-core/types/models/reports';
import { type RuleConditionEntity } from 'loot-core/types/models/rule';
import { useAccounts } from '../../../hooks/useAccounts';
@@ -118,7 +115,7 @@ type LineGraphProps = {
filters: RuleConditionEntity[];
groupBy: string;
compact?: boolean;
balanceTypeOp: balanceTypeOpType;
balanceTypeOp: 'totalAssets' | 'totalDebts' | 'totalTotals';
showHiddenCategories?: boolean;
showOffBudget?: boolean;
interval?: string;

View File

@@ -17,10 +17,7 @@ import {
amountToCurrency,
amountToCurrencyNoDecimal,
} from 'loot-core/src/shared/util';
import {
type balanceTypeOpType,
type DataEntity,
} from 'loot-core/src/types/models/reports';
import { type DataEntity } from 'loot-core/src/types/models/reports';
import { type RuleConditionEntity } from 'loot-core/types/models/rule';
import { useAccounts } from '../../../hooks/useAccounts';
@@ -147,7 +144,7 @@ type StackedBarGraphProps = {
groupBy: string;
compact?: boolean;
viewLabels: boolean;
balanceTypeOp: balanceTypeOpType;
balanceTypeOp: 'totalAssets' | 'totalDebts' | 'totalTotals';
showHiddenCategories?: boolean;
showOffBudget?: boolean;
interval?: string;
@@ -197,7 +194,6 @@ export function StackedBarGraph({
data={data.intervalData}
margin={{ top: 0, right: 0, left: leftMargin, bottom: 10 }}
style={{ cursor: pointer }}
stackOffset="sign" //stacked by sign
>
{(!isNarrowWidth || !compact) && (
<Tooltip

View File

@@ -4,7 +4,6 @@ import * as monthUtils from 'loot-core/src/shared/months';
import { type AccountEntity } from 'loot-core/types/models/account';
import { type CategoryEntity } from 'loot-core/types/models/category';
import { type CategoryGroupEntity } from 'loot-core/types/models/category-group';
import { type balanceTypeOpType } from 'loot-core/types/models/reports';
import { type RuleConditionEntity } from 'loot-core/types/models/rule';
import { ReportOptions } from '../ReportOptions';
@@ -13,7 +12,7 @@ type showActivityProps = {
navigate: NavigateFunction;
categories: { list: CategoryEntity[]; grouped: CategoryGroupEntity[] };
accounts: AccountEntity[];
balanceTypeOp: balanceTypeOpType;
balanceTypeOp: 'totalAssets' | 'totalDebts' | 'totalTotals';
filters: RuleConditionEntity[];
showHiddenCategories: boolean;
showOffBudget: boolean;
@@ -51,7 +50,7 @@ export function showActivity({
'FromDate') as 'dayFromDate' | 'monthFromDate' | 'yearFromDate');
const isDateOp = interval === 'Weekly' || type !== 'time';
const filterConditions = [
const conditions = [
...filters,
id && { field, op: 'is', value: id, type: 'id' },
{
@@ -67,9 +66,8 @@ export function showActivity({
options: { date: true },
},
!(
['netAssets', 'netDebts'].includes(balanceTypeOp) ||
(balanceTypeOp === 'totalTotals' &&
(type === 'totals' || type === 'time'))
balanceTypeOp === 'totalTotals' &&
(type === 'totals' || type === 'time')
) && {
field: 'amount',
op: 'gte',
@@ -98,7 +96,7 @@ export function showActivity({
navigate('/accounts', {
state: {
goBack: true,
filterConditions,
conditions,
},
});
}

View File

@@ -9,7 +9,6 @@ import React, {
import {
type GroupedEntity,
type DataEntity,
type balanceTypeOpType,
} from 'loot-core/src/types/models/reports';
import { type RuleConditionEntity } from 'loot-core/types/models/rule';
@@ -29,7 +28,7 @@ type ReportTableProps = {
totalScrollRef: RefObject<HTMLDivElement>;
handleScroll: UIEventHandler<HTMLDivElement>;
groupBy: string;
balanceTypeOp: balanceTypeOpType;
balanceTypeOp: 'totalDebts' | 'totalTotals' | 'totalAssets';
data: DataEntity;
filters?: RuleConditionEntity[];
mode: string;

View File

@@ -1,9 +1,6 @@
import React, { type RefObject, type UIEventHandler } from 'react';
import {
type balanceTypeOpType,
type IntervalEntity,
} from 'loot-core/src/types/models/reports';
import { type IntervalEntity } from 'loot-core/src/types/models/reports';
import { theme } from '../../../../style';
import { type CSSProperties } from '../../../../style/types';
@@ -15,7 +12,7 @@ type ReportTableHeaderProps = {
groupBy: string;
interval: string;
data: IntervalEntity[];
balanceTypeOp: balanceTypeOpType;
balanceTypeOp: 'totalDebts' | 'totalTotals' | 'totalAssets';
headerScrollRef: RefObject<HTMLDivElement>;
handleScroll: UIEventHandler<HTMLDivElement>;
compact: boolean;

View File

@@ -38,8 +38,6 @@ export function ReportTableList({
date: interval.date,
totalAssets: interval.totalAssets,
totalDebts: interval.totalDebts,
netAssets: interval.netAssets,
netDebts: interval.netDebts,
totalTotals: interval.totalTotals,
intervalData: [],
categories: [],

View File

@@ -5,10 +5,7 @@ import {
amountToInteger,
integerToCurrency,
} from 'loot-core/src/shared/util';
import {
type balanceTypeOpType,
type GroupedEntity,
} from 'loot-core/types/models/reports';
import { type GroupedEntity } from 'loot-core/types/models/reports';
import { type RuleConditionEntity } from 'loot-core/types/models/rule';
import { useAccounts } from '../../../../hooks/useAccounts';
@@ -16,14 +13,13 @@ import { useCategories } from '../../../../hooks/useCategories';
import { useNavigate } from '../../../../hooks/useNavigate';
import { useResponsive } from '../../../../ResponsiveProvider';
import { type CSSProperties, theme } from '../../../../style';
import { Text } from '../../../common/Text';
import { View } from '../../../common/View';
import { Row, Cell } from '../../../table';
import { showActivity } from '../showActivity';
type ReportTableRowProps = {
item: GroupedEntity;
balanceTypeOp: balanceTypeOpType;
balanceTypeOp: 'totalAssets' | 'totalDebts' | 'totalTotals';
groupBy: string;
mode: string;
filters?: RuleConditionEntity[];
@@ -129,9 +125,7 @@ export const ReportTableRow = memo(
style={{
minWidth: compact ? 50 : 85,
}}
unexposedContent={({ value }) => (
<Text style={hoverUnderline}>{value}</Text>
)}
linkStyle={hoverUnderline}
valueStyle={compactStyle}
value={amountToCurrency(intervalItem[balanceTypeOp])}
title={
@@ -179,9 +173,7 @@ export const ReportTableRow = memo(
style={{
minWidth: compact ? 50 : 85,
}}
unexposedContent={({ value }) => (
<Text style={hoverUnderline}>{value}</Text>
)}
linkStyle={hoverUnderline}
valueStyle={compactStyle}
onClick={() =>
!isNarrowWidth &&
@@ -216,9 +208,7 @@ export const ReportTableRow = memo(
style={{
minWidth: compact ? 50 : 85,
}}
unexposedContent={({ value }) => (
<Text style={hoverUnderline}>{value}</Text>
)}
linkStyle={hoverUnderline}
valueStyle={compactStyle}
onClick={() =>
!isNarrowWidth &&
@@ -254,9 +244,7 @@ export const ReportTableRow = memo(
fontWeight: 600,
minWidth: compact ? 50 : 85,
}}
unexposedContent={({ value }) => (
<Text style={hoverUnderline}>{value}</Text>
)}
linkStyle={hoverUnderline}
valueStyle={compactStyle}
onClick={() =>
!isNarrowWidth &&

View File

@@ -85,8 +85,6 @@ export function ReportTableTotals({
intervalData: data.intervalData,
totalAssets: data.totalAssets,
totalDebts: data.totalDebts,
netAssets: data.netAssets,
netDebts: data.netDebts,
totalTotals: data.totalTotals,
};

View File

@@ -28,12 +28,12 @@ import { useReport } from '../useReport';
export function CashFlow() {
const {
conditions,
filters,
conditionsOp,
onApply: onApplyFilter,
onDelete: onDeleteFilter,
onUpdate: onUpdateFilter,
onConditionsOpChange,
onCondOpChange,
} = useFilters<RuleConditionEntity>();
const [allMonths, setAllMonths] = useState<null | Array<{
@@ -55,8 +55,8 @@ export function CashFlow() {
});
const params = useMemo(
() => cashFlowByDate(start, end, isConcise, conditions, conditionsOp),
[start, end, isConcise, conditions, conditionsOp],
() => cashFlowByDate(start, end, isConcise, filters, conditionsOp),
[start, end, isConcise, filters, conditionsOp],
);
const data = useReport('cash_flow', params);
@@ -129,11 +129,11 @@ export function CashFlow() {
show1Month
onChangeDates={onChangeDates}
onApply={onApplyFilter}
filters={conditions}
filters={filters}
onUpdateFilter={onUpdateFilter}
onDeleteFilter={onDeleteFilter}
conditionsOp={conditionsOp}
onConditionsOpChange={onConditionsOpChange}
onCondOpChange={onCondOpChange}
headerPrefixItems={undefined}
>
<View

View File

@@ -8,7 +8,6 @@ import * as monthUtils from 'loot-core/src/shared/months';
import { amountToCurrency } from 'loot-core/src/shared/util';
import { type CategoryEntity } from 'loot-core/types/models/category';
import {
type balanceTypeOpType,
type CustomReportEntity,
type DataEntity,
} from 'loot-core/types/models/reports';
@@ -69,12 +68,12 @@ export function CustomReport() {
useLocalPref('reportsViewLabel');
const {
conditions,
filters,
conditionsOp,
onApply: onApplyFilter,
onDelete: onDeleteFilter,
onUpdate: onUpdateFilter,
onConditionsOpChange,
onCondOpChange,
} = useFilters();
const location = useLocation();
@@ -249,7 +248,7 @@ export function CustomReport() {
}
}, [interval, startDate, endDate, firstDayOfWeekIdx]);
const balanceTypeOp: balanceTypeOpType =
const balanceTypeOp: 'totalAssets' | 'totalDebts' | 'totalTotals' =
ReportOptions.balanceTypeMap.get(balanceType) || 'totalDebts';
const payees = usePayees();
const accounts = useAccounts();
@@ -261,7 +260,7 @@ export function CustomReport() {
interval,
categories,
selectedCategories,
conditions,
conditions: filters,
conditionsOp,
showEmpty,
showOffBudget,
@@ -277,7 +276,7 @@ export function CustomReport() {
balanceTypeOp,
categories,
selectedCategories,
conditions,
filters,
conditionsOp,
showEmpty,
showOffBudget,
@@ -294,7 +293,7 @@ export function CustomReport() {
interval,
categories,
selectedCategories,
conditions,
conditions: filters,
conditionsOp,
showEmpty,
showOffBudget,
@@ -318,7 +317,7 @@ export function CustomReport() {
selectedCategories,
payees,
accounts,
conditions,
filters,
conditionsOp,
showEmpty,
showOffBudget,
@@ -350,7 +349,7 @@ export function CustomReport() {
showUncategorized,
selectedCategories,
graphType,
conditions,
conditions: filters,
conditionsOp,
};
@@ -495,7 +494,7 @@ export function CustomReport() {
setGraphType(input.graphType);
onApplyFilter(null);
(input.conditions || []).forEach(condition => onApplyFilter(condition));
onConditionsOpChange(input.conditionsOp);
onCondOpChange(input.conditionsOp);
};
const onReportChange = ({
@@ -624,7 +623,7 @@ export function CustomReport() {
defaultItems={defaultItems}
/>
)}
{conditions && conditions.length > 0 && (
{filters && filters.length > 0 && (
<View
style={{
marginBottom: 10,
@@ -636,11 +635,11 @@ export function CustomReport() {
}}
>
<AppliedFilters
conditions={conditions}
filters={filters}
onUpdate={(oldFilter, newFilter) => {
setSessionReport(
'conditions',
conditions.map(f => (f === oldFilter ? newFilter : f)),
filters.map(f => (f === oldFilter ? newFilter : f)),
);
onReportChange({ type: 'modify' });
onUpdateFilter(oldFilter, newFilter);
@@ -648,14 +647,14 @@ export function CustomReport() {
onDelete={deletedFilter => {
setSessionReport(
'conditions',
conditions.filter(f => f !== deletedFilter),
filters.filter(f => f !== deletedFilter),
);
onDeleteFilter(deletedFilter);
onReportChange({ type: 'modify' });
}}
conditionsOp={conditionsOp}
onConditionsOpChange={co => {
onConditionsOpChange(co);
onCondOpChange={co => {
onCondOpChange(co);
onReportChange({ type: 'modify' });
}}
/>
@@ -693,7 +692,7 @@ export function CustomReport() {
right={
<Text>
<PrivacyFilter blurIntensity={5}>
{amountToCurrency(data[balanceTypeOp])}
{amountToCurrency(Math.abs(data[balanceTypeOp]))}
</PrivacyFilter>
</Text>
}
@@ -705,7 +704,7 @@ export function CustomReport() {
{dataCheck ? (
<ChooseGraph
data={data}
filters={conditions}
filters={filters}
mode={mode}
graphType={graphType}
balanceType={balanceType}

View File

@@ -26,13 +26,13 @@ import { fromDateRepr } from '../util';
export function NetWorth() {
const accounts = useAccounts();
const {
conditions,
filters,
saved,
conditionsOp,
onApply: onApplyFilter,
onDelete: onDeleteFilter,
onUpdate: onUpdateFilter,
onConditionsOpChange,
onCondOpChange,
} = useFilters();
const [allMonths, setAllMonths] = useState(null);
@@ -42,8 +42,8 @@ export function NetWorth() {
const [end, setEnd] = useState(monthUtils.currentMonth());
const params = useMemo(
() => netWorthSpreadsheet(start, end, accounts, conditions, conditionsOp),
[start, end, accounts, conditions, conditionsOp],
() => netWorthSpreadsheet(start, end, accounts, filters, conditionsOp),
[start, end, accounts, filters, conditionsOp],
);
const data = useReport('net_worth', params);
useEffect(() => {
@@ -108,13 +108,13 @@ export function NetWorth() {
start={start}
end={end}
onChangeDates={onChangeDates}
filters={conditions}
filters={filters}
saved={saved}
onApply={onApplyFilter}
onUpdateFilter={onUpdateFilter}
onDeleteFilter={onDeleteFilter}
conditionsOp={conditionsOp}
onConditionsOpChange={onConditionsOpChange}
onCondOpChange={onCondOpChange}
/>
<View

View File

@@ -29,12 +29,12 @@ export function Spending() {
const categories = useCategories();
const {
conditions,
filters,
conditionsOp,
onApply: onApplyFilter,
onDelete: onDeleteFilter,
onUpdate: onUpdateFilter,
onConditionsOpChange,
onCondOpChange,
} = useFilters<RuleConditionEntity>();
const [dataCheck, setDataCheck] = useState(false);
@@ -44,11 +44,11 @@ export function Spending() {
setDataCheck(false);
return createSpendingSpreadsheet({
categories,
conditions,
conditions: filters,
conditionsOp,
setDataCheck,
});
}, [categories, conditions, conditionsOp]);
}, [categories, filters, conditionsOp]);
const data = useReport('default', getGraphData);
const navigate = useNavigate();
@@ -100,7 +100,7 @@ export function Spending() {
flexShrink: 0,
}}
>
{conditions && (
{filters && (
<View style={{ flexDirection: 'row' }}>
<FilterButton
onApply={onApplyFilter}
@@ -126,7 +126,7 @@ export function Spending() {
flexGrow: 1,
}}
>
{conditions && conditions.length > 0 && (
{filters && filters.length > 0 && (
<View
style={{
marginBottom: 10,
@@ -139,11 +139,11 @@ export function Spending() {
}}
>
<AppliedFilters
conditions={conditions}
filters={filters}
onUpdate={onUpdateFilter}
onDelete={onDeleteFilter}
conditionsOp={conditionsOp}
onConditionsOpChange={onConditionsOpChange}
onCondOpChange={onCondOpChange}
/>
</View>
)}

View File

@@ -2,7 +2,6 @@ import {
type LegendEntity,
type IntervalEntity,
type GroupedEntity,
type balanceTypeOpType,
} from 'loot-core/src/types/models/reports';
import { theme } from '../../../style';
@@ -13,7 +12,7 @@ export function calculateLegend(
calcDataFiltered: GroupedEntity[],
groupBy: string,
graphType?: string,
balanceTypeOp?: balanceTypeOpType,
balanceTypeOp?: 'totalAssets' | 'totalDebts' | 'totalTotals',
): LegendEntity[] {
const colorScale = getColorScale('qualitative');
const chooseData =

View File

@@ -13,7 +13,6 @@ import {
type CategoryGroupEntity,
} from 'loot-core/src/types/models';
import {
type balanceTypeOpType,
type DataEntity,
type GroupedEntity,
type IntervalEntity,
@@ -47,7 +46,7 @@ export type createCustomSpreadsheetProps = {
showHiddenCategories: boolean;
showUncategorized: boolean;
groupBy?: string;
balanceTypeOp?: balanceTypeOpType;
balanceTypeOp?: 'totalAssets' | 'totalDebts' | 'totalTotals';
payees?: PayeeEntity[];
accounts?: AccountEntity[];
graphType?: string;
@@ -154,16 +153,11 @@ export function createCustomSpreadsheet({
let totalAssets = 0;
let totalDebts = 0;
let netAssets = 0;
let netDebts = 0;
const intervalData = intervals.reduce(
(arr: IntervalEntity[], intervalItem, index) => {
let perIntervalAssets = 0;
let perIntervalDebts = 0;
let perIntervalNetAssets = 0;
let perIntervalNetDebts = 0;
let perIntervalTotals = 0;
const stacked: Record<string, number> = {};
groupByList.map(item => {
@@ -199,43 +193,20 @@ export function createCustomSpreadsheet({
.reduce((a, v) => (a = a + v.amount), 0);
perIntervalDebts += intervalDebts;
const netAmounts = intervalAssets + intervalDebts;
if (balanceTypeOp === 'totalAssets') {
stackAmounts += intervalAssets;
}
if (balanceTypeOp === 'totalDebts') {
stackAmounts += Math.abs(intervalDebts);
}
if (balanceTypeOp === 'netAssets') {
stackAmounts += netAmounts > 0 ? netAmounts : 0;
}
if (balanceTypeOp === 'netDebts') {
stackAmounts = netAmounts < 0 ? Math.abs(netAmounts) : 0;
}
if (balanceTypeOp === 'totalTotals') {
stackAmounts += netAmounts;
stackAmounts += intervalDebts;
}
if (stackAmounts !== 0) {
stacked[item.name] = integerToAmount(stackAmounts);
stacked[item.name] = integerToAmount(Math.abs(stackAmounts));
}
perIntervalNetAssets =
netAmounts > 0
? perIntervalNetAssets + netAmounts
: perIntervalNetAssets;
perIntervalNetDebts =
netAmounts < 0
? perIntervalNetDebts + netAmounts
: perIntervalNetDebts;
perIntervalTotals += netAmounts;
return null;
});
totalAssets += perIntervalAssets;
totalDebts += perIntervalDebts;
netAssets += perIntervalNetAssets;
netDebts += perIntervalNetDebts;
arr.push({
date: d.format(
@@ -248,11 +219,9 @@ export function createCustomSpreadsheet({
index + 1 === intervals.length
? endDate
: monthUtils.subDays(intervals[index + 1], 1),
totalAssets: integerToAmount(perIntervalAssets),
totalDebts: integerToAmount(perIntervalDebts),
netAssets: integerToAmount(perIntervalNetAssets),
netDebts: integerToAmount(perIntervalNetDebts),
totalTotals: integerToAmount(perIntervalTotals),
totalAssets: integerToAmount(perIntervalAssets),
totalTotals: integerToAmount(perIntervalDebts + perIntervalAssets),
});
return arr;
@@ -293,10 +262,8 @@ export function createCustomSpreadsheet({
legend,
startDate,
endDate,
totalAssets: integerToAmount(totalAssets),
totalDebts: integerToAmount(totalDebts),
netAssets: integerToAmount(netAssets),
netDebts: integerToAmount(netDebts),
totalAssets: integerToAmount(totalAssets),
totalTotals: integerToAmount(totalAssets + totalDebts),
});
setDataCheck?.(true);

View File

@@ -1,7 +1,4 @@
import {
type balanceTypeOpType,
type GroupedEntity,
} from 'loot-core/src/types/models/reports';
import { type GroupedEntity } from 'loot-core/src/types/models/reports';
export function filterEmptyRows({
showEmpty,
@@ -10,7 +7,7 @@ export function filterEmptyRows({
}: {
showEmpty: boolean;
data: GroupedEntity;
balanceTypeOp?: balanceTypeOpType;
balanceTypeOp?: 'totalAssets' | 'totalDebts' | 'totalTotals';
}): boolean {
let showHide: boolean;
if (balanceTypeOp === 'totalTotals') {

View File

@@ -111,16 +111,11 @@ export function createGroupedSpreadsheet({
group => {
let totalAssets = 0;
let totalDebts = 0;
let netAssets = 0;
let netDebts = 0;
const intervalData = intervals.reduce(
(arr: IntervalEntity[], intervalItem) => {
let groupedAssets = 0;
let groupedDebts = 0;
let groupedNetAssets = 0;
let groupedNetDebts = 0;
let groupedTotals = 0;
if (!group.categories) {
return [];
@@ -156,32 +151,16 @@ export function createGroupedSpreadsheet({
)
.reduce((a, v) => (a = a + v.amount), 0);
groupedDebts += intervalDebts;
const intervalTotals = intervalAssets + intervalDebts;
groupedNetAssets =
intervalTotals > 0
? groupedNetAssets + intervalTotals
: groupedNetAssets;
groupedNetDebts =
intervalTotals < 0
? groupedNetDebts + intervalTotals
: groupedNetDebts;
groupedTotals += intervalTotals;
});
totalAssets += groupedAssets;
totalDebts += groupedDebts;
netAssets += groupedNetAssets;
netDebts += groupedNetDebts;
arr.push({
date: intervalItem,
totalAssets: integerToAmount(groupedAssets),
totalDebts: integerToAmount(groupedDebts),
netAssets: integerToAmount(groupedNetAssets),
netDebts: integerToAmount(groupedNetDebts),
totalTotals: integerToAmount(groupedTotals),
totalTotals: integerToAmount(groupedDebts + groupedAssets),
});
return arr;
@@ -212,8 +191,6 @@ export function createGroupedSpreadsheet({
name: group.name,
totalAssets: integerToAmount(totalAssets),
totalDebts: integerToAmount(totalDebts),
netAssets: integerToAmount(netAssets),
netDebts: integerToAmount(netDebts),
totalTotals: integerToAmount(totalAssets + totalDebts),
intervalData,
categories:

View File

@@ -73,18 +73,14 @@ export function recalculate({
.reduce((a, v) => (a = a + v.amount), 0);
totalDebts += intervalDebts;
const intervalTotals = intervalAssets + intervalDebts;
const change = last
? intervalTotals - amountToInteger(last.totalTotals)
? intervalAssets + intervalDebts - amountToInteger(last.totalTotals)
: 0;
arr.push({
totalAssets: integerToAmount(intervalAssets),
totalDebts: integerToAmount(intervalDebts),
netAssets: intervalTotals > 0 ? integerToAmount(intervalTotals) : 0,
netDebts: intervalTotals < 0 ? integerToAmount(intervalTotals) : 0,
totalTotals: integerToAmount(intervalTotals),
totalTotals: integerToAmount(intervalAssets + intervalDebts),
change,
intervalStartDate: index === 0 ? startDate : intervalItem,
intervalEndDate:
@@ -98,16 +94,12 @@ export function recalculate({
[],
);
const totalTotals = totalAssets + totalDebts;
return {
id: item.id || '',
name: item.name,
totalAssets: integerToAmount(totalAssets),
totalDebts: integerToAmount(totalDebts),
netAssets: totalTotals > 0 ? integerToAmount(totalTotals) : 0,
netDebts: totalTotals < 0 ? integerToAmount(totalTotals) : 0,
totalTotals: integerToAmount(totalTotals),
totalTotals: integerToAmount(totalAssets + totalDebts),
intervalData,
};
}

Some files were not shown because too many files have changed in this diff Show More