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:
parent
dbfb37a756
commit
3c0f4ec194
10 changed files with 173 additions and 129 deletions
16
new-lamassu-admin/src/components/PromptWhenDirty.js
Normal file
16
new-lamassu-admin/src/components/PromptWhenDirty.js
Normal 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
|
||||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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)}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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')}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue