diff --git a/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-creates-a-transaction-via-footer-button-5-chromium-linux.png b/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-creates-a-transaction-via-footer-button-5-chromium-linux.png index 7faf369647c..94ee19d49d1 100644 Binary files a/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-creates-a-transaction-via-footer-button-5-chromium-linux.png and b/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-creates-a-transaction-via-footer-button-5-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-opens-individual-account-page-and-checks-that-filtering-is-working-1-chromium-linux.png b/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-opens-individual-account-page-and-checks-that-filtering-is-working-1-chromium-linux.png index 623c76625f1..d155399a88c 100644 Binary files a/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-opens-individual-account-page-and-checks-that-filtering-is-working-1-chromium-linux.png and b/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-opens-individual-account-page-and-checks-that-filtering-is-working-1-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-opens-individual-account-page-and-checks-that-filtering-is-working-3-chromium-linux.png b/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-opens-individual-account-page-and-checks-that-filtering-is-working-3-chromium-linux.png index 78e8dc758e8..2c233815a96 100644 Binary files a/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-opens-individual-account-page-and-checks-that-filtering-is-working-3-chromium-linux.png and b/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-opens-individual-account-page-and-checks-that-filtering-is-working-3-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-opens-individual-account-page-and-checks-that-filtering-is-working-5-chromium-linux.png b/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-opens-individual-account-page-and-checks-that-filtering-is-working-5-chromium-linux.png index 51e86751f51..62f4c3567cd 100644 Binary files a/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-opens-individual-account-page-and-checks-that-filtering-is-working-5-chromium-linux.png and b/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-opens-individual-account-page-and-checks-that-filtering-is-working-5-chromium-linux.png differ diff --git a/packages/desktop-client/src/components/Modals.tsx b/packages/desktop-client/src/components/Modals.tsx index bcc1ade800f..bdd7cc587c7 100644 --- a/packages/desktop-client/src/components/Modals.tsx +++ b/packages/desktop-client/src/components/Modals.tsx @@ -6,16 +6,21 @@ import { useLocation } from 'react-router-dom'; import { type State } from 'loot-core/src/client/state-types'; import { type PopModalAction } from 'loot-core/src/client/state-types/modals'; import { send } from 'loot-core/src/platform/client/fetch'; +import * as monthUtils from 'loot-core/src/shared/months'; import { useActions } from '../hooks/useActions'; import { useSyncServerStatus } from '../hooks/useSyncServerStatus'; -import { CategoryGroupMenu } from './modals/CategoryGroupMenu'; -import { CategoryMenu } from './modals/CategoryMenu'; -import { CloseAccount } from './modals/CloseAccount'; +import { AccountAutocompleteModal } from './modals/AccountAutocompleteModal'; +import { AccountMenuModal } from './modals/AccountMenuModal'; +import { CategoryAutocompleteModal } from './modals/CategoryAutocompleteModal'; +import { CategoryGroupMenuModal } from './modals/CategoryGroupMenuModal'; +import { CategoryMenuModal } from './modals/CategoryMenuModal'; +import { CloseAccountModal } from './modals/CloseAccountModal'; import { ConfirmCategoryDelete } from './modals/ConfirmCategoryDelete'; import { ConfirmTransactionEdit } from './modals/ConfirmTransactionEdit'; import { ConfirmUnlinkAccount } from './modals/ConfirmUnlinkAccount'; +import { CoverModal } from './modals/CoverModal'; import { CreateAccount } from './modals/CreateAccount'; import { CreateEncryptionKey } from './modals/CreateEncryptionKey'; import { CreateLocalAccount } from './modals/CreateLocalAccount'; @@ -24,22 +29,30 @@ import { EditRule } from './modals/EditRule'; import { FixEncryptionKey } from './modals/FixEncryptionKey'; import { GoCardlessExternalMsg } from './modals/GoCardlessExternalMsg'; import { GoCardlessInitialise } from './modals/GoCardlessInitialise'; +import { HoldBufferModal } from './modals/HoldBufferModal'; import { ImportTransactions } from './modals/ImportTransactions'; import { LoadBackup } from './modals/LoadBackup'; import { ManageRulesModal } from './modals/ManageRulesModal'; import { MergeUnusedPayees } from './modals/MergeUnusedPayees'; import { Notes } from './modals/Notes'; +import { PayeeAutocompleteModal } from './modals/PayeeAutocompleteModal'; import { PlaidExternalMsg } from './modals/PlaidExternalMsg'; -import { ReportBudgetSummary } from './modals/ReportBudgetSummary'; -import { RolloverBudgetSummary } from './modals/RolloverBudgetSummary'; +import { ReportBalanceMenuModal } from './modals/ReportBalanceMenuModal'; +import { ReportBudgetSummaryModal } from './modals/ReportBudgetSummaryModal'; +import { RolloverBalanceMenuModal } from './modals/RolloverBalanceMenuModal'; +import { RolloverBudgetSummaryModal } from './modals/RolloverBudgetSummaryModal'; +import { RolloverToBudgetMenuModal } from './modals/RolloverToBudgetMenuModal'; +import { ScheduledTransactionMenuModal } from './modals/ScheduledTransactionMenuModal'; import { SelectLinkedAccounts } from './modals/SelectLinkedAccounts'; import { SimpleFinInitialise } from './modals/SimpleFinInitialise'; -import { SingleInput } from './modals/SingleInput'; +import { SingleInputModal } from './modals/SingleInputModal'; import { SwitchBudgetType } from './modals/SwitchBudgetType'; +import { TransferModal } from './modals/TransferModal'; import { DiscoverSchedules } from './schedules/DiscoverSchedules'; import { PostsOfflineNotification } from './schedules/PostsOfflineNotification'; import { ScheduleDetails } from './schedules/ScheduleDetails'; import { ScheduleLink } from './schedules/ScheduleLink'; +import { NamespaceContext } from './spreadsheet/NamespaceContext'; export type CommonModalProps = { onClose: () => PopModalAction; @@ -97,12 +110,11 @@ export function Modals() { case 'close-account': return ( - ); @@ -255,9 +267,51 @@ export function Modals() { /> ); + case 'category-autocomplete': + return ( + + ); + + case 'account-autocomplete': + return ( + + ); + + case 'payee-autocomplete': + return ( + + ); + case 'new-category': return ( - + value={monthUtils.sheetForMonth(options.month)} + > + + ); case 'report-budget-summary': return ( - ); + case 'account-menu': + return ( + + ); + case 'category-menu': return ( - ); + case 'rollover-balance-menu': + return ( + + + + ); + + case 'rollover-to-budget-menu': + return ( + + + + ); + + case 'hold-buffer': + return ( + + + + ); + + case 'report-balance-menu': + return ( + + + + ); + + case 'transfer': + return ( + + ); + + case 'cover': + return ( + + ); + + case 'scheduled-transaction-menu': + return ( + + ); + default: console.error('Unknown modal:', name); return null; diff --git a/packages/desktop-client/src/components/autocomplete/AccountAutocomplete.tsx b/packages/desktop-client/src/components/autocomplete/AccountAutocomplete.tsx index e6efee159a2..bc053ed99b2 100644 --- a/packages/desktop-client/src/components/autocomplete/AccountAutocomplete.tsx +++ b/packages/desktop-client/src/components/autocomplete/AccountAutocomplete.tsx @@ -166,7 +166,7 @@ type AccountItemProps = { embedded?: boolean; }; -export function AccountItem({ +function AccountItem({ item, className, highlighted, diff --git a/packages/desktop-client/src/components/autocomplete/CategoryAutocomplete.tsx b/packages/desktop-client/src/components/autocomplete/CategoryAutocomplete.tsx index fc2617b930f..8ff9d228ff1 100644 --- a/packages/desktop-client/src/components/autocomplete/CategoryAutocomplete.tsx +++ b/packages/desktop-client/src/components/autocomplete/CategoryAutocomplete.tsx @@ -297,7 +297,7 @@ type CategoryItemProps = { embedded?: boolean; }; -export function CategoryItem({ +function CategoryItem({ item, className, style, diff --git a/packages/desktop-client/src/components/autocomplete/PayeeAutocomplete.tsx b/packages/desktop-client/src/components/autocomplete/PayeeAutocomplete.tsx index 5d660aab80f..a65ed413d7d 100644 --- a/packages/desktop-client/src/components/autocomplete/PayeeAutocomplete.tsx +++ b/packages/desktop-client/src/components/autocomplete/PayeeAutocomplete.tsx @@ -398,6 +398,7 @@ type CreatePayeeButtonProps = { style?: CSSProperties; }; +// eslint-disable-next-line import/no-unused-modules export function CreatePayeeButton({ Icon, payeeName, @@ -471,7 +472,7 @@ type PayeeItemProps = { embedded?: boolean; }; -export function PayeeItem({ +function PayeeItem({ item, className, highlighted, diff --git a/packages/desktop-client/src/components/budget/index.tsx b/packages/desktop-client/src/components/budget/index.tsx index cad97746ba6..a95139bf720 100644 --- a/packages/desktop-client/src/components/budget/index.tsx +++ b/packages/desktop-client/src/components/budget/index.tsx @@ -28,6 +28,7 @@ import { useLocalPref } from '../../hooks/useLocalPref'; import { useNavigate } from '../../hooks/useNavigate'; import { styles } from '../../style'; import { View } from '../common/View'; +import { NamespaceContext } from '../spreadsheet/NamespaceContext'; import { SWITCH_BUDGET_MESSAGE_TYPE, TitlebarContext, @@ -404,7 +405,11 @@ function BudgetInner(props: BudgetInnerProps) { ); } - return {table}; + return ( + + {table} + + ); } const RolloverBudgetSummary = memo<{ month: string }>(props => { diff --git a/packages/desktop-client/src/components/budget/report/BalanceMenu.tsx b/packages/desktop-client/src/components/budget/report/BalanceMenu.tsx index 0878a98310b..0673620ff37 100644 --- a/packages/desktop-client/src/components/budget/report/BalanceMenu.tsx +++ b/packages/desktop-client/src/components/budget/report/BalanceMenu.tsx @@ -28,7 +28,7 @@ export function BalanceMenu({ onCarryover?.(!carryover); break; default: - throw new Error(`Unsupported item: ${name}`); + throw new Error(`Unrecognized menu option: ${name}`); } }} items={[ diff --git a/packages/desktop-client/src/components/budget/rollover/BalanceMenu.tsx b/packages/desktop-client/src/components/budget/rollover/BalanceMenu.tsx index 803fcbe58b0..3ec3c5f0f6c 100644 --- a/packages/desktop-client/src/components/budget/rollover/BalanceMenu.tsx +++ b/packages/desktop-client/src/components/budget/rollover/BalanceMenu.tsx @@ -39,7 +39,7 @@ export function BalanceMenu({ onCover?.(); break; default: - throw new Error(`Unsupported item: ${name}`); + throw new Error(`Unrecognized menu option: ${name}`); } }} items={[ diff --git a/packages/desktop-client/src/components/budget/rollover/RolloverContext.tsx b/packages/desktop-client/src/components/budget/rollover/RolloverContext.tsx index ea05cd87374..bf68c07afaa 100644 --- a/packages/desktop-client/src/components/budget/rollover/RolloverContext.tsx +++ b/packages/desktop-client/src/components/budget/rollover/RolloverContext.tsx @@ -9,7 +9,7 @@ type RolloverContextDefinition = { currentMonth: string; }; -const Context = createContext({ +const RolloverContext = createContext({ summaryCollapsed: false, onBudgetAction: () => { throw new Error('Unitialised context method called: onBudgetAction'); @@ -34,7 +34,7 @@ export function RolloverProvider({ const currentMonth = monthUtils.currentMonth(); return ( - {children} - + ); } export function useRollover() { - return useContext(Context); + return useContext(RolloverContext); } diff --git a/packages/desktop-client/src/components/budget/rollover/budgetsummary/ToBudgetMenu.tsx b/packages/desktop-client/src/components/budget/rollover/budgetsummary/ToBudgetMenu.tsx index 183e7d28753..95cf54a8f14 100644 --- a/packages/desktop-client/src/components/budget/rollover/budgetsummary/ToBudgetMenu.tsx +++ b/packages/desktop-client/src/components/budget/rollover/budgetsummary/ToBudgetMenu.tsx @@ -31,7 +31,7 @@ export function ToBudgetMenu({ onResetHoldBuffer?.(); break; default: - throw new Error(`Unsupported item: ${name}`); + throw new Error(`Unrecognized menu option: ${name}`); } }} items={[ diff --git a/packages/desktop-client/src/components/mobile/MobileForms.tsx b/packages/desktop-client/src/components/mobile/MobileForms.tsx index 6141192bdab..9b5394a024a 100644 --- a/packages/desktop-client/src/components/mobile/MobileForms.tsx +++ b/packages/desktop-client/src/components/mobile/MobileForms.tsx @@ -1,5 +1,5 @@ import React, { - type ComponentPropsWithoutRef, + type ComponentPropsWithRef, forwardRef, type ReactNode, } from 'react'; @@ -45,7 +45,7 @@ const valueStyle = { height: styles.mobileMinHeight, }; -type InputFieldProps = ComponentPropsWithoutRef; +type InputFieldProps = ComponentPropsWithRef; export const InputField = forwardRef( ({ disabled, style, onUpdate, ...props }, ref) => { @@ -72,7 +72,7 @@ export const InputField = forwardRef( InputField.displayName = 'InputField'; -type TapFieldProps = ComponentPropsWithoutRef & { +type TapFieldProps = ComponentPropsWithRef & { rightContent?: ReactNode; }; diff --git a/packages/desktop-client/src/components/mobile/accounts/Account.jsx b/packages/desktop-client/src/components/mobile/accounts/Account.jsx index 25c0bd4b17f..4a07c391e6e 100644 --- a/packages/desktop-client/src/components/mobile/accounts/Account.jsx +++ b/packages/desktop-client/src/components/mobile/accounts/Account.jsx @@ -12,7 +12,7 @@ import { } from 'loot-core/src/client/data-hooks/schedules'; import * as queries from 'loot-core/src/client/queries'; import { pagedQuery } from 'loot-core/src/client/query-helpers'; -import { listen } from 'loot-core/src/platform/client/fetch'; +import { listen, send } from 'loot-core/src/platform/client/fetch'; import { isPreviewId, ungroupTransactions, @@ -126,6 +126,10 @@ export function Account(props) { updateQuery(query); }, [makeRootQuery, updateQuery]); + const refetchTransactions = () => { + paged.current?.run(); + }; + useEffect(() => { let unlisten; @@ -137,7 +141,7 @@ export function Account(props) { tables.includes('category_mapping') || tables.includes('payee_mapping') ) { - paged.current?.run(); + refetchTransactions(); } if (tables.includes('payees') || tables.includes('payee_mapping')) { @@ -222,6 +226,23 @@ export function Account(props) { // details of how the native app used to handle preview transactions here can be found at commit 05e58279 if (!isPreviewId(transaction.id)) { navigate(`transactions/${transaction.id}`); + } else { + dispatch( + actions.pushModal('scheduled-transaction-menu', { + transactionId: transaction.id, + onPost: async transactionId => { + const parts = transactionId.split('/'); + await send('schedule/post-transaction', { id: parts[1] }); + refetchTransactions(); + dispatch(actions.collapseModals('scheduled-transaction-menu')); + }, + onSkip: async transactionId => { + const parts = transactionId.split('/'); + await send('schedule/skip-next-date', { id: parts[1] }); + dispatch(actions.collapseModals('scheduled-transaction-menu')); + }, + }), + ); } }; diff --git a/packages/desktop-client/src/components/mobile/accounts/AccountDetails.jsx b/packages/desktop-client/src/components/mobile/accounts/AccountDetails.jsx index b73909fd337..be92e97ffa6 100644 --- a/packages/desktop-client/src/components/mobile/accounts/AccountDetails.jsx +++ b/packages/desktop-client/src/components/mobile/accounts/AccountDetails.jsx @@ -1,7 +1,14 @@ import React, { useState, useMemo } from 'react'; import { useDispatch } from 'react-redux'; -import { syncAndDownload } from 'loot-core/client/actions'; +import { + openAccountCloseModal, + pushModal, + reopenAccount, + syncAndDownload, + updateAccount, +} from 'loot-core/client/actions'; +import { send } from 'loot-core/platform/client/fetch'; import { SvgAdd } from '../../../icons/v1'; import { SvgSearchAlternate } from '../../../icons/v2'; @@ -9,6 +16,7 @@ import { styles, theme } from '../../../style'; import { ButtonLink } from '../../common/ButtonLink'; import { InputWithContent } from '../../common/InputWithContent'; import { Label } from '../../common/Label'; +import { Text } from '../../common/Text'; import { View } from '../../common/View'; import { MobileBackButton } from '../../MobileBackButton'; import { Page } from '../../Page'; @@ -60,6 +68,79 @@ function TransactionSearchInput({ accountName, onSearch }) { ); } +function AccountName({ account, pending, failed }) { + const dispatch = useDispatch(); + + const onSave = account => { + dispatch(updateAccount(account)); + }; + + const onSaveNotes = async (id, notes) => { + await send('notes-save', { id, note: notes }); + }; + + const onEditNotes = () => { + dispatch( + pushModal('notes', { + id: account.id, + name: account.name, + onSave: onSaveNotes, + }), + ); + }; + + const onCloseAccount = () => { + dispatch(openAccountCloseModal(account.id)); + }; + + const onReopenAccount = () => { + dispatch(reopenAccount(account.id)); + }; + + const onClick = () => { + dispatch( + pushModal('account-menu', { + accountId: account.id, + onSave, + onEditNotes, + onCloseAccount, + onReopenAccount, + }), + ); + }; + return ( + + {account.bankId && ( +
+ )} + + {`${account.closed ? 'Closed: ' : ''}${account.name}`} + + + ); +} + export function AccountDetails({ account, pending, @@ -89,32 +170,7 @@ export function AccountDetails({ return ( -
- {account.name} - - ) + } headerLeftContent={} headerRightContent={ diff --git a/packages/desktop-client/src/components/mobile/budget/BudgetTable.jsx b/packages/desktop-client/src/components/mobile/budget/BudgetTable.jsx index 92bdf7fd602..eed6c851f02 100644 --- a/packages/desktop-client/src/components/mobile/budget/BudgetTable.jsx +++ b/packages/desktop-client/src/components/mobile/budget/BudgetTable.jsx @@ -3,7 +3,7 @@ import { useDispatch } from 'react-redux'; import memoizeOne from 'memoize-one'; -import { pushModal } from 'loot-core/client/actions'; +import { collapseModals, pushModal } from 'loot-core/client/actions'; import { rolloverBudget, reportBudget } from 'loot-core/src/client/queries'; import * as monthUtils from 'loot-core/src/shared/months'; @@ -21,8 +21,6 @@ import { import { useResponsive } from '../../../ResponsiveProvider'; import { theme, styles } from '../../../style'; import { BalanceWithCarryover } from '../../budget/BalanceWithCarryover'; -import { BalanceTooltip as ReportBudgetBalanceTooltip } from '../../budget/report/BalanceTooltip'; -import { BalanceTooltip as RolloverBudgetBalanceTooltip } from '../../budget/rollover/BalanceTooltip'; import { makeAmountGrey } from '../../budget/util'; import { Button } from '../../common/Button'; import { Card } from '../../common/Card'; @@ -32,19 +30,13 @@ import { Text } from '../../common/Text'; import { View } from '../../common/View'; import { Page } from '../../Page'; import { CellValue } from '../../spreadsheet/CellValue'; -import { NamespaceContext } from '../../spreadsheet/NamespaceContext'; import { useFormat } from '../../spreadsheet/useFormat'; import { useSheetValue } from '../../spreadsheet/useSheetValue'; import { Tooltip, useTooltip } from '../../tooltips'; import { AmountInput } from '../../util/AmountInput'; import { MOBILE_NAV_HEIGHT } from '../MobileNavTabs'; import { PullToRefresh } from '../PullToRefresh'; -// import { -// AmountAccessoryContext, -// MathOperations -// } from '../mobile/AmountInput'; -// import { DragDrop, Draggable, Droppable, DragDropHighlight } from './dragdrop'; import { ListItem, ROW_HEIGHT } from './ListItem'; function ToBudget({ toBudget, onClick }) { @@ -257,10 +249,11 @@ const ExpenseCategory = memo(function ExpenseCategory({ showBudgetedCol, }) { const opacity = blank ? 0 : 1; - const balanceTooltip = useTooltip(); + const [budgetType = 'rollover'] = useLocalPref('budgetType'); const [isEditingBudget, setIsEditingBudget] = useState(false); const { onRequestActiveEdit, onClearActiveEdit } = useSingleActiveEditForm(); + const dispatch = useDispatch(); const onEditBudget = () => { onRequestActiveEdit(`${category.id}-budget`, () => { @@ -269,23 +262,64 @@ const ExpenseCategory = memo(function ExpenseCategory({ }); }; - const onOpenBalanceActionMenu = () => { - onRequestActiveEdit(`${category.id}-balance`, () => { - balanceTooltip.open(); - return () => balanceTooltip.close(); + const onCarryover = carryover => { + onBudgetAction(month, 'carryover', { + category: category.id, + flag: carryover, }); + dispatch(collapseModals(`${budgetType}-balance-menu`)); }; - const listItemRef = useRef(); + const catBalance = useSheetValue( + type === 'rollover' + ? rolloverBudget.catBalance(category.id) + : reportBudget.catBalance(category.id), + ); - const _onBudgetAction = (monthIndex, action, arg) => { - onBudgetAction?.( - monthUtils.getMonthFromIndex(monthUtils.getYear(month), monthIndex), - action, - arg, + const onTransfer = () => { + dispatch( + pushModal('transfer', { + title: `Transfer: ${category.name}`, + amount: catBalance, + onSubmit: (amount, toCategoryId) => { + onBudgetAction(month, 'transfer-category', { + amount, + from: category.id, + to: toCategoryId, + }); + }, + showToBeBudgeted: true, + }), ); }; + const onCover = () => { + dispatch( + pushModal('cover', { + categoryId: category.id, + onSubmit: fromCategoryId => { + onBudgetAction(month, 'cover', { + to: category.id, + from: fromCategoryId, + }); + }, + }), + ); + }; + + const onOpenBalanceActionMenu = () => { + dispatch( + pushModal(`${budgetType}-balance-menu`, { + categoryId: category.id, + month, + onCarryover, + ...(budgetType === 'rollover' && { onTransfer, onCover }), + }), + ); + }; + + const listItemRef = useRef(); + const content = ( - onOpenBalanceActionMenu?.()} - onPointerDown={e => e.preventDefault()} - > + onOpenBalanceActionMenu?.()}> - {balanceTooltip.isOpen && - (type === 'report' ? ( - { - onClearActiveEdit(); - }} - /> - ) : ( - { - onClearActiveEdit(); - }} - /> - ))} @@ -1172,255 +1178,253 @@ export function BudgetTable({ }; return ( - - + } + headerRightContent={ + !editMode ? ( + - } - headerRightContent={ - !editMode ? ( - - ) : ( - - ) - } - style={{ flex: 1 }} + ) : ( + + ) + } + style={{ flex: 1 }} + > + - - {type === 'report' ? ( - = monthUtils.currentMonth()} - onClick={onShowBudgetSummary} - /> - ) : ( - - )} - - {(show3Cols || !showSpentColumn) && ( - - )} - {(show3Cols || showSpentColumn) && ( - + )} + {(show3Cols || showSpentColumn) && ( + - )} + /> + + + )} + + + + + {!editMode ? ( + // (this.list = el)} + // keyboardShouldPersistTaps="always" + // refreshControl={refreshControl} + // style={{ backgroundColor: colors.n10 }} + // automaticallyAdjustContentInsets={false} + // > - + ) : ( + // + // + // {({ + // dragging, + // onGestureEvent, + // onHandlerStateChange, + // scrollRef, + // onScroll + // }) => ( + + - - - {!editMode ? ( - // (this.list = el)} - // keyboardShouldPersistTaps="always" - // refreshControl={refreshControl} - // style={{ backgroundColor: colors.n10 }} - // automaticallyAdjustContentInsets={false} - // > - - - - ) : ( - // - // - // {({ - // dragging, - // onGestureEvent, - // onHandlerStateChange, - // scrollRef, - // onScroll - // }) => ( - - - - // - // - )} - - - + // + // + )} + + ); } diff --git a/packages/desktop-client/src/components/mobile/budget/index.tsx b/packages/desktop-client/src/components/mobile/budget/index.tsx index d221f00a50c..eb5e5a7fd37 100644 --- a/packages/desktop-client/src/components/mobile/budget/index.tsx +++ b/packages/desktop-client/src/components/mobile/budget/index.tsx @@ -33,6 +33,7 @@ import { AnimatedLoading } from '../../../icons/AnimatedLoading'; import { theme } from '../../../style'; import { prewarmMonth, switchBudgetType } from '../../budget/util'; import { View } from '../../common/View'; +import { NamespaceContext } from '../../spreadsheet/NamespaceContext'; import { SyncRefresh } from '../../SyncRefresh'; import { BudgetTable } from './BudgetTable'; @@ -369,42 +370,44 @@ function BudgetInner(props: BudgetInnerProps) { } return ( - { - dispatch(sync()); - }} - > - {({ onRefresh }) => ( - setEditMode(flag)} - onShowBudgetSummary={onShowBudgetSummary} - onPrevMonth={onPrevMonth} - onNextMonth={onNextMonth} - onSaveGroup={onSaveGroup} - onDeleteGroup={onDeleteGroup} - onAddGroup={onAddGroup} - onAddCategory={onAddCategory} - onSaveCategory={onSaveCategory} - onDeleteCategory={onDeleteCategory} - onReorderCategory={onReorderCategory} - onReorderGroup={onReorderGroup} - onOpenMonthActionMenu={() => {}} //onOpenMonthActionMenu} - onBudgetAction={onBudgetAction} - onRefresh={onRefresh} - onSwitchBudgetType={onSwitchBudgetType} - onEditGroup={onEditGroup} - onEditCategory={onEditCategory} - /> - )} - + + { + dispatch(sync()); + }} + > + {({ onRefresh }) => ( + setEditMode(flag)} + onShowBudgetSummary={onShowBudgetSummary} + onPrevMonth={onPrevMonth} + onNextMonth={onNextMonth} + onSaveGroup={onSaveGroup} + onDeleteGroup={onDeleteGroup} + onAddGroup={onAddGroup} + onAddCategory={onAddCategory} + onSaveCategory={onSaveCategory} + onDeleteCategory={onDeleteCategory} + onReorderCategory={onReorderCategory} + onReorderGroup={onReorderGroup} + onOpenMonthActionMenu={() => {}} //onOpenMonthActionMenu} + onBudgetAction={onBudgetAction} + onRefresh={onRefresh} + onSwitchBudgetType={onSwitchBudgetType} + onEditGroup={onEditGroup} + onEditCategory={onEditCategory} + /> + )} + + ); } diff --git a/packages/desktop-client/src/components/mobile/transactions/TransactionEdit.jsx b/packages/desktop-client/src/components/mobile/transactions/TransactionEdit.jsx index 9e9be907aa1..98b02cd0e9e 100644 --- a/packages/desktop-client/src/components/mobile/transactions/TransactionEdit.jsx +++ b/packages/desktop-client/src/components/mobile/transactions/TransactionEdit.jsx @@ -428,6 +428,7 @@ const TransactionEditInner = memo(function TransactionEditInner({ [], [unserializedTransactions, dateFormat], ); + const { grouped: categoryGroups } = useCategories(); const [transaction, ...childTransactions] = transactions; @@ -542,24 +543,90 @@ const TransactionEditInner = memo(function TransactionEditInner({ }; const onClick = (transactionId, name) => { - onRequestActiveEdit?.(getFieldName(transaction.id, 'payee'), () => { - dispatch( - pushModal('edit-field', { - name, - onSubmit: (name, value) => { - const transaction = unserializedTransactions.find( - t => t.id === transactionId, - ); - // This is a deficiency of this API, need to fix. It - // assumes that it receives a serialized transaction, - // but we only have access to the raw transaction - onEdit(serializeTransaction(transaction, dateFormat), name, value); - }, - onClose: () => { - onClearActiveEdit(); - }, - }), + onRequestActiveEdit?.(getFieldName(transaction.id, name), () => { + const transaction = unserializedTransactions.find( + t => t.id === transactionId, ); + switch (name) { + case 'category': + dispatch( + pushModal('category-autocomplete', { + categoryGroups, + onSelect: categoryId => { + // This is a deficiency of this API, need to fix. It + // assumes that it receives a serialized transaction, + // but we only have access to the raw transaction + onEdit( + serializeTransaction(transaction, dateFormat), + name, + categoryId, + ); + }, + onClose: () => { + onClearActiveEdit(); + }, + }), + ); + break; + case 'account': + dispatch( + pushModal('account-autocomplete', { + onSelect: accountId => { + // This is a deficiency of this API, need to fix. It + // assumes that it receives a serialized transaction, + // but we only have access to the raw transaction + onEdit( + serializeTransaction(transaction, dateFormat), + name, + accountId, + ); + }, + onClose: () => { + onClearActiveEdit(); + }, + }), + ); + break; + case 'payee': + dispatch( + pushModal('payee-autocomplete', { + onSelect: payeeId => { + // This is a deficiency of this API, need to fix. It + // assumes that it receives a serialized transaction, + // but we only have access to the raw transaction + onEdit( + serializeTransaction(transaction, dateFormat), + name, + payeeId, + ); + }, + onClose: () => { + onClearActiveEdit(); + }, + }), + ); + break; + default: + dispatch( + pushModal('edit-field', { + name, + onSubmit: (name, value) => { + // This is a deficiency of this API, need to fix. It + // assumes that it receives a serialized transaction, + // but we only have access to the raw transaction + onEdit( + serializeTransaction(transaction, dateFormat), + name, + value, + ); + }, + onClose: () => { + onClearActiveEdit(); + }, + }), + ); + break; + } }); }; diff --git a/packages/desktop-client/src/components/modals/AccountAutocompleteModal.tsx b/packages/desktop-client/src/components/modals/AccountAutocompleteModal.tsx new file mode 100644 index 00000000000..5ec7911bef1 --- /dev/null +++ b/packages/desktop-client/src/components/modals/AccountAutocompleteModal.tsx @@ -0,0 +1,78 @@ +import React, { type ComponentPropsWithoutRef } from 'react'; + +import { useResponsive } from '../../ResponsiveProvider'; +import { theme } from '../../style'; +import { AccountAutocomplete } from '../autocomplete/AccountAutocomplete'; +import { Modal } from '../common/Modal'; +import { View } from '../common/View'; +import { SectionLabel } from '../forms'; +import { type CommonModalProps } from '../Modals'; + +type AccountAutocompleteModalProps = { + modalProps: CommonModalProps; + autocompleteProps: ComponentPropsWithoutRef; + onClose: () => void; +}; + +export function AccountAutocompleteModal({ + modalProps, + autocompleteProps, + onClose, +}: AccountAutocompleteModalProps) { + const _onClose = () => { + modalProps.onClose(); + onClose?.(); + }; + + const { isNarrowWidth } = useResponsive(); + const defaultAutocompleteProps = { + containerProps: { style: { height: isNarrowWidth ? '90vh' : 275 } }, + }; + + return ( + + {() => ( + + {!isNarrowWidth && ( + + )} + + + + + )} + + ); +} diff --git a/packages/desktop-client/src/components/modals/AccountMenuModal.tsx b/packages/desktop-client/src/components/modals/AccountMenuModal.tsx new file mode 100644 index 00000000000..c6759989192 --- /dev/null +++ b/packages/desktop-client/src/components/modals/AccountMenuModal.tsx @@ -0,0 +1,250 @@ +import React, { useState } from 'react'; + +import { useLiveQuery } from 'loot-core/src/client/query-hooks'; +import { q } from 'loot-core/src/shared/query'; +import { type AccountEntity } from 'loot-core/types/models'; + +import { useAccounts } from '../../hooks/useAccounts'; +import { SvgClose, SvgDotsHorizontalTriple, SvgLockOpen } from '../../icons/v1'; +import { SvgNotesPaper } from '../../icons/v2'; +import { type CSSProperties, styles, theme } from '../../style'; +import { Button } from '../common/Button'; +import { Menu } from '../common/Menu'; +import { Modal } from '../common/Modal'; +import { View } from '../common/View'; +import { type CommonModalProps } from '../Modals'; +import { Notes } from '../Notes'; +import { Tooltip } from '../tooltips'; + +type NoteEntity = { + id: string; + note: string; +}; + +type AccountMenuModalProps = { + modalProps: CommonModalProps; + accountId: string; + onSave: (account: AccountEntity) => void; + onCloseAccount: (accountId: string) => void; + onReopenAccount: (accountId: string) => void; + onEditNotes: (id: string) => void; + onClose?: () => void; +}; + +export function AccountMenuModal({ + modalProps, + accountId, + onSave, + onCloseAccount, + onReopenAccount, + onEditNotes, + onClose, +}: AccountMenuModalProps) { + const accounts = useAccounts(); + const account = accounts.find(c => c.id === accountId); + const data = useLiveQuery( + () => q('notes').filter({ id: account?.id }).select('*'), + [account?.id], + ) as NoteEntity[] | null; + const originalNotes = data && data.length > 0 ? data[0].note : null; + + const _onClose = () => { + modalProps?.onClose(); + onClose?.(); + }; + + const onRename = (newName: string) => { + if (!account) { + return; + } + + if (newName !== account.name) { + onSave?.({ + ...account, + name: newName, + }); + } + }; + + const _onEditNotes = () => { + if (!account) { + return; + } + + onEditNotes?.(account.id); + }; + + const buttonStyle: CSSProperties = { + ...styles.mediumText, + height: styles.mobileMinHeight, + color: theme.formLabelText, + // Adjust based on desired number of buttons per row. + flexBasis: '100%', + }; + + if (!account) { + return null; + } + + return ( + + } + > + {({ isEditingTitle }) => ( + + + 0 + ? originalNotes + : 'No notes' + } + editable={false} + focused={false} + getStyle={() => ({ + borderRadius: 6, + ...((!originalNotes || originalNotes.length === 0) && { + justifySelf: 'center', + alignSelf: 'center', + color: theme.pageTextSubdued, + }), + })} + /> + + + + + + )} + + ); +} + +type AdditionalAccountMenuProps = { + account: AccountEntity; + onClose?: (accountId: string) => void; + onReopen?: (accountId: string) => void; +}; + +function AdditionalAccountMenu({ + account, + onClose, + onReopen, +}: AdditionalAccountMenuProps) { + const [menuOpen, setMenuOpen] = useState(false); + const itemStyle: CSSProperties = { + ...styles.mediumText, + height: styles.mobileMinHeight, + }; + + return ( + + + + ); +} diff --git a/packages/desktop-client/src/components/modals/CategoryAutocompleteModal.tsx b/packages/desktop-client/src/components/modals/CategoryAutocompleteModal.tsx new file mode 100644 index 00000000000..6d5c9f9722f --- /dev/null +++ b/packages/desktop-client/src/components/modals/CategoryAutocompleteModal.tsx @@ -0,0 +1,80 @@ +import React, { type ComponentPropsWithoutRef } from 'react'; + +import { useResponsive } from '../../ResponsiveProvider'; +import { theme } from '../../style'; +import { CategoryAutocomplete } from '../autocomplete/CategoryAutocomplete'; +import { Modal } from '../common/Modal'; +import { View } from '../common/View'; +import { SectionLabel } from '../forms'; +import { type CommonModalProps } from '../Modals'; + +type CategoryAutocompleteModalProps = { + modalProps: CommonModalProps; + autocompleteProps: ComponentPropsWithoutRef; + onClose: () => void; +}; + +export function CategoryAutocompleteModal({ + modalProps, + autocompleteProps, + onClose, +}: CategoryAutocompleteModalProps) { + const _onClose = () => { + modalProps.onClose(); + onClose?.(); + }; + + const { isNarrowWidth } = useResponsive(); + + const defaultAutocompleteProps = { + containerProps: { style: { height: isNarrowWidth ? '90vh' : 275 } }, + }; + + return ( + + {() => ( + + {!isNarrowWidth && ( + + )} + + + + + )} + + ); +} diff --git a/packages/desktop-client/src/components/modals/CategoryGroupMenu.tsx b/packages/desktop-client/src/components/modals/CategoryGroupMenuModal.tsx similarity index 92% rename from packages/desktop-client/src/components/modals/CategoryGroupMenu.tsx rename to packages/desktop-client/src/components/modals/CategoryGroupMenuModal.tsx index 42e24a0b809..c963e668fb8 100644 --- a/packages/desktop-client/src/components/modals/CategoryGroupMenu.tsx +++ b/packages/desktop-client/src/components/modals/CategoryGroupMenuModal.tsx @@ -17,9 +17,7 @@ import { type CommonModalProps } from '../Modals'; import { Notes } from '../Notes'; import { Tooltip } from '../tooltips'; -const BUTTON_HEIGHT = 40; - -type CategoryGroupMenuProps = { +type CategoryGroupMenuModalProps = { modalProps: CommonModalProps; groupId: string; onSave: (group: CategoryGroupEntity) => void; @@ -30,7 +28,7 @@ type CategoryGroupMenuProps = { onClose?: () => void; }; -export function CategoryGroupMenu({ +export function CategoryGroupMenuModal({ modalProps, groupId, onSave, @@ -38,7 +36,7 @@ export function CategoryGroupMenu({ onEditNotes, onDelete, onClose, -}: CategoryGroupMenuProps) { +}: CategoryGroupMenuModalProps) { const { grouped: categoryGroups } = useCategories(); const group = categoryGroups.find(g => g.id === groupId); const data = useLiveQuery( @@ -47,47 +45,43 @@ export function CategoryGroupMenu({ ); const notes = data && data.length > 0 ? data[0].note : null; - function _onClose() { + const _onClose = () => { modalProps?.onClose(); onClose?.(); - } + }; - function _onRename(newName) { + const onRename = newName => { if (newName !== group.name) { onSave?.({ ...group, name: newName, }); } - } + }; - function _onAddCategory() { + const _onAddCategory = () => { onAddCategory?.(group.id, group.is_income); - } + }; - function _onEditNotes() { + const _onEditNotes = () => { onEditNotes?.(group.id); - } + }; - function _onToggleVisibility() { + const _onToggleVisibility = () => { onSave?.({ ...group, hidden: !!!group.hidden, }); _onClose(); - } + }; - function _onDelete() { + const _onDelete = () => { onDelete?.(group.id); - } - - function onNameUpdate(newName) { - _onRename(newName); - } + }; const buttonStyle: CSSProperties = { ...styles.mediumText, - height: BUTTON_HEIGHT, + height: styles.mobileMinHeight, color: theme.formLabelText, // Adjust based on desired number of buttons per row. flexBasis: '48%', @@ -111,7 +105,7 @@ export function CategoryGroupMenu({ }} editableTitle={true} titleStyle={styles.underlinedText} - onTitleUpdate={onNameUpdate} + onTitleUpdate={onRename} leftHeaderContent={ itemStyle} items={ [ { @@ -231,7 +226,6 @@ function AdditionalCategoryGroupMenu({ group, onDelete, onToggleVisibility }) { text: group.hidden ? 'Show' : 'Hide', icon: group.hidden ? SvgViewShow : SvgViewHide, iconSize: 16, - style: itemStyle, }, ...(!group.is_income && [ Menu.line, @@ -240,7 +234,6 @@ function AdditionalCategoryGroupMenu({ group, onDelete, onToggleVisibility }) { text: 'Delete', icon: SvgTrash, iconSize: 15, - style: itemStyle, }, ]), ].filter(i => i != null) as ComponentProps['items'] diff --git a/packages/desktop-client/src/components/modals/CategoryMenu.tsx b/packages/desktop-client/src/components/modals/CategoryMenuModal.tsx similarity index 91% rename from packages/desktop-client/src/components/modals/CategoryMenu.tsx rename to packages/desktop-client/src/components/modals/CategoryMenuModal.tsx index 5d2c704f88a..a67c1188957 100644 --- a/packages/desktop-client/src/components/modals/CategoryMenu.tsx +++ b/packages/desktop-client/src/components/modals/CategoryMenuModal.tsx @@ -17,9 +17,7 @@ import { type CommonModalProps } from '../Modals'; import { Notes } from '../Notes'; import { Tooltip } from '../tooltips'; -const BUTTON_HEIGHT = 40; - -type CategoryMenuProps = { +type CategoryMenuModalProps = { modalProps: CommonModalProps; categoryId: string; onSave: (category: CategoryEntity) => void; @@ -28,14 +26,14 @@ type CategoryMenuProps = { onClose?: () => void; }; -export function CategoryMenu({ +export function CategoryMenuModal({ modalProps, categoryId, onSave, onEditNotes, onDelete, onClose, -}: CategoryMenuProps) { +}: CategoryMenuModalProps) { const { list: categories } = useCategories(); const category = categories.find(c => c.id === categoryId); const data = useLiveQuery( @@ -44,43 +42,39 @@ export function CategoryMenu({ ); const originalNotes = data && data.length > 0 ? data[0].note : null; - function _onClose() { + const _onClose = () => { modalProps?.onClose(); onClose?.(); - } + }; - function _onRename(newName) { + const onRename = newName => { if (newName !== category.name) { onSave?.({ ...category, name: newName, }); } - } + }; - function _onToggleVisibility() { + const _onToggleVisibility = () => { onSave?.({ ...category, hidden: !category.hidden, }); _onClose(); - } + }; - function _onEditNotes() { + const _onEditNotes = () => { onEditNotes?.(category.id); - } + }; - function _onDelete() { + const _onDelete = () => { onDelete?.(category.id); - } - - function onNameUpdate(newName) { - _onRename(newName); - } + }; const buttonStyle: CSSProperties = { ...styles.mediumText, - height: BUTTON_HEIGHT, + height: styles.mobileMinHeight, color: theme.formLabelText, // Adjust based on desired number of buttons per row. flexBasis: '100%', @@ -102,7 +96,7 @@ export function CategoryMenu({ borderRadius: '6px', }} editableTitle={true} - onTitleUpdate={onNameUpdate} + onTitleUpdate={onRename} leftHeaderContent={ itemStyle} items={[ { name: 'toggleVisibility', text: category.hidden ? 'Show' : 'Hide', icon: category.hidden ? SvgViewShow : SvgViewHide, iconSize: 16, - style: itemStyle, }, Menu.line, { @@ -212,7 +206,6 @@ function AdditionalCategoryMenu({ category, onDelete, onToggleVisibility }) { text: 'Delete', icon: SvgTrash, iconSize: 15, - style: itemStyle, }, ]} onMenuSelect={itemName => { diff --git a/packages/desktop-client/src/components/modals/CloseAccount.tsx b/packages/desktop-client/src/components/modals/CloseAccountModal.tsx similarity index 57% rename from packages/desktop-client/src/components/modals/CloseAccount.tsx rename to packages/desktop-client/src/components/modals/CloseAccountModal.tsx index afac51f8a1e..aa71110c21e 100644 --- a/packages/desktop-client/src/components/modals/CloseAccount.tsx +++ b/packages/desktop-client/src/components/modals/CloseAccountModal.tsx @@ -1,13 +1,19 @@ // @ts-strict-ignore import React, { useState } from 'react'; +import { useDispatch } from 'react-redux'; +import { + closeAccount, + forceCloseAccount, + pushModal, +} from 'loot-core/client/actions'; import { integerToCurrency } from 'loot-core/src/shared/util'; import { type AccountEntity } from 'loot-core/src/types/models'; import { useAccounts } from '../../hooks/useAccounts'; -import { type BoundActions } from '../../hooks/useActions'; import { useCategories } from '../../hooks/useCategories'; -import { theme } from '../../style'; +import { useResponsive } from '../../ResponsiveProvider'; +import { type CSSProperties, styles, theme } from '../../style'; import { AccountAutocomplete } from '../autocomplete/AccountAutocomplete'; import { CategoryAutocomplete } from '../autocomplete/CategoryAutocomplete'; import { Button } from '../common/Button'; @@ -32,29 +38,53 @@ function needsCategory( return account.offbudget === 0 && isOffBudget; } -type CloseAccountProps = { +type CloseAccountModalProps = { account: AccountEntity; balance: number; canDelete: boolean; - actions: BoundActions; modalProps: CommonModalProps; }; -export function CloseAccount({ +export function CloseAccountModal({ account, balance, canDelete, - actions, modalProps, -}: CloseAccountProps) { +}: CloseAccountModalProps) { + const accounts = useAccounts().filter(a => a.closed === 0); + const { grouped: categoryGroups, list: categories } = useCategories(); const [loading, setLoading] = useState(false); - const [transfer, setTransfer] = useState(''); - const [category, setCategory] = useState(''); + const [transferAccountId, setTransferAccountId] = useState(''); + const transferAccount = accounts.find(a => a.id === transferAccountId); + const [categoryId, setCategoryId] = useState(''); + const category = categories.find(c => c.id === categoryId); const [transferError, setTransferError] = useState(false); const [categoryError, setCategoryError] = useState(false); - const accounts = useAccounts().filter(a => a.closed === 0); - const { grouped: categoryGroups } = useCategories(); + const dispatch = useDispatch(); + const { isNarrowWidth } = useResponsive(); + + const onSelectAccount = accId => { + setTransferAccountId(accId); + if (transferError && accId) { + setTransferError(false); + } + }; + + const onSelectCategory = catId => { + setCategoryId(catId); + if (categoryError && catId) { + setCategoryError(false); + } + }; + + const narrowStyle: CSSProperties = isNarrowWidth + ? { + userSelect: 'none', + height: styles.mobileMinHeight, + ...styles.mediumText, + } + : {}; return ( { event.preventDefault(); - const transferError = balance !== 0 && transfer === ''; + const transferError = balance !== 0 && transferAccountId === ''; setTransferError(transferError); const categoryError = - needsCategory(account, transfer, accounts) && category === ''; + needsCategory(account, transferAccountId, accounts) && + categoryId === ''; setCategoryError(categoryError); if (!transferError && !categoryError) { setLoading(true); - actions - .closeAccount(account.id, transfer || null, category || null) - .then(() => { - modalProps.onClose(); - }); + dispatch( + closeAccount( + account.id, + transferAccountId || null, + categoryId || null, + ), + ); + modalProps.onClose(); } }} > @@ -112,16 +146,25 @@ export function CloseAccount({ { + dispatch( + pushModal('account-autocomplete', { + includeClosedAccounts: false, + onSelect: onSelectAccount, + }), + ); + }, + }), }} - onSelect={acc => { - setTransfer(acc); - if (transferError && acc) { - setTransferError(false); - } - }} + onSelect={onSelectAccount} /> @@ -131,7 +174,7 @@ export function CloseAccount({ )} - {needsCategory(account, transfer, accounts) && ( + {needsCategory(account, transferAccountId, accounts) && ( Since you are transferring the balance from a budgeted @@ -141,17 +184,26 @@ export function CloseAccount({ { + dispatch( + pushModal('category-autocomplete', { + categoryGroups, + showHiddenCategories: true, + onSelect: onSelectCategory, + }), + ); + }, + }), }} - onSelect={newValue => { - setCategory(newValue); - if (categoryError && newValue) { - setCategoryError(false); - } - }} - showHiddenCategories={true} + onSelect={onSelectCategory} /> {categoryError && ( @@ -170,9 +222,8 @@ export function CloseAccount({ onClick={() => { setLoading(true); - actions - .forceCloseAccount(account.id) - .then(() => modalProps.onClose()); + dispatch(forceCloseAccount(account.id)); + modalProps.onClose(); }} style={{ color: theme.errorText }} > @@ -191,10 +242,23 @@ export function CloseAccount({ justifyContent: 'flex-end', }} > - - + diff --git a/packages/desktop-client/src/components/modals/CoverModal.tsx b/packages/desktop-client/src/components/modals/CoverModal.tsx new file mode 100644 index 00000000000..90f4369da98 --- /dev/null +++ b/packages/desktop-client/src/components/modals/CoverModal.tsx @@ -0,0 +1,115 @@ +import React, { useCallback, useEffect, useState } from 'react'; +import { useDispatch } from 'react-redux'; + +import { pushModal } from 'loot-core/client/actions'; + +import { useCategories } from '../../hooks/useCategories'; +import { useInitialMount } from '../../hooks/useInitialMount'; +import { styles } from '../../style'; +import { addToBeBudgetedGroup } from '../budget/util'; +import { Button } from '../common/Button'; +import { Modal } from '../common/Modal'; +import { View } from '../common/View'; +import { FieldLabel, TapField } from '../mobile/MobileForms'; +import { type CommonModalProps } from '../Modals'; + +type CoverModalProps = { + modalProps: CommonModalProps; + categoryId: string; + onSubmit: (categoryId: string) => void; +}; + +export function CoverModal({ + modalProps, + categoryId, + onSubmit, +}: CoverModalProps) { + const { grouped: originalCategoryGroups, list: categories } = useCategories(); + const categoryGroups = addToBeBudgetedGroup( + originalCategoryGroups.filter(g => !g.is_income), + ); + + const [fromCategoryId, setFromCategoryId] = useState(null); + const dispatch = useDispatch(); + + const onCategoryClick = useCallback(() => { + dispatch( + pushModal('category-autocomplete', { + categoryGroups, + onSelect: categoryId => { + setFromCategoryId(categoryId); + }, + }), + ); + }, [categoryGroups, dispatch]); + + const _onSubmit = (categoryId: string | null) => { + if (categoryId) { + onSubmit?.(categoryId); + } + + modalProps.onClose(); + }; + + const initialMount = useInitialMount(); + + useEffect(() => { + if (initialMount) { + onCategoryClick(); + } + }, [initialMount, onCategoryClick]); + + const category = categories.find(c => c.id === categoryId); + + if (!category) { + return null; + } + + return ( + + {() => ( + <> + + + c.id === fromCategoryId)?.name} + onClick={onCategoryClick} + /> + + + + + + + )} + + ); +} diff --git a/packages/desktop-client/src/components/modals/EditField.jsx b/packages/desktop-client/src/components/modals/EditField.jsx index 52f1bb79125..a4822faf32f 100644 --- a/packages/desktop-client/src/components/modals/EditField.jsx +++ b/packages/desktop-client/src/components/modals/EditField.jsx @@ -5,28 +5,10 @@ import { parseISO, format as formatDate, parse as parseDate } from 'date-fns'; import { currentDay, dayFromDate } from 'loot-core/src/shared/months'; import { amountToInteger } from 'loot-core/src/shared/util'; -import { useAccounts } from '../../hooks/useAccounts'; -import { useActions } from '../../hooks/useActions'; import { useCategories } from '../../hooks/useCategories'; import { useDateFormat } from '../../hooks/useDateFormat'; -import { usePayees } from '../../hooks/usePayees'; -import { SvgAdd } from '../../icons/v1'; import { useResponsive } from '../../ResponsiveProvider'; -import { styles, theme } from '../../style'; -import { - AccountAutocomplete, - AccountItem, -} from '../autocomplete/AccountAutocomplete'; -import { - CategoryAutocomplete, - CategoryItem, -} from '../autocomplete/CategoryAutocomplete'; -import { ItemHeader } from '../autocomplete/ItemHeader'; -import { - PayeeAutocomplete, - CreatePayeeButton, - PayeeItem, -} from '../autocomplete/PayeeAutocomplete'; +import { theme } from '../../style'; import { Button } from '../common/Button'; import { Input } from '../common/Input'; import { Modal } from '../common/Modal'; @@ -34,17 +16,13 @@ import { View } from '../common/View'; import { SectionLabel } from '../forms'; import { DateSelect } from '../select/DateSelect'; -function CreatePayeeIcon(props) { - return ; -} +import { AccountAutocompleteModal } from './AccountAutocompleteModal'; +import { CategoryAutocompleteModal } from './CategoryAutocompleteModal'; +import { PayeeAutocompleteModal } from './PayeeAutocompleteModal'; export function EditField({ modalProps, name, onSubmit, onClose }) { const dateFormat = useDateFormat() || 'MM/dd/yyyy'; const { grouped: categoryGroups } = useCategories(); - const accounts = useAccounts(); - const payees = usePayees(); - - const { createPayee } = useActions(); const onCloseInner = () => { modalProps.onClose(); onClose?.(); @@ -82,15 +60,11 @@ export function EditField({ modalProps, name, onSubmit, onClose }) { ':focus': { boxShadow: 0 }, ...(isNarrowWidth && itemStyle), }; - const autocompleteProps = { - inputProps: { style: inputStyle }, - containerProps: { style: { height: isNarrowWidth ? '90vh' : 275 } }, - }; const [noteAmend, onChangeMode] = useState('replace'); switch (name) { - case 'date': { + case 'date': const today = currentDay(); label = 'Date'; minWidth = 350; @@ -107,105 +81,50 @@ export function EditField({ modalProps, name, onSubmit, onClose }) { /> ); break; - } - case 'account': - label = 'Account'; - editor = ( - { - if (value) { - onSelect(value); - } + case 'category': + return ( + { + onSelect(categoryId); + }, }} - {...(isNarrowWidth && { - renderAccountItemGroupHeader: props => ( - - ), - renderAccountItem: props => ( - - ), - })} - {...autocompleteProps} + onClose={onClose} /> ); - break; case 'payee': - label = 'Payee'; - editor = ( - { - if (value && value.startsWith('new:')) { - value = await createPayee(value.slice('new:'.length)); - } + return ( + { + onSelect(payeeId); + }, + }} + onClose={onClose} + /> + ); - onSelect(value); + case 'account': + return ( + { + onSelect(accountId); + }, }} - {...(isNarrowWidth && { - renderCreatePayeeButton: props => ( - - ), - renderPayeeItemGroupHeader: props => ( - - ), - renderPayeeItem: props => ( - - ), - })} - {...autocompleteProps} + onClose={onClose} /> ); - break; case 'notes': label = 'Notes'; @@ -330,50 +249,6 @@ export function EditField({ modalProps, name, onSubmit, onClose }) { ); break; - case 'category': - label = 'Category'; - editor = ( - {}} - onSelect={value => { - onSelect(value); - }} - {...(isNarrowWidth && { - renderCategoryItemGroupHeader: props => ( - - ), - renderCategoryItem: props => ( - - ), - })} - {...autocompleteProps} - showHiddenItems={false} - /> - ); - break; - case 'amount': label = 'Amount'; editor = ( diff --git a/packages/desktop-client/src/components/modals/HoldBufferModal.tsx b/packages/desktop-client/src/components/modals/HoldBufferModal.tsx new file mode 100644 index 00000000000..74ea959aae5 --- /dev/null +++ b/packages/desktop-client/src/components/modals/HoldBufferModal.tsx @@ -0,0 +1,88 @@ +import React, { useState } from 'react'; + +import { rolloverBudget } from 'loot-core/client/queries'; +import { evalArithmetic } from 'loot-core/shared/arithmetic'; +import { amountToInteger, integerToCurrency } from 'loot-core/shared/util'; + +import { styles } from '../../style'; +import { Button } from '../common/Button'; +import { InitialFocus } from '../common/InitialFocus'; +import { Modal } from '../common/Modal'; +import { View } from '../common/View'; +import { FieldLabel, InputField } from '../mobile/MobileForms'; +import { type CommonModalProps } from '../Modals'; +import { useSheetValue } from '../spreadsheet/useSheetValue'; + +type HoldBufferModalProps = { + modalProps: CommonModalProps; + month: string; + onSubmit: (amount: number) => void; +}; + +export function HoldBufferModal({ + modalProps, + onSubmit, +}: HoldBufferModalProps) { + const available = useSheetValue(rolloverBudget.toBudget); + const initialAmount = integerToCurrency(Math.max(available, 0)); + const [amount, setAmount] = useState(null); + + const _onSubmit = (newAmount: string | null) => { + const parsedAmount = evalArithmetic(newAmount || ''); + if (parsedAmount) { + onSubmit?.(amountToInteger(parsedAmount)); + } + + modalProps.onClose(); + }; + + return ( + + {() => ( + <> + + + + setAmount(value)} + onEnter={() => _onSubmit(amount)} + /> + + + + + + + )} + + ); +} diff --git a/packages/desktop-client/src/components/modals/PayeeAutocompleteModal.tsx b/packages/desktop-client/src/components/modals/PayeeAutocompleteModal.tsx new file mode 100644 index 00000000000..6eaf1a5a58b --- /dev/null +++ b/packages/desktop-client/src/components/modals/PayeeAutocompleteModal.tsx @@ -0,0 +1,71 @@ +import React, { type ComponentPropsWithoutRef } from 'react'; + +import { useAccounts } from '../../hooks/useAccounts'; +import { usePayees } from '../../hooks/usePayees'; +import { useResponsive } from '../../ResponsiveProvider'; +import { theme } from '../../style'; +import { PayeeAutocomplete } from '../autocomplete/PayeeAutocomplete'; +import { Modal } from '../common/Modal'; +import { type CommonModalProps } from '../Modals'; + +type PayeeAutocompleteModalProps = { + modalProps: CommonModalProps; + autocompleteProps: ComponentPropsWithoutRef; + onClose: () => void; +}; + +export function PayeeAutocompleteModal({ + modalProps, + autocompleteProps, + onClose, +}: PayeeAutocompleteModalProps) { + const payees = usePayees() || []; + const accounts = useAccounts() || []; + + const _onClose = () => { + modalProps.onClose(); + onClose?.(); + }; + + const { isNarrowWidth } = useResponsive(); + const defaultAutocompleteProps = { + containerProps: { style: { height: isNarrowWidth ? '90vh' : 275 } }, + }; + + return ( + + {() => ( + + )} + + ); +} diff --git a/packages/desktop-client/src/components/modals/ReportBalanceMenuModal.tsx b/packages/desktop-client/src/components/modals/ReportBalanceMenuModal.tsx new file mode 100644 index 00000000000..6c4d5e6e1ea --- /dev/null +++ b/packages/desktop-client/src/components/modals/ReportBalanceMenuModal.tsx @@ -0,0 +1,49 @@ +import React, { type ComponentPropsWithoutRef } from 'react'; + +import { type CSSProperties, theme, styles } from '../../style'; +import { BalanceMenu } from '../budget/report/BalanceMenu'; +import { Modal } from '../common/Modal'; +import { type CommonModalProps } from '../Modals'; + +type ReportBalanceMenuModalProps = ComponentPropsWithoutRef< + typeof BalanceMenu +> & { + modalProps: CommonModalProps; +}; + +export function ReportBalanceMenuModal({ + modalProps, + categoryId, + onCarryover, +}: ReportBalanceMenuModalProps) { + const defaultMenuItemStyle: CSSProperties = { + ...styles.mobileMenuItem, + color: theme.menuItemText, + borderRadius: 0, + borderTop: `1px solid ${theme.pillBorder}`, + }; + + return ( + + {() => ( + defaultMenuItemStyle} + onCarryover={onCarryover} + /> + )} + + ); +} diff --git a/packages/desktop-client/src/components/modals/ReportBudgetSummary.tsx b/packages/desktop-client/src/components/modals/ReportBudgetSummaryModal.tsx similarity index 92% rename from packages/desktop-client/src/components/modals/ReportBudgetSummary.tsx rename to packages/desktop-client/src/components/modals/ReportBudgetSummaryModal.tsx index d16acc20541..a58338ce3b2 100644 --- a/packages/desktop-client/src/components/modals/ReportBudgetSummary.tsx +++ b/packages/desktop-client/src/components/modals/ReportBudgetSummaryModal.tsx @@ -12,15 +12,15 @@ import { Stack } from '../common/Stack'; import { type CommonModalProps } from '../Modals'; import { NamespaceContext } from '../spreadsheet/NamespaceContext'; -type ReportBudgetSummaryProps = { +type ReportBudgetSummaryModalProps = { modalProps: CommonModalProps; month: string; }; -export function ReportBudgetSummary({ +export function ReportBudgetSummaryModal({ month, modalProps, -}: ReportBudgetSummaryProps) { +}: ReportBudgetSummaryModalProps) { const currentMonth = monthUtils.currentMonth(); return ( diff --git a/packages/desktop-client/src/components/modals/RolloverBalanceMenuModal.tsx b/packages/desktop-client/src/components/modals/RolloverBalanceMenuModal.tsx new file mode 100644 index 00000000000..75c4159103f --- /dev/null +++ b/packages/desktop-client/src/components/modals/RolloverBalanceMenuModal.tsx @@ -0,0 +1,53 @@ +import React, { type ComponentPropsWithoutRef } from 'react'; + +import { type CSSProperties, theme, styles } from '../../style'; +import { BalanceMenu } from '../budget/rollover/BalanceMenu'; +import { Modal } from '../common/Modal'; +import { type CommonModalProps } from '../Modals'; + +type RolloverBalanceMenuModalProps = ComponentPropsWithoutRef< + typeof BalanceMenu +> & { + modalProps: CommonModalProps; +}; + +export function RolloverBalanceMenuModal({ + modalProps, + categoryId, + onCarryover, + onTransfer, + onCover, +}: RolloverBalanceMenuModalProps) { + const defaultMenuItemStyle: CSSProperties = { + ...styles.mobileMenuItem, + color: theme.menuItemText, + borderRadius: 0, + borderTop: `1px solid ${theme.pillBorder}`, + }; + + return ( + + {() => ( + defaultMenuItemStyle} + onCarryover={onCarryover} + onTransfer={onTransfer} + onCover={onCover} + /> + )} + + ); +} diff --git a/packages/desktop-client/src/components/modals/RolloverBudgetSummary.tsx b/packages/desktop-client/src/components/modals/RolloverBudgetSummary.tsx deleted file mode 100644 index 58a0e4260cd..00000000000 --- a/packages/desktop-client/src/components/modals/RolloverBudgetSummary.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import React from 'react'; - -import { format, sheetForMonth, prevMonth } from 'loot-core/src/shared/months'; - -import { styles } from '../../style'; -import { ToBudget } from '../budget/rollover/budgetsummary/ToBudget'; -import { TotalsList } from '../budget/rollover/budgetsummary/TotalsList'; -import { Modal } from '../common/Modal'; -import { type CommonModalProps } from '../Modals'; -import { NamespaceContext } from '../spreadsheet/NamespaceContext'; - -type RolloverBudgetSummaryProps = { - modalProps: CommonModalProps; - onBudgetAction: (idx: string | number, action: string, arg: unknown) => void; - month: string; -}; - -export function RolloverBudgetSummary({ - month, - onBudgetAction, - modalProps, -}: RolloverBudgetSummaryProps) { - const prevMonthName = format(prevMonth(month), 'MMM'); - - return ( - - {() => ( - - - - - )} - - ); -} diff --git a/packages/desktop-client/src/components/modals/RolloverBudgetSummaryModal.tsx b/packages/desktop-client/src/components/modals/RolloverBudgetSummaryModal.tsx new file mode 100644 index 00000000000..2839c3ee666 --- /dev/null +++ b/packages/desktop-client/src/components/modals/RolloverBudgetSummaryModal.tsx @@ -0,0 +1,104 @@ +import React from 'react'; +import { useDispatch } from 'react-redux'; + +import { collapseModals, pushModal } from 'loot-core/client/actions'; +import { rolloverBudget } from 'loot-core/client/queries'; +import { format, sheetForMonth, prevMonth } from 'loot-core/src/shared/months'; + +import { styles } from '../../style'; +import { ToBudgetAmount } from '../budget/rollover/budgetsummary/ToBudgetAmount'; +import { TotalsList } from '../budget/rollover/budgetsummary/TotalsList'; +import { Modal } from '../common/Modal'; +import { type CommonModalProps } from '../Modals'; +import { NamespaceContext } from '../spreadsheet/NamespaceContext'; +import { useSheetValue } from '../spreadsheet/useSheetValue'; + +type RolloverBudgetSummaryModalProps = { + modalProps: CommonModalProps; + onBudgetAction: (idx: string | number, action: string, arg?: unknown) => void; + month: string; +}; + +export function RolloverBudgetSummaryModal({ + month, + onBudgetAction, + modalProps, +}: RolloverBudgetSummaryModalProps) { + const dispatch = useDispatch(); + const prevMonthName = format(prevMonth(month), 'MMM'); + const sheetValue = useSheetValue({ + name: rolloverBudget.toBudget, + value: 0, + }); + + const openTransferModal = () => { + dispatch( + pushModal('transfer', { + title: 'Transfer', + amount: sheetValue, + onSubmit: (amount, toCategoryId) => { + onBudgetAction?.(month, 'transfer-available', { + amount, + month, + category: toCategoryId, + }); + dispatch(collapseModals('transfer')); + }, + }), + ); + }; + + const onHoldBuffer = () => { + dispatch( + pushModal('hold-buffer', { + month, + onSubmit: amount => { + onBudgetAction(month, 'hold', { amount }); + dispatch(collapseModals('hold-buffer')); + }, + }), + ); + }; + + const onResetHoldBuffer = () => { + onBudgetAction?.(month, 'reset-hold'); + modalProps.onClose(); + }; + + const onClick = () => { + dispatch( + pushModal('rollover-to-budget-menu', { + month, + onTransfer: openTransferModal, + onResetHoldBuffer, + onHoldBuffer, + }), + ); + }; + + return ( + + {() => ( + + + + + )} + + ); +} diff --git a/packages/desktop-client/src/components/modals/RolloverToBudgetMenuModal.tsx b/packages/desktop-client/src/components/modals/RolloverToBudgetMenuModal.tsx new file mode 100644 index 00000000000..da62d53d08f --- /dev/null +++ b/packages/desktop-client/src/components/modals/RolloverToBudgetMenuModal.tsx @@ -0,0 +1,51 @@ +import React, { type ComponentPropsWithoutRef } from 'react'; + +import { type CSSProperties, theme, styles } from '../../style'; +import { ToBudgetMenu } from '../budget/rollover/budgetsummary/ToBudgetMenu'; +import { Modal } from '../common/Modal'; +import { type CommonModalProps } from '../Modals'; + +type RolloverToBudgetMenuModalProps = ComponentPropsWithoutRef< + typeof ToBudgetMenu +> & { + modalProps: CommonModalProps; +}; + +export function RolloverToBudgetMenuModal({ + modalProps, + onTransfer, + onHoldBuffer, + onResetHoldBuffer, +}: RolloverToBudgetMenuModalProps) { + const defaultMenuItemStyle: CSSProperties = { + ...styles.mobileMenuItem, + color: theme.menuItemText, + borderRadius: 0, + borderTop: `1px solid ${theme.pillBorder}`, + }; + + return ( + + {() => ( + defaultMenuItemStyle} + onTransfer={onTransfer} + onHoldBuffer={onHoldBuffer} + onResetHoldBuffer={onResetHoldBuffer} + /> + )} + + ); +} diff --git a/packages/desktop-client/src/components/modals/ScheduledTransactionMenuModal.tsx b/packages/desktop-client/src/components/modals/ScheduledTransactionMenuModal.tsx new file mode 100644 index 00000000000..cfadec05dab --- /dev/null +++ b/packages/desktop-client/src/components/modals/ScheduledTransactionMenuModal.tsx @@ -0,0 +1,93 @@ +import React, { type ComponentPropsWithoutRef } from 'react'; + +import { type CSSProperties, theme, styles } from '../../style'; +import { Menu } from '../common/Menu'; +import { Modal } from '../common/Modal'; +import { type CommonModalProps } from '../Modals'; + +type ScheduledTransactionMenuModalProps = ScheduledTransactionMenuProps & { + modalProps: CommonModalProps; +}; + +export function ScheduledTransactionMenuModal({ + modalProps, + transactionId, + onSkip, + onPost, +}: ScheduledTransactionMenuModalProps) { + const defaultMenuItemStyle: CSSProperties = { + ...styles.mobileMenuItem, + color: theme.menuItemText, + borderRadius: 0, + borderTop: `1px solid ${theme.pillBorder}`, + }; + + return ( + + {() => ( + defaultMenuItemStyle} + /> + )} + + ); +} + +type ScheduledTransactionMenuProps = Omit< + ComponentPropsWithoutRef, + 'onMenuSelect' | 'items' +> & { + transactionId: string; + onSkip: (transactionId: string) => void; + onPost: (transactionId: string) => void; +}; + +function ScheduledTransactionMenu({ + transactionId, + onSkip, + onPost, + ...props +}: ScheduledTransactionMenuProps) { + return ( + { + switch (name) { + case 'post': + onPost?.(transactionId); + break; + case 'skip': + onSkip?.(transactionId); + break; + default: + throw new Error(`Unrecognized menu option: ${name}`); + } + }} + items={[ + { + name: 'post', + text: 'Post transaction', + }, + { + name: 'skip', + text: 'Skip scheduled date', + }, + ]} + /> + ); +} diff --git a/packages/desktop-client/src/components/modals/SingleInput.tsx b/packages/desktop-client/src/components/modals/SingleInputModal.tsx similarity index 51% rename from packages/desktop-client/src/components/modals/SingleInput.tsx rename to packages/desktop-client/src/components/modals/SingleInputModal.tsx index 5906142a400..c95d820fabe 100644 --- a/packages/desktop-client/src/components/modals/SingleInput.tsx +++ b/packages/desktop-client/src/components/modals/SingleInputModal.tsx @@ -5,12 +5,12 @@ import { styles } from '../../style'; import { Button } from '../common/Button'; import { FormError } from '../common/FormError'; import { InitialFocus } from '../common/InitialFocus'; -import { Input } from '../common/Input'; import { Modal } from '../common/Modal'; import { View } from '../common/View'; +import { InputField } from '../mobile/MobileForms'; import { type CommonModalProps } from '../Modals'; -type SingleInputProps = { +type SingleInputModalProps = { modalProps: Partial; title: string; buttonText: string; @@ -19,14 +19,14 @@ type SingleInputProps = { inputPlaceholder?: string; }; -export function SingleInput({ +export function SingleInputModal({ modalProps, title, buttonText, onSubmit, onValidate, inputPlaceholder, -}: SingleInputProps) { +}: SingleInputModalProps) { const [value, setValue] = useState(''); const [errorMessage, setErrorMessage] = useState(null); @@ -40,49 +40,57 @@ export function SingleInput({ onSubmit?.(value); modalProps.onClose(); }; + return ( - + {() => ( <> - - - - _onSubmit(e.currentTarget.value)} - /> - - {errorMessage && ( - - * {errorMessage} - - )} - + + + _onSubmit(e.currentTarget.value)} + /> + + {errorMessage && ( + + * {errorMessage} + + )} - diff --git a/packages/desktop-client/src/components/modals/TransferModal.tsx b/packages/desktop-client/src/components/modals/TransferModal.tsx new file mode 100644 index 00000000000..7def9a00471 --- /dev/null +++ b/packages/desktop-client/src/components/modals/TransferModal.tsx @@ -0,0 +1,129 @@ +import React, { useState } from 'react'; +import { useDispatch } from 'react-redux'; + +import { pushModal } from 'loot-core/client/actions'; +import { evalArithmetic } from 'loot-core/shared/arithmetic'; +import { amountToInteger, integerToCurrency } from 'loot-core/shared/util'; + +import { useCategories } from '../../hooks/useCategories'; +import { styles } from '../../style'; +import { addToBeBudgetedGroup } from '../budget/util'; +import { Button } from '../common/Button'; +import { InitialFocus } from '../common/InitialFocus'; +import { Modal } from '../common/Modal'; +import { View } from '../common/View'; +import { FieldLabel, InputField, TapField } from '../mobile/MobileForms'; +import { type CommonModalProps } from '../Modals'; + +type TransferModalProps = { + modalProps: CommonModalProps; + title: string; + amount: number; + showToBeBudgeted: boolean; + onSubmit: (amount: number, toCategoryId: string) => void; +}; + +export function TransferModal({ + modalProps, + title, + amount: initialAmount, + showToBeBudgeted, + onSubmit, +}: TransferModalProps) { + const { grouped: originalCategoryGroups, list: categories } = useCategories(); + let categoryGroups = originalCategoryGroups.filter(g => !g.is_income); + if (showToBeBudgeted) { + categoryGroups = addToBeBudgetedGroup(categoryGroups); + } + + const _initialAmount = integerToCurrency(Math.max(initialAmount, 0)); + const [amount, setAmount] = useState(null); + const [toCategoryId, setToCategoryId] = useState(null); + const dispatch = useDispatch(); + + const openCategoryModal = () => { + dispatch( + pushModal('category-autocomplete', { + categoryGroups, + showHiddenCategories: true, + onSelect: categoryId => { + setToCategoryId(categoryId); + }, + }), + ); + }; + + const _onSubmit = (newAmount: string | null, categoryId: string | null) => { + const parsedAmount = evalArithmetic(newAmount || ''); + if (parsedAmount && categoryId) { + onSubmit?.(amountToInteger(parsedAmount), categoryId); + } + + modalProps.onClose(); + }; + + return ( + + {() => ( + <> + + + + { + if (!toCategoryId) { + openCategoryModal(); + } + }} + /> + + + + + c.id === toCategoryId)?.name} + onClick={openCategoryModal} + onFocus={openCategoryModal} + /> + + + + + + )} + + ); +} diff --git a/packages/desktop-client/src/hooks/useInitialMount.ts b/packages/desktop-client/src/hooks/useInitialMount.ts new file mode 100644 index 00000000000..6d6b2d990e9 --- /dev/null +++ b/packages/desktop-client/src/hooks/useInitialMount.ts @@ -0,0 +1,12 @@ +import { useRef } from 'react'; + +export function useInitialMount(): boolean { + const initial = useRef(true); + + if (initial.current) { + initial.current = false; + return true; + } + + return false; +} 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 c4d54d17d93..f780f445e7f 100644 --- a/packages/loot-core/src/client/state-types/modals.d.ts +++ b/packages/loot-core/src/client/state-types/modals.d.ts @@ -103,6 +103,24 @@ type FinanceModals = { onClose: () => void; }; + 'category-autocomplete': { + categoryGroups: CategoryGroupEntity[]; + onSelect: (categoryId: string, categoryName: string) => void; + showHiddenCategories?: boolean; + onClose?: () => void; + }; + + 'account-autocomplete': { + onSelect: (accountId: string, accountName: string) => void; + includeClosedAccounts?: boolean; + onClose?: () => void; + }; + + 'payee-autocomplete': { + onSelect: (payeeId: string) => void; + onClose?: () => void; + }; + 'budget-summary': { month: string; }; @@ -115,6 +133,14 @@ type FinanceModals = { 'schedule-posts-offline-notification': null; 'switch-budget-type': { onSwitch: () => void }; + 'account-menu': { + accountId: string; + onSave: (account: AccountEntity) => void; + onCloseAccount: (accountId: string) => void; + onReopenAccount: (accountId: string) => void; + onEditNotes: (id: string) => void; + onClose?: () => void; + }; 'category-menu': { categoryId: string; onSave: (category: CategoryEntity) => void; @@ -152,6 +178,43 @@ type FinanceModals = { onValidate?: (value: string) => string; onSubmit: (value: string) => Promise; }; + 'rollover-balance-menu': { + categoryId: string; + month: string; + onCarryover: (carryover: boolean) => void; + onTransfer: () => void; + onCover: () => void; + }; + 'rollover-to-budget-menu': { + month: string; + onTransfer: () => void; + onHoldBuffer: () => void; + onResetHoldBuffer: () => void; + }; + 'report-balance-menu': { + categoryId: string; + month: string; + onCarryover: (carryover: boolean) => void; + }; + transfer: { + title: string; + amount: number; + onSubmit: (amount: number, toCategoryId: string) => void; + showToBeBudgeted?: boolean; + }; + cover: { + categoryId: string; + onSubmit: (fromCategoryId: string) => void; + }; + 'hold-buffer': { + month: string; + onSubmit: (amount: number) => void; + }; + 'scheduled-transaction-menu': { + transactionId: string; + onPost: (transactionId: string) => void; + onSkip: (transactionId: string) => void; + }; }; export type PushModalAction = { diff --git a/upcoming-release-notes/2472.md b/upcoming-release-notes/2472.md new file mode 100644 index 00000000000..dcbd90e59d6 --- /dev/null +++ b/upcoming-release-notes/2472.md @@ -0,0 +1,6 @@ +--- +category: Enhancements +authors: [joel-jeremy] +--- + +Add more modals in mobile for account, scheduled transactions, budget summary, and balance actions.