decidim-comments 0.0.1 → 0.0.2

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 (47) hide show
  1. checksums.yaml +4 -4
  2. data/Rakefile +1 -1
  3. data/app/assets/javascripts/decidim/comments/bundle.js +43 -44
  4. data/app/assets/javascripts/decidim/comments/bundle.js.map +1 -1
  5. data/app/commands/decidim/comments/vote_comment.rb +38 -0
  6. data/app/frontend/application/icon.component.jsx +9 -4
  7. data/app/frontend/application/icon.component.test.jsx +12 -2
  8. data/app/frontend/comments/add_comment_form.component.jsx +43 -11
  9. data/app/frontend/comments/add_comment_form.mutation.graphql +1 -4
  10. data/app/frontend/comments/comment.component.jsx +67 -7
  11. data/app/frontend/comments/comment.component.test.jsx +49 -1
  12. data/app/frontend/comments/comment_data.fragment.graphql +3 -0
  13. data/app/frontend/comments/comment_order_selector.component.jsx +51 -6
  14. data/app/frontend/comments/comment_order_selector.component.test.jsx +12 -1
  15. data/app/frontend/comments/comment_thread.component.jsx +11 -5
  16. data/app/frontend/comments/comment_thread.component.test.jsx +13 -3
  17. data/app/frontend/comments/comment_thread.fragment.graphql +1 -3
  18. data/app/frontend/comments/comments.component.jsx +37 -11
  19. data/app/frontend/comments/comments.component.test.jsx +44 -7
  20. data/app/frontend/comments/comments.query.graphql +2 -2
  21. data/app/frontend/comments/down_vote.fragment.graphql +6 -0
  22. data/app/frontend/comments/down_vote.mutation.graphql +7 -0
  23. data/app/frontend/comments/down_vote_button.component.jsx +84 -0
  24. data/app/frontend/comments/down_vote_button.component.test.jsx +48 -0
  25. data/app/frontend/comments/up_vote.fragment.graphql +6 -0
  26. data/app/frontend/comments/up_vote.mutation.graphql +7 -0
  27. data/app/frontend/comments/up_vote_button.component.jsx +84 -0
  28. data/app/frontend/comments/up_vote_button.component.test.jsx +48 -0
  29. data/app/frontend/comments/vote_button.component.jsx +19 -0
  30. data/app/frontend/comments/vote_button_component.test.jsx +38 -0
  31. data/app/frontend/support/generate_comments_data.js +7 -2
  32. data/app/helpers/decidim/comments/comments_helper.rb +1 -1
  33. data/app/models/decidim/comments/application_record.rb +9 -0
  34. data/app/models/decidim/comments/comment.rb +19 -4
  35. data/app/models/decidim/comments/comment_vote.rb +22 -0
  36. data/app/models/decidim/comments/seed.rb +27 -0
  37. data/app/queries/decidim/comments/comments_with_replies.rb +88 -0
  38. data/app/resolvers/decidim/comments/vote_comment_resolver.rb +20 -0
  39. data/app/types/decidim/comments/comment_mutation_type.rb +19 -0
  40. data/app/types/decidim/comments/comment_type.rb +38 -2
  41. data/config/locales/en.yml +1 -0
  42. data/db/migrate/20161130143508_create_comments.rb +4 -2
  43. data/db/migrate/20161219150806_create_comment_votes.rb +13 -0
  44. data/lib/decidim/comments/mutation_extensions.rb +9 -0
  45. data/lib/decidim/comments/query_extensions.rb +3 -5
  46. data/lib/decidim/comments/test/factories.rb +22 -0
  47. metadata +26 -49
@@ -5,6 +5,7 @@ import gql from 'graphql-tag';
5
5
  import { Comments } from './comments.component';
6
6
  import CommentThread from './comment_thread.component';
7
7
  import AddCommentForm from './add_comment_form.component';
8
+ import CommentOrderSelector from './comment_order_selector.component';
8
9
 
9
10
  import commentsQuery from './comments.query.graphql'
