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
@@ -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
  });