[Mobile] Show transactions upon clicking income categories (#4940)

* [Mobile] Show transactions upon clicking income categories

* Fix lint error
This commit is contained in:
Joel Jeremy Marquez
2025-05-06 14:09:40 -07:00
committed by GitHub
parent 090345bd95
commit 3a18718fa0
8 changed files with 302 additions and 227 deletions

View File

@@ -81,11 +81,18 @@ type BalanceWithCarryoverProps = Omit<
'children' | 'binding'
> & {
children?: ChildrenWithClassName;
carryover: Binding<'envelope-budget', 'carryover'>;
balance: Binding<'envelope-budget', 'leftover'>;
goal: Binding<'envelope-budget', 'goal'>;
budgeted: Binding<'envelope-budget', 'budget'>;
longGoal: Binding<'envelope-budget', 'long-goal'>;
carryover: Binding<'envelope-budget' | 'tracking-budget', 'carryover'>;
/**
* Expense category balance binding is `leftover`,
* while income category balance binding is `sum-amount`.
*/
balance: Binding<
'envelope-budget' | 'tracking-budget',
'leftover' | 'sum-amount'
>;
goal: Binding<'envelope-budget' | 'tracking-budget', 'goal'>;
budgeted: Binding<'envelope-budget' | 'tracking-budget', 'budget'>;
longGoal: Binding<'envelope-budget' | 'tracking-budget', 'long-goal'>;
isDisabled?: boolean;
isMobileEnvelopeModal?: boolean;
CarryoverIndicator?: ComponentType<CarryoverIndicatorProps>;

View File

@@ -514,21 +514,13 @@ export function IncomeCategoryMonth({
}}
>
<span onClick={() => onShowActivity(category.id, month)}>
<EnvelopeCellValue
binding={envelopeBudget.catSumAmount(category.id)}
type="financial"
>
{props => (
<CellValueText
{...props}
className={css({
cursor: 'pointer',
':hover': { textDecoration: 'underline' },
...makeAmountGrey(props.value),
})}
/>
)}
</EnvelopeCellValue>
<BalanceWithCarryover
carryover={envelopeBudget.catCarryover(category.id)}
balance={envelopeBudget.catSumAmount(category.id)}
goal={envelopeBudget.catGoal(category.id)}
budgeted={envelopeBudget.catBudgeted(category.id)}
longGoal={envelopeBudget.catLongGoal(category.id)}
/>
</span>
</Field>
</View>

View File

@@ -0,0 +1,134 @@
import { type CSSProperties } from 'react';
import { useTranslation } from 'react-i18next';
import { Button } from '@actual-app/components/button';
import { SvgArrowThickRight } from '@actual-app/components/icons/v1';
import { Text } from '@actual-app/components/text';
import { theme } from '@actual-app/components/theme';
import { View } from '@actual-app/components/view';
import { css, cx } from '@emotion/css';
import { AutoTextSize } from 'auto-text-size';
import { envelopeBudget, trackingBudget } from 'loot-core/client/queries';
import { type CategoryEntity } from 'loot-core/types/models';
import { getColumnWidth, PILL_STYLE } from './BudgetTable';
import { BalanceWithCarryover } from '@desktop-client/components/budget/BalanceWithCarryover';
import { PrivacyFilter } from '@desktop-client/components/PrivacyFilter';
import { type Binding } from '@desktop-client/components/spreadsheet';
import { useFormat } from '@desktop-client/components/spreadsheet/useFormat';
import { useSyncedPref } from '@desktop-client/hooks/useSyncedPref';
type BalanceCellProps = {
binding: Binding<
'envelope-budget' | 'tracking-budget',
'leftover' | 'sum-amount'
>;
category: CategoryEntity;
show3Columns?: boolean;
onPress?: () => void;
'aria-label'?: string;
};
export function BalanceCell({
binding,
category,
show3Columns,
onPress,
'aria-label': ariaLabel,
}: BalanceCellProps) {
const { t } = useTranslation();
const [budgetType = 'rollover'] = useSyncedPref('budgetType');
const columnWidth = getColumnWidth({
show3Columns,
});
const goal =
budgetType === 'report'
? trackingBudget.catGoal(category.id)
: envelopeBudget.catGoal(category.id);
const longGoal =
budgetType === 'report'
? trackingBudget.catLongGoal(category.id)
: envelopeBudget.catLongGoal(category.id);
const budgeted =
budgetType === 'report'
? trackingBudget.catBudgeted(category.id)
: envelopeBudget.catBudgeted(category.id);
const carryover =
budgetType === 'report'
? trackingBudget.catCarryover(category.id)
: envelopeBudget.catCarryover(category.id);
const format = useFormat();
return (
<BalanceWithCarryover
aria-label={t('Balance for {{categoryName}} category', {
categoryName: category.name,
})} // Translated aria-label
type="financial"
carryover={carryover}
balance={binding}
goal={goal}
budgeted={budgeted}
longGoal={longGoal}
CarryoverIndicator={MobileCarryoverIndicator}
>
{({ type, value, className: defaultClassName }) => (
<Button
variant="bare"
style={{
...PILL_STYLE,
maxWidth: columnWidth,
}}
onPress={onPress}
aria-label={ariaLabel}
>
<PrivacyFilter>
<AutoTextSize
key={value}
as={Text}
minFontSizePx={6}
maxFontSizePx={12}
mode="oneline"
className={cx(
defaultClassName,
css({
maxWidth: columnWidth,
textAlign: 'right',
fontSize: 12,
}),
)}
>
{format(value, type)}
</AutoTextSize>
</PrivacyFilter>
</Button>
)}
</BalanceWithCarryover>
);
}
function MobileCarryoverIndicator({ style }: { style?: CSSProperties }) {
return (
<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>
);
}

View File

@@ -4,7 +4,6 @@ import { useTranslation } from 'react-i18next';
import { Button } from '@actual-app/components/button';
import { type CSSProperties } from '@actual-app/components/styles';
import { Text } from '@actual-app/components/text';
import { View } from '@actual-app/components/view';
import { AutoTextSize } from 'auto-text-size';
import { pushModal } from 'loot-core/client/modals/modalsSlice';
@@ -147,24 +146,22 @@ export function BudgetCell<
categoryName: category.name,
})}
>
<View>
<PrivacyFilter>
<AutoTextSize
key={value}
as={Text}
minFontSizePx={6}
maxFontSizePx={12}
mode="oneline"
style={{
maxWidth: columnWidth,
textAlign: 'right',
fontSize: 12,
}}
>
{format(value, type)}
</AutoTextSize>
</PrivacyFilter>
</View>
<PrivacyFilter>
<AutoTextSize
key={value}
as={Text}
minFontSizePx={6}
maxFontSizePx={12}
mode="oneline"
style={{
maxWidth: columnWidth,
textAlign: 'right',
fontSize: 12,
}}
>
{format(value, type)}
</AutoTextSize>
</PrivacyFilter>
</Button>
)
}

View File

@@ -3,15 +3,11 @@ import { GridListItem } from 'react-aria-components';
import { useTranslation } from 'react-i18next';
import { Button } from '@actual-app/components/button';
import {
SvgArrowThickRight,
SvgCheveronRight,
} from '@actual-app/components/icons/v1';
import { SvgCheveronRight } from '@actual-app/components/icons/v1';
import { styles, type CSSProperties } from '@actual-app/components/styles';
import { Text } from '@actual-app/components/text';
import { theme } from '@actual-app/components/theme';
import { View } from '@actual-app/components/view';
import { AutoTextSize } from 'auto-text-size';
import { collapseModals, pushModal } from 'loot-core/client/modals/modalsSlice';
import { envelopeBudget, trackingBudget } from 'loot-core/client/queries';
@@ -20,20 +16,16 @@ import { groupById, integerToCurrency } from 'loot-core/shared/util';
import { type CategoryEntity } from 'loot-core/types/models';
import { useCategories } from '../../../hooks/useCategories';
import { useFeatureFlag } from '../../../hooks/useFeatureFlag';
import { useNavigate } from '../../../hooks/useNavigate';
import { useSyncedPref } from '../../../hooks/useSyncedPref';
import { useUndo } from '../../../hooks/useUndo';
import { useDispatch } from '../../../redux';
import { BalanceWithCarryover } from '../../budget/BalanceWithCarryover';
import { makeAmountGrey, makeBalanceAmountStyle } from '../../budget/util';
import { PrivacyFilter } from '../../PrivacyFilter';
import { CellValue } from '../../spreadsheet/CellValue';
import { useFormat } from '../../spreadsheet/useFormat';
import { useSheetValue } from '../../spreadsheet/useSheetValue';
import { BalanceCell } from './BalanceCell';
import { BudgetCell } from './BudgetCell';
import { getColumnWidth, PILL_STYLE, ROW_HEIGHT } from './BudgetTable';
import { getColumnWidth, ROW_HEIGHT } from './BudgetTable';
import { SpentCell } from './SpentCell';
type ExpenseCategoryNameProps = {
category: CategoryEntity;
@@ -125,24 +117,12 @@ function ExpenseCategoryCells({
onShowActivity,
}: ExpenseCategoryCellsProps) {
const { t } = useTranslation();
const format = useFormat();
const columnWidth = getColumnWidth({
show3Columns,
isSidebar: false,
});
const isGoalTemplatesEnabled = useFeatureFlag('goalTemplatesEnabled');
const [budgetType = 'rollover'] = useSyncedPref('budgetType');
const goal =
budgetType === 'report'
? trackingBudget.catGoal(category.id)
: envelopeBudget.catGoal(category.id);
const longGoal =
budgetType === 'report'
? trackingBudget.catLongGoal(category.id)
: envelopeBudget.catLongGoal(category.id);
const budgeted =
budgetType === 'report'
? trackingBudget.catBudgeted(category.id)
@@ -158,34 +138,6 @@ function ExpenseCategoryCells({
? trackingBudget.catBalance(category.id)
: envelopeBudget.catBalance(category.id);
const carryover =
budgetType === 'report'
? trackingBudget.catCarryover(category.id)
: envelopeBudget.catCarryover(category.id);
const goalTemp = useSheetValue<'envelope-budget' | 'tracking-budget', 'goal'>(
goal,
);
const goalValue = isGoalTemplatesEnabled ? goalTemp : null;
const budgetedtmp = useSheetValue<
'envelope-budget' | 'tracking-budget',
'budget'
>(budgeted);
const balancetmp = useSheetValue<
'envelope-budget' | 'tracking-budget',
'leftover'
>(balance);
const isLongGoal =
useSheetValue<'envelope-budget' | 'tracking-budget', 'long-goal'>(
longGoal,
) === 1;
const budgetedValue = isGoalTemplatesEnabled
? isLongGoal
? balancetmp
: budgetedtmp
: null;
return (
<View
style={{
@@ -218,44 +170,12 @@ function ExpenseCategoryCells({
alignItems: 'flex-end',
}}
>
<CellValue<'envelope-budget' | 'tracking-budget', 'sum-amount'>
<SpentCell
binding={spent}
type="financial"
aria-label={t('Spent amount for {{categoryName}} category', {
categoryName: category.name,
})} // Translated aria-label
>
{({ type, value }) => (
<Button
variant="bare"
style={{
...PILL_STYLE,
}}
onPress={onShowActivity}
aria-label={t('Show transactions for {{categoryName}} category', {
categoryName: category.name,
})} // Translated aria-label
>
<PrivacyFilter>
<AutoTextSize
key={value}
as={Text}
minFontSizePx={6}
maxFontSizePx={12}
mode="oneline"
style={{
...makeAmountGrey(value),
maxWidth: columnWidth,
textAlign: 'right',
fontSize: 12,
}}
>
{format(value, type)}
</AutoTextSize>
</PrivacyFilter>
</Button>
)}
</CellValue>
category={category}
show3Columns={show3Columns}
onPress={onShowActivity}
/>
</View>
<View
style={{
@@ -264,66 +184,15 @@ function ExpenseCategoryCells({
alignItems: 'flex-end',
}}
>
<BalanceWithCarryover
aria-label={t('Balance for {{categoryName}} category', {
<BalanceCell
binding={balance}
category={category}
show3Columns={show3Columns}
onPress={onOpenBalanceMenu}
aria-label={t('Open balance menu for {{categoryName}} category', {
categoryName: category.name,
})} // Translated aria-label
type="financial"
carryover={carryover}
balance={balance}
goal={goal}
budgeted={budgeted}
longGoal={longGoal}
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>
)}
>
{({ type, value }) => (
<Button
variant="bare"
style={{
...PILL_STYLE,
maxWidth: columnWidth,
}}
onPress={onOpenBalanceMenu}
aria-label={t('Open balance menu for {{categoryName}} category', {
categoryName: category.name,
})} // Translated aria-label
>
<PrivacyFilter>
<AutoTextSize
key={value}
as={Text}
minFontSizePx={6}
maxFontSizePx={12}
mode="oneline"
style={{
maxWidth: columnWidth,
...makeBalanceAmountStyle(value, goalValue, budgetedValue),
textAlign: 'right',
fontSize: 12,
}}
>
{format(value, type)}
</AutoTextSize>
</PrivacyFilter>
</Button>
)}
</BalanceWithCarryover>
})}
/>
</View>
</View>
);

View File

@@ -1,4 +1,4 @@
import { type ComponentPropsWithoutRef } from 'react';
import { useCallback, type ComponentPropsWithoutRef } from 'react';
import { GridListItem } from 'react-aria-components';
import { useTranslation } from 'react-i18next';
@@ -8,20 +8,18 @@ import { styles } from '@actual-app/components/styles';
import { Text } from '@actual-app/components/text';
import { theme } from '@actual-app/components/theme';
import { View } from '@actual-app/components/view';
import { AutoTextSize } from 'auto-text-size';
import { envelopeBudget, trackingBudget } from 'loot-core/client/queries';
import * as monthUtils from 'loot-core/shared/months';
import { type CategoryEntity } from 'loot-core/types/models';
import { useSyncedPref } from '../../../hooks/useSyncedPref';
import { PrivacyFilter } from '../../PrivacyFilter';
import { CellValue } from '../../spreadsheet/CellValue';
import { useFormat } from '../../spreadsheet/useFormat';
import { BalanceCell } from './BalanceCell';
import { BudgetCell } from './BudgetCell';
import { getColumnWidth, ROW_HEIGHT } from './BudgetTable';
import { useNavigate } from '@desktop-client/hooks/useNavigate';
import { useSyncedPref } from '@desktop-client/hooks/useSyncedPref';
type IncomeCategoryNameProps = {
category: CategoryEntity;
onEdit: (id: CategoryEntity['id']) => void;
@@ -92,20 +90,23 @@ type IncomeCategoryCellsProps = {
category: CategoryEntity;
month: string;
onBudgetAction: (month: string, action: string, args: unknown) => void;
onShowActivity: () => void;
};
function IncomeCategoryCells({
category,
month,
onBudgetAction,
onShowActivity,
}: IncomeCategoryCellsProps) {
const { t } = useTranslation();
const format = useFormat();
const columnWidth = getColumnWidth();
const [budgetType = 'rollover'] = useSyncedPref('budgetType');
const budgeted =
budgetType === 'report' ? trackingBudget.catBudgeted(category.id) : null;
budgetType === 'report'
? trackingBudget.catBudgeted(category.id)
: envelopeBudget.catBudgeted(category.id);
const balance =
budgetType === 'report'
@@ -120,7 +121,7 @@ function IncomeCategoryCells({
alignItems: 'center',
}}
>
{budgeted && (
{budgetType === 'report' && (
<View
style={{
width: columnWidth,
@@ -137,37 +138,22 @@ function IncomeCategoryCells({
/>
</View>
)}
<CellValue<'envelope-budget' | 'tracking-budget', 'sum-amount'>
binding={balance}
type="financial"
aria-label={t('Balance for {{categoryName}} category', {
categoryName: category.name,
})} // Translated aria-label
<View
style={{
width: columnWidth,
justifyContent: 'center',
alignItems: 'flex-end',
}}
>
{({ type, value }) => (
<View>
<PrivacyFilter>
<AutoTextSize
key={value}
as={Text}
minFontSizePx={6}
maxFontSizePx={12}
mode="oneline"
style={{
width: columnWidth,
justifyContent: 'center',
alignItems: 'flex-end',
textAlign: 'right',
fontSize: 12,
paddingRight: 5,
}}
>
{format(value, type)}
</AutoTextSize>
</PrivacyFilter>
</View>
)}
</CellValue>
<BalanceCell
binding={balance}
category={category}
onPress={onShowActivity}
aria-label={t('Show transactions for {{categoryName}} category', {
categoryName: category.name,
})}
/>
</View>
</View>
);
}
@@ -187,6 +173,14 @@ export function IncomeCategoryListItem({
...props
}: IncomeCategoryListItemProps) {
const { value: category } = props;
const navigate = useNavigate();
const onShowActivity = useCallback(() => {
if (!category) {
return;
}
navigate(`/categories/${category.id}?month=${month}`);
}, [category, month, navigate]);
if (!category) {
return null;
@@ -219,6 +213,7 @@ export function IncomeCategoryListItem({
category={category}
month={month}
onBudgetAction={onBudgetAction}
onShowActivity={onShowActivity}
/>
</View>
</GridListItem>

View File

@@ -0,0 +1,75 @@
import { useTranslation } from 'react-i18next';
import { Button } from '@actual-app/components/button';
import { Text } from '@actual-app/components/text';
import { AutoTextSize } from 'auto-text-size';
import { type CategoryEntity } from 'loot-core/types/models';
import { getColumnWidth, PILL_STYLE } from './BudgetTable';
import { makeAmountGrey } from '@desktop-client/components/budget/util';
import { PrivacyFilter } from '@desktop-client/components/PrivacyFilter';
import { type Binding } from '@desktop-client/components/spreadsheet';
import { CellValue } from '@desktop-client/components/spreadsheet/CellValue';
import { useFormat } from '@desktop-client/components/spreadsheet/useFormat';
type SpentCellProps = {
binding: Binding<'envelope-budget' | 'tracking-budget', 'sum-amount'>;
category: CategoryEntity;
show3Columns?: boolean;
onPress?: () => void;
};
export function SpentCell({
binding,
category,
show3Columns,
onPress,
}: SpentCellProps) {
const { t } = useTranslation();
const format = useFormat();
const columnWidth = getColumnWidth({
show3Columns,
});
return (
<CellValue<'envelope-budget' | 'tracking-budget', 'sum-amount'>
binding={binding}
type="financial"
aria-label={t('Spent amount for {{categoryName}} category', {
categoryName: category.name,
})}
>
{({ type, value }) => (
<Button
variant="bare"
style={{
...PILL_STYLE,
}}
onPress={onPress}
aria-label={t('Show transactions for {{categoryName}} category', {
categoryName: category.name,
})}
>
<PrivacyFilter>
<AutoTextSize
key={value}
as={Text}
minFontSizePx={6}
maxFontSizePx={12}
mode="oneline"
style={{
...makeAmountGrey(value),
maxWidth: columnWidth,
textAlign: 'right',
fontSize: 12,
}}
>
{format(value, type)}
</AutoTextSize>
</PrivacyFilter>
</Button>
)}
</CellValue>
);
}

View File

@@ -0,0 +1,6 @@
---
category: Bugfix
authors: [joel-jeremy]
---
[Mobile] Show transactions upon clicking income categories