feat(currency): add currency display to rules (#5639)

* feat(currency): add to rules

* doc: release notes

* feat: remove keydown from Input

* doc: release notes

* fix: make onEnter optional

* fix: ai remark

* refactor: remove onKeyDown from Input.tsx

* fix: handle Amount (inflow) and Amount (outflow) properly

* [autofix.ci] apply automated fixes

* fix: update AmountInput to sign and on outflow set +

* refactor: onSubmit handling of input value

* coderabbit suggestions

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
Michael Süssemilch
2025-10-07 19:44:21 +02:00
committed by GitHub
parent 90ac8d8520
commit 99ca34458e
8 changed files with 133 additions and 115 deletions

View File

@@ -5,8 +5,9 @@ import React, {
useState,
useEffect,
type FocusEventHandler,
type KeyboardEventHandler,
type KeyboardEvent,
type CSSProperties,
useCallback,
} from 'react';
import { useTranslation } from 'react-i18next';
@@ -17,22 +18,19 @@ import { theme } from '@actual-app/components/theme';
import { View } from '@actual-app/components/view';
import { css, cx } from '@emotion/css';
import { evalArithmetic } from 'loot-core/shared/arithmetic';
import { amountToInteger, appendDecimals } from 'loot-core/shared/util';
import { useFormat } from '@desktop-client/hooks/useFormat';
import { useMergedRefs } from '@desktop-client/hooks/useMergedRefs';
import { useSyncedPref } from '@desktop-client/hooks/useSyncedPref';
type AmountInputProps = {
id?: string;
inputRef?: Ref<HTMLInputElement>;
value: number;
zeroSign?: '-' | '+';
sign?: '-' | '+';
onChangeValue?: (value: string) => void;
onFocus?: FocusEventHandler<HTMLInputElement>;
onBlur?: FocusEventHandler<HTMLInputElement>;
onEnter?: KeyboardEventHandler<HTMLInputElement>;
onEnter?: (event: KeyboardEvent<HTMLInputElement>, amount?: number) => void;
onUpdate?: (amount: number) => void;
style?: CSSProperties;
inputStyle?: CSSProperties;
@@ -47,6 +45,7 @@ export function AmountInput({
inputRef,
value: initialValue,
zeroSign = '-', // + or -
sign,
onFocus,
onBlur,
onChangeValue,
@@ -61,20 +60,32 @@ export function AmountInput({
}: AmountInputProps) {
const { t } = useTranslation();
const format = useFormat();
const [symbol, setSymbol] = useState<'+' | '-'>(
initialValue === 0 ? zeroSign : initialValue > 0 ? '+' : '-',
);
const [symbol, setSymbol] = useState<'+' | '-'>(() => {
if (sign) return sign;
return initialValue === 0 ? zeroSign : initialValue > 0 ? '+' : '-';
});
const [isFocused, setIsFocused] = useState(focused ?? false);
const initialValueAbsolute = format(Math.abs(initialValue || 0), 'financial');
const [value, setValue] = useState(initialValueAbsolute);
useEffect(() => setValue(initialValueAbsolute), [initialValueAbsolute]);
const getDisplayValue = useCallback(
(value: number, isEditing: boolean) => {
const absoluteValue = Math.abs(value || 0);
return isEditing
? format.forEdit(absoluteValue)
: format(absoluteValue, 'financial');
},
[format],
);
const [value, setValue] = useState(getDisplayValue(initialValue, false));
useEffect(
() => setValue(getDisplayValue(initialValue, isFocused)),
[initialValue, isFocused, getDisplayValue],
);
const buttonRef = useRef(null);
const ref = useRef<HTMLInputElement>(null);
const mergedRef = useMergedRefs<HTMLInputElement>(inputRef, ref);
const [hideFraction] = useSyncedPref('hideFraction');
useEffect(() => {
if (focused) {
@@ -82,7 +93,30 @@ export function AmountInput({
}
}, [focused]);
useEffect(() => {
if (sign) {
setSymbol(sign);
}
}, [sign]);
const getAmount = useCallback(() => {
const signedValued = symbol === '-' ? symbol + value : value;
return format.fromEdit(signedValued, 0);
}, [symbol, value, format]);
useEffect(() => {
if (ref.current) {
(
ref.current as HTMLInputElement & { getCurrentAmount?: () => number }
).getCurrentAmount = () => getAmount();
}
}, [getAmount]);
function onSwitch() {
if (sign) {
return;
}
const amount = getAmount();
if (amount === 0) {
setSymbol(symbol === '+' ? '-' : '+');
@@ -90,26 +124,35 @@ export function AmountInput({
fireUpdate(amount * -1);
}
function getAmount() {
const signedValued = symbol === '-' ? symbol + value : value;
return amountToInteger(evalArithmetic(signedValued));
}
function onInputTextChange(val) {
val = autoDecimals
? appendDecimals(val, String(hideFraction) === 'true')
: val;
setValue(val ? val : '');
onChangeValue?.(val);
let newText = val;
if (autoDecimals) {
const digits = val.replace(/\D/g, '');
if (digits === '') {
newText = '';
} else {
const intValue = parseInt(digits, 10);
newText = format.forEdit(intValue);
}
}
setValue(newText || '');
onChangeValue?.(newText);
}
function fireUpdate(amount) {
onUpdate?.(amount);
if (amount > 0) {
setSymbol('+');
} else if (amount < 0) {
setSymbol('-');
if (sign) {
setSymbol(sign);
} else {
if (amount > 0) {
setSymbol('+');
} else if (amount < 0) {
setSymbol('-');
}
}
setValue(format(Math.abs(amount), 'financial'));
}
function onInputAmountBlur(e) {
@@ -136,7 +179,7 @@ export function AmountInput({
>
<Button
variant="bare"
isDisabled={disabled}
isDisabled={disabled || !!sign}
aria-label={symbol === '-' ? t('Make positive') : t('Make negative')}
style={{ padding: '0 7px' }}
onPress={onSwitch}
@@ -172,6 +215,7 @@ export function AmountInput({
)}
onFocus={e => {
setIsFocused(true);
setValue(format.forEdit(Math.abs(initialValue ?? 0)));
onFocus?.(e);
}}
onBlur={e => {
@@ -179,9 +223,9 @@ export function AmountInput({
onInputAmountBlur(e);
}}
onEnter={(_, e) => {
onEnter?.(e);
const amount = getAmount();
fireUpdate(amount);
onEnter?.(e, amount);
}}
onChangeValue={onInputTextChange}
/>