From 0af834b79f3355f50ededd0f727cd0e849956744 Mon Sep 17 00:00:00 2001 From: Theophile Sandoz Date: Fri, 5 Apr 2024 14:55:28 +0200 Subject: [PATCH 1/2] Fix the InputNumber behavior --- .../common/components/forms/InputNumber.tsx | 19 +++++++++++++------ packages/ui/src/common/utils/bn.ts | 5 +++++ 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/packages/ui/src/common/components/forms/InputNumber.tsx b/packages/ui/src/common/components/forms/InputNumber.tsx index d809874eb0..7bd5fd48dd 100644 --- a/packages/ui/src/common/components/forms/InputNumber.tsx +++ b/packages/ui/src/common/components/forms/InputNumber.tsx @@ -4,7 +4,7 @@ import { useFormContext, Controller } from 'react-hook-form' import NumberFormat, { NumberFormatValues, SourceInfo } from 'react-number-format' import styled from 'styled-components' -import { asBN, powerOf10, whenDefined } from '@/common/utils' +import { asBN, divToNum, whenDefined } from '@/common/utils' import { Input, InputProps } from './InputComponent' @@ -55,7 +55,7 @@ export const InputNumber = React.memo(({ name, isInBN = false, ...props }: Numbe return } - const exp = props.decimalScale ?? 0 + const exp = 10 ** (props.decimalScale ?? 0) return ( { - const num = isInBN ? asBN(value).toNumber() : value // TODO convert to number safely - return String(num / 10 ** exp) + if (value === undefined || value === null) { + return '' + } + const numValue = isInBN ? divToNum(asBN(value), exp) : value / exp + return numValue.toFixed(props.decimalScale ?? 0) })} - onChange={(_, numValue) => { - const value = isInBN ? new BN(numValue).mul(powerOf10(exp)) : numValue * 10 ** exp + onChange={(event, numValue) => { + if (!event) return + if (event.target.value === '') { + return field.onChange(null) + } + const value = isInBN ? new BN(numValue).muln(exp) : numValue * exp return field.onChange(value) }} onBlur={field.onBlur} diff --git a/packages/ui/src/common/utils/bn.ts b/packages/ui/src/common/utils/bn.ts index 0a11b6c24e..ed268099cd 100644 --- a/packages/ui/src/common/utils/bn.ts +++ b/packages/ui/src/common/utils/bn.ts @@ -13,3 +13,8 @@ export const sumBN = (a: BN | undefined, b: BN | undefined): BN => new BN(a ?? 0 export const powerOf10 = (value: any) => BN_TEN.pow(asBN(value)) export const powerOf2 = (value: any) => BN_TWO.pow(asBN(value)) + +export const divToNum = (dividend: BN, divisor: number): number => { + const div = dividend.divmod(new BN(divisor)) + return div.div.toNumber() + div.mod.toNumber() / divisor +} From d9c2e8952958814ec579c431f951adebdecf55a2 Mon Sep 17 00:00:00 2001 From: Theophile Sandoz Date: Fri, 5 Apr 2024 14:56:41 +0200 Subject: [PATCH 2/2] Fix the validation by replacing `null` by `undefined` --- packages/ui/src/common/utils/validation.tsx | 2 + .../TransferInviteFormModal.tsx | 5 +- .../modals/AddNewProposal/helpers.ts | 48 ++++++++----------- .../modals/CreateOpening/types.tsx | 10 ++-- 4 files changed, 28 insertions(+), 37 deletions(-) diff --git a/packages/ui/src/common/utils/validation.tsx b/packages/ui/src/common/utils/validation.tsx index 21abd6b935..160d33928b 100644 --- a/packages/ui/src/common/utils/validation.tsx +++ b/packages/ui/src/common/utils/validation.tsx @@ -16,6 +16,8 @@ import { formatJoyValue } from '@/common/model/formatters' export const BNSchema = Yup.mixed() +export const NumberSchema = Yup.number().transform((_, value) => value ?? undefined) + export const whenDefined = (key: string, schema: Yup.AnySchema) => Yup.mixed().when(key, { is: undefined, diff --git a/packages/ui/src/memberships/modals/TransferInviteModal/TransferInviteFormModal.tsx b/packages/ui/src/memberships/modals/TransferInviteModal/TransferInviteFormModal.tsx index 7607efd1a5..cbcdbcde18 100644 --- a/packages/ui/src/memberships/modals/TransferInviteModal/TransferInviteFormModal.tsx +++ b/packages/ui/src/memberships/modals/TransferInviteModal/TransferInviteFormModal.tsx @@ -7,7 +7,7 @@ import { ButtonPrimary } from '@/common/components/buttons' import { InputComponent, InputNumber } from '@/common/components/forms' import { Modal, ModalBody, ModalFooter, ModalHeader, Row, TransactionAmount } from '@/common/components/Modal' import { TextMedium } from '@/common/components/typography' -import { maxContext, useYupValidationResolver } from '@/common/utils/validation' +import { maxContext, NumberSchema, useYupValidationResolver } from '@/common/utils/validation' import { InviteMembershipFormFields } from '@/memberships/modals/BuyMembershipModal/BuyMembershipFormModal' import { AccountSchema } from '@/memberships/model/validation' @@ -29,8 +29,7 @@ const formDefaultValues = { const TransferInviteSchema = Yup.object().shape({ from: AccountSchema.required('This field is required'), - amount: Yup.number() - .min(1, 'Number of invitation has to be greater than 0') + amount: NumberSchema.min(1, 'Number of invitation has to be greater than 0') .test(maxContext('You only have ${max} invites left.', 'inviteCount', false)) .required('This field is required'), to: AccountSchema.required('This field is required'), diff --git a/packages/ui/src/proposals/modals/AddNewProposal/helpers.ts b/packages/ui/src/proposals/modals/AddNewProposal/helpers.ts index 38f7010777..fe9a691578 100644 --- a/packages/ui/src/proposals/modals/AddNewProposal/helpers.ts +++ b/packages/ui/src/proposals/modals/AddNewProposal/helpers.ts @@ -8,6 +8,7 @@ import { QuestionValueProps } from '@/common/components/EditableInputList/Editab import { isDefined } from '@/common/utils' import { BNSchema, + NumberSchema, lessThanMixed, maxContext, maxMixed, @@ -224,8 +225,7 @@ export const schemaFactory = (api?: Api) => { trigger: Yup.boolean(), triggerBlock: Yup.number().when('trigger', { is: true, - then: Yup.number() - .test(minContext('The minimum block number is ${min}', 'minTriggerBlock', false)) + then: NumberSchema.test(minContext('The minimum block number is ${min}', 'minTriggerBlock', false)) .test(maxContext('The maximum block number is ${max}', 'maxTriggerBlock', false)) .required('Field is required'), }), @@ -300,7 +300,7 @@ export const schemaFactory = (api?: Api) => { isLimited: Yup.boolean(), duration: Yup.number().when('isLimited', { is: true, - then: Yup.number().required('Field is required'), + then: NumberSchema.required('Field is required'), }), }), applicationForm: Yup.object().shape({ @@ -369,20 +369,19 @@ export const schemaFactory = (api?: Api) => { ), }), setReferralCut: Yup.object().shape({ - referralCut: Yup.number() - .test( - maxContext( - 'Input must be equal or less than ${max}% for proposal to execute', - 'maximumReferralCut', - false, - 'execution' - ) + referralCut: NumberSchema.test( + maxContext( + 'Input must be equal or less than ${max}% for proposal to execute', + 'maximumReferralCut', + false, + 'execution' ) + ) .max(100, 'Value exceed maximal percentage') .required('Field is required'), }), setMembershipLeadInvitationQuota: Yup.object().shape({ - count: Yup.number().min(1, 'Quota must be greater than zero').required('Field is required'), + count: NumberSchema.min(1, 'Quota must be greater than zero').required('Field is required'), leadId: Yup.string().test('execution', (value) => !!value), }), setInitialInvitationBalance: Yup.object().shape({ @@ -443,8 +442,7 @@ export const schemaFactory = (api?: Api) => { .required('Field is required'), }), setEraPayoutDampingFactor: Yup.object().shape({ - dampingFactor: Yup.number() - .min(0, 'The value must be between 0 and 100%.') + dampingFactor: NumberSchema.min(0, 'The value must be between 0 and 100%.') .max(100, 'The value must be between 0 and 100%.') .required('Field is required'), }), @@ -462,22 +460,14 @@ export const schemaFactory = (api?: Api) => { }), updateTokenPalletTokenConstraints: Yup.object() .shape({ - maxYearlyRate: Yup.number() - .min(0, 'Rate must be 0 or greater') - .max(10 ** 6, 'Rate must be 100 or less'), + maxYearlyRate: NumberSchema.min(0, 'Rate must be 0 or greater').max(10 ** 6, 'Rate must be 100 or less'), minAmmSlope: BNSchema.test(moreThanMixed(0, 'Amount must be greater than zero')), - minSaleDuration: Yup.number().min(0, 'Duration must be 0 or greater'), - minRevenueSplitDuration: Yup.number().min(0, 'Duration must be 0 or greater'), - minRevenueSplitTimeToStart: Yup.number().min(0, 'Duration must be 0 or greater'), - salePlatformFee: Yup.number() - .min(0, 'Rate must be 0 or greater') - .max(10 ** 6, 'Rate must be 100 or less'), - ammBuyTxFees: Yup.number() - .min(0, 'Rate must be 0 or greater') - .max(10 ** 6, 'Rate must be 100 or less'), - ammSellTxFees: Yup.number() - .min(0, 'Rate must be 0 or greater') - .max(10 ** 6, 'Rate must be 100 or less'), + minSaleDuration: NumberSchema.min(0, 'Duration must be 0 or greater'), + minRevenueSplitDuration: NumberSchema.min(0, 'Duration must be 0 or greater'), + minRevenueSplitTimeToStart: NumberSchema.min(0, 'Duration must be 0 or greater'), + salePlatformFee: NumberSchema.min(0, 'Rate must be 0 or greater').max(10 ** 6, 'Rate must be 100 or less'), + ammBuyTxFees: NumberSchema.min(0, 'Rate must be 0 or greater').max(10 ** 6, 'Rate must be 100 or less'), + ammSellTxFees: NumberSchema.min(0, 'Rate must be 0 or greater').max(10 ** 6, 'Rate must be 100 or less'), bloatBond: BNSchema.test(moreThanMixed(0, 'Amount must be greater than zero')), }) .test((fields) => { diff --git a/packages/ui/src/working-groups/modals/CreateOpening/types.tsx b/packages/ui/src/working-groups/modals/CreateOpening/types.tsx index 4cfc63a95d..82c21abedb 100644 --- a/packages/ui/src/working-groups/modals/CreateOpening/types.tsx +++ b/packages/ui/src/working-groups/modals/CreateOpening/types.tsx @@ -4,7 +4,7 @@ import * as Yup from 'yup' import { QuestionValueProps } from '@/common/components/EditableInputList/EditableInputList' import { ModalWithDataCall } from '@/common/providers/modal/types' -import { BNSchema, minContext, minMixed } from '@/common/utils/validation' +import { BNSchema, NumberSchema, minContext, minMixed } from '@/common/utils/validation' import { GroupIdName, WorkingGroup } from '@/working-groups/types' export interface OpeningModalData { @@ -44,11 +44,11 @@ export const OpeningSchema = Yup.object().shape({ isLimited: Yup.boolean(), duration: Yup.number().when('isLimited', { is: true, - then: Yup.number().required('Duration is required'), + then: NumberSchema.required('Duration is required'), }), - target: Yup.number() - .min(1, 'Minimum hiring target must be greater than zero') - .required('Hiring target is required'), + target: NumberSchema.min(1, 'Minimum hiring target must be greater than zero').required( + 'Hiring target is required' + ), }), stakingPolicyAndReward: Yup.object().shape({ stakingAmount: BNSchema.test(