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:
William O'Beirne 2018-11-02 12:24:28 -04:00 committed by GitHub
parent cdc3ea0107
commit 118d7b645e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 459 additions and 42 deletions

View File

@ -27,6 +27,22 @@ proposal_team = db.Table(
db.Column('proposal_id', db.Integer, db.ForeignKey('proposal.id')) 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): class Proposal(db.Model):
__tablename__ = "proposal" __tablename__ = "proposal"
@ -42,6 +58,7 @@ class Proposal(db.Model):
team = db.relationship("User", secondary=proposal_team) team = db.relationship("User", secondary=proposal_team)
comments = db.relationship(Comment, backref="proposal", lazy=True) comments = db.relationship(Comment, backref="proposal", lazy=True)
updates = db.relationship(ProposalUpdate, backref="proposal", lazy=True)
milestones = db.relationship("Milestone", backref="proposal", lazy=True) milestones = db.relationship("Milestone", backref="proposal", lazy=True)
def __init__( def __init__(
@ -90,6 +107,7 @@ class ProposalSchema(ma.Schema):
"proposal_id", "proposal_id",
"body", "body",
"comments", "comments",
"updates",
"milestones", "milestones",
"category", "category",
"team" "team"
@ -100,6 +118,7 @@ class ProposalSchema(ma.Schema):
body = ma.Method("get_body") body = ma.Method("get_body")
comments = ma.Nested("CommentSchema", many=True) comments = ma.Nested("CommentSchema", many=True)
updates = ma.Nested("ProposalUpdateSchema", many=True)
team = ma.Nested("UserSchema", many=True) team = ma.Nested("UserSchema", many=True)
milestones = ma.Nested("MilestoneSchema", many=True) milestones = ma.Nested("MilestoneSchema", many=True)
@ -115,3 +134,28 @@ class ProposalSchema(ma.Schema):
proposal_schema = ProposalSchema() proposal_schema = ProposalSchema()
proposals_schema = ProposalSchema(many=True) 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)

View File

@ -8,7 +8,7 @@ from sqlalchemy.exc import IntegrityError
from grant.comment.models import Comment, comment_schema from grant.comment.models import Comment, comment_schema
from grant.milestone.models import Milestone from grant.milestone.models import Milestone
from grant.user.models import User, SocialMedia, Avatar 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") 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) results = proposal_schema.dump(proposal)
return results, 201 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

View File

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

View File

@ -1,5 +1,5 @@
import axios from './axios'; import axios from './axios';
import { Proposal, TeamMember } from 'types'; import { Proposal, TeamMember, Update } from 'types';
import { formatTeamMemberForPost, formatTeamMemberFromGet } from 'utils/api'; import { formatTeamMemberForPost, formatTeamMemberFromGet } from 'utils/api';
import { PROPOSAL_CATEGORY } from './constants'; import { PROPOSAL_CATEGORY } from './constants';
@ -76,4 +76,16 @@ export function updateUser(user: TeamMember): Promise<{ data: TeamMember }> {
export function verifyEmail(code: string): Promise<any> { export function verifyEmail(code: string): Promise<any> {
return axios.post(`/api/v1/email/${code}/verify`); 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,
});
} }

View File

@ -1,4 +1,5 @@
import React from 'react'; import React from 'react';
import classnames from 'classnames';
import { convert, MARKDOWN_TYPE } from 'utils/markdown'; import { convert, MARKDOWN_TYPE } from 'utils/markdown';
import './Markdown.less'; import './Markdown.less';
@ -15,8 +16,8 @@ export default class Markdown extends React.PureComponent<Props> {
const divProps = rest as any; const divProps = rest as any;
return ( return (
<div <div
className="Markdown"
{...divProps} {...divProps}
className={classnames('Markdown', divProps.className)}
dangerouslySetInnerHTML={{ __html: html }} dangerouslySetInnerHTML={{ __html: html }}
/> />
); );

View File

@ -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? Youll 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);

View File

@ -0,0 +1,13 @@
@import '~styles/variables.less';
.UpdateModal {
&-form {
&-title {
margin-bottom: 1rem;
}
}
.ant-alert {
margin-top: 1rem;
}
}

View File

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

View File

@ -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>
);
}
}

View File

