stacker.news/components/form.js

585 lines
20 KiB
JavaScript
Raw Normal View History

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'
2022-07-30 13:25:46 +00:00
import { Formik, Form as FormikForm, useFormikContext, useField, FieldArray } from 'formik'
import React, { createContext, useContext, 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'
2023-07-24 18:35:05 +00:00
import Col from 'react-bootstrap/Col'
import Dropdown from 'react-bootstrap/Dropdown'
import Nav from 'react-bootstrap/Nav'
import Row from 'react-bootstrap/Row'
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'
2022-08-25 18:46:07 +00:00
import CloseIcon from '../svgs/close-line.svg'
2022-08-26 22:20:09 +00:00
import { useLazyQuery } from '@apollo/client'
import { USER_SEARCH } from '../fragments/users'
import TextareaAutosize from 'react-textarea-autosize'
import { useToast } from './toast'
import { useInvoiceable } from './invoice'
import { numWithUnits } from '../lib/format'
2021-04-14 00:57:32 +00:00
2022-02-17 17:23:43 +00:00
export function SubmitButton ({
children, variant, value, onClick, disabled, cost, ...props
2022-02-17 17:23:43 +00:00
}) {
const formik = useFormikContext()
useEffect(() => {
formik?.setFieldValue('cost', cost)
}, [formik?.setFieldValue, formik?.getFieldProps('cost').value, cost])
2021-04-14 00:57:32 +00:00
return (
<Button
variant={variant || 'main'}
type='submit'
disabled={disabled || formik.isSubmitting}
2021-09-10 18:55:36 +00:00
onClick={value
? e => {
formik.setFieldValue('submit', value)
2023-07-25 14:14:45 +00:00
onClick && onClick(e)
}
2021-09-10 18:55:36 +00:00
: 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
2023-07-24 18:35:05 +00:00
className={styles.appendButton}
2021-06-02 23:15:28 +00:00
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>}
2023-07-31 17:35:58 +00:00
<div className='form-control clouds' style={{ color: 'transparent' }}>.</div>
2022-01-23 17:21:55 +00:00
{hint &&
<BootstrapForm.Text>
{hint}
</BootstrapForm.Text>}
2021-05-13 13:28:38 +00:00
</BootstrapForm.Group>
)
}
export function MarkdownInput ({ label, topLevel, groupClassName, onChange, setHasImgLink, onKeyDown, innerRef, ...props }) {
2021-07-01 23:51:58 +00:00
const [tab, setTab] = useState('write')
const [, meta, helpers] = useField(props)
const [selectionRange, setSelectionRange] = useState({ start: 0, end: 0 })
innerRef = innerRef || useRef(null)
2021-07-01 23:51:58 +00:00
props.as ||= TextareaAutosize
props.rows ||= props.minRows || 6
2021-07-01 23:51:58 +00:00
useEffect(() => {
!meta.value && setTab('write')
}, [meta.value])
useEffect(() => {
if (selectionRange.start <= selectionRange.end && innerRef?.current) {
const { start, end } = selectionRange
const input = innerRef.current
input.setSelectionRange(start, end)
}
}, [innerRef, selectionRange.start, selectionRange.end])
2021-07-01 23:51:58 +00:00
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
2023-07-24 18:35:05 +00:00
className='ms-auto text-muted d-flex align-items-center'
2021-07-01 23:51:58 +00:00
href='https://guides.github.com/features/mastering-markdown/' target='_blank' rel='noreferrer'
>
<Markdown width={18} height={18} />
</a>
</Nav>
{tab === 'write'
? (
<div>
<InputInner
{...props} onChange={(formik, e) => {
if (onChange) onChange(formik, e)
if (setHasImgLink) {
setHasImgLink(mdHas(e.target.value, ['link', 'image']))
}
}}
innerRef={innerRef}
onKeyDown={(e) => {
const metaOrCtrl = e.metaKey || e.ctrlKey
if (metaOrCtrl) {
if (e.key === 'k') {
// some browsers use CTRL+K to focus search bar so we have to prevent that behavior
e.preventDefault()
insertMarkdownLinkFormatting(innerRef.current, helpers.setValue, setSelectionRange)
}
if (e.key === 'b') {
// some browsers use CTRL+B to open bookmarks so we have to prevent that behavior
e.preventDefault()
insertMarkdownBoldFormatting(innerRef.current, helpers.setValue, setSelectionRange)
}
if (e.key === 'i') {
// some browsers might use CTRL+I to do something else so prevent that behavior too
e.preventDefault()
insertMarkdownItalicFormatting(innerRef.current, helpers.setValue, setSelectionRange)
}
if (e.key === 'Tab' && e.altKey) {
e.preventDefault()
insertMarkdownTabFormatting(innerRef.current, helpers.setValue, setSelectionRange)
}
}
if (onKeyDown) onKeyDown(e)
}}
/>
</div>)
: (
<div className='form-group'>
<div className={`${styles.text} form-control`}>
<Text topLevel={topLevel} noFragments fetchOnlyImgProxy={false}>{meta.value}</Text>
</div>
</div>
)}
2021-07-01 23:51:58 +00:00
</div>
</FormGroup>
)
}
function insertMarkdownFormatting (replaceFn, selectFn) {
return function (input, setValue, setSelectionRange) {
const start = input.selectionStart
const end = input.selectionEnd
const val = input.value
const selectedText = val.substring(start, end)
const mdFormatted = replaceFn(selectedText)
const newVal = val.substring(0, start) + mdFormatted + val.substring(end)
setValue(newVal)
// required for undo, see https://stackoverflow.com/a/27028258
document.execCommand('insertText', false, mdFormatted)
// see https://github.com/facebook/react/issues/6483
// for why we don't use `input.setSelectionRange` directly (hint: event order)
setSelectionRange(selectFn ? selectFn(start, end, mdFormatted) : { start: start + mdFormatted.length, end: start + mdFormatted.length })
}
}
const insertMarkdownTabFormatting = insertMarkdownFormatting(
val => `\t${val}`,
(start, end, mdFormatted) => ({ start: start + 1, end: end + 1 }) // move inside tab
)
const insertMarkdownLinkFormatting = insertMarkdownFormatting(
val => `[${val}](url)`,
(start, end, mdFormatted) => (
start === end
? { start: start + 1, end: end + 1 } // move inside brackets
: { start: start + mdFormatted.length - 4, end: start + mdFormatted.length - 1 }) // move to select url part
)
const insertMarkdownBoldFormatting = insertMarkdownFormatting(
val => `**${val}**`,
(start, end, mdFormatted) => ({ start: start + 2, end: end + 2 }) // move inside bold
)
const insertMarkdownItalicFormatting = insertMarkdownFormatting(
val => `_${val}_`,
(start, end, mdFormatted) => ({ start: start + 1, end: end + 1 }) // move inside italic
)
2021-07-01 23:51:58 +00:00
function FormGroup ({ className, label, children }) {
2021-04-14 00:57:32 +00:00
return (
2023-07-24 18:35:05 +00:00
<BootstrapForm.Group className={`form-group ${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, onBlur, overrideValue,
innerRef, noForm, clear, onKeyDown, inputGroupClassName, debounce, maxLength, ...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()
2023-05-11 19:34:42 +00:00
const storageKeyPrefix = useContext(StorageKeyPrefixContext)
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) {
2023-07-25 14:14:45 +00:00
window.localStorage.setItem(storageKey, overrideValue)
2022-01-07 18:28:23 +00:00
}
} else if (storageKey) {
2023-07-25 14:14:45 +00:00
const draft = window.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])
2023-05-11 19:34:42 +00:00
const invalid = (!formik || formik.submitCount > 0) && meta.touched && meta.error
2022-08-25 18:46:07 +00:00
const debounceRef = useRef(-1)
useEffect(() => {
if (debounceRef.current !== -1) {
clearTimeout(debounceRef.current)
}
if (!noForm && !isNaN(debounce) && debounce > 0) {
debounceRef.current = setTimeout(() => formik.validateForm(), debounce)
}
return () => clearTimeout(debounceRef.current)
}, [noForm, formik, field.value])
const remaining = maxLength && maxLength - (field.value || '').length
2021-07-01 23:51:58 +00:00
return (
<>
2023-08-28 19:31:28 +00:00
<InputGroup hasValidation className={inputGroupClassName}>
2023-07-24 18:35:05 +00:00
{prepend}
2021-04-14 00:57:32 +00:00
<BootstrapForm.Control
2021-08-19 21:13:33 +00:00
onKeyDown={(e) => {
const metaOrCtrl = e.metaKey || e.ctrlKey
if (metaOrCtrl) {
if (e.key === 'Enter') formik?.submitForm()
}
2022-08-26 22:20:09 +00:00
if (onKeyDown) onKeyDown(e)
2021-08-19 21:13:33 +00:00
}}
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) {
2023-07-25 14:14:45 +00:00
window.localStorage.setItem(storageKey, e.target.value)
2022-01-07 18:28:23 +00:00
}
2021-08-22 15:25:17 +00:00
if (onChange) {
onChange(formik, e)
}
}}
onBlur={(e) => {
field.onBlur(e)
onBlur && onBlur(e)
}}
2022-08-25 18:46:07 +00:00
isInvalid={invalid}
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
/>
2023-07-24 18:35:05 +00:00
{(clear && field.value) &&
<Button
variant={null}
onClick={(e) => {
helpers.setValue('')
if (storageKey) {
2023-07-25 14:14:45 +00:00
window.localStorage.removeItem(storageKey)
2023-07-24 18:35:05 +00:00
}
if (onChange) {
onChange(formik, { target: { value: '' } })
}
}}
className={`${styles.clearButton} ${styles.appendButton} ${invalid ? styles.isInvalid : ''}`}
><CloseIcon className='fill-grey' height={20} width={20} />
</Button>}
{append}
2021-04-14 00:57:32 +00:00
<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>
)}
{maxLength && !(meta.touched && meta.error && invalid) && (
<BootstrapForm.Text className={remaining < 0 ? 'text-danger' : undefined}>
{`${numWithUnits(remaining, { abbreviate: false, unitSingular: 'character', unitPlural: 'characters' })} remaining`}
</BootstrapForm.Text>
)}
2021-07-01 23:51:58 +00:00
</>
)
}
2022-08-26 22:20:09 +00:00
export function InputUserSuggest ({ label, groupClassName, ...props }) {
const [getSuggestions] = useLazyQuery(USER_SEARCH, {
onCompleted: data => {
setSuggestions({ array: data.searchUsers, index: 0 })
}
})
const INITIAL_SUGGESTIONS = { array: [], index: 0 }
const [suggestions, setSuggestions] = useState(INITIAL_SUGGESTIONS)
const [ovalue, setOValue] = useState()
return (
<FormGroup label={label} className={groupClassName}>
<InputInner
{...props}
autoComplete='off'
onChange={(_, e) => {
setOValue(e.target.value)
getSuggestions({ variables: { q: e.target.value.replace(/^[@ ]+|[ ]+$/g, '') } })
}}
2022-08-26 22:20:09 +00:00
overrideValue={ovalue}
onKeyDown={(e) => {
switch (e.code) {
case 'ArrowUp':
e.preventDefault()
setSuggestions(
{
...suggestions,
index: Math.max(suggestions.index - 1, 0)
})
break
case 'ArrowDown':
e.preventDefault()
setSuggestions(
{
...suggestions,
index: Math.min(suggestions.index + 1, suggestions.array.length - 1)
})
break
case 'Enter':
e.preventDefault()
setOValue(suggestions.array[suggestions.index].name)
setSuggestions(INITIAL_SUGGESTIONS)
break
case 'Escape':
e.preventDefault()
setSuggestions(INITIAL_SUGGESTIONS)
break
default:
break
}
}}
/>
2023-07-24 18:35:05 +00:00
<Dropdown show={suggestions.array.length > 0}>
<Dropdown.Menu className={styles.suggestionsMenu}>
2022-08-26 22:20:09 +00:00
{suggestions.array.map((v, i) =>
2023-07-24 18:35:05 +00:00
<Dropdown.Item
2022-08-26 22:20:09 +00:00
key={v.name}
active={suggestions.index === i}
onClick={() => {
setOValue(v.name)
setSuggestions(INITIAL_SUGGESTIONS)
}}
>
{v.name}
2023-07-24 18:35:05 +00:00
</Dropdown.Item>)}
</Dropdown.Menu>
</Dropdown>
2022-08-26 22:20:09 +00:00
</FormGroup>
)
}
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
)
}
multiple forwards on a post (#403) * multiple forwards on a post first phase of the multi-forward support * update the graphql mutation for discussion posts to accept and validate multiple forwards * update the discussion form to allow multiple forwards in the UI * start working on db schema changes * uncomment db schema, add migration to create the new model, and update create_item, update_item stored procedures * Propagate updates from discussion to poll, link, and bounty forms Update the create, update poll sql functions for multi forward support * Update gql, typedefs, and resolver to return forwarded users in items responses * UI changes to show multiple forward recipients, and conditional upvote logic changes * Update notification text to reflect multiple forwards upon vote action * Disallow duplicate stacker entries * reduce duplication in populating adv-post-form initial values * Update item_act sql function to implement multi-way forwarding * Update referral functions to scale referral bonuses for forwarded users * Update notification text to reflect non-100% forwarded sats cases * Update wallet history sql queries to accommodate multi-forward use cases * Block zaps for posts you are forwarded zaps at the API layer, in addition to in the UI * Delete fwdUserId column from Item table as part of migration * Fix how we calculate stacked sats after partial forwards in wallet history * Exclude entries from wallet history that are 0 stacked sats from posts with 100% forwarded to other users * Fix wallet history query for forwarded stacked sats to be scaled by the fwd pct * Reduce duplication in adv post form, and do some style tweaks for better layout * Use MAX_FORWARDS constants * Address various PR feedback * first enhancement pass * enhancement pass too --------- Co-authored-by: keyan <keyan.kousha+huumn@gmail.com> Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>
2023-08-23 22:44:17 +00:00
export function VariableInput ({ label, groupClassName, name, hint, max, min, readOnlyLen, children, emptyItem = '', ...props }) {
2022-07-30 13:25:46 +00:00
return (
<FormGroup label={label} className={groupClassName}>
multiple forwards on a post (#403) * multiple forwards on a post first phase of the multi-forward support * update the graphql mutation for discussion posts to accept and validate multiple forwards * update the discussion form to allow multiple forwards in the UI * start working on db schema changes * uncomment db schema, add migration to create the new model, and update create_item, update_item stored procedures * Propagate updates from discussion to poll, link, and bounty forms Update the create, update poll sql functions for multi forward support * Update gql, typedefs, and resolver to return forwarded users in items responses * UI changes to show multiple forward recipients, and conditional upvote logic changes * Update notification text to reflect multiple forwards upon vote action * Disallow duplicate stacker entries * reduce duplication in populating adv-post-form initial values * Update item_act sql function to implement multi-way forwarding * Update referral functions to scale referral bonuses for forwarded users * Update notification text to reflect non-100% forwarded sats cases * Update wallet history sql queries to accommodate multi-forward use cases * Block zaps for posts you are forwarded zaps at the API layer, in addition to in the UI * Delete fwdUserId column from Item table as part of migration * Fix how we calculate stacked sats after partial forwards in wallet history * Exclude entries from wallet history that are 0 stacked sats from posts with 100% forwarded to other users * Fix wallet history query for forwarded stacked sats to be scaled by the fwd pct * Reduce duplication in adv post form, and do some style tweaks for better layout * Use MAX_FORWARDS constants * Address various PR feedback * first enhancement pass * enhancement pass too --------- Co-authored-by: keyan <keyan.kousha+huumn@gmail.com> Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>
2023-08-23 22:44:17 +00:00
<FieldArray name={name} hasValidation>
2022-07-30 13:25:46 +00:00
{({ form, ...fieldArrayHelpers }) => {
2022-08-18 18:15:24 +00:00
const options = form.values[name]
2022-07-30 13:25:46 +00:00
return (
<>
2023-01-07 00:53:09 +00:00
{options?.map((_, i) => (
2022-07-30 13:25:46 +00:00
<div key={i}>
2023-07-24 18:35:05 +00:00
<Row className='mb-2'>
2022-07-30 13:25:46 +00:00
<Col>
multiple forwards on a post (#403) * multiple forwards on a post first phase of the multi-forward support * update the graphql mutation for discussion posts to accept and validate multiple forwards * update the discussion form to allow multiple forwards in the UI * start working on db schema changes * uncomment db schema, add migration to create the new model, and update create_item, update_item stored procedures * Propagate updates from discussion to poll, link, and bounty forms Update the create, update poll sql functions for multi forward support * Update gql, typedefs, and resolver to return forwarded users in items responses * UI changes to show multiple forward recipients, and conditional upvote logic changes * Update notification text to reflect multiple forwards upon vote action * Disallow duplicate stacker entries * reduce duplication in populating adv-post-form initial values * Update item_act sql function to implement multi-way forwarding * Update referral functions to scale referral bonuses for forwarded users * Update notification text to reflect non-100% forwarded sats cases * Update wallet history sql queries to accommodate multi-forward use cases * Block zaps for posts you are forwarded zaps at the API layer, in addition to in the UI * Delete fwdUserId column from Item table as part of migration * Fix how we calculate stacked sats after partial forwards in wallet history * Exclude entries from wallet history that are 0 stacked sats from posts with 100% forwarded to other users * Fix wallet history query for forwarded stacked sats to be scaled by the fwd pct * Reduce duplication in adv post form, and do some style tweaks for better layout * Use MAX_FORWARDS constants * Address various PR feedback * first enhancement pass * enhancement pass too --------- Co-authored-by: keyan <keyan.kousha+huumn@gmail.com> Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>
2023-08-23 22:44:17 +00:00
{children
? children({ index: i, readOnly: i < readOnlyLen, placeholder: i >= min ? 'optional' : undefined })
: <InputInner name={`${name}[${i}]`} {...props} readOnly={i < readOnlyLen} placeholder={i >= min ? 'optional' : undefined} />}
2022-07-30 13:25:46 +00:00
</Col>
multiple forwards on a post (#403) * multiple forwards on a post first phase of the multi-forward support * update the graphql mutation for discussion posts to accept and validate multiple forwards * update the discussion form to allow multiple forwards in the UI * start working on db schema changes * uncomment db schema, add migration to create the new model, and update create_item, update_item stored procedures * Propagate updates from discussion to poll, link, and bounty forms Update the create, update poll sql functions for multi forward support * Update gql, typedefs, and resolver to return forwarded users in items responses * UI changes to show multiple forward recipients, and conditional upvote logic changes * Update notification text to reflect multiple forwards upon vote action * Disallow duplicate stacker entries * reduce duplication in populating adv-post-form initial values * Update item_act sql function to implement multi-way forwarding * Update referral functions to scale referral bonuses for forwarded users * Update notification text to reflect non-100% forwarded sats cases * Update wallet history sql queries to accommodate multi-forward use cases * Block zaps for posts you are forwarded zaps at the API layer, in addition to in the UI * Delete fwdUserId column from Item table as part of migration * Fix how we calculate stacked sats after partial forwards in wallet history * Exclude entries from wallet history that are 0 stacked sats from posts with 100% forwarded to other users * Fix wallet history query for forwarded stacked sats to be scaled by the fwd pct * Reduce duplication in adv post form, and do some style tweaks for better layout * Use MAX_FORWARDS constants * Address various PR feedback * first enhancement pass * enhancement pass too --------- Co-authored-by: keyan <keyan.kousha+huumn@gmail.com> Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>
2023-08-23 22:44:17 +00:00
<Col className='d-flex ps-0' xs='auto'>
{options.length - 1 === i && options.length !== max
? <AddIcon className='fill-grey align-self-center justify-self-center pointer' onClick={() => fieldArrayHelpers.push(emptyItem)} />
// filler div for col alignment across rows
: <div style={{ width: '24px', height: '24px' }} />}
</Col>
{options.length - 1 === i &&
<>
{hint && <BootstrapForm.Text>{hint}</BootstrapForm.Text>}
{form.touched[name] && typeof form.errors[name] === 'string' &&
<div className='invalid-feedback d-block'>{form.errors[name]}</div>}
</>}
2023-07-24 18:35:05 +00:00
</Row>
2022-07-30 13:25:46 +00:00
</div>
))}
</>
)
}}
</FieldArray>
</FormGroup>
)
}
export function Checkbox ({ children, label, groupClassName, hiddenLabel, extra, handleChange, inline, disabled, ...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
const [field,, helpers] = useField({ ...props, type: 'checkbox' })
2021-09-12 16:55:38 +00:00
return (
2023-07-24 18:35:05 +00:00
<FormGroup className={groupClassName}>
2022-03-07 21:50:13 +00:00
{hiddenLabel && <BootstrapForm.Label className='invisible'>{label}</BootstrapForm.Label>}
<BootstrapForm.Check
id={props.id || props.name}
inline={inline}
>
<BootstrapForm.Check.Input
{...field} {...props} disabled={disabled} type='checkbox' onChange={(e) => {
2022-03-07 21:50:13 +00:00
field.onChange(e)
handleChange && handleChange(e.target.checked, helpers.setValue)
2022-03-07 21:50:13 +00:00
}}
/>
2023-08-04 00:14:04 +00:00
<BootstrapForm.Check.Label className={'d-inline-flex flex-nowrap align-items-center' + (disabled ? ' text-muted' : '')}>
2022-03-07 21:50:13 +00:00
<div className='flex-grow-1'>{label}</div>
{extra &&
<div className={styles.checkboxExtra}>
{extra}
</div>}
</BootstrapForm.Check.Label>
</BootstrapForm.Check>
2023-07-24 18:35:05 +00:00
</FormGroup>
2021-09-12 16:55:38 +00:00
)
}
2023-05-11 19:34:42 +00:00
const StorageKeyPrefixContext = createContext()
2021-04-14 00:57:32 +00:00
export function Form ({
initial, schema, onSubmit, children, initialError, validateImmediately, storageKeyPrefix, validateOnChange = true, invoiceable, ...props
2021-04-14 00:57:32 +00:00
}) {
const toaster = useToast()
const initialErrorToasted = useRef(false)
useEffect(() => {
if (initialError && !initialErrorToasted) {
toaster.danger(initialError.message || initialError.toString?.())
initialErrorToasted.current = true
}
}, [])
2021-04-14 00:57:32 +00:00
function clearLocalStorage (values) {
Object.keys(values).forEach(v => {
window.localStorage.removeItem(storageKeyPrefix + '-' + v)
if (Array.isArray(values[v])) {
values[v].forEach(
(iv, i) => {
Object.keys(iv).forEach(k => {
window.localStorage.removeItem(`${storageKeyPrefix}-${v}[${i}].${k}`)
})
window.localStorage.removeItem(`${storageKeyPrefix}-${v}[${i}]`)
})
}
})
}
// if `invoiceable` is set,
// support for payment per invoice if they are lurking or don't have enough balance
// is added to submit handlers.
// submit handlers need to accept { satsReceived, hash, hmac } in their first argument
// and use them as variables in their GraphQL mutation
if (invoiceable && onSubmit) {
const options = typeof invoiceable === 'object' ? invoiceable : undefined
onSubmit = useInvoiceable(onSubmit, { callback: clearLocalStorage, ...options })
}
2021-04-14 00:57:32 +00:00
return (
<Formik
initialValues={initial}
validateOnChange={validateOnChange}
2021-04-14 00:57:32 +00:00
validationSchema={schema}
2021-05-25 00:08:56 +00:00
initialTouched={validateImmediately && initial}
validateOnBlur={false}
onSubmit={async (values, ...args) => {
try {
if (onSubmit) {
const options = await onSubmit(values, ...args)
if (!storageKeyPrefix || options?.keepLocalStorage) return
clearLocalStorage(values)
2022-07-30 13:25:46 +00:00
}
} catch (err) {
console.log(err)
toaster.danger(err.message || err.toString?.())
}
}}
2021-04-14 00:57:32 +00:00
>
<FormikForm {...props} noValidate>
2023-05-11 19:34:42 +00:00
<StorageKeyPrefixContext.Provider value={storageKeyPrefix}>
2021-04-24 21:05:07 +00:00
{children}
2023-05-11 19:34:42 +00:00
</StorageKeyPrefixContext.Provider>
</FormikForm>
2021-04-24 21:05:07 +00:00
</Formik>
)
}
export function Select ({ label, items, groupClassName, onChange, noForm, overrideValue, ...props }) {
const [field, meta, helpers] = noForm ? [{}, {}, {}] : useField(props)
2022-10-20 22:44:44 +00:00
const formik = noForm ? null : useFormikContext()
2023-05-11 00:26:07 +00:00
const invalid = meta.touched && meta.error
useEffect(() => {
if (overrideValue) {
helpers.setValue(overrideValue)
}
}, [overrideValue])
return (
<FormGroup label={label} className={groupClassName}>
2023-07-24 18:35:05 +00:00
<BootstrapForm.Select
2022-10-20 22:44:44 +00:00
{...field} {...props}
onChange={(e) => {
2023-05-11 00:26:07 +00:00
if (field?.onChange) {
field.onChange(e)
}
2022-10-20 22:44:44 +00:00
if (onChange) {
onChange(formik, e)
}
}}
2023-05-11 00:26:07 +00:00
isInvalid={invalid}
2022-10-20 22:44:44 +00:00
>
{items.map(item => <option key={item}>{item}</option>)}
2023-07-24 18:35:05 +00:00
</BootstrapForm.Select>
2023-05-11 00:26:07 +00:00
<BootstrapForm.Control.Feedback type='invalid'>
{meta.touched && meta.error}
</BootstrapForm.Control.Feedback>
</FormGroup>
)
}