Skip to content

Commit

Permalink
feat(ui): Adding transaction edit screen for mobile. (#1201)
Browse files Browse the repository at this point in the history
  • Loading branch information
elliotcourant committed Nov 7, 2022
1 parent e205e2c commit 6b2596b
Show file tree
Hide file tree
Showing 4 changed files with 259 additions and 20 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
import React, { Fragment } from 'react';
import NiceModal, { useModal } from '@ebay/nice-modal-react';
import { Edit, ExpandMore } from '@mui/icons-material';
import { Accordion, AccordionDetails, AccordionSummary, Button, Dialog, DialogActions, DialogContent, DialogTitle, Divider, FormControl, FormControlLabel, FormLabel, InputBase, List, ListItem, ListItemText, Radio, RadioGroup, Slide, TextField, Typography } from '@mui/material';
import { TransitionProps } from '@mui/material/transitions';
import { Formik, FormikErrors, FormikHelpers } from 'formik';

import TransactionIcon from '../components/TransactionIcon';

import { useSelectedBankAccountId } from 'hooks/bankAccounts';
import useIsMobile from 'hooks/useIsMobile';
import Transaction from 'models/Transaction';
import { useSpending } from 'hooks/spending';


export interface EditTransactionDialogMobileProps {
transaction: Transaction;
}

interface EditTransactionForm {
name: string;
spendingId: number | null;
}

const Transition = React.forwardRef(function Transition(
props: TransitionProps & {
children: React.ReactElement<any, any>;
},
ref: React.Ref<unknown>,
) {
return <Slide direction="left" ref={ ref } { ...props } />;
});

function EditTransactionDialogMobile(props: EditTransactionDialogMobileProps): JSX.Element {
const modal = useModal();
const isMobile = useIsMobile();
const selectedBankAccountId = useSelectedBankAccountId();
const spending = useSpending(props.transaction.spendingId);

const { transaction } = props;

async function closeDialog() {
modal.hide()
.then(() => modal.remove());
}

async function validateInput(input: EditTransactionForm): FormikErrors<EditTransactionForm> {
return {};
}

async function submit(values: EditTransactionForm, helper: FormikHelpers<EditTransactionForm>): Promise<void> {
return Promise.resolve();
}

const initialValues: EditTransactionForm = {
name: transaction.getName(),
spendingId: transaction.spendingId,
};

function onFocus(e: React.FocusEvent<any>) {
setTimeout(() => {
e.target.setSelectionRange(e.target.value.length, e.target.value.length);
});
}

function EditItem({ name, children }): JSX.Element {
return (
<Fragment>
<ListItem className='pl-0 pr-0'>
<ListItemText
className='flex-none'
primary={ name }
primaryTypographyProps={ {
className: 'text-xl',
} }
/>
{ children }
</ListItem>
<Divider />
</Fragment>
);
}

function SpentFrom(): JSX.Element {
let name: JSX.Element | string;
if (transaction.spendingId && spending) {
name = <span className="text-semibold">{ spending.name }</span>
} else if (transaction.spendingId && !spending) {
name = <span className="text-semibold">...</span>
} else {
name = 'Safe-To-Spend';
}

return (
<Fragment>
<ListItem className='pl-0 pr-0'>
<Accordion square className='pl-0 pr-0 shadow-none w-full'>
<AccordionSummary
style={{
height: 28 + 16,
minHeight: 28 + 16,
}}
classes={{
content: 'p-0 m-0 text-xl',
}}
className='p-0 m-0'
>
<ListItemText
className='flex-none'
primary={ 'Spent From' }
primaryTypographyProps={ {
className: 'text-xl',
} }
/>
<span className="self-center flex-1 text-end text-xl">
{ name }
</span>
</AccordionSummary>
<AccordionDetails>
<FormControl>
<RadioGroup
aria-labelledby="demo-radio-buttons-group-label"
defaultValue="female"
name="radio-buttons-group"
>
<FormControlLabel value="female" control={<Radio />} label="Safe-To-Spend" />
<FormControlLabel value="male" control={<Radio />} label="Amazon" />
<FormControlLabel value="other" control={<Radio />} label="Vacation" />
</RadioGroup>
</FormControl>
</AccordionDetails>
</Accordion>
</ListItem>
<Divider />
</Fragment>
)
}

return (
<Dialog
open={ modal.visible }
maxWidth="sm"
fullScreen={ isMobile }
TransitionComponent={ Transition }
keepMounted={ false }
>
<Formik
initialValues={ initialValues }
validate={ validateInput }
onSubmit={ submit }
>
{ ({
values,
errors,
touched,
handleChange,
handleBlur,
handleSubmit,
setFieldValue,
isSubmitting,
submitForm,
isValid,
}) => (
<Fragment>
<DialogTitle>
<div className='w-full flex justify-center'>
<TransactionIcon transaction={ transaction } size={ 80 } />
</div>
</DialogTitle>
<DialogContent>
<List className='pl-0 pr-0'>
<Divider />
<EditItem name="Name">
<InputBase
name='name'
className='flex-1 flex text-end'
style={ { height: 28 } }
onChange={ handleChange }
onBlur={ handleBlur }
onFocus={ onFocus }
value={ values.name }
inputProps={ {
className: 'flex text-end text-xl',
} }
/>
</EditItem>
<EditItem name="Original Name">
<span className="flex-1 text-end text-xl opacity-75">
{ transaction.getOriginalName() }
</span>
</EditItem>
<EditItem name="Date">
<span className="flex-1 text-end text-xl opacity-75">
{ transaction.date.format('MMMM Do, YYYY') }
</span>
</EditItem>
<EditItem name="Status">
<span className="flex-1 text-end text-xl opacity-75">
{ transaction.isPending ? 'Pending' : 'Complete' }
</span>
</EditItem>
<SpentFrom />
</List>
</DialogContent>
<DialogActions>
<Button
color="secondary"
disabled={ false }
onClick={ closeDialog }
>
Cancel
</Button>
<Button
disabled={ false }
onClick={ () => {} }
color="primary"
type="submit"
>
Save
</Button>
</DialogActions>
</Fragment>
) }
</Formik>
</Dialog>
);
}

const editTransactionMobileModal = NiceModal.create(EditTransactionDialogMobile);
export default editTransactionMobileModal;

export function showEditTransactionMobileDialog(props: EditTransactionDialogMobileProps): void {
NiceModal.show(editTransactionMobileModal, props);
}
Original file line number Diff line number Diff line change
@@ -1,20 +1,19 @@
import { AccessTime } from '@mui/icons-material';
import classnames from 'classnames';
import { useSpending } from 'hooks/spending';
import React, { Fragment } from 'react';
import { Chip, Divider, ListItem, ListItemAvatar, ListItemButton, ListItemText, Skeleton } from '@mui/material';
import { AccessTime, ChevronRight } from '@mui/icons-material';
import { Divider, ListItemAvatar, ListItemButton, ListItemText } from '@mui/material';
import classnames from 'classnames';

import TransactionIcon from 'components/Transactions/components/TransactionIcon';
import { useSpending } from 'hooks/spending';
import Transaction from 'models/Transaction';

import 'components/Transactions/TransactionsView/styles/TransactionItem.scss';
import { showEditTransactionMobileDialog } from './EditTransactionDialog.mobile';

interface Props {
transaction: Transaction;
}

export default function TransactionItemMobile(props: Props): JSX.Element {
const spending = useSpending(props.transaction.spendingId)
const spending = useSpending(props.transaction.spendingId);

function SpentFromLine(): JSX.Element {
if (props.transaction.getIsAddition()) {
Expand All @@ -30,7 +29,7 @@ export default function TransactionItemMobile(props: Props): JSX.Element {
<span className="text-ellipsis overflow-hidden">
Spent From <span className="opacity-75">...</span>
</span>
)
);
}

const name = spending ?
Expand All @@ -45,33 +44,38 @@ export default function TransactionItemMobile(props: Props): JSX.Element {
);
}

const showEditDialog = () => showEditTransactionMobileDialog({
transaction: props.transaction,
});

return (
<Fragment>
<ListItemButton className="pr-0">
<ListItemButton className="pr-0" onClick={ showEditDialog }>
<ListItemAvatar>
<TransactionIcon transaction={ props.transaction }/>
<TransactionIcon transaction={ props.transaction } />
</ListItemAvatar>
<ListItemText
className="flex-initial w-7/12"
primaryTypographyProps={{
className: "text-ellipsis overflow-hidden truncate"
}}
primaryTypographyProps={ {
className: 'text-ellipsis overflow-hidden truncate',
} }
primary={ props.transaction.getName() }
secondaryTypographyProps={{
className: "text-ellipsis overflow-hidden truncate"
}}
secondaryTypographyProps={ {
className: 'text-ellipsis overflow-hidden truncate',
} }
secondary={ <SpentFromLine /> }
/>
<div className="flex-1 flex justify-start">
{ props.transaction.isPending && <AccessTime />
}
}
</div>
<span className={ classnames('h-full flex-none amount align-middle self-center justify-end place-self-center text-sm pr-1', {
'text-green-600': props.transaction.getIsAddition(),
'text-red-600': !props.transaction.getIsAddition(),
}) }>
<b>{ props.transaction.getAmountString() }</b>
</span>
<ChevronRight className='opacity-75' />
</ListItemButton>
<Divider />
</Fragment>
Expand Down
6 changes: 3 additions & 3 deletions ui/components/Transactions/components/TransactionIcon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,13 @@ import theme from 'theme';

interface Props {
transaction: Transaction;
size?: number;
}

export default function TransactionIcon(props: Props): JSX.Element {
// Try to retrieve the icon. If the icon cannot be retrieved or icons are not currently enabled in the application
// config then this will simply return null.
const icon = useIconSearch(props.transaction.name);
const isMobile = useIsMobile();
if (icon?.svg) {
// It is possible for colors to be missing for a given icon. When this happens just fall back to a black color.
const colorStyles = icon?.colors?.length > 0 ?
Expand All @@ -25,8 +25,8 @@ export default function TransactionIcon(props: Props): JSX.Element {
const styles = {
WebkitMaskImage: `url(data:image/svg+xml;base64,${ icon.svg })`,
WebkitMaskRepeat: 'no-repeat',
height: '40px',
width: '40px',
height: `${props.size || 40}px`,
width: `${props.size || 40}px`,
...colorStyles,
};

Expand Down
1 change: 1 addition & 0 deletions ui/theme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const theme = createTheme({
styleOverrides: {
root: {
backgroundColor: defaultPrimary,
backgroundImage: 'none',
},
},
},
Expand Down

0 comments on commit 6b2596b

Please sign in to comment.