mirror of
https://github.com/actualbudget/actual.git
synced 2026-04-27 01:28:19 -05:00
[Maintenance] Reduce budget table re-renders (#3448)
* Reduce budget table re-renders * Release notes
This commit is contained in:
committed by
GitHub
parent
c5193b6d43
commit
bdbf6e9ca6
@@ -1,5 +1,5 @@
|
||||
// @ts-strict-ignore
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
import {
|
||||
@@ -20,10 +20,6 @@ import {
|
||||
import { useSpreadsheet } from 'loot-core/src/client/SpreadsheetProvider';
|
||||
import { send, listen } from 'loot-core/src/platform/client/fetch';
|
||||
import * as monthUtils from 'loot-core/src/shared/months';
|
||||
import {
|
||||
type CategoryEntity,
|
||||
type CategoryGroupEntity,
|
||||
} from 'loot-core/src/types/models';
|
||||
|
||||
import { useCategories } from '../../../hooks/useCategories';
|
||||
import { useLocalPref } from '../../../hooks/useLocalPref';
|
||||
@@ -43,15 +39,13 @@ function isBudgetType(input?: string): input is 'rollover' | 'report' {
|
||||
return ['rollover', 'report'].includes(input);
|
||||
}
|
||||
|
||||
type BudgetInnerProps = {
|
||||
categories: CategoryEntity[];
|
||||
categoryGroups: CategoryGroupEntity[];
|
||||
budgetType: 'rollover' | 'report';
|
||||
spreadsheet: ReturnType<typeof useSpreadsheet>;
|
||||
};
|
||||
export function Budget() {
|
||||
useSetThemeColor(theme.mobileViewTheme);
|
||||
|
||||
function BudgetInner(props: BudgetInnerProps) {
|
||||
const { categoryGroups, categories, budgetType, spreadsheet } = props;
|
||||
const { list: categories, grouped: categoryGroups } = useCategories();
|
||||
const [budgetTypePref] = useMetadataPref('budgetType');
|
||||
const budgetType = isBudgetType(budgetTypePref) ? budgetTypePref : 'rollover';
|
||||
const spreadsheet = useSpreadsheet();
|
||||
|
||||
const currMonth = monthUtils.currentMonth();
|
||||
const [startMonth = currMonth, setStartMonthPref] =
|
||||
@@ -60,9 +54,8 @@ function BudgetInner(props: BudgetInnerProps) {
|
||||
start: startMonth,
|
||||
end: startMonth,
|
||||
});
|
||||
const [initialized, setInitialized] = useState(false);
|
||||
// const [editMode, setEditMode] = useState(false);
|
||||
|
||||
const [initialized, setInitialized] = useState(false);
|
||||
const [_numberFormat] = useSyncedPref('numberFormat');
|
||||
const numberFormat = _numberFormat || 'comma-dot';
|
||||
const [hideFraction] = useSyncedPref('hideFraction');
|
||||
@@ -95,11 +88,14 @@ function BudgetInner(props: BudgetInnerProps) {
|
||||
return () => unlisten();
|
||||
}, [budgetType, startMonth, dispatch, spreadsheet]);
|
||||
|
||||
const onBudgetAction = async (month, type, args) => {
|
||||
dispatch(applyBudgetAction(month, type, args));
|
||||
};
|
||||
const onBudgetAction = useCallback(
|
||||
async (month, type, args) => {
|
||||
dispatch(applyBudgetAction(month, type, args));
|
||||
},
|
||||
[dispatch],
|
||||
);
|
||||
|
||||
const onShowBudgetSummary = () => {
|
||||
const onShowBudgetSummary = useCallback(() => {
|
||||
if (budgetType === 'report') {
|
||||
dispatch(
|
||||
pushModal('report-budget-summary', {
|
||||
@@ -114,9 +110,9 @@ function BudgetInner(props: BudgetInnerProps) {
|
||||
}),
|
||||
);
|
||||
}
|
||||
};
|
||||
}, [budgetType, dispatch, onBudgetAction, startMonth]);
|
||||
|
||||
const onOpenNewCategoryGroupModal = () => {
|
||||
const onOpenNewCategoryGroupModal = useCallback(() => {
|
||||
dispatch(
|
||||
pushModal('new-category-group', {
|
||||
onValidate: name => (!name ? 'Name is required.' : null),
|
||||
@@ -126,152 +122,180 @@ function BudgetInner(props: BudgetInnerProps) {
|
||||
},
|
||||
}),
|
||||
);
|
||||
};
|
||||
}, [dispatch]);
|
||||
|
||||
const onOpenNewCategoryModal = (groupId, isIncome) => {
|
||||
dispatch(
|
||||
pushModal('new-category', {
|
||||
onValidate: name => (!name ? 'Name is required.' : null),
|
||||
onSubmit: async name => {
|
||||
dispatch(collapseModals('category-group-menu'));
|
||||
dispatch(createCategory(name, groupId, isIncome, false));
|
||||
},
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const onSaveGroup = group => {
|
||||
dispatch(updateGroup(group));
|
||||
};
|
||||
|
||||
const onDeleteGroup = async groupId => {
|
||||
const group = categoryGroups?.find(g => g.id === groupId);
|
||||
|
||||
if (!group) {
|
||||
return;
|
||||
}
|
||||
|
||||
let mustTransfer = false;
|
||||
for (const category of group.categories ?? []) {
|
||||
if (await send('must-category-transfer', { id: category.id })) {
|
||||
mustTransfer = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (mustTransfer) {
|
||||
const onOpenNewCategoryModal = useCallback(
|
||||
(groupId, isIncome) => {
|
||||
dispatch(
|
||||
pushModal('confirm-category-delete', {
|
||||
group: groupId,
|
||||
onDelete: transferCategory => {
|
||||
pushModal('new-category', {
|
||||
onValidate: name => (!name ? 'Name is required.' : null),
|
||||
onSubmit: async name => {
|
||||
dispatch(collapseModals('category-group-menu'));
|
||||
dispatch(deleteGroup(groupId, transferCategory));
|
||||
dispatch(createCategory(name, groupId, isIncome, false));
|
||||
},
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
dispatch(collapseModals('category-group-menu'));
|
||||
dispatch(deleteGroup(groupId));
|
||||
}
|
||||
};
|
||||
},
|
||||
[dispatch],
|
||||
);
|
||||
|
||||
const onToggleGroupVisibility = groupId => {
|
||||
const group = categoryGroups.find(g => g.id === groupId);
|
||||
onSaveGroup({
|
||||
...group,
|
||||
hidden: !!!group.hidden,
|
||||
});
|
||||
dispatch(collapseModals('category-group-menu'));
|
||||
};
|
||||
const onSaveGroup = useCallback(
|
||||
group => {
|
||||
dispatch(updateGroup(group));
|
||||
},
|
||||
[dispatch],
|
||||
);
|
||||
|
||||
const onSaveCategory = category => {
|
||||
dispatch(updateCategory(category));
|
||||
};
|
||||
const onDeleteGroup = useCallback(
|
||||
async groupId => {
|
||||
const group = categoryGroups?.find(g => g.id === groupId);
|
||||
|
||||
const onDeleteCategory = async categoryId => {
|
||||
const mustTransfer = await send('must-category-transfer', {
|
||||
id: categoryId,
|
||||
});
|
||||
|
||||
if (mustTransfer) {
|
||||
dispatch(
|
||||
pushModal('confirm-category-delete', {
|
||||
category: categoryId,
|
||||
onDelete: transferCategory => {
|
||||
if (categoryId !== transferCategory) {
|
||||
dispatch(collapseModals('category-menu'));
|
||||
dispatch(deleteCategory(categoryId, transferCategory));
|
||||
}
|
||||
},
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
dispatch(collapseModals('category-menu'));
|
||||
dispatch(deleteCategory(categoryId));
|
||||
}
|
||||
};
|
||||
|
||||
const onToggleCategoryVisibility = categoryId => {
|
||||
const category = categories.find(c => c.id === categoryId);
|
||||
onSaveCategory({
|
||||
...category,
|
||||
hidden: !!!category.hidden,
|
||||
});
|
||||
dispatch(collapseModals('category-menu'));
|
||||
};
|
||||
|
||||
const onReorderCategory = (id, { inGroup, aroundCategory }) => {
|
||||
let groupId, targetId;
|
||||
|
||||
if (inGroup) {
|
||||
groupId = inGroup;
|
||||
} else if (aroundCategory) {
|
||||
const { id: originalCatId, position } = aroundCategory;
|
||||
|
||||
let catId = originalCatId;
|
||||
const group = categoryGroups.find(group =>
|
||||
group.categories?.find(cat => cat.id === catId),
|
||||
);
|
||||
|
||||
if (position === 'bottom') {
|
||||
const idx = group?.categories?.findIndex(cat => cat.id === catId) ?? -1;
|
||||
catId = group?.categories
|
||||
? idx < group.categories.length - 1
|
||||
? group.categories[idx + 1].id
|
||||
: null
|
||||
: null;
|
||||
if (!group) {
|
||||
return;
|
||||
}
|
||||
|
||||
groupId = group?.id;
|
||||
targetId = catId;
|
||||
}
|
||||
let mustTransfer = false;
|
||||
for (const category of group.categories ?? []) {
|
||||
if (await send('must-category-transfer', { id: category.id })) {
|
||||
mustTransfer = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
dispatch(moveCategory(id, groupId, targetId));
|
||||
};
|
||||
if (mustTransfer) {
|
||||
dispatch(
|
||||
pushModal('confirm-category-delete', {
|
||||
group: groupId,
|
||||
onDelete: transferCategory => {
|
||||
dispatch(collapseModals('category-group-menu'));
|
||||
dispatch(deleteGroup(groupId, transferCategory));
|
||||
},
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
dispatch(collapseModals('category-group-menu'));
|
||||
dispatch(deleteGroup(groupId));
|
||||
}
|
||||
},
|
||||
[categoryGroups, dispatch],
|
||||
);
|
||||
|
||||
const onReorderGroup = (id, targetId, position) => {
|
||||
if (position === 'bottom') {
|
||||
const idx = categoryGroups.findIndex(group => group.id === targetId);
|
||||
targetId =
|
||||
idx < categoryGroups.length - 1 ? categoryGroups[idx + 1].id : null;
|
||||
}
|
||||
const onToggleGroupVisibility = useCallback(
|
||||
groupId => {
|
||||
const group = categoryGroups.find(g => g.id === groupId);
|
||||
onSaveGroup({
|
||||
...group,
|
||||
hidden: !!!group.hidden,
|
||||
});
|
||||
dispatch(collapseModals('category-group-menu'));
|
||||
},
|
||||
[categoryGroups, dispatch, onSaveGroup],
|
||||
);
|
||||
|
||||
dispatch(moveCategoryGroup(id, targetId));
|
||||
};
|
||||
const onSaveCategory = useCallback(
|
||||
category => {
|
||||
dispatch(updateCategory(category));
|
||||
},
|
||||
[dispatch],
|
||||
);
|
||||
|
||||
const onPrevMonth = async () => {
|
||||
const onDeleteCategory = useCallback(
|
||||
async categoryId => {
|
||||
const mustTransfer = await send('must-category-transfer', {
|
||||
id: categoryId,
|
||||
});
|
||||
|
||||
if (mustTransfer) {
|
||||
dispatch(
|
||||
pushModal('confirm-category-delete', {
|
||||
category: categoryId,
|
||||
onDelete: transferCategory => {
|
||||
if (categoryId !== transferCategory) {
|
||||
dispatch(collapseModals('category-menu'));
|
||||
dispatch(deleteCategory(categoryId, transferCategory));
|
||||
}
|
||||
},
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
dispatch(collapseModals('category-menu'));
|
||||
dispatch(deleteCategory(categoryId));
|
||||
}
|
||||
},
|
||||
[dispatch],
|
||||
);
|
||||
|
||||
const onToggleCategoryVisibility = useCallback(
|
||||
categoryId => {
|
||||
const category = categories.find(c => c.id === categoryId);
|
||||
onSaveCategory({
|
||||
...category,
|
||||
hidden: !!!category.hidden,
|
||||
});
|
||||
dispatch(collapseModals('category-menu'));
|
||||
},
|
||||
[categories, dispatch, onSaveCategory],
|
||||
);
|
||||
|
||||
const onReorderCategory = useCallback(
|
||||
(id, { inGroup, aroundCategory }) => {
|
||||
let groupId, targetId;
|
||||
|
||||
if (inGroup) {
|
||||
groupId = inGroup;
|
||||
} else if (aroundCategory) {
|
||||
const { id: originalCatId, position } = aroundCategory;
|
||||
|
||||
let catId = originalCatId;
|
||||
const group = categoryGroups.find(group =>
|
||||
group.categories?.find(cat => cat.id === catId),
|
||||
);
|
||||
|
||||
if (position === 'bottom') {
|
||||
const idx =
|
||||
group?.categories?.findIndex(cat => cat.id === catId) ?? -1;
|
||||
catId = group?.categories
|
||||
? idx < group.categories.length - 1
|
||||
? group.categories[idx + 1].id
|
||||
: null
|
||||
: null;
|
||||
}
|
||||
|
||||
groupId = group?.id;
|
||||
targetId = catId;
|
||||
}
|
||||
|
||||
dispatch(moveCategory(id, groupId, targetId));
|
||||
},
|
||||
[categoryGroups, dispatch],
|
||||
);
|
||||
|
||||
const onReorderGroup = useCallback(
|
||||
(id, targetId, position) => {
|
||||
if (position === 'bottom') {
|
||||
const idx = categoryGroups.findIndex(group => group.id === targetId);
|
||||
targetId =
|
||||
idx < categoryGroups.length - 1 ? categoryGroups[idx + 1].id : null;
|
||||
}
|
||||
|
||||
dispatch(moveCategoryGroup(id, targetId));
|
||||
},
|
||||
[categoryGroups, dispatch],
|
||||
);
|
||||
|
||||
const onPrevMonth = useCallback(async () => {
|
||||
const month = monthUtils.subMonths(startMonth, 1);
|
||||
await prewarmMonth(budgetType, spreadsheet, month);
|
||||
setStartMonthPref(month);
|
||||
setInitialized(true);
|
||||
};
|
||||
}, [budgetType, setStartMonthPref, spreadsheet, startMonth]);
|
||||
|
||||
const onNextMonth = async () => {
|
||||
const onNextMonth = useCallback(async () => {
|
||||
const month = monthUtils.addMonths(startMonth, 1);
|
||||
await prewarmMonth(budgetType, spreadsheet, month);
|
||||
setStartMonthPref(month);
|
||||
setInitialized(true);
|
||||
};
|
||||
}, [budgetType, setStartMonthPref, spreadsheet, startMonth]);
|
||||
|
||||
// const onOpenMonthActionMenu = () => {
|
||||
// const options = [
|
||||
@@ -312,94 +336,128 @@ function BudgetInner(props: BudgetInnerProps) {
|
||||
// );
|
||||
// };
|
||||
|
||||
const onSaveNotes = async (id, notes) => {
|
||||
const onSaveNotes = useCallback(async (id, notes) => {
|
||||
await send('notes-save', { id, note: notes });
|
||||
};
|
||||
}, []);
|
||||
|
||||
const onOpenCategoryGroupNotesModal = id => {
|
||||
const group = categoryGroups.find(g => g.id === id);
|
||||
dispatch(
|
||||
pushModal('notes', {
|
||||
id,
|
||||
name: group.name,
|
||||
onSave: onSaveNotes,
|
||||
}),
|
||||
);
|
||||
};
|
||||
const onOpenCategoryGroupNotesModal = useCallback(
|
||||
id => {
|
||||
const group = categoryGroups.find(g => g.id === id);
|
||||
dispatch(
|
||||
pushModal('notes', {
|
||||
id,
|
||||
name: group.name,
|
||||
onSave: onSaveNotes,
|
||||
}),
|
||||
);
|
||||
},
|
||||
[categoryGroups, dispatch, onSaveNotes],
|
||||
);
|
||||
|
||||
const onOpenCategoryNotesModal = id => {
|
||||
const category = categories.find(c => c.id === id);
|
||||
dispatch(
|
||||
pushModal('notes', {
|
||||
id,
|
||||
name: category.name,
|
||||
onSave: onSaveNotes,
|
||||
}),
|
||||
);
|
||||
};
|
||||
const onOpenCategoryNotesModal = useCallback(
|
||||
id => {
|
||||
const category = categories.find(c => c.id === id);
|
||||
dispatch(
|
||||
pushModal('notes', {
|
||||
id,
|
||||
name: category.name,
|
||||
onSave: onSaveNotes,
|
||||
}),
|
||||
);
|
||||
},
|
||||
[categories, dispatch, onSaveNotes],
|
||||
);
|
||||
|
||||
const onOpenCategoryGroupMenuModal = id => {
|
||||
const group = categoryGroups.find(g => g.id === id);
|
||||
dispatch(
|
||||
pushModal('category-group-menu', {
|
||||
groupId: group.id,
|
||||
onSave: onSaveGroup,
|
||||
onAddCategory: onOpenNewCategoryModal,
|
||||
onEditNotes: onOpenCategoryGroupNotesModal,
|
||||
onDelete: onDeleteGroup,
|
||||
onToggleVisibility: onToggleGroupVisibility,
|
||||
}),
|
||||
);
|
||||
};
|
||||
const onOpenCategoryGroupMenuModal = useCallback(
|
||||
id => {
|
||||
const group = categoryGroups.find(g => g.id === id);
|
||||
dispatch(
|
||||
pushModal('category-group-menu', {
|
||||
groupId: group.id,
|
||||
onSave: onSaveGroup,
|
||||
onAddCategory: onOpenNewCategoryModal,
|
||||
onEditNotes: onOpenCategoryGroupNotesModal,
|
||||
onDelete: onDeleteGroup,
|
||||
onToggleVisibility: onToggleGroupVisibility,
|
||||
}),
|
||||
);
|
||||
},
|
||||
[
|
||||
categoryGroups,
|
||||
dispatch,
|
||||
onDeleteGroup,
|
||||
onOpenCategoryGroupNotesModal,
|
||||
onOpenNewCategoryModal,
|
||||
onSaveGroup,
|
||||
onToggleGroupVisibility,
|
||||
],
|
||||
);
|
||||
|
||||
const onOpenCategoryMenuModal = id => {
|
||||
const category = categories.find(c => c.id === id);
|
||||
dispatch(
|
||||
pushModal('category-menu', {
|
||||
categoryId: category.id,
|
||||
onSave: onSaveCategory,
|
||||
onEditNotes: onOpenCategoryNotesModal,
|
||||
onDelete: onDeleteCategory,
|
||||
onToggleVisibility: onToggleCategoryVisibility,
|
||||
onBudgetAction,
|
||||
}),
|
||||
);
|
||||
};
|
||||
const onOpenCategoryMenuModal = useCallback(
|
||||
id => {
|
||||
const category = categories.find(c => c.id === id);
|
||||
dispatch(
|
||||
pushModal('category-menu', {
|
||||
categoryId: category.id,
|
||||
onSave: onSaveCategory,
|
||||
onEditNotes: onOpenCategoryNotesModal,
|
||||
onDelete: onDeleteCategory,
|
||||
onToggleVisibility: onToggleCategoryVisibility,
|
||||
onBudgetAction,
|
||||
}),
|
||||
);
|
||||
},
|
||||
[
|
||||
categories,
|
||||
dispatch,
|
||||
onBudgetAction,
|
||||
onDeleteCategory,
|
||||
onOpenCategoryNotesModal,
|
||||
onSaveCategory,
|
||||
onToggleCategoryVisibility,
|
||||
],
|
||||
);
|
||||
|
||||
const [showHiddenCategories, setShowHiddenCategoriesPref] = useLocalPref(
|
||||
'budget.showHiddenCategories',
|
||||
);
|
||||
|
||||
const onToggleHiddenCategories = () => {
|
||||
const onToggleHiddenCategories = useCallback(() => {
|
||||
setShowHiddenCategoriesPref(!showHiddenCategories);
|
||||
dispatch(collapseModals('budget-page-menu'));
|
||||
};
|
||||
}, [dispatch, setShowHiddenCategoriesPref, showHiddenCategories]);
|
||||
|
||||
const onOpenBudgetMonthNotesModal = month => {
|
||||
dispatch(
|
||||
pushModal('notes', {
|
||||
id: `budget-${month}`,
|
||||
name: monthUtils.format(month, 'MMMM ‘yy'),
|
||||
onSave: onSaveNotes,
|
||||
}),
|
||||
);
|
||||
};
|
||||
const onOpenBudgetMonthNotesModal = useCallback(
|
||||
month => {
|
||||
dispatch(
|
||||
pushModal('notes', {
|
||||
id: `budget-${month}`,
|
||||
name: monthUtils.format(month, 'MMMM ‘yy'),
|
||||
onSave: onSaveNotes,
|
||||
}),
|
||||
);
|
||||
},
|
||||
[dispatch, onSaveNotes],
|
||||
);
|
||||
|
||||
const onSwitchBudgetFile = () => {
|
||||
const onSwitchBudgetFile = useCallback(() => {
|
||||
dispatch(pushModal('budget-list'));
|
||||
};
|
||||
}, [dispatch]);
|
||||
|
||||
const onOpenBudgetMonthMenu = month => {
|
||||
dispatch(
|
||||
pushModal(`${budgetType}-budget-month-menu`, {
|
||||
month,
|
||||
onBudgetAction,
|
||||
onEditNotes: onOpenBudgetMonthNotesModal,
|
||||
}),
|
||||
);
|
||||
};
|
||||
const onOpenBudgetMonthMenu = useCallback(
|
||||
month => {
|
||||
dispatch(
|
||||
pushModal(`${budgetType}-budget-month-menu`, {
|
||||
month,
|
||||
onBudgetAction,
|
||||
onEditNotes: onOpenBudgetMonthNotesModal,
|
||||
}),
|
||||
);
|
||||
},
|
||||
[budgetType, dispatch, onBudgetAction, onOpenBudgetMonthNotesModal],
|
||||
);
|
||||
|
||||
const onOpenBudgetPageMenu = () => {
|
||||
const onOpenBudgetPageMenu = useCallback(() => {
|
||||
dispatch(
|
||||
pushModal('budget-page-menu', {
|
||||
onAddCategoryGroup: onOpenNewCategoryGroupModal,
|
||||
@@ -407,7 +465,12 @@ function BudgetInner(props: BudgetInnerProps) {
|
||||
onSwitchBudgetFile,
|
||||
}),
|
||||
);
|
||||
};
|
||||
}, [
|
||||
dispatch,
|
||||
onOpenNewCategoryGroupModal,
|
||||
onSwitchBudgetFile,
|
||||
onToggleHiddenCategories,
|
||||
]);
|
||||
|
||||
if (!categoryGroups || !initialized) {
|
||||
return (
|
||||
@@ -464,18 +527,3 @@ function BudgetInner(props: BudgetInnerProps) {
|
||||
</NamespaceContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function Budget() {
|
||||
const { list: categories, grouped: categoryGroups } = useCategories();
|
||||
const [budgetType] = useMetadataPref('budgetType');
|
||||
const spreadsheet = useSpreadsheet();
|
||||
useSetThemeColor(theme.mobileViewTheme);
|
||||
return (
|
||||
<BudgetInner
|
||||
categoryGroups={categoryGroups}
|
||||
categories={categories}
|
||||
budgetType={isBudgetType(budgetType) ? budgetType : 'rollover'}
|
||||
spreadsheet={spreadsheet}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
6
upcoming-release-notes/3448.md
Normal file
6
upcoming-release-notes/3448.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
category: Maintenance
|
||||
authors: [joel-jeremy]
|
||||
---
|
||||
|
||||
Reduce budget table re-renders
|
||||
Reference in New Issue
Block a user