Skip to content
Snippets Groups Projects
Unverified Commit 6f600a4f authored by Joel Jeremy Marquez's avatar Joel Jeremy Marquez Committed by GitHub
Browse files

Convert BudgetTable to a functional component (#2459)

* Convert BudgetTable to a functional component

* Release notes

* Fix lint errors

* Remove undo-event listener
parent c82a6dc5
No related branches found
No related tags found
No related merge requests found
import React, { createRef, Component } from 'react';
import { connect } from 'react-redux';
import React, { useRef, useState } from 'react';
import { savePrefs } from 'loot-core/src/client/actions';
import * as monthUtils from 'loot-core/src/shared/months';
import { useCategories } from '../../hooks/useCategories';
import { useLocalPref } from '../../hooks/useLocalPref';
import { theme, styles } from '../../style';
import { View } from '../common/View';
import { IntersectionBoundary } from '../tooltips';
......@@ -14,28 +14,41 @@ import { BudgetTotals } from './BudgetTotals';
import { MonthsProvider } from './MonthsContext';
import { findSortDown, findSortUp, getScrollbarWidth } from './util';
class BudgetTableInner extends Component {
constructor(props) {
super(props);
this.budgetCategoriesRef = createRef();
this.state = {
editing: null,
draggingState: null,
};
}
onEditMonth = (id, monthIndex) => {
this.setState({ editing: id ? { id, cell: monthIndex } : null });
export function BudgetTable(props) {
const {
type,
prewarmStartMonth,
startMonth,
numMonths,
monthBounds,
dataComponents,
onSaveCategory,
onDeleteCategory,
onSaveGroup,
onDeleteGroup,
onReorderCategory,
onReorderGroup,
onShowActivity,
onBudgetAction,
} = props;
const budgetCategoriesRef = useRef();
const { grouped: categoryGroups } = useCategories();
const [collapsed = [], setCollapsedPref] = useLocalPref('budget.collapsed');
const [showHiddenCategories, setShowHiddenCategoriesPef] = useLocalPref(
'budget.showHiddenCategories',
);
const [editing, setEditing] = useState(null);
const onEditMonth = (id, monthIndex) => {
setEditing(id ? { id, cell: monthIndex } : null);
};
onEditName = id => {
this.setState({ editing: id ? { id, cell: 'name' } : null });
const onEditName = id => {
setEditing(id ? { id, cell: 'name' } : null);
};
onReorderCategory = (id, dropPos, targetId) => {
const { categoryGroups } = this.props;
const _onReorderCategory = (id, dropPos, targetId) => {
const isGroup = !!categoryGroups.find(g => g.id === targetId);
if (isGroup) {
......@@ -48,7 +61,7 @@ class BudgetTableInner extends Component {
if (group) {
const { categories } = group;
this.props.onReorderCategory({
onReorderCategory({
id,
groupId: group.id,
targetId:
......@@ -67,7 +80,7 @@ class BudgetTableInner extends Component {
}
}
this.props.onReorderCategory({
onReorderCategory({
id,
groupId: targetGroup.id,
...findSortDown(targetGroup.categories, dropPos, targetId),
......@@ -75,19 +88,14 @@ class BudgetTableInner extends Component {
}
};
onReorderGroup = (id, dropPos, targetId) => {
const { categoryGroups } = this.props;
this.props.onReorderGroup({
const _onReorderGroup = (id, dropPos, targetId) => {
onReorderGroup({
id,
...findSortDown(categoryGroups, dropPos, targetId),
});
};
moveVertically = dir => {
const { editing } = this.state;
const { type, categoryGroups, collapsed } = this.props;
const moveVertically = dir => {
const flattened = categoryGroups.reduce((all, group) => {
if (collapsed.includes(group.id)) {
return all.concat({ id: group.id, isGroup: true });
......@@ -106,7 +114,7 @@ class BudgetTableInner extends Component {
nextIdx += dir;
continue;
} else if (type === 'report' || !next.is_income) {
this.onEditMonth(next.id, editing.cell);
onEditMonth(next.id, editing.cell);
return;
} else {
break;
......@@ -115,187 +123,136 @@ class BudgetTableInner extends Component {
}
};
onKeyDown = e => {
if (!this.state.editing) {
const onKeyDown = e => {
if (!editing) {
return null;
}
if (e.key === 'Enter' || e.key === 'Tab') {
e.preventDefault();
this.moveVertically(e.shiftKey ? -1 : 1);
moveVertically(e.shiftKey ? -1 : 1);
}
};
onShowActivity = (catId, monthIndex) => {
this.props.onShowActivity(catId, this.resolveMonth(monthIndex));
const resolveMonth = monthIndex => {
return monthUtils.addMonths(startMonth, monthIndex);
};
onBudgetAction = (monthIndex, type, args) => {
this.props.onBudgetAction(this.resolveMonth(monthIndex), type, args);
const _onShowActivity = (catId, monthIndex) => {
onShowActivity(catId, resolveMonth(monthIndex));
};
resolveMonth = monthIndex => {
return monthUtils.addMonths(this.props.startMonth, monthIndex);
const _onBudgetAction = (monthIndex, type, args) => {
onBudgetAction(resolveMonth(monthIndex), type, args);
};
// This is called via ref.
clearEditing() {
this.setState({ editing: null });
}
const onCollapse = collapsedIds => {
setCollapsedPref(collapsedIds);
};
toggleHiddenCategories = () => {
this.props.onToggleHiddenCategories();
const onToggleHiddenCategories = () => {
setShowHiddenCategoriesPef(!showHiddenCategories);
};
expandAllCategories = () => {
this.props.onCollapse([]);
const toggleHiddenCategories = () => {
onToggleHiddenCategories();
};
collapseAllCategories = () => {
const { onCollapse, categoryGroups } = this.props;
onCollapse(categoryGroups.map(g => g.id));
const expandAllCategories = () => {
onCollapse([]);
};
render() {
const {
type,
categoryGroups,
prewarmStartMonth,
startMonth,
numMonths,
monthBounds,
dataComponents,
onSaveCategory,
onSaveGroup,
onDeleteCategory,
onDeleteGroup,
} = this.props;
const { editing, draggingState } = this.state;
const collapseAllCategories = () => {
onCollapse(categoryGroups.map(g => g.id));
};
return (
return (
<View
data-testid="budget-table"
style={{
flex: 1,
...(styles.lightScrollbar && {
'& ::-webkit-scrollbar': {
backgroundColor: 'transparent',
},
'& ::-webkit-scrollbar-thumb:vertical': {
backgroundColor: theme.tableHeaderBackground,
},
}),
}}
>
<View
data-testid="budget-table"
style={{
flex: 1,
...(styles.lightScrollbar && {
'& ::-webkit-scrollbar': {
backgroundColor: 'transparent',
},
'& ::-webkit-scrollbar-thumb:vertical': {
backgroundColor: theme.tableHeaderBackground,
},
}),
flexDirection: 'row',
overflow: 'hidden',
flexShrink: 0,
// This is necessary to align with the table because the
// table has this padding to allow the shadow to show
paddingLeft: 5,
paddingRight: 5 + getScrollbarWidth(),
}}
>
<View
style={{
flexDirection: 'row',
overflow: 'hidden',
flexShrink: 0,
// This is necessary to align with the table because the
// table has this padding to allow the shadow to show
paddingLeft: 5,
paddingRight: 5 + getScrollbarWidth(),
}}
>
<View style={{ width: 200 }} />
<MonthsProvider
startMonth={prewarmStartMonth}
numMonths={numMonths}
monthBounds={monthBounds}
type={type}
>
<BudgetSummaries
SummaryComponent={dataComponents.SummaryComponent}
/>
</MonthsProvider>
</View>
<View style={{ width: 200 }} />
<MonthsProvider
startMonth={startMonth}
startMonth={prewarmStartMonth}
numMonths={numMonths}
monthBounds={monthBounds}
type={type}
>
<BudgetTotals
MonthComponent={dataComponents.BudgetTotalsComponent}
toggleHiddenCategories={this.toggleHiddenCategories}
expandAllCategories={this.expandAllCategories}
collapseAllCategories={this.collapseAllCategories}
/>
<IntersectionBoundary.Provider value={this.budgetCategoriesRef}>
<BudgetSummaries SummaryComponent={dataComponents.SummaryComponent} />
</MonthsProvider>
</View>
<MonthsProvider
startMonth={startMonth}
numMonths={numMonths}
monthBounds={monthBounds}
type={type}
>
<BudgetTotals
MonthComponent={dataComponents.BudgetTotalsComponent}
toggleHiddenCategories={toggleHiddenCategories}
expandAllCategories={expandAllCategories}
collapseAllCategories={collapseAllCategories}
/>
<IntersectionBoundary.Provider value={budgetCategoriesRef}>
<View
style={{
overflowY: 'scroll',
overflowAnchor: 'none',
flex: 1,
paddingLeft: 5,
paddingRight: 5,
}}
innerRef={budgetCategoriesRef}
>
<View
style={{
overflowY: 'scroll',
overflowAnchor: 'none',
flex: 1,
paddingLeft: 5,
paddingRight: 5,
flexShrink: 0,
}}
innerRef={this.budgetCategoriesRef}
onKeyDown={onKeyDown}
>
<View
style={{
opacity: draggingState ? 0.5 : 1,
flexShrink: 0,
}}
onKeyDown={this.onKeyDown}
innerRef={el => (this.budgetDataNode = el)}
>
<BudgetCategories
categoryGroups={categoryGroups}
editingCell={editing}
dataComponents={dataComponents}
onEditMonth={this.onEditMonth}
onEditName={this.onEditName}
onSaveCategory={onSaveCategory}
onSaveGroup={onSaveGroup}
onDeleteCategory={onDeleteCategory}
onDeleteGroup={onDeleteGroup}
onReorderCategory={this.onReorderCategory}
onReorderGroup={this.onReorderGroup}
onBudgetAction={this.onBudgetAction}
onShowActivity={this.onShowActivity}
/>
</View>
<BudgetCategories
categoryGroups={categoryGroups}
editingCell={editing}
dataComponents={dataComponents}
onEditMonth={onEditMonth}
onEditName={onEditName}
onSaveCategory={onSaveCategory}
onSaveGroup={onSaveGroup}
onDeleteCategory={onDeleteCategory}
onDeleteGroup={onDeleteGroup}
onReorderCategory={_onReorderCategory}
onReorderGroup={_onReorderGroup}
onBudgetAction={_onBudgetAction}
onShowActivity={_onShowActivity}
/>
</View>
</IntersectionBoundary.Provider>
</MonthsProvider>
</View>
);
}
</View>
</IntersectionBoundary.Provider>
</MonthsProvider>
</View>
);
}
const mapStateToProps = state => {
const { grouped: categoryGroups } = state.queries.categories;
const collapsed = state.prefs.local?.['budget.collapsed'] || [];
return {
categoryGroups,
collapsed,
};
};
const mapDispatchToProps = dispatch => {
const onCollapse = collapsedIds => {
dispatch(savePrefs({ 'budget.collapsed': collapsedIds }));
};
const onToggleHiddenCategories = () =>
dispatch((innerDispatch, getState) => {
const { prefs } = getState();
const showHiddenCategories = prefs.local['budget.showHiddenCategories'];
innerDispatch(
savePrefs({
'budget.showHiddenCategories': !showHiddenCategories,
}),
);
});
return {
onCollapse,
onToggleHiddenCategories,
};
};
export const BudgetTable = connect(mapStateToProps, mapDispatchToProps, null, {
forwardRef: true,
})(BudgetTableInner);
BudgetTable.displayName = 'BudgetTable';
// @ts-strict-ignore
import React, { forwardRef, useEffect, type ComponentProps } from 'react';
import React, { useEffect, type ComponentProps } from 'react';
import AutoSizer from 'react-virtualized-auto-sizer';
import { useActions } from '../../hooks/useActions';
import { View } from '../common/View';
import { useBudgetMonthCount } from './BudgetMonthCountContext';
......@@ -30,90 +29,72 @@ function getNumPossibleMonths(width: number) {
type DynamicBudgetTableInnerProps = {
width: number;
height: number;
} & ComponentProps<typeof BudgetTable>;
} & DynamicBudgetTableProps;
const DynamicBudgetTableInner = forwardRef<
typeof BudgetTable,
DynamicBudgetTableInnerProps
>(
(
{
width,
height,
prewarmStartMonth,
startMonth,
maxMonths = 3,
monthBounds,
onMonthSelect: onMonthSelect_,
onPreload,
...props
},
ref,
) => {
const { setDisplayMax } = useBudgetMonthCount();
const actions = useActions();
const DynamicBudgetTableInner = ({
width,
height,
prewarmStartMonth,
startMonth,
maxMonths = 3,
monthBounds,
onMonthSelect,
...props
}: DynamicBudgetTableInnerProps) => {
const { setDisplayMax } = useBudgetMonthCount();
const numPossible = getNumPossibleMonths(width);
const numMonths = Math.min(numPossible, maxMonths);
const maxWidth = 200 + 500 * numMonths;
const numPossible = getNumPossibleMonths(width);
const numMonths = Math.min(numPossible, maxMonths);
const maxWidth = 200 + 500 * numMonths;
useEffect(() => {
setDisplayMax(numPossible);
}, [numPossible]);
useEffect(() => {
setDisplayMax(numPossible);
}, [numPossible]);
function onMonthSelect(month) {
onMonthSelect_(month, numMonths);
}
function _onMonthSelect(month) {
onMonthSelect(month, 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
ref={ref}
prewarmStartMonth={prewarmStartMonth}
startMonth={startMonth}
numMonths={numMonths}
monthBounds={monthBounds}
{...actions}
{...props}
/>
</View>
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
prewarmStartMonth={prewarmStartMonth}
startMonth={startMonth}
numMonths={numMonths}
monthBounds={monthBounds}
{...props}
/>
</View>
);
},
);
</View>
);
};
DynamicBudgetTableInner.displayName = 'DynamicBudgetTableInner';
export const DynamicBudgetTable = forwardRef<
typeof BudgetTable,
DynamicBudgetTableInnerProps
>((props, ref) => {
type DynamicBudgetTableProps = ComponentProps<typeof BudgetTable>;
export const DynamicBudgetTable = (props: DynamicBudgetTableProps) => {
return (
<AutoSizer>
{({ width, height }) => (
<DynamicBudgetTableInner
ref={ref}
width={width}
height={height}
{...props}
/>
<DynamicBudgetTableInner width={width} height={height} {...props} />
)}
</AutoSizer>
);
});
};
DynamicBudgetTable.displayName = 'DynamicBudgetTable';
// @ts-strict-ignore
import React, {
memo,
useContext,
useMemo,
useState,
useEffect,
useRef,
} from 'react';
import React, { memo, useContext, useMemo, useState, useEffect } from 'react';
import { useDispatch } from 'react-redux';
import {
......@@ -69,16 +62,15 @@ type RolloverComponents = {
IncomeHeaderComponent: typeof rollover.IncomeHeaderMonth;
};
type BudgetProps = {
type BudgetInnerProps = {
accountId?: string;
reportComponents: ReportComponents;
rolloverComponents: RolloverComponents;
titlebar: TitlebarContextValue;
};
function BudgetInner(props: BudgetProps) {
function BudgetInner(props: BudgetInnerProps) {
const currentMonth = monthUtils.currentMonth();
const tableRef = useRef(null);
const spreadsheet = useSpreadsheet();
const dispatch = useDispatch();
const navigate = useNavigate();
......@@ -138,17 +130,6 @@ function BudgetInner(props: BudgetProps) {
}),
listen('undo-event', ({ tables }) => {
if (tableRef.current) {
// g dammit
// We need to clear the editing cell, otherwise when
// the user navigates away from the page they will
// accidentally clear the undo stack if they have pressed
// undo, since the cell will save itself on blur (worst case:
// undo takes you to another screen and then you can't redo
// any of the budget changes)
tableRef.current.clearEditing();
}
if (tables.includes('categories')) {
loadCategories();
}
......@@ -377,7 +358,6 @@ function BudgetInner(props: BudgetProps) {
onToggleSummaryCollapse={onToggleCollapse}
>
<DynamicBudgetTable
ref={tableRef}
type={budgetType}
prewarmStartMonth={startMonth}
startMonth={startMonth}
......@@ -404,7 +384,6 @@ function BudgetInner(props: BudgetProps) {
onToggleSummaryCollapse={onToggleCollapse}
>
<DynamicBudgetTable
ref={tableRef}
type={budgetType}
prewarmStartMonth={startMonth}
startMonth={startMonth}
......
---
category: Maintenance
authors: [joel-jeremy]
---
Convert BudgetTable component to a functional component.
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment