mirror of
https://github.com/actualbudget/actual.git
synced 2026-03-09 03:32:54 -05:00
Use amount input on rules page instead of plain text field (#2566)
* Use amount input on rules page instead of plain text field * Add release notes * Remove unneeded attributes * Support percent formatting * Lint + typecheck * Fix latent bug * Handle existing data correctly * PR feedback: naming * PR feedback: force percent to a positive number * PR feedback: reset percent to 100 upon changing input type * Fix input clamping behaviour * Empty commit to bump ci * PR feedback: prop cleanup * PR feedback: no default number format * PR feedback: cosmetic refactor
This commit is contained in:
committed by
GitHub
parent
d62919a357
commit
b89a32025a
@@ -235,6 +235,7 @@ function ConditionEditor({
|
||||
value={value}
|
||||
multi={op === 'oneOf' || op === 'notOneOf'}
|
||||
onChange={v => onChange('value', v)}
|
||||
numberFormatType="currency"
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -365,6 +366,7 @@ function ActionEditor({ action, editorStyle, onChange, onDelete, onAdd }) {
|
||||
op={op}
|
||||
value={value}
|
||||
onChange={v => onChange('value', v)}
|
||||
numberFormatType="currency"
|
||||
/>
|
||||
</View>
|
||||
</>
|
||||
@@ -386,6 +388,9 @@ function ActionEditor({ action, editorStyle, onChange, onDelete, onAdd }) {
|
||||
key={inputKey}
|
||||
field={field}
|
||||
type="number"
|
||||
numberFormatType={
|
||||
options.method === 'fixed-percent' ? 'percentage' : 'currency'
|
||||
}
|
||||
value={value}
|
||||
onChange={v => onChange('value', v)}
|
||||
/>
|
||||
|
||||
@@ -7,6 +7,7 @@ import { integerToCurrency } from 'loot-core/src/shared/util';
|
||||
export type FormatType =
|
||||
| 'string'
|
||||
| 'number'
|
||||
| 'percentage'
|
||||
| 'financial'
|
||||
| 'financial-with-sign';
|
||||
|
||||
@@ -25,6 +26,8 @@ function format(
|
||||
return val;
|
||||
case 'number':
|
||||
return '' + value;
|
||||
case 'percentage':
|
||||
return value + '%';
|
||||
case 'financial-with-sign':
|
||||
const formatted = format(value, 'financial', formatter);
|
||||
if (typeof value === 'number' && value >= 0) {
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useSelector } from 'react-redux';
|
||||
|
||||
import { useReports } from 'loot-core/client/data-hooks/reports';
|
||||
import { getMonthYearFormat } from 'loot-core/src/shared/months';
|
||||
import { integerToAmount, amountToInteger } from 'loot-core/src/shared/util';
|
||||
|
||||
import { useCategories } from '../../hooks/useCategories';
|
||||
import { useDateFormat } from '../../hooks/useDateFormat';
|
||||
@@ -18,10 +19,14 @@ import { Checkbox } from '../forms';
|
||||
import { DateSelect } from '../select/DateSelect';
|
||||
import { RecurringSchedulePicker } from '../select/RecurringSchedulePicker';
|
||||
|
||||
import { AmountInput } from './AmountInput';
|
||||
import { PercentInput } from './PercentInput';
|
||||
|
||||
export function GenericInput({
|
||||
field,
|
||||
subfield,
|
||||
type,
|
||||
numberFormatType = undefined,
|
||||
multi,
|
||||
value,
|
||||
inputRef,
|
||||
@@ -33,6 +38,37 @@ export function GenericInput({
|
||||
const saved = useSelector(state => state.queries.saved);
|
||||
const dateFormat = useDateFormat() || 'MM/dd/yyyy';
|
||||
|
||||
const getNumberInputByFormatType = numberFormatType => {
|
||||
switch (numberFormatType) {
|
||||
case 'currency':
|
||||
return (
|
||||
<AmountInput
|
||||
inputRef={inputRef}
|
||||
value={amountToInteger(value)}
|
||||
onUpdate={v => onChange(integerToAmount(v))}
|
||||
/>
|
||||
);
|
||||
case 'percentage':
|
||||
return (
|
||||
<PercentInput
|
||||
inputRef={inputRef}
|
||||
value={value}
|
||||
onUpdatePercent={onChange}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<Input
|
||||
inputRef={inputRef}
|
||||
defaultValue={value || ''}
|
||||
placeholder="nothing"
|
||||
onEnter={e => onChange(e.target.value)}
|
||||
onBlur={e => onChange(e.target.value)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// This makes the UI more resilient in case of faulty data
|
||||
if (multi && !Array.isArray(value)) {
|
||||
value = [];
|
||||
@@ -208,6 +244,8 @@ export function GenericInput({
|
||||
onSelect={onChange}
|
||||
/>
|
||||
);
|
||||
} else if (type === 'number') {
|
||||
content = getNumberInputByFormatType(numberFormatType);
|
||||
} else {
|
||||
content = (
|
||||
<Input
|
||||
|
||||
124
packages/desktop-client/src/components/util/PercentInput.tsx
Normal file
124
packages/desktop-client/src/components/util/PercentInput.tsx
Normal file
@@ -0,0 +1,124 @@
|
||||
import React, {
|
||||
type Ref,
|
||||
useRef,
|
||||
useState,
|
||||
useEffect,
|
||||
type FocusEventHandler,
|
||||
type FocusEvent,
|
||||
} from 'react';
|
||||
|
||||
import { evalArithmetic } from 'loot-core/src/shared/arithmetic';
|
||||
|
||||
import { useMergedRefs } from '../../hooks/useMergedRefs';
|
||||
import { type CSSProperties } from '../../style';
|
||||
import { Input } from '../common/Input';
|
||||
import { useFormat } from '../spreadsheet/useFormat';
|
||||
|
||||
type PercentInputProps = {
|
||||
id?: string;
|
||||
inputRef?: Ref<HTMLInputElement>;
|
||||
value?: number;
|
||||
onFocus?: FocusEventHandler<HTMLInputElement>;
|
||||
onBlur?: FocusEventHandler<HTMLInputElement>;
|
||||
onChangeValue?: (value: string) => void;
|
||||
onUpdatePercent?: (percent: number) => void;
|
||||
style?: CSSProperties;
|
||||
focused?: boolean;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
const clampToPercent = (value: number) => Math.max(Math.min(value, 100), 0);
|
||||
|
||||
export function PercentInput({
|
||||
id,
|
||||
inputRef,
|
||||
value: initialValue = 0,
|
||||
onFocus,
|
||||
onBlur,
|
||||
onChangeValue,
|
||||
onUpdatePercent,
|
||||
style,
|
||||
focused,
|
||||
disabled = false,
|
||||
}: PercentInputProps) {
|
||||
const format = useFormat();
|
||||
|
||||
const [value, setValue] = useState(() =>
|
||||
format(clampToPercent(initialValue), 'percentage'),
|
||||
);
|
||||
useEffect(() => {
|
||||
const clampedInitialValue = clampToPercent(initialValue);
|
||||
if (clampedInitialValue !== initialValue) {
|
||||
setValue(format(clampedInitialValue, 'percentage'));
|
||||
onUpdatePercent?.(clampedInitialValue);
|
||||
}
|
||||
}, [initialValue, onUpdatePercent, format]);
|
||||
|
||||
const ref = useRef<HTMLInputElement>(null);
|
||||
const mergedRef = useMergedRefs<HTMLInputElement>(inputRef, ref);
|
||||
|
||||
useEffect(() => {
|
||||
if (focused) {
|
||||
ref.current?.focus();
|
||||
}
|
||||
}, [focused]);
|
||||
|
||||
function onSelectionChange() {
|
||||
if (!ref.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const selectionStart = ref.current.selectionStart;
|
||||
const selectionEnd = ref.current.selectionEnd;
|
||||
if (
|
||||
selectionStart === selectionEnd &&
|
||||
selectionStart !== null &&
|
||||
selectionStart >= ref.current.value.length
|
||||
) {
|
||||
ref.current.setSelectionRange(
|
||||
ref.current.value.length - 1,
|
||||
ref.current.value.length - 1,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function onInputTextChange(val: string) {
|
||||
const number = val.replace(/[^0-9.]/g, '');
|
||||
setValue(number ? format(number, 'percentage') : '');
|
||||
onChangeValue?.(number);
|
||||
}
|
||||
|
||||
function fireUpdate() {
|
||||
const clampedValue = clampToPercent(evalArithmetic(value.replace('%', '')));
|
||||
onUpdatePercent?.(clampedValue);
|
||||
onInputTextChange(String(clampedValue));
|
||||
}
|
||||
|
||||
function onInputAmountBlur(e: FocusEvent<HTMLInputElement>) {
|
||||
if (!ref.current?.contains(e.relatedTarget)) {
|
||||
fireUpdate();
|
||||
}
|
||||
onBlur?.(e);
|
||||
}
|
||||
|
||||
return (
|
||||
<Input
|
||||
id={id}
|
||||
inputRef={mergedRef}
|
||||
inputMode="decimal"
|
||||
value={value}
|
||||
disabled={disabled}
|
||||
focused={focused}
|
||||
style={{ flex: 1, alignItems: 'stretch', ...style }}
|
||||
onKeyUp={e => {
|
||||
if (e.key === 'Enter') {
|
||||
fireUpdate();
|
||||
}
|
||||
}}
|
||||
onChangeValue={onInputTextChange}
|
||||
onBlur={onInputAmountBlur}
|
||||
onFocus={onFocus}
|
||||
onSelect={onSelectionChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
6
upcoming-release-notes/2566.md
Normal file
6
upcoming-release-notes/2566.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
category: Enhancements
|
||||
authors: [jfdoming]
|
||||
---
|
||||
|
||||
Use `AmountInput` on rules page to get formatting/sign toggle button
|
||||
Reference in New Issue
Block a user