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 ( +
    + +
    + ); + } +} + +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 ? - - : - - } { (!this.props.hasDraft && this.props.fileInfo.permission === 'rw')? : null } - { - (this.props.showFileHistory) && (!this.props.isShowHistory && ) - } - + ); } 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: