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
@@ -21,21 +21,25 @@ interface AddCommentFormProps {
21
21
  user: any;
22
22
  } | null;
23
23
  commentable: AddCommentFormCommentableFragment;
24
+ rootCommentable: AddCommentFormCommentableFragment;
24
25
  showTitle?: boolean;
25
26
  submitButtonClassName?: string;
26
27
  autoFocus?: boolean;
27
- maxLength?: number;
28
28
  arguable?: boolean;
29
29
  addComment?: (data: { body: string, alignment: number, userGroupId?: string }) => void;
30
30
  onCommentAdded?: () => void;
31
+ orderBy: string;
31
32
  }
32
33
 
33
34
  interface AddCommentFormState {
34
35
  disabled: boolean;
35
36
  error: boolean;
36
37
  alignment: number;
38
+ remainingCharacterCount: number;
37
39
  }
38
40
 
41
+ export const MAX_LENGTH = 500;
42
+
39
43
  /**
40
44
  * Renders a form to create new comments.
41
45
  * @class
@@ -47,7 +51,6 @@ export class AddCommentForm extends React.Component<AddCommentFormProps, AddComm
47
51
  submitButtonClassName: "button button--sc",
48
52
  arguable: false,
49
53
  autoFocus: false,
50
- maxLength: 1000,
51
54
  };
52
55
 
53
56
  public bodyTextArea: HTMLTextAreaElement;
@@ -60,6 +63,7 @@ export class AddCommentForm extends React.Component<AddCommentFormProps, AddComm
60
63
  disabled: true,
61
64
  error: false,
62
65
  alignment: 0,
66
+ remainingCharacterCount: MAX_LENGTH,
63
67
  };
64
68
  }
65
69
 
@@ -124,7 +128,7 @@ export class AddCommentForm extends React.Component<AddCommentFormProps, AddComm
124
128
  */
