Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Funding Request Proposal for Multiple Recipients #2365 #4453

Merged
merged 26 commits into from
Aug 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
254936f
preview multiple receipients modal
vrrayz Jun 20, 2023
dead262
multiple funding request validation
vrrayz Jun 20, 2023
4d0bfa0
csv reg exp pattern
vrrayz Jun 20, 2023
30651dc
Preview And Validate Modal validation
vrrayz Jun 21, 2023
e7517cc
lint fix
vrrayz Jun 21, 2023
e7d4b82
Tx fail fix
vrrayz Jun 21, 2023
51ddf50
suggestions
vrrayz Jul 24, 2023
dc0fb3d
Merge branch 'dev' of https://github.com/vrrayz/pioneer into issue-2365
vrrayz Jul 24, 2023
383089d
lint fix
vrrayz Jul 24, 2023
d06d0ca
side drawer
vrrayz Jul 25, 2023
1abf1d0
csv pattern change and cleanup
vrrayz Jul 25, 2023
283dd51
Multiple funding request test
vrrayz Jul 28, 2023
91cb882
lint
vrrayz Jul 28, 2023
f4477df
Merge branch 'Joystream:dev' into issue-2365
vrrayz Jul 28, 2023
e018470
Merge branch 'issue-2365' of https://github.com/vrrayz/pioneer into i…
vrrayz Jul 28, 2023
855aa76
Update packages/ui/src/app/pages/Proposals/CurrentProposals.stories.tsx
vrrayz Aug 6, 2023
44dcd9a
requested changes
vrrayz Aug 7, 2023
8fdbc0b
Merge branch 'Joystream:dev' into issue-2365
vrrayz Aug 7, 2023
172518f
lint
vrrayz Aug 7, 2023
617ab12
disable preview button
vrrayz Aug 11, 2023
18b3cf5
Merge branch 'dev' of https://github.com/vrrayz/pioneer into issue-2365
vrrayz Aug 11, 2023
904d370
Merge branch 'Joystream:dev' into issue-2365
vrrayz Aug 11, 2023
59f10f2
updated storybook test for multiple funding request
vrrayz Aug 11, 2023
9a3d8bb
Merge branch 'issue-2365' of https://github.com/vrrayz/pioneer into i…
vrrayz Aug 11, 2023
313eb98
requested changes
vrrayz Aug 11, 2023
f58a6fb
requested changes
vrrayz Aug 11, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions packages/ui/.storybook/preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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' })

Expand Down Expand Up @@ -64,11 +67,23 @@ const ModalDecorator: Decorator = (Story) => (
</TransactionStatusProvider>
)

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 <KeyringContext.Provider value={keyring}><Story /></KeyringContext.Provider>
}

export const decorators = [
ModalDecorator,
stylesWrapperDecorator,
i18nextDecorator,
RHFDecorator,
KeyringDecorator,
MockProvidersDecorator,
MockRouterDecorator,
]
Expand Down
89 changes: 89 additions & 0 deletions packages/ui/src/app/pages/Proposals/CurrentProposals.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -781,6 +781,95 @@ 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()

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(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')
expect(previewButton).toBeDisabled()
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()
thesan marked this conversation as resolved.
Show resolved Hide resolved
expect(previewButton).toBeEnabled()

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 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
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 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())
await userEvent.click(modal.getByTestId('sidePanel-overlay'))

// Valid
await userEvent.clear(csvField)
await userEvent.type(csvField, `${alice.controllerAccount},500\n${bob.controllerAccount},500`)
thesan marked this conversation as resolved.
Show resolved Hide resolved
expect(nextButton).toBeDisabled()

