diff --git a/frontend/src/components/comments-list.js b/frontend/src/components/comments-list.js
new file mode 100644
index 0000000000..d774c8adad
--- /dev/null
+++ b/frontend/src/components/comments-list.js
@@ -0,0 +1,215 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import moment from 'moment';
+import { processor } from '@seafile/seafile-editor/dist/utils/seafile-markdown2html';
+import { Button, Dropdown, DropdownToggle, DropdownMenu, DropdownItem } from 'reactstrap';
+import { gettext } from '../utils/constants';
+import { seafileAPI } from '../utils/seafile-api';
+import '../css/comments-list.css';
+
+const { repoID, filePath } = window.app.pageOptions;
+const { username } = window.app.userInfo;
+
+const CommentsListPropTypes = {
+ toggleCommentsList: PropTypes.func.isRequired
+};
+
+class CommentsList extends React.Component {
+
+ constructor(props) {
+ super(props);
+ this.state = {
+ commentsList: [],
+ showResolvedComment: true,
+ };
+ }
+
+ toggleResolvedComment = () => {
+ this.setState({
+ showResolvedComment: !this.state.showResolvedComment
+ });
+ }
+
+ listComments = () => {
+ seafileAPI.listComments(repoID, filePath).then((res) => {
+ this.setState({
+ commentsList: res.data.comments
+ });
+ });
+ }
+
+ handleCommentChange = (event) => {
+ this.setState({
+ comment: event.target.value,
+ });
+ }
+
+ submitComment = () => {
+ let comment = this.refs.commentTextarea.value;
+ if (comment.trim()) {
+ seafileAPI.postComment(repoID, filePath, comment).then(() => {
+ this.listComments();
+ });
+ }
+ this.refs.commentTextarea.value = '';
+ }
+
+ resolveComment = (event) => {
+ seafileAPI.updateComment(repoID, event.target.id, 'true').then(() => {
+ this.listComments();
+ });
+ }
+
+ deleteComment = (event) => {
+ seafileAPI.deleteComment(repoID, event.target.id).then(() => {
+ this.listComments();
+ });
+ }
+
+ componentDidMount() {
+ this.listComments();
+ }
+
+ render() {
+ return (
+
+
+
+
+
+
{gettext('comments')}
+
+
+
{gettext('Show resolved comments')}
+
+
+
+
+
+ {this.state.commentsList.length > 0 &&
+ this.state.commentsList.map((item, index = 0, arr) => {
+ let oldTime = (new Date(item.created_at)).getTime();
+ let time = moment(oldTime).format('YYYY-MM-DD HH:mm');
+ return (
+
+
+
+ );
+ })
+ }
+ {(this.state.commentsList.length == 0 ) &&
+ - {gettext('no_comment_yet')}
}
+
+
+
+
+
+
+ );
+ }
+}
+
+CommentsList.propTypes = CommentsListPropTypes;
+
+
+const commentItemPropTypes = {
+ time: PropTypes.string.isRequired,
+ item: PropTypes.object.isRequired,
+ deleteComment: PropTypes.func.isRequired,
+ resolveComment: PropTypes.func.isRequired,
+ showResolvedComment: PropTypes.bool.isRequired,
+};
+
+class CommentItem extends React.Component {
+
+ constructor(props) {
+ super(props);
+ this.state = {
+ dropdownOpen: false,
+ html: '',
+ };
+ }
+
+ toggleDropDownMenu = () => {
+ this.setState({
+ dropdownOpen: !this.state.dropdownOpen,
+ });
+ }
+
+ convertComment = (mdFile) => {
+ processor.process(mdFile).then(
+ (result) => {
+ let html = String(result);
+ this.setState({
+ html: html
+ });
+ }
+ );
+ }
+
+ componentWillMount() {
+ this.convertComment(this.props.item.comment);
+ }
+
+ componentWillReceiveProps(nextProps) {
+ this.convertComment(nextProps.item.comment);
+ }
+
+ render() {
+ const item = this.props.item;
+ if (item.resolved && !this.props.showResolvedComment) {
+ return null;
+ }
+ return (
+
+
+

+
+
{item.user_name}
+
{this.props.time}
+
+
+
+
+
+
+ {
+ (item.user_email === username) &&
+ {gettext('Delete')}}
+ {
+ !item.resolved &&
+ {gettext('Mark as resolved')}
+ }
+
+
+
+
+
+ );
+ }
+}
+
+CommentItem.propTypes = commentItemPropTypes;
+
+export default CommentsList;
diff --git a/frontend/src/css/comments-list.css b/frontend/src/css/comments-list.css
new file mode 100644
index 0000000000..6c9ba562c5
--- /dev/null
+++ b/frontend/src/css/comments-list.css
@@ -0,0 +1,118 @@
+.seafile-comment {
+ border-left: 1px solid #e6e6dd;
+ display: flex;
+ flex-direction: column;
+}
+.seafile-comment-title {
+ border-bottom: 1px solid #e5e5e5;
+ min-height: 3em;
+ line-height: 3em;
+ padding: 0 1em;
+ display: flex;
+ background-color: #fafaf9;
+}
+.seafile-comment-title .seafile-comment-title-text {
+ width: 100%;
+ text-align: center;
+ font-weight: 700;
+}
+.seafile-comment-title .seafile-comment-title-close {
+ color: #b9b9b9;
+}
+.seafile-comment-title .seafile-comment-title-close:hover {
+ color: #888;
+}
+.seafile-comment-toggle-resolved {
+ margin-top: 45px;
+ border-bottom: 1px solid #e5e5e5;
+ padding: 5px 10px;
+ display: flex;
+ position: absolute;
+ background-color: #fff;
+ justify-content: space-between;
+ width: 29%;
+}
+.seafile-comment-list {
+ height: calc(100% - 40px);
+ margin-top: 30px;
+ overflow-y: auto;
+ margin-bottom: 0;
+}
+.seafile-comment-list .comment-vacant {
+ padding: 1em;
+ text-align: center;
+}
+.seafile-comment-item {
+ overflow-y: hidden;
+ padding: 15px 10px;
+ margin-bottom: 0;
+}
+.seafile-comment-item .seafile-comment-info {
+ padding-bottom: 0.5em;
+ height: 3em;
+ display: flex;
+ justify-content: flex-start;
+}
+.seafile-comment-item .seafile-comment-info .reviewer-info {
+ padding-left: 10px;
+}
+.seafile-comment-item .seafile-comment-info .review-time {
+ font-size: 10px;
+ color: #777;
+}
+.seafile-comment-item .seafile-comment-info .seafile-comment-dropdown {
+ margin-left: auto;
+}
+.seafile-comment-item .seafile-comment-info .seafile-comment-dropdown button {
+ border: none;
+ box-shadow: none;
+ background-color: #fff;
+}
+.seafile-comment-item .seafile-comment-info .seafile-comment-dropdown .seafile-comment-dropdown-btn {
+ color: #999;
+ background-color: transparent;
+}
+.seafile-comment-item .seafile-comment-info .seafile-comment-dropdown:hover .seafile-comment-dropdown-btn {
+ color: #555;
+}
+.seafile-comment-item .seafile-comment-info .seafile-comment-dropdown button:hover,
+.seafile-comment-item .seafile-comment-info .seafile-comment-dropdown button:focus {
+ border: none;
+ box-shadow: none;
+ background-color: #eee;
+}
+.seafile-comment-item .seafile-comment-content {
+ margin-left: 42px;
+}
+.seafile-comment-item .seafile-comment-content ol,
+.seafile-comment-item .seafile-comment-content ul,
+.seafile-comment-item .seafile-comment-content li {
+ margin-left: 10px;
+}
+.seafile-comment-item .seafile-comment-content table,
+.seafile-comment-item .seafile-comment-content th,
+.seafile-comment-item .seafile-comment-content td {
+ border: 1px solid #333;
+}
+.seafile-comment-item-resolved {
+ background-color: #e6ffed;
+}
+.seafile-comment-footer {
+ background-color: #fafaf9;
+ padding: 10px 10px 0;
+ border-top: 1px solid #e5e5e5;
+ display: flex;
+ flex-direction: column;
+ min-height: 150px;
+}
+.seafile-comment-footer .add-comment-input {
+ border: 1px solid #e6e6dd;
+ padding: 5px;
+ width: 23em;
+ min-height: 90px;
+}
+.seafile-comment-footer .submit-comment {
+ margin-top: 5px;
+ width: 60px;
+ height: 28px;
+}
diff --git a/frontend/src/css/view-file-text.css b/frontend/src/css/view-file-text.css
index fc860dee6d..000c7a4992 100644
--- a/frontend/src/css/view-file-text.css
+++ b/frontend/src/css/view-file-text.css
@@ -80,4 +80,20 @@
.file-internal-link {
color: #585858;
-}
\ No newline at end of file
+}
+
+.txt-file-view-body .txt-view-comment {
+ background: #fff;
+ display: flex;
+ height: 100%;
+}
+
+.txt-file-view-body .txt-view-comment .ReactCodeMirror {
+ width: 70%;
+ margin: 5px 20px;
+ overflow-y: auto;
+}
+
+.txt-file-view-body .txt-view-comment .seafile-comment {
+ width: 30%;
+}
diff --git a/frontend/src/view-file-text.js b/frontend/src/view-file-text.js
index e0de470eed..97f5e44b1d 100644
--- a/frontend/src/view-file-text.js
+++ b/frontend/src/view-file-text.js
@@ -9,6 +9,7 @@ import { seafileAPI } from './utils/seafile-api';
import { Utils } from './utils/utils';
import { serviceURL, gettext, mediaUrl } from './utils/constants';
import InternalLinkDialog from './components/dialog/internal-link-dialog';
+import CommentsList from './components/comments-list';
import 'codemirror/lib/codemirror.css';
import 'codemirror/mode/javascript/javascript';
import 'codemirror/mode/css/css';
@@ -87,6 +88,9 @@ class ViewFileText extends React.Component {
case 'download':
window.location.href = serviceURL + '/lib/' + repoID + '/file/' + filePath +'?dl=1';
break;
+ case 'comment':
+ this.toggleCommentsList();
+ break;
}
}
@@ -128,19 +132,23 @@ class ViewFileText extends React.Component {
onMouseDown={() => this.handleMouseDown('download')}
icon={'fa fa-download'}
/>
- {/*
-