diff --git a/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-creates-a-transaction-from-accounts-id-page-1-chromium-linux.png b/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-creates-a-transaction-from-accounts-id-page-1-chromium-linux.png index 1aeaa14f98c..ce13427af87 100644 Binary files a/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-creates-a-transaction-from-accounts-id-page-1-chromium-linux.png and b/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-creates-a-transaction-from-accounts-id-page-1-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-creates-a-transaction-from-accounts-id-page-2-chromium-linux.png b/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-creates-a-transaction-from-accounts-id-page-2-chromium-linux.png index 76c562bd188..4b22f0e2f6e 100644 Binary files a/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-creates-a-transaction-from-accounts-id-page-2-chromium-linux.png and b/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-creates-a-transaction-from-accounts-id-page-2-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-creates-a-transaction-via-footer-button-1-chromium-linux.png b/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-creates-a-transaction-via-footer-button-1-chromium-linux.png index 39f7c042e38..85905bc0ee1 100644 Binary files a/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-creates-a-transaction-via-footer-button-1-chromium-linux.png and b/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-creates-a-transaction-via-footer-button-1-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-creates-a-transaction-via-footer-button-2-chromium-linux.png b/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-creates-a-transaction-via-footer-button-2-chromium-linux.png index 8320db53651..efc32336eed 100644 Binary files a/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-creates-a-transaction-via-footer-button-2-chromium-linux.png and b/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-creates-a-transaction-via-footer-button-2-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-creates-a-transaction-via-footer-button-3-chromium-linux.png b/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-creates-a-transaction-via-footer-button-3-chromium-linux.png index 591897b5a47..42cb054ab58 100644 Binary files a/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-creates-a-transaction-via-footer-button-3-chromium-linux.png and b/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-creates-a-transaction-via-footer-button-3-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-creates-a-transaction-via-footer-button-4-chromium-linux.png b/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-creates-a-transaction-via-footer-button-4-chromium-linux.png index 3d879590fd4..034509229c4 100644 Binary files a/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-creates-a-transaction-via-footer-button-4-chromium-linux.png and b/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-creates-a-transaction-via-footer-button-4-chromium-linux.png differ diff --git a/packages/desktop-client/src/components/Modals.tsx b/packages/desktop-client/src/components/Modals.tsx index 95e0713602f..824a59460bf 100644 --- a/packages/desktop-client/src/components/Modals.tsx +++ b/packages/desktop-client/src/components/Modals.tsx @@ -14,7 +14,7 @@ import { useSyncServerStatus } from '../hooks/useSyncServerStatus'; import { ModalTitle } from './common/Modal'; import { AccountAutocompleteModal } from './modals/AccountAutocompleteModal'; import { AccountMenuModal } from './modals/AccountMenuModal'; -import { BudgetMenuModal } from './modals/BudgetMenuModal'; +import { BudgetMonthMenuModal } from './modals/BudgetMonthMenuModal'; import { CategoryAutocompleteModal } from './modals/CategoryAutocompleteModal'; import { CategoryGroupMenuModal } from './modals/CategoryGroupMenuModal'; import { CategoryMenuModal } from './modals/CategoryMenuModal'; @@ -40,8 +40,10 @@ import { Notes } from './modals/Notes'; import { PayeeAutocompleteModal } from './modals/PayeeAutocompleteModal'; import { PlaidExternalMsg } from './modals/PlaidExternalMsg'; import { ReportBalanceMenuModal } from './modals/ReportBalanceMenuModal'; +import { ReportBudgetMenuModal } from './modals/ReportBudgetMenuModal'; import { ReportBudgetSummaryModal } from './modals/ReportBudgetSummaryModal'; import { RolloverBalanceMenuModal } from './modals/RolloverBalanceMenuModal'; +import { RolloverBudgetMenuModal } from './modals/RolloverBudgetMenuModal'; import { RolloverBudgetSummaryModal } from './modals/RolloverBudgetSummaryModal'; import { RolloverToBudgetMenuModal } from './modals/RolloverToBudgetMenuModal'; import { ScheduledTransactionMenuModal } from './modals/ScheduledTransactionMenuModal'; @@ -429,7 +431,6 @@ export function Modals() { key={name} modalProps={modalProps} categoryId={options.categoryId} - categoryGroup={options.categoryGroup} onSave={options.onSave} onEditNotes={options.onEditNotes} onDelete={options.onDelete} @@ -437,6 +438,40 @@ export function Modals() { /> ); + case 'rollover-budget-menu': + return ( + + + + ); + + case 'report-budget-menu': + return ( + + + + ); + case 'category-group-menu': return ( ); - case 'rollover-to-budget-menu': + case 'rollover-summary-to-budget-menu': return ( ); - case 'budget-menu': + case 'budget-month-menu': return ( - { - setEditing(id ? { id, cell: monthIndex } : null); + const onEditMonth = (id, month) => { + setEditing(id ? { id, cell: month } : null); }; const onEditName = id => { @@ -134,18 +132,6 @@ export function BudgetTable(props) { } }; - const resolveMonth = monthIndex => { - return monthUtils.addMonths(startMonth, monthIndex); - }; - - const _onShowActivity = (catId, monthIndex) => { - onShowActivity(catId, resolveMonth(monthIndex)); - }; - - const _onBudgetAction = (monthIndex, type, args) => { - onBudgetAction(resolveMonth(monthIndex), type, args); - }; - const onCollapse = collapsedIds => { setCollapsedPref(collapsedIds); }; @@ -244,8 +230,8 @@ export function BudgetTable(props) { onDeleteGroup={onDeleteGroup} onReorderCategory={_onReorderCategory} onReorderGroup={_onReorderGroup} - onBudgetAction={_onBudgetAction} - onShowActivity={_onShowActivity} + onBudgetAction={onBudgetAction} + onShowActivity={onShowActivity} /> diff --git a/packages/desktop-client/src/components/budget/ExpenseCategory.tsx b/packages/desktop-client/src/components/budget/ExpenseCategory.tsx index 5007b81c8d1..bb9da828c58 100644 --- a/packages/desktop-client/src/components/budget/ExpenseCategory.tsx +++ b/packages/desktop-client/src/components/budget/ExpenseCategory.tsx @@ -28,12 +28,12 @@ type ExpenseCategoryProps = { dragState: DragState; MonthComponent: ComponentProps['component']; onEditName?: ComponentProps['onEditName']; - onEditMonth?: (id: string, monthIndex: number) => void; + onEditMonth?: (id: string, month: string) => void; onSave?: ComponentProps['onSave']; onDelete?: ComponentProps['onDelete']; onDragChange: OnDragChangeCallback; - onBudgetAction: (idx: number, action: string, arg: unknown) => void; - onShowActivity: (id: string, idx: number) => void; + onBudgetAction: (month: number, action: string, arg: unknown) => void; + onShowActivity: (id: string, month: string) => void; onReorder: OnDropCallback; }; @@ -101,7 +101,7 @@ export function ExpenseCategory({ ['component']; onEditName: ComponentProps['onEditName']; - onEditMonth?: (id: string, monthIndex: number) => void; + onEditMonth?: (id: string, month: string) => void; onSave: ComponentProps['onSave']; onDelete: ComponentProps['onDelete']; onDragChange: OnDragChangeCallback; - onBudgetAction: (idx: number, action: string, arg: unknown) => void; + onBudgetAction: (month: string, action: string, arg: unknown) => void; onReorder: OnDropCallback; - onShowActivity: (id: string, idx: number) => void; + onShowActivity: (id: string, month: string) => void; }; export function IncomeCategory({ @@ -76,7 +76,7 @@ export function IncomeCategory({ /> ; - editingIndex?: string | number; + component?: ComponentType<{ month: string; editing: boolean }>; + editingMonth?: string; args?: object; style?: CSSProperties; }; export function RenderMonths({ component: Component, - editingIndex, + editingMonth, args, style, }: RenderMonthsProps) { const { months } = useContext(MonthsContext); return months.map((month, index) => { - const editing = editingIndex === index; + const editing = editingMonth === month; return ( - + ); diff --git a/packages/desktop-client/src/components/budget/report/BalanceTooltip.tsx b/packages/desktop-client/src/components/budget/report/BalanceTooltip.tsx index 66999627bf7..91aa1ade6d1 100644 --- a/packages/desktop-client/src/components/budget/report/BalanceTooltip.tsx +++ b/packages/desktop-client/src/components/budget/report/BalanceTooltip.tsx @@ -7,15 +7,15 @@ import { BalanceMenu } from './BalanceMenu'; type BalanceTooltipProps = { categoryId: string; tooltip: { close: () => void }; - monthIndex: number; - onBudgetAction: (idx: number, action: string, arg: unknown) => void; + month: string; + onBudgetAction: (month: string, action: string, arg: unknown) => void; onClose?: () => void; }; export function BalanceTooltip({ categoryId, tooltip, - monthIndex, + month, onBudgetAction, onClose, ...tooltipProps @@ -36,7 +36,7 @@ export function BalanceTooltip({ { - onBudgetAction?.(monthIndex, 'carryover', { + onBudgetAction?.(month, 'carryover', { category: categoryId, flag: carryover, }); diff --git a/packages/desktop-client/src/components/budget/report/BudgetMenu.tsx b/packages/desktop-client/src/components/budget/report/BudgetMenu.tsx new file mode 100644 index 00000000000..aeed3cddd88 --- /dev/null +++ b/packages/desktop-client/src/components/budget/report/BudgetMenu.tsx @@ -0,0 +1,75 @@ +import React, { type ComponentPropsWithoutRef } from 'react'; + +import { useFeatureFlag } from '../../../hooks/useFeatureFlag'; +import { Menu } from '../../common/Menu'; + +type BudgetMenuProps = Omit< + ComponentPropsWithoutRef, + 'onMenuSelect' | 'items' +> & { + onCopyLastMonthAverage: () => void; + onSetMonthsAverage: (numberOfMonths: number) => void; + onApplyBudgetTemplate: () => void; +}; +export function BudgetMenu({ + onCopyLastMonthAverage, + onSetMonthsAverage, + onApplyBudgetTemplate, + ...props +}: BudgetMenuProps) { + const isGoalTemplatesEnabled = useFeatureFlag('goalTemplatesEnabled'); + const onMenuSelect = (name: string) => { + switch (name) { + case 'copy-single-last': + onCopyLastMonthAverage?.(); + break; + case 'set-single-3-avg': + onSetMonthsAverage?.(3); + break; + case 'set-single-6-avg': + onSetMonthsAverage?.(6); + break; + case 'set-single-12-avg': + onSetMonthsAverage?.(12); + break; + case 'apply-single-category-template': + onApplyBudgetTemplate?.(); + break; + default: + throw new Error(`Unrecognized menu item: ${name}`); + } + }; + + return ( + + ); +} diff --git a/packages/desktop-client/src/components/budget/report/ReportComponents.tsx b/packages/desktop-client/src/components/budget/report/ReportComponents.tsx index 3181c8da7cb..5d09472c8ff 100644 --- a/packages/desktop-client/src/components/budget/report/ReportComponents.tsx +++ b/packages/desktop-client/src/components/budget/report/ReportComponents.tsx @@ -5,11 +5,9 @@ import { reportBudget } from 'loot-core/src/client/queries'; import { evalArithmetic } from 'loot-core/src/shared/arithmetic'; import { integerToCurrency, amountToInteger } from 'loot-core/src/shared/util'; -import { useFeatureFlag } from '../../../hooks/useFeatureFlag'; import { SvgCheveronDown } from '../../../icons/v1'; import { styles, theme, type CSSProperties } from '../../../style'; import { Button } from '../../common/Button'; -import { Menu } from '../../common/Menu'; import { Text } from '../../common/Text'; import { View } from '../../common/View'; import { CellValue } from '../../spreadsheet/CellValue'; @@ -20,6 +18,7 @@ import { BalanceWithCarryover } from '../BalanceWithCarryover'; import { makeAmountGrey } from '../util'; import { BalanceTooltip } from './BalanceTooltip'; +import { BudgetMenu } from './BudgetMenu'; const headerLabelStyle: CSSProperties = { flex: 1, @@ -142,15 +141,15 @@ export const GroupMonth = memo(function GroupMonth({ group }: GroupMonthProps) { }); type CategoryMonthProps = { - monthIndex: number; + month: string; category: { id: string; name: string; is_income: boolean }; editing: boolean; - onEdit: (id: string | null, idx?: number) => void; - onBudgetAction: (idx: number, action: string, arg: unknown) => void; - onShowActivity: (id: string, idx: number) => void; + onEdit: (id: string | null, month?: string) => void; + onBudgetAction: (month: string, action: string, arg: unknown) => void; + onShowActivity: (id: string, month: string) => void; }; export const CategoryMonth = memo(function CategoryMonth({ - monthIndex, + month, category, editing, onEdit, @@ -160,7 +159,6 @@ export const CategoryMonth = memo(function CategoryMonth({ const balanceTooltip = useTooltip(); const [menuOpen, setMenuOpen] = useState(false); const [hover, setHover] = useState(false); - const isGoalTemplatesEnabled = useFeatureFlag('goalTemplatesEnabled'); return ( setMenuOpen(false)} > - { - onBudgetAction(monthIndex, type, { category: category.id }); - setMenuOpen(false); + { + onBudgetAction?.(month, 'copy-single-last', { + category: category.id, + }); + }} + onSetMonthsAverage={numberOfMonths => { + if ( + numberOfMonths !== 3 && + numberOfMonths !== 6 && + numberOfMonths !== 12 + ) { + return; + } + + onBudgetAction?.( + month, + `set-single-${numberOfMonths}-avg`, + { + category: category.id, + }, + ); + }} + onApplyBudgetTemplate={() => { + onBudgetAction?.(month, 'apply-single-category-template', { + category: category.id, + }); }} - items={[ - { - name: 'copy-single-last', - text: 'Copy last month’s budget', - }, - { - name: 'set-single-3-avg', - text: 'Set to 3 month average', - }, - { - name: 'set-single-6-avg', - text: 'Set to 6 month average', - }, - { - name: 'set-single-12-avg', - text: 'Set to yearly average', - }, - isGoalTemplatesEnabled && { - name: 'apply-single-category-template', - text: 'Apply budget template', - }, - ]} /> )} @@ -258,7 +257,7 @@ export const CategoryMonth = memo(function CategoryMonth({ exposed={editing} focused={editing} width="flex" - onExpose={() => onEdit(category.id, monthIndex)} + onExpose={() => onEdit(category.id, month)} style={{ ...(editing && { zIndex: 100 }), ...styles.tnum }} textAlign="right" valueStyle={{ @@ -291,7 +290,7 @@ export const CategoryMonth = memo(function CategoryMonth({ }, }} onSave={amount => { - onBudgetAction(monthIndex, 'budget-amount', { + onBudgetAction(month, 'budget-amount', { category: category.id, amount, }); @@ -301,7 +300,7 @@ export const CategoryMonth = memo(function CategoryMonth({ onShowActivity(category.id, monthIndex)} + onClick={() => onShowActivity(category.id, month)} > )} diff --git a/packages/desktop-client/src/components/budget/report/ReportContext.tsx b/packages/desktop-client/src/components/budget/report/ReportContext.tsx index c1c90e93d72..d93d8cbe66b 100644 --- a/packages/desktop-client/src/components/budget/report/ReportContext.tsx +++ b/packages/desktop-client/src/components/budget/report/ReportContext.tsx @@ -7,7 +7,7 @@ const Context = createContext(null); type ReportProviderProps = { summaryCollapsed: boolean; - onBudgetAction: (idx: number, action: string, arg: unknown) => void; + onBudgetAction: (month: string, action: string, arg: unknown) => void; onToggleSummaryCollapse: () => void; children: ReactNode; }; diff --git a/packages/desktop-client/src/components/budget/rollover/BalanceTooltip.tsx b/packages/desktop-client/src/components/budget/rollover/BalanceTooltip.tsx index 66b8f86ba30..1432556c096 100644 --- a/packages/desktop-client/src/components/budget/rollover/BalanceTooltip.tsx +++ b/packages/desktop-client/src/components/budget/rollover/BalanceTooltip.tsx @@ -12,15 +12,15 @@ import { TransferTooltip } from './TransferTooltip'; type BalanceTooltipProps = { categoryId: string; tooltip: { close: () => void }; - monthIndex: number; - onBudgetAction: (idx: number, action: string, arg?: unknown) => void; + month: string; + onBudgetAction: (month: string, action: string, arg?: unknown) => void; onClose?: () => void; }; export function BalanceTooltip({ categoryId, tooltip, - monthIndex, + month, onBudgetAction, onClose, ...tooltipProps @@ -46,7 +46,7 @@ export function BalanceTooltip({ { - onBudgetAction(monthIndex, 'carryover', { + onBudgetAction(month, 'carryover', { category: categoryId, flag: carryover, }); @@ -64,7 +64,7 @@ export function BalanceTooltip({ showToBeBudgeted={true} onClose={_onClose} onSubmit={(amount, toCategoryId) => { - onBudgetAction(monthIndex, 'transfer-category', { + onBudgetAction(month, 'transfer-category', { amount, from: categoryId, to: toCategoryId, @@ -77,7 +77,7 @@ export function BalanceTooltip({ { - onBudgetAction(monthIndex, 'cover', { + onBudgetAction(month, 'cover', { to: categoryId, from: fromCategoryId, }); diff --git a/packages/desktop-client/src/components/budget/rollover/BudgetMenu.tsx b/packages/desktop-client/src/components/budget/rollover/BudgetMenu.tsx new file mode 100644 index 00000000000..aeed3cddd88 --- /dev/null +++ b/packages/desktop-client/src/components/budget/rollover/BudgetMenu.tsx @@ -0,0 +1,75 @@ +import React, { type ComponentPropsWithoutRef } from 'react'; + +import { useFeatureFlag } from '../../../hooks/useFeatureFlag'; +import { Menu } from '../../common/Menu'; + +type BudgetMenuProps = Omit< + ComponentPropsWithoutRef, + 'onMenuSelect' | 'items' +> & { + onCopyLastMonthAverage: () => void; + onSetMonthsAverage: (numberOfMonths: number) => void; + onApplyBudgetTemplate: () => void; +}; +export function BudgetMenu({ + onCopyLastMonthAverage, + onSetMonthsAverage, + onApplyBudgetTemplate, + ...props +}: BudgetMenuProps) { + const isGoalTemplatesEnabled = useFeatureFlag('goalTemplatesEnabled'); + const onMenuSelect = (name: string) => { + switch (name) { + case 'copy-single-last': + onCopyLastMonthAverage?.(); + break; + case 'set-single-3-avg': + onSetMonthsAverage?.(3); + break; + case 'set-single-6-avg': + onSetMonthsAverage?.(6); + break; + case 'set-single-12-avg': + onSetMonthsAverage?.(12); + break; + case 'apply-single-category-template': + onApplyBudgetTemplate?.(); + break; + default: + throw new Error(`Unrecognized menu item: ${name}`); + } + }; + + return ( + + ); +} diff --git a/packages/desktop-client/src/components/budget/rollover/RolloverComponents.tsx b/packages/desktop-client/src/components/budget/rollover/RolloverComponents.tsx index a8e326f9601..b1ba0ff719e 100644 --- a/packages/desktop-client/src/components/budget/rollover/RolloverComponents.tsx +++ b/packages/desktop-client/src/components/budget/rollover/RolloverComponents.tsx @@ -1,14 +1,13 @@ -import React, { memo, useState } from 'react'; +import type React from 'react'; +import { memo, useState } from 'react'; import { rolloverBudget } from 'loot-core/src/client/queries'; import { evalArithmetic } from 'loot-core/src/shared/arithmetic'; import { integerToCurrency, amountToInteger } from 'loot-core/src/shared/util'; -import { useFeatureFlag } from '../../../hooks/useFeatureFlag'; import { SvgCheveronDown } from '../../../icons/v1'; import { styles, theme, type CSSProperties } from '../../../style'; import { Button } from '../../common/Button'; -import { Menu } from '../../common/Menu'; import { Text } from '../../common/Text'; import { View } from '../../common/View'; import { CellValue } from '../../spreadsheet/CellValue'; @@ -19,6 +18,7 @@ import { BalanceWithCarryover } from '../BalanceWithCarryover'; import { makeAmountGrey } from '../util'; import { BalanceTooltip } from './BalanceTooltip'; +import { BudgetMenu } from './BudgetMenu'; const headerLabelStyle: CSSProperties = { flex: 1, @@ -137,15 +137,15 @@ export const ExpenseGroupMonth = memo(function ExpenseGroupMonth({ }); type ExpenseCategoryMonthProps = { - monthIndex: number; + month: string; category: { id: string; name: string; is_income: boolean }; editing: boolean; - onEdit: (id: string | null, idx?: number) => void; - onBudgetAction: (idx: number, action: string, arg?: unknown) => void; - onShowActivity: (id: string, idx: number) => void; + onEdit: (id: string | null, month?: string) => void; + onBudgetAction: (month: string, action: string, arg?: unknown) => void; + onShowActivity: (id: string, month: string) => void; }; export const ExpenseCategoryMonth = memo(function ExpenseCategoryMonth({ - monthIndex, + month, category, editing, onEdit, @@ -155,7 +155,6 @@ export const ExpenseCategoryMonth = memo(function ExpenseCategoryMonth({ const balanceTooltip = useTooltip(); const [menuOpen, setMenuOpen] = useState(false); const [hover, setHover] = useState(false); - const isGoalTemplatesEnabled = useFeatureFlag('goalTemplatesEnabled'); return ( setMenuOpen(false)} > - { - onBudgetAction(monthIndex, type, { category: category.id }); - setMenuOpen(false); + { + onBudgetAction?.(month, 'copy-single-last', { + category: category.id, + }); + }} + onSetMonthsAverage={numberOfMonths => { + if ( + numberOfMonths !== 3 && + numberOfMonths !== 6 && + numberOfMonths !== 12 + ) { + return; + } + + onBudgetAction?.( + month, + `set-single-${numberOfMonths}-avg`, + { + category: category.id, + }, + ); + }} + onApplyBudgetTemplate={() => { + onBudgetAction?.(month, 'apply-single-category-template', { + category: category.id, + }); }} - items={[ - { - name: 'copy-single-last', - text: 'Copy last month’s budget', - }, - { - name: 'set-single-3-avg', - text: 'Set to 3 month average', - }, - { - name: 'set-single-6-avg', - text: 'Set to 6 month average', - }, - { - name: 'set-single-12-avg', - text: 'Set to yearly average', - }, - ...(isGoalTemplatesEnabled - ? [ - { - name: 'apply-single-category-template', - text: 'Apply budget template', - }, - ] - : []), - ]} /> )} @@ -257,7 +253,7 @@ export const ExpenseCategoryMonth = memo(function ExpenseCategoryMonth({ exposed={editing} focused={editing} width="flex" - onExpose={() => onEdit(category.id, monthIndex)} + onExpose={() => onEdit(category.id, month)} style={{ ...(editing && { zIndex: 100 }), ...styles.tnum }} textAlign="right" valueStyle={{ @@ -290,7 +286,7 @@ export const ExpenseCategoryMonth = memo(function ExpenseCategoryMonth({ }, }} onSave={amount => { - onBudgetAction(monthIndex, 'budget-amount', { + onBudgetAction(month, 'budget-amount', { category: category.id, amount, }); @@ -300,7 +296,7 @@ export const ExpenseCategoryMonth = memo(function ExpenseCategoryMonth({ onShowActivity(category.id, monthIndex)} + onClick={() => onShowActivity(category.id, month)} > )} @@ -369,13 +365,13 @@ export function IncomeGroupMonth() { type IncomeCategoryMonthProps = { category: { id: string; name: string }; isLast: boolean; - monthIndex: number; - onShowActivity: (id: string, idx: number) => void; + month: string; + onShowActivity: (id: string, month: string) => void; }; export function IncomeCategoryMonth({ category, isLast, - monthIndex, + month, onShowActivity, }: IncomeCategoryMonthProps) { return ( @@ -389,7 +385,7 @@ export function IncomeCategoryMonth({ ...(isLast && { borderBottomWidth: 0 }), }} > - onShowActivity(category.id, monthIndex)}> + onShowActivity(category.id, month)}> void; + onBudgetAction: (month: string, action: string, arg?: unknown) => void; onToggleSummaryCollapse: () => void; currentMonth: string; }; diff --git a/packages/desktop-client/src/components/budget/rollover/budgetsummary/ToBudget.tsx b/packages/desktop-client/src/components/budget/rollover/budgetsummary/ToBudget.tsx index 75051811aa0..5630ad92bfe 100644 --- a/packages/desktop-client/src/components/budget/rollover/budgetsummary/ToBudget.tsx +++ b/packages/desktop-client/src/components/budget/rollover/budgetsummary/ToBudget.tsx @@ -13,7 +13,7 @@ import { ToBudgetMenu } from './ToBudgetMenu'; type ToBudgetProps = { month: string; - onBudgetAction: (idx: string, action: string, arg?: unknown) => void; + onBudgetAction: (month: string, action: string, arg?: unknown) => void; prevMonthName: string; showTotalsTooltipOnHover?: boolean; style?: CSSProperties; diff --git a/packages/desktop-client/src/components/mobile/budget/BudgetTable.jsx b/packages/desktop-client/src/components/mobile/budget/BudgetTable.jsx index 4eb5cad5626..c7fcdd68fdb 100644 --- a/packages/desktop-client/src/components/mobile/budget/BudgetTable.jsx +++ b/packages/desktop-client/src/components/mobile/budget/BudgetTable.jsx @@ -1,4 +1,4 @@ -import React, { memo, useRef, useState } from 'react'; +import React, { memo, useRef } from 'react'; import { useDispatch } from 'react-redux'; import memoizeOne from 'memoize-one'; @@ -9,10 +9,6 @@ import * as monthUtils from 'loot-core/src/shared/months'; import { useLocalPref } from '../../../hooks/useLocalPref'; import { useNavigate } from '../../../hooks/useNavigate'; -import { - SingleActiveEditFormProvider, - useSingleActiveEditForm, -} from '../../../hooks/useSingleActiveEditForm'; import { SvgLogo } from '../../../icons/logo'; import { SvgAdd, SvgArrowThinLeft, SvgArrowThinRight } from '../../../icons/v1'; import { useResponsive } from '../../../ResponsiveProvider'; @@ -28,7 +24,6 @@ import { Page } from '../../Page'; import { CellValue } from '../../spreadsheet/CellValue'; import { useFormat } from '../../spreadsheet/useFormat'; import { useSheetValue } from '../../spreadsheet/useSheetValue'; -import { AmountInput } from '../../util/AmountInput'; import { MOBILE_NAV_HEIGHT } from '../MobileNavTabs'; import { PullToRefresh } from '../PullToRefresh'; @@ -123,66 +118,67 @@ function BudgetCell({ name, binding, style, - textStyle, categoryId, month, onBudgetAction, - onEdit, - onBlur, - isEditing, }) { - const sheetValue = useSheetValue(binding); + const dispatch = useDispatch(); + const [budgetType = 'rollover'] = useLocalPref('budgetType'); - function updateBudgetAmount(amount) { - onBudgetAction?.(month, 'budget-amount', { - category: categoryId, - amount, - }); - } + const categoryBudgetMenuModal = `${budgetType}-budget-menu`; - function onAmountClick() { - onEdit?.(); - } + const onOpenCategoryBudgetMenu = () => { + dispatch( + pushModal(categoryBudgetMenuModal, { + categoryId, + month, + onUpdateBudget: amount => { + onBudgetAction(month, 'budget-amount', { + category: categoryId, + amount, + }); + }, + onCopyLastMonthAverage: () => { + onBudgetAction(month, 'copy-single-last', { + category: categoryId, + }); + }, + onSetMonthsAverage: numberOfMonths => { + if ( + numberOfMonths !== 3 && + numberOfMonths !== 6 && + numberOfMonths !== 12 + ) { + return; + } + + onBudgetAction(month, `set-single-${numberOfMonths}-avg`, { + category: categoryId, + }); + }, + onApplyBudgetTemplate: () => { + onBudgetAction(month, 'apply-single-category-template', { + category: categoryId, + }); + }, + }), + ); + }; return ( - - - - e.preventDefault()} - /> - - + ); } @@ -247,17 +243,8 @@ const ExpenseCategory = memo(function ExpenseCategory({ const opacity = blank ? 0 : 1; const [budgetType = 'rollover'] = useLocalPref('budgetType'); - const [isEditingBudget, setIsEditingBudget] = useState(false); - const { onRequestActiveEdit, onClearActiveEdit } = useSingleActiveEditForm(); const dispatch = useDispatch(); - const onEditBudget = () => { - onRequestActiveEdit(`${category.id}-budget`, () => { - setIsEditingBudget(true); - return () => setIsEditingBudget(false); - }); - }; - const onCarryover = carryover => { onBudgetAction(month, 'carryover', { category: category.id, @@ -305,7 +292,7 @@ const ExpenseCategory = memo(function ExpenseCategory({ ); }; - const onOpenBalanceActionMenu = () => { + const onOpenBalanceMenu = () => { dispatch( pushModal(`${budgetType}-balance-menu`, { categoryId: category.id, @@ -367,16 +354,12 @@ const ExpenseCategory = memo(function ExpenseCategory({ name="budgeted" binding={budgeted} style={{ - ...(!show3Cols && !showBudgetedCol && { display: 'none' }), width: 90, + ...(!show3Cols && !showBudgetedCol && { display: 'none' }), }} - textStyle={{ ...styles.smallText, textAlign: 'right' }} categoryId={category.id} month={month} onBudgetAction={onBudgetAction} - isEditing={isEditingBudget} - onEdit={onEditBudget} - onBlur={onClearActiveEdit} /> - onOpenBalanceActionMenu?.()}> + onOpenBalanceMenu?.()}> { - onRequestActiveEdit(`${category.id}-budget`, () => { - setIsEditingBudget(true); - return () => setIsEditingBudget(false); - }); - }; return ( {budgeted && ( - - - + )} - - {expenseGroups - .filter(group => !group.hidden || showHiddenCategories) - .map(group => { - return ( - - ); - })} + + {expenseGroups + .filter(group => !group.hidden || showHiddenCategories) + .map(group => { + return ( + + ); + })} - {incomeGroup && ( - - )} - - + {incomeGroup && ( + + )} + ); } @@ -1118,7 +1075,6 @@ export function BudgetTable({ month, monthBounds, // editMode, - // refreshControl, onPrevMonth, onNextMonth, onSaveGroup, @@ -1134,7 +1090,7 @@ export function BudgetTable({ onRefresh, onEditGroup, onEditCategory, - onOpenBudgetActionMenu, + onOpenBudgetMonthMenu, }) { const { width } = useResponsive(); const show3Cols = width >= 360; @@ -1185,7 +1141,7 @@ export function BudgetTable({ }} hoveredStyle={noBackgroundColorStyle} activeStyle={noBackgroundColorStyle} - onClick={() => onOpenBudgetActionMenu?.(month)} + onClick={() => onOpenBudgetMonthMenu?.(month)} > diff --git a/packages/desktop-client/src/components/mobile/budget/index.tsx b/packages/desktop-client/src/components/mobile/budget/index.tsx index d3c2bd2a788..0e244fb1f7b 100644 --- a/packages/desktop-client/src/components/mobile/budget/index.tsx +++ b/packages/desktop-client/src/components/mobile/budget/index.tsx @@ -343,14 +343,13 @@ function BudgetInner(props: BudgetInnerProps) { const onEditCategory = id => { const category = categories.find(c => c.id === id); - const categoryGroup = categoryGroups.find(g => g.id === category.cat_group); dispatch( pushModal('category-menu', { categoryId: category.id, - categoryGroup, onSave: onSaveCategory, onEditNotes: onEditCategoryNotes, onDelete: onDeleteCategory, + onBudgetAction, }), ); }; @@ -360,7 +359,7 @@ function BudgetInner(props: BudgetInnerProps) { pushModal('switch-budget-type', { onSwitch: () => { onSwitchBudgetType?.(); - dispatch(collapseModals('budget-menu')); + dispatch(collapseModals('budget-month-menu')); }, }), ); @@ -372,12 +371,12 @@ function BudgetInner(props: BudgetInnerProps) { const onToggleHiddenCategories = () => { setShowHiddenCategoriesPref(!showHiddenCategories); - dispatch(collapseModals('budget-menu')); + dispatch(collapseModals('budget-month-menu')); }; - const onOpenBudgetActionMenu = month => { + const onOpenBudgetMonthMenu = month => { dispatch( - pushModal('budget-menu', { + pushModal('budget-month-menu', { month, onToggleHiddenCategories, onSwitchBudgetType: _onSwitchBudgetType, @@ -433,7 +432,7 @@ function BudgetInner(props: BudgetInnerProps) { onRefresh={onRefresh} onEditGroup={onEditGroup} onEditCategory={onEditCategory} - onOpenBudgetActionMenu={onOpenBudgetActionMenu} + onOpenBudgetMonthMenu={onOpenBudgetMonthMenu} /> )} diff --git a/packages/desktop-client/src/components/mobile/transactions/FocusableAmountInput.jsx b/packages/desktop-client/src/components/mobile/transactions/FocusableAmountInput.tsx similarity index 67% rename from packages/desktop-client/src/components/mobile/transactions/FocusableAmountInput.jsx rename to packages/desktop-client/src/components/mobile/transactions/FocusableAmountInput.tsx index 482759d5f76..17b0d0d3281 100644 --- a/packages/desktop-client/src/components/mobile/transactions/FocusableAmountInput.jsx +++ b/packages/desktop-client/src/components/mobile/transactions/FocusableAmountInput.tsx @@ -1,4 +1,12 @@ -import { memo, useEffect, useRef, useState } from 'react'; +import React, { + type Ref, + type ComponentPropsWithRef, + type HTMLProps, + memo, + useEffect, + useRef, + useState, +} from 'react'; import { toRelaxedNumber, @@ -8,23 +16,41 @@ import { import { useLocalPref } from '../../../hooks/useLocalPref'; import { useMergedRefs } from '../../../hooks/useMergedRefs'; -import { theme } from '../../../style'; +import { type CSSProperties, theme } from '../../../style'; import { Button } from '../../common/Button'; import { Text } from '../../common/Text'; import { View } from '../../common/View'; +type AmountInputProps = { + value: number; + focused?: boolean; + style?: CSSProperties; + textStyle?: CSSProperties; + inputRef?: Ref; + onFocus?: HTMLProps['onFocus']; + onBlur?: HTMLProps['onBlur']; + onEnter?: HTMLProps['onKeyUp']; + onChangeValue?: (value: string) => void; + onUpdate?: (value: string) => void; + onUpdateAmount?: (value: number) => void; +}; + const AmountInput = memo(function AmountInput({ focused, style, textStyle, ...props -}) { +}: AmountInputProps) { const [editing, setEditing] = useState(false); const [text, setText] = useState(''); const [value, setValue] = useState(0); - const inputRef = useRef(); + const inputRef = useRef(); const [hideFraction = false] = useLocalPref('hideFraction'); - const mergedInputRef = useMergedRefs(props.inputRef, inputRef); + + const mergedInputRef = useMergedRefs( + props.inputRef, + inputRef, + ); const initialValue = Math.abs(props.value); @@ -44,11 +70,17 @@ const AmountInput = memo(function AmountInput({ return toRelaxedNumber(text.replace(/[,.]/, getNumberFormat().separator)); }; - const onKeyPress = e => { + const onKeyUp: HTMLProps['onKeyUp'] = e => { if (e.key === 'Backspace' && text === '') { setEditing(true); + } else if (e.key === 'Enter') { + props.onEnter?.(e); + if (!e.defaultPrevented) { + onUpdate(e.currentTarget.value); + } } }; + const applyText = () => { const parsed = parseText(); const newValue = editing ? parsed : value; @@ -60,17 +92,24 @@ const AmountInput = memo(function AmountInput({ return newValue; }; - const onFocus = e => { + const onFocus: HTMLProps['onFocus'] = e => { props.onFocus?.(e); }; - const onBlur = e => { - const value = applyText(); + const onUpdate = (value: string) => { props.onUpdate?.(value); + const amount = applyText(); + props.onUpdateAmount?.(amount); + }; + + const onBlur: HTMLProps['onBlur'] = e => { props.onBlur?.(e); + if (!e.defaultPrevented) { + onUpdate(e.target.value); + } }; - const onChangeText = text => { + const onChangeText = (text: string) => { if (text.slice(-1) === '.') { text = text.slice(0, -1); } @@ -83,7 +122,7 @@ const AmountInput = memo(function AmountInput({ setEditing(true); setText(text); - props.onChange?.(text); + props.onChangeValue?.(text); }; const input = ( @@ -96,7 +135,7 @@ const AmountInput = memo(function AmountInput({ onChange={e => onChangeText(e.target.value)} onFocus={onFocus} onBlur={onBlur} - onKeyUp={onKeyPress} + onKeyUp={onKeyUp} data-testid="amount-input" style={{ flex: 1, textAlign: 'center', position: 'absolute' }} /> @@ -111,14 +150,17 @@ const AmountInput = memo(function AmountInput({ borderRadius: 4, padding: 5, backgroundColor: theme.tableBackground, + maxWidth: 'calc(100% - 40px)', ...style, }} > {input} {editing ? amountToCurrency(text) : amountToCurrency(value)} @@ -126,11 +168,22 @@ const AmountInput = memo(function AmountInput({ ); }); +type FocusableAmountInputProps = Omit & { + sign?: '+' | '-'; + zeroSign?: '+' | '-'; + focused?: boolean; + disabled?: boolean; + focusedStyle?: CSSProperties; + buttonProps?: ComponentPropsWithRef; + onFocus?: () => void; +}; + export const FocusableAmountInput = memo(function FocusableAmountInput({ value, - sign, // + or - - zeroSign, // + or - + sign, + zeroSign, focused, + disabled, textStyle, style, focusedStyle, @@ -138,9 +191,18 @@ export const FocusableAmountInput = memo(function FocusableAmountInput({ onFocus, onBlur, ...props -}) { +}: FocusableAmountInputProps) { const [isNegative, setIsNegative] = useState(true); + const maybeApplyNegative = (amount: number, negative: boolean) => { + const absValue = Math.abs(amount); + return negative ? -absValue : absValue; + }; + + const onUpdateAmount = (amount: number, negative: boolean) => { + props.onUpdateAmount?.(maybeApplyNegative(amount, negative)); + }; + useEffect(() => { if (sign) { setIsNegative(sign === '-'); @@ -150,17 +212,12 @@ export const FocusableAmountInput = memo(function FocusableAmountInput({ }, [sign, value, zeroSign]); const toggleIsNegative = () => { - setIsNegative(!isNegative); - props.onUpdate?.(maybeApplyNegative(value, !isNegative)); - }; - - const maybeApplyNegative = (val, negative) => { - const absValue = Math.abs(val); - return negative ? -absValue : absValue; - }; + if (disabled) { + return; + } - const onUpdate = val => { - props.onUpdate?.(maybeApplyNegative(val, isNegative)); + onUpdateAmount(value, !isNegative); + setIsNegative(!isNegative); }; return ( @@ -170,8 +227,8 @@ export const FocusableAmountInput = memo(function FocusableAmountInput({ value={value} onFocus={onFocus} onBlur={onBlur} - onUpdate={onUpdate} - focused={focused} + onUpdateAmount={amount => onUpdateAmount(amount, isNegative)} + focused={focused && !disabled} style={{ width: 80, justifyContent: 'center', @@ -216,7 +273,6 @@ export const FocusableAmountInput = memo(function FocusableAmountInput({ borderBottomWidth: 1, borderColor: '#e0e0e0', justifyContent: 'center', - transform: [{ translateY: 0.5 }], ...style, }} > diff --git a/packages/desktop-client/src/components/mobile/transactions/TransactionEdit.jsx b/packages/desktop-client/src/components/mobile/transactions/TransactionEdit.jsx index b800f9595cc..02864a9b5b8 100644 --- a/packages/desktop-client/src/components/mobile/transactions/TransactionEdit.jsx +++ b/packages/desktop-client/src/components/mobile/transactions/TransactionEdit.jsx @@ -720,16 +720,15 @@ const TransactionEditInner = memo(function TransactionEditInner({ zeroSign="-" focused={totalAmountFocused} onFocus={onTotalAmountEdit} - onUpdate={onTotalAmountUpdate} + onUpdateAmount={onTotalAmountUpdate} focusedStyle={{ width: 'auto', padding: '5px', paddingLeft: '20px', paddingRight: '20px', - minWidth: 120, - transform: [{ translateY: -0.5 }], + minWidth: '100%', }} - textStyle={{ fontSize: 30, textAlign: 'center' }} + textStyle={{ ...styles.veryLargeText, textAlign: 'center' }} /> diff --git a/packages/desktop-client/src/components/modals/AccountMenuModal.tsx b/packages/desktop-client/src/components/modals/AccountMenuModal.tsx index 33c456f8739..f798c50327a 100644 --- a/packages/desktop-client/src/components/modals/AccountMenuModal.tsx +++ b/packages/desktop-client/src/components/modals/AccountMenuModal.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { type ComponentProps, useState } from 'react'; import { useLiveQuery } from 'loot-core/src/client/query-hooks'; import { q } from 'loot-core/src/shared/query'; @@ -146,7 +146,8 @@ export function AccountMenuModal({ flexWrap: 'wrap', justifyContent: 'space-between', alignContent: 'space-between', - margin: '10px 0', + paddingTop: 10, + paddingBottom: 10, }} >