4571: Fix UI lockout when using In/Out mode with existing split columns (#4715)

* Fix catch 22 problem when using In/Out with split columns already enabled
- Also changed UI and transaction amount parsing behavior to accept all options combined
- Repositioned some amount options
- Adjusted the labels of a few options

* Fix amount options triggering state resets

* Update VRT

* Revert "Update VRT"

This reverts commit 0a4b70afad.

* Update VRT

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Matt Fiddaman <github@m.fiddaman.uk>
This commit is contained in:
Grant
2025-04-17 21:03:09 -05:00
committed by GitHub
parent 7e3ebb7e5f
commit 83f7a79c76
14 changed files with 136 additions and 109 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 171 KiB

After

Width:  |  Height:  |  Size: 171 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 166 KiB

After

Width:  |  Height:  |  Size: 166 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 169 KiB

After

Width:  |  Height:  |  Size: 168 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 153 KiB

After

Width:  |  Height:  |  Size: 153 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 152 KiB

After

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 151 KiB

After

Width:  |  Height:  |  Size: 151 KiB

View File

@@ -95,7 +95,7 @@ export function FieldMappings({
firstTransaction={transactions[0]} firstTransaction={transactions[0]}
/> />
</View> </View>
{splitMode ? ( {splitMode && !inOutMode ? (
<> <>
<View style={{ flex: 0.5 }}> <View style={{ flex: 0.5 }}>
<SubLabel title="Outflow" /> <SubLabel title="Outflow" />

View File

@@ -207,6 +207,7 @@ export function ImportTransactionsModal({
multiplierAmount, multiplierAmount,
) => { ) => {
const previewTransactions = []; const previewTransactions = [];
const inOutModeEnabled = isOfxFile(filetype) ? false : inOutMode;
for (let trans of transactions) { for (let trans of transactions) {
if (trans.isMatchedTransaction) { if (trans.isMatchedTransaction) {
@@ -237,7 +238,7 @@ export function ImportTransactionsModal({
const { amount } = parseAmountFields( const { amount } = parseAmountFields(
trans, trans,
splitMode, splitMode,
isOfxFile(filetype) ? false : inOutMode, inOutModeEnabled,
outValue, outValue,
flipAmount, flipAmount,
multiplierAmount, multiplierAmount,
@@ -412,7 +413,9 @@ export function ImportTransactionsModal({
setTransactions(transactionPreview); setTransactions(transactionPreview);
} }
}, },
[accountId, getImportPreview, inOutMode, multiplierAmount, outValue, prefs], // We use some state variables from the component, but do not want to re-parse when they change
// eslint-disable-next-line react-hooks/exhaustive-deps
[accountId, getImportPreview, prefs],
); );
function onMultiplierChange(e) { function onMultiplierChange(e) {
@@ -449,14 +452,8 @@ export function ImportTransactionsModal({
return; return;
} }
if (flipAmount === true) {
setFlipAmount(!flipAmount);
}
const isSplit = !splitMode; const isSplit = !splitMode;
setSplitMode(isSplit); setSplitMode(isSplit);
setInOutMode(false);
setFlipAmount(false);
// Run auto-detection on the fields to try to detect the fields // Run auto-detection on the fields to try to detect the fields
// automatically // automatically
@@ -1026,7 +1023,6 @@ export function ImportTransactionsModal({
<CheckboxOption <CheckboxOption
id="form_flip" id="form_flip"
checked={flipAmount} checked={flipAmount}
disabled={splitMode || inOutMode}
onChange={() => { onChange={() => {
setFlipAmount(!flipAmount); setFlipAmount(!flipAmount);
runImportPreview(); runImportPreview();
@@ -1034,31 +1030,6 @@ export function ImportTransactionsModal({
> >
{t('Flip amount')} {t('Flip amount')}
</CheckboxOption> </CheckboxOption>
{filetype === 'csv' && (
<>
<CheckboxOption
id="form_split"
checked={splitMode}
disabled={inOutMode || flipAmount}
onChange={() => {
onSplitMode();
runImportPreview();
}}
>
{t('Split amount into separate inflow/outflow columns')}
</CheckboxOption>
<InOutOption
inOutMode={inOutMode}
outValue={outValue}
disabled={splitMode || flipAmount}
onToggle={() => {
setInOutMode(!inOutMode);
runImportPreview();
}}
onChangeText={setOutValue}
/>
</>
)}
<MultiplierOption <MultiplierOption
multiplierEnabled={multiplierEnabled} multiplierEnabled={multiplierEnabled}
multiplierAmount={multiplierAmount} multiplierAmount={multiplierAmount}
@@ -1069,6 +1040,29 @@ export function ImportTransactionsModal({
}} }}
onChangeAmount={onMultiplierChange} onChangeAmount={onMultiplierChange}
/> />
{filetype === 'csv' && (
<>
<CheckboxOption
id="form_split"
checked={splitMode}
onChange={() => {
onSplitMode();
runImportPreview();
}}
>
{t('Split amount into separate inflow/outflow columns')}
</CheckboxOption>
<InOutOption
inOutMode={inOutMode}
outValue={outValue}
onToggle={() => {
setInOutMode(!inOutMode);
runImportPreview();
}}
onChangeText={setOutValue}
/>
</>
)}
</View> </View>
</Stack> </Stack>
</View> </View>

View File

@@ -29,7 +29,7 @@ export function InOutOption({
onChange={onToggle} onChange={onToggle}
> >
{inOutMode {inOutMode
? 'in/out identifier' ? 'In/Out outflow value'
: 'Select column to specify if amount goes in/out'} : 'Select column to specify if amount goes in/out'}
</CheckboxOption> </CheckboxOption>
{inOutMode && ( {inOutMode && (
@@ -37,7 +37,7 @@ export function InOutOption({
type="text" type="text"
value={outValue} value={outValue}
onChangeValue={onChangeText} onChangeValue={onChangeText}
placeholder="Value for out rows, i.e. Credit" placeholder="Value for out rows, e.g: Credit"
/> />
)} )}
</View> </View>

View File

@@ -25,7 +25,7 @@ export function MultiplierOption({
checked={multiplierEnabled} checked={multiplierEnabled}
onChange={onToggle} onChange={onToggle}
> >
Add multiplier Multiply amount
</CheckboxOption> </CheckboxOption>
<Input <Input
type="text" type="text"

View File

@@ -19,20 +19,22 @@ export function SelectField({
hasHeaderRow, hasHeaderRow,
firstTransaction, firstTransaction,
}: SelectFieldProps) { }: SelectFieldProps) {
const columns = options.map(
option =>
[
option,
hasHeaderRow
? option
: `Column ${parseInt(option) + 1} (${firstTransaction[option]})`,
] as const,
);
// If selected column does not exist in transaction sheet, ignore
if (!columns.find(col => col[0] === value)) value = null;
return ( return (
<Select <Select
options={[ options={[['choose-field', 'Choose field...'], ...columns]}
['choose-field', 'Choose field...'],
...options.map(
option =>
[
option,
hasHeaderRow
? option
: `Column ${parseInt(option) + 1} (${firstTransaction[option]})`,
] as const,
),
]}
value={value === null ? 'choose-field' : value} value={value === null ? 'choose-field' : value}
onChange={onChange} onChange={onChange}
style={style} style={style}

View File

@@ -30,7 +30,7 @@ type TransactionProps = {
dateFormat: ComponentProps<typeof ParsedDate>['dateFormat']; dateFormat: ComponentProps<typeof ParsedDate>['dateFormat'];
splitMode: boolean; splitMode: boolean;
inOutMode: boolean; inOutMode: boolean;
outValue: number; outValue: string;
flipAmount: boolean; flipAmount: boolean;
multiplierAmount: string; multiplierAmount: string;
categories: CategoryEntity[]; categories: CategoryEntity[];
@@ -208,6 +208,19 @@ export function Transaction({
> >
{categoryList.includes(transaction.category) && transaction.category} {categoryList.includes(transaction.category) && transaction.category}
</Field> </Field>
{inOutMode && (
<Field
width={90}
contentStyle={{ textAlign: 'left', ...styles.tnum }}
title={
transaction.inOut === undefined
? undefined
: String(transaction.inOut)
}
>
{transaction.inOut}
</Field>
)}
{splitMode ? ( {splitMode ? (
<> <>
<Field <Field
@@ -246,36 +259,21 @@ export function Transaction({
</Field> </Field>
</> </>
) : ( ) : (
<> <Field
{inOutMode && ( width={90}
<Field contentStyle={{
width={90} textAlign: 'right',
contentStyle={{ textAlign: 'left', ...styles.tnum }} ...styles.tnum,
title={ ...(amount === null ? { color: theme.errorText } : {}),
transaction.inOut === undefined }}
? undefined title={
: String(transaction.inOut) amount === null
} ? `Invalid: unable to parse the value (${transaction.amount})`
> : amountToCurrency(amount)
{transaction.inOut} }
</Field> >
)} {amountToCurrency(amount || 0)}
<Field </Field>
width={90}
contentStyle={{
textAlign: 'right',
...styles.tnum,
...(amount === null ? { color: theme.errorText } : {}),
}}
title={
amount === null
? `Invalid: unable to parse the value (${transaction.amount})`
: amountToCurrency(amount)
}
>
{amountToCurrency(amount || 0)}
</Field>
</>
)} )}
</Row> </Row>
); );

View File

@@ -126,7 +126,7 @@ export type ImportTransaction = {
amount: number; amount: number;
inflow: number; inflow: number;
outflow: number; outflow: number;
inOut: number; inOut: string;
} & Record<string, string>; } & Record<string, string>;
export type FieldMapping = { export type FieldMapping = {
@@ -161,7 +161,6 @@ export function applyFieldMappings(
function parseAmount( function parseAmount(
amount: number | string | undefined | null, amount: number | string | undefined | null,
mapper: (parsed: number) => number, mapper: (parsed: number) => number,
multiplier: number,
) { ) {
if (amount == null) { if (amount == null) {
return null; return null;
@@ -174,54 +173,82 @@ function parseAmount(
return null; return null;
} }
return mapper(parsed) * multiplier; return mapper(parsed);
} }
export function parseAmountFields( export function parseAmountFields(
trans: Partial<ImportTransaction>, trans: Partial<ImportTransaction>,
splitMode: boolean, splitMode: boolean,
inOutMode: boolean, inOutMode: boolean,
outValue: number, outValue: string,
flipAmount: boolean, flipAmount: boolean,
multiplierAmount: string, multiplierAmount: string,
) { ) {
const multiplier = parseFloat(multiplierAmount) || 1.0; const multiplier = parseFloat(multiplierAmount) || 1.0;
if (splitMode) { /** Keep track of the transaction amount as inflow and outflow.
*
* Inflow/outflow is taken from a positive/negative transaction amount
* respectively, or the inflow/outflow fields if split mode is enabled.
*/
const value = {
outflow: 0,
inflow: 0,
};
// Determine the base value of the transaction from the amount or inflow/outflow fields
if (splitMode && !inOutMode) {
// Split mode is a little weird; first we look for an outflow and // Split mode is a little weird; first we look for an outflow and
// if that has a value, we never want to show a number in the // if that has a value, we never want to show a number in the
// inflow. Same for `amount`; we choose outflow first and then inflow // inflow. Same for `amount`; we choose outflow first and then inflow
const outflow = parseAmount(trans.outflow, n => -Math.abs(n), multiplier); value.outflow = parseAmount(trans.outflow, n => -Math.abs(n)) || 0;
const inflow = outflow value.inflow = value.outflow
? 0 ? 0
: parseAmount(trans.inflow, n => Math.abs(n), multiplier); : parseAmount(trans.inflow, n => Math.abs(n)) || 0;
} else {
return { const amount = parseAmount(trans.amount, n => n) || 0;
amount: outflow || inflow, if (amount >= 0) value.inflow = amount;
outflow, else value.outflow = amount;
inflow,
};
} }
// Apply in/out
if (inOutMode) { if (inOutMode) {
// The 'In/Out' field of a transaction will tell us
// whether the transaction value is inflow or outflow.
const transactionValue = value.outflow || value.inflow;
if (trans.inOut === outValue) {
value.outflow = -Math.abs(transactionValue);
value.inflow = 0;
} else {
value.inflow = Math.abs(transactionValue);
value.outflow = 0;
}
}
// Apply flip
if (flipAmount) {
const oldInflow = value.inflow;
value.inflow = Math.abs(value.outflow);
value.outflow = -Math.abs(oldInflow);
}
// Apply multiplier
value.inflow *= multiplier;
value.outflow *= multiplier;
if (splitMode) {
return { return {
amount: parseAmount( amount: value.outflow || value.inflow,
trans.amount, outflow: value.outflow,
n => (trans.inOut === outValue ? Math.abs(n) * -1 : Math.abs(n)), inflow: value.inflow,
multiplier, };
), } else {
return {
amount: value.outflow || value.inflow,
outflow: null, outflow: null,
inflow: null, inflow: null,
}; };
} }
return {
amount: parseAmount(
trans.amount,
n => (flipAmount ? n * -1 : n),
multiplier,
),
outflow: null,
inflow: null,
};
} }
export function stripCsvImportTransaction(transaction: ImportTransaction) { export function stripCsvImportTransaction(transaction: ImportTransaction) {

View File

@@ -0,0 +1,6 @@
---
category: Bugfix
authors: [Gmanicus]
---
Reworked import to accept all amount options at the same time to fix UI lockout when using In/Out mode with existing split columns.