From 254936fb13829d7219607af442b22ccfb602aafe Mon Sep 17 00:00:00 2001 From: Victor Date: Tue, 20 Jun 2023 22:28:34 +0100 Subject: [PATCH 01/27] preview multiple receipients modal --- .../ui/src/common/components/Modal/Modal.tsx | 26 ++++++- .../AddNewProposal/AddNewProposalModal.tsx | 5 ++ .../PreviewAndValidateModal.tsx | 76 +++++++++++++++++++ .../modals/PreviewAndValidate/index.ts | 1 + 4 files changed, 106 insertions(+), 2 deletions(-) create mode 100644 packages/ui/src/proposals/modals/AddNewProposal/components/SpecificParameters/modals/PreviewAndValidate/PreviewAndValidateModal.tsx create mode 100644 packages/ui/src/proposals/modals/AddNewProposal/components/SpecificParameters/modals/PreviewAndValidate/index.ts diff --git a/packages/ui/src/common/components/Modal/Modal.tsx b/packages/ui/src/common/components/Modal/Modal.tsx index 731d068bc8..2811b5811f 100644 --- a/packages/ui/src/common/components/Modal/Modal.tsx +++ b/packages/ui/src/common/components/Modal/Modal.tsx @@ -54,9 +54,20 @@ interface ModalProps { children: ReactNode isDark?: boolean className?: any + customModalSize?: any + marginRight?: any } -export const Modal = ({ onClose, modalHeight = 'm', children, modalSize, isDark, className }: ModalProps) => { +export const Modal = ({ + onClose, + modalHeight = 'm', + children, + modalSize, + isDark, + className, + customModalSize, + marginRight, +}: ModalProps) => { function onBackgroundClick(e: React.MouseEvent) { if (e.target === e.currentTarget) { onClose() @@ -68,7 +79,14 @@ export const Modal = ({ onClose, modalHeight = 'm', children, modalSize, isDark, return ( <> - + {children} @@ -203,6 +221,7 @@ interface ModalWrapProps { modalMaxSize: string isDark?: boolean modalHeight?: ModalHeight + marginRight?: string } export const ModalWrap = styled.section` @@ -210,6 +229,7 @@ export const ModalWrap = styled.section` position: absolute; inset: 0; margin: auto auto; + margin-right: ${({ marginRight }) => (marginRight ? `${marginRight}px` : 'auto')}; display: grid; @media only screen and (max-height: 700px) { max-height: 100%; @@ -233,6 +253,8 @@ export const ModalWrap = styled.section` return '904px' case 'l': return '1240px' + default: + return `${modalMaxSize}px` } }}; height: ${({ modalHeight }) => (modalHeight === 'xl' ? '90vh' : 'min-content')}; diff --git a/packages/ui/src/proposals/modals/AddNewProposal/AddNewProposalModal.tsx b/packages/ui/src/proposals/modals/AddNewProposal/AddNewProposalModal.tsx index e083d9fbc1..d007c520dd 100644 --- a/packages/ui/src/proposals/modals/AddNewProposal/AddNewProposalModal.tsx +++ b/packages/ui/src/proposals/modals/AddNewProposal/AddNewProposalModal.tsx @@ -24,6 +24,7 @@ import { TextMedium, TokenValue } from '@/common/components/typography' import { BN_ZERO } from '@/common/constants' import { camelCaseToText } from '@/common/helpers' import { useCurrentBlockNumber } from '@/common/hooks/useCurrentBlockNumber' +import { useKeyring } from '@/common/hooks/useKeyring' import { useLocalStorage } from '@/common/hooks/useLocalStorage' import { useMachine } from '@/common/hooks/useMachine' import { useModal } from '@/common/hooks/useModal' @@ -87,6 +88,9 @@ export const AddNewProposalModal = () => { const schema = useMemo(() => schemaFactory(api), [!api]) const path = useMemo(() => machineStateConverter(state.value), [state.value]) + + const keyring = useKeyring() + const form = useForm({ resolver: useYupValidationResolver(schema, path), mode: 'onChange', @@ -105,6 +109,7 @@ export const AddNewProposalModal = () => { minTriggerBlock: currentBlock ? currentBlock.addn(constants?.votingPeriod ?? 0).addn(constants?.gracePeriod ?? 0) : BN_ZERO, + keyring, } as IStakingAccountSchema, defaultValues: defaultProposalValues, }) diff --git a/packages/ui/src/proposals/modals/AddNewProposal/components/SpecificParameters/modals/PreviewAndValidate/PreviewAndValidateModal.tsx b/packages/ui/src/proposals/modals/AddNewProposal/components/SpecificParameters/modals/PreviewAndValidate/PreviewAndValidateModal.tsx new file mode 100644 index 0000000000..0462347d7a --- /dev/null +++ b/packages/ui/src/proposals/modals/AddNewProposal/components/SpecificParameters/modals/PreviewAndValidate/PreviewAndValidateModal.tsx @@ -0,0 +1,76 @@ +import BN from 'bn.js' +import React, { useEffect, useState } from 'react' +import styled from 'styled-components' + +import { AccountInfo } from '@/accounts/components/AccountInfo' +import { useMyAccounts } from '@/accounts/hooks/useMyAccounts' +import { accountOrNamed } from '@/accounts/model/accountOrNamed' +import { Account, AccountOption } from '@/accounts/types' +import { Close, CloseButton } from '@/common/components/buttons' +import { + AccountRow, + BalanceInfoInRow, + InfoTitle, + InfoValue, + Modal, + ModalBody, + ModalHeader, +} from '@/common/components/Modal' +import { TokenValue } from '@/common/components/typography' + +interface PreviewAndValidateModalProps { + setIsPreviewModalShown: (bool: boolean) => void + previewModalData: string[] +} +interface AccountAndAmount { + account: Account + amount: BN +} + +export const PreviewAndValidateModal = ({ setIsPreviewModalShown, previewModalData }: PreviewAndValidateModalProps) => { + const { allAccounts } = useMyAccounts() + const accounts = allAccounts as AccountOption[] + const [previewAccounts, setPreviewAccounts] = useState() + + useEffect(() => { + setPreviewAccounts( + previewModalData.map((item) => { + const splitAccountsAndAmounts = item.split(',') + const amount: BN = new BN(splitAccountsAndAmounts[1].replace(';', '')) + return { account: accountOrNamed(accounts, splitAccountsAndAmounts[0], 'Unknown Member'), amount: amount } + }) + ) + }, []) + return ( + undefined} modalSize="s" customModalSize={'552'} marginRight={'68'} modalHeight="xl"> + setIsPreviewModalShown(false)} title="Preview And Validate" /> + + {previewAccounts?.map((previewAccount, i) => ( + + + + Amount + + + + + + + ))} + + + ) +} +const CustomModalBody = styled(ModalBody)` + display: block; +` +const CustomAccountRow = styled(AccountRow)` + margin-bottom: 4px; + padding-right: 16px; +` +const CustomBalanceInfoInRow = styled(BalanceInfoInRow)` + grid-template-columns: 1fr 168px 72px; + ${Close} { + margin-left: auto; + } +` diff --git a/packages/ui/src/proposals/modals/AddNewProposal/components/SpecificParameters/modals/PreviewAndValidate/index.ts b/packages/ui/src/proposals/modals/AddNewProposal/components/SpecificParameters/modals/PreviewAndValidate/index.ts new file mode 100644 index 0000000000..83739a6af7 --- /dev/null +++ b/packages/ui/src/proposals/modals/AddNewProposal/components/SpecificParameters/modals/PreviewAndValidate/index.ts @@ -0,0 +1 @@ +export * from './PreviewAndValidateModal' From dead262c1bedd95f2aa72d407926e2f472be8e84 Mon Sep 17 00:00:00 2001 From: Victor Date: Tue, 20 Jun 2023 22:29:04 +0100 Subject: [PATCH 02/27] multiple funding request validation --- .../SpecificParameters/FundingRequest.tsx | 141 +++++++++++++++--- .../components/SpecificParameters/Prompt.tsx | 57 +++++++ .../modals/AddNewProposal/helpers.ts | 46 ++++-- packages/ui/src/proposals/model/validation.ts | 74 +++++++++ 4 files changed, 291 insertions(+), 27 deletions(-) create mode 100644 packages/ui/src/proposals/modals/AddNewProposal/components/SpecificParameters/Prompt.tsx create mode 100644 packages/ui/src/proposals/model/validation.ts diff --git a/packages/ui/src/proposals/modals/AddNewProposal/components/SpecificParameters/FundingRequest.tsx b/packages/ui/src/proposals/modals/AddNewProposal/components/SpecificParameters/FundingRequest.tsx index c0c86840ec..2c640840fd 100644 --- a/packages/ui/src/proposals/modals/AddNewProposal/components/SpecificParameters/FundingRequest.tsx +++ b/packages/ui/src/proposals/modals/AddNewProposal/components/SpecificParameters/FundingRequest.tsx @@ -1,13 +1,47 @@ -import React from 'react' +import React, { useCallback, useState } from 'react' +import { useFormContext } from 'react-hook-form' +import styled from 'styled-components' import { SelectAccount } from '@/accounts/components/SelectAccount' import { CurrencyName } from '@/app/constants/currency' -import { InputComponent, TokenInput } from '@/common/components/forms' +import { ButtonPrimary } from '@/common/components/buttons' +import { + InlineToggleWrap, + InputComponent, + Label, + ToggleCheckbox, + TokenInput, + InputTextarea, +} from '@/common/components/forms' +import { Arrow } from '@/common/components/icons' import { Row } from '@/common/components/Modal' import { RowGapBlock } from '@/common/components/page/PageContent' -import { TextMedium } from '@/common/components/typography' +import { Tooltip, TooltipDefault } from '@/common/components/Tooltip' +import { TextMedium, TextSmall, TextInlineSmall } from '@/common/components/typography' + +import { PreviewAndValidateModal } from './modals/PreviewAndValidate' +import { ErrorPrompt, Prompt } from './Prompt' export const FundingRequest = () => { + const { watch, setValue, getValues } = useFormContext() + const [isPreviewModalShown, setIsPreviewModalShown] = useState(false) + const [previewModalData, setPreviewModalData] = useState([]) + const [payMultiple] = watch(['fundingRequest.payMultiple']) + const [hasPreviewedInput] = watch(['fundingRequest.hasPreviewedInput'],{'fundingRequest.hasPreviewedInput':true}) + const verifyInput = useCallback((input: string) => { + setValue('fundingRequest.hasPreviewedInput', false,{shouldValidate: true}) + setValue('fundingRequest.accountsAndAmounts',input, {shouldValidate: true}) + },[]) + const previewInput = useCallback(() => { + const pattern = /^([^,:;]+),([^,:;]+)(;\n[^,:;]+,[^,:;]+)*(;\n)?$/; + const input = getValues('fundingRequest.accountsAndAmounts') + if(pattern.test(input)){ + const inputSplit = input.split(';\n') + setValue('fundingRequest.hasPreviewedInput', true,{shouldValidate: true}) + setIsPreviewModalShown(true) + setPreviewModalData(inputSplit) + } + },[]) return ( @@ -17,22 +51,95 @@ export const FundingRequest = () => { - - - - - - - + + + + + + + + + + + {payMultiple && ( + + + + For multiple accounts and amounts, follow this CSV pattern: +
+ account1, amount1; +
+ account2, amount2; +
+ ... +
+ account20, amount20 +
+
+
+ )}
+ + {payMultiple ? ( + + + verifyInput(event.currentTarget.value)} + /> + + + {!hasPreviewedInput && Please preview and validate the inputs to proceed} + previewInput()} + > + Preview and Validate + + + ) : ( + + + + + + + + + )} + + {isPreviewModalShown && ( + + )} ) } +const HiddenCheckBox = styled.input.attrs({ type: 'checkbox' })` +margin-top: -12px; +height: 0px; +visibility: hidden; +` \ No newline at end of file diff --git a/packages/ui/src/proposals/modals/AddNewProposal/components/SpecificParameters/Prompt.tsx b/packages/ui/src/proposals/modals/AddNewProposal/components/SpecificParameters/Prompt.tsx new file mode 100644 index 0000000000..6c2c79869a --- /dev/null +++ b/packages/ui/src/proposals/modals/AddNewProposal/components/SpecificParameters/Prompt.tsx @@ -0,0 +1,57 @@ +import React from 'react' +import styled from 'styled-components' + +import { QuestionIcon } from '@/common/components/icons' +import { BorderRad, Colors } from '@/common/constants' + +export interface PromptProps { + children: React.ReactNode +} + +export const Prompt = ({ children }: PromptProps) => { + return ( + + + + + + + {children} + + ) +} +const PromptContainer = styled.div` + display: grid; + grid-template-columns: 48px 1fr; + border-left: 4px solid ${Colors.Blue[200]}; + color: ${Colors.Black[500]}; +` +const IconSection = styled.div` + display: flex; + justify-content: center; + flex-direction: column; + align-items: center; +` +const PromptQuestion = styled.div` + display: flex; + position: relative; + justify-content: center; + align-items: center; + width: 16px; + height: 16px; + border-radius: ${BorderRad.full}; + border: 1px solid ${Colors.Black[900]}; +` +export const ErrorPrompt = styled.div` + display: flex; + justify-content: center; + flex-direction: column; + border-left: 4px solid ${Colors.Red[200]}; + padding-left: 10.78px; + background-color: ${Colors.Red[50]}; + color: ${Colors.Red[400]}; + width: 504px; + height: 56px; + border-radius: 2px; + font-weight: 400; +` diff --git a/packages/ui/src/proposals/modals/AddNewProposal/helpers.ts b/packages/ui/src/proposals/modals/AddNewProposal/helpers.ts index 41ee89adb5..b04fa3e722 100644 --- a/packages/ui/src/proposals/modals/AddNewProposal/helpers.ts +++ b/packages/ui/src/proposals/modals/AddNewProposal/helpers.ts @@ -17,6 +17,7 @@ import { } from '@/common/utils/validation' import { AccountSchema, StakingAccountSchema } from '@/memberships/model/validation' import { Member } from '@/memberships/types' +import { duplicateAccounts, isValidCSV, maxAccounts, maxFundingAmount } from '@/proposals/model/validation' import { ProposalType } from '@/proposals/types' import { GroupIdName } from '@/working-groups/types' @@ -36,6 +37,10 @@ export const defaultProposalValues = { discussionWhitelist: [], isDiscussionClosed: false, }, + fundingRequest: { + payMultiple: false, + hasPreviewedInput: true, + }, updateWorkingGroupBudget: { isPositive: true, }, @@ -69,8 +74,11 @@ export interface AddNewProposalForm { signal?: string } fundingRequest: { - amount: BN - account: Account + amount?: BN + account?: Account + payMultiple?: boolean + accountsAndAmounts?: string + hasPreviewedInput?: boolean } runtimeUpgrade: { runtime?: File @@ -204,14 +212,32 @@ export const schemaFactory = (api?: Api) => { signal: Yup.string().required('Field is required').trim(), }), fundingRequest: Yup.object().shape({ - amount: BNSchema.test(moreThanMixed(0, '')) - // todo: change funding request to allow upload request in file + payMultiple: Yup.boolean().required(), + amount: BNSchema.when('payMultiple',{ + is: false, + then: (schema) => schema.test(moreThanMixed(0, '')) // todo: change funding request to allow upload request in file + .test( + maxMixed(api?.consts.proposalsCodex.fundingRequestProposalMaxTotalAmount, 'Maximal amount allowed is ${max}') + ).required('Field is required') + }), + account: AccountSchema.when('payMultiple',{ + is: false, + then: (schema) => schema.required('Field is required') + }), + hasPreviewedInput: Yup.boolean().when('payMultiple',{ + is: true, + then: (schema) => schema.test('previewedinput','Please preview', (value) => typeof value !== 'undefined' && value).required('Field is required') + }), + accountsAndAmounts: Yup.string().when('payMultiple',{ + is: true, + then: (schema) => schema + .test(duplicateAccounts('Duplicate accounts are not allowed')).test(isValidCSV('Not valid CSV format')) .test( - maxMixed(api?.consts.proposalsCodex.fundingRequestProposalMaxTotalAmount, 'Maximal amount allowed is ${max}') - ) - .required('Field is required'), - account: AccountSchema.required('Field is required'), - }), + maxAccounts('Maximum allowed accounts is ${max}',api?.consts.proposalsCodex.fundingRequestProposalMaxAccounts.toNumber()) + ).test( + maxFundingAmount('Maximal amount allowed is ${max}',api?.consts.proposalsCodex.fundingRequestProposalMaxTotalAmount) + ).required('Field is required') + })}), runtimeUpgrade: Yup.object().shape({ runtime: Yup.mixed() .required('Field is required') @@ -368,4 +394,4 @@ export const schemaFactory = (api?: Api) => { channelCashoutsEnabled: Yup.boolean(), }), }) -} +} \ No newline at end of file diff --git a/packages/ui/src/proposals/model/validation.ts b/packages/ui/src/proposals/model/validation.ts new file mode 100644 index 0000000000..ff60134f97 --- /dev/null +++ b/packages/ui/src/proposals/model/validation.ts @@ -0,0 +1,74 @@ +import { isBn } from '@polkadot/util' +import BN from 'bn.js' +import * as Yup from 'yup' +import { AnyObject } from 'yup/lib/types' + +import { isValidAddress } from '@/accounts/model/isValidAddress' +import { formatJoyValue } from '@/common/model/formatters' + +export const maxAccounts = (message:string, max: number | undefined): Yup.TestConfig => ({ + message, + name: 'maxAccounts', + params: {max}, + exclusive: false, + test(value: string) { + const pairs = value.split(';\n') + return max ? pairs.length <= max : false + }, + }) + + export const duplicateAccounts = (message:string): Yup.TestConfig => ({ + message, + name: 'duplicateAccounts', + exclusive: false, + test(value: string) { + const pairs = value.split(';\n'); + const addresses:string[] = [] + + for(const pair of pairs){ + const [address,] = pair.split(',') + if(addresses.indexOf(address) >= 0) return false + addresses.push(address) + } + + return true + }, + }) + + export const isValidCSV = (message:string): Yup.TestConfig =>({ + message, + name: 'isValidCSV', + exclusive: false, + test(value: string, testContext){ + const pattern = /^([^,:;]+),([^,:;]+)(;\n[^,:;]+,[^,:;]+)*(;\n)?$/; + if(!pattern.test(value)) return false; + + const pairs = value.split(';\n'); + const keyring = testContext?.options?.context?.keyring + + for(const pair of pairs){ + const [address,amount] = pair.split(',') + if(!Number(amount) || !isValidAddress(address,keyring))return false + } + + return true + } + }) + + export const maxFundingAmount = (message:string,max: number | BN | undefined,isJoyValue = true): Yup.TestConfig =>({ + message, + name: 'maxFundingAmount', + params: { max: isJoyValue && isBn(max) ? formatJoyValue(max, { precision: 2 }) : max }, + exclusive: false, + test(value: string){ + + const pairs = value.split(';\n'); + let total = 0; + for(const pair of pairs){ + const [,amount] = pair.split(',') + total += Number(amount) + } + const formattedTotal = new BN(total) + return max ? formattedTotal.lte(new BN(max)) : false + } + }) \ No newline at end of file From 4d0bfa035d0a3dae830b4f3027b834bd2cf9a04f Mon Sep 17 00:00:00 2001 From: Victor Date: Tue, 20 Jun 2023 22:35:23 +0100 Subject: [PATCH 03/27] csv reg exp pattern --- packages/ui/src/proposals/constants/regExp.ts | 1 + .../components/SpecificParameters/FundingRequest.tsx | 4 ++-- packages/ui/src/proposals/model/validation.ts | 5 +++-- 3 files changed, 6 insertions(+), 4 deletions(-) create mode 100644 packages/ui/src/proposals/constants/regExp.ts diff --git a/packages/ui/src/proposals/constants/regExp.ts b/packages/ui/src/proposals/constants/regExp.ts new file mode 100644 index 0000000000..cc60456a9a --- /dev/null +++ b/packages/ui/src/proposals/constants/regExp.ts @@ -0,0 +1 @@ +export const CSV_PATTERN = /^([^,:;]+),([^,:;]+)(;\n[^,:;]+,[^,:;]+)*(;\n)?$/; \ No newline at end of file diff --git a/packages/ui/src/proposals/modals/AddNewProposal/components/SpecificParameters/FundingRequest.tsx b/packages/ui/src/proposals/modals/AddNewProposal/components/SpecificParameters/FundingRequest.tsx index 2c640840fd..8591f56eed 100644 --- a/packages/ui/src/proposals/modals/AddNewProposal/components/SpecificParameters/FundingRequest.tsx +++ b/packages/ui/src/proposals/modals/AddNewProposal/components/SpecificParameters/FundingRequest.tsx @@ -18,6 +18,7 @@ import { Row } from '@/common/components/Modal' import { RowGapBlock } from '@/common/components/page/PageContent' import { Tooltip, TooltipDefault } from '@/common/components/Tooltip' import { TextMedium, TextSmall, TextInlineSmall } from '@/common/components/typography' +import { CSV_PATTERN } from '@/proposals/constants/regExp' import { PreviewAndValidateModal } from './modals/PreviewAndValidate' import { ErrorPrompt, Prompt } from './Prompt' @@ -33,9 +34,8 @@ export const FundingRequest = () => { setValue('fundingRequest.accountsAndAmounts',input, {shouldValidate: true}) },[]) const previewInput = useCallback(() => { - const pattern = /^([^,:;]+),([^,:;]+)(;\n[^,:;]+,[^,:;]+)*(;\n)?$/; const input = getValues('fundingRequest.accountsAndAmounts') - if(pattern.test(input)){ + if(CSV_PATTERN.test(input)){ const inputSplit = input.split(';\n') setValue('fundingRequest.hasPreviewedInput', true,{shouldValidate: true}) setIsPreviewModalShown(true) diff --git a/packages/ui/src/proposals/model/validation.ts b/packages/ui/src/proposals/model/validation.ts index ff60134f97..fcf3f12fad 100644 --- a/packages/ui/src/proposals/model/validation.ts +++ b/packages/ui/src/proposals/model/validation.ts @@ -6,6 +6,8 @@ import { AnyObject } from 'yup/lib/types' import { isValidAddress } from '@/accounts/model/isValidAddress' import { formatJoyValue } from '@/common/model/formatters' +import { CSV_PATTERN } from '../constants/regExp' + export const maxAccounts = (message:string, max: number | undefined): Yup.TestConfig => ({ message, name: 'maxAccounts', @@ -40,8 +42,7 @@ export const maxAccounts = (message:string, max: number | undefined): Yup.TestCo name: 'isValidCSV', exclusive: false, test(value: string, testContext){ - const pattern = /^([^,:;]+),([^,:;]+)(;\n[^,:;]+,[^,:;]+)*(;\n)?$/; - if(!pattern.test(value)) return false; + if(!CSV_PATTERN.test(value)) return false; const pairs = value.split(';\n'); const keyring = testContext?.options?.context?.keyring From 30651dcdc7b70d2638b1e86dbc4b4976e3b21faa Mon Sep 17 00:00:00 2001 From: Victor Date: Wed, 21 Jun 2023 05:01:52 +0100 Subject: [PATCH 04/27] Preview And Validate Modal validation --- .../SpecificParameters/FundingRequest.tsx | 2 +- .../PreviewAndValidateModal.tsx | 111 +++++++++++++++--- .../modals/AddNewProposal/helpers.ts | 5 +- packages/ui/src/proposals/model/validation.ts | 14 ++- 4 files changed, 106 insertions(+), 26 deletions(-) diff --git a/packages/ui/src/proposals/modals/AddNewProposal/components/SpecificParameters/FundingRequest.tsx b/packages/ui/src/proposals/modals/AddNewProposal/components/SpecificParameters/FundingRequest.tsx index 8591f56eed..27b531bc20 100644 --- a/packages/ui/src/proposals/modals/AddNewProposal/components/SpecificParameters/FundingRequest.tsx +++ b/packages/ui/src/proposals/modals/AddNewProposal/components/SpecificParameters/FundingRequest.tsx @@ -133,7 +133,7 @@ export const FundingRequest = () => { )} {isPreviewModalShown && ( - + )} ) diff --git a/packages/ui/src/proposals/modals/AddNewProposal/components/SpecificParameters/modals/PreviewAndValidate/PreviewAndValidateModal.tsx b/packages/ui/src/proposals/modals/AddNewProposal/components/SpecificParameters/modals/PreviewAndValidate/PreviewAndValidateModal.tsx index 0462347d7a..eca99c106d 100644 --- a/packages/ui/src/proposals/modals/AddNewProposal/components/SpecificParameters/modals/PreviewAndValidate/PreviewAndValidateModal.tsx +++ b/packages/ui/src/proposals/modals/AddNewProposal/components/SpecificParameters/modals/PreviewAndValidate/PreviewAndValidateModal.tsx @@ -1,11 +1,14 @@ +import { isBn } from '@polkadot/util' import BN from 'bn.js' -import React, { useEffect, useState } from 'react' +import React, { useCallback, useEffect, useState } from 'react' import styled from 'styled-components' import { AccountInfo } from '@/accounts/components/AccountInfo' import { useMyAccounts } from '@/accounts/hooks/useMyAccounts' import { accountOrNamed } from '@/accounts/model/accountOrNamed' +import { isValidAddress } from '@/accounts/model/isValidAddress' import { Account, AccountOption } from '@/accounts/types' +import { useApi } from '@/api/hooks/useApi' import { Close, CloseButton } from '@/common/components/buttons' import { AccountRow, @@ -14,50 +17,119 @@ import { InfoValue, Modal, ModalBody, + ModalFooter, ModalHeader, + Row, + TransactionInfoContainer, } from '@/common/components/Modal' +import { RowGapBlock } from '@/common/components/page/PageContent' +import { TransactionFee } from '@/common/components/TransactionFee' import { TokenValue } from '@/common/components/typography' +import { Colors, JOY_DECIMAL_PLACES } from '@/common/constants' +import { useKeyring } from '@/common/hooks/useKeyring' + +import { ErrorPrompt } from '../../Prompt' interface PreviewAndValidateModalProps { setIsPreviewModalShown: (bool: boolean) => void previewModalData: string[] + setValue: (name: string, value: any, options: {shouldValidate: boolean}) => void } interface AccountAndAmount { account: Account amount: BN + isValidAccount: boolean } -export const PreviewAndValidateModal = ({ setIsPreviewModalShown, previewModalData }: PreviewAndValidateModalProps) => { +export const PreviewAndValidateModal = ({ setIsPreviewModalShown, previewModalData, setValue }: PreviewAndValidateModalProps) => { + const { api } = useApi() + const maxTotalAmount = api?.consts.proposalsCodex.fundingRequestProposalMaxTotalAmount + const maxAllowedAccounts = api?.consts.proposalsCodex.fundingRequestProposalMaxAccounts.toNumber() + const keyring = useKeyring() const { allAccounts } = useMyAccounts() const accounts = allAccounts as AccountOption[] - const [previewAccounts, setPreviewAccounts] = useState() + const [previewAccounts, setPreviewAccounts] = useState([]) + const [totalAmount, setTotalAmount] = useState(new BN(0)) + const [errorMessages, setErrorMessages] = useState([]) + const decimals = new BN(10).pow(new BN(JOY_DECIMAL_PLACES)) + + + const removeAccount = useCallback((index: number) => { + setErrorMessages([]) + setPreviewAccounts((prev) => prev.filter((item,i) => index !== i)) + },[]) + + const closeModalWithData = useCallback(() => { + let textAreaValue = '' + previewAccounts.map((item,index) => { + textAreaValue+=`${item.account.address},${item.amount.div(decimals)}` + textAreaValue+=( previewAccounts.length - 1) === index ? '':';\n' + }) + setValue('fundingRequest.accountsAndAmounts',textAreaValue,{shouldValidate: true}) + setIsPreviewModalShown(false) + },[previewAccounts]) useEffect(() => { setPreviewAccounts( previewModalData.map((item) => { const splitAccountsAndAmounts = item.split(',') - const amount: BN = new BN(splitAccountsAndAmounts[1].replace(';', '')) - return { account: accountOrNamed(accounts, splitAccountsAndAmounts[0], 'Unknown Member'), amount: amount } + const amount = new BN(splitAccountsAndAmounts[1].replace(';', '')).mul(decimals) + const isValidAccount = isValidAddress(splitAccountsAndAmounts[0], keyring) + return { + account: accountOrNamed(accounts, splitAccountsAndAmounts[0], 'Unknown Member'), + amount: amount, + isValidAccount, + } }) ) }, []) + useEffect(() => { + let total = new BN(0) + let totalInvalidAccounts = 0 + previewAccounts?.map((item) => { + total = total.add(item.amount) + totalInvalidAccounts += !item.isValidAccount ? 1 : 0 + }) + setErrorMessages((prev) => totalInvalidAccounts > 0 ? [...prev, 'Incorrect destination accounts detected'] : [...prev]) + setErrorMessages((prev) => maxAllowedAccounts && (previewAccounts?.length > maxAllowedAccounts) ? [...prev, 'Maximum allowed accounts exceeded'] : [...prev]) + setTotalAmount(total) + }, [previewAccounts]) + useEffect(() => { + setErrorMessages((prev) => totalAmount.gt(isBn(maxTotalAmount) ? maxTotalAmount : new BN(0)) ? [...prev, 'Max payment amount is exceeded'] : [...prev]) + }, [totalAmount]) return ( undefined} modalSize="s" customModalSize={'552'} marginRight={'68'} modalHeight="xl"> - setIsPreviewModalShown(false)} title="Preview And Validate" /> + closeModalWithData()} title="Preview And Validate" /> - {previewAccounts?.map((previewAccount, i) => ( - - - - Amount - - - - - - - ))} + + + + {errorMessages?.map((message) => ( + {message} + ))} + + + + {previewAccounts?.map((previewAccount, i) => ( + + + + Amount + + + + removeAccount(i)} /> + + + ))} + + + + + + + ) } @@ -67,6 +139,9 @@ const CustomModalBody = styled(ModalBody)` const CustomAccountRow = styled(AccountRow)` margin-bottom: 4px; padding-right: 16px; + &.error { + border-color: ${Colors.Red[400]}; + } ` const CustomBalanceInfoInRow = styled(BalanceInfoInRow)` grid-template-columns: 1fr 168px 72px; diff --git a/packages/ui/src/proposals/modals/AddNewProposal/helpers.ts b/packages/ui/src/proposals/modals/AddNewProposal/helpers.ts index b04fa3e722..81c4ebce1d 100644 --- a/packages/ui/src/proposals/modals/AddNewProposal/helpers.ts +++ b/packages/ui/src/proposals/modals/AddNewProposal/helpers.ts @@ -233,9 +233,10 @@ export const schemaFactory = (api?: Api) => { then: (schema) => schema .test(duplicateAccounts('Duplicate accounts are not allowed')).test(isValidCSV('Not valid CSV format')) .test( - maxAccounts('Maximum allowed accounts is ${max}',api?.consts.proposalsCodex.fundingRequestProposalMaxAccounts.toNumber()) - ).test( maxFundingAmount('Maximal amount allowed is ${max}',api?.consts.proposalsCodex.fundingRequestProposalMaxTotalAmount) + ) + .test( + maxAccounts('Maximum allowed accounts is ${max}',api?.consts.proposalsCodex.fundingRequestProposalMaxAccounts.toNumber()) ).required('Field is required') })}), runtimeUpgrade: Yup.object().shape({ diff --git a/packages/ui/src/proposals/model/validation.ts b/packages/ui/src/proposals/model/validation.ts index fcf3f12fad..87d93b88db 100644 --- a/packages/ui/src/proposals/model/validation.ts +++ b/packages/ui/src/proposals/model/validation.ts @@ -1,13 +1,16 @@ import { isBn } from '@polkadot/util' import BN from 'bn.js' import * as Yup from 'yup' +import Reference from 'yup/lib/Reference' import { AnyObject } from 'yup/lib/types' import { isValidAddress } from '@/accounts/model/isValidAddress' +import { JOY_DECIMAL_PLACES } from '@/common/constants' import { formatJoyValue } from '@/common/model/formatters' import { CSV_PATTERN } from '../constants/regExp' + export const maxAccounts = (message:string, max: number | undefined): Yup.TestConfig => ({ message, name: 'maxAccounts', @@ -56,7 +59,7 @@ export const maxAccounts = (message:string, max: number | undefined): Yup.TestCo } }) - export const maxFundingAmount = (message:string,max: number | BN | undefined,isJoyValue = true): Yup.TestConfig =>({ + export const maxFundingAmount = (message:string,max: Reference | number | BN | undefined,isJoyValue = true): Yup.TestConfig =>({ message, name: 'maxFundingAmount', params: { max: isJoyValue && isBn(max) ? formatJoyValue(max, { precision: 2 }) : max }, @@ -64,12 +67,13 @@ export const maxAccounts = (message:string, max: number | undefined): Yup.TestCo test(value: string){ const pairs = value.split(';\n'); - let total = 0; + let total = new BN(0); + const decimals = new BN(10).pow(new BN(JOY_DECIMAL_PLACES)) for(const pair of pairs){ const [,amount] = pair.split(',') - total += Number(amount) + total = total.add(new BN(amount)) } - const formattedTotal = new BN(total) - return max ? formattedTotal.lte(new BN(max)) : false + total = total.mul(decimals) + return !total || typeof max === 'undefined' || !isBn(total) || total.lte(new BN(this.resolve(max))) } }) \ No newline at end of file From e7517cc8df34efdf5905066a3271dac29ae188e5 Mon Sep 17 00:00:00 2001 From: Victor Date: Wed, 21 Jun 2023 05:22:08 +0100 Subject: [PATCH 05/27] lint fix --- packages/ui/src/proposals/constants/regExp.ts | 2 +- .../SpecificParameters/FundingRequest.tsx | 35 +++--- .../PreviewAndValidateModal.tsx | 39 ++++-- .../modals/AddNewProposal/helpers.ts | 63 ++++++---- packages/ui/src/proposals/model/validation.ts | 116 +++++++++--------- 5 files changed, 145 insertions(+), 110 deletions(-) diff --git a/packages/ui/src/proposals/constants/regExp.ts b/packages/ui/src/proposals/constants/regExp.ts index cc60456a9a..eedb0b5e7c 100644 --- a/packages/ui/src/proposals/constants/regExp.ts +++ b/packages/ui/src/proposals/constants/regExp.ts @@ -1 +1 @@ -export const CSV_PATTERN = /^([^,:;]+),([^,:;]+)(;\n[^,:;]+,[^,:;]+)*(;\n)?$/; \ No newline at end of file +export const CSV_PATTERN = /^([^,:;]+),([^,:;]+)(;\n[^,:;]+,[^,:;]+)*(;\n)?$/ diff --git a/packages/ui/src/proposals/modals/AddNewProposal/components/SpecificParameters/FundingRequest.tsx b/packages/ui/src/proposals/modals/AddNewProposal/components/SpecificParameters/FundingRequest.tsx index 27b531bc20..e1bb401b48 100644 --- a/packages/ui/src/proposals/modals/AddNewProposal/components/SpecificParameters/FundingRequest.tsx +++ b/packages/ui/src/proposals/modals/AddNewProposal/components/SpecificParameters/FundingRequest.tsx @@ -28,20 +28,20 @@ export const FundingRequest = () => { const [isPreviewModalShown, setIsPreviewModalShown] = useState(false) const [previewModalData, setPreviewModalData] = useState([]) const [payMultiple] = watch(['fundingRequest.payMultiple']) - const [hasPreviewedInput] = watch(['fundingRequest.hasPreviewedInput'],{'fundingRequest.hasPreviewedInput':true}) + const [hasPreviewedInput] = watch(['fundingRequest.hasPreviewedInput'], { 'fundingRequest.hasPreviewedInput': true }) const verifyInput = useCallback((input: string) => { - setValue('fundingRequest.hasPreviewedInput', false,{shouldValidate: true}) - setValue('fundingRequest.accountsAndAmounts',input, {shouldValidate: true}) - },[]) + setValue('fundingRequest.hasPreviewedInput', false, { shouldValidate: true }) + setValue('fundingRequest.accountsAndAmounts', input, { shouldValidate: true }) + }, []) const previewInput = useCallback(() => { const input = getValues('fundingRequest.accountsAndAmounts') - if(CSV_PATTERN.test(input)){ + if (CSV_PATTERN.test(input)) { const inputSplit = input.split(';\n') - setValue('fundingRequest.hasPreviewedInput', true,{shouldValidate: true}) + setValue('fundingRequest.hasPreviewedInput', true, { shouldValidate: true }) setIsPreviewModalShown(true) setPreviewModalData(inputSplit) } - },[]) + }, []) return ( @@ -105,12 +105,9 @@ export const FundingRequest = () => { onInput={(event) => verifyInput(event.currentTarget.value)} /> - + {!hasPreviewedInput && Please preview and validate the inputs to proceed} - previewInput()} - > + previewInput()}> Preview and Validate @@ -133,13 +130,17 @@ export const FundingRequest = () => { )} {isPreviewModalShown && ( - + )} ) } const HiddenCheckBox = styled.input.attrs({ type: 'checkbox' })` -margin-top: -12px; -height: 0px; -visibility: hidden; -` \ No newline at end of file + margin-top: -12px; + height: 0px; + visibility: hidden; +` diff --git a/packages/ui/src/proposals/modals/AddNewProposal/components/SpecificParameters/modals/PreviewAndValidate/PreviewAndValidateModal.tsx b/packages/ui/src/proposals/modals/AddNewProposal/components/SpecificParameters/modals/PreviewAndValidate/PreviewAndValidateModal.tsx index eca99c106d..a52850761d 100644 --- a/packages/ui/src/proposals/modals/AddNewProposal/components/SpecificParameters/modals/PreviewAndValidate/PreviewAndValidateModal.tsx +++ b/packages/ui/src/proposals/modals/AddNewProposal/components/SpecificParameters/modals/PreviewAndValidate/PreviewAndValidateModal.tsx @@ -33,7 +33,7 @@ import { ErrorPrompt } from '../../Prompt' interface PreviewAndValidateModalProps { setIsPreviewModalShown: (bool: boolean) => void previewModalData: string[] - setValue: (name: string, value: any, options: {shouldValidate: boolean}) => void + setValue: (name: string, value: any, options: { shouldValidate: boolean }) => void } interface AccountAndAmount { account: Account @@ -41,7 +41,11 @@ interface AccountAndAmount { isValidAccount: boolean } -export const PreviewAndValidateModal = ({ setIsPreviewModalShown, previewModalData, setValue }: PreviewAndValidateModalProps) => { +export const PreviewAndValidateModal = ({ + setIsPreviewModalShown, + previewModalData, + setValue, +}: PreviewAndValidateModalProps) => { const { api } = useApi() const maxTotalAmount = api?.consts.proposalsCodex.fundingRequestProposalMaxTotalAmount const maxAllowedAccounts = api?.consts.proposalsCodex.fundingRequestProposalMaxAccounts.toNumber() @@ -53,21 +57,20 @@ export const PreviewAndValidateModal = ({ setIsPreviewModalShown, previewModalDa const [errorMessages, setErrorMessages] = useState([]) const decimals = new BN(10).pow(new BN(JOY_DECIMAL_PLACES)) - const removeAccount = useCallback((index: number) => { setErrorMessages([]) - setPreviewAccounts((prev) => prev.filter((item,i) => index !== i)) - },[]) + setPreviewAccounts((prev) => prev.filter((item, i) => index !== i)) + }, []) const closeModalWithData = useCallback(() => { let textAreaValue = '' - previewAccounts.map((item,index) => { - textAreaValue+=`${item.account.address},${item.amount.div(decimals)}` - textAreaValue+=( previewAccounts.length - 1) === index ? '':';\n' + previewAccounts.map((item, index) => { + textAreaValue += `${item.account.address},${item.amount.div(decimals)}` + textAreaValue += previewAccounts.length - 1 === index ? '' : ';\n' }) - setValue('fundingRequest.accountsAndAmounts',textAreaValue,{shouldValidate: true}) + setValue('fundingRequest.accountsAndAmounts', textAreaValue, { shouldValidate: true }) setIsPreviewModalShown(false) - },[previewAccounts]) + }, [previewAccounts]) useEffect(() => { setPreviewAccounts( @@ -90,12 +93,22 @@ export const PreviewAndValidateModal = ({ setIsPreviewModalShown, previewModalDa total = total.add(item.amount) totalInvalidAccounts += !item.isValidAccount ? 1 : 0 }) - setErrorMessages((prev) => totalInvalidAccounts > 0 ? [...prev, 'Incorrect destination accounts detected'] : [...prev]) - setErrorMessages((prev) => maxAllowedAccounts && (previewAccounts?.length > maxAllowedAccounts) ? [...prev, 'Maximum allowed accounts exceeded'] : [...prev]) + setErrorMessages((prev) => + totalInvalidAccounts > 0 ? [...prev, 'Incorrect destination accounts detected'] : [...prev] + ) + setErrorMessages((prev) => + maxAllowedAccounts && previewAccounts?.length > maxAllowedAccounts + ? [...prev, 'Maximum allowed accounts exceeded'] + : [...prev] + ) setTotalAmount(total) }, [previewAccounts]) useEffect(() => { - setErrorMessages((prev) => totalAmount.gt(isBn(maxTotalAmount) ? maxTotalAmount : new BN(0)) ? [...prev, 'Max payment amount is exceeded'] : [...prev]) + setErrorMessages((prev) => + totalAmount.gt(isBn(maxTotalAmount) ? maxTotalAmount : new BN(0)) + ? [...prev, 'Max payment amount is exceeded'] + : [...prev] + ) }, [totalAmount]) return ( undefined} modalSize="s" customModalSize={'552'} marginRight={'68'} modalHeight="xl"> diff --git a/packages/ui/src/proposals/modals/AddNewProposal/helpers.ts b/packages/ui/src/proposals/modals/AddNewProposal/helpers.ts index 81c4ebce1d..6e4cd63827 100644 --- a/packages/ui/src/proposals/modals/AddNewProposal/helpers.ts +++ b/packages/ui/src/proposals/modals/AddNewProposal/helpers.ts @@ -213,32 +213,51 @@ export const schemaFactory = (api?: Api) => { }), fundingRequest: Yup.object().shape({ payMultiple: Yup.boolean().required(), - amount: BNSchema.when('payMultiple',{ - is: false, - then: (schema) => schema.test(moreThanMixed(0, '')) // todo: change funding request to allow upload request in file - .test( - maxMixed(api?.consts.proposalsCodex.fundingRequestProposalMaxTotalAmount, 'Maximal amount allowed is ${max}') - ).required('Field is required') - }), - account: AccountSchema.when('payMultiple',{ + amount: BNSchema.when('payMultiple', { is: false, - then: (schema) => schema.required('Field is required') + then: (schema) => + schema + .test(moreThanMixed(0, '')) // todo: change funding request to allow upload request in file + .test( + maxMixed( + api?.consts.proposalsCodex.fundingRequestProposalMaxTotalAmount, + 'Maximal amount allowed is ${max}' + ) + ) + .required('Field is required'), }), - hasPreviewedInput: Yup.boolean().when('payMultiple',{ + account: AccountSchema.when('payMultiple', { + is: false, + then: (schema) => schema.required('Field is required'), + }), + hasPreviewedInput: Yup.boolean().when('payMultiple', { is: true, - then: (schema) => schema.test('previewedinput','Please preview', (value) => typeof value !== 'undefined' && value).required('Field is required') + then: (schema) => + schema + .test('previewedinput', 'Please preview', (value) => typeof value !== 'undefined' && value) + .required('Field is required'), }), - accountsAndAmounts: Yup.string().when('payMultiple',{ + accountsAndAmounts: Yup.string().when('payMultiple', { is: true, - then: (schema) => schema - .test(duplicateAccounts('Duplicate accounts are not allowed')).test(isValidCSV('Not valid CSV format')) - .test( - maxFundingAmount('Maximal amount allowed is ${max}',api?.consts.proposalsCodex.fundingRequestProposalMaxTotalAmount) - ) - .test( - maxAccounts('Maximum allowed accounts is ${max}',api?.consts.proposalsCodex.fundingRequestProposalMaxAccounts.toNumber()) - ).required('Field is required') - })}), + then: (schema) => + schema + .test(duplicateAccounts('Duplicate accounts are not allowed')) + .test(isValidCSV('Not valid CSV format')) + .test( + maxFundingAmount( + 'Maximal amount allowed is ${max}', + api?.consts.proposalsCodex.fundingRequestProposalMaxTotalAmount + ) + ) + .test( + maxAccounts( + 'Maximum allowed accounts is ${max}', + api?.consts.proposalsCodex.fundingRequestProposalMaxAccounts.toNumber() + ) + ) + .required('Field is required'), + }), + }), runtimeUpgrade: Yup.object().shape({ runtime: Yup.mixed() .required('Field is required') @@ -395,4 +414,4 @@ export const schemaFactory = (api?: Api) => { channelCashoutsEnabled: Yup.boolean(), }), }) -} \ No newline at end of file +} diff --git a/packages/ui/src/proposals/model/validation.ts b/packages/ui/src/proposals/model/validation.ts index 87d93b88db..774fdeae23 100644 --- a/packages/ui/src/proposals/model/validation.ts +++ b/packages/ui/src/proposals/model/validation.ts @@ -10,70 +10,72 @@ import { formatJoyValue } from '@/common/model/formatters' import { CSV_PATTERN } from '../constants/regExp' +export const maxAccounts = (message: string, max: number | undefined): Yup.TestConfig => ({ + message, + name: 'maxAccounts', + params: { max }, + exclusive: false, + test(value: string) { + const pairs = value.split(';\n') + return max ? pairs.length <= max : false + }, +}) -export const maxAccounts = (message:string, max: number | undefined): Yup.TestConfig => ({ - message, - name: 'maxAccounts', - params: {max}, - exclusive: false, - test(value: string) { - const pairs = value.split(';\n') - return max ? pairs.length <= max : false - }, - }) +export const duplicateAccounts = (message: string): Yup.TestConfig => ({ + message, + name: 'duplicateAccounts', + exclusive: false, + test(value: string) { + const pairs = value.split(';\n') + const addresses: string[] = [] - export const duplicateAccounts = (message:string): Yup.TestConfig => ({ - message, - name: 'duplicateAccounts', - exclusive: false, - test(value: string) { - const pairs = value.split(';\n'); - const addresses:string[] = [] - - for(const pair of pairs){ - const [address,] = pair.split(',') - if(addresses.indexOf(address) >= 0) return false - addresses.push(address) - } - - return true - }, - }) + for (const pair of pairs) { + const [address] = pair.split(',') + if (addresses.indexOf(address) >= 0) return false + addresses.push(address) + } - export const isValidCSV = (message:string): Yup.TestConfig =>({ - message, - name: 'isValidCSV', - exclusive: false, - test(value: string, testContext){ - if(!CSV_PATTERN.test(value)) return false; + return true + }, +}) - const pairs = value.split(';\n'); - const keyring = testContext?.options?.context?.keyring +export const isValidCSV = (message: string): Yup.TestConfig => ({ + message, + name: 'isValidCSV', + exclusive: false, + test(value: string, testContext) { + if (!CSV_PATTERN.test(value)) return false - for(const pair of pairs){ - const [address,amount] = pair.split(',') - if(!Number(amount) || !isValidAddress(address,keyring))return false - } + const pairs = value.split(';\n') + const keyring = testContext?.options?.context?.keyring - return true + for (const pair of pairs) { + const [address, amount] = pair.split(',') + if (!Number(amount) || !isValidAddress(address, keyring)) return false } - }) - export const maxFundingAmount = (message:string,max: Reference | number | BN | undefined,isJoyValue = true): Yup.TestConfig =>({ - message, - name: 'maxFundingAmount', - params: { max: isJoyValue && isBn(max) ? formatJoyValue(max, { precision: 2 }) : max }, - exclusive: false, - test(value: string){ + return true + }, +}) - const pairs = value.split(';\n'); - let total = new BN(0); - const decimals = new BN(10).pow(new BN(JOY_DECIMAL_PLACES)) - for(const pair of pairs){ - const [,amount] = pair.split(',') - total = total.add(new BN(amount)) - } - total = total.mul(decimals) - return !total || typeof max === 'undefined' || !isBn(total) || total.lte(new BN(this.resolve(max))) +export const maxFundingAmount = ( + message: string, + max: Reference | number | BN | undefined, + isJoyValue = true +): Yup.TestConfig => ({ + message, + name: 'maxFundingAmount', + params: { max: isJoyValue && isBn(max) ? formatJoyValue(max, { precision: 2 }) : max }, + exclusive: false, + test(value: string) { + const pairs = value.split(';\n') + let total = new BN(0) + const decimals = new BN(10).pow(new BN(JOY_DECIMAL_PLACES)) + for (const pair of pairs) { + const [, amount] = pair.split(',') + total = total.add(new BN(amount)) } - }) \ No newline at end of file + total = total.mul(decimals) + return !total || typeof max === 'undefined' || !isBn(total) || total.lte(new BN(this.resolve(max))) + }, +}) From e7d4b82911d7a18f496fa25305ee8aa291dadbcd Mon Sep 17 00:00:00 2001 From: Victor Date: Wed, 21 Jun 2023 06:16:43 +0100 Subject: [PATCH 06/27] Tx fail fix --- .../AddNewProposal/getSpecificParameters.ts | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/packages/ui/src/proposals/modals/AddNewProposal/getSpecificParameters.ts b/packages/ui/src/proposals/modals/AddNewProposal/getSpecificParameters.ts index 7220595444..2ea84b04d8 100644 --- a/packages/ui/src/proposals/modals/AddNewProposal/getSpecificParameters.ts +++ b/packages/ui/src/proposals/modals/AddNewProposal/getSpecificParameters.ts @@ -1,7 +1,8 @@ import { OpeningMetadata } from '@joystream/metadata-protobuf' +import BN from 'bn.js' import { Api } from '@/api' -import { BN_ZERO } from '@/common/constants' +import { BN_ZERO, JOY_DECIMAL_PLACES } from '@/common/constants' import { createType } from '@/common/model/createType' import { metadataToBytes } from '@/common/model/JoystreamNode' import { last } from '@/common/utils' @@ -14,6 +15,15 @@ const idToRuntimeId = (id: string): number => Number(last(id.split('-'))) const getWorkingGroupParam = (groupId: GroupIdName | undefined) => groupId && GroupIdToGroupParam[groupId] +const mapAccountsAndAmounts = (input: string | undefined) => { + const decimals = new BN(10).pow(new BN(JOY_DECIMAL_PLACES)) + return input?.split(';\n').map((item) => { + const [address, amount] = item.split(',') + const formattedAmount = new BN(amount).mul(decimals) + return { amount: formattedAmount, account: address } + }) +} + export const getSpecificParameters = async ( api: Api, specifics: Omit @@ -30,9 +40,9 @@ export const getSpecificParameters = async ( } case 'fundingRequest': { return createType('PalletProposalsCodexProposalDetails', { - FundingRequest: [ - { amount: specifics?.fundingRequest?.amount, account: specifics?.fundingRequest?.account?.address }, - ], + FundingRequest: specifics?.fundingRequest?.payMultiple + ? mapAccountsAndAmounts(specifics?.fundingRequest?.accountsAndAmounts) + : [{ amount: specifics?.fundingRequest?.amount, account: specifics?.fundingRequest?.account?.address }], }) } case 'runtimeUpgrade': { From 51ddf50f8f21feaceb911e30b9354cac74240735 Mon Sep 17 00:00:00 2001 From: Victor Date: Mon, 24 Jul 2023 20:30:57 +0100 Subject: [PATCH 07/27] suggestions --- .../SpecificParameters/FundingRequest.tsx | 36 ++++++------- .../PreviewAndValidateModal.tsx | 53 +++++++++++-------- .../AddNewProposal/getSpecificParameters.ts | 22 ++++---- .../modals/AddNewProposal/helpers.ts | 28 +++------- 4 files changed, 63 insertions(+), 76 deletions(-) diff --git a/packages/ui/src/proposals/modals/AddNewProposal/components/SpecificParameters/FundingRequest.tsx b/packages/ui/src/proposals/modals/AddNewProposal/components/SpecificParameters/FundingRequest.tsx index e1bb401b48..c821c9119c 100644 --- a/packages/ui/src/proposals/modals/AddNewProposal/components/SpecificParameters/FundingRequest.tsx +++ b/packages/ui/src/proposals/modals/AddNewProposal/components/SpecificParameters/FundingRequest.tsx @@ -1,4 +1,5 @@ -import React, { useCallback, useState } from 'react' +/* eslint-disable no-console */ +import React, { useCallback, useEffect, useState } from 'react' import { useFormContext } from 'react-hook-form' import styled from 'styled-components' @@ -24,22 +25,22 @@ import { PreviewAndValidateModal } from './modals/PreviewAndValidate' import { ErrorPrompt, Prompt } from './Prompt' export const FundingRequest = () => { - const { watch, setValue, getValues } = useFormContext() + const { watch, setValue, getValues, getFieldState } = useFormContext() const [isPreviewModalShown, setIsPreviewModalShown] = useState(false) - const [previewModalData, setPreviewModalData] = useState([]) const [payMultiple] = watch(['fundingRequest.payMultiple']) const [hasPreviewedInput] = watch(['fundingRequest.hasPreviewedInput'], { 'fundingRequest.hasPreviewedInput': true }) - const verifyInput = useCallback((input: string) => { - setValue('fundingRequest.hasPreviewedInput', false, { shouldValidate: true }) - setValue('fundingRequest.accountsAndAmounts', input, { shouldValidate: true }) - }, []) + const csvInput = watch('fundingRequest.csvInput') + useEffect(() => { + if (getFieldState('hasPreviewedInput')) { + setValue('fundingRequest.hasPreviewedInput', false, { shouldValidate: true }) + console.log('Value ', getValues('fundingRequest.accountsAndAmounts')) + console.log('Previewed ', getValues('fundingRequest.hasPreviewedInput')) + } + }, [csvInput]) const previewInput = useCallback(() => { - const input = getValues('fundingRequest.accountsAndAmounts') + const input = getValues('fundingRequest.csvInput') if (CSV_PATTERN.test(input)) { - const inputSplit = input.split(';\n') - setValue('fundingRequest.hasPreviewedInput', true, { shouldValidate: true }) setIsPreviewModalShown(true) - setPreviewModalData(inputSplit) } }, []) return ( @@ -94,15 +95,14 @@ export const FundingRequest = () => { label="Destination accounts and transfer amounts" required message={'You can select up to 20 recipients'} - name="fundingRequest.accountsAndAmounts" + name="fundingRequest.csvInput" id="accounts-amounts" inputSize="xl" > verifyInput(event.currentTarget.value)} /> @@ -129,13 +129,7 @@ export const FundingRequest = () => { )} - {isPreviewModalShown && ( - - )} + {isPreviewModalShown && } ) } diff --git a/packages/ui/src/proposals/modals/AddNewProposal/components/SpecificParameters/modals/PreviewAndValidate/PreviewAndValidateModal.tsx b/packages/ui/src/proposals/modals/AddNewProposal/components/SpecificParameters/modals/PreviewAndValidate/PreviewAndValidateModal.tsx index a52850761d..fec1ac3a04 100644 --- a/packages/ui/src/proposals/modals/AddNewProposal/components/SpecificParameters/modals/PreviewAndValidate/PreviewAndValidateModal.tsx +++ b/packages/ui/src/proposals/modals/AddNewProposal/components/SpecificParameters/modals/PreviewAndValidate/PreviewAndValidateModal.tsx @@ -1,6 +1,8 @@ +/* eslint-disable no-console */ import { isBn } from '@polkadot/util' import BN from 'bn.js' import React, { useCallback, useEffect, useState } from 'react' +import { useFormContext } from 'react-hook-form' import styled from 'styled-components' import { AccountInfo } from '@/accounts/components/AccountInfo' @@ -32,8 +34,6 @@ import { ErrorPrompt } from '../../Prompt' interface PreviewAndValidateModalProps { setIsPreviewModalShown: (bool: boolean) => void - previewModalData: string[] - setValue: (name: string, value: any, options: { shouldValidate: boolean }) => void } interface AccountAndAmount { account: Account @@ -41,12 +41,9 @@ interface AccountAndAmount { isValidAccount: boolean } -export const PreviewAndValidateModal = ({ - setIsPreviewModalShown, - previewModalData, - setValue, -}: PreviewAndValidateModalProps) => { +export const PreviewAndValidateModal = ({ setIsPreviewModalShown }: PreviewAndValidateModalProps) => { const { api } = useApi() + const { setValue, getValues } = useFormContext() const maxTotalAmount = api?.consts.proposalsCodex.fundingRequestProposalMaxTotalAmount const maxAllowedAccounts = api?.consts.proposalsCodex.fundingRequestProposalMaxAccounts.toNumber() const keyring = useKeyring() @@ -64,17 +61,27 @@ export const PreviewAndValidateModal = ({ const closeModalWithData = useCallback(() => { let textAreaValue = '' + const accountsAndAmounts: { amount: BN; account: string }[] = [] previewAccounts.map((item, index) => { textAreaValue += `${item.account.address},${item.amount.div(decimals)}` textAreaValue += previewAccounts.length - 1 === index ? '' : ';\n' + accountsAndAmounts.push({ amount: item.amount, account: item.account.address }) }) - setValue('fundingRequest.accountsAndAmounts', textAreaValue, { shouldValidate: true }) + setValue('fundingRequest.csvInput', textAreaValue, { shouldValidate: true }) + setValue('fundingRequest.hasPreviewedInput', true, { shouldValidate: true }) + console.log('error length ', errorMessages.length) + if (errorMessages.length === 0) { + setValue('fundingRequest.accountsAndAmounts', accountsAndAmounts, { shouldValidate: true }) + } else { + setValue('fundingRequest.accountsAndAmounts', undefined, { shouldValidate: true }) + } setIsPreviewModalShown(false) - }, [previewAccounts]) + }, [previewAccounts, errorMessages]) useEffect(() => { + const csvInput = getValues('fundingRequest.csvInput').split(';\n') setPreviewAccounts( - previewModalData.map((item) => { + csvInput.map((item: string) => { const splitAccountsAndAmounts = item.split(',') const amount = new BN(splitAccountsAndAmounts[1].replace(';', '')).mul(decimals) const isValidAccount = isValidAddress(splitAccountsAndAmounts[0], keyring) @@ -93,22 +100,22 @@ export const PreviewAndValidateModal = ({ total = total.add(item.amount) totalInvalidAccounts += !item.isValidAccount ? 1 : 0 }) - setErrorMessages((prev) => - totalInvalidAccounts > 0 ? [...prev, 'Incorrect destination accounts detected'] : [...prev] - ) - setErrorMessages((prev) => - maxAllowedAccounts && previewAccounts?.length > maxAllowedAccounts - ? [...prev, 'Maximum allowed accounts exceeded'] - : [...prev] - ) + const messages: string[] = [] + if (totalInvalidAccounts > 0) { + messages.push('Incorrect destination accounts detected') + } + if (maxAllowedAccounts && previewAccounts?.length > maxAllowedAccounts) { + messages.push('Maximum allowed accounts exceeded') + } + if (messages.length > 0) { + setErrorMessages((prev) => [...prev, ...messages]) + } setTotalAmount(total) }, [previewAccounts]) useEffect(() => { - setErrorMessages((prev) => - totalAmount.gt(isBn(maxTotalAmount) ? maxTotalAmount : new BN(0)) - ? [...prev, 'Max payment amount is exceeded'] - : [...prev] - ) + if (totalAmount.gt(isBn(maxTotalAmount) ? maxTotalAmount : new BN(0))) { + setErrorMessages((prev) => [...prev, 'Max payment amount is exceeded']) + } }, [totalAmount]) return ( undefined} modalSize="s" customModalSize={'552'} marginRight={'68'} modalHeight="xl"> diff --git a/packages/ui/src/proposals/modals/AddNewProposal/getSpecificParameters.ts b/packages/ui/src/proposals/modals/AddNewProposal/getSpecificParameters.ts index 2ea84b04d8..5a0bc504b0 100644 --- a/packages/ui/src/proposals/modals/AddNewProposal/getSpecificParameters.ts +++ b/packages/ui/src/proposals/modals/AddNewProposal/getSpecificParameters.ts @@ -1,8 +1,8 @@ import { OpeningMetadata } from '@joystream/metadata-protobuf' -import BN from 'bn.js' +// import BN from 'bn.js' import { Api } from '@/api' -import { BN_ZERO, JOY_DECIMAL_PLACES } from '@/common/constants' +import { BN_ZERO /*JOY_DECIMAL_PLACES*/ } from '@/common/constants' import { createType } from '@/common/model/createType' import { metadataToBytes } from '@/common/model/JoystreamNode' import { last } from '@/common/utils' @@ -15,14 +15,14 @@ const idToRuntimeId = (id: string): number => Number(last(id.split('-'))) const getWorkingGroupParam = (groupId: GroupIdName | undefined) => groupId && GroupIdToGroupParam[groupId] -const mapAccountsAndAmounts = (input: string | undefined) => { - const decimals = new BN(10).pow(new BN(JOY_DECIMAL_PLACES)) - return input?.split(';\n').map((item) => { - const [address, amount] = item.split(',') - const formattedAmount = new BN(amount).mul(decimals) - return { amount: formattedAmount, account: address } - }) -} +// const mapAccountsAndAmounts = (input: string | undefined) => { +// const decimals = new BN(10).pow(new BN(JOY_DECIMAL_PLACES)) +// return input?.split(';\n').map((item) => { +// const [address, amount] = item.split(',') +// const formattedAmount = new BN(amount).mul(decimals) +// return { amount: formattedAmount, account: address } +// }) +// } export const getSpecificParameters = async ( api: Api, @@ -41,7 +41,7 @@ export const getSpecificParameters = async ( case 'fundingRequest': { return createType('PalletProposalsCodexProposalDetails', { FundingRequest: specifics?.fundingRequest?.payMultiple - ? mapAccountsAndAmounts(specifics?.fundingRequest?.accountsAndAmounts) + ? specifics?.fundingRequest?.accountsAndAmounts : [{ amount: specifics?.fundingRequest?.amount, account: specifics?.fundingRequest?.account?.address }], }) } diff --git a/packages/ui/src/proposals/modals/AddNewProposal/helpers.ts b/packages/ui/src/proposals/modals/AddNewProposal/helpers.ts index 6e4cd63827..125d8967cc 100644 --- a/packages/ui/src/proposals/modals/AddNewProposal/helpers.ts +++ b/packages/ui/src/proposals/modals/AddNewProposal/helpers.ts @@ -17,7 +17,7 @@ import { } from '@/common/utils/validation' import { AccountSchema, StakingAccountSchema } from '@/memberships/model/validation' import { Member } from '@/memberships/types' -import { duplicateAccounts, isValidCSV, maxAccounts, maxFundingAmount } from '@/proposals/model/validation' +import { isValidCSV } from '@/proposals/model/validation' import { ProposalType } from '@/proposals/types' import { GroupIdName } from '@/working-groups/types' @@ -77,7 +77,8 @@ export interface AddNewProposalForm { amount?: BN account?: Account payMultiple?: boolean - accountsAndAmounts?: string + csvInput?: string + accountsAndAmounts?: { amount: BN; account: string }[] hasPreviewedInput?: boolean } runtimeUpgrade: { @@ -217,7 +218,7 @@ export const schemaFactory = (api?: Api) => { is: false, then: (schema) => schema - .test(moreThanMixed(0, '')) // todo: change funding request to allow upload request in file + .test(moreThanMixed(0, '')) .test( maxMixed( api?.consts.proposalsCodex.fundingRequestProposalMaxTotalAmount, @@ -237,26 +238,11 @@ export const schemaFactory = (api?: Api) => { .test('previewedinput', 'Please preview', (value) => typeof value !== 'undefined' && value) .required('Field is required'), }), - accountsAndAmounts: Yup.string().when('payMultiple', { + csvInput: Yup.string().when('payMultiple', { is: true, - then: (schema) => - schema - .test(duplicateAccounts('Duplicate accounts are not allowed')) - .test(isValidCSV('Not valid CSV format')) - .test( - maxFundingAmount( - 'Maximal amount allowed is ${max}', - api?.consts.proposalsCodex.fundingRequestProposalMaxTotalAmount - ) - ) - .test( - maxAccounts( - 'Maximum allowed accounts is ${max}', - api?.consts.proposalsCodex.fundingRequestProposalMaxAccounts.toNumber() - ) - ) - .required('Field is required'), + then: (schema) => schema.test(isValidCSV('Not valid CSV format')).required('Field is required'), }), + accountsAndAmounts: Yup.array().required(), }), runtimeUpgrade: Yup.object().shape({ runtime: Yup.mixed() From 383089d8ab6fbbb16e64dadf39a4ccebcb7fdd49 Mon Sep 17 00:00:00 2001 From: Victor Date: Mon, 24 Jul 2023 21:16:44 +0100 Subject: [PATCH 08/27] lint fix --- .../proposals/modals/AddNewProposal/AddNewProposalModal.tsx | 3 +-- packages/ui/src/proposals/modals/AddNewProposal/helpers.ts | 5 ++++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/ui/src/proposals/modals/AddNewProposal/AddNewProposalModal.tsx b/packages/ui/src/proposals/modals/AddNewProposal/AddNewProposalModal.tsx index 33116a8ed4..988e6ddf0a 100644 --- a/packages/ui/src/proposals/modals/AddNewProposal/AddNewProposalModal.tsx +++ b/packages/ui/src/proposals/modals/AddNewProposal/AddNewProposalModal.tsx @@ -87,10 +87,9 @@ export const AddNewProposalModal = () => { const stakingStatus = useStakingAccountStatus(formMap[0]?.address, activeMember?.id, [state.matches('transaction')]) const schema = useMemo(() => schemaFactory(api), [!api]) - const keyring = useKeyring() const path = useMemo(() => machineStateConverter(state.value) as keyof AddNewProposalForm, [state.value]) - + const form = useForm({ resolver: useYupValidationResolver(schema, path), mode: 'onChange', diff --git a/packages/ui/src/proposals/modals/AddNewProposal/helpers.ts b/packages/ui/src/proposals/modals/AddNewProposal/helpers.ts index c6fc464a66..d5f07c38f8 100644 --- a/packages/ui/src/proposals/modals/AddNewProposal/helpers.ts +++ b/packages/ui/src/proposals/modals/AddNewProposal/helpers.ts @@ -242,7 +242,10 @@ export const schemaFactory = (api?: Api) => { is: true, then: (schema) => schema.test(isValidCSV('Not valid CSV format')).required('Field is required'), }), - accountsAndAmounts: Yup.array().required(), + accountsAndAmounts: Yup.array().when('payMultiple', { + is: true, + then: (schema) => schema.required(), + }), }), runtimeUpgrade: Yup.object().shape({ runtime: Yup.mixed() From d06d0cad2846b8bbccd6f4e16b4fd2b700b2e03a Mon Sep 17 00:00:00 2001 From: Victor Date: Tue, 25 Jul 2023 11:33:48 +0100 Subject: [PATCH 09/27] side drawer --- .../PreviewAndValidateModal.tsx | 100 +++++++++++------- 1 file changed, 62 insertions(+), 38 deletions(-) diff --git a/packages/ui/src/proposals/modals/AddNewProposal/components/SpecificParameters/modals/PreviewAndValidate/PreviewAndValidateModal.tsx b/packages/ui/src/proposals/modals/AddNewProposal/components/SpecificParameters/modals/PreviewAndValidate/PreviewAndValidateModal.tsx index fec1ac3a04..07dc19c564 100644 --- a/packages/ui/src/proposals/modals/AddNewProposal/components/SpecificParameters/modals/PreviewAndValidate/PreviewAndValidateModal.tsx +++ b/packages/ui/src/proposals/modals/AddNewProposal/components/SpecificParameters/modals/PreviewAndValidate/PreviewAndValidateModal.tsx @@ -17,14 +17,19 @@ import { BalanceInfoInRow, InfoTitle, InfoValue, - Modal, - ModalBody, ModalFooter, - ModalHeader, Row, TransactionInfoContainer, } from '@/common/components/Modal' import { RowGapBlock } from '@/common/components/page/PageContent' +import { + SidePane, + SidePaneBody, + SidePaneGlass, + SidePaneHeader, + SidePanelTop, + SidePaneTitle, +} from '@/common/components/SidePane' import { TransactionFee } from '@/common/components/TransactionFee' import { TokenValue } from '@/common/components/typography' import { Colors, JOY_DECIMAL_PLACES } from '@/common/constants' @@ -54,6 +59,12 @@ export const PreviewAndValidateModal = ({ setIsPreviewModalShown }: PreviewAndVa const [errorMessages, setErrorMessages] = useState([]) const decimals = new BN(10).pow(new BN(JOY_DECIMAL_PLACES)) + const onBackgroundClick = (e: React.MouseEvent) => { + if (e.target === e.currentTarget) { + closeModalWithData() + } + } + const removeAccount = useCallback((index: number) => { setErrorMessages([]) setPreviewAccounts((prev) => prev.filter((item, i) => index !== i)) @@ -118,44 +129,48 @@ export const PreviewAndValidateModal = ({ setIsPreviewModalShown }: PreviewAndVa } }, [totalAmount]) return ( - undefined} modalSize="s" customModalSize={'552'} marginRight={'68'} modalHeight="xl"> - closeModalWithData()} title="Preview And Validate" /> - - - - - {errorMessages?.map((message) => ( - {message} + + + + + Preview And Validate + closeModalWithData()}> + + + + + + + {errorMessages?.map((message) => ( + {message} + ))} + + + + {previewAccounts?.map((previewAccount, i) => ( + + + + Amount + + + + removeAccount(i)} /> + + ))} - - - - {previewAccounts?.map((previewAccount, i) => ( - - - - Amount - - - - removeAccount(i)} /> - - - ))} - - - - - - - - - + + + + + + + + + + ) } -const CustomModalBody = styled(ModalBody)` - display: block; -` const CustomAccountRow = styled(AccountRow)` margin-bottom: 4px; padding-right: 16px; @@ -169,3 +184,12 @@ const CustomBalanceInfoInRow = styled(BalanceInfoInRow)` margin-left: auto; } ` +const PreviewPanel = styled(SidePane)` + grid-template-rows: auto 1fr auto; +` +const PreviewPanelHeader = styled(SidePaneHeader)` + padding: 12px 24px; +` +const PreviewPanelBody = styled(SidePaneBody)` + padding: 12px 24px; +` From 1abf1d06c417d0ea85aa7929f7479a9e5928e300 Mon Sep 17 00:00:00 2001 From: Victor Date: Tue, 25 Jul 2023 13:28:17 +0100 Subject: [PATCH 10/27] csv pattern change and cleanup --- packages/ui/src/proposals/constants/regExp.ts | 2 +- .../SpecificParameters/FundingRequest.tsx | 12 +++---- .../PreviewAndValidateModal.tsx | 31 ++++++++++--------- packages/ui/src/proposals/model/validation.ts | 8 ++--- 4 files changed, 27 insertions(+), 26 deletions(-) diff --git a/packages/ui/src/proposals/constants/regExp.ts b/packages/ui/src/proposals/constants/regExp.ts index eedb0b5e7c..2223a831e0 100644 --- a/packages/ui/src/proposals/constants/regExp.ts +++ b/packages/ui/src/proposals/constants/regExp.ts @@ -1 +1 @@ -export const CSV_PATTERN = /^([^,:;]+),([^,:;]+)(;\n[^,:;]+,[^,:;]+)*(;\n)?$/ +export const CSV_PATTERN = /^([^,:;]+),([^,:;]+)(\n[^,:;]+,[^,:;]+)*(\n)?$/ diff --git a/packages/ui/src/proposals/modals/AddNewProposal/components/SpecificParameters/FundingRequest.tsx b/packages/ui/src/proposals/modals/AddNewProposal/components/SpecificParameters/FundingRequest.tsx index c821c9119c..b15db8327b 100644 --- a/packages/ui/src/proposals/modals/AddNewProposal/components/SpecificParameters/FundingRequest.tsx +++ b/packages/ui/src/proposals/modals/AddNewProposal/components/SpecificParameters/FundingRequest.tsx @@ -1,4 +1,3 @@ -/* eslint-disable no-console */ import React, { useCallback, useEffect, useState } from 'react' import { useFormContext } from 'react-hook-form' import styled from 'styled-components' @@ -31,10 +30,9 @@ export const FundingRequest = () => { const [hasPreviewedInput] = watch(['fundingRequest.hasPreviewedInput'], { 'fundingRequest.hasPreviewedInput': true }) const csvInput = watch('fundingRequest.csvInput') useEffect(() => { - if (getFieldState('hasPreviewedInput')) { + if (getFieldState('fundingRequest.accountsAndAmounts')) { + setValue('fundingRequest.accountsAndAmounts', undefined, { shouldValidate: true }) setValue('fundingRequest.hasPreviewedInput', false, { shouldValidate: true }) - console.log('Value ', getValues('fundingRequest.accountsAndAmounts')) - console.log('Previewed ', getValues('fundingRequest.hasPreviewedInput')) } }, [csvInput]) const previewInput = useCallback(() => { @@ -75,9 +73,9 @@ export const FundingRequest = () => { For multiple accounts and amounts, follow this CSV pattern:
- account1, amount1; + account1, amount1
- account2, amount2; + account2, amount2
...
@@ -129,7 +127,7 @@ export const FundingRequest = () => { )} - {isPreviewModalShown && } + {isPreviewModalShown && } ) } diff --git a/packages/ui/src/proposals/modals/AddNewProposal/components/SpecificParameters/modals/PreviewAndValidate/PreviewAndValidateModal.tsx b/packages/ui/src/proposals/modals/AddNewProposal/components/SpecificParameters/modals/PreviewAndValidate/PreviewAndValidateModal.tsx index 07dc19c564..0693cc425d 100644 --- a/packages/ui/src/proposals/modals/AddNewProposal/components/SpecificParameters/modals/PreviewAndValidate/PreviewAndValidateModal.tsx +++ b/packages/ui/src/proposals/modals/AddNewProposal/components/SpecificParameters/modals/PreviewAndValidate/PreviewAndValidateModal.tsx @@ -1,4 +1,3 @@ -/* eslint-disable no-console */ import { isBn } from '@polkadot/util' import BN from 'bn.js' import React, { useCallback, useEffect, useState } from 'react' @@ -32,13 +31,15 @@ import { } from '@/common/components/SidePane' import { TransactionFee } from '@/common/components/TransactionFee' import { TokenValue } from '@/common/components/typography' -import { Colors, JOY_DECIMAL_PLACES } from '@/common/constants' +import { Colors } from '@/common/constants' import { useKeyring } from '@/common/hooks/useKeyring' +import { formatJoyValue } from '@/common/model/formatters' +import { joy } from '@/mocks/helpers' import { ErrorPrompt } from '../../Prompt' interface PreviewAndValidateModalProps { - setIsPreviewModalShown: (bool: boolean) => void + onClose: (bool: boolean) => void } interface AccountAndAmount { account: Account @@ -46,7 +47,7 @@ interface AccountAndAmount { isValidAccount: boolean } -export const PreviewAndValidateModal = ({ setIsPreviewModalShown }: PreviewAndValidateModalProps) => { +export const PreviewAndValidateModal = ({ onClose }: PreviewAndValidateModalProps) => { const { api } = useApi() const { setValue, getValues } = useFormContext() const maxTotalAmount = api?.consts.proposalsCodex.fundingRequestProposalMaxTotalAmount @@ -57,7 +58,6 @@ export const PreviewAndValidateModal = ({ setIsPreviewModalShown }: PreviewAndVa const [previewAccounts, setPreviewAccounts] = useState([]) const [totalAmount, setTotalAmount] = useState(new BN(0)) const [errorMessages, setErrorMessages] = useState([]) - const decimals = new BN(10).pow(new BN(JOY_DECIMAL_PLACES)) const onBackgroundClick = (e: React.MouseEvent) => { if (e.target === e.currentTarget) { @@ -71,30 +71,33 @@ export const PreviewAndValidateModal = ({ setIsPreviewModalShown }: PreviewAndVa }, []) const closeModalWithData = useCallback(() => { - let textAreaValue = '' const accountsAndAmounts: { amount: BN; account: string }[] = [] - previewAccounts.map((item, index) => { - textAreaValue += `${item.account.address},${item.amount.div(decimals)}` - textAreaValue += previewAccounts.length - 1 === index ? '' : ';\n' + previewAccounts.map((item) => { accountsAndAmounts.push({ amount: item.amount, account: item.account.address }) }) - setValue('fundingRequest.csvInput', textAreaValue, { shouldValidate: true }) setValue('fundingRequest.hasPreviewedInput', true, { shouldValidate: true }) - console.log('error length ', errorMessages.length) if (errorMessages.length === 0) { setValue('fundingRequest.accountsAndAmounts', accountsAndAmounts, { shouldValidate: true }) } else { setValue('fundingRequest.accountsAndAmounts', undefined, { shouldValidate: true }) } - setIsPreviewModalShown(false) + onClose(false) }, [previewAccounts, errorMessages]) + useEffect(() => { + if (previewAccounts.length > 0) { + const value = previewAccounts + .map(({ account, amount }) => `${account.address},${formatJoyValue(amount).replaceAll(',', '')}`) + .join('\n') + setValue('fundingRequest.csvInput', value) + } + }, [previewAccounts]) useEffect(() => { - const csvInput = getValues('fundingRequest.csvInput').split(';\n') + const csvInput = getValues('fundingRequest.csvInput').split('\n') setPreviewAccounts( csvInput.map((item: string) => { const splitAccountsAndAmounts = item.split(',') - const amount = new BN(splitAccountsAndAmounts[1].replace(';', '')).mul(decimals) + const amount = new BN(joy(splitAccountsAndAmounts[1])) const isValidAccount = isValidAddress(splitAccountsAndAmounts[0], keyring) return { account: accountOrNamed(accounts, splitAccountsAndAmounts[0], 'Unknown Member'), diff --git a/packages/ui/src/proposals/model/validation.ts b/packages/ui/src/proposals/model/validation.ts index 774fdeae23..4bfa559174 100644 --- a/packages/ui/src/proposals/model/validation.ts +++ b/packages/ui/src/proposals/model/validation.ts @@ -16,7 +16,7 @@ export const maxAccounts = (message: string, max: number | undefined): Yup.TestC params: { max }, exclusive: false, test(value: string) { - const pairs = value.split(';\n') + const pairs = value.split('\n') return max ? pairs.length <= max : false }, }) @@ -26,7 +26,7 @@ export const duplicateAccounts = (message: string): Yup.TestConfig => ( test(value: string, testContext) { if (!CSV_PATTERN.test(value)) return false - const pairs = value.split(';\n') + const pairs = value.split('\n') const keyring = testContext?.options?.context?.keyring for (const pair of pairs) { @@ -68,7 +68,7 @@ export const maxFundingAmount = ( params: { max: isJoyValue && isBn(max) ? formatJoyValue(max, { precision: 2 }) : max }, exclusive: false, test(value: string) { - const pairs = value.split(';\n') + const pairs = value.split('\n') let total = new BN(0) const decimals = new BN(10).pow(new BN(JOY_DECIMAL_PLACES)) for (const pair of pairs) { From 283dd51b8b604f6ac17ff9c0d344356e4726d3b0 Mon Sep 17 00:00:00 2001 From: Victor Date: Fri, 28 Jul 2023 18:08:01 +0100 Subject: [PATCH 11/27] Multiple funding request test --- packages/ui/.storybook/preview.tsx | 15 +++++++ .../Proposals/CurrentProposals.stories.tsx | 40 +++++++++++++++++++ .../SpecificParameters/FundingRequest.tsx | 2 +- .../PreviewAndValidateModal.tsx | 4 +- packages/ui/src/proposals/model/validation.ts | 8 ++-- 5 files changed, 61 insertions(+), 8 deletions(-) diff --git a/packages/ui/.storybook/preview.tsx b/packages/ui/.storybook/preview.tsx index fc6b5ec906..afe1f720e6 100644 --- a/packages/ui/.storybook/preview.tsx +++ b/packages/ui/.storybook/preview.tsx @@ -15,6 +15,9 @@ import { ModalContextProvider } from '../src/common/providers/modal/provider' import { TransactionStatusProvider } from '../src/common/providers/transactionStatus/provider' import { MockProvidersDecorator, MockRouterDecorator } from '../src/mocks/providers' import { i18next } from '../src/services/i18n' +import { KeyringContext } from '../src/common/providers/keyring/context' +import { Keyring } from '@polkadot/ui-keyring' + configure({ testIdAttribute: 'id' }) @@ -64,11 +67,23 @@ const ModalDecorator: Decorator = (Story) => ( ) +const KeyringDecorator: Decorator = (Story) => { + const keyring = { + encodeAddress: (address: string) => address, + decodeAddress: (address: string) => { + if (!/^[A-HJ-NP-Za-km-z1-9]{10,}$/.test(address)) throw new Error('Invalid address') + else return address + }, + } as unknown as Keyring + return +} + export const decorators = [ ModalDecorator, stylesWrapperDecorator, i18nextDecorator, RHFDecorator, + KeyringDecorator, MockProvidersDecorator, MockRouterDecorator, ] diff --git a/packages/ui/src/app/pages/Proposals/CurrentProposals.stories.tsx b/packages/ui/src/app/pages/Proposals/CurrentProposals.stories.tsx index 20eac6baec..0cf8a297d6 100644 --- a/packages/ui/src/app/pages/Proposals/CurrentProposals.stories.tsx +++ b/packages/ui/src/app/pages/Proposals/CurrentProposals.stories.tsx @@ -781,6 +781,46 @@ export const SpecificParametersFundingRequest: Story = { }), } +export const SpecificParametersMultipleFundingRequest: Story = { + play: specificParametersTest('Funding Request', async ({ args, createProposal, modal, step }) => { + const bob = member('bob') + await createProposal(async () => { + const nextButton = getButtonByText(modal, 'Create proposal') + expect(nextButton).toBeDisabled() + + await userEvent.click(modal.getByTestId('pay-multiple')) + + const csvField = modal.getByTestId('accounts-amounts') + + // Invalid + await userEvent.clear(csvField) + await userEvent.type(csvField, `${alice.controllerAccount},500${bob.controllerAccount},500`) + expect(modal.findByText(/Not valid CSV format/)) + expect(nextButton).toBeDisabled() + + // Valid + await userEvent.clear(csvField) + await userEvent.type( + csvField, + `${alice.controllerAccount},500\n${bob.controllerAccount},500` + ) + await waitFor(() => expect(modal.queryByText(/Not valid CSV format/)).toBeNull()) + expect(nextButton).toBeDisabled() + const previewButton = getButtonByText(modal, 'Preview and Validate') + + await userEvent.click(previewButton) + await userEvent.click(modal.getByTestId('sidePanel')) + }) + + step('Transaction parameters', () => { + const [, specificParameters] = args.onCreateProposal.mock.calls.at(-1) + expect(specificParameters.toJSON()).toEqual({ + fundingRequest: [{ account: alice.controllerAccount, amount: 500_0000000000 },{ account: bob.controllerAccount, amount: 500_0000000000 }], + }) + }) + }), +} + export const SpecificParametersSetReferralCut: Story = { play: specificParametersTest('Set Referral Cut', async ({ args, createProposal, modal, step }) => { await createProposal(async () => { diff --git a/packages/ui/src/proposals/modals/AddNewProposal/components/SpecificParameters/FundingRequest.tsx b/packages/ui/src/proposals/modals/AddNewProposal/components/SpecificParameters/FundingRequest.tsx index b15db8327b..6bc26d0598 100644 --- a/packages/ui/src/proposals/modals/AddNewProposal/components/SpecificParameters/FundingRequest.tsx +++ b/packages/ui/src/proposals/modals/AddNewProposal/components/SpecificParameters/FundingRequest.tsx @@ -65,7 +65,7 @@ export const FundingRequest = () => { - + {payMultiple && ( diff --git a/packages/ui/src/proposals/modals/AddNewProposal/components/SpecificParameters/modals/PreviewAndValidate/PreviewAndValidateModal.tsx b/packages/ui/src/proposals/modals/AddNewProposal/components/SpecificParameters/modals/PreviewAndValidate/PreviewAndValidateModal.tsx index 0693cc425d..18e7cf43cc 100644 --- a/packages/ui/src/proposals/modals/AddNewProposal/components/SpecificParameters/modals/PreviewAndValidate/PreviewAndValidateModal.tsx +++ b/packages/ui/src/proposals/modals/AddNewProposal/components/SpecificParameters/modals/PreviewAndValidate/PreviewAndValidateModal.tsx @@ -51,7 +51,7 @@ export const PreviewAndValidateModal = ({ onClose }: PreviewAndValidateModalProp const { api } = useApi() const { setValue, getValues } = useFormContext() const maxTotalAmount = api?.consts.proposalsCodex.fundingRequestProposalMaxTotalAmount - const maxAllowedAccounts = api?.consts.proposalsCodex.fundingRequestProposalMaxAccounts.toNumber() + const maxAllowedAccounts = api?.consts.proposalsCodex.fundingRequestProposalMaxAccounts?.toNumber() const keyring = useKeyring() const { allAccounts } = useMyAccounts() const accounts = allAccounts as AccountOption[] @@ -132,7 +132,7 @@ export const PreviewAndValidateModal = ({ onClose }: PreviewAndValidateModalProp } }, [totalAmount]) return ( - + diff --git a/packages/ui/src/proposals/model/validation.ts b/packages/ui/src/proposals/model/validation.ts index 4bfa559174..a06843e8a2 100644 --- a/packages/ui/src/proposals/model/validation.ts +++ b/packages/ui/src/proposals/model/validation.ts @@ -4,7 +4,6 @@ import * as Yup from 'yup' import Reference from 'yup/lib/Reference' import { AnyObject } from 'yup/lib/types' -import { isValidAddress } from '@/accounts/model/isValidAddress' import { JOY_DECIMAL_PLACES } from '@/common/constants' import { formatJoyValue } from '@/common/model/formatters' @@ -43,15 +42,14 @@ export const isValidCSV = (message: string): Yup.TestConfig => ( message, name: 'isValidCSV', exclusive: false, - test(value: string, testContext) { + test(value: string) { if (!CSV_PATTERN.test(value)) return false const pairs = value.split('\n') - const keyring = testContext?.options?.context?.keyring for (const pair of pairs) { - const [address, amount] = pair.split(',') - if (!Number(amount) || !isValidAddress(address, keyring)) return false + const [, amount] = pair.split(',') + if (!Number(amount)) return false } return true From 91cb882258759b52ffd669280b52c922641867e6 Mon Sep 17 00:00:00 2001 From: Victor Date: Fri, 28 Jul 2023 18:25:10 +0100 Subject: [PATCH 12/27] lint --- .../pages/Proposals/CurrentProposals.stories.tsx | 14 +++++++------- .../SpecificParameters/FundingRequest.tsx | 2 +- .../PreviewAndValidate/PreviewAndValidateModal.tsx | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/ui/src/app/pages/Proposals/CurrentProposals.stories.tsx b/packages/ui/src/app/pages/Proposals/CurrentProposals.stories.tsx index 0cf8a297d6..f647b77fce 100644 --- a/packages/ui/src/app/pages/Proposals/CurrentProposals.stories.tsx +++ b/packages/ui/src/app/pages/Proposals/CurrentProposals.stories.tsx @@ -791,7 +791,7 @@ export const SpecificParametersMultipleFundingRequest: Story = { await userEvent.click(modal.getByTestId('pay-multiple')) const csvField = modal.getByTestId('accounts-amounts') - + // Invalid await userEvent.clear(csvField) await userEvent.type(csvField, `${alice.controllerAccount},500${bob.controllerAccount},500`) @@ -800,14 +800,11 @@ export const SpecificParametersMultipleFundingRequest: Story = { // Valid await userEvent.clear(csvField) - await userEvent.type( - csvField, - `${alice.controllerAccount},500\n${bob.controllerAccount},500` - ) + await userEvent.type(csvField, `${alice.controllerAccount},500\n${bob.controllerAccount},500`) await waitFor(() => expect(modal.queryByText(/Not valid CSV format/)).toBeNull()) expect(nextButton).toBeDisabled() const previewButton = getButtonByText(modal, 'Preview and Validate') - + await userEvent.click(previewButton) await userEvent.click(modal.getByTestId('sidePanel')) }) @@ -815,7 +812,10 @@ export const SpecificParametersMultipleFundingRequest: Story = { step('Transaction parameters', () => { const [, specificParameters] = args.onCreateProposal.mock.calls.at(-1) expect(specificParameters.toJSON()).toEqual({ - fundingRequest: [{ account: alice.controllerAccount, amount: 500_0000000000 },{ account: bob.controllerAccount, amount: 500_0000000000 }], + fundingRequest: [ + { account: alice.controllerAccount, amount: 500_0000000000 }, + { account: bob.controllerAccount, amount: 500_0000000000 }, + ], }) }) }), diff --git a/packages/ui/src/proposals/modals/AddNewProposal/components/SpecificParameters/FundingRequest.tsx b/packages/ui/src/proposals/modals/AddNewProposal/components/SpecificParameters/FundingRequest.tsx index 6bc26d0598..8ae5b78b66 100644 --- a/packages/ui/src/proposals/modals/AddNewProposal/components/SpecificParameters/FundingRequest.tsx +++ b/packages/ui/src/proposals/modals/AddNewProposal/components/SpecificParameters/FundingRequest.tsx @@ -65,7 +65,7 @@ export const FundingRequest = () => { - + {payMultiple && ( diff --git a/packages/ui/src/proposals/modals/AddNewProposal/components/SpecificParameters/modals/PreviewAndValidate/PreviewAndValidateModal.tsx b/packages/ui/src/proposals/modals/AddNewProposal/components/SpecificParameters/modals/PreviewAndValidate/PreviewAndValidateModal.tsx index 18e7cf43cc..82121a1a08 100644 --- a/packages/ui/src/proposals/modals/AddNewProposal/components/SpecificParameters/modals/PreviewAndValidate/PreviewAndValidateModal.tsx +++ b/packages/ui/src/proposals/modals/AddNewProposal/components/SpecificParameters/modals/PreviewAndValidate/PreviewAndValidateModal.tsx @@ -132,7 +132,7 @@ export const PreviewAndValidateModal = ({ onClose }: PreviewAndValidateModalProp } }, [totalAmount]) return ( - + From 855aa76d7a46d712f289b781460505cd5616afe3 Mon Sep 17 00:00:00 2001 From: Victor Emmanuel <33874323+vrrayz@users.noreply.github.com> Date: Sun, 6 Aug 2023 14:40:34 +0100 Subject: [PATCH 13/27] Update packages/ui/src/app/pages/Proposals/CurrentProposals.stories.tsx Co-authored-by: Theophile Sandoz --- .../ui/src/app/pages/Proposals/CurrentProposals.stories.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ui/src/app/pages/Proposals/CurrentProposals.stories.tsx b/packages/ui/src/app/pages/Proposals/CurrentProposals.stories.tsx index f647b77fce..281995ae4b 100644 --- a/packages/ui/src/app/pages/Proposals/CurrentProposals.stories.tsx +++ b/packages/ui/src/app/pages/Proposals/CurrentProposals.stories.tsx @@ -795,7 +795,7 @@ export const SpecificParametersMultipleFundingRequest: Story = { // Invalid await userEvent.clear(csvField) await userEvent.type(csvField, `${alice.controllerAccount},500${bob.controllerAccount},500`) - expect(modal.findByText(/Not valid CSV format/)) + expect(await modal.findByText(/Not valid CSV format/)) expect(nextButton).toBeDisabled() // Valid From 44dcd9a0d09b35f0481a7074e5821527df05a66c Mon Sep 17 00:00:00 2001 From: Victor Date: Mon, 7 Aug 2023 13:16:35 +0100 Subject: [PATCH 14/27] requested changes --- .../Proposals/CurrentProposals.stories.tsx | 50 +++++++++++++++++-- packages/ui/src/mocks/data/proposals.ts | 1 + .../PreviewAndValidateModal.tsx | 4 +- .../AddNewProposal/getSpecificParameters.ts | 12 +---- 4 files changed, 51 insertions(+), 16 deletions(-) diff --git a/packages/ui/src/app/pages/Proposals/CurrentProposals.stories.tsx b/packages/ui/src/app/pages/Proposals/CurrentProposals.stories.tsx index 281995ae4b..94d386a436 100644 --- a/packages/ui/src/app/pages/Proposals/CurrentProposals.stories.tsx +++ b/packages/ui/src/app/pages/Proposals/CurrentProposals.stories.tsx @@ -784,6 +784,7 @@ export const SpecificParametersFundingRequest: Story = { export const SpecificParametersMultipleFundingRequest: Story = { play: specificParametersTest('Funding Request', async ({ args, createProposal, modal, step }) => { const bob = member('bob') + const charlie = member('charlie') await createProposal(async () => { const nextButton = getButtonByText(modal, 'Create proposal') expect(nextButton).toBeDisabled() @@ -796,17 +797,60 @@ export const SpecificParametersMultipleFundingRequest: Story = { await userEvent.clear(csvField) await userEvent.type(csvField, `${alice.controllerAccount},500${bob.controllerAccount},500`) expect(await modal.findByText(/Not valid CSV format/)) + // ensure its not being open-able while the CSV syntax is valid + const previewButton = getButtonByText(modal, 'Preview and Validate') + await userEvent.click(previewButton) + await waitFor(() => expect(modal.queryByTestId('sidePanel-overlay')).toBeNull()) + expect(nextButton).toBeDisabled() + + // Invalid Accounts error + await userEvent.clear(csvField) + await userEvent.type(csvField, `5GNJqTPy,500\n${bob.controllerAccount},500`) + + await waitFor(() => expect(modal.queryByText(/Not valid CSV format/)).toBeNull()) + expect(await modal.findByText(/Please preview and validate the inputs to proceed/)) + expect(nextButton).toBeDisabled() + + await userEvent.click(previewButton) + expect(await modal.findByText(/Incorrect destination accounts detected/)) + await userEvent.click(modal.getByTestId('sidePanel-overlay')) + + // Max Amount error + await userEvent.clear(csvField) + await userEvent.type(csvField, `${alice.controllerAccount},166667\n${bob.controllerAccount},500`) + expect(await modal.findByText(/Please preview and validate the inputs to proceed/)) expect(nextButton).toBeDisabled() + await userEvent.click(previewButton) + expect(await modal.findByText(/Max payment amount is exceeded/)) + await userEvent.click(modal.getByTestId('sidePanel-overlay')) //ensure create proposal is still disabled + expect(nextButton).toBeDisabled() + + // Max Allowed Accounts error + await userEvent.clear(csvField) + await userEvent.type( + csvField, + `${alice.controllerAccount},400\n${bob.controllerAccount},500\n${charlie.controllerAccount},500` + ) + expect(await modal.findByText(/Please preview and validate the inputs to proceed/)) + expect(nextButton).toBeDisabled() + await userEvent.click(previewButton) + expect(await modal.findByText(/Maximum allowed accounts exceeded/)) + await userEvent.click(modal.getByTestId('sidePanel-overlay')) //ensure create proposal is still disabled + expect(nextButton).toBeDisabled() + + // delete one account from the list' + await userEvent.click(previewButton) + await userEvent.click(modal.getByTestId('removeAccount-2')) + await waitFor(() => expect(modal.queryByText(/Maximum allowed accounts exceeded/)).toBeNull()) + await userEvent.click(modal.getByTestId('sidePanel-overlay')) // Valid await userEvent.clear(csvField) await userEvent.type(csvField, `${alice.controllerAccount},500\n${bob.controllerAccount},500`) - await waitFor(() => expect(modal.queryByText(/Not valid CSV format/)).toBeNull()) expect(nextButton).toBeDisabled() - const previewButton = getButtonByText(modal, 'Preview and Validate') await userEvent.click(previewButton) - await userEvent.click(modal.getByTestId('sidePanel')) + await userEvent.click(modal.getByTestId('sidePanel-overlay')) }) step('Transaction parameters', () => { diff --git a/packages/ui/src/mocks/data/proposals.ts b/packages/ui/src/mocks/data/proposals.ts index cee1a07020..e1a05b49da 100644 --- a/packages/ui/src/mocks/data/proposals.ts +++ b/packages/ui/src/mocks/data/proposals.ts @@ -203,6 +203,7 @@ export const proposalsPagesChain = ( proposalsCodex: { fundingRequestProposalMaxTotalAmount: joy(166_666), + fundingRequestProposalMaxAccounts:2, setMaxValidatorCountProposalMaxValidators, ...Object.fromEntries( diff --git a/packages/ui/src/proposals/modals/AddNewProposal/components/SpecificParameters/modals/PreviewAndValidate/PreviewAndValidateModal.tsx b/packages/ui/src/proposals/modals/AddNewProposal/components/SpecificParameters/modals/PreviewAndValidate/PreviewAndValidateModal.tsx index 82121a1a08..db13623f14 100644 --- a/packages/ui/src/proposals/modals/AddNewProposal/components/SpecificParameters/modals/PreviewAndValidate/PreviewAndValidateModal.tsx +++ b/packages/ui/src/proposals/modals/AddNewProposal/components/SpecificParameters/modals/PreviewAndValidate/PreviewAndValidateModal.tsx @@ -132,7 +132,7 @@ export const PreviewAndValidateModal = ({ onClose }: PreviewAndValidateModalProp } }, [totalAmount]) return ( - + @@ -158,7 +158,7 @@ export const PreviewAndValidateModal = ({ onClose }: PreviewAndValidateModalProp - removeAccount(i)} /> + removeAccount(i)} /> ))} diff --git a/packages/ui/src/proposals/modals/AddNewProposal/getSpecificParameters.ts b/packages/ui/src/proposals/modals/AddNewProposal/getSpecificParameters.ts index 5a0bc504b0..1331d34af4 100644 --- a/packages/ui/src/proposals/modals/AddNewProposal/getSpecificParameters.ts +++ b/packages/ui/src/proposals/modals/AddNewProposal/getSpecificParameters.ts @@ -1,8 +1,7 @@ import { OpeningMetadata } from '@joystream/metadata-protobuf' -// import BN from 'bn.js' import { Api } from '@/api' -import { BN_ZERO /*JOY_DECIMAL_PLACES*/ } from '@/common/constants' +import { BN_ZERO } from '@/common/constants' import { createType } from '@/common/model/createType' import { metadataToBytes } from '@/common/model/JoystreamNode' import { last } from '@/common/utils' @@ -15,15 +14,6 @@ const idToRuntimeId = (id: string): number => Number(last(id.split('-'))) const getWorkingGroupParam = (groupId: GroupIdName | undefined) => groupId && GroupIdToGroupParam[groupId] -// const mapAccountsAndAmounts = (input: string | undefined) => { -// const decimals = new BN(10).pow(new BN(JOY_DECIMAL_PLACES)) -// return input?.split(';\n').map((item) => { -// const [address, amount] = item.split(',') -// const formattedAmount = new BN(amount).mul(decimals) -// return { amount: formattedAmount, account: address } -// }) -// } - export const getSpecificParameters = async ( api: Api, specifics: Omit From 172518fa3c3361016df7940233308c49cc1f6a86 Mon Sep 17 00:00:00 2001 From: Victor Date: Mon, 7 Aug 2023 13:36:32 +0100 Subject: [PATCH 15/27] lint --- packages/ui/src/mocks/data/proposals.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ui/src/mocks/data/proposals.ts b/packages/ui/src/mocks/data/proposals.ts index e1a05b49da..9c5bdd8f3e 100644 --- a/packages/ui/src/mocks/data/proposals.ts +++ b/packages/ui/src/mocks/data/proposals.ts @@ -203,7 +203,7 @@ export const proposalsPagesChain = ( proposalsCodex: { fundingRequestProposalMaxTotalAmount: joy(166_666), - fundingRequestProposalMaxAccounts:2, + fundingRequestProposalMaxAccounts: 2, setMaxValidatorCountProposalMaxValidators, ...Object.fromEntries( From 617ab12830622a6aeed1209d42e79527eabc211e Mon Sep 17 00:00:00 2001 From: Victor Date: Fri, 11 Aug 2023 10:19:48 +0100 Subject: [PATCH 16/27] disable preview button --- .../SpecificParameters/FundingRequest.tsx | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/packages/ui/src/proposals/modals/AddNewProposal/components/SpecificParameters/FundingRequest.tsx b/packages/ui/src/proposals/modals/AddNewProposal/components/SpecificParameters/FundingRequest.tsx index 8ae5b78b66..2f833715a9 100644 --- a/packages/ui/src/proposals/modals/AddNewProposal/components/SpecificParameters/FundingRequest.tsx +++ b/packages/ui/src/proposals/modals/AddNewProposal/components/SpecificParameters/FundingRequest.tsx @@ -18,7 +18,6 @@ import { Row } from '@/common/components/Modal' import { RowGapBlock } from '@/common/components/page/PageContent' import { Tooltip, TooltipDefault } from '@/common/components/Tooltip' import { TextMedium, TextSmall, TextInlineSmall } from '@/common/components/typography' -import { CSV_PATTERN } from '@/proposals/constants/regExp' import { PreviewAndValidateModal } from './modals/PreviewAndValidate' import { ErrorPrompt, Prompt } from './Prompt' @@ -35,12 +34,18 @@ export const FundingRequest = () => { setValue('fundingRequest.hasPreviewedInput', false, { shouldValidate: true }) } }, [csvInput]) - const previewInput = useCallback(() => { + + const shouldDisablePreviewButton = () => { + const input = getFieldState('fundingRequest.csvInput') + return input.error !== undefined || !input.isDirty + } + const shouldPreview = useCallback(() => { const input = getValues('fundingRequest.csvInput') - if (CSV_PATTERN.test(input)) { - setIsPreviewModalShown(true) + if(getFieldState('fundingRequest.csvInput').error !== undefined || !input || hasPreviewedInput) { + return false } - }, []) + return true + },[hasPreviewedInput]) return ( @@ -104,8 +109,10 @@ export const FundingRequest = () => { /> - {!hasPreviewedInput && Please preview and validate the inputs to proceed} - previewInput()}> + {shouldPreview() && ( + Please preview and validate the inputs to proceed + )} + setIsPreviewModalShown(true)}> Preview and Validate From 59f10f277531b9ee0d44e488291e625a205bc565 Mon Sep 17 00:00:00 2001 From: Victor Date: Fri, 11 Aug 2023 11:15:03 +0100 Subject: [PATCH 17/27] updated storybook test for multiple funding request --- .../pages/Proposals/CurrentProposals.stories.tsx | 7 ++++++- .../SpecificParameters/FundingRequest.tsx | 16 +++++++++------- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/packages/ui/src/app/pages/Proposals/CurrentProposals.stories.tsx b/packages/ui/src/app/pages/Proposals/CurrentProposals.stories.tsx index 94d386a436..1852e1232d 100644 --- a/packages/ui/src/app/pages/Proposals/CurrentProposals.stories.tsx +++ b/packages/ui/src/app/pages/Proposals/CurrentProposals.stories.tsx @@ -799,7 +799,7 @@ export const SpecificParametersMultipleFundingRequest: Story = { expect(await modal.findByText(/Not valid CSV format/)) // ensure its not being open-able while the CSV syntax is valid const previewButton = getButtonByText(modal, 'Preview and Validate') - await userEvent.click(previewButton) + expect(previewButton).toBeDisabled() await waitFor(() => expect(modal.queryByTestId('sidePanel-overlay')).toBeNull()) expect(nextButton).toBeDisabled() @@ -810,6 +810,7 @@ export const SpecificParametersMultipleFundingRequest: Story = { await waitFor(() => expect(modal.queryByText(/Not valid CSV format/)).toBeNull()) expect(await modal.findByText(/Please preview and validate the inputs to proceed/)) expect(nextButton).toBeDisabled() + expect(previewButton).toBeEnabled() await userEvent.click(previewButton) expect(await modal.findByText(/Incorrect destination accounts detected/)) @@ -820,6 +821,7 @@ export const SpecificParametersMultipleFundingRequest: Story = { await userEvent.type(csvField, `${alice.controllerAccount},166667\n${bob.controllerAccount},500`) expect(await modal.findByText(/Please preview and validate the inputs to proceed/)) expect(nextButton).toBeDisabled() + await waitFor(() => expect(previewButton).toBeEnabled()) await userEvent.click(previewButton) expect(await modal.findByText(/Max payment amount is exceeded/)) await userEvent.click(modal.getByTestId('sidePanel-overlay')) //ensure create proposal is still disabled @@ -833,12 +835,14 @@ export const SpecificParametersMultipleFundingRequest: Story = { ) expect(await modal.findByText(/Please preview and validate the inputs to proceed/)) expect(nextButton).toBeDisabled() + await waitFor(() => expect(previewButton).toBeEnabled()) await userEvent.click(previewButton) expect(await modal.findByText(/Maximum allowed accounts exceeded/)) await userEvent.click(modal.getByTestId('sidePanel-overlay')) //ensure create proposal is still disabled expect(nextButton).toBeDisabled() // delete one account from the list' + await waitFor(() => expect(previewButton).toBeEnabled()) await userEvent.click(previewButton) await userEvent.click(modal.getByTestId('removeAccount-2')) await waitFor(() => expect(modal.queryByText(/Maximum allowed accounts exceeded/)).toBeNull()) @@ -849,6 +853,7 @@ export const SpecificParametersMultipleFundingRequest: Story = { await userEvent.type(csvField, `${alice.controllerAccount},500\n${bob.controllerAccount},500`) expect(nextButton).toBeDisabled() + await waitFor(() => expect(previewButton).toBeEnabled()) await userEvent.click(previewButton) await userEvent.click(modal.getByTestId('sidePanel-overlay')) }) diff --git a/packages/ui/src/proposals/modals/AddNewProposal/components/SpecificParameters/FundingRequest.tsx b/packages/ui/src/proposals/modals/AddNewProposal/components/SpecificParameters/FundingRequest.tsx index 2f833715a9..e09c4aa0a4 100644 --- a/packages/ui/src/proposals/modals/AddNewProposal/components/SpecificParameters/FundingRequest.tsx +++ b/packages/ui/src/proposals/modals/AddNewProposal/components/SpecificParameters/FundingRequest.tsx @@ -34,18 +34,18 @@ export const FundingRequest = () => { setValue('fundingRequest.hasPreviewedInput', false, { shouldValidate: true }) } }, [csvInput]) - + const shouldDisablePreviewButton = () => { const input = getFieldState('fundingRequest.csvInput') return input.error !== undefined || !input.isDirty } const shouldPreview = useCallback(() => { const input = getValues('fundingRequest.csvInput') - if(getFieldState('fundingRequest.csvInput').error !== undefined || !input || hasPreviewedInput) { + if (getFieldState('fundingRequest.csvInput').error !== undefined || !input || hasPreviewedInput) { return false } return true - },[hasPreviewedInput]) + }, [hasPreviewedInput]) return ( @@ -109,10 +109,12 @@ export const FundingRequest = () => { /> - {shouldPreview() && ( - Please preview and validate the inputs to proceed - )} - setIsPreviewModalShown(true)}> + {shouldPreview() && Please preview and validate the inputs to proceed} + setIsPreviewModalShown(true)} + > Preview and Validate From 313eb983fab460b411f4037143269750fc51620e Mon Sep 17 00:00:00 2001 From: Victor Date: Fri, 11 Aug 2023 13:46:33 +0100 Subject: [PATCH 18/27] requested changes --- .../ui/src/common/components/Modal/Modal.tsx | 26 +-------- .../AddNewProposal/AddNewProposalModal.tsx | 3 - .../SpecificParameters/FundingRequest.tsx | 27 +++------ packages/ui/src/proposals/model/validation.ts | 57 ------------------- 4 files changed, 10 insertions(+), 103 deletions(-) diff --git a/packages/ui/src/common/components/Modal/Modal.tsx b/packages/ui/src/common/components/Modal/Modal.tsx index 2811b5811f..731d068bc8 100644 --- a/packages/ui/src/common/components/Modal/Modal.tsx +++ b/packages/ui/src/common/components/Modal/Modal.tsx @@ -54,20 +54,9 @@ interface ModalProps { children: ReactNode isDark?: boolean className?: any - customModalSize?: any - marginRight?: any } -export const Modal = ({ - onClose, - modalHeight = 'm', - children, - modalSize, - isDark, - className, - customModalSize, - marginRight, -}: ModalProps) => { +export const Modal = ({ onClose, modalHeight = 'm', children, modalSize, isDark, className }: ModalProps) => { function onBackgroundClick(e: React.MouseEvent) { if (e.target === e.currentTarget) { onClose() @@ -79,14 +68,7 @@ export const Modal = ({ return ( <> - + {children} @@ -221,7 +203,6 @@ interface ModalWrapProps { modalMaxSize: string isDark?: boolean modalHeight?: ModalHeight - marginRight?: string } export const ModalWrap = styled.section` @@ -229,7 +210,6 @@ export const ModalWrap = styled.section` position: absolute; inset: 0; margin: auto auto; - margin-right: ${({ marginRight }) => (marginRight ? `${marginRight}px` : 'auto')}; display: grid; @media only screen and (max-height: 700px) { max-height: 100%; @@ -253,8 +233,6 @@ export const ModalWrap = styled.section` return '904px' case 'l': return '1240px' - default: - return `${modalMaxSize}px` } }}; height: ${({ modalHeight }) => (modalHeight === 'xl' ? '90vh' : 'min-content')}; diff --git a/packages/ui/src/proposals/modals/AddNewProposal/AddNewProposalModal.tsx b/packages/ui/src/proposals/modals/AddNewProposal/AddNewProposalModal.tsx index 988e6ddf0a..c1fe25bee3 100644 --- a/packages/ui/src/proposals/modals/AddNewProposal/AddNewProposalModal.tsx +++ b/packages/ui/src/proposals/modals/AddNewProposal/AddNewProposalModal.tsx @@ -24,7 +24,6 @@ import { TextMedium, TokenValue } from '@/common/components/typography' import { BN_ZERO } from '@/common/constants' import { camelCaseToText } from '@/common/helpers' import { useCurrentBlockNumber } from '@/common/hooks/useCurrentBlockNumber' -import { useKeyring } from '@/common/hooks/useKeyring' import { useLocalStorage } from '@/common/hooks/useLocalStorage' import { useMachine } from '@/common/hooks/useMachine' import { useModal } from '@/common/hooks/useModal' @@ -87,7 +86,6 @@ export const AddNewProposalModal = () => { const stakingStatus = useStakingAccountStatus(formMap[0]?.address, activeMember?.id, [state.matches('transaction')]) const schema = useMemo(() => schemaFactory(api), [!api]) - const keyring = useKeyring() const path = useMemo(() => machineStateConverter(state.value) as keyof AddNewProposalForm, [state.value]) const form = useForm({ @@ -108,7 +106,6 @@ export const AddNewProposalModal = () => { minTriggerBlock: currentBlock ? currentBlock.addn(constants?.votingPeriod ?? 0).addn(constants?.gracePeriod ?? 0) : BN_ZERO, - keyring, } as IStakingAccountSchema, defaultValues: defaultProposalValues, }) diff --git a/packages/ui/src/proposals/modals/AddNewProposal/components/SpecificParameters/FundingRequest.tsx b/packages/ui/src/proposals/modals/AddNewProposal/components/SpecificParameters/FundingRequest.tsx index e09c4aa0a4..121a9e70a6 100644 --- a/packages/ui/src/proposals/modals/AddNewProposal/components/SpecificParameters/FundingRequest.tsx +++ b/packages/ui/src/proposals/modals/AddNewProposal/components/SpecificParameters/FundingRequest.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useEffect, useState } from 'react' +import React, { useEffect, useState } from 'react' import { useFormContext } from 'react-hook-form' import styled from 'styled-components' @@ -23,7 +23,7 @@ import { PreviewAndValidateModal } from './modals/PreviewAndValidate' import { ErrorPrompt, Prompt } from './Prompt' export const FundingRequest = () => { - const { watch, setValue, getValues, getFieldState } = useFormContext() + const { watch, setValue, getFieldState } = useFormContext() const [isPreviewModalShown, setIsPreviewModalShown] = useState(false) const [payMultiple] = watch(['fundingRequest.payMultiple']) const [hasPreviewedInput] = watch(['fundingRequest.hasPreviewedInput'], { 'fundingRequest.hasPreviewedInput': true }) @@ -35,17 +35,8 @@ export const FundingRequest = () => { } }, [csvInput]) - const shouldDisablePreviewButton = () => { - const input = getFieldState('fundingRequest.csvInput') - return input.error !== undefined || !input.isDirty - } - const shouldPreview = useCallback(() => { - const input = getValues('fundingRequest.csvInput') - if (getFieldState('fundingRequest.csvInput').error !== undefined || !input || hasPreviewedInput) { - return false - } - return true - }, [hasPreviewedInput]) + const canPreviewInput = csvInput.isDirty && !csvInput.error + return ( @@ -109,12 +100,10 @@ export const FundingRequest = () => { /> - {shouldPreview() && Please preview and validate the inputs to proceed} - setIsPreviewModalShown(true)} - > + {canPreviewInput && !hasPreviewedInput && ( + Please preview and validate the inputs to proceed + )} + setIsPreviewModalShown(true)}> Preview and Validate diff --git a/packages/ui/src/proposals/model/validation.ts b/packages/ui/src/proposals/model/validation.ts index a06843e8a2..bd8b63a66d 100644 --- a/packages/ui/src/proposals/model/validation.ts +++ b/packages/ui/src/proposals/model/validation.ts @@ -1,43 +1,8 @@ -import { isBn } from '@polkadot/util' -import BN from 'bn.js' import * as Yup from 'yup' -import Reference from 'yup/lib/Reference' import { AnyObject } from 'yup/lib/types' -import { JOY_DECIMAL_PLACES } from '@/common/constants' -import { formatJoyValue } from '@/common/model/formatters' - import { CSV_PATTERN } from '../constants/regExp' -export const maxAccounts = (message: string, max: number | undefined): Yup.TestConfig => ({ - message, - name: 'maxAccounts', - params: { max }, - exclusive: false, - test(value: string) { - const pairs = value.split('\n') - return max ? pairs.length <= max : false - }, -}) - -export const duplicateAccounts = (message: string): Yup.TestConfig => ({ - message, - name: 'duplicateAccounts', - exclusive: false, - test(value: string) { - const pairs = value.split('\n') - const addresses: string[] = [] - - for (const pair of pairs) { - const [address] = pair.split(',') - if (addresses.indexOf(address) >= 0) return false - addresses.push(address) - } - - return true - }, -}) - export const isValidCSV = (message: string): Yup.TestConfig => ({ message, name: 'isValidCSV', @@ -55,25 +20,3 @@ export const isValidCSV = (message: string): Yup.TestConfig => ( return true }, }) - -export const maxFundingAmount = ( - message: string, - max: Reference | number | BN | undefined, - isJoyValue = true -): Yup.TestConfig => ({ - message, - name: 'maxFundingAmount', - params: { max: isJoyValue && isBn(max) ? formatJoyValue(max, { precision: 2 }) : max }, - exclusive: false, - test(value: string) { - const pairs = value.split('\n') - let total = new BN(0) - const decimals = new BN(10).pow(new BN(JOY_DECIMAL_PLACES)) - for (const pair of pairs) { - const [, amount] = pair.split(',') - total = total.add(new BN(amount)) - } - total = total.mul(decimals) - return !total || typeof max === 'undefined' || !isBn(total) || total.lte(new BN(this.resolve(max))) - }, -}) From f58a6fb479ec0dfbfa4646765e955a6c5298bf07 Mon Sep 17 00:00:00 2001 From: Victor Date: Fri, 11 Aug 2023 15:11:01 +0100 Subject: [PATCH 19/27] requested changes --- .../components/SpecificParameters/FundingRequest.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/ui/src/proposals/modals/AddNewProposal/components/SpecificParameters/FundingRequest.tsx b/packages/ui/src/proposals/modals/AddNewProposal/components/SpecificParameters/FundingRequest.tsx index 121a9e70a6..602940d9ed 100644 --- a/packages/ui/src/proposals/modals/AddNewProposal/components/SpecificParameters/FundingRequest.tsx +++ b/packages/ui/src/proposals/modals/AddNewProposal/components/SpecificParameters/FundingRequest.tsx @@ -34,9 +34,8 @@ export const FundingRequest = () => { setValue('fundingRequest.hasPreviewedInput', false, { shouldValidate: true }) } }, [csvInput]) - - const canPreviewInput = csvInput.isDirty && !csvInput.error - + const canPreviewInput = + getFieldState('fundingRequest.csvInput').isDirty && !getFieldState('fundingRequest.csvInput').error return ( From 548fc6dc7b59679ed5af862ab32911fe3fc31059 Mon Sep 17 00:00:00 2001 From: Theophile Sandoz Date: Wed, 9 Aug 2023 17:51:18 +0200 Subject: [PATCH 20/27] Preview multi recipients proposal in storybook --- .../app/pages/Proposals/ProposalPreview.stories.tsx | 8 +++++++- packages/ui/src/mocks/data/proposals.ts | 11 +++++++++++ .../src/mocks/helpers/proposalDetailsToConstantKey.ts | 5 ++++- 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/packages/ui/src/app/pages/Proposals/ProposalPreview.stories.tsx b/packages/ui/src/app/pages/Proposals/ProposalPreview.stories.tsx index 1e9bf67846..088e7f98ab 100644 --- a/packages/ui/src/app/pages/Proposals/ProposalPreview.stories.tsx +++ b/packages/ui/src/app/pages/Proposals/ProposalPreview.stories.tsx @@ -153,7 +153,10 @@ export default { title: PROPOSAL_DATA.title, description: PROPOSAL_DATA.description, status, - type: args.type, + type: + args.type === 'FundingRequestMultipleRecipientsProposalDetails' + ? 'FundingRequestProposalDetails' + : args.type, creator: args.isProposer ? alice : bob, discussionThread: { @@ -228,6 +231,9 @@ export const FillWorkingGroupLeadOpening: Story = { export const FundingRequest: Story = { args: { type: 'FundingRequestProposalDetails' }, } +export const FundingRequestMultipleRecipients: Story = { + args: { type: 'FundingRequestMultipleRecipientsProposalDetails' }, +} export const RuntimeUpgrade: Story = { args: { type: 'RuntimeUpgradeProposalDetails', constitutionality: 2 }, parameters: { diff --git a/packages/ui/src/mocks/data/proposals.ts b/packages/ui/src/mocks/data/proposals.ts index 9c5bdd8f3e..f693b749e0 100644 --- a/packages/ui/src/mocks/data/proposals.ts +++ b/packages/ui/src/mocks/data/proposals.ts @@ -56,6 +56,17 @@ const proposalDetails = { destinations: [{ __typename: 'FundingRequestDestination', amount: joy(200), account: membership.rootAccount }], }, }, + FundingRequestMultipleRecipientsProposalDetails: { + destinationsList: { + __typename: 'FundingRequestDestinationsList', + destinations: [ + { __typename: 'FundingRequestDestination', amount: joy(200), account: membership.rootAccount }, + { __typename: 'FundingRequestDestination', amount: joy(20), account: member('alice').rootAccount }, + { __typename: 'FundingRequestDestination', amount: joy(1), account: member('bob').rootAccount }, + { __typename: 'FundingRequestDestination', amount: joy(500), account: member('charlie').rootAccount }, + ], + }, + }, RuntimeUpgradeProposalDetails: { newRuntimeBytecode: { __typename: 'RuntimeWasmBytecode', id: '0' } }, SetCouncilBudgetIncrementProposalDetails: { newAmount: joy(200) }, SetCouncilorRewardProposalDetails: { newRewardPerBlock: joy(200) }, diff --git a/packages/ui/src/mocks/helpers/proposalDetailsToConstantKey.ts b/packages/ui/src/mocks/helpers/proposalDetailsToConstantKey.ts index efbb50a7d7..18a24e6ecc 100644 --- a/packages/ui/src/mocks/helpers/proposalDetailsToConstantKey.ts +++ b/packages/ui/src/mocks/helpers/proposalDetailsToConstantKey.ts @@ -1,7 +1,9 @@ import { Api } from '@/api' import { ProposalFieldsFragment } from '@/proposals/queries' -export type ProposalDetailsType = ProposalFieldsFragment['details']['__typename'] +export type ProposalDetailsType = + | ProposalFieldsFragment['details']['__typename'] + | 'FundingRequestMultipleRecipientsProposalDetails' export const proposalDetailsToConstantKey = (details: ProposalDetailsType) => proposalDetailsToConstantKeyMap.get(details) as string @@ -13,6 +15,7 @@ const proposalDetailsToConstantKeyMap = new Map Date: Fri, 18 Aug 2023 11:36:50 +0200 Subject: [PATCH 21/27] Fix the story --- .../src/app/pages/Proposals/ProposalPreview.stories.tsx | 5 +---- packages/ui/src/mocks/data/proposals.ts | 8 ++++---- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/packages/ui/src/app/pages/Proposals/ProposalPreview.stories.tsx b/packages/ui/src/app/pages/Proposals/ProposalPreview.stories.tsx index 088e7f98ab..f7fc5515b1 100644 --- a/packages/ui/src/app/pages/Proposals/ProposalPreview.stories.tsx +++ b/packages/ui/src/app/pages/Proposals/ProposalPreview.stories.tsx @@ -153,10 +153,7 @@ export default { title: PROPOSAL_DATA.title, description: PROPOSAL_DATA.description, status, - type: - args.type === 'FundingRequestMultipleRecipientsProposalDetails' - ? 'FundingRequestProposalDetails' - : args.type, + type: args.type, creator: args.isProposer ? alice : bob, discussionThread: { diff --git a/packages/ui/src/mocks/data/proposals.ts b/packages/ui/src/mocks/data/proposals.ts index f693b749e0..af545621a3 100644 --- a/packages/ui/src/mocks/data/proposals.ts +++ b/packages/ui/src/mocks/data/proposals.ts @@ -90,10 +90,10 @@ const proposalDetails = { VetoProposalDetails: { proposal: { __typename: 'Proposal', id: '0', title: random.words(4) } }, } as Record> -export const proposalDetailsMap = mapValues( - proposalDetails, - (value, __typename) => Object.assign(value, { __typename }) as Partial -) +export const proposalDetailsMap = mapValues(proposalDetails, (value, key) => { + const __typename = key === 'FundingRequestMultipleRecipientsProposalDetails' ? 'FundingRequestProposalDetails' : key + return Object.assign(value, { __typename }) as Partial +}) export const proposalTypes = Object.keys(proposalDetailsMap) as ProposalDetailsType[] From 397bc2c05135692da8dab5c500028c50cbb6420a Mon Sep 17 00:00:00 2001 From: Victor Date: Wed, 30 Aug 2023 11:02:58 +0100 Subject: [PATCH 22/27] Preview fix --- .../ProposalDetails/ProposalDetails.tsx | 12 +++ .../renderers/DestinationsPreview.tsx | 102 ++++++++++++++++++ .../ProposalDetails/renderers/index.ts | 1 + .../helpers/getDetailsRenderStructure.ts | 44 +++++--- .../PreviewAndValidateModal.tsx | 8 +- 5 files changed, 151 insertions(+), 16 deletions(-) create mode 100644 packages/ui/src/proposals/components/ProposalDetails/renderers/DestinationsPreview.tsx diff --git a/packages/ui/src/proposals/components/ProposalDetails/ProposalDetails.tsx b/packages/ui/src/proposals/components/ProposalDetails/ProposalDetails.tsx index 65397dcac5..ac5eeb6517 100644 --- a/packages/ui/src/proposals/components/ProposalDetails/ProposalDetails.tsx +++ b/packages/ui/src/proposals/components/ProposalDetails/ProposalDetails.tsx @@ -15,6 +15,7 @@ import { useWorkingGroup } from '@/working-groups/hooks/useWorkingGroup' import { Address, Amount, + DestinationsPreview, Divider, Hash, Markdown, @@ -49,6 +50,7 @@ const renderTypeMapper: Partial> = { OpeningLink: OpeningLink, Percentage: Percentage, Hash: Hash, + DestinationsPreview: DestinationsPreview, } export const ProposalDetails = ({ proposalDetails }: Props) => { @@ -78,6 +80,16 @@ export const ProposalDetails = ({ proposalDetails }: Props) => { ] as RenderNode[] } + if (proposalDetails?.type === 'fundingRequest') { + return [ + { + renderType: 'Amount', + label: 'Current WG Budget', + value: group?.budget, + }, + ] as RenderNode[] + } + if (proposalDetails?.type === 'updateWorkingGroupBudget') { return [ { diff --git a/packages/ui/src/proposals/components/ProposalDetails/renderers/DestinationsPreview.tsx b/packages/ui/src/proposals/components/ProposalDetails/renderers/DestinationsPreview.tsx new file mode 100644 index 0000000000..a22220cc6b --- /dev/null +++ b/packages/ui/src/proposals/components/ProposalDetails/renderers/DestinationsPreview.tsx @@ -0,0 +1,102 @@ +import BN from 'bn.js' +import React, { useEffect, useState } from 'react' + +import { AccountInfo } from '@/accounts/components/AccountInfo' +import { useMyAccounts } from '@/accounts/hooks/useMyAccounts' +import { accountOrNamed } from '@/accounts/model/accountOrNamed' +import { AccountOption } from '@/accounts/types' +import { CloseButton } from '@/common/components/buttons' +import { ArrowRightIcon } from '@/common/components/icons' +import { + BalanceInfoInRow, + InfoTitle, + InfoValue, + ModalFooter, + Row, + TransactionInfoContainer, +} from '@/common/components/Modal' +import { RowGapBlock } from '@/common/components/page/PageContent' +import { SidePaneGlass, SidePaneTitle, SidePanelTop } from '@/common/components/SidePane' +import { StatisticButton } from '@/common/components/statistics/StatisticButton' +import { TransactionFee } from '@/common/components/TransactionFee' +import { TextInlineBig, TokenValue } from '@/common/components/typography' +import { + CustomAccountRow, + PreviewPanel, + PreviewPanelBody, + PreviewPanelHeader, +} from '@/proposals/modals/AddNewProposal/components/SpecificParameters/modals/PreviewAndValidate' + +interface Props { + label: string + value: { account: string; amount: BN }[] +} +export const DestinationsPreview = ({ label, value }: Props) => { + const [isDescriptionVisible, setDescriptionVisible] = useState(false) + const [totalAmount, setTotalAmount] = useState(new BN(0)) + const { allAccounts } = useMyAccounts() + const accounts = allAccounts as AccountOption[] + + const onBackgroundClick = (e: React.MouseEvent) => { + if (e.target === e.currentTarget) { + setDescriptionVisible(false) + } + } + + useEffect(() => { + let total = new BN(0) + value?.map((item) => { + total = total.add(item.amount) + }) + setTotalAmount(total) + }, []) + return ( + <> + { + setDescriptionVisible(true) + }} + icon={} + > + + {label} + + + {isDescriptionVisible && ( + + + + + {label} + setDescriptionVisible(false)} /> + + + + + + {value?.map((previewAccount, i) => ( + + + + Amount + + + + + + ))} + + + + + + + + + + + )} + + ) +} diff --git a/packages/ui/src/proposals/components/ProposalDetails/renderers/index.ts b/packages/ui/src/proposals/components/ProposalDetails/renderers/index.ts index ff221a26b0..eb6ff878fa 100644 --- a/packages/ui/src/proposals/components/ProposalDetails/renderers/index.ts +++ b/packages/ui/src/proposals/components/ProposalDetails/renderers/index.ts @@ -10,3 +10,4 @@ export * from './Numeric' export * from './ProposalLink' export * from './OpeningLink' export * from './Hash' +export * from './DestinationsPreview' diff --git a/packages/ui/src/proposals/helpers/getDetailsRenderStructure.ts b/packages/ui/src/proposals/helpers/getDetailsRenderStructure.ts index 194f1123b1..6c70038e80 100644 --- a/packages/ui/src/proposals/helpers/getDetailsRenderStructure.ts +++ b/packages/ui/src/proposals/helpers/getDetailsRenderStructure.ts @@ -1,3 +1,4 @@ +import BN from 'bn.js' import { omit } from 'lodash' import { TooltipContentProp } from '@/common/components/Tooltip' @@ -39,6 +40,7 @@ export type RenderType = | 'OpeningLink' | 'Percentage' | 'Hash' + | 'DestinationsPreview' export interface RenderNode { label: string @@ -55,23 +57,41 @@ type Mapper = ( const destinationsMapper: Mapper = (value): RenderNode[] => { const result: RenderNode[] = [] - value.forEach((destination) => { - result.push({ - label: 'amount', - value: destination.amount, - renderType: 'Amount', + if (value.length === 1) { + value.forEach((destination) => { + result.push({ + label: 'amount', + value: destination.amount, + renderType: 'Amount', + }) + result.push({ + label: 'destination', + value: destination.account, + renderType: 'Address', + }) + result.push({ + label: '', + value: undefined, + renderType: 'Divider', + }) + }) + } + if (value.length > 1) { + let total = new BN(0) + value.forEach((destination) => { + total = total.add(destination.amount) }) result.push({ - label: 'destination', - value: destination.account, - renderType: 'Address', + label: 'Total Payment', + value: total, + renderType: 'Amount', }) result.push({ - label: '', - value: undefined, - renderType: 'Divider', + label: 'Payment Details', + value: value, + renderType: 'DestinationsPreview', }) - }) + } return result } diff --git a/packages/ui/src/proposals/modals/AddNewProposal/components/SpecificParameters/modals/PreviewAndValidate/PreviewAndValidateModal.tsx b/packages/ui/src/proposals/modals/AddNewProposal/components/SpecificParameters/modals/PreviewAndValidate/PreviewAndValidateModal.tsx index db13623f14..2398af8fdc 100644 --- a/packages/ui/src/proposals/modals/AddNewProposal/components/SpecificParameters/modals/PreviewAndValidate/PreviewAndValidateModal.tsx +++ b/packages/ui/src/proposals/modals/AddNewProposal/components/SpecificParameters/modals/PreviewAndValidate/PreviewAndValidateModal.tsx @@ -174,7 +174,7 @@ export const PreviewAndValidateModal = ({ onClose }: PreviewAndValidateModalProp ) } -const CustomAccountRow = styled(AccountRow)` +export const CustomAccountRow = styled(AccountRow)` margin-bottom: 4px; padding-right: 16px; &.error { @@ -187,12 +187,12 @@ const CustomBalanceInfoInRow = styled(BalanceInfoInRow)` margin-left: auto; } ` -const PreviewPanel = styled(SidePane)` +export const PreviewPanel = styled(SidePane)` grid-template-rows: auto 1fr auto; ` -const PreviewPanelHeader = styled(SidePaneHeader)` +export const PreviewPanelHeader = styled(SidePaneHeader)` padding: 12px 24px; ` -const PreviewPanelBody = styled(SidePaneBody)` +export const PreviewPanelBody = styled(SidePaneBody)` padding: 12px 24px; ` From 733d99bd02566783af2427414402171e4f921f39 Mon Sep 17 00:00:00 2001 From: Victor Date: Thu, 31 Aug 2023 07:43:42 +0100 Subject: [PATCH 23/27] build fix --- .../helpers/getDetailsRenderStructure.test.ts | 58 ++++++------------- 1 file changed, 18 insertions(+), 40 deletions(-) diff --git a/packages/ui/test/proposals/helpers/getDetailsRenderStructure.test.ts b/packages/ui/test/proposals/helpers/getDetailsRenderStructure.test.ts index f02b28b5d4..29f76b3109 100644 --- a/packages/ui/test/proposals/helpers/getDetailsRenderStructure.test.ts +++ b/packages/ui/test/proposals/helpers/getDetailsRenderStructure.test.ts @@ -60,49 +60,27 @@ describe('getDetailsRenderStructure()', () => { expect(structure).toEqual({ structure: [ { - label: 'amount', - value: new BN(20), - renderType: 'Amount', - }, - { - label: 'destination', - value: alice.address, - renderType: 'Address', - }, - { - label: '', - value: undefined, - renderType: 'Divider', - }, - { - label: 'amount', - value: new BN(30), - renderType: 'Amount', - }, - { - label: 'destination', - value: bob.address, - renderType: 'Address', - }, - { - label: '', - value: undefined, - renderType: 'Divider', - }, - { - label: 'amount', - value: new BN(10), + label: 'Total Payment', + value: new BN(60), renderType: 'Amount', }, { - label: 'destination', - value: aliceStash.address, - renderType: 'Address', - }, - { - label: '', - value: undefined, - renderType: 'Divider', + label: 'Payment Details', + value: [ + { + account: alice.address, + amount: new BN(20), + }, + { + account: bob.address, + amount: new BN(30), + }, + { + account: aliceStash.address, + amount: new BN(10), + }, + ], + renderType: 'DestinationsPreview', }, ], }) From 2da82e890380d949384be395c2666ddca936b51d Mon Sep 17 00:00:00 2001 From: Victor Date: Fri, 1 Sep 2023 02:15:14 +0100 Subject: [PATCH 24/27] requested changes --- .../proposals/components/ProposalDetails/ProposalDetails.tsx | 4 ++-- .../ui/src/proposals/helpers/getDetailsRenderStructure.ts | 5 ----- .../test/proposals/helpers/getDetailsRenderStructure.test.ts | 5 ----- 3 files changed, 2 insertions(+), 12 deletions(-) diff --git a/packages/ui/src/proposals/components/ProposalDetails/ProposalDetails.tsx b/packages/ui/src/proposals/components/ProposalDetails/ProposalDetails.tsx index ac5eeb6517..8bf3eee227 100644 --- a/packages/ui/src/proposals/components/ProposalDetails/ProposalDetails.tsx +++ b/packages/ui/src/proposals/components/ProposalDetails/ProposalDetails.tsx @@ -56,7 +56,7 @@ const renderTypeMapper: Partial> = { export const ProposalDetails = ({ proposalDetails }: Props) => { const { api } = useApi() const { budget } = useCouncilStatistics() - const { group } = useWorkingGroup({ name: (proposalDetails as UpdateGroupBudgetDetails)?.group?.id }) + const { group } = useWorkingGroup({ name: proposalDetails?.type === 'fundingRequest' ? 'contentWorkingGroup':(proposalDetails as UpdateGroupBudgetDetails)?.group?.id }) const membershipPrice = useFirstObservableValue(() => api?.query.members.membershipPrice(), [api?.isConnected]) const renderProposalDetail = useCallback((detail: RenderNode, index: number) => { const Component = renderTypeMapper[detail.renderType] @@ -84,7 +84,7 @@ export const ProposalDetails = ({ proposalDetails }: Props) => { return [ { renderType: 'Amount', - label: 'Current WG Budget', + label: 'Current Content WG Budget', value: group?.budget, }, ] as RenderNode[] diff --git a/packages/ui/src/proposals/helpers/getDetailsRenderStructure.ts b/packages/ui/src/proposals/helpers/getDetailsRenderStructure.ts index 6c70038e80..3a148f79ae 100644 --- a/packages/ui/src/proposals/helpers/getDetailsRenderStructure.ts +++ b/packages/ui/src/proposals/helpers/getDetailsRenderStructure.ts @@ -69,11 +69,6 @@ const destinationsMapper: Mapper = (value): value: destination.account, renderType: 'Address', }) - result.push({ - label: '', - value: undefined, - renderType: 'Divider', - }) }) } if (value.length > 1) { diff --git a/packages/ui/test/proposals/helpers/getDetailsRenderStructure.test.ts b/packages/ui/test/proposals/helpers/getDetailsRenderStructure.test.ts index 29f76b3109..792c242755 100644 --- a/packages/ui/test/proposals/helpers/getDetailsRenderStructure.test.ts +++ b/packages/ui/test/proposals/helpers/getDetailsRenderStructure.test.ts @@ -28,11 +28,6 @@ describe('getDetailsRenderStructure()', () => { value: alice.address, renderType: 'Address', }, - { - label: '', - value: undefined, - renderType: 'Divider', - }, ], }) }) From ba0c7ab0378d4da9dbd31cb0ec4033d1adc64224 Mon Sep 17 00:00:00 2001 From: Victor Date: Fri, 1 Sep 2023 02:25:54 +0100 Subject: [PATCH 25/27] requested changes --- .../components/ProposalDetails/ProposalDetails.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/ui/src/proposals/components/ProposalDetails/ProposalDetails.tsx b/packages/ui/src/proposals/components/ProposalDetails/ProposalDetails.tsx index 8bf3eee227..82c3b05026 100644 --- a/packages/ui/src/proposals/components/ProposalDetails/ProposalDetails.tsx +++ b/packages/ui/src/proposals/components/ProposalDetails/ProposalDetails.tsx @@ -56,7 +56,12 @@ const renderTypeMapper: Partial> = { export const ProposalDetails = ({ proposalDetails }: Props) => { const { api } = useApi() const { budget } = useCouncilStatistics() - const { group } = useWorkingGroup({ name: proposalDetails?.type === 'fundingRequest' ? 'contentWorkingGroup':(proposalDetails as UpdateGroupBudgetDetails)?.group?.id }) + const { group } = useWorkingGroup({ + name: + proposalDetails?.type === 'fundingRequest' + ? 'contentWorkingGroup' + : (proposalDetails as UpdateGroupBudgetDetails)?.group?.id, + }) const membershipPrice = useFirstObservableValue(() => api?.query.members.membershipPrice(), [api?.isConnected]) const renderProposalDetail = useCallback((detail: RenderNode, index: number) => { const Component = renderTypeMapper[detail.renderType] From 6b04de31c7f5b09e824258dc56944230eb129da8 Mon Sep 17 00:00:00 2001 From: Victor Date: Fri, 1 Sep 2023 09:34:20 +0100 Subject: [PATCH 26/27] council budget added to funding proposal --- .../components/ProposalDetails/ProposalDetails.tsx | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/packages/ui/src/proposals/components/ProposalDetails/ProposalDetails.tsx b/packages/ui/src/proposals/components/ProposalDetails/ProposalDetails.tsx index 82c3b05026..8e588192d9 100644 --- a/packages/ui/src/proposals/components/ProposalDetails/ProposalDetails.tsx +++ b/packages/ui/src/proposals/components/ProposalDetails/ProposalDetails.tsx @@ -57,10 +57,7 @@ export const ProposalDetails = ({ proposalDetails }: Props) => { const { api } = useApi() const { budget } = useCouncilStatistics() const { group } = useWorkingGroup({ - name: - proposalDetails?.type === 'fundingRequest' - ? 'contentWorkingGroup' - : (proposalDetails as UpdateGroupBudgetDetails)?.group?.id, + name: (proposalDetails as UpdateGroupBudgetDetails)?.group?.id, }) const membershipPrice = useFirstObservableValue(() => api?.query.members.membershipPrice(), [api?.isConnected]) const renderProposalDetail = useCallback((detail: RenderNode, index: number) => { @@ -89,8 +86,8 @@ export const ProposalDetails = ({ proposalDetails }: Props) => { return [ { renderType: 'Amount', - label: 'Current Content WG Budget', - value: group?.budget, + label: 'Current Council Budget', + value: budget.amount, }, ] as RenderNode[] } From 9c64ed7dccdaa21630b5d60271608bd982f35234 Mon Sep 17 00:00:00 2001 From: Victor Date: Mon, 4 Sep 2023 18:30:33 +0100 Subject: [PATCH 27/27] council budget added to funding proposal --- .../proposals/components/ProposalDetails/ProposalDetails.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ui/src/proposals/components/ProposalDetails/ProposalDetails.tsx b/packages/ui/src/proposals/components/ProposalDetails/ProposalDetails.tsx index 8e588192d9..fbd320866c 100644 --- a/packages/ui/src/proposals/components/ProposalDetails/ProposalDetails.tsx +++ b/packages/ui/src/proposals/components/ProposalDetails/ProposalDetails.tsx @@ -113,7 +113,7 @@ export const ProposalDetails = ({ proposalDetails }: Props) => { } return [] - }, [membershipPrice, !group]) + }, [membershipPrice, !group, budget]) const extraInformation = useMemo(() => { if (proposalDetails?.type === 'updateWorkingGroupBudget') {