Proposal updates (#171)
* Add models & endpoints * Implement proposal updates. * Reset state on close. Add missing key. Prompt on close if they will lose stuff. * Dont warn if they submitted.
This commit is contained in:
parent
cdc3ea0107
commit
118d7b645e
|
@ -27,6 +27,22 @@ proposal_team = db.Table(
|
|||
db.Column('proposal_id', db.Integer, db.ForeignKey('proposal.id'))
|
||||
)
|
||||
|
||||
class ProposalUpdate(db.Model):
|
||||
__tablename__ = "proposal_update"
|
||||
|
||||
id = db.Column(db.Integer(), primary_key=True)
|
||||
date_created = db.Column(db.DateTime)
|
||||
|
||||
proposal_id = db.Column(db.Integer, db.ForeignKey("proposal.id"), nullable=False)
|
||||
title = db.Column(db.String(255), nullable=False)
|
||||
content = db.Column(db.Text, nullable=False)
|
||||
|
||||
def __init__(self, proposal_id: int, title: str, content: str):
|
||||
self.proposal_id = proposal_id
|
||||
self.title = title
|
||||
self.content = content
|
||||
self.date_created = datetime.datetime.now()
|
||||
|
||||
|
||||
class Proposal(db.Model):
|
||||
__tablename__ = "proposal"
|
||||
|
@ -42,6 +58,7 @@ class Proposal(db.Model):
|
|||
|
||||
team = db.relationship("User", secondary=proposal_team)
|
||||
comments = db.relationship(Comment, backref="proposal", lazy=True)
|
||||
updates = db.relationship(ProposalUpdate, backref="proposal", lazy=True)
|
||||
milestones = db.relationship("Milestone", backref="proposal", lazy=True)
|
||||
|
||||
def __init__(
|
||||
|
@ -90,6 +107,7 @@ class ProposalSchema(ma.Schema):
|
|||
"proposal_id",
|
||||
"body",
|
||||
"comments",
|
||||
"updates",
|
||||
"milestones",
|
||||
"category",
|
||||
"team"
|
||||
|
@ -100,6 +118,7 @@ class ProposalSchema(ma.Schema):
|
|||
body = ma.Method("get_body")
|
||||
|
||||
comments = ma.Nested("CommentSchema", many=True)
|
||||
updates = ma.Nested("ProposalUpdateSchema", many=True)
|
||||
team = ma.Nested("UserSchema", many=True)
|
||||
milestones = ma.Nested("MilestoneSchema", many=True)
|
||||
|
||||
|
@ -115,3 +134,28 @@ class ProposalSchema(ma.Schema):
|
|||
|
||||
proposal_schema = ProposalSchema()
|
||||
proposals_schema = ProposalSchema(many=True)
|
||||
|
||||
|
||||
class ProposalUpdateSchema(ma.Schema):
|
||||
class Meta:
|
||||
model = ProposalUpdate
|
||||
# Fields to expose
|
||||
fields = (
|
||||
"date_created",
|
||||
"proposal_id",
|
||||
"title",
|
||||
"content"
|
||||
)
|
||||
|
||||
date_created = ma.Method("get_date_created")
|
||||
proposal_id = ma.Method("get_proposal_id")
|
||||
|
||||
def get_proposal_id(self, obj):
|
||||
return obj.proposal_id
|
||||
|
||||
def get_date_created(self, obj):
|
||||
return dt_to_unix(obj.date_created)
|
||||
|
||||
|
||||
proposal_update_schema = ProposalUpdateSchema()
|
||||
proposals_update_schema = ProposalUpdateSchema(many=True)
|
|
@ -8,7 +8,7 @@ from sqlalchemy.exc import IntegrityError
|
|||
from grant.comment.models import Comment, comment_schema
|
||||
from grant.milestone.models import Milestone
|
||||
from grant.user.models import User, SocialMedia, Avatar
|
||||
from .models import Proposal, proposals_schema, proposal_schema, db
|
||||
from .models import Proposal, proposals_schema, proposal_schema, ProposalUpdate, proposal_update_schema, db
|
||||
|
||||
blueprint = Blueprint("proposal", __name__, url_prefix="/api/v1/proposals")
|
||||
|
||||
|
@ -159,3 +159,51 @@ def make_proposal(crowd_fund_contract_address, content, title, milestones, categ
|
|||
|
||||
results = proposal_schema.dump(proposal)
|
||||
return results, 201
|
||||
|
||||
|
||||
@blueprint.route("/<proposal_id>/updates", methods=["GET"])
|
||||
@endpoint.api()
|
||||
def get_proposal_updates(proposal_id):
|
||||
proposal = Proposal.query.filter_by(proposal_id=proposal_id).first()
|
||||
if proposal:
|
||||
dumped_proposal = proposal_schema.dump(proposal)
|
||||
return dumped_proposal["updates"]
|
||||
else:
|
||||
return {"message": "No proposal matching id"}, 404
|
||||
|
||||
|
||||
@blueprint.route("/<proposal_id>/updates/<update_id>", methods=["GET"])
|
||||
@endpoint.api()
|
||||
def get_proposal_update(proposal_id, update_id):
|
||||
proposal = Proposal.query.filter_by(proposal_id=proposal_id).first()
|
||||
if proposal:
|
||||
update = ProposalUpdate.query.filter_by(proposal_id=proposal.id, id=update_id).first()
|
||||
if update:
|
||||
return update
|
||||
else:
|
||||
return {"message": "No update matching id"}
|
||||
else:
|
||||
return {"message": "No proposal matching id"}, 404
|
||||
|
||||
|
||||
# TODO: Add authentication to endpoint
|
||||
@blueprint.route("/<proposal_id>/updates", methods=["POST"])
|
||||
@endpoint.api(
|
||||
parameter('title', type=str, required=True),
|
||||
parameter('content', type=str, required=True)
|
||||
)
|
||||
def post_proposal_update(proposal_id, title, content):
|
||||
proposal = Proposal.query.filter_by(proposal_id=proposal_id).first()
|
||||
if proposal:
|
||||
update = ProposalUpdate(
|
||||
proposal_id=proposal.id,
|
||||
title=title,
|
||||
content=content
|
||||
)
|
||||
db.session.add(update)
|
||||
db.session.commit()
|
||||
|
||||
dumped_update = proposal_update_schema.dump(update)
|
||||
return dumped_update, 201
|
||||
else:
|
||||
return {"message": "No proposal matching id"}, 404
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
"""empty message
|
||||
|
||||
Revision ID: d0cf0f715331
|
||||
Revises: 5f38d8603897
|
||||
Create Date: 2018-10-31 16:47:19.695642
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'd0cf0f715331'
|
||||
down_revision = '5f38d8603897'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('proposal_update',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('date_created', sa.DateTime(), nullable=True),
|
||||
sa.Column('proposal_id', sa.Integer(), nullable=False),
|
||||
sa.Column('title', sa.String(length=255), nullable=False),
|
||||
sa.Column('content', sa.Text(), nullable=False),
|
||||
sa.ForeignKeyConstraint(['proposal_id'], ['proposal.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_table('proposal_update')
|
||||
# ### end Alembic commands ###
|
|
@ -1,5 +1,5 @@
|
|||
import axios from './axios';
|
||||
import { Proposal, TeamMember } from 'types';
|
||||
import { Proposal, TeamMember, Update } from 'types';
|
||||
import { formatTeamMemberForPost, formatTeamMemberFromGet } from 'utils/api';
|
||||
import { PROPOSAL_CATEGORY } from './constants';
|
||||
|
||||
|
@ -77,3 +77,15 @@ export function updateUser(user: TeamMember): Promise<{ data: TeamMember }> {
|
|||
export function verifyEmail(code: string): Promise<any> {
|
||||
return axios.post(`/api/v1/email/${code}/verify`);
|
||||
}
|
||||
|
||||
export function postProposalUpdate(
|
||||
proposalId: string,
|
||||
title: string,
|
||||
content: string,
|
||||
): Promise<{ data: Update }> {
|
||||
return axios
|
||||
.post(`/api/v1/proposals/${proposalId}/updates`, {
|
||||
title,
|
||||
content,
|
||||
});
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
import React from 'react';
|
||||
import classnames from 'classnames';
|
||||
import { convert, MARKDOWN_TYPE } from 'utils/markdown';
|
||||
import './Markdown.less';
|
||||
|
||||
|
@ -15,8 +16,8 @@ export default class Markdown extends React.PureComponent<Props> {
|
|||
const divProps = rest as any;
|
||||
return (
|
||||
<div
|
||||
className="Markdown"
|
||||
{...divProps}
|
||||
className={classnames('Markdown', divProps.className)}
|
||||
dangerouslySetInnerHTML={{ __html: html }}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -0,0 +1,146 @@
|
|||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { Modal, Alert, Input, Button } from 'antd';
|
||||
import Result from 'ant-design-pro/lib/Result';
|
||||
import { fetchProposalUpdates } from 'modules/proposals/actions';
|
||||
import { postProposalUpdate } from 'api/api';
|
||||
import MarkdownEditor from 'components/MarkdownEditor';
|
||||
import './style.less';
|
||||
|
||||
interface DispatchProps {
|
||||
fetchProposalUpdates: typeof fetchProposalUpdates
|
||||
}
|
||||
|
||||
interface OwnProps {
|
||||
proposalId: string;
|
||||
isVisible: boolean;
|
||||
handleClose(): void;
|
||||
}
|
||||
|
||||
type Props = DispatchProps & OwnProps;
|
||||
|
||||
interface State {
|
||||
title: string;
|
||||
content: string;
|
||||
isSubmitting: boolean;
|
||||
hasSubmitted: boolean;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
const INITIAL_STATE = {
|
||||
title: '',
|
||||
content: '',
|
||||
isSubmitting: false,
|
||||
hasSubmitted: false,
|
||||
error: null,
|
||||
}
|
||||
|
||||
class UpdateModal extends React.Component<Props, State> {
|
||||
state: State = { ...INITIAL_STATE };
|
||||
|
||||
componentDidUpdate(prevProps: Props) {
|
||||
if (prevProps.isVisible && !this.props.isVisible) {
|
||||
this.setState({ ...INITIAL_STATE });
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { isVisible } = this.props;
|
||||
const { isSubmitting, hasSubmitted, error, title, content } = this.state;
|
||||
const isMissingFields = !title || !content;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title="Post an Update"
|
||||
visible={isVisible}
|
||||
okText="Submit"
|
||||
cancelText="Cancel"
|
||||
onOk={this.postUpdate}
|
||||
onCancel={this.closeModal}
|
||||
okButtonProps={{
|
||||
loading: isSubmitting,
|
||||
disabled: isMissingFields,
|
||||
}}
|
||||
cancelButtonProps={{ disabled: isSubmitting }}
|
||||
footer={hasSubmitted ? '' : undefined}
|
||||
width={800}
|
||||
centered
|
||||
>
|
||||
<div className="UpdateModal">
|
||||
{hasSubmitted ? (
|
||||
<Result
|
||||
type="success"
|
||||
title="Update has been posted"
|
||||
description="Your funders have been notified, thanks for updating them"
|
||||
actions={<Button onClick={this.closeModal}>Close</Button>}
|
||||
/>
|
||||
) : (
|
||||
<div className="UpdateModal-form">
|
||||
<Input
|
||||
className="UpdateModal-form-title"
|
||||
size="large"
|
||||
value={title}
|
||||
placeholder="Title (60 char max)"
|
||||
onChange={this.handleChangeTitle}
|
||||
/>
|
||||
<MarkdownEditor onChange={this.handleChangeContent} />
|
||||
</div>
|
||||
)}
|
||||
{error && (
|
||||
<Alert
|
||||
type="error"
|
||||
message="Failed to post update proposal"
|
||||
description={error}
|
||||
showIcon
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
private handleChangeTitle = (ev: React.ChangeEvent<HTMLInputElement>) => {
|
||||
this.setState({ title: ev.currentTarget.value });
|
||||
};
|
||||
|
||||
private handleChangeContent = (markdown: string) => {
|
||||
this.setState({ content: markdown });
|
||||
};
|
||||
|
||||
private closeModal = () => {
|
||||
const { isSubmitting, hasSubmitted, title, content } = this.state;
|
||||
if (!isSubmitting) {
|
||||
const empty = !title && !content;
|
||||
if (empty || hasSubmitted || confirm('Are you sure you want to close? You’ll lose this draft.')) {
|
||||
this.props.handleClose();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private postUpdate = () => {
|
||||
const { proposalId } = this.props;
|
||||
const { title, content, hasSubmitted, isSubmitting } = this.state;
|
||||
|
||||
if (hasSubmitted || isSubmitting) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({ isSubmitting: true });
|
||||
postProposalUpdate(proposalId, title, content)
|
||||
.then(() => {
|
||||
this.setState({
|
||||
hasSubmitted: true,
|
||||
isSubmitting: false,
|
||||
});
|
||||
this.props.fetchProposalUpdates(proposalId);
|
||||
})
|
||||
.catch(err => {
|
||||
this.setState({
|
||||
error: err.message || err.toString(),
|
||||
isSubmitting: false,
|
||||
});
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(undefined, { fetchProposalUpdates })(UpdateModal);
|
|
@ -0,0 +1,13 @@
|
|||
@import '~styles/variables.less';
|
||||
|
||||
.UpdateModal {
|
||||
&-form {
|
||||
&-title {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-alert {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
.FullUpdate {
|
||||
padding-left: 0.8rem;
|
||||
|
||||
&-back {
|
||||
display: inline-block;
|
||||
margin-bottom: 1rem;
|
||||
opacity: 0.8;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.anticon {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
}
|
||||
|
||||
&-title {
|
||||
font-size: 2em;
|
||||
margin-bottom: 0.2rem;
|
||||
}
|
||||
|
||||
&-date {
|
||||
font-size: 1rem;
|
||||
opacity: 0.5;
|
||||
margin-top: -0.2rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
&-body {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
import React from 'react';
|
||||
import moment from 'moment';
|
||||
import { Icon } from 'antd';
|
||||
import { Update } from 'types';
|
||||
import Markdown from 'components/Markdown';
|
||||
import './FullUpdate.less';
|
||||
|
||||
interface Props {
|
||||
update: Update;
|
||||
goBack(): void;
|
||||
}
|
||||
|
||||
export default class FullUpdate extends React.Component<Props> {
|
||||
render() {
|
||||
const { update, goBack } = this.props;
|
||||
|
||||
return (
|
||||
<div className="FullUpdate">
|
||||
<a className="FullUpdate-back" onClick={goBack}>
|
||||
<Icon type="arrow-left" /> Back to Updates
|
||||
</a>
|
||||
<h2 className="FullUpdate-title">{update.title}</h2>
|
||||
<div className="FullUpdate-date">
|
||||
{moment(update.dateCreated * 1000).format('MMMM Do, YYYY')}
|
||||
</div>
|
||||
<Markdown source={update.content} className="FullUpdate-body" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -3,8 +3,10 @@ import { connect } from 'react-redux';
|
|||
import { Spin } from 'antd';
|
||||
import Markdown from 'components/Markdown';
|
||||
import moment from 'moment';
|
||||
import Placeholder from 'components/Placeholder';
|
||||
import FullUpdate from './FullUpdate';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { ProposalWithCrowdFund } from 'types';
|
||||
import { ProposalWithCrowdFund, Update } from 'types';
|
||||
import { fetchProposalUpdates } from 'modules/proposals/actions';
|
||||
import {
|
||||
getProposalUpdates,
|
||||
|
@ -29,7 +31,15 @@ interface DispatchProps {
|
|||
|
||||
type Props = DispatchProps & OwnProps & StateProps;
|
||||
|
||||
class ProposalUpdates extends React.Component<Props> {
|
||||
interface State {
|
||||
activeUpdate: Update | null;
|
||||
}
|
||||
|
||||
class ProposalUpdates extends React.Component<Props, State> {
|
||||
state: State = {
|
||||
activeUpdate: null,
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
if (this.props.proposalId) {
|
||||
this.props.fetchProposalUpdates(this.props.proposalId);
|
||||
|
@ -44,30 +54,45 @@ class ProposalUpdates extends React.Component<Props> {
|
|||
|
||||
render() {
|
||||
const { updates, isFetchingUpdates, updatesError } = this.props;
|
||||
const { activeUpdate } = this.state;
|
||||
let content = null;
|
||||
|
||||
if (isFetchingUpdates) {
|
||||
content = <Spin />;
|
||||
} else if (updatesError) {
|
||||
content = (
|
||||
<>
|
||||
<h2>Something went wrong</h2>
|
||||
<p>{updatesError}</p>
|
||||
</>
|
||||
<Placeholder
|
||||
title="Something went wrong"
|
||||
subtitle={updatesError}
|
||||
/>
|
||||
);
|
||||
} else if (updates) {
|
||||
if (updates.length) {
|
||||
if (activeUpdate) {
|
||||
content = (
|
||||
<FullUpdate
|
||||
update={activeUpdate}
|
||||
goBack={() => this.setActiveUpdate(null)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
else if (updates.length) {
|
||||
content = updates.map(update => (
|
||||
<div className="ProposalUpdates-update">
|
||||
<div
|
||||
key={update.updateId}
|
||||
className="ProposalUpdates-update"
|
||||
onClick={() => this.setActiveUpdate(update)}
|
||||
>
|
||||
<h3 className="ProposalUpdates-update-title">{update.title}</h3>
|
||||
<div className="ProposalUpdates-update-date">
|
||||
{moment(update.dateCreated * 1000).format('MMMM Do, YYYY')}
|
||||
</div>
|
||||
<div className="ProposalUpdates-update-body">
|
||||
<Markdown source={this.truncate(update.body)} />
|
||||
<Markdown source={this.truncate(update.content)} />
|
||||
</div>
|
||||
<div className="ProposalUpdates-update-controls">
|
||||
<a className="ProposalUpdates-update-controls-button">Read more</a>
|
||||
<a className="ProposalUpdates-update-controls-button">
|
||||
Read more
|
||||
</a>
|
||||
<a className="ProposalUpdates-update-controls-button">
|
||||
{update.totalComments} comments
|
||||
</a>
|
||||
|
@ -76,7 +101,10 @@ class ProposalUpdates extends React.Component<Props> {
|
|||
));
|
||||
} else {
|
||||
content = (
|
||||
<h3 className="ProposalUpdates-noUpdates">No updates have been posted yet</h3>
|
||||
<Placeholder
|
||||
title="No updates have been posted"
|
||||
subtitle="Check back later to see updates from the team"
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -84,6 +112,10 @@ class ProposalUpdates extends React.Component<Props> {
|
|||
return <div className="ProposalUpdates">{content}</div>;
|
||||
}
|
||||
|
||||
private setActiveUpdate = (activeUpdate: Update | null) => {
|
||||
this.setState({ activeUpdate });
|
||||
};
|
||||
|
||||
private truncate(text: string) {
|
||||
if (text.length < 250) {
|
||||
return text;
|
||||
|
|
|
@ -1,21 +1,33 @@
|
|||
.ProposalUpdates {
|
||||
padding-left: 0.2rem;
|
||||
max-width: 880px;
|
||||
|
||||
&-update {
|
||||
padding: 1rem;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
margin-bottom: 3rem;
|
||||
margin-bottom: 2rem;
|
||||
border-radius: 8px;
|
||||
transition: box-shadow 100ms ease;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 0 3px rgba(#000, 0.15);
|
||||
}
|
||||
|
||||
&-title {
|
||||
font-size: 2rem;
|
||||
font-size: 1.8em;
|
||||
margin-bottom: 0.2rem;
|
||||
}
|
||||
|
||||
&-date {
|
||||
font-size: 1.1rem;
|
||||
font-size: 0.9rem;
|
||||
opacity: 0.5;
|
||||
margin-bottom: 1.5rem;
|
||||
margin-top: -0.2rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
&-body {
|
||||
font-size: 1.1rem;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
&-controls {
|
||||
|
@ -33,7 +45,7 @@
|
|||
width: 2px;
|
||||
height: 2px;
|
||||
border-radius: 100%;
|
||||
background: #888;
|
||||
background: #AAA;
|
||||
}
|
||||
|
||||
&:last-child:after {
|
||||
|
@ -42,11 +54,4 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-noUpdates {
|
||||
text-align: center;
|
||||
font-size: 3rem;
|
||||
line-height: 10rem;
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@ import UpdatesTab from './Updates';
|
|||
import GovernanceTab from './Governance';
|
||||
import ContributorsTab from './Contributors';
|
||||
// import CommunityTab from './Community';
|
||||
import UpdateModal from './UpdateModal';
|
||||
import CancelModal from './CancelModal';
|
||||
import './style.less';
|
||||
import classnames from 'classnames';
|
||||
|
@ -46,6 +47,7 @@ type Props = StateProps & DispatchProps & Web3Props & OwnProps;
|
|||
interface State {
|
||||
isBodyExpanded: boolean;
|
||||
isBodyOverflowing: boolean;
|
||||
isUpdateOpen: boolean;
|
||||
isCancelOpen: boolean;
|
||||
bodyId: string;
|
||||
}
|
||||
|
@ -54,6 +56,7 @@ export class ProposalDetail extends React.Component<Props, State> {
|
|||
state: State = {
|
||||
isBodyExpanded: false,
|
||||
isBodyOverflowing: false,
|
||||
isUpdateOpen: false,
|
||||
isCancelOpen: false,
|
||||
bodyId: `body-${Math.floor(Math.random() * 1000000)}`,
|
||||
};
|
||||
|
@ -79,7 +82,7 @@ export class ProposalDetail extends React.Component<Props, State> {
|
|||
|
||||
render() {
|
||||
const { proposal, isPreview, account } = this.props;
|
||||
const { isBodyExpanded, isBodyOverflowing, isCancelOpen, bodyId } = this.state;
|
||||
const { isBodyExpanded, isBodyOverflowing, isCancelOpen, isUpdateOpen, bodyId } = this.state;
|
||||
const showExpand = !isBodyExpanded && isBodyOverflowing;
|
||||
|
||||
if (!proposal) {
|
||||
|
@ -94,6 +97,9 @@ export class ProposalDetail extends React.Component<Props, State> {
|
|||
|
||||
const adminMenu = isTrustee && (
|
||||
<Menu>
|
||||
<Menu.Item onClick={this.openUpdateModal}>
|
||||
Post an Update
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
onClick={() => alert('Sorry, not yet implemented!')}
|
||||
disabled={!isProposalActive}
|
||||
|
@ -177,7 +183,6 @@ export class ProposalDetail extends React.Component<Props, State> {
|
|||
<CommentsTab proposalId={proposal.proposalId} />
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane tab="Updates" key="updates" disabled={isPreview}>
|
||||
<div style={{ marginTop: '1.5rem' }} />
|
||||
<UpdatesTab proposalId={proposal.proposalId} />
|
||||
</Tabs.TabPane>
|
||||
{isContributor && (
|
||||
|
@ -191,11 +196,18 @@ export class ProposalDetail extends React.Component<Props, State> {
|
|||
</Tabs>
|
||||
)}
|
||||
{isTrustee && (
|
||||
<>
|
||||
<UpdateModal
|
||||
proposalId={proposal.proposalId}
|
||||
isVisible={isUpdateOpen}
|
||||
handleClose={this.closeUpdateModal}
|
||||
/>
|
||||
<CancelModal
|
||||
proposal={proposal}
|
||||
isVisible={isCancelOpen}
|
||||
handleClose={this.closeCancelModal}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
@ -225,6 +237,9 @@ export class ProposalDetail extends React.Component<Props, State> {
|
|||
}
|
||||
};
|
||||
|
||||
private openUpdateModal = () => this.setState({ isUpdateOpen: true });
|
||||
private closeUpdateModal = () => this.setState({ isUpdateOpen: false });
|
||||
|
||||
private openCancelModal = () => this.setState({ isCancelOpen: true });
|
||||
private closeCancelModal = () => this.setState({ isCancelOpen: false });
|
||||
}
|
||||
|
|
|
@ -100,7 +100,10 @@ export function fetchProposalUpdates(proposalId: ProposalWithCrowdFund['proposal
|
|||
return (dispatch: Dispatch<any>) => {
|
||||
dispatch({
|
||||
type: types.PROPOSAL_UPDATES,
|
||||
payload: getProposalUpdates(proposalId),
|
||||
payload: getProposalUpdates(proposalId).then(res => ({
|
||||
proposalId,
|
||||
updates: res.data,
|
||||
})),
|
||||
});
|
||||
};
|
||||
}
|
||||
|
|
|
@ -78,12 +78,12 @@ function addComments(state: ProposalState, payload: { data: ProposalComments })
|
|||
};
|
||||
}
|
||||
|
||||
function addUpdates(state: ProposalState, payload: { data: ProposalUpdates }) {
|
||||
function addUpdates(state: ProposalState, payload: ProposalUpdates) {
|
||||
return {
|
||||
...state,
|
||||
proposalUpdates: {
|
||||
...state.proposalUpdates,
|
||||
[payload.data.proposalId]: payload.data,
|
||||
[payload.proposalId]: payload,
|
||||
},
|
||||
isFetchingUpdates: false,
|
||||
};
|
||||
|
|
|
@ -51,9 +51,9 @@ export function getProposalUpdates(
|
|||
export function getProposalUpdateCount(
|
||||
state: AppState,
|
||||
proposalId: ProposalWithCrowdFund['proposalId'],
|
||||
): ProposalUpdates['totalUpdates'] | null {
|
||||
): number | null {
|
||||
const pu = state.proposal.proposalUpdates[proposalId];
|
||||
return pu ? pu.totalUpdates : null;
|
||||
return pu ? pu.updates.length : null;
|
||||
}
|
||||
|
||||
export function getIsFetchingUpdates(state: AppState) {
|
||||
|
|
|
@ -134,7 +134,7 @@
|
|||
|
||||
blockquote {
|
||||
margin: 0 0 1rem;
|
||||
padding: 0 0 0 0.5rem;
|
||||
padding: 0 0 0 1rem;
|
||||
color: #777;
|
||||
border-left: 4px solid rgba(0, 0, 0, 0.08);
|
||||
|
||||
|
|
|
@ -55,7 +55,6 @@ export interface ProposalComments {
|
|||
|
||||
export interface ProposalUpdates {
|
||||
proposalId: ProposalWithCrowdFund['proposalId'];
|
||||
totalUpdates: number;
|
||||
updates: Update[];
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
export interface Update {
|
||||
updateId: number | string;
|
||||
title: string;
|
||||
body: string;
|
||||
content: string;
|
||||
dateCreated: number;
|
||||
totalComments: number;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue