import React, { useState, useRef, useMemo, useCallback, useLayoutEffect, useEffect, useContext, useReducer } from 'react'; import { useSelector, useDispatch } from 'react-redux'; import { format as formatDate, parseISO, isValid as isDateValid } from 'date-fns'; import { useCachedSchedules } from 'loot-core/src/client/data-hooks/schedules'; import { getAccountsById, getPayeesById, getCategoriesById } from 'loot-core/src/client/reducers/queries'; import evalArithmetic from 'loot-core/src/shared/arithmetic'; import { currentDay } from 'loot-core/src/shared/months'; import { getScheduledAmount } from 'loot-core/src/shared/schedules'; import { splitTransaction, updateTransaction, deleteTransaction, addSplitTransaction } from 'loot-core/src/shared/transactions'; import { integerToCurrency, amountToInteger, titleFirst } from 'loot-core/src/shared/util'; import AccountAutocomplete from 'loot-design/src/components/AccountAutocomplete'; import CategoryAutocomplete from 'loot-design/src/components/CategorySelect'; import { View, Text, Tooltip, Button } from 'loot-design/src/components/common'; import DateSelect from 'loot-design/src/components/DateSelect'; import PayeeAutocomplete from 'loot-design/src/components/PayeeAutocomplete'; import { Cell, Field, Row, InputCell, SelectCell, DeleteCell, CustomCell, CellButton, useTableNavigator, Table } from 'loot-design/src/components/table'; import { useMergedRefs } from 'loot-design/src/components/useMergedRefs'; import { useSelectedDispatch, useSelectedItems } from 'loot-design/src/components/useSelected'; import { styles, colors } from 'loot-design/src/style'; import LeftArrow2 from 'loot-design/src/svg/LeftArrow2'; import RightArrow2 from 'loot-design/src/svg/RightArrow2'; import CheveronDown from 'loot-design/src/svg/v1/CheveronDown'; import ArrowsSynchronize from 'loot-design/src/svg/v2/ArrowsSynchronize'; import CalendarIcon from 'loot-design/src/svg/v2/Calendar'; import Hyperlink2 from 'loot-design/src/svg/v2/Hyperlink2'; import { getStatusProps } from '../schedules/StatusBadge'; let TABLE_BACKGROUND_COLOR = colors.n11; function getDisplayValue(obj, name) { return obj ? obj[name] : ''; } function serializeTransaction(transaction, showZeroInDeposit, dateFormat) { let { amount, date } = transaction; if (isPreviewId(transaction.id)) { amount = getScheduledAmount(amount); } let debit = amount < 0 ? -amount : null; let credit = amount > 0 ? amount : null; if (amount === 0) { if (showZeroInDeposit) { credit = 0; } else { debit = 0; } } // Validate the date format if (!isDateValid(parseISO(date))) { // Be a little forgiving if the date isn't valid. This at least // stops the UI from crashing, but this is a serious problem with // the data. This allows the user to go through and see empty // dates and manually fix them. date = null; } return { ...transaction, date, debit: debit != null ? integerToCurrency(debit) : '', credit: credit != null ? integerToCurrency(credit) : '' }; } function deserializeTransaction(transaction, originalTransaction, dateFormat) { let { debit, credit, date, ...realTransaction } = transaction; let amount; if (debit !== '') { let parsed = evalArithmetic(debit, null); amount = parsed != null ? -parsed : null; } else { amount = evalArithmetic(credit, null); } amount = amount != null ? amountToInteger(amount) : originalTransaction.amount; if (date == null) { date = originalTransaction.date || currentDay(); } return { ...realTransaction, date, amount }; } function getParentTransaction(transactions, fromIndex) { let trans = transactions[fromIndex]; let parent; let parentIdx = fromIndex; while (parentIdx >= 0) { if (transactions[parentIdx].id === trans.parent_id) { // Found the parent return transactions[parentIdx]; } parentIdx--; } return null; } function isLastChild(transactions, index) { let trans = transactions[index]; return ( trans && trans.is_child && (transactions[index + 1] == null || transactions[index + 1].parent_id !== trans.parent_id) ); } let SplitsExpandedContext = React.createContext(null); export function useSplitsExpanded() { let data = useContext(SplitsExpandedContext); return useMemo( () => ({ ...data, expanded: id => data.state.mode === 'collapse' ? !data.state.ids.has(id) : data.state.ids.has(id) }), [data] ); } export function SplitsExpandedProvider({ children, initialMode = 'expand' }) { let cachedState = useSelector(state => state.app.lastSplitState); let reduxDispatch = useDispatch(); let [state, dispatch] = useReducer((state, action) => { switch (action.type) { case 'toggle-split': { let ids = new Set([...state.ids]); let { id } = action; if (ids.has(id)) { ids.delete(id); } else { ids.add(id); } return { ...state, ids }; } case 'open-split': { let ids = new Set([...state.ids]); let { id } = action; if (state.mode === 'collapse') { ids.delete(id); } else { ids.add(id); } return { ...state, ids }; } case 'set-mode': { return { ...state, mode: action.mode, ids: new Set(), transitionId: null }; } case 'switch-mode': if (state.transitionId != null) { // You can only transition once at a time return state; } return { ...state, mode: state.mode === 'expand' ? 'collapse' : 'expand', transitionId: action.id, ids: new Set() }; case 'finish-switch-mode': return { ...state, transitionId: null }; default: throw new Error('Unknown action type: ' + action.type); } }, cachedState.current || { ids: new Set(), mode: initialMode }); useEffect(() => { if (state.transitionId != null) { // This timeout allows animations to finish setTimeout(() => { dispatch({ type: 'finish-switch-mode' }); }, 250); } }, [state.transitionId]); useEffect(() => { // In a finished state, cache the state if (state.transitionId == null) { reduxDispatch({ type: 'SET_LAST_SPLIT_STATE', splitState: state }); } }, [state]); let value = useMemo(() => ({ state, dispatch }), [state, dispatch]); return ( {children} ); } export const TransactionHeader = React.memo( ({ hasSelected, showAccount, showCategory, showBalance }) => { let dispatchSelected = useSelectedDispatch(); return ( dispatchSelected({ type: 'select-all' })} /> {showAccount && } {showCategory && } {showBalance && } ); } ); function getPayeePretty(transaction, payee, transferAcct) { let { payee: payeeId } = transaction; if (transferAcct) { const Icon = transaction.amount > 0 ? LeftArrow2 : RightArrow2; return (
{transferAcct.name}
); } else if (payee && !payee.transfer_acct) { // Check to make sure this isn't a transfer because in the rare // occasion that the account has been deleted but the payee is // still there, we don't want to show the name. return payee.name; } else if (payeeId && payeeId.startsWith('new:')) { return payeeId.slice('new:'.length); } return ''; } function StatusCell({ id, focused, selected, status, isChild, onEdit, onUpdate }) { let isClearedField = status === 'cleared' || status == null; let statusProps = getStatusProps(status); let props = { color: status === 'cleared' ? colors.g5 : status === 'missed' ? colors.r6 : status === 'due' ? colors.y5 : selected ? colors.b7 : colors.n6 }; function onSelect() { if (isClearedField) { onUpdate('cleared', !(status === 'cleared')); } } return ( onEdit(id, 'cleared')} onSelect={onSelect} > {React.createElement(statusProps.Icon, { style: { width: 13, height: 13, color: props.color, marginTop: status === 'due' ? -1 : 0 } })} ); } function PayeeCell({ id, payeeId, focused, inherited, payees, accounts, valueStyle, transaction, payee, transferAcct, importedPayee, isPreview, onEdit, onUpdate, onCreatePayee, onManagePayees }) { let isCreatingPayee = useRef(false); return ( getPayeePretty(transaction, payee, transferAcct)} exposed={focused} title={importedPayee || payeeId} onExpose={!isPreview && (name => onEdit(id, name))} onUpdate={async value => { onUpdate('payee', value); if (value && value.startsWith('new:') && !isCreatingPayee.current) { isCreatingPayee.current = true; let id = await onCreatePayee(value.slice('new:'.length)); onUpdate('payee', id); isCreatingPayee.current = false; } }} > {({ onBlur, onKeyDown, onUpdate, onSave, shouldSaveFromKey, inputStyle }) => { return ( <> onManagePayees(payeeId)} /> ); }} ); } function CellWithScheduleIcon({ scheduleId, children }) { let scheduleData = useCachedSchedules(); let schedule = scheduleData.schedules.find(s => s.id === scheduleId); if (schedule == null) { // This must be a deleted schedule return children; } let recurring = schedule._date && !!schedule._date.frequency; let style = { width: 13, height: 13, marginLeft: 5, marginRight: 3, color: 'inherit' }; let Icon = recurring ? ArrowsSynchronize : CalendarIcon; return ( {() => recurring ? ( ) : ( ) } {children} ); } export const Transaction = React.memo(function Transaction(props) { let { transaction: originalTransaction, editing, backgroundColor = 'white', showAccount, showBalance, showZeroInDeposit, style, hovered, selected, highlighted, added, matched, expanded, inheritedFields, focusedField, categoryGroups, payees, accounts, balance, dateFormat = 'MM/dd/yyyy', onSave, onEdit, onHover, onDelete, onSplit, onManagePayees, onCreatePayee, onToggleSplit } = props; let dispatchSelected = useSelectedDispatch(); let [prevShowZero, setPrevShowZero] = useState(showZeroInDeposit); let [prevTransaction, setPrevTransaction] = useState(originalTransaction); let [transaction, setTransaction] = useState( serializeTransaction(originalTransaction, showZeroInDeposit, dateFormat) ); let isPreview = isPreviewId(transaction.id); if ( originalTransaction !== prevTransaction || showZeroInDeposit !== prevShowZero ) { setTransaction( serializeTransaction(originalTransaction, showZeroInDeposit, dateFormat) ); setPrevTransaction(originalTransaction); setPrevShowZero(showZeroInDeposit); } function onUpdate(name, value) { if (transaction[name] !== value) { let newTransaction = { ...transaction, [name]: value }; if ( name === 'account' && value && getAccountsById(accounts)[value].offbudget ) { newTransaction.category = null; } // If entering an amount in either of the credit/debit fields, we // need to clear out the other one so it's always properly // translated into the desired amount (see // `deserializeTransaction`) if (name === 'credit') { newTransaction['debit'] = ''; } else if (name === 'debit') { newTransaction['credit'] = ''; } // Don't save a temporary value (a new payee) which will be // filled in with a real id later if (name === 'payee' && value && value.startsWith('new:')) { setTransaction(newTransaction); } else { let deserialized = deserializeTransaction( newTransaction, originalTransaction, dateFormat ); // Run the transaction through the formatting so that we know // it's always showing the formatted result setTransaction( serializeTransaction(deserialized, showZeroInDeposit, dateFormat) ); onSave(deserialized); } } } let { id, debit, credit, payee: payeeId, imported_payee: importedPayee, notes, date, account: accountId, category, cleared, is_parent: isParent, _unmatched = false } = transaction; // Join in some data let payee = payees && payeeId && getPayeesById(payees)[payeeId]; let account = accounts && accountId && getAccountsById(accounts)[accountId]; let transferAcct = payee && payee.transfer_acct && getAccountsById(accounts)[payee.transfer_acct]; let isChild = transaction.is_child; let borderColor = selected ? colors.b8 : colors.border; let isBudgetTransfer = transferAcct && transferAcct.offbudget === 0; let isOffBudget = account && account.offbudget === 1; let valueStyle = added ? { fontWeight: 600 } : null; let backgroundFocus = hovered || focusedField === 'select'; return ( onHover && onHover(transaction.id)} > {isChild && ( )} {isChild && showAccount && ( )} {isTemporaryId(transaction.id) ? ( isChild ? ( onDelete && onDelete(transaction.id)} exposed={hovered || editing} style={[isChild && { borderLeftWidth: 1 }, { lineHeight: 0 }]} /> ) : ( ) ) : ( { dispatchSelected({ type: 'select', id: transaction.id }); }} onEdit={() => onEdit(id, 'select')} selected={selected} style={[isChild && { borderLeftWidth: 1 }]} value={ matched && ( ) } /> )} {!isChild && ( date ? formatDate(parseISO(date), dateFormat) : '' } onExpose={!isPreview && (name => onEdit(id, name))} onUpdate={value => { onUpdate('date', value); }} > {({ onBlur, onKeyDown, onUpdate, onSave, shouldSaveFromKey, inputStyle }) => ( )} )} {!isChild && showAccount && ( { let acct = acctId && getAccountsById(accounts)[acctId]; if (acct) { return acct.name; } return ''; }} valueStyle={valueStyle} exposed={focusedField === 'account'} onExpose={!isPreview && (name => onEdit(id, name))} onUpdate={async value => { // Only ever allow non-null values if (value) { onUpdate('account', value); } }} > {({ onBlur, onKeyDown, onUpdate, onSave, shouldSaveFromKey, inputStyle }) => ( )} )} {(() => { let cell = ( ); if (transaction.schedule) { return ( {cell} ); } return cell; })()} {isPreview ? ( ) : ( onEdit(id, name))} inputProps={{ value: notes || '', onUpdate: onUpdate.bind(null, 'notes') }} /> )} {isPreview ? ( {() => ( {titleFirst(notes)} )} ) : isParent ? ( onEdit(id, 'category')} onSelect={() => onToggleSplit(id)} > {isParent && ( )} Split ) : isBudgetTransfer || isOffBudget || isPreview ? ( onEdit(id, name))} value={ isParent ? 'Split' : isOffBudget ? 'Off Budget' : isBudgetTransfer ? 'Transfer' : '' } valueStyle={valueStyle} style={{ fontStyle: 'italic', color: '#c0c0c0', fontWeight: 300 }} inputProps={{ readOnly: true, style: { fontStyle: 'italic' } }} /> ) : ( value ? getDisplayValue( getCategoriesById(categoryGroups)[value], 'name' ) : transaction.id ? 'Categorize' : '' } exposed={focusedField === 'category'} onExpose={name => onEdit(id, name)} valueStyle={ !category ? { fontStyle: 'italic', fontWeight: 300, color: colors.p8 } : valueStyle } onUpdate={async value => { if (value === 'split') { onSplit(transaction.id); } else { onUpdate('category', value); } }} > {({ onBlur, onKeyDown, onUpdate, onSave, shouldSaveFromKey, inputStyle }) => ( )} )} onEdit(id, name))} style={[isParent && { fontStyle: 'italic' }, styles.tnum]} inputProps={{ value: debit, onUpdate: onUpdate.bind(null, 'debit') }} /> onEdit(id, name))} style={[isParent && { fontStyle: 'italic' }, styles.tnum]} inputProps={{ value: credit, onUpdate: onUpdate.bind(null, 'credit') }} /> {showBalance && ( )} ); }); export function TransactionError({ error, isDeposit, onAddSplit, style }) { switch (error.type) { case 'SplitTransactionError': if (error.version === 1) { return ( Amount left:{' '} {integerToCurrency( isDeposit ? error.difference : -error.difference )} ); } break; default: return null; } } function makeTemporaryTransactions(currentAccountId, lastDate) { return [ { id: 'temp', date: lastDate || currentDay(), account: currentAccountId || null, cleared: false, amount: 0 } ]; } function isTemporaryId(id) { return id.indexOf('temp') !== -1; } export function isPreviewId(id) { return id.indexOf('preview/') !== -1; } function NewTransaction({ transactions, accounts, currentAccountId, categoryGroups, payees, editingTransaction, hoveredTransaction, focusedField, showAccount, showCategory, showBalance, dateFormat, onHover, onClose, onSplit, onEdit, onDelete, onSave, onAdd, onAddSplit, onManagePayees, onCreatePayee }) { const error = transactions[0].error; const isDeposit = transactions[0].amount > 0; return ( { if (e.keyCode === 27) { onClose(); } }} onMouseLeave={() => onHover(null)} > {transactions.map((transaction, idx) => ( ))} {error ? ( onAddSplit(transactions[0].id)} /> ) : ( )} ); } class TransactionTable_ extends React.Component { container = React.createRef(); state = { highlightedRows: null }; componentDidMount() { this.highlight = ids => { this.setState({ highlightedRows: new Set(ids) }, () => { this.setState({ highlightedRows: null }); }); }; } componentWillReceiveProps(nextProps) { const { isAdding } = this.props; if (!isAdding && nextProps.isAdding) { this.props.newNavigator.onEdit('temp', 'date'); } } componentDidUpdate() { this._cachedParent = null; } getParent(trans, index) { let { transactions } = this.props; if (this._cachedParent && this._cachedParent.id === trans.parent_id) { return this._cachedParent; } if (trans.parent_id) { this._cachedParent = getParentTransaction(transactions, index); return this._cachedParent; } return null; } renderRow = ({ item, index, position, editing, focusedFied, onEdit }) => { const { highlightedRows } = this.state; const { transactions, selectedItems, hoveredTransaction, accounts, categoryGroups, payees, showAccount, showCategory, balances, dateFormat = 'MM/dd/yyyy', tableNavigator, isNew, isMatched, isExpanded } = this.props; let trans = item; let hovered = hoveredTransaction === trans.id; let selected = selectedItems.has(trans.id); let highlighted = !selected && (highlightedRows ? highlightedRows.has(trans.id) : false); let parent = this.getParent(trans, index); let isChildDeposit = parent && parent.amount > 0; let expanded = isExpanded && isExpanded((parent || trans).id); // For backwards compatibility, read the error of the transaction // since in previous versions we stored it there. In the future we // can simplify this to just the parent let error = expanded ? (parent && parent.error) || trans.error : trans.error; return ( <> {(!expanded || isLastChild(transactions, index)) && error && error.type === 'SplitTransactionError' && ( this.props.onAddSplit(trans.id)} /> )} ); }; render() { let { props } = this; let { tableNavigator, tableRef, dateFormat = 'MM/dd/yyyy', newNavigator, renderEmpty, onHover, onScroll } = props; return ( 0} showAccount={props.showAccount} showCategory={props.showCategory} showBalance={!!props.balances} /> {props.isAdding && ( props.onCheckNewEnter(e) })} > )} {/*// * On Windows, makes the scrollbar always appear // the full height of the container ??? */} onHover(null)} > props.selectedItems.has(id)} onKeyDown={e => props.onCheckEnter(e)} onScroll={onScroll} /> {props.isAdding && (
)} ); } } export let TransactionTable = React.forwardRef((props, ref) => { let [newTransactions, setNewTransactions] = useState(null); let [hoveredTransaction, setHoveredTransaction] = useState( props.hoveredTransaction ); let [prevIsAdding, setPrevIsAdding] = useState(false); let splitsExpanded = useSplitsExpanded(); let prevSplitsExpanded = useRef(null); let tableRef = useRef(null); let mergedRef = useMergedRefs(tableRef, ref); let transactions = useMemo(() => { let result; if (splitsExpanded.state.transitionId != null) { let index = props.transactions.findIndex( t => t.id === splitsExpanded.state.transitionId ); result = props.transactions.filter((t, idx) => { if (t.parent_id) { if (idx >= index) { return splitsExpanded.expanded(t.parent_id); } else if (prevSplitsExpanded.current) { return prevSplitsExpanded.current.expanded(t.parent_id); } } return true; }); } else { if ( prevSplitsExpanded.current && prevSplitsExpanded.current.state.transitionId != null ) { tableRef.current.anchor(); tableRef.current.setRowAnimation(false); } prevSplitsExpanded.current = splitsExpanded; result = props.transactions.filter(t => { if (t.parent_id) { return splitsExpanded.expanded(t.parent_id); } return true; }); } prevSplitsExpanded.current = splitsExpanded; return result; }, [props.transactions, splitsExpanded]); useEffect(() => { // If it's anchored that means we've also disabled animations. To // reduce the chance for side effect collision, only do this if // we've actually anchored it if (tableRef.current.isAnchored()) { tableRef.current.unanchor(); tableRef.current.setRowAnimation(true); } }, [prevSplitsExpanded.current]); let newNavigator = useTableNavigator(newTransactions, getFields); let tableNavigator = useTableNavigator(transactions, getFields); let shouldAdd = useRef(false); let latestState = useRef({ newTransactions, newNavigator, tableNavigator }); let savePending = useRef(false); let afterSaveFunc = useRef(false); // eslint-disable-next-line let [_, forceRerender] = useState({}); let selectedItems = useSelectedItems(); let dispatchSelected = useSelectedDispatch(); useLayoutEffect(() => { latestState.current = { newTransactions, newNavigator, tableNavigator, transactions: props.transactions }; }); // Derive new transactions from the `isAdding` prop if (prevIsAdding !== props.isAdding) { if (!prevIsAdding && props.isAdding) { setNewTransactions(makeTemporaryTransactions(props.currentAccountId)); } setPrevIsAdding(props.isAdding); } useEffect(() => { if (shouldAdd.current) { if (newTransactions[0].account == null) { props.addNotification({ type: 'error', message: 'Account is a required field' }); newNavigator.onEdit('temp', 'account'); } else { let transactions = latestState.current.newTransactions; let lastDate = transactions.length > 0 ? transactions[0].date : null; setNewTransactions( makeTemporaryTransactions(props.currentAccountId, lastDate) ); newNavigator.onEdit('temp', 'date'); props.onAdd(transactions); } shouldAdd.current = false; } }); useEffect(() => { if (savePending.current && afterSaveFunc.current) { afterSaveFunc.current(props); afterSaveFunc.current = null; } savePending.current = false; }, [newTransactions, props.transactions]); function getFields(item) { let fields = [ 'select', 'date', 'account', 'payee', 'notes', 'category', 'debit', 'credit', 'cleared' ]; fields = item.is_child ? ['select', 'payee', 'notes', 'category', 'debit', 'credit'] : fields.filter( f => (props.showAccount || f !== 'account') && (props.showCategory || f !== 'category') ); if (isPreviewId(item.id)) { fields = ['select', 'cleared']; } if (isTemporaryId(item.id)) { // You can't focus the select/delete button of temporary // transactions fields = fields.slice(1); } return fields; } function afterSave(func) { if (savePending.current) { afterSaveFunc.current = func; } else { func(props); } } function onCheckNewEnter(e) { const ENTER = 13; if (e.keyCode === ENTER) { if (e.metaKey) { e.stopPropagation(); onAddTemporary(); } else if (!e.shiftKey) { function getLastTransaction(state) { let { newTransactions } = state.current; return newTransactions[newTransactions.length - 1]; } // Right now, the table navigator does some funky stuff with // focus, so we want to stop it from handling this event. We // still want enter to move up/down normally, so we only stop // it if we are on the last transaction (where we are about to // do some logic). I don't like this. if (newNavigator.editingId === getLastTransaction(latestState).id) { e.stopPropagation(); } afterSave(() => { let lastTransaction = getLastTransaction(latestState); let isSplit = lastTransaction.parent_id || lastTransaction.is_parent; if ( latestState.current.newTransactions[0].error && newNavigator.editingId === lastTransaction.id ) { // add split onAddSplit(lastTransaction.id); } else if ( (newNavigator.focusedField === 'debit' || newNavigator.focusedField === 'credit' || newNavigator.focusedField === 'cleared') && newNavigator.editingId === lastTransaction.id && (!isSplit || !lastTransaction.error) ) { onAddTemporary(); } }); } } } function onCheckEnter(e) { const ENTER = 13; if (e.keyCode === ENTER && !e.shiftKey) { let { editingId: id, focusedField } = tableNavigator; afterSave(props => { let transactions = latestState.current.transactions; let idx = transactions.findIndex(t => t.id === id); let parent = getParentTransaction(transactions, idx); if ( isLastChild(transactions, idx) && parent && parent.error && focusedField !== 'select' ) { e.stopPropagation(); onAddSplit(id); } }); } } let onAddTemporary = useCallback(() => { shouldAdd.current = true; // A little hacky - this forces a rerender which will cause the // effect we want to run. We have to wait for all updates to be // committed (the input could still be saving a value). forceRerender({}); }, [props.onAdd, newNavigator.onEdit]); let onSave = useCallback( async transaction => { savePending.current = true; if (isTemporaryId(transaction.id)) { if (props.onApplyRules) { transaction = await props.onApplyRules(transaction); } let newTrans = latestState.current.newTransactions; setNewTransactions(updateTransaction(newTrans, transaction).data); } else { props.onSave(transaction); } }, [props.onSave] ); let onHover = useCallback(id => { setHoveredTransaction(id); }, []); let onDelete = useCallback(id => { let temporary = isTemporaryId(id); if (temporary) { let newTrans = latestState.current.newTransactions; if (id === newTrans[0].id) { // You can never delete the parent new transaction return; } setNewTransactions(deleteTransaction(newTrans, id).data); } }, []); let onSplit = useMemo(() => { return id => { if (isTemporaryId(id)) { let { newNavigator } = latestState.current; let newTrans = latestState.current.newTransactions; let { data, diff } = splitTransaction(newTrans, id); setNewTransactions(data); // TODO: what is this for??? // if (newTrans[0].amount == null) { // newNavigator.onEdit(newTrans[0].id, 'debit'); // } else { newNavigator.onEdit( diff.added[0].id, latestState.current.newNavigator.focusedField ); // } } else { let trans = latestState.current.transactions.find(t => t.id === id); let newId = props.onSplit(id); splitsExpanded.dispatch({ type: 'open-split', id: trans.id }); let { tableNavigator } = latestState.current; if (trans.amount == null) { tableNavigator.onEdit(trans.id, 'debit'); } else { tableNavigator.onEdit(newId, tableNavigator.focusedField); } } }; }, [props.onSplit, splitsExpanded.dispatch]); let onAddSplit = useCallback( id => { if (isTemporaryId(id)) { let newTrans = latestState.current.newTransactions; let { data, diff } = addSplitTransaction(newTrans, id); setNewTransactions(data); newNavigator.onEdit( diff.added[0].id, latestState.current.newNavigator.focusedField ); } else { let newId = props.onAddSplit(id); tableNavigator.onEdit( newId, latestState.current.tableNavigator.focusedField ); } }, [props.onAddSplit] ); function onCloseAddTransaction() { setNewTransactions(makeTemporaryTransactions(props.currentAccountId)); props.onCloseAddTransaction(); } let onToggleSplit = useCallback( id => splitsExpanded.dispatch({ type: 'toggle-split', id }), [splitsExpanded.dispatch] ); return ( // eslint-disable-next-line ); });