feat: Prevent leaving the page without saving (#431)

* fix: make all fields required on the Terms & Conditions page if Show on
screen is enabled

fix: enable/disable the Terms & Conditions form based on the Show on
screen toggle

fix: replaced deactivated field with plain text when not editing

fix: make de non editable text content field scrollable

style: make it follow the same style as the other screens, with the edit
button and links to save and cancel

feat: created Prompt component to avoid leaving pages without saving

feat: applied component to the editable table

feat: applied component to the Cashout, Commissions, Locales, Cashboxes,
Notifications, CryptoBalanceOverrides and Wallet pages

feat: applied component to the ContactInfo and ReceiptPrinting pages

refactor: export the default prompt message to be used in other contexts

fix: applied prompt component to the Operator Info pages

fix: create routes for the operator info components

feat: applied the Prompt component to the Contact Info and Receipt pages

feat: applied the Prompt component to the Terms & Conditions page

* refactor: move prompt to components

* feat: use formik on the boolean properties table

* chore: removed console.logs

chore: removed comments

refactor: moved BooleanCell to the BooleanPropertiesTable file and make
it not a formik field
This commit is contained in:
Liordino Neto 2020-09-21 08:45:29 -03:00 committed by GitHub
parent dbfb37a756
commit 3c0f4ec194
10 changed files with 173 additions and 129 deletions

View file

@ -0,0 +1,16 @@
import { useFormikContext } from 'formik'
import React from 'react'
import { Prompt } from 'react-router-dom'
const PROMPT_DEFAULT_MESSAGE =
'You have unsaved changes on this page. Are you sure you want to leave?'
const PromptWhenDirty = ({ message = PROMPT_DEFAULT_MESSAGE }) => {
const formik = useFormikContext()
return (
<Prompt when={formik.dirty && formik.submitCount === 0} message={message} />
)
}
export default PromptWhenDirty

View file

@ -1,117 +1,110 @@
import { makeStyles } from '@material-ui/core/styles' import { makeStyles } from '@material-ui/core/styles'
import classnames from 'classnames' import classnames from 'classnames'
import { useFormikContext, Form, Formik, Field as FormikField } from 'formik'
import _ from 'lodash'
import React, { useState, memo } from 'react' import React, { useState, memo } from 'react'
import * as Yup from 'yup'
import PromptWhenDirty from 'src/components/PromptWhenDirty'
import { Link } from 'src/components/buttons' import { Link } from 'src/components/buttons'
import { RadioGroup } from 'src/components/inputs' import { RadioGroup } from 'src/components/inputs/formik'
import { Table, TableBody, TableRow, TableCell } from 'src/components/table' import { Table, TableBody, TableRow, TableCell } from 'src/components/table'
import BooleanCell from 'src/components/tables/BooleanCell'
import { H4 } from 'src/components/typography' import { H4 } from 'src/components/typography'
import { ReactComponent as EditIconDisabled } from 'src/styling/icons/action/edit/disabled.svg' import { ReactComponent as EditIconDisabled } from 'src/styling/icons/action/edit/disabled.svg'
import { ReactComponent as EditIcon } from 'src/styling/icons/action/edit/enabled.svg' import { ReactComponent as EditIcon } from 'src/styling/icons/action/edit/enabled.svg'
import { ReactComponent as FalseIcon } from 'src/styling/icons/table/false.svg'
import { ReactComponent as TrueIcon } from 'src/styling/icons/table/true.svg'
import { booleanPropertiesTableStyles } from './BooleanPropertiesTable.styles' import { booleanPropertiesTableStyles } from './BooleanPropertiesTable.styles'
const useStyles = makeStyles(booleanPropertiesTableStyles) const useStyles = makeStyles(booleanPropertiesTableStyles)
const BooleanCell = ({ name }) => {
const { values } = useFormikContext()
return values[name] === 'true' ? <TrueIcon /> : <FalseIcon />
}
const BooleanPropertiesTable = memo( const BooleanPropertiesTable = memo(
({ title, disabled, data, elements, save }) => { ({ title, disabled, data, elements, save }) => {
const initialValues = _.fromPairs(elements.map(it => [it.name, '']))
const schemaValidation = _.fromPairs(
elements.map(it => [it.name, Yup.boolean().required()])
)
const [editing, setEditing] = useState(false) const [editing, setEditing] = useState(false)
const [radioGroupValues, setRadioGroupValues] = useState(elements)
const classes = useStyles() const classes = useStyles()
const innerSave = () => { const innerSave = async value => {
radioGroupValues.forEach(element => { save(value)
data[element.name] = element.value
})
save(data)
setEditing(false) setEditing(false)
} }
const innerCancel = () => { const innerCancel = () => setEditing(false)
setRadioGroupValues(elements)
setEditing(false)
}
const handleRadioButtons = (elementName, newValue) => {
setRadioGroupValues(
radioGroupValues.map(element =>
element.name === elementName
? { ...element, value: newValue }
: element
)
)
}
const radioButtonOptions = [ const radioButtonOptions = [
{ display: 'Yes', code: true }, { display: 'Yes', code: 'true' },
{ display: 'No', code: false } { display: 'No', code: 'false' }
] ]
if (!elements || radioGroupValues?.length === 0) return null
return ( return (
<div className={classes.booleanPropertiesTableWrapper}> <div className={classes.booleanPropertiesTableWrapper}>
<div className={classes.rowWrapper}> <Formik
<H4>{title}</H4> enableReinitialize
{editing ? ( onSubmit={innerSave}
<div className={classes.rightAligned}> initialValues={data || initialValues}
<Link onClick={innerCancel} color="secondary"> schemaValidation={schemaValidation}>
Cancel <Form>
</Link> <div className={classes.rowWrapper}>
<Link <H4>{title}</H4>
className={classes.rightLink} {editing ? (
onClick={innerSave} <div className={classes.rightAligned}>
color="primary"> <Link onClick={innerCancel} color="secondary">
Save Cancel
</Link> </Link>
<Link
className={classes.rightLink}
type="submit"
color="primary">
Save
</Link>
</div>
) : (
<div className={classes.transparentButton}>
<button disabled={disabled} onClick={() => setEditing(true)}>
{disabled ? <EditIconDisabled /> : <EditIcon />}
</button>
</div>
)}
</div> </div>
) : ( <PromptWhenDirty />
<div className={classes.transparentButton}> <Table className={classes.fillColumn}>
<button disabled={disabled} onClick={() => setEditing(true)}> <TableBody className={classes.fillColumn}>
{disabled ? <EditIconDisabled /> : <EditIcon />} {elements.map((it, idx) => (
</button> <TableRow key={idx} size="sm" className={classes.tableRow}>
</div> <TableCell className={classes.leftTableCell}>
)} {it.display}
</div> </TableCell>
<Table className={classes.fillColumn}> <TableCell className={classes.rightTableCell}>
<TableBody className={classes.fillColumn}> {editing && (
{radioGroupValues && <FormikField
radioGroupValues.map((element, idx) => ( component={RadioGroup}
<TableRow key={idx} size="sm" className={classes.tableRow}> name={it.name}
<TableCell className={classes.leftTableCell}> options={radioButtonOptions}
{element.display} className={classnames(
</TableCell> classes.radioButtons,
<TableCell className={classes.rightTableCell}> classes.rightTableCell
{editing && ( )}
<RadioGroup />
options={radioButtonOptions} )}
value={element.value} {!editing && <BooleanCell name={it.name} />}
onChange={event => </TableCell>
handleRadioButtons( </TableRow>
element.name, ))}
event.target.value === 'true' </TableBody>
) </Table>
} </Form>
className={classnames( </Formik>
classes.radioButtons,
classes.rightTableCell
)}
/>
)}
{!editing && (
<BooleanCell
className={classes.rightTableCell}
value={element.value}
/>
)}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div> </div>
) )
} }

View file

@ -4,6 +4,7 @@ import * as R from 'ramda'
import React, { useState } from 'react' import React, { useState } from 'react'
import { v4 } from 'uuid' import { v4 } from 'uuid'
import PromptWhenDirty from 'src/components/PromptWhenDirty'
import Link from 'src/components/buttons/Link.js' import Link from 'src/components/buttons/Link.js'
import { AddButton } from 'src/components/buttons/index.js' import { AddButton } from 'src/components/buttons/index.js'
import { TBody, Table } from 'src/components/fake-table/Table' import { TBody, Table } from 'src/components/fake-table/Table'
@ -159,6 +160,7 @@ const ETable = ({
validationSchema={validationSchema} validationSchema={validationSchema}
onSubmit={innerSave}> onSubmit={innerSave}>
<Form> <Form>
<PromptWhenDirty />
<ERow editing={true} disabled={forceDisable} /> <ERow editing={true} disabled={forceDisable} />
</Form> </Form>
</Formik> </Formik>

View file

@ -1,8 +0,0 @@
import React from 'react'
import { ReactComponent as FalseIcon } from 'src/styling/icons/table/false.svg'
import { ReactComponent as TrueIcon } from 'src/styling/icons/table/true.svg'
const BooleanCell = ({ value }) => (value ? <TrueIcon /> : <FalseIcon />)
export default BooleanCell

View file

@ -2,6 +2,8 @@ import { Form, Formik } from 'formik'
import React, { useContext } from 'react' import React, { useContext } from 'react'
import * as Yup from 'yup' import * as Yup from 'yup'
import PromptWhenDirty from 'src/components/PromptWhenDirty'
import NotificationsCtx from '../NotificationsContext' import NotificationsCtx from '../NotificationsContext'
import Header from './EditHeader' import Header from './EditHeader'
@ -41,6 +43,7 @@ const SingleFieldEditableNumber = ({
setEditing(name, false) setEditing(name, false)
}}> }}>
<Form className={className}> <Form className={className}>
<PromptWhenDirty />
<Header <Header
title={title} title={title}
editing={isEditing(name)} editing={isEditing(name)}

View file

@ -3,6 +3,7 @@ import { Form, Formik } from 'formik'
import React, { useContext } from 'react' import React, { useContext } from 'react'
import * as Yup from 'yup' import * as Yup from 'yup'
import PromptWhenDirty from 'src/components/PromptWhenDirty'
import { TL2 } from 'src/components/typography' import { TL2 } from 'src/components/typography'
import { Cashbox } from '../../../components/inputs/cashbox/Cashbox' import { Cashbox } from '../../../components/inputs/cashbox/Cashbox'
@ -59,6 +60,7 @@ const FiatBalance = ({
setEditing(NAME, false) setEditing(NAME, false)
}}> }}>
<Form className={classes.form}> <Form className={classes.form}>
<PromptWhenDirty />
<Header <Header
title="Cash out (Empty)" title="Cash out (Empty)"
editing={editing} editing={editing}

View file

@ -9,6 +9,7 @@ import React, { useState } from 'react'
import * as Yup from 'yup' import * as Yup from 'yup'
import ErrorMessage from 'src/components/ErrorMessage' import ErrorMessage from 'src/components/ErrorMessage'
import PromptWhenDirty from 'src/components/PromptWhenDirty'
import { Link } from 'src/components/buttons' import { Link } from 'src/components/buttons'
import Switch from 'src/components/inputs/base/Switch' import Switch from 'src/components/inputs/base/Switch'
import { TextInput, NumberInput } from 'src/components/inputs/formik' import { TextInput, NumberInput } from 'src/components/inputs/formik'
@ -167,12 +168,13 @@ const ContactInfo = () => {
name: 'phone', name: 'phone',
label: 'Phone number', label: 'Phone number',
value: value:
info.phone && info.phone && locale.country
parsePhoneNumberFromString( ? parsePhoneNumberFromString(
info.phone, info.phone,
locale.country locale.country
).formatInternational(), ).formatInternational()
component: NumberInput : '',
component: TextInput
}, },
{ {
name: 'email', name: 'email',
@ -250,6 +252,7 @@ const ContactInfo = () => {
setError(null) setError(null)
}}> }}>
<Form> <Form>
<PromptWhenDirty />
<div className={classes.row}> <div className={classes.row}>
<Field <Field
field={findField('name')} field={findField('name')}

View file

@ -1,6 +1,13 @@
import { makeStyles } from '@material-ui/core' import { makeStyles } from '@material-ui/core'
import Grid from '@material-ui/core/Grid' import Grid from '@material-ui/core/Grid'
import React, { useState } from 'react' import React from 'react'
import {
Route,
Switch,
Redirect,
useLocation,
useHistory
} from 'react-router-dom'
import Sidebar from 'src/components/layout/Sidebar' import Sidebar from 'src/components/layout/Sidebar'
import TitleSection from 'src/components/layout/TitleSection' import TitleSection from 'src/components/layout/TitleSection'
@ -24,34 +31,66 @@ const styles = {
const useStyles = makeStyles(styles) const useStyles = makeStyles(styles)
const CONTACT_INFORMATION = 'Contact information' const innerRoutes = [
const RECEIPT = 'Receipt' {
const COIN_ATM_RADAR = 'Coin ATM Radar' label: 'Contact information',
const TERMS_CONDITIONS = 'Terms & Conditions' route: '/settings/operator-info/contact-info',
component: ContactInfo
},
{
label: 'Receipt',
route: '/settings/operator-info/receipt-printing',
component: ReceiptPrinting
},
{
label: 'Coin ATM Radar',
route: '/settings/operator-info/coin-atm-radar',
component: CoinAtmRadar
},
{
label: 'Terms & Conditions',
route: '/settings/operator-info/terms-conditions',
component: TermsConditions
}
]
const pages = [CONTACT_INFORMATION, RECEIPT, COIN_ATM_RADAR, TERMS_CONDITIONS] const Routes = () => (
<Switch>
<Redirect
exact
from="/settings/operator-info"
to="/settings/operator-info/contact-info"
/>
<Route exact path="/" />
{innerRoutes.map(({ route, component: Page, key }) => (
<Route path={route} key={key}>
<Page name={key} />
</Route>
))}
</Switch>
)
const OperatorInfo = () => { const OperatorInfo = () => {
const [selected, setSelected] = useState(CONTACT_INFORMATION)
const classes = useStyles() const classes = useStyles()
const history = useHistory()
const location = useLocation()
const isSelected = it => selected === it const isSelected = it => location.pathname === it.route
const onClick = it => history.push(it.route)
return ( return (
<> <>
<TitleSection title="Operator information"></TitleSection> <TitleSection title="Operator information"></TitleSection>
<Grid container className={classes.grid}> <Grid container className={classes.grid}>
<Sidebar <Sidebar
data={pages} data={innerRoutes}
isSelected={isSelected} isSelected={isSelected}
displayName={it => it} displayName={it => it.label}
onClick={it => setSelected(it)} onClick={onClick}
/> />
<div className={classes.content}> <div className={classes.content}>
{isSelected(CONTACT_INFORMATION) && <ContactInfo />} <Routes />
{isSelected(RECEIPT) && <ReceiptPrinting />}
{isSelected(TERMS_CONDITIONS) && <TermsConditions />}
{isSelected(COIN_ATM_RADAR) && <CoinAtmRadar />}
</div> </div>
</Grid> </Grid>
</> </>

View file

@ -99,43 +99,35 @@ const ReceiptPrinting = memo(() => {
elements={[ elements={[
{ {
name: 'operatorWebsite', name: 'operatorWebsite',
display: 'Operator website', display: 'Operator website'
value: receiptPrintingConfig.operatorWebsite
}, },
{ {
name: 'operatorEmail', name: 'operatorEmail',
display: 'Operator email', display: 'Operator email'
value: receiptPrintingConfig.operatorEmail
}, },
{ {
name: 'operatorPhone', name: 'operatorPhone',
display: 'Operator phone', display: 'Operator phone'
value: receiptPrintingConfig.operatorPhone
}, },
{ {
name: 'companyNumber', name: 'companyNumber',
display: 'Company number', display: 'Company number'
value: receiptPrintingConfig.companyNumber
}, },
{ {
name: 'machineLocation', name: 'machineLocation',
display: 'Machine location', display: 'Machine location'
value: receiptPrintingConfig.machineLocation
}, },
{ {
name: 'customerNameOrPhoneNumber', name: 'customerNameOrPhoneNumber',
display: 'Customer name or phone number (if known)', display: 'Customer name or phone number (if known)'
value: receiptPrintingConfig.customerNameOrPhoneNumber
}, },
{ {
name: 'exchangeRate', name: 'exchangeRate',
display: 'Exchange rate', display: 'Exchange rate'
value: receiptPrintingConfig.exchangeRate
}, },
{ {
name: 'addressQRCode', name: 'addressQRCode',
display: 'Address QR code', display: 'Address QR code'
value: receiptPrintingConfig.addressQRCode
} }
]} ]}
save={save} save={save}

View file

@ -8,6 +8,7 @@ import React, { useState } from 'react'
import * as Yup from 'yup' import * as Yup from 'yup'
import ErrorMessage from 'src/components/ErrorMessage' import ErrorMessage from 'src/components/ErrorMessage'
import PromptWhenDirty from 'src/components/PromptWhenDirty'
import { Link } from 'src/components/buttons' import { Link } from 'src/components/buttons'
import { Switch } from 'src/components/inputs' import { Switch } from 'src/components/inputs'
import { TextInput } from 'src/components/inputs/formik' import { TextInput } from 'src/components/inputs/formik'
@ -218,6 +219,7 @@ const TermsConditions = () => {
setError(null) setError(null)
}}> }}>
<Form> <Form>
<PromptWhenDirty />
{fields.map((f, idx) => ( {fields.map((f, idx) => (
<div className={classes.row} key={idx}> <div className={classes.row} key={idx}>
<Field <Field