From af53f06eac32ff4ea4bc8342d1bd62261c29d5c7 Mon Sep 17 00:00:00 2001 From: Jed Fox Date: Fri, 21 Jul 2023 13:14:30 -0400 Subject: [PATCH] Add support for importing the first row of a CSV file without a header row (#1373) --- .../components/modals/ImportTransactions.js | 257 ++++++++++-------- .../src/client/state-types/prefs.d.ts | 1 + .../src/server/accounts/parse-file.ts | 8 +- upcoming-release-notes/1373.md | 6 + 4 files changed, 151 insertions(+), 121 deletions(-) create mode 100644 upcoming-release-notes/1373.md diff --git a/packages/desktop-client/src/components/modals/ImportTransactions.js b/packages/desktop-client/src/components/modals/ImportTransactions.js index 4165a49d513..22b4c268391 100644 --- a/packages/desktop-client/src/components/modals/ImportTransactions.js +++ b/packages/desktop-client/src/components/modals/ImportTransactions.js @@ -1,9 +1,8 @@ import React, { useState, useEffect, useMemo } from 'react'; -import { connect } from 'react-redux'; +import { useSelector } from 'react-redux'; import * as d from 'date-fns'; -import * as actions from 'loot-core/src/client/actions'; import { format as formatDate_ } from 'loot-core/src/shared/months'; import { amountToCurrency, @@ -11,6 +10,7 @@ import { looselyParseAmount, } from 'loot-core/src/shared/util'; +import { useActions } from '../../hooks/useActions'; import { colors, styles } from '../../style'; import { View, @@ -340,12 +340,24 @@ function SubLabel({ title }) { ); } -function SelectField({ width, style, options, value, onChange }) { +function SelectField({ + style, + options, + value, + onChange, + hasHeaderRow, + firstTransaction, +}) { return ( ); } -function FieldMappings({ transactions, mappings, onChange, splitMode }) { +function FieldMappings({ + transactions, + mappings, + onChange, + splitMode, + hasHeaderRow, +}) { if (transactions.length === 0) { return null; } @@ -472,6 +486,8 @@ function FieldMappings({ transactions, mappings, onChange, splitMode }) { value={mappings.date} style={{ marginRight: 5 }} onChange={name => onChange('date', name)} + hasHeaderRow={hasHeaderRow} + firstTransaction={transactions[0]} /> @@ -481,6 +497,8 @@ function FieldMappings({ transactions, mappings, onChange, splitMode }) { value={mappings.payee} style={{ marginRight: 5 }} onChange={name => onChange('payee', name)} + hasHeaderRow={hasHeaderRow} + firstTransaction={transactions[0]} /> @@ -490,6 +508,8 @@ function FieldMappings({ transactions, mappings, onChange, splitMode }) { value={mappings.notes} style={{ marginRight: 5 }} onChange={name => onChange('notes', name)} + hasHeaderRow={hasHeaderRow} + firstTransaction={transactions[0]} /> {splitMode ? ( @@ -500,6 +520,8 @@ function FieldMappings({ transactions, mappings, onChange, splitMode }) { options={options} value={mappings.outflow} onChange={name => onChange('outflow', name)} + hasHeaderRow={hasHeaderRow} + firstTransaction={transactions[0]} /> @@ -508,6 +530,8 @@ function FieldMappings({ transactions, mappings, onChange, splitMode }) { options={options} value={mappings.inflow} onChange={name => onChange('inflow', name)} + hasHeaderRow={hasHeaderRow} + firstTransaction={transactions[0]} /> @@ -518,6 +542,8 @@ function FieldMappings({ transactions, mappings, onChange, splitMode }) { options={options} value={mappings.amount} onChange={name => onChange('amount', name)} + hasHeaderRow={hasHeaderRow} + firstTransaction={transactions[0]} /> )} @@ -526,30 +552,14 @@ function FieldMappings({ transactions, mappings, onChange, splitMode }) { ); } -function MultipliersField({ multiplierCB, value, onChange }) { - const styl = multiplierCB ? 'inherit' : 'none'; - - return ( - +export default function ImportTransactions({ modalProps, options }) { + let dateFormat = useSelector( + state => state.prefs.local.dateFormat || 'MM/dd/yyyy', ); -} + let prefs = useSelector(state => state.prefs.local); + let { parseTransactions, importTransactions, getPayees, savePrefs } = + useActions(); -function ImportTransactions({ - modalProps, - options, - dateFormat = 'MM/dd/yyyy', - prefs, - parseTransactions, - importTransactions, - getPayees, - savePrefs, -}) { let [multiplierAmount, setMultiplierAmount] = useState(''); let [loadingState, setLoadingState] = useState('parsing'); let [error, setError] = useState(null); @@ -571,6 +581,9 @@ function ImportTransactions({ prefs[`csv-delimiter-${accountId}`] || (filename.endsWith('.tsv') ? '\t' : ','), ); + let [hasHeaderRow, setHasHeaderRow] = useState( + prefs[`csv-has-header-${accountId}`] ?? true, + ); let [parseDateFormat, setParseDateFormat] = useState(null); @@ -640,7 +653,7 @@ function ImportTransactions({ parse( options.filename, getFileType(options.filename) === 'csv' - ? { delimiter: csvDelimiter } + ? { delimiter: csvDelimiter, hasHeaderRow } : null, ); }, [parseTransactions, options.filename]); @@ -867,6 +880,7 @@ function ImportTransactions({ onChange={onUpdateFields} mappings={fieldMappings} splitMode={splitMode} + hasHeaderRow={hasHeaderRow} /> )} @@ -892,11 +906,19 @@ function ImportTransactions({ )} - {/*csv Delimiter */} - - {filetype === 'csv' && ( - - + {/* CSV Options */} + {filetype === 'csv' && ( + + +