decidim-comments 0.19.1 → 0.23.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (123) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/javascripts/decidim/comments/bundle.js +66 -66
  3. data/app/assets/javascripts/decidim/comments/bundle.js.map +1 -1
  4. data/app/cells/decidim/comments/comment_activity_cell.rb +2 -22
  5. data/app/cells/decidim/comments/comment_cell.rb +22 -0
  6. data/app/cells/decidim/comments/comment_m/footer.erb +5 -0
  7. data/app/cells/decidim/comments/comment_m/top.erb +7 -0
  8. data/app/cells/decidim/comments/comment_m_cell.rb +29 -0
  9. data/app/commands/decidim/comments/create_comment.rb +8 -8
  10. data/app/events/decidim/comments/comment_by_followed_user_group_event.rb +9 -0
  11. data/app/events/decidim/comments/comment_event.rb +15 -2
  12. data/app/events/decidim/comments/user_group_mentioned_event.rb +10 -0
  13. data/app/forms/decidim/comments/comment_form.rb +17 -1
  14. data/app/frontend/application/icon.component.tsx +16 -4
  15. data/app/frontend/comments/add_comment_form.component.test.tsx +34 -29
  16. data/app/frontend/comments/add_comment_form.component.tsx +48 -19
  17. data/app/frontend/comments/comment.component.test.tsx +36 -5
  18. data/app/frontend/comments/comment.component.tsx +311 -89
  19. data/app/frontend/comments/comment_order_selector.component.tsx +26 -7
  20. data/app/frontend/comments/comment_thread.component.test.tsx +9 -8
  21. data/app/frontend/comments/comment_thread.component.tsx +3 -1
  22. data/app/frontend/comments/comments.component.test.tsx +17 -14
  23. data/app/frontend/comments/comments.component.tsx +90 -9
  24. data/app/frontend/comments/down_vote_button.component.tsx +27 -9
  25. data/app/frontend/comments/up_vote_button.component.tsx +27 -9
  26. data/app/frontend/comments/vote_button.component.tsx +4 -0
  27. data/app/frontend/comments/vote_button_component.test.tsx +14 -8
  28. data/app/frontend/entry.ts +19 -0
  29. data/app/frontend/entry_test.ts +2 -0
  30. data/app/frontend/mutations/add_comment.mutation.graphql +2 -2
  31. data/app/frontend/mutations/down_vote.mutation.graphql +2 -2
  32. data/app/frontend/mutations/up_vote.mutation.graphql +2 -2
  33. data/app/frontend/queries/comments.query.graphql +3 -3
  34. data/app/frontend/support/schema.ts +326 -0
  35. data/app/helpers/decidim/comments/comment_cells_helper.rb +33 -0
  36. data/app/models/decidim/comments/comment.rb +96 -18
  37. data/app/models/decidim/comments/seed.rb +1 -1
  38. data/app/queries/decidim/comments/metrics/comments_metric_manage.rb +1 -6
  39. data/app/queries/decidim/comments/sorted_comments.rb +8 -2
  40. data/app/scrubbers/decidim/comments/user_input_scrubber.rb +20 -0
  41. data/app/services/decidim/comments/new_comment_notification_creator.rb +28 -3
  42. data/app/types/decidim/comments/commentable_interface.rb +3 -2
  43. data/app/types/decidim/comments/commentable_mutation_type.rb +6 -3
  44. data/config/locales/am-ET.yml +1 -0
  45. data/config/locales/ar.yml +10 -1
  46. data/config/locales/bg-BG.yml +6 -0
  47. data/config/locales/bg.yml +6 -0
  48. data/config/locales/ca.yml +24 -1
  49. data/config/locales/cs.yml +36 -13
  50. data/config/locales/da-DK.yml +1 -0
  51. data/config/locales/da.yml +1 -0
  52. data/config/locales/de.yml +23 -1
  53. data/config/locales/el-GR.yml +1 -0
  54. data/config/locales/el.yml +122 -0
  55. data/config/locales/en.yml +24 -1
  56. data/config/locales/eo.yml +1 -0
  57. data/config/locales/es-MX.yml +24 -1
  58. data/config/locales/es-PY.yml +24 -1
  59. data/config/locales/es.yml +24 -1
  60. data/config/locales/et-EE.yml +1 -0
  61. data/config/locales/et.yml +1 -0
  62. data/config/locales/eu.yml +4 -1
  63. data/config/locales/fi-plain.yml +24 -1
  64. data/config/locales/fi.yml +31 -8
  65. data/config/locales/fr-CA.yml +123 -0
  66. data/config/locales/fr.yml +25 -2
  67. data/config/locales/ga-IE.yml +1 -0
  68. data/config/locales/gl.yml +4 -1
  69. data/config/locales/hr-HR.yml +1 -0
  70. data/config/locales/hr.yml +1 -0
  71. data/config/locales/hu.yml +18 -2
  72. data/config/locales/id-ID.yml +4 -1
  73. data/config/locales/is-IS.yml +74 -0
  74. data/config/locales/is.yml +76 -0
  75. data/config/locales/it.yml +23 -1
  76. data/config/locales/ja-JP.yml +120 -0
  77. data/config/locales/ja.yml +121 -0
  78. data/config/locales/ko-KR.yml +1 -0
  79. data/config/locales/ko.yml +1 -0
  80. data/config/locales/lt-LT.yml +1 -0
  81. data/config/locales/lt.yml +1 -0
  82. data/config/locales/lv.yml +118 -0
  83. data/config/locales/mt-MT.yml +1 -0
  84. data/config/locales/mt.yml +1 -0
  85. data/config/locales/nl.yml +26 -3
  86. data/config/locales/no.yml +88 -1
  87. data/config/locales/om-ET.yml +1 -0
  88. data/config/locales/pl.yml +62 -40
  89. data/config/locales/pt-BR.yml +5 -2
  90. data/config/locales/pt.yml +47 -25
  91. data/config/locales/ro-RO.yml +124 -0
  92. data/config/locales/ru.yml +4 -1
  93. data/config/locales/sk-SK.yml +116 -0
  94. data/config/locales/sk.yml +120 -0
  95. data/config/locales/sl.yml +4 -0
  96. data/config/locales/so-SO.yml +1 -0
  97. data/config/locales/sr-CS.yml +20 -0
  98. data/config/locales/sv.yml +26 -3
  99. data/config/locales/ti-ER.yml +1 -0
  100. data/config/locales/tr-TR.yml +4 -1
  101. data/config/locales/uk.yml +4 -2
  102. data/config/locales/vi-VN.yml +1 -0
  103. data/config/locales/vi.yml +1 -0
  104. data/config/locales/zh-CN.yml +121 -0
  105. data/config/locales/zh-TW.yml +1 -0
  106. data/db/migrate/20200320105911_index_foreign_keys_in_decidim_comments_comments.rb +7 -0
  107. data/db/migrate/20200706123136_make_comments_handle_i18n.rb +41 -0
  108. data/db/migrate/20200828101910_add_commentable_counter_cache_to_comments.rb +9 -0
  109. data/lib/decidim/comments.rb +1 -0
  110. data/lib/decidim/comments/api/comment_type.rb +5 -1
  111. data/lib/decidim/comments/comment_serializer.rb +7 -2
  112. data/lib/decidim/comments/comment_vote_serializer.rb +5 -1
  113. data/lib/decidim/comments/commentable.rb +11 -0
  114. data/lib/decidim/comments/comments_helper.rb +28 -4
  115. data/lib/decidim/comments/engine.rb +13 -0
  116. data/lib/decidim/comments/markdown.rb +55 -0
  117. data/lib/decidim/comments/mutation_extensions.rb +8 -0
  118. data/lib/decidim/comments/query_extensions.rb +4 -0
  119. data/lib/decidim/comments/test/factories.rb +10 -1
  120. data/lib/decidim/comments/test/shared_examples/comment_event.rb +12 -2
  121. data/lib/decidim/comments/test/shared_examples/create_comment_context.rb +3 -2
  122. data/lib/decidim/comments/version.rb +1 -1
  123. metadata +72 -9
