2021-04-14 00:57:32 +00:00
|
|
|
import Button from 'react-bootstrap/Button'
|
|
|
|
import InputGroup from 'react-bootstrap/InputGroup'
|
|
|
|
import BootstrapForm from 'react-bootstrap/Form'
|
|
|
|
import Alert from 'react-bootstrap/Alert'
|
2022-07-30 13:25:46 +00:00
|
|
|
import { Formik, Form as FormikForm, useFormikContext, useField, FieldArray } from 'formik'
|
2022-01-07 18:28:23 +00:00
|
|
|
import React, { useEffect, useRef, useState } from 'react'
|
2021-05-13 13:28:38 +00:00
|
|
|
import copy from 'clipboard-copy'
|
|
|
|
import Thumb from '../svgs/thumb-up-fill.svg'
|
2022-07-30 13:25:46 +00:00
|
|
|
import { Col, Nav } from 'react-bootstrap'
|
2021-07-01 23:51:58 +00:00
|
|
|
import Markdown from '../svgs/markdown-line.svg'
|
|
|
|
import styles from './form.module.css'
|
|
|
|
import Text from '../components/text'
|
2022-07-30 13:25:46 +00:00
|
|
|
import AddIcon from '../svgs/add-fill.svg'
|
2022-08-10 15:06:31 +00:00
|
|
|
import { mdHas } from '../lib/md'
|
2021-04-14 00:57:32 +00:00
|
|
|
|
2022-02-17 17:23:43 +00:00
|
|
|
export function SubmitButton ({
|
|
|
|
children, variant, value, onClick, ...props
|
|
|
|
}) {
|
2021-09-10 18:55:36 +00:00
|
|
|
const { isSubmitting, setFieldValue } = useFormikContext()
|
2021-04-14 00:57:32 +00:00
|
|
|
return (
|
|
|
|
<Button
|
|
|
|
variant={variant || 'main'}
|
|
|
|
type='submit'
|
|
|
|
disabled={isSubmitting}
|
2021-09-10 18:55:36 +00:00
|
|
|
onClick={value
|
|
|
|
? e => {
|
|
|
|
setFieldValue('submit', value)
|
|
|
|
onClick && onClick(e)
|
|
|
|
}
|
|
|
|
: onClick}
|
2021-04-14 00:57:32 +00:00
|
|
|
{...props}
|
|
|
|
>
|
|
|
|
{children}
|
|
|
|
</Button>
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2021-05-13 13:28:38 +00:00
|
|
|
export function CopyInput (props) {
|
|
|
|
const [copied, setCopied] = useState(false)
|
|
|
|
|
|
|
|
const handleClick = () => {
|
|
|
|
copy(props.placeholder)
|
|
|
|
setCopied(true)
|
|
|
|
setTimeout(() => setCopied(false), 1500)
|
|
|
|
}
|
|
|
|
|
|
|
|
return (
|
|
|
|
<Input
|
|
|
|
onClick={handleClick}
|
2021-06-02 23:15:28 +00:00
|
|
|
append={
|
|
|
|
<Button
|
|
|
|
size={props.size}
|
|
|
|
onClick={handleClick}
|
|
|
|
>
|
|
|
|
{copied ? <Thumb width={18} height={18} /> : 'copy'}
|
|
|
|
</Button>
|
|
|
|
}
|
2021-05-13 13:28:38 +00:00
|
|
|
{...props}
|
|
|
|
/>
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2022-01-23 17:21:55 +00:00
|
|
|
export function InputSkeleton ({ label, hint }) {
|
2021-05-13 13:28:38 +00:00
|
|
|
return (
|
|
|
|
<BootstrapForm.Group>
|
|
|
|
{label && <BootstrapForm.Label>{label}</BootstrapForm.Label>}
|
2021-11-04 18:22:03 +00:00
|
|
|
<div className='form-control clouds' />
|
2022-01-23 17:21:55 +00:00
|
|
|
{hint &&
|
|
|
|
<BootstrapForm.Text>
|
|
|
|
{hint}
|
|
|
|
</BootstrapForm.Text>}
|
2021-05-13 13:28:38 +00:00
|
|
|
</BootstrapForm.Group>
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2022-08-10 15:06:31 +00:00
|
|
|
export function MarkdownInput ({ label, topLevel, groupClassName, onChange, setHasImgLink, ...props }) {
|
2021-07-01 23:51:58 +00:00
|
|
|
const [tab, setTab] = useState('write')
|
|
|
|
const [, meta] = useField(props)
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
!meta.value && setTab('write')
|
|
|
|
}, [meta.value])
|
|
|
|
|
|
|
|
return (
|
|
|
|
<FormGroup label={label} className={groupClassName}>
|
|
|
|
<div className={`${styles.markdownInput} ${tab === 'write' ? styles.noTopLeftRadius : ''}`}>
|
|
|
|
<Nav variant='tabs' defaultActiveKey='write' activeKey={tab} onSelect={tab => setTab(tab)}>
|
|
|
|
<Nav.Item>
|
|
|
|
<Nav.Link eventKey='write'>write</Nav.Link>
|
|
|
|
</Nav.Item>
|
|
|
|
<Nav.Item>
|
|
|
|
<Nav.Link eventKey='preview' disabled={!meta.value}>preview</Nav.Link>
|
|
|
|
</Nav.Item>
|
|
|
|
<a
|
|
|
|
className='ml-auto text-muted d-flex align-items-center'
|
|
|
|
href='https://guides.github.com/features/mastering-markdown/' target='_blank' rel='noreferrer'
|
|
|
|
>
|
|
|
|
<Markdown width={18} height={18} />
|
|
|
|
</a>
|
|
|
|
</Nav>
|
|
|
|
<div className={tab !== 'write' ? 'd-none' : ''}>
|
|
|
|
<InputInner
|
2022-08-10 15:06:31 +00:00
|
|
|
{...props} onChange={(formik, e) => {
|
|
|
|
if (onChange) onChange(formik, e)
|
|
|
|
if (setHasImgLink) {
|
|
|
|
setHasImgLink(mdHas(e.target.value, ['link', 'image']))
|
|
|
|
}
|
|
|
|
}}
|
2021-07-01 23:51:58 +00:00
|
|
|
/>
|
|
|
|
</div>
|
2021-08-10 22:59:06 +00:00
|
|
|
<div className={tab !== 'preview' ? 'd-none' : 'form-group'}>
|
|
|
|
<div className={`${styles.text} form-control`}>
|
2022-07-17 15:33:55 +00:00
|
|
|
{tab === 'preview' && <Text topLevel={topLevel} noFragments>{meta.value}</Text>}
|
2021-07-01 23:51:58 +00:00
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</FormGroup>
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
function FormGroup ({ className, label, children }) {
|
2021-04-14 00:57:32 +00:00
|
|
|
return (
|
2021-07-01 23:51:58 +00:00
|
|
|
<BootstrapForm.Group className={className}>
|
2021-04-14 00:57:32 +00:00
|
|
|
{label && <BootstrapForm.Label>{label}</BootstrapForm.Label>}
|
2021-07-01 23:51:58 +00:00
|
|
|
{children}
|
|
|
|
</BootstrapForm.Group>
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2022-01-07 18:28:23 +00:00
|
|
|
function InputInner ({
|
|
|
|
prepend, append, hint, showValid, onChange, overrideValue,
|
2022-08-18 18:15:24 +00:00
|
|
|
innerRef, storageKeyPrefix, noForm, ...props
|
2022-01-07 18:28:23 +00:00
|
|
|
}) {
|
2022-08-18 18:15:24 +00:00
|
|
|
const [field, meta, helpers] = noForm ? [{}, {}, {}] : useField(props)
|
|
|
|
const formik = noForm ? null : useFormikContext()
|
2021-08-22 15:25:17 +00:00
|
|
|
|
2022-01-07 18:28:23 +00:00
|
|
|
const storageKey = storageKeyPrefix ? storageKeyPrefix + '-' + props.name : undefined
|
|
|
|
|
2021-08-22 15:25:17 +00:00
|
|
|
useEffect(() => {
|
|
|
|
if (overrideValue) {
|
|
|
|
helpers.setValue(overrideValue)
|
2022-01-07 18:28:23 +00:00
|
|
|
if (storageKey) {
|
2022-01-08 15:31:37 +00:00
|
|
|
localStorage.setItem(storageKey, overrideValue)
|
2022-01-07 18:28:23 +00:00
|
|
|
}
|
|
|
|
} else if (storageKey) {
|
2022-01-08 15:31:37 +00:00
|
|
|
const draft = localStorage.getItem(storageKey)
|
2022-01-07 18:28:23 +00:00
|
|
|
if (draft) {
|
|
|
|
// for some reason we have to turn off validation to get formik to
|
|
|
|
// not assume this is invalid
|
|
|
|
helpers.setValue(draft, false)
|
|
|
|
}
|
2021-08-22 15:25:17 +00:00
|
|
|
}
|
|
|
|
}, [overrideValue])
|
|
|
|
|
2021-07-01 23:51:58 +00:00
|
|
|
return (
|
|
|
|
<>
|
2021-04-14 00:57:32 +00:00
|
|
|
<InputGroup hasValidation>
|
|
|
|
{prepend && (
|
|
|
|
<InputGroup.Prepend>
|
2021-05-13 13:28:38 +00:00
|
|
|
{prepend}
|
2021-04-14 00:57:32 +00:00
|
|
|
</InputGroup.Prepend>
|
|
|
|
)}
|
|
|
|
<BootstrapForm.Control
|
2021-08-19 21:13:33 +00:00
|
|
|
onKeyDown={(e) => {
|
|
|
|
if (e.keyCode === 13 && (e.metaKey || e.ctrlKey)) {
|
|
|
|
formik?.submitForm()
|
|
|
|
}
|
|
|
|
}}
|
2021-09-10 18:55:36 +00:00
|
|
|
ref={innerRef}
|
2021-04-14 00:57:32 +00:00
|
|
|
{...field} {...props}
|
2021-08-22 15:25:17 +00:00
|
|
|
onChange={(e) => {
|
|
|
|
field.onChange(e)
|
2022-01-07 18:28:23 +00:00
|
|
|
|
|
|
|
if (storageKey) {
|
|
|
|
localStorage.setItem(storageKey, e.target.value)
|
|
|
|
}
|
|
|
|
|
2021-08-22 15:25:17 +00:00
|
|
|
if (onChange) {
|
|
|
|
onChange(formik, e)
|
|
|
|
}
|
|
|
|
}}
|
2021-04-14 00:57:32 +00:00
|
|
|
isInvalid={meta.touched && meta.error}
|
2022-04-19 18:32:39 +00:00
|
|
|
isValid={showValid && meta.initialValue !== meta.value && meta.touched && !meta.error}
|
2021-04-14 00:57:32 +00:00
|
|
|
/>
|
|
|
|
{append && (
|
|
|
|
<InputGroup.Append>
|
2021-05-13 13:28:38 +00:00
|
|
|
{append}
|
2021-04-14 00:57:32 +00:00
|
|
|
</InputGroup.Append>
|
|
|
|
)}
|
|
|
|
<BootstrapForm.Control.Feedback type='invalid'>
|
|
|
|
{meta.touched && meta.error}
|
|
|
|
</BootstrapForm.Control.Feedback>
|
|
|
|
</InputGroup>
|
2021-05-25 00:08:56 +00:00
|
|
|
{hint && (
|
|
|
|
<BootstrapForm.Text>
|
|
|
|
{hint}
|
|
|
|
</BootstrapForm.Text>
|
|
|
|
)}
|
2021-07-01 23:51:58 +00:00
|
|
|
</>
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
export function Input ({ label, groupClassName, ...props }) {
|
|
|
|
return (
|
|
|
|
<FormGroup label={label} className={groupClassName}>
|
|
|
|
<InputInner {...props} />
|
|
|
|
</FormGroup>
|
2021-04-14 00:57:32 +00:00
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2022-08-18 18:15:24 +00:00
|
|
|
export function VariableInput ({ label, groupClassName, name, hint, max, readOnlyLen, ...props }) {
|
2022-07-30 13:25:46 +00:00
|
|
|
return (
|
|
|
|
<FormGroup label={label} className={groupClassName}>
|
|
|
|
<FieldArray name={name}>
|
|
|
|
{({ form, ...fieldArrayHelpers }) => {
|
2022-08-18 18:15:24 +00:00
|
|
|
const options = form.values[name]
|
2022-07-30 13:25:46 +00:00
|
|
|
return (
|
|
|
|
<>
|
|
|
|
{options.map((_, i) => (
|
|
|
|
<div key={i}>
|
|
|
|
<BootstrapForm.Row className='mb-2'>
|
|
|
|
<Col>
|
2022-08-18 18:15:24 +00:00
|
|
|
<InputInner name={`${name}[${i}]`} {...props} readOnly={i < readOnlyLen} placeholder={i > 1 ? 'optional' : undefined} />
|
2022-07-30 13:25:46 +00:00
|
|
|
</Col>
|
|
|
|
{options.length - 1 === i && options.length !== max
|
|
|
|
? <AddIcon className='fill-grey align-self-center pointer mx-2' onClick={() => fieldArrayHelpers.push('')} />
|
|
|
|
: null}
|
|
|
|
</BootstrapForm.Row>
|
|
|
|
</div>
|
|
|
|
))}
|
|
|
|
</>
|
|
|
|
)
|
|
|
|
}}
|
|
|
|
</FieldArray>
|
|
|
|
{hint && (
|
|
|
|
<BootstrapForm.Text>
|
|
|
|
{hint}
|
|
|
|
</BootstrapForm.Text>
|
|
|
|
)}
|
|
|
|
</FormGroup>
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2022-03-07 21:50:13 +00:00
|
|
|
export function Checkbox ({ children, label, groupClassName, hiddenLabel, extra, handleChange, inline, ...props }) {
|
2021-09-12 16:55:38 +00:00
|
|
|
// React treats radios and checkbox inputs differently other input types, select, and textarea.
|
|
|
|
// Formik does this too! When you specify `type` to useField(), it will
|
|
|
|
// return the correct bag of props for you
|
2021-12-16 17:27:12 +00:00
|
|
|
const [field] = useField({ ...props, type: 'checkbox' })
|
2021-09-12 16:55:38 +00:00
|
|
|
return (
|
2022-03-07 21:50:13 +00:00
|
|
|
<BootstrapForm.Group className={groupClassName}>
|
|
|
|
{hiddenLabel && <BootstrapForm.Label className='invisible'>{label}</BootstrapForm.Label>}
|
|
|
|
<BootstrapForm.Check
|
|
|
|
custom
|
|
|
|
id={props.id || props.name}
|
|
|
|
inline={inline}
|
|
|
|
>
|
|
|
|
<BootstrapForm.Check.Input
|
|
|
|
{...field} {...props} type='checkbox' onChange={(e) => {
|
|
|
|
field.onChange(e)
|
|
|
|
handleChange && handleChange(e.target.checked)
|
|
|
|
}}
|
|
|
|
/>
|
|
|
|
<BootstrapForm.Check.Label className='d-flex'>
|
|
|
|
<div className='flex-grow-1'>{label}</div>
|
|
|
|
{extra &&
|
|
|
|
<div className={styles.checkboxExtra}>
|
|
|
|
{extra}
|
|
|
|
</div>}
|
|
|
|
</BootstrapForm.Check.Label>
|
|
|
|
</BootstrapForm.Check>
|
|
|
|
</BootstrapForm.Group>
|
2021-09-12 16:55:38 +00:00
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2021-04-14 00:57:32 +00:00
|
|
|
export function Form ({
|
2022-01-07 18:28:23 +00:00
|
|
|
initial, schema, onSubmit, children, initialError, validateImmediately, storageKeyPrefix, ...props
|
2021-04-14 00:57:32 +00:00
|
|
|
}) {
|
2021-05-13 21:19:51 +00:00
|
|
|
const [error, setError] = useState(initialError)
|
2021-04-14 00:57:32 +00:00
|
|
|
|
|
|
|
return (
|
|
|
|
<Formik
|
|
|
|
initialValues={initial}
|
|
|
|
validationSchema={schema}
|
2021-05-25 00:08:56 +00:00
|
|
|
initialTouched={validateImmediately && initial}
|
|
|
|
validateOnBlur={false}
|
2022-07-30 13:25:46 +00:00
|
|
|
onSubmit={async (values, ...args) =>
|
|
|
|
onSubmit && onSubmit(values, ...args).then(() => {
|
2022-01-07 18:28:23 +00:00
|
|
|
if (!storageKeyPrefix) return
|
2022-07-30 13:25:46 +00:00
|
|
|
Object.keys(values).forEach(v => {
|
|
|
|
localStorage.removeItem(storageKeyPrefix + '-' + v)
|
|
|
|
if (Array.isArray(values[v])) {
|
|
|
|
values[v].forEach(
|
|
|
|
(_, i) => localStorage.removeItem(`${storageKeyPrefix}-${v}[${i}]`))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
)
|
2022-01-07 18:28:23 +00:00
|
|
|
}).catch(e => setError(e.message || e))}
|
2021-04-14 00:57:32 +00:00
|
|
|
>
|
|
|
|
<FormikForm {...props} noValidate>
|
|
|
|
{error && <Alert variant='danger' onClose={() => setError(undefined)} dismissible>{error}</Alert>}
|
2022-01-07 18:28:23 +00:00
|
|
|
{storageKeyPrefix
|
|
|
|
? React.Children.map(children, (child) => {
|
2022-07-17 15:33:55 +00:00
|
|
|
// if child has a type that's a string, it's a dom element and can't get a prop
|
2022-01-07 18:28:23 +00:00
|
|
|
if (child) {
|
2022-07-17 15:33:55 +00:00
|
|
|
let childProps = {}
|
|
|
|
if (typeof child.type !== 'string') {
|
|
|
|
childProps = { storageKeyPrefix }
|
|
|
|
}
|
|
|
|
return React.cloneElement(child, childProps)
|
2022-01-07 18:28:23 +00:00
|
|
|
}
|
|
|
|
})
|
|
|
|
: children}
|
2021-04-14 00:57:32 +00:00
|
|
|
</FormikForm>
|
|
|
|
</Formik>
|
|
|
|
)
|
|
|
|
}
|
2021-04-24 21:05:07 +00:00
|
|
|
|
|
|
|
export function SyncForm ({
|
|
|
|
initial, schema, children, action, ...props
|
|
|
|
}) {
|
|
|
|
const ref = useRef(null)
|
|
|
|
return (
|
|
|
|
<Formik
|
|
|
|
initialValues={initial}
|
|
|
|
validationSchema={schema}
|
|
|
|
validateOnBlur={false}
|
|
|
|
onSubmit={() => ref.current.submit()}
|
|
|
|
>
|
|
|
|
{props => (
|
|
|
|
<form
|
|
|
|
ref={ref}
|
|
|
|
onSubmit={props.handleSubmit}
|
|
|
|
onReset={props.handleReset}
|
|
|
|
action={action}
|
|
|
|
method='POST'
|
|
|
|
noValidate
|
|
|
|
>
|
|
|
|
{children}
|
|
|
|
</form>
|
|
|
|
)}
|
|
|
|
</Formik>
|
|
|
|
)
|
|
|
|
}
|