Skip to content

Commit

Permalink
Cancel proposal feature (Joystream#4750)
Browse files Browse the repository at this point in the history
* cancel proposal feature

* cancel proposal story

* Update packages/ui/src/app/pages/Proposals/ProposalPreview.tsx

Co-authored-by: Theophile Sandoz <[email protected]>

---------

Co-authored-by: Theophile Sandoz <[email protected]>
  • Loading branch information
vrrayz and thesan authored Jan 30, 2024
1 parent a102234 commit 3c166ec
Show file tree
Hide file tree
Showing 7 changed files with 149 additions and 0 deletions.
3 changes: 3 additions & 0 deletions packages/ui/src/app/GlobalModals.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ import { SwitchMemberModal, SwitchMemberModalCall } from '@/memberships/modals/S
import { TransferInviteModal, TransferInvitesModalCall } from '@/memberships/modals/TransferInviteModal'
import { UpdateMembershipModal, UpdateMembershipModalCall } from '@/memberships/modals/UpdateMembershipModal'
import { AddNewProposalModal, AddNewProposalModalCall } from '@/proposals/modals/AddNewProposal'
import { CancelProposalModal, CancelProposalModalCall } from '@/proposals/modals/CancelProposal'
import { VoteForProposalModal, VoteForProposalModalCall } from '@/proposals/modals/VoteForProposal'
import { VoteRationaleModalCall } from '@/proposals/modals/VoteRationale/types'
import { VoteRationale } from '@/proposals/modals/VoteRationale/VoteRationale'
Expand Down Expand Up @@ -133,6 +134,7 @@ export type ModalNames =
| ModalName<EmailSubscriptionModalCall>
| ModalName<EmailConfirmationModalCall>
| ModalName<NominatingRedirectModalCall>
| ModalName<CancelProposalModalCall>

const modals: Record<ModalNames, ReactElement> = {
Member: <MemberProfile />,
Expand Down Expand Up @@ -186,6 +188,7 @@ const modals: Record<ModalNames, ReactElement> = {
EmailSubscriptionModal: <EmailSubscriptionModal />,
EmailConfirmationModal: <EmailConfirmationModal />,
NominatingRedirect: <NominatingRedirectModal />,
CancelProposalModal: <CancelProposalModal />,
}

const GUEST_ACCESSIBLE_MODALS: ModalNames[] = [
Expand Down
37 changes: 37 additions & 0 deletions packages/ui/src/app/pages/Proposals/ProposalPreview.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ type Args = {
vote2: VoteArg
vote3: VoteArg
onVote: jest.Mock
onCancel: jest.Mock
}
type Story = StoryObj<FC<Args>>

Expand All @@ -65,6 +66,7 @@ export default {
vote2: { control: { type: 'inline-radio' }, options: voteArgs },
vote3: { control: { type: 'inline-radio' }, options: voteArgs },
onVote: { action: 'ProposalsEngine.Voted' },
onCancel: { action: 'ProposalsEngine.Cancelled' },
},

args: {
Expand Down Expand Up @@ -142,6 +144,11 @@ export default {
onSend: args.onVote,
failure: parameters.txFailure,
},
cancelProposal: {
event: 'Cancelled',
onSend: args.onCancel,
failure: parameters.txFailure,
},
},
},
},
Expand Down Expand Up @@ -608,3 +615,33 @@ export const TestVoteTxFailure: Story = {
expect(await modal.findByText('Some error message'))
},
}

