decidim-comments 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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
+ });