decidim-comments 0.3.2 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (27) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/javascripts/decidim/comments/bundle.js +30 -30
  3. data/app/assets/javascripts/decidim/comments/bundle.js.map +1 -1
  4. data/app/commands/decidim/comments/create_comment.rb +10 -14
  5. data/app/frontend/application/application.component.tsx +1 -1
  6. data/app/frontend/comments/add_comment_form.component.test.tsx +33 -26
  7. data/app/frontend/comments/add_comment_form.component.tsx +109 -83
  8. data/app/frontend/comments/comment.component.test.tsx +29 -24
  9. data/app/frontend/comments/comment.component.tsx +12 -5
  10. data/app/frontend/comments/comment_thread.component.test.tsx +12 -7
  11. data/app/frontend/comments/comment_thread.component.tsx +7 -2
  12. data/app/frontend/comments/comments.component.tsx +26 -15
  13. data/app/frontend/comments/down_vote_button.component.test.tsx +9 -4
  14. data/app/frontend/comments/down_vote_button.component.tsx +56 -37
  15. data/app/frontend/comments/up_vote_button.component.test.tsx +9 -4
  16. data/app/frontend/comments/up_vote_button.component.tsx +31 -16
  17. data/app/frontend/comments/vote_button.component.tsx +3 -0
  18. data/app/mailers/decidim/comments/comment_notification_mailer.rb +8 -6
  19. data/app/models/decidim/comments/comment.rb +13 -1
  20. data/app/views/decidim/comments/comment_notification_mailer/comment_created.html.erb +1 -1
  21. data/app/views/decidim/comments/comment_notification_mailer/reply_created.html.erb +1 -1
  22. data/config/locales/ca.yml +2 -0
  23. data/config/locales/en.yml +2 -0
  24. data/config/locales/es.yml +2 -0
  25. data/config/locales/fr.yml +2 -2
  26. data/lib/decidim/comments/commentable.rb +1 -0
  27. metadata +6 -6
@@ -9,6 +9,7 @@ import DownVoteButton from "./down_vote_button.component";
9
9
  import UpVoteButton from "./up_vote_button.component";
10
10
 
11
11
  import {
12
+ AddCommentFormCommentableFragment,
12
13
  AddCommentFormSessionFragment,
13
14
  CommentFragment,
14
15
  } from "../support/schema";
@@ -23,6 +24,8 @@ interface CommentProps {
23
24
  articleClassName?: string;
24
25
  isRootComment?: boolean;
25
26
  votable?: boolean;
27
+ rootCommentable: AddCommentFormCommentableFragment;
28
+ orderBy: string;
26
29
  }
27
30
 
