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

View file

@ -4,6 +4,7 @@ import * as R from 'ramda'
import React, { useState } from 'react'
import { v4 } from 'uuid'
import PromptWhenDirty from 'src/components/PromptWhenDirty'
import Link from 'src/components/buttons/Link.js'
import { AddButton } from 'src/components/buttons/index.js'
import { TBody, Table } from 'src/components/fake-table/Table'
@ -159,6 +160,7 @@ const ETable = ({
validationSchema={validationSchema}
onSubmit={innerSave}>
<Form>
<PromptWhenDirty />
<ERow editing={true} disabled={forceDisable} />
</Form>
</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 * as Yup from 'yup'
import PromptWhenDirty from 'src/components/PromptWhenDirty'
import NotificationsCtx from '../NotificationsContext'
import Header from './EditHeader'
@ -41,6 +43,7 @@ const SingleFieldEditableNumber = ({
setEditing(name, false)
}}>
<Form className={className}>
<PromptWhenDirty />
<Header
title={title}
editing={isEditing(name)}

View file

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

View file

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

View file

@ -1,6 +1,13 @@
import { makeStyles } from '@material-ui/core'
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 TitleSection from 'src/components/layout/TitleSection'
@ -24,34 +31,66 @@ const styles = {
const useStyles = makeStyles(styles)
const CONTACT_INFORMATION = 'Contact information'
const RECEIPT = 'Receipt'
const COIN_ATM_RADAR = 'Coin ATM Radar'
const TERMS_CONDITIONS = 'Terms & Conditions'
const innerRoutes = [
{
label: 'Contact information',
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 [selected, setSelected] = useState(CONTACT_INFORMATION)
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 (
<>
<TitleSection title="Operator information"></TitleSection>
<Grid container className={classes.grid}>
<Sidebar
data={pages}
data={innerRoutes}
isSelected={isSelected}
displayName={it => it}
onClick={it => setSelected(it)}
displayName={it => it.label}
onClick={onClick}
/>
<div className={classes.content}>
{isSelected(CONTACT_INFORMATION) && <ContactInfo />}
{isSelected(RECEIPT) && <ReceiptPrinting />}
{isSelected(TERMS_CONDITIONS) && <TermsConditions />}
{isSelected(COIN_ATM_RADAR) && <CoinAtmRadar />}
<Routes />
</div>
</Grid>
</>

View file

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

View file

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