diff --git a/frontend/src/components/file-view/comment-panel.js b/frontend/src/components/file-view/comment-panel.js
index dc4d79da73..c50d12d3c3 100644
--- a/frontend/src/components/file-view/comment-panel.js
+++ b/frontend/src/components/file-view/comment-panel.js
@@ -65,6 +65,12 @@ class CommentPanel extends React.Component {
});
}
+ editComment = (commentID, newComment) => {
+ seafileAPI.updateComment(repoID, commentID, null, null, newComment).then((res) => {
+ this.listComments();
+ });
+ }
+
componentDidMount() {
this.listComments();
}
@@ -101,6 +107,7 @@ class CommentPanel extends React.Component {
item={item} time={time}
deleteComment={this.deleteComment}
resolveComment={this.resolveComment}
+ editComment={this.editComment}
showResolvedComment={this.state.showResolvedComment}
/>
@@ -111,14 +118,14 @@ class CommentPanel extends React.Component {
{gettext('No comment yet.')}}
-
-
+
+
);
@@ -134,6 +141,7 @@ const commentItemPropTypes = {
deleteComment: PropTypes.func.isRequired,
resolveComment: PropTypes.func.isRequired,
showResolvedComment: PropTypes.bool.isRequired,
+ editComment: PropTypes.func.isRequired,
};
class CommentItem extends React.Component {
@@ -143,6 +151,8 @@ class CommentItem extends React.Component {
this.state = {
dropdownOpen: false,
html: '',
+ newComment: this.props.item.comment,
+ editable: false,
};
}
@@ -163,6 +173,26 @@ class CommentItem extends React.Component {
);
}
+ toggleEditComment = () => {
+ this.setState({
+ editable: !this.state.editable
+ });
+ }
+
+ updateComment = (event) => {
+ const newComment = this.state.newComment;
+ if (this.props.item.comment !== newComment) {
+ this.props.editComment(event.target.id, newComment);
+ }
+ this.toggleEditComment();
+ }
+
+ handleCommentChange = (event) => {
+ this.setState({
+ newComment: event.target.value,
+ });
+ }
+
componentWillMount() {
this.convertComment(this.props.item.comment);
}
@@ -176,6 +206,24 @@ class CommentItem extends React.Component {
if (item.resolved && !this.props.showResolvedComment) {
return null;
}
+ if (this.state.editable) {
+ return(
+
+
+

+
+
{item.user_name}
+
{this.props.time}
+
+
+
+
+ {' '}
+
+
+
+ );
+ }
return (
@@ -195,6 +243,11 @@ class CommentItem extends React.Component {
(item.user_email === username) &&
{gettext('Delete')}}
+ {
+ (item.user_email === username) &&
+ {gettext('Edit')}
+ }
{
!item.resolved &&
{
+ let comment = event.target.value;
+ this.setState({
+ comment: comment
+ });
+ }
+
+ submitComment = () => {
+ let comment = this.state.comment.trim();
+ if (comment.length > 0 && this.props.quote.length > 0) {
+ let detail = {
+ quote: this.props.quote,
+ position: this.props.commentPosition,
+ };
+ let detailJSON = JSON.stringify(detail);
+ this.props.editorUtilities.postComment(comment, detailJSON).then((res) => {
+ this.props.onCommentAdded();
+ });
+ }
+ }
+
+ setQuoteText = (mdQuote) => {
+ processor.process(mdQuote).then(
+ (result) => {
+ let quote = String(result);
+ this.setState({
+ quote: quote
+ });
+ }
+ );
+ }
+
+ componentDidMount() {
+ this.setQuoteText(this.props.quote);
+ }
+
+ componentWillReceiveProps(nextProps) {
+ if (this.props.quote !== nextProps.quote) {
+ this.setQuoteText(nextProps.quote);
+ }
+ }
+
+ render() {
+ return (
+
+
{this.props.editorUtilities.name}
+
+
+
+
+
+
+
+
+
+
+ );
+ }
+}
+
+CommentDialog.propTypes = propTypes;
+
+export default CommentDialog;
\ No newline at end of file
diff --git a/frontend/src/components/markdown-view/comments-list.js b/frontend/src/components/markdown-view/comments-list.js
new file mode 100644
index 0000000000..53f44cb6bb
--- /dev/null
+++ b/frontend/src/components/markdown-view/comments-list.js
@@ -0,0 +1,306 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { processor } from '@seafile/seafile-editor/dist/utils/seafile-markdown2html';
+import { Button, Dropdown, DropdownToggle, DropdownMenu, DropdownItem } from 'reactstrap';
+import Loading from '../loading';
+import { gettext } from '../../utils/constants';
+import moment from 'moment';
+import '../../css/markdown-viewer/comments-list.css';
+
+const propTypes = {
+ editorUtilities: PropTypes.object.isRequired,
+ scrollToQuote: PropTypes.func.isRequired,
+ getCommentsNumber: PropTypes.func.isRequired,
+ commentsNumber: PropTypes.number.isRequired,
+};
+
+class CommentsList extends React.Component {
+
+ constructor(props) {
+ super(props);
+ this.state = {
+ commentsList: [],
+ showResolvedComment: true,
+ };
+ }
+
+ listComments = () => {
+ this.props.editorUtilities.listComments().then((response) => {
+ this.setState({
+ commentsList: response.data.comments
+ });
+ });
+ }
+
+ handleCommentChange = (event) => {
+ this.setState({
+ comment: event.target.value,
+ });
+ }
+
+ submitComment = () => {
+ let comment = this.refs.commentTextarea.value;
+ if (comment.trim().length > 0) {
+ this.props.editorUtilities.postComment(comment.trim()).then((response) => {
+ this.listComments();
+ this.props.getCommentsNumber();
+ });
+ }
+ this.refs.commentTextarea.value = '';
+ }
+
+ resolveComment = (event) => {
+ this.props.editorUtilities.updateComment(event.target.id, 'true').then((response) => {
+ this.listComments();
+ });
+ }
+
+ deleteComment = (event) => {
+ this.props.editorUtilities.deleteComment(event.target.id).then((response) => {
+ this.props.getCommentsNumber();
+ this.listComments();
+ });
+ }
+
+ editComment = (commentID, newComment) => {
+ this.props.editorUtilities.updateComment(commentID, null, null, newComment).then((res) => {
+ this.props.getCommentsNumber();
+ this.listComments();
+ });
+ }
+
+ setQuoteText = (text) => {
+ if (text.length > 0) {
+ this.refs.commentTextarea.value = '> ' + text;
+ }
+ }
+
+ scrollToQuote = (detail) => {
+ this.props.scrollToQuote(detail);
+ this.refs.commentTextarea.value = '';
+ }
+
+ toggleResolvedComment = () => {
+ this.setState({
+ showResolvedComment: !this.state.showResolvedComment
+ });
+ }
+
+ componentWillMount() {
+ this.listComments();
+ }
+
+ componentWillReceiveProps(nextProps) {
+ if (this.props.commentsNumber !== nextProps.commentsNumber) {
+ this.listComments();
+ }
+ }
+
+ render() {
+ return (
+
+
+
{gettext('Show resolved comments')}
+
+
+
+
+
+ { (this.state.commentsList.length > 0 && this.props.commentsNumber > 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 && this.props.commentsNumber > 0) && }
+ { this.props.commentsNumber == 0 &&
+ - {gettext('No comment yet.')}
+ }
+
+
+
+
+
+
+
+
+ );
+ }
+}
+
+CommentsList.propTypes = propTypes;
+
+const CommentItempropTypes = {
+ editorUtilities: PropTypes.object.isRequired,
+ item: PropTypes.object,
+ time: PropTypes.string,
+ key: PropTypes.number,
+ editComment: PropTypes.func,
+ showResolvedComment: PropTypes.bool,
+ deleteComment: PropTypes.func,
+ resolveComment: PropTypes.func,
+ commentsList: PropTypes.array,
+ scrollToQuote: PropTypes.func.isRequired,
+};
+
+class CommentItem extends React.Component {
+
+ constructor(props) {
+ super(props);
+ this.state = {
+ dropdownOpen: false,
+ html: '',
+ quote: '',
+ newComment: this.props.item.comment,
+ editable: false,
+ };
+ }
+
+ toggleDropDownMenu = () => {
+ this.setState({
+ dropdownOpen: !this.state.dropdownOpen,
+ });
+ }
+
+ convertComment = (item) => {
+ processor.process(item.comment).then(
+ (result) => {
+ let comment = String(result);
+ this.setState({
+ comment: comment
+ });
+ }
+ );
+ if (item.detail) {
+ const quote = JSON.parse(item.detail).quote;
+ processor.process(quote).then(
+ (result) => {
+ let quote = String(result);
+ this.setState({
+ quote: quote
+ });
+ }
+ );
+ }
+ }
+
+ toggleEditComment = () => {
+ this.setState({
+ editable: !this.state.editable
+ });
+ }
+
+ updateComment = (event) => {
+ const newComment = this.state.newComment;
+ if (this.props.item.comment !== newComment) {
+ this.props.editComment(event.target.id, newComment);
+ }
+ this.toggleEditComment();
+ }
+
+ handleCommentChange = (event) => {
+ this.setState({
+ newComment: event.target.value,
+ });
+ }
+
+ scrollToQuote = () => {
+ const position = JSON.parse(this.props.item.detail).position;
+ this.props.scrollToQuote(position);
+ }
+
+ componentWillMount() {
+ this.convertComment(this.props.item);
+ }
+
+ componentWillReceiveProps(nextProps) {
+ this.convertComment(nextProps.item);
+ }
+
+ render() {
+ const item = this.props.item;
+ const { id, user_email, avatar_url, user_name, resolved } = item;
+ if (item.resolved && !this.props.showResolvedComment) {
+ return null;
+ }
+ if (this.state.editable) {
+ return(
+
+
+

+
+
{user_name}
+
{this.props.time}
+
+
+
+
+ {' '}
+
+
+
+ );
+ }
+ return (
+
+
+

+
+
{user_name}
+
{this.props.time}
+
+
+
+
+
+
+ {(user_email === this.props.editorUtilities.userName) &&
+ {gettext('Delete')}
+ }
+ {(user_email === this.props.editorUtilities.userName) &&
+ {gettext('Edit')}
+ }
+ {!resolved &&
+ {gettext('Mark as resolved')}
+ }
+
+
+
+ {item.detail &&
+
+
+
+ }
+
+
+ );
+ }
+}
+
+CommentsList.propTypes = CommentItempropTypes;
+
+export default CommentsList;
diff --git a/frontend/src/components/markdown-view/history-list.js b/frontend/src/components/markdown-view/history-list.js
new file mode 100644
index 0000000000..e12fabf1cd
--- /dev/null
+++ b/frontend/src/components/markdown-view/history-list.js
@@ -0,0 +1,158 @@
+/* eslint-disable linebreak-style */
+import React from 'react';
+import PropTypes from 'prop-types';
+import axios from 'axios';
+import Loading from '../loading';
+import moment from 'moment';
+import '../../css/markdown-viewer/history-viewer.css';
+
+const propTypes = {
+ editorUtilities: PropTypes.object.isRequired,
+ showDiffViewer: PropTypes.func.isRequired,
+ setDiffViewerContent: PropTypes.func.isRequired,
+ reloadDiffContent: PropTypes.func.isRequired,
+};
+
+class HistoryList extends React.Component {
+
+ constructor(props) {
+ super(props);
+ this.perPage = 25;
+ this.state = {
+ historyList: [],
+ activeItem: -1,
+ currentPage: 1,
+ totalReversionCount: 0,
+ loading: false
+ };
+ }
+
+ componentDidMount() {
+ this.props.editorUtilities.listFileHistoryRecords(1, this.perPage).then((res) => {
+ this.setState({
+ historyList: res.data.data,
+ totalReversionCount: res.data.total_count
+ });
+ if (res.data.data.length > 1) {
+ axios.all([
+ this.props.editorUtilities.getFileHistoryVersion(res.data.data[0].commit_id, res.data.data[0].path),
+ this.props.editorUtilities.getFileHistoryVersion(res.data.data[1].commit_id, res.data.data[1].path)
+ ]).then(axios.spread((res1, res2) => {
+ axios.all([this.props.editorUtilities.getFileContent(res1.data), this.props.editorUtilities.getFileContent(res2.data)]).then(axios.spread((content1,content2) => {
+ this.props.showDiffViewer();
+ this.props.setDiffViewerContent(content1.data, content2.data);
+ }));
+ }));
+ } else {
+ this.props.editorUtilities.getFileHistoryVersion(res.data.data[0].commit_id, res.data.data[0].path).then((res) => {
+ this.props.editorUtilities.getFileContent(res.data).then((content) => {
+ this.props.showDiffViewer();
+ this.props.setDiffViewerContent(content.data, '');
+ });
+ });
+ }
+ });
+ }
+
+ onClick = (event, key, preItem, currentItem)=> {
+ if (key === this.state.activeItem) return false;
+ this.props.reloadDiffContent();
+ this.setState({
+ activeItem: key,
+ });
+ axios.all([
+ this.props.editorUtilities.getFileHistoryVersion(currentItem.commit_id, currentItem.path),
+ this.props.editorUtilities.getFileHistoryVersion(preItem.commit_id, preItem.path)
+ ]).then(axios.spread((res1, res2) => {
+ axios.all([this.props.editorUtilities.getFileContent(res1.data), this.props.editorUtilities.getFileContent(res2.data)]).then(axios.spread((content1,content2) => {
+ this.props.showDiffViewer();
+ this.props.setDiffViewerContent(content1.data, content2.data);
+ }));
+ }));
+ }
+
+ onScroll = (event) => {
+ const clientHeight = event.target.clientHeight;
+ const scrollHeight = event.target.scrollHeight;
+ const scrollTop = event.target.scrollTop;
+ const isBottom = (clientHeight + scrollTop + 1 >= scrollHeight);
+ if (isBottom) {
+ if (this.state.totalReversionCount > this.perPage * this.state.currentPage) {
+ let currentPage = this.state.currentPage + 1;
+ this.setState({
+ currentPage: currentPage,
+ loading : true
+ });
+ this.props.editorUtilities.listFileHistoryRecords(currentPage, this.perPage).then((res) => {
+ let currentHistoryList = Object.assign([], this.state.historyList);
+ this.setState({
+ historyList: [...currentHistoryList, ...res.data.data],
+ loading : false
+ });
+ });
+ }
+ }
+ }
+
+ render() {
+ return (
+
+
+ {this.state.historyList ?
+ this.state.historyList.map((item, index = 0, arr) => {
+ let preItemIndex = index + 1;
+ if (preItemIndex === arr.length) {
+ preItemIndex = index;
+ }
+ return (
+
+ );
+ }) :
+ }
+ {
+ this.state.loading &&
+
+ }
+
+
+ );
+ }
+}
+
+HistoryList.propTypes = propTypes;
+
+
+const HistoryItempropTypes = {
+ ctime: PropTypes.number,
+ onClick: PropTypes.func,
+ index: PropTypes.number,
+ preItem: PropTypes.string,
+ currewntItem: PropTypes.string,
+ name: PropTypes.string,
+};
+
+class HistoryItem extends React.Component {
+ render() {
+ let time = moment.parseZone(this.props.ctime).format('YYYY-MM-DD HH:mm');
+ return (
+ this.props.onClick(event, this.props.index, this.props.preItem, this.props.currentItem)} className={'history-item-container ' + this.props.className}>
+ {time}
+ {this.props.name}
+
+ );
+ }
+}
+
+HistoryList.propTypes = HistoryItempropTypes;
+
+
+export default HistoryList;
diff --git a/frontend/src/components/markdown-view/markdown-viewer-side-panel.js b/frontend/src/components/markdown-view/markdown-viewer-side-panel.js
new file mode 100644
index 0000000000..03c24fde29
--- /dev/null
+++ b/frontend/src/components/markdown-view/markdown-viewer-side-panel.js
@@ -0,0 +1,164 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import classnames from 'classnames';
+import { Nav, NavItem, NavLink, TabContent, TabPane } from 'reactstrap';
+import HistoryList from './history-list';
+import CommentsList from './comments-list';
+import OutlineView from './outline';
+
+const URL = require('url-parse');
+
+const propTypes = {
+ editorUtilities: PropTypes.object.isRequired,
+ markdownContent: PropTypes.string.isRequired,
+ commentsNumber: PropTypes.number.isRequired,
+ viewer: PropTypes.object.isRequired,
+ value: PropTypes.object.isRequired,
+ activeTab: PropTypes.string.isRequired,
+ showDiffViewer: PropTypes.func.isRequired,
+ setDiffViewerContent: PropTypes.func.isRequired,
+ reloadDiffContent: PropTypes.func.isRequired,
+ tabItemClick: PropTypes.func.isRequired,
+ getCommentsNumber: PropTypes.func.isRequired,
+};
+
+class MarkdownViewerSidePanel extends React.Component {
+
+ constructor(props) {
+ super(props);
+ }
+
+ tabItemClick = (tab) => {
+ this.props.tabItemClick(tab);
+ }
+
+ showNavItem = (showTab) => {
+ switch(showTab) {
+ case 'outline':
+ return (
+ { this.tabItemClick('outline');}} >
+
+ );
+ case 'comments':
+ return (
+ {this.tabItemClick('comments');}}>
+ {this.props.commentsNumber > 0 && {this.props.commentsNumber}
}
+
+ );
+ case 'history':
+ return (
+ { this.tabItemClick('history');}}>
+
+ );
+ }
+ }
+
+ renderNavItems = () => {
+ return (
+
+ );
+ }
+
+ scrollToNode = (node) => {
+ let url = new URL(window.location.href);
+ url.set('hash', 'user-content-' + node.text);
+ window.location.href = url.toString();
+ }
+
+ findScrollContainer = (el, window) => {
+ let parent = el.parentNode;
+ const OVERFLOWS = ['auto', 'overlay', 'scroll'];
+ let scroller;
+ while (!scroller) {
+ if (!parent.parentNode) break;
+ const style = window.getComputedStyle(parent);
+ const { overflowY } = style;
+ if (OVERFLOWS.includes(overflowY)) {
+ scroller = parent;
+ break;
+ }
+ parent = parent.parentNode;
+ }
+ if (!scroller) {
+ return window.document.body;
+ }
+ return scroller;
+ }
+
+ scrollToQuote = (path) => {
+ if (!path) return;
+ const win = window;
+ if (path.length > 2) {
+ // deal with code block or chart
+ path[0] = path[0] > 1 ? path[0] - 1 : path[0] + 1;
+ path = path.slice(0, 1);
+ }
+ let node = this.props.value.document.getNode(path);
+ if (!node) {
+ path = path.slice(0, 1);
+ node = this.props.value.document.getNode(path);
+ }
+ if (node) {
+ let element = win.document.querySelector(`[data-key="${node.key}"]`);
+ while (element.tagName === 'CODE') {
+ element = element.parentNode;
+ }
+ const scroller = this.findScrollContainer(element, win);
+ const isWindow = scroller == win.document.body || scroller == win.document.documentElement;
+ if (isWindow) {
+ win.scrollTo(0, element.offsetTop);
+ } else {
+ scroller.scrollTop = element.offsetTop;
+ }
+ }
+ }
+
+ componentDidMount() {
+ this.tabItemClick('outline');
+ }
+
+ render() {
+ return (
+
+ {this.renderNavItems()}
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+ }
+}
+
+MarkdownViewerSidePanel.propTypes = propTypes;
+
+export default MarkdownViewerSidePanel;
diff --git a/frontend/src/components/markdown-view/outline.js b/frontend/src/components/markdown-view/outline.js
new file mode 100644
index 0000000000..747b19952b
--- /dev/null
+++ b/frontend/src/components/markdown-view/outline.js
@@ -0,0 +1,70 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { gettext } from '../../utils/constants';
+
+const propTypes = {
+ scrollToNode: PropTypes.func.isRequired,
+ isViewer: PropTypes.bool.isRequired,
+ document: PropTypes.object.isRequired,
+ editor: PropTypes.object.isRequired,
+};
+
+class OutlineItem extends React.PureComponent {
+
+ constructor(props) {
+ super(props);
+ }
+
+ onClick = (event) => {
+ this.props.scrollToNode(this.props.node);
+ }
+
+ render() {
+ const node = this.props.node;
+ var c;
+ if (node.type === 'header_two') {
+ c = 'outline-h2';
+ } else if (node.type === 'header_three') {
+ c = 'outline-h3';
+ }
+ c = c + this.props.active;
+
+ return (
+ {node.text}
+ );
+ }
+}
+
+class OutlineView extends React.PureComponent {
+
+ render() {
+ const document = this.props.document;
+ var headerList = document.nodes.filter(node => {
+ return (node.type === 'header_two' || node.type === 'header_three');
+ });
+
+ return (
+
+ {headerList.size > 0 ?
+ headerList.map((node, index) => {
+ let active = (index === this.props.activeTitleIndex) ? ' active' : '';
+ return (
+
+ );
+ }) :
{gettext('No out line.')}
}
+
+ );
+ }
+
+}
+
+OutlineView.propTypes = propTypes;
+
+export default OutlineView;
diff --git a/frontend/src/components/review-list-view/review-comments.js b/frontend/src/components/review-list-view/review-comments.js
index b2f7a8be25..ac8812fbb0 100644
--- a/frontend/src/components/review-list-view/review-comments.js
+++ b/frontend/src/components/review-list-view/review-comments.js
@@ -73,6 +73,13 @@ class ReviewComments extends React.Component {
});
}
+ editComment = (commentID, newComment) => {
+ seafileAPI.updateComment(draftRepoID, commentID, null, null, newComment).then((res) => {
+ this.props.getCommentsNumber();
+ this.listComments();
+ });
+ }
+
toggleResolvedComment = () => {
this.setState({
showResolvedComment: !this.state.showResolvedComment
@@ -173,7 +180,7 @@ class ReviewComments extends React.Component {
this.state.commentsList.map((item, index) => {
return (
);
})
@@ -187,7 +194,7 @@ class ReviewComments extends React.Component {
-
@@ -203,6 +210,7 @@ const commentItemPropTypes = {
item: PropTypes.object.isRequired,
deleteComment: PropTypes.func.isRequired,
resolveComment: PropTypes.func.isRequired,
+ editComment: PropTypes.func.isRequired,
showResolvedComment: PropTypes.bool.isRequired,
scrollToQuote: PropTypes.func.isRequired
};
@@ -215,6 +223,8 @@ class CommentItem extends React.Component {
dropdownOpen: false,
comment: '',
quote: '',
+ newComment: this.props.item.comment,
+ editable: false,
};
}
@@ -248,6 +258,26 @@ class CommentItem extends React.Component {
this.props.scrollToQuote(item.newIndex, item.oldIndex, item.quote);
}
+ toggleEditComment = () => {
+ this.setState({
+ editable: !this.state.editable
+ });
+ }
+
+ updateComment = (event) => {
+ const newComment = this.state.newComment;
+ if (this.props.item.comment !== newComment) {
+ this.props.editComment(event.target.id, newComment);
+ }
+ this.toggleEditComment();
+ }
+
+ handleCommentChange = (event) => {
+ this.setState({
+ newComment: event.target.value,
+ });
+ }
+
componentWillMount() {
this.convertComment(this.props.item);
}
@@ -261,6 +291,24 @@ class CommentItem extends React.Component {
if (item.resolved && !this.props.showResolvedComment) {
return null;
}
+ if (this.state.editable) {
+ return(
+
+
+

+
+
{item.name}
+
{item.time}
+
+
+
+
+ {' '}
+
+
+
+ );
+ }
return (
@@ -280,6 +328,9 @@ class CommentItem extends React.Component {
{ (item.userEmail === username) &&
{gettext('Delete')}}
+ { (item.userEmail === username) &&
+ {gettext('Edit')}}
{gettext('Mark as resolved')}
diff --git a/frontend/src/components/toolbar/markdown-viewer-toolbar.js b/frontend/src/components/toolbar/markdown-viewer-toolbar.js
index f780f75396..f8567734cb 100644
--- a/frontend/src/components/toolbar/markdown-viewer-toolbar.js
+++ b/frontend/src/components/toolbar/markdown-viewer-toolbar.js
@@ -7,17 +7,13 @@ import FileInfo from '@seafile/seafile-editor/dist/components/topbarcomponent/fi
const propTypes = {
hasDraft: PropTypes.bool.isRequired,
isDraft: PropTypes.bool.isRequired,
- showFileHistory: PropTypes.bool.isRequired,
editorUtilities: PropTypes.object.isRequired,
collabUsers: PropTypes.array.isRequired,
fileInfo: PropTypes.object.isRequired,
fileTagList: PropTypes.array.isRequired,
relatedFiles: PropTypes.array.isRequired,
- commentsNumber: PropTypes.number.isRequired,
- toggleCommentList: PropTypes.func.isRequired,
toggleShareLinkDialog: PropTypes.func.isRequired,
onEdit: PropTypes.func.isRequired,
- toggleHistory: PropTypes.func.isRequired,
toggleNewDraft: PropTypes.func.isRequired,
toggleStar: PropTypes.func.isRequired,
backToParentDirectory: PropTypes.func.isRequired,
@@ -51,29 +47,15 @@ class MarkdownViewerToolbar extends React.Component {
- {
- this.props.commentsNumber > 0 ?
-
- :
-
-
+
);
}
diff --git a/frontend/src/css/comments-list.css b/frontend/src/css/comments-list.css
index 6c9ba562c5..b39d026f76 100644
--- a/frontend/src/css/comments-list.css
+++ b/frontend/src/css/comments-list.css
@@ -2,6 +2,7 @@
border-left: 1px solid #e6e6dd;
display: flex;
flex-direction: column;
+ width: 29%;
}
.seafile-comment-title {
border-bottom: 1px solid #e5e5e5;
@@ -43,7 +44,6 @@
text-align: center;
}
.seafile-comment-item {
- overflow-y: hidden;
padding: 15px 10px;
margin-bottom: 0;
}
@@ -105,14 +105,19 @@
flex-direction: column;
min-height: 150px;
}
-.seafile-comment-footer .add-comment-input {
+.seafile-comment-footer .add-comment-input,
+.seafile-edit-comment .edit-comment-input {
border: 1px solid #e6e6dd;
padding: 5px;
width: 23em;
min-height: 90px;
+ border-radius: 5px;
}
.seafile-comment-footer .submit-comment {
margin-top: 5px;
width: 60px;
height: 28px;
}
+.seafile-edit-comment .comment-btn {
+ height: 28px;
+}
diff --git a/frontend/src/css/markdown-viewer/comment-dialog.css b/frontend/src/css/markdown-viewer/comment-dialog.css
new file mode 100644
index 0000000000..a31e15c092
--- /dev/null
+++ b/frontend/src/css/markdown-viewer/comment-dialog.css
@@ -0,0 +1,45 @@
+.comment-dialog {
+ width: 500px;
+ position: absolute;
+ top: 30%;
+ right: 0;
+ padding: 15px;
+ background-color: #fafafa;
+ border: 1px solid rgba(0,0,0,.2);
+ border-radius: .3rem;
+ box-shadow: 0 0 3px #ccc;
+ z-index: 1000;
+}
+.comment-dialog-triangle {
+ position: absolute;
+ left: -5px;
+ top: 50%;
+ transform: rotate(45deg);
+ border: 1px solid rgba(0,0,0,.2);
+ border-top: none;
+ border-right: none;
+ width: 10px;
+ height: 10px;
+ background-color: #fafafa;
+ box-shadow: -1px 1px #ccc;
+}
+.comment-dialog textarea {
+ width: 100%;
+ min-height: 100px;
+ max-height: 300px;
+ padding: 5px;
+ background-color: #fff;
+}
+.comment-dialog .button-group .btn {
+ margin-right: 10px;
+}
+.comment-dialog .comment-dialog-quote {
+ margin-top: 10px;
+ max-height: 6rem;
+ overflow: auto;
+ padding-left: 1rem;
+}
+.comment-dialog .comment-dialog-quote ul,
+.comment-dialog .comment-dialog-quote ol {
+ padding-left: 1rem;
+}
\ No newline at end of file
diff --git a/frontend/src/css/markdown-viewer/comments-list.css b/frontend/src/css/markdown-viewer/comments-list.css
new file mode 100644
index 0000000000..31835f3f08
--- /dev/null
+++ b/frontend/src/css/markdown-viewer/comments-list.css
@@ -0,0 +1,132 @@
+.seafile-comment {
+ border-left: 1px solid #e6e6dd;
+ background-color: #fff;
+ display: flex;
+ flex-direction: column;
+ flex: 0 0 auto;
+ min-height: 18.5em;
+ z-index: 3;
+ width: 380px;
+}
+.seafile-comment-title {
+ border-bottom: 1px solid #e5e5e5;
+ min-height: 2em;
+ line-height: 2em;
+ padding: 0 1em;
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+ background-color: #fafaf9;
+}
+.seafile-comment-title .seafile-comment-title-close {
+ color: #b9b9b9;
+}
+.seafile-comment-title .seafile-comment-title-close:hover {
+ color: #888;
+}
+.seafile-comment-list {
+ height: calc(100% - 40px);
+ overflow-y: auto;
+ margin-bottom: 120px;
+}
+.seafile-comment-list .comment-vacant {
+ padding: 1em;
+ text-align: center;
+}
+.seafile-comment-item {
+ 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;
+ background-color: transparent;
+}
+.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 blockquote {
+ cursor: pointer;
+}
+.seafile-comment-footer {
+ background-color: #fafaf9;
+ border-top: 1px solid #e5e5e5;
+ min-height: 120px;
+ position: absolute;
+ bottom: 0;
+ padding: 0;
+ width: inherit;
+}
+.seafile-comment-footer .seafile-add-comment {
+ margin: 10px 20px 5px 15px;
+ height: 100%;
+ width: 100%;
+}
+.seafile-add-comment .add-comment-input,
+.seafile-edit-comment .edit-comment-input {
+ background-color: #fff;
+ border: 1px solid #e6e6dd;
+ padding: 5px;
+ min-height: 70px;
+ border-radius: 5px;
+ width: 100%;
+}
+.seafile-add-comment .add-comment-input {
+ height: calc(100% - 50px);
+ width: calc(100% - 40px);
+}
+.seafile-comment-footer .seafile-add-comment .submit-comment {
+ margin-top: 5px;
+ width: 60px;
+ height: 28px;
+}
+.seafile-comment-item-resolved {
+ background-color: #e6ffed;
+}
+.seafile-comment-footer .seafile-add-comment .comment-btn,
+.seafile-edit-comment .comment-btn {
+ height: 28px;
+}
+.seafile-edit-comment {
+ margin-top: 10px;
+}
diff --git a/frontend/src/css/markdown-viewer/history-viewer.css b/frontend/src/css/markdown-viewer/history-viewer.css
new file mode 100644
index 0000000000..6bb40a71df
--- /dev/null
+++ b/frontend/src/css/markdown-viewer/history-viewer.css
@@ -0,0 +1,105 @@
+.seafile-history-side-panel {
+ user-select: none;
+ border-left: 1px solid #e5e5e5;
+ background-color: #fff;
+ display: flex;
+ flex-direction: column;
+ flex: 0 0 auto;
+}
+
+.history-side-panel-title {
+ height: 50px;
+ border-bottom: 1px solid #e5e5e5;
+ line-height: 50px;
+ font-size: 1rem;
+ padding: 0 10px;
+ box-sizing: border-box;
+ background-color: rgb(250,250,249);
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+}
+
+.history-side-panel-title .history-tile-text {
+ font-weight: bolder;
+}
+
+
+.history-side-panel-title .history-title-close {
+ color: #b9b9b9;
+}
+
+.history-side-panel-title .history-title-close:hover {
+ color: #888;
+}
+
+
+.history-list-container {
+ height: calc(100% - 36px);
+ overflow-y: hidden;
+}
+.history-list-container:hover {
+ overflow-y: auto;
+}
+
+.item-active {
+ background-color: #fdc297;
+}
+
+.history-item-container {
+ padding: 0.5rem .8rem;
+}
+
+.history-item-container:not(.item-active):hover {
+ background-color: #ffe7d5;
+}
+
+.history-item-container div {
+ width: 100%;
+}
+
+.history-item-container .owner {
+ margin-top: 0.2rem;
+}
+
+.history-item-container .owner i {
+ color: #549b5a;
+ font-size: 0.2rem;
+ margin-right: 0.2rem;
+ vertical-align: middle;
+}
+
+.history-item-container .owner span {
+ vertical-align: middle;
+}
+
+.diff-container {
+ flex: 1 1 auto;
+ overflow: auto;
+ box-sizing: border-box;
+}
+
+.diff-wrapper {
+ width: 90%;
+ border: 1px solid #e5e5e5;
+ margin: 20px auto;
+ background-color: #fff;
+ min-height: calc(100% - 40px);
+ padding: 70px 75px;
+}
+
+@media (max-width:991.8px) {
+ .diff-container {
+ width: 100%;
+ box-sizing: border-box;
+ }
+ .diff-wrapper {
+ padding: 20px;
+ }
+}
+@media (min-width:992px) {
+ .seafile-history-side-panel {
+ /* width: 260px; */
+ width: 100%;
+ }
+}
diff --git a/frontend/src/css/markdown-viewer/markdown-editor.css b/frontend/src/css/markdown-viewer/markdown-editor.css
new file mode 100644
index 0000000000..8a0c593b5b
--- /dev/null
+++ b/frontend/src/css/markdown-viewer/markdown-editor.css
@@ -0,0 +1,170 @@
+.seafile-md-viewer {
+ height: 100%;
+ flex-direction: row;
+}
+.sf-md-viewer-topbar-first {
+ padding: 4px 10px;
+ background-color: #fff;
+ border-bottom: 1px solid #e5e5e5;
+ box-shadow: 0 3px 2px -2px rgba(200,200,200,.15);
+ flex-shrink:0;
+ align-items: center;
+}
+.seafile-md-viewer-container {
+ width: 70%;
+ background-color: #fafaf9;
+ overflow-y: auto;
+}
+.seafile-md-viewer-main {
+ flex:auto;
+ overflow:auto;
+ background:#fafaf9;
+ width: 70%;
+}
+
+.seafile-md-viewer-outline-heading2,
+.seafile-md-viewer-outline-heading3 {
+ margin-left: .75rem;
+ line-height: 2.5;
+ color:#666;
+ white-space: nowrap;
+ overflow:hidden;
+ text-overflow:ellipsis;
+ cursor:pointer;
+}
+.seafile-md-viewer-outline-heading3 {
+ margin-left: 2rem;
+}
+.seafile-md-viewer-outline-heading2:hover,
+.seafile-md-viewer-outline-heading3:hover {
+ color: #eb8205;
+}
+.seafile-markdown-outline {
+ position: fixed;
+ padding-right: 1rem;
+ top: 97px;
+ right: 0;
+ width: 200px;
+ overflow: scroll;
+ height: 80%;
+}
+.seafile-editor-outline {
+ border-left: 1px solid #ddd;
+}
+.seafile-markdown-outline .active {
+ color: #eb8205;
+ border-left: 1px solid #eb8205;
+}
+.seafile-markdown-outline .outline-h2, .seafile-markdown-outline .outline-h3 {
+ height: 30px;
+ margin-left: 0;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ font-size: 14px;
+}
+.seafile-markdown-outline .outline-h2 {
+ padding-left: 20px;
+}
+.seafile-markdown-outline .outline-h3 {
+ padding-left: 40px;
+}
+
+/* side-panel */
+.seafile-md-viewer-side-panel {
+ height: 100%;
+ overflow:hidden;
+ user-select: none;
+}
+.seafile-md-viewer-side-panel .seafile-editor-outline {
+ border-left: 0;
+}
+.seafile-md-viewer-side-panel:hover {
+ overflow:auto;
+}
+.seafile-md-viewer-side-panel-heading {
+ padding:7px 0;
+ border-bottom: 1px solid #eee;
+ color: #a0a0a0;
+}
+.seafile-md-viewer-side-panel-content {
+ padding:8px 0;
+ font-size: 0.875rem;
+}
+
+.seafile-md-viewer-side-panel {
+ border-left: 1px solid #e6e6dd;
+ background-color: #fff;
+ height: 100%;
+ overflow: hidden;
+}
+.seafile-md-viewer-side-panel .tab-content {
+ height: calc(100% - 39px);
+ overflow-y: scroll;
+}
+.seafile-md-viewer-side-panel .md-side-panel-nav {
+ margin: 0;
+}
+.md-side-panel-nav .nav-item {
+ width: 33.3%;
+ padding-top: 4px;
+}
+.md-side-panel-nav .nav-item .nav-link {
+ margin: 0 auto;
+}
+.md-side-panel-nav .nav-item i {
+ padding: 0 8px;
+ font-size: 1rem;
+ width: 1rem;
+}
+.comments-number {
+ font-size: 12px;
+ width: 16px;
+ height: 16px;
+ border-radius: 8px;
+ text-align: center;
+ line-height: 16px;
+ font-weight: 600;
+ background-color: #fd9644;
+ position: absolute;
+ top: 10%;
+ right: 30%;
+ color: #fff;
+}
+
+.seafile-viewer-comment-btn {
+ position: absolute;
+ top: 0;
+ right: 5000px;
+ border: 1px solid rgba(0, 40, 100, 0.12);
+ box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
+ border-radius: 3px;
+ background-color: #fff;
+ padding: 5px;
+}
+.seafile-viewer-comment-btn:hover {
+ cursor: pointer;
+ background-color: #eee;
+}
+.seafile-md-viewer-slate {
+ flex: auto;
+ position: relative;
+ margin: 20px 40px;
+}
+@media (max-width:991.8px) {
+ .seafile-md-viewer-side-panel {
+ display:none;
+ }
+ .seafile-markdown-outline {
+ display: none;
+ }
+ .seafile-md-viewer-slate {
+ width: calc(100% - 80px);
+ margin: 20px 40px;
+ }
+}
+@media (min-width:992px) {
+ .seafile-md-viewer-side-panel {
+ width: 30%;
+ }
+}
\ No newline at end of file
diff --git a/frontend/src/css/review-comments.css b/frontend/src/css/review-comments.css
index 133aa32539..6452889b45 100644
--- a/frontend/src/css/review-comments.css
+++ b/frontend/src/css/review-comments.css
@@ -38,7 +38,6 @@
text-align: center;
}
.seafile-comment-item {
- overflow-y: hidden;
padding: 15px 10px;
}
.seafile-comment-item-resolved {
@@ -125,7 +124,8 @@
margin: 10px 20px 5px 15px;
height: 100%;
}
-.seafile-comment-footer .seafile-add-comment .add-comment-input {
+.seafile-add-comment .add-comment-input,
+.seafile-edit-comment .edit-comment-input {
box-sizing: border-box;
background-color: #fff;
border: 1px solid #e6e6dd;
@@ -134,10 +134,15 @@
min-height: 70px;
width: 100%;
resize: none;
+ border-radius: 5px;
}
-.seafile-comment-footer .seafile-add-comment .submit-comment {
+.seafile-comment-footer .seafile-add-comment .comment-btn,
+.seafile-edit-comment .comment-btn {
height: 28px;
}
+.seafile-edit-comment {
+ margin-top: 10px;
+}
@media (max-width: 992px) {
.seafile-comment-footer {
min-height: 80px;
@@ -151,7 +156,7 @@
.seafile-comment {
font-size: 12px;
}
- .seafile-comment-footer .seafile-add-comment .submit-comment {
+ .seafile-comment-footer .seafile-add-comment .comment-btn {
height: 24px;
}
.seafile-comment-footer {
diff --git a/frontend/src/markdown-editor.js b/frontend/src/markdown-editor.js
index 44f31e1cae..7d9044e582 100644
--- a/frontend/src/markdown-editor.js
+++ b/frontend/src/markdown-editor.js
@@ -1,19 +1,28 @@
import React from 'react';
import SeafileEditor from '@seafile/seafile-editor';
import 'whatwg-fetch';
+import { Value, Document, Block } from 'slate';
import { seafileAPI } from './utils/seafile-api';
import { Utils } from './utils/utils';
+import { gettext } from './utils/constants';
import ModalPortal from './components/modal-portal';
import EditFileTagDialog from './components/dialog/edit-filetag-dialog';
import ListRelatedFileDialog from './components/dialog/list-related-file-dialog';
import AddRelatedFileDialog from './components/dialog/add-related-file-dialog';
import ShareDialog from './components/dialog/share-dialog';
+import CommentDialog from './components/markdown-view/comment-dialog';
import MarkdownViewerSlate from '@seafile/seafile-editor/dist/viewer/markdown-viewer-slate';
import io from "socket.io-client";
import toaster from "./components/toast";
-import { serialize } from "@seafile/seafile-editor/dist/utils/slate2markdown";
+import { serialize, deserialize } from "@seafile/seafile-editor/dist/utils/slate2markdown";
import LocalDraftDialog from "@seafile/seafile-editor/dist/components/local-draft-dialog";
+import DiffViewer from '@seafile/seafile-editor/dist/viewer/diff-viewer';
import MarkdownViewerToolbar from './components/toolbar/markdown-viewer-toolbar';
+import MarkdownViewerSidePanel from './components/markdown-view/markdown-viewer-side-panel';
+import Loading from './components/loading';
+import { Editor, findRange } from '@seafile/slate-react';
+
+import './css/markdown-viewer/markdown-editor.css';
const CryptoJS = require('crypto-js');
const { repoID, repoName, filePath, fileName, mode, draftID, draftFilePath, draftOriginFilePath, isDraft, hasDraft, shareLinkExpireDaysMin, shareLinkExpireDaysMax } = window.app.pageOptions;
@@ -150,21 +159,15 @@ class EditorUtilities {
}
getFileHistory() {
- return (
- seafileAPI.getFileHistory(repoID, filePath)
- );
+ return seafileAPI.getFileHistory(repoID, filePath);
}
getFileInfo() {
- return (
- seafileAPI.getFileInfo(repoID, filePath)
- );
+ return seafileAPI.getFileInfo(repoID, filePath);
}
getRepoInfo(newRepoID) {
- return (
- seafileAPI.getRepoInfo(newRepoID)
- );
+ return seafileAPI.getRepoInfo(newRepoID);
}
getInternalLink() {
@@ -192,9 +195,7 @@ class EditorUtilities {
}
listFileHistoryRecords(page, perPage) {
- return (
- seafileAPI.listFileHistoryRecords(repoID, filePath, page, perPage)
- );
+ return seafileAPI.listFileHistoryRecords(repoID, filePath, page, perPage);
}
getFileHistoryVersion(commitID, filePath) {
@@ -213,8 +214,8 @@ class EditorUtilities {
return seafileAPI.listComments(this.repoID, this.filePath);
}
- updateComment(commentID, resolved, detail) {
- return seafileAPI.updateComment(this.repoID, commentID, resolved, detail);
+ updateComment(commentID, resolved, detail, newComment) {
+ return seafileAPI.updateComment(this.repoID, commentID, resolved, detail, newComment);
}
deleteComment(commentID) {
@@ -270,6 +271,7 @@ class MarkdownEditor extends React.Component {
this.draftPlainValue = '';
this.state = {
markdownContent: '',
+ oldMarkdownContent: '',
loading: true,
mode: 'editor',
fileInfo: {
@@ -293,12 +295,14 @@ class MarkdownEditor extends React.Component {
showAddRelatedFileDialog: false,
showMarkdownEditorDialog: false,
showShareLinkDialog: false,
+ showCommentDialog: false,
showDraftSaved: false,
collabUsers: userInfo ?
[{user: userInfo, is_editing: false}] : [],
- isShowHistory: false,
- isShowComments: false,
commentsNumber: null,
+ activeTab: 'outline',
+ loadingDiff: false,
+ value: null,
};
if (this.state.collabServer) {
@@ -333,8 +337,8 @@ class MarkdownEditor extends React.Component {
if (res.data.id !== this.state.fileInfo.id) {
toaster.notify(
- {this.props.t('this_file_has_been_updated')}
- {' '}{this.props.t('refresh')}
+ {gettext('This file has been updated.')}
+ {' '}{gettext('Refresh')}
,
{id: 'repo_updated', duration: 3600});
}
@@ -384,6 +388,7 @@ class MarkdownEditor extends React.Component {
showAddRelatedFileDialog: false,
showMarkdownEditorDialog: false,
showShareLinkDialog: false,
+ showCommentDialog: false,
});
}
@@ -400,9 +405,12 @@ class MarkdownEditor extends React.Component {
this.draftPlainValue = value;
}
}
+
setContent = (str) => {
+ let value = deserialize(str);
this.setState({
- markdownContent: str
+ markdownContent: str,
+ value: value,
});
}
@@ -489,6 +497,12 @@ class MarkdownEditor extends React.Component {
showShareLinkDialog: true,
});
break;
+ case 'comment':
+ this.setState({
+ showMarkdownEditorDialog: true,
+ showCommentDialog: true,
+ });
+ break;
default:
return;
}
@@ -504,6 +518,7 @@ class MarkdownEditor extends React.Component {
contact_email: this.props.editorUtilities.contact_email,
},
});
+ document.removeEventListener('selectionchange', this.setBtnPosition);
}
componentDidMount() {
@@ -531,6 +546,7 @@ class MarkdownEditor extends React.Component {
let isBlankFile = (contentLength === 0 || contentLength === 1);
let hasPermission = (this.state.fileInfo.permission === 'rw');
let isEditMode = mode === 'edit' ? true : false;
+ let value = deserialize(res.data);
this.setState({
markdownContent: res.data,
loading: false,
@@ -540,6 +556,7 @@ class MarkdownEditor extends React.Component {
// case2: If mode == 'edit' and the file has no draft
// case3: The length of markDownContent is 1 when clear all content in editor and the file has no draft
editorMode: (hasPermission && (isDraft || (isEditMode && !hasDraft) || (isBlankFile && !hasDraft))) ? 'rich' : 'viewer',
+ value: value,
});
});
});
@@ -562,11 +579,18 @@ class MarkdownEditor extends React.Component {
},
});
}
-
this.checkDraft();
this.listRelatedFiles();
this.listFileTags();
this.getCommentsNumber();
+
+ document.addEventListener('selectionchange', this.setBtnPosition);
+ setTimeout(() => {
+ let url = new URL(window.location.href);
+ if (url.hash) {
+ window.location.href = window.location.href;
+ }
+ }, 100);
}
listRelatedFiles = () => {
@@ -662,19 +686,6 @@ class MarkdownEditor extends React.Component {
this.openDialogs('share_link');
}
- toggleHistory = () => {
- this.setState({ isShowHistory: !this.state.isShowHistory });
- }
-
- toggleCommentList = () => {
- if (this.state.isShowHistory) {
- this.setState({ isShowHistory: false, isShowComments: true });
- }
- else {
- this.setState({ isShowComments: !this.state.isShowComments });
- }
- }
-
getCommentsNumber = () => {
editorUtilities.getCommentsNumber().then((res) => {
let commentsNumber = res.data[Object.getOwnPropertyNames(res.data)[0]];
@@ -686,10 +697,166 @@ class MarkdownEditor extends React.Component {
onCommentAdded = () => {
this.getCommentsNumber();
+ this.toggleCancel();
+ }
+
+ showDiffViewer = () => {
+ this.setState({
+ loadingDiff: false,
+ });
+ }
+
+ setDiffViewerContent = (markdownContent, oldMarkdownContent) => {
+ this.setState({
+ markdownContent: markdownContent,
+ oldMarkdownContent: oldMarkdownContent
+ });
+ this.showDiffViewer();
+ }
+
+ reloadDiffContent = () =>{
+ this.setState({
+ loadingDiff: true,
+ });
+ }
+
+ tabItemClick = (tab) => {
+ if (this.state.activeTab !== tab) {
+ this.setState({
+ activeTab: tab
+ });
+ }
+ }
+
+ setBtnPosition = (e) => {
+ let isShowComments = this.state.activeTab === 'comments' ? true : false;
+ if (!isShowComments) return;
+ const nativeSelection = window.getSelection();
+ if (!nativeSelection.rangeCount) {
+ this.range = null;
+ return;
+ }
+ if (nativeSelection.isCollapsed === false) {
+ const nativeRange = nativeSelection.getRangeAt(0);
+ const focusNode = nativeSelection.focusNode;
+ if ((focusNode.tagName === 'I') ||
+ (focusNode.nodeType !== 3 && focusNode.getAttribute('class') === 'language-type')) {
+ // fix select last paragraph
+ let fragment = nativeRange.cloneContents();
+ let startNode = fragment.firstChild.firstChild;
+ if (!startNode) return;
+ let newNativeRange = document.createRange();
+ newNativeRange.setStartBefore(startNode);
+ newNativeRange.setEndAfter(startNode);
+
+ this.range = findRange(newNativeRange, this.state.value);
+ }
+
+ else {
+ this.range = findRange(nativeRange, this.state.value);
+ }
+ if (!this.range) return;
+ let rect = nativeRange.getBoundingClientRect();
+ // fix Safari bug
+ if (navigator.userAgent.indexOf('Chrome') < 0 && navigator.userAgent.indexOf('Safari') > 0) {
+ if (nativeRange.collapsed && rect.top == 0 && rect.height == 0) {
+ if (nativeRange.startOffset == 0) {
+ nativeRange.setEnd(nativeRange.endContainer, 1);
+ } else {
+ nativeRange.setStart(nativeRange.startContainer, nativeRange.startOffset - 1);
+ }
+ rect = nativeRange.getBoundingClientRect();
+ if (rect.top == 0 && rect.height == 0) {
+ if (nativeRange.getClientRects().length) {
+ rect = nativeRange.getClientRects()[0];
+ }
+ }
+ }
+ }
+ let style = this.refs.commentbtn.style;
+ style.top = `${rect.top - 63 + this.refs.markdownContainer.scrollTop}px`;
+ style.right = '0px';
+ }
+ else {
+ let style = this.refs.commentbtn.style;
+ style.top = '-1000px';
+ }
+ }
+
+ addComment = (e) => {
+ e.stopPropagation();
+ this.getQuote();
+ this.openDialogs('comment');
+ }
+
+ getQuote = () => {
+ let range = this.range;
+ if (!range) return;
+ const { document } = this.state.value;
+ let { anchor, focus } = range;
+ const anchorText = document.getNode(anchor.key);
+ const focusText = document.getNode(focus.key);
+ const anchorInline = document.getClosestInline(anchor.key);
+ const focusInline = document.getClosestInline(focus.key);
+ // COMPAT: If the selection is at the end of a non-void inline node, and
+ // there is a node after it, put it in the node after instead. This
+ // standardizes the behavior, since it's indistinguishable to the user.
+ if (anchorInline && anchor.offset == anchorText.text.length) {
+ const block = document.getClosestBlock(anchor.key);
+ const nextText = block.getNextText(anchor.key);
+ if (nextText) {
+ range = range.moveAnchorTo(nextText.key, 0);
+ }
+ }
+ if (focusInline && focus.offset == focusText.text.length) {
+ const block = document.getClosestBlock(focus.key);
+ const nextText = block.getNextText(focus.key);
+ if (nextText) {
+ range = range.moveFocusTo(nextText.key, 0);
+ }
+ }
+ let fragment = document.getFragmentAtRange(range);
+ let nodes = this.removeNullNode(fragment.nodes);
+ let newFragment = Document.create({
+ nodes: nodes
+ });
+ let newValue = Value.create({
+ document: newFragment
+ });
+ this.quote = serialize(newValue.toJSON());
+ let selection = document.createSelection(range);
+ selection = selection.setIsFocused(true);
+ this.setState({
+ commentPosition: selection.anchor.path
+ });
+ }
+
+ removeNullNode = (oldNodes) => {
+ let newNodes = [];
+ oldNodes.map((node) => {
+ const text = node.text.trim();
+ const childNodes = node.nodes;
+ if (!text) return;
+ if ((childNodes && childNodes.size === 1) || (!childNodes)) {
+ newNodes.push(node);
+ }
+ else if (childNodes.size > 1) {
+ let nodes = this.removeNullNode(childNodes);
+ let newNode = Block.create({
+ nodes: nodes,
+ data: node.data,
+ key: node.key,
+ type: node.type
+ });
+ newNodes.push(newNode);
+ }
+ });
+ return newNodes;
}
render() {
let component;
+ let isShowComments = this.state.activeTab === 'comments' ? true : false;
if (this.state.loading) {
return (
@@ -711,42 +878,51 @@ class MarkdownEditor extends React.Component {
openDialogs={this.openDialogs}
fileTagList={this.state.fileTagList}
relatedFiles={this.state.relatedFiles}
- commentsNumber={this.state.commentsNumber}
- toggleCommentList={this.toggleCommentList}
toggleShareLinkDialog={this.toggleShareLinkDialog}
onEdit={this.onEdit}
- showFileHistory={true}
- toggleHistory={this.toggleHistory}
toggleNewDraft={editorUtilities.createDraftFile}
/>
-
+
+
+ {
+ this.state.activeTab === "history" ?
+
+
+ { this.state.loadingDiff ?
+ :
+
+ }
+
+
+ :
+
+
+ {isShowComments &&
+ }
+
+ }
+
+
+
)
} else {
@@ -836,6 +1012,17 @@ class MarkdownEditor extends React.Component {
/>
}
+ {this.state.showCommentDialog &&
+
+
+
+ }
)}
diff --git a/seahub/api2/endpoints/file_comment.py b/seahub/api2/endpoints/file_comment.py
index dc6df59dc8..da9950b6cb 100644
--- a/seahub/api2/endpoints/file_comment.py
+++ b/seahub/api2/endpoints/file_comment.py
@@ -109,6 +109,15 @@ class FileCommentView(APIView):
logger.error(e)
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, 'Internal error.')
+ comment = request.data.get('comment')
+ if comment is not None:
+ try:
+ file_comment.comment = comment
+ file_comment.save()
+ except Exception as e:
+ logger.error(e)
+ return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, 'Internal error.')
+
try:
avatar_size = int(request.GET.get('avatar_size', AVATAR_DEFAULT_SIZE))
except ValueError: