Files
actual/packages/desktop-client/src/components/budget/DynamicBudgetTable.tsx
2026-01-12 09:47:27 -08:00

179 lines
4.4 KiB
TypeScript

// @ts-strict-ignore
import React, { useEffect, type ComponentProps } from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
import AutoSizer from 'react-virtualized-auto-sizer';
import { View } from '@actual-app/components/view';
import * as monthUtils from 'loot-core/shared/months';
import { useBudgetMonthCount } from './BudgetMonthCountContext';
import { BudgetPageHeader } from './BudgetPageHeader';
import { BudgetTable } from './BudgetTable';
import { useFeatureFlag } from '@desktop-client/hooks/useFeatureFlag';
import { useGlobalPref } from '@desktop-client/hooks/useGlobalPref';
function getNumPossibleMonths(width: number, categoryWidth: number) {
const estimatedTableWidth = width - categoryWidth;
if (estimatedTableWidth < 500) {
return 1;
} else if (estimatedTableWidth < 750) {
return 2;
} else if (estimatedTableWidth < 1000) {
return 3;
} else if (estimatedTableWidth < 1250) {
return 4;
} else if (estimatedTableWidth < 1500) {
return 5;
}
return 6;
}
type DynamicBudgetTableProps = {
width: number;
height: number;
} & AutoSizingBudgetTableProps;
const DynamicBudgetTable = ({
type,
width,
height,
prewarmStartMonth,
startMonth,
maxMonths = 3,
monthBounds,
onMonthSelect,
...props
}: DynamicBudgetTableProps) => {
const { setDisplayMax } = useBudgetMonthCount();
const [categoryExpandedStatePref] = useGlobalPref('categoryExpandedState');
const categoryExpandedState = categoryExpandedStatePref ?? 0;
const numPossible = getNumPossibleMonths(
width,
200 + 100 * categoryExpandedState,
);
const numMonths = Math.min(numPossible, maxMonths);
const maxWidth = 200 + 100 * categoryExpandedState + 500 * numMonths;
useEffect(() => {
setDisplayMax(numPossible);
}, [numPossible]);
function getValidMonth(month) {
const start = monthBounds.start;
const end = monthUtils.subMonths(monthBounds.end, numMonths - 1);
if (month < start) {
return start;
} else if (month > end) {
return end;
}
return month;
}
function _onMonthSelect(month) {
onMonthSelect(getValidMonth(month), numMonths);
}
// Table V2 uses alt+left/right for month navigation
// so that users can use left/right to navigate cells
const budgetTableV2Enabled = useFeatureFlag('budgetTableV2');
useHotkeys(
budgetTableV2Enabled ? 'alt+left' : 'left',
() => {
_onMonthSelect(monthUtils.prevMonth(startMonth));
},
{
preventDefault: true,
scopes: ['app'],
},
[_onMonthSelect, startMonth],
);
useHotkeys(
budgetTableV2Enabled ? 'alt+right' : 'right',
() => {
_onMonthSelect(monthUtils.nextMonth(startMonth));
},
{
preventDefault: true,
scopes: ['app'],
},
[_onMonthSelect, startMonth],
);
useHotkeys(
'0',
() => {
_onMonthSelect(
monthUtils.subMonths(
monthUtils.currentMonth(),
type === 'envelope'
? Math.floor((numMonths - 1) / 2)
: numMonths === 2
? 1
: Math.max(numMonths - 2, 0),
),
);
},
{
preventDefault: true,
scopes: ['app'],
},
[_onMonthSelect, startMonth, numMonths],
);
return (
<View
style={{
width,
height,
alignItems: 'center',
opacity: width <= 0 || height <= 0 ? 0 : 1,
}}
>
<View style={{ width: '100%', maxWidth }}>
<BudgetPageHeader
startMonth={prewarmStartMonth}
numMonths={numMonths}
monthBounds={monthBounds}
onMonthSelect={_onMonthSelect}
/>
<BudgetTable
type={type}
prewarmStartMonth={prewarmStartMonth}
startMonth={startMonth}
numMonths={numMonths}
monthBounds={monthBounds}
{...props}
/>
</View>
</View>
);
};
DynamicBudgetTable.displayName = 'DynamicBudgetTable';
type AutoSizingBudgetTableProps = Omit<
ComponentProps<typeof BudgetTable>,
'numMonths'
> & {
maxMonths: number;
onMonthSelect: (month: string, numMonths: number) => void;
};
export const AutoSizingBudgetTable = (props: AutoSizingBudgetTableProps) => {
return (
<AutoSizer>
{({ width, height }) => (
<DynamicBudgetTable width={width} height={height} {...props} />
)}
</AutoSizer>
);
};
AutoSizingBudgetTable.displayName = 'AutoSizingBudgetTable';