fix: get triggers up to spec

This commit is contained in:
Taranto 2020-08-08 12:05:52 +01:00 committed by Josh Harvey
parent b07c0e180a
commit 0b28e7f98a
22 changed files with 347 additions and 95 deletions

View file

@ -9,13 +9,14 @@ import React, { useEffect, useState, memo } from 'react'
import { Button, IconButton } from 'src/components/buttons'
import { ReactComponent as CloseIcon } from 'src/styling/icons/action/close/zodiac.svg'
import { fontSize3 } from 'src/styling/variables'
import { TextInput } from './inputs'
import { H4, P } from './typography'
const useStyles = makeStyles({
label: {
fontSize: 16
fontSize: fontSize3
},
spacing: {
padding: 32

View file

@ -24,6 +24,25 @@ const styles = {
borderRadius: 8,
outline: 0
}),
infoPanelWrapper: ({ width, infoPanelHeight }) => ({
width,
height: infoPanelHeight,
marginTop: 16,
display: 'flex',
flexDirection: 'column',
minHeight: infoPanelHeight ?? 200,
maxHeight: '90vh',
overflowY: 'auto',
borderRadius: 8,
outline: 0
}),
panelContent: {
width: '100%',
display: 'flex',
flexDirection: 'column',
flex: 1,
padding: [[0, 24]]
},
content: ({ small }) => ({
width: '100%',
display: 'flex',
@ -48,17 +67,24 @@ const useStyles = makeStyles(styles)
const Modal = ({
width,
height,
infoPanelHeight,
title,
small,
infoPanel,
handleClose,
children,
secondaryModal,
className,
closeOnEscape,
closeOnBackdropClick,
...props
}) => {
const classes = useStyles({ width, height, small })
const classes = useStyles({
width,
height,
small,
infoPanelHeight
})
const TitleCase = small ? H4 : H1
const closeSize = small ? 16 : 20
@ -84,8 +110,8 @@ const Modal = ({
<div className={classes.content}>{children}</div>
</Paper>
{infoPanel && (
<Paper className={classnames(classes.wrapper, className)}>
{infoPanel}
<Paper className={classnames(classes.infoPanelWrapper, className)}>
<div className={classes.panelContent}>{infoPanel}</div>
</Paper>
)}
</>

View file

@ -1,4 +1,5 @@
import { makeStyles } from '@material-ui/core'
import classnames from 'classnames'
import { Field, useFormikContext } from 'formik'
import * as R from 'ramda'
import React, { useContext } from 'react'
@ -158,7 +159,7 @@ const groupStriped = elements => {
)
}
const ERow = ({ editing, disabled }) => {
const ERow = ({ editing, disabled, lastOfGroup }) => {
const { errors } = useFormikContext()
const {
elements,
@ -169,6 +170,8 @@ const ERow = ({ editing, disabled }) => {
stripeWhen
} = useContext(TableCtx)
const classes = useStyles()
const { values } = useFormikContext()
const shouldStripe = stripeWhen && stripeWhen(values) && !editing
@ -187,8 +190,13 @@ const ERow = ({ editing, disabled }) => {
it => it.editable === undefined || it.editable
)
const classNames = {
[classes.lastOfGroup]: lastOfGroup
}
return (
<Tr
className={classnames(classNames)}
size={rowSize}
error={errors && errors.length}
errorMessage={errors && errors.toString()}>

View file

@ -4,6 +4,9 @@ export default {
cancelButton: {
marginRight: 20
},
lastOfGroup: {
marginBottom: 24
},
extraPaddingLeft: {
paddingLeft: 35
},

View file

@ -47,6 +47,8 @@ const ETable = ({
setEditing,
stripeWhen,
disableRowEdit,
groupBy,
sortBy,
createText = 'Add override'
}) => {
const [editingId, setEditingId] = useState(null)
@ -102,6 +104,8 @@ const ETable = ({
const canAdd = !forceDisable && !editingId && !disableAdd && !adding
const showTable = adding || data.length !== 0
const innerData = sortBy ? R.sortWith(sortBy)(data) : data
const ctxValue = {
elements,
enableEdit,
@ -159,24 +163,33 @@ const ETable = ({
</Form>
</Formik>
)}
{data.map((it, idx) => (
<Formik
key={it.id ?? idx}
enableReinitialize
initialValues={it}
onReset={onReset}
validationSchema={validationSchema}
onSubmit={innerSave}>
<Form>
<ERow
editing={editingId === it.id}
disabled={
forceDisable || (editingId && editingId !== it.id)
}
/>
</Form>
</Formik>
))}
{innerData.map((it, idx) => {
const nextElement = innerData[idx + 1]
const isLastOfGroup =
groupBy &&
nextElement &&
nextElement[groupBy] !== it[groupBy]
return (
<Formik
key={it.id ?? idx}
enableReinitialize
initialValues={it}
onReset={onReset}
validationSchema={validationSchema}
onSubmit={innerSave}>
<Form>
<ERow
lastOfGroup={isLastOfGroup}
editing={editingId === it.id}
disabled={
forceDisable || (editingId && editingId !== it.id)
}
/>
</Form>
</Formik>
)
})}
</TBody>
</Table>
</>

View file

@ -4,7 +4,7 @@ import CheckBoxIcon from '@material-ui/icons/CheckBox'
import CheckBoxOutlineBlankIcon from '@material-ui/icons/CheckBoxOutlineBlank'
import React from 'react'
import { secondaryColor } from '../../../styling/variables'
import { fontSize2, fontSize3, secondaryColor } from 'src/styling/variables'
const useStyles = makeStyles({
root: {
@ -30,9 +30,11 @@ const CheckboxInput = ({ name, onChange, value, label, ...props }) => {
value={value}
checked={value}
icon={
<CheckBoxOutlineBlankIcon style={{ marginLeft: 2, fontSize: 16 }} />
<CheckBoxOutlineBlankIcon
style={{ marginLeft: 2, fontSize: fontSize3 }}
/>
}
checkedIcon={<CheckBoxIcon style={{ fontSize: 20 }} />}
checkedIcon={<CheckBoxIcon style={{ fontSize: fontSize2 }} />}
disableRipple
{...props}
/>

View file

@ -3,7 +3,7 @@ import { secondaryColor } from 'src/styling/variables'
export default {
size: ({ size }) => ({
marginTop: size === 'lg' ? -2 : 0,
marginTop: size === 'lg' ? 0 : 2,
...bySize(size)
}),
bold,

View file

@ -47,7 +47,7 @@ const Row = ({
return (
<div className={classes.rowWrapper}>
<div className={classnames({ [classes.before]: expanded })}>
<div className={classnames({ [classes.before]: expanded && id !== 0 })}>
<Tr
className={classnames(trClasses)}
onClick={() => {

View file

@ -12,6 +12,7 @@ export default {
padding: 1
},
row: {
border: [[2, 'solid', 'transparent']],
borderRadius: 0
},
expanded: {

View file

@ -66,6 +66,21 @@ function H4({ children, noMargin, className, ...props }) {
)
}
function H5({ children, noMargin, className, ...props }) {
const classes = useStyles()
const classNames = {
[classes.h5]: true,
[classes.noMargin]: noMargin,
[className]: !!className
}
return (
<h5 className={classnames(classNames)} {...props}>
{children}
</h5>
)
}
const P = pBuilder('p')
const Info1 = pBuilder('info1')
const Info2 = pBuilder('info2')
@ -99,6 +114,7 @@ export {
H2,
H3,
H4,
H5,
TL1,
TL2,
P,

View file

@ -11,7 +11,7 @@ import {
} from 'src/styling/variables'
const base = {
lineHeight: '110%',
lineHeight: '120%',
color: fontColor
}
@ -40,6 +40,12 @@ export default {
fontFamily: fontPrimary,
fontWeight: 700
},
h5: {
extend: base,
fontSize: fontSize3,
fontFamily: fontPrimary,
fontWeight: 700
},
p: {
extend: base,
fontSize: fontSize4,

View file

@ -75,7 +75,7 @@ const getOverridesFields = (getData, currency) => {
{
name: 'fixedFee',
display: 'Fixed fee',
width: 140,
width: 144,
input: NumberInput,
doubleHeader: 'Cash-in only',
textAlign: 'right',
@ -87,7 +87,7 @@ const getOverridesFields = (getData, currency) => {
{
name: 'minimumTx',
display: 'Minimun Tx',
width: 140,
width: 144,
input: NumberInput,
doubleHeader: 'Cash-in only',
textAlign: 'right',

View file

@ -1,6 +1,6 @@
import typographyStyles from 'src/components/typography/styles'
import baseStyles from 'src/pages/Logs.styles'
import { zircon, primaryColor } from 'src/styling/variables'
import { zircon, primaryColor, fontSize4 } from 'src/styling/variables'
const { label1 } = typographyStyles
const { titleWrapper, titleAndButtonsContainer } = baseStyles
@ -30,7 +30,7 @@ export default {
},
p: {
fontFamily: 'MuseoSans',
fontSize: 14,
fontSize: fontSize4,
fontWeight: 500,
fontStretch: 'normal',
fontStyle: 'normal',

View file

@ -5,7 +5,8 @@ import {
tomato,
spring3,
spring4,
comet
comet,
fontSize5
} from 'src/styling/variables'
const propertyCardStyles = {
@ -25,7 +26,7 @@ const propertyCardStyles = {
},
label1: {
fontFamily: 'MuseoSans',
fontSize: 12,
fontSize: fontSize5,
fontWeight: 500,
fontStretch: 'normal',
fontStyle: 'normal',

View file

@ -1,3 +1,5 @@
import { fontSize5 } from 'src/styling/variables'
export default {
titleWrapper: {
display: 'flex',
@ -41,7 +43,7 @@ export default {
margin: 8,
display: 'flex',
alignItems: 'center',
fontSize: 12,
fontSize: fontSize5,
padding: [[0, 12]]
},
shareIcon: {

View file

@ -1,13 +1,13 @@
import { fade } from '@material-ui/core/styles/colorManipulator'
import { offColor, comet } from 'src/styling/variables'
import { fontSize4, offColor, comet } from 'src/styling/variables'
export default {
wrapper: {
display: 'flex',
marginTop: 24,
marginBottom: 32,
fontSize: 14
fontSize: fontSize4
},
column1: {
width: 600

View file

@ -118,17 +118,17 @@ const Transactions = () => {
},
{
header: 'Date (UTC)',
view: it => moment.utc(it.created).format('YYYY-MM-D'),
view: it => moment.utc(it.created).format('YYYY-MM-DD'),
textAlign: 'right',
size: 'sm',
width: 124
width: 144
},
{
header: 'Time (UTC)',
view: it => moment.utc(it.created).format('HH:mm:ss'),
textAlign: 'right',
size: 'sm',
width: 124
width: 144
}
]

View file

@ -8,10 +8,11 @@ import { v4 } from 'uuid'
import Title from 'src/components/Title'
import { Link } from 'src/components/buttons'
import { Table as EditableTable } from 'src/components/editableTable'
import { fromNamespace, namespaces } from 'src/utils/config'
import { mainStyles } from './Triggers.styles'
import Wizard from './Wizard'
import { Schema, elements } from './helper'
import { Schema, elements, sortBy } from './helper'
const useStyles = makeStyles(mainStyles)
@ -28,6 +29,7 @@ const GET_INFO = gql`
`
const Triggers = () => {
const classes = useStyles()
const [wizard, setWizard] = useState(false)
const [error, setError] = useState(false)
@ -51,7 +53,9 @@ const Triggers = () => {
return saveConfig({ variables: { config } })
}
const classes = useStyles()
const currency = R.path(['fiatCurrency'])(
fromNamespace(namespaces.LOCALE)(data?.config)
)
return (
<>
@ -69,13 +73,20 @@ const Triggers = () => {
data={triggers}
name="triggers"
enableEdit
sortBy={sortBy}
groupBy="triggerType"
enableDelete
save={save}
validationSchema={Schema}
elements={elements}
/>
{wizard && (
<Wizard error={error} save={add} onClose={() => setWizard(null)} />
<Wizard
currency={currency}
error={error}
save={add}
onClose={() => setWizard(null)}
/>
)}
</>
)

View file

@ -1,12 +1,14 @@
import { makeStyles } from '@material-ui/core'
import { Form, Formik } from 'formik'
import { Form, Formik, useFormikContext } from 'formik'
import * as R from 'ramda'
import React, { useState, Fragment } from 'react'
import React, { useState, Fragment, useEffect } from 'react'
import ErrorMessage from 'src/components/ErrorMessage'
import Modal from 'src/components/Modal'
import Stepper from 'src/components/Stepper'
import { Button } from 'src/components/buttons'
import { H5, Info3 } from 'src/components/typography'
import { comet } from 'src/styling/variables'
import { direction, type, requirements } from './helper'
@ -28,6 +30,12 @@ const styles = {
height: '100%',
display: 'flex',
flexDirection: 'column'
},
infoTitle: {
margin: [[18, 0, 20, 0]]
},
infoCurrentText: {
color: comet
}
}
@ -46,9 +54,118 @@ const getStep = step => {
}
}
const Wizard = ({ machine, onClose, save, error }) => {
const getText = (step, config, currency) => {
switch (step) {
case 1:
return `In ${getDirectionText(config)} transactions`
case 2:
return `if the user ${getTypeText(config, currency)}`
case 3:
return `the user will be ${getRequirementText(config)}.`
default:
return ''
}
}
const orUnderline = value => {
return R.isEmpty(value) || R.isNil(value) ? '⎼⎼⎼⎼⎼ ' : value
}
const getDirectionText = config => {
switch (config.cashDirection) {
case 'both':
return 'both cash-in and cash-out'
case 'cashIn':
return 'cash-in'
case 'cashOut':
return 'cash-out'
default:
return orUnderline(null)
}
}
const getTypeText = (config, currency) => {
switch (config.triggerType) {
case 'txAmount':
return `makes a single transaction over ${orUnderline(
config.threshold
)} ${currency}`
case 'txVolume':
return `makes transactions over ${orUnderline(
config.threshold
)} ${currency} in ${orUnderline(config.days)} days`
case 'txVelocity':
return `makes ${orUnderline(
config.threshold
)} transactions in ${orUnderline(config.days)} days`
case 'consecutiveDays':
return `at least one transaction every day for ${orUnderline(
config.days
)} days`
default:
return ''
}
}
const getRequirementText = config => {
switch (config.requirement) {
case 'sms':
return 'asked to enter code provided through SMS verification'
case 'idPhoto':
return 'asked to scan a ID with photo'
case 'idData':
return 'asked to scan a ID'
case 'facephoto':
return 'asked to have a photo taken'
case 'sanctions':
return 'matched against the OFAC sanctions list'
case 'superuser':
return ''
case 'suspend':
return 'suspended'
case 'block':
return 'blocked'
default:
return orUnderline(null)
}
}
const InfoPanel = ({ step, config = {}, liveValues = {}, currency }) => {
const classes = useStyles()
const oldText = R.range(1, step)
.map(it => getText(it, config, currency))
.join(', ')
const newText = getText(step, liveValues, currency)
const isLastStep = step === LAST_STEP
return (
<>
<H5 className={classes.infoTitle}>Trigger overview so far</H5>
<Info3 noMargin>
{oldText}
{step !== 1 && ', '}
<span className={classes.infoCurrentText}>{newText}</span>
{!isLastStep && '...'}
</Info3>
</>
)
}
const GetValues = ({ setValues }) => {
const { values } = useFormikContext()
useEffect(() => {
console.log('triggered')
setValues && values && setValues(values)
}, [setValues, values])
return null
}
const Wizard = ({ onClose, save, error, currency }) => {
const classes = useStyles()
const [liveValues, setLiveValues] = useState({})
const [{ step, config }, setState] = useState({
step: 1
})
@ -70,33 +187,45 @@ const Wizard = ({ machine, onClose, save, error }) => {
}
return (
<Modal
title="New compliance trigger"
handleClose={onClose}
width={520}
height={480}
open={true}>
<Stepper
className={classes.stepper}
steps={LAST_STEP}
currentStep={step}
/>
<Formik
enableReinitialize
onSubmit={onContinue}
initialValues={stepOptions.initialValues}
validationSchema={stepOptions.schema}>
<Form className={classes.form}>
<stepOptions.Component />
<div className={classes.submit}>
{error && <ErrorMessage>Failed to save</ErrorMessage>}
<Button className={classes.button} type="submit">
{isLastStep ? 'Finish' : 'Next'}
</Button>
</div>
</Form>
</Formik>
</Modal>
<>
<Modal
title="New compliance trigger"
handleClose={onClose}
width={520}
height={480}
infoPanel={
<InfoPanel
currency={currency}
step={step}
config={config}
liveValues={liveValues}
/>
}
infoPanelHeight={172}
open={true}>
<Stepper
className={classes.stepper}
steps={LAST_STEP}
currentStep={step}
/>
<Formik
enableReinitialize
onSubmit={onContinue}
initialValues={stepOptions.initialValues}
validationSchema={stepOptions.schema}>
<Form onChange={console.log} className={classes.form}>
<GetValues setValues={setLiveValues} />
<stepOptions.Component />
<div className={classes.submit}>
{error && <ErrorMessage>Failed to save</ErrorMessage>}
<Button className={classes.button} type="submit">
{isLastStep ? 'Finish' : 'Next'}
</Button>
</div>
</Form>
</Formik>
</Modal>
</>
)
}

View file

@ -8,6 +8,8 @@ import * as Yup from 'yup'
import { TextInput, RadioGroup } from 'src/components/inputs/formik'
import Autocomplete from 'src/components/inputs/formik/Autocomplete'
import { H4 } from 'src/components/typography'
import { ReactComponent as TxInIcon } from 'src/styling/icons/direction/cash-in.svg'
import { ReactComponent as TxOutIcon } from 'src/styling/icons/direction/cash-out.svg'
import { errorColor } from 'src/styling/variables'
const useStyles = makeStyles({
@ -32,6 +34,12 @@ const useStyles = makeStyles({
specialGrid: {
display: 'grid',
gridTemplateColumns: [[182, 162, 141]]
},
directionIcon: {
marginRight: 2
},
directionName: {
marginLeft: 6
}
})
@ -98,8 +106,8 @@ const typeSchema = Yup.object().shape({
const typeOptions = [
{ display: 'Transaction amount', code: 'txAmount' },
{ display: 'Transaction velocity', code: 'txVelocity' },
{ display: 'Transaction volume', code: 'txVolume' },
{ display: 'Transaction velocity', code: 'txVelocity' },
{ display: 'Consecutive days', code: 'consecutiveDays' }
]
@ -131,9 +139,6 @@ const Type = () => {
size="lg"
name="threshold"
options={typeOptions}
labelClassName={classes.radioLabel}
radioClassName={classes.radio}
className={classes.radioGroup}
/>
</>
)
@ -199,12 +204,29 @@ const getView = (data, code, compare) => it => {
return R.compose(R.prop(code), R.find(R.propEq(compare ?? 'code', it)))(data)
}
const DirectionDisplay = ({ code }) => {
const classes = useStyles()
const displayName = getView(directionOptions, 'display')(code)
const showCashIn = code === 'cashIn' || code === 'both'
const showCashOut = code === 'cashOut' || code === 'both'
return (
<div>
{showCashOut && <TxOutIcon className={classes.directionIcon} />}
{showCashIn && <TxInIcon className={classes.directionIcon} />}
<span className={classes.directionName}>{displayName}</span>
</div>
)
}
const elements = [
{
name: 'triggerType',
size: 'sm',
width: 271,
input: Autocomplete,
width: 230,
input: ({ field: { value: name } }) => (
<>{getView(typeOptions, 'display')(name)}</>
),
view: getView(typeOptions, 'display'),
inputProps: {
options: typeOptions,
@ -216,8 +238,10 @@ const elements = [
{
name: 'requirement',
size: 'sm',
width: 271,
input: Autocomplete,
width: 230,
input: ({ field: { value: name } }) => (
<>{getView(requirementOptions, 'display')(name)}</>
),
view: getView(requirementOptions, 'display'),
inputProps: {
options: requirementOptions,
@ -229,14 +253,15 @@ const elements = [
{
name: 'threshold',
size: 'sm',
width: 271,
width: 260,
textAlign: 'right',
input: TextInput
},
{
name: 'cashDirection',
size: 'sm',
width: 200,
view: getView(directionOptions, 'display'),
width: 282,
view: it => <DirectionDisplay code={it} />,
input: Autocomplete,
inputProps: {
options: directionOptions,
@ -247,4 +272,12 @@ const elements = [
}
]
export { Schema, elements, direction, type, requirements }
const triggerOrder = R.map(R.prop('code'))(typeOptions)
const sortBy = [
R.comparator(
(a, b) =>
triggerOrder.indexOf(a.triggerType) < triggerOrder.indexOf(b.triggerType)
)
]
export { Schema, elements, direction, type, requirements, sortBy }

View file

@ -63,11 +63,11 @@ const fontPrimary = 'Mont'
const fontSecondary = 'MuseoSans'
const fontMonospaced = 'BPmono'
let fontSize1 = 24
let fontSize2 = 20
let fontSize3 = 16
let fontSize4 = 14
let fontSize5 = 12
let fontSize1 = 25
let fontSize2 = 21
let fontSize3 = 17
let fontSize4 = 15
let fontSize5 = 13
if (version === 8) {
fontSize1 = 32

View file

@ -5,7 +5,6 @@ Overall:
- validation is bad rn, negatives being allowed
- locale based mil separators 1.000 1,000
- Table should be loaded on slow internet (we want to load the table with no data)
- font sizes could be better
- tooltip like components should close on esc
- saving should be a one time thing. disable buttons so user doesnt spam it
- disable edit on non-everrides => overrides
@ -21,9 +20,6 @@ Locale:
Notifications:
- one of the crypto balance alerts has to be optional because of migration
Machine status:
- font-size of the 'write to confirm'
Server:
- Takes too long to load. Investigate
@ -47,3 +43,7 @@ Compliance:
Ideas
- Transactions could have a link to the customer
- Transactions table on customer should have a link to "transactions"
Feedback needed
- font sizes could be better (I've bumped all font sizes by 1px, looks pretty good as fonts do a good vertical bump in size. Maybe some of the fonts don't like even values)