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>
|
Before Width: | Height: | Size: 171 KiB After Width: | Height: | Size: 171 KiB |
|
Before Width: | Height: | Size: 166 KiB After Width: | Height: | Size: 166 KiB |
|
Before Width: | Height: | Size: 169 KiB After Width: | Height: | Size: 168 KiB |
|
Before Width: | Height: | Size: 153 KiB After Width: | Height: | Size: 153 KiB |
|
Before Width: | Height: | Size: 152 KiB After Width: | Height: | Size: 152 KiB |
|
Before Width: | Height: | Size: 151 KiB After Width: | Height: | Size: 151 KiB |
@@ -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" />
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
6
upcoming-release-notes/4715.md
Normal 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.
|
||||||