From b101f9989b6c75838879be1e37f33947d03c91b2 Mon Sep 17 00:00:00 2001 From: Joel Jeremy Marquez <joeljeremy.marquez@gmail.com> Date: Tue, 16 Apr 2024 12:14:20 -0700 Subject: [PATCH] Display balances in category autocomplete (#2551) * Display balances in category autocomplete * Release notes * Fix typecheck error * Update balance colors * Show category balances in mobile * Patch unit tests * Darket midnight theme autocomplete hover color * Category autocomplete split transaction highlight * Update 2551.md * Extract modals from EditField * Fix failing tests * Update variable names --------- Co-authored-by: Matiss Janis Aboltins <matiss@mja.lv> --- .../desktop-client/src/components/Modals.tsx | 4 +- .../src/components/accounts/Account.jsx | 82 +++++++++++++------ .../autocomplete/CategoryAutocomplete.tsx | 64 ++++++++++++--- .../src/components/budget/util.ts | 18 +++- .../components/mobile/budget/BudgetTable.jsx | 2 + .../mobile/transactions/TransactionEdit.jsx | 5 ++ .../modals/CategoryAutocompleteModal.tsx | 27 ++++-- .../src/components/modals/CoverModal.tsx | 5 +- .../src/components/modals/EditField.jsx | 50 ----------- .../modals/RolloverBudgetSummaryModal.tsx | 1 + .../src/components/modals/TransferModal.tsx | 3 + .../transactions/TransactionsTable.jsx | 34 +++++--- .../transactions/TransactionsTable.test.jsx | 53 ++++++------ .../desktop-client/src/style/themes/dark.ts | 2 +- .../src/style/themes/midnight.ts | 8 +- .../src/client/state-types/modals.d.ts | 4 + upcoming-release-notes/2551.md | 6 ++ 17 files changed, 229 insertions(+), 139 deletions(-) create mode 100644 upcoming-release-notes/2551.md diff --git a/packages/desktop-client/src/components/Modals.tsx b/packages/desktop-client/src/components/Modals.tsx index 39926082a..689896761 100644 --- a/packages/desktop-client/src/components/Modals.tsx +++ b/packages/desktop-client/src/components/Modals.tsx @@ -300,10 +300,10 @@ export function Modals() { modalProps={modalProps} autocompleteProps={{ value: null, - categoryGroups: options.categoryGroups, onSelect: options.onSelect, showHiddenCategories: options.showHiddenCategories, }} + month={options.month} onClose={options.onClose} /> ); @@ -587,6 +587,7 @@ export function Modals() { key={name} modalProps={modalProps} title={options.title} + month={options.month} amount={options.amount} onSubmit={options.onSubmit} showToBeBudgeted={options.showToBeBudgeted} @@ -599,6 +600,7 @@ export function Modals() { key={name} modalProps={modalProps} categoryId={options.categoryId} + month={options.month} onSubmit={options.onSubmit} /> ); diff --git a/packages/desktop-client/src/components/accounts/Account.jsx b/packages/desktop-client/src/components/accounts/Account.jsx index 25bef7453..5a4025793 100644 --- a/packages/desktop-client/src/components/accounts/Account.jsx +++ b/packages/desktop-client/src/components/accounts/Account.jsx @@ -13,6 +13,7 @@ import * as queries from 'loot-core/src/client/queries'; import { runQuery, pagedQuery } from 'loot-core/src/client/query-helpers'; import { send, listen } from 'loot-core/src/platform/client/fetch'; import { currentDay } from 'loot-core/src/shared/months'; +import * as monthUtils from 'loot-core/src/shared/months'; import { q } from 'loot-core/src/shared/query'; import { getScheduledAmount } from 'loot-core/src/shared/schedules'; import { @@ -801,29 +802,31 @@ class AccountInternal extends PureComponent { }; onBatchEdit = async (name, ids) => { + const { data } = await runQuery( + q('transactions') + .filter({ id: { $oneof: ids } }) + .select('*') + .options({ splits: 'grouped' }), + ); + const transactions = ungroupTransactions(data); + const onChange = async (name, value, mode) => { + let transactionsToChange = transactions; + const newValue = value === null ? '' : value; this.setState({ workingHard: true }); - const { data } = await runQuery( - q('transactions') - .filter({ id: { $oneof: ids } }) - .select('*') - .options({ splits: 'grouped' }), - ); - let transactions = ungroupTransactions(data); - const changes = { deleted: [], updated: [] }; // Cleared is a special case right now if (name === 'cleared') { // Clear them if any are uncleared, otherwise unclear them - value = !!transactions.find(t => !t.cleared); + value = !!transactionsToChange.find(t => !t.cleared); } const idSet = new Set(ids); - transactions.forEach(trans => { + transactionsToChange.forEach(trans => { if (name === 'cleared' && trans.reconciled) { // Skip transactions that are reconciled. Don't want to set them as // uncleared. @@ -856,13 +859,13 @@ class AccountInternal extends PureComponent { transaction.reconciled = false; } - const { diff } = updateTransaction(transactions, transaction); + const { diff } = updateTransaction(transactionsToChange, transaction); // TODO: We need to keep an updated list of transactions so // the logic in `updateTransaction`, particularly about // updating split transactions, works. This isn't ideal and we // should figure something else out - transactions = applyChanges(diff, transactions); + transactionsToChange = applyChanges(diff, transactionsToChange); changes.deleted = changes.deleted ? changes.deleted.concat(diff.deleted) @@ -879,28 +882,55 @@ class AccountInternal extends PureComponent { await this.refetchTransactions(); if (this.table.current) { - this.table.current.edit(transactions[0].id, 'select', false); + this.table.current.edit(transactionsToChange[0].id, 'select', false); } }; + const pushPayeeAutocompleteModal = () => { + this.props.pushModal('payee-autocomplete', { + onSelect: payeeId => onChange(name, payeeId), + }); + }; + + const pushAccountAutocompleteModal = () => { + this.props.pushModal('account-autocomplete', { + onSelect: accountId => onChange(name, accountId), + }); + }; + + const pushCategoryAutocompleteModal = () => { + // Only show balances when all selected transaction are in the same month. + const transactionMonth = transactions[0]?.date + ? monthUtils.monthFromDate(transactions[0]?.date) + : null; + const transactionsHaveSameMonth = + transactionMonth && + transactions.every( + t => monthUtils.monthFromDate(t.date) === transactionMonth, + ); + this.props.pushModal('category-autocomplete', { + month: transactionsHaveSameMonth ? transactionMonth : undefined, + onSelect: categoryId => onChange(name, categoryId), + }); + }; + if ( name === 'amount' || name === 'payee' || name === 'account' || name === 'date' ) { - const { data } = await runQuery( - q('transactions') - .filter({ id: { $oneof: ids }, reconciled: true }) - .select('*') - .options({ splits: 'grouped' }), - ); - const transactions = ungroupTransactions(data); - - if (transactions.length > 0) { + const reconciledTransactions = transactions.filter(t => t.reconciled); + if (reconciledTransactions.length > 0) { this.props.pushModal('confirm-transaction-edit', { onConfirm: () => { - this.props.pushModal('edit-field', { name, onSubmit: onChange }); + if (name === 'payee') { + pushPayeeAutocompleteModal(); + } else if (name === 'account') { + pushAccountAutocompleteModal(); + } else { + this.props.pushModal('edit-field', { name, onSubmit: onChange }); + } }, confirmReason: 'batchEditWithReconciled', }); @@ -912,6 +942,12 @@ class AccountInternal extends PureComponent { // Cleared just toggles it on/off and it depends on the data // loaded. Need to clean this up in the future. onChange('cleared', null); + } else if (name === 'category') { + pushCategoryAutocompleteModal(); + } else if (name === 'payee') { + pushPayeeAutocompleteModal(); + } else if (name === 'account') { + pushAccountAutocompleteModal(); } else { this.props.pushModal('edit-field', { name, onSubmit: onChange }); } diff --git a/packages/desktop-client/src/components/autocomplete/CategoryAutocomplete.tsx b/packages/desktop-client/src/components/autocomplete/CategoryAutocomplete.tsx index 419467e2a..a742bcbfc 100644 --- a/packages/desktop-client/src/components/autocomplete/CategoryAutocomplete.tsx +++ b/packages/desktop-client/src/components/autocomplete/CategoryAutocomplete.tsx @@ -11,17 +11,23 @@ import React, { import { css } from 'glamor'; +import { reportBudget, rolloverBudget } from 'loot-core/client/queries'; +import { integerToCurrency } from 'loot-core/shared/util'; import { type CategoryEntity, type CategoryGroupEntity, } from 'loot-core/src/types/models'; +import { useCategories } from '../../hooks/useCategories'; +import { useLocalPref } from '../../hooks/useLocalPref'; import { SvgSplit } from '../../icons/v0'; import { useResponsive } from '../../ResponsiveProvider'; import { type CSSProperties, theme, styles } from '../../style'; +import { makeAmountFullStyle } from '../budget/util'; import { Text } from '../common/Text'; import { TextOneLine } from '../common/TextOneLine'; import { View } from '../common/View'; +import { useSheetValue } from '../spreadsheet/useSheetValue'; import { Autocomplete, defaultFilterSuggestion } from './Autocomplete'; import { ItemHeader } from './ItemHeader'; @@ -48,6 +54,7 @@ export type CategoryListProps = { props: ComponentPropsWithoutRef<typeof CategoryItem>, ) => ReactElement<typeof CategoryItem>; showHiddenItems?: boolean; + showBalances?: boolean; }; function CategoryList({ items, @@ -59,6 +66,7 @@ function CategoryList({ renderCategoryItemGroupHeader = defaultRenderCategoryItemGroupHeader, renderCategoryItem = defaultRenderCategoryItem, showHiddenItems, + showBalances, }: CategoryListProps) { let lastGroup: string | undefined | null = null; @@ -111,6 +119,7 @@ function CategoryList({ ...(showHiddenItems && item.hidden && { color: theme.pageTextSubdued }), }, + showBalances, })} </Fragment> </Fragment> @@ -125,7 +134,8 @@ function CategoryList({ type CategoryAutocompleteProps = ComponentProps< typeof Autocomplete<CategoryAutocompleteItem> > & { - categoryGroups: Array<CategoryGroupEntity>; + categoryGroups?: Array<CategoryGroupEntity>; + showBalances?: boolean; showSplitOption?: boolean; renderSplitTransactionButton?: ( props: ComponentPropsWithoutRef<typeof SplitTransactionButton>, @@ -141,6 +151,7 @@ type CategoryAutocompleteProps = ComponentProps< export function CategoryAutocomplete({ categoryGroups, + showBalances = true, showSplitOption, embedded, closeOnBlur, @@ -150,9 +161,10 @@ export function CategoryAutocomplete({ showHiddenCategories, ...props }: CategoryAutocompleteProps) { + const { grouped: defaultCategoryGroups = [] } = useCategories(); const categorySuggestions: CategoryAutocompleteItem[] = useMemo( () => - categoryGroups.reduce( + (categoryGroups || defaultCategoryGroups).reduce( (list, group) => list.concat( (group.categories || []) @@ -164,7 +176,7 @@ export function CategoryAutocomplete({ ), showSplitOption ? [{ id: 'split', name: '' } as CategoryEntity] : [], ), - [showSplitOption, categoryGroups], + [defaultCategoryGroups, categoryGroups, showSplitOption], ); return ( @@ -200,6 +212,7 @@ export function CategoryAutocomplete({ renderCategoryItemGroupHeader={renderCategoryItemGroupHeader} renderCategoryItem={renderCategoryItem} showHiddenItems={showHiddenCategories} + showBalances={showBalances} /> )} {...props} @@ -261,9 +274,7 @@ function SplitTransactionButton({ alignItems: 'center', fontSize: 11, fontWeight: 500, - color: highlighted - ? theme.menuAutoCompleteTextHover - : theme.noticeTextMenu, + color: theme.noticeTextMenu, padding: '6px 8px', ':active': { backgroundColor: 'rgba(100, 100, 100, .25)', @@ -297,6 +308,7 @@ type CategoryItemProps = { style?: CSSProperties; highlighted?: boolean; embedded?: boolean; + showBalances?: boolean; }; function CategoryItem({ @@ -305,6 +317,7 @@ function CategoryItem({ style, highlighted, embedded, + showBalances, ...props }: CategoryItemProps) { const { isNarrowWidth } = useResponsive(); @@ -315,6 +328,16 @@ function CategoryItem({ borderTop: `1px solid ${theme.pillBorder}`, } : {}; + const [budgetType] = useLocalPref('budgetType'); + + const balance = useSheetValue( + budgetType === 'rollover' + ? rolloverBudget.catBalance(item.id) + : reportBudget.catBalance(item.id), + ); + + const isToBeBudgetedItem = item.id === 'to-be-budgeted'; + const toBudget = useSheetValue(rolloverBudget.toBudget); return ( <div @@ -339,10 +362,31 @@ function CategoryItem({ data-highlighted={highlighted || undefined} {...props} > - <TextOneLine> - {item.name} - {item.hidden ? ' (hidden)' : null} - </TextOneLine> + <View style={{ flexDirection: 'row', justifyContent: 'space-between' }}> + <TextOneLine> + {item.name} + {item.hidden ? ' (hidden)' : null} + </TextOneLine> + <TextOneLine + style={{ + display: !showBalances ? 'none' : undefined, + marginLeft: 5, + flexShrink: 0, + ...makeAmountFullStyle(isToBeBudgetedItem ? toBudget : balance, { + positiveColor: theme.noticeTextMenu, + negativeColor: theme.errorTextMenu, + }), + }} + > + {isToBeBudgetedItem + ? toBudget != null + ? ` ${integerToCurrency(toBudget || 0)}` + : null + : balance != null + ? ` ${integerToCurrency(balance || 0)}` + : null} + </TextOneLine> + </View> </div> ); } diff --git a/packages/desktop-client/src/components/budget/util.ts b/packages/desktop-client/src/components/budget/util.ts index 3c5bc8fe1..733012c54 100644 --- a/packages/desktop-client/src/components/budget/util.ts +++ b/packages/desktop-client/src/components/budget/util.ts @@ -67,14 +67,24 @@ export function makeAmountStyle( } } -export function makeAmountFullStyle(value: number) { +export function makeAmountFullStyle( + value: number, + colors?: { + positiveColor?: string; + negativeColor?: string; + zeroColor?: string; + }, +) { + const positiveColorToUse = colors.positiveColor || theme.noticeText; + const negativeColorToUse = colors.negativeColor || theme.errorText; + const zeroColorToUse = colors.zeroColor || theme.tableTextSubdued; return { color: value < 0 - ? theme.errorText + ? negativeColorToUse : value === 0 - ? theme.tableTextSubdued - : theme.noticeText, + ? zeroColorToUse + : positiveColorToUse, }; } diff --git a/packages/desktop-client/src/components/mobile/budget/BudgetTable.jsx b/packages/desktop-client/src/components/mobile/budget/BudgetTable.jsx index c7fcdd68f..c3db205c3 100644 --- a/packages/desktop-client/src/components/mobile/budget/BudgetTable.jsx +++ b/packages/desktop-client/src/components/mobile/budget/BudgetTable.jsx @@ -263,6 +263,7 @@ const ExpenseCategory = memo(function ExpenseCategory({ dispatch( pushModal('transfer', { title: `Transfer: ${category.name}`, + month, amount: catBalance, onSubmit: (amount, toCategoryId) => { onBudgetAction(month, 'transfer-category', { @@ -281,6 +282,7 @@ const ExpenseCategory = memo(function ExpenseCategory({ dispatch( pushModal('cover', { categoryId: category.id, + month, onSubmit: fromCategoryId => { onBudgetAction(month, 'cover', { to: category.id, diff --git a/packages/desktop-client/src/components/mobile/transactions/TransactionEdit.jsx b/packages/desktop-client/src/components/mobile/transactions/TransactionEdit.jsx index 02864a9b5..a48918513 100644 --- a/packages/desktop-client/src/components/mobile/transactions/TransactionEdit.jsx +++ b/packages/desktop-client/src/components/mobile/transactions/TransactionEdit.jsx @@ -545,11 +545,15 @@ const TransactionEditInner = memo(function TransactionEditInner({ const onClick = (transactionId, name) => { onRequestActiveEdit?.(getFieldName(transaction.id, name), () => { const transactionToEdit = transactions.find(t => t.id === transactionId); + const unserializedTransaction = unserializedTransactions.find( + t => t.id === transactionId, + ); switch (name) { case 'category': dispatch( pushModal('category-autocomplete', { categoryGroups, + month: monthUtils.monthFromDate(unserializedTransaction.date), onSelect: categoryId => { onEdit(transactionToEdit, name, categoryId); }, @@ -587,6 +591,7 @@ const TransactionEditInner = memo(function TransactionEditInner({ dispatch( pushModal('edit-field', { name, + month: monthUtils.monthFromDate(unserializedTransaction.date), onSubmit: (name, value) => { onEdit(transactionToEdit, name, value); }, diff --git a/packages/desktop-client/src/components/modals/CategoryAutocompleteModal.tsx b/packages/desktop-client/src/components/modals/CategoryAutocompleteModal.tsx index 971269655..b561d27ca 100644 --- a/packages/desktop-client/src/components/modals/CategoryAutocompleteModal.tsx +++ b/packages/desktop-client/src/components/modals/CategoryAutocompleteModal.tsx @@ -1,5 +1,7 @@ import React, { type ComponentPropsWithoutRef } from 'react'; +import * as monthUtils from 'loot-core/src/shared/months'; + import { useResponsive } from '../../ResponsiveProvider'; import { theme } from '../../style'; import { CategoryAutocomplete } from '../autocomplete/CategoryAutocomplete'; @@ -7,16 +9,19 @@ import { ModalCloseButton, Modal, ModalTitle } from '../common/Modal'; import { View } from '../common/View'; import { SectionLabel } from '../forms'; import { type CommonModalProps } from '../Modals'; +import { NamespaceContext } from '../spreadsheet/NamespaceContext'; type CategoryAutocompleteModalProps = { modalProps: CommonModalProps; autocompleteProps: ComponentPropsWithoutRef<typeof CategoryAutocomplete>; onClose: () => void; + month?: string; }; export function CategoryAutocompleteModal({ modalProps, autocompleteProps, + month, onClose, }: CategoryAutocompleteModalProps) { const _onClose = () => { @@ -71,15 +76,19 @@ export function CategoryAutocompleteModal({ /> )} <View style={{ flex: 1 }}> - <CategoryAutocomplete - focused={true} - embedded={true} - closeOnBlur={false} - showSplitOption={false} - onClose={_onClose} - {...defaultAutocompleteProps} - {...autocompleteProps} - /> + <NamespaceContext.Provider + value={month ? monthUtils.sheetForMonth(month) : ''} + > + <CategoryAutocomplete + focused={true} + embedded={true} + closeOnBlur={false} + showSplitOption={false} + onClose={_onClose} + {...defaultAutocompleteProps} + {...autocompleteProps} + /> + </NamespaceContext.Provider> </View> </View> )} diff --git a/packages/desktop-client/src/components/modals/CoverModal.tsx b/packages/desktop-client/src/components/modals/CoverModal.tsx index 90f4369da..e11baeb6e 100644 --- a/packages/desktop-client/src/components/modals/CoverModal.tsx +++ b/packages/desktop-client/src/components/modals/CoverModal.tsx @@ -16,12 +16,14 @@ import { type CommonModalProps } from '../Modals'; type CoverModalProps = { modalProps: CommonModalProps; categoryId: string; + month: string; onSubmit: (categoryId: string) => void; }; export function CoverModal({ modalProps, categoryId, + month, onSubmit, }: CoverModalProps) { const { grouped: originalCategoryGroups, list: categories } = useCategories(); @@ -36,12 +38,13 @@ export function CoverModal({ dispatch( pushModal('category-autocomplete', { categoryGroups, + month, onSelect: categoryId => { setFromCategoryId(categoryId); }, }), ); - }, [categoryGroups, dispatch]); + }, [categoryGroups, dispatch, month]); const _onSubmit = (categoryId: string | null) => { if (categoryId) { diff --git a/packages/desktop-client/src/components/modals/EditField.jsx b/packages/desktop-client/src/components/modals/EditField.jsx index 4d0b06465..bdcabb487 100644 --- a/packages/desktop-client/src/components/modals/EditField.jsx +++ b/packages/desktop-client/src/components/modals/EditField.jsx @@ -5,7 +5,6 @@ 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 { useCategories } from '../../hooks/useCategories'; import { useDateFormat } from '../../hooks/useDateFormat'; import { useResponsive } from '../../ResponsiveProvider'; import { theme } from '../../style'; @@ -16,13 +15,8 @@ import { View } from '../common/View'; import { SectionLabel } from '../forms'; import { DateSelect } from '../select/DateSelect'; -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 onCloseInner = () => { modalProps.onClose(); onClose?.(); @@ -82,50 +76,6 @@ export function EditField({ modalProps, name, onSubmit, onClose }) { ); break; - case 'category': - return ( - <CategoryAutocompleteModal - modalProps={modalProps} - autocompleteProps={{ - categoryGroups, - showHiddenCategories: false, - value: null, - onSelect: categoryId => { - onSelect(categoryId); - }, - }} - onClose={onClose} - /> - ); - - case 'payee': - return ( - <PayeeAutocompleteModal - modalProps={modalProps} - autocompleteProps={{ - value: null, - onSelect: payeeId => { - onSelect(payeeId); - }, - }} - onClose={onClose} - /> - ); - - case 'account': - return ( - <AccountAutocompleteModal - modalProps={modalProps} - autocompleteProps={{ - value: null, - onSelect: accountId => { - onSelect(accountId); - }, - }} - onClose={onClose} - /> - ); - case 'notes': label = 'Notes'; editor = ( diff --git a/packages/desktop-client/src/components/modals/RolloverBudgetSummaryModal.tsx b/packages/desktop-client/src/components/modals/RolloverBudgetSummaryModal.tsx index f0d55b7ab..cd2f12c90 100644 --- a/packages/desktop-client/src/components/modals/RolloverBudgetSummaryModal.tsx +++ b/packages/desktop-client/src/components/modals/RolloverBudgetSummaryModal.tsx @@ -35,6 +35,7 @@ export function RolloverBudgetSummaryModal({ dispatch( pushModal('transfer', { title: 'Transfer', + month, amount: sheetValue, onSubmit: (amount, toCategoryId) => { onBudgetAction?.(month, 'transfer-available', { diff --git a/packages/desktop-client/src/components/modals/TransferModal.tsx b/packages/desktop-client/src/components/modals/TransferModal.tsx index 7def9a004..0e5577f46 100644 --- a/packages/desktop-client/src/components/modals/TransferModal.tsx +++ b/packages/desktop-client/src/components/modals/TransferModal.tsx @@ -18,6 +18,7 @@ import { type CommonModalProps } from '../Modals'; type TransferModalProps = { modalProps: CommonModalProps; title: string; + month: string; amount: number; showToBeBudgeted: boolean; onSubmit: (amount: number, toCategoryId: string) => void; @@ -26,6 +27,7 @@ type TransferModalProps = { export function TransferModal({ modalProps, title, + month, amount: initialAmount, showToBeBudgeted, onSubmit, @@ -45,6 +47,7 @@ export function TransferModal({ dispatch( pushModal('category-autocomplete', { categoryGroups, + month, showHiddenCategories: true, onSelect: categoryId => { setToCategoryId(categoryId); diff --git a/packages/desktop-client/src/components/transactions/TransactionsTable.jsx b/packages/desktop-client/src/components/transactions/TransactionsTable.jsx index d05689f40..e02e39cf3 100644 --- a/packages/desktop-client/src/components/transactions/TransactionsTable.jsx +++ b/packages/desktop-client/src/components/transactions/TransactionsTable.jsx @@ -25,6 +25,7 @@ import { } from 'loot-core/src/client/reducers/queries'; import { evalArithmetic } from 'loot-core/src/shared/arithmetic'; import { currentDay } from 'loot-core/src/shared/months'; +import * as monthUtils from 'loot-core/src/shared/months'; import { getScheduledAmount } from 'loot-core/src/shared/schedules'; import { splitTransaction, @@ -62,6 +63,7 @@ import { Text } from '../common/Text'; import { View } from '../common/View'; import { getStatusProps } from '../schedules/StatusBadge'; import { DateSelect } from '../select/DateSelect'; +import { NamespaceContext } from '../spreadsheet/NamespaceContext'; import { Cell, Field, @@ -1171,19 +1173,25 @@ const Transaction = memo(function Transaction(props) { shouldSaveFromKey, inputStyle, }) => ( - <CategoryAutocomplete - categoryGroups={categoryGroups} - value={categoryId} - focused={true} - clearOnBlur={false} - showSplitOption={!isChild && !isParent} - shouldSaveFromKey={shouldSaveFromKey} - inputProps={{ onBlur, onKeyDown, style: inputStyle }} - onUpdate={onUpdate} - onSelect={onSave} - menuPortalTarget={undefined} - showHiddenCategories={false} - /> + <NamespaceContext.Provider + value={monthUtils.sheetForMonth( + monthUtils.monthFromDate(transaction.date), + )} + > + <CategoryAutocomplete + categoryGroups={categoryGroups} + value={categoryId} + focused={true} + clearOnBlur={false} + showSplitOption={!isChild && !isParent} + shouldSaveFromKey={shouldSaveFromKey} + inputProps={{ onBlur, onKeyDown, style: inputStyle }} + onUpdate={onUpdate} + onSelect={onSave} + menuPortalTarget={undefined} + showHiddenCategories={false} + /> + </NamespaceContext.Provider> )} </CustomCell> )} diff --git a/packages/desktop-client/src/components/transactions/TransactionsTable.test.jsx b/packages/desktop-client/src/components/transactions/TransactionsTable.test.jsx index 3b03b5b4f..d06dbd2fd 100644 --- a/packages/desktop-client/src/components/transactions/TransactionsTable.test.jsx +++ b/packages/desktop-client/src/components/transactions/TransactionsTable.test.jsx @@ -5,6 +5,7 @@ import userEvent from '@testing-library/user-event'; import { format as formatDate, parse as parseDate } from 'date-fns'; import { v4 as uuidv4 } from 'uuid'; +import { SpreadsheetProvider } from 'loot-core/src/client/SpreadsheetProvider'; import { generateTransaction, generateAccount, @@ -118,26 +119,28 @@ function LiveTransactionTable(props) { return ( <TestProvider> <ResponsiveProvider> - <SelectedProviderWithItems - name="transactions" - items={transactions} - fetchAllIds={() => transactions.map(t => t.id)} - > - <SplitsExpandedProvider> - <TransactionTable - {...props} - transactions={transactions} - loadMoreTransactions={() => {}} - payees={payees} - addNotification={n => console.log(n)} - onSave={onSave} - onSplit={onSplit} - onAdd={onAdd} - onAddSplit={onAddSplit} - onCreatePayee={onCreatePayee} - /> - </SplitsExpandedProvider> - </SelectedProviderWithItems> + <SpreadsheetProvider> + <SelectedProviderWithItems + name="transactions" + items={transactions} + fetchAllIds={() => transactions.map(t => t.id)} + > + <SplitsExpandedProvider> + <TransactionTable + {...props} + transactions={transactions} + loadMoreTransactions={() => {}} + payees={payees} + addNotification={n => console.log(n)} + onSave={onSave} + onSplit={onSplit} + onAdd={onAdd} + onAddSplit={onAddSplit} + onCreatePayee={onCreatePayee} + /> + </SplitsExpandedProvider> + </SelectedProviderWithItems> + </SpreadsheetProvider> </ResponsiveProvider> </TestProvider> ); @@ -155,6 +158,10 @@ function initBasicServer() { throw new Error(`queried unknown table: ${query.table}`); } }, + getCell: () => ({ + value: 129_87, + }), + 'get-categories': () => ({ grouped: categoryGroups, list: categories }), }); } @@ -447,7 +454,7 @@ describe('Transactions', () => { let items = tooltip.querySelectorAll('[data-testid*="category-item"]'); expect(items.length).toBe(2); expect(items[0].textContent).toBe('Usual Expenses'); - expect(items[1].textContent).toBe('General'); + expect(items[1].textContent).toBe('General 129.87'); expect(items[1].dataset['highlighted']).toBeDefined(); // It should not allow filtering on group names @@ -473,7 +480,7 @@ describe('Transactions', () => { // The right item should be highlighted highlighted = tooltip.querySelector('[data-highlighted]'); expect(highlighted).not.toBeNull(); - expect(highlighted.textContent).toBe('General'); + expect(highlighted.textContent).toBe('General 129.87'); expect(getTransactions()[2].category).toBe( categories.find(category => category.name === 'Food').id, @@ -515,7 +522,7 @@ describe('Transactions', () => { // Make sure the expected category is highlighted highlighted = tooltip.querySelector('[data-highlighted]'); expect(highlighted).not.toBeNull(); - expect(highlighted.textContent).toBe('General'); + expect(highlighted.textContent).toBe('General 129.87'); // Click the item and check the before/after values expect(getTransactions()[2].category).toBe( diff --git a/packages/desktop-client/src/style/themes/dark.ts b/packages/desktop-client/src/style/themes/dark.ts index 81b756b8d..185b042e4 100644 --- a/packages/desktop-client/src/style/themes/dark.ts +++ b/packages/desktop-client/src/style/themes/dark.ts @@ -149,7 +149,7 @@ export const errorBackground = colorPalette.red800; export const errorText = colorPalette.red200; export const errorTextDark = colorPalette.red150; export const errorTextDarker = errorTextDark; -export const errorTextMenu = colorPalette.red500; +export const errorTextMenu = colorPalette.red200; export const errorBorder = colorPalette.red500; export const upcomingBackground = colorPalette.purple700; export const upcomingText = colorPalette.purple100; diff --git a/packages/desktop-client/src/style/themes/midnight.ts b/packages/desktop-client/src/style/themes/midnight.ts index cb6a41e72..135194af8 100644 --- a/packages/desktop-client/src/style/themes/midnight.ts +++ b/packages/desktop-client/src/style/themes/midnight.ts @@ -50,7 +50,7 @@ export const sidebarItemTextSelected = colorPalette.purple200; export const menuBackground = colorPalette.gray700; export const menuItemBackground = colorPalette.gray200; export const menuItemBackgroundHover = colorPalette.gray500; -export const menuItemText = colorPalette.gray100; /* controls all dropdowns */ +export const menuItemText = colorPalette.gray100; export const menuItemTextHover = colorPalette.gray50; export const menuItemTextSelected = colorPalette.purple400; export const menuItemTextHeader = colorPalette.purple200; @@ -58,11 +58,11 @@ export const menuBorder = colorPalette.gray800; export const menuBorderHover = colorPalette.purple300; export const menuKeybindingText = colorPalette.gray500; export const menuAutoCompleteBackground = colorPalette.gray600; -export const menuAutoCompleteBackgroundHover = colorPalette.gray50; +export const menuAutoCompleteBackgroundHover = colorPalette.gray500; export const menuAutoCompleteText = colorPalette.gray100; export const menuAutoCompleteTextHover = colorPalette.green900; export const menuAutoCompleteTextHeader = colorPalette.purple200; -export const menuAutoCompleteItemTextHover = colorPalette.gray700; +export const menuAutoCompleteItemTextHover = colorPalette.gray50; export const menuAutoCompleteItemText = menuItemText; export const modalBackground = colorPalette.gray700; export const modalBorder = colorPalette.gray200; @@ -151,7 +151,7 @@ export const errorBackground = colorPalette.red800; export const errorText = colorPalette.red200; export const errorTextDark = colorPalette.red150; export const errorTextDarker = errorTextDark; -export const errorTextMenu = colorPalette.red500; +export const errorTextMenu = colorPalette.red200; export const errorBorder = colorPalette.red500; export const upcomingBackground = colorPalette.purple800; export const upcomingText = colorPalette.purple200; 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 96c3b65cc..88583c8fc 100644 --- a/packages/loot-core/src/client/state-types/modals.d.ts +++ b/packages/loot-core/src/client/state-types/modals.d.ts @@ -99,6 +99,7 @@ type FinanceModals = { 'edit-field': { name: string; + month: string; onSubmit: (name: string, value: string) => void; onClose: () => void; }; @@ -106,6 +107,7 @@ type FinanceModals = { 'category-autocomplete': { categoryGroups: CategoryGroupEntity[]; onSelect: (categoryId: string, categoryName: string) => void; + month?: string; showHiddenCategories?: boolean; onClose?: () => void; }; @@ -215,12 +217,14 @@ type FinanceModals = { }; transfer: { title: string; + month: string; amount: number; onSubmit: (amount: number, toCategoryId: string) => void; showToBeBudgeted?: boolean; }; cover: { categoryId: string; + month: string; onSubmit: (fromCategoryId: string) => void; }; 'hold-buffer': { diff --git a/upcoming-release-notes/2551.md b/upcoming-release-notes/2551.md new file mode 100644 index 000000000..7044cc016 --- /dev/null +++ b/upcoming-release-notes/2551.md @@ -0,0 +1,6 @@ +--- +category: Features +authors: [joel-jeremy,MatissJanis] +--- + +Display category balances in category autocomplete. -- GitLab