Cancel / Refund proposal (#100)
* Cancel / refund modal for proposals. Fix some states where frozen contract still allowed interaction. * Add more refund states. Move styles to less. * Fix tsc, simplify logic
This commit is contained in:
parent
8be518fff7
commit
75f0b72022
|
@ -25,6 +25,7 @@ class CreateFlowPreview extends React.Component<Props> {
|
||||||
banner
|
banner
|
||||||
/>
|
/>
|
||||||
<ProposalDetail
|
<ProposalDetail
|
||||||
|
account="0x0"
|
||||||
proposalId="preview"
|
proposalId="preview"
|
||||||
fetchProposal={() => null}
|
fetchProposal={() => null}
|
||||||
proposal={proposal}
|
proposal={proposal}
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
text-align: center;
|
||||||
border: 2px dashed #d9d9d9;
|
border: 2px dashed #d9d9d9;
|
||||||
padding: 3rem;
|
padding: 3rem;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
|
|
|
@ -89,7 +89,9 @@ export class ProposalCampaignBlock extends React.Component<Props, State> {
|
||||||
if (proposal) {
|
if (proposal) {
|
||||||
const { crowdFund } = proposal;
|
const { crowdFund } = proposal;
|
||||||
const isFundingOver =
|
const isFundingOver =
|
||||||
crowdFund.isRaiseGoalReached || crowdFund.deadline < Date.now();
|
crowdFund.isRaiseGoalReached ||
|
||||||
|
crowdFund.deadline < Date.now() ||
|
||||||
|
crowdFund.isFrozen;
|
||||||
const isDisabled = isFundingOver || !!amountError || !amountFloat || isPreview;
|
const isDisabled = isFundingOver || !!amountError || !amountFloat || isPreview;
|
||||||
const remainingEthNum = parseFloat(
|
const remainingEthNum = parseFloat(
|
||||||
web3.utils.fromWei(crowdFund.target.sub(crowdFund.funded), 'ether'),
|
web3.utils.fromWei(crowdFund.target.sub(crowdFund.funded), 'ether'),
|
||||||
|
@ -150,7 +152,7 @@ export class ProposalCampaignBlock extends React.Component<Props, State> {
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<Icon type="close-circle-o" />
|
<Icon type="close-circle-o" />
|
||||||
<span>Proposal didn’t reach target</span>
|
<span>Proposal didn’t get funded</span>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -0,0 +1,105 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { Modal, Alert } from 'antd';
|
||||||
|
import { ProposalWithCrowdFund } from 'modules/proposals/reducers';
|
||||||
|
import { web3Actions } from 'modules/web3';
|
||||||
|
import { AppState } from 'store/reducers';
|
||||||
|
|
||||||
|
interface OwnProps {
|
||||||
|
proposal: ProposalWithCrowdFund;
|
||||||
|
isVisible: boolean;
|
||||||
|
handleClose(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface StateProps {
|
||||||
|
isRefundActionPending: AppState['web3']['isRefundActionPending'];
|
||||||
|
refundActionError: AppState['web3']['refundActionError'];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DispatchProps {
|
||||||
|
triggerRefund: typeof web3Actions['triggerRefund'];
|
||||||
|
}
|
||||||
|
|
||||||
|
type Props = StateProps & DispatchProps & OwnProps;
|
||||||
|
|
||||||
|
class CancelModal extends React.Component<Props> {
|
||||||
|
componentDidUpdate() {
|
||||||
|
if (this.props.proposal.crowdFund.isFrozen) {
|
||||||
|
this.props.handleClose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { proposal, isVisible, isRefundActionPending, refundActionError } = this.props;
|
||||||
|
const hasBeenFunded = proposal.crowdFund.isRaiseGoalReached;
|
||||||
|
const hasContributors = !!proposal.crowdFund.contributors.length;
|
||||||
|
const disabled = isRefundActionPending;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
title={<>Cancel proposal</>}
|
||||||
|
visible={isVisible}
|
||||||
|
okText="Confirm"
|
||||||
|
cancelText="Never mind"
|
||||||
|
onOk={this.cancelProposal}
|
||||||
|
onCancel={this.closeModal}
|
||||||
|
okButtonProps={{ type: 'danger', loading: disabled }}
|
||||||
|
cancelButtonProps={{ disabled }}
|
||||||
|
>
|
||||||
|
{hasBeenFunded ? (
|
||||||
|
<p>
|
||||||
|
Are you sure you would like to issue a refund?{' '}
|
||||||
|
<strong>This cannot be undone</strong>. Once you issue a refund, all
|
||||||
|
contributors will be able to receive a refund of the remaining proposal
|
||||||
|
balance.
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
|
<p>
|
||||||
|
Are you sure you would like to cancel this proposal?{' '}
|
||||||
|
<strong>This cannot be undone</strong>. Once you cancel it, all contributors
|
||||||
|
will be able to receive refunds.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
<p>
|
||||||
|
Canceled proposals cannot be deleted and will still be viewable by contributors
|
||||||
|
or anyone with a direct link. However, they will be de-listed everywhere else on
|
||||||
|
Grant.io.
|
||||||
|
</p>
|
||||||
|
{hasContributors && (
|
||||||
|
<p>
|
||||||
|
Should you choose to cancel, we highly recommend posting an update to let your
|
||||||
|
contributors know why you’ve decided to do so.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{refundActionError && (
|
||||||
|
<Alert
|
||||||
|
type="error"
|
||||||
|
message={`Failed to ${hasBeenFunded ? 'refund' : 'cancel'} proposal`}
|
||||||
|
description={refundActionError}
|
||||||
|
showIcon
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private closeModal = () => {
|
||||||
|
if (!this.props.isRefundActionPending) {
|
||||||
|
this.props.handleClose();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private cancelProposal = () => {
|
||||||
|
this.props.triggerRefund(this.props.proposal.crowdFundContract);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect<StateProps, DispatchProps, OwnProps, AppState>(
|
||||||
|
state => ({
|
||||||
|
isRefundActionPending: state.web3.isRefundActionPending,
|
||||||
|
refundActionError: state.web3.refundActionError,
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
triggerRefund: web3Actions.triggerRefund,
|
||||||
|
},
|
||||||
|
)(CancelModal);
|
|
@ -7,6 +7,7 @@ import { web3Actions } from 'modules/web3';
|
||||||
import { AppState } from 'store/reducers';
|
import { AppState } from 'store/reducers';
|
||||||
import Web3Container, { Web3RenderProps } from 'lib/Web3Container';
|
import Web3Container, { Web3RenderProps } from 'lib/Web3Container';
|
||||||
import UnitDisplay from 'components/UnitDisplay';
|
import UnitDisplay from 'components/UnitDisplay';
|
||||||
|
import Placeholder from 'components/Placeholder';
|
||||||
|
|
||||||
interface OwnProps {
|
interface OwnProps {
|
||||||
proposal: ProposalWithCrowdFund;
|
proposal: ProposalWithCrowdFund;
|
||||||
|
@ -38,6 +39,19 @@ export class Milestones extends React.Component<Props> {
|
||||||
milestoneActionError,
|
milestoneActionError,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const { crowdFund } = proposal;
|
const { crowdFund } = proposal;
|
||||||
|
|
||||||
|
if (!crowdFund.isRaiseGoalReached) {
|
||||||
|
return (
|
||||||
|
<Placeholder
|
||||||
|
title="Milestone governance isn’t available yet"
|
||||||
|
subtitle={`
|
||||||
|
Milestone history and voting status will be displayed here
|
||||||
|
once the project has been funded
|
||||||
|
`}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const contributor = crowdFund.contributors.find(c => c.address === accounts[0]);
|
const contributor = crowdFund.contributors.find(c => c.address === accounts[0]);
|
||||||
const isTrustee = crowdFund.trustees.includes(accounts[0]);
|
const isTrustee = crowdFund.trustees.includes(accounts[0]);
|
||||||
const firstMilestone = crowdFund.milestones[0];
|
const firstMilestone = crowdFund.milestones[0];
|
||||||
|
|
|
@ -6,6 +6,7 @@ import Web3Container, { Web3RenderProps } from 'lib/Web3Container';
|
||||||
import { web3Actions } from 'modules/web3';
|
import { web3Actions } from 'modules/web3';
|
||||||
import { AppState } from 'store/reducers';
|
import { AppState } from 'store/reducers';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
|
import Placeholder from 'components/Placeholder';
|
||||||
|
|
||||||
interface OwnProps {
|
interface OwnProps {
|
||||||
proposal: ProposalWithCrowdFund;
|
proposal: ProposalWithCrowdFund;
|
||||||
|
@ -32,16 +33,63 @@ class GovernanceRefunds extends React.Component<Props> {
|
||||||
render() {
|
render() {
|
||||||
const { proposal, account, isRefundActionPending, refundActionError } = this.props;
|
const { proposal, account, isRefundActionPending, refundActionError } = this.props;
|
||||||
const { crowdFund } = proposal;
|
const { crowdFund } = proposal;
|
||||||
|
const isStillFunding =
|
||||||
|
!crowdFund.isRaiseGoalReached && crowdFund.deadline > Date.now();
|
||||||
|
|
||||||
|
if (isStillFunding && !crowdFund.isFrozen) {
|
||||||
|
return (
|
||||||
|
<Placeholder
|
||||||
|
title="Refund governance isn’t available yet"
|
||||||
|
subtitle={`
|
||||||
|
Refund voting and status will be displayed here once the
|
||||||
|
project has been funded, or if it the project is canceled
|
||||||
|
or fails to receive funding
|
||||||
|
`}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Cyclomatic complexity is too damn high here. This state should be
|
||||||
|
// figured out on the backend and enumerated, not calculated in this component.
|
||||||
const contributor = crowdFund.contributors.find(c => c.address === account);
|
const contributor = crowdFund.contributors.find(c => c.address === account);
|
||||||
const isTrustee = crowdFund.trustees.includes(account);
|
const isTrustee = crowdFund.trustees.includes(account);
|
||||||
const hasVotedForRefund = contributor && contributor.refundVote;
|
const hasVotedForRefund = contributor && contributor.refundVote;
|
||||||
const hasRefunded = contributor && contributor.refunded;
|
const hasRefunded = contributor && contributor.refunded;
|
||||||
const refundPct = crowdFund.percentVotingForRefund;
|
const refundPct = crowdFund.percentVotingForRefund;
|
||||||
|
const didFundingFail =
|
||||||
|
!crowdFund.isRaiseGoalReached && crowdFund.deadline < Date.now();
|
||||||
|
|
||||||
let text;
|
let text;
|
||||||
let button;
|
let button;
|
||||||
if (!isTrustee && contributor) {
|
if (!isTrustee && contributor) {
|
||||||
if (refundPct < 50) {
|
let canRefund = false;
|
||||||
|
if (hasRefunded) {
|
||||||
|
return (
|
||||||
|
<Alert
|
||||||
|
type="success"
|
||||||
|
message="Your refund has been processed"
|
||||||
|
description={`
|
||||||
|
We apologize for any inconvenience this propsal has caused you. Please
|
||||||
|
let us know if there's anything we could have done to improve your
|
||||||
|
experience.
|
||||||
|
`}
|
||||||
|
showIcon
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else if (refundPct > 50) {
|
||||||
|
text = `
|
||||||
|
The majority of funders have voted for a refund. Click below
|
||||||
|
to receive your refund.
|
||||||
|
`;
|
||||||
|
canRefund = true;
|
||||||
|
} else if (didFundingFail || crowdFund.isFrozen) {
|
||||||
|
text = `
|
||||||
|
The project was either canceled, or failed to reach its funding
|
||||||
|
target before the deadline. Click below to receive a refund of
|
||||||
|
your contribution.
|
||||||
|
`;
|
||||||
|
canRefund = true;
|
||||||
|
} else {
|
||||||
text = `
|
text = `
|
||||||
As a funder of this project, you have the right to vote for a refund. If the
|
As a funder of this project, you have the right to vote for a refund. If the
|
||||||
amount of funds contributed by refund voters exceeds half of the project's
|
amount of funds contributed by refund voters exceeds half of the project's
|
||||||
|
@ -60,47 +108,38 @@ class GovernanceRefunds extends React.Component<Props> {
|
||||||
onClick: () => this.voteRefund(true),
|
onClick: () => this.voteRefund(true),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
if (hasRefunded) {
|
|
||||||
return (
|
if (canRefund) {
|
||||||
<Alert
|
text = (
|
||||||
type="success"
|
<>
|
||||||
message="Your refund has been processed"
|
{text}
|
||||||
description={`
|
{!crowdFund.isFrozen && (
|
||||||
We apologize for any inconvenience this propsal has caused you. Please
|
<Alert
|
||||||
let us know if there's anything we could have done to improve your
|
style={{ marginTop: '1rem' }}
|
||||||
experience.
|
type="info"
|
||||||
`}
|
showIcon
|
||||||
showIcon
|
message={`
|
||||||
/>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
text = (
|
|
||||||
<>
|
|
||||||
The majority of funders have voted for a refund. Click below to receive your
|
|
||||||
refund.
|
|
||||||
{!crowdFund.isFrozen && (
|
|
||||||
<Alert
|
|
||||||
style={{ marginTop: '1rem' }}
|
|
||||||
type="info"
|
|
||||||
message={`
|
|
||||||
This will require multiple transactions to process, sorry
|
This will require multiple transactions to process, sorry
|
||||||
for the inconvenience
|
for the inconvenience
|
||||||
`}
|
`}
|
||||||
showIcon
|
/>
|
||||||
/>
|
)}
|
||||||
)}
|
</>
|
||||||
</>
|
);
|
||||||
);
|
button = {
|
||||||
button = {
|
text: 'Get your refund',
|
||||||
text: 'Get your refund',
|
type: 'primary',
|
||||||
type: 'primary',
|
onClick: () => this.withdrawRefund(),
|
||||||
onClick: () => this.withdrawRefund(),
|
};
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (refundPct < 50) {
|
if (crowdFund.isFrozen || didFundingFail) {
|
||||||
|
text = `
|
||||||
|
The project failed to receive funding or was canceled. Contributors are
|
||||||
|
open to refund their contributions.
|
||||||
|
`;
|
||||||
|
} else if (refundPct < 50) {
|
||||||
text = `
|
text = `
|
||||||
Funders can vote to request refunds. If the amount of funds contributed by
|
Funders can vote to request refunds. If the amount of funds contributed by
|
||||||
refund voters exceeds half of the funds contributed, all funders will be able
|
refund voters exceeds half of the funds contributed, all funders will be able
|
||||||
|
|
|
@ -2,7 +2,6 @@ import React from 'react';
|
||||||
import GovernanceMilestones from './Milestones';
|
import GovernanceMilestones from './Milestones';
|
||||||
import GovernanceRefunds from './Refunds';
|
import GovernanceRefunds from './Refunds';
|
||||||
import { ProposalWithCrowdFund } from 'modules/proposals/reducers';
|
import { ProposalWithCrowdFund } from 'modules/proposals/reducers';
|
||||||
import Placeholder from 'components/Placeholder';
|
|
||||||
import './style.less';
|
import './style.less';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
@ -12,29 +11,15 @@ interface Props {
|
||||||
export default class ProposalGovernance extends React.Component<Props> {
|
export default class ProposalGovernance extends React.Component<Props> {
|
||||||
render() {
|
render() {
|
||||||
const { proposal } = this.props;
|
const { proposal } = this.props;
|
||||||
|
|
||||||
if (!proposal.crowdFund.isRaiseGoalReached) {
|
|
||||||
return (
|
|
||||||
<Placeholder
|
|
||||||
style={{ minHeight: '220px' }}
|
|
||||||
title="Governance isn’t available yet"
|
|
||||||
subtitle={`
|
|
||||||
Milestone history and voting will be displayed here once the
|
|
||||||
project has been funded
|
|
||||||
`}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="ProposalGovernance">
|
<div className="ProposalGovernance">
|
||||||
<div style={{ flex: 1 }}>
|
<div className="ProposalGovernance-section">
|
||||||
<h2 style={{ marginBottom: '1rem' }}>Milestone Voting</h2>
|
<h2 className="ProposalGovernance-section-title">Milestone Voting</h2>
|
||||||
<GovernanceMilestones proposal={proposal} />
|
<GovernanceMilestones proposal={proposal} />
|
||||||
</div>
|
</div>
|
||||||
<div className="ProposalGovernance-divider" />
|
<div className="ProposalGovernance-divider" />
|
||||||
<div style={{ flex: 1 }}>
|
<div className="ProposalGovernance-section">
|
||||||
<h2 style={{ marginBottom: '1rem' }}>Refunds</h2>
|
<h2 className="ProposalGovernance-section-title">Refunds</h2>
|
||||||
<GovernanceRefunds proposal={proposal} />
|
<GovernanceRefunds proposal={proposal} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -8,6 +8,15 @@
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&-section {
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
&-title {
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&-divider {
|
&-divider {
|
||||||
width: 1px;
|
width: 1px;
|
||||||
background: rgba(0, 0, 0, 0.05);
|
background: rgba(0, 0, 0, 0.05);
|
||||||
|
|
|
@ -7,16 +7,16 @@ import { bindActionCreators, Dispatch } from 'redux';
|
||||||
import { AppState } from 'store/reducers';
|
import { AppState } from 'store/reducers';
|
||||||
import { ProposalWithCrowdFund } from 'modules/proposals/reducers';
|
import { ProposalWithCrowdFund } from 'modules/proposals/reducers';
|
||||||
import { getProposal } from 'modules/proposals/selectors';
|
import { getProposal } from 'modules/proposals/selectors';
|
||||||
import { Spin, Tabs, Icon } from 'antd';
|
import { Spin, Tabs, Icon, Dropdown, Menu, Button } from 'antd';
|
||||||
import CampaignBlock from './CampaignBlock';
|
import CampaignBlock from './CampaignBlock';
|
||||||
import TeamBlock from './TeamBlock';
|
import TeamBlock from './TeamBlock';
|
||||||
import Milestones from './Milestones';
|
import Milestones from './Milestones';
|
||||||
|
|
||||||
import CommentsTab from './Comments';
|
import CommentsTab from './Comments';
|
||||||
import UpdatesTab from './Updates';
|
import UpdatesTab from './Updates';
|
||||||
import GovernanceTab from './Governance';
|
import GovernanceTab from './Governance';
|
||||||
import ContributorsTab from './Contributors';
|
import ContributorsTab from './Contributors';
|
||||||
// import CommunityTab from './Community';
|
// import CommunityTab from './Community';
|
||||||
|
import CancelModal from './CancelModal';
|
||||||
import './style.less';
|
import './style.less';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import { withRouter } from 'react-router';
|
import { withRouter } from 'react-router';
|
||||||
|
@ -36,11 +36,16 @@ interface DispatchProps {
|
||||||
fetchProposal: proposalActions.TFetchProposal;
|
fetchProposal: proposalActions.TFetchProposal;
|
||||||
}
|
}
|
||||||
|
|
||||||
type Props = StateProps & DispatchProps & OwnProps;
|
interface Web3Props {
|
||||||
|
account: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type Props = StateProps & DispatchProps & Web3Props & OwnProps;
|
||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
isBodyExpanded: boolean;
|
isBodyExpanded: boolean;
|
||||||
isBodyOverflowing: boolean;
|
isBodyOverflowing: boolean;
|
||||||
|
isCancelOpen: boolean;
|
||||||
bodyId: string;
|
bodyId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -48,6 +53,7 @@ export class ProposalDetail extends React.Component<Props, State> {
|
||||||
state: State = {
|
state: State = {
|
||||||
isBodyExpanded: false,
|
isBodyExpanded: false,
|
||||||
isBodyOverflowing: false,
|
isBodyOverflowing: false,
|
||||||
|
isCancelOpen: false,
|
||||||
bodyId: `body-${Math.floor(Math.random() * 1000000)}`,
|
bodyId: `body-${Math.floor(Math.random() * 1000000)}`,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -71,14 +77,37 @@ export class ProposalDetail extends React.Component<Props, State> {
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { proposal, isPreview } = this.props;
|
const { proposal, isPreview, account } = this.props;
|
||||||
const { isBodyExpanded, isBodyOverflowing, bodyId } = this.state;
|
const { isBodyExpanded, isBodyOverflowing, isCancelOpen, bodyId } = this.state;
|
||||||
const showExpand = !isBodyExpanded && isBodyOverflowing;
|
const showExpand = !isBodyExpanded && isBodyOverflowing;
|
||||||
|
|
||||||
if (!proposal) {
|
if (!proposal) {
|
||||||
return <Spin />;
|
return <Spin />;
|
||||||
} else {
|
} else {
|
||||||
const { crowdFund } = proposal;
|
const { crowdFund } = proposal;
|
||||||
|
const isTrustee = crowdFund.trustees.includes(account);
|
||||||
|
const hasBeenFunded = crowdFund.isRaiseGoalReached;
|
||||||
|
const isProposalActive = !hasBeenFunded && crowdFund.deadline > Date.now();
|
||||||
|
const canRefund = (hasBeenFunded || isProposalActive) && !crowdFund.isFrozen;
|
||||||
|
|
||||||
|
const adminMenu = isTrustee && (
|
||||||
|
<Menu>
|
||||||
|
<Menu.Item
|
||||||
|
onClick={() => alert('Sorry, not yet implemented!')}
|
||||||
|
disabled={!isProposalActive}
|
||||||
|
>
|
||||||
|
Edit proposal
|
||||||
|
</Menu.Item>
|
||||||
|
<Menu.Item
|
||||||
|
style={{ color: canRefund ? '#e74c3c' : undefined }}
|
||||||
|
onClick={this.openCancelModal}
|
||||||
|
disabled={!canRefund}
|
||||||
|
>
|
||||||
|
{hasBeenFunded ? 'Refund contributors' : 'Cancel proposal'}
|
||||||
|
</Menu.Item>
|
||||||
|
</Menu>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="Proposal">
|
<div className="Proposal">
|
||||||
<div className="Proposal-top">
|
<div className="Proposal-top">
|
||||||
|
@ -105,6 +134,20 @@ export class ProposalDetail extends React.Component<Props, State> {
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
{isTrustee && (
|
||||||
|
<div className="Proposal-top-main-menu">
|
||||||
|
<Dropdown
|
||||||
|
overlay={adminMenu}
|
||||||
|
trigger={['click']}
|
||||||
|
placement="bottomRight"
|
||||||
|
>
|
||||||
|
<Button>
|
||||||
|
<span>Actions</span>
|
||||||
|
<Icon type="down" style={{ marginRight: '-0.25rem' }} />
|
||||||
|
</Button>
|
||||||
|
</Dropdown>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="Proposal-top-side">
|
<div className="Proposal-top-side">
|
||||||
<CampaignBlock proposal={proposal} isPreview={isPreview} />
|
<CampaignBlock proposal={proposal} isPreview={isPreview} />
|
||||||
|
@ -134,6 +177,13 @@ export class ProposalDetail extends React.Component<Props, State> {
|
||||||
</Tabs.TabPane>
|
</Tabs.TabPane>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
)}
|
)}
|
||||||
|
{isTrustee && (
|
||||||
|
<CancelModal
|
||||||
|
proposal={proposal}
|
||||||
|
isVisible={isCancelOpen}
|
||||||
|
handleClose={this.closeCancelModal}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -161,6 +211,9 @@ export class ProposalDetail extends React.Component<Props, State> {
|
||||||
this.setState({ isBodyOverflowing: true });
|
this.setState({ isBodyOverflowing: true });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private openCancelModal = () => this.setState({ isCancelOpen: true });
|
||||||
|
private closeCancelModal = () => this.setState({ isCancelOpen: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapStateToProps(state: AppState, ownProps: OwnProps) {
|
function mapStateToProps(state: AppState, ownProps: OwnProps) {
|
||||||
|
@ -178,7 +231,7 @@ const withConnect = connect<StateProps, DispatchProps, OwnProps, AppState>(
|
||||||
mapDispatchToProps,
|
mapDispatchToProps,
|
||||||
);
|
);
|
||||||
|
|
||||||
const ConnectedProposal = compose<Props, OwnProps>(
|
const ConnectedProposal = compose<Props, OwnProps & Web3Props>(
|
||||||
withRouter,
|
withRouter,
|
||||||
withConnect,
|
withConnect,
|
||||||
)(ProposalDetail);
|
)(ProposalDetail);
|
||||||
|
@ -194,6 +247,6 @@ export default (props: OwnProps) => (
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
render={() => <ConnectedProposal {...props} />}
|
render={({ accounts }) => <ConnectedProposal account={accounts[0]} {...props} />}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
|
@ -15,6 +15,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
&-main {
|
&-main {
|
||||||
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
width: calc(100% - 19rem);
|
width: calc(100% - 19rem);
|
||||||
|
@ -104,6 +105,12 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&-menu {
|
||||||
|
position: absolute;
|
||||||
|
top: 1rem;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&-side {
|
&-side {
|
||||||
|
|
|
@ -14,7 +14,7 @@ export interface Web3RenderProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface OwnProps {
|
interface OwnProps {
|
||||||
render(props: Web3RenderProps & any): React.ReactNode;
|
render(props: Web3RenderProps & { props: any }): React.ReactNode;
|
||||||
renderLoading(): React.ReactNode;
|
renderLoading(): React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -332,6 +332,49 @@ export function voteRefund(crowdFundContract: any, vote: boolean) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function freezeContract(crowdFundContract: any, account: string) {
|
||||||
|
let isFrozen = await crowdFundContract.methods.frozen().call({ from: account });
|
||||||
|
// Already frozen, all good here
|
||||||
|
if (isFrozen) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
crowdFundContract.methods
|
||||||
|
.refund()
|
||||||
|
.send({ from: account })
|
||||||
|
.once('confirmation', async () => {
|
||||||
|
await sleep(5000);
|
||||||
|
isFrozen = await crowdFundContract.methods.frozen().call({ from: account });
|
||||||
|
resolve();
|
||||||
|
})
|
||||||
|
.catch((err: Error) => reject(err));
|
||||||
|
});
|
||||||
|
if (!isFrozen) {
|
||||||
|
throw new Error('Proposal isn’t in a refundable state yet.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function triggerRefund(crowdFundContract: any) {
|
||||||
|
return async (dispatch: Dispatch<any>, getState: GetState) => {
|
||||||
|
dispatch({ type: types.WITHDRAW_REFUND_PENDING });
|
||||||
|
const state = getState();
|
||||||
|
const account = state.web3.accounts[0];
|
||||||
|
|
||||||
|
try {
|
||||||
|
await freezeContract(crowdFundContract, account);
|
||||||
|
await dispatch(fetchProposal(crowdFundContract._address));
|
||||||
|
dispatch({ type: types.TRIGGER_REFUND_FULFILLED });
|
||||||
|
} catch (err) {
|
||||||
|
dispatch({
|
||||||
|
type: types.TRIGGER_REFUND_REJECTED,
|
||||||
|
payload: err.message || err.toString(),
|
||||||
|
error: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function withdrawRefund(crowdFundContract: any, address: string) {
|
export function withdrawRefund(crowdFundContract: any, address: string) {
|
||||||
return async (dispatch: Dispatch<any>, getState: GetState) => {
|
return async (dispatch: Dispatch<any>, getState: GetState) => {
|
||||||
dispatch({ type: types.WITHDRAW_REFUND_PENDING });
|
dispatch({ type: types.WITHDRAW_REFUND_PENDING });
|
||||||
|
@ -339,24 +382,7 @@ export function withdrawRefund(crowdFundContract: any, address: string) {
|
||||||
const account = state.web3.accounts[0];
|
const account = state.web3.accounts[0];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let isFrozen = await crowdFundContract.methods.frozen().call({ from: account });
|
await freezeContract(crowdFundContract, account);
|
||||||
if (!isFrozen) {
|
|
||||||
await new Promise(resolve => {
|
|
||||||
crowdFundContract.methods
|
|
||||||
.refund()
|
|
||||||
.send({ from: account })
|
|
||||||
.once('confirmation', async () => {
|
|
||||||
await sleep(5000);
|
|
||||||
isFrozen = await crowdFundContract.methods.frozen().call({ from: account });
|
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isFrozen) {
|
|
||||||
throw new Error('Proposal isn’t in a refundable state yet.');
|
|
||||||
}
|
|
||||||
|
|
||||||
await crowdFundContract.methods
|
await crowdFundContract.methods
|
||||||
.withdraw(address)
|
.withdraw(address)
|
||||||
.send({ from: account })
|
.send({ from: account })
|
||||||
|
|
|
@ -214,6 +214,7 @@ export default (state = INITIAL_STATE, action: any): Web3State => {
|
||||||
|
|
||||||
case types.VOTE_REFUND_PENDING:
|
case types.VOTE_REFUND_PENDING:
|
||||||
case types.WITHDRAW_REFUND_PENDING:
|
case types.WITHDRAW_REFUND_PENDING:
|
||||||
|
case types.TRIGGER_REFUND_PENDING:
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
isRefundActionPending: true,
|
isRefundActionPending: true,
|
||||||
|
@ -221,12 +222,14 @@ export default (state = INITIAL_STATE, action: any): Web3State => {
|
||||||
};
|
};
|
||||||
case types.VOTE_REFUND_FULFILLED:
|
case types.VOTE_REFUND_FULFILLED:
|
||||||
case types.WITHDRAW_REFUND_FULFILLED:
|
case types.WITHDRAW_REFUND_FULFILLED:
|
||||||
|
case types.TRIGGER_REFUND_FULFILLED:
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
isRefundActionPending: false,
|
isRefundActionPending: false,
|
||||||
};
|
};
|
||||||
case types.VOTE_REFUND_REJECTED:
|
case types.VOTE_REFUND_REJECTED:
|
||||||
case types.WITHDRAW_REFUND_REJECTED:
|
case types.WITHDRAW_REFUND_REJECTED:
|
||||||
|
case types.TRIGGER_REFUND_REJECTED:
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
refundActionError: payload,
|
refundActionError: payload,
|
||||||
|
|
|
@ -46,6 +46,11 @@ enum web3Types {
|
||||||
WITHDRAW_REFUND_REJECTED = 'WITHDRAW_REFUND_REJECTED',
|
WITHDRAW_REFUND_REJECTED = 'WITHDRAW_REFUND_REJECTED',
|
||||||
WITHDRAW_REFUND_PENDING = 'WITHDRAW_REFUND_PENDING',
|
WITHDRAW_REFUND_PENDING = 'WITHDRAW_REFUND_PENDING',
|
||||||
|
|
||||||
|
TRIGGER_REFUND = 'TRIGGER_REFUND',
|
||||||
|
TRIGGER_REFUND_FULFILLED = 'TRIGGER_REFUND_FULFILLED',
|
||||||
|
TRIGGER_REFUND_REJECTED = 'TRIGGER_REFUND_REJECTED',
|
||||||
|
TRIGGER_REFUND_PENDING = 'TRIGGER_REFUND_PENDING',
|
||||||
|
|
||||||
ACCOUNTS = 'ACCOUNTS',
|
ACCOUNTS = 'ACCOUNTS',
|
||||||
ACCOUNTS_FULFILLED = 'ACCOUNTS_FULFILLED',
|
ACCOUNTS_FULFILLED = 'ACCOUNTS_FULFILLED',
|
||||||
ACCOUNTS_REJECTED = 'ACCOUNTS_REJECTED',
|
ACCOUNTS_REJECTED = 'ACCOUNTS_REJECTED',
|
||||||
|
|
Loading…
Reference in New Issue