decidim-comments 0.23.2 → 0.24.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (148) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/javascripts/decidim/comments/bundle.js.map +1 -1
  3. data/app/assets/javascripts/decidim/comments/comments.component.js.es6 +292 -0
  4. data/app/assets/javascripts/decidim/comments/comments.component.test.js +581 -0
  5. data/app/assets/javascripts/decidim/comments/comments.js.erb +1 -1
  6. data/app/cells/decidim/comments/comment/actions.erb +7 -0
  7. data/app/cells/decidim/comments/comment/alignment_badge.erb +4 -0
  8. data/app/cells/decidim/comments/comment/author.erb +1 -0
  9. data/app/cells/decidim/comments/comment/show.erb +40 -0
  10. data/app/cells/decidim/comments/comment/utilities.erb +13 -0
  11. data/app/cells/decidim/comments/comment/votes.erb +25 -0
  12. data/app/cells/decidim/comments/comment_card_cell.rb +22 -0
  13. data/app/cells/decidim/comments/comment_cell.rb +142 -8
  14. data/app/cells/decidim/comments/comment_form/comment_as.erb +10 -0
  15. data/app/cells/decidim/comments/comment_form/show.erb +24 -0
  16. data/app/cells/decidim/comments/comment_form_cell.rb +94 -0
  17. data/app/cells/decidim/comments/comment_m/top.erb +1 -1
  18. data/app/cells/decidim/comments/comment_thread/show.erb +6 -0
  19. data/app/cells/decidim/comments/comment_thread/title.erb +3 -0
  20. data/app/cells/decidim/comments/comment_thread_cell.rb +30 -0
  21. data/app/cells/decidim/comments/comments/add_comment.erb +30 -0
  22. data/app/cells/decidim/comments/comments/blocked_comments_warning.erb +3 -0
  23. data/app/cells/decidim/comments/comments/order_control.erb +37 -0
  24. data/app/cells/decidim/comments/comments/show.erb +32 -0
  25. data/app/cells/decidim/comments/comments/single_comment_warning.erb +9 -0
  26. data/app/cells/decidim/comments/comments/user_comments_blocked_warning.erb +3 -0
  27. data/app/cells/decidim/comments/comments_cell.rb +134 -0
  28. data/app/commands/decidim/comments/vote_comment.rb +34 -10
  29. data/app/controllers/decidim/comments/application_controller.rb +16 -0
  30. data/app/controllers/decidim/comments/comments_controller.rb +122 -0
  31. data/app/controllers/decidim/comments/votes_controller.rb +41 -0
  32. data/app/events/decidim/comments/comment_created_event.rb +1 -1
  33. data/app/events/decidim/comments/comment_downvoted_event.rb +8 -0
  34. data/app/events/decidim/comments/comment_upvoted_event.rb +8 -0
  35. data/app/events/decidim/comments/comment_voted_event.rb +26 -0
  36. data/app/forms/decidim/comments/comment_form.rb +6 -2
  37. data/app/models/decidim/comments/comment.rb +22 -6
  38. data/app/models/decidim/comments/seed.rb +1 -1
  39. data/app/permissions/decidim/comments/permissions.rb +59 -0
  40. data/app/queries/decidim/comments/metrics/comment_participants_metric_measure.rb +2 -2
  41. data/app/queries/decidim/comments/metrics/comments_metric_manage.rb +5 -6
  42. data/app/queries/decidim/comments/sorted_comments.rb +18 -14
  43. data/app/views/decidim/comments/comments/_comment.html.erb +5 -0
  44. data/app/views/decidim/comments/comments/_comments.html.erb +1 -0
  45. data/app/views/decidim/comments/comments/create.js.erb +16 -0
  46. data/app/views/decidim/comments/comments/error.js.erb +1 -0
  47. data/app/views/decidim/comments/comments/index.js.erb +24 -0
  48. data/app/views/decidim/comments/comments/reload.js.erb +21 -0
  49. data/app/views/decidim/comments/votes/create.js.erb +23 -0
  50. data/app/views/decidim/comments/votes/error.js.erb +1 -0
  51. data/config/locales/ar.yml +0 -2
  52. data/config/locales/ca.yml +17 -2
  53. data/config/locales/cs.yml +19 -2
  54. data/config/locales/de.yml +17 -2
  55. data/config/locales/el.yml +0 -2
  56. data/config/locales/en.yml +17 -2
  57. data/config/locales/es-MX.yml +17 -2
  58. data/config/locales/es-PY.yml +17 -2
  59. data/config/locales/es.yml +17 -2
  60. data/config/locales/eu.yml +0 -2
  61. data/config/locales/fi-plain.yml +17 -2
  62. data/config/locales/fi.yml +17 -2
  63. data/config/locales/fr-CA.yml +17 -2
  64. data/config/locales/fr.yml +17 -2
  65. data/config/locales/gl.yml +7 -2
  66. data/config/locales/hu.yml +0 -2
  67. data/config/locales/id-ID.yml +0 -2
  68. data/config/locales/is-IS.yml +0 -1
  69. data/config/locales/it.yml +4 -2
  70. data/config/locales/ja.yml +9 -5
  71. data/config/locales/lv.yml +0 -2
  72. data/config/locales/nl.yml +0 -2
  73. data/config/locales/no.yml +0 -2
  74. data/config/locales/pl.yml +21 -4
  75. data/config/locales/pt-BR.yml +0 -2
  76. data/config/locales/pt.yml +0 -2
  77. data/config/locales/ro-RO.yml +15 -2
  78. data/config/locales/ru.yml +0 -2
  79. data/config/locales/sk.yml +0 -2
  80. data/config/locales/sv.yml +7 -2
  81. data/config/locales/tr-TR.yml +7 -2
  82. data/config/locales/uk.yml +0 -1
  83. data/config/locales/zh-CN.yml +0 -2
  84. data/lib/decidim/api/add_comment_type.rb +13 -0
  85. data/lib/decidim/api/comment_mutation_type.rb +22 -0
  86. data/lib/decidim/api/comment_type.rb +89 -0
  87. data/lib/decidim/api/commentable_interface.rb +50 -0
  88. data/lib/decidim/api/commentable_mutation_type.rb +30 -0
  89. data/{app/types/decidim/comments → lib/decidim/api}/commentable_type.rb +2 -5
  90. data/lib/decidim/comments.rb +1 -3
  91. data/lib/decidim/comments/api.rb +12 -0
  92. data/lib/decidim/comments/comments_helper.rb +10 -52
  93. data/lib/decidim/comments/engine.rb +9 -7
  94. data/lib/decidim/comments/mutation_extensions.rb +22 -22
  95. data/lib/decidim/comments/query_extensions.rb +12 -14
  96. data/lib/decidim/comments/test.rb +1 -0
  97. data/lib/decidim/comments/test/shared_examples/comment_voted_event.rb +65 -0
  98. data/lib/decidim/comments/version.rb +1 -1
  99. metadata +65 -64
  100. data/app/assets/javascripts/decidim/comments/bundle.js +0 -268
  101. data/app/frontend/application/apollo_client.ts +0 -12
  102. data/app/frontend/application/application.component.test.tsx +0 -23
  103. data/app/frontend/application/application.component.tsx +0 -35
  104. data/app/frontend/application/icon.component.test.tsx +0 -38
  105. data/app/frontend/application/icon.component.tsx +0 -46
  106. data/app/frontend/comments/add_comment_form.component.test.tsx +0 -265
  107. data/app/frontend/comments/add_comment_form.component.tsx +0 -482
  108. data/app/frontend/comments/comment.component.test.tsx +0 -490
  109. data/app/frontend/comments/comment.component.tsx +0 -677
  110. data/app/frontend/comments/comment_order_selector.component.test.tsx +0 -29
  111. data/app/frontend/comments/comment_order_selector.component.tsx +0 -106
  112. data/app/frontend/comments/comment_thread.component.test.tsx +0 -82
  113. data/app/frontend/comments/comment_thread.component.tsx +0 -81
  114. data/app/frontend/comments/comments.component.test.tsx +0 -150
  115. data/app/frontend/comments/comments.component.tsx +0 -289
  116. data/app/frontend/comments/down_vote_button.component.test.tsx +0 -59
  117. data/app/frontend/comments/down_vote_button.component.tsx +0 -133
  118. data/app/frontend/comments/up_vote_button.component.test.tsx +0 -59
  119. data/app/frontend/comments/up_vote_button.component.tsx +0 -133
  120. data/app/frontend/comments/vote_button.component.tsx +0 -50
  121. data/app/frontend/comments/vote_button_component.test.tsx +0 -64
  122. data/app/frontend/entry.ts +0 -38
  123. data/app/frontend/entry_test.ts +0 -6
  124. data/app/frontend/fragments/add_comment_form_commentable.fragment.graphql +0 -4
  125. data/app/frontend/fragments/add_comment_form_session.fragment.graphql +0 -6
  126. data/app/frontend/fragments/comment.fragment.graphql +0 -14
  127. data/app/frontend/fragments/comment_data.fragment.graphql +0 -27
  128. data/app/frontend/fragments/comment_thread.fragment.graphql +0 -6
  129. data/app/frontend/fragments/down_vote_button.fragment.graphql +0 -6
  130. data/app/frontend/fragments/up_vote_button.fragment.graphql +0 -6
  131. data/app/frontend/mutations/add_comment.mutation.graphql +0 -9
  132. data/app/frontend/mutations/down_vote.mutation.graphql +0 -9
  133. data/app/frontend/mutations/up_vote.mutation.graphql +0 -9
  134. data/app/frontend/queries/comments.query.graphql +0 -26
  135. data/app/frontend/support/asset_url.ts +0 -11
  136. data/app/frontend/support/generate_comments_data.ts +0 -49
  137. data/app/frontend/support/generate_user_data.ts +0 -14
  138. data/app/frontend/support/generate_user_group_data.ts +0 -14
  139. data/app/frontend/support/graphql_transformer.js +0 -32
  140. data/app/frontend/support/load_translations.ts +0 -48
  141. data/app/frontend/support/require_all.ts +0 -10
  142. data/app/frontend/support/resolve_graphql_query.ts +0 -37
  143. data/app/frontend/support/schema.ts +0 -2026
  144. data/app/types/decidim/comments/commentable_interface.rb +0 -61
  145. data/app/types/decidim/comments/commentable_mutation_type.rb +0 -33
  146. data/lib/decidim/comments/api/add_comment_type.rb +0 -13
  147. data/lib/decidim/comments/api/comment_mutation_type.rb +0 -20
  148. data/lib/decidim/comments/api/comment_type.rb +0 -89
