decidim-comments 0.0.1

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 (58) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +42 -0
  3. data/Rakefile +2 -0
  4. data/app/assets/config/decidim_comments_manifest.js +1 -0
  5. data/app/assets/javascripts/decidim/comments/bundle.js +504 -0
  6. data/app/assets/javascripts/decidim/comments/bundle.js.map +1 -0
  7. data/app/assets/javascripts/decidim/comments/comments.js.erb +8 -0
  8. data/app/commands/decidim/comments/create_comment.rb +40 -0
  9. data/app/forms/decidim/comments/comment_form.rb +16 -0
  10. data/app/frontend/application/apollo_client.js +16 -0
  11. data/app/frontend/application/application.component.jsx +37 -0
  12. data/app/frontend/application/application.component.test.jsx +33 -0
  13. data/app/frontend/application/icon.component.jsx +21 -0
  14. data/app/frontend/application/icon.component.test.jsx +43 -0
  15. data/app/frontend/comments/add_comment_form.component.jsx +250 -0
  16. data/app/frontend/comments/add_comment_form.component.test.jsx +173 -0
  17. data/app/frontend/comments/add_comment_form.mutation.graphql +8 -0
  18. data/app/frontend/comments/comment.component.jsx +202 -0
  19. data/app/frontend/comments/comment.component.test.jsx +125 -0
  20. data/app/frontend/comments/comment.fragment.graphql +12 -0
  21. data/app/frontend/comments/comment_data.fragment.graphql +11 -0
  22. data/app/frontend/comments/comment_order_selector.component.jsx +28 -0
  23. data/app/frontend/comments/comment_order_selector.component.test.jsx +9 -0
  24. data/app/frontend/comments/comment_thread.component.jsx +64 -0
  25. data/app/frontend/comments/comment_thread.component.test.jsx +71 -0
  26. data/app/frontend/comments/comment_thread.fragment.graphql +9 -0
  27. data/app/frontend/comments/comments.component.jsx +139 -0
  28. data/app/frontend/comments/comments.component.test.jsx +106 -0
  29. data/app/frontend/comments/comments.query.graphql +10 -0
  30. data/app/frontend/comments/featured_comment.component.jsx +23 -0
  31. data/app/frontend/comments/featured_comment.component.test.jsx +15 -0
  32. data/app/frontend/entry.js +24 -0
  33. data/app/frontend/entry.test.js +29 -0
  34. data/app/frontend/support/asset_url.js +11 -0
  35. data/app/frontend/support/generate_comments_data.js +29 -0
  36. data/app/frontend/support/generate_current_user_data.js +13 -0
  37. data/app/frontend/support/load_translations.js +23 -0
  38. data/app/frontend/support/require_all.js +10 -0
  39. data/app/frontend/support/resolve_graphql_query.js +37 -0
  40. data/app/frontend/support/stub_component.js +29 -0
  41. data/app/helpers/decidim/comments/comments_helper.rb +51 -0
  42. data/app/models/decidim/comments/comment.rb +55 -0
  43. data/app/types/decidim/comments/add_comment_type.rb +12 -0
  44. data/app/types/decidim/comments/author_type.rb +15 -0
  45. data/app/types/decidim/comments/comment_type.rb +28 -0
  46. data/config/i18n-tasks.yml +124 -0
  47. data/config/locales/ca.yml +35 -0
  48. data/config/locales/en.yml +36 -0
  49. data/config/locales/es.yml +35 -0
  50. data/db/migrate/20161130143508_create_comments.rb +11 -0
  51. data/db/migrate/20161214082645_add_depth_to_comments.rb +5 -0
  52. data/db/migrate/20161216102820_add_alignment_to_comments.rb +5 -0
  53. data/db/seeds.rb +11 -0
  54. data/lib/decidim/comments.rb +10 -0
  55. data/lib/decidim/comments/engine.rb +34 -0
  56. data/lib/decidim/comments/mutation_extensions.rb +36 -0
  57. data/lib/decidim/comments/query_extensions.rb +33 -0
  58. metadata +228 -0
