Skip to content
Snippets Groups Projects
TransactionsTable.js 49.2 KiB
Newer Older
  • Learn to ignore specific revisions
  • James Long's avatar
    James Long committed
    import React, {
      useState,
      useRef,
      useMemo,
      useCallback,
      useLayoutEffect,
      useEffect,
      useContext,
    
    James Long's avatar
    James Long committed
    } from 'react';
    import { useSelector, useDispatch } from 'react-redux';
    
    James Long's avatar
    James Long committed
    import {
      format as formatDate,
      parseISO,
    
      isValid as isDateValid,
    
    James Long's avatar
    James Long committed
    } from 'date-fns';
    
    import { useCachedSchedules } from 'loot-core/src/client/data-hooks/schedules';
    
    James Long's avatar
    James Long committed
    import {
      getAccountsById,
      getPayeesById,
    
    James Long's avatar
    James Long committed
    } 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';
    
    James Long's avatar
    James Long committed
    import {
      splitTransaction,
      updateTransaction,
      deleteTransaction,
    
    James Long's avatar
    James Long committed
    } from 'loot-core/src/shared/transactions';
    
    import {
      integerToCurrency,
      amountToInteger,
    
    } 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';
    
    James Long's avatar
    James Long committed
    import {
      Cell,
      Field,
      Row,
      InputCell,
      SelectCell,
      DeleteCell,
      CustomCell,
      CellButton,
      useTableNavigator,
    
    James Long's avatar
    James Long committed
    } from 'loot-design/src/components/table';
    
    import { useMergedRefs } from 'loot-design/src/components/useMergedRefs';
    
    James Long's avatar
    James Long committed
    import {
      useSelectedDispatch,
    
    James Long's avatar
    James Long committed
    } from 'loot-design/src/components/useSelected';
    
    import { styles, colors } from 'loot-design/src/style';
    
    import LeftArrow2 from 'loot-design/src/svg/v0/LeftArrow2';
    import RightArrow2 from 'loot-design/src/svg/v0/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';
    
    James Long's avatar
    James Long committed
    
    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) : '',
    
    James Long's avatar
    James Long committed
      };
    }
    
    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 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),
    
    James Long's avatar
    James Long committed
        }),
    
    James Long's avatar
    James Long committed
      );
    }
    
    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(),
    
    James Long's avatar
    James Long committed
            };
          }
          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,
    
    James Long's avatar
    James Long committed
            };
          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 (
        <SplitsExpandedContext.Provider value={value}>
          {children}
        </SplitsExpandedContext.Provider>
      );
    }
    
    export const TransactionHeader = React.memo(
    
      ({ hasSelected, showAccount, showCategory, showBalance, showCleared }) => {
    
    James Long's avatar
    James Long committed
        let dispatchSelected = useSelectedDispatch();
    
        return (
          <Row
            borderColor={colors.n9}
            backgroundColor="white"
            style={{
              color: colors.n4,
              fontWeight: 300,
    
    James Long's avatar
    James Long committed
            }}
          >
            <SelectCell
              exposed={true}
              focused={false}
              selected={hasSelected}
              width={20}
              onSelect={() => dispatchSelected({ type: 'select-all' })}
            />
            <Cell value="Date" width={110} />
            {showAccount && <Cell value="Account" width="flex" />}
            <Cell value="Payee" width="flex" />
            <Cell value="Notes" width="flex" />
            {showCategory && <Cell value="Category" width="flex" />}
            <Cell value="Payment" width={80} textAlign="right" />
            <Cell value="Deposit" width={80} textAlign="right" />
            {showBalance && <Cell value="Balance" width={85} textAlign="right" />}
    
            {showCleared && <Field width={21} truncate={false} />}
    
    James Long's avatar
    James Long committed
            <Cell value="" width={15 + styles.scrollbarWidth} />
          </Row>
        );
    
    James Long's avatar
    James Long committed
    );
    
    function getPayeePretty(transaction, payee, transferAcct) {
      let { payee: payeeId } = transaction;
    
      if (transferAcct) {
        const Icon = transaction.amount > 0 ? LeftArrow2 : RightArrow2;
        return (
          <View
            style={{
              flexDirection: 'row',
    
    James Long's avatar
    James Long committed
            }}
          >
            <Icon width={10} height={8} style={{ marginRight: 5, flexShrink: 0 }} />
    
                textOverflow: 'ellipsis',
    
    James Long's avatar
    James Long committed
          </View>
        );
      } 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,
    
    James Long's avatar
    James Long committed
    }) {
      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
    
    James Long's avatar
    James Long committed
      };
    
      function onSelect() {
        if (isClearedField) {
          onUpdate('cleared', !(status === 'cleared'));
        }
      }
    
      return (
        <Cell
          name="cleared"
          width="auto"
          focused={focused}
          style={{ padding: 1 }}
          plain
        >
          <CellButton
            style={[
              {
                padding: 3,
                border: '1px solid transparent',
                borderRadius: 50,
                ':focus': {
                  border: '1px solid ' + props.color,
    
                  boxShadow: `0 1px 2px ${props.color}`,
    
                cursor: isClearedField ? 'pointer' : 'default',
    
              isChild && { visibility: 'hidden' },
    
    James Long's avatar
    James Long committed
            ]}
            onEdit={() => onEdit(id, 'cleared')}
            onSelect={onSelect}
          >
            {React.createElement(statusProps.Icon, {
              style: {
                width: 13,
                height: 13,
                color: props.color,
    
                marginTop: status === 'due' ? -1 : 0,
              },
    
    James Long's avatar
    James Long committed
            })}
          </CellButton>
        </Cell>
      );
    }
    
    function PayeeCell({
      id,
      payeeId,
      focused,
      inherited,
      payees,
      accounts,
      valueStyle,
      transaction,
      payee,
      transferAcct,
      importedPayee,
      isPreview,
      onEdit,
      onUpdate,
      onCreatePayee,
    
    James Long's avatar
    James Long committed
    }) {
      let isCreatingPayee = useRef(false);
    
      return (
        <CustomCell
          width="flex"
          name="payee"
          value={payeeId}
          valueStyle={[valueStyle, inherited && { color: colors.n8 }]}
          formatter={value => getPayeePretty(transaction, payee, transferAcct)}
          exposed={focused}
          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,
    
    James Long's avatar
    James Long committed
          }) => {
            return (
              <>
                <PayeeAutocomplete
                  payees={payees}
                  accounts={accounts}
                  value={payeeId}
                  shouldSaveFromKey={shouldSaveFromKey}
                  inputProps={{
                    onBlur,
                    onKeyDown,
    
    James Long's avatar
    James Long committed
                  }}
                  showManagePayees={true}
                  tableBehavior={true}
                  defaultFocusTransferPayees={transaction.is_child}
                  focused={true}
                  onUpdate={onUpdate}
                  onSelect={onSave}
                  onManagePayees={() => onManagePayees(payeeId)}
                />
              </>
            );
          }}
        </CustomCell>
      );
    }
    
    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,
    
    James Long's avatar
    James Long committed
      };
    
      return (
        <View style={{ flex: 1, flexDirection: 'row', alignItems: 'stretch' }}>
          <Cell exposed={true}>
            {() =>
              recurring ? (
                <ArrowsSynchronize style={style} />
              ) : (
                <CalendarIcon style={{ ...style, transform: 'translateY(-1px)' }} />
              )
            }
          </Cell>
    
          {children}
        </View>
      );
    }
    
    export const Transaction = React.memo(function Transaction(props) {
      let {
        transaction: originalTransaction,
        editing,
        backgroundColor = 'white',
        showAccount,
        showBalance,
    
    James Long's avatar
    James Long committed
        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,
    
    James Long's avatar
    James Long committed
      } = props;
    
      let dispatchSelected = useSelectedDispatch();
    
      let [prevShowZero, setPrevShowZero] = useState(showZeroInDeposit);
      let [prevTransaction, setPrevTransaction] = useState(originalTransaction);
      let [transaction, setTransaction] = useState(
    
        serializeTransaction(originalTransaction, showZeroInDeposit, dateFormat),
    
    James Long's avatar
    James Long committed
      );
      let isPreview = isPreviewId(transaction.id);
    
      if (
        originalTransaction !== prevTransaction ||
        showZeroInDeposit !== prevShowZero
      ) {
        setTransaction(
    
          serializeTransaction(originalTransaction, showZeroInDeposit, dateFormat),
    
    James Long's avatar
    James Long committed
        );
        setPrevTransaction(originalTransaction);
        setPrevShowZero(showZeroInDeposit);
      }
    
      function onUpdate(name, value) {
        if (transaction[name] !== value) {
          let newTransaction = { ...transaction, [name]: value };
    
    
          // Don't change the note to an empty string if it's null (since they are both rendered the same)
          if (name === 'note' && value === '' && transaction.note == null) {
            return;
          }
    
    
    James Long's avatar
    James Long committed
          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,
    
    James Long's avatar
    James Long committed
            );
            // Run the transaction through the formatting so that we know
            // it's always showing the formatted result
            setTransaction(
    
              serializeTransaction(deserialized, showZeroInDeposit, dateFormat),
    
    James Long's avatar
    James Long committed
            );
            onSave(deserialized);
          }
        }
      }
    
      let {
        id,
        debit,
        credit,
        payee: payeeId,
        imported_payee: importedPayee,
        notes,
        date,
        account: accountId,
        category,
        cleared,
        is_parent: isParent,
    
    James Long's avatar
    James Long committed
      } = 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 (
        <Row
          borderColor={borderColor}
          backgroundColor={
            selected
              ? colors.selected
              : backgroundFocus
              ? colors.hover
              : isPreview
              ? '#fcfcfc'
              : backgroundColor
          }
          highlighted={highlighted}
          style={[
            style,
            isPreview && { color: colors.n5, fontStyle: 'italic' },
    
            _unmatched && { opacity: 0.5 },
    
    James Long's avatar
    James Long committed
          ]}
          onMouseEnter={() => onHover && onHover(transaction.id)}
        >
          {isChild && (
            <Field
              borderColor="transparent"
              width={110}
              style={{
                width: 110,
                backgroundColor: colors.n11,
    
    James Long's avatar
    James Long committed
              }}
            />
          )}
          {isChild && showAccount && (
            <Field
              borderColor="transparent"
              style={{
                flex: 1,
                backgroundColor: colors.n11,
    
    James Long's avatar
    James Long committed
              }}
            />
          )}
    
          {isTemporaryId(transaction.id) ? (
            isChild ? (
              <DeleteCell
                onDelete={() => onDelete && onDelete(transaction.id)}
                exposed={hovered || editing}
                style={[isChild && { borderLeftWidth: 1 }, { lineHeight: 0 }]}
              />
            ) : (
              <Cell width={20} />
            )
          ) : (
            <SelectCell
              exposed={hovered || selected || editing}
              focused={focusedField === 'select'}
              onSelect={() => {
                dispatchSelected({ type: 'select', id: transaction.id });
              }}
              onEdit={() => onEdit(id, 'select')}
              selected={selected}
              style={[isChild && { borderLeftWidth: 1 }]}
              value={
                matched && (
                  <Hyperlink2 style={{ width: 13, height: 13, color: colors.n7 }} />
                )
              }
            />
          )}
    
          {!isChild && (
            <CustomCell
              name="date"
              width={110}
              exposed={focusedField === 'date'}
              value={date}
              valueStyle={valueStyle}
              formatter={date =>
                date ? formatDate(parseISO(date), dateFormat) : ''
              }
              onExpose={!isPreview && (name => onEdit(id, name))}
              onUpdate={value => {
                onUpdate('date', value);
              }}
            >
              {({
                onBlur,
                onKeyDown,
                onUpdate,
                onSave,
                shouldSaveFromKey,
    
    James Long's avatar
    James Long committed
              }) => (
                <DateSelect
                  value={date || ''}
                  dateFormat={dateFormat}
                  inputProps={{ onBlur, onKeyDown, style: inputStyle }}
                  shouldSaveFromKey={shouldSaveFromKey}
                  tableBehavior={true}
                  onUpdate={onUpdate}
                  onSelect={onSave}
                />
              )}
            </CustomCell>
          )}
    
          {!isChild && showAccount && (
            <CustomCell
              name="account"
              width="flex"
              value={accountId}
              formatter={acctId => {
                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,
    
    James Long's avatar
    James Long committed
              }) => (
                <AccountAutocomplete
                  value={accountId}
                  accounts={accounts}
                  shouldSaveFromKey={shouldSaveFromKey}
                  tableBehavior={true}
                  focused={true}
                  inputProps={{ onBlur, onKeyDown, style: inputStyle }}
                  onUpdate={onUpdate}
                  onSelect={onSave}
                />
              )}
            </CustomCell>
          )}
          {(() => {
            let cell = (
              <PayeeCell
                id={id}
                payeeId={payeeId}
                focused={focusedField === 'payee'}
                inherited={inheritedFields && inheritedFields.has('payee')}
                payees={payees}
                accounts={accounts}
                valueStyle={valueStyle}
                transaction={transaction}
                payee={payee}
                transferAcct={transferAcct}
                importedPayee={importedPayee}
                isPreview={isPreview}
                onEdit={onEdit}
                onUpdate={onUpdate}
                onCreatePayee={onCreatePayee}
                onManagePayees={onManagePayees}
              />
            );
    
            if (transaction.schedule) {
              return (
                <CellWithScheduleIcon scheduleId={transaction.schedule}>
                  {cell}
                </CellWithScheduleIcon>
              );
            }
            return cell;
          })()}
    
          {isPreview ? (
            <Cell name="notes" width="flex" />
          ) : (
            <InputCell
              width="flex"
              name="notes"
              exposed={focusedField === 'notes'}
              focused={focusedField === 'notes'}
              value={notes || ''}
              valueStyle={valueStyle}
              onExpose={!isPreview && (name => onEdit(id, name))}
              inputProps={{
                value: notes || '',
    
                onUpdate: onUpdate.bind(null, 'notes'),
    
    James Long's avatar
    James Long committed
              }}
            />
          )}
    
          {isPreview ? (
            <Cell width="flex" style={{ alignItems: 'flex-start' }} exposed={true}>
              {() => (
                <View
                  style={{
                    color:
                      notes === 'missed'
                        ? colors.r6
                        : notes === 'due'
                        ? colors.y4
                        : selected
                        ? colors.b5
                        : colors.n6,
                    backgroundColor:
                      notes === 'missed'
                        ? colors.r10
                        : notes === 'due'
                        ? colors.y9
                        : selected
                        ? colors.b8
                        : colors.n10,
                    margin: '0 5px',
                    padding: '3px 7px',
    
    James Long's avatar
    James Long committed
                  }}
                >
                  {titleFirst(notes)}
                </View>
              )}
            </Cell>
          ) : isParent ? (
            <Cell
              name="category"
              width="flex"
              focused={focusedField === 'category'}
              style={{ padding: 0 }}
              plain
            >
              <CellButton
                style={{
                  alignSelf: 'flex-start',
                  color: colors.n6,
                  borderRadius: 4,
                  transition: 'none',
                  '&:hover': {
                    backgroundColor: 'rgba(100, 100, 100, .15)',
    
    James Long's avatar
    James Long committed
                }}
                disabled={isTemporaryId(transaction.id)}
                onEdit={() => onEdit(id, 'category')}
                onSelect={() => onToggleSplit(id)}
              >
                <View
                  style={{
                    flexDirection: 'row',
                    alignItems: 'center',
                    alignSelf: 'stretch',
                    borderRadius: 4,
                    flex: 1,
    
    James Long's avatar
    James Long committed
                  }}
                >
                  {isParent && (
                    <CheveronDown
                      style={{
                        width: 14,
                        height: 14,
                        color: 'currentColor',
                        transition: 'transform .08s',
    
                        transform: expanded ? 'rotateZ(0)' : 'rotateZ(-90deg)',
    
    James Long's avatar
    James Long committed
                      }}
                    />
                  )}
                  <Text style={{ fontStyle: 'italic', userSelect: 'none' }}>
                    Split
                  </Text>
                </View>
              </CellButton>
            </Cell>
          ) : isBudgetTransfer || isOffBudget || isPreview ? (
            <InputCell
              name="category"
              width="flex"
              exposed={focusedField === 'category'}
              focused={focusedField === 'category'}
              onExpose={!isPreview && (name => 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' },
    
    James Long's avatar
    James Long committed
              }}
            />
          ) : (
            <CustomCell
              name="category"
              width="flex"
              value={category}
              formatter={value =>
                value
                  ? getDisplayValue(
                      getCategoriesById(categoryGroups)[value],
    
    James Long's avatar
    James Long committed
                    )
                  : transaction.id
                  ? 'Categorize'
                  : ''
              }
              exposed={focusedField === 'category'}
              onExpose={name => onEdit(id, name)}
              valueStyle={
                !category
                  ? {
                      fontStyle: 'italic',
                      fontWeight: 300,
    
    James Long's avatar
    James Long committed
                    }
                  : valueStyle
              }
              onUpdate={async value => {
                if (value === 'split') {
                  onSplit(transaction.id);
                } else {
                  onUpdate('category', value);
                }
              }}
            >
              {({
                onBlur,
                onKeyDown,
                onUpdate,
                onSave,
                shouldSaveFromKey,
    
    James Long's avatar
    James Long committed
              }) => (
                <CategoryAutocomplete
                  categoryGroups={categoryGroups}
                  value={category}
                  focused={true}
                  tableBehavior={true}
                  showSplitOption={!isChild && !isParent}
                  shouldSaveFromKey={shouldSaveFromKey}
                  inputProps={{ onBlur, onKeyDown, style: inputStyle }}
                  onUpdate={onUpdate}
                  onSelect={onSave}
                />
              )}
            </CustomCell>
          )}
    
          <InputCell
            type="input"
            width={80}
            name="debit"
            exposed={focusedField === 'debit'}
            focused={focusedField === 'debit'}
            value={debit == null ? '' : debit}
            valueStyle={valueStyle}