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

👨‍🚀 POC - Form vendor #2836

Merged
merged 9 commits into from
Apr 13, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-dropzone": "^11.4.2",
"react-hook-form": "^7.29.0",
"react-i18next": "^11.15.1",
"react-intersection-observer": "^8.32.1",
"react-markdown": "^7.0.1",
Expand Down
166 changes: 62 additions & 104 deletions packages/ui/src/bounty/modals/AddBountyModal/AddBountyModal.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { BountyMetadata } from '@joystream/metadata-protobuf'
import { useMachine } from '@xstate/react'
import { at } from 'lodash'
import React, { useCallback, useEffect } from 'react'
import { useForm, FormProvider } from 'react-hook-form'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'

Expand All @@ -18,31 +20,31 @@ import {
Conditions,
createBountyMetadataFactory,
createBountyParametersFactory,
getSchemaFields,
formDefaultValues,
IFormFields,
} from '@/bounty/modals/AddBountyModal/helpers'
import { addBountyMachine, AddBountyModalMachineState, AddBountyStates } from '@/bounty/modals/AddBountyModal/machine'
import { addBountyMachine, AddBountyStates } from '@/bounty/modals/AddBountyModal/machine'
import { AuthorizeTransactionModal } from '@/bounty/modals/AuthorizeTransactionModal'
import { ButtonGhost, ButtonPrimary, ButtonsGroup } from '@/common/components/buttons'
import { FailureModal } from '@/common/components/FailureModal'
import { hasError, getErrorMessage } from '@/common/components/forms/FieldError'
import { Arrow } from '@/common/components/icons'
import { Modal, ModalFooter, ModalHeader } from '@/common/components/Modal'
import { Stepper, StepperBody, StepperModalBody, StepperModalWrapper } from '@/common/components/StepperModal'
import { TokenValue } from '@/common/components/typography'
import { WaitModal } from '@/common/components/WaitModal'
import { useApi } from '@/common/hooks/useApi'
import { useModal } from '@/common/hooks/useModal'
import { useSchema } from '@/common/hooks/useSchema'
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 { useMyMemberships } from '@/memberships/hooks/useMyMemberships'
import { SwitchMemberModalCall } from '@/memberships/modals/SwitchMemberModal'
import { Member } from '@/memberships/types'

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

