Skip to content

Commit

Permalink
⛑️ Refactor apply on wg validation (#2860)
Browse files Browse the repository at this point in the history
* Initial commit

* Validation on pasting account address

* New validation helpers

* Making SelectAccount.tsx controllable

* AddBountyModal update on validation helpers

* Rework on stake step of ApplyForRoleModal.tsx

* Rework on form step of ApplyForRoleModal.tsx

* Extracting helpers to new file

* Making form question component controllable

* Main modal refactor

* Code cleanup

* Test fix
  • Loading branch information
WRadoslaw authored Apr 20, 2022
1 parent d6a9b12 commit de1c6a1
Show file tree
Hide file tree
Showing 18 changed files with 241 additions and 337 deletions.
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

1 comment on commit de1c6a1

@vercel
Copy link

@vercel vercel bot commented on de1c6a1 Apr 20, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

pioneer-2 – ./

pioneer-2-git-dev-joystream.vercel.app
pioneer-2-joystream.vercel.app
pioneer-2.vercel.app

Please sign in to comment.