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.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/Rakefile +1 -1
  3. data/app/assets/javascripts/decidim/comments/bundle.js +43 -44
  4. data/app/assets/javascripts/decidim/comments/bundle.js.map +1 -1
  5. data/app/commands/decidim/comments/vote_comment.rb +38 -0
  6. data/app/frontend/application/icon.component.jsx +9 -4
  7. data/app/frontend/application/icon.component.test.jsx +12 -2
  8. data/app/frontend/comments/add_comment_form.component.jsx +43 -11
  9. data/app/frontend/comments/add_comment_form.mutation.graphql +1 -4
  10. data/app/frontend/comments/comment.component.jsx +67 -7
  11. data/app/frontend/comments/comment.component.test.jsx +49 -1
  12. data/app/frontend/comments/comment_data.fragment.graphql +3 -0
  13. data/app/frontend/comments/comment_order_selector.component.jsx +51 -6
  14. data/app/frontend/comments/comment_order_selector.component.test.jsx +12 -1
  15. data/app/frontend/comments/comment_thread.component.jsx +11 -5
  16. data/app/frontend/comments/comment_thread.component.test.jsx +13 -3
  17. data/app/frontend/comments/comment_thread.fragment.graphql +1 -3
  18. data/app/frontend/comments/comments.component.jsx +37 -11
  19. data/app/frontend/comments/comments.component.test.jsx +44 -7
  20. data/app/frontend/comments/comments.query.graphql +2 -2
  21. data/app/frontend/comments/down_vote.fragment.graphql +6 -0
  22. data/app/frontend/comments/down_vote.mutation.graphql +7 -0
  23. data/app/frontend/comments/down_vote_button.component.jsx +84 -0
  24. data/app/frontend/comments/down_vote_button.component.test.jsx +48 -0
  25. data/app/frontend/comments/up_vote.fragment.graphql +6 -0
  26. data/app/frontend/comments/up_vote.mutation.graphql +7 -0
  27. data/app/frontend/comments/up_vote_button.component.jsx +84 -0
  28. data/app/frontend/comments/up_vote_button.component.test.jsx +48 -0
  29. data/app/frontend/comments/vote_button.component.jsx +19 -0
  30. data/app/frontend/comments/vote_button_component.test.jsx +38 -0
  31. data/app/frontend/support/generate_comments_data.js +7 -2
  32. data/app/helpers/decidim/comments/comments_helper.rb +1 -1
  33. data/app/models/decidim/comments/application_record.rb +9 -0
  34. data/app/models/decidim/comments/comment.rb +19 -4
  35. data/app/models/decidim/comments/comment_vote.rb +22 -0
  36. data/app/models/decidim/comments/seed.rb +27 -0
  37. data/app/queries/decidim/comments/comments_with_replies.rb +88 -0
  38. data/app/resolvers/decidim/comments/vote_comment_resolver.rb +20 -0
  39. data/app/types/decidim/comments/comment_mutation_type.rb +19 -0
  40. data/app/types/decidim/comments/comment_type.rb +38 -2
  41. data/config/locales/en.yml +1 -0
  42. data/db/migrate/20161130143508_create_comments.rb +4 -2
  43. data/db/migrate/20161219150806_create_comment_votes.rb +13 -0
  44. data/lib/decidim/comments/mutation_extensions.rb +9 -0
  45. data/lib/decidim/comments/query_extensions.rb +3 -5
  46. data/lib/decidim/comments/test/factories.rb +22 -0
  47. 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
- ...CommentData
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 <div>&nbsp;</div>;
86
+ return <span>&nbsp;</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>&nbsp;</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 replies a separate Comment components", () => {
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
  });
@@ -6,6 +6,9 @@ fragment CommentData on Comment {
6
6
  name,
7
7
  avatarUrl
8
8
  }
9
+ hasReplies
9
10
  canHaveReplies
10
11
  alignment
12
+ ...UpVote
13
+ ...DownVote
11
14
  }
@@ -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
- export default class CommentOrderSelector extends Component {
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("components.comment_order_selector.order.most_voted") }</a>
31
+ <a>{ I18n.t(`components.comment_order_selector.${orderBy}`) }</a>
18
32
  <ul className="menu">
19
- <li><a>{ I18n.t("components.comment_order_selector.order.most_voted") }</a></li>
20
- <li><a>{ I18n.t("components.comment_order_selector.order.recent") }</a></li>
21
- <li><a>{ I18n.t("components.comment_order_selector.order.older") }</a></li>
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 comment={filter(Comment.fragments.comment, comment)} currentUser={currentUser} />
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, replies } } = this.props;
41
+ const { comment: { author, hasReplies } } = this.props;
37
42
 
38
- if (replies.length > 0) {
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.replies = generateCommentsData(3);
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
  });
@@ -2,8 +2,6 @@ fragment CommentThread on Comment {
2
2
  author {
3
3
  name
4
4
  }
5
- replies {
6
- id
7
- }
5
+ hasReplies,
8
6
  ...Comment
9
7
  }
@@ -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="comments">
35
+ <section className={commentClasses}>
28
36
  <div className="row collapse order-by">
29
37
  <h2 className="order-by__text section-heading">
30
- { I18n.t("components.comments.title", { count: comments.length }) }
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: { pollInterval: 15000 },
103
- props: ({ ownProps, data: { currentUser, comments }}) => ({
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
- options: ownProps.options
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
  );