2019-01-09 10:23:08 -08:00
|
|
|
import React from 'react';
|
2019-02-13 08:54:46 -08:00
|
|
|
import BN from 'bn.js';
|
2019-01-09 10:23:08 -08:00
|
|
|
import { view } from 'react-easy-state';
|
|
|
|
import { RouteComponentProps, withRouter } from 'react-router';
|
2019-01-29 15:50:27 -08:00
|
|
|
import {
|
|
|
|
Row,
|
|
|
|
Col,
|
|
|
|
Card,
|
|
|
|
Alert,
|
|
|
|
Button,
|
|
|
|
Collapse,
|
|
|
|
Popconfirm,
|
|
|
|
Input,
|
2019-05-28 15:33:49 -07:00
|
|
|
Tag,
|
2019-02-13 08:54:46 -08:00
|
|
|
message,
|
2019-01-29 15:50:27 -08:00
|
|
|
} from 'antd';
|
2019-01-09 10:23:08 -08:00
|
|
|
import TextArea from 'antd/lib/input/TextArea';
|
|
|
|
import store from 'src/store';
|
2019-02-15 19:35:25 -08:00
|
|
|
import { formatDateSeconds, formatDurationSeconds } from 'util/time';
|
|
|
|
import {
|
|
|
|
PROPOSAL_STATUS,
|
|
|
|
PROPOSAL_ARBITER_STATUS,
|
|
|
|
MILESTONE_STAGE,
|
|
|
|
PROPOSAL_STAGE,
|
|
|
|
} from 'src/types';
|
2019-01-09 10:23:08 -08:00
|
|
|
import { Link } from 'react-router-dom';
|
2019-01-16 21:01:29 -08:00
|
|
|
import Back from 'components/Back';
|
2019-01-09 10:23:08 -08:00
|
|
|
import Markdown from 'components/Markdown';
|
2019-02-06 10:38:07 -08:00
|
|
|
import ArbiterControl from 'components/ArbiterControl';
|
2019-02-13 08:54:46 -08:00
|
|
|
import { toZat, fromZat } from 'src/util/units';
|
2019-02-25 08:41:00 -08:00
|
|
|
import FeedbackModal from '../FeedbackModal';
|
2019-12-03 16:02:39 -08:00
|
|
|
import { formatUsd } from 'util/formatters';
|
2019-02-25 08:41:00 -08:00
|
|
|
import './index.less';
|
2019-01-09 10:23:08 -08:00
|
|
|
|
|
|
|
type Props = RouteComponentProps<any>;
|
|
|
|
|
|
|
|
const STATE = {
|
2019-02-13 08:54:46 -08:00
|
|
|
paidTxId: '',
|
2019-03-06 12:25:58 -08:00
|
|
|
showCancelAndRefundPopover: false,
|
2019-10-23 14:44:19 -07:00
|
|
|
showChangeToAcceptedWithFundingPopover: false,
|
2019-01-09 10:23:08 -08:00
|
|
|
};
|
|
|
|
|
|
|
|
type State = typeof STATE;
|
|
|
|
|
|
|
|
class ProposalDetailNaked extends React.Component<Props, State> {
|
|
|
|
state = STATE;
|
|
|
|
rejectInput: null | TextArea = null;
|
|
|
|
componentDidMount() {
|
|
|
|
this.loadDetail();
|
|
|
|
}
|
|
|
|
render() {
|
|
|
|
const id = this.getIdFromQuery();
|
2019-02-25 08:41:00 -08:00
|
|
|
const { proposalDetail: p, proposalDetailFetching } = store;
|
2019-01-09 10:23:08 -08:00
|
|
|
|
|
|
|
if (!p || (p && p.proposalId !== id) || proposalDetailFetching) {
|
|
|
|
return 'loading proposal...';
|
|
|
|
}
|
|
|
|
|
2019-02-15 19:35:25 -08:00
|
|
|
const needsArbiter =
|
|
|
|
PROPOSAL_ARBITER_STATUS.MISSING === p.arbiter.status &&
|
|
|
|
p.status === PROPOSAL_STATUS.LIVE &&
|
2019-03-06 12:25:58 -08:00
|
|
|
!p.isFailed &&
|
|
|
|
p.stage !== PROPOSAL_STAGE.COMPLETED;
|
2019-02-23 13:38:06 -08:00
|
|
|
const refundablePct = p.milestones.reduce((prev, m) => {
|
|
|
|
return m.datePaid ? prev - parseFloat(m.payoutPercent) : prev;
|
|
|
|
}, 100);
|
|
|
|
|
2019-10-23 14:44:19 -07:00
|
|
|
const { isVersionTwo } = p;
|
2019-10-16 20:43:20 -07:00
|
|
|
const shouldShowArbiter =
|
2019-10-23 14:44:19 -07:00
|
|
|
!isVersionTwo || (isVersionTwo && p.acceptedWithFunding === true);
|
|
|
|
const cancelButtonText = isVersionTwo ? 'Cancel' : 'Cancel & refund';
|
|
|
|
const shouldShowChangeToAcceptedWithFunding =
|
|
|
|
isVersionTwo && p.acceptedWithFunding === false;
|
2019-10-16 20:43:20 -07:00
|
|
|
|
2019-02-27 14:50:53 -08:00
|
|
|
const renderCancelControl = () => {
|
2019-03-06 12:25:58 -08:00
|
|
|
const disabled = this.getCancelAndRefundDisabled();
|
2019-02-27 14:50:53 -08:00
|
|
|
|
|
|
|
return (
|
|
|
|
<Popconfirm
|
|
|
|
title={
|
2019-12-05 16:06:03 -08:00
|
|
|
isVersionTwo ? (
|
|
|
|
<p>
|
|
|
|
Are you sure you want to cancel proposal?
|
|
|
|
<br />
|
|
|
|
This cannot be undone.
|
|
|
|
</p>
|
|
|
|
) : (
|
|
|
|
<p>
|
|
|
|
Are you sure you want to cancel proposal and begin
|
|
|
|
<br />
|
|
|
|
the refund process? This cannot be undone.
|
|
|
|
</p>
|
|
|
|
)
|
2019-02-23 13:38:06 -08:00
|
|
|
}
|
2019-02-27 14:50:53 -08:00
|
|
|
placement="left"
|
|
|
|
cancelText="cancel"
|
|
|
|
okText="confirm"
|
2019-03-06 12:25:58 -08:00
|
|
|
visible={this.state.showCancelAndRefundPopover}
|
2019-02-27 14:50:53 -08:00
|
|
|
okButtonProps={{
|
|
|
|
loading: store.proposalDetailCanceling,
|
|
|
|
}}
|
2019-03-06 12:25:58 -08:00
|
|
|
onCancel={this.handleCancelCancel}
|
|
|
|
onConfirm={this.handleConfirmCancel}
|
2019-02-23 13:38:06 -08:00
|
|
|
>
|
2019-02-27 14:50:53 -08:00
|
|
|
<Button
|
|
|
|
icon="close-circle"
|
|
|
|
className="ProposalDetail-controls-control"
|
|
|
|
loading={store.proposalDetailCanceling}
|
2019-03-06 12:25:58 -08:00
|
|
|
onClick={this.handleCancelAndRefundClick}
|
2019-02-27 14:50:53 -08:00
|
|
|
disabled={disabled}
|
|
|
|
block
|
|
|
|
>
|
2019-10-23 14:44:19 -07:00
|
|
|
{cancelButtonText}
|
|
|
|
</Button>
|
|
|
|
</Popconfirm>
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
|
|
|
const renderChangeToAcceptedWithFundingControl = () => {
|
|
|
|
return (
|
|
|
|
<Popconfirm
|
|
|
|
title={
|
|
|
|
<p>
|
|
|
|
Are you sure you want to accept the proposal
|
|
|
|
<br />
|
|
|
|
with funding? This cannot be undone.
|
|
|
|
</p>
|
|
|
|
}
|
|
|
|
placement="left"
|
|
|
|
cancelText="cancel"
|
|
|
|
okText="confirm"
|
|
|
|
visible={this.state.showChangeToAcceptedWithFundingPopover}
|
|
|
|
okButtonProps={{
|
|
|
|
loading: store.proposalDetailCanceling,
|
|
|
|
}}
|
|
|
|
onCancel={this.handleChangeToAcceptWithFundingCancel}
|
|
|
|
onConfirm={this.handleChangeToAcceptWithFundingConfirm}
|
|
|
|
>
|
|
|
|
<Button
|
|
|
|
icon="close-circle"
|
|
|
|
className="ProposalDetail-controls-control"
|
|
|
|
loading={store.proposalDetailChangingToAcceptedWithFunding}
|
|
|
|
onClick={this.handleChangeToAcceptedWithFunding}
|
|
|
|
block
|
|
|
|
>
|
|
|
|
Accept With Funding
|
2019-02-27 14:50:53 -08:00
|
|
|
</Button>
|
|
|
|
</Popconfirm>
|
|
|
|
);
|
|
|
|
};
|
2019-02-23 13:38:06 -08:00
|
|
|
|
2019-02-06 10:38:07 -08:00
|
|
|
const renderArbiterControl = () => (
|
|
|
|
<ArbiterControl
|
|
|
|
{...p}
|
|
|
|
buttonProps={{
|
|
|
|
type: 'default',
|
|
|
|
className: 'ProposalDetail-controls-control',
|
|
|
|
block: true,
|
2019-03-06 12:25:58 -08:00
|
|
|
disabled:
|
|
|
|
p.status !== PROPOSAL_STATUS.LIVE ||
|
|
|
|
p.isFailed ||
|
|
|
|
p.stage === PROPOSAL_STAGE.COMPLETED,
|
2019-02-06 10:38:07 -08:00
|
|
|
}}
|
|
|
|
/>
|
|
|
|
);
|
|
|
|
|
2019-01-09 10:23:08 -08:00
|
|
|
const renderApproved = () =>
|
|
|
|
p.status === PROPOSAL_STATUS.APPROVED && (
|
|
|
|
<Alert
|
|
|
|
showIcon
|
|
|
|
type="success"
|
|
|
|
message={`Approved on ${formatDateSeconds(p.dateApproved)}`}
|
|
|
|
description={`
|
|
|
|
This proposal has been approved and will become live when a team-member
|
|
|
|
publishes it.
|
|
|
|
`}
|
|
|
|
/>
|
|
|
|
);
|
|
|
|
|
|
|
|
const renderReview = () =>
|
|
|
|
p.status === PROPOSAL_STATUS.PENDING && (
|
2019-12-05 17:12:46 -08:00
|
|
|
<>
|
|
|
|
<Row gutter={16}>
|
|
|
|
<Col span={isVersionTwo ? 16 : 24}>
|
|
|
|
<Alert
|
2019-01-09 10:23:08 -08:00
|
|
|
showIcon
|
|
|
|
type="warning"
|
|
|
|
message="Review Pending"
|
|
|
|
description={
|
|
|
|
<div>
|
|
|
|
<p>Please review this proposal and render your judgment.</p>
|
|
|
|
<Button
|
2019-10-23 14:34:31 -07:00
|
|
|
className="ProposalDetail-review"
|
2019-01-09 10:23:08 -08:00
|
|
|
loading={store.proposalDetailApproving}
|
|
|
|
icon="check"
|
|
|
|
type="primary"
|
2019-10-16 20:43:20 -07:00
|
|
|
onClick={() => this.handleApprove(true)}
|
|
|
|
>
|
|
|
|
Approve With Funding
|
|
|
|
</Button>
|
|
|
|
<Button
|
2019-10-23 14:34:31 -07:00
|
|
|
className="ProposalDetail-review"
|
2019-10-16 20:43:20 -07:00
|
|
|
loading={store.proposalDetailApproving}
|
|
|
|
icon="check"
|
|
|
|
type="default"
|
|
|
|
onClick={() => this.handleApprove(false)}
|
2019-01-09 10:23:08 -08:00
|
|
|
>
|
2019-10-16 20:43:20 -07:00
|
|
|
Approve Without Funding
|
2019-01-09 10:23:08 -08:00
|
|
|
</Button>
|
|
|
|
<Button
|
2019-10-23 14:34:31 -07:00
|
|
|
className="ProposalDetail-review"
|
2019-01-09 10:23:08 -08:00
|
|
|
loading={store.proposalDetailApproving}
|
|
|
|
icon="close"
|
|
|
|
type="danger"
|
|
|
|
onClick={() => {
|
2019-02-25 08:41:00 -08:00
|
|
|
FeedbackModal.open({
|
2019-12-05 17:12:46 -08:00
|
|
|
title: 'Request changes to this proposal?',
|
2019-02-25 08:41:00 -08:00
|
|
|
label: 'Please provide a reason:',
|
|
|
|
okText: 'Reject',
|
|
|
|
onOk: this.handleReject,
|
|
|
|
});
|
2019-01-09 10:23:08 -08:00
|
|
|
}}
|
|
|
|
>
|
2019-12-05 17:12:46 -08:00
|
|
|
Request changes
|
2019-01-09 10:23:08 -08:00
|
|
|
</Button>
|
|
|
|
</div>
|
|
|
|
}
|
|
|
|
/>
|
2019-12-05 17:12:46 -08:00
|
|
|
</Col>
|
|
|
|
{p.isVersionTwo && <Col span={8}>
|
|
|
|
<Alert
|
|
|
|
showIcon
|
|
|
|
type={p.rfpOptIn ? "success" : "error"}
|
|
|
|
message={p.rfpOptIn ? "KYC accepted" : "KYC rejected"}
|
|
|
|
description={
|
|
|
|
<div>
|
|
|
|
{p.rfpOptIn ? <p>KYC has been accepted by the proposer.</p> : <p>KYC has been rejected. Recommend against approving with funding.</p>}
|
|
|
|
|
|
|
|
</div>}
|
|
|
|
/>
|
|
|
|
</Col>
|
|
|
|
}
|
|
|
|
|
|
|
|
</Row>
|
|
|
|
</>
|
2019-01-09 10:23:08 -08:00
|
|
|
);
|
|
|
|
|
|
|
|
const renderRejected = () =>
|
|
|
|
p.status === PROPOSAL_STATUS.REJECTED && (
|
|
|
|
<Alert
|
|
|
|
showIcon
|
|
|
|
type="error"
|
2019-11-20 13:37:58 -08:00
|
|
|
message="Changes requested"
|
2019-01-09 10:23:08 -08:00
|
|
|
description={
|
|
|
|
<div>
|
|
|
|
<p>
|
2019-11-24 07:05:08 -08:00
|
|
|
This proposal has changes requested. The team will be able to re-submit it
|
|
|
|
for approval should they desire to do so.
|
2019-01-09 10:23:08 -08:00
|
|
|
</p>
|
|
|
|
<b>Reason:</b>
|
|
|
|
<br />
|
|
|
|
<i>{p.rejectReason}</i>
|
|
|
|
</div>
|
|
|
|
}
|
|
|
|
/>
|
|
|
|
);
|
|
|
|
|
2019-02-09 19:00:49 -08:00
|
|
|
const renderNominateArbiter = () =>
|
2019-10-23 14:44:19 -07:00
|
|
|
needsArbiter &&
|
|
|
|
shouldShowArbiter && (
|
2019-02-05 12:45:26 -08:00
|
|
|
<Alert
|
|
|
|
showIcon
|
|
|
|
type="warning"
|
2019-02-09 19:00:49 -08:00
|
|
|
message="No arbiter on live proposal"
|
2019-02-05 12:45:26 -08:00
|
|
|
description={
|
|
|
|
<div>
|
|
|
|
<p>An arbiter is required to review milestone payout requests.</p>
|
2019-02-06 10:38:07 -08:00
|
|
|
<ArbiterControl {...p} />
|
2019-02-05 12:45:26 -08:00
|
|
|
</div>
|
|
|
|
}
|
|
|
|
/>
|
|
|
|
);
|
|
|
|
|
2019-02-09 19:00:49 -08:00
|
|
|
const renderNominatedArbiter = () =>
|
|
|
|
PROPOSAL_ARBITER_STATUS.NOMINATED === p.arbiter.status &&
|
|
|
|
p.status === PROPOSAL_STATUS.LIVE && (
|
|
|
|
<Alert
|
|
|
|
showIcon
|
|
|
|
type="info"
|
|
|
|
message="Arbiter has been nominated"
|
|
|
|
description={
|
|
|
|
<div>
|
|
|
|
<p>
|
|
|
|
<b>{p.arbiter.user!.displayName}</b> has been nominated for arbiter of
|
|
|
|
this proposal but has not yet accepted.
|
|
|
|
</p>
|
|
|
|
<ArbiterControl {...p} />
|
|
|
|
</div>
|
|
|
|
}
|
|
|
|
/>
|
|
|
|
);
|
|
|
|
|
2019-02-13 08:54:46 -08:00
|
|
|
const renderMilestoneAccepted = () => {
|
2019-02-23 13:38:06 -08:00
|
|
|
if (p.stage === PROPOSAL_STAGE.FAILED || p.stage === PROPOSAL_STAGE.CANCELED) {
|
|
|
|
return;
|
|
|
|
}
|
2019-02-13 08:54:46 -08:00
|
|
|
if (
|
|
|
|
!(
|
|
|
|
p.status === PROPOSAL_STATUS.LIVE &&
|
|
|
|
p.currentMilestone &&
|
|
|
|
p.currentMilestone.stage === MILESTONE_STAGE.ACCEPTED
|
|
|
|
)
|
|
|
|
) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
const ms = p.currentMilestone;
|
2019-12-03 16:02:39 -08:00
|
|
|
|
|
|
|
let paymentMsg;
|
|
|
|
if (p.isVersionTwo) {
|
|
|
|
const target = parseFloat(p.target.toString());
|
|
|
|
const payoutPercent = parseFloat(ms.payoutPercent);
|
|
|
|
const amountNum = (target * payoutPercent) / 100;
|
|
|
|
const amount = formatUsd(amountNum, true, 2);
|
|
|
|
paymentMsg = `${amount} in ZEC`;
|
|
|
|
} else {
|
|
|
|
const amount = fromZat(
|
|
|
|
toZat(p.target)
|
|
|
|
.mul(new BN(ms.payoutPercent))
|
|
|
|
.divn(100),
|
|
|
|
);
|
|
|
|
paymentMsg = `${amount} ZEC`;
|
|
|
|
}
|
|
|
|
|
2019-02-13 08:54:46 -08:00
|
|
|
return (
|
|
|
|
<Alert
|
|
|
|
className="ProposalDetail-alert"
|
|
|
|
showIcon
|
|
|
|
type="warning"
|
|
|
|
message={null}
|
|
|
|
description={
|
|
|
|
<div>
|
|
|
|
<p>
|
|
|
|
<b>
|
|
|
|
Milestone {ms.index + 1} - {ms.title}
|
|
|
|
</b>{' '}
|
|
|
|
was accepted on {formatDateSeconds(ms.dateAccepted)}.
|
|
|
|
</p>
|
|
|
|
<p>
|
|
|
|
{' '}
|
2019-12-03 16:02:39 -08:00
|
|
|
Please make a payment of <b>{paymentMsg}</b> to:
|
2019-02-13 08:54:46 -08:00
|
|
|
</p>{' '}
|
|
|
|
<pre>{p.payoutAddress}</pre>
|
|
|
|
<Input.Search
|
|
|
|
placeholder="please enter payment txid"
|
|
|
|
value={this.state.paidTxId}
|
|
|
|
enterButton="Mark Paid"
|
|
|
|
onChange={e => this.setState({ paidTxId: e.target.value })}
|
|
|
|
onSearch={this.handlePaidMilestone}
|
|
|
|
/>
|
|
|
|
</div>
|
|
|
|
}
|
|
|
|
/>
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
2019-02-15 19:35:25 -08:00
|
|
|
const renderFailed = () =>
|
|
|
|
p.isFailed && (
|
|
|
|
<Alert
|
|
|
|
showIcon
|
|
|
|
type="error"
|
2019-02-25 08:41:00 -08:00
|
|
|
message={
|
|
|
|
p.stage === PROPOSAL_STAGE.FAILED ? 'Proposal failed' : 'Proposal canceled'
|
|
|
|
}
|
2019-02-15 19:35:25 -08:00
|
|
|
description={
|
2019-02-23 13:38:06 -08:00
|
|
|
p.stage === PROPOSAL_STAGE.FAILED ? (
|
|
|
|
<>
|
|
|
|
This proposal failed to reach its funding goal of <b>{p.target} ZEC</b> by{' '}
|
2019-02-25 08:41:00 -08:00
|
|
|
<b>{formatDateSeconds(p.datePublished + p.deadlineDuration)}</b>. All
|
|
|
|
contributors will need to be refunded.
|
2019-02-23 13:38:06 -08:00
|
|
|
</>
|
|
|
|
) : (
|
|
|
|
<>
|
2019-02-25 08:41:00 -08:00
|
|
|
This proposal was canceled by an admin, and will be refunding contributors{' '}
|
|
|
|
<b>{refundablePct}%</b> of their contributions.
|
2019-02-23 13:38:06 -08:00
|
|
|
</>
|
|
|
|
)
|
2019-02-15 19:35:25 -08:00
|
|
|
}
|
|
|
|
/>
|
|
|
|
);
|
|
|
|
|
2019-01-09 10:23:08 -08:00
|
|
|
const renderDeetItem = (name: string, val: any) => (
|
|
|
|
<div className="ProposalDetail-deet">
|
|
|
|
<span>{name}</span>
|
2019-01-29 15:50:27 -08:00
|
|
|
{val}
|
2019-01-09 10:23:08 -08:00
|
|
|
</div>
|
|
|
|
);
|
|
|
|
|
|
|
|
return (
|
|
|
|
<div className="ProposalDetail">
|
2019-01-16 21:01:29 -08:00
|
|
|
<Back to="/proposals" text="Proposals" />
|
2019-01-09 10:23:08 -08:00
|
|
|
<h1>{p.title}</h1>
|
|
|
|
<Row gutter={16}>
|
|
|
|
{/* MAIN */}
|
|
|
|
<Col span={18}>
|
|
|
|
{renderApproved()}
|
|
|
|
{renderReview()}
|
|
|
|
{renderRejected()}
|
2019-02-09 19:00:49 -08:00
|
|
|
{renderNominateArbiter()}
|
|
|
|
{renderNominatedArbiter()}
|
2019-02-13 08:54:46 -08:00
|
|
|
{renderMilestoneAccepted()}
|
2019-02-15 19:35:25 -08:00
|
|
|
{renderFailed()}
|
2019-05-28 15:33:49 -07:00
|
|
|
<Collapse defaultActiveKey={['brief', 'content', 'milestones']}>
|
2019-01-09 10:23:08 -08:00
|
|
|
<Collapse.Panel key="brief" header="brief">
|
|
|
|
{p.brief}
|
|
|
|
</Collapse.Panel>
|
|
|
|
|
|
|
|
<Collapse.Panel key="content" header="content">
|
|
|
|
<Markdown source={p.content} />
|
|
|
|
</Collapse.Panel>
|
|
|
|
|
2019-05-28 15:33:49 -07:00
|
|
|
<Collapse.Panel key="milestones" header="milestones">
|
2019-10-16 20:43:20 -07:00
|
|
|
{p.milestones.map((milestone, i) => (
|
|
|
|
<Card
|
|
|
|
title={
|
|
|
|
<>
|
|
|
|
{milestone.title + ' '}
|
|
|
|
{milestone.immediatePayout && (
|
|
|
|
<Tag color="magenta">Immediate Payout</Tag>
|
|
|
|
)}
|
|
|
|
</>
|
|
|
|
}
|
|
|
|
extra={`${milestone.payoutPercent}% Payout`}
|
|
|
|
key={i}
|
|
|
|
>
|
2019-11-13 14:38:17 -08:00
|
|
|
{p.isVersionTwo && (
|
|
|
|
<p>
|
|
|
|
<b>Estimated Days to Complete:</b>{' '}
|
|
|
|
{milestone.immediatePayout ? 'N/A' : milestone.daysEstimated}{' '}
|
|
|
|
</p>
|
|
|
|
)}
|
2019-10-16 20:43:20 -07:00
|
|
|
<p>
|
2019-11-13 14:38:17 -08:00
|
|
|
<b>Estimated Date:</b>{' '}
|
|
|
|
{milestone.dateEstimated
|
|
|
|
? formatDateSeconds(milestone.dateEstimated)
|
|
|
|
: 'N/A'}{' '}
|
2019-10-16 20:43:20 -07:00
|
|
|
</p>
|
2019-11-13 14:38:17 -08:00
|
|
|
|
2019-10-16 20:43:20 -07:00
|
|
|
<p>{milestone.content}</p>
|
|
|
|
</Card>
|
|
|
|
))}
|
2019-05-28 15:33:49 -07:00
|
|
|
</Collapse.Panel>
|
|
|
|
|
2019-01-09 10:23:08 -08:00
|
|
|
<Collapse.Panel key="json" header="json">
|
|
|
|
<pre>{JSON.stringify(p, null, 4)}</pre>
|
|
|
|
</Collapse.Panel>
|
|
|
|
</Collapse>
|
|
|
|
</Col>
|
|
|
|
|
|
|
|
{/* RIGHT SIDE */}
|
|
|
|
<Col span={6}>
|
2019-12-05 17:12:46 -08:00
|
|
|
{p.isVersionTwo && !p.acceptedWithFunding && p.stage === PROPOSAL_STAGE.WIP && <Alert
|
|
|
|
message="Accepted without funding"
|
|
|
|
description="This proposal has been posted publicly, but isn't being funded by the Zcash Foundation."
|
|
|
|
type="info"
|
|
|
|
showIcon
|
|
|
|
/>
|
|
|
|
}
|
|
|
|
|
2019-01-09 10:23:08 -08:00
|
|
|
{/* ACTIONS */}
|
2019-01-29 15:50:27 -08:00
|
|
|
<Card size="small" className="ProposalDetail-controls">
|
2019-02-23 13:38:06 -08:00
|
|
|
{renderCancelControl()}
|
2019-02-06 10:38:07 -08:00
|
|
|
{renderArbiterControl()}
|
2019-10-23 14:44:19 -07:00
|
|
|
{shouldShowChangeToAcceptedWithFunding &&
|
|
|
|
renderChangeToAcceptedWithFundingControl()}
|
2019-01-09 10:23:08 -08:00
|
|
|
</Card>
|
|
|
|
|
|
|
|
{/* DETAILS */}
|
2019-02-01 11:13:30 -08:00
|
|
|
<Card title="Details" size="small">
|
2019-01-09 10:23:08 -08:00
|
|
|
{renderDeetItem('id', p.proposalId)}
|
|
|
|
{renderDeetItem('created', formatDateSeconds(p.dateCreated))}
|
2019-03-14 09:47:42 -07:00
|
|
|
{renderDeetItem(
|
|
|
|
'published',
|
|
|
|
p.datePublished ? formatDateSeconds(p.datePublished) : 'n/a',
|
|
|
|
)}
|
2019-02-15 19:35:25 -08:00
|
|
|
{renderDeetItem(
|
|
|
|
'deadlineDuration',
|
|
|
|
formatDurationSeconds(p.deadlineDuration),
|
|
|
|
)}
|
|
|
|
{p.datePublished &&
|
|
|
|
renderDeetItem(
|
|
|
|
'(deadline)',
|
|
|
|
formatDateSeconds(p.datePublished + p.deadlineDuration),
|
|
|
|
)}
|
|
|
|
{renderDeetItem('isFailed', JSON.stringify(p.isFailed))}
|
2019-01-09 10:23:08 -08:00
|
|
|
{renderDeetItem('status', p.status)}
|
2019-02-13 08:54:46 -08:00
|
|
|
{renderDeetItem('stage', p.stage)}
|
2019-12-03 16:02:39 -08:00
|
|
|
{renderDeetItem('target', p.isVersionTwo ? formatUsd(p.target) : p.target)}
|
2019-01-29 15:50:27 -08:00
|
|
|
{renderDeetItem('contributed', p.contributed)}
|
2019-12-03 16:02:39 -08:00
|
|
|
{renderDeetItem(
|
|
|
|
'funded (inc. matching)',
|
|
|
|
p.isVersionTwo ? formatUsd(p.funded) : p.funded,
|
|
|
|
)}
|
2019-01-29 15:50:27 -08:00
|
|
|
{renderDeetItem('matching', p.contributionMatching)}
|
2019-03-06 12:25:58 -08:00
|
|
|
{renderDeetItem('bounty', p.contributionBounty)}
|
|
|
|
{renderDeetItem('rfpOptIn', JSON.stringify(p.rfpOptIn))}
|
2019-10-23 14:44:19 -07:00
|
|
|
{renderDeetItem(
|
|
|
|
'acceptedWithFunding',
|
|
|
|
JSON.stringify(p.acceptedWithFunding),
|
|
|
|
)}
|
2019-02-09 19:00:49 -08:00
|
|
|
{renderDeetItem(
|
|
|
|
'arbiter',
|
|
|
|
<>
|
|
|
|
{p.arbiter.user && (
|
|
|
|
<Link to={`/users/${p.arbiter.user.userid}`}>
|
|
|
|
{p.arbiter.user.displayName}
|
|
|
|
</Link>
|
|
|
|
)}
|
|
|
|
({p.arbiter.status})
|
|
|
|
</>,
|
|
|
|
)}
|
2019-02-01 11:13:30 -08:00
|
|
|
{p.rfp &&
|
2019-02-05 12:45:26 -08:00
|
|
|
renderDeetItem(
|
|
|
|
'rfp',
|
|
|
|
<Link to={`/rfps/${p.rfp.id}`}>{p.rfp.title}</Link>,
|
|
|
|
)}
|
2019-01-09 10:23:08 -08:00
|
|
|
</Card>
|
|
|
|
|
|
|
|
{/* TEAM */}
|
2019-02-01 11:13:30 -08:00
|
|
|
<Card title="Team" size="small">
|
2019-01-09 10:23:08 -08:00
|
|
|
{p.team.map(t => (
|
2019-01-16 21:01:29 -08:00
|
|
|
<div key={t.userid}>
|
|
|
|
<Link to={`/users/${t.userid}`}>{t.displayName}</Link>
|
|
|
|
</div>
|
2019-01-09 10:23:08 -08:00
|
|
|
))}
|
|
|
|
</Card>
|
|
|
|
</Col>
|
|
|
|
</Row>
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2019-03-06 12:25:58 -08:00
|
|
|
private getCancelAndRefundDisabled = () => {
|
|
|
|
const { proposalDetail: p } = store;
|
|
|
|
if (!p) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
return (
|
|
|
|
p.status !== PROPOSAL_STATUS.LIVE ||
|
|
|
|
p.stage === PROPOSAL_STAGE.FAILED ||
|
|
|
|
p.stage === PROPOSAL_STAGE.CANCELED ||
|
|
|
|
p.isFailed
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
|
|
|
private handleCancelAndRefundClick = () => {
|
|
|
|
const disabled = this.getCancelAndRefundDisabled();
|
|
|
|
if (!disabled) {
|
|
|
|
if (!this.state.showCancelAndRefundPopover) {
|
|
|
|
this.setState({ showCancelAndRefundPopover: true });
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2019-10-23 14:44:19 -07:00
|
|
|
private handleChangeToAcceptedWithFunding = () => {
|
|
|
|
this.setState({ showChangeToAcceptedWithFundingPopover: true });
|
|
|
|
};
|
|
|
|
|
|
|
|
private handleChangeToAcceptWithFundingCancel = () => {
|
|
|
|
this.setState({ showChangeToAcceptedWithFundingPopover: false });
|
|
|
|
};
|
|
|
|
|
|
|
|
private handleChangeToAcceptWithFundingConfirm = () => {
|
|
|
|
if (!store.proposalDetail) return;
|
|
|
|
store.changeProposalToAcceptedWithFunding(store.proposalDetail.proposalId);
|
|
|
|
this.setState({ showChangeToAcceptedWithFundingPopover: false });
|
|
|
|
};
|
|
|
|
|
2019-01-09 10:23:08 -08:00
|
|
|
private getIdFromQuery = () => {
|
|
|
|
return Number(this.props.match.params.id);
|
|
|
|
};
|
|
|
|
|
|
|
|
private loadDetail = () => {
|
|
|
|
store.fetchProposalDetail(this.getIdFromQuery());
|
|
|
|
};
|
|
|
|
|
2019-03-06 12:25:58 -08:00
|
|
|
private handleCancelCancel = () => {
|
|
|
|
this.setState({ showCancelAndRefundPopover: false });
|
|
|
|
};
|
|
|
|
|
|
|
|
private handleConfirmCancel = () => {
|
2019-02-23 13:38:06 -08:00
|
|
|
if (!store.proposalDetail) return;
|
|
|
|
store.cancelProposal(store.proposalDetail.proposalId);
|
2019-03-06 12:25:58 -08:00
|
|
|
this.setState({ showCancelAndRefundPopover: false });
|
2019-02-23 13:38:06 -08:00
|
|
|
};
|
|
|
|
|
2019-10-16 20:43:20 -07:00
|
|
|
private handleApprove = (withFunding: boolean) => {
|
|
|
|
store.approveProposal(true, withFunding);
|
2019-01-09 10:23:08 -08:00
|
|
|
};
|
|
|
|
|
2019-02-25 08:41:00 -08:00
|
|
|
private handleReject = async (reason: string) => {
|
2019-10-16 20:43:20 -07:00
|
|
|
await store.approveProposal(false, false, reason);
|
2019-11-20 13:37:58 -08:00
|
|
|
message.info('Proposal changes requested');
|
2019-01-09 10:23:08 -08:00
|
|
|
};
|
2019-01-29 15:50:27 -08:00
|
|
|
|
2019-02-13 08:54:46 -08:00
|
|
|
private handlePaidMilestone = async () => {
|
|
|
|
const pid = store.proposalDetail!.proposalId;
|
|
|
|
const mid = store.proposalDetail!.currentMilestone!.id;
|
|
|
|
await store.markMilestonePaid(pid, mid, this.state.paidTxId);
|
|
|
|
message.success('Marked milestone paid.');
|
|
|
|
};
|
2019-01-09 10:23:08 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
const ProposalDetail = withRouter(view(ProposalDetailNaked));
|
|
|
|
export default ProposalDetail;
|