2018-09-20 11:58:47 -07:00
|
|
|
|
import React from 'react';
|
|
|
|
|
import { connect } from 'react-redux';
|
2019-11-13 15:23:36 -08:00
|
|
|
|
import { Timeline } from 'antd';
|
2018-09-20 11:58:47 -07:00
|
|
|
|
import { getCreateErrors, KeyOfForm, FIELD_NAME_MAP } from 'modules/create/utils';
|
|
|
|
|
import Markdown from 'components/Markdown';
|
2018-11-14 08:43:00 -08:00
|
|
|
|
import UserAvatar from 'components/UserAvatar';
|
2018-09-20 11:58:47 -07:00
|
|
|
|
import { AppState } from 'store/reducers';
|
|
|
|
|
import { CREATE_STEP } from './index';
|
2018-11-14 08:43:00 -08:00
|
|
|
|
import { ProposalDraft } from 'types';
|
2019-12-03 16:02:39 -08:00
|
|
|
|
import { formatUsd } from 'utils/formatters';
|
2018-09-20 11:58:47 -07:00
|
|
|
|
import './Review.less';
|
|
|
|
|
|
|
|
|
|
interface OwnProps {
|
|
|
|
|
setStep(step: CREATE_STEP): void;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface StateProps {
|
2018-11-14 08:43:00 -08:00
|
|
|
|
form: ProposalDraft;
|
2018-09-20 11:58:47 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type Props = OwnProps & StateProps;
|
|
|
|
|
|
|
|
|
|
interface Field {
|
|
|
|
|
key: KeyOfForm;
|
|
|
|
|
content: React.ReactNode;
|
2018-10-19 15:03:37 -07:00
|
|
|
|
error: string | Falsy;
|
2019-03-06 12:25:58 -08:00
|
|
|
|
isHide?: boolean;
|
2018-09-20 11:58:47 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface Section {
|
|
|
|
|
step: CREATE_STEP;
|
|
|
|
|
name: string;
|
|
|
|
|
fields: Field[];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class CreateReview extends React.Component<Props> {
|
|
|
|
|
render() {
|
|
|
|
|
const { form } = this.props;
|
|
|
|
|
const errors = getCreateErrors(this.props.form);
|
|
|
|
|
const sections: Section[] = [
|
|
|
|
|
{
|
|
|
|
|
step: CREATE_STEP.BASICS,
|
|
|
|
|
name: 'Basics',
|
|
|
|
|
fields: [
|
|
|
|
|
{
|
|
|
|
|
key: 'title',
|
|
|
|
|
content: <h2 style={{ fontSize: '1.6rem', margin: 0 }}>{form.title}</h2>,
|
|
|
|
|
error: errors.title,
|
|
|
|
|
},
|
2019-03-06 12:25:58 -08:00
|
|
|
|
{
|
|
|
|
|
key: 'rfpOptIn',
|
|
|
|
|
content: <div>{form.rfpOptIn ? 'Accepted' : 'Declined'}</div>,
|
|
|
|
|
error: errors.rfpOptIn,
|
|
|
|
|
},
|
2018-09-20 11:58:47 -07:00
|
|
|
|
{
|
|
|
|
|
key: 'brief',
|
|
|
|
|
content: form.brief,
|
|
|
|
|
error: errors.brief,
|
|
|
|
|
},
|
|
|
|
|
{
|
2018-11-14 08:43:00 -08:00
|
|
|
|
key: 'target',
|
2019-12-03 16:02:39 -08:00
|
|
|
|
content: <div style={{ fontSize: '1.2rem' }}>{formatUsd(form.target)}</div>,
|
2018-11-14 08:43:00 -08:00
|
|
|
|
error: errors.target,
|
2018-09-20 11:58:47 -07:00
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
},
|
2018-09-27 13:25:49 -07:00
|
|
|
|
{
|
|
|
|
|
step: CREATE_STEP.TEAM,
|
|
|
|
|
name: 'Team',
|
|
|
|
|
fields: [
|
|
|
|
|
{
|
|
|
|
|
key: 'team',
|
2018-11-16 08:57:03 -08:00
|
|
|
|
content: <ReviewTeam team={form.team} invites={form.invites} />,
|
2018-09-27 13:25:49 -07:00
|
|
|
|
error: errors.team && errors.team.join(' '),
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
},
|
2018-09-20 11:58:47 -07:00
|
|
|
|
{
|
|
|
|
|
step: CREATE_STEP.DETAILS,
|
|
|
|
|
name: 'Details',
|
|
|
|
|
fields: [
|
|
|
|
|
{
|
2018-11-14 09:59:48 -08:00
|
|
|
|
key: 'content',
|
|
|
|
|
content: <Markdown source={form.content} />,
|
2018-11-14 13:21:41 -08:00
|
|
|
|
error: errors.content,
|
2018-09-20 11:58:47 -07:00
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
step: CREATE_STEP.MILESTONES,
|
|
|
|
|
name: 'Milestones',
|
|
|
|
|
fields: [
|
|
|
|
|
{
|
|
|
|
|
key: 'milestones',
|
|
|
|
|
content: <ReviewMilestones milestones={form.milestones} />,
|
|
|
|
|
error: errors.milestones && errors.milestones.join(' '),
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
{
|
2018-12-21 10:27:39 -08:00
|
|
|
|
step: CREATE_STEP.PAYMENT,
|
2019-03-04 12:02:51 -08:00
|
|
|
|
name: 'Payment',
|
2018-09-20 11:58:47 -07:00
|
|
|
|
fields: [
|
|
|
|
|
{
|
2018-11-14 08:43:00 -08:00
|
|
|
|
key: 'payoutAddress',
|
|
|
|
|
content: <code>{form.payoutAddress}</code>,
|
|
|
|
|
error: errors.payoutAddress,
|
2018-09-20 11:58:47 -07:00
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
},
|
2019-11-20 13:37:26 -08:00
|
|
|
|
{
|
2019-12-02 08:40:24 -08:00
|
|
|
|
step: CREATE_STEP.PAYMENT,
|
2019-11-20 13:37:26 -08:00
|
|
|
|
name: 'Tipping',
|
|
|
|
|
fields: [
|
|
|
|
|
{
|
|
|
|
|
key: 'tipJarAddress',
|
|
|
|
|
content: <code>{form.tipJarAddress}</code>,
|
|
|
|
|
error: errors.tipJarAddress,
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
},
|
2018-09-20 11:58:47 -07:00
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="CreateReview">
|
CCRs (#86)
* CCRs API / Models boilerplate
* start on frontend
* backendy things
* Create CCR redux module, integrate API endpoints, create types
* Fix/Cleanup API
* Wire up CreateRequestDraftList
* bounty->target
* Add 'Create Request Flow' MVP
* cleanup
* Tweak filenames
* Simplify migrations
* fix migrations
* CCR Staking MVP
* tslint
* Get Pending Requests into Profile
* Remove staking requirement
* more staking related removals
* MVP Admin integration
* Make RFP when CCR is accepted
* Add pagination to CCRs in Admin
Improve styles for Proposals
* Hookup notifications
Adjust copy
* Simplify ccr->rfp relationship
Add admin approval email
Fixup copy
* Show Message on RFP Detail
Make Header CTAs change based on draft status
Adjust proposal card style
* Bugfix: Show header for non signed in users
* Add 'create a request' to intro
* Profile Created CCRs
RFP CCR attribution
* ignore
* CCR Price in USD (#85)
* init profile tipjar backend
* init profile tipjar frontend
* fix lint
* implement tip jar block
* fix wrapping, hide tip block on self
* init backend proposal tipjar
* init frontend proposal tipjar
* add hide title, fix bug
* uncomment rate limit
* rename vars, use null check
* allow address and view key to be unset
* add api tests
* fix tsc errors
* fix lint
* fix CopyInput styling
* fix migrations
* hide tipping in proposal if address not set
* add tip address to create flow
* redesign campaign block
* fix typo
* init backend changes
* init admin changes
* init frontend changes
* fix backend tests
* update campaign block
* be - init rfp usd changes
* admin - init rfp usd changes
* fe - fully adapt api util functions to usd
* fe - init rfp usd changes
* adapt profile created to usd
* misc usd changes
* add tip jar to dedicated card
* fix tipjar bug
* use zf light logo
* switch to zf grants logo
* hide profile tip jar if address not set
* add comment, run prettier
* conditionally add info icon and tooltip to funding line
* admin - disallow decimals in RFPs
* fe - cover usd string edge case
* add Usd as rfp bounty type
* fix migration order
* fix email bug
* adapt CCRs to USD
* implement CCR preview
* fix tsc
* Copy Updates and UX Tweaks (#87)
* Add default structure to proposal content
* Landing page copy
* Hide contributors tab for v2 proposals
* Minor UX tweaks for Liking/Following/Tipping
* Copy for Tipping Tooltip, proposal explainer for review, and milestone day estimate notice.
* Fix header styles bug and remove commented out styles.
* Revert "like" / "unfollow" hyphenication
* Comment out unused tests related to staking
Increase PROPOSAL_TARGET_MAX in .env.example
* Comment out ccr approval email send until ready
* Adjust styles, copy.
* fix proposal prune test (#88)
* fix USD display in preview, fix non-unique key (#90)
* Pre-stepper explainer for CCRs.
* Tweak styles
* Default content for CCRs
* fix tsc
* CCR approval and rejection emails
* add back admin_approval_ccr email templates
* Link ccr author name to profile in RFPs
* copy tweaks
* copy tweak
* hookup mangle user command
* Fix/add endif in jinja
* fix tests
* review
* fix review
2019-12-05 17:01:02 -08:00
|
|
|
|
{sections.map((s, i) => (
|
|
|
|
|
<div className="CreateReview-section" key={`${s.step}${i}`}>
|
2019-03-06 12:25:58 -08:00
|
|
|
|
{s.fields.map(
|
|
|
|
|
f =>
|
|
|
|
|
!f.isHide && (
|
|
|
|
|
<div className="ReviewField" key={f.key}>
|
|
|
|
|
<div className="ReviewField-label">
|
|
|
|
|
{FIELD_NAME_MAP[f.key]}
|
|
|
|
|
{f.error && (
|
|
|
|
|
<div className="ReviewField-label-error">{f.error}</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
<div className="ReviewField-content">
|
|
|
|
|
{this.isEmpty(form[f.key]) ? (
|
|
|
|
|
<div className="ReviewField-content-empty">N/A</div>
|
|
|
|
|
) : (
|
|
|
|
|
f.content
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
),
|
|
|
|
|
)}
|
2018-09-20 11:58:47 -07:00
|
|
|
|
<div className="ReviewField">
|
|
|
|
|
<div className="ReviewField-label" />
|
|
|
|
|
<div className="ReviewField-content">
|
|
|
|
|
<button
|
|
|
|
|
className="ReviewField-content-edit"
|
|
|
|
|
onClick={() => this.setStep(s.step)}
|
|
|
|
|
>
|
|
|
|
|
Edit {s.name}
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private setStep = (step: CREATE_STEP) => {
|
|
|
|
|
this.props.setStep(step);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
private isEmpty(value: any) {
|
2019-03-06 12:25:58 -08:00
|
|
|
|
if (typeof value === 'boolean') {
|
|
|
|
|
return false; // defined booleans are never empty
|
|
|
|
|
}
|
2018-09-20 11:58:47 -07:00
|
|
|
|
return !value || value.length === 0;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default connect<StateProps, {}, OwnProps, AppState>(state => ({
|
2018-11-14 08:43:00 -08:00
|
|
|
|
form: state.create.form as ProposalDraft,
|
2018-09-20 11:58:47 -07:00
|
|
|
|
}))(CreateReview);
|
|
|
|
|
|
2018-09-27 13:25:49 -07:00
|
|
|
|
const ReviewMilestones = ({
|
|
|
|
|
milestones,
|
|
|
|
|
}: {
|
2018-11-14 08:43:00 -08:00
|
|
|
|
milestones: ProposalDraft['milestones'];
|
2018-09-27 13:25:49 -07:00
|
|
|
|
}) => (
|
2018-09-20 11:58:47 -07:00
|
|
|
|
<Timeline>
|
|
|
|
|
{milestones.map(m => (
|
2018-10-09 12:30:09 -07:00
|
|
|
|
<Timeline.Item key={m.title}>
|
2018-09-20 11:58:47 -07:00
|
|
|
|
<div className="ReviewMilestone">
|
2019-02-19 13:42:40 -08:00
|
|
|
|
<div className="ReviewMilestone-title">{m.title || <em>No title</em>}</div>
|
2018-09-20 11:58:47 -07:00
|
|
|
|
<div className="ReviewMilestone-info">
|
2019-11-13 14:38:17 -08:00
|
|
|
|
{m.immediatePayout || !m.daysEstimated ? '0' : m.daysEstimated} days
|
2018-09-20 11:58:47 -07:00
|
|
|
|
{' – '}
|
2019-11-13 14:38:17 -08:00
|
|
|
|
{m.payoutPercent || '0'}% of funds
|
2018-09-20 11:58:47 -07:00
|
|
|
|
</div>
|
2019-02-19 13:42:40 -08:00
|
|
|
|
<div className="ReviewMilestone-description">
|
|
|
|
|
{m.content || <em>No description</em>}
|
|
|
|
|
</div>
|
2018-09-20 11:58:47 -07:00
|
|
|
|
</div>
|
|
|
|
|
</Timeline.Item>
|
|
|
|
|
))}
|
|
|
|
|
</Timeline>
|
|
|
|
|
);
|
2018-09-27 13:25:49 -07:00
|
|
|
|
|
2018-11-16 08:57:03 -08:00
|
|
|
|
const ReviewTeam: React.SFC<{
|
|
|
|
|
team: ProposalDraft['team'];
|
|
|
|
|
invites: ProposalDraft['invites'];
|
2019-01-15 11:13:57 -08:00
|
|
|
|
}> = ({ team, invites }) => {
|
|
|
|
|
const pendingInvites = invites.filter(inv => inv.accepted === null).length;
|
|
|
|
|
return (
|
|
|
|
|
<div className="ReviewTeam">
|
|
|
|
|
{team.map((u, idx) => (
|
|
|
|
|
<div className="ReviewTeam-member" key={idx}>
|
|
|
|
|
<UserAvatar className="ReviewTeam-member-avatar" user={u} />
|
|
|
|
|
<div className="ReviewTeam-member-info">
|
|
|
|
|
<div className="ReviewTeam-member-info-name">{u.displayName}</div>
|
|
|
|
|
<div className="ReviewTeam-member-info-title">{u.title}</div>
|
|
|
|
|
</div>
|
2018-09-27 13:25:49 -07:00
|
|
|
|
</div>
|
2019-01-15 11:13:57 -08:00
|
|
|
|
))}
|
|
|
|
|
{!!pendingInvites && (
|
|
|
|
|
<div className="ReviewTeam-invites">+ {pendingInvites} invite(s) pending</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
};
|