decidim-comments 0.3.2 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
  });