diff --git a/packages/desktop-client/src/components/budget-views/BudgetViews.tsx b/packages/desktop-client/src/components/budget-views/BudgetViews.tsx index 8567bb37cb..468bcc5902 100644 --- a/packages/desktop-client/src/components/budget-views/BudgetViews.tsx +++ b/packages/desktop-client/src/components/budget-views/BudgetViews.tsx @@ -3,14 +3,16 @@ import { Trans, useTranslation } from 'react-i18next'; import { Button } from '@actual-app/components/button'; import { SvgDelete } from '@actual-app/components/icons/v0'; -import { SvgAdd, SvgEditPencil, SvgPencilWrite } from '@actual-app/components/icons/v1'; +import { + SvgAdd, + SvgEditPencil, + SvgPencilWrite, +} from '@actual-app/components/icons/v1'; import { SpaceBetween } from '@actual-app/components/space-between'; import { Text } from '@actual-app/components/text'; import { theme } from '@actual-app/components/theme'; import { View } from '@actual-app/components/view'; -import { type CategoryEntity } from 'loot-core/types/models'; - import { useDraggable, useDroppable, @@ -19,6 +21,7 @@ import { type OnDropCallback, type DragState, } from '@desktop-client/components/sort'; +import { useBudgetViews } from '@desktop-client/hooks/useBudgetViews'; import { useCategories } from '@desktop-client/hooks/useCategories'; import { useDragRef } from '@desktop-client/hooks/useDragRef'; import { useSyncedPrefJson } from '@desktop-client/hooks/useSyncedPrefJson'; @@ -123,7 +126,8 @@ function BudgetViewItem({ export function BudgetViews() { const { t } = useTranslation(); const dispatch = useDispatch(); - const { list: categories, grouped: categoryGroups = [] } = useCategories(); + const { list: categories } = useCategories(); + const { setViewCategoryOrder, setViewGroupOrder } = useBudgetViews(); const [budgetViewMap = {}, setBudgetViewMapPref] = useSyncedPrefJson< 'budget.budgetViewMap', Record @@ -207,6 +211,8 @@ export function BudgetViews() { if (Array.isArray(customViews)) { setCustomViews(customViews.filter((v: BudgetView) => v.id !== viewId)); } + setViewCategoryOrder(viewId, []); + setViewGroupOrder(viewId, []); }, [ budgetViewMap, @@ -214,6 +220,8 @@ export function BudgetViews() { views, setBudgetViewMapPref, setCustomViews, + setViewCategoryOrder, + setViewGroupOrder, t, ], ); diff --git a/packages/desktop-client/src/components/budget/index.tsx b/packages/desktop-client/src/components/budget/index.tsx index cbaec6d706..d7ecfdd257 100644 --- a/packages/desktop-client/src/components/budget/index.tsx +++ b/packages/desktop-client/src/components/budget/index.tsx @@ -95,6 +95,9 @@ function BudgetInner(props: BudgetInnerProps) { string[] | null >(null); const { + views = [], + viewCategoryOrder = {}, + viewGroupOrder = {}, setViewCategoryOrder, setViewGroupOrder, viewMap = {}, @@ -128,6 +131,76 @@ function BudgetInner(props: BudgetInnerProps) { }); }, [props.accountId]); + useEffect(() => { + if (!categoryGroups || categoryGroups.length === 0) { + return; + } + + const validGroupIds = new Set(categoryGroups.map(group => group.id)); + + Object.entries(viewGroupOrder).forEach(([viewId, order]) => { + if (!Array.isArray(order) || order.length === 0) { + return; + } + + const filtered = order.filter(groupId => validGroupIds.has(groupId)); + + if (filtered.length !== order.length) { + setViewGroupOrder(viewId, filtered); + } + }); + }, [categoryGroups, viewGroupOrder, setViewGroupOrder]); + + useEffect(() => { + if (!categoryGroups || categoryGroups.length === 0) { + return; + } + + const globalCategoryOrder = categoryGroups.flatMap(group => + (group.categories || []).map(category => category.id), + ); + + const getCategoriesForView = (viewId: string) => + globalCategoryOrder.filter(catId => + Array.isArray(viewMap[catId]) ? viewMap[catId].includes(viewId) : false, + ); + + const getGroupsForView = (viewId: string) => + categoryGroups + .filter(group => + (group.categories || []).some(category => + Array.isArray(viewMap[category.id]) + ? viewMap[category.id].includes(viewId) + : false, + ), + ) + .map(group => group.id); + + views.forEach(view => { + if (!viewCategoryOrder[view.id]) { + const categoryIds = getCategoriesForView(view.id); + if (categoryIds.length > 0) { + setViewCategoryOrder(view.id, categoryIds); + } + } + + if (!viewGroupOrder[view.id]) { + const groupIds = getGroupsForView(view.id); + if (groupIds.length > 0) { + setViewGroupOrder(view.id, groupIds); + } + } + }); + }, [ + categoryGroups, + views, + viewMap, + viewCategoryOrder, + viewGroupOrder, + setViewCategoryOrder, + setViewGroupOrder, + ]); + const onMonthSelect = async (month, numDisplayed) => { setStartMonthPref(month); diff --git a/packages/desktop-client/src/hooks/useBudgetViews.ts b/packages/desktop-client/src/hooks/useBudgetViews.ts index 3cb18eefdb..bc7c2287b4 100644 --- a/packages/desktop-client/src/hooks/useBudgetViews.ts +++ b/packages/desktop-client/src/hooks/useBudgetViews.ts @@ -136,6 +136,23 @@ export function useBudgetViews(): { // Set category order for a specific view const setViewCategoryOrder = (viewId: string, categoryIds: string[]) => { + if (!categoryIds || categoryIds.length === 0) { + if (viewCategoryOrder[viewId]) { + const { [viewId]: _, ...rest } = viewCategoryOrder; + setViewCategoryOrderPref(rest); + } + return; + } + + const existingOrder = viewCategoryOrder[viewId]; + if ( + existingOrder && + existingOrder.length === categoryIds.length && + existingOrder.every((id, index) => id === categoryIds[index]) + ) { + return; + } + setViewCategoryOrderPref({ ...viewCategoryOrder, [viewId]: categoryIds, @@ -143,6 +160,23 @@ export function useBudgetViews(): { }; const setViewGroupOrder = (viewId: string, groupIds: string[]) => { + if (!groupIds || groupIds.length === 0) { + if (viewGroupOrder[viewId]) { + const { [viewId]: _, ...rest } = viewGroupOrder; + setViewGroupOrderPref(rest); + } + return; + } + + const existingOrder = viewGroupOrder[viewId]; + if ( + existingOrder && + existingOrder.length === groupIds.length && + existingOrder.every((id, index) => id === groupIds[index]) + ) { + return; + } + setViewGroupOrderPref({ ...viewGroupOrder, [viewId]: groupIds,