Merge pull request #236 from grant-project/proposal-unlink

Proposal RFP unlinking
This commit is contained in:
Daniel Ternyak 2019-02-19 16:44:40 -06:00 committed by GitHub
commit 95d57983e3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 137 additions and 24 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

@ -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

@ -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 {