paginate comments & "All" stages filter in frontend
This commit is contained in:
parent
4ce3ea4dbe
commit
da889a4f58
|
@ -74,7 +74,6 @@ class UserCommentSchema(ma.Schema):
|
|||
proposal = ma.Nested(
|
||||
"ProposalSchema",
|
||||
exclude=[
|
||||
"comments",
|
||||
"team",
|
||||
"milestones",
|
||||
"content",
|
||||
|
|
|
@ -1,8 +1,12 @@
|
|||
import click
|
||||
import datetime
|
||||
from random import randint
|
||||
from math import floor
|
||||
from flask.cli import with_appcontext
|
||||
|
||||
from .models import Proposal, db
|
||||
from grant.milestone.models import Milestone
|
||||
from grant.comment.models import Comment
|
||||
from grant.utils.enums import ProposalStatus, Category
|
||||
from grant.user.models import User
|
||||
|
||||
|
@ -43,6 +47,27 @@ def create_proposals(count):
|
|||
p.date_published = datetime.datetime.now()
|
||||
p.team.append(user)
|
||||
db.session.add(p)
|
||||
db.session.flush()
|
||||
num_ms = randint(1, 9)
|
||||
for j in range(num_ms):
|
||||
m = Milestone(
|
||||
title=f'Fake MS {j}',
|
||||
content=f'Fake milestone #{j} on fake proposal #{i}!',
|
||||
date_estimated=datetime.datetime.now(),
|
||||
payout_percent=str(floor(1 / num_ms * 100)),
|
||||
immediate_payout=j == 0,
|
||||
proposal_id=p.id,
|
||||
index=j
|
||||
)
|
||||
db.session.add(m)
|
||||
for j in range(100):
|
||||
c = Comment(
|
||||
proposal_id=p.id,
|
||||
user_id=user.id,
|
||||
parent_comment_id=None,
|
||||
content=f'Fake comment #{j} on fake proposal #{i}!'
|
||||
)
|
||||
db.session.add(c)
|
||||
|
||||
db.session.commit()
|
||||
print(f'Added {count} LIVE fake proposals')
|
||||
|
|
|
@ -516,7 +516,6 @@ class ProposalSchema(ma.Schema):
|
|||
"is_failed",
|
||||
"funded",
|
||||
"content",
|
||||
"comments",
|
||||
"updates",
|
||||
"milestones",
|
||||
"current_milestone",
|
||||
|
@ -535,7 +534,6 @@ class ProposalSchema(ma.Schema):
|
|||
date_published = ma.Method("get_date_published")
|
||||
proposal_id = ma.Method("get_proposal_id")
|
||||
|
||||
comments = ma.Nested("CommentSchema", many=True)
|
||||
updates = ma.Nested("ProposalUpdateSchema", many=True)
|
||||
team = ma.Nested("UserSchema", many=True)
|
||||
milestones = ma.Nested("MilestoneSchema", many=True)
|
||||
|
|
|
@ -59,20 +59,24 @@ def get_proposal(proposal_id):
|
|||
|
||||
|
||||
@blueprint.route("/<proposal_id>/comments", methods=["GET"])
|
||||
@endpoint.api()
|
||||
def get_proposal_comments(proposal_id):
|
||||
proposal = Proposal.query.filter_by(id=proposal_id).first()
|
||||
if not proposal:
|
||||
return {"message": "No proposal matching id"}, 404
|
||||
|
||||
# Only pull top comments, replies will be attached to them
|
||||
comments = Comment.query.filter_by(proposal_id=proposal_id, parent_comment_id=None)
|
||||
num_comments = Comment.query.filter_by(proposal_id=proposal_id).count()
|
||||
return {
|
||||
"proposalId": proposal_id,
|
||||
"totalComments": num_comments,
|
||||
"comments": comments_schema.dump(comments)
|
||||
}
|
||||
@endpoint.api(
|
||||
parameter('page', type=int, required=False),
|
||||
parameter('filters', type=list, required=False),
|
||||
parameter('search', type=str, required=False),
|
||||
parameter('sort', type=str, required=False)
|
||||
)
|
||||
def get_proposal_comments(proposal_id, page, filters, search, sort):
|
||||
# only using page, currently
|
||||
filters_workaround = request.args.getlist('filters[]')
|
||||
page = pagination.comment(
|
||||
schema=comments_schema,
|
||||
query=Comment.query.filter_by(proposal_id=proposal_id, parent_comment_id=None),
|
||||
page=page,
|
||||
filters=filters_workaround,
|
||||
search=search,
|
||||
sort=sort,
|
||||
)
|
||||
return page
|
||||
|
||||
|
||||
@blueprint.route("/<proposal_id>/comments", methods=["POST"])
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import abc
|
||||
from sqlalchemy import or_, and_
|
||||
|
||||
from grant.comment.models import Comment, comments_schema
|
||||
from grant.proposal.models import db, ma, Proposal, ProposalContribution, ProposalArbiter, proposal_contributions_schema
|
||||
from grant.user.models import User, users_schema
|
||||
from grant.milestone.models import Milestone
|
||||
|
@ -229,8 +230,56 @@ class UserPagination(Pagination):
|
|||
}
|
||||
|
||||
|
||||
class CommentPagination(Pagination):
|
||||
def __init__(self):
|
||||
self.FILTERS = []
|
||||
self.PAGE_SIZE = 10
|
||||
self.SORT_MAP = {
|
||||
'CREATED:DESC': Comment.date_created.desc(),
|
||||
'CREATED:ASC': Comment.date_created,
|
||||
}
|
||||
|
||||
def paginate(
|
||||
self,
|
||||
schema: ma.Schema=comments_schema,
|
||||
query: db.Query=None,
|
||||
page: int=1,
|
||||
filters: list=None,
|
||||
search: str=None,
|
||||
sort: str='CREATED:DESC',
|
||||
):
|
||||
query = query or Comment.query
|
||||
sort = sort or 'CREATED:DESC'
|
||||
|
||||
# FILTER
|
||||
if filters:
|
||||
pass
|
||||
|
||||
# SORT (see self.SORT_MAP)
|
||||
if sort:
|
||||
self.validate_sort(sort)
|
||||
query = query.order_by(self.SORT_MAP[sort])
|
||||
|
||||
# SEARCH can match txids or amounts
|
||||
if search:
|
||||
query = query.filter(or_(
|
||||
Comment.content.ilike(f'%{search}%'),
|
||||
))
|
||||
|
||||
res = query.paginate(page, self.PAGE_SIZE, False)
|
||||
return {
|
||||
'page': res.page,
|
||||
'total': res.total,
|
||||
'page_size': self.PAGE_SIZE,
|
||||
'items': schema.dump(res.items),
|
||||
'filters': filters,
|
||||
'search': search,
|
||||
'sort': sort
|
||||
}
|
||||
|
||||
|
||||
# expose pagination methods here
|
||||
proposal = ProposalPagination().paginate
|
||||
contribution = ContributionPagination().paginate
|
||||
# comment = CommentPagination().paginate
|
||||
comment = CommentPagination().paginate
|
||||
user = UserPagination().paginate
|
||||
|
|
|
@ -12,6 +12,7 @@ import {
|
|||
EmailSubscriptions,
|
||||
RFP,
|
||||
ProposalPageParams,
|
||||
PageParams,
|
||||
} from 'types';
|
||||
import {
|
||||
formatUserForPost,
|
||||
|
@ -40,8 +41,8 @@ export function getProposal(proposalId: number | string): Promise<{ data: Propos
|
|||
});
|
||||
}
|
||||
|
||||
export function getProposalComments(proposalId: number | string) {
|
||||
return axios.get(`/api/v1/proposals/${proposalId}/comments`);
|
||||
export function getProposalComments(proposalId: number | string, params: PageParams) {
|
||||
return axios.get(`/api/v1/proposals/${proposalId}/comments`, { params });
|
||||
}
|
||||
|
||||
export function getProposalUpdates(proposalId: number | string) {
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
.ProposalComments {
|
||||
max-width: 780px;
|
||||
|
||||
&-skellie {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
&-alert {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
&-post {
|
||||
margin-bottom: 2rem;
|
||||
|
||||
& > button {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
&-verify {
|
||||
font-size: 1rem;
|
||||
opacity: 0.7;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
}
|
|
@ -1,29 +1,21 @@
|
|||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { Button, message } from 'antd';
|
||||
import { Button, message, Skeleton, Alert } from 'antd';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { Proposal } from 'types';
|
||||
import { fetchProposalComments, postProposalComment } from 'modules/proposals/actions';
|
||||
import {
|
||||
getCommentsError,
|
||||
getIsFetchingComments,
|
||||
getProposalComments,
|
||||
} from 'modules/proposals/selectors';
|
||||
import { getIsVerified, getIsSignedIn } from 'modules/auth/selectors';
|
||||
import Comments from 'components/Comments';
|
||||
import Placeholder from 'components/Placeholder';
|
||||
import Loader from 'components/Loader';
|
||||
import MarkdownEditor, { MARKDOWN_TYPE } from 'components/MarkdownEditor';
|
||||
import './style.less';
|
||||
import './index.less';
|
||||
|
||||
interface OwnProps {
|
||||
proposalId: Proposal['proposalId'];
|
||||
}
|
||||
|
||||
interface StateProps {
|
||||
comments: ReturnType<typeof getProposalComments>;
|
||||
isFetchingComments: ReturnType<typeof getIsFetchingComments>;
|
||||
commentsError: ReturnType<typeof getCommentsError>;
|
||||
detailComments: AppState['proposal']['detailComments'];
|
||||
isPostCommentPending: AppState['proposal']['isPostCommentPending'];
|
||||
postCommentError: AppState['proposal']['postCommentError'];
|
||||
isVerified: ReturnType<typeof getIsVerified>;
|
||||
|
@ -39,17 +31,19 @@ type Props = DispatchProps & OwnProps & StateProps;
|
|||
|
||||
interface State {
|
||||
comment: string;
|
||||
curtainsMatchDrapes: boolean;
|
||||
}
|
||||
|
||||
class ProposalComments extends React.Component<Props, State> {
|
||||
state: State = {
|
||||
comment: '',
|
||||
curtainsMatchDrapes: this.props.detailComments.parentId === this.props.proposalId,
|
||||
};
|
||||
|
||||
private editor: MarkdownEditor | null = null;
|
||||
|
||||
componentDidMount() {
|
||||
if (this.props.proposalId) {
|
||||
if (!this.state.curtainsMatchDrapes) {
|
||||
this.props.fetchProposalComments(this.props.proposalId);
|
||||
}
|
||||
}
|
||||
|
@ -74,41 +68,51 @@ class ProposalComments extends React.Component<Props, State> {
|
|||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
comments,
|
||||
isFetchingComments,
|
||||
commentsError,
|
||||
isPostCommentPending,
|
||||
isVerified,
|
||||
isSignedIn,
|
||||
} = this.props;
|
||||
const { detailComments, isPostCommentPending, isVerified, isSignedIn } = this.props;
|
||||
const { comment } = this.state;
|
||||
let content = null;
|
||||
|
||||
if (isFetchingComments) {
|
||||
content = <Loader />;
|
||||
} else if (commentsError) {
|
||||
const { hasFetched, isFetching, hasMore, pages, fetchError, total } = detailComments;
|
||||
if (!hasFetched) {
|
||||
content = [1, 2, 3].map(i => (
|
||||
<Skeleton
|
||||
className="ProposalComments-skellie"
|
||||
key={i}
|
||||
active
|
||||
avatar={{ shape: 'square' }}
|
||||
paragraph={{ rows: 2 }}
|
||||
/>
|
||||
));
|
||||
} else if (total) {
|
||||
content = (
|
||||
<>
|
||||
<h2>Something went wrong</h2>
|
||||
<p>{commentsError}</p>
|
||||
{pages.map((p, i) => (
|
||||
<Comments key={i} comments={p} />
|
||||
))}
|
||||
<div>
|
||||
{hasMore && (
|
||||
<Button
|
||||
onClick={() => this.props.fetchProposalComments()}
|
||||
loading={isFetching}
|
||||
block
|
||||
>
|
||||
Older Comments
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
} else if (comments) {
|
||||
if (comments.length) {
|
||||
content = <Comments comments={comments} />;
|
||||
} else {
|
||||
content = (
|
||||
<Placeholder
|
||||
title="No comments have been made yet"
|
||||
subtitle="Why not be the first?"
|
||||
/>
|
||||
);
|
||||
}
|
||||
} else {
|
||||
content = (
|
||||
<Placeholder
|
||||
title="No comments have been made yet"
|
||||
subtitle="Why not be the first?"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="ProposalComments">
|
||||
<div className="ProposalComments-post">
|
||||
{isVerified && (
|
||||
<>
|
||||
|
@ -117,7 +121,6 @@ class ProposalComments extends React.Component<Props, State> {
|
|||
onChange={this.handleCommentChange}
|
||||
type={MARKDOWN_TYPE.REDUCED}
|
||||
/>
|
||||
<div style={{ marginTop: '0.5rem' }} />
|
||||
<Button
|
||||
onClick={this.postComment}
|
||||
disabled={!comment.length}
|
||||
|
@ -142,6 +145,14 @@ class ProposalComments extends React.Component<Props, State> {
|
|||
)}
|
||||
</div>
|
||||
{content}
|
||||
{fetchError && (
|
||||
<Alert
|
||||
className="ProposalComments-alert"
|
||||
type="error"
|
||||
message="Oopsy, there was a problem loading comments!"
|
||||
description={fetchError}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -156,10 +167,8 @@ class ProposalComments extends React.Component<Props, State> {
|
|||
}
|
||||
|
||||
export default connect<StateProps, DispatchProps, OwnProps, AppState>(
|
||||
(state, ownProps) => ({
|
||||
comments: getProposalComments(state, ownProps.proposalId),
|
||||
isFetchingComments: getIsFetchingComments(state),
|
||||
commentsError: getCommentsError(state),
|
||||
state => ({
|
||||
detailComments: state.proposal.detailComments,
|
||||
isPostCommentPending: state.proposal.isPostCommentPending,
|
||||
postCommentError: state.proposal.postCommentError,
|
||||
isVerified: getIsVerified(state),
|
||||
|
|
|
@ -1,11 +0,0 @@
|
|||
.ProposalComments {
|
||||
&-post {
|
||||
margin-bottom: 2rem;
|
||||
max-width: 780px;
|
||||
}
|
||||
&-verify {
|
||||
font-size: 1rem;
|
||||
opacity: 0.7;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
}
|
|
@ -55,18 +55,30 @@ export default class ProposalFilters extends React.Component<Props> {
|
|||
<Divider />
|
||||
|
||||
<h3>Proposal stage</h3>
|
||||
{typedKeys(PROPOSAL_STAGE).map(s => (
|
||||
<div key={s} style={{ marginBottom: '0.25rem' }}>
|
||||
<Radio
|
||||
value={s}
|
||||
name="stage"
|
||||
checked={filters.stage.includes(s as PROPOSAL_STAGE)}
|
||||
onChange={this.handleStageChange}
|
||||
>
|
||||
{STAGE_UI[s].label}
|
||||
</Radio>
|
||||
</div>
|
||||
))}
|
||||
<div style={{ marginBottom: '0.25rem' }}>
|
||||
<Radio
|
||||
value="ALL"
|
||||
name="stage"
|
||||
checked={filters.stage.length === 0}
|
||||
onChange={this.handleStageChange}
|
||||
>
|
||||
All
|
||||
</Radio>
|
||||
</div>
|
||||
{typedKeys(PROPOSAL_STAGE)
|
||||
.filter(s => s !== PROPOSAL_STAGE.PREVIEW) // skip this one
|
||||
.map(s => (
|
||||
<div key={s} style={{ marginBottom: '0.25rem' }}>
|
||||
<Radio
|
||||
value={s}
|
||||
name="stage"
|
||||
checked={filters.stage.includes(s as PROPOSAL_STAGE)}
|
||||
onChange={this.handleStageChange}
|
||||
>
|
||||
{STAGE_UI[s].label}
|
||||
</Radio>
|
||||
</div>
|
||||
))}
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
|
@ -86,9 +98,13 @@ export default class ProposalFilters extends React.Component<Props> {
|
|||
};
|
||||
|
||||
private handleStageChange = (ev: RadioChangeEvent) => {
|
||||
let stage = [] as PROPOSAL_STAGE[];
|
||||
if (ev.target.value !== 'ALL') {
|
||||
stage = [ev.target.value as PROPOSAL_STAGE];
|
||||
}
|
||||
this.props.handleChangeFilters({
|
||||
...this.props.filters,
|
||||
stage: [ev.target.value as PROPOSAL_STAGE],
|
||||
stage,
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
@ -13,7 +13,7 @@ import {
|
|||
import { Dispatch } from 'redux';
|
||||
import { Proposal, Comment, ProposalPageParams } from 'types';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { getProposalPageSettings } from './selectors';
|
||||
import { getProposalPageSettings, getProposalCommentPageParams } from './selectors';
|
||||
|
||||
type GetState = () => AppState;
|
||||
|
||||
|
@ -118,12 +118,33 @@ export function fetchProposal(proposalId: Proposal['proposalId']) {
|
|||
};
|
||||
}
|
||||
|
||||
export function fetchProposalComments(proposalId: Proposal['proposalId']) {
|
||||
return (dispatch: Dispatch<any>) => {
|
||||
export function fetchProposalComments(id?: number) {
|
||||
return async (dispatch: Dispatch<any>, getState: GetState) => {
|
||||
const state = getState();
|
||||
if (!state.proposal.detail) {
|
||||
return;
|
||||
}
|
||||
const proposalId = id || state.proposal.detail.proposalId;
|
||||
dispatch({
|
||||
type: types.PROPOSAL_COMMENTS,
|
||||
payload: getProposalComments(proposalId),
|
||||
type: types.PROPOSAL_COMMENTS_PENDING,
|
||||
payload: {
|
||||
parentId: proposalId, // payload gets the proposalId
|
||||
},
|
||||
});
|
||||
// get fresh params after PENDING has run, above
|
||||
const params = getProposalCommentPageParams(getState());
|
||||
try {
|
||||
const comments = (await getProposalComments(proposalId, params)).data;
|
||||
return dispatch({
|
||||
type: types.PROPOSAL_COMMENTS_FULFILLED,
|
||||
payload: comments,
|
||||
});
|
||||
} catch (error) {
|
||||
dispatch({
|
||||
type: types.PROPOSAL_COMMENTS_REJECTED,
|
||||
payload: error,
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
import types from './types';
|
||||
import { findComment } from 'utils/helpers';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { pendingMoreablePage, fulfilledMoreablePage } from 'utils/helpers';
|
||||
import {
|
||||
Proposal,
|
||||
ProposalComments,
|
||||
ProposalUpdates,
|
||||
Comment,
|
||||
ProposalContributions,
|
||||
LoadableProposalPage,
|
||||
Moreable,
|
||||
} from 'types';
|
||||
import { PROPOSAL_SORT } from 'api/constants';
|
||||
|
||||
|
@ -26,9 +27,7 @@ export interface ProposalState {
|
|||
isFetchingDetail: boolean;
|
||||
detailError: null | string;
|
||||
|
||||
proposalComments: { [id: string]: ProposalComments };
|
||||
commentsError: null | string;
|
||||
isFetchingComments: boolean;
|
||||
detailComments: Moreable<Comment>;
|
||||
|
||||
proposalUpdates: { [id: string]: ProposalUpdates };
|
||||
updatesError: null | string;
|
||||
|
@ -76,9 +75,21 @@ export const INITIAL_STATE: ProposalState = {
|
|||
isFetchingDetail: false,
|
||||
detailError: null,
|
||||
|
||||
proposalComments: {},
|
||||
commentsError: null,
|
||||
isFetchingComments: false,
|
||||
detailComments: {
|
||||
pages: [],
|
||||
hasMore: false,
|
||||
page: 1,
|
||||
pageSize: 0,
|
||||
total: 0,
|
||||
search: '',
|
||||
sort: '',
|
||||
filters: [],
|
||||
isFetching: false,
|
||||
hasFetched: false,
|
||||
fetchError: '',
|
||||
fetchTime: 0,
|
||||
parentId: null,
|
||||
},
|
||||
|
||||
proposalUpdates: {},
|
||||
updatesError: null,
|
||||
|
@ -95,17 +106,6 @@ export const INITIAL_STATE: ProposalState = {
|
|||
deleteContributionError: null,
|
||||
};
|
||||
|
||||
function addComments(state: ProposalState, payload: { data: ProposalComments }) {
|
||||
return {
|
||||
...state,
|
||||
proposalComments: {
|
||||
...state.proposalComments,
|
||||
[payload.data.proposalId]: payload.data,
|
||||
},
|
||||
isFetchingComments: false,
|
||||
};
|
||||
}
|
||||
|
||||
function addUpdates(state: ProposalState, payload: ProposalUpdates) {
|
||||
return {
|
||||
...state,
|
||||
|
@ -134,36 +134,40 @@ interface PostCommentPayload {
|
|||
parentCommentId?: Comment['id'];
|
||||
}
|
||||
function addPostedComment(state: ProposalState, payload: PostCommentPayload) {
|
||||
const { proposalId, comment, parentCommentId } = payload;
|
||||
const newComments = state.proposalComments[proposalId]
|
||||
? {
|
||||
...state.proposalComments[proposalId],
|
||||
totalComments: state.proposalComments[proposalId].totalComments + 1,
|
||||
comments: [...state.proposalComments[proposalId].comments],
|
||||
}
|
||||
: {
|
||||
proposalId: payload.proposalId,
|
||||
totalComments: 1,
|
||||
comments: [],
|
||||
};
|
||||
|
||||
if (parentCommentId) {
|
||||
const parentComment = findComment(parentCommentId, newComments.comments);
|
||||
if (parentComment) {
|
||||
// FIXME: Object mutation because I'm lazy, but this probably shouldn’t
|
||||
// exist once API hookup is done. We'll just re-request from server.
|
||||
parentComment.replies.unshift(comment);
|
||||
const { comment, parentCommentId } = payload;
|
||||
// clone so we can mutate with great abandon!
|
||||
const pages = cloneDeep(state.detailComments.pages);
|
||||
if (!parentCommentId) {
|
||||
// its a new comment, pop it into the very first position
|
||||
if (pages[0]) {
|
||||
pages[0].unshift(comment);
|
||||
} else {
|
||||
pages[0] = [comment];
|
||||
}
|
||||
} else {
|
||||
newComments.comments.unshift(comment);
|
||||
// recursive populate replies for nested comment
|
||||
const f = (id: number, p: Comment) => {
|
||||
if (p.id === id) {
|
||||
p.replies.unshift(comment);
|
||||
return;
|
||||
} else {
|
||||
p.replies.forEach(x => f(id, x));
|
||||
}
|
||||
};
|
||||
// pages > page > comments
|
||||
pages.forEach(p =>
|
||||
p.forEach(c => {
|
||||
f(parentCommentId, c);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
isPostCommentPending: false,
|
||||
proposalComments: {
|
||||
...state.proposalComments,
|
||||
[payload.proposalId]: newComments,
|
||||
detailComments: {
|
||||
...state.detailComments,
|
||||
pages,
|
||||
total: state.detailComments.total + 1,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
@ -314,17 +318,22 @@ export default (state = INITIAL_STATE, action: any) => {
|
|||
case types.PROPOSAL_COMMENTS_PENDING:
|
||||
return {
|
||||
...state,
|
||||
commentsError: null,
|
||||
isFetchingComments: true,
|
||||
detailComments: pendingMoreablePage(state.detailComments, payload),
|
||||
};
|
||||
case types.PROPOSAL_COMMENTS_FULFILLED:
|
||||
return addComments(state, payload);
|
||||
return {
|
||||
...state,
|
||||
detailComments: fulfilledMoreablePage(state.detailComments, payload),
|
||||
};
|
||||
case types.PROPOSAL_COMMENTS_REJECTED:
|
||||
return {
|
||||
...state,
|
||||
// TODO: Get action to send real error
|
||||
commentsError: 'Failed to fetch comments',
|
||||
isFetchingComments: false,
|
||||
detailComments: {
|
||||
...state.detailComments,
|
||||
hasFetched: true,
|
||||
isFetching: false,
|
||||
fetchError: (payload && payload.message) || payload.toString(),
|
||||
},
|
||||
};
|
||||
|
||||
case types.PROPOSAL_UPDATES_PENDING:
|
||||
|
|
|
@ -1,36 +1,12 @@
|
|||
import { AppState } from 'store/reducers';
|
||||
import {
|
||||
Proposal,
|
||||
ProposalComments,
|
||||
ProposalUpdates,
|
||||
ProposalContributions,
|
||||
ProposalPageParams,
|
||||
PageParams,
|
||||
} from 'types';
|
||||
|
||||
export function getProposalComments(
|
||||
state: AppState,
|
||||
proposalId: Proposal['proposalId'],
|
||||
): ProposalComments['comments'] | null {
|
||||
const pc = state.proposal.proposalComments[proposalId];
|
||||
return pc ? pc.comments : null;
|
||||
}
|
||||
|
||||
export function getProposalCommentCount(
|
||||
state: AppState,
|
||||
proposalId: Proposal['proposalId'],
|
||||
): ProposalComments['totalComments'] | null {
|
||||
const pc = state.proposal.proposalComments[proposalId];
|
||||
return pc ? pc.totalComments : null;
|
||||
}
|
||||
|
||||
export function getIsFetchingComments(state: AppState) {
|
||||
return state.proposal.isFetchingComments;
|
||||
}
|
||||
|
||||
export function getCommentsError(state: AppState) {
|
||||
return state.proposal.commentsError;
|
||||
}
|
||||
|
||||
export function getProposalUpdates(
|
||||
state: AppState,
|
||||
proposalId: Proposal['proposalId'],
|
||||
|
@ -80,3 +56,13 @@ export function getProposalPageSettings(state: AppState): ProposalPageParams {
|
|||
filters,
|
||||
};
|
||||
}
|
||||
|
||||
export function getProposalCommentPageParams(state: AppState): PageParams {
|
||||
const { page, search, sort, filters } = state.proposal.detailComments;
|
||||
return {
|
||||
page,
|
||||
search,
|
||||
sort,
|
||||
filters,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { pick } from 'lodash';
|
||||
import { Comment } from 'types';
|
||||
import { Comment, Moreable, ServerPage } from 'types';
|
||||
|
||||
export function isNumeric(n: any) {
|
||||
return !isNaN(parseFloat(n)) && isFinite(n);
|
||||
|
@ -45,3 +45,56 @@ export function urlToPublic(url: string) {
|
|||
}
|
||||
return withPublicHost;
|
||||
}
|
||||
|
||||
export function pendingMoreablePage<T>(
|
||||
state: Moreable<T>,
|
||||
params: Partial<Moreable<T>>,
|
||||
): Moreable<T> {
|
||||
let newState: Partial<Moreable<T>> = {
|
||||
isFetching: true,
|
||||
page: state.page + 1,
|
||||
fetchError: '',
|
||||
};
|
||||
// if we ever use search, filter or sort we'll want to check those here
|
||||
if (state.parentId !== params.parentId) {
|
||||
// reset
|
||||
newState = {
|
||||
...newState,
|
||||
parentId: params.parentId,
|
||||
pages: [],
|
||||
page: 1,
|
||||
pageSize: 0,
|
||||
total: 0,
|
||||
hasFetched: false,
|
||||
};
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
...newState,
|
||||
};
|
||||
}
|
||||
|
||||
export function fulfilledMoreablePage<T>(
|
||||
state: Moreable<T>,
|
||||
serverPage: ServerPage<T>,
|
||||
): Moreable<T> {
|
||||
const { total, pageSize, page, items } = serverPage;
|
||||
let pages = [...state.pages, items];
|
||||
if (page !== state.pages.length + 1) {
|
||||
if (page === 1) {
|
||||
pages = [items];
|
||||
}
|
||||
}
|
||||
const hasMore = page * pageSize < total;
|
||||
return {
|
||||
...state,
|
||||
total,
|
||||
pageSize,
|
||||
page,
|
||||
pages,
|
||||
hasMore,
|
||||
hasFetched: true,
|
||||
isFetching: false,
|
||||
fetchTime: Date.now(),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -10,6 +10,10 @@ export interface Page {
|
|||
filters: string[];
|
||||
}
|
||||
|
||||
export interface ServerPage<T> extends Page {
|
||||
items: T[];
|
||||
}
|
||||
|
||||
export type PageParams = Omit<Page, 'pageSize' | 'total'>;
|
||||
|
||||
export interface Loadable {
|
||||
|
@ -28,6 +32,14 @@ export interface ProposalPage extends Omit<Page, 'filters' | 'sort'> {
|
|||
};
|
||||
}
|
||||
|
||||
export type LoadablePage = Page & Loadable;
|
||||
|
||||
export type LoadableProposalPage = ProposalPage & Loadable;
|
||||
|
||||
export type ProposalPageParams = Omit<ProposalPage, 'items' | 'pageSize' | 'total'>;
|
||||
|
||||
export interface Moreable<T> extends LoadablePage {
|
||||
pages: T[][]; // ex: Comment
|
||||
hasMore: boolean;
|
||||
parentId: null | number; // ex: proposalId, parentCommentId... (optional)
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue