Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

⛑️ Refactor apply on wg validation #2860

Merged
merged 14 commits into from
Apr 20, 2022
Merged
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import BN from 'bn.js'
import React, { useEffect, useMemo, useState } from 'react'
import { useFormContext, Controller } from 'react-hook-form'

import { useKeyring } from '@/common/hooks/useKeyring'
import { Address } from '@/common/types'
Expand All @@ -19,13 +20,14 @@ export const filterAccount = (filterOut: Account | Address | undefined) => {
return filterOut ? (account: Account) => account.address !== filterOutAddress : () => true
}

interface Props extends Pick<SelectProps<Account>, 'id' | 'selected' | 'disabled'> {
onChange: (selected: Account) => void
interface Props extends Pick<SelectProps<Account>, 'id' | 'selected' | 'disabled' | 'onBlur'> {
onChange?: (selected: Account) => void
filter?: (option: Account) => boolean
minBalance?: BN
name?: string
}

export const SelectAccount = React.memo(({ id, onChange, filter, selected, disabled }: Props) => {
export const BaseSelectAccount = React.memo(({ id, onChange, filter, selected, disabled, onBlur }: Props) => {
const { allAccounts } = useMyAccounts()
const options = allAccounts.filter(filter || (() => true))

Expand All @@ -38,11 +40,11 @@ export const SelectAccount = React.memo(({ id, onChange, filter, selected, disab
filteredOptions.length === 0 &&
isValidAddress(search, keyring) &&
(!selected || selected.address !== search) &&
onChange(accountOrNamed(allAccounts, search, 'Unsaved account'))
onChange?.(accountOrNamed(allAccounts, search, 'Unsaved account'))
}, [filteredOptions, search, selected])

const change = (selected: Account, close: () => void) => {
onChange(selected)
onChange?.(selected)
close()
}

Expand All @@ -51,6 +53,7 @@ export const SelectAccount = React.memo(({ id, onChange, filter, selected, disab
id={id}
selected={selected}
onChange={change}
onBlur={onBlur}
disabled={disabled}
renderSelected={renderSelected}
placeholder="Select account or paste account address"
Expand All @@ -65,3 +68,21 @@ const renderSelected = (option: Account) => (
<OptionAccount option={option} />
</SelectedOption>
)

export const SelectAccount = ({ name, ...props }: Props) => {
const form = useFormContext()

if (!form || !name) {
return <BaseSelectAccount {...props} />
}

return (
<Controller
name={name}
control={form.control}
render={({ field }) => (
<BaseSelectAccount {...props} selected={field.value} onChange={field.onChange} onBlur={field.onBlur} />
)}
/>
)
}
34 changes: 8 additions & 26 deletions packages/ui/src/bounty/modals/AddBountyModal/AddBountyModal.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { BountyMetadata } from '@joystream/metadata-protobuf'
import { useMachine } from '@xstate/react'
import { at } from 'lodash'
import React, { useCallback, useEffect } from 'react'
import React, { useEffect } from 'react'
import { useForm, FormProvider } from 'react-hook-form'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
Expand Down Expand Up @@ -37,16 +36,10 @@ import { useModal } from '@/common/hooks/useModal'
import { isLastStepActive } from '@/common/modals/utils'
import { metadataToBytes } from '@/common/model/JoystreamNode'
import { getSteps } from '@/common/model/machines/getSteps'
import { useYupValidationResolver } from '@/common/utils/validation'
import { enhancedGetErrorMessage, enhancedHasError, useYupValidationResolver } from '@/common/utils/validation'
import { useMyMemberships } from '@/memberships/hooks/useMyMemberships'
import { SwitchMemberModalCall } from '@/memberships/modals/SwitchMemberModal'

export interface ValidationHelpers {
errorMessageGetter: (field: string) => string | undefined
errorChecker: (field: string) => boolean
formValueGetter?: () => any
}

const transactionSteps = [{ title: 'Create Thread' }, { title: 'Create Bounty' }]

export const AddBountyModal = () => {
Expand Down Expand Up @@ -75,17 +68,6 @@ export const AddBountyModal = () => {
defaultValues: formDefaultValues,
})

const errorChecker = useCallback(
(field: string) =>
!!at(form.formState.errors, typeof state.value === 'string' ? `${state.value}.${field}` : field)[0],
[form.formState.errors, state.value]
)
const errorMessageGetter = useCallback(
(field: string) =>
at(form.formState.errors, typeof state.value === 'string' ? `${state.value}.${field}` : field)[0]?.message,
[form.formState.errors, state.value]
)

if (!service.initialized) {
service.start()
}
Expand Down Expand Up @@ -214,24 +196,24 @@ export const AddBountyModal = () => {
{state.matches(AddBountyStates.generalParameters) && (
<GeneralParametersStep
activeMember={activeMember}
errorChecker={errorChecker}
errorMessageGetter={errorMessageGetter}
errorChecker={enhancedHasError(form.formState.errors, state.value as string)}
errorMessageGetter={enhancedGetErrorMessage(form.formState.errors, state.value as string)}
/>
)}

{state.matches(AddBountyStates.fundingPeriodDetails) && (
<FundingDetailsStep
minCherryLimit={bountyApi?.minCherryLimit.toNumber() || 0}
errorChecker={errorChecker}
errorMessageGetter={errorMessageGetter}
errorChecker={enhancedHasError(form.formState.errors, state.value as string)}
errorMessageGetter={enhancedGetErrorMessage(form.formState.errors, state.value as string)}
/>
)}
{state.matches(AddBountyStates.workingPeriodDetails) && (
<WorkingDetailsStep
minEntrantStake={bountyApi?.minWorkEntrantStake}
whitelistLimit={bountyApi?.closedContractSizeLimit}
errorChecker={errorChecker}
errorMessageGetter={errorMessageGetter}
errorChecker={enhancedHasError(form.formState.errors, state.value as string)}
errorMessageGetter={enhancedGetErrorMessage(form.formState.errors, state.value as string)}
/>
)}
{state.matches(AddBountyStates.judgingPeriodDetails) && <JudgingDetailsStep />}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import React, { useEffect } from 'react'
import { useFormContext } from 'react-hook-form'
import styled from 'styled-components'

import { ValidationHelpers } from '@/bounty/modals/AddBountyModal'
import { AddBountyStates } from '@/bounty/modals/AddBountyModal/machine'
import { InputNumber, InputComponent, Label, ToggleCheckbox } from '@/common/components/forms'
import { LinkSymbol } from '@/common/components/icons/symbols'
Expand All @@ -11,6 +10,7 @@ import { Tooltip, TooltipContainer, TooltipDefault, TooltipExternalLink } from '
import { TextMedium } from '@/common/components/typography'
import { BN_ZERO, Colors } from '@/common/constants'
import { inBlocksDate } from '@/common/model/inBlocksDate'
import { ValidationHelpers } from '@/common/utils/validation'

export interface FundingDetailsStepProps extends ValidationHelpers {
minCherryLimit: number
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import React from 'react'
import styled from 'styled-components'

import { ValidationHelpers } from '@/bounty/modals/AddBountyModal'
import { CKEditor } from '@/common/components/CKEditor'
import { InputText, InputComponent, InputNotification } from '@/common/components/forms'
import { Row } from '@/common/components/Modal'
import { RowGapBlock } from '@/common/components/page/PageContent'
import { TextMedium } from '@/common/components/typography'
import { ValidationHelpers } from '@/common/utils/validation'
import { SelectedMember } from '@/memberships/components/SelectMember'
import { Member } from '@/memberships/types'

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import React from 'react'
import { useFormContext } from 'react-hook-form'
import styled from 'styled-components'

import { ValidationHelpers } from '@/bounty/modals/AddBountyModal'
import { AddBountyStates } from '@/bounty/modals/AddBountyModal/machine'
import { CloseButton } from '@/common/components/buttons'
import { InputNumber, ToggleCheckbox, InlineToggleWrap, InputComponent, Label } from '@/common/components/forms'
Expand All @@ -15,6 +14,7 @@ import { Tooltip, TooltipDefault } from '@/common/components/Tooltip'
import { TextHuge, TextMedium } from '@/common/components/typography'
import { Colors } from '@/common/constants'
import { inBlocksDate } from '@/common/model/inBlocksDate'
import { ValidationHelpers } from '@/common/utils/validation'
import { MemberInfo } from '@/memberships/components'
import { SelectMember } from '@/memberships/components/SelectMember'
import { Member } from '@/memberships/types'
Expand Down
2 changes: 1 addition & 1 deletion packages/ui/src/common/components/forms/FieldError.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { ValidationError } from 'yup'

export const getError = <T extends any>(field: keyof T, errors: ValidationError[]) =>
errors.find((error) => error.path === field)
errors?.find((error) => error.path === field)

export const getErrorMessage = <T extends any>(field: keyof T, errors: ValidationError[]) => {
const error = getError(field, errors)
Expand Down
2 changes: 2 additions & 0 deletions packages/ui/src/common/components/selects/select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export const Select = <T extends any, V extends any = T>({
renderSelected,
renderList,
className,
onBlur,
}: SelectProps<T, V>) => {
const [filterInput, setFilterInput] = useState('')
const search = filterInput
Expand All @@ -36,6 +37,7 @@ export const Select = <T extends any, V extends any = T>({
(option: T) => {
onChange(option, () => {
toggleOpen()
onBlur?.()
setFilterInput('')
})
},
Expand Down
1 change: 1 addition & 0 deletions packages/ui/src/common/components/selects/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export interface SelectProps<T, V = T> {
onNavigate?: KeyboardEventHandler
onChange: (selected: T, close: () => void) => void
onSearch?: (search: string) => void
onBlur?: () => void
renderSelected: (option: V) => ReactNode
renderList: (onOptionClick: (option: T) => void, toggle: () => void) => ReactNode
className?: string
Expand Down
14 changes: 13 additions & 1 deletion packages/ui/src/common/utils/validation.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { isBn } from '@polkadot/util'
import BN from 'bn.js'
import { at } from 'lodash'
import { useCallback } from 'react'
import { Resolver } from 'react-hook-form'
import { FieldErrors, Resolver } from 'react-hook-form'
import * as Yup from 'yup'
import { AnyObjectSchema, ValidationError } from 'yup'
import Reference from 'yup/lib/Reference'
Expand Down Expand Up @@ -120,3 +121,14 @@ export const useYupValidationResolver = (validationSchema: AnyObjectSchema, path
},
[validationSchema, path]
)

export interface ValidationHelpers {
errorMessageGetter: (field: string) => string | undefined
errorChecker: (field: string) => boolean
formValueGetter?: () => any
}

export const enhancedHasError = (errors: FieldErrors, depthPath?: string) => (field: string) =>
!!at(errors, `${depthPath ? depthPath + '.' : ''}${field}`)[0]
export const enhancedGetErrorMessage = (errors: FieldErrors, depthPath?: string) => (field: string) =>
at(errors, `${depthPath ? depthPath + '.' : ''}${field}`)[0]?.message
Original file line number Diff line number Diff line change
@@ -1,23 +1,24 @@
import React from 'react'

import { CKEditor } from '../../common/components/CKEditor'
import { CKEditor } from '@/common/components/CKEditor'

import { InputComponent, InputText } from '../../common/components/forms'
import { ApplicationQuestionType } from '../types'

interface ApplicationQuestionInputProps {
type: ApplicationQuestionType
index: number
question: string
onChange: (value: string) => void
name: string
}

export const ApplicationQuestionInput = ({ question, index, type, onChange }: ApplicationQuestionInputProps) => {
export const ApplicationQuestionInput = ({ question, index, type, name }: ApplicationQuestionInputProps) => {
const inputId = `field-${index}`

return (
<InputComponent label={question} required inputSize={type === 'TEXTAREA' ? 'auto' : 'm'} id={inputId}>
{type === 'TEXT' && <InputText id={inputId} onChange={(event) => onChange(event.target.value)} />}
{type === 'TEXTAREA' && <CKEditor onChange={(event, editor) => onChange(editor.getData())} />}
{type === 'TEXT' && <InputText id={inputId} name={name} />}
{type === 'TEXTAREA' && <CKEditor name={name} />}
</InputComponent>
)
}
Original file line number Diff line number Diff line change
@@ -1,32 +1,16 @@
import React, { useEffect, useMemo } from 'react'
import * as Yup from 'yup'
import React from 'react'

import { RowGapBlock } from '@/common/components/page/PageContent'
import { ValidationHelpers } from '@/common/utils/validation'

import { RowGapBlock } from '../../../common/components/page/PageContent'
import { useForm } from '../../../common/hooks/useForm'
import { ApplicationQuestionInput } from '../../components/ApplicationQuestionInput'
import { ApplicationQuestion } from '../../types'

interface ApplicationStepProps {
interface ApplicationStepProps extends ValidationHelpers {
questions: ApplicationQuestion[]
onChange: (isValid: boolean, answers: Record<number, string>) => void
}

const validationSchemaFromQuestions = (questions: ApplicationQuestion[]) => {
const shapeDefinition = questions.reduce(
(schema, question, index) => ({
[index]: Yup.string().required(),
...schema,
}),
{}
)
return Yup.object().shape(shapeDefinition)
}

export const ApplicationStep = ({ questions, onChange }: ApplicationStepProps) => {
const schema = useMemo(() => validationSchemaFromQuestions(questions), [JSON.stringify(questions)])
const { fields, changeField, validation } = useForm<Record<number, string>>({}, schema)
useEffect(() => onChange(validation.isValid, fields), [JSON.stringify(fields), validation.isValid])

export const ApplicationStep = ({ questions }: ApplicationStepProps) => {
return (
<RowGapBlock gap={24}>
<h4>Application</h4>
Expand All @@ -39,7 +23,7 @@ export const ApplicationStep = ({ questions, onChange }: ApplicationStepProps) =
question={question.question}
index={question.index}
key={question.index}
onChange={(value) => changeField(index, value)}
name={`form.question${index}`}
/>
))}
</RowGapBlock>
Expand Down

This file was deleted.

Loading