From c62c2f2a0946b1650720f76b2cbc6fbcd4051a9a Mon Sep 17 00:00:00 2001 From: Joel Jeremy Marquez Date: Fri, 28 Feb 2025 12:37:11 -0800 Subject: [PATCH] Cleanup --- .../components/mobile/budget/BudgetCell.tsx | 160 +++++++ .../components/mobile/budget/BudgetTable.jsx | 405 +----------------- .../mobile/budget/IncomeCategoryList.tsx | 302 +++++++++++++ 3 files changed, 470 insertions(+), 397 deletions(-) create mode 100644 packages/desktop-client/src/components/mobile/budget/BudgetCell.tsx create mode 100644 packages/desktop-client/src/components/mobile/budget/IncomeCategoryList.tsx diff --git a/packages/desktop-client/src/components/mobile/budget/BudgetCell.tsx b/packages/desktop-client/src/components/mobile/budget/BudgetCell.tsx new file mode 100644 index 00000000000..24f2a6d8dbc --- /dev/null +++ b/packages/desktop-client/src/components/mobile/budget/BudgetCell.tsx @@ -0,0 +1,160 @@ +import { type ComponentPropsWithoutRef } from 'react'; +import { useTranslation } from 'react-i18next'; + +import { Button } from '@actual-app/components/button'; +import { type CSSProperties } from '@actual-app/components/styles'; +import { Text } from '@actual-app/components/text'; +import { View } from '@actual-app/components/view'; +import { AutoTextSize } from 'auto-text-size'; + +import { pushModal } from 'loot-core/client/actions'; +import { integerToCurrency } from 'loot-core/shared/util'; +import { type CategoryEntity } from 'loot-core/types/models'; + +import { useNotes } from '../../../hooks/useNotes'; +import { useSyncedPref } from '../../../hooks/useSyncedPref'; +import { useUndo } from '../../../hooks/useUndo'; +import { useDispatch } from '../../../redux'; +import { makeAmountGrey } from '../../budget/util'; +import { PrivacyFilter } from '../../PrivacyFilter'; +import { type SheetFields } from '../../spreadsheet'; +import { CellValue } from '../../spreadsheet/CellValue'; +import { useFormat } from '../../spreadsheet/useFormat'; + +import { getColumnWidth, PILL_STYLE } from './BudgetTable'; + +type BudgetCellProps< + SheetFieldName extends SheetFields<'envelope-budget' | 'tracking-budget'>, +> = ComponentPropsWithoutRef< + typeof CellValue<'envelope-budget' | 'tracking-budget', SheetFieldName> +> & { + category: CategoryEntity; + style?: CSSProperties; + month: string; + onBudgetAction: (month: string, action: string, args: unknown) => void; +}; + +export function BudgetCell< + SheetFieldName extends SheetFields<'envelope-budget' | 'tracking-budget'>, +>({ + binding, + category, + month, + onBudgetAction, + style, + children, + ...props +}: BudgetCellProps) { + const { t } = useTranslation(); + const columnWidth = getColumnWidth(); + const dispatch = useDispatch(); + const format = useFormat(); + const { showUndoNotification } = useUndo(); + const [budgetType = 'rollover'] = useSyncedPref('budgetType'); + const modalBudgetType = budgetType === 'rollover' ? 'envelope' : 'tracking'; + + const categoryBudgetMenuModal = `${modalBudgetType}-budget-menu` as const; + const categoryNotes = useNotes(category.id); + + const onOpenCategoryBudgetMenu = () => { + dispatch( + pushModal(categoryBudgetMenuModal, { + categoryId: category.id, + month, + onUpdateBudget: amount => { + onBudgetAction(month, 'budget-amount', { + category: category.id, + amount, + }); + showUndoNotification({ + message: `${category.name} budget has been updated to ${integerToCurrency(amount)}.`, + }); + }, + onCopyLastMonthAverage: () => { + onBudgetAction(month, 'copy-single-last', { + category: category.id, + }); + showUndoNotification({ + message: `${category.name} budget has been set last to month’s budgeted amount.`, + }); + }, + onSetMonthsAverage: numberOfMonths => { + if ( + numberOfMonths !== 3 && + numberOfMonths !== 6 && + numberOfMonths !== 12 + ) { + return; + } + + onBudgetAction(month, `set-single-${numberOfMonths}-avg`, { + category: category.id, + }); + showUndoNotification({ + message: `${category.name} budget has been set to ${numberOfMonths === 12 ? 'yearly' : `${numberOfMonths} month`} average.`, + }); + }, + onApplyBudgetTemplate: () => { + onBudgetAction(month, 'apply-single-category-template', { + category: category.id, + }); + showUndoNotification({ + message: `${category.name} budget templates have been applied.`, + pre: categoryNotes, + }); + }, + }), + ); + }; + + return ( + + {({ type, name, value }) => + children?.({ + type, + name, + value, + }) || ( + + ) + } + + ); +} diff --git a/packages/desktop-client/src/components/mobile/budget/BudgetTable.jsx b/packages/desktop-client/src/components/mobile/budget/BudgetTable.jsx index a1b6553a066..49abe2e0cf5 100644 --- a/packages/desktop-client/src/components/mobile/budget/BudgetTable.jsx +++ b/packages/desktop-client/src/components/mobile/budget/BudgetTable.jsx @@ -1,12 +1,5 @@ import React, { memo, useCallback, useRef } from 'react'; -import { - DropIndicator, - ListBox, - ListBoxItem, - useDragAndDrop, -} from 'react-aria-components'; import { useTranslation } from 'react-i18next'; -import { useListData } from 'react-stately'; import { Button } from '@actual-app/components/button'; import { Card } from '@actual-app/components/card'; @@ -24,7 +17,6 @@ import { trackingBudget, uncategorizedCount, } from 'loot-core/client/queries'; -import { moveCategory } from 'loot-core/client/queries/queriesSlice'; import * as monthUtils from 'loot-core/shared/months'; import { groupById, integerToCurrency } from 'loot-core/shared/util'; @@ -32,7 +24,6 @@ import { useCategories } from '../../../hooks/useCategories'; import { useFeatureFlag } from '../../../hooks/useFeatureFlag'; import { useLocalPref } from '../../../hooks/useLocalPref'; import { useNavigate } from '../../../hooks/useNavigate'; -import { useNotes } from '../../../hooks/useNotes'; import { useSyncedPref } from '../../../hooks/useSyncedPref'; import { useUndo } from '../../../hooks/useUndo'; import { SvgLogo } from '../../../icons/logo'; @@ -58,15 +49,21 @@ import { useSheetValue } from '../../spreadsheet/useSheetValue'; import { MOBILE_NAV_HEIGHT } from '../MobileNavTabs'; import { PullToRefresh } from '../PullToRefresh'; +import { BudgetCell } from './BudgetCell'; +import { IncomeCategoryList } from './IncomeCategoryList'; import { ListItem } from './ListItem'; -const PILL_STYLE = { +export const PILL_STYLE = { borderRadius: 16, color: theme.pillText, backgroundColor: theme.pillBackgroundLight, }; -function getColumnWidth({ show3Cols, isSidebar = false, offset = 0 } = {}) { +export function getColumnWidth({ + show3Cols, + isSidebar = false, + offset = 0, +} = {}) { // If show3Cols = 35vw | 20vw | 20vw | 20vw, // Else = 45vw | 25vw | 25vw, if (!isSidebar) { @@ -227,131 +224,6 @@ function Saved({ projected, onPress, show3Cols }) { ); } -function BudgetCell({ - name, - binding, - style, - category, - month, - onBudgetAction, - children, - ...props -}) { - const { t } = useTranslation(); - const columnWidth = getColumnWidth(); - const dispatch = useDispatch(); - const format = useFormat(); - const { showUndoNotification } = useUndo(); - const [budgetType = 'rollover'] = useSyncedPref('budgetType'); - const modalBudgetType = budgetType === 'rollover' ? 'envelope' : 'tracking'; - - const categoryBudgetMenuModal = `${modalBudgetType}-budget-menu`; - const categoryNotes = useNotes(category.id); - - const onOpenCategoryBudgetMenu = () => { - dispatch( - pushModal(categoryBudgetMenuModal, { - categoryId: category.id, - month, - onUpdateBudget: amount => { - onBudgetAction(month, 'budget-amount', { - category: category.id, - amount, - }); - showUndoNotification({ - message: `${category.name} budget has been updated to ${integerToCurrency(amount)}.`, - }); - }, - onCopyLastMonthAverage: () => { - onBudgetAction(month, 'copy-single-last', { - category: category.id, - }); - showUndoNotification({ - message: `${category.name} budget has been set last to month’s budgeted amount.`, - }); - }, - onSetMonthsAverage: numberOfMonths => { - if ( - numberOfMonths !== 3 && - numberOfMonths !== 6 && - numberOfMonths !== 12 - ) { - return; - } - - onBudgetAction(month, `set-single-${numberOfMonths}-avg`, { - category: category.id, - }); - showUndoNotification({ - message: `${category.name} budget has been set to ${numberOfMonths === 12 ? 'yearly' : `${numberOfMonths} month`} average.`, - }); - }, - onApplyBudgetTemplate: () => { - onBudgetAction(month, 'apply-single-category-template', { - category: category.id, - }); - showUndoNotification({ - message: `${category.name} budget templates have been applied.`, - pre: categoryNotes, - }); - }, - }), - ); - }; - - return ( - - {({ type, name, value }) => - children?.({ - type, - name, - value, - onPress: onOpenCategoryBudgetMenu, - }) || ( - - ) - } - - ); -} - // eslint-disable-next-line @typescript-eslint/no-unused-vars function ExpenseGroupPreview({ group, pending, style }) { return ( @@ -1153,172 +1025,6 @@ const IncomeGroupHeader = memo(function IncomeGroupHeader({ ); }); -function IncomeCategoryName({ category, onEdit }) { - const sidebarColumnWidth = getColumnWidth({ isSidebar: true, offset: -10 }); - return ( - - - - ); -} - -function IncomeCategoryCells({ category, month, onBudgetAction }) { - const { t } = useTranslation(); - const format = useFormat(); - const columnWidth = getColumnWidth(); - const [budgetType = 'rollover'] = useSyncedPref('budgetType'); - - const budgeted = - budgetType === 'report' ? trackingBudget.catBudgeted(category.id) : null; - - const balance = - budgetType === 'report' - ? trackingBudget.catSumAmount(category.id) - : envelopeBudget.catSumAmount(category.id); - - return ( - - {budgeted && ( - - - - )} - - {({ type, value }) => ( - - - - {format(value, type)} - - - - )} - - - ); -} - -function IncomeCategoryListItem({ - month, - style, - onEdit, - onBudgetAction, - ...props -}) { - const listItemRef = useRef(); - const { value: category } = props; - - return ( - - - - - - - ); -} - const ExpenseGroup = memo(function ExpenseGroup({ type, group, @@ -1537,101 +1243,6 @@ function IncomeGroup({ ); } -function IncomeCategoryList({ - categories, - month, - onEditCategory, - onBudgetAction, -}) { - const { t } = useTranslation(); - const categoryListData = useListData({ - initialItems: categories, - getKey: category => category.id, - }); - const dispatch = useDispatch(); - - const { dragAndDropHooks } = useDragAndDrop({ - getItems: keys => - [...keys].map(key => ({ - 'text/plain': categoryListData.getItem(key).id, - })), - renderDropIndicator(target) { - return ( - - ); - }, - onReorder(e) { - const [key] = e.keys; - const categoryIdToMove = key; - const categoryGroupId = categoryListData.getItem(key).cat_group; - const targetCategoryId = e.target.key; - - if (e.target.dropPosition === 'before') { - categoryListData.moveBefore(e.target.key, e.keys); - - dispatch( - moveCategory({ - id: categoryIdToMove, - groupId: categoryGroupId, - targetId: targetCategoryId, - }), - ); - } else if (e.target.dropPosition === 'after') { - categoryListData.moveAfter(e.target.key, e.keys); - - const { index: targetIndex } = categoryListData.getItem(e.target.key); - const nextToTargetCategory = categoryListData.items[targetIndex + 1]; - - dispatch( - moveCategory({ - id: categoryIdToMove, - groupId: categoryGroupId, - // Due to the way `moveCategory` works, we use the category next to the - // actual target category here because `moveCategory` always shoves the - // category *before* the target category. - // On the other hand, using `null` as `targetId` moves the category - // to the end of the list. - targetId: nextToTargetCategory?.id || null, - }), - ); - } - }, - }); - - return ( - - {category => ( - - )} - - ); -} - function UncategorizedButton() { const count = useSheetValue(uncategorizedCount()); if (count === null || count <= 0) { diff --git a/packages/desktop-client/src/components/mobile/budget/IncomeCategoryList.tsx b/packages/desktop-client/src/components/mobile/budget/IncomeCategoryList.tsx new file mode 100644 index 00000000000..2b9f945e0a2 --- /dev/null +++ b/packages/desktop-client/src/components/mobile/budget/IncomeCategoryList.tsx @@ -0,0 +1,302 @@ +import { useRef } from 'react'; +import { + DropIndicator, + ListBox, + ListBoxItem, + useDragAndDrop, +} from 'react-aria-components'; +import { useTranslation } from 'react-i18next'; +import { useListData } from 'react-stately'; + +import { Button } from '@actual-app/components/button'; +import { styles } from '@actual-app/components/styles'; +import { Text } from '@actual-app/components/text'; +import { theme } from '@actual-app/components/theme'; +import { View } from '@actual-app/components/view'; +import { AutoTextSize } from 'auto-text-size'; + +import { envelopeBudget, trackingBudget } from 'loot-core/client/queries'; +import { moveCategory } from 'loot-core/client/queries/queriesSlice'; +import * as monthUtils from 'loot-core/shared/months'; +import { type CategoryEntity } from 'loot-core/types/models'; + +import { useSyncedPref } from '../../../hooks/useSyncedPref'; +import { SvgCheveronRight } from '../../../icons/v1'; +import { useDispatch } from '../../../redux'; +import { PrivacyFilter } from '../../PrivacyFilter'; +import { CellValue } from '../../spreadsheet/CellValue'; +import { useFormat } from '../../spreadsheet/useFormat'; + +import { BudgetCell } from './BudgetCell'; +import { getColumnWidth } from './BudgetTable'; + +type IncomeCategoryListProps = { + categories: CategoryEntity[]; + month: string; + onEditCategory: (id: string) => void; + onBudgetAction: (month: string, action: string, args: unknown) => void; +}; + +export function IncomeCategoryList({ + categories, + month, + onEditCategory, + onBudgetAction, +}: IncomeCategoryListProps) { + const { t } = useTranslation(); + const categoryListData = useListData({ + initialItems: categories.map((category, index) => ({ + ...category, + index, + })), + getKey: category => category.id, + }); + const dispatch = useDispatch(); + + const { dragAndDropHooks } = useDragAndDrop({ + getItems: keys => + [...keys].map(key => ({ + 'text/plain': categoryListData.getItem(key).id, + })), + renderDropIndicator(target) { + return ( + + ); + }, + onReorder(e) { + const [key] = e.keys; + const categoryIdToMove = key as CategoryEntity['id']; + const categoryGroupId = categoryListData.getItem(key).cat_group; + const targetCategoryId = e.target.key as CategoryEntity['id']; + + if (e.target.dropPosition === 'before') { + categoryListData.moveBefore(e.target.key, e.keys); + + dispatch( + moveCategory({ + id: categoryIdToMove, + groupId: categoryGroupId, + targetId: targetCategoryId, + }), + ); + } else if (e.target.dropPosition === 'after') { + categoryListData.moveAfter(e.target.key, e.keys); + + const { index: targetIndex } = categoryListData.getItem(e.target.key); + const nextToTargetCategory = categoryListData.items[targetIndex + 1]; + + dispatch( + moveCategory({ + id: categoryIdToMove, + groupId: categoryGroupId, + // Due to the way `moveCategory` works, we use the category next to the + // actual target category here because `moveCategory` always shoves the + // category *before* the target category. + // On the other hand, using `null` as `targetId` moves the category + // to the end of the list. + targetId: nextToTargetCategory?.id || null, + }), + ); + } + }, + }); + + return ( + + {category => ( + + )} + + ); +} + +function IncomeCategoryName({ category, onEdit }) { + const sidebarColumnWidth = getColumnWidth({ isSidebar: true, offset: -10 }); + return ( + + + + ); +} + +function IncomeCategoryCells({ category, month, onBudgetAction }) { + const { t } = useTranslation(); + const format = useFormat(); + const columnWidth = getColumnWidth(); + const [budgetType = 'rollover'] = useSyncedPref('budgetType'); + + const budgeted = + budgetType === 'report' ? trackingBudget.catBudgeted(category.id) : null; + + const balance = + budgetType === 'report' + ? trackingBudget.catSumAmount(category.id) + : envelopeBudget.catSumAmount(category.id); + + return ( + + {budgeted && ( + + + + )} + + binding={balance} + type="financial" + aria-label={t('Balance for {{categoryName}} category', { + categoryName: category.name, + })} // Translated aria-label + > + {({ type, value }) => ( + + + + {format(value, type)} + + + + )} + + + ); +} + +function IncomeCategoryListItem({ + month, + style, + onEdit, + onBudgetAction, + ...props +}) { + const listItemRef = useRef(); + const { value: category } = props; + + return ( + + + + + + + ); +}