125
129
  private _renderForm() {
126
130
  const { session, submitButtonClassName, commentable: { id, type } } = this.props;
127
- const { disabled } = this.state;
131
+ const { disabled, remainingCharacterCount } = this.state;
128
132
 
129
133
  if (session) {
130
134
  return (
@@ -141,6 +145,9 @@ export class AddCommentForm extends React.Component<AddCommentFormProps, AddComm
141
145
  >
142
146
  {I18n.t("components.add_comment_form.form.submit")}
143
147
  </button>
148
+ <span className="remaining-character-count">
149
+ {I18n.t("components.add_comment_form.remaining_characters", { count: remainingCharacterCount })}
150
+ </span>
144
151
  </div>
145
152
  </form>
146
153
  );
@@ -155,7 +162,7 @@ export class AddCommentForm extends React.Component<AddCommentFormProps, AddComm
155
162
  * @returns {Void|DOMElement} - The heading or an empty element
156
163
  */
157
164
  private _renderTextArea() {
158
- const { commentable: { id, type }, autoFocus, maxLength } = this.props;
165
+ const { commentable: { id, type }, autoFocus } = this.props;
159
166
  const { error } = this.state;
160
167
  const className = classnames({ "is-invalid-input": error });
161
168
 
@@ -164,9 +171,9 @@ export class AddCommentForm extends React.Component<AddCommentFormProps, AddComm
164
171
  id: `add-comment-${type}-${id}`,
165
172
  className,
166
173
  rows: "4",
167
- maxLength,
174
+ maxLength: MAX_LENGTH,
168
175
  required: "required",
169
- pattern: `^(.){0,${maxLength}}$`,
176
+ pattern: `^(.){0,${MAX_LENGTH}}$`,
170
177
  placeholder: I18n.t("components.add_comment_form.form.body.placeholder"),
171
178
  onChange: (evt: React.ChangeEvent<HTMLTextAreaElement>) => this._checkCommentBody(evt.target.value),
172
179
  };
@@ -186,13 +193,12 @@ export class AddCommentForm extends React.Component<AddCommentFormProps, AddComm
186
193
  * @returns {Void|DOMElement} - The error or an empty element
187
194
  */
188
195
  private _renderTextAreaError() {
189
- const { maxLength } = this.props;
190
196
  const { error } = this.state;
191
197
 
192
198
  if (error) {
193
199
  return (
194
200
  <span className="form-error is-visible">
195
- {I18n.t("components.add_comment_form.form.form_error", { length: maxLength })}
201
+ {I18n.t("components.add_comment_form.form.form_error", { length: MAX_LENGTH })}
196
202
  </span>
197
203
  );
198
204
  }
@@ -298,8 +304,10 @@ export class AddCommentForm extends React.Component<AddCommentFormProps, AddComm
298
304
  * @returns {Void} - Returns nothing
299
305
  */
300
306
  private _checkCommentBody(body: string) {
301
- const { maxLength } = this.props;
302
- this.setState({ disabled: body === "", error: body === "" || (maxLength !== undefined && body.length > maxLength) });
307
+ this.setState({
308
+ disabled: body === "", error: body === "" || body.length > MAX_LENGTH,
309
+ remainingCharacterCount: MAX_LENGTH - body.length,
310
+ });
303
311
  }
304
312
 
305
313
  /**
@@ -334,89 +342,107 @@ export class AddCommentForm extends React.Component<AddCommentFormProps, AddComm
334
342
  }
335
343
 
336
344
  const addCommentMutation = require("../mutations/add_comment.mutation.graphql");
345
+ const getCommentsQuery = require("../queries/comments.query.graphql");
337
346
 
338
- const AddCommentFormWithMutation = graphql(addCommentMutation, {
347
+ const AddCommentFormWithMutation = graphql<AddCommentMutation, AddCommentFormProps>(addCommentMutation, {
339
348
  props: ({ ownProps, mutate }) => ({
340
- addComment: ({ body, alignment, userGroupId }: { body: string, alignment: number, userGroupId: string }) => mutate({
341
- variables: {
342
- commentableId: ownProps.commentable.id,
343
- commentableType: ownProps.commentable.type,
344
- body,
345
- alignment,
346
- userGroupId,
347
- },
348
- optimisticResponse: {
349
- commentable: {
350
- __typename: "CommentableMutation",
351
- addComment: {
352
- __typename: "Comment",
353
- id: uuid(),
354
- sgid: uuid(),
355
- type: "Decidim::Comments::Comment",
356
- createdAt: new Date().toISOString(),
349
+ addComment: ({ body, alignment, userGroupId }: { body: string, alignment: number, userGroupId: string }) => {
350
+ if (mutate) {
351
+ mutate({
352
+ variables: {
353
+ commentableId: ownProps.commentable.id,
354
+ commentableType: ownProps.commentable.type,
357
355
  body,
358
356
  alignment,
359
- author: {
360
- __typename: "User",
361
- name: ownProps.session.user.name,
362
- avatarUrl: ownProps.session.user.avatarUrl,
363
- deleted: false,
357
+ userGroupId,
358
+ },
359
+ optimisticResponse: {
360
+ commentable: {
361
+ __typename: "CommentableMutation",
362
+ addComment: {
363
+ __typename: "Comment",
364
+ id: uuid(),
365
+ sgid: uuid(),
366
+ type: "Decidim::Comments::Comment",
367
+ createdAt: new Date().toISOString(),
368
+ body,
369
+ alignment,
370
+ author: {
371
+ __typename: "User",
372
+ name: ownProps.session && ownProps.session.user.name,
373
+ avatarUrl: ownProps.session && ownProps.session.user.avatarUrl,
374
+ deleted: false,
375
+ isVerified: true,
376
+ isUser: true,
377
+ },
378
+ comments: [],
379
+ hasComments: false,
380
+ acceptsNewComments: false,
381
+ upVotes: 0,
382
+ upVoted: false,
383
+ downVotes: 0,
384
+ downVoted: false,
385
+ alreadyReported: false,
386
+ },
364
387
  },
365
- comments: [],
366
- hasComments: false,
367
- acceptsNewComments: false,
368
- upVotes: 0,
369
- upVoted: false,
370
- downVotes: 0,
371
- downVoted: false,
372
- alreadyReported: false,
373
388
  },
374
- },
375
- },
376
- updateQueries: {
377
- GetComments: (prev: GetCommentsQuery, { mutationResult: { data } }: { mutationResult: { data: AddCommentMutation }}) => {
378
- const { id, type } = ownProps.commentable;
379
- const newComment = data.commentable && data.commentable.addComment;
380
- let comments = [];
381
-
382
- const commentReducer = (comment: CommentFragment): CommentFragment => {
383
- const replies = comment.comments || [];
384
-
385
- if (newComment && comment.id === id) {
389
+ update: (store, { data }: { data: AddCommentMutation }) => {
390
+ const variables = {
391
+ commentableId: ownProps.rootCommentable.id,
392
+ commentableType: ownProps.rootCommentable.type,
393
+ orderBy: ownProps.orderBy,
394
+ };
395
+ const prev = store.readQuery<GetCommentsQuery>({
396
+ query: getCommentsQuery,
397
+ variables,
398
+ });
399
+ const { id, type } = ownProps.commentable;
400
+ const newComment = data.commentable && data.commentable.addComment;
401
+ let comments = [];
402
+
403
+ const commentReducer = (comment: CommentFragment): CommentFragment => {
404
+ const replies = comment.comments || [];
405
+
406
+ if (newComment && comment.id === id) {
407
+ return {
408
+ ...comment,
409
+ hasComments: true,
410
+ comments: [
411
+ ...replies,
412
+ newComment,
413
+ ],
414
+ };
415
+ }
386
416
  return {
387
417
  ...comment,
388
- hasComments: true,
389
- comments: [
390
- ...replies,
391
- newComment,
392
- ],
418
+ comments: replies.map(commentReducer),
393
419
  };
394
- }
395
- return {
396
- ...comment,
397
- comments: replies.map(commentReducer),
398
420
  };
399
- };
400
-
401
- if (type === "Decidim::Comments::Comment") {
402
- comments = prev.commentable.comments.map(commentReducer);
403
- } else {
404
- comments = [
405
- ...prev.commentable.comments,
406
- newComment,
407
- ];
408
- }
409
-
410
- return {
411
- ...prev,
412
- commentable: {
413
- ...prev.commentable,
414
- comments,
415
- },
416
- };
417
- },
418
- },
419
- }),
421
+
422
+ if (type === "Decidim::Comments::Comment") {
423
+ comments = prev.commentable.comments.map(commentReducer);
424
+ } else {
425
+ comments = [
426
+ ...prev.commentable.comments,
427
+ newComment,
428
+ ];
429
+ }
430
+
431
+ store.writeQuery({
432
+ query: getCommentsQuery,
433
+ data: {
434
+ ...prev,
435
+ commentable: {
436
+ ...prev.commentable,
437
+ comments,
438
+ },
439
+ },
440
+ variables,
441
+ });
442
+ },
443
+ });
444
+ }
445
+ },
420
446
  }),
421
447
  })(AddCommentForm);
422
448
 
@@ -14,6 +14,11 @@ import generateUserData from "../support/generate_user_data";
14
14
  import { loadLocaleTranslations } from "../support/load_translations";
15
15
 
16
16
  describe("<Comment />", () => {
17
+ const orderBy = "older";
18
+ const rootCommentable = {
19
+ id: "1",
20
+ type: "Decidim::DummyResource",
21
+ };
17
22
  let comment: CommentFragment;
18
23
  let session: any = null;
19
24
 
@@ -31,17 +36,17 @@ describe("<Comment />", () => {
31
36
  });
32
37
 
33
38
  it("should render an article with class comment", () => {
34
- const wrapper = shallow(<Comment comment={comment} session={session} />);
39
+ const wrapper = shallow(<Comment comment={comment} session={session} rootCommentable={rootCommentable} orderBy={orderBy} />);
35
40
  expect(wrapper.find("article.comment").exists()).toBeTruthy();
36
41
  });
37
42
 
38
43
  it("should render a time tag with comment's created at", () => {
39
- const wrapper = shallow(<Comment comment={comment} session={session} />);
44
+ const wrapper = shallow(<Comment comment={comment} session={session} rootCommentable={rootCommentable} orderBy={orderBy} />);
40
45
  expect(wrapper.find("time").prop("dateTime")).toEqual(comment.createdAt);
41
46
  });
42
47
 
43
48
  it("should render author's name in a link with class author__name", () => {
44
- const wrapper = shallow(<Comment comment={comment} session={session} />);
49
+ const wrapper = shallow(<Comment comment={comment} session={session} rootCommentable={rootCommentable} orderBy={orderBy} />);
45
50
  expect(wrapper.find("a.author__name").text()).toEqual(comment.author.name);
46
51
  });
47
52
 
@@ -51,28 +56,28 @@ describe("<Comment />", () => {
51
56
  });
52
57
 
53
58
  it("should render 'Deleted user' inside a badge", () => {
54
- const wrapper = shallow(<Comment comment={comment} session={session} />);
59
+ const wrapper = shallow(<Comment comment={comment} session={session} rootCommentable={rootCommentable} orderBy={orderBy} />);
55
60
  expect(wrapper.find("span.label.label--small.label--basic").text()).toEqual("Deleted user");
56
61
  });
57
62
  });
58
63
 
59
64
  it("should render author's avatar as a image tag", () => {
60
- const wrapper = shallow(<Comment comment={comment} session={session} />);
65
+ const wrapper = shallow(<Comment comment={comment} session={session} rootCommentable={rootCommentable} orderBy={orderBy} />);
61
66
  expect(wrapper.find("a.author__avatar img").prop("src")).toEqual(comment.author.avatarUrl);
62
67
  });
63
68
 
64
69
  it("should render comment's body on a div with class comment__content", () => {
65
- const wrapper = shallow(<Comment comment={comment} session={session} />);
70
+ const wrapper = shallow(<Comment comment={comment} session={session} rootCommentable={rootCommentable} orderBy={orderBy} />);
66
71
  expect(wrapper.find("div.comment__content").text()).toEqual(comment.body);
67
72
  });
68
73
 
69
74
  it("should initialize with a state property showReplyForm as false", () => {
70
- const wrapper = shallow(<Comment comment={comment} session={session} />);
75
+ const wrapper = shallow(<Comment comment={comment} session={session} rootCommentable={rootCommentable} orderBy={orderBy} />);
71
76
  expect(wrapper.state()).toHaveProperty("showReplyForm", false);
72
77
  });
73
78
 
74
79
  it("should render a AddCommentForm component with the correct props when clicking the reply button", () => {
75
- const wrapper = shallow(<Comment comment={comment} session={session} />);
80
+ const wrapper = shallow(<Comment comment={comment} session={session} rootCommentable={rootCommentable} orderBy={orderBy} />);
76
81
  expect(wrapper.find(AddCommentForm).exists()).toBeFalsy();
77
82
  wrapper.find("button.comment__reply").simulate("click");
78
83
  expect(wrapper.find(AddCommentForm).prop("session")).toEqual(session);
@@ -83,24 +88,24 @@ describe("<Comment />", () => {
83
88
 
84
89
  it("should not render the additional reply button if the parent comment has no comments and isRootcomment", () => {
85
90
  comment.hasComments = false;
86
- const wrapper = shallow(<Comment comment={comment} session={session} isRootComment={true} />);
91
+ const wrapper = shallow(<Comment comment={comment} session={session} isRootComment={true} rootCommentable={rootCommentable} orderBy={orderBy} />);
87
92
  expect(wrapper.find("div.comment__additionalreply").exists()).toBeFalsy();
88
93
  });
89
94
 
90
95
  it("should not render the additional reply button if the parent comment has comments and not isRootcomment", () => {
91
96
  comment.hasComments = true;
92
- const wrapper = shallow(<Comment comment={comment} session={session} />);
97
+ const wrapper = shallow(<Comment comment={comment} session={session} rootCommentable={rootCommentable} orderBy={orderBy} />);
93
98
  expect(wrapper.find("div.comment__additionalreply").exists()).toBeFalsy();
94
99
  });
95
100
 
96
101
  it("should render the additional reply button if the parent comment has comments and isRootcomment", () => {
97
102
  comment.hasComments = true;
98
- const wrapper = shallow(<Comment comment={comment} session={session} isRootComment={true} />);
103
+ const wrapper = shallow(<Comment comment={comment} session={session} isRootComment={true} rootCommentable={rootCommentable} orderBy={orderBy} />);
99
104
  expect(wrapper.find("div.comment__additionalreply").exists()).toBeTruthy();
100
105
  });
101
106
 
102
107
  it("should render comment's comments as a separate Comment components", () => {
103
- const wrapper = shallow(<Comment comment={comment} session={session} votable={true} />);
108
+ const wrapper = shallow(<Comment comment={comment} session={session} votable={true} rootCommentable={rootCommentable} orderBy={orderBy} />);
104
109
  wrapper.find(Comment).forEach((node, idx) => {
105
110
  expect(node.prop("comment")).toEqual(comment.comments[idx]);
106
111
  expect(node.prop("session")).toEqual(session);
@@ -110,19 +115,19 @@ describe("<Comment />", () => {
110
115
  });
111
116
 
112
117
  it("should render comment's comments with articleClassName as 'comment comment--nested comment--nested--alt' when articleClassName is 'comment comment--nested'", () => {
113
- const wrapper = shallow(<Comment comment={comment} session={session} articleClassName="comment comment--nested" />);
118
+ const wrapper = shallow(<Comment comment={comment} session={session} articleClassName="comment comment--nested" rootCommentable={rootCommentable} orderBy={orderBy} />);
114
119
  wrapper.find(Comment).forEach((node) => {
115
120
  expect(node.prop("articleClassName")).toEqual("comment comment--nested comment--nested--alt");
116
121
  });
117
122
  });
118
123
 
119
124
  it("should have a default prop articleClassName with value 'comment'", () => {
120
- const wrapper = mount(<Comment comment={comment} session={session} />);
125
+ const wrapper = mount(<Comment comment={comment} session={session} rootCommentable={rootCommentable} orderBy={orderBy} />);
121
126
  expect(wrapper.prop("articleClassName")).toEqual("comment");
122
127
  });
123
128
 
124
129
  it("should have a default prop isRootComment with value false", () => {
125
- const wrapper = mount(<Comment comment={comment} session={session} />);
130
+ const wrapper = mount(<Comment comment={comment} session={session} rootCommentable={rootCommentable} orderBy={orderBy} />);
126
131
  expect(wrapper.prop("isRootComment")).toBeFalsy();
127
132
  });
128
133
 
@@ -132,7 +137,7 @@ describe("<Comment />", () => {
132
137
  });
133
138
 
134
139
  it("should not render the reply button", () => {
135
- const wrapper = shallow(<Comment comment={comment} session={session} />);
140
+ const wrapper = shallow(<Comment comment={comment} session={session} rootCommentable={rootCommentable} orderBy={orderBy} />);
136
141
  expect(wrapper.find("button.comment__reply").exists()).toBeFalsy();
137
142
  });
138
143
  });
@@ -143,49 +148,49 @@ describe("<Comment />", () => {
143
148
  });
144
149
 
145
150
  it("should not render reply button", () => {
146
- const wrapper = shallow(<Comment comment={comment} session={session} />);
151
+ const wrapper = shallow(<Comment comment={comment} session={session} rootCommentable={rootCommentable} orderBy={orderBy} />);
147
152
  expect(wrapper.find("button.comment__reply").exists()).toBeFalsy();
148
153
  });
149
154
 
150
155
  it("should not render the flag modal", () => {
151
- const wrapper = shallow(<Comment comment={comment} session={session} />);
156
+ const wrapper = shallow(<Comment comment={comment} session={session} rootCommentable={rootCommentable} orderBy={orderBy} />);
152
157
  expect(wrapper.find(".flag-modal").exists()).toBeFalsy();
153
158
  });
154
159
  });
155
160
 
156
161
  it("should render a 'in favor' badge if comment's alignment is 1", () => {
157
162
  comment.alignment = 1;
158
- const wrapper = shallow(<Comment comment={comment} session={session} />);
163
+ const wrapper = shallow(<Comment comment={comment} session={session} rootCommentable={rootCommentable} orderBy={orderBy} />);
159
164
  expect(wrapper.find("span.alignment.label").text()).toEqual("In favor");
160
165
  });
161
166
 
162
167
  it("should render a 'against' badge if comment's alignment is -1", () => {
163
168
  comment.alignment = -1;
164
- const wrapper = shallow(<Comment comment={comment} session={session} />);
169
+ const wrapper = shallow(<Comment comment={comment} session={session} rootCommentable={rootCommentable} orderBy={orderBy} />);
165
170
  expect(wrapper.find("span.alert.label").text()).toEqual("Against");
166
171
  });
167
172
 
168
173
  it("should render the flag modal", () => {
169
- const wrapper = shallow(<Comment comment={comment} session={session} />);
174
+ const wrapper = shallow(<Comment comment={comment} session={session} rootCommentable={rootCommentable} orderBy={orderBy} />);
170
175
  expect(wrapper.find(".flag-modal").exists()).toBeTruthy();
171
176
  });
172
177
 
173
178
  describe("when user has already reported the comment", () => {
174
179
  it("should not render the flag form", () => {
175
180
  comment.alreadyReported = true;
176
- const wrapper = shallow(<Comment comment={comment} session= {session} />);
181
+ const wrapper = shallow(<Comment comment={comment} session={session} rootCommentable={rootCommentable} orderBy={orderBy} />);
177
182
  expect(wrapper.find(".flag-modal form").exists()).toBeFalsy();
178
183
  });
179
184
  });
180
185
 
181
186
  describe("when the comment is votable", () => {
182
187
  it("should render an UpVoteButton component", () => {
183
- const wrapper = shallow(<Comment comment={comment} session={session} votable={true} />);
188
+ const wrapper = shallow(<Comment comment={comment} session={session} votable={true} rootCommentable={rootCommentable} orderBy={orderBy} />);
184
189
  expect(wrapper.find(UpVoteButton).prop("comment")).toEqual(comment);
185
190
  });
186
191
 
187
192
  it("should render an DownVoteButton component", () => {
188
- const wrapper = shallow(<Comment comment={comment} session={session} votable={true} />);
193
+ const wrapper = shallow(<Comment comment={comment} session={session} votable={true} rootCommentable={rootCommentable} orderBy={orderBy} />);
189
194
  expect(wrapper.find(DownVoteButton).prop("comment")).toEqual(comment);
190
195
  });
191
196
  });