export const TestCancelProposalHappy: Story = {
args: { type: 'SignalProposalDetails', isCouncilMember: false, isProposer: true },

name: 'Test CancelProposal Happy',

play: async ({ canvasElement, step, args: { onCancel } }) => {
const activeMember = member('alice')

const screen = within(canvasElement)
const modal = withinModal(canvasElement)

await step('Cancel', async () => {
await userEvent.click(screen.getByText('Cancel Proposal'))

await step('Sign', async () => {
expect(await modal.findByText('Authorize transaction'))
expect(modal.getByText('You intend to cancel your proposal.'))

await userEvent.click(modal.getByText(/^Sign And Cancel Proposal/))
})

await step('Confirm', async () => {
expect(await modal.findByText('Your propsal has been cancelled.'))

expect(onCancel).toHaveBeenLastCalledWith(activeMember.id, PROPOSAL_DATA.id)
})
})
},
}
6 changes: 6 additions & 0 deletions packages/ui/src/app/pages/Proposals/ProposalPreview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { getUrl } from '@/common/utils/getUrl'
import { useElectedCouncil } from '@/council/hooks/useElectedCouncil'
import { MemberInfo } from '@/memberships/components'
import { useMyMemberships } from '@/memberships/hooks/useMyMemberships'
import { CancelProposalButton } from '@/proposals/components/CancelProposalButton'
import { ProposalDetails } from '@/proposals/components/ProposalDetails/ProposalDetails'
import { ProposalDiscussions } from '@/proposals/components/ProposalDiscussions'
import { ProposalHistory } from '@/proposals/components/ProposalHistory'
Expand Down Expand Up @@ -117,6 +118,11 @@ export const ProposalPreview = () => {
<PageTitle>{proposal.title}</PageTitle>
</PreviousPage>
<ButtonsGroup>
{active?.id === proposal.proposer.id &&
proposal.votes.length === 0 &&
(proposal.status === 'deciding' || proposal.status === 'dormant') && (
<CancelProposalButton member={active} proposalId={proposal.id} />
)}
{active?.isCouncilMember &&
proposal.status === 'deciding' &&
(!hasVoted ? (
Expand Down
27 changes: 27 additions & 0 deletions packages/ui/src/proposals/components/CancelProposalButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import React, { useCallback } from 'react'

import { ButtonSecondary } from '@/common/components/buttons'
import { useModal } from '@/common/hooks/useModal'
import { Member } from '@/memberships/types'

import { CancelProposalModalCall } from '../modals/CancelProposal'

interface Props {
member: Member
proposalId: string
}

export const CancelProposalButton = ({ member, proposalId }: Props) => {
const { showModal } = useModal()
const cancelProposalModal = useCallback(() => {
showModal<CancelProposalModalCall>({
modal: 'CancelProposalModal',
data: { member, proposalId },
})
}, [])
return (
<ButtonSecondary onClick={cancelProposalModal} size="medium">
Cancel Proposal
</ButtonSecondary>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import React, { useEffect, useMemo } from 'react'

import { useTransactionFee } from '@/accounts/hooks/useTransactionFee'
import { InsufficientFundsModal } from '@/accounts/modals/InsufficientFundsModal'
import { useApi } from '@/api/hooks/useApi'
import { TextMedium } from '@/common/components/typography'
import { useMachine } from '@/common/hooks/useMachine'
import { useModal } from '@/common/hooks/useModal'
import { SignTransactionModal } from '@/common/modals/SignTransactionModal/SignTransactionModal'
import { defaultTransactionModalMachine } from '@/common/model/machines/defaultTransactionModalMachine'

import { CancelProposalModalCall } from './types'

export const CancelProposalModal = () => {
const { hideModal, modalData } = useModal<CancelProposalModalCall>()
const { member, proposalId } = modalData
const machine = useMemo(
() =>
defaultTransactionModalMachine(
'There was a problem cancelling your proposal.',
'Your propsal has been cancelled.'
),
[]
)
const [state, send] = useMachine(machine, { context: { validateBeforeTransaction: true } })
const { api, isConnected } = useApi()

const { transaction, feeInfo } = useTransactionFee(
member.controllerAccount,
() => {
if (api && isConnected) {
return api.tx.proposalsEngine.cancelProposal(member.id, proposalId)
}
},
[modalData, isConnected]
)

useEffect(() => {
if (state.matches('requirementsVerification')) {
if (transaction && feeInfo) {
feeInfo.canAfford && send('PASS')
!feeInfo.canAfford && send('FAIL')
}
}

if (state.matches('beforeTransaction')) {
send(feeInfo?.canAfford ? 'PASS' : 'FAIL')
}
}, [state.value, member, transaction, feeInfo?.canAfford])

if (state.matches('transaction') && transaction && member) {
return (
<SignTransactionModal
buttonText="Sign And Cancel Proposal"
transaction={transaction}
signer={member.controllerAccount}
service={state.children.transaction}
>
<TextMedium>You intend to cancel your proposal.</TextMedium>
</SignTransactionModal>
)
}

if (state.matches('requirementsFailed') && member && feeInfo) {
return (
<InsufficientFundsModal onClose={hideModal} address={member.controllerAccount} amount={feeInfo.transactionFee} />
)
}
return null
}
2 changes: 2 additions & 0 deletions packages/ui/src/proposals/modals/CancelProposal/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export type { CancelProposalModalCall } from './types'
export * from './CancelProposalModal'
4 changes: 4 additions & 0 deletions packages/ui/src/proposals/modals/CancelProposal/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { ModalWithDataCall } from '@/common/providers/modal/types'
import { Member } from '@/memberships/types'

export type CancelProposalModalCall = ModalWithDataCall<'CancelProposalModal', { member: Member; proposalId: string }>

0 comments on commit 3c166ec

Please sign in to comment.