Skip to content

Commit c689a90

Browse files
committed
15-Exercise: Commenting Feature
1 parent 87dc6ee commit c689a90

File tree

11 files changed

+269
-5
lines changed

11 files changed

+269
-5
lines changed

src/Comment/CommentAdd/index.js

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import React, { Component } from 'react';
2+
import { Mutation } from 'react-apollo';
3+
4+
import { ADD_COMMENT } from './mutations';
5+
6+
import TextArea from '../../TextArea';
7+
import Button from '../../Button';
8+
import ErrorMessage from '../../Error';
9+
10+
class CommentAdd extends Component {
11+
state = {
12+
value: '',
13+
};
14+
15+
onChange = value => {
16+
this.setState({ value });
17+
};
18+
19+
onSubmit = (event, addComment) => {
20+
addComment().then(() => this.setState({ value: '' }));
21+
22+
event.preventDefault();
23+
};
24+
25+
render() {
26+
const { issueId } = this.props;
27+
const { value } = this.state;
28+
29+
return (
30+
<Mutation
31+
mutation={ADD_COMMENT}
32+
variables={{ body: value, subjectId: issueId }}
33+
>
34+
{(addComment, { data, loading, error }) => (
35+
<div>
36+
{error && <ErrorMessage error={error} />}
37+
38+
<form onSubmit={e => this.onSubmit(e, addComment)}>
39+
<TextArea
40+
value={value}
41+
onChange={e => this.onChange(e.target.value)}
42+
placeholder="Leave a comment"
43+
/>
44+
<Button type="submit">Comment</Button>
45+
</form>
46+
</div>
47+
)}
48+
</Mutation>
49+
);
50+
}
51+
}
52+
53+
export default CommentAdd;
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import gql from 'graphql-tag';
2+
3+
export const ADD_COMMENT = gql`
4+
mutation($subjectId: ID!, $body: String!) {
5+
addComment(input: { subjectId: $subjectId, body: $body }) {
6+
commentEdge {
7+
node {
8+
body
9+
}
10+
}
11+
}
12+
}
13+
`;

src/Comment/CommentItem/index.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import React from 'react';
2+
3+
import './style.css';
4+
5+
const Comment = ({ comment }) => (
6+
<div className="CommentItem">
7+
<div>{comment.author.login}:</div>
8+
&nbsp;
9+
<div dangerouslySetInnerHTML={{ __html: comment.bodyHTML }} />
10+
</div>
11+
);
12+
13+
export default Comment;

src/Comment/CommentItem/style.css

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
.CommentItem {
2+
display: flex;
3+
align-items: baseline;
4+
}

src/Comment/CommentList/index.js

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import React, { Fragment } from 'react';
2+
import { Query } from 'react-apollo';
3+
4+
import { GET_COMMENTS_OF_ISSUE } from './queries';
5+
import CommentItem from '../CommentItem';
6+
import CommentAdd from '../CommentAdd';
7+
8+
import Loading from '../../Loading';
9+
import ErrorMessage from '../../Error';
10+
import FetchMore from '../../FetchMore';
11+
12+
import './style.css';
13+
14+
const updateQuery = (previousResult, { fetchMoreResult }) => {
15+
if (!fetchMoreResult) {
16+
return previousResult;
17+
}
18+
19+
return {
20+
...previousResult,
21+
repository: {
22+
...previousResult.repository,
23+
issue: {
24+
...previousResult.repository.issue,
25+
...fetchMoreResult.repository.issue,
26+
comments: {
27+
...previousResult.repository.issue.comments,
28+
...fetchMoreResult.repository.issue.comments,
29+
edges: [
30+
...previousResult.repository.issue.comments.edges,
31+
...fetchMoreResult.repository.issue.comments.edges,
32+
],
33+
},
34+
},
35+
},
36+
};
37+
};
38+
39+
const Comments = ({ repositoryOwner, repositoryName, issue }) => (
40+
<Query
41+
query={GET_COMMENTS_OF_ISSUE}
42+
variables={{
43+
repositoryOwner,
44+
repositoryName,
45+
number: issue.number,
46+
}}
47+
notifyOnNetworkStatusChange={true}
48+
>
49+
{({ data, loading, error, fetchMore }) => {
50+
if (error) {
51+
return <ErrorMessage error={error} />;
52+
}
53+
54+
const { repository } = data;
55+
56+
if (loading && !repository) {
57+
return <Loading />;
58+
}
59+
60+
return (
61+
<Fragment>
62+
<CommentList
63+
comments={repository.issue.comments}
64+
loading={loading}
65+
number={issue.number}
66+
repositoryOwner={repositoryOwner}
67+
repositoryName={repositoryName}
68+
fetchMore={fetchMore}
69+
/>
70+
71+
<CommentAdd issueId={repository.issue.id} />
72+
</Fragment>
73+
);
74+
}}
75+
</Query>
76+
);
77+
78+
const CommentList = ({
79+
comments,
80+
loading,
81+
repositoryOwner,
82+
repositoryName,
83+
number,
84+
fetchMore,
85+
}) => (
86+
<div className="CommentList">
87+
{comments.edges.map(({ node }) => (
88+
<CommentItem key={node.id} comment={node} />
89+
))}
90+
91+
<FetchMore
92+
loading={loading}
93+
hasNextPage={comments.pageInfo.hasNextPage}
94+
variables={{
95+
cursor: comments.pageInfo.endCursor,
96+
repositoryOwner,
97+
repositoryName,
98+
number,
99+
}}
100+
updateQuery={updateQuery}
101+
fetchMore={fetchMore}
102+
>
103+
Comments
104+
</FetchMore>
105+
</div>
106+
);
107+
108+
export default Comments;

