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.
- checksums.yaml +7 -0
- data/README.md +42 -0
- data/Rakefile +2 -0
- data/app/assets/config/decidim_comments_manifest.js +1 -0
- data/app/assets/javascripts/decidim/comments/bundle.js +504 -0
- data/app/assets/javascripts/decidim/comments/bundle.js.map +1 -0
- data/app/assets/javascripts/decidim/comments/comments.js.erb +8 -0
- data/app/commands/decidim/comments/create_comment.rb +40 -0
- data/app/forms/decidim/comments/comment_form.rb +16 -0
- data/app/frontend/application/apollo_client.js +16 -0
- data/app/frontend/application/application.component.jsx +37 -0
- data/app/frontend/application/application.component.test.jsx +33 -0
- data/app/frontend/application/icon.component.jsx +21 -0
- data/app/frontend/application/icon.component.test.jsx +43 -0
- data/app/frontend/comments/add_comment_form.component.jsx +250 -0
- data/app/frontend/comments/add_comment_form.component.test.jsx +173 -0
- data/app/frontend/comments/add_comment_form.mutation.graphql +8 -0
- data/app/frontend/comments/comment.component.jsx +202 -0
- data/app/frontend/comments/comment.component.test.jsx +125 -0
- data/app/frontend/comments/comment.fragment.graphql +12 -0
- data/app/frontend/comments/comment_data.fragment.graphql +11 -0
- data/app/frontend/comments/comment_order_selector.component.jsx +28 -0
- data/app/frontend/comments/comment_order_selector.component.test.jsx +9 -0
- data/app/frontend/comments/comment_thread.component.jsx +64 -0
- data/app/frontend/comments/comment_thread.component.test.jsx +71 -0
- data/app/frontend/comments/comment_thread.fragment.graphql +9 -0
- data/app/frontend/comments/comments.component.jsx +139 -0
- data/app/frontend/comments/comments.component.test.jsx +106 -0
- data/app/frontend/comments/comments.query.graphql +10 -0
- data/app/frontend/comments/featured_comment.component.jsx +23 -0
- data/app/frontend/comments/featured_comment.component.test.jsx +15 -0
- data/app/frontend/entry.js +24 -0
- data/app/frontend/entry.test.js +29 -0
- data/app/frontend/support/asset_url.js +11 -0
- data/app/frontend/support/generate_comments_data.js +29 -0
- data/app/frontend/support/generate_current_user_data.js +13 -0
- data/app/frontend/support/load_translations.js +23 -0
- data/app/frontend/support/require_all.js +10 -0
- data/app/frontend/support/resolve_graphql_query.js +37 -0
- data/app/frontend/support/stub_component.js +29 -0
- data/app/helpers/decidim/comments/comments_helper.rb +51 -0
- data/app/models/decidim/comments/comment.rb +55 -0
- data/app/types/decidim/comments/add_comment_type.rb +12 -0
- data/app/types/decidim/comments/author_type.rb +15 -0
- data/app/types/decidim/comments/comment_type.rb +28 -0
- data/config/i18n-tasks.yml +124 -0
- data/config/locales/ca.yml +35 -0
- data/config/locales/en.yml +36 -0
- data/config/locales/es.yml +35 -0
- data/db/migrate/20161130143508_create_comments.rb +11 -0
- data/db/migrate/20161214082645_add_depth_to_comments.rb +5 -0
- data/db/migrate/20161216102820_add_alignment_to_comments.rb +5 -0
- data/db/seeds.rb +11 -0
- data/lib/decidim/comments.rb +10 -0
- data/lib/decidim/comments/engine.rb +34 -0
- data/lib/decidim/comments/mutation_extensions.rb +36 -0
- data/lib/decidim/comments/query_extensions.rb +33 -0
- metadata +228 -0
@@ -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,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
|
+
});
|