const transactionSteps = [{ title: 'Create Thread' }, { title: 'Create Bounty' }]
Expand All @@ -58,38 +60,36 @@ export const AddBountyModal = () => {
const { api } = useApi()
const balance = useBalance(activeMember?.controllerAccount)
const bountyApi = api?.consts.bounty

const { setContext, errors, isValid } = useSchema<Conditions>(
getSchemaFields(state as AddBountyModalMachineState),
addBountyModalSchema,
typeof state.value === 'string' ? state.value : undefined
)
const form = useForm({
resolver: useYupValidationResolver(addBountyModalSchema, typeof state.value === 'string' ? state.value : undefined),
context: {
isThreadCategoryLoading,
minCherryLimit: bountyApi?.minCherryLimit,
maxCherryLimit: balance?.transferable,
minFundingLimit: bountyApi?.minFundingLimit,
maxWhitelistSize: bountyApi?.closedContractSizeLimit,
minWorkEntrantStake: bountyApi?.minWorkEntrantStake,
} as Conditions,
mode: 'onBlur',
reValidateMode: 'onChange',
defaultValues: formDefaultValues,
})

const errorChecker = useCallback(
(field: string) => hasError(typeof state.value === 'string' ? `${state.value}.${field}` : field, errors),
[errors, state.value]
(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) => getErrorMessage(typeof state.value === 'string' ? `${state.value}.${field}` : field, errors),
[errors, state.value]
(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()
}

useEffect(() => {
setContext({
isThreadCategoryLoading,
minCherryLimit: bountyApi?.minCherryLimit,
maxCherryLimit: balance?.transferable,
minFundingLimit: bountyApi?.minFundingLimit,
maxWhitelistSize: bountyApi?.closedContractSizeLimit,
minWorkEntrantStake: bountyApi?.minWorkEntrantStake,
isLimited: state.context.fundingPeriodType === 'limited',
})
}, [bountyApi, JSON.stringify(balance?.transferable), state.context.fundingPeriodType])

useEffect(() => {
if (state.matches(AddBountyStates.requirementsVerification)) {
if (!activeMember) {
Expand All @@ -107,11 +107,6 @@ export const AddBountyModal = () => {
}, [state, isThreadCategoryLoading, api])

useEffect(() => {
if (state.matches(AddBountyStates.generalParameters)) {
if (activeMember && !state.context.creator) {
send('SET_CREATOR', { creator: activeMember })
}
}
if (state.matches(AddBountyStates.judgingPeriodDetails)) {
if (threadCategory && !state.context.threadCategoryId) {
send('SET_THREAD_CATEGORY_ID', { threadCategoryId: threadCategory.id })
Expand All @@ -138,10 +133,12 @@ export const AddBountyModal = () => {
}

if (state.matches(AddBountyStates.createThread) && threadCategory) {
const { title, creator, threadCategoryId } = state.context
const {
[AddBountyStates.generalParameters]: { title, creator },
} = form.getValues()
const transaction = api.tx.forum.createThread(
activeMember.id,
threadCategoryId,
threadCategory.id,
`${title} by ${creator?.handle}`,
`This is the description thread for ${title}`,
null
Expand All @@ -164,10 +161,11 @@ export const AddBountyModal = () => {
}

if (state.matches(AddBountyStates.transaction)) {
const fromFields = form.getValues()
const service = state.children.transaction
const transaction = api.tx.bounty.createBounty(
createBountyParametersFactory(state as AddBountyModalMachineState),
metadataToBytes(BountyMetadata, createBountyMetadataFactory(state as AddBountyModalMachineState))
createBountyParametersFactory(fromFields as IFormFields),
metadataToBytes(BountyMetadata, createBountyMetadataFactory(fromFields as IFormFields, state.context.newThreadId))
)
const controllerAccount = accountOrNamed(allAccounts, activeMember.controllerAccount, 'Controller Account')

Expand All @@ -179,7 +177,8 @@ export const AddBountyModal = () => {
buttonLabel="Sign transaction and Create"
description={
<>
You intend to create a bounty. You will be charged <TokenValue value={state.context.cherry} /> for cherry.
You intend to create a bounty. You will be charged{' '}
<TokenValue value={fromFields[AddBountyStates.fundingPeriodDetails].cherry} /> for cherry.
</>
}
controllerAccount={controllerAccount}
Expand Down Expand Up @@ -211,73 +210,32 @@ export const AddBountyModal = () => {
<AddBountyModalWrapper>
<Stepper steps={getSteps(service)} />
<StepperBody>
{state.matches(AddBountyStates.generalParameters) && (
<GeneralParametersStep
title={state.context.title}
setTitle={(title: string) => send('SET_BOUNTY_TITLE', { title })}
description={state.context.description}
setDescription={(description: string) => send('SET_BOUNTY_DESCRIPTION', { description })}
coverPhotoLink={state.context.coverPhotoLink}
setCoverPhoto={(coverPhotoLink: string) => send('SET_COVER_PHOTO', { coverPhotoLink })}
activeMember={activeMember}
errorChecker={errorChecker}
errorMessageGetter={errorMessageGetter}
/>
)}
<FormProvider {...form}>
{state.matches(AddBountyStates.generalParameters) && (
<GeneralParametersStep
activeMember={activeMember}
errorChecker={errorChecker}
errorMessageGetter={errorMessageGetter}
/>
)}

{state.matches(AddBountyStates.fundingPeriodDetails) && (
<FundingDetailsStep
cherry={state.context.cherry}
setCherry={(cherry) => send('SET_CHERRY', { cherry })}
minCherryLimit={bountyApi?.minCherryLimit.toNumber() || 0}
fundingMaximalRange={state.context.fundingMaximalRange}
setFundingMaximalRange={(fundingMaximalRange) =>
send('SET_FUNDING_MAXIMAL_RANGE', { fundingMaximalRange })
}
fundingMinimalRange={state.context.fundingMinimalRange}
setFundingMinimalRange={(fundingMinimalRange) =>
send('SET_FUNDING_MINIMAL_RANGE', { fundingMinimalRange })
}
fundingPeriodType={state.context.fundingPeriodType}
setFundingPeriodType={(fundingPeriodType) => send('SET_FUNDING_PERIOD_TYPE', { fundingPeriodType })}
fundingPeriodLength={state.context.fundingPeriodLength}
setFundingPeriodLength={(fundingPeriodLength) =>
send('SET_FUNDING_PERIOD_LENGTH', { fundingPeriodLength })
}
errorChecker={errorChecker}
errorMessageGetter={errorMessageGetter}
/>
)}
{state.matches(AddBountyStates.workingPeriodDetails) && (
<WorkingDetailsStep
minEntrantStake={bountyApi?.minWorkEntrantStake}
workingPeriodType={state.context.workingPeriodType}
workingPeriodLength={state.context.workingPeriodLength}
workingPeriodStake={state.context.workingPeriodStake}
workingPeriodWhitelist={state.context.workingPeriodWhitelist}
setWorkingPeriodWhitelist={(members: Member[]) =>
send('SET_WORKING_PERIOD_WHITELIST', { workingPeriodWhitelist: members })
}
setWorkingPeriodLength={(workingPeriodLength) =>
send('SET_WORKING_PERIOD_LENGTH', { workingPeriodLength })
}
setWorkingPeriodStake={(workingPeriodStake) => send('SET_WORKING_PERIOD_STAKE', { workingPeriodStake })}
setWorkingPeriodType={(workingPeriodType) => send('SET_WORKING_PERIOD_TYPE', { workingPeriodType })}
whitelistLimit={bountyApi?.closedContractSizeLimit}
errorChecker={errorChecker}
errorMessageGetter={errorMessageGetter}
/>
)}
{state.matches(AddBountyStates.judgingPeriodDetails) && (
<JudgingDetailsStep
oracle={state.context.oracle}
judgingPeriodLength={state.context.judgingPeriodLength}
setJudgingPeriodLength={(judgingPeriodLength) =>
send('SET_JUDGING_PERIOD_LENGTH', { judgingPeriodLength })
}
setOracle={(oracle) => send('SET_ORACLE', { oracle })}
/>
)}
{state.matches(AddBountyStates.fundingPeriodDetails) && (
<FundingDetailsStep
minCherryLimit={bountyApi?.minCherryLimit.toNumber() || 0}
errorChecker={errorChecker}
errorMessageGetter={errorMessageGetter}
/>
)}
{state.matches(AddBountyStates.workingPeriodDetails) && (
<WorkingDetailsStep
minEntrantStake={bountyApi?.minWorkEntrantStake}
whitelistLimit={bountyApi?.closedContractSizeLimit}
errorChecker={errorChecker}
errorMessageGetter={errorMessageGetter}
/>
)}
{state.matches(AddBountyStates.judgingPeriodDetails) && <JudgingDetailsStep />}
</FormProvider>
</StepperBody>
</AddBountyModalWrapper>
</StepperModalBody>
Expand All @@ -291,7 +249,7 @@ export const AddBountyModal = () => {
)}
</ButtonsGroup>
<ButtonsGroup align="right">
<ButtonPrimary disabled={!isValid} onClick={() => send('NEXT')} size="medium">
<ButtonPrimary disabled={!form.formState.isValid} onClick={() => send('NEXT')} size="medium">
{isLastStepActive(getSteps(service)) ? 'Create bounty' : 'Next step'}
</ButtonPrimary>
</ButtonsGroup>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { Meta, Story } from '@storybook/react'
import React, { useState } from 'react'
import React from 'react'

import { BN_ZERO } from '@/common/constants'
import { MockApolloProvider } from '@/mocks/components/storybook/MockApolloProvider'

import { FundingDetailsStep } from './FundingDetailsStep'
Expand All @@ -12,29 +11,9 @@ export default {
} as Meta

const FundingDetailsTemplate: Story = () => {
const [fundingMaximalRange, setFundingMaximalRange] = useState(BN_ZERO)
const [fundingMinimalRange] = useState(BN_ZERO)
const [cherry, setCherry] = useState(BN_ZERO)
const [, setFundingPeriodType] = useState('')
const [fundingPeriodLength, setFundingPeriodLength] = useState(BN_ZERO)

return (
<MockApolloProvider members>
<FundingDetailsStep
setFundingMaximalRange={setFundingMaximalRange}
setFundingMinimalRange={() => undefined}
fundingMaximalRange={fundingMaximalRange}
fundingMinimalRange={fundingMinimalRange}
cherry={cherry}
setCherry={setCherry}
minCherryLimit={10}
setFundingPeriodLength={setFundingPeriodLength}
fundingPeriodLength={fundingPeriodLength}
setFundingPeriodType={setFundingPeriodType}
fundingPeriodType="limited"
errorChecker={() => false}
errorMessageGetter={() => undefined}
/>
<FundingDetailsStep minCherryLimit={10} errorChecker={() => false} errorMessageGetter={() => undefined} />
</MockApolloProvider>
)
}
Expand Down
Loading