@@ -0,0 +1,9 @@
1
+ # frozen-string_literal: true
2
+
3
+ module Decidim
4
+ module Comments
5
+ class CommentByFollowedUserGroupEvent < Decidim::Events::SimpleEvent
6
+ include Decidim::Comments::CommentEvent
7
+ end
8
+ end
9
+ end
@@ -9,8 +9,6 @@ module Decidim
9
9
  include Decidim::Events::AuthorEvent
10
10
 
11
11
  included do
12
- delegate :author, to: :comment
13
-
14
12
  def resource_path
15
13
  resource_locator.path(url_params)
16
14
  end
@@ -23,6 +21,21 @@ module Decidim
23
21
  comment.formatted_body
24
22
  end
25
23
 
24
+ def author
25
+ comment.normalized_author
26
+ end
27
+
28
+ def author_presenter
29
+ return unless author
30
+
31
+ @author_presenter ||= case author
32
+ when Decidim::User
33
+ Decidim::UserPresenter.new(author)
34
+ when Decidim::UserGroup
35
+ Decidim::UserGroupPresenter.new(author)
36
+ end
37
+ end
38
+
26
39
  private
27
40
 
28
41
  def comment
@@ -0,0 +1,10 @@
1
+ # frozen-string_literal: true
2
+
3
+ module Decidim
4
+ module Comments
5
+ class UserGroupMentionedEvent < Decidim::Events::SimpleEvent
6
+ include Decidim::Comments::CommentEvent
7
+ include Decidim::Events::UserGroupEvent
8
+ end
9
+ end
10
+ end
@@ -8,11 +8,27 @@ module Decidim
8
8
  attribute :body, String
9
9
  attribute :alignment, Integer
10
10
  attribute :user_group_id, Integer
11
+ attribute :commentable
11
12
 
12
13
  mimic :comment
13
14
 
14
- validates :body, presence: true, length: { maximum: 1000 }
15
+ validates :body, presence: true, length: { maximum: ->(form) { form.max_length } }
15
16
  validates :alignment, inclusion: { in: [0, 1, -1] }, if: ->(form) { form.alignment.present? }
17
+
18
+ validate :max_depth
19
+
20
+ def max_length
21
+ return current_component.settings.comments_max_length if current_component.try { settings.comments_max_length.positive? }
22
+ return current_organization.comments_max_length if current_organization.comments_max_length.positive?
23
+
24
+ 1000
25
+ end
26
+
27
+ def max_depth
28
+ return unless commentable.respond_to?(:depth)
29
+
30
+ errors.add(:base, :invalid) if commentable.depth >= Comment::MAX_DEPTH
31
+ end
16
32
  end
17
33
  end
18
34
  end
@@ -3,12 +3,20 @@ import assetUrl from "../support/asset_url";
3
3
 
4
4
  interface IconProps {
5
5
  name: string;
6
+ title?: string;
6
7
  iconExtraClassName?: string;
8
+ role?: string;
7
9
  }
8
10
 
9
- export const Icon: React.SFC<IconProps> = ({ name, iconExtraClassName }) => {
11
+ export const Icon: React.SFC<IconProps> = ({ name, title, iconExtraClassName, role = "none presentation" }) => {
12
+ let titleElement = null;
13
+ if (title) {
14
+ titleElement = <title>{title}</title>;
15
+ }
16
+
10
17
  return (
11
- <svg className={`icon ${iconExtraClassName} ${name}`}>
18
+ <svg className={`icon ${iconExtraClassName} ${name}`} role={role}>
19
+ {titleElement}
12
20
  <use
13
21
  xmlnsXlink="http://www.w3.org/1999/xlink"
14
22
  xlinkHref={`${assetUrl("icons.svg")}#${name}`}
@@ -23,12 +31,16 @@ Icon.defaultProps = {
23
31
 
24
32
  interface IconWithoutUserAgentProps {
25
33
  name: string;
34
+ title?: string;
26
35
  iconExtraClassName?: string;
36
+ role?: string;
27
37
  }
28
38
 
29
39
  const IconWithoutUserAgent: React.SFC<IconWithoutUserAgentProps> = ({
30
40
  name,
31
- iconExtraClassName
32
- }) => <Icon name={name} iconExtraClassName={iconExtraClassName} />;
41
+ title,
42
+ iconExtraClassName,
43
+ role = "none presentation"
44
+ }) => <Icon name={name} title={title} iconExtraClassName={iconExtraClassName} role={role} />;
33
45
 
34
46
  export default IconWithoutUserAgent;
@@ -1,14 +1,16 @@
1
1
  import { mount, ReactWrapper, shallow } from "enzyme";
2
+ import * as $ from "jquery";
2
3
  import * as React from "react";
3
4
 
4
- import { AddCommentForm, MAX_LENGTH } from "./add_comment_form.component";
5
+ import { AddCommentForm } from "./add_comment_form.component";
5
6
 
6
7
  import generateUserData from "../support/generate_user_data";
7
8
  import generateUserGroupData from "../support/generate_user_group_data";
8
9
  import { loadLocaleTranslations } from "../support/load_translations";
9
10
 
10
- describe("<AddCommentForm />", () => {
11
+ describe("<AddCommentForm commentsMaxLength={commentsMaxLength} />", () => {
11
12
  let session: any = null;
13
+ const commentsMaxLength: number = 1000;
12
14
  const commentable = {
13
15
  id: "1",
14
16
  type: "Decidim::DummyResources::DummyResource"
@@ -17,6 +19,7 @@ describe("<AddCommentForm />", () => {
17
19
  const addCommentStub = (): any => {
18
20
  return null;
19
21
  };
22
+ const context = {locale: undefined, toggleTranslations: undefined};
20
23
 
21
24
  beforeEach(() => {
22
25
  loadLocaleTranslations("en");
@@ -29,47 +32,49 @@ describe("<AddCommentForm />", () => {
29
32
  "icons.svg": "/assets/icons.svg"
30
33
  }
31
34
  };
35
+
36
+ window.$ = $;
32
37
  });
33
38
 
34
39
  it("should render a div with class add-comment", () => {
35
- const wrapper = shallow(<AddCommentForm addComment={addCommentStub} session={session} commentable={commentable} rootCommentable={commentable} orderBy={orderBy} />);
40
+ const wrapper = shallow(<AddCommentForm commentsMaxLength={commentsMaxLength} addComment={addCommentStub} session={session} commentable={commentable} rootCommentable={commentable} orderBy={orderBy} />);
36
41
  expect(wrapper.find("div.add-comment")).toBeDefined();
37
42
  });
38
43
 
39
44
  it("should have a reference to body textarea", () => {
40
- const wrapper = mount(<AddCommentForm addComment={addCommentStub} session={session} commentable={commentable} rootCommentable={commentable} orderBy={orderBy} />);
45
+ const wrapper = mount(<AddCommentForm commentsMaxLength={commentsMaxLength} addComment={addCommentStub} session={session} commentable={commentable} rootCommentable={commentable} orderBy={orderBy} />);
41
46
  expect((wrapper.instance() as AddCommentForm).bodyTextArea).toBeDefined();
42
47
  });
43
48
 
44
49
  it("should initialize with a state property disabled as true", () => {
45
- const wrapper = mount(<AddCommentForm addComment={addCommentStub} session={session} commentable={commentable} rootCommentable={commentable} orderBy={orderBy} />);
50
+ const wrapper = mount(<AddCommentForm commentsMaxLength={commentsMaxLength} addComment={addCommentStub} session={session} commentable={commentable} rootCommentable={commentable} orderBy={orderBy} />);
46
51
  expect(wrapper.state()).toHaveProperty("disabled", true);
47
52
  });
48
53
 
49
54
  it("should have a default prop showTitle as true", () => {
50
- const wrapper = mount(<AddCommentForm addComment={addCommentStub} session={session} commentable={commentable} rootCommentable={commentable} orderBy={orderBy} />);
55
+ const wrapper = mount(<AddCommentForm commentsMaxLength={commentsMaxLength} addComment={addCommentStub} session={session} commentable={commentable} rootCommentable={commentable} orderBy={orderBy} />);
51
56
  expect(wrapper.props()).toHaveProperty("showTitle", true);
52
57
  });
53
58
 
54
59
  it("should not render the title if prop showTitle is false", () => {
55
- const wrapper = shallow(<AddCommentForm addComment={addCommentStub} session={session} commentable={commentable} showTitle={false} rootCommentable={commentable} orderBy={orderBy} />);
56
- expect(wrapper.find("h5.section-heading").exists()).toBeFalsy();
60
+ const wrapper = shallow(<AddCommentForm commentsMaxLength={commentsMaxLength} addComment={addCommentStub} session={session} commentable={commentable} showTitle={false} rootCommentable={commentable} orderBy={orderBy} />);
61
+ expect(wrapper.find("h4.section-heading").exists()).toBeFalsy();
57
62
  });
58
63
 
59
64
  it("should have a default prop submitButtonClassName as 'button button--sc'", () => {
60
- const wrapper = mount(<AddCommentForm addComment={addCommentStub} session={session} commentable={commentable} rootCommentable={commentable} orderBy={orderBy} />);
65
+ const wrapper = mount(<AddCommentForm commentsMaxLength={commentsMaxLength} addComment={addCommentStub} session={session} commentable={commentable} rootCommentable={commentable} orderBy={orderBy} />);
61
66
  expect(wrapper.props()).toHaveProperty("submitButtonClassName", "button button--sc");
62
67
  });
63
68
 
64
69
  it("should use prop submitButtonClassName as a className prop for submit button", () => {
65
- const wrapper = shallow(<AddCommentForm addComment={addCommentStub} session={session} commentable={commentable} submitButtonClassName="button small hollow" rootCommentable={commentable} orderBy={orderBy} />);
70
+ const wrapper = shallow(<AddCommentForm commentsMaxLength={commentsMaxLength} addComment={addCommentStub} session={session} commentable={commentable} submitButtonClassName="button small hollow" rootCommentable={commentable} orderBy={orderBy} />);
66
71
  expect(wrapper.find('button[type="submit"]').hasClass("button")).toBeTruthy();
67
72
  expect(wrapper.find('button[type="submit"]').hasClass("small")).toBeTruthy();
68
73
  expect(wrapper.find('button[type="submit"]').hasClass("hollow")).toBeTruthy();
69
74
  });
70
75
 
71
76
  it("should enable the submit button if textarea is not blank", () => {
72
- const wrapper = mount(<AddCommentForm addComment={addCommentStub} session={session} commentable={commentable} rootCommentable={commentable} orderBy={orderBy} />);
77
+ const wrapper = mount(<AddCommentForm commentsMaxLength={commentsMaxLength} addComment={addCommentStub} session={session} commentable={commentable} rootCommentable={commentable} orderBy={orderBy} />);
73
78
  wrapper.find("textarea").simulate("change", {
74
79
  target: {
75
80
  value: "This is a comment"
@@ -79,7 +84,7 @@ describe("<AddCommentForm />", () => {
79
84
  });
80
85
 
81
86
  it("should disable the submit button if textarea is blank", () => {
82
- const wrapper = mount(<AddCommentForm addComment={addCommentStub} session={session} commentable={commentable} rootCommentable={commentable} orderBy={orderBy} />);
87
+ const wrapper = mount(<AddCommentForm commentsMaxLength={commentsMaxLength} addComment={addCommentStub} session={session} commentable={commentable} rootCommentable={commentable} orderBy={orderBy} />);
83
88
  wrapper.find("textarea").simulate("change", {
84
89
  target: {
85
90
  value: "This will be deleted"
@@ -94,19 +99,19 @@ describe("<AddCommentForm />", () => {
94
99
  });
95
100
 
96
101
  it("should not render a div with class 'opinion-toggle'", () => {
97
- const wrapper = shallow(<AddCommentForm addComment={addCommentStub} session={session} commentable={commentable} rootCommentable={commentable} orderBy={orderBy} />);
102
+ const wrapper = shallow(<AddCommentForm commentsMaxLength={commentsMaxLength} addComment={addCommentStub} session={session} commentable={commentable} rootCommentable={commentable} orderBy={orderBy} />);
98
103
  expect(wrapper.find(".opinion-toggle").exists()).toBeFalsy();
99
104
  });
100
105
 
101
106
  it("should render the remaining character count", () => {
102
- const wrapper = shallow(<AddCommentForm addComment={addCommentStub} session={session} commentable={commentable} rootCommentable={commentable} orderBy={orderBy} />);
107
+ const wrapper = shallow(<AddCommentForm commentsMaxLength={commentsMaxLength} addComment={addCommentStub} session={session} commentable={commentable} rootCommentable={commentable} orderBy={orderBy} />);
103
108
  const commentBody = "This is a new comment!";
104
109
  wrapper.find("textarea").simulate("change", {
105
110
  target: {
106
111
  value: commentBody
107
112
  }
108
113
  });
109
- expect(wrapper.find(".remaining-character-count").text()).toContain(MAX_LENGTH - commentBody.length);
114
+ expect(wrapper.find(".remaining-character-count").text()).toContain(commentsMaxLength - commentBody.length);
110
115
  });
111
116
 
112
117
  describe("submitting the form", () => {
@@ -118,14 +123,14 @@ describe("<AddCommentForm />", () => {
118
123
  beforeEach(() => {
119
124
  addComment = jasmine.createSpy("addComment");
120
125
  onCommentAdded = jasmine.createSpy("onCommentAdded");
121
- wrapper = mount(<AddCommentForm addComment={addComment} session={session} commentable={commentable} onCommentAdded={onCommentAdded} rootCommentable={commentable} orderBy={orderBy} />);
126
+ wrapper = mount(<AddCommentForm commentsMaxLength={commentsMaxLength} addComment={addComment} session={session} commentable={commentable} onCommentAdded={onCommentAdded} rootCommentable={commentable} orderBy={orderBy} />);
122
127
  message = "This will be submitted";
123
128
  (wrapper.instance() as AddCommentForm).bodyTextArea.value = message;
124
129
  });
125
130
 
126
131
  it("should call addComment prop with the textarea value and state property alignment", () => {
127
132
  wrapper.find("form").simulate("submit");
128
- expect(addComment).toHaveBeenCalledWith({ body: message, alignment: 0 });
133
+ expect(addComment).toHaveBeenCalledWith({ body: message, alignment: 0 }, context);
129
134
  });
130
135
 
131
136
  it("should reset textarea", () => {
@@ -146,25 +151,25 @@ describe("<AddCommentForm />", () => {
146
151
  });
147
152
 
148
153
  it("should initialize state with a property alignment and value 0", () => {
149
- const wrapper = shallow(<AddCommentForm addComment={addCommentStub} session={session} commentable={commentable} arguable={true} rootCommentable={commentable} orderBy={orderBy} />);
154
+ const wrapper = shallow(<AddCommentForm commentsMaxLength={commentsMaxLength} addComment={addCommentStub} session={session} commentable={commentable} arguable={true} rootCommentable={commentable} orderBy={orderBy} />);
150
155
  expect(wrapper.state()).toHaveProperty("alignment", 0);
151
156
  });
152
157
 
153
158
  describe("when receiving an optional prop arguable with value true", () => {
154
159
  it("should render a div with class 'opinion-toggle'", () => {
155
- const wrapper = shallow(<AddCommentForm addComment={addCommentStub} session={session} commentable={commentable} arguable={true} rootCommentable={commentable} orderBy={orderBy} />);
160
+ const wrapper = shallow(<AddCommentForm commentsMaxLength={commentsMaxLength} addComment={addCommentStub} session={session} commentable={commentable} arguable={true} rootCommentable={commentable} orderBy={orderBy} />);
156
161
  expect(wrapper.find(".opinion-toggle")).toBeDefined();
157
162
  });
158
163
 
159
164
  it("should set state alignment to 1 if user clicks ok button and change its class", () => {
160
- const wrapper = shallow(<AddCommentForm addComment={addCommentStub} session={session} commentable={commentable} arguable={true} rootCommentable={commentable} orderBy={orderBy} />);
165
+ const wrapper = shallow(<AddCommentForm commentsMaxLength={commentsMaxLength} addComment={addCommentStub} session={session} commentable={commentable} arguable={true} rootCommentable={commentable} orderBy={orderBy} />);
161
166
  wrapper.find(".opinion-toggle--ok").simulate("click");
162
167
  expect(wrapper.find(".opinion-toggle--ok").hasClass("is-active")).toBeTruthy();
163
168
  expect(wrapper.state()).toHaveProperty("alignment", 1);
164
169
  });
165
170
 
166
171
  it("should set state alignment to -11 if user clicks ko button and change its class", () => {
167
- const wrapper = shallow(<AddCommentForm addComment={addCommentStub} session={session} commentable={commentable} arguable={true} rootCommentable={commentable} orderBy={orderBy} />);
172
+ const wrapper = shallow(<AddCommentForm commentsMaxLength={commentsMaxLength} addComment={addCommentStub} session={session} commentable={commentable} arguable={true} rootCommentable={commentable} orderBy={orderBy} />);
168
173
  wrapper.find(".opinion-toggle--ko").simulate("click");
169
174
  expect(wrapper.find(".opinion-toggle--ko").hasClass("is-active")).toBeTruthy();
170
175
  expect(wrapper.state()).toHaveProperty("alignment", -1);
@@ -177,7 +182,7 @@ describe("<AddCommentForm />", () => {
177
182
 
178
183
  beforeEach(() => {
179
184
  addComment = jasmine.createSpy("addComment");
180
- wrapper = mount(<AddCommentForm addComment={addComment} session={session} commentable={commentable} arguable={true} rootCommentable={commentable} orderBy={orderBy} />);
185
+ wrapper = mount(<AddCommentForm commentsMaxLength={commentsMaxLength} addComment={addComment} session={session} commentable={commentable} arguable={true} rootCommentable={commentable} orderBy={orderBy} />);
181
186
  message = "This will be submitted";
182
187
  (wrapper.instance() as AddCommentForm).bodyTextArea.value = message;
183
188
  });
@@ -185,7 +190,7 @@ describe("<AddCommentForm />", () => {
185
190
  it("should call addComment prop with the state's property alignment", () => {
186
191
  wrapper.find("button.opinion-toggle--ko").simulate("click");
187
192
  wrapper.find("form").simulate("submit");
188
- expect(addComment).toHaveBeenCalledWith({ body: message, alignment: -1 });
193
+ expect(addComment).toHaveBeenCalledWith({ body: message, alignment: -1 }, context);
189
194
  });
190
195
 
191
196
  it("should reset the state to its initial state", () => {
@@ -205,12 +210,12 @@ describe("<AddCommentForm />", () => {
205
210
  });
206
211
 
207
212
  it("should have a reference to user_group_id select", () => {
208
- const wrapper = mount(<AddCommentForm addComment={addCommentStub} session={session} commentable={commentable} rootCommentable={commentable} orderBy={orderBy} />);
213
+ const wrapper = mount(<AddCommentForm commentsMaxLength={commentsMaxLength} addComment={addCommentStub} session={session} commentable={commentable} rootCommentable={commentable} orderBy={orderBy} />);
209
214
  expect((wrapper.instance() as AddCommentForm).userGroupIdSelect).toBeDefined();
210
215
  });
211
216
 
212
217
  it("should render a select with option tags for each verified user group", () => {
213
- const wrapper = mount(<AddCommentForm addComment={addCommentStub} session={session} commentable={commentable} rootCommentable={commentable} orderBy={orderBy} />);
218
+ const wrapper = mount(<AddCommentForm commentsMaxLength={commentsMaxLength} addComment={addCommentStub} session={session} commentable={commentable} rootCommentable={commentable} orderBy={orderBy} />);
214
219
  expect(wrapper.find("select").children("option").length).toBe(3);
215
220
  });
216
221
 
@@ -222,7 +227,7 @@ describe("<AddCommentForm />", () => {
222
227
 
223
228
  beforeEach(() => {
224
229
  addComment = jasmine.createSpy("addComment");
225
- wrapper = mount(<AddCommentForm addComment={addComment} session={session} commentable={commentable} rootCommentable={commentable} orderBy={orderBy} />);
230
+ wrapper = mount(<AddCommentForm commentsMaxLength={commentsMaxLength} addComment={addComment} session={session} commentable={commentable} rootCommentable={commentable} orderBy={orderBy} />);
226
231
  message = "This will be submitted";
227
232
  userGroupId = session.verifiedUserGroups[1].id;
228
233
  (wrapper.instance() as AddCommentForm).bodyTextArea.value = message;
@@ -231,7 +236,7 @@ describe("<AddCommentForm />", () => {
231
236
 
232
237
  it("should call addComment prop with the body textarea, alignment and user_group_id select values", () => {
233
238
  wrapper.find("form").simulate("submit");
234
- expect(addComment).toHaveBeenCalledWith({ body: message, alignment: 0, userGroupId });
239
+ expect(addComment).toHaveBeenCalledWith({ body: message, alignment: 0, userGroupId }, context);
235
240
  });
236
241
 
237
242
  describe("when user_group_id is blank", () => {
@@ -241,7 +246,7 @@ describe("<AddCommentForm />", () => {
241
246
 
242
247
  it("should call addComment prop with the body textarea and alignment", () => {
243
248
  wrapper.find("form").simulate("submit");
244
- expect(addComment).toHaveBeenCalledWith({ body: message, alignment: 0 });
249
+ expect(addComment).toHaveBeenCalledWith({ body: message, alignment: 0 }, context);
245
250
  });
246
251
  });
247
252
  });
@@ -253,7 +258,7 @@ describe("<AddCommentForm />", () => {
253
258
  });
254
259
 
255
260
  it("display a message to sign in or sign up", () => {
256
- const wrapper = mount(<AddCommentForm addComment={addCommentStub} session={session} commentable={commentable} rootCommentable={commentable} orderBy={orderBy} />);
261
+ const wrapper = mount(<AddCommentForm commentsMaxLength={commentsMaxLength} addComment={addCommentStub} session={session} commentable={commentable} rootCommentable={commentable} orderBy={orderBy} />);
257
262
  expect(wrapper.find("span").text()).toContain("sign up");
258
263
  });
259
264
  });
@@ -4,6 +4,8 @@ import * as React from "react";
4
4
  import { graphql } from "react-apollo";
5
5
  import * as uuid from "uuid";
6
6
 
7
+ const PropTypes = require("prop-types");
8
+
7
9
  import Icon from "../application/icon.component";
8
10
 
9
11
  const { I18n, Translate } = require("react-i18nify");
@@ -27,9 +29,10 @@ interface AddCommentFormProps {
27
29
  autoFocus?: boolean;
28
30
  arguable?: boolean;
29
31
  userAllowedToComment?: boolean;
30
- addComment?: (data: { body: string, alignment: number, userGroupId?: string }) => void;
32
+ addComment?: (data: { body: string, alignment: number, userGroupId?: string }, context: any) => void;
31
33
  onCommentAdded?: () => void;
32
34
  orderBy: string;
35
+ commentsMaxLength: number;
33
36
  }
34
37
 
35
38
  interface AddCommentFormState {
@@ -39,8 +42,6 @@ interface AddCommentFormState {
39
42
  remainingCharacterCount: number;
40
43
  }
41
44
 
42
- export const MAX_LENGTH = 1000;
43
-
44
45
  /**
45
46
  * Renders a form to create new comments.
46
47
  * @class
@@ -54,6 +55,11 @@ export class AddCommentForm extends React.Component<AddCommentFormProps, AddComm
54
55
  autoFocus: false
55
56
  };
56
57
 
58
+ public static contextTypes: any = {
59
+ locale: PropTypes.string,
60
+ toggleTranslations: PropTypes.bool
61
+ };
62
+
57
63
  public bodyTextArea: HTMLTextAreaElement;
58
64
  public userGroupIdSelect: HTMLSelectElement;
59
65
 
@@ -64,7 +70,7 @@ export class AddCommentForm extends React.Component<AddCommentFormProps, AddComm
64
70
  disabled: true,
65
71
  error: false,
66
72
  alignment: 0,
67
- remainingCharacterCount: MAX_LENGTH
73
+ remainingCharacterCount: props.commentsMaxLength
68
74
  };
69
75
  }
70
76
 
@@ -79,6 +85,17 @@ export class AddCommentForm extends React.Component<AddCommentFormProps, AddComm
79
85
  );
80
86
  }
81
87
 
88
+ public componentDidMount() {
89
+ this._attachMentions();
90
+ }
91
+
92
+ /**
93
+ * Trick to reuse input_mentions.js logic
94
+ */
95
+ private _attachMentions() {
96
+ window.$(document).trigger("attach-mentions-element", this.bodyTextArea);
97
+ }
98
+
82
99
  /**
83
100
  * Render the form heading based on showTitle prop
84
101
  * @private
@@ -89,9 +106,9 @@ export class AddCommentForm extends React.Component<AddCommentFormProps, AddComm
89
106
 
90
107
  if (showTitle) {
91
108
  return (
92
- <h5 className="section-heading">
109
+ <h4 className="section-heading">
93
110
  {I18n.t("components.add_comment_form.title")}
94
- </h5>
111
+ </h4>
95
112
  );
96
113
  }
97
114
 
@@ -137,7 +154,9 @@ export class AddCommentForm extends React.Component<AddCommentFormProps, AddComm
137
154
  {this._renderCommentAs()}
138
155
  <div className="field">
139
156
  <label className="show-for-sr" htmlFor={`add-comment-${type}-${id}`}>{I18n.t("components.add_comment_form.form.body.label")}</label>
140
- {this._renderTextArea()}
157
+ <div className="hashtags__container">
158
+ {this._renderTextArea()}
159
+ </div>
141
160
  {this._renderTextAreaError()}
142
161
  <button
143
162
  type="submit"
@@ -163,7 +182,7 @@ export class AddCommentForm extends React.Component<AddCommentFormProps, AddComm
163
182
  * @returns {Void|DOMElement} - The heading or an empty element
164
183
  */
165
184
  private _renderTextArea() {
166
- const { commentable: { id, type }, autoFocus } = this.props;
185
+ const { commentable: { id, type }, autoFocus, commentsMaxLength } = this.props;
167
186
  const { error } = this.state;
168
187
  const className = classnames({ "is-invalid-input": error });
169
188
 
@@ -172,11 +191,11 @@ export class AddCommentForm extends React.Component<AddCommentFormProps, AddComm
172
191
  id: `add-comment-${type}-${id}`,
173
192
  className,
174
193
  rows: "4",
175
- maxLength: MAX_LENGTH,
194
+ maxLength: commentsMaxLength,
176
195
  required: "required",
177
- pattern: `^(.){0,${MAX_LENGTH}}$`,
196
+ pattern: `^(.){0,${commentsMaxLength}}$`,
178
197
  placeholder: I18n.t("components.add_comment_form.form.body.placeholder"),
179
- onChange: (evt: React.ChangeEvent<HTMLTextAreaElement>) => this._checkCommentBody(evt.target.value)
198
+ onChange: (evt: React.ChangeEvent<HTMLTextAreaElement>) => this._checkCommentBody(evt.target.value, commentsMaxLength as number)
180
199
  };
181
200
 
182
201
  if (autoFocus) {
@@ -194,12 +213,13 @@ export class AddCommentForm extends React.Component<AddCommentFormProps, AddComm
194
213
  * @returns {Void|DOMElement} - The error or an empty element
195
214
  */
196
215
  private _renderTextAreaError() {
216
+ const { commentsMaxLength } = this.props;
197
217
  const { error } = this.state;
198
218
 
199
219
  if (error) {
200
220
  return (
201
221
  <span className="form-error is-visible">
202
- {I18n.t("components.add_comment_form.form.form_error", { length: MAX_LENGTH })}
222
+ {I18n.t("components.add_comment_form.form.form_error", { length: commentsMaxLength })}
203
223
  </span>
204
224
  );
205
225
  }
@@ -304,10 +324,10 @@ export class AddCommentForm extends React.Component<AddCommentFormProps, AddComm
304
324
  * @param {string} body - The comment's body
305
325
  * @returns {Void} - Returns nothing
306
326
  */
307
- private _checkCommentBody(body: string) {
327
+ private _checkCommentBody(body: string, commentsMaxLength: number) {
308
328
  this.setState({
309
- disabled: body === "", error: body === "" || body.length > MAX_LENGTH,
310
- remainingCharacterCount: MAX_LENGTH - body.length
329
+ disabled: body === "", error: body === "" || body.length > commentsMaxLength,
330
+ remainingCharacterCount: commentsMaxLength - body.length
311
331
  });
312
332
  }
313
333
 
@@ -330,7 +350,7 @@ export class AddCommentForm extends React.Component<AddCommentFormProps, AddComm
330
350
  }
331
351
 
332
352
  if (addComment) {
333
- addComment(addCommentParams);
353
+ addComment(addCommentParams, this.context);
334
354
  }
335
355
 
336
356
  this.bodyTextArea.value = "";
@@ -347,10 +367,12 @@ const getCommentsQuery = require("../queries/comments.query.graphql");
347
367
 
348
368
  const AddCommentFormWithMutation = graphql<addCommentMutation, AddCommentFormProps>(addCommentMutation, {
349
369
  props: ({ ownProps, mutate }) => ({
350
- addComment: ({ body, alignment, userGroupId }: { body: string, alignment: number, userGroupId: string }) => {
370
+ addComment: ({ body, alignment, userGroupId }: { body: string, alignment: number, userGroupId: string }, { locale, toggleTranslations }: any) => {
351
371
  if (mutate) {
352
372
  mutate({
353
373
  variables: {
374
+ locale,
375
+ toggleTranslations,
354
376
  commentableId: ownProps.commentable.id,
355
377
  commentableType: ownProps.commentable.type,
356
378
  body,
@@ -368,10 +390,14 @@ const AddCommentFormWithMutation = graphql<addCommentMutation, AddCommentFormPro
368
390
  createdAt: new Date().toISOString(),
369
391
  body,
370
392
  formattedBody: body,
393
+ formattedCreatedAt: new Date().toISOString(),
371
394
  alignment,
372
395
  author: {
373
396
  __typename: "User",
374
397
  name: ownProps.session && ownProps.session.user.name,
398
+ nickname: ownProps.session && ownProps.session.user.name,
399
+ profilePath: null,
400
+ badge: null,
375
401
  avatarUrl: ownProps.session && ownProps.session.user.avatarUrl,
376
402
  deleted: false
377
403
  },
@@ -389,14 +415,17 @@ const AddCommentFormWithMutation = graphql<addCommentMutation, AddCommentFormPro
389
415
  },
390
416
  update: (store, { data }: { data: addCommentMutation }) => {
391
417
  const variables = {
418
+ locale,
419
+ toggleTranslations,
392
420
  commentableId: ownProps.rootCommentable.id,
393
421
  commentableType: ownProps.rootCommentable.type,
394
- orderBy: ownProps.orderBy
422
+ orderBy: ownProps.orderBy,
423
+ singleCommentId: null
395
424
  };
396
425
  const prev = store.readQuery<GetCommentsQuery>({
397
426
  query: getCommentsQuery,
398
427
  variables
399
- });
428
+ });
400
429
  const { id, type } = ownProps.commentable;
401
430
  const newComment = data.commentable && data.commentable.addComment;
402
431
  let comments = [];