From 3c166ec350530b58df15c8bc6fbeadc600734074 Mon Sep 17 00:00:00 2001 From: Victor Emmanuel <33874323+vrrayz@users.noreply.github.com> Date: Tue, 30 Jan 2024 16:54:49 +0100 Subject: [PATCH] Cancel proposal feature (#4750) * cancel proposal feature * cancel proposal story * Update packages/ui/src/app/pages/Proposals/ProposalPreview.tsx Co-authored-by: Theophile Sandoz --------- Co-authored-by: Theophile Sandoz --- packages/ui/src/app/GlobalModals.tsx | 3 + .../Proposals/ProposalPreview.stories.tsx | 37 ++++++++++ .../app/pages/Proposals/ProposalPreview.tsx | 6 ++ .../components/CancelProposalButton.tsx | 27 +++++++ .../CancelProposal/CancelProposalModal.tsx | 70 +++++++++++++++++++ .../proposals/modals/CancelProposal/index.ts | 2 + .../proposals/modals/CancelProposal/types.ts | 4 ++ 7 files changed, 149 insertions(+) create mode 100644 packages/ui/src/proposals/components/CancelProposalButton.tsx create mode 100644 packages/ui/src/proposals/modals/CancelProposal/CancelProposalModal.tsx create mode 100644 packages/ui/src/proposals/modals/CancelProposal/index.ts create mode 100644 packages/ui/src/proposals/modals/CancelProposal/types.ts diff --git a/packages/ui/src/app/GlobalModals.tsx b/packages/ui/src/app/GlobalModals.tsx index 8b86bbf08d..f3bfdc6a4d 100644 --- a/packages/ui/src/app/GlobalModals.tsx +++ b/packages/ui/src/app/GlobalModals.tsx @@ -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' @@ -133,6 +134,7 @@ export type ModalNames = | ModalName | ModalName | ModalName + | ModalName const modals: Record = { Member: , @@ -186,6 +188,7 @@ const modals: Record = { EmailSubscriptionModal: , EmailConfirmationModal: , NominatingRedirect: , + CancelProposalModal: , } const GUEST_ACCESSIBLE_MODALS: ModalNames[] = [ diff --git a/packages/ui/src/app/pages/Proposals/ProposalPreview.stories.tsx b/packages/ui/src/app/pages/Proposals/ProposalPreview.stories.tsx index e8244641a6..0ba815ce41 100644 --- a/packages/ui/src/app/pages/Proposals/ProposalPreview.stories.tsx +++ b/packages/ui/src/app/pages/Proposals/ProposalPreview.stories.tsx @@ -51,6 +51,7 @@ type Args = { vote2: VoteArg vote3: VoteArg onVote: jest.Mock + onCancel: jest.Mock } type Story = StoryObj> @@ -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: { @@ -142,6 +144,11 @@ export default { onSend: args.onVote, failure: parameters.txFailure, }, + cancelProposal: { + event: 'Cancelled', + onSend: args.onCancel, + failure: parameters.txFailure, + }, }, }, }, @@ -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) + }) + }) + }, +} diff --git a/packages/ui/src/app/pages/Proposals/ProposalPreview.tsx b/packages/ui/src/app/pages/Proposals/ProposalPreview.tsx index d2714fe570..3f084c52cc 100644 --- a/packages/ui/src/app/pages/Proposals/ProposalPreview.tsx +++ b/packages/ui/src/app/pages/Proposals/ProposalPreview.tsx @@ -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' @@ -117,6 +118,11 @@ export const ProposalPreview = () => { {proposal.title} + {active?.id === proposal.proposer.id && + proposal.votes.length === 0 && + (proposal.status === 'deciding' || proposal.status === 'dormant') && ( + + )} {active?.isCouncilMember && proposal.status === 'deciding' && (!hasVoted ? ( diff --git a/packages/ui/src/proposals/components/CancelProposalButton.tsx b/packages/ui/src/proposals/components/CancelProposalButton.tsx new file mode 100644 index 0000000000..6f38d32a13 --- /dev/null +++ b/packages/ui/src/proposals/components/CancelProposalButton.tsx @@ -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({ + modal: 'CancelProposalModal', + data: { member, proposalId }, + }) + }, []) + return ( + + Cancel Proposal + + ) +} diff --git a/packages/ui/src/proposals/modals/CancelProposal/CancelProposalModal.tsx b/packages/ui/src/proposals/modals/CancelProposal/CancelProposalModal.tsx new file mode 100644 index 0000000000..17c0d682ce --- /dev/null +++ b/packages/ui/src/proposals/modals/CancelProposal/CancelProposalModal.tsx @@ -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() + 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 ( + + You intend to cancel your proposal. + + ) + } + + if (state.matches('requirementsFailed') && member && feeInfo) { + return ( + + ) + } + return null +} diff --git a/packages/ui/src/proposals/modals/CancelProposal/index.ts b/packages/ui/src/proposals/modals/CancelProposal/index.ts new file mode 100644 index 0000000000..3ae4dd6f55 --- /dev/null +++ b/packages/ui/src/proposals/modals/CancelProposal/index.ts @@ -0,0 +1,2 @@ +export type { CancelProposalModalCall } from './types' +export * from './CancelProposalModal' diff --git a/packages/ui/src/proposals/modals/CancelProposal/types.ts b/packages/ui/src/proposals/modals/CancelProposal/types.ts new file mode 100644 index 0000000000..f506ea0e0c --- /dev/null +++ b/packages/ui/src/proposals/modals/CancelProposal/types.ts @@ -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 }>