From 5229fe7d1649be910dd9e470199871514c1262c5 Mon Sep 17 00:00:00 2001 From: Joel Jeremy Marquez <joeljeremy.marquez@gmail.com> Date: Thu, 5 Sep 2024 09:42:41 -0700 Subject: [PATCH] [Mobile] Budget page actions undo notifications (#3085) * Mobile transaction long press * Fix typecheck error * Use react-aria useLongPress * Category transactions batch updates * Remove undo notification title * Fix types * Fix notes undo notification * Move SelectedProvider to TransactionListWithBalances * Notification inset + reuse useTransactionBatchActions * Allow clicking action bar when notifications are present * VRT * VRT * VRT * VRT * Show undo notification on budget page when applying budget actions/goal templates * Release notes * Transfer and cover undo notification * More budget actions undo --- .../desktop-client/src/components/Modals.tsx | 2 +- .../budget/report/ReportComponents.tsx | 12 ++++++ .../budget/rollover/RolloverComponents.tsx | 12 ++++++ .../components/mobile/budget/BudgetTable.jsx | 43 +++++++++++++++---- .../mobile/transactions/Transaction.jsx | 1 - .../modals/ReportBudgetMonthMenuModal.tsx | 21 ++++++++- .../modals/RolloverBudgetMonthMenuModal.tsx | 24 ++++++++++- .../modals/RolloverBudgetSummaryModal.tsx | 13 ++++++ upcoming-release-notes/3085.md | 6 +++ 9 files changed, 121 insertions(+), 13 deletions(-) create mode 100644 upcoming-release-notes/3085.md diff --git a/packages/desktop-client/src/components/Modals.tsx b/packages/desktop-client/src/components/Modals.tsx index 5a45d2159..351abf0cc 100644 --- a/packages/desktop-client/src/components/Modals.tsx +++ b/packages/desktop-client/src/components/Modals.tsx @@ -87,7 +87,7 @@ export function Modals() { .map(({ name, options }) => { switch (name) { case 'keyboard-shortcuts': - return <KeyboardShortcutModal />; + return <KeyboardShortcutModal key={name} />; case 'import-transactions': return <ImportTransactions key={name} options={options} />; diff --git a/packages/desktop-client/src/components/budget/report/ReportComponents.tsx b/packages/desktop-client/src/components/budget/report/ReportComponents.tsx index fa77bc002..d6ae0650b 100644 --- a/packages/desktop-client/src/components/budget/report/ReportComponents.tsx +++ b/packages/desktop-client/src/components/budget/report/ReportComponents.tsx @@ -7,6 +7,7 @@ import { evalArithmetic } from 'loot-core/src/shared/arithmetic'; import * as monthUtils from 'loot-core/src/shared/months'; import { integerToCurrency, amountToInteger } from 'loot-core/src/shared/util'; +import { useUndo } from '../../../hooks/useUndo'; import { SvgCheveronDown } from '../../../icons/v1'; import { styles, theme, type CSSProperties } from '../../../style'; import { Button } from '../../common/Button2'; @@ -215,6 +216,8 @@ export const CategoryMonth = memo(function CategoryMonth({ setMenuOpen(false); }; + const { showUndoNotification } = useUndo(); + return ( <View style={{ @@ -280,6 +283,9 @@ export const CategoryMonth = memo(function CategoryMonth({ onMenuAction(month, 'copy-single-last', { category: category.id, }); + showUndoNotification({ + message: `Budget set to last month’s budget.`, + }); }} onSetMonthsAverage={numberOfMonths => { if ( @@ -293,11 +299,17 @@ export const CategoryMonth = memo(function CategoryMonth({ onMenuAction(month, `set-single-${numberOfMonths}-avg`, { category: category.id, }); + showUndoNotification({ + message: `Budget set to ${numberOfMonths}-month average.`, + }); }} onApplyBudgetTemplate={() => { onMenuAction(month, 'apply-single-category-template', { category: category.id, }); + showUndoNotification({ + message: `Budget template applied.`, + }); }} /> </Popover> diff --git a/packages/desktop-client/src/components/budget/rollover/RolloverComponents.tsx b/packages/desktop-client/src/components/budget/rollover/RolloverComponents.tsx index 86c03d2c7..ab1071967 100644 --- a/packages/desktop-client/src/components/budget/rollover/RolloverComponents.tsx +++ b/packages/desktop-client/src/components/budget/rollover/RolloverComponents.tsx @@ -5,6 +5,7 @@ import { evalArithmetic } from 'loot-core/src/shared/arithmetic'; import * as monthUtils from 'loot-core/src/shared/months'; import { integerToCurrency, amountToInteger } from 'loot-core/src/shared/util'; +import { useUndo } from '../../../hooks/useUndo'; import { SvgCheveronDown } from '../../../icons/v1'; import { styles, theme, type CSSProperties } from '../../../style'; import { Button } from '../../common/Button2'; @@ -202,6 +203,8 @@ export const ExpenseCategoryMonth = memo(function ExpenseCategoryMonth({ setBudgetMenuOpen(false); }; + const { showUndoNotification } = useUndo(); + return ( <View style={{ @@ -268,6 +271,9 @@ export const ExpenseCategoryMonth = memo(function ExpenseCategoryMonth({ onMenuAction(month, 'copy-single-last', { category: category.id, }); + showUndoNotification({ + message: `Budget set to last month’s budget.`, + }); }} onSetMonthsAverage={numberOfMonths => { if ( @@ -281,11 +287,17 @@ export const ExpenseCategoryMonth = memo(function ExpenseCategoryMonth({ onMenuAction(month, `set-single-${numberOfMonths}-avg`, { category: category.id, }); + showUndoNotification({ + message: `Budget set to ${numberOfMonths}-month average.`, + }); }} onApplyBudgetTemplate={() => { onMenuAction(month, 'apply-single-category-template', { category: category.id, }); + showUndoNotification({ + message: `Budget template applied.`, + }); }} /> </Popover> diff --git a/packages/desktop-client/src/components/mobile/budget/BudgetTable.jsx b/packages/desktop-client/src/components/mobile/budget/BudgetTable.jsx index ce8d398e1..493e2635e 100644 --- a/packages/desktop-client/src/components/mobile/budget/BudgetTable.jsx +++ b/packages/desktop-client/src/components/mobile/budget/BudgetTable.jsx @@ -5,13 +5,17 @@ import { AutoTextSize } from 'auto-text-size'; import memoizeOne from 'memoize-one'; import { collapseModals, pushModal } from 'loot-core/client/actions'; +import { groupById, integerToCurrency } from 'loot-core/shared/util'; import { rolloverBudget, reportBudget } from 'loot-core/src/client/queries'; import * as monthUtils from 'loot-core/src/shared/months'; +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'; import { SvgExpandArrow } from '../../../icons/v0'; import { @@ -214,30 +218,35 @@ function BudgetCell({ name, binding, style, - categoryId, + category, month, onBudgetAction, ...props }) { const dispatch = useDispatch(); - const [budgetType = 'rollover'] = useSyncedPref('budgetType'); + const { showUndoNotification } = useUndo(); + const [budgetType = 'rollover'] = useLocalPref('budgetType'); const categoryBudgetMenuModal = `${budgetType}-budget-menu`; + const categoryNotes = useNotes(category.id); const onOpenCategoryBudgetMenu = () => { dispatch( pushModal(categoryBudgetMenuModal, { - categoryId, + categoryId: category.id, month, onUpdateBudget: amount => { onBudgetAction(month, 'budget-amount', { - category: categoryId, + category: category.id, amount, }); }, onCopyLastMonthAverage: () => { onBudgetAction(month, 'copy-single-last', { - category: categoryId, + category: category.id, + }); + showUndoNotification({ + message: `${category.name} budget has been set last to month’s budgeted amount.`, }); }, onSetMonthsAverage: numberOfMonths => { @@ -250,12 +259,19 @@ function BudgetCell({ } onBudgetAction(month, `set-single-${numberOfMonths}-avg`, { - category: categoryId, + 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: categoryId, + category: category.id, + }); + showUndoNotification({ + message: `${category.name} budget templates have been applied.`, + pre: categoryNotes, }); }, }), @@ -344,6 +360,9 @@ const ExpenseCategory = memo(function ExpenseCategory({ const [budgetType = 'rollover'] = useSyncedPref('budgetType'); const dispatch = useDispatch(); + const { showUndoNotification } = useUndo(); + const { list: categories } = useCategories(); + const categoriesById = groupById(categories); const onCarryover = carryover => { onBudgetAction(month, 'carryover', { @@ -380,6 +399,9 @@ const ExpenseCategory = memo(function ExpenseCategory({ to: toCategoryId, }); dispatch(collapseModals(`${budgetType}-balance-menu`)); + showUndoNotification({ + message: `Transferred ${integerToCurrency(amount)} from ${category.name} to ${categoriesById[toCategoryId].name}.`, + }); }, showToBeBudgeted: true, }), @@ -398,6 +420,9 @@ const ExpenseCategory = memo(function ExpenseCategory({ from: fromCategoryId, }); dispatch(collapseModals(`${budgetType}-balance-menu`)); + showUndoNotification({ + message: `Covered ${category.name} overspending from ${categoriesById[fromCategoryId].name}.`, + }); }, }), ); @@ -495,7 +520,7 @@ const ExpenseCategory = memo(function ExpenseCategory({ <BudgetCell name="budgeted" binding={budgeted} - categoryId={category.id} + category={category} month={month} onBudgetAction={onBudgetAction} formatter={value => ( @@ -1162,7 +1187,7 @@ const IncomeCategory = memo(function IncomeCategory({ <BudgetCell name="budgeted" binding={budgeted} - categoryId={category.id} + category={category} month={month} onBudgetAction={onBudgetAction} formatter={value => ( diff --git a/packages/desktop-client/src/components/mobile/transactions/Transaction.jsx b/packages/desktop-client/src/components/mobile/transactions/Transaction.jsx index 85c8100e9..0d4693ad5 100644 --- a/packages/desktop-client/src/components/mobile/transactions/Transaction.jsx +++ b/packages/desktop-client/src/components/mobile/transactions/Transaction.jsx @@ -76,7 +76,6 @@ export const Transaction = memo(function Transaction({ const payee = usePayee(payeeId); const account = useAccount(accountId); const transferAcct = useAccount(payee?.transfer_acct); - const isPreview = isPreviewId(id); const { longPressProps } = useLongPress({ diff --git a/packages/desktop-client/src/components/modals/ReportBudgetMonthMenuModal.tsx b/packages/desktop-client/src/components/modals/ReportBudgetMonthMenuModal.tsx index 4c996dfc0..8ec998d08 100644 --- a/packages/desktop-client/src/components/modals/ReportBudgetMonthMenuModal.tsx +++ b/packages/desktop-client/src/components/modals/ReportBudgetMonthMenuModal.tsx @@ -4,6 +4,7 @@ import React, { useState } from 'react'; import * as monthUtils from 'loot-core/src/shared/months'; import { useNotes } from '../../hooks/useNotes'; +import { useUndo } from '../../hooks/useUndo'; import { SvgCheveronDown, SvgCheveronUp } from '../../icons/v1'; import { SvgNotesPaper } from '../../icons/v2'; import { type CSSProperties, styles, theme } from '../../style'; @@ -25,6 +26,7 @@ export function ReportBudgetMonthMenuModal({ onEditNotes, }: ReportBudgetMonthMenuModalProps) { const originalNotes = useNotes(`budget-${month}`); + const { showUndoNotification } = useUndo(); const _onEditNotes = () => { onEditNotes?.(month); @@ -51,6 +53,8 @@ export function ReportBudgetMonthMenuModal({ setShowMore(!showMore); }; + const displayMonth = monthUtils.format(month, 'MMMM ‘yy'); + return ( <Modal name="report-budget-month-menu" @@ -61,7 +65,7 @@ export function ReportBudgetMonthMenuModal({ {({ state: { close } }) => ( <> <ModalHeader - title={monthUtils.format(month, 'MMMM ‘yy')} + title={displayMonth} rightContent={<ModalCloseButton onClick={close} />} /> <View @@ -148,14 +152,23 @@ export function ReportBudgetMonthMenuModal({ onCopyLastMonthBudget={() => { onBudgetAction(month, 'copy-last'); close(); + showUndoNotification({ + message: `${displayMonth} budgets have all been set to last month’s budgeted amounts.`, + }); }} onSetBudgetsToZero={() => { onBudgetAction(month, 'set-zero'); close(); + showUndoNotification({ + message: `${displayMonth} budgets have all been set to zero.`, + }); }} onSetMonthsAverage={numberOfMonths => { onBudgetAction(month, `set-${numberOfMonths}-avg`); close(); + showUndoNotification({ + message: `${displayMonth} budgets have all been set to ${numberOfMonths === 12 ? 'yearly' : `${numberOfMonths} month`} average.`, + }); }} onCheckTemplates={() => { onBudgetAction(month, 'check-templates'); @@ -164,10 +177,16 @@ export function ReportBudgetMonthMenuModal({ onApplyBudgetTemplates={() => { onBudgetAction(month, 'apply-goal-template'); close(); + showUndoNotification({ + message: `${displayMonth} budget templates have been applied.`, + }); }} onOverwriteWithBudgetTemplates={() => { onBudgetAction(month, 'overwrite-goal-template'); close(); + showUndoNotification({ + message: `${displayMonth} budget templates have been overwritten.`, + }); }} /> )} diff --git a/packages/desktop-client/src/components/modals/RolloverBudgetMonthMenuModal.tsx b/packages/desktop-client/src/components/modals/RolloverBudgetMonthMenuModal.tsx index a34353e5d..cb8aa0f5f 100644 --- a/packages/desktop-client/src/components/modals/RolloverBudgetMonthMenuModal.tsx +++ b/packages/desktop-client/src/components/modals/RolloverBudgetMonthMenuModal.tsx @@ -4,6 +4,7 @@ import React, { useState } from 'react'; import * as monthUtils from 'loot-core/src/shared/months'; import { useNotes } from '../../hooks/useNotes'; +import { useUndo } from '../../hooks/useUndo'; import { SvgCheveronDown, SvgCheveronUp } from '../../icons/v1'; import { SvgNotesPaper } from '../../icons/v2'; import { type CSSProperties, styles, theme } from '../../style'; @@ -25,6 +26,7 @@ export function RolloverBudgetMonthMenuModal({ onEditNotes, }: RolloverBudgetMonthMenuModalProps) { const originalNotes = useNotes(`budget-${month}`); + const { showUndoNotification } = useUndo(); const _onEditNotes = () => { onEditNotes?.(month); @@ -51,6 +53,8 @@ export function RolloverBudgetMonthMenuModal({ setShowMore(!showMore); }; + const displayMonth = monthUtils.format(month, 'MMMM ‘yy'); + return ( <Modal name="rollover-budget-month-menu" @@ -61,7 +65,7 @@ export function RolloverBudgetMonthMenuModal({ {({ state: { close } }) => ( <> <ModalHeader - title={monthUtils.format(month, 'MMMM ‘yy')} + title={displayMonth} rightContent={<ModalCloseButton onClick={close} />} /> <View @@ -148,14 +152,23 @@ export function RolloverBudgetMonthMenuModal({ onCopyLastMonthBudget={() => { onBudgetAction(month, 'copy-last'); close(); + showUndoNotification({ + message: `${displayMonth} budgets have all been set to last month’s budgeted amounts.`, + }); }} onSetBudgetsToZero={() => { onBudgetAction(month, 'set-zero'); close(); + showUndoNotification({ + message: `${displayMonth} budgets have all been set to zero.`, + }); }} onSetMonthsAverage={numberOfMonths => { onBudgetAction(month, `set-${numberOfMonths}-avg`); close(); + showUndoNotification({ + message: `${displayMonth} budgets have all been set to ${numberOfMonths === 12 ? 'yearly' : `${numberOfMonths} month`} average.`, + }); }} onCheckTemplates={() => { onBudgetAction(month, 'check-templates'); @@ -164,14 +177,23 @@ export function RolloverBudgetMonthMenuModal({ onApplyBudgetTemplates={() => { onBudgetAction(month, 'apply-goal-template'); close(); + showUndoNotification({ + message: `${displayMonth} budget templates have been applied.`, + }); }} onOverwriteWithBudgetTemplates={() => { onBudgetAction(month, 'overwrite-goal-template'); close(); + showUndoNotification({ + message: `${displayMonth} budget templates have been overwritten.`, + }); }} onEndOfMonthCleanup={() => { onBudgetAction(month, 'cleanup-goal-template'); close(); + showUndoNotification({ + message: `${displayMonth} end-of-month cleanup templates have been applied.`, + }); }} /> )} diff --git a/packages/desktop-client/src/components/modals/RolloverBudgetSummaryModal.tsx b/packages/desktop-client/src/components/modals/RolloverBudgetSummaryModal.tsx index 0c64e6268..d86523af2 100644 --- a/packages/desktop-client/src/components/modals/RolloverBudgetSummaryModal.tsx +++ b/packages/desktop-client/src/components/modals/RolloverBudgetSummaryModal.tsx @@ -3,8 +3,11 @@ import { useDispatch } from 'react-redux'; import { collapseModals, pushModal } from 'loot-core/client/actions'; import { rolloverBudget } from 'loot-core/client/queries'; +import { groupById, integerToCurrency } from 'loot-core/shared/util'; import { format, sheetForMonth, prevMonth } from 'loot-core/src/shared/months'; +import { useCategories } from '../../hooks/useCategories'; +import { useUndo } from '../../hooks/useUndo'; import { styles } from '../../style'; import { ToBudgetAmount } from '../budget/rollover/budgetsummary/ToBudgetAmount'; import { TotalsList } from '../budget/rollover/budgetsummary/TotalsList'; @@ -29,6 +32,10 @@ export function RolloverBudgetSummaryModal({ value: 0, }) ?? 0; + const { showUndoNotification } = useUndo(); + const { list: categories } = useCategories(); + const categoriesById = groupById(categories); + const openTransferAvailableModal = () => { dispatch( pushModal('transfer', { @@ -42,6 +49,9 @@ export function RolloverBudgetSummaryModal({ category: toCategoryId, }); dispatch(collapseModals('transfer')); + showUndoNotification({ + message: `Transferred ${integerToCurrency(amount)} to ${categoriesById[toCategoryId].name}`, + }); }, }), ); @@ -58,6 +68,9 @@ export function RolloverBudgetSummaryModal({ category: categoryId, }); dispatch(collapseModals('cover')); + showUndoNotification({ + message: `Covered overbudgeted from ${categoriesById[categoryId].name}`, + }); }, }), ); diff --git a/upcoming-release-notes/3085.md b/upcoming-release-notes/3085.md new file mode 100644 index 000000000..48aa86842 --- /dev/null +++ b/upcoming-release-notes/3085.md @@ -0,0 +1,6 @@ +--- +category: Enhancements +authors: [joel-jeremy] +--- + +Show undo notifications when applying goal templates / budget actions in mobile. -- GitLab