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) && (
+
+
-
+
-
-
-
-
- )}
+ />
+
+
+ )}
+
+
+
+
+
+
+ {!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,
+ }),
+ })}
+ />
+
+
+
+
+ Edit notes
+
+
+
+ )}
+
+ );
+}
+
+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 (
+
+ {
+ setMenuOpen(true);
+ }}
+ >
+
+ {menuOpen && (
+ {
+ setMenuOpen(false);
+ }}
+ >
+
+ )}
+
+
+ );
+}
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={