28
31
  interface CommentState {
@@ -202,13 +205,13 @@ class Comment extends React.Component<CommentProps, CommentState> {
202
205
  * @returns {Void|DOMElement} - Render the upVote and downVote buttons or not
203
206
  */
204
207
  private _renderVoteButtons() {
205
- const { session, comment, votable } = this.props;
208
+ const { session, comment, votable, rootCommentable, orderBy } = this.props;
206
209
 
207
210
  if (votable) {
208
211
  return (
209
212
  <div className="comment__votes">
210
- <UpVoteButton session={session} comment={comment} />
211
- <DownVoteButton session={session} comment={comment} />
213
+ <UpVoteButton session={session} comment={comment} rootCommentable={rootCommentable} orderBy={orderBy} />
214
+ <DownVoteButton session={session} comment={comment} rootCommentable={rootCommentable} orderBy={orderBy} />
212
215
  </div>
213
216
  );
214
217
  }
@@ -222,7 +225,7 @@ class Comment extends React.Component<CommentProps, CommentState> {
222
225
  * @returns {Void|DomElement} - A wrapper element with comment's comments inside
223
226
  */
224
227
  private _renderReplies() {
225
- const { comment: { id, hasComments, comments }, session, votable, articleClassName } = this.props;
228
+ const { comment: { id, hasComments, comments }, session, votable, articleClassName, rootCommentable, orderBy } = this.props;
226
229
  let replyArticleClassName = "comment comment--nested";
227
230
 
228
231
  if (articleClassName === "comment comment--nested") {
@@ -240,6 +243,8 @@ class Comment extends React.Component<CommentProps, CommentState> {
240
243
  session={session}
241
244
  votable={votable}
242
245
  articleClassName={replyArticleClassName}
246
+ rootCommentable={rootCommentable}
247
+ orderBy={orderBy}
243
248
  />
244
249
  ))
245
250
  }
@@ -256,7 +261,7 @@ class Comment extends React.Component<CommentProps, CommentState> {
256
261
  * @returns {Void|ReactElement} - Render the AddCommentForm component or not
257
262
  */
258
263
  private _renderReplyForm() {
259
- const { session, comment } = this.props;
264
+ const { session, comment, rootCommentable, orderBy } = this.props;
260
265
  const { showReplyForm } = this.state;
261
266
 
262
267
  if (session && showReplyForm) {
@@ -268,6 +273,8 @@ class Comment extends React.Component<CommentProps, CommentState> {
268
273
  submitButtonClassName="button small hollow"
269
274
  onCommentAdded={this.toggleReplyForm}
270
275
  autoFocus={true}
276
+ rootCommentable={rootCommentable}
277
+ orderBy={orderBy}
271
278
  />
272
279
  );
273
280
  }
@@ -10,6 +10,11 @@ import generateCUserData from "../support/generate_user_data";
10
10
  import { loadLocaleTranslations } from "../support/load_translations";
11
11
 
12
12
  describe("<CommentThread />", () => {
13
+ const orderBy = "older";
14
+ const rootCommentable = {
15
+ id: "1",
16
+ type: "Decidim::DummyResource",
17
+ };
13
18
  let comment: CommentFragment;
14
19
  let session: any = null;
15
20
 
@@ -25,7 +30,7 @@ describe("<CommentThread />", () => {
25
30
 
26
31
  describe("when comment doesn't have comments", () => {
27
32
  it("should not render a title with author name", () => {
28
- const wrapper = shallow(<CommentThread comment={comment} session={session} />);
33
+ const wrapper = shallow(<CommentThread comment={comment} session={session} rootCommentable={rootCommentable} orderBy={orderBy} />);
29
34
  expect(wrapper.find("h6.comment-thread__title").exists()).toBeFalsy();
30
35
  });
31
36
  });
@@ -36,7 +41,7 @@ describe("<CommentThread />", () => {
36
41
  });
37
42
 
38
43
  it("should render a h6 comment-thread__title with author name", () => {
39
- const wrapper = shallow(<CommentThread comment={comment} session={session} />);
44
+ const wrapper = shallow(<CommentThread comment={comment} session={session} rootCommentable={rootCommentable} orderBy={orderBy} />);
40
45
  expect(wrapper.find("h6.comment-thread__title").text()).toContain(`Conversation with ${comment.author.name}`);
41
46
  });
42
47
 
@@ -46,7 +51,7 @@ describe("<CommentThread />", () => {
46
51
  });
47
52
 
48
53
  it("should render a h6 comment-thread__title with 'Deleted user'", () => {
49
- const wrapper = shallow(<CommentThread comment={comment} session={session} />);
54
+ const wrapper = shallow(<CommentThread comment={comment} session={session} rootCommentable={rootCommentable} orderBy={orderBy} />);
50
55
  expect(wrapper.find("h6.comment-thread__title").text()).toContain("Conversation with Deleted user");
51
56
  });
52
57
  });
@@ -54,22 +59,22 @@ describe("<CommentThread />", () => {
54
59
 
55
60
  describe("should render a Comment", () => {
56
61
  it("and pass the session as a prop to it", () => {
57
- const wrapper = shallow(<CommentThread comment={comment} session={session} />);
62
+ const wrapper = shallow(<CommentThread comment={comment} session={session} rootCommentable={rootCommentable} orderBy={orderBy} />);
58
63
  expect(wrapper.find(Comment).first().props()).toHaveProperty("session", session);
59
64
  });
60
65
 
61
66
  it("and pass comment data as a prop to it", () => {
62
- const wrapper = shallow(<CommentThread comment={comment} session={session} />);
67
+ const wrapper = shallow(<CommentThread comment={comment} session={session} rootCommentable={rootCommentable} orderBy={orderBy} />);
63
68
  expect(wrapper.find(Comment).first().props()).toHaveProperty("comment", comment);
64
69
  });
65
70
 
66
71
  it("and pass the votable as a prop to it", () => {
67
- const wrapper = shallow(<CommentThread comment={comment} session={session} votable={true} />);
72
+ const wrapper = shallow(<CommentThread comment={comment} session={session} votable={true} rootCommentable={rootCommentable} orderBy={orderBy} />);
68
73
  expect(wrapper.find(Comment).first().props()).toHaveProperty("votable", true);
69
74
  });
70
75
 
71
76
  it("and pass the isRootComment equal true", () => {
72
- const wrapper = shallow(<CommentThread comment={comment} session={session} votable={true} />);
77
+ const wrapper = shallow(<CommentThread comment={comment} session={session} votable={true} rootCommentable={rootCommentable} orderBy={orderBy} />);
73
78
  expect(wrapper.find(Comment).first().props()).toHaveProperty("isRootComment", true);
74
79
  });
75
80
  });
@@ -3,6 +3,7 @@ import * as React from "react";
3
3
  import Comment from "./comment.component";
4
4
 
5
5
  import {
6
+ AddCommentFormCommentableFragment,
6
7
  AddCommentFormSessionFragment,
7
8
  CommentFragment,
8
9
  } from "../support/schema";
@@ -15,6 +16,8 @@ interface CommentThreadProps {
15
16
  user: any;
16
17
  } | null;
17
18
  votable?: boolean;
19
+ rootCommentable: AddCommentFormCommentableFragment;
20
+ orderBy: string;
18
21
  }
19
22
 
20
23
  /**
@@ -23,14 +26,14 @@ interface CommentThreadProps {
23
26
  * @augments Component
24
27
  * @todo It doesn't handle multiple comments yet
25
28
  */
26
- class CommentThread extends React.Component<CommentThreadProps, undefined> {
29
+ class CommentThread extends React.Component<CommentThreadProps> {
27
30
  public static defaultProps: any = {
28
31
  session: null,
29
32
  votable: false,
30
33
  };
31
34
 
32
35
  public render() {
33
- const { comment, session, votable } = this.props;
36
+ const { comment, session, votable, rootCommentable, orderBy } = this.props;
34
37
 
35
38
  return (
36
39
  <div>
@@ -41,6 +44,8 @@ class CommentThread extends React.Component<CommentThreadProps, undefined> {
41
44
  session={session}
42
45
  votable={votable}
43
46
  isRootComment={true}
47
+ rootCommentable={rootCommentable}
48
+ orderBy={orderBy}
44
49
  />
45
50
  </div>
46
51
  </div>
@@ -27,7 +27,7 @@ interface CommentsProps extends GetCommentsQuery {
27
27
  * @class
28
28
  * @augments Component
29
29
  */
30
- export class Comments extends React.Component<CommentsProps, undefined> {
30
+ export class Comments extends React.Component<CommentsProps> {
31
31
  public static defaultProps: any = {
32
32
  loading: false,
33
33
  session: null,
@@ -91,7 +91,8 @@ export class Comments extends React.Component<CommentsProps, undefined> {
91
91
  * @returns {ReactComponent[]} - A collection of CommentThread components
92
92
  */
93
93
  private _renderCommentThreads() {
94
- const { session, commentable: { comments, commentsHaveVotes } } = this.props;
94
+ const { session, commentable, orderBy } = this.props;
95
+ const { comments, commentsHaveVotes } = commentable;
95
96
 
96
97
  return comments.map((comment) => (
97
98
  <CommentThread
@@ -99,6 +100,8 @@ export class Comments extends React.Component<CommentsProps, undefined> {
99
100
  comment={comment}
100
101
  session={session}
101
102
  votable={commentsHaveVotes}
103
+ rootCommentable={commentable}
104
+ orderBy={orderBy}
102
105
  />
103
106
  ));
104
107
  }
@@ -109,7 +112,7 @@ export class Comments extends React.Component<CommentsProps, undefined> {
109
112
  * @returns {Void|ReactComponent} - A AddCommentForm component or nothing
110
113
  */
111
114
  private _renderAddCommentForm() {
112
- const { session, commentable } = this.props;
115
+ const { session, commentable, orderBy } = this.props;
113
116
  const { acceptsNewComments, commentsHaveAlignment } = commentable;
114
117
 
115
118
  if (acceptsNewComments) {
@@ -118,6 +121,8 @@ export class Comments extends React.Component<CommentsProps, undefined> {
118
121
  session={session}
119
122
  commentable={commentable}
120
123
  arguable={commentsHaveAlignment}
124
+ rootCommentable={commentable}
125
+ orderBy={orderBy}
121
126
  />
122
127
  );
123
128
  }
@@ -135,21 +140,27 @@ window.Comments = Comments;
135
140
 
136
141
  export const commentsQuery = require("../queries/comments.query.graphql");
137
142
 
138
- const CommentsWithData: any = graphql(commentsQuery, {
143
+ const CommentsWithData: any = graphql<GetCommentsQuery, CommentsProps>(commentsQuery, {
139
144
  options: {
140
145
  pollInterval: 15000,
141
146
  },
142
- props: ({ ownProps, data: { loading, session, commentable, refetch }}) => ({
143
- loading,
144
- session,
145
- commentable,
146
- orderBy: ownProps.orderBy,
147
- reorderComments: (orderBy: string) => {
148
- return refetch({
149
- orderBy,
150
- });
151
- },
152
- }),
147
+ props: ({ ownProps, data }) => {
148
+ if (data) {
149
+ const { loading, session, commentable, refetch } = data;
150
+
151
+ return {
152
+ loading,
153
+ session,
154
+ commentable,
155
+ orderBy: ownProps.orderBy,
156
+ reorderComments: (orderBy: string) => {
157
+ return refetch({
158
+ orderBy,
159
+ });
160
+ },
161
+ };
162
+ }
163
+ },
153
164
  })(Comments);
154
165
 
155
166
  export interface CommentsApplicationProps extends GetCommentsQueryVariables {
@@ -10,6 +10,11 @@ import generateUserData from "../support/generate_user_data";
10
10
  import { DownVoteButtonFragment } from "../support/schema";
11
11
 
12
12
  describe("<DownVoteButton />", () => {
13
+ const orderBy = "older";
14
+ const rootCommentable = {
15
+ id: "1",
16
+ type: "Decidim::DummyResource",
17
+ };
13
18
  let comment: DownVoteButtonFragment;
14
19
  let session: any = null;
15
20
  const downVote = jasmine.createSpy("downVote");
@@ -23,7 +28,7 @@ describe("<DownVoteButton />", () => {
23
28
  });
24
29
 
25
30
  it("should render a VoteButton component with the correct props", () => {
26
- const wrapper = shallow(<DownVoteButton session={session} comment={comment} downVote={downVote} />);
31
+ const wrapper = shallow(<DownVoteButton session={session} comment={comment} downVote={downVote} rootCommentable={rootCommentable} orderBy={orderBy} />);
27
32
  expect(wrapper.find(VoteButton).prop("buttonClassName")).toEqual("comment__votes--down");
28
33
  expect(wrapper.find(VoteButton).prop("iconName")).toEqual("icon-chevron-bottom");
29
34
  expect(wrapper.find(VoteButton).prop("votes")).toEqual(comment.downVotes);
@@ -31,13 +36,13 @@ describe("<DownVoteButton />", () => {
31
36
 
32
37
  it("should pass disabled prop as true if comment downVoted is true", () => {
33
38
  comment.downVoted = true;
34
- const wrapper = shallow(<DownVoteButton session={session} comment={comment} downVote={downVote} />);
39
+ const wrapper = shallow(<DownVoteButton session={session} comment={comment} downVote={downVote} rootCommentable={rootCommentable} orderBy={orderBy} />);
35
40
  expect(wrapper.find(VoteButton).prop("disabled")).toBeTruthy();
36
41
  });
37
42
 
38
43
  it("should pass disabled prop as true if comment downVoted is true", () => {
39
44
  comment.downVoted = true;
40
- const wrapper = shallow(<DownVoteButton session={session} comment={comment} downVote={downVote} />);
45
+ const wrapper = shallow(<DownVoteButton session={session} comment={comment} downVote={downVote} rootCommentable={rootCommentable} orderBy={orderBy} />);
41
46
  expect(wrapper.find(VoteButton).prop("disabled")).toBeTruthy();
42
47
  });
43
48
 
@@ -47,7 +52,7 @@ describe("<DownVoteButton />", () => {
47
52
  });
48
53
 
49
54
  it("should pass userLoggedIn as false", () => {
50
- const wrapper = shallow(<DownVoteButton session={session} comment={comment} downVote={downVote} />);
55
+ const wrapper = shallow(<DownVoteButton session={session} comment={comment} downVote={downVote} rootCommentable={rootCommentable} orderBy={orderBy} />);
51
56
  expect(wrapper.find(VoteButton).prop("userLoggedIn")).toBeFalsy();
52
57
  });
53
58
  });
@@ -4,6 +4,7 @@ import { graphql } from "react-apollo";
4
4
  import VoteButton from "./vote_button.component";
5
5
 
6
6
  import {
7
+ AddCommentFormCommentableFragment,
7
8
  AddCommentFormSessionFragment,
8
9
  CommentFragment,
9
10
  DownVoteButtonFragment,
@@ -17,6 +18,8 @@ interface DownVoteButtonProps {
17
18
  } | null;
18
19
  comment: DownVoteButtonFragment;
19
20
  downVote?: () => void;
21
+ rootCommentable: AddCommentFormCommentableFragment;
22
+ orderBy: string;
20
23
  }
21
24
 
22
25
  export const DownVoteButton: React.SFC<DownVoteButtonProps> = ({
@@ -49,49 +52,65 @@ export const DownVoteButton: React.SFC<DownVoteButtonProps> = ({
49
52
  };
50
53
 
51
54
  const downVoteMutation = require("../mutations/down_vote.mutation.graphql");
55
+ const getCommentsQuery = require("../queries/comments.query.graphql");
52
56
 
53
- const DownVoteButtonWithMutation = graphql(downVoteMutation, {
57
+ const DownVoteButtonWithMutation = graphql<DownVoteMutation, DownVoteButtonProps>(downVoteMutation, {
54
58
  props: ({ ownProps, mutate }) => ({
55
- downVote: () => mutate({
56
- variables: {
57
- id: ownProps.comment.id,
58
- },
59
- optimisticResponse: {
60
- __typename: "Mutation",
61
- comment: {
62
- __typename: "CommentMutation",
63
- downVote: {
64
- __typename: "Comment",
65
- ...ownProps.comment,
66
- downVotes: ownProps.comment.downVotes + 1,
67
- downVoted: true,
59
+ downVote() {
60
+ if (mutate) {
61
+ mutate({
62
+ variables: {
63
+ id: ownProps.comment.id,
68
64
  },
69
- },
70
- },
71
- updateQueries: {
72
- GetComments: (prev: GetCommentsQuery, { mutationResult: { data } }: { mutationResult: { data: DownVoteMutation }}) => {
73
- const commentReducer = (comment: CommentFragment): CommentFragment => {
74
- const replies = comment.comments || [];
65
+ optimisticResponse: {
66
+ __typename: "Mutation",
67
+ comment: {
68
+ __typename: "CommentMutation",
69
+ downVote: {
70
+ __typename: "Comment",
71
+ ...ownProps.comment,
72
+ downVotes: ownProps.comment.downVotes + 1,
73
+ downVoted: true,
74
+ },
75
+ },
76
+ },
77
+ update: (store, result: DownVoteMutation) => {
78
+ const variables = {
79
+ commentableId: ownProps.rootCommentable.id,
80
+ commentableType: ownProps.rootCommentable.type,
81
+ orderBy: ownProps.orderBy,
82
+ };
83
+
84
+ const commentReducer = (comment: CommentFragment): CommentFragment => {
85
+ const replies = comment.comments || [];
75
86
 
76
- if (comment.id === ownProps.comment.id && data.comment) {
77
- return data.comment.downVote;
78
- }
79
- return {
80
- ...comment,
81
- comments: replies.map(commentReducer),
87
+ if (comment.id === ownProps.comment.id && result.comment) {
88
+ return result.comment.downVote;
89
+ }
90
+
91
+ return {
92
+ ...comment,
93
+ comments: replies.map(commentReducer),
94
+ };
82
95
  };
83
- };
84
96
 
85
- return {
86
- ...prev,
87
- commentable: {
88
- ...prev.commentable,
89
- comments: prev.commentable.comments.map(commentReducer),
90
- },
91
- };
92
- },
93
- },
94
- }),
97
+ const prev = store.readQuery<GetCommentsQuery>({ query: getCommentsQuery, variables });
98
+
99
+ store.writeQuery({
100
+ query: getCommentsQuery,
101
+ data: {
102
+ ...prev,
103
+ commentable: {
104
+ ...prev.commentable,
105
+ comments: prev.commentable.comments.map(commentReducer),
106
+ },
107
+ },
108
+ variables,
109
+ });
110
+ },
111
+ });
112
+ }
113
+ },
95
114
  }),
96
115
  })(DownVoteButton);
97
116
 
@@ -10,6 +10,11 @@ import generateUserData from "../support/generate_user_data";
10
10
  import { UpVoteButtonFragment } from "../support/schema";
11
11
 
12
12
  describe("<UpVoteButton />", () => {
13
+ const orderBy = "older";
14
+ const rootCommentable = {
15
+ id: "1",
16
+ type: "Decidim::DummyResource",
17
+ };
13
18
  let comment: UpVoteButtonFragment;
14
19
  let session: any = null;
15
20
  const upVote = jasmine.createSpy("upVote");
@@ -23,7 +28,7 @@ describe("<UpVoteButton />", () => {
23
28
  });
24
29
 
25
30
  it("should render a VoteButton component with the correct props", () => {
26
- const wrapper = shallow(<UpVoteButton session={session} comment={comment} upVote={upVote} />);
31
+ const wrapper = shallow(<UpVoteButton session={session} comment={comment} upVote={upVote} rootCommentable={rootCommentable} orderBy={orderBy} />);
27
32
  expect(wrapper.find(VoteButton).prop("buttonClassName")).toEqual("comment__votes--up");
28
33
  expect(wrapper.find(VoteButton).prop("iconName")).toEqual("icon-chevron-top");
29
34
  expect(wrapper.find(VoteButton).prop("votes")).toEqual(comment.upVotes);
@@ -31,13 +36,13 @@ describe("<UpVoteButton />", () => {
31
36
 
32
37
  it("should pass disabled prop as true if comment upVoted is true", () => {
33
38
  comment.upVoted = true;
34
- const wrapper = shallow(<UpVoteButton session={session} comment={comment} upVote={upVote} />);
39
+ const wrapper = shallow(<UpVoteButton session={session} comment={comment} upVote={upVote} rootCommentable={rootCommentable} orderBy={orderBy} />);
35
40
  expect(wrapper.find(VoteButton).prop("disabled")).toBeTruthy();
36
41
  });
37
42
 
38
43
  it("should pass disabled prop as true if comment downVoted is true", () => {
39
44
  comment.downVoted = true;
40
- const wrapper = shallow(<UpVoteButton session={session} comment={comment} upVote={upVote} />);
45
+ const wrapper = shallow(<UpVoteButton session={session} comment={comment} upVote={upVote} rootCommentable={rootCommentable} orderBy={orderBy} />);
41
46
  expect(wrapper.find(VoteButton).prop("disabled")).toBeTruthy();
42
47
  });
43
48
 
@@ -47,7 +52,7 @@ describe("<UpVoteButton />", () => {
47
52
  });
48
53
 
49
54
  it("should pass userLoggedIn as false", () => {
50
- const wrapper = shallow(<UpVoteButton session={session} comment={comment} upVote={upVote} />);
55
+ const wrapper = shallow(<UpVoteButton session={session} comment={comment} upVote={upVote} rootCommentable={rootCommentable} orderBy={orderBy} />);
51
56
  expect(wrapper.find(VoteButton).prop("userLoggedIn")).toBeFalsy();
52
57
  });
53
58
  });