10
11
 
@@ -18,6 +19,8 @@ describe('<Comments />', () => {
18
19
  let currentUser = null;
19
20
  const commentableId = "1";
20
21
  const commentableType = "Decidim::ParticipatoryProcess";
22
+ const orderBy = "older";
23
+ const reorderComments = () => {};
21
24
 
22
25
  const commentThreadFragment = gql`
23
26
  fragment CommentThread on Comment {
@@ -25,12 +28,14 @@ describe('<Comments />', () => {
25
28
  }
26
29
  `;
27
30
 
31
+ stubComponent(CommentOrderSelector)
32
+
28
33
  stubComponent(CommentThread, {
29
34
  fragments: {
30
35
  comment: commentThreadFragment
31
36
  }
32
37
  });
33
-
38
+
34
39
  stubComponent(AddCommentForm);
35
40
 
36
41
  beforeEach(() => {
@@ -49,6 +54,7 @@ describe('<Comments />', () => {
49
54
  comments: commentsData
50
55
  },
51
56
  variables: {
57
+ orderBy,
52
58
  commentableId,
53
59
  commentableType
54
60
  }
@@ -58,14 +64,20 @@ describe('<Comments />', () => {
58
64
  comments = result.comments;
59
65
  });
60
66
 
67
+ it("should render loading-comments calss and the respective loading text", () => {
68
+ const wrapper = shallow(<Comments comments={comments} commentableId={commentableId} commentableType={commentableType} currentUser={currentUser} options={{}} reorderComments={reorderComments} orderBy={orderBy} loading />);
69
+ expect(wrapper.find('.loading-comments')).to.be.present();
70
+ expect(wrapper.find('h2')).to.have.text("Loading comments ...");
71
+ });
72
+
61
73
  it("should render a div of id comments", () => {
62
- const wrapper = shallow(<Comments comments={comments} commentableId={commentableId} commentableType={commentableType} currentUser={currentUser} options={{}} />);
74
+ const wrapper = shallow(<Comments comments={comments} commentableId={commentableId} commentableType={commentableType} currentUser={currentUser} options={{}} reorderComments={reorderComments} orderBy={orderBy} />);
63
75
  expect(wrapper.find('#comments')).to.be.present();
64
76
  });
65
77
 
66
78
  describe("should render a CommentThread component for each comment", () => {
67
79
  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={{}} />);
80
+ const wrapper = shallow(<Comments comments={comments} commentableId={commentableId} commentableType={commentableType} currentUser={currentUser} options={{}} reorderComments={reorderComments} orderBy={orderBy} />);
69
81
  expect(wrapper).to.have.exactly(comments.length).descendants(CommentThread);
70
82
  wrapper.find(CommentThread).forEach((node, idx) => {
71
83
  expect(node).to.have.prop("comment").deep.equal(filter(commentThreadFragment, comments[idx]));
@@ -73,22 +85,30 @@ describe('<Comments />', () => {
73
85
  });
74
86
 
75
87
  it("and pass the currentUser as a prop to it", () => {
76
- const wrapper = shallow(<Comments comments={comments} commentableId={commentableId} commentableType={commentableType} currentUser={currentUser} options={{}} />);
88
+ const wrapper = shallow(<Comments comments={comments} commentableId={commentableId} commentableType={commentableType} currentUser={currentUser} options={{}} reorderComments={reorderComments} orderBy={orderBy} />);
77
89
  expect(wrapper).to.have.exactly(comments.length).descendants(CommentThread);
78
90
  wrapper.find(CommentThread).forEach((node) => {
79
91
  expect(node).to.have.prop("currentUser").deep.equal(currentUser);
80
92
  });
81
93
  });
94
+
95
+ it("and pass the option votable as a prop to it", () => {
96
+ const wrapper = shallow(<Comments comments={comments} commentableId={commentableId} commentableType={commentableType} currentUser={currentUser} options={{ votable: true }} reorderComments={reorderComments} orderBy={orderBy} />);
97
+ expect(wrapper).to.have.exactly(comments.length).descendants(CommentThread);
98
+ wrapper.find(CommentThread).forEach((node) => {
99
+ expect(node).to.have.prop("votable").equal(true);
100
+ });
101
+ });
82
102
  });
83
103
 
84
104
  it("should render comments count", () => {
85
- const wrapper = shallow(<Comments comments={comments} commentableId={commentableId} commentableType={commentableType} currentUser={currentUser} options={{}} />);
105
+ const wrapper = shallow(<Comments comments={comments} commentableId={commentableId} commentableType={commentableType} currentUser={currentUser} options={{}} reorderComments={reorderComments} orderBy={orderBy} />);
86
106
  const rex = new RegExp(`${comments.length} comments`);
87
107
  expect(wrapper.find('h2.section-heading')).to.have.text().match(rex);
88
108
  });
89
109
 
90
110
  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 }} />);
111
+ const wrapper = shallow(<Comments comments={comments} commentableId={commentableId} commentableType={commentableType} currentUser={currentUser} options={{ arguable: true }} reorderComments={reorderComments} orderBy={orderBy} />);
92
112
  expect(wrapper).to.have.exactly(1).descendants(AddCommentForm);
93
113
  expect(wrapper.find(AddCommentForm)).to.have.prop('arguable').equal(true);
94
114
  });
@@ -99,8 +119,25 @@ describe('<Comments />', () => {
99
119
  });
100
120
 
101
121
  it("should not render a AddCommentForm component", () => {
102
- const wrapper = shallow(<Comments comments={comments} commentableId={commentableId} commentableType={commentableType} currentUser={currentUser} options={{}} />);
122
+ const wrapper = shallow(<Comments comments={comments} commentableId={commentableId} commentableType={commentableType} currentUser={currentUser} options={{}} reorderComments={reorderComments} orderBy={orderBy} />);
103
123
  expect(wrapper.find(AddCommentForm)).not.to.be.present();
104
124
  });
105
125
  });
