From 5163d87172dd815e4ef270e1d9e33a7244e4e6d6 Mon Sep 17 00:00:00 2001 From: Will O'Beirne Date: Tue, 19 Feb 2019 14:48:51 -0500 Subject: [PATCH] Add link for unlinking proposals from requests. --- backend/grant/proposal/views.py | 10 ++ frontend/client/api/api.ts | 4 + .../client/components/CreateFlow/Basics.tsx | 95 ++++++++++++++----- frontend/client/modules/create/actions.ts | 23 ++++- frontend/client/modules/create/reducers.ts | 24 +++++ frontend/client/modules/create/types.ts | 5 + 6 files changed, 137 insertions(+), 24 deletions(-) diff --git a/backend/grant/proposal/views.py b/backend/grant/proposal/views.py index 1075d0e8..356b9b36 100644 --- a/backend/grant/proposal/views.py +++ b/backend/grant/proposal/views.py @@ -239,6 +239,16 @@ def update_proposal(milestones, proposal_id, **kwargs): return proposal_schema.dump(g.current_proposal), 200 +@blueprint.route("//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("/", methods=["DELETE"]) @requires_team_member_auth @endpoint.api() diff --git a/frontend/client/api/api.ts b/frontend/client/api/api.ts index 45320df1..acfbe1ca 100644 --- a/frontend/client/api/api.ts +++ b/frontend/client/api/api.ts @@ -229,6 +229,10 @@ export async function putProposalPublish( }); } +export async function deleteProposalRFPLink(proposalId: number): Promise { + return axios.delete(`/api/v1/proposals/${proposalId}/rfp`); +} + export async function requestProposalPayout( proposalId: number, milestoneId: number, diff --git a/frontend/client/components/CreateFlow/Basics.tsx b/frontend/client/components/CreateFlow/Basics.tsx index f88e5ed0..a602c502 100644 --- a/frontend/client/components/CreateFlow/Basics.tsx +++ b/frontend/client/components/CreateFlow/Basics.tsx @@ -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; + updateForm(form: Partial): 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 { title: string; @@ -15,12 +35,7 @@ interface State extends Partial { rfp?: RFP; } -interface Props { - initialState?: Partial; - updateForm(form: Partial): void; -} - -export default class CreateFlowBasics extends React.Component { +class CreateFlowBasics extends React.Component { constructor(props: Props) { super(props); this.state = { @@ -32,22 +47,22 @@ export default class CreateFlowBasics extends React.Component { }; } - handleInputChange = ( - event: React.ChangeEvent, - ) => { - 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 { {rfp.title} - . If you didn’t mean to do this, you can delete this proposal and create a - new one. + . If you didn’t mean to do this, or want to unlink it,{' '} + + click here + {' '} + to do so. } style={{ marginBottom: '2rem' }} @@ -138,4 +160,31 @@ export default class CreateFlowBasics extends React.Component { ); } + + private handleInputChange = ( + event: React.ChangeEvent, + ) => { + 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( + state => ({ + isUnlinkingProposalRFP: state.create.isUnlinkingProposalRFP, + unlinkProposalRFPError: state.create.unlinkProposalRFPError, + }), + { unlinkProposalRFP }, +)(CreateFlowBasics); diff --git a/frontend/client/modules/create/actions.ts b/frontend/client/modules/create/actions.ts index 94878d6f..a34e4d77 100644 --- a/frontend/client/modules/create/actions.ts +++ b/frontend/client/modules/create/actions.ts @@ -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) => { + 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, + }); + } + }; +} diff --git a/frontend/client/modules/create/reducers.ts b/frontend/client/modules/create/reducers.ts index 4c1cc1a2..a670c1c2 100644 --- a/frontend/client/modules/create/reducers.ts +++ b/frontend/client/modules/create/reducers.ts @@ -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; } diff --git a/frontend/client/modules/create/types.ts b/frontend/client/modules/create/types.ts index 835e1f23..72b63563 100644 --- a/frontend/client/modules/create/types.ts +++ b/frontend/client/modules/create/types.ts @@ -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 {