FE: rework milestones first pass
This commit is contained in:
parent
4e5c0eaea7
commit
ac5bef5c6f
|
@ -2,7 +2,7 @@ import lodash from 'lodash';
|
|||
import React from 'react';
|
||||
import moment from 'moment';
|
||||
import { Alert, Steps } from 'antd';
|
||||
import { Proposal, MILESTONE_STATE } from 'types';
|
||||
import { Proposal, Milestone, ProposalMilestone, MILESTONE_STAGE } from 'types';
|
||||
import UnitDisplay from 'components/UnitDisplay';
|
||||
import Loader from 'components/Loader';
|
||||
import { AppState } from 'store/reducers';
|
||||
|
@ -10,22 +10,24 @@ import { connect } from 'react-redux';
|
|||
import classnames from 'classnames';
|
||||
import './style.less';
|
||||
import Placeholder from 'components/Placeholder';
|
||||
import { AlertProps } from 'antd/lib/alert';
|
||||
import { StepProps } from 'antd/lib/steps';
|
||||
|
||||
const { WAITING, ACTIVE, PAID, REJECTED } = MILESTONE_STATE;
|
||||
// const { WAITING, ACTIVE, PAID, REJECTED } = MILESTONE_STATE;
|
||||
|
||||
enum STEP_STATUS {
|
||||
WAIT = 'wait',
|
||||
PROCESS = 'process',
|
||||
FINISH = 'finish',
|
||||
ERROR = 'error',
|
||||
}
|
||||
// enum STEP_STATUS {
|
||||
// WAIT = 'wait',
|
||||
// PROCESS = 'process',
|
||||
// FINISH = 'finish',
|
||||
// ERROR = 'error',
|
||||
// }
|
||||
|
||||
const milestoneStateToStepState = {
|
||||
[WAITING]: STEP_STATUS.WAIT,
|
||||
[ACTIVE]: STEP_STATUS.PROCESS,
|
||||
[PAID]: STEP_STATUS.FINISH,
|
||||
[REJECTED]: STEP_STATUS.ERROR,
|
||||
};
|
||||
// const milestoneStateToStepState = {
|
||||
// [WAITING]: STEP_STATUS.WAIT,
|
||||
// [ACTIVE]: STEP_STATUS.PROCESS,
|
||||
// [PAID]: STEP_STATUS.FINISH,
|
||||
// [REJECTED]: STEP_STATUS.ERROR,
|
||||
// };
|
||||
|
||||
interface OwnProps {
|
||||
proposal: Proposal;
|
||||
|
@ -87,107 +89,20 @@ class ProposalMilestones extends React.Component<Props, State> {
|
|||
return <Loader />;
|
||||
}
|
||||
const { milestones } = proposal;
|
||||
|
||||
const isTrustee = false; // TODO: Replace with being on the team
|
||||
const milestoneCount = milestones.length;
|
||||
|
||||
const milestoneSteps = milestones.map((milestone, i) => {
|
||||
const status =
|
||||
this.state.activeMilestoneIdx === i && milestone.state === WAITING
|
||||
? STEP_STATUS.PROCESS
|
||||
: milestoneStateToStepState[milestone.state];
|
||||
|
||||
const status: StepProps['status'] = 'wait';
|
||||
// this.state.activeMilestoneIdx === i && milestone.state === WAITING
|
||||
// ? STEP_STATUS.PROCESS
|
||||
// : milestoneStateToStepState[milestone.state];
|
||||
const className = this.state.step === i ? 'is-active' : 'is-inactive';
|
||||
const estimatedDate = moment(milestone.dateEstimated * 1000).format('MMMM YYYY');
|
||||
const reward = (
|
||||
<UnitDisplay value={milestone.amount} symbol="ZEC" displayShortBalance={4} />
|
||||
);
|
||||
const alertStyle = { width: 'fit-content', margin: '0 0 1rem 0' };
|
||||
|
||||
const stepProps = {
|
||||
title: <div ref={this.stepTitleRefs[i]}>{milestone.title}</div>,
|
||||
status,
|
||||
className,
|
||||
onClick: () => this.setState({ step: i }),
|
||||
};
|
||||
|
||||
let notification;
|
||||
|
||||
switch (milestone.state) {
|
||||
case PAID:
|
||||
notification = (
|
||||
<Alert
|
||||
type="success"
|
||||
message={
|
||||
<span>
|
||||
The team was awarded <strong>{reward}</strong>{' '}
|
||||
{milestone.immediatePayout
|
||||
? 'as an initial payout'
|
||||
: // TODO: Add property for payout date on milestones
|
||||
`on ${moment().format('MMM Do, YYYY')}`}
|
||||
.
|
||||
</span>
|
||||
}
|
||||
style={alertStyle}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
case ACTIVE:
|
||||
notification = (
|
||||
<Alert
|
||||
type="info"
|
||||
message={`
|
||||
The team has requested a payout for this milestone. It is
|
||||
currently under review.
|
||||
`}
|
||||
style={alertStyle}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
case REJECTED:
|
||||
notification = (
|
||||
<Alert
|
||||
type="warning"
|
||||
message={
|
||||
<span>
|
||||
Payout for this milestone was rejected on{' '}
|
||||
{/* TODO: add property for payout rejection date on milestones */}
|
||||
{moment().format('MMM Do, YYYY')}.{isTrustee ? ' You ' : ' The team '}{' '}
|
||||
can request another review for payout at any time.
|
||||
</span>
|
||||
}
|
||||
style={alertStyle}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
const statuses = (
|
||||
<div className="ProposalMilestones-milestone-status">
|
||||
{!milestone.immediatePayout && (
|
||||
<div>
|
||||
Estimate: <strong>{estimatedDate}</strong>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
Reward: <strong>{reward}</strong>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const content = (
|
||||
<div className="ProposalMilestones-milestone">
|
||||
<div className="ProposalMilestones-milestone-body">
|
||||
<div className="ProposalMilestones-milestone-description">
|
||||
<h3 className="ProposalMilestones-milestone-title">{milestone.title}</h3>
|
||||
{statuses}
|
||||
{notification}
|
||||
{milestone.content}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
return { key: i, stepProps, content };
|
||||
return { key: i, stepProps };
|
||||
});
|
||||
|
||||
const stepSize = milestoneCount > 5 ? 'small' : 'default';
|
||||
|
@ -198,7 +113,6 @@ class ProposalMilestones extends React.Component<Props, State> {
|
|||
className={classnames({
|
||||
['ProposalMilestones']: true,
|
||||
['do-titles-overflow']: this.state.doTitlesOverflow,
|
||||
[`is-count-${milestoneCount}`]: true,
|
||||
})}
|
||||
>
|
||||
{!!milestoneSteps.length ? (
|
||||
|
@ -208,7 +122,10 @@ class ProposalMilestones extends React.Component<Props, State> {
|
|||
<Steps.Step key={mss.key} {...mss.stepProps} />
|
||||
))}
|
||||
</Steps>
|
||||
{milestoneSteps[this.state.step].content}
|
||||
<Milestone
|
||||
{...proposal.milestones[this.state.step]}
|
||||
isTeamMember={proposal.isTeamMember || false}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<Placeholder
|
||||
|
@ -221,16 +138,17 @@ class ProposalMilestones extends React.Component<Props, State> {
|
|||
}
|
||||
|
||||
private getActiveMilestoneIdx = () => {
|
||||
const { milestones } = this.props.proposal;
|
||||
const activeMilestone =
|
||||
milestones.find(
|
||||
m =>
|
||||
m.state === WAITING ||
|
||||
m.state === ACTIVE ||
|
||||
(m.state === PAID && !m.isPaid) ||
|
||||
m.state === REJECTED,
|
||||
) || milestones[0];
|
||||
return milestones.indexOf(activeMilestone);
|
||||
return 0;
|
||||
// const { milestones } = this.props.proposal;
|
||||
// const activeMilestone =
|
||||
// milestones.find(
|
||||
// m =>
|
||||
// m.state === WAITING ||
|
||||
// m.state === ACTIVE ||
|
||||
// (m.state === PAID && !m.isPaid) ||
|
||||
// m.state === REJECTED,
|
||||
// ) || milestones[0];
|
||||
// return milestones.indexOf(activeMilestone);
|
||||
};
|
||||
|
||||
private updateDoTitlesOverflow = () => {
|
||||
|
@ -268,6 +186,81 @@ class ProposalMilestones extends React.Component<Props, State> {
|
|||
};
|
||||
}
|
||||
|
||||
const Milestone: React.SFC<ProposalMilestone & { isTeamMember: boolean }> = p => {
|
||||
const estimatedDate = moment(p.dateEstimated * 1000).format('MMMM YYYY');
|
||||
const reward = <UnitDisplay value={p.amount} symbol="ZEC" displayShortBalance={4} />;
|
||||
const fmtDate = (n: undefined | number) =>
|
||||
(n && moment(n * 1000).format('MMM Do, YYYY')) || undefined;
|
||||
const getAlertProps = {
|
||||
[MILESTONE_STAGE.IDLE]: () => null,
|
||||
[MILESTONE_STAGE.REQUESTED]: () => ({
|
||||
type: 'info',
|
||||
message: (
|
||||
<>
|
||||
The team has requested a payout for this milestone. It is currently under
|
||||
review.
|
||||
</>
|
||||
),
|
||||
}),
|
||||
[MILESTONE_STAGE.REJECTED]: () => ({
|
||||
type: 'warning',
|
||||
message: (
|
||||
<span>
|
||||
Payout for this milestone was rejected on {fmtDate(p.dateRejected)}.
|
||||
{p.isTeamMember ? ' You ' : ' The team '} can request another review for payout
|
||||
at any time.
|
||||
</span>
|
||||
),
|
||||
}),
|
||||
[MILESTONE_STAGE.ACCEPTED]: () => ({
|
||||
type: 'info',
|
||||
message: (
|
||||
<span>
|
||||
Payout for this milestone was accepted on {fmtDate(p.dateAccepted)}.
|
||||
<strong>{reward}</strong> will be sent to{' '}
|
||||
{p.isTeamMember ? ' you ' : ' the team '} soon.
|
||||
</span>
|
||||
),
|
||||
}),
|
||||
[MILESTONE_STAGE.PAID]: () => ({
|
||||
type: 'success',
|
||||
message: (
|
||||
<span>
|
||||
The team was awarded <strong>{reward}</strong>{' '}
|
||||
{p.immediatePayout && ` as an initial payout `} on ${fmtDate(p.datePaid)}
|
||||
`.
|
||||
</span>
|
||||
),
|
||||
}),
|
||||
} as { [key in MILESTONE_STAGE]: () => AlertProps | null };
|
||||
|
||||
const alertProps = getAlertProps[p.stage]();
|
||||
|
||||
return (
|
||||
<div className="ProposalMilestones-milestone">
|
||||
<div className="ProposalMilestones-milestone-body">
|
||||
<div className="ProposalMilestones-milestone-description">
|
||||
<h3 className="ProposalMilestones-milestone-title">{p.title}</h3>
|
||||
<div className="ProposalMilestones-milestone-status">
|
||||
{!p.immediatePayout && (
|
||||
<div>
|
||||
Estimate: <strong>{estimatedDate}</strong>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
Reward: <strong>{reward}</strong>
|
||||
</div>
|
||||
</div>
|
||||
{alertProps && (
|
||||
<Alert {...alertProps} className="ProposalMilestones-milestone-alert" />
|
||||
)}
|
||||
{p.content}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const ConnectedProposalMilestones = connect((state: AppState) => {
|
||||
console.warn('TODO - new redux accounts/user-role-for-proposal', state);
|
||||
return {
|
||||
|
|
|
@ -39,6 +39,11 @@
|
|||
margin-left: 0;
|
||||
}
|
||||
|
||||
&-alert {
|
||||
width: fit-content;
|
||||
margin: 0 0 1rem 0;
|
||||
}
|
||||
|
||||
&-title {
|
||||
display: none;
|
||||
white-space: nowrap;
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { ProposalDraft, CreateMilestone, STATUS } from 'types';
|
||||
import { ProposalDraft, CreateMilestone, STATUS, MILESTONE_STAGE } from 'types';
|
||||
import { User } from 'types';
|
||||
import { getAmountError, isValidAddress } from 'utils/validators';
|
||||
import { MILESTONE_STATE, Proposal } from 'types';
|
||||
import { Proposal } from 'types';
|
||||
import { Zat, toZat } from 'utils/units';
|
||||
import { ONE_DAY } from 'utils/time';
|
||||
import { PROPOSAL_CATEGORY } from 'api/constants';
|
||||
|
@ -199,9 +199,8 @@ export function makeProposalPreviewFromDraft(draft: ProposalDraft): Proposal {
|
|||
amount: toZat(target * (parseInt(m.payoutPercent, 10) / 100)),
|
||||
dateEstimated: m.dateEstimated,
|
||||
immediatePayout: m.immediatePayout,
|
||||
isPaid: false,
|
||||
payoutPercent: m.payoutPercent.toString(),
|
||||
state: MILESTONE_STATE.WAITING,
|
||||
stage: MILESTONE_STAGE.IDLE,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -14,6 +14,19 @@ import { getProposalPageSettings } from './selectors';
|
|||
|
||||
type GetState = () => AppState;
|
||||
|
||||
function addProposalUserRoles(p: Proposal, state: AppState) {
|
||||
if (state.auth.user) {
|
||||
const authUserId = state.auth.user.userid;
|
||||
// TODO: add arbiter roll
|
||||
// user.arbitratedProposals...
|
||||
console.warn('TODO: add user arbitration role to Proposal');
|
||||
if (p.team.find(t => t.userid === authUserId)) {
|
||||
p.isTeamMember = true;
|
||||
}
|
||||
}
|
||||
return p;
|
||||
}
|
||||
|
||||
// change page, sort, filter, search
|
||||
export function setProposalPage(pageParams: Partial<ProposalPageParams>) {
|
||||
return async (dispatch: Dispatch<any>, getState: GetState) => {
|
||||
|
@ -49,7 +62,7 @@ export function fetchProposals() {
|
|||
|
||||
export type TFetchProposal = typeof fetchProposal;
|
||||
export function fetchProposal(proposalId: Proposal['proposalId']) {
|
||||
return async (dispatch: Dispatch<any>) => {
|
||||
return async (dispatch: Dispatch<any>, getState: GetState) => {
|
||||
dispatch({
|
||||
type: types.PROPOSAL_DATA_PENDING,
|
||||
payload: { proposalId },
|
||||
|
@ -58,7 +71,7 @@ export function fetchProposal(proposalId: Proposal['proposalId']) {
|
|||
const proposal = (await getProposal(proposalId)).data;
|
||||
return dispatch({
|
||||
type: types.PROPOSAL_DATA_FULFILLED,
|
||||
payload: proposal,
|
||||
payload: addProposalUserRoles(proposal, getState()),
|
||||
});
|
||||
} catch (error) {
|
||||
dispatch({
|
||||
|
|
|
@ -6,7 +6,6 @@ import {
|
|||
PageParams,
|
||||
UserProposal,
|
||||
RFP,
|
||||
MILESTONE_STATE,
|
||||
ProposalPage,
|
||||
} from 'types';
|
||||
import { UserState } from 'modules/users/reducers';
|
||||
|
@ -88,16 +87,12 @@ export function formatProposalFromGet(p: any): Proposal {
|
|||
? 0
|
||||
: proposal.funded.div(proposal.target.divn(100)).toNumber();
|
||||
if (proposal.milestones) {
|
||||
proposal.milestones = proposal.milestones.map((m: any, index: number) => {
|
||||
return {
|
||||
...m,
|
||||
index,
|
||||
amount: proposal.target.mul(new BN(m.payoutPercent)).divn(100),
|
||||
// TODO: Get data from backend
|
||||
state: MILESTONE_STATE.WAITING,
|
||||
isPaid: false,
|
||||
};
|
||||
const msToFe = (m: any) => ({
|
||||
...m,
|
||||
amount: proposal.target.mul(new BN(m.payoutPercent)).divn(100),
|
||||
});
|
||||
proposal.milestones = proposal.milestones.map(msToFe);
|
||||
proposal.currentMilestone = msToFe(proposal.currentMilestone);
|
||||
}
|
||||
return proposal;
|
||||
}
|
||||
|
|
|
@ -5,18 +5,18 @@ import { Provider } from 'react-redux';
|
|||
import { configureStore } from 'store/configure';
|
||||
import { combineInitialState } from 'store/reducers';
|
||||
import Milestones from 'components/Proposal/Milestones';
|
||||
import { MILESTONE_STATE } from 'types';
|
||||
const { WAITING, ACTIVE, PAID, REJECTED } = MILESTONE_STATE;
|
||||
import { MILESTONE_STAGE } from 'types';
|
||||
const { IDLE, ACCEPTED, PAID, REJECTED } = MILESTONE_STAGE;
|
||||
|
||||
import 'styles/style.less';
|
||||
import 'components/Proposal/style.less';
|
||||
import 'components/Proposal/Governance/style.less';
|
||||
import { generateProposal } from './props';
|
||||
|
||||
const msWaiting = { state: WAITING, isPaid: false };
|
||||
const msPaid = { state: PAID, isPaid: true };
|
||||
const msActive = { state: ACTIVE, isPaid: false };
|
||||
const msRejected = { state: REJECTED, isPaid: false };
|
||||
const msWaiting = { stage: IDLE };
|
||||
const msPaid = { stage: PAID };
|
||||
const msActive = { stage: ACCEPTED };
|
||||
const msRejected = { stage: REJECTED };
|
||||
|
||||
const trustee = 'z123';
|
||||
const contributor = 'z456';
|
||||
|
@ -38,11 +38,7 @@ const cases: { [index: string]: any } = {
|
|||
['first - not paid']: generateProposal({
|
||||
amount: 5,
|
||||
funded: 5,
|
||||
milestoneOverrides: [
|
||||
{ state: PAID, isPaid: false },
|
||||
msWaiting,
|
||||
msWaiting,
|
||||
],
|
||||
milestoneOverrides: [{ stage: PAID }, msWaiting, msWaiting],
|
||||
}),
|
||||
|
||||
// trustee - second
|
||||
|
@ -59,20 +55,12 @@ const cases: { [index: string]: any } = {
|
|||
['second - not paid']: generateProposal({
|
||||
amount: 5,
|
||||
funded: 5,
|
||||
milestoneOverrides: [
|
||||
msPaid,
|
||||
{ state: PAID, isPaid: false },
|
||||
msWaiting,
|
||||
],
|
||||
milestoneOverrides: [msPaid, { stage: PAID }, msWaiting],
|
||||
}),
|
||||
['second - no vote']: generateProposal({
|
||||
amount: 5,
|
||||
funded: 5,
|
||||
milestoneOverrides: [
|
||||
msPaid,
|
||||
{ state: ACTIVE, isPaid: false },
|
||||
msWaiting,
|
||||
],
|
||||
milestoneOverrides: [msPaid, { stage: ACCEPTED }, msWaiting],
|
||||
contributorOverrides: [{ milestoneNoVotes: [false, true, false] }],
|
||||
}),
|
||||
['second - rejected']: generateProposal({
|
||||
|
@ -95,20 +83,12 @@ const cases: { [index: string]: any } = {
|
|||
['final - not paid']: generateProposal({
|
||||
amount: 5,
|
||||
funded: 5,
|
||||
milestoneOverrides: [
|
||||
msPaid,
|
||||
msPaid,
|
||||
{ state: PAID, isPaid: false },
|
||||
],
|
||||
milestoneOverrides: [msPaid, msPaid, { stage: PAID }],
|
||||
}),
|
||||
['final - no vote']: generateProposal({
|
||||
amount: 5,
|
||||
funded: 5,
|
||||
milestoneOverrides: [
|
||||
msPaid,
|
||||
msPaid,
|
||||
{ state: ACTIVE, isPaid: false },
|
||||
],
|
||||
milestoneOverrides: [msPaid, msPaid, { stage: ACCEPTED }],
|
||||
contributorOverrides: [{ milestoneNoVotes: [false, true, false] }],
|
||||
}),
|
||||
['final - rejected']: generateProposal({
|
||||
|
|
|
@ -1,11 +1,4 @@
|
|||
import {
|
||||
Contributor,
|
||||
Milestone,
|
||||
MILESTONE_STATE,
|
||||
Proposal,
|
||||
ProposalMilestone,
|
||||
STATUS,
|
||||
} from 'types';
|
||||
import { Contributor, MILESTONE_STAGE, Proposal, ProposalMilestone, STATUS } from 'types';
|
||||
import { PROPOSAL_CATEGORY } from 'api/constants';
|
||||
import BN from 'bn.js';
|
||||
import moment from 'moment';
|
||||
|
@ -40,7 +33,7 @@ export function generateProposal({
|
|||
funded?: number;
|
||||
created?: number;
|
||||
deadline?: number;
|
||||
milestoneOverrides?: Array<Partial<Milestone>>;
|
||||
milestoneOverrides?: Array<Partial<ProposalMilestone>>;
|
||||
contributorOverrides?: Array<Partial<Contributor>>;
|
||||
milestoneCount?: number;
|
||||
}) {
|
||||
|
@ -115,9 +108,8 @@ export function generateProposal({
|
|||
dateEstimated: moment().unix(),
|
||||
immediatePayout: true,
|
||||
index: 0,
|
||||
state: MILESTONE_STATE.WAITING,
|
||||
stage: MILESTONE_STAGE.IDLE,
|
||||
amount: amountBn,
|
||||
isPaid: false,
|
||||
payoutPercent: '33',
|
||||
};
|
||||
return { ...defaults, ...overrides };
|
||||
|
|
|
@ -7,13 +7,25 @@ export enum MILESTONE_STATE {
|
|||
PAID = 'PAID',
|
||||
}
|
||||
|
||||
// NOTE: sync with /backend/grand/utils/enums.py MilestoneStage
|
||||
export enum MILESTONE_STAGE {
|
||||
IDLE = 'IDLE',
|
||||
REQUESTED = 'REQUESTED',
|
||||
REJECTED = 'REJECTED',
|
||||
ACCEPTED = 'ACCEPTED',
|
||||
PAID = 'PAID',
|
||||
}
|
||||
|
||||
export interface Milestone {
|
||||
index: number;
|
||||
state: MILESTONE_STATE;
|
||||
stage: MILESTONE_STAGE;
|
||||
amount: Zat;
|
||||
isPaid: boolean;
|
||||
immediatePayout: boolean;
|
||||
dateEstimated: number;
|
||||
dateRequested?: number;
|
||||
dateRejected?: number;
|
||||
dateAccepted?: number;
|
||||
datePaid?: number;
|
||||
}
|
||||
|
||||
export interface ProposalMilestone extends Milestone {
|
||||
|
|
|
@ -47,8 +47,11 @@ export interface Proposal extends Omit<ProposalDraft, 'target' | 'invites'> {
|
|||
percentFunded: number;
|
||||
contributionMatching: number;
|
||||
milestones: ProposalMilestone[];
|
||||
currentMilestone?: ProposalMilestone;
|
||||
datePublished: number | null;
|
||||
dateApproved: number | null;
|
||||
isTeamMember?: boolean; // FE derived
|
||||
isArbiter?: boolean; // FE derived
|
||||
}
|
||||
|
||||
export interface TeamInviteWithProposal extends TeamInvite {
|
||||
|
|
Loading…
Reference in New Issue