126
+
127
+ describe("should render a CommentOrderSelector component", () => {
128
+ it("should render a CommentOrderSelector component", () => {
129
+ const wrapper = shallow(<Comments comments={comments} commentableId={commentableId} commentableType={commentableType} currentUser={currentUser} options={{}} reorderComments={reorderComments} orderBy={orderBy} />);
130
+ expect(wrapper.find(CommentOrderSelector)).to.be.present();
131
+ });
132
+
133
+ it("and pass the reorderComments as a prop to it", () => {
134
+ const wrapper = shallow(<Comments comments={comments} commentableId={commentableId} commentableType={commentableType} currentUser={currentUser} options={{}} reorderComments={reorderComments} orderBy={orderBy} />);
135
+ expect(wrapper.find(CommentOrderSelector)).to.have.prop('reorderComments').deep.equal(reorderComments);
136
+ });
137
+
138
+ it("and pass the orderBy as a prop to it", () => {
139
+ const wrapper = shallow(<Comments comments={comments} commentableId={commentableId} commentableType={commentableType} currentUser={currentUser} options={{}} reorderComments={reorderComments} orderBy={orderBy} />);
140
+ expect(wrapper.find(CommentOrderSelector)).to.have.prop('defaultOrderBy').equal('older');
141
+ });
142
+ });
106
143
  });
