Merge branch 'develop' into admin-users

This commit is contained in:
Aaron 2019-02-20 11:22:42 -06:00
commit 135ff9fff3
No known key found for this signature in database
GPG Key ID: 3B5B7597106F0A0E
11 changed files with 195 additions and 53 deletions

View File

@ -239,6 +239,16 @@ def update_proposal(milestones, proposal_id, **kwargs):
return proposal_schema.dump(g.current_proposal), 200
@blueprint.route("/<proposal_id>/rfp", methods=["DELETE"])
@requires_team_member_auth
@endpoint.api()
def unlink_proposal_from_rfp(proposal_id):
g.current_proposal.rfp_id = None
db.session.add(g.current_proposal)
db.session.commit()
return proposal_schema.dump(g.current_proposal), 200
@blueprint.route("/<proposal_id>", methods=["DELETE"])
@requires_team_member_auth
@endpoint.api()

View File

@ -229,6 +229,10 @@ export async function putProposalPublish(
});
}
export async function deleteProposalRFPLink(proposalId: number): Promise<any> {
return axios.delete(`/api/v1/proposals/${proposalId}/rfp`);
}
export async function requestProposalPayout(
proposalId: number,
milestoneId: number,

View File

@ -47,13 +47,11 @@ export default class PaymentInfo extends React.Component<Props, State> {
return (
<Form className="PaymentInfo" layout="vertical">
<div className="PaymentInfo-text">
{text || (
<>
Thank you for contributing! Just send using whichever method works best for
you, and we'll let you know when your contribution has been confirmed.
</>
)}
{/* TODO: Help / FAQ page for sending */} Need help sending? <a>Click here</a>.
{text ||
`
Thank you for contributing! Just send using whichever method works best for
you, and we'll let you know when your contribution has been confirmed.
`}
</div>
<Radio.Group

View File

@ -1,11 +1,31 @@
import React from 'react';
import { Input, Form, Icon, Select, Alert } from 'antd';
import { connect } from 'react-redux';
import { Input, Form, Icon, Select, Alert, Popconfirm, message } from 'antd';
import { SelectValue } from 'antd/lib/select';
import { PROPOSAL_CATEGORY, CATEGORY_UI } from 'api/constants';
import { ProposalDraft, RFP } from 'types';
import { getCreateErrors } from 'modules/create/utils';
import { typedKeys } from 'utils/ts';
import { Link } from 'react-router-dom';
import { unlinkProposalRFP } from 'modules/create/actions';
import { AppState } from 'store/reducers';
interface OwnProps {
proposalId: number;
initialState?: Partial<State>;
updateForm(form: Partial<ProposalDraft>): void;
}
interface StateProps {
isUnlinkingProposalRFP: AppState['create']['isUnlinkingProposalRFP'];
unlinkProposalRFPError: AppState['create']['unlinkProposalRFPError'];
}
interface DispatchProps {
unlinkProposalRFP: typeof unlinkProposalRFP;
}
type Props = OwnProps & StateProps & DispatchProps;
interface State extends Partial<ProposalDraft> {
title: string;
@ -15,12 +35,7 @@ interface State extends Partial<ProposalDraft> {
rfp?: RFP;
}
interface Props {
initialState?: Partial<State>;
updateForm(form: Partial<ProposalDraft>): void;
}
export default class CreateFlowBasics extends React.Component<Props, State> {
class CreateFlowBasics extends React.Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
@ -32,22 +47,22 @@ export default class CreateFlowBasics extends React.Component<Props, State> {
};
}
handleInputChange = (
event: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>,
) => {
const { value, name } = event.currentTarget;
this.setState({ [name]: value } as any, () => {
this.props.updateForm(this.state);
});
};
handleCategoryChange = (value: SelectValue) => {
this.setState({ category: value as PROPOSAL_CATEGORY }, () => {
this.props.updateForm(this.state);
});
};
componentDidUpdate(prevProps: Props) {
const { unlinkProposalRFPError, isUnlinkingProposalRFP } = this.props;
if (
unlinkProposalRFPError &&
unlinkProposalRFPError !== prevProps.unlinkProposalRFPError
) {
console.error('Failed to unlink request:', unlinkProposalRFPError);
message.error('Failed to unlink request');
} else if (!isUnlinkingProposalRFP && prevProps.isUnlinkingProposalRFP) {
this.setState({ rfp: undefined });
message.success('Unlinked proposal from request');
}
}
render() {
const { isUnlinkingProposalRFP } = this.props;
const { title, brief, category, target, rfp } = this.state;
const errors = getCreateErrors(this.state, true);
@ -63,8 +78,15 @@ export default class CreateFlowBasics extends React.Component<Props, State> {
<Link to={`/requests/${rfp.id}`} target="_blank">
{rfp.title}
</Link>
. If you didnt mean to do this, you can delete this proposal and create a
new one.
. If you didnt mean to do this, or want to unlink it,{' '}
<Popconfirm
title="Are you sure? This cannot be undone."
onConfirm={this.unlinkRfp}
okButtonProps={{ loading: isUnlinkingProposalRFP }}
>
<a>click here</a>
</Popconfirm>{' '}
to do so.
</>
}
style={{ marginBottom: '2rem' }}
@ -138,4 +160,31 @@ export default class CreateFlowBasics extends React.Component<Props, State> {
</Form>
);
}
private handleInputChange = (
event: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>,
) => {
const { value, name } = event.currentTarget;
this.setState({ [name]: value } as any, () => {
this.props.updateForm(this.state);
});
};
private handleCategoryChange = (value: SelectValue) => {
this.setState({ category: value as PROPOSAL_CATEGORY }, () => {
this.props.updateForm(this.state);
});
};
private unlinkRfp = () => {
this.props.unlinkProposalRFP(this.props.proposalId);
};
}
export default connect<StateProps, DispatchProps, OwnProps, AppState>(
state => ({
isUnlinkingProposalRFP: state.create.isUnlinkingProposalRFP,
unlinkProposalRFPError: state.create.unlinkProposalRFPError,
}),
{ unlinkProposalRFP },
)(CreateFlowBasics);

View File

@ -49,7 +49,6 @@ interface State {
isBodyOverflowing: boolean;
isUpdateOpen: boolean;
isCancelOpen: boolean;
bodyId: string;
}
export class ProposalDetail extends React.Component<Props, State> {
@ -58,9 +57,10 @@ export class ProposalDetail extends React.Component<Props, State> {
isBodyOverflowing: false,
isUpdateOpen: false,
isCancelOpen: false,
bodyId: `body-${Math.floor(Math.random() * 1000000)}`,
};
bodyEl: HTMLElement | null = null;
componentDidMount() {
// always refresh from server
this.props.fetchProposal(this.props.proposalId);
@ -87,13 +87,7 @@ export class ProposalDetail extends React.Component<Props, State> {
render() {
const { user, detail: proposal, isPreview, detailError } = this.props;
const {
isBodyExpanded,
isBodyOverflowing,
isCancelOpen,
isUpdateOpen,
bodyId,
} = this.state;
const { isBodyExpanded, isBodyOverflowing, isCancelOpen, isUpdateOpen } = this.state;
const showExpand = !isBodyExpanded && isBodyOverflowing;
const wrongProposal = proposal && proposal.proposalId !== this.props.proposalId;
@ -209,7 +203,7 @@ export class ProposalDetail extends React.Component<Props, State> {
</h1>
<div className="Proposal-top-main-block" style={{ flexGrow: 1 }}>
<div
id={bodyId}
ref={el => (this.bodyEl = el)}
className={classnames({
['Proposal-top-main-block-bodyText']: true,
['is-expanded']: isBodyExpanded,
@ -291,20 +285,17 @@ export class ProposalDetail extends React.Component<Props, State> {
};
private checkBodyOverflow = () => {
const { isBodyExpanded, bodyId, isBodyOverflowing } = this.state;
if (isBodyExpanded) {
const { isBodyExpanded, isBodyOverflowing } = this.state;
if (isBodyExpanded || !this.bodyEl) {
return;
}
// Use id instead of ref because styled component ref doesn't return html element
const bodyEl = document.getElementById(bodyId);
if (!bodyEl) {
return;
}
if (isBodyOverflowing && bodyEl.scrollHeight <= bodyEl.clientHeight) {
if (isBodyOverflowing && this.bodyEl.scrollHeight <= this.bodyEl.clientHeight) {
this.setState({ isBodyOverflowing: false });
} else if (!isBodyOverflowing && bodyEl.scrollHeight > bodyEl.clientHeight) {
} else if (
!isBodyOverflowing &&
this.bodyEl.scrollHeight > this.bodyEl.clientHeight
) {
this.setState({ isBodyOverflowing: true });
}
};

View File

@ -23,11 +23,22 @@
}
}
&-brief {
font-size: 1rem;
margin-bottom: 1.75rem;
text-align: center;
}
&-tags {
text-align: center;
margin-bottom: 1.75rem;
}
&-title {
font-size: 2.4rem;
text-align: center;
font-weight: bold;
margin-bottom: 1.75rem;
margin-bottom: 1rem;
}
&-content {

View File

@ -2,7 +2,7 @@ import React from 'react';
import moment from 'moment';
import { connect } from 'react-redux';
import { Link } from 'react-router-dom';
import { Icon, Button, Affix } from 'antd';
import { Icon, Button, Affix, Tag } from 'antd';
import ExceptionPage from 'components/ExceptionPage';
import { fetchRfp } from 'modules/rfps/actions';
import { getRfp } from 'modules/rfps/selectors';
@ -47,17 +47,40 @@ class RFPDetail extends React.Component<Props> {
}
}
const tags = [];
if (rfp.matching) {
tags.push(
<Tag key="matching" color="#1890ff">
x2 matching
</Tag>,
);
}
if (rfp.bounty) {
tags.push(
<Tag key="bounty" color="#CF8A00">
<UnitDisplay value={rfp.bounty} symbol="ZEC" /> bounty
</Tag>,
);
}
return (
<div className="RFPDetail">
<div className="RFPDetail-top">
<Link className="RFPDetail-top-back" to="/requests">
<Icon type="arrow-left" /> Back to Requests
</Link>
<div className="RFPDetail-top-date">
Opened {moment(rfp.dateOpened * 1000).format('LL')}
</div>
</div>
<h1 className="RFPDetail-title">{rfp.title}</h1>
<div className="RFPDetail-tags">{tags}</div>
<p className="RFPDetail-brief">{rfp.brief}</p>
<Markdown className="RFPDetail-content" source={rfp.content} />
<div className="RFPDetail-rules">
<ul>

View File

@ -1,7 +1,11 @@
import { Dispatch } from 'redux';
import { ProposalDraft } from 'types';
import types, { CreateDraftOptions } from './types';
import { putProposal, putProposalSubmitForApproval } from 'api/api';
import {
putProposal,
putProposalSubmitForApproval,
deleteProposalRFPLink,
} from 'api/api';
export function initializeForm(proposalId: number) {
return {
@ -68,3 +72,20 @@ export function submitProposal(form: ProposalDraft) {
}
};
}
export function unlinkProposalRFP(proposalId: number) {
return async (dispatch: Dispatch<any>) => {
dispatch({ type: types.UNLINK_PROPOSAL_RFP_PENDING });
try {
await deleteProposalRFPLink(proposalId);
dispatch({ type: types.UNLINK_PROPOSAL_RFP_FULFILLED });
dispatch(fetchDrafts());
} catch (err) {
dispatch({
type: types.UNLINK_PROPOSAL_RFP_REJECTED,
payload: err.message || err.toString(),
error: true,
});
}
};
}

View File

@ -28,6 +28,9 @@ export interface CreateState {
publishedProposal: Proposal | null;
isPublishing: boolean;
publishError: string | null;
isUnlinkingProposalRFP: boolean;
unlinkProposalRFPError: string | null;
}
export const INITIAL_STATE: CreateState = {
@ -57,6 +60,9 @@ export const INITIAL_STATE: CreateState = {
publishedProposal: null,
isPublishing: false,
publishError: null,
isUnlinkingProposalRFP: false,
unlinkProposalRFPError: null,
};
export default function createReducer(
@ -190,6 +196,24 @@ export default function createReducer(
submitError: action.payload,
isSubmitting: false,
};
case types.UNLINK_PROPOSAL_RFP_PENDING:
return {
...state,
isUnlinkingProposalRFP: true,
unlinkProposalRFPError: null,
};
case types.UNLINK_PROPOSAL_RFP_FULFILLED:
return {
...state,
isUnlinkingProposalRFP: false,
};
case types.UNLINK_PROPOSAL_RFP_REJECTED:
return {
...state,
isUnlinkingProposalRFP: false,
unlinkProposalRFPError: action.payload,
};
}
return state;
}

View File

@ -32,6 +32,11 @@ enum CreateTypes {
SUBMIT_PROPOSAL_PENDING = 'SUBMIT_PROPOSAL_PENDING',
SUBMIT_PROPOSAL_FULFILLED = 'SUBMIT_PROPOSAL_FULFILLED',
SUBMIT_PROPOSAL_REJECTED = 'SUBMIT_PROPOSAL_REJECTED',
UNLINK_PROPOSAL_RFP = 'UNLINK_PROPOSAL_RFP',
UNLINK_PROPOSAL_RFP_PENDING = 'UNLINK_PROPOSAL_RFP_PENDING',
UNLINK_PROPOSAL_RFP_FULFILLED = 'UNLINK_PROPOSAL_RFP_FULFILLED',
UNLINK_PROPOSAL_RFP_REJECTED = 'UNLINK_PROPOSAL_RFP_REJECTED',
}
export interface CreateDraftOptions {

View File

@ -145,6 +145,12 @@ export function massageSerializedState(state: AppState) {
(state.proposal.detail.funded as any) as string,
16,
);
if (state.proposal.detail.rfp && state.proposal.detail.rfp.bounty) {
state.proposal.detail.rfp.bounty = new BN(
(state.proposal.detail.rfp.bounty as any) as string,
16,
);
}
}
// proposals
state.proposal.page.items = state.proposal.page.items.map(p => ({