@ -3,8 +3,10 @@ import { connect } from 'react-redux';
import { Spin } from 'antd'; import { Spin } from 'antd';
import Markdown from 'components/Markdown'; import Markdown from 'components/Markdown';
import moment from 'moment'; import moment from 'moment';
import Placeholder from 'components/Placeholder';
import FullUpdate from './FullUpdate';
import { AppState } from 'store/reducers'; import { AppState } from 'store/reducers';
import { ProposalWithCrowdFund } from 'types'; import { ProposalWithCrowdFund, Update } from 'types';
import { fetchProposalUpdates } from 'modules/proposals/actions'; import { fetchProposalUpdates } from 'modules/proposals/actions';
import { import {
getProposalUpdates, getProposalUpdates,
@ -29,7 +31,15 @@ interface DispatchProps {
type Props = DispatchProps & OwnProps & StateProps; 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() { componentDidMount() {
if (this.props.proposalId) { if (this.props.proposalId) {
this.props.fetchProposalUpdates(this.props.proposalId); this.props.fetchProposalUpdates(this.props.proposalId);
@ -44,30 +54,45 @@ class ProposalUpdates extends React.Component<Props> {
render() { render() {
const { updates, isFetchingUpdates, updatesError } = this.props; const { updates, isFetchingUpdates, updatesError } = this.props;
const { activeUpdate } = this.state;
let content = null; let content = null;
if (isFetchingUpdates) { if (isFetchingUpdates) {
content = <Spin />; content = <Spin />;
} else if (updatesError) { } else if (updatesError) {
content = ( content = (
<> <Placeholder
<h2>Something went wrong</h2> title="Something went wrong"
<p>{updatesError}</p> subtitle={updatesError}
</> />
); );
} else if (updates) { } else if (updates) {
if (updates.length) { if (activeUpdate) {
content = (
<FullUpdate
update={activeUpdate}
goBack={() => this.setActiveUpdate(null)}
/>
);
}
else if (updates.length) {
content = updates.map(update => ( 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> <h3 className="ProposalUpdates-update-title">{update.title}</h3>
<div className="ProposalUpdates-update-date"> <div className="ProposalUpdates-update-date">
{moment(update.dateCreated * 1000).format('MMMM Do, YYYY')} {moment(update.dateCreated * 1000).format('MMMM Do, YYYY')}
</div> </div>
<div className="ProposalUpdates-update-body"> <div className="ProposalUpdates-update-body">
<Markdown source={this.truncate(update.body)} /> <Markdown source={this.truncate(update.content)} />
</div> </div>
<div className="ProposalUpdates-update-controls"> <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"> <a className="ProposalUpdates-update-controls-button">
{update.totalComments} comments {update.totalComments} comments
</a> </a>
@ -76,7 +101,10 @@ class ProposalUpdates extends React.Component<Props> {
)); ));
} else { } else {
content = ( 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>; return <div className="ProposalUpdates">{content}</div>;
} }
private setActiveUpdate = (activeUpdate: Update | null) => {
this.setState({ activeUpdate });
};
private truncate(text: string) { private truncate(text: string) {
if (text.length < 250) { if (text.length < 250) {
return text; return text;

View File

@ -1,21 +1,33 @@
.ProposalUpdates { .ProposalUpdates {
padding-left: 0.2rem;
max-width: 880px;
&-update { &-update {
padding: 1rem;
cursor: pointer;
position: relative; 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 { &-title {
font-size: 2rem; font-size: 1.8em;
margin-bottom: 0.2rem; margin-bottom: 0.2rem;
} }
&-date { &-date {
font-size: 1.1rem; font-size: 0.9rem;
opacity: 0.5; opacity: 0.5;
margin-bottom: 1.5rem; margin-top: -0.2rem;
margin-bottom: 1rem;
} }
&-body { &-body {
font-size: 1.1rem; font-size: 1rem;
} }
&-controls { &-controls {
@ -33,7 +45,7 @@
width: 2px; width: 2px;
height: 2px; height: 2px;
border-radius: 100%; border-radius: 100%;
background: #888; background: #AAA;
} }
&:last-child:after { &:last-child:after {
@ -42,11 +54,4 @@
} }
} }
} }
&-noUpdates {
text-align: center;
font-size: 3rem;
line-height: 10rem;
opacity: 0.5;
}
} }

View File

@ -16,6 +16,7 @@ import UpdatesTab from './Updates';
import GovernanceTab from './Governance'; import GovernanceTab from './Governance';
import ContributorsTab from './Contributors'; import ContributorsTab from './Contributors';
// import CommunityTab from './Community'; // import CommunityTab from './Community';
import UpdateModal from './UpdateModal';
import CancelModal from './CancelModal'; import CancelModal from './CancelModal';
import './style.less'; import './style.less';
import classnames from 'classnames'; import classnames from 'classnames';
@ -46,6 +47,7 @@ type Props = StateProps & DispatchProps & Web3Props & OwnProps;
interface State { interface State {
isBodyExpanded: boolean; isBodyExpanded: boolean;
isBodyOverflowing: boolean; isBodyOverflowing: boolean;
isUpdateOpen: boolean;
isCancelOpen: boolean; isCancelOpen: boolean;
bodyId: string; bodyId: string;
} }
@ -54,6 +56,7 @@ export class ProposalDetail extends React.Component<Props, State> {
state: State = { state: State = {
isBodyExpanded: false, isBodyExpanded: false,
isBodyOverflowing: false, isBodyOverflowing: false,
isUpdateOpen: false,
isCancelOpen: false, isCancelOpen: false,
bodyId: `body-${Math.floor(Math.random() * 1000000)}`, bodyId: `body-${Math.floor(Math.random() * 1000000)}`,
}; };
@ -79,7 +82,7 @@ export class ProposalDetail extends React.Component<Props, State> {
render() { render() {
const { proposal, isPreview, account } = this.props; 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; const showExpand = !isBodyExpanded && isBodyOverflowing;
if (!proposal) { if (!proposal) {
@ -94,6 +97,9 @@ export class ProposalDetail extends React.Component<Props, State> {
const adminMenu = isTrustee && ( const adminMenu = isTrustee && (
<Menu> <Menu>
<Menu.Item onClick={this.openUpdateModal}>
Post an Update
</Menu.Item>
<Menu.Item <Menu.Item
onClick={() => alert('Sorry, not yet implemented!')} onClick={() => alert('Sorry, not yet implemented!')}
disabled={!isProposalActive} disabled={!isProposalActive}
@ -177,7 +183,6 @@ export class ProposalDetail extends React.Component<Props, State> {
<CommentsTab proposalId={proposal.proposalId} /> <CommentsTab proposalId={proposal.proposalId} />
</Tabs.TabPane> </Tabs.TabPane>
<Tabs.TabPane tab="Updates" key="updates" disabled={isPreview}> <Tabs.TabPane tab="Updates" key="updates" disabled={isPreview}>
<div style={{ marginTop: '1.5rem' }} />
<UpdatesTab proposalId={proposal.proposalId} /> <UpdatesTab proposalId={proposal.proposalId} />
</Tabs.TabPane> </Tabs.TabPane>
{isContributor && ( {isContributor && (
@ -191,11 +196,18 @@ export class ProposalDetail extends React.Component<Props, State> {
</Tabs> </Tabs>
)} )}
{isTrustee && ( {isTrustee && (
<CancelModal <>
proposal={proposal} <UpdateModal
isVisible={isCancelOpen} proposalId={proposal.proposalId}
handleClose={this.closeCancelModal} isVisible={isUpdateOpen}
/> handleClose={this.closeUpdateModal}
/>
<CancelModal
proposal={proposal}
isVisible={isCancelOpen}
handleClose={this.closeCancelModal}
/>
</>
)} )}
</div> </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 openCancelModal = () => this.setState({ isCancelOpen: true });
private closeCancelModal = () => this.setState({ isCancelOpen: false }); private closeCancelModal = () => this.setState({ isCancelOpen: false });
} }

View File

@ -100,7 +100,10 @@ export function fetchProposalUpdates(proposalId: ProposalWithCrowdFund['proposal
return (dispatch: Dispatch<any>) => { return (dispatch: Dispatch<any>) => {
dispatch({ dispatch({
type: types.PROPOSAL_UPDATES, type: types.PROPOSAL_UPDATES,
payload: getProposalUpdates(proposalId), payload: getProposalUpdates(proposalId).then(res => ({
proposalId,
updates: res.data,
})),
}); });
}; };
} }

View File

@ -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 { return {
...state, ...state,
proposalUpdates: { proposalUpdates: {
...state.proposalUpdates, ...state.proposalUpdates,
[payload.data.proposalId]: payload.data, [payload.proposalId]: payload,
}, },
isFetchingUpdates: false, isFetchingUpdates: false,
}; };

View File

@ -51,9 +51,9 @@ export function getProposalUpdates(
export function getProposalUpdateCount( export function getProposalUpdateCount(
state: AppState, state: AppState,
proposalId: ProposalWithCrowdFund['proposalId'], proposalId: ProposalWithCrowdFund['proposalId'],
): ProposalUpdates['totalUpdates'] | null { ): number | null {
const pu = state.proposal.proposalUpdates[proposalId]; const pu = state.proposal.proposalUpdates[proposalId];
return pu ? pu.totalUpdates : null; return pu ? pu.updates.length : null;
} }
export function getIsFetchingUpdates(state: AppState) { export function getIsFetchingUpdates(state: AppState) {

View File

@ -134,7 +134,7 @@
blockquote { blockquote {
margin: 0 0 1rem; margin: 0 0 1rem;
padding: 0 0 0 0.5rem; padding: 0 0 0 1rem;
color: #777; color: #777;
border-left: 4px solid rgba(0, 0, 0, 0.08); border-left: 4px solid rgba(0, 0, 0, 0.08);

View File

@ -55,7 +55,6 @@ export interface ProposalComments {
export interface ProposalUpdates { export interface ProposalUpdates {
proposalId: ProposalWithCrowdFund['proposalId']; proposalId: ProposalWithCrowdFund['proposalId'];
totalUpdates: number;
updates: Update[]; updates: Update[];
} }

View File

@ -1,7 +1,7 @@
export interface Update { export interface Update {
updateId: number | string; updateId: number | string;
title: string; title: string;
body: string; content: string;
dateCreated: number; dateCreated: number;
totalComments: number; totalComments: number;
} }