@@ -1,9 +1,9 @@
1
- query GetComments($commentableId: String!, $commentableType: String!) {
1
+ query GetComments($commentableId: String!, $commentableType: String!, $orderBy: String) {
2
2
  currentUser {
3
3
  name
4
4
  avatarUrl
5
5
  }
6
- comments(commentableId: $commentableId, commentableType: $commentableType) {
6
+ comments(commentableId: $commentableId, commentableType: $commentableType, orderBy: $orderBy) {
7
7
  id
8
8
  ...CommentThread
9
9
  }
@@ -0,0 +1,6 @@
1
+ fragment DownVote on Comment {
2
+ id
3
+ downVotes
4
+ downVoted
5
+ upVoted
6
+ }
@@ -0,0 +1,7 @@
1
+ mutation DownVote($id: ID!) {
2
+ comment(id: $id) {
3
+ downVote {
4
+ ...Comment
5
+ }
6
+ }
7
+ }
@@ -0,0 +1,84 @@
1
+ import { PropTypes } from 'react';
2
+ import { propType } from 'graphql-anywhere';
3
+ import { graphql } from 'react-apollo';
4
+ import gql from 'graphql-tag';
5
+
6
+ import VoteButton from './vote_button.component';
7
+
8
+ import downVoteMutation from './down_vote.mutation.graphql';
9
+
10
+ import commentFragment from './comment.fragment.graphql';
11
+ import commentDataFragment from './comment_data.fragment.graphql';
12
+ import upVoteFragment from './up_vote.fragment.graphql';
13
+ import downVoteFragment from './down_vote.fragment.graphql';
14
+
15
+ export const DownVoteButton = ({ comment: { downVotes, upVoted, downVoted }, downVote }) => (
16
+ <VoteButton
17
+ buttonClassName="comment__votes--down"
18
+ iconName="icon-chevron-bottom"
19
+ votes={downVotes}
20
+ voteAction={downVote}
21
+ disabled={upVoted || downVoted}
22
+ />
23
+ );
24
+
25
+ DownVoteButton.fragments = {
26
+ comment: gql`
27
+ ${downVoteFragment}
28
+ `
29
+ };
30
+
31
+ DownVoteButton.propTypes = {
32
+ comment: propType(DownVoteButton.fragments.comment).isRequired,
33
+ downVote: PropTypes.func.isRequired
34
+ };
35
+
36
+ const DownVoteButtonWithMutation = graphql(gql`
37
+ ${downVoteMutation}
38
+ ${commentFragment}
39
+ ${commentDataFragment}
40
+ ${upVoteFragment}
41
+ ${downVoteFragment}
42
+ `, {
43
+ props: ({ ownProps, mutate }) => ({
44
+ downVote: () => mutate({
45
+ variables: {
46
+ id: ownProps.comment.id
47
+ },
48
+ optimisticResponse: {
49
+ __typename: 'Mutation',
50
+ comment: {
51
+ __typename: 'CommentMutation',
52
+ downVote: {
53
+ __typename: 'Comment',
54
+ ...ownProps.comment,
55
+ downVotes: ownProps.comment.downVotes + 1,
56
+ downVoted: true
57
+ }
58
+ }
59
+ },
60
+ updateQueries: {
61
+ GetComments: (prev, { mutationResult: { data } }) => {
62
+ const commentReducer = (comment) => {
63
+ const replies = comment.replies || [];
64
+
65
+ if (comment.id === ownProps.comment.id) {
66
+ return data.comment.downVote;
67
+ }
68
+ return {
69
+ ...comment,
70
+ replies: replies.map(commentReducer)
71
+ };
72
+ };
73
+
74
+ return {
75
+ ...prev,
76
+ comments: prev.comments.map(commentReducer)
77
+ }
78
+ }
79
+ }
80
+ })
81
+ })
82
+ })(DownVoteButton);
83
+
84
+ export default DownVoteButtonWithMutation;
@@ -0,0 +1,48 @@
1
+ import { shallow } from 'enzyme';
2
+ import { filter } from 'graphql-anywhere';
3
+ import gql from 'graphql-tag';
4
+
5
+ import { DownVoteButton } from './down_vote_button.component';
6
+
7
+ import VoteButton from './vote_button.component';
8
+
9
+ import downVoteFragment from './down_vote.fragment.graphql';
10
+
11
+ import stubComponent from '../support/stub_component';
12
+ import generateCommentsData from '../support/generate_comments_data';
13
+
14
+ describe("<DownVoteButton />", () => {
15
+ let comment = {};
16
+ const downVote = () => {};
17
+
18
+ stubComponent(VoteButton);
19
+
20
+ beforeEach(() => {
21
+ let commentsData = generateCommentsData(1);
22
+
23
+ const fragment = gql`
24
+ ${downVoteFragment}
25
+ `;
26
+
27
+ comment = filter(fragment, commentsData[0]);
28
+ });
29
+
30
+ it("should render a VoteButton component with the correct props", () => {
31
+ const wrapper = shallow(<DownVoteButton comment={comment} downVote={downVote} />);
32
+ expect(wrapper.find(VoteButton)).to.have.prop("buttonClassName").equal("comment__votes--down");
33
+ expect(wrapper.find(VoteButton)).to.have.prop("iconName").equal("icon-chevron-bottom");
34
+ expect(wrapper.find(VoteButton)).to.have.prop("votes").equal(comment.downVotes);
35
+ });
36
+
37
+ it("should pass disabled prop as true if comment downVoted is true", () => {
38
+ comment.downVoted = true;
39
+ const wrapper = shallow(<DownVoteButton comment={comment} downVote={downVote} />);
40
+ expect(wrapper.find(VoteButton)).to.have.prop("disabled").equal(true);
41
+ });
42
+
43
+ it("should pass disabled prop as true if comment downVoted is true", () => {
44
+ comment.downVoted = true;
45
+ const wrapper = shallow(<DownVoteButton comment={comment} downVote={downVote} />);
46
+ expect(wrapper.find(VoteButton)).to.have.prop("disabled").equal(true);
47
+ });
48
+ });
@@ -0,0 +1,6 @@
1
+ fragment UpVote on Comment {
2
+ id
3
+ upVotes
4
+ upVoted
5
+ downVoted
6
+ }
@@ -0,0 +1,7 @@
1
+ mutation UpVote($id: ID!) {
2
+ comment(id: $id) {
3
+ upVote {
4
+ ...Comment
5
+ }
6
+ }
7
+ }
@@ -0,0 +1,84 @@
1
+ import { PropTypes } from 'react';
2
+ import { propType } from 'graphql-anywhere';
3
+ import { graphql } from 'react-apollo';
4
+ import gql from 'graphql-tag';
5
+
6
+ import VoteButton from './vote_button.component';
7
+
8
+ import upVoteMutation from './up_vote.mutation.graphql';
9
+
10
+ import commentFragment from './comment.fragment.graphql';
11
+ import commentDataFragment from './comment_data.fragment.graphql';
12
+ import upVoteFragment from './up_vote.fragment.graphql';
13
+ import downVoteFragment from './down_vote.fragment.graphql';
14
+
15
+ export const UpVoteButton = ({ comment: { upVotes, upVoted, downVoted }, upVote }) => (
16
+ <VoteButton
17
+ buttonClassName="comment__votes--up"
18
+ iconName="icon-chevron-top"
19
+ votes={upVotes}
20
+ voteAction={upVote}
21
+ disabled={upVoted || downVoted}
22
+ />
23
+ );
24
+
25
+ UpVoteButton.fragments = {
26
+ comment: gql`
27
+ ${upVoteFragment}
28
+ `
29
+ };
30
+
31
+ UpVoteButton.propTypes = {
32
+ comment: propType(UpVoteButton.fragments.comment).isRequired,
33
+ upVote: PropTypes.func.isRequired
34
+ };
35
+
36
+ const UpVoteButtonWithMutation = graphql(gql`
37
+ ${upVoteMutation}
38
+ ${commentFragment}
39
+ ${commentDataFragment}
40
+ ${upVoteFragment}
41
+ ${downVoteFragment}
42
+ `, {
43
+ props: ({ ownProps, mutate }) => ({
44
+ upVote: () => mutate({
45
+ variables: {
46
+ id: ownProps.comment.id
47
+ },
48
+ optimisticResponse: {
49
+ __typename: 'Mutation',
50
+ comment: {
51
+ __typename: 'CommentMutation',
52
+ upVote: {
53
+ __typename: 'Comment',
54
+ ...ownProps.comment,
55
+ upVotes: ownProps.comment.upVotes + 1,
56
+ upVoted: true
57
+ }
58
+ }
59
+ },
60
+ updateQueries: {
61
+ GetComments: (prev, { mutationResult: { data } }) => {
62
+ const commentReducer = (comment) => {
63
+ const replies = comment.replies || [];
64
+
65
+ if (comment.id === ownProps.comment.id) {
66
+ return data.comment.upVote;
67
+ }
68
+ return {
69
+ ...comment,
70
+ replies: replies.map(commentReducer)
71
+ };
72
+ };
73
+
74
+ return {
75
+ ...prev,
76
+ comments: prev.comments.map(commentReducer)
77
+ }
78
+ }
79
+ }
80
+ })
81
+ })
82
+ })(UpVoteButton);
83
+
84
+ export default UpVoteButtonWithMutation;
@@ -0,0 +1,48 @@
1
+ import { shallow } from 'enzyme';
2
+ import { filter } from 'graphql-anywhere';
3
+ import gql from 'graphql-tag';
4
+
5
+ import { UpVoteButton } from './up_vote_button.component';
6
+
7
+ import VoteButton from './vote_button.component';
8
+
9
+ import upVoteFragment from './up_vote.fragment.graphql';
10
+
11
+ import stubComponent from '../support/stub_component';
12
+ import generateCommentsData from '../support/generate_comments_data';
13
+
14
+ describe("<UpVoteButton />", () => {
15
+ let comment = {};
16
+ const upVote = () => {};
17
+
18
+ stubComponent(VoteButton);
19
+
20
+ beforeEach(() => {
21
+ let commentsData = generateCommentsData(1);
22
+
23
+ const fragment = gql`
24
+ ${upVoteFragment}
25
+ `;
26
+
27
+ comment = filter(fragment, commentsData[0]);
28
+ });
29
+
30
+ it("should render a VoteButton component with the correct props", () => {
31
+ const wrapper = shallow(<UpVoteButton comment={comment} upVote={upVote} />);
32
+ expect(wrapper.find(VoteButton)).to.have.prop("buttonClassName").equal("comment__votes--up");
33
+ expect(wrapper.find(VoteButton)).to.have.prop("iconName").equal("icon-chevron-top");
34
+ expect(wrapper.find(VoteButton)).to.have.prop("votes").equal(comment.upVotes);
35
+ });
36
+
37
+ it("should pass disabled prop as true if comment upVoted is true", () => {
38
+ comment.upVoted = true;
39
+ const wrapper = shallow(<UpVoteButton comment={comment} upVote={upVote} />);
40
+ expect(wrapper.find(VoteButton)).to.have.prop("disabled").equal(true);
41
+ });
42
+
43
+ it("should pass disabled prop as true if comment downVoted is true", () => {
44
+ comment.downVoted = true;
45
+ const wrapper = shallow(<UpVoteButton comment={comment} upVote={upVote} />);
46
+ expect(wrapper.find(VoteButton)).to.have.prop("disabled").equal(true);
47
+ });
48
+ });
@@ -0,0 +1,19 @@
1
+ import { PropTypes } from 'react';
2
+ import Icon from '../application/icon.component';
3
+
4
+ const VoteButton = ({ buttonClassName, iconName, votes, voteAction, disabled }) => (
5
+ <button className={buttonClassName} onClick={() => voteAction()} disabled={disabled}>
6
+ <Icon name={iconName} iconExtraClassName="icon--small" />
7
+ { ` ${votes}` }
8
+ </button>
9
+ );
10
+
11
+ VoteButton.propTypes = {
12
+ buttonClassName: PropTypes.string.isRequired,
13
+ iconName: PropTypes.string.isRequired,
14
+ votes: PropTypes.number.isRequired,
15
+ voteAction: PropTypes.func.isRequired,
16
+ disabled: PropTypes.bool
17
+ };
18
+
19
+ export default VoteButton;