decidim-comments 0.0.1 → 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Rakefile +1 -1
- data/app/assets/javascripts/decidim/comments/bundle.js +43 -44
- data/app/assets/javascripts/decidim/comments/bundle.js.map +1 -1
- data/app/commands/decidim/comments/vote_comment.rb +38 -0
- data/app/frontend/application/icon.component.jsx +9 -4
- data/app/frontend/application/icon.component.test.jsx +12 -2
- data/app/frontend/comments/add_comment_form.component.jsx +43 -11
- data/app/frontend/comments/add_comment_form.mutation.graphql +1 -4
- data/app/frontend/comments/comment.component.jsx +67 -7
- data/app/frontend/comments/comment.component.test.jsx +49 -1
- data/app/frontend/comments/comment_data.fragment.graphql +3 -0
- data/app/frontend/comments/comment_order_selector.component.jsx +51 -6
- data/app/frontend/comments/comment_order_selector.component.test.jsx +12 -1
- data/app/frontend/comments/comment_thread.component.jsx +11 -5
- data/app/frontend/comments/comment_thread.component.test.jsx +13 -3
- data/app/frontend/comments/comment_thread.fragment.graphql +1 -3
- data/app/frontend/comments/comments.component.jsx +37 -11
- data/app/frontend/comments/comments.component.test.jsx +44 -7
- data/app/frontend/comments/comments.query.graphql +2 -2
- data/app/frontend/comments/down_vote.fragment.graphql +6 -0
- data/app/frontend/comments/down_vote.mutation.graphql +7 -0
- data/app/frontend/comments/down_vote_button.component.jsx +84 -0
- data/app/frontend/comments/down_vote_button.component.test.jsx +48 -0
- data/app/frontend/comments/up_vote.fragment.graphql +6 -0
- data/app/frontend/comments/up_vote.mutation.graphql +7 -0
- data/app/frontend/comments/up_vote_button.component.jsx +84 -0
- data/app/frontend/comments/up_vote_button.component.test.jsx +48 -0
- data/app/frontend/comments/vote_button.component.jsx +19 -0
- data/app/frontend/comments/vote_button_component.test.jsx +38 -0
- data/app/frontend/support/generate_comments_data.js +7 -2
- data/app/helpers/decidim/comments/comments_helper.rb +1 -1
- data/app/models/decidim/comments/application_record.rb +9 -0
- data/app/models/decidim/comments/comment.rb +19 -4
- data/app/models/decidim/comments/comment_vote.rb +22 -0
- data/app/models/decidim/comments/seed.rb +27 -0
- data/app/queries/decidim/comments/comments_with_replies.rb +88 -0
- data/app/resolvers/decidim/comments/vote_comment_resolver.rb +20 -0
- data/app/types/decidim/comments/comment_mutation_type.rb +19 -0
- data/app/types/decidim/comments/comment_type.rb +38 -2
- data/config/locales/en.yml +1 -0
- data/db/migrate/20161130143508_create_comments.rb +4 -2
- data/db/migrate/20161219150806_create_comment_votes.rb +13 -0
- data/lib/decidim/comments/mutation_extensions.rb +9 -0
- data/lib/decidim/comments/query_extensions.rb +3 -5
- data/lib/decidim/comments/test/factories.rb +22 -0
- metadata +26 -49
@@ -1,8 +1,5 @@
|
|
1
1
|
mutation addComment($commentableId: String!, $commentableType: String!, $body: String!, $alignment: Int) {
|
2
2
|
addComment(commentableId: $commentableId, commentableType: $commentableType, body: $body, alignment: $alignment) {
|
3
|
-
...
|
4
|
-
replies {
|
5
|
-
id
|
6
|
-
}
|
3
|
+
...CommentThread
|
7
4
|
}
|
8
5
|
}
|
@@ -6,6 +6,8 @@ import { I18n } from 'react-i18nify';
|
|
6
6
|
import classnames from 'classnames';
|
7
7
|
|
8
8
|
import AddCommentForm from './add_comment_form.component';
|
9
|
+
import UpVoteButton from './up_vote_button.component';
|
10
|
+
import DownVoteButton from './down_vote_button.component';
|
9
11
|
|
10
12
|
import commentFragment from './comment.fragment.graphql';
|
11
13
|
import commentDataFragment from './comment_data.fragment.graphql';
|
@@ -26,8 +28,7 @@ class Comment extends Component {
|
|
26
28
|
|
27
29
|
render() {
|
28
30
|
const { comment: { id, author, body, createdAt }, articleClassName } = this.props;
|
29
|
-
|
30
|
-
const formattedCreatedAt = ` ${moment(createdAt, "YYYY-MM-DD HH:mm:ss z").format("LLL")}`;
|
31
|
+
const formattedCreatedAt = ` ${moment(createdAt).format("LLL")}`;
|
31
32
|
|
32
33
|
return (
|
33
34
|
<article id={`comment_${id}`} className={articleClassName}>
|
@@ -50,10 +51,12 @@ class Comment extends Component {
|
|
50
51
|
{ body }
|
51
52
|
</p>
|
52
53
|
</div>
|
53
|
-
{this._renderReplies()}
|
54
54
|
<div className="comment__footer">
|
55
55
|
{this._renderReplyButton()}
|
56
|
+
{this._renderVoteButtons()}
|
56
57
|
</div>
|
58
|
+
{this._renderReplies()}
|
59
|
+
{this._renderAdditionalReplyButton()}
|
57
60
|
{this._renderReplyForm()}
|
58
61
|
</article>
|
59
62
|
);
|
@@ -80,7 +83,55 @@ class Comment extends Component {
|
|
80
83
|
);
|
81
84
|
}
|
82
85
|
|
83
|
-
return <
|
86
|
+
return <span> </span>;
|
87
|
+
}
|
88
|
+
|
89
|
+
/**
|
90
|
+
* Render additional reply button if user can reply the comment at the bottom of a conversation
|
91
|
+
* @private
|
92
|
+
* @returns {Void|DOMElement} - Render the reply button or not if user can reply
|
93
|
+
*/
|
94
|
+
_renderAdditionalReplyButton() {
|
95
|
+
const { comment: { canHaveReplies, hasReplies }, currentUser, isRootComment } = this.props;
|
96
|
+
const { showReplyForm } = this.state;
|
97
|
+
|
98
|
+
if (currentUser && canHaveReplies) {
|
99
|
+
if (hasReplies && isRootComment) {
|
100
|
+
|
101
|
+
return (
|
102
|
+
<div className="comment__additionalreply">
|
103
|
+
<button
|
104
|
+
className="comment__reply muted-link"
|
105
|
+
aria-controls="comment1-reply"
|
106
|
+
onClick={() => this.setState({ showReplyForm: !showReplyForm })}
|
107
|
+
>
|
108
|
+
{ I18n.t("components.comment.reply") }
|
109
|
+
</button>
|
110
|
+
</div>
|
111
|
+
);
|
112
|
+
}
|
113
|
+
}
|
114
|
+
return null;
|
115
|
+
}
|
116
|
+
|
117
|
+
/**
|
118
|
+
* Render upVote and downVote buttons when the comment is votable
|
119
|
+
* @private
|
120
|
+
* @returns {Void|DOMElement} - Render the upVote and downVote buttons or not
|
121
|
+
*/
|
122
|
+
_renderVoteButtons() {
|
123
|
+
const { comment, votable } = this.props;
|
124
|
+
|
125
|
+
if (votable) {
|
126
|
+
return (
|
127
|
+
<div className="comment__votes">
|
128
|
+
<UpVoteButton comment={comment} />
|
129
|
+
<DownVoteButton comment={comment} />
|
130
|
+
</div>
|
131
|
+
);
|
132
|
+
}
|
133
|
+
|
134
|
+
return <span> </span>;
|
84
135
|
}
|
85
136
|
|
86
137
|
/**
|
@@ -89,7 +140,7 @@ class Comment extends Component {
|
|
89
140
|
* @returns {Void|DomElement} - A wrapper element with comment replies inside
|
90
141
|
*/
|
91
142
|
_renderReplies() {
|
92
|
-
const { comment: { id, replies }, currentUser, articleClassName } = this.props;
|
143
|
+
const { comment: { id, replies }, currentUser, votable, articleClassName } = this.props;
|
93
144
|
let replyArticleClassName = 'comment comment--nested';
|
94
145
|
|
95
146
|
if (articleClassName === 'comment comment--nested') {
|
@@ -105,6 +156,7 @@ class Comment extends Component {
|
|
105
156
|
key={`comment_${id}_reply_${reply.id}`}
|
106
157
|
comment={reply}
|
107
158
|
currentUser={currentUser}
|
159
|
+
votable={votable}
|
108
160
|
articleClassName={replyArticleClassName}
|
109
161
|
/>
|
110
162
|
))
|
@@ -134,6 +186,7 @@ class Comment extends Component {
|
|
134
186
|
showTitle={false}
|
135
187
|
submitButtonClassName="button small hollow"
|
136
188
|
onCommentAdded={() => this.setState({ showReplyForm: false })}
|
189
|
+
autoFocus
|
137
190
|
/>
|
138
191
|
);
|
139
192
|
}
|
@@ -178,14 +231,19 @@ Comment.fragments = {
|
|
178
231
|
comment: gql`
|
179
232
|
${commentFragment}
|
180
233
|
${commentDataFragment}
|
234
|
+
${UpVoteButton.fragments.comment}
|
235
|
+
${DownVoteButton.fragments.comment}
|
181
236
|
`,
|
182
237
|
commentData: gql`
|
183
238
|
${commentDataFragment}
|
239
|
+
${UpVoteButton.fragments.comment}
|
240
|
+
${DownVoteButton.fragments.comment}
|
184
241
|
`
|
185
242
|
};
|
186
243
|
|
187
244
|
Comment.defaultProps = {
|
188
|
-
articleClassName: 'comment'
|
245
|
+
articleClassName: 'comment',
|
246
|
+
isRootComment: false
|
189
247
|
};
|
190
248
|
|
191
249
|
Comment.propTypes = {
|
@@ -196,7 +254,9 @@ Comment.propTypes = {
|
|
196
254
|
currentUser: PropTypes.shape({
|
197
255
|
name: PropTypes.string.isRequired
|
198
256
|
}),
|
199
|
-
articleClassName: PropTypes.string.isRequired
|
257
|
+
articleClassName: PropTypes.string.isRequired,
|
258
|
+
isRootComment: PropTypes.bool,
|
259
|
+
votable: PropTypes.bool
|
200
260
|
};
|
201
261
|
|
202
262
|
export default Comment;
|
@@ -1,12 +1,17 @@
|
|
1
|
+
/* eslint-disable no-unused-expressions */
|
1
2
|
import { shallow, mount } from 'enzyme';
|
2
3
|
import { filter } from 'graphql-anywhere';
|
3
4
|
import gql from 'graphql-tag';
|
4
5
|
|
5
6
|
import Comment from './comment.component';
|
6
7
|
import AddCommentForm from './add_comment_form.component';
|
8
|
+
import UpVoteButton from './up_vote_button.component';
|
9
|
+
import DownVoteButton from './down_vote_button.component';
|
7
10
|
|
8
11
|
import commentFragment from './comment.fragment.graphql';
|
9
12
|
import commentDataFragment from './comment_data.fragment.graphql';
|
13
|
+
import upVoteFragment from './up_vote.fragment.graphql';
|
14
|
+
import downVoteFragment from './down_vote.fragment.graphql';
|
10
15
|
|
11
16
|
import stubComponent from '../support/stub_component';
|
12
17
|
import generateCommentsData from '../support/generate_comments_data';
|
@@ -17,6 +22,8 @@ describe("<Comment />", () => {
|
|
17
22
|
let currentUser = null;
|
18
23
|
|
19
24
|
stubComponent(AddCommentForm);
|
25
|
+
stubComponent(UpVoteButton);
|
26
|
+
stubComponent(DownVoteButton);
|
20
27
|
|
21
28
|
beforeEach(() => {
|
22
29
|
let commentsData = generateCommentsData(1);
|
@@ -26,6 +33,8 @@ describe("<Comment />", () => {
|
|
26
33
|
const fragment = gql`
|
27
34
|
${commentFragment}
|
28
35
|
${commentDataFragment}
|
36
|
+
${upVoteFragment}
|
37
|
+
${downVoteFragment}
|
29
38
|
`;
|
30
39
|
|
31
40
|
comment = filter(fragment, commentsData[0]);
|
@@ -79,12 +88,34 @@ describe("<Comment />", () => {
|
|
79
88
|
expect(wrapper.find('button.comment__reply')).not.to.be.present();
|
80
89
|
});
|
81
90
|
|
82
|
-
it("should render comment
|
91
|
+
it("should not render the additional reply button if the parent comment has no replies and isRootcomment", () => {
|
92
|
+
comment.canHaveReplies = true;
|
93
|
+
comment.hasReplies = false;
|
94
|
+
const wrapper = shallow(<Comment comment={comment} currentUser={currentUser} isRootComment />);
|
95
|
+
expect(wrapper.find('div.comment__additionalreply')).not.to.be.present();
|
96
|
+
});
|
97
|
+
|
98
|
+
it("should not render the additional reply button if the parent comment has replies and not isRootcomment", () => {
|
99
|
+
comment.canHaveReplies = true;
|
100
|
+
comment.hasReplies = true;
|
83
101
|
const wrapper = shallow(<Comment comment={comment} currentUser={currentUser} />);
|
102
|
+
expect(wrapper.find('div.comment__additionalreply')).not.to.be.present();
|
103
|
+
});
|
104
|
+
|
105
|
+
it("should render the additional reply button if the parent comment has replies and isRootcomment", () => {
|
106
|
+
comment.canHaveReplies = true;
|
107
|
+
comment.hasReplies = true;
|
108
|
+
const wrapper = shallow(<Comment comment={comment} currentUser={currentUser} isRootComment />);
|
109
|
+
expect(wrapper.find('div.comment__additionalreply')).to.be.present();
|
110
|
+
});
|
111
|
+
|
112
|
+
it("should render comment replies a separate Comment components", () => {
|
113
|
+
const wrapper = shallow(<Comment comment={comment} currentUser={currentUser} votable />);
|
84
114
|
wrapper.find(Comment).forEach((node, idx) => {
|
85
115
|
expect(node).to.have.prop("comment").deep.equal(comment.replies[idx]);
|
86
116
|
expect(node).to.have.prop("currentUser").deep.equal(currentUser);
|
87
117
|
expect(node).to.have.prop("articleClassName").equal("comment comment--nested")
|
118
|
+
expect(node).to.have.prop("votable").equal(true);
|
88
119
|
});
|
89
120
|
});
|
90
121
|
|
@@ -100,6 +131,11 @@ describe("<Comment />", () => {
|
|
100
131
|
expect(wrapper).to.have.prop("articleClassName").equal("comment");
|
101
132
|
});
|
102
133
|
|
134
|
+
it("should have a default prop isRootComment with value false", () => {
|
135
|
+
const wrapper = mount(<Comment comment={comment} currentUser={currentUser} />);
|
136
|
+
expect(wrapper).to.have.prop("isRootComment").equal(false);
|
137
|
+
});
|
138
|
+
|
103
139
|
describe("when user is not logged in", () => {
|
104
140
|
beforeEach(() => {
|
105
141
|
currentUser = null;
|
@@ -122,4 +158,16 @@ describe("<Comment />", () => {
|
|
122
158
|
const wrapper = shallow(<Comment comment={comment} currentUser={currentUser} />);
|
123
159
|
expect(wrapper.find('span.alert.label')).to.have.text('Against');
|
124
160
|
});
|
161
|
+
|
162
|
+
describe("when the comment is votable", () => {
|
163
|
+
it("should render an UpVoteButton component", () => {
|
164
|
+
const wrapper = shallow(<Comment comment={comment} currentUser={currentUser} votable />);
|
165
|
+
expect(wrapper.find(UpVoteButton)).to.have.prop("comment").deep.equal(comment);
|
166
|
+
})
|
167
|
+
|
168
|
+
it("should render an DownVoteButton component", () => {
|
169
|
+
const wrapper = shallow(<Comment comment={comment} currentUser={currentUser} votable />);
|
170
|
+
expect(wrapper.find(DownVoteButton)).to.have.prop("comment").deep.equal(comment);
|
171
|
+
})
|
172
|
+
});
|
125
173
|
});
|
@@ -1,4 +1,4 @@
|
|
1
|
-
import { Component } from 'react';
|
1
|
+
import { Component, PropTypes } from 'react';
|
2
2
|
import { I18n } from 'react-i18nify';
|
3
3
|
|
4
4
|
/**
|
@@ -7,22 +7,67 @@ import { I18n } from 'react-i18nify';
|
|
7
7
|
* @augments Component
|
8
8
|
* @todo Needs a proper implementation
|
9
9
|
*/
|
10
|
-
|
10
|
+
class CommentOrderSelector extends Component {
|
11
|
+
|
12
|
+
constructor(props) {
|
13
|
+
super(props);
|
14
|
+
this.state = {
|
15
|
+
orderBy: this.props.defaultOrderBy
|
16
|
+
}
|
17
|
+
}
|
18
|
+
|
19
|
+
componentDidMount() {
|
20
|
+
$(document).foundation();
|
21
|
+
}
|
22
|
+
|
11
23
|
render() {
|
24
|
+
const { orderBy } = this.state;
|
25
|
+
|
12
26
|
return (
|
13
27
|
<div className="order-by__dropdown order-by__dropdown--right">
|
14
28
|
<span className="order-by__text">{ I18n.t("components.comment_order_selector.title") }</span>
|
15
29
|
<ul className="dropdown menu" data-dropdown-menu>
|
16
30
|
<li>
|
17
|
-
<a>{ I18n.t(
|
31
|
+
<a>{ I18n.t(`components.comment_order_selector.${orderBy}`) }</a>
|
18
32
|
<ul className="menu">
|
19
|
-
<li
|
20
|
-
|
21
|
-
|
33
|
+
<li>
|
34
|
+
<a href="" className="test" onClick={(event) => this._updateOrder(event, "best_rated")} >
|
35
|
+
{ I18n.t("components.comment_order_selector.order.best_rated") }
|
36
|
+
</a>
|
37
|
+
</li>
|
38
|
+
<li>
|
39
|
+
<a href="" onClick={(event) => this._updateOrder(event, "recent")} >
|
40
|
+
{ I18n.t("components.comment_order_selector.order.recent") }
|
41
|
+
</a>
|
42
|
+
</li>
|
43
|
+
<li>
|
44
|
+
<a href="" onClick={(event) => this._updateOrder(event, "older")} >
|
45
|
+
{ I18n.t("components.comment_order_selector.order.older") }
|
46
|
+
</a>
|
47
|
+
</li>
|
48
|
+
<li>
|
49
|
+
<a href="" onClick={(event) => this._updateOrder(event, "most_discussed")} >
|
50
|
+
{ I18n.t("components.comment_order_selector.order.most_discussed") }
|
51
|
+
</a>
|
52
|
+
</li>
|
22
53
|
</ul>
|
23
54
|
</li>
|
24
55
|
</ul>
|
25
56
|
</div>
|
26
57
|
);
|
27
58
|
}
|
59
|
+
|
60
|
+
_updateOrder(event, orderBy) {
|
61
|
+
event.preventDefault();
|
62
|
+
this.setState({ orderBy });
|
63
|
+
this.props.reorderComments(orderBy);
|
64
|
+
}
|
65
|
+
|
28
66
|
}
|
67
|
+
|
68
|
+
CommentOrderSelector.propTypes = {
|
69
|
+
reorderComments: PropTypes.func.isRequired,
|
70
|
+
defaultOrderBy: PropTypes.string.isRequired
|
71
|
+
};
|
72
|
+
|
73
|
+
export default CommentOrderSelector;
|
@@ -2,8 +2,19 @@ import { shallow } from 'enzyme';
|
|
2
2
|
import CommentOrderSelector from './comment_order_selector.component';
|
3
3
|
|
4
4
|
describe('<CommentOrderSelector />', () => {
|
5
|
+
const orderBy = "older";
|
6
|
+
const reorderComments = sinon.spy();
|
7
|
+
|
5
8
|
it("renders a div with classes order-by__dropdown order-by__dropdown--right", () => {
|
6
|
-
const wrapper = shallow(<CommentOrderSelector />);
|
9
|
+
const wrapper = shallow(<CommentOrderSelector reorderComments={reorderComments} defaultOrderBy={orderBy} />);
|
7
10
|
expect(wrapper.find('div.order-by__dropdown.order-by__dropdown--right')).to.present();
|
8
11
|
})
|
12
|
+
|
13
|
+
it("should set state order to best_rated if user clicks on the first element", () => {
|
14
|
+
const preventDefault = sinon.spy();
|
15
|
+
const wrapper = shallow(<CommentOrderSelector reorderComments={reorderComments} defaultOrderBy={orderBy} />);
|
16
|
+
wrapper.find('a.test').simulate('click', {preventDefault});
|
17
|
+
expect(reorderComments).to.calledWith("best_rated");
|
18
|
+
});
|
9
19
|
})
|
20
|
+
|
@@ -15,13 +15,18 @@ import commentThreadFragment from './comment_thread.fragment.graphql'
|
|
15
15
|
*/
|
16
16
|
class CommentThread extends Component {
|
17
17
|
render() {
|
18
|
-
const { comment, currentUser } = this.props;
|
18
|
+
const { comment, currentUser, votable } = this.props;
|
19
19
|
|
20
20
|
return (
|
21
21
|
<div>
|
22
22
|
{this._renderTitle()}
|
23
23
|
<div className="comment-thread">
|
24
|
-
<Comment
|
24
|
+
<Comment
|
25
|
+
comment={filter(Comment.fragments.comment, comment)}
|
26
|
+
currentUser={currentUser}
|
27
|
+
votable={votable}
|
28
|
+
isRootComment
|
29
|
+
/>
|
25
30
|
</div>
|
26
31
|
</div>
|
27
32
|
);
|
@@ -33,9 +38,9 @@ class CommentThread extends Component {
|
|
33
38
|
* @returns {Void|DOMElement} - The conversation's title
|
34
39
|
*/
|
35
40
|
_renderTitle() {
|
36
|
-
const { comment: { author,
|
41
|
+
const { comment: { author, hasReplies } } = this.props;
|
37
42
|
|
38
|
-
if (
|
43
|
+
if (hasReplies) {
|
39
44
|
return (
|
40
45
|
<h6 className="comment-thread__title">
|
41
46
|
{ I18n.t("components.comment_thread.title", { authorName: author.name }) }
|
@@ -58,7 +63,8 @@ CommentThread.propTypes = {
|
|
58
63
|
currentUser: PropTypes.shape({
|
59
64
|
name: PropTypes.string.isRequired
|
60
65
|
}),
|
61
|
-
comment: propType(CommentThread.fragments.comment).isRequired
|
66
|
+
comment: propType(CommentThread.fragments.comment).isRequired,
|
67
|
+
votable: PropTypes.bool
|
62
68
|
};
|
63
69
|
|
64
70
|
export default CommentThread;
|
@@ -48,7 +48,7 @@ describe('<CommentThread />', () => {
|
|
48
48
|
|
49
49
|
describe("when comment does have replies", () => {
|
50
50
|
beforeEach(() => {
|
51
|
-
comment.
|
51
|
+
comment.hasReplies = true;
|
52
52
|
});
|
53
53
|
|
54
54
|
it("should render a h6 comment-thread__title with author name", () => {
|
@@ -66,6 +66,16 @@ describe('<CommentThread />', () => {
|
|
66
66
|
it("and pass filter comment data as a prop to it", () => {
|
67
67
|
const wrapper = shallow(<CommentThread comment={comment} currentUser={currentUser} />);
|
68
68
|
expect(wrapper.find(Comment).first()).to.have.prop("comment").deep.equal(filter(commentFragment, comment));
|
69
|
-
});
|
70
|
-
|
69
|
+
});
|
70
|
+
|
71
|
+
it("and pass the votable as a prop to it", () => {
|
72
|
+
const wrapper = shallow(<CommentThread comment={comment} currentUser={currentUser} votable />);
|
73
|
+
expect(wrapper.find(Comment).first()).to.have.prop("votable").equal(true);
|
74
|
+
});
|
75
|
+
|
76
|
+
it("and pass the isRootComment equal true", () => {
|
77
|
+
const wrapper = shallow(<CommentThread comment={comment} currentUser={currentUser} votable isRootComment />);
|
78
|
+
expect(wrapper.find(Comment).first()).to.have.prop("isRootComment").equal(true);
|
79
|
+
});
|
80
|
+
});
|
71
81
|
});
|
@@ -8,6 +8,7 @@ import Application from '../application/application.component';
|
|
8
8
|
|
9
9
|
import CommentThread from './comment_thread.component';
|
10
10
|
import AddCommentForm from './add_comment_form.component';
|
11
|
+
import CommentOrderSelector from './comment_order_selector.component';
|
11
12
|
|
12
13
|
import commentsQuery from './comments.query.graphql';
|
13
14
|
|
@@ -20,15 +21,26 @@ import commentsQuery from './comments.query.graphql';
|
|
20
21
|
*/
|
21
22
|
export class Comments extends Component {
|
22
23
|
render() {
|
23
|
-
const { comments } = this.props;
|
24
|
+
const { comments, reorderComments, orderBy, loading } = this.props;
|
25
|
+
let commentClasses = "comments";
|
26
|
+
let commentHeader = I18n.t("components.comments.title", { count: comments.length });
|
27
|
+
|
28
|
+
if (loading) {
|
29
|
+
commentClasses += " loading-comments"
|
30
|
+
commentHeader = I18n.t("components.comments.loading");
|
31
|
+
}
|
24
32
|
|
25
33
|
return (
|
26
34
|
<div className="columns large-9" id="comments">
|
27
|
-
<section className=
|
35
|
+
<section className={commentClasses}>
|
28
36
|
<div className="row collapse order-by">
|
29
37
|
<h2 className="order-by__text section-heading">
|
30
|
-
{
|
38
|
+
{ commentHeader }
|
31
39
|
</h2>
|
40
|
+
<CommentOrderSelector
|
41
|
+
reorderComments={reorderComments}
|
42
|
+
defaultOrderBy={orderBy}
|
43
|
+
/>
|
32
44
|
</div>
|
33
45
|
{this._renderCommentThreads()}
|
34
46
|
{this._renderAddCommentForm()}
|
@@ -36,24 +48,25 @@ export class Comments extends Component {
|
|
36
48
|
</div>
|
37
49
|
);
|
38
50
|
}
|
39
|
-
|
51
|
+
|
40
52
|
/**
|
41
53
|
* Iterates the comment's collection and render a CommentThread for each one
|
42
54
|
* @private
|
43
55
|
* @returns {ReactComponent[]} - A collection of CommentThread components
|
44
56
|
*/
|
45
57
|
_renderCommentThreads() {
|
46
|
-
const { comments, currentUser } = this.props;
|
58
|
+
const { comments, currentUser, options: { votable } } = this.props;
|
47
59
|
|
48
60
|
return comments.map((comment) => (
|
49
61
|
<CommentThread
|
50
62
|
key={comment.id}
|
51
63
|
comment={filter(CommentThread.fragments.comment, comment)}
|
52
64
|
currentUser={currentUser}
|
65
|
+
votable={votable}
|
53
66
|
/>
|
54
67
|
))
|
55
68
|
}
|
56
|
-
|
69
|
+
|
57
70
|
/**
|
58
71
|
* If current user is present it renders the add comment form
|
59
72
|
* @private
|
@@ -61,7 +74,7 @@ export class Comments extends Component {
|
|
61
74
|
*/
|
62
75
|
_renderAddCommentForm() {
|
63
76
|
const { currentUser, commentableId, commentableType, options: { arguable } } = this.props;
|
64
|
-
|
77
|
+
|
65
78
|
if (currentUser) {
|
66
79
|
return (
|
67
80
|
<AddCommentForm
|
@@ -78,6 +91,7 @@ export class Comments extends Component {
|
|
78
91
|
}
|
79
92
|
|
80
93
|
Comments.propTypes = {
|
94
|
+
loading: PropTypes.bool,
|
81
95
|
comments: PropTypes.arrayOf(PropTypes.shape({
|
82
96
|
id: PropTypes.string.isRequired
|
83
97
|
})),
|
@@ -88,7 +102,9 @@ Comments.propTypes = {
|
|
88
102
|
commentableType: PropTypes.string.isRequired,
|
89
103
|
options: PropTypes.shape({
|
90
104
|
arguable: PropTypes.bool
|
91
|
-
}).isRequired
|
105
|
+
}).isRequired,
|
106
|
+
orderBy: PropTypes.string.isRequired,
|
107
|
+
reorderComments: PropTypes.func.isRequired
|
92
108
|
};
|
93
109
|
|
94
110
|
/**
|
@@ -99,13 +115,22 @@ const CommentsWithData = graphql(gql`
|
|
99
115
|
${commentsQuery}
|
100
116
|
${CommentThread.fragments.comment}
|
101
117
|
`, {
|
102
|
-
options: {
|
103
|
-
|
118
|
+
options: {
|
119
|
+
pollInterval: 15000
|
120
|
+
},
|
121
|
+
props: ({ ownProps, data: {loading, currentUser, comments, refetch }}) => ({
|
122
|
+
loading: loading,
|
104
123
|
comments: comments || [],
|
105
124
|
currentUser: currentUser || null,
|
106
125
|
commentableId: ownProps.commentableId,
|
107
126
|
commentableType: ownProps.commentableType,
|
108
|
-
|
127
|
+
orderBy: ownProps.orderBy,
|
128
|
+
options: ownProps.options,
|
129
|
+
reorderComments: (orderBy) => {
|
130
|
+
return refetch({
|
131
|
+
orderBy
|
132
|
+
});
|
133
|
+
}
|
109
134
|
})
|
110
135
|
})(Comments);
|
111
136
|
|
@@ -120,6 +145,7 @@ const CommentsApplication = ({ locale, commentableId, commentableType, options }
|
|
120
145
|
commentableId={commentableId}
|
121
146
|
commentableType={commentableType}
|
122
147
|
options={options}
|
148
|
+
orderBy="older"
|
123
149
|
/>
|
124
150
|
</Application>
|
125
151
|
);
|