mirror of
https://github.com/actualbudget/actual.git
synced 2026-04-28 18:40:34 -05:00
(typescript) Refactoring the mobile TransactionListWithBalance component into typescript. (#4061)
* refactor: convert txListwBal to tsx * docs: add release notes * docs: rename notes * refactor: fix missing cleared/uncleared balance * refactor: use Binding type
This commit is contained in:
@@ -1,197 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { SelectedProvider, useSelected } from '../../../hooks/useSelected';
|
||||
import { SvgSearchAlternate } from '../../../icons/v2';
|
||||
import { styles, theme } from '../../../style';
|
||||
import { InputWithContent } from '../../common/InputWithContent';
|
||||
import { Label } from '../../common/Label';
|
||||
import { View } from '../../common/View';
|
||||
import { CellValue, CellValueText } from '../../spreadsheet/CellValue';
|
||||
import { useSheetValue } from '../../spreadsheet/useSheetValue';
|
||||
import { PullToRefresh } from '../PullToRefresh';
|
||||
|
||||
import { TransactionList } from './TransactionList';
|
||||
|
||||
function TransactionSearchInput({ placeholder, onSearch }) {
|
||||
const [text, setText] = useState('');
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
backgroundColor: theme.mobilePageBackground,
|
||||
padding: 10,
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
<InputWithContent
|
||||
leftContent={
|
||||
<SvgSearchAlternate
|
||||
style={{
|
||||
width: 13,
|
||||
height: 13,
|
||||
flexShrink: 0,
|
||||
color: text ? theme.formInputTextHighlight : 'inherit',
|
||||
margin: 5,
|
||||
marginRight: 0,
|
||||
}}
|
||||
/>
|
||||
}
|
||||
value={text}
|
||||
onChangeValue={text => {
|
||||
setText(text);
|
||||
onSearch(text);
|
||||
}}
|
||||
placeholder={placeholder}
|
||||
style={{
|
||||
backgroundColor: theme.tableBackground,
|
||||
border: `1px solid ${theme.formInputBorder}`,
|
||||
flex: 1,
|
||||
height: styles.mobileMinHeight,
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
export function TransactionListWithBalances({
|
||||
isLoading,
|
||||
transactions,
|
||||
balance,
|
||||
balanceCleared,
|
||||
balanceUncleared,
|
||||
searchPlaceholder = 'Search...',
|
||||
onSearch,
|
||||
isLoadingMore,
|
||||
onLoadMore,
|
||||
onOpenTransaction,
|
||||
onRefresh,
|
||||
}) {
|
||||
const selectedInst = useSelected('transactions', transactions);
|
||||
|
||||
return (
|
||||
<SelectedProvider instance={selectedInst}>
|
||||
<View
|
||||
style={{
|
||||
flexShrink: 0,
|
||||
marginTop: 10,
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-evenly',
|
||||
}}
|
||||
>
|
||||
{balanceCleared && balanceUncleared ? (
|
||||
<BalanceWithCleared
|
||||
balance={balance}
|
||||
balanceCleared={balanceCleared}
|
||||
balanceUncleared={balanceUncleared}
|
||||
/>
|
||||
) : (
|
||||
<Balance balance={balance} />
|
||||
)}
|
||||
</View>
|
||||
<TransactionSearchInput
|
||||
placeholder={searchPlaceholder}
|
||||
onSearch={onSearch}
|
||||
/>
|
||||
</View>
|
||||
<PullToRefresh isPullable={!!onRefresh} onRefresh={onRefresh}>
|
||||
<TransactionList
|
||||
isLoading={isLoading}
|
||||
transactions={transactions}
|
||||
isLoadingMore={isLoadingMore}
|
||||
onLoadMore={onLoadMore}
|
||||
onOpenTransaction={onOpenTransaction}
|
||||
/>
|
||||
</PullToRefresh>
|
||||
</SelectedProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function BalanceWithCleared({ balanceUncleared, balanceCleared, balance }) {
|
||||
const { t } = useTranslation();
|
||||
const unclearedAmount = useSheetValue(balanceUncleared);
|
||||
|
||||
return (
|
||||
<>
|
||||
<View
|
||||
style={{
|
||||
display: !unclearedAmount ? 'none' : undefined,
|
||||
flexBasis: '33%',
|
||||
}}
|
||||
>
|
||||
<Label
|
||||
title={t('Cleared')}
|
||||
style={{ textAlign: 'center', fontSize: 12 }}
|
||||
/>
|
||||
<CellValue binding={balanceCleared} type="financial">
|
||||
{props => (
|
||||
<CellValueText
|
||||
{...props}
|
||||
style={{
|
||||
fontSize: 12,
|
||||
textAlign: 'center',
|
||||
fontWeight: '500',
|
||||
}}
|
||||
data-testid="transactions-balance-cleared"
|
||||
/>
|
||||
)}
|
||||
</CellValue>
|
||||
</View>
|
||||
<Balance balance={balance} />
|
||||
<View
|
||||
style={{
|
||||
display: !unclearedAmount ? 'none' : undefined,
|
||||
flexBasis: '33%',
|
||||
}}
|
||||
>
|
||||
<Label
|
||||
title={t('Uncleared')}
|
||||
style={{ textAlign: 'center', fontSize: 12 }}
|
||||
/>
|
||||
<CellValue binding={balanceUncleared} type="financial">
|
||||
{props => (
|
||||
<CellValueText
|
||||
{...props}
|
||||
style={{
|
||||
fontSize: 12,
|
||||
textAlign: 'center',
|
||||
fontWeight: '500',
|
||||
}}
|
||||
data-testid="transactions-balance-uncleared"
|
||||
/>
|
||||
)}
|
||||
</CellValue>
|
||||
</View>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function Balance({ balance }) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<View style={{ flexBasis: '33%' }}>
|
||||
<Label title={t('Balance')} style={{ textAlign: 'center' }} />
|
||||
<CellValue binding={balance} type="financial">
|
||||
{props => (
|
||||
<CellValueText
|
||||
{...props}
|
||||
style={{
|
||||
fontSize: 18,
|
||||
textAlign: 'center',
|
||||
fontWeight: '500',
|
||||
color:
|
||||
props.value < 0 ? theme.errorText : theme.pillTextHighlighted,
|
||||
}}
|
||||
data-testid="transactions-balance"
|
||||
/>
|
||||
)}
|
||||
</CellValue>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,277 @@
|
||||
import React, { type ComponentProps, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { type TransactionEntity } from 'loot-core/types/models/transaction';
|
||||
|
||||
import { SelectedProvider, useSelected } from '../../../hooks/useSelected';
|
||||
import { SvgSearchAlternate } from '../../../icons/v2';
|
||||
import { styles, theme } from '../../../style';
|
||||
import { InputWithContent } from '../../common/InputWithContent';
|
||||
import { Label } from '../../common/Label';
|
||||
import { View } from '../../common/View';
|
||||
import type { Binding, SheetNames, SheetFields } from '../../spreadsheet';
|
||||
import { CellValue, CellValueText } from '../../spreadsheet/CellValue';
|
||||
import { useSheetValue } from '../../spreadsheet/useSheetValue';
|
||||
import { PullToRefresh } from '../PullToRefresh';
|
||||
|
||||
import { TransactionList } from './TransactionList';
|
||||
|
||||
type TransactionSearchInputProps = {
|
||||
placeholder: string;
|
||||
onSearch: TransactionListWithBalancesProps['onSearch'];
|
||||
};
|
||||
|
||||
function TransactionSearchInput({
|
||||
placeholder,
|
||||
onSearch,
|
||||
}: TransactionSearchInputProps) {
|
||||
const [text, setText] = useState('');
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
backgroundColor: theme.mobilePageBackground,
|
||||
padding: 10,
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
<InputWithContent
|
||||
leftContent={
|
||||
<SvgSearchAlternate
|
||||
style={{
|
||||
width: 13,
|
||||
height: 13,
|
||||
flexShrink: 0,
|
||||
color: text ? theme.formInputTextHighlight : 'inherit',
|
||||
margin: 5,
|
||||
marginRight: 0,
|
||||
}}
|
||||
/>
|
||||
}
|
||||
value={text}
|
||||
onChangeValue={text => {
|
||||
setText(text);
|
||||
onSearch(text);
|
||||
}}
|
||||
placeholder={placeholder}
|
||||
style={{
|
||||
backgroundColor: theme.tableBackground,
|
||||
border: `1px solid ${theme.formInputBorder}`,
|
||||
flex: 1,
|
||||
height: styles.mobileMinHeight,
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
type TransactionListWithBalancesProps = {
|
||||
isLoading: boolean;
|
||||
transactions: readonly TransactionEntity[];
|
||||
balance:
|
||||
| Binding<'account', 'onbudget-accounts-balance'>
|
||||
| Binding<'account', 'offbudget-accounts-balance'>
|
||||
| Binding<SheetNames, 'uncategorized-balance'>
|
||||
| Binding<'category', 'balance'>
|
||||
| Binding<'account', 'balance'>
|
||||
| Binding<'account', 'accounts-balance'>;
|
||||
balanceCleared?:
|
||||
| Binding<'category', 'balanceCleared'>
|
||||
| Binding<'account', 'balanceCleared'>;
|
||||
balanceUncleared?:
|
||||
| Binding<'category', 'balanceUncleared'>
|
||||
| Binding<'account', 'balanceUncleared'>;
|
||||
searchPlaceholder: string;
|
||||
onSearch: (searchText: string) => void;
|
||||
isLoadingMore: boolean;
|
||||
onLoadMore: () => void;
|
||||
onOpenTransaction: (transaction: TransactionEntity) => void;
|
||||
onRefresh?: () => void;
|
||||
};
|
||||
|
||||
export function TransactionListWithBalances({
|
||||
isLoading,
|
||||
transactions,
|
||||
balance,
|
||||
balanceCleared,
|
||||
balanceUncleared,
|
||||
searchPlaceholder = 'Search...',
|
||||
onSearch,
|
||||
isLoadingMore,
|
||||
onLoadMore,
|
||||
onOpenTransaction,
|
||||
onRefresh,
|
||||
}: TransactionListWithBalancesProps) {
|
||||
const selectedInst = useSelected('transactions', [...transactions], []);
|
||||
|
||||
return (
|
||||
<SelectedProvider instance={selectedInst}>
|
||||
<>
|
||||
<View
|
||||
style={{
|
||||
flexShrink: 0,
|
||||
marginTop: 10,
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-evenly',
|
||||
}}
|
||||
>
|
||||
{balanceCleared && balanceUncleared ? (
|
||||
<BalanceWithCleared
|
||||
balance={balance}
|
||||
balanceCleared={balanceCleared}
|
||||
balanceUncleared={balanceUncleared}
|
||||
/>
|
||||
) : (
|
||||
<Balance balance={balance} />
|
||||
)}
|
||||
</View>
|
||||
<TransactionSearchInput
|
||||
placeholder={searchPlaceholder}
|
||||
onSearch={onSearch}
|
||||
/>
|
||||
</View>
|
||||
<PullToRefresh
|
||||
isPullable={!!onRefresh}
|
||||
onRefresh={async () => onRefresh?.()}
|
||||
>
|
||||
<TransactionList
|
||||
isLoading={isLoading}
|
||||
transactions={transactions}
|
||||
isLoadingMore={isLoadingMore}
|
||||
onLoadMore={onLoadMore}
|
||||
onOpenTransaction={onOpenTransaction}
|
||||
/>
|
||||
</PullToRefresh>
|
||||
</>
|
||||
</SelectedProvider>
|
||||
);
|
||||
}
|
||||
|
||||
const TransactionListBalanceCellValue = <
|
||||
FieldName extends SheetFields<'account'> | SheetFields<'category'>,
|
||||
>(
|
||||
props: ComponentProps<
|
||||
typeof CellValue<
|
||||
FieldName extends SheetFields<'account'> ? 'account' : 'category',
|
||||
FieldName
|
||||
>
|
||||
>,
|
||||
) => {
|
||||
return <CellValue {...props} />;
|
||||
};
|
||||
|
||||
type BalanceWithClearedProps = {
|
||||
balanceUncleared: NonNullable<
|
||||
TransactionListWithBalancesProps['balanceUncleared']
|
||||
>;
|
||||
balanceCleared: NonNullable<
|
||||
TransactionListWithBalancesProps['balanceCleared']
|
||||
>;
|
||||
balance: TransactionListWithBalancesProps['balance'];
|
||||
};
|
||||
|
||||
function BalanceWithCleared({
|
||||
balanceUncleared,
|
||||
balanceCleared,
|
||||
balance,
|
||||
}: BalanceWithClearedProps) {
|
||||
const { t } = useTranslation();
|
||||
const unclearedAmount = useSheetValue<
|
||||
'account' | 'category',
|
||||
'balanceUncleared'
|
||||
>(balanceUncleared);
|
||||
|
||||
return (
|
||||
<>
|
||||
<View
|
||||
style={{
|
||||
display: !unclearedAmount ? 'none' : undefined,
|
||||
flexBasis: '33%',
|
||||
}}
|
||||
>
|
||||
<Label
|
||||
title={t('Cleared')}
|
||||
style={{ textAlign: 'center', fontSize: 12 }}
|
||||
/>
|
||||
<TransactionListBalanceCellValue
|
||||
binding={balanceCleared}
|
||||
type="financial"
|
||||
>
|
||||
{props => (
|
||||
<CellValueText
|
||||
{...props}
|
||||
style={{
|
||||
fontSize: 12,
|
||||
textAlign: 'center',
|
||||
fontWeight: '500',
|
||||
}}
|
||||
data-testid="transactions-balance-cleared"
|
||||
/>
|
||||
)}
|
||||
</TransactionListBalanceCellValue>
|
||||
</View>
|
||||
<Balance balance={balance} />
|
||||
<View
|
||||
style={{
|
||||
display: !unclearedAmount ? 'none' : undefined,
|
||||
flexBasis: '33%',
|
||||
}}
|
||||
>
|
||||
<Label
|
||||
title={t('Uncleared')}
|
||||
style={{ textAlign: 'center', fontSize: 12 }}
|
||||
/>
|
||||
<TransactionListBalanceCellValue
|
||||
binding={balanceUncleared}
|
||||
type="financial"
|
||||
>
|
||||
{props => (
|
||||
<CellValueText
|
||||
{...props}
|
||||
style={{
|
||||
fontSize: 12,
|
||||
textAlign: 'center',
|
||||
fontWeight: '500',
|
||||
}}
|
||||
data-testid="transactions-balance-uncleared"
|
||||
/>
|
||||
)}
|
||||
</TransactionListBalanceCellValue>
|
||||
</View>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
type BalanceProps = {
|
||||
balance: TransactionListWithBalancesProps['balance'];
|
||||
};
|
||||
|
||||
function Balance({ balance }: BalanceProps) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<View style={{ flexBasis: '33%' }}>
|
||||
<Label title={t('Balance')} style={{ textAlign: 'center' }} />
|
||||
<TransactionListBalanceCellValue binding={balance} type="financial">
|
||||
{props => (
|
||||
<CellValueText
|
||||
{...props}
|
||||
style={{
|
||||
fontSize: 18,
|
||||
textAlign: 'center',
|
||||
fontWeight: '500',
|
||||
color:
|
||||
props.value < 0 ? theme.errorText : theme.pillTextHighlighted,
|
||||
}}
|
||||
data-testid="transactions-balance"
|
||||
/>
|
||||
)}
|
||||
</TransactionListBalanceCellValue>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -15,6 +15,15 @@ export type Spreadsheets = {
|
||||
balanceCleared: number;
|
||||
balanceUncleared: number;
|
||||
};
|
||||
category: {
|
||||
// Common fields
|
||||
'uncategorized-amount': number;
|
||||
'uncategorized-balance': number;
|
||||
|
||||
balance: number;
|
||||
balanceCleared: number;
|
||||
balanceUncleared: number;
|
||||
};
|
||||
'envelope-budget': {
|
||||
// Common fields
|
||||
'uncategorized-amount': number;
|
||||
|
||||
@@ -25,6 +25,7 @@ type BudgetType<SheetName extends SheetNames> = Record<
|
||||
>;
|
||||
|
||||
const accountParametrizedField = parametrizedField<'account'>();
|
||||
const categoryParametrizedField = parametrizedField<'category'>();
|
||||
const envelopeParametrizedField = parametrizedField<'envelope-budget'>();
|
||||
const trackingParametrizedField = parametrizedField<'tracking-budget'>();
|
||||
|
||||
@@ -179,7 +180,7 @@ export function offBudgetAccountBalance() {
|
||||
|
||||
export function categoryBalance(category: CategoryEntity, month: string) {
|
||||
return {
|
||||
name: `balance-${category.id}`,
|
||||
name: categoryParametrizedField('balance')(category.id),
|
||||
query: q('transactions')
|
||||
.filter({
|
||||
category: category.id,
|
||||
@@ -187,7 +188,7 @@ export function categoryBalance(category: CategoryEntity, month: string) {
|
||||
})
|
||||
.options({ splits: 'inline' })
|
||||
.calculate({ $sum: '$amount' }),
|
||||
};
|
||||
} satisfies Binding<'category', 'balance'>;
|
||||
}
|
||||
|
||||
export function categoryBalanceCleared(
|
||||
@@ -195,7 +196,7 @@ export function categoryBalanceCleared(
|
||||
month: string,
|
||||
) {
|
||||
return {
|
||||
name: `balanceCleared-${category.id}`,
|
||||
name: categoryParametrizedField('balanceCleared')(category.id),
|
||||
query: q('transactions')
|
||||
.filter({
|
||||
category: category.id,
|
||||
@@ -204,7 +205,7 @@ export function categoryBalanceCleared(
|
||||
})
|
||||
.options({ splits: 'inline' })
|
||||
.calculate({ $sum: '$amount' }),
|
||||
};
|
||||
} satisfies Binding<'category', 'balanceCleared'>;
|
||||
}
|
||||
|
||||
export function categoryBalanceUncleared(
|
||||
@@ -212,7 +213,7 @@ export function categoryBalanceUncleared(
|
||||
month: string,
|
||||
) {
|
||||
return {
|
||||
name: `balanceUncleared-${category.id}`,
|
||||
name: categoryParametrizedField('balanceUncleared')(category.id),
|
||||
query: q('transactions')
|
||||
.filter({
|
||||
category: category.id,
|
||||
@@ -221,7 +222,7 @@ export function categoryBalanceUncleared(
|
||||
})
|
||||
.options({ splits: 'inline' })
|
||||
.calculate({ $sum: '$amount' }),
|
||||
};
|
||||
} satisfies Binding<'category', 'balanceUncleared'>;
|
||||
}
|
||||
|
||||
const uncategorizedQuery = q('transactions').filter({
|
||||
@@ -235,11 +236,11 @@ const uncategorizedQuery = q('transactions').filter({
|
||||
],
|
||||
});
|
||||
|
||||
export function uncategorizedBalance() {
|
||||
export function uncategorizedBalance<SheetName extends SheetNames>() {
|
||||
return {
|
||||
name: 'uncategorized-balance',
|
||||
query: uncategorizedQuery.calculate({ $sum: '$amount' }),
|
||||
};
|
||||
} satisfies Binding<SheetName, 'uncategorized-balance'>;
|
||||
}
|
||||
|
||||
export function uncategorizedCount<SheetName extends SheetNames>() {
|
||||
|
||||
6
upcoming-release-notes/4061.md
Normal file
6
upcoming-release-notes/4061.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
category: Maintenance
|
||||
authors: [leoltl]
|
||||
---
|
||||
|
||||
Refactoring the mobile TransactionListWithBalance component into typescript
|
||||
Reference in New Issue
Block a user