mirror of
https://github.com/actualbudget/actual.git
synced 2026-03-09 03:32:54 -05:00
Compare commits
10 Commits
PayeeAutoc
...
dm-fix-sec
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d1cf1042af | ||
|
|
f0f42044eb | ||
|
|
fb9924aced | ||
|
|
da068173e4 | ||
|
|
eb84ddaff2 | ||
|
|
e970a2d11f | ||
|
|
8438845e92 | ||
|
|
027aa00d0b | ||
|
|
a978b686dc | ||
|
|
58d752e94c |
@@ -165,7 +165,7 @@ const AmountInput = memo(function AmountInput({
|
||||
type FocusableAmountInputProps = Omit<AmountInputProps, 'onFocus'> & {
|
||||
sign?: '+' | '-';
|
||||
zeroSign?: '+' | '-';
|
||||
focused?: boolean;
|
||||
defaultFocused?: boolean;
|
||||
disabled?: boolean;
|
||||
focusedStyle?: CSSProperties;
|
||||
buttonProps?: ComponentPropsWithRef<typeof Button>;
|
||||
@@ -176,17 +176,20 @@ export const FocusableAmountInput = memo(function FocusableAmountInput({
|
||||
value,
|
||||
sign,
|
||||
zeroSign,
|
||||
focused,
|
||||
defaultFocused,
|
||||
disabled,
|
||||
textStyle,
|
||||
style,
|
||||
focusedStyle,
|
||||
buttonProps,
|
||||
onFocus,
|
||||
onBlur,
|
||||
...props
|
||||
}: FocusableAmountInputProps) {
|
||||
const [isNegative, setIsNegative] = useState(true);
|
||||
const [focused, setFocused] = useState(defaultFocused ?? false);
|
||||
|
||||
useEffect(() => {
|
||||
setFocused(defaultFocused ?? false);
|
||||
}, [defaultFocused]);
|
||||
|
||||
const maybeApplyNegative = (amount: number, negative: boolean) => {
|
||||
const absValue = Math.abs(amount);
|
||||
@@ -219,8 +222,8 @@ export const FocusableAmountInput = memo(function FocusableAmountInput({
|
||||
<AmountInput
|
||||
{...props}
|
||||
value={value}
|
||||
onFocus={onFocus}
|
||||
onBlur={onBlur}
|
||||
onFocus={() => setFocused(true)}
|
||||
onBlur={() => setFocused(false)}
|
||||
onUpdateAmount={amount => onUpdateAmount(amount, isNegative)}
|
||||
focused={focused && !disabled}
|
||||
style={{
|
||||
@@ -252,7 +255,7 @@ export const FocusableAmountInput = memo(function FocusableAmountInput({
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
onPress={onFocus}
|
||||
onPress={() => setFocused(true)}
|
||||
// Defines how far touch can start away from the button
|
||||
// hitSlop={{ top: 5, bottom: 5, left: 5, right: 5 }}
|
||||
{...buttonProps}
|
||||
|
||||
@@ -68,22 +68,13 @@ import { AmountInput } from '@desktop-client/components/util/AmountInput';
|
||||
import { useAccounts } from '@desktop-client/hooks/useAccounts';
|
||||
import { useCategories } from '@desktop-client/hooks/useCategories';
|
||||
import { useDateFormat } from '@desktop-client/hooks/useDateFormat';
|
||||
import { useInitialMount } from '@desktop-client/hooks/useInitialMount';
|
||||
import { useNavigate } from '@desktop-client/hooks/useNavigate';
|
||||
import { usePayees } from '@desktop-client/hooks/usePayees';
|
||||
import {
|
||||
SingleActiveEditFormProvider,
|
||||
useSingleActiveEditForm,
|
||||
} from '@desktop-client/hooks/useSingleActiveEditForm';
|
||||
import { pushModal } from '@desktop-client/modals/modalsSlice';
|
||||
import { aqlQuery } from '@desktop-client/queries/aqlQuery';
|
||||
import { setLastTransaction } from '@desktop-client/queries/queriesSlice';
|
||||
import { useSelector, useDispatch } from '@desktop-client/redux';
|
||||
|
||||
function getFieldName(transactionId, field) {
|
||||
return `${field}-${transactionId}`;
|
||||
}
|
||||
|
||||
function serializeTransaction(transaction, dateFormat) {
|
||||
const { date, amount } = transaction;
|
||||
return {
|
||||
@@ -174,7 +165,6 @@ function Footer({
|
||||
onSplit,
|
||||
onAddSplit,
|
||||
onEmptySplitFound,
|
||||
editingField,
|
||||
onEditField,
|
||||
}) {
|
||||
const [transaction, ...childTransactions] = transactions;
|
||||
@@ -208,7 +198,6 @@ function Footer({
|
||||
<Button
|
||||
variant="primary"
|
||||
style={{ height: styles.mobileMinHeight }}
|
||||
isDisabled={editingField}
|
||||
onPress={onClickRemainingSplit}
|
||||
>
|
||||
<SvgSplit width={17} height={17} />
|
||||
@@ -248,7 +237,6 @@ function Footer({
|
||||
<Button
|
||||
variant="primary"
|
||||
style={{ height: styles.mobileMinHeight }}
|
||||
isDisabled={editingField}
|
||||
onPress={() => onEditField(transaction.id, 'account')}
|
||||
>
|
||||
<SvgPiggyBank width={17} height={17} />
|
||||
@@ -265,7 +253,6 @@ function Footer({
|
||||
<Button
|
||||
variant="primary"
|
||||
style={{ height: styles.mobileMinHeight }}
|
||||
isDisabled={editingField}
|
||||
onPress={onAdd}
|
||||
>
|
||||
<SvgAdd width={17} height={17} />
|
||||
@@ -282,7 +269,6 @@ function Footer({
|
||||
<Button
|
||||
variant="primary"
|
||||
style={{ height: styles.mobileMinHeight }}
|
||||
isDisabled={editingField}
|
||||
onPress={onSave}
|
||||
>
|
||||
<SvgPencilWriteAlternate width={16} height={16} />
|
||||
@@ -318,8 +304,6 @@ const ChildTransactionEdit = forwardRef(
|
||||
ref,
|
||||
) => {
|
||||
const { t } = useTranslation();
|
||||
const { editingField, onRequestActiveEdit, onClearActiveEdit } =
|
||||
useSingleActiveEditForm();
|
||||
const prettyPayee = getPrettyPayee({
|
||||
transaction,
|
||||
payee: getPayee(transaction),
|
||||
@@ -344,10 +328,6 @@ const ChildTransactionEdit = forwardRef(
|
||||
<View style={{ flexBasis: '75%' }}>
|
||||
<FieldLabel title={t('Payee')} />
|
||||
<TapField
|
||||
isDisabled={
|
||||
editingField &&
|
||||
editingField !== getFieldName(transaction.id, 'payee')
|
||||
}
|
||||
value={prettyPayee}
|
||||
onPress={() => onEditField(transaction.id, 'payee')}
|
||||
data-testid={`payee-field-${transaction.id}`}
|
||||
@@ -360,10 +340,6 @@ const ChildTransactionEdit = forwardRef(
|
||||
>
|
||||
<FieldLabel title={t('Amount')} style={{ padding: 0 }} />
|
||||
<AmountInput
|
||||
disabled={
|
||||
editingField &&
|
||||
editingField !== getFieldName(transaction.id, 'amount')
|
||||
}
|
||||
focused={amountFocused}
|
||||
value={amountToInteger(transaction.amount)}
|
||||
zeroSign={amountSign}
|
||||
@@ -373,15 +349,10 @@ const ChildTransactionEdit = forwardRef(
|
||||
textAlign: 'right',
|
||||
minWidth: 0,
|
||||
}}
|
||||
onFocus={() =>
|
||||
onRequestActiveEdit(getFieldName(transaction.id, 'amount'))
|
||||
}
|
||||
onUpdate={value => {
|
||||
const amount = integerToAmount(value);
|
||||
if (transaction.amount !== amount) {
|
||||
onUpdate(transaction, 'amount', amount);
|
||||
} else {
|
||||
onClearActiveEdit();
|
||||
}
|
||||
}}
|
||||
autoDecimals={true}
|
||||
@@ -400,12 +371,7 @@ const ChildTransactionEdit = forwardRef(
|
||||
}),
|
||||
}}
|
||||
value={getCategory(transaction, isOffBudget)}
|
||||
isDisabled={
|
||||
(editingField &&
|
||||
editingField !== getFieldName(transaction.id, 'category')) ||
|
||||
isOffBudget ||
|
||||
isBudgetTransfer(transaction)
|
||||
}
|
||||
isDisabled={isOffBudget || isBudgetTransfer(transaction)}
|
||||
onPress={() => onEditField(transaction.id, 'category')}
|
||||
data-testid={`category-field-${transaction.id}`}
|
||||
/>
|
||||
@@ -414,14 +380,7 @@ const ChildTransactionEdit = forwardRef(
|
||||
<View>
|
||||
<FieldLabel title={t('Notes')} />
|
||||
<InputField
|
||||
disabled={
|
||||
editingField &&
|
||||
editingField !== getFieldName(transaction.id, 'notes')
|
||||
}
|
||||
defaultValue={transaction.notes}
|
||||
onFocus={() =>
|
||||
onRequestActiveEdit(getFieldName(transaction.id, 'notes'))
|
||||
}
|
||||
onUpdate={value => onUpdate(transaction, 'notes', value)}
|
||||
/>
|
||||
</View>
|
||||
@@ -495,34 +454,12 @@ const TransactionEditInner = memo(function TransactionEditInner({
|
||||
|
||||
const [transaction, ...childTransactions] = transactions;
|
||||
|
||||
const { editingField, onRequestActiveEdit, onClearActiveEdit } =
|
||||
useSingleActiveEditForm();
|
||||
const [totalAmountFocused, setTotalAmountFocused] = useState(
|
||||
// iOS does not support automatically opening up the keyboard for the
|
||||
// total amount field. Hence we should not focus on it on page render.
|
||||
!Platform.isIOSAgent,
|
||||
);
|
||||
const childTransactionElementRefMap = useRef({});
|
||||
const hasAccountChanged = useRef(false);
|
||||
|
||||
const payeesById = useMemo(() => groupById(payees), [payees]);
|
||||
const accountsById = useMemo(() => groupById(accounts), [accounts]);
|
||||
|
||||
const onTotalAmountEdit = useCallback(() => {
|
||||
onRequestActiveEdit?.(getFieldName(transaction.id, 'amount'), () => {
|
||||
setTotalAmountFocused(true);
|
||||
return () => setTotalAmountFocused(false);
|
||||
});
|
||||
}, [onRequestActiveEdit, transaction.id]);
|
||||
|
||||
const isInitialMount = useInitialMount();
|
||||
|
||||
useEffect(() => {
|
||||
if (isInitialMount && isAdding && !Platform.isIOSAgent) {
|
||||
onTotalAmountEdit();
|
||||
}
|
||||
}, [isAdding, isInitialMount, onTotalAmountEdit]);
|
||||
|
||||
const getAccount = useCallback(
|
||||
trans => {
|
||||
return trans?.account && accountsById?.[trans.account];
|
||||
@@ -604,13 +541,12 @@ const TransactionEditInner = memo(function TransactionEditInner({
|
||||
async (serializedTransaction, name, value) => {
|
||||
const newTransaction = { ...serializedTransaction, [name]: value };
|
||||
await onUpdate(newTransaction, name);
|
||||
onClearActiveEdit();
|
||||
|
||||
if (name === 'account') {
|
||||
hasAccountChanged.current = serializedTransaction.account !== value;
|
||||
}
|
||||
},
|
||||
[onClearActiveEdit, onUpdate],
|
||||
[onUpdate],
|
||||
);
|
||||
|
||||
const onTotalAmountUpdate = useCallback(
|
||||
@@ -622,103 +558,98 @@ const TransactionEditInner = memo(function TransactionEditInner({
|
||||
[onUpdateInner, transaction],
|
||||
);
|
||||
|
||||
const onEditFieldInner = useCallback(
|
||||
const transactionsRef = useRef(transactions);
|
||||
useEffect(() => {
|
||||
transactionsRef.current = transactions;
|
||||
}, [transactions]);
|
||||
|
||||
// getTransaction prevents stale-closure issue with dialogs
|
||||
const getTransaction = useCallback(
|
||||
transactionId => transactionsRef.current.find(t => t.id === transactionId),
|
||||
[],
|
||||
);
|
||||
|
||||
const onEditField = useCallback(
|
||||
(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({
|
||||
modal: {
|
||||
name: 'category-autocomplete',
|
||||
options: {
|
||||
categoryGroups,
|
||||
month: monthUtils.monthFromDate(
|
||||
unserializedTransaction.date,
|
||||
),
|
||||
onSelect: categoryId => {
|
||||
onUpdateInner(transactionToEdit, name, categoryId);
|
||||
},
|
||||
onClose: () => {
|
||||
onClearActiveEdit();
|
||||
},
|
||||
const unserializedTransaction = unserializedTransactions.find(
|
||||
t => t.id === transactionId,
|
||||
);
|
||||
switch (name) {
|
||||
case 'category':
|
||||
dispatch(
|
||||
pushModal({
|
||||
modal: {
|
||||
name: 'category-autocomplete',
|
||||
options: {
|
||||
categoryGroups,
|
||||
month: monthUtils.monthFromDate(unserializedTransaction.date),
|
||||
onSelect: categoryId => {
|
||||
onUpdateInner(
|
||||
getTransaction(transactionId),
|
||||
name,
|
||||
categoryId,
|
||||
);
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
break;
|
||||
case 'account':
|
||||
dispatch(
|
||||
pushModal({
|
||||
modal: {
|
||||
name: 'account-autocomplete',
|
||||
options: {
|
||||
onSelect: accountId => {
|
||||
onUpdateInner(transactionToEdit, name, accountId);
|
||||
},
|
||||
onClose: () => {
|
||||
onClearActiveEdit();
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
break;
|
||||
case 'account':
|
||||
dispatch(
|
||||
pushModal({
|
||||
modal: {
|
||||
name: 'account-autocomplete',
|
||||
options: {
|
||||
onSelect: accountId => {
|
||||
onUpdateInner(
|
||||
getTransaction(transactionId),
|
||||
name,
|
||||
accountId,
|
||||
);
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
break;
|
||||
case 'payee':
|
||||
dispatch(
|
||||
pushModal({
|
||||
modal: {
|
||||
name: 'payee-autocomplete',
|
||||
options: {
|
||||
onSelect: payeeId => {
|
||||
onUpdateInner(transactionToEdit, name, payeeId);
|
||||
},
|
||||
onClose: () => {
|
||||
onClearActiveEdit();
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
break;
|
||||
case 'payee':
|
||||
dispatch(
|
||||
pushModal({
|
||||
modal: {
|
||||
name: 'payee-autocomplete',
|
||||
options: {
|
||||
onSelect: payeeId => {
|
||||
onUpdateInner(getTransaction(transactionId), name, payeeId);
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
break;
|
||||
default:
|
||||
dispatch(
|
||||
pushModal({
|
||||
modal: {
|
||||
name: 'edit-field',
|
||||
options: {
|
||||
name,
|
||||
month: monthUtils.monthFromDate(
|
||||
unserializedTransaction.date,
|
||||
),
|
||||
onSubmit: (name, value) => {
|
||||
onUpdateInner(transactionToEdit, name, value);
|
||||
},
|
||||
onClose: () => {
|
||||
onClearActiveEdit();
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
break;
|
||||
default:
|
||||
dispatch(
|
||||
pushModal({
|
||||
modal: {
|
||||
name: 'edit-field',
|
||||
options: {
|
||||
name,
|
||||
month: monthUtils.monthFromDate(unserializedTransaction.date),
|
||||
onSubmit: (name, value) => {
|
||||
onUpdateInner(getTransaction(transactionId), name, value);
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
break;
|
||||
}
|
||||
});
|
||||
},
|
||||
}),
|
||||
);
|
||||
break;
|
||||
}
|
||||
},
|
||||
[
|
||||
categoryGroups,
|
||||
dispatch,
|
||||
getTransaction,
|
||||
onUpdateInner,
|
||||
onClearActiveEdit,
|
||||
onRequestActiveEdit,
|
||||
transaction.id,
|
||||
transactions,
|
||||
unserializedTransactions,
|
||||
],
|
||||
);
|
||||
@@ -738,7 +669,6 @@ const TransactionEditInner = memo(function TransactionEditInner({
|
||||
|
||||
if (unserializedTransaction.id !== id) {
|
||||
// Only a child transaction was deleted.
|
||||
onClearActiveEdit();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -766,7 +696,7 @@ const TransactionEditInner = memo(function TransactionEditInner({
|
||||
onConfirmDelete();
|
||||
}
|
||||
},
|
||||
[dispatch, navigate, onClearActiveEdit, onDelete, unserializedTransactions],
|
||||
[dispatch, navigate, onDelete, unserializedTransactions],
|
||||
);
|
||||
|
||||
const scrollChildTransactionIntoView = useCallback(id => {
|
||||
@@ -835,8 +765,7 @@ const TransactionEditInner = memo(function TransactionEditInner({
|
||||
onSplit={onSplit}
|
||||
onAddSplit={onAddSplit}
|
||||
onEmptySplitFound={onEmptySplitFound}
|
||||
editingField={editingField}
|
||||
onEditField={onEditFieldInner}
|
||||
onEditField={onEditField}
|
||||
/>
|
||||
}
|
||||
padding={0}
|
||||
@@ -854,9 +783,7 @@ const TransactionEditInner = memo(function TransactionEditInner({
|
||||
<FocusableAmountInput
|
||||
value={transaction.amount}
|
||||
zeroSign="-"
|
||||
focused={totalAmountFocused}
|
||||
onFocus={onTotalAmountEdit}
|
||||
onBlur={onClearActiveEdit}
|
||||
defaultFocused={!Platform.isIOSAgent}
|
||||
onUpdateAmount={onTotalAmountUpdate}
|
||||
focusedStyle={{
|
||||
width: 'auto',
|
||||
@@ -878,12 +805,12 @@ const TransactionEditInner = memo(function TransactionEditInner({
|
||||
fontWeight: 300,
|
||||
}),
|
||||
}}
|
||||
value={title}
|
||||
isDisabled={
|
||||
editingField &&
|
||||
editingField !== getFieldName(transaction.id, 'payee')
|
||||
}
|
||||
onPress={() => onEditFieldInner(transaction.id, 'payee')}
|
||||
value={getPrettyPayee({
|
||||
transaction,
|
||||
payee: getPayee(transaction),
|
||||
transferAccount: getTransferAccount(transaction),
|
||||
})}
|
||||
onPress={() => onEditField(transaction.id, 'payee')}
|
||||
data-testid="payee-field"
|
||||
/>
|
||||
</View>
|
||||
@@ -900,13 +827,8 @@ const TransactionEditInner = memo(function TransactionEditInner({
|
||||
}),
|
||||
}}
|
||||
value={getCategory(transaction, isOffBudget)}
|
||||
isDisabled={
|
||||
(editingField &&
|
||||
editingField !== getFieldName(transaction.id, 'category')) ||
|
||||
isOffBudget ||
|
||||
isBudgetTransfer(transaction)
|
||||
}
|
||||
onPress={() => onEditFieldInner(transaction.id, 'category')}
|
||||
isDisabled={isOffBudget || isBudgetTransfer(transaction)}
|
||||
onPress={() => onEditField(transaction.id, 'category')}
|
||||
data-testid="category-field"
|
||||
/>
|
||||
</View>
|
||||
@@ -930,7 +852,7 @@ const TransactionEditInner = memo(function TransactionEditInner({
|
||||
getTransferAccount={getTransferAccount}
|
||||
isBudgetTransfer={isBudgetTransfer}
|
||||
onUpdate={onUpdateInner}
|
||||
onEditField={onEditFieldInner}
|
||||
onEditField={onEditField}
|
||||
onDelete={onDeleteInner}
|
||||
/>
|
||||
))}
|
||||
@@ -939,7 +861,6 @@ const TransactionEditInner = memo(function TransactionEditInner({
|
||||
<View style={{ alignItems: 'center' }}>
|
||||
<Button
|
||||
variant="bare"
|
||||
isDisabled={editingField}
|
||||
style={{
|
||||
height: 40,
|
||||
borderWidth: 0,
|
||||
@@ -971,12 +892,8 @@ const TransactionEditInner = memo(function TransactionEditInner({
|
||||
<View>
|
||||
<FieldLabel title={t('Account')} />
|
||||
<TapField
|
||||
isDisabled={
|
||||
editingField &&
|
||||
editingField !== getFieldName(transaction.id, 'account')
|
||||
}
|
||||
value={account?.name}
|
||||
onPress={() => onEditFieldInner(transaction.id, 'account')}
|
||||
onPress={() => onEditField(transaction.id, 'account')}
|
||||
data-testid="account-field"
|
||||
/>
|
||||
</View>
|
||||
@@ -986,21 +903,14 @@ const TransactionEditInner = memo(function TransactionEditInner({
|
||||
<FieldLabel title={t('Date')} />
|
||||
<InputField
|
||||
type="date"
|
||||
disabled={
|
||||
editingField &&
|
||||
editingField !== getFieldName(transaction.id, 'date')
|
||||
}
|
||||
required
|
||||
style={{ color: theme.tableText, minWidth: '150px' }}
|
||||
defaultValue={dateDefaultValue}
|
||||
onFocus={() =>
|
||||
onRequestActiveEdit(getFieldName(transaction.id, 'date'))
|
||||
}
|
||||
onChange={event =>
|
||||
onUpdateInner(
|
||||
onUpdate={value =>
|
||||
onUpdate(
|
||||
transaction,
|
||||
'date',
|
||||
formatDate(parseISO(event.target.value), dateFormat),
|
||||
formatDate(parseISO(value), dateFormat),
|
||||
)
|
||||
}
|
||||
/>
|
||||
@@ -1025,17 +935,8 @@ const TransactionEditInner = memo(function TransactionEditInner({
|
||||
<View>
|
||||
<FieldLabel title={t('Notes')} />
|
||||
<InputField
|
||||
disabled={
|
||||
editingField &&
|
||||
editingField !== getFieldName(transaction.id, 'notes')
|
||||
}
|
||||
defaultValue={transaction.notes}
|
||||
onFocus={() => {
|
||||
onRequestActiveEdit(getFieldName(transaction.id, 'notes'));
|
||||
}}
|
||||
onChange={event =>
|
||||
onUpdateInner(transaction, 'notes', event.target.value)
|
||||
}
|
||||
onUpdate={value => onUpdateInner(transaction, 'notes', value)}
|
||||
/>
|
||||
</View>
|
||||
|
||||
@@ -1331,15 +1232,13 @@ export const TransactionEdit = props => {
|
||||
const dateFormat = useDateFormat() || 'MM/dd/yyyy';
|
||||
|
||||
return (
|
||||
<SingleActiveEditFormProvider formName="mobile-transaction">
|
||||
<TransactionEditUnconnected
|
||||
{...props}
|
||||
categories={categories}
|
||||
payees={payees}
|
||||
lastTransaction={lastTransaction}
|
||||
accounts={accounts}
|
||||
dateFormat={dateFormat}
|
||||
/>
|
||||
</SingleActiveEditFormProvider>
|
||||
<TransactionEditUnconnected
|
||||
{...props}
|
||||
categories={categories}
|
||||
payees={payees}
|
||||
lastTransaction={lastTransaction}
|
||||
accounts={accounts}
|
||||
dateFormat={dateFormat}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState, useEffect, type CSSProperties } from 'react';
|
||||
import React, { type CSSProperties } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { styles } from '@actual-app/components/styles';
|
||||
@@ -46,20 +46,11 @@ export function EnvelopeBudgetMenuModal({
|
||||
envelopeBudget.catBudgeted(categoryId),
|
||||
);
|
||||
const category = useCategory(categoryId);
|
||||
const [amountFocused, setAmountFocused] = useState(false);
|
||||
|
||||
const _onUpdateBudget = (amount: number) => {
|
||||
onUpdateBudget?.(amountToInteger(amount));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// iOS does not support automatically opening up the keyboard for the
|
||||
// total amount field. Hence we should not focus on it on page render.
|
||||
if (!Platform.isIOSAgent) {
|
||||
setAmountFocused(true);
|
||||
}
|
||||
}, []);
|
||||
|
||||
if (!category) {
|
||||
return null;
|
||||
}
|
||||
@@ -89,11 +80,9 @@ export function EnvelopeBudgetMenuModal({
|
||||
</Text>
|
||||
<FocusableAmountInput
|
||||
value={integerToAmount(budgeted || 0)}
|
||||
focused={amountFocused}
|
||||
onFocus={() => setAmountFocused(true)}
|
||||
onBlur={() => setAmountFocused(false)}
|
||||
onEnter={close}
|
||||
zeroSign="+"
|
||||
defaultFocused={!Platform.isIOSAgent}
|
||||
focusedStyle={{
|
||||
width: 'auto',
|
||||
padding: '5px',
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState, useEffect, type CSSProperties } from 'react';
|
||||
import React, { type CSSProperties } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { styles } from '@actual-app/components/styles';
|
||||
@@ -46,20 +46,11 @@ export function TrackingBudgetMenuModal({
|
||||
trackingBudget.catBudgeted(categoryId),
|
||||
);
|
||||
const category = useCategory(categoryId);
|
||||
const [amountFocused, setAmountFocused] = useState(false);
|
||||
|
||||
const _onUpdateBudget = (amount: number) => {
|
||||
onUpdateBudget?.(amountToInteger(amount));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// iOS does not support automatically opening up the keyboard for the
|
||||
// total amount field. Hence we should not focus on it on page render.
|
||||
if (!Platform.isIOSAgent) {
|
||||
setAmountFocused(true);
|
||||
}
|
||||
}, []);
|
||||
|
||||
if (!category) {
|
||||
return null;
|
||||
}
|
||||
@@ -89,11 +80,9 @@ export function TrackingBudgetMenuModal({
|
||||
</Text>
|
||||
<FocusableAmountInput
|
||||
value={integerToAmount(budgeted || 0)}
|
||||
focused={amountFocused}
|
||||
onFocus={() => setAmountFocused(true)}
|
||||
onBlur={() => setAmountFocused(false)}
|
||||
onEnter={close}
|
||||
zeroSign="+"
|
||||
defaultFocused={!Platform.isIOSAgent}
|
||||
focusedStyle={{
|
||||
width: 'auto',
|
||||
padding: '5px',
|
||||
|
||||
@@ -1,119 +0,0 @@
|
||||
// @ts-strict-ignore
|
||||
import React, {
|
||||
type ReactNode,
|
||||
createContext,
|
||||
useContext,
|
||||
useState,
|
||||
useRef,
|
||||
} from 'react';
|
||||
|
||||
type ActiveEditCleanup = () => void;
|
||||
type ActiveEditAction = () => void | ActiveEditCleanup;
|
||||
|
||||
type SingleActiveEditFormContextValue = {
|
||||
formName: string;
|
||||
editingField: string;
|
||||
onRequestActiveEdit: (
|
||||
field: string,
|
||||
action?: ActiveEditAction,
|
||||
options?: {
|
||||
clearActiveEditDelayMs?: number;
|
||||
},
|
||||
) => void;
|
||||
onClearActiveEdit: (delayMs?: number) => void;
|
||||
};
|
||||
|
||||
const SingleActiveEditFormContext = createContext<
|
||||
SingleActiveEditFormContextValue | undefined
|
||||
>(undefined);
|
||||
|
||||
type SingleActiveEditFormProviderProps = {
|
||||
formName: string;
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
export function SingleActiveEditFormProvider({
|
||||
formName,
|
||||
children,
|
||||
}: SingleActiveEditFormProviderProps) {
|
||||
const [editingField, setEditingField] = useState(null);
|
||||
const cleanupRef = useRef<ActiveEditCleanup | void>(null);
|
||||
|
||||
const runCleanup = () => {
|
||||
const editCleanup = cleanupRef.current;
|
||||
if (typeof editCleanup === 'function') {
|
||||
editCleanup?.();
|
||||
}
|
||||
cleanupRef.current = null;
|
||||
};
|
||||
|
||||
const runAction = (action: ActiveEditAction) => {
|
||||
cleanupRef.current = action?.();
|
||||
};
|
||||
|
||||
const onClearActiveEdit = (delayMs?: number) => {
|
||||
setTimeout(() => {
|
||||
runCleanup();
|
||||
setEditingField(null);
|
||||
}, delayMs);
|
||||
};
|
||||
|
||||
const onActiveEdit = (field: string, action: ActiveEditAction) => {
|
||||
runAction(action);
|
||||
setEditingField(field);
|
||||
};
|
||||
|
||||
const onRequestActiveEdit = (
|
||||
field: string,
|
||||
action: ActiveEditAction,
|
||||
options: {
|
||||
clearActiveEditDelayMs?: number;
|
||||
},
|
||||
) => {
|
||||
if (editingField === field) {
|
||||
// Already active.
|
||||
return;
|
||||
}
|
||||
|
||||
if (editingField) {
|
||||
onClearActiveEdit(options?.clearActiveEditDelayMs);
|
||||
} else {
|
||||
onActiveEdit(field, action);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<SingleActiveEditFormContext.Provider
|
||||
value={{
|
||||
formName,
|
||||
editingField,
|
||||
onRequestActiveEdit,
|
||||
onClearActiveEdit,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</SingleActiveEditFormContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
type UseSingleActiveEditFormResult = {
|
||||
formName: SingleActiveEditFormContextValue['formName'];
|
||||
editingField?: SingleActiveEditFormContextValue['editingField'];
|
||||
onRequestActiveEdit: SingleActiveEditFormContextValue['onRequestActiveEdit'];
|
||||
onClearActiveEdit: SingleActiveEditFormContextValue['onClearActiveEdit'];
|
||||
};
|
||||
|
||||
export function useSingleActiveEditForm(): UseSingleActiveEditFormResult | null {
|
||||
const context = useContext(SingleActiveEditFormContext);
|
||||
|
||||
if (!context) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
formName: context.formName,
|
||||
editingField: context.editingField,
|
||||
onRequestActiveEdit: context.onRequestActiveEdit,
|
||||
onClearActiveEdit: context.onClearActiveEdit,
|
||||
};
|
||||
}
|
||||
6
upcoming-release-notes/3274.md
Normal file
6
upcoming-release-notes/3274.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
category: Bugfix
|
||||
authors: [minajevs]
|
||||
---
|
||||
|
||||
Fix mobile transaction form requires additional click to unfocus amount input
|
||||
Reference in New Issue
Block a user