src/Comment/CommentList/queries.js

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import gql from 'graphql-tag';
2+
3+
export const GET_COMMENTS_OF_ISSUE = gql`
4+
query(
5+
$repositoryOwner: String!
6+
$repositoryName: String!
7+
$number: Int!
8+
$cursor: String
9+
) {
10+
repository(name: $repositoryName, owner: $repositoryOwner) {
11+
issue(number: $number) {
12+
id
13+
comments(first: 1, after: $cursor) {
14+
edges {
15+
node {
16+
id
17+
bodyHTML
18+
author {
19+
login
20+
}
21+
}
22+
}
23+
pageInfo {
24+
endCursor
25+
hasNextPage
26+
}
27+
}
28+
}
29+
}
30+
}
31+
`;

src/Comment/CommentList/style.css

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
.CommentList {
2+
padding-left: 20px;
3+
border-left: 1px solid #000;
4+
}

src/Comment/index.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import Comments from './CommentList';
2+
3+
export default Comments;

src/Issue/IssueItem/index.js

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,41 @@
11
import React from 'react';
2+
import { withState } from 'recompose';
23

4+
import Button from '../../Button';
5+
import Comments from '../../Comment';
36
import Link from '../../Link';
47

58
import './style.css';
69

7-
const IssueItem = ({ issue }) => (
10+
const IssueItem = ({
11+
issue,
12+
repositoryOwner,
13+
repositoryName,
14+
isShowComments,
15+
onShowComments,
16+
}) => (
817
<div className="IssueItem">
9-
{/* placeholder to add a show/hide comment button later */}
18+
<Button onClick={() => onShowComments(!isShowComments)}>
19+
{isShowComments ? '-' : '+'}
20+
</Button>
1021

1122
<div className="IssueItem-content">
1223
<h3>
1324
<Link href={issue.url}>{issue.title}</Link>
1425
</h3>
1526
<div dangerouslySetInnerHTML={{ __html: issue.bodyHTML }} />
1627

17-
{/* placeholder to render a list of comments later */}
28+
{isShowComments && (
29+
<Comments
30+
repositoryOwner={repositoryOwner}
31+
repositoryName={repositoryName}
32+
issue={issue}
33+
/>
34+
)}
1835
</div>
1936
</div>
2037
);
2138

22-
export default IssueItem;
39+
export default withState('isShowComments', 'onShowComments', false)(
40+
IssueItem,
41+
);

src/Issue/IssueList/index.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,12 @@ const IssueList = ({
172172
}) => (
173173
<div className="IssueList">
174174
{issues.edges.map(({ node }) => (
175-
<IssueItem key={node.id} issue={node} />
175+
<IssueItem
176+
key={node.id}
177+
issue={node}
178+
repositoryOwner={repositoryOwner}
179+
repositoryName={repositoryName}
180+
/>
176181
))}
177182

178183
<FetchMore

0 commit comments

Comments
 (0)