@@ -0,0 +1,12 @@
1
+ fragment Comment on Comment {
2
+ ...CommentData
3
+ replies {
4
+ ...CommentData
5
+ replies {
6
+ ...CommentData
7
+ replies {
8
+ ...CommentData
9
+ }
10
+ }
11
+ }
12
+ }
@@ -0,0 +1,11 @@
1
+ fragment CommentData on Comment {
2
+ id
3
+ body
4
+ createdAt
5
+ author {
6
+ name,
7
+ avatarUrl
8
+ }
9
+ canHaveReplies
10
+ alignment
11
+ }
@@ -0,0 +1,28 @@
1
+ import { Component } from 'react';
2
+ import { I18n } from 'react-i18nify';
3
+
4
+ /**
5
+ * A simple static component with the comment's order selector markup
6
+ * @class
7
+ * @augments Component
8
+ * @todo Needs a proper implementation
9
+ */
10
+ export default class CommentOrderSelector extends Component {
11
+ render() {
12
+ return (
13
+ <div className="order-by__dropdown order-by__dropdown--right">
14
+ <span className="order-by__text">{ I18n.t("components.comment_order_selector.title") }</span>
15
+ <ul className="dropdown menu" data-dropdown-menu>
16
+ <li>
17
+ <a>{ I18n.t("components.comment_order_selector.order.most_voted") }</a>
18
+ <ul className="menu">
19
+ <li><a>{ I18n.t("components.comment_order_selector.order.most_voted") }</a></li>
20
+ <li><a>{ I18n.t("components.comment_order_selector.order.recent") }</a></li>
21
+ <li><a>{ I18n.t("components.comment_order_selector.order.older") }</a></li>
22
+ </ul>
23
+ </li>
24
+ </ul>
25
+ </div>
26
+ );
27
+ }
28
+ }
@@ -0,0 +1,9 @@
1
+ import { shallow } from 'enzyme';
2
+ import CommentOrderSelector from './comment_order_selector.component';
3
+
4
+ describe('<CommentOrderSelector />', () => {
5
+ it("renders a div with classes order-by__dropdown order-by__dropdown--right", () => {
6
+ const wrapper = shallow(<CommentOrderSelector />);
7
+ expect(wrapper.find('div.order-by__dropdown.order-by__dropdown--right')).to.present();
8
+ })
9
+ })
@@ -0,0 +1,64 @@
1
+ import { Component, PropTypes } from 'react';
2
+ import { filter, propType } from 'graphql-anywhere';
3
+ import gql from 'graphql-tag';
4
+ import { I18n } from 'react-i18nify';
5
+
6
+ import Comment from './comment.component';
7
+
8
+ import commentThreadFragment from './comment_thread.fragment.graphql'
9
+
10
+ /**
11
+ * Define a collection of comments. It represents a conversation with multiple users.
12
+ * @class
13
+ * @augments Component
14
+ * @todo It doesn't handle multiple comments yet
15
+ */
16
+ class CommentThread extends Component {
17
+ render() {
18
+ const { comment, currentUser } = this.props;
19
+
20
+ return (
21
+ <div>
22
+ {this._renderTitle()}
23
+ <div className="comment-thread">
24
+ <Comment comment={filter(Comment.fragments.comment, comment)} currentUser={currentUser} />
25
+ </div>
26
+ </div>
27
+ );
28
+ }
29
+
30
+ /**
31
+ * Render conversation title if comment has replies
32
+ * @private
33
+ * @returns {Void|DOMElement} - The conversation's title
34
+ */
35
+ _renderTitle() {
36
+ const { comment: { author, replies } } = this.props;
37
+
38
+ if (replies.length > 0) {
39
+ return (
40
+ <h6 className="comment-thread__title">
41
+ { I18n.t("components.comment_thread.title", { authorName: author.name }) }
42
+ </h6>
43
+ );
44
+ }
45
+
46
+ return null;
47
+ }
48
+ }
49
+
50
+ CommentThread.fragments = {
51
+ comment: gql`
52
+ ${commentThreadFragment}
53
+ ${Comment.fragments.comment}
54
+ `
55
+ };
56
+
57
+ CommentThread.propTypes = {
58
+ currentUser: PropTypes.shape({
59
+ name: PropTypes.string.isRequired
60
+ }),
61
+ comment: propType(CommentThread.fragments.comment).isRequired
62
+ };
63
+
64
+ export default CommentThread;
@@ -0,0 +1,71 @@
1
+ import { shallow } from 'enzyme';
2
+ import { filter } from 'graphql-anywhere';
3
+ import gql from 'graphql-tag';
4
+
5
+ import CommentThread from './comment_thread.component';
6
+ import Comment from './comment.component';
7
+
8
+ import commentThreadFragment from './comment_thread.fragment.graphql'
9
+
10
+ import stubComponent from '../support/stub_component';
11
+ import generateCommentsData from '../support/generate_comments_data';
12
+ import generateCurrentUserData from '../support/generate_current_user_data';
13
+
14
+ describe('<CommentThread />', () => {
15
+ let comment = {};
16
+ let currentUser = null;
17
+
18
+ const commentFragment = gql`
19
+ fragment Comment on Comment {
20
+ body
21
+ }
22
+ `;
23
+
24
+ stubComponent(Comment, {
25
+ fragments: {
26
+ comment: commentFragment
27
+ }
28
+ });
29
+
30
+ beforeEach(() => {
31
+ const commentsData = generateCommentsData(1);
32
+
33
+ const fragment = gql`
34
+ ${commentThreadFragment}
35
+ ${commentFragment}
36
+ `;
37
+
38
+ currentUser = generateCurrentUserData();
39
+ comment = filter(fragment, commentsData[0]);
40
+ });
41
+
42
+ describe("when comment doesn't have replies", () => {
43
+ it("should not render a title with author name", () => {
44
+ const wrapper = shallow(<CommentThread comment={comment} currentUser={currentUser} />);
45
+ expect(wrapper.find('h6.comment-thread__title')).not.to.present();
46
+ });
47
+ });
48
+
49
+ describe("when comment does have replies", () => {
50
+ beforeEach(() => {
51
+ comment.replies = generateCommentsData(3);
52
+ });
53
+
54
+ it("should render a h6 comment-thread__title with author name", () => {
55
+ const wrapper = shallow(<CommentThread comment={comment} currentUser={currentUser} />);
56
+ expect(wrapper.find('h6.comment-thread__title')).to.have.text(`Conversation with ${comment.author.name}`);
57
+ });
58
+ });
59
+
60
+ describe("should render a Comment", () => {
61
+ it("and pass the currentUser as a prop to it", () => {
62
+ const wrapper = shallow(<CommentThread comment={comment} currentUser={currentUser} />);
63
+ expect(wrapper.find(Comment).first()).to.have.prop("currentUser").deep.equal(currentUser);
64
+ });
65
+
66
+ it("and pass filter comment data as a prop to it", () => {
67
+ const wrapper = shallow(<CommentThread comment={comment} currentUser={currentUser} />);
68
+ expect(wrapper.find(Comment).first()).to.have.prop("comment").deep.equal(filter(commentFragment, comment));
69
+ });
70
+ });
71
+ });
@@ -0,0 +1,9 @@
1
+ fragment CommentThread on Comment {
2
+ author {
3
+ name
4
+ }
5
+ replies {
6
+ id
7
+ }
8
+ ...Comment
9
+ }
@@ -0,0 +1,139 @@
1
+ import { Component, PropTypes } from 'react';
2
+ import { graphql } from 'react-apollo';
3
+ import gql from 'graphql-tag';
4
+ import { filter } from 'graphql-anywhere';
5
+ import { I18n } from 'react-i18nify';
6
+
7
+ import Application from '../application/application.component';
8
+
9
+ import CommentThread from './comment_thread.component';
10
+ import AddCommentForm from './add_comment_form.component';
11
+
12
+ import commentsQuery from './comments.query.graphql';
13
+
14
+ /**
15
+ * The core class of the Decidim Comments engine.
16
+ * It renders a collection of comments given a commentable id and type.
17
+ * @global
18
+ * @class
19
+ * @augments Component
20
+ */
21
+ export class Comments extends Component {
22
+ render() {
23
+ const { comments } = this.props;
24
+
25
+ return (
26
+ <div className="columns large-9" id="comments">
27
+ <section className="comments">
28
+ <div className="row collapse order-by">
29
+ <h2 className="order-by__text section-heading">
30
+ { I18n.t("components.comments.title", { count: comments.length }) }
31
+ </h2>
32
+ </div>
33
+ {this._renderCommentThreads()}
34
+ {this._renderAddCommentForm()}
35
+ </section>
36
+ </div>
37
+ );
38
+ }
39
+
40
+ /**
41
+ * Iterates the comment's collection and render a CommentThread for each one
42
+ * @private
43
+ * @returns {ReactComponent[]} - A collection of CommentThread components
44
+ */
45
+ _renderCommentThreads() {
46
+ const { comments, currentUser } = this.props;
47
+
48
+ return comments.map((comment) => (
49
+ <CommentThread
50
+ key={comment.id}
51
+ comment={filter(CommentThread.fragments.comment, comment)}
52
+ currentUser={currentUser}
53
+ />
54
+ ))
55
+ }
56
+
57
+ /**
58
+ * If current user is present it renders the add comment form
59
+ * @private
60
+ * @returns {Void|ReactComponent} - A AddCommentForm component or nothing
61
+ */
62
+ _renderAddCommentForm() {
63
+ const { currentUser, commentableId, commentableType, options: { arguable } } = this.props;
64
+
65
+ if (currentUser) {
66
+ return (
67
+ <AddCommentForm
68
+ currentUser={currentUser}
69
+ commentableId={commentableId}
70
+ commentableType={commentableType}
71
+ arguable={arguable}
72
+ />
73
+ );
74
+ }
75
+
76
+ return null;
77
+ }
78
+ }
79
+
80
+ Comments.propTypes = {
81
+ comments: PropTypes.arrayOf(PropTypes.shape({
82
+ id: PropTypes.string.isRequired
83
+ })),
84
+ currentUser: PropTypes.shape({
85
+ name: PropTypes.string.isRequired
86
+ }),
87
+ commentableId: PropTypes.string.isRequired,
88
+ commentableType: PropTypes.string.isRequired,
89
+ options: PropTypes.shape({
90
+ arguable: PropTypes.bool
91
+ }).isRequired
92
+ };
93
+
94
+ /**
95
+ * Wrap the Comments component with a GraphQL query and children
96
+ * fragments.
97
+ */
98
+ const CommentsWithData = graphql(gql`
99
+ ${commentsQuery}
100
+ ${CommentThread.fragments.comment}
101
+ `, {
102
+ options: { pollInterval: 15000 },
103
+ props: ({ ownProps, data: { currentUser, comments }}) => ({
104
+ comments: comments || [],
105
+ currentUser: currentUser || null,
106
+ commentableId: ownProps.commentableId,
107
+ commentableType: ownProps.commentableType,
108
+ options: ownProps.options
109
+ })
110
+ })(Comments);
111
+
112
+ /**
113
+ * Wrap the CommentsWithData component within an Application component to
114
+ * connect it with Apollo client and store.
115
+ * @returns {ReactComponent} - A component wrapped within an Application component
116
+ */
117
+ const CommentsApplication = ({ locale, commentableId, commentableType, options }) => (
118
+ <Application locale={locale}>
119
+ <CommentsWithData
120
+ commentableId={commentableId}
121
+ commentableType={commentableType}
122
+ options={options}
123
+ />
124
+ </Application>
125
+ );
126
+
127
+ CommentsApplication.propTypes = {
128
+ locale: PropTypes.string.isRequired,
129
+ commentableId: React.PropTypes.oneOfType([
130
+ PropTypes.string,
131
+ PropTypes.number
132
+ ]),
133
+ commentableType: PropTypes.string.isRequired,
134
+ options: PropTypes.shape({
135
+ arguable: PropTypes.bool
136
+ }).isRequired
137
+ };
138
+
139
+ export default CommentsApplication;
@@ -0,0 +1,106 @@
1
+ import { shallow } from 'enzyme';
2
+ import { filter } from 'graphql-anywhere';
3
+ import gql from 'graphql-tag';
4
+
5
+ import { Comments } from './comments.component';
6
+ import CommentThread from './comment_thread.component';
7
+ import AddCommentForm from './add_comment_form.component';
8
+
9
+ import commentsQuery from './comments.query.graphql'
10
+
11
+ import stubComponent from '../support/stub_component';
12
+ import generateCommentsData from '../support/generate_comments_data';
13
+ import generateCurrentUserData from '../support/generate_current_user_data';
14
+ import resolveGraphQLQuery from '../support/resolve_graphql_query';
15
+
16
+ describe('<Comments />', () => {
17
+ let comments = [];
18
+ let currentUser = null;
19
+ const commentableId = "1";
20
+ const commentableType = "Decidim::ParticipatoryProcess";
21
+
22
+ const commentThreadFragment = gql`
23
+ fragment CommentThread on Comment {
24
+ author
25
+ }
26
+ `;
27
+
28
+ stubComponent(CommentThread, {
29
+ fragments: {
30
+ comment: commentThreadFragment
31
+ }
32
+ });
33
+
34
+ stubComponent(AddCommentForm);
35
+
36
+ beforeEach(() => {
37
+ const currentUserData = generateCurrentUserData();
38
+ const commentsData = generateCommentsData(15);
39
+
40
+ const query = gql`
41
+ ${commentsQuery}
42
+ ${commentThreadFragment}
43
+ `;
44
+
45
+ const result = resolveGraphQLQuery(query, {
46
+ filterResult: false,
47
+ rootValue: {
48
+ currentUser: currentUserData,
49
+ comments: commentsData
50
+ },
51
+ variables: {
52
+ commentableId,
53
+ commentableType
54
+ }
55
+ });
56
+
57
+ currentUser = result.currentUser;
58
+ comments = result.comments;
59
+ });
60
+
61
+ it("should render a div of id comments", () => {
62
+ const wrapper = shallow(<Comments comments={comments} commentableId={commentableId} commentableType={commentableType} currentUser={currentUser} options={{}} />);
63
+ expect(wrapper.find('#comments')).to.be.present();
64
+ });
65
+
66
+ describe("should render a CommentThread component for each comment", () => {
67
+ it("and pass filter comment data as a prop to it", () => {
68
+ const wrapper = shallow(<Comments comments={comments} commentableId={commentableId} commentableType={commentableType} currentUser={currentUser} options={{}} />);
69
+ expect(wrapper).to.have.exactly(comments.length).descendants(CommentThread);
70
+ wrapper.find(CommentThread).forEach((node, idx) => {
71
+ expect(node).to.have.prop("comment").deep.equal(filter(commentThreadFragment, comments[idx]));
72
+ });
73
+ });
74
+
75
+ it("and pass the currentUser as a prop to it", () => {
76
+ const wrapper = shallow(<Comments comments={comments} commentableId={commentableId} commentableType={commentableType} currentUser={currentUser} options={{}} />);
77
+ expect(wrapper).to.have.exactly(comments.length).descendants(CommentThread);
78
+ wrapper.find(CommentThread).forEach((node) => {
79
+ expect(node).to.have.prop("currentUser").deep.equal(currentUser);
80
+ });
81
+ });
82
+ });
83
+
84
+ it("should render comments count", () => {
85
+ const wrapper = shallow(<Comments comments={comments} commentableId={commentableId} commentableType={commentableType} currentUser={currentUser} options={{}} />);
86
+ const rex = new RegExp(`${comments.length} comments`);
87
+ expect(wrapper.find('h2.section-heading')).to.have.text().match(rex);
88
+ });
89
+
90
+ it("should render a AddCommentForm component and pass 'options.arguable' as a prop", () => {
91
+ const wrapper = shallow(<Comments comments={comments} commentableId={commentableId} commentableType={commentableType} currentUser={currentUser} options={{ arguable: true }} />);
92
+ expect(wrapper).to.have.exactly(1).descendants(AddCommentForm);
93
+ expect(wrapper.find(AddCommentForm)).to.have.prop('arguable').equal(true);
94
+ });
95
+
96
+ describe("if currentUser is not present", () => {
97
+ beforeEach(() => {
98
+ currentUser = null;
99
+ });
100
+
101
+ it("should not render a AddCommentForm component", () => {
102
+ const wrapper = shallow(<Comments comments={comments} commentableId={commentableId} commentableType={commentableType} currentUser={currentUser} options={{}} />);
103
+ expect(wrapper.find(AddCommentForm)).not.to.be.present();
104
+ });
105
+ });
106
+ });