diff --git a/packages/desktop-client/src/components/budget/index.js b/packages/desktop-client/src/components/budget/index.js index e33ca86327ec49398118460ba03f735f058ff890..d5a0497c93dadbde4663d81d7800411a81ea945c 100644 --- a/packages/desktop-client/src/components/budget/index.js +++ b/packages/desktop-client/src/components/budget/index.js @@ -1,4 +1,11 @@ -import React, { memo, PureComponent, useContext, useMemo } from 'react'; +import React, { + memo, + useContext, + useMemo, + useState, + useEffect, + useRef, +} from 'react'; import { useSelector } from 'react-redux'; import { useLocation, useMatch, useNavigate } from 'react-router-dom'; @@ -29,38 +36,44 @@ import { ReportProvider } from './report/ReportContext'; import * as rollover from './rollover/rollover-components'; import { RolloverContext } from './rollover/RolloverContext'; -let _initialBudgetMonth = null; - -class Budget extends PureComponent { - constructor(props) { - super(props); - - const currentMonth = _initialBudgetMonth || monthUtils.currentMonth(); - this.state = { - initialized: false, - prewarmStartMonth: props.startMonth || currentMonth, - newCategoryForGroup: null, - isAddingGroup: false, - collapsed: props.collapsedPrefs || [], - bounds: { start: currentMonth, end: currentMonth }, - categoryGroups: null, - summaryCollapsed: props.summaryCollapsed, - }; - } +function Budget(props) { + const currentMonth = monthUtils.currentMonth(); + const tableRef = useRef(null); + + const [initialized, setInitialized] = useState(false); + const [prewarmStartMonth, setPrewarmStartMonth] = useState( + props.startMonth || currentMonth, + ); + const [newCategoryForGroup, setNewCategoryForGroup] = useState(null); + const [isAddingGroup, setIsAddingGroup] = useState(false); + const [collapsed, setCollapsed] = useState(props.collapsedPrefs || []); + const [bounds, setBounds] = useState({ + start: currentMonth, + end: currentMonth, + }); + const [categoryGroups, setCategoryGroups] = useState(null); + const [summaryCollapsed, setSummaryCollapsed] = useState( + props.summaryCollapsed, + ); - async loadCategories() { - let result = await this.props.getCategories(); - this.setState({ categoryGroups: result.grouped }); + async function loadCategories() { + let result = await props.getCategories(); + setCategoryGroups(result.grouped); } - async componentDidMount() { - let { titlebar, budgetType } = this.props; - this.loadCategories(); + useEffect(() => { + let { titlebar, budgetType } = props; + + async function run() { + loadCategories(); - let { start, end } = await send('get-budget-bounds'); - this.setState({ bounds: { start, end } }); + let { start, end } = await send('get-budget-bounds'); + setBounds({ start, end }); + + prewarmAllMonths({ start, end }, budgetType); + } - this.prewarmAllMonths({ start, end }, budgetType); + run(); let unlistens = [ listen('sync-event', ({ type, tables }) => { @@ -70,12 +83,12 @@ class Budget extends PureComponent { tables.includes('category_mapping') || tables.includes('category_groups')) ) { - this.loadCategories(); + loadCategories(); } }), listen('undo-event', ({ tables }) => { - if (this.table) { + if (tableRef.current) { // g dammit // We need to clear the editing cell, otherwise when // the user navigates away from the page they will @@ -83,56 +96,36 @@ class Budget extends PureComponent { // 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) - this.table.clearEditing(); + tableRef.current.clearEditing(); } if (tables.includes('categories')) { - this.loadCategories(); + loadCategories(); } }), - titlebar.subscribe(this.onTitlebarEvent), + titlebar.subscribe(onTitlebarEvent), ]; - this.cleanup = () => { + return () => { unlistens.forEach(unlisten => unlisten()); }; - } - - componentDidUpdate(prevProps, prevState) { - if (prevState.collapsed !== this.state.collapsed) { - this.props.savePrefs({ 'budget.collapsed': this.state.collapsed }); - } + }, []); - const currentMonth = _initialBudgetMonth || monthUtils.currentMonth(); - const startMonth = this.props.startMonth || currentMonth; + useEffect(() => { + props.savePrefs({ 'budget.collapsed': collapsed }); + }, [collapsed]); - if (prevState.startMonth !== startMonth) { - // Save it off into a global state so if the component re-mounts - // we keep this state (but don't need to subscribe to it) - _initialBudgetMonth = startMonth; - } - - if (this.props.accountId !== prevProps.accountId) { - // Make to sure to check if the budget bounds have changed, and - // if so reload the budget data - send('get-budget-bounds').then(({ start, end }) => { - let { bounds } = this.state; - if (bounds.start !== start || bounds.end !== end) { - this.setState({ bounds: { start, end } }); - } - }); - } - } - - componentWillUnmount() { - if (this.cleanup) { - this.cleanup(); - } - } + useEffect(() => { + send('get-budget-bounds').then(({ start, end }) => { + if (bounds.start !== start || bounds.end !== end) { + setBounds({ start, end }); + } + }); + }, [props.accountId]); - prewarmMonth = async (month, type = null) => { - type = type || this.props.budgetType; + const prewarmMonth = async (month, type = null) => { + type = type || props.budgetType; let method = type === 'report' ? 'report-budget-month' : 'rollover-budget-month'; @@ -140,15 +133,14 @@ class Budget extends PureComponent { let values = await send(method, { month }); for (let value of values) { - this.props.spreadsheet.prewarmCache(value.name, value); + props.spreadsheet.prewarmCache(value.name, value); } }; - async prewarmAllMonths(bounds, type = null) { + async function prewarmAllMonths(bounds, type = null) { let numMonths = 3; - const currentMonth = _initialBudgetMonth || monthUtils.currentMonth(); - const startMonth = this.props.startMonth || currentMonth; + const startMonth = props.startMonth || currentMonth; bounds = getValidMonthBounds( bounds, @@ -157,18 +149,17 @@ class Budget extends PureComponent { ); let months = monthUtils.rangeInclusive(bounds.start, bounds.end); - await Promise.all(months.map(month => this.prewarmMonth(month, type))); + await Promise.all(months.map(month => prewarmMonth(month, type))); - this.setState({ initialized: true }); + setInitialized(true); } - onMonthSelect = async (month, numDisplayed) => { - this.setState({ prewarmStartMonth: month }); + const onMonthSelect = async (month, numDisplayed) => { + setPrewarmStartMonth(month); - this.warmingMonth = month; + const warmingMonth = month; - const currentMonth = _initialBudgetMonth || monthUtils.currentMonth(); - const startMonth = this.props.startMonth || currentMonth; + const startMonth = props.startMonth || currentMonth; // We could be smarter about this, but this is a good start. We // optimize for the case where users press the left/right button @@ -178,130 +169,106 @@ class Budget extends PureComponent { // but it will just load in some unnecessary data. if (month < startMonth) { // pre-warm prev month - await this.prewarmMonth(monthUtils.subMonths(month, 1)); + await prewarmMonth(monthUtils.subMonths(month, 1)); } else if (month > startMonth) { // pre-warm next month - await this.prewarmMonth(monthUtils.addMonths(month, numDisplayed)); + await prewarmMonth(monthUtils.addMonths(month, numDisplayed)); } - if (this.warmingMonth === month) { - this.props.savePrefs({ 'budget.startMonth': month }); + if (warmingMonth === month) { + props.savePrefs({ 'budget.startMonth': month }); } }; - onShowNewCategory = groupId => { - this.setState({ - newCategoryForGroup: groupId, - collapsed: this.state.collapsed.filter(c => c !== groupId), - }); - }; - - onHideNewCategory = () => { - this.setState({ newCategoryForGroup: null }); + const onShowNewCategory = groupId => { + setNewCategoryForGroup(groupId); + setCollapsed(state => state.filter(c => c !== groupId)); }; - onShowNewGroup = () => { - this.setState({ isAddingGroup: true }); + const onHideNewCategory = () => { + setNewCategoryForGroup(null); }; - onHideNewGroup = () => { - this.setState({ isAddingGroup: false }); + const onShowNewGroup = () => { + setIsAddingGroup(true); }; - setCollapsed = collapsed => { - this.setState({ collapsed }); + const onHideNewGroup = () => { + setIsAddingGroup(false); }; - onCopy(month) { - this.props.copyPreviousMonthInto(month, this.props.categories); - } - - onSaveCategory = async category => { - let { categoryGroups } = this.state; - + const onSaveCategory = async category => { if (category.id === 'new') { - let id = await this.props.createCategory( + let id = await props.createCategory( category.name, category.cat_group, category.is_income, ); - this.setState({ - newCategoryForGroup: null, - categoryGroups: addCategory(categoryGroups, { + setNewCategoryForGroup(null); + setCategoryGroups(state => + addCategory(state, { ...category, is_income: category.is_income ? 1 : 0, id, }), - }); + ); } else { const cat = { ...category, hidden: category.hidden ? 1 : 0, }; - this.props.updateCategory(cat); - this.setState({ - categoryGroups: updateCategory(categoryGroups, cat), - }); + props.updateCategory(cat); + setCategoryGroups(state => updateCategory(state, cat)); } }; - onDeleteCategory = async id => { + const onDeleteCategory = async id => { const mustTransfer = await send('must-category-transfer', { id }); - let { categoryGroups } = this.state; if (mustTransfer) { - this.props.pushModal('confirm-category-delete', { + props.pushModal('confirm-category-delete', { category: id, onDelete: transferCategory => { if (id !== transferCategory) { - this.props.deleteCategory(id, transferCategory); + props.deleteCategory(id, transferCategory); - this.setState({ - categoryGroups: deleteCategory(categoryGroups, id), - }); + setCategoryGroups(state => deleteCategory(state, id)); } }, }); } else { - this.props.deleteCategory(id); + props.deleteCategory(id); - this.setState({ - categoryGroups: deleteCategory(categoryGroups, id), - }); + setCategoryGroups(state => deleteCategory(state, id)); } }; - onSaveGroup = async group => { - let { categoryGroups } = this.state; - + const onSaveGroup = async group => { if (group.id === 'new') { - let id = await this.props.createGroup(group.name); - this.setState({ - isAddingGroup: false, - categoryGroups: addGroup(categoryGroups, { + let id = await props.createGroup(group.name); + setIsAddingGroup(false); + setCategoryGroups(state => + addGroup(state, { ...group, is_income: 0, categories: group.categories || [], id, }), - }); + ); } else { const grp = { ...group, hidden: group.hidden ? 1 : 0, }; - this.props.updateGroup(grp); - this.setState({ - categoryGroups: updateGroup(categoryGroups, grp), - }); + props.updateGroup(grp); + setCategoryGroups(state => updateGroup(state, grp)); } }; - onDeleteGroup = async id => { - let { categoryGroups } = this.state; + const onDeleteGroup = async id => { let group = categoryGroups.find(g => g.id === id); let mustTransfer = false; @@ -313,31 +280,27 @@ class Budget extends PureComponent { } if (mustTransfer) { - this.props.pushModal('confirm-category-delete', { + props.pushModal('confirm-category-delete', { group: id, onDelete: transferCategory => { - this.props.deleteGroup(id, transferCategory); + props.deleteGroup(id, transferCategory); - this.setState({ - categoryGroups: deleteGroup(categoryGroups, id), - }); + setCategoryGroups(state => deleteGroup(state, id)); }, }); } else { - this.props.deleteGroup(id); + props.deleteGroup(id); - this.setState({ - categoryGroups: deleteGroup(categoryGroups, id), - }); + setCategoryGroups(state => deleteGroup(state, id)); } }; - onBudgetAction = (month, type, args) => { - this.props.applyBudgetAction(month, type, args); + const onBudgetAction = (month, type, args) => { + props.applyBudgetAction(month, type, args); }; - onShowActivity = (categoryName, categoryId, month) => { - this.props.navigate('/accounts', { + const onShowActivity = (categoryName, categoryId, month) => { + props.navigate('/accounts', { state: { goBack: true, filterName: `${categoryName} (${monthUtils.format( @@ -352,165 +315,135 @@ class Budget extends PureComponent { }); }; - onReorderCategory = async sortInfo => { - let { categoryGroups } = this.state; - - this.props.moveCategory(sortInfo.id, sortInfo.groupId, sortInfo.targetId); - this.setState({ - categoryGroups: moveCategory( - categoryGroups, - sortInfo.id, - sortInfo.groupId, - sortInfo.targetId, - ), - }); + const onReorderCategory = async sortInfo => { + props.moveCategory(sortInfo.id, sortInfo.groupId, sortInfo.targetId); + setCategoryGroups(state => + moveCategory(state, sortInfo.id, sortInfo.groupId, sortInfo.targetId), + ); }; - onReorderGroup = async sortInfo => { - let { categoryGroups } = this.state; - - this.props.moveCategoryGroup(sortInfo.id, sortInfo.targetId); - this.setState({ - categoryGroups: moveCategoryGroup( - categoryGroups, - sortInfo.id, - sortInfo.targetId, - ), - }); + const onReorderGroup = async sortInfo => { + props.moveCategoryGroup(sortInfo.id, sortInfo.targetId); + setCategoryGroups(state => + moveCategoryGroup(state, sortInfo.id, sortInfo.targetId), + ); }; - onToggleCollapse = () => { - let collapsed = !this.state.summaryCollapsed; - this.setState({ summaryCollapsed: collapsed }); - this.props.savePrefs({ 'budget.summaryCollapsed': collapsed }); + const onToggleCollapse = () => { + let collapsed = !summaryCollapsed; + setSummaryCollapsed(collapsed); + props.savePrefs({ 'budget.summaryCollapsed': collapsed }); }; - onTitlebarEvent = async msg => { + const onTitlebarEvent = async msg => { switch (msg) { case 'budget/switch-type': { - let type = this.props.budgetType; + let type = props.budgetType; let newType = type === 'rollover' ? 'report' : 'rollover'; - this.props.spreadsheet.disableObservers(); + props.spreadsheet.disableObservers(); await send('budget-set-type', { type: newType }); - await this.prewarmAllMonths(this.state.bounds, newType); - this.props.spreadsheet.enableObservers(); - this.props.loadPrefs(); + await prewarmAllMonths(bounds, newType); + props.spreadsheet.enableObservers(); + props.loadPrefs(); break; } default: } }; - render() { - let { - maxMonths, - budgetType: type, - reportComponents, - rolloverComponents, - } = this.props; - let { - initialized, - categoryGroups, - prewarmStartMonth, - newCategoryForGroup, - isAddingGroup, - collapsed, - summaryCollapsed, - bounds, - } = this.state; + let { + maxMonths, + budgetType: type, + reportComponents, + rolloverComponents, + } = props; - maxMonths = maxMonths || 1; + maxMonths = maxMonths || 1; - if (!initialized || !categoryGroups) { - return null; - } + if (!initialized || !categoryGroups) { + return null; + } - const currentMonth = _initialBudgetMonth || monthUtils.currentMonth(); - const startMonth = this.props.startMonth || currentMonth; - - let table; - if (type === 'report') { - table = ( - <ReportProvider - summaryCollapsed={summaryCollapsed} - onBudgetAction={this.onBudgetAction} - onToggleSummaryCollapse={this.onToggleCollapse} - > - <DynamicBudgetTable - ref={el => (this.table = el)} - type={type} - categoryGroups={categoryGroups} - prewarmStartMonth={prewarmStartMonth} - startMonth={startMonth} - monthBounds={bounds} - maxMonths={maxMonths} - collapsed={collapsed} - setCollapsed={this.setCollapsed} - newCategoryForGroup={newCategoryForGroup} - isAddingGroup={isAddingGroup} - dataComponents={reportComponents} - onMonthSelect={this.onMonthSelect} - onShowNewCategory={this.onShowNewCategory} - onHideNewCategory={this.onHideNewCategory} - onShowNewGroup={this.onShowNewGroup} - onHideNewGroup={this.onHideNewGroup} - onDeleteCategory={this.onDeleteCategory} - onDeleteGroup={this.onDeleteGroup} - onSaveCategory={this.onSaveCategory} - onSaveGroup={this.onSaveGroup} - onBudgetAction={this.onBudgetAction} - onShowActivity={this.onShowActivity} - onReorderCategory={this.onReorderCategory} - onReorderGroup={this.onReorderGroup} - /> - </ReportProvider> - ); - } else { - table = ( - <RolloverContext - categoryGroups={categoryGroups} - summaryCollapsed={summaryCollapsed} - onBudgetAction={this.onBudgetAction} - onToggleSummaryCollapse={this.onToggleCollapse} - > - <DynamicBudgetTable - ref={el => (this.table = el)} - type={type} - categoryGroups={categoryGroups} - prewarmStartMonth={prewarmStartMonth} - startMonth={startMonth} - monthBounds={bounds} - maxMonths={maxMonths} - collapsed={collapsed} - setCollapsed={this.setCollapsed} - newCategoryForGroup={newCategoryForGroup} - isAddingGroup={isAddingGroup} - dataComponents={rolloverComponents} - onMonthSelect={this.onMonthSelect} - onShowNewCategory={this.onShowNewCategory} - onHideNewCategory={this.onHideNewCategory} - onShowNewGroup={this.onShowNewGroup} - onHideNewGroup={this.onHideNewGroup} - onDeleteCategory={this.onDeleteCategory} - onDeleteGroup={this.onDeleteGroup} - onSaveCategory={this.onSaveCategory} - onSaveGroup={this.onSaveGroup} - onBudgetAction={this.onBudgetAction} - onShowActivity={this.onShowActivity} - onReorderCategory={this.onReorderCategory} - onReorderGroup={this.onReorderGroup} - /> - </RolloverContext> - ); - } + const startMonth = props.startMonth || currentMonth; - return ( - <View style={{ flex: 1 }} innerRef={el => (this.page = el)}> - {table} - </View> + let table; + if (type === 'report') { + table = ( + <ReportProvider + summaryCollapsed={summaryCollapsed} + onBudgetAction={onBudgetAction} + onToggleSummaryCollapse={onToggleCollapse} + > + <DynamicBudgetTable + ref={tableRef} + type={type} + categoryGroups={categoryGroups} + prewarmStartMonth={prewarmStartMonth} + startMonth={startMonth} + monthBounds={bounds} + maxMonths={maxMonths} + collapsed={collapsed} + setCollapsed={setCollapsed} + newCategoryForGroup={newCategoryForGroup} + isAddingGroup={isAddingGroup} + dataComponents={reportComponents} + onMonthSelect={onMonthSelect} + onShowNewCategory={onShowNewCategory} + onHideNewCategory={onHideNewCategory} + onShowNewGroup={onShowNewGroup} + onHideNewGroup={onHideNewGroup} + onDeleteCategory={onDeleteCategory} + onDeleteGroup={onDeleteGroup} + onSaveCategory={onSaveCategory} + onSaveGroup={onSaveGroup} + onBudgetAction={onBudgetAction} + onShowActivity={onShowActivity} + onReorderCategory={onReorderCategory} + onReorderGroup={onReorderGroup} + /> + </ReportProvider> + ); + } else { + table = ( + <RolloverContext + categoryGroups={categoryGroups} + summaryCollapsed={summaryCollapsed} + onBudgetAction={onBudgetAction} + onToggleSummaryCollapse={onToggleCollapse} + > + <DynamicBudgetTable + ref={tableRef} + type={type} + categoryGroups={categoryGroups} + prewarmStartMonth={prewarmStartMonth} + startMonth={startMonth} + monthBounds={bounds} + maxMonths={maxMonths} + collapsed={collapsed} + setCollapsed={setCollapsed} + newCategoryForGroup={newCategoryForGroup} + isAddingGroup={isAddingGroup} + dataComponents={rolloverComponents} + onMonthSelect={onMonthSelect} + onShowNewCategory={onShowNewCategory} + onHideNewCategory={onHideNewCategory} + onShowNewGroup={onShowNewGroup} + onHideNewGroup={onHideNewGroup} + onDeleteCategory={onDeleteCategory} + onDeleteGroup={onDeleteGroup} + onSaveCategory={onSaveCategory} + onSaveGroup={onSaveGroup} + onBudgetAction={onBudgetAction} + onShowActivity={onShowActivity} + onReorderCategory={onReorderCategory} + onReorderGroup={onReorderGroup} + /> + </RolloverContext> ); } + + return <View style={{ flex: 1 }}>{table}</View>; } const RolloverBudgetSummary = memo(props => { diff --git a/upcoming-release-notes/1566.md b/upcoming-release-notes/1566.md new file mode 100644 index 0000000000000000000000000000000000000000..c15e58af5c205919194c7ab01d0964519d546ddf --- /dev/null +++ b/upcoming-release-notes/1566.md @@ -0,0 +1,6 @@ +--- +category: Maintenance +authors: [MatissJanis] +--- + +Convert budget page component from class component to functional