@@ -1,12 +0,0 @@
1
- import { InMemoryCache } from "apollo-cache-inmemory";
2
- import { ApolloClient } from "apollo-client";
3
- import { HttpLink } from "apollo-link-http";
4
-
5
- import "unfetch/polyfill";
6
-
7
- const client = new ApolloClient({
8
- link: new HttpLink({ uri: "/api", credentials: "same-origin", fetch }),
9
- cache: new InMemoryCache()
10
- });
11
-
12
- export default client;
@@ -1,23 +0,0 @@
1
- import { shallow } from "enzyme";
2
- import * as React from "react";
3
-
4
- import Application from "./application.component";
5
-
6
- const { I18n } = require("react-i18nify");
7
-
8
- describe("<Application />", () => {
9
- afterEach(() => {
10
- I18n.setLocale("en");
11
- });
12
-
13
- it("should set I18n locale to locale prop", () => {
14
- spyOn(I18n, "setLocale");
15
- const locale = "ca";
16
- shallow(
17
- <Application locale={locale}>
18
- <div>My application</div>
19
- </Application>
20
- );
21
- expect(I18n.setLocale).toHaveBeenCalledWith(locale);
22
- });
23
- });
@@ -1,35 +0,0 @@
1
- import * as React from "react";
2
- import { ApolloProvider } from "react-apollo";
3
-
4
- import apolloClient from "./apollo_client";
5
-
6
- const { I18n } = require("react-i18nify");
7
-
8
- interface ApplicationProps {
9
- locale: string;
10
- }
11
-
12
- /**
13
- * Wrapper component for all React applications using Apollo
14
- * @class
15
- * @augments Component
16
- */
17
- export default class Application extends React.Component<ApplicationProps> {
18
- constructor(props: ApplicationProps) {
19
- const { locale } = props;
20
-
21
- I18n.setLocale(locale);
22
-
23
- super(props);
24
- }
25
-
26
- public render() {
27
- const { children } = this.props;
28
-
29
- return (
30
- <ApolloProvider client={apolloClient}>
31
- {children}
32
- </ApolloProvider>
33
- );
34
- }
35
- }
@@ -1,38 +0,0 @@
1
- import { mount, shallow } from "enzyme";
2
- import * as React from "react";
3
-
4
- import { Icon } from "./icon.component";
5
-
6
- describe("<Icon /", () => {
7
- beforeEach(() => {
8
- window.DecidimComments = {
9
- assets: {
10
- "icons.svg": "/assets/icons.svg"
11
- }
12
- };
13
- });
14
-
15
- it("should render a svg with class defined by prop className", () => {
16
- const wrapper = shallow(<Icon name="icon-thumb-down" />);
17
- expect(wrapper.find("svg.icon-thumb-down").exists()).toBeTruthy();
18
- });
19
-
20
- it("should render a svg icon using the 'icons.svg' url and name", () => {
21
- const wrapper = shallow(<Icon name="icon-thumb-up" />);
22
- expect(wrapper.find("svg use").prop("xlinkHref")).toBe(
23
- "/assets/icons.svg#icon-thumb-up"
24
- );
25
- });
26
-
27
- it("has a default prop iconExtraClassName with value 'icon--before'", () => {
28
- const wrapper = mount(<Icon name="icon-thumb-up" />);
29
- expect(wrapper.prop("iconExtraClassName")).toBe("icon--before");
30
- });
31
-
32
- it("renders the svg with an extra class defined by iconExtraClassName", () => {
33
- const wrapper = mount(
34
- <Icon name="icon-thumb-up" iconExtraClassName="icon--small" />
35
- );
36
- expect(wrapper.find(".icon--small").exists()).toBeTruthy();
37
- });
38
- });
@@ -1,46 +0,0 @@
1
- import * as React from "react";
2
- import assetUrl from "../support/asset_url";
3
-
4
- interface IconProps {
5
- name: string;
6
- title?: string;
7
- iconExtraClassName?: string;
8
- role?: string;
9
- }
10
-
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
-
17
- return (
18
- <svg className={`icon ${iconExtraClassName} ${name}`} role={role}>
19
- {titleElement}
20
- <use
21
- xmlnsXlink="http://www.w3.org/1999/xlink"
22
- xlinkHref={`${assetUrl("icons.svg")}#${name}`}
23
- />
24
- </svg>
25
- );
26
- };
27
-
28
- Icon.defaultProps = {
29
- iconExtraClassName: "icon--before"
30
- };
31
-
32
- interface IconWithoutUserAgentProps {
33
- name: string;
34
- title?: string;
35
- iconExtraClassName?: string;
36
- role?: string;
37
- }
38
-
39
- const IconWithoutUserAgent: React.SFC<IconWithoutUserAgentProps> = ({
40
- name,
41
- title,
42
- iconExtraClassName,
43
- role = "none presentation"
44
- }) => <Icon name={name} title={title} iconExtraClassName={iconExtraClassName} role={role} />;
45
-
46
- export default IconWithoutUserAgent;
@@ -1,265 +0,0 @@
1
- import { mount, ReactWrapper, shallow } from "enzyme";
2
- import * as $ from "jquery";
3
- import * as React from "react";
4
-
5
- import { AddCommentForm } from "./add_comment_form.component";
6
-
7
- import generateUserData from "../support/generate_user_data";
8
- import generateUserGroupData from "../support/generate_user_group_data";
9
- import { loadLocaleTranslations } from "../support/load_translations";
10
-
11
- describe("<AddCommentForm commentsMaxLength={commentsMaxLength} />", () => {
12
- let session: any = null;
13
- const commentsMaxLength: number = 1000;
14
- const commentable = {
15
- id: "1",
16
- type: "Decidim::DummyResources::DummyResource"
17
- };
18
- const orderBy = "older";
19
- const addCommentStub = (): any => {
20
- return null;
21
- };
22
- const context = {locale: undefined, toggleTranslations: undefined};
23
-
24
- beforeEach(() => {
25
- loadLocaleTranslations("en");
26
- session = {
27
- user: generateUserData(),
28
- verifiedUserGroups: []
29
- };
30
- window.DecidimComments = {
31
- assets: {
32
- "icons.svg": "/assets/icons.svg"
33
- }
34
- };
35
-
36
- window.$ = $;
37
- });
38
-
39
- it("should render a div with class add-comment", () => {
40
- const wrapper = shallow(<AddCommentForm commentsMaxLength={commentsMaxLength} addComment={addCommentStub} session={session} commentable={commentable} rootCommentable={commentable} orderBy={orderBy} />);
41
- expect(wrapper.find("div.add-comment")).toBeDefined();
42
- });
43
-
44
- it("should have a reference to body textarea", () => {
45
- const wrapper = mount(<AddCommentForm commentsMaxLength={commentsMaxLength} addComment={addCommentStub} session={session} commentable={commentable} rootCommentable={commentable} orderBy={orderBy} />);
46
- expect((wrapper.instance() as AddCommentForm).bodyTextArea).toBeDefined();
47
- });
48
-
49
- it("should initialize with a state property disabled as true", () => {
50
- const wrapper = mount(<AddCommentForm commentsMaxLength={commentsMaxLength} addComment={addCommentStub} session={session} commentable={commentable} rootCommentable={commentable} orderBy={orderBy} />);
51
- expect(wrapper.state()).toHaveProperty("disabled", true);
52
- });
53
-
54
- it("should have a default prop showTitle as true", () => {
55
- const wrapper = mount(<AddCommentForm commentsMaxLength={commentsMaxLength} addComment={addCommentStub} session={session} commentable={commentable} rootCommentable={commentable} orderBy={orderBy} />);
56
- expect(wrapper.props()).toHaveProperty("showTitle", true);
57
- });
58
-
59
- it("should not render the title if prop showTitle is false", () => {
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();
62
- });
63
-
64
- it("should have a default prop submitButtonClassName as 'button button--sc'", () => {
65
- const wrapper = mount(<AddCommentForm commentsMaxLength={commentsMaxLength} addComment={addCommentStub} session={session} commentable={commentable} rootCommentable={commentable} orderBy={orderBy} />);
66
- expect(wrapper.props()).toHaveProperty("submitButtonClassName", "button button--sc");
67
- });
68
-
69
- it("should use prop submitButtonClassName as a className prop for submit button", () => {
70
- const wrapper = shallow(<AddCommentForm commentsMaxLength={commentsMaxLength} addComment={addCommentStub} session={session} commentable={commentable} submitButtonClassName="button small hollow" rootCommentable={commentable} orderBy={orderBy} />);
71
- expect(wrapper.find('button[type="submit"]').hasClass("button")).toBeTruthy();
72
- expect(wrapper.find('button[type="submit"]').hasClass("small")).toBeTruthy();
73
- expect(wrapper.find('button[type="submit"]').hasClass("hollow")).toBeTruthy();
74
- });
75
-
76
- it("should enable the submit button if textarea is not blank", () => {
77
- const wrapper = mount(<AddCommentForm commentsMaxLength={commentsMaxLength} addComment={addCommentStub} session={session} commentable={commentable} rootCommentable={commentable} orderBy={orderBy} />);
78
- wrapper.find("textarea").simulate("change", {
79
- target: {
80
- value: "This is a comment"
81
- }
82
- });
83
- expect(wrapper.find('button[type="submit"]').props()).not.toHaveProperty("disabled", true);
84
- });
85
-
86
- it("should disable the submit button if textarea is blank", () => {
87
- const wrapper = mount(<AddCommentForm commentsMaxLength={commentsMaxLength} addComment={addCommentStub} session={session} commentable={commentable} rootCommentable={commentable} orderBy={orderBy} />);
88
- wrapper.find("textarea").simulate("change", {
89
- target: {
90
- value: "This will be deleted"
91
- }
92
- });
93
- wrapper.find("textarea").simulate("change", {
94
- target: {
95
- value: ""
96
- }
97
- });
98
- expect(wrapper.find('button[type="submit"]').props()).toHaveProperty("disabled", true);
99
- });
100
-
101
- it("should not render a div with class 'opinion-toggle'", () => {
102
- const wrapper = shallow(<AddCommentForm commentsMaxLength={commentsMaxLength} addComment={addCommentStub} session={session} commentable={commentable} rootCommentable={commentable} orderBy={orderBy} />);
103
- expect(wrapper.find(".opinion-toggle").exists()).toBeFalsy();
104
- });
105
-
106
- it("should render the remaining character count", () => {
107
- const wrapper = shallow(<AddCommentForm commentsMaxLength={commentsMaxLength} addComment={addCommentStub} session={session} commentable={commentable} rootCommentable={commentable} orderBy={orderBy} />);
108
- const commentBody = "This is a new comment!";
109
- wrapper.find("textarea").simulate("change", {
110
- target: {
111
- value: commentBody
112
- }
113
- });
114
- expect(wrapper.find(".remaining-character-count").text()).toContain(commentsMaxLength - commentBody.length);
115
- });
116
-
117
- describe("submitting the form", () => {
118
- let addComment: jasmine.Spy;
119
- let onCommentAdded: jasmine.Spy ;
120
- let wrapper: ReactWrapper<any, {}>;
121
- let message: any = null;
122
-
123
- beforeEach(() => {
124
- addComment = jasmine.createSpy("addComment");
125
- onCommentAdded = jasmine.createSpy("onCommentAdded");
126
- wrapper = mount(<AddCommentForm commentsMaxLength={commentsMaxLength} addComment={addComment} session={session} commentable={commentable} onCommentAdded={onCommentAdded} rootCommentable={commentable} orderBy={orderBy} />);
127
- message = "This will be submitted";
128
- (wrapper.instance() as AddCommentForm).bodyTextArea.value = message;
129
- });
130
-
131
- it("should call addComment prop with the textarea value and state property alignment", () => {
132
- wrapper.find("form").simulate("submit");
133
- expect(addComment).toHaveBeenCalledWith({ body: message, alignment: 0 }, context);
134
- });
135
-
136
- it("should reset textarea", () => {
137
- wrapper.find("form").simulate("submit");
138
- expect((wrapper.instance() as AddCommentForm).bodyTextArea.value).toBe("");
139
- });
140
-
141
- it("should prevent default form submission", () => {
142
- const preventDefault = jasmine.createSpy("preventDefault");
143
- wrapper.find("form").simulate("submit", { preventDefault });
144
- expect(preventDefault).toHaveBeenCalled();
145
- });
146
-
147
- it("should call the prop onCommentAdded function", () => {
148
- wrapper.find("form").simulate("submit");
149
- expect(onCommentAdded).toHaveBeenCalled();
150
- });
151
- });
152
-
153
- it("should initialize state with a property alignment and value 0", () => {
154
- const wrapper = shallow(<AddCommentForm commentsMaxLength={commentsMaxLength} addComment={addCommentStub} session={session} commentable={commentable} arguable={true} rootCommentable={commentable} orderBy={orderBy} />);
155
- expect(wrapper.state()).toHaveProperty("alignment", 0);
156
- });
157
-
158
- describe("when receiving an optional prop arguable with value true", () => {
159
- it("should render a div with class 'opinion-toggle'", () => {
160
- const wrapper = shallow(<AddCommentForm commentsMaxLength={commentsMaxLength} addComment={addCommentStub} session={session} commentable={commentable} arguable={true} rootCommentable={commentable} orderBy={orderBy} />);
161
- expect(wrapper.find(".opinion-toggle")).toBeDefined();
162
- });
163
-
164
- it("should set state alignment to 1 if user clicks ok button and change its class", () => {
165
- const wrapper = shallow(<AddCommentForm commentsMaxLength={commentsMaxLength} addComment={addCommentStub} session={session} commentable={commentable} arguable={true} rootCommentable={commentable} orderBy={orderBy} />);
166
- wrapper.find(".opinion-toggle--ok").simulate("click");
167
- expect(wrapper.find(".opinion-toggle--ok").hasClass("is-active")).toBeTruthy();
168
- expect(wrapper.state()).toHaveProperty("alignment", 1);
169
- });
170
-
171
- it("should set state alignment to -11 if user clicks ko button and change its class", () => {
172
- const wrapper = shallow(<AddCommentForm commentsMaxLength={commentsMaxLength} addComment={addCommentStub} session={session} commentable={commentable} arguable={true} rootCommentable={commentable} orderBy={orderBy} />);
173
- wrapper.find(".opinion-toggle--ko").simulate("click");
174
- expect(wrapper.find(".opinion-toggle--ko").hasClass("is-active")).toBeTruthy();
175
- expect(wrapper.state()).toHaveProperty("alignment", -1);
176
- });
177
-
178
- describe("submitting the form", () => {
179
- let wrapper: ReactWrapper<any, {}>;
180
- let addComment: jasmine.Spy;
181
- let message: string;
182
-
183
- beforeEach(() => {
184
- addComment = jasmine.createSpy("addComment");
185
- wrapper = mount(<AddCommentForm commentsMaxLength={commentsMaxLength} addComment={addComment} session={session} commentable={commentable} arguable={true} rootCommentable={commentable} orderBy={orderBy} />);
186
- message = "This will be submitted";
187
- (wrapper.instance() as AddCommentForm).bodyTextArea.value = message;
188
- });
189
-
190
- it("should call addComment prop with the state's property alignment", () => {
191
- wrapper.find("button.opinion-toggle--ko").simulate("click");
192
- wrapper.find("form").simulate("submit");
193
- expect(addComment).toHaveBeenCalledWith({ body: message, alignment: -1 }, context);
194
- });
195
-
196
- it("should reset the state to its initial state", () => {
197
- wrapper.find("button.opinion-toggle--ok").simulate("click");
198
- wrapper.find("form").simulate("submit");
199
- expect(wrapper.state()).toHaveProperty("alignment", 0);
200
- });
201
- });
202
- });
203
-
204
- describe("when user groups are greater than 0", () => {
205
- beforeEach(() => {
206
- session.verifiedUserGroups = [
207
- generateUserGroupData(),
208
- generateUserGroupData()
209
- ];
210
- });
211
-
212
- it("should have a reference to user_group_id select", () => {
213
- const wrapper = mount(<AddCommentForm commentsMaxLength={commentsMaxLength} addComment={addCommentStub} session={session} commentable={commentable} rootCommentable={commentable} orderBy={orderBy} />);
214
- expect((wrapper.instance() as AddCommentForm).userGroupIdSelect).toBeDefined();
215
- });
216
-
217
- it("should render a select with option tags for each verified user group", () => {
218
- const wrapper = mount(<AddCommentForm commentsMaxLength={commentsMaxLength} addComment={addCommentStub} session={session} commentable={commentable} rootCommentable={commentable} orderBy={orderBy} />);
219
- expect(wrapper.find("select").children("option").length).toBe(3);
220
- });
221
-
222
- describe("submitting the form", () => {
223
- let addComment: jasmine.Spy;
224
- let wrapper: ReactWrapper<any, {}>;
225
- let message: string;
226
- let userGroupId: string;
227
-
228
- beforeEach(() => {
229
- addComment = jasmine.createSpy("addComment");
230
- wrapper = mount(<AddCommentForm commentsMaxLength={commentsMaxLength} addComment={addComment} session={session} commentable={commentable} rootCommentable={commentable} orderBy={orderBy} />);
231
- message = "This will be submitted";
232
- userGroupId = session.verifiedUserGroups[1].id;
233
- (wrapper.instance() as AddCommentForm).bodyTextArea.value = message;
234
- (wrapper.instance() as AddCommentForm).userGroupIdSelect.value = userGroupId;
235
- });
236
-
237
- it("should call addComment prop with the body textarea, alignment and user_group_id select values", () => {
238
- wrapper.find("form").simulate("submit");
239
- expect(addComment).toHaveBeenCalledWith({ body: message, alignment: 0, userGroupId }, context);
240
- });
241
-
242
- describe("when user_group_id is blank", () => {
243
- beforeEach(() => {
244
- (wrapper.instance() as AddCommentForm).userGroupIdSelect.value = "";
245
- });
246
-
247
- it("should call addComment prop with the body textarea and alignment", () => {
248
- wrapper.find("form").simulate("submit");
249
- expect(addComment).toHaveBeenCalledWith({ body: message, alignment: 0 }, context);
250
- });
251
- });
252
- });
253
- });
254
-
255
- describe("when session is null", () => {
256
- beforeEach(() => {
257
- session = null;
258
- });
259
-
260
- it("display a message to sign in or sign up", () => {
261
- const wrapper = mount(<AddCommentForm commentsMaxLength={commentsMaxLength} addComment={addCommentStub} session={session} commentable={commentable} rootCommentable={commentable} orderBy={orderBy} />);
262
- expect(wrapper.find("span").text()).toContain("sign up");
263
- });
264
- });
265
- });
@@ -1,482 +0,0 @@
1
- /* eslint-disable no-return-assign, react/no-unused-prop-types, max-lines */
2
- import * as classnames from "classnames";
3
- import * as React from "react";
4
- import { graphql } from "react-apollo";
5
- import * as uuid from "uuid";
6
-
7
- const PropTypes = require("prop-types");
8
-
9
- import Icon from "../application/icon.component";
10
-
11
- const { I18n, Translate } = require("react-i18nify");
12
-
13
- import {
14
- AddCommentFormCommentableFragment,
15
- AddCommentFormSessionFragment,
16
- addCommentMutation,
17
- CommentFragment,
18
- GetCommentsQuery
19
- } from "../support/schema";
20
-
21
- interface AddCommentFormProps {
22
- session: AddCommentFormSessionFragment & {
23
- user: any;
24
- } | null;
25
- commentable: AddCommentFormCommentableFragment;
26
- rootCommentable: AddCommentFormCommentableFragment;
27
- showTitle?: boolean;
28
- submitButtonClassName?: string;
29
- autoFocus?: boolean;
30
- arguable?: boolean;
31
- userAllowedToComment?: boolean;
32
- addComment?: (data: { body: string, alignment: number, userGroupId?: string }, context: any) => void;
33
- onCommentAdded?: () => void;
34
- orderBy: string;
35
- commentsMaxLength: number;
36
- }
37
-
38
- interface AddCommentFormState {
39
- disabled: boolean;
40
- error: boolean;
41
- alignment: number;
42
- remainingCharacterCount: number;
43
- }
44
-
45
- /**
46
- * Renders a form to create new comments.
47
- * @class
48
- * @augments Component
49
- */
50
- export class AddCommentForm extends React.Component<AddCommentFormProps, AddCommentFormState> {
51
- public static defaultProps = {
52
- showTitle: true,
53
- submitButtonClassName: "button button--sc",
54
- arguable: false,
55
- autoFocus: false
56
- };
57
-
58
- public static contextTypes: any = {
59
- locale: PropTypes.string,
60
- toggleTranslations: PropTypes.bool
61
- };
62
-
63
- public bodyTextArea: HTMLTextAreaElement;
64
- public userGroupIdSelect: HTMLSelectElement;
65
-
66
- constructor(props: AddCommentFormProps) {
67
- super(props);
68
-
69
- this.state = {
70
- disabled: true,
71
- error: false,
72
- alignment: 0,
73
- remainingCharacterCount: props.commentsMaxLength
74
- };
75
- }
76
-
77
- public render() {
78
- return (
79
- <div className="add-comment">
80
- {this._renderHeading()}
81
- {this._renderAccountMessage()}
82
- {this._renderOpinionButtons()}
83
- {this._renderForm()}
84
- </div>
85
- );
86
- }
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
-
99
- /**
100
- * Render the form heading based on showTitle prop
101
- * @private
102
- * @returns {Void|DOMElement} - The heading or an empty element
103
- */
104
- private _renderHeading() {
105
- const { showTitle } = this.props;
106
-
107
- if (showTitle) {
108
- return (
109
- <h4 className="section-heading">
110
- {I18n.t("components.add_comment_form.title")}
111
- </h4>
112
- );
113
- }
114
-
115
- return null;
116
- }
117
-
118
- /**
119
- * Render a message telling the user to sign in or sign up to leave a comment.
120
- * @private
121
- * @returns {Void|DOMElement} - The message or an empty element.
122
- */
123
- private _renderAccountMessage() {
124
- const { session } = this.props;
125
-
126
- if (!session) {
127
- return (
128
- <p>
129
- <Translate
130
- value="components.add_comment_form.account_message"
131
- sign_in_url="/users/sign_in"
132
- sign_up_url="/users/sign_up"
133
- dangerousHTML={true}
134
- />
135
- </p>
136
- );
137
- }
138
-
139
- return null;
140
- }
141
-
142
- /**
143
- * Render the add comment form if session is present.
144
- * @private
145
- * @returns {Void|DOMElement} - The add comment form on an empty element.
146
- */
147
- private _renderForm() {
148
- const { session, submitButtonClassName, commentable: { id, type } } = this.props;
149
- const { disabled, remainingCharacterCount } = this.state;
150
-
151
- if (session) {
152
- return (
153
- <form onSubmit={this.addComment}>
154
- {this._renderCommentAs()}
155
- <div className="field">
156
- <label className="show-for-sr" htmlFor={`add-comment-${type}-${id}`}>{I18n.t("components.add_comment_form.form.body.label")}</label>
157
- <div className="hashtags__container">
158
- {this._renderTextArea()}
159
- </div>
160
- {this._renderTextAreaError()}
161
- <button
162
- type="submit"
163
- className={submitButtonClassName}
164
- disabled={disabled}
165
- >
166
- {I18n.t("components.add_comment_form.form.submit")}
167
- </button>
168
- <span className="remaining-character-count">
169
- {I18n.t("components.add_comment_form.remaining_characters", { count: remainingCharacterCount })}
170
- </span>
171
- </div>
172
- </form>
173
- );
174
- }
175
-
176
- return null;
177
- }
178
-
179
- /**
180
- * Render the form heading based on showTitle prop
181
- * @private
182
- * @returns {Void|DOMElement} - The heading or an empty element
183
- */
184
- private _renderTextArea() {
185
- const { commentable: { id, type }, autoFocus, commentsMaxLength } = this.props;
186
- const { error } = this.state;
187
- const className = classnames({ "is-invalid-input": error });
188
-
189
- const textAreaProps: any = {
190
- ref: (textarea: HTMLTextAreaElement) => {this.bodyTextArea = textarea; },
191
- id: `add-comment-${type}-${id}`,
192
- className,
193
- rows: "4",
194
- maxLength: commentsMaxLength,
195
- required: "required",
196
- pattern: `^(.){0,${commentsMaxLength}}$`,
197
- placeholder: I18n.t("components.add_comment_form.form.body.placeholder"),
198
- onChange: (evt: React.ChangeEvent<HTMLTextAreaElement>) => this._checkCommentBody(evt.target.value, commentsMaxLength as number)
199
- };
200
-
201
- if (autoFocus) {
202
- textAreaProps.autoFocus = "autoFocus";
203
- }
204
-
205
- return (
206
- <textarea {...textAreaProps} />
207
- );
208
- }
209
-
210
- /**
211
- * Render the text area form error if state has an error
212
- * @private
213
- * @returns {Void|DOMElement} - The error or an empty element
214
- */
215
- private _renderTextAreaError() {
216
- const { commentsMaxLength } = this.props;
217
- const { error } = this.state;
218
-
219
- if (error) {
220
- return (
221
- <span className="form-error is-visible">
222
- {I18n.t("components.add_comment_form.form.form_error", { length: commentsMaxLength })}
223
- </span>
224
- );
225
- }
226
-
227
- return null;
228
- }
229
-
230
- private setAlignment = (alignment: number) => {
231
- return () => {
232
- this.setState({ alignment });
233
- };
234
- }
235
-
236
- /**
237
- * Render opinion buttons or not based on the arguable prop
238
- * @private
239
- * @returns {Void|DOMElement} - Returns nothing or a wrapper with buttons
240
- */
241
- private _renderOpinionButtons() {
242
- const { session, arguable } = this.props;
243
- const { alignment } = this.state;
244
- const buttonClassName = classnames("button", "tiny", "button--muted");
245
- const okButtonClassName = classnames(buttonClassName, "opinion-toggle--ok", {
246
- "is-active": alignment === 1
247
- });
248
- const koButtonClassName = classnames(buttonClassName, "opinion-toggle--ko", {
249
- "is-active": alignment === -1
250
- });
251
- const neutralButtonClassName = classnames(buttonClassName, "opinion-toggle--meh", {
252
- "is-active": alignment === 0
253
- });
254
-
255
- if (session && arguable) {
256
- return (
257
- <div className="opinion-toggle button-group">
258
- <button
259
- className={okButtonClassName}
260
- onClick={this.setAlignment(1)}
261
- >
262
- <Icon iconExtraClassName="" name="icon-thumb-up" />
263
- </button>
264
- <button
265
- className={neutralButtonClassName}
266
- onClick={this.setAlignment(0)}
267
- >
268
- {I18n.t("components.add_comment_form.opinion.neutral")}
269
- </button>
270
- <button
271
- className={koButtonClassName}
272
- onClick={this.setAlignment(-1)}
273
- >
274
- <Icon iconExtraClassName="" name="icon-thumb-down" />
275
- </button>
276
- </div>
277
- );
278
- }
279
-
280
- return null;
281
- }
282
-
283
- private setUserGroupIdSelect = (select: HTMLSelectElement) => {this.userGroupIdSelect = select; };
284
-
285
- /**
286
- * Render a select with an option for each user's verified group
287
- * @private
288
- * @returns {Void|DOMElement} - Returns nothing or a form field.
289
- */
290
- private _renderCommentAs() {
291
- const { session, commentable: { id, type } } = this.props;
292
-
293
- if (session) {
294
- const { user, verifiedUserGroups } = session;
295
-
296
- if (verifiedUserGroups.length > 0) {
297
- return (
298
- <div className="field">
299
- <label htmlFor={`add-comment-${type}-${id}-user-group-id`}>
300
- {I18n.t("components.add_comment_form.form.user_group_id.label")}
301
- </label>
302
- <select
303
- ref={this.setUserGroupIdSelect}
304
- id={`add-comment-${type}-${id}-user-group-id`}
305
- >
306
- <option value="">{user.name}</option>
307
- {
308
- verifiedUserGroups.map((userGroup) => (
309
- <option key={userGroup.id} value={userGroup.id}>{userGroup.name}</option>
310
- ))
311
- }
312
- </select>
313
- </div>
314
- );
315
- }
316
- }
317
-
318
- return null;
319
- }
320
-
321
- /**
322
- * Check comment's body and disable form if it's empty
323
- * @private
324
- * @param {string} body - The comment's body
325
- * @returns {Void} - Returns nothing
326
- */
327
- private _checkCommentBody(body: string, commentsMaxLength: number) {
328
- this.setState({
329
- disabled: body === "", error: body === "" || body.length > commentsMaxLength,
330
- remainingCharacterCount: commentsMaxLength - body.length
331
- });
332
- }
333
-
334
- /**
335
- * Handle form's submission and calls `addComment` prop with the value of the
336
- * form's textarea. It prevents the default form submission event.
337
- * @private
338
- * @param {DOMEvent} evt - The form's submission event
339
- * @returns {Void} - Returns nothing
340
- */
341
- private addComment = (evt: React.FormEvent<HTMLFormElement>) => {
342
- const { alignment } = this.state;
343
- const { addComment, onCommentAdded } = this.props;
344
- const addCommentParams: { body: string, alignment: number, userGroupId?: string } = { body: this.bodyTextArea.value, alignment };
345
-
346
- evt.preventDefault();
347
-
348
- if (this.userGroupIdSelect && this.userGroupIdSelect.value !== "") {
349
- addCommentParams.userGroupId = this.userGroupIdSelect.value;
350
- }
351
-
352
- if (addComment) {
353
- addComment(addCommentParams, this.context);
354
- }
355
-
356
- this.bodyTextArea.value = "";
357
- this.setState({ alignment: 0 });
358
-
359
- if (onCommentAdded) {
360
- onCommentAdded();
361
- }
362
- }
363
- }
364
-
365
- const addCommentMutation = require("../mutations/add_comment.mutation.graphql");
366
- const getCommentsQuery = require("../queries/comments.query.graphql");
367
-
368
- const AddCommentFormWithMutation = graphql<addCommentMutation, AddCommentFormProps>(addCommentMutation, {
369
- props: ({ ownProps, mutate }) => ({
370
- addComment: ({ body, alignment, userGroupId }: { body: string, alignment: number, userGroupId: string }, { locale, toggleTranslations }: any) => {
371
- if (mutate) {
372
- mutate({
373
- variables: {
374
- locale,
375
- toggleTranslations,
376
- commentableId: ownProps.commentable.id,
377
- commentableType: ownProps.commentable.type,
378
- body,
379
- alignment,
380
- userGroupId
381
- },
382
- optimisticResponse: {
383
- commentable: {
384
- __typename: "CommentableMutation",
385
- addComment: {
386
- __typename: "Comment",
387
- id: uuid(),
388
- sgid: uuid(),
389
- type: "Decidim::Comments::Comment",
390
- createdAt: new Date().toISOString(),
391
- body,
392
- formattedBody: body,
393
- formattedCreatedAt: new Date().toISOString(),
394
- alignment,
395
- author: {
396
- __typename: "User",
397
- name: ownProps.session && ownProps.session.user.name,
398
- nickname: ownProps.session && ownProps.session.user.name,
399
- profilePath: null,
400
- badge: null,
401
- avatarUrl: ownProps.session && ownProps.session.user.avatarUrl,
402
- deleted: false
403
- },
404
- comments: [],
405
- hasComments: false,
406
- acceptsNewComments: false,
407
- userAllowedToComment: false,
408
- upVotes: 0,
409
- upVoted: false,
410
- downVotes: 0,
411
- downVoted: false,
412
- alreadyReported: false
413
- }
414
- }
415
- },
416
- update: (store, { data }: { data: addCommentMutation }) => {
417
- const variables = {
418
- locale,
419
- toggleTranslations,
420
- commentableId: ownProps.rootCommentable.id,
421
- commentableType: ownProps.rootCommentable.type,
422
- orderBy: ownProps.orderBy,
423
- singleCommentId: null
424
- };
425
- const prev = store.readQuery<GetCommentsQuery>({
426
- query: getCommentsQuery,
427
- variables
428
- });
429
- const { id, type } = ownProps.commentable;
430
- const newComment = data.commentable && data.commentable.addComment;
431
- let comments = [];
432
-
433
- const commentReducer = (comment: CommentFragment): CommentFragment => {
434
- const replies = comment.comments || [];
435
-
436
- if (newComment && comment.id === id) {
437
- return {
438
- ...comment,
439
- hasComments: true,
440
- comments: [
441
- ...replies,
442
- newComment
443
- ]
444
- };
445
- }
446
- return {
447
- ...comment,
448
- comments: replies.map(commentReducer)
449
- };
450
- };
451
-
452
- if (prev) {
453
- if (type === "Decidim::Comments::Comment") {
454
- comments = prev.commentable.comments.map(commentReducer);
455
- } else {
456
- comments = [
457
- ...prev.commentable.comments,
458
- newComment
459
- ];
460
- }
461
-
462
- store.writeQuery({
463
- query: getCommentsQuery,
464
- data: {
465
- ...prev,
466
- commentable: {
467
- ...prev.commentable,
468
- totalCommentsCount: prev.commentable.totalCommentsCount + 1,
469
- comments
470
- }
471
- },
472
- variables
473
- });
474
- }
475
- }
476
- });
477
- }
478
- }
479
- })
480
- })(AddCommentForm);
481
-
482
- export default AddCommentFormWithMutation;