await waitFor(() => expect(previewButton).toBeEnabled())
await userEvent.click(previewButton)
await userEvent.click(modal.getByTestId('sidePanel-overlay'))
})

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 () => {
Expand Down
1 change: 1 addition & 0 deletions packages/ui/src/mocks/data/proposals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,7 @@ export const proposalsPagesChain = (

proposalsCodex: {
fundingRequestProposalMaxTotalAmount: joy(166_666),
fundingRequestProposalMaxAccounts: 2,
setMaxValidatorCountProposalMaxValidators,

...Object.fromEntries(
Expand Down
1 change: 1 addition & 0 deletions packages/ui/src/proposals/constants/regExp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const CSV_PATTERN = /^([^,:;]+),([^,:;]+)(\n[^,:;]+,[^,:;]+)*(\n)?$/
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ export const AddNewProposalModal = () => {
const schema = useMemo(() => schemaFactory(api), [!api])

const path = useMemo(() => machineStateConverter(state.value) as keyof AddNewProposalForm, [state.value])

const form = useForm<AddNewProposalForm>({
resolver: useYupValidationResolver<AddNewProposalForm>(schema, path),
mode: 'onChange',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,41 @@
import React from 'react'
import React, { useEffect, 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, getFieldState } = useFormContext()
const [isPreviewModalShown, setIsPreviewModalShown] = useState(false)
const [payMultiple] = watch(['fundingRequest.payMultiple'])
const [hasPreviewedInput] = watch(['fundingRequest.hasPreviewedInput'], { 'fundingRequest.hasPreviewedInput': true })
const csvInput = watch('fundingRequest.csvInput')
useEffect(() => {
if (getFieldState('fundingRequest.accountsAndAmounts')) {
setValue('fundingRequest.accountsAndAmounts', undefined, { shouldValidate: true })
setValue('fundingRequest.hasPreviewedInput', false, { shouldValidate: true })
}
}, [csvInput])
const canPreviewInput =
getFieldState('fundingRequest.csvInput').isDirty && !getFieldState('fundingRequest.csvInput').error
return (
<RowGapBlock gap={24}>
<Row>
Expand All @@ -17,22 +45,91 @@ export const FundingRequest = () => {
</RowGapBlock>
</Row>
<Row>
<RowGapBlock gap={20}>
<InputComponent
label="Amount"
tight
units={CurrencyName.integerValue}
required
message="Amount must be greater than zero"
name="fundingRequest.amount"
>
<TokenInput id="amount-input" placeholder="0" name="fundingRequest.amount" />
</InputComponent>
<InputComponent label="Recipient account" required inputSize="l">
<SelectAccount name="fundingRequest.account" />
</InputComponent>
<RowGapBlock gap={payMultiple ? 6 : 24}>
<Row>
<InlineToggleWrap>
<Label>Pay multiple</Label>
<Tooltip
tooltipTitle="Pay multiple"
tooltipText="For multiple accounts and amounts, follow this CSV pattern:<br/>
account1, amount1<br/>
account2, amount2<br/>
...<br/>
account20, amount20"
>
<TooltipDefault />
</Tooltip>
</InlineToggleWrap>
<ToggleCheckbox falseLabel="No" trueLabel="Yes" name="fundingRequest.payMultiple" id="pay-multiple" />
</Row>
{payMultiple && (
<Row>
<Prompt>
<TextSmall>
For <TextInlineSmall bold>multiple accounts and amounts</TextInlineSmall>, follow this CSV pattern:
<br />
account1, amount1
<br />
account2, amount2
<br />
...
<br />
account20, amount20
</TextSmall>
</Prompt>
</Row>
)}
</RowGapBlock>
</Row>
<Row>
{payMultiple ? (
<RowGapBlock gap={12}>
<InputComponent
label="Destination accounts and transfer amounts"
required
message={'You can select up to 20 recipients'}
name="fundingRequest.csvInput"
id="accounts-amounts"
inputSize="xl"
>
<InputTextarea
id="accounts-amounts"
name="fundingRequest.csvInput"
placeholder="Destination account address and amount"
/>
</InputComponent>
<HiddenCheckBox name="fundingRequest.hasPreviewedInput" checked={hasPreviewedInput} />
{canPreviewInput && !hasPreviewedInput && (
<ErrorPrompt>Please preview and validate the inputs to proceed</ErrorPrompt>
)}
<ButtonPrimary size="medium" disabled={!canPreviewInput} onClick={() => setIsPreviewModalShown(true)}>
Preview and Validate <Arrow direction="right" />
</ButtonPrimary>
</RowGapBlock>
) : (
<RowGapBlock gap={20}>
<InputComponent
label="Amount"
tight
units={CurrencyName.integerValue}
required
message="Amount must be greater than zero"
name="fundingRequest.amount"
>
<TokenInput id="amount-input" placeholder="0" name="fundingRequest.amount" />
</InputComponent>
<InputComponent label="Recipient account" required inputSize="l">
<SelectAccount name="fundingRequest.account" />
</InputComponent>
</RowGapBlock>
)}
</Row>
{isPreviewModalShown && <PreviewAndValidateModal onClose={setIsPreviewModalShown} />}
</RowGapBlock>
)
}
const HiddenCheckBox = styled.input.attrs({ type: 'checkbox' })`
margin-top: -12px;
height: 0px;
visibility: hidden;
`
Original file line number Diff line number Diff line change
@@ -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 (
<PromptContainer>
<IconSection>
<PromptQuestion>
<QuestionIcon />
</PromptQuestion>
</IconSection>
{children}
</PromptContainer>
)
}
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;
`
Loading