diff --git a/src/core/Accounts/Transactions/TransactionDialog.jsx b/src/core/Accounts/Transactions/TransactionDialog.jsx index 41c8bc9..d47e266 100644 --- a/src/core/Accounts/Transactions/TransactionDialog.jsx +++ b/src/core/Accounts/Transactions/TransactionDialog.jsx @@ -24,8 +24,6 @@ import Autocomplete from '@material-ui/lab/Autocomplete' import format from 'date-fns/format' import parse from 'date-fns/parse' import grey from '@material-ui/core/colors/grey' -import chroma from 'chroma-js' -import AutoComplete from '../../../common/AutoComplete' import ModalDialog from '../../../common/ModalDialog' import { createTransaction, updateTransaction } from '../../../store/transactions/actions' import LinkTo from '../../../common/LinkTo' @@ -60,48 +58,28 @@ const styles = (theme) => ({ accountName: { paddingLeft: theme.spacing(2) }, - option: { - marginLeft: theme.spacing(2) + categoryOption: { + marginLeft: theme.spacing(1), + marginRight: 8 }, - transferLabel: { - marginTop: 6, - marginBottom: -6 - } -}) - -const dot = (color = '#ccc') => ({ - alignItems: 'center', - display: 'flex', - ':before': { - backgroundColor: color, + categoryDot: { borderRadius: 4, content: '" "', - display: 'block', - marginRight: 8, + display: 'inline-block', height: 15, - width: 15 + width: 15, + marginBottom: -2 + }, + categoryName: { + display: 'inline-block', + marginLeft: theme.spacing(1) + }, + transferLabel: { + marginTop: 6, + marginBottom: -6 } }) -const colourStyles = { - control: (newStyles) => ({ ...newStyles, backgroundColor: 'white' }), - option: (newStyles, { data, isDisabled, isSelected }) => { - const color = chroma(data.colour) - return { - ...newStyles, - ...dot(data.colour), - cursor: isDisabled ? 'not-allowed' : 'default', - ':active': { - ...newStyles[':active'], - backgroundColor: !isDisabled && (isSelected ? data.colour : color.alpha(0.3).css()) - } - } - }, - input: (newStyles) => ({ ...newStyles, ...dot() }), - placeholder: (newStyles) => ({ ...newStyles, ...dot() }), - singleValue: (newStyles, { data }) => ({ ...newStyles, ...dot(data.colour) }) -} - const mapStateToProps = ({ budget, accounts }) => ({ budget, accounts }) const mapDispatchToProps = (dispatch) => { @@ -140,217 +118,243 @@ export const TransactionDialogComponent = ({ onCancel, onDelete, open, + account, transaction, budget, accounts -}) => { - return ( - - - - - {TRANSACTION_TYPES.map((type) => ( - } - label={type.charAt(0).toUpperCase() + type.slice(1)} - /> - ))} - - - ), - inputProps: { - 'aria-label': 'Description', - maxLength: 150 - } - }} - className={classes.input} - value={values.description} - name="description" - onChange={handleChange} - error={errors.description && touched.description} - helperText={errors.description} - /> - - - - ), - inputProps: { - 'aria-label': 'Amount', - step: 0.01, - min: 0, - max: 999999999.99 - } - }} - className={classes.input} - value={values.amount} - name="amount" - onChange={handleChange} - error={errors.amount && touched.amount} - helperText={errors.amount} - /> - - - - - - ) - }} - InputLabelProps={{ - shrink: true, - 'aria-label': 'Date' - }} - name="createdAt" - className={classes.input} - value={values.createdAt} - onChange={handleChange} - error={errors.createdAt && touched.createdAt} - helperText={errors.createdAt} - /> - +}) => ( + + + + + {TRANSACTION_TYPES.map((type) => ( + } + label={type.charAt(0).toUpperCase() + type.slice(1)} + /> + ))} + + + ), + inputProps: { + 'aria-label': 'Description', + maxLength: 150 + } + }} + className={classes.input} + value={values.description} + name="description" + onChange={handleChange} + error={errors.description && touched.description} + helperText={errors.description} + /> + + + + ), + inputProps: { + 'aria-label': 'Amount', + step: 0.01, + min: 0, + max: 999999999.99 + } + }} + className={classes.input} + value={values.amount} + name="amount" + onChange={handleChange} + error={errors.amount && touched.amount} + helperText={errors.amount} + /> - {values.transactionType !== 'transfer' && ( - <> - - Category - - - )} - placeholder="Category" - name="categoryId" - value={values.categoryId} - options={budget.categoryTree} - onChange={setFieldValue} - error={errors.categoryId && touched.categoryId} - helperText={errors.categoryId} - styles={colourStyles} - isClearable={true} - /> - - - )} - label="Apply to all matches" + + + + + ) + }} + InputLabelProps={{ + shrink: true, + 'aria-label': 'Date' + }} + name="createdAt" + className={classes.input} + value={values.createdAt} + onChange={handleChange} + error={errors.createdAt && touched.createdAt} + helperText={errors.createdAt} + /> + + + {values.transactionType !== 'transfer' && ( + <> + { + setFieldValue('categoryId', item ? item.id : null) + setFieldValue('category', item) + }} + className={classes.input} + options={Object.values(budget.categoriesById).filter((category) => 'parentId' in category)} + groupBy={(option) => budget.categoriesById[option.parentId].name} + getOptionLabel={(option) => option.name} + noOptionsText="No category found" + value={values.category} + renderOption={(option) => ( +
+
+
{option.name}
+
+ )} + renderInput={(params) => ( + +
+ + ) + }} /> - - - )} - {values.transactionType === 'transfer' && ( + )} + /> - - - Transfer - - } label="From" /> - } label="To" /> - - - - + + )} + label="Apply to all matches" + /> + + + + - )} - - ), - inputProps: { - length: 2 - } - }} - name="notes" - multiline - rowsMax="4" - className={classes.input} - value={values.notes} - onChange={handleChange} - error={errors.notes && touched.notes} - helperText={errors.notes} - /> - + + )} + {values.transactionType === 'transfer' && ( + + + + Transfer + + } label="From" /> + } label="To" /> + + + + + { + setFieldValue('transferAccountId', item ? item.id : null) + setFieldValue('transferAccount', item) + }} + className={classes.input} + classes={{ option: classes.accountName }} + options={Object.values(accounts.byId).filter((a) => account.id !== a.id)} + groupBy={(option) => option.institution} + getOptionLabel={(option) => option.name} + noOptionsText="No accounts found" + value={values.transferAccount} + renderInput={(params) => ( + + )} + /> + + + )} + + ), + inputProps: { + length: 2 + } + }} + name="notes" + multiline + rowsMax="4" + className={classes.input} + value={values.notes} + onChange={handleChange} + error={errors.notes && touched.notes} + helperText={errors.notes} + /> - - ) -} + + +) TransactionDialogComponent.propTypes = { classes: PropTypes.object.isRequired, @@ -365,6 +369,7 @@ TransactionDialogComponent.propTypes = { onDelete: PropTypes.func.isRequired, budget: PropTypes.object.isRequired, transaction: PropTypes.object, + account: PropTypes.object.isRequired, accounts: PropTypes.object.isRequired } @@ -383,6 +388,7 @@ export default compose( transactionType: 'expense', description: '', categoryId: null, + category: null, createAndApplyRule: true, amount: '', transferDirection: 'to', @@ -398,12 +404,7 @@ export default compose( transactionType: getTransactioType(transaction), amount: Math.abs(transaction.amount.accountCurrency), createAndApplyRule: true, - categoryId: transaction.categoryId === undefined ? null : { - id: budget.categoriesById[transaction.categoryId].id, - label: budget.categoriesById[transaction.categoryId].name, - value: budget.categoriesById[transaction.categoryId].name, - colour: budget.categoriesById[transaction.categoryId].colour - }, + category: transaction.categoryId ? budget.categoriesById[transaction.categoryId] : null, notes: transaction.notes || '', transferDirection: transaction.transferDirection || 'to', transferAccount: transaction.transferAccountId ? accounts.byId[transaction.transferAccountId] : null, @@ -430,6 +431,7 @@ export default compose( .required('Please select the date of this transaction') }), handleSubmit: (values, { props, setSubmitting, resetForm }) => { + console.log(values) setSubmitting(true) const { createAndApplyRule, ...rest } = values props.handleSave( @@ -440,7 +442,7 @@ export default compose( accountCurrency: getAmount(values), localCurrency: null }, - categoryId: (values.categoryId === null ? undefined : values.categoryId.id), + categoryId: (!values.categoryId || values.transactionType === 'transfer' ? undefined : values.categoryId), transferAccountId: values.transactionType === 'transfer' ? values.transferAccountId : null, notes: values.notes ? values.notes.trim() : null, createdAt: parse(values.createdAt, 'yyyy-M-d', new Date()).getTime() diff --git a/src/core/Accounts/Transactions/TransactionsTable.jsx b/src/core/Accounts/Transactions/TransactionsTable.jsx index 8bdeb63..0e9242f 100644 --- a/src/core/Accounts/Transactions/TransactionsTable.jsx +++ b/src/core/Accounts/Transactions/TransactionsTable.jsx @@ -9,6 +9,7 @@ import Checkbox from '@material-ui/core/Checkbox' import grey from '@material-ui/core/colors/grey' import Chip from '@material-ui/core/Chip' import Tooltip from '@material-ui/core/Tooltip' +import Typography from '@material-ui/core/Typography' import NoteIcon from '@material-ui/icons/Note' import Icon from '@mdi/react' import Link from '@material-ui/core/Link' @@ -335,26 +336,9 @@ export class TransactionsTableComponent extends React.Component { flexGrow={1} cellRenderer={ ({ rowData }) => { - const icons = [] - if (rowData.transferAccountId && accounts.byId[rowData.transferAccountId]) { - const transferAccount = accounts.byId[rowData.transferAccountId] - let title = `Transfer ${rowData.transferDirection} ${transferAccount.name} account` - if (transferAccount.institution !== account.institution) { - title += ` at ${transferAccount.institution}` - } - icons.push( - - - - ) - } + let commentIcon if (rowData.notes) { - icons.push( + commentIcon = ( @@ -362,7 +346,7 @@ export class TransactionsTableComponent extends React.Component { } return ( toolbarProps.handleEdit(rowData)} className={classes.link} underline="none"> - {icons[0]} {icons[1]} {rowData.description} + {commentIcon} {rowData.description} ) } @@ -374,6 +358,28 @@ export class TransactionsTableComponent extends React.Component { dataKey="categoryId" cellRenderer={ ({ rowData }) => { + if (rowData.transferAccountId && accounts.byId[rowData.transferAccountId]) { + const transferAccount = accounts.byId[rowData.transferAccountId] + const transferLabel = ` ${rowData.transferDirection} ${transferAccount.name}` + let title = `Transfer ${transferLabel}` + if (transferAccount.institution !== account.institution) { + title += ` at ${transferAccount.institution}` + } + return ( + + + + {transferLabel} + + + ) + } + if (rowData.categoryId === undefined) return null const category = budget.categoriesById[rowData.categoryId] if (category === undefined) return null diff --git a/src/core/Accounts/Transactions/__tests__/TransactionDialog.test.jsx b/src/core/Accounts/Transactions/__tests__/TransactionDialog.test.jsx index c902133..02391a3 100644 --- a/src/core/Accounts/Transactions/__tests__/TransactionDialog.test.jsx +++ b/src/core/Accounts/Transactions/__tests__/TransactionDialog.test.jsx @@ -1,19 +1,15 @@ import React from 'react' -import { mount } from 'enzyme' -import { Provider } from 'react-redux' -import { BrowserRouter } from 'react-router-dom' +import { render } from '@testing-library/react' +import '@testing-library/jest-dom/extend-expect' import configureMockStore from 'redux-mock-store' +import { MemoryRouter } from 'react-router-dom' +import { Provider } from 'react-redux' import TransactionDialog from '../TransactionDialog' import ThemeProvider from '../../../ThemeProvider' import { initialState as settingsInitialState } from '../../../../store/settings/reducer' import { groupByInstitution } from '../../../../store/accounts/reducer' import { initialState as budgetInitialState } from '../../../../store/budget/reducer' -beforeEach(() => { - jest.resetModules() - jest.clearAllMocks() -}) - const account = { id: 1, name: 'Checking', @@ -24,33 +20,57 @@ const account = { currency: settingsInitialState.currency } -describe('ConfirmDialog', () => { - const mockHandleCancel = jest.fn() - const mockHandleDelete = jest.fn() - - it('matches snapshot', () => { - const mockStore = configureMockStore() - const byId = { [account.id]: account } - const store = mockStore({ - accounts: { byId, byInstitution: groupByInstitution({ byId, byInstitution: {} }) }, - settings: settingsInitialState, - budget: budgetInitialState +const mockHandleCancel = jest.fn() +const mockHandleDelete = jest.fn() + +const renderContent = (props) => { + const mockStore = configureMockStore() + const byId = { [account.id]: account } + const store = mockStore({ + accounts: { byId, byInstitution: groupByInstitution({ byId, byInstitution: {} }) }, + settings: settingsInitialState, + budget: budgetInitialState + }) + + const newProps = { + open: true, + onCancel: mockHandleCancel, + onDelete: mockHandleDelete, + account, + transaction: null, + ...props + } + return { + ...render( + + + + + + + + ), + props + } +} + +describe('TransactionDialog', () => { + it('renders correctly in new mode', async () => { + const { getByText } = await renderContent() + + expect(getByText('New transaction')).toBeInTheDocument() + }) + + it('renders correctly in edit mode', async () => { + const { getByText } = await renderContent({ + transaction: { + id: 1, + accountId: account.id, + createdAt: new Date('2019-01-01').getTime(), + amount: { accountCurrency: 1 } + } }) - const wrapper = mount(( - - - - - - - - )) - expect(wrapper.debug()).toMatchSnapshot() + + expect(getByText('Edit transaction')).toBeInTheDocument() }) }) diff --git a/src/core/Accounts/Transactions/__tests__/__snapshots__/TransactionDialog.test.jsx.snap b/src/core/Accounts/Transactions/__tests__/__snapshots__/TransactionDialog.test.jsx.snap deleted file mode 100644 index 9277600..0000000 --- a/src/core/Accounts/Transactions/__tests__/__snapshots__/TransactionDialog.test.jsx.snap +++ /dev/null @@ -1,787 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`ConfirmDialog matches snapshot 1`] = ` -" - - - - - - - - - - - - - - - - -
- - - - -
- - - - - -
- - -
- - -
-
- - -
- - -
- Edit transaction -
-
-
- - - - - - - - - -
-
-
-
- - -
- - -
- - -
- - - -
- - - - - - - - - - - - - - - -
-
-
-
- - - - -
- - - - - - - - - - - - - -
- - -
- - - - - - - - - -
-
-
- -
-
-
-
-
-
-
-
-
-
- - -
- - -
- - - - -
- - - - - - - - - - - - - -
- - -
- - - - - - - - - -
-
-
- -
-
-
-
-
-
-
-
-
-
-
-
-
- - -
- - - - -
- - - - - - - - - - - - - -
- - -
- - - - - - - - - -
-
-
- -
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- -
- - - -
- Category - - - - - - - - - - Manage categories - - - - - - - - - - - - - - - - - - -
-
-
-
- - - -
- -
- -
-
- -
- - -
-
- - - -
- - - - - - - -
- - - - - - - - - -
-
-
-
-
-
-
- - - -
- - - - -
- - - - - - - - - - - - - - - -
- - - - - - - - - - - - - -
- - -
- - - - - - - - - -
-
-
- -