diff --git a/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-loads-the-budget-page-with-budgeted-amounts-1-chromium-linux.png b/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-loads-the-budget-page-with-budgeted-amounts-1-chromium-linux.png index 18956272b5608ab7d263cf0da4fa4160f6f7d6b0..7925d23a6f9ce7e608476447ce0e839eeba8c261 100644 Binary files a/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-loads-the-budget-page-with-budgeted-amounts-1-chromium-linux.png and b/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-loads-the-budget-page-with-budgeted-amounts-1-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-loads-the-budget-page-with-budgeted-amounts-2-chromium-linux.png b/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-loads-the-budget-page-with-budgeted-amounts-2-chromium-linux.png index 897bc6b1af0963e85a141a95d6c3a5ee96415593..fd505d8b93c95d8e0e3fc9e90f75dc77b219a6cf 100644 Binary files a/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-loads-the-budget-page-with-budgeted-amounts-2-chromium-linux.png and b/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-loads-the-budget-page-with-budgeted-amounts-2-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/schedules.test.js-snapshots/Schedules-creates-a-new-schedule-posts-the-transaction-and-later-completes-it-1-chromium-linux.png b/packages/desktop-client/e2e/schedules.test.js-snapshots/Schedules-creates-a-new-schedule-posts-the-transaction-and-later-completes-it-1-chromium-linux.png index e718ec2f7189548f76ff52059a3708d8261bd1b3..04dd45ff949bd38a364a00ef7ea9c4e3e319b009 100644 Binary files a/packages/desktop-client/e2e/schedules.test.js-snapshots/Schedules-creates-a-new-schedule-posts-the-transaction-and-later-completes-it-1-chromium-linux.png and b/packages/desktop-client/e2e/schedules.test.js-snapshots/Schedules-creates-a-new-schedule-posts-the-transaction-and-later-completes-it-1-chromium-linux.png differ diff --git a/packages/desktop-client/src/components/Modals.tsx b/packages/desktop-client/src/components/Modals.tsx index 38095bb40c5564bf197b0f361aa0f48caa53154d..a08467e3f85ad965ecce72d1648311bcf677715e 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 { BudgetMonthMenuModal } from './modals/BudgetMonthMenuModal'; +import { BudgetPageMenuModal } from './modals/BudgetPageMenuModal'; import { CategoryAutocompleteModal } from './modals/CategoryAutocompleteModal'; import { CategoryGroupMenuModal } from './modals/CategoryGroupMenuModal'; import { CategoryMenuModal } from './modals/CategoryMenuModal'; @@ -40,9 +40,11 @@ import { Notes } from './modals/Notes'; import { PayeeAutocompleteModal } from './modals/PayeeAutocompleteModal'; import { ReportBalanceMenuModal } from './modals/ReportBalanceMenuModal'; import { ReportBudgetMenuModal } from './modals/ReportBudgetMenuModal'; +import { ReportBudgetMonthMenuModal } from './modals/ReportBudgetMonthMenuModal'; import { ReportBudgetSummaryModal } from './modals/ReportBudgetSummaryModal'; import { RolloverBalanceMenuModal } from './modals/RolloverBalanceMenuModal'; import { RolloverBudgetMenuModal } from './modals/RolloverBudgetMenuModal'; +import { RolloverBudgetMonthMenuModal } from './modals/RolloverBudgetMonthMenuModal'; import { RolloverBudgetSummaryModal } from './modals/RolloverBudgetSummaryModal'; import { RolloverToBudgetMenuModal } from './modals/RolloverToBudgetMenuModal'; import { ScheduledTransactionMenuModal } from './modals/ScheduledTransactionMenuModal'; @@ -601,17 +603,43 @@ export function Modals() { /> ); - case 'budget-month-menu': + case 'budget-page-menu': + return ( + <BudgetPageMenuModal + key={name} + modalProps={modalProps} + onAddCategoryGroup={options.onAddCategoryGroup} + onToggleHiddenCategories={options.onToggleHiddenCategories} + onSwitchBudgetType={options.onSwitchBudgetType} + /> + ); + + case 'rollover-budget-month-menu': return ( <NamespaceContext.Provider key={name} value={monthUtils.sheetForMonth(options.month)} > - <BudgetMonthMenuModal + <RolloverBudgetMonthMenuModal modalProps={modalProps} month={options.month} - onToggleHiddenCategories={options.onToggleHiddenCategories} - onSwitchBudgetType={options.onSwitchBudgetType} + onBudgetAction={options.onBudgetAction} + onEditNotes={options.onEditNotes} + /> + </NamespaceContext.Provider> + ); + + case 'report-budget-month-menu': + return ( + <NamespaceContext.Provider + key={name} + value={monthUtils.sheetForMonth(options.month)} + > + <ReportBudgetMonthMenuModal + modalProps={modalProps} + month={options.month} + onBudgetAction={options.onBudgetAction} + onEditNotes={options.onEditNotes} /> </NamespaceContext.Provider> ); diff --git a/packages/desktop-client/src/components/budget/index.tsx b/packages/desktop-client/src/components/budget/index.tsx index 927e553dd3deabbd46fa285b2618f0f166df1e75..67f3ec43c1e2de7083a0629360f5353d11190183 100644 --- a/packages/desktop-client/src/components/budget/index.tsx +++ b/packages/desktop-client/src/components/budget/index.tsx @@ -22,7 +22,6 @@ import { send, listen } from 'loot-core/src/platform/client/fetch'; import * as monthUtils from 'loot-core/src/shared/months'; import { useCategories } from '../../hooks/useCategories'; -import { useFeatureFlag } from '../../hooks/useFeatureFlag'; import { useGlobalPref } from '../../hooks/useGlobalPref'; import { useLocalPref } from '../../hooks/useLocalPref'; import { useNavigate } from '../../hooks/useNavigate'; @@ -411,13 +410,7 @@ function BudgetInner(props: BudgetInnerProps) { } const RolloverBudgetSummary = memo<{ month: string }>(props => { - const isGoalTemplatesEnabled = useFeatureFlag('goalTemplatesEnabled'); - return ( - <rollover.BudgetSummary - {...props} - isGoalTemplatesEnabled={isGoalTemplatesEnabled} - /> - ); + return <rollover.BudgetSummary {...props} />; }); RolloverBudgetSummary.displayName = 'RolloverBudgetSummary'; diff --git a/packages/desktop-client/src/components/budget/report/budgetsummary/BudgetMonthMenu.tsx b/packages/desktop-client/src/components/budget/report/budgetsummary/BudgetMonthMenu.tsx new file mode 100644 index 0000000000000000000000000000000000000000..aa5c379369a72675cc6e38b8d19c65b3c3e0d85f --- /dev/null +++ b/packages/desktop-client/src/components/budget/report/budgetsummary/BudgetMonthMenu.tsx @@ -0,0 +1,79 @@ +import React, { type ComponentPropsWithoutRef } from 'react'; + +import { useFeatureFlag } from '../../../../hooks/useFeatureFlag'; +import { Menu } from '../../../common/Menu'; + +type BudgetMonthMenuProps = Omit< + ComponentPropsWithoutRef<typeof Menu>, + 'onMenuSelect' | 'items' +> & { + onCopyLastMonthBudget: () => void; + onSetBudgetsToZero: () => void; + onSetMonthsAverage: (numberOfMonths: number) => void; + onCheckTemplates: () => void; + onApplyBudgetTemplates: () => void; + onOverwriteWithBudgetTemplates: () => void; +}; + +export function BudgetMonthMenu({ + onCopyLastMonthBudget, + onSetBudgetsToZero, + onSetMonthsAverage, + onCheckTemplates, + onApplyBudgetTemplates, + onOverwriteWithBudgetTemplates, + ...props +}: BudgetMonthMenuProps) { + const isGoalTemplatesEnabled = useFeatureFlag('goalTemplatesEnabled'); + return ( + <Menu + {...props} + onMenuSelect={name => { + switch (name) { + case 'copy-last': + onCopyLastMonthBudget(); + break; + case 'set-zero': + onSetBudgetsToZero(); + break; + case 'set-3-avg': + onSetMonthsAverage(3); + break; + case 'check-templates': + onCheckTemplates(); + break; + case 'apply-goal-template': + onApplyBudgetTemplates(); + break; + case 'overwrite-goal-template': + onOverwriteWithBudgetTemplates(); + break; + } + }} + items={[ + { name: 'copy-last', text: 'Copy last month’s budget' }, + { name: 'set-zero', text: 'Set budgets to zero' }, + { + name: 'set-3-avg', + text: 'Set budgets to 3 month average', + }, + ...(isGoalTemplatesEnabled + ? [ + { + name: 'check-templates', + text: 'Check templates', + }, + { + name: 'apply-goal-template', + text: 'Apply budget template', + }, + { + name: 'overwrite-goal-template', + text: 'Overwrite with budget template', + }, + ] + : []), + ]} + /> + ); +} diff --git a/packages/desktop-client/src/components/budget/report/budgetsummary/BudgetSummary.tsx b/packages/desktop-client/src/components/budget/report/budgetsummary/BudgetSummary.tsx index 9633789119e671eb737d1fea41899bba7b72e8d0..a2202bad3195aa0ceecffa5bcd700edd4f11a4a1 100644 --- a/packages/desktop-client/src/components/budget/report/budgetsummary/BudgetSummary.tsx +++ b/packages/desktop-client/src/components/budget/report/budgetsummary/BudgetSummary.tsx @@ -5,12 +5,10 @@ import { css } from 'glamor'; import * as monthUtils from 'loot-core/src/shared/months'; -import { useFeatureFlag } from '../../../../hooks/useFeatureFlag'; import { SvgDotsHorizontalTriple } from '../../../../icons/v1'; import { SvgArrowButtonDown1, SvgArrowButtonUp1 } from '../../../../icons/v2'; import { theme, styles } from '../../../../style'; import { Button } from '../../../common/Button'; -import { Menu } from '../../../common/Menu'; import { Stack } from '../../../common/Stack'; import { View } from '../../../common/View'; import { NotesButton } from '../../../NotesButton'; @@ -18,6 +16,7 @@ import { NamespaceContext } from '../../../spreadsheet/NamespaceContext'; import { Tooltip } from '../../../tooltips'; import { useReport } from '../ReportContext'; +import { BudgetMonthMenu } from './BudgetMonthMenu'; import { ExpenseTotal } from './ExpenseTotal'; import { IncomeTotal } from './IncomeTotal'; import { Saved } from './Saved'; @@ -33,8 +32,6 @@ export function BudgetSummary({ month }: BudgetSummaryProps) { onToggleSummaryCollapse, } = useReport(); - const isGoalTemplatesEnabled = useFeatureFlag('goalTemplatesEnabled'); - const [menuOpen, setMenuOpen] = useState(false); function onMenuOpen() { setMenuOpen(true); @@ -146,31 +143,31 @@ export function BudgetSummary({ month }: BudgetSummaryProps) { style={{ padding: 0 }} onClose={onMenuClose} > - <Menu - onMenuSelect={type => { + <BudgetMonthMenu + onCopyLastMonthBudget={() => { + onBudgetAction(month, 'copy-last'); + onMenuClose(); + }} + onSetBudgetsToZero={() => { + onBudgetAction(month, 'set-zero'); + onMenuClose(); + }} + onSetMonthsAverage={numberOfMonths => { + onBudgetAction(month, `set-${numberOfMonths}-avg`); + onMenuClose(); + }} + onCheckTemplates={() => { + onBudgetAction(month, 'check-templates'); + onMenuClose(); + }} + onApplyBudgetTemplates={() => { + onBudgetAction(month, 'apply-goal-template'); + onMenuClose(); + }} + onOverwriteWithBudgetTemplates={() => { + onBudgetAction(month, 'overwrite-goal-template'); onMenuClose(); - onBudgetAction(month, type); }} - items={[ - { name: 'copy-last', text: 'Copy last month’s budget' }, - { name: 'set-zero', text: 'Set budgets to zero' }, - { - name: 'set-3-avg', - text: 'Set budgets to 3 month average', - }, - isGoalTemplatesEnabled && { - name: 'check-templates', - text: 'Check templates', - }, - isGoalTemplatesEnabled && { - name: 'apply-goal-template', - text: 'Apply budget template', - }, - isGoalTemplatesEnabled && { - name: 'overwrite-goal-template', - text: 'Overwrite with budget template', - }, - ]} /> </Tooltip> )} diff --git a/packages/desktop-client/src/components/budget/rollover/budgetsummary/BudgetMonthMenu.tsx b/packages/desktop-client/src/components/budget/rollover/budgetsummary/BudgetMonthMenu.tsx new file mode 100644 index 0000000000000000000000000000000000000000..3f4c97da90473a216642d0edf0b19bed860c437d --- /dev/null +++ b/packages/desktop-client/src/components/budget/rollover/budgetsummary/BudgetMonthMenu.tsx @@ -0,0 +1,87 @@ +import React, { type ComponentPropsWithoutRef } from 'react'; + +import { useFeatureFlag } from '../../../../hooks/useFeatureFlag'; +import { Menu } from '../../../common/Menu'; + +type BudgetMonthMenuProps = Omit< + ComponentPropsWithoutRef<typeof Menu>, + 'onMenuSelect' | 'items' +> & { + onCopyLastMonthBudget: () => void; + onSetBudgetsToZero: () => void; + onSetMonthsAverage: (numberOfMonths: number) => void; + onCheckTemplates: () => void; + onApplyBudgetTemplates: () => void; + onOverwriteWithBudgetTemplates: () => void; + onEndOfMonthCleanup: () => void; +}; +export function BudgetMonthMenu({ + onCopyLastMonthBudget, + onSetBudgetsToZero, + onSetMonthsAverage, + onCheckTemplates, + onApplyBudgetTemplates, + onOverwriteWithBudgetTemplates, + onEndOfMonthCleanup, + ...props +}: BudgetMonthMenuProps) { + const isGoalTemplatesEnabled = useFeatureFlag('goalTemplatesEnabled'); + return ( + <Menu + {...props} + onMenuSelect={name => { + switch (name) { + case 'copy-last': + onCopyLastMonthBudget(); + break; + case 'set-zero': + onSetBudgetsToZero(); + break; + case 'set-3-avg': + onSetMonthsAverage(3); + break; + case 'check-templates': + onCheckTemplates(); + break; + case 'apply-goal-template': + onApplyBudgetTemplates(); + break; + case 'overwrite-goal-template': + onOverwriteWithBudgetTemplates(); + break; + case 'cleanup-goal-template': + onEndOfMonthCleanup(); + break; + } + }} + items={[ + { name: 'copy-last', text: 'Copy last month’s budget' }, + { name: 'set-zero', text: 'Set budgets to zero' }, + { + name: 'set-3-avg', + text: 'Set budgets to 3 month average', + }, + ...(isGoalTemplatesEnabled + ? [ + { + name: 'check-templates', + text: 'Check templates', + }, + { + name: 'apply-goal-template', + text: 'Apply budget template', + }, + { + name: 'overwrite-goal-template', + text: 'Overwrite with budget template', + }, + { + name: 'cleanup-goal-template', + text: 'End of month cleanup', + }, + ] + : []), + ]} + /> + ); +} diff --git a/packages/desktop-client/src/components/budget/rollover/budgetsummary/BudgetSummary.tsx b/packages/desktop-client/src/components/budget/rollover/budgetsummary/BudgetSummary.tsx index 2f9a25d45555401d7ad9bc72c1d63e6002a84f17..a63e573fbde8e8e91cfc88a028c97fc629234f63 100644 --- a/packages/desktop-client/src/components/budget/rollover/budgetsummary/BudgetSummary.tsx +++ b/packages/desktop-client/src/components/budget/rollover/budgetsummary/BudgetSummary.tsx @@ -8,13 +8,13 @@ import { SvgDotsHorizontalTriple } from '../../../../icons/v1'; import { SvgArrowButtonDown1, SvgArrowButtonUp1 } from '../../../../icons/v2'; import { theme, styles } from '../../../../style'; import { Button } from '../../../common/Button'; -import { Menu } from '../../../common/Menu'; import { View } from '../../../common/View'; import { NotesButton } from '../../../NotesButton'; import { NamespaceContext } from '../../../spreadsheet/NamespaceContext'; import { Tooltip } from '../../../tooltips'; import { useRollover } from '../RolloverContext'; +import { BudgetMonthMenu } from './BudgetMonthMenu'; import { ToBudget } from './ToBudget'; import { TotalsList } from './TotalsList'; @@ -22,10 +22,7 @@ type BudgetSummaryProps = { month: string; isGoalTemplatesEnabled?: boolean; }; -export function BudgetSummary({ - month, - isGoalTemplatesEnabled, -}: BudgetSummaryProps) { +export function BudgetSummary({ month }: BudgetSummaryProps) { const { currentMonth, summaryCollapsed: collapsed, @@ -148,39 +145,35 @@ export function BudgetSummary({ style={{ padding: 0 }} onClose={onMenuClose} > - <Menu - onMenuSelect={type => { + <BudgetMonthMenu + onCopyLastMonthBudget={() => { + onBudgetAction(month, 'copy-last'); + onMenuClose(); + }} + onSetBudgetsToZero={() => { + onBudgetAction(month, 'set-zero'); + onMenuClose(); + }} + onSetMonthsAverage={numberOfMonths => { + onBudgetAction(month, `set-${numberOfMonths}-avg`); + onMenuClose(); + }} + onCheckTemplates={() => { + onBudgetAction(month, 'check-templates'); + onMenuClose(); + }} + onApplyBudgetTemplates={() => { + onBudgetAction(month, 'apply-goal-template'); + onMenuClose(); + }} + onOverwriteWithBudgetTemplates={() => { + onBudgetAction(month, 'overwrite-goal-template'); + onMenuClose(); + }} + onEndOfMonthCleanup={() => { + onBudgetAction(month, 'cleanup-goal-template'); onMenuClose(); - onBudgetAction(month, type); }} - items={[ - { name: 'copy-last', text: 'Copy last month’s budget' }, - { name: 'set-zero', text: 'Set budgets to zero' }, - { - name: 'set-3-avg', - text: 'Set budgets to 3 month average', - }, - ...(isGoalTemplatesEnabled - ? [ - { - name: 'check-templates', - text: 'Check templates', - }, - { - name: 'apply-goal-template', - text: 'Apply budget template', - }, - { - name: 'overwrite-goal-template', - text: 'Overwrite with budget template', - }, - { - name: 'cleanup-goal-template', - text: 'End of month cleanup', - }, - ] - : []), - ]} /> </Tooltip> )} diff --git a/packages/desktop-client/src/components/mobile/budget/BudgetTable.jsx b/packages/desktop-client/src/components/mobile/budget/BudgetTable.jsx index 952c8f90fd708f8b8223e5c47c1a9b7b801c75e5..b5d64af8af9ef5ac6c951455feeac02db5dcf064 100644 --- a/packages/desktop-client/src/components/mobile/budget/BudgetTable.jsx +++ b/packages/desktop-client/src/components/mobile/budget/BudgetTable.jsx @@ -11,7 +11,6 @@ import { useLocalPref } from '../../../hooks/useLocalPref'; import { useNavigate } from '../../../hooks/useNavigate'; import { SvgLogo } from '../../../icons/logo'; import { - SvgAdd, SvgArrowThinLeft, SvgArrowThinRight, SvgCheveronDown, @@ -1154,7 +1153,6 @@ export function BudgetTable({ onNextMonth, onSaveGroup, onDeleteGroup, - onAddGroup, onAddCategory, onSaveCategory, onDeleteCategory, @@ -1165,6 +1163,7 @@ export function BudgetTable({ onRefresh, onEditGroup, onEditCategory, + onOpenBudgetPageMenu, onOpenBudgetMonthMenu, }) { const { width } = useResponsive(); @@ -1203,6 +1202,7 @@ export function BudgetTable({ <MonthSelector month={month} monthBounds={monthBounds} + onOpenMonthMenu={onOpenBudgetMonthMenu} onPrevMonth={onPrevMonth} onNextMonth={onNextMonth} /> @@ -1216,25 +1216,11 @@ export function BudgetTable({ }} hoveredStyle={noBackgroundColorStyle} activeStyle={noBackgroundColorStyle} - onClick={() => onOpenBudgetMonthMenu?.(month)} + onClick={() => onOpenBudgetPageMenu?.()} > <SvgLogo width="20" height="20" /> </Button> } - headerRightContent={ - <Button - type="bare" - style={{ - color: theme.mobileHeaderText, - margin: 10, - }} - hoveredStyle={noBackgroundColorStyle} - activeStyle={noBackgroundColorStyle} - onClick={onAddGroup} - > - <SvgAdd width="20" height="20" /> - </Button> - } style={{ flex: 1 }} > <View @@ -1402,7 +1388,13 @@ export function BudgetTable({ ); } -function MonthSelector({ month, monthBounds, onPrevMonth, onNextMonth }) { +function MonthSelector({ + month, + monthBounds, + onOpenMonthMenu, + onPrevMonth, + onNextMonth, +}) { const prevEnabled = month > monthBounds.start; const nextEnabled = month < monthUtils.subMonths(monthBounds.end, 1); @@ -1442,7 +1434,10 @@ function MonthSelector({ month, monthBounds, onPrevMonth, onNextMonth }) { textAlign: 'center', fontSize: 16, fontWeight: 500, + margin: '0 5px', + ...styles.underlinedText, }} + onClick={() => onOpenMonthMenu?.(month)} > {monthUtils.format(month, 'MMMM ‘yy')} </Text> diff --git a/packages/desktop-client/src/components/mobile/budget/index.tsx b/packages/desktop-client/src/components/mobile/budget/index.tsx index 48d4705fb9d9950fd5e318d2be1f2326d85a0b86..a4c5d4cf3b8dc5bba32a4788923d341bef1d3c04 100644 --- a/packages/desktop-client/src/components/mobile/budget/index.tsx +++ b/packages/desktop-client/src/components/mobile/budget/index.tsx @@ -111,18 +111,19 @@ function BudgetInner(props: BudgetInnerProps) { } }; - const onAddGroup = () => { + const onOpenNewCategoryGroupModal = () => { dispatch( pushModal('new-category-group', { onValidate: name => (!name ? 'Name is required.' : null), onSubmit: async name => { + dispatch(collapseModals('budget-page-menu')); dispatch(createGroup(name)); }, }), ); }; - const onAddCategory = (groupId, isIncome) => { + const onOpenNewCategoryModal = (groupId, isIncome) => { dispatch( pushModal('new-category', { onValidate: name => (!name ? 'Name is required.' : null), @@ -309,7 +310,7 @@ function BudgetInner(props: BudgetInnerProps) { await send('notes-save', { id, note: notes }); }; - const onEditGroupNotes = id => { + const onOpenCategoryGroupNotesModal = id => { const group = categoryGroups.find(g => g.id === id); dispatch( pushModal('notes', { @@ -320,7 +321,7 @@ function BudgetInner(props: BudgetInnerProps) { ); }; - const onEditCategoryNotes = id => { + const onOpenCategoryNotesModal = id => { const category = categories.find(c => c.id === id); dispatch( pushModal('notes', { @@ -331,38 +332,38 @@ function BudgetInner(props: BudgetInnerProps) { ); }; - const onEditGroup = id => { + const onOpenCategoryGroupMenuModal = id => { const group = categoryGroups.find(g => g.id === id); dispatch( pushModal('category-group-menu', { groupId: group.id, onSave: onSaveGroup, - onAddCategory, - onEditNotes: onEditGroupNotes, + onAddCategory: onOpenNewCategoryModal, + onEditNotes: onOpenCategoryGroupNotesModal, onDelete: onDeleteGroup, }), ); }; - const onEditCategory = id => { + const onOpenCategoryMenuModal = id => { const category = categories.find(c => c.id === id); dispatch( pushModal('category-menu', { categoryId: category.id, onSave: onSaveCategory, - onEditNotes: onEditCategoryNotes, + onEditNotes: onOpenCategoryNotesModal, onDelete: onDeleteCategory, onBudgetAction, }), ); }; - const _onSwitchBudgetType = () => { + const onOpenSwitchBudgetTypeModal = () => { dispatch( pushModal('switch-budget-type', { onSwitch: () => { - onSwitchBudgetType?.(); - dispatch(collapseModals('budget-month-menu')); + onSwitchBudgetType(); + dispatch(collapseModals('budget-page-menu')); }, }), ); @@ -374,15 +375,35 @@ function BudgetInner(props: BudgetInnerProps) { const onToggleHiddenCategories = () => { setShowHiddenCategoriesPref(!showHiddenCategories); - dispatch(collapseModals('budget-month-menu')); + dispatch(collapseModals('budget-page-menu')); + }; + + const onOpenBudgetMonthNotesModal = id => { + dispatch( + pushModal('notes', { + id, + name: monthUtils.format(startMonth, 'MMMM ‘yy'), + onSave: onSaveNotes, + }), + ); }; const onOpenBudgetMonthMenu = month => { dispatch( - pushModal('budget-month-menu', { + pushModal(`${budgetType}-budget-month-menu`, { month, + onBudgetAction, + onEditNotes: onOpenBudgetMonthNotesModal, + }), + ); + }; + + const onOpenBudgetPageMenu = () => { + dispatch( + pushModal('budget-page-menu', { + onAddCategoryGroup: onOpenNewCategoryGroupModal, onToggleHiddenCategories, - onSwitchBudgetType: _onSwitchBudgetType, + onSwitchBudgetType: onOpenSwitchBudgetTypeModal, }), ); }; @@ -425,16 +446,16 @@ function BudgetInner(props: BudgetInnerProps) { onNextMonth={onNextMonth} onSaveGroup={onSaveGroup} onDeleteGroup={onDeleteGroup} - onAddGroup={onAddGroup} - onAddCategory={onAddCategory} + onAddCategory={onOpenNewCategoryModal} onSaveCategory={onSaveCategory} onDeleteCategory={onDeleteCategory} onReorderCategory={onReorderCategory} onReorderGroup={onReorderGroup} onBudgetAction={onBudgetAction} onRefresh={onRefresh} - onEditGroup={onEditGroup} - onEditCategory={onEditCategory} + onEditGroup={onOpenCategoryGroupMenuModal} + onEditCategory={onOpenCategoryMenuModal} + onOpenBudgetPageMenu={onOpenBudgetPageMenu} onOpenBudgetMonthMenu={onOpenBudgetMonthMenu} /> )} diff --git a/packages/desktop-client/src/components/modals/BudgetMonthMenuModal.tsx b/packages/desktop-client/src/components/modals/BudgetPageMenuModal.tsx similarity index 72% rename from packages/desktop-client/src/components/modals/BudgetMonthMenuModal.tsx rename to packages/desktop-client/src/components/modals/BudgetPageMenuModal.tsx index 05c7dd0352f5ac82346ff92fc4540f01a0ea2054..beeffccbee3724746b36ae3e82b337a0c31c25f3 100644 --- a/packages/desktop-client/src/components/modals/BudgetMonthMenuModal.tsx +++ b/packages/desktop-client/src/components/modals/BudgetPageMenuModal.tsx @@ -1,23 +1,24 @@ import React, { type ComponentPropsWithoutRef } from 'react'; import { useFeatureFlag } from '../../hooks/useFeatureFlag'; +import { useLocalPref } from '../../hooks/useLocalPref'; import { type CSSProperties, theme, styles } from '../../style'; import { Menu } from '../common/Menu'; import { Modal } from '../common/Modal'; import { type CommonModalProps } from '../Modals'; -type BudgetMonthMenuModalProps = ComponentPropsWithoutRef< - typeof BudgetMonthMenu +type BudgetPageMenuModalProps = ComponentPropsWithoutRef< + typeof BudgetPageMenu > & { modalProps: CommonModalProps; }; -export function BudgetMonthMenuModal({ +export function BudgetPageMenuModal({ modalProps, - month, + onAddCategoryGroup, onToggleHiddenCategories, onSwitchBudgetType, -}: BudgetMonthMenuModalProps) { +}: BudgetPageMenuModalProps) { const defaultMenuItemStyle: CSSProperties = { ...styles.mobileMenuItem, color: theme.menuItemText, @@ -38,9 +39,9 @@ export function BudgetMonthMenuModal({ borderRadius: '6px', }} > - <BudgetMonthMenu - month={month} + <BudgetPageMenu getItemStyle={() => defaultMenuItemStyle} + onAddCategoryGroup={onAddCategoryGroup} onToggleHiddenCategories={onToggleHiddenCategories} onSwitchBudgetType={onSwitchBudgetType} /> @@ -48,29 +49,29 @@ export function BudgetMonthMenuModal({ ); } -type BudgetMonthMenuProps = Omit< +type BudgetPageMenuProps = Omit< ComponentPropsWithoutRef<typeof Menu>, 'onMenuSelect' | 'items' > & { - month: string; + onAddCategoryGroup: () => void; onToggleHiddenCategories: () => void; onSwitchBudgetType: () => void; }; -function BudgetMonthMenu({ - // onEditMode, - month, +function BudgetPageMenu({ + onAddCategoryGroup, onToggleHiddenCategories, onSwitchBudgetType, ...props -}: BudgetMonthMenuProps) { +}: BudgetPageMenuProps) { const isReportBudgetEnabled = useFeatureFlag('reportBudget'); + const [showHiddenCategories] = useLocalPref('budget.showHiddenCategories'); const onMenuSelect = (name: string) => { switch (name) { - // case 'edit-mode': - // onEditMode?.(true); - // break; + case 'add-category-group': + onAddCategoryGroup?.(); + break; case 'toggle-hidden-categories': onToggleHiddenCategories?.(); break; @@ -87,11 +88,13 @@ function BudgetMonthMenu({ {...props} onMenuSelect={onMenuSelect} items={[ - // Removing for now until we work on mobile category drag and drop. - // { name: 'edit-mode', text: 'Edit mode' }, + { + name: 'add-category-group', + text: 'Add category group', + }, { name: 'toggle-hidden-categories', - text: 'Toggle hidden categories', + text: `${!showHiddenCategories ? 'Show' : 'Hide'} hidden categories`, }, ...(isReportBudgetEnabled ? [ diff --git a/packages/desktop-client/src/components/modals/ReportBudgetMonthMenuModal.tsx b/packages/desktop-client/src/components/modals/ReportBudgetMonthMenuModal.tsx new file mode 100644 index 0000000000000000000000000000000000000000..18cab06ad8034ec98c1c948b2d29d7277d5b48b5 --- /dev/null +++ b/packages/desktop-client/src/components/modals/ReportBudgetMonthMenuModal.tsx @@ -0,0 +1,192 @@ +// @ts-strict-ignore +import React, { useState } from 'react'; + +import { useLiveQuery } from 'loot-core/src/client/query-hooks'; +import * as monthUtils from 'loot-core/src/shared/months'; +import { q } from 'loot-core/src/shared/query'; +import { type NoteEntity } from 'loot-core/src/types/models'; + +import { SvgCheveronDown, SvgCheveronUp } from '../../icons/v1'; +import { SvgNotesPaper } from '../../icons/v2'; +import { type CSSProperties, styles, theme } from '../../style'; +import { BudgetMonthMenu } from '../budget/report/budgetsummary/BudgetMonthMenu'; +import { Button } from '../common/Button'; +import { Modal, ModalTitle } from '../common/Modal'; +import { View } from '../common/View'; +import { type CommonModalProps } from '../Modals'; +import { Notes } from '../Notes'; + +type ReportBudgetMonthMenuModalProps = { + modalProps: CommonModalProps; + month: string; + onBudgetAction: (month: string, action: string, arg?: unknown) => void; + onEditNotes: (id: string) => void; +}; + +export function ReportBudgetMonthMenuModal({ + modalProps, + month, + onBudgetAction, + onEditNotes, +}: ReportBudgetMonthMenuModalProps) { + const notesId = `budget-${month}`; + const data = useLiveQuery<NoteEntity[]>( + () => q('notes').filter({ id: notesId }).select('*'), + [notesId], + ); + const originalNotes = data && data.length > 0 ? data[0].note : null; + + const onClose = () => { + modalProps.onClose(); + }; + + const _onEditNotes = () => { + onEditNotes?.(notesId); + }; + + const defaultMenuItemStyle: CSSProperties = { + ...styles.mobileMenuItem, + color: theme.menuItemText, + borderRadius: 0, + borderTop: `1px solid ${theme.pillBorder}`, + }; + + const buttonStyle: CSSProperties = { + ...styles.mediumText, + height: styles.mobileMinHeight, + color: theme.formLabelText, + // Adjust based on desired number of buttons per row. + flexBasis: '100%', + }; + + const [showMore, setShowMore] = useState(false); + + const onShowMore = () => { + setShowMore(!showMore); + }; + + return ( + <Modal + title={<ModalTitle title={monthUtils.format(month, 'MMMM ‘yy')} />} + showHeader + focusAfterClose={false} + {...modalProps} + onClose={onClose} + padding={10} + style={{ + flex: 1, + height: '50vh', + borderRadius: '6px', + }} + > + <View + style={{ + flex: 1, + flexDirection: 'column', + }} + > + <View + style={{ + display: showMore ? 'none' : undefined, + overflowY: 'auto', + flex: 1, + }} + > + <Notes + notes={originalNotes?.length > 0 ? originalNotes : 'No notes'} + editable={false} + focused={false} + getStyle={() => ({ + borderRadius: 6, + ...((!originalNotes || originalNotes.length === 0) && { + justifySelf: 'center', + alignSelf: 'center', + color: theme.pageTextSubdued, + }), + })} + /> + </View> + <View style={{ paddingTop: 10, gap: 5 }}> + <View + style={{ + display: showMore ? 'none' : undefined, + flexDirection: 'row', + flexWrap: 'wrap', + justifyContent: 'space-between', + alignContent: 'space-between', + }} + > + <Button style={buttonStyle} onClick={_onEditNotes}> + <SvgNotesPaper + width={20} + height={20} + style={{ paddingRight: 5 }} + /> + Edit notes + </Button> + </View> + <View> + <Button + type="bare" + style={buttonStyle} + activeStyle={{ + backgroundColor: 'transparent', + color: buttonStyle.color, + }} + hoveredStyle={{ + backgroundColor: 'transparent', + color: buttonStyle.color, + }} + onClick={onShowMore} + > + {!showMore ? ( + <SvgCheveronUp + width={30} + height={30} + style={{ paddingRight: 5 }} + /> + ) : ( + <SvgCheveronDown + width={30} + height={30} + style={{ paddingRight: 5 }} + /> + )} + Actions + </Button> + </View> + </View> + {showMore && ( + <BudgetMonthMenu + style={{ overflowY: 'auto', paddingTop: 10 }} + getItemStyle={() => defaultMenuItemStyle} + onCopyLastMonthBudget={() => { + onBudgetAction(month, 'copy-last'); + onClose(); + }} + onSetBudgetsToZero={() => { + onBudgetAction(month, 'set-zero'); + onClose(); + }} + onSetMonthsAverage={numberOfMonths => { + onBudgetAction(month, `set-${numberOfMonths}-avg`); + onClose(); + }} + onCheckTemplates={() => { + onBudgetAction(month, 'check-templates'); + onClose(); + }} + onApplyBudgetTemplates={() => { + onBudgetAction(month, 'apply-goal-template'); + onClose(); + }} + onOverwriteWithBudgetTemplates={() => { + onBudgetAction(month, 'overwrite-goal-template'); + onClose(); + }} + /> + )} + </View> + </Modal> + ); +} diff --git a/packages/desktop-client/src/components/modals/RolloverBudgetMonthMenuModal.tsx b/packages/desktop-client/src/components/modals/RolloverBudgetMonthMenuModal.tsx new file mode 100644 index 0000000000000000000000000000000000000000..caf9df76923bde849a582349d8697fb862ee8a68 --- /dev/null +++ b/packages/desktop-client/src/components/modals/RolloverBudgetMonthMenuModal.tsx @@ -0,0 +1,196 @@ +// @ts-strict-ignore +import React, { useState } from 'react'; + +import { useLiveQuery } from 'loot-core/src/client/query-hooks'; +import * as monthUtils from 'loot-core/src/shared/months'; +import { q } from 'loot-core/src/shared/query'; +import { type NoteEntity } from 'loot-core/src/types/models'; + +import { SvgCheveronDown, SvgCheveronUp } from '../../icons/v1'; +import { SvgNotesPaper } from '../../icons/v2'; +import { type CSSProperties, styles, theme } from '../../style'; +import { BudgetMonthMenu } from '../budget/rollover/budgetsummary/BudgetMonthMenu'; +import { Button } from '../common/Button'; +import { Modal, ModalTitle } from '../common/Modal'; +import { View } from '../common/View'; +import { type CommonModalProps } from '../Modals'; +import { Notes } from '../Notes'; + +type RolloverBudgetMonthMenuModalProps = { + modalProps: CommonModalProps; + month: string; + onBudgetAction: (month: string, action: string, arg?: unknown) => void; + onEditNotes: (id: string) => void; +}; + +export function RolloverBudgetMonthMenuModal({ + modalProps, + month, + onBudgetAction, + onEditNotes, +}: RolloverBudgetMonthMenuModalProps) { + const notesId = `budget-${month}`; + const data = useLiveQuery<NoteEntity[]>( + () => q('notes').filter({ id: notesId }).select('*'), + [notesId], + ); + const originalNotes = data && data.length > 0 ? data[0].note : null; + + const onClose = () => { + modalProps.onClose(); + }; + + const _onEditNotes = () => { + onEditNotes?.(notesId); + }; + + const defaultMenuItemStyle: CSSProperties = { + ...styles.mobileMenuItem, + color: theme.menuItemText, + borderRadius: 0, + borderTop: `1px solid ${theme.pillBorder}`, + }; + + const buttonStyle: CSSProperties = { + ...styles.mediumText, + height: styles.mobileMinHeight, + color: theme.formLabelText, + // Adjust based on desired number of buttons per row. + flexBasis: '100%', + }; + + const [showMore, setShowMore] = useState(false); + + const onShowMore = () => { + setShowMore(!showMore); + }; + + return ( + <Modal + title={<ModalTitle title={monthUtils.format(month, 'MMMM ‘yy')} />} + showHeader + focusAfterClose={false} + {...modalProps} + onClose={onClose} + padding={10} + style={{ + flex: 1, + height: '50vh', + borderRadius: '6px', + }} + > + <View + style={{ + flex: 1, + flexDirection: 'column', + }} + > + <View + style={{ + display: showMore ? 'none' : undefined, + overflowY: 'auto', + flex: 1, + }} + > + <Notes + notes={originalNotes?.length > 0 ? originalNotes : 'No notes'} + editable={false} + focused={false} + getStyle={() => ({ + borderRadius: 6, + ...((!originalNotes || originalNotes.length === 0) && { + justifySelf: 'center', + alignSelf: 'center', + color: theme.pageTextSubdued, + }), + })} + /> + </View> + <View style={{ paddingTop: 10, gap: 5 }}> + <View + style={{ + display: showMore ? 'none' : undefined, + flexDirection: 'row', + flexWrap: 'wrap', + justifyContent: 'space-between', + alignContent: 'space-between', + }} + > + <Button style={buttonStyle} onClick={_onEditNotes}> + <SvgNotesPaper + width={20} + height={20} + style={{ paddingRight: 5 }} + /> + Edit notes + </Button> + </View> + <View> + <Button + type="bare" + style={buttonStyle} + activeStyle={{ + backgroundColor: 'transparent', + color: buttonStyle.color, + }} + hoveredStyle={{ + backgroundColor: 'transparent', + color: buttonStyle.color, + }} + onClick={onShowMore} + > + {!showMore ? ( + <SvgCheveronUp + width={30} + height={30} + style={{ paddingRight: 5 }} + /> + ) : ( + <SvgCheveronDown + width={30} + height={30} + style={{ paddingRight: 5 }} + /> + )} + Actions + </Button> + </View> + </View> + {showMore && ( + <BudgetMonthMenu + style={{ overflowY: 'auto', paddingTop: 10 }} + getItemStyle={() => defaultMenuItemStyle} + onCopyLastMonthBudget={() => { + onBudgetAction(month, 'copy-last'); + onClose(); + }} + onSetBudgetsToZero={() => { + onBudgetAction(month, 'set-zero'); + onClose(); + }} + onSetMonthsAverage={numberOfMonths => { + onBudgetAction(month, `set-${numberOfMonths}-avg`); + onClose(); + }} + onCheckTemplates={() => { + onBudgetAction(month, 'check-templates'); + onClose(); + }} + onApplyBudgetTemplates={() => { + onBudgetAction(month, 'apply-goal-template'); + onClose(); + }} + onOverwriteWithBudgetTemplates={() => { + onBudgetAction(month, 'overwrite-goal-template'); + onClose(); + }} + onEndOfMonthCleanup={() => { + onBudgetAction(month, 'cleanup-goal-template'); + onClose(); + }} + /> + )} + </View> + </Modal> + ); +} diff --git a/packages/desktop-client/src/style/styles.ts b/packages/desktop-client/src/style/styles.ts index 53c9cd57ff602a0177d04632f1eb558f264c81b9..573d980659010e30214c7cbb1472ab4c05735d72 100644 --- a/packages/desktop-client/src/style/styles.ts +++ b/packages/desktop-client/src/style/styles.ts @@ -26,6 +26,7 @@ export const styles = { paddingTop: 8, paddingBottom: 8, height: MOBILE_MIN_HEIGHT, + minHeight: MOBILE_MIN_HEIGHT, }, mobileEditingPadding: 12, altMenuMaxHeight: 250, diff --git a/packages/loot-core/src/client/state-types/modals.d.ts b/packages/loot-core/src/client/state-types/modals.d.ts index 83646290c1d6560974e698a7eee443286baed893..fb29aa9ebfb5e415f783e516e72e827f461d13ae 100644 --- a/packages/loot-core/src/client/state-types/modals.d.ts +++ b/packages/loot-core/src/client/state-types/modals.d.ts @@ -230,11 +230,21 @@ type FinanceModals = { onPost: (transactionId: string) => void; onSkip: (transactionId: string) => void; }; - 'budget-month-menu': { - month: string; + 'budget-page-menu': { + onAddCategoryGroup: () => void; onToggleHiddenCategories: () => void; onSwitchBudgetType: () => void; }; + 'rollover-budget-month-menu': { + month: string; + onBudgetAction: (month: string, action: string, arg?: unknown) => void; + onEditNotes: (id: string) => void; + }; + 'report-budget-month-menu': { + month: string; + onBudgetAction: (month: string, action: string, arg?: unknown) => void; + onEditNotes: (id: string) => void; + }; }; export type PushModalAction = { diff --git a/packages/loot-core/src/shared/months.ts b/packages/loot-core/src/shared/months.ts index 49108e8cb0cf14304b4136dea021559a9b177a72..f059bbf36cf4f24b596a36dcc3ff4bb2c016bd48 100644 --- a/packages/loot-core/src/shared/months.ts +++ b/packages/loot-core/src/shared/months.ts @@ -370,8 +370,7 @@ export function sheetForMonth(month: string): string { } export function nameForMonth(month: DateLike): string { - // eslint-disable-next-line rulesdir/typography - return d.format(_parse(month), "MMMM 'yy"); + return d.format(_parse(month), 'MMMM ‘yy'); } export function format(month: DateLike, str: string): string { diff --git a/upcoming-release-notes/2610.md b/upcoming-release-notes/2610.md new file mode 100644 index 0000000000000000000000000000000000000000..98d60ea52bd2f3cc1cddc5cf93b1135e8fad6213 --- /dev/null +++ b/upcoming-release-notes/2610.md @@ -0,0 +1,6 @@ +--- +category: Features +authors: [joel-jeremy] +--- + +Add month notes and budget/template action menus for mobile.