mirror of
https://github.com/haiwen/seahub.git
synced 2025-09-05 08:53:14 +00:00
update file comment (#3057)
* update file comment * [update] edit comment * Add markdown side panel
This commit is contained in:
@@ -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}
|
||||
/>
|
||||
</React.Fragment>
|
||||
@@ -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(
|
||||
<li className="seafile-comment-item" id={item.id}>
|
||||
<div className="seafile-comment-info">
|
||||
<img className="avatar" src={item.avatar_url} alt=""/>
|
||||
<div className="reviewer-info">
|
||||
<div className="reviewer-name">{item.user_name}</div>
|
||||
<div className="review-time">{this.props.time}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="seafile-edit-comment">
|
||||
<textarea className="edit-comment-input" value={this.state.newComment} onChange={this.handleCommentChange} clos="100" rows="3" warp="virtual"></textarea>
|
||||
<Button className="comment-btn" color="success" size="sm" onClick={this.updateComment} id={item.id}>{gettext('Update')}</Button>{' '}
|
||||
<Button className="comment-btn" color="secondary" size="sm" onClick={this.toggleEditComment}> {gettext('Cancle')}</Button>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<li className={item.resolved ? 'seafile-comment-item seafile-comment-item-resolved'
|
||||
: 'seafile-comment-item'} id={item.id}>
|
||||
@@ -195,6 +243,11 @@ class CommentItem extends React.Component {
|
||||
(item.user_email === username) &&
|
||||
<DropdownItem onClick={this.props.deleteComment} className="delete-comment"
|
||||
id={item.id}>{gettext('Delete')}</DropdownItem>}
|
||||
{
|
||||
(item.user_email === username) &&
|
||||
<DropdownItem onClick={this.toggleEditComment}
|
||||
className="edit-comment" id={item.id}>{gettext('Edit')}</DropdownItem>
|
||||
}
|
||||
{
|
||||
!item.resolved &&
|
||||
<DropdownItem onClick={this.props.resolveComment} className="seafile-comment-resolved"
|
||||
|
88
frontend/src/components/markdown-view/comment-dialog.js
Normal file
88
frontend/src/components/markdown-view/comment-dialog.js
Normal file
@@ -0,0 +1,88 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Button } from 'reactstrap';
|
||||
import { processor } from '@seafile/seafile-editor/dist/utils/seafile-markdown2html';
|
||||
import { gettext } from '../../utils/constants';
|
||||
import '../../css/markdown-viewer/comment-dialog.css';
|
||||
|
||||
const propTypes = {
|
||||
editorUtilities: PropTypes.object.isRequired,
|
||||
quote: PropTypes.string.isRequired,
|
||||
commentPosition: PropTypes.object.isRequired,
|
||||
onCommentAdded: PropTypes.func.isRequired,
|
||||
toggleCommentDialog: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
class CommentDialog extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
comment: '',
|
||||
quote: '',
|
||||
};
|
||||
}
|
||||
|
||||
handleCommentChange = (event) => {
|
||||
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 (
|
||||
<div className="comment-dialog">
|
||||
<div>{this.props.editorUtilities.name}</div>
|
||||
<blockquote className="comment-dialog-quote">
|
||||
<div dangerouslySetInnerHTML={{ __html: this.state.quote}}></div>
|
||||
</blockquote>
|
||||
<textarea value={this.state.comment} onChange={this.handleCommentChange}></textarea>
|
||||
<div className="button-group">
|
||||
<Button size="sm" color="primary" onClick={this.submitComment}>{gettext('Submit')}</Button>
|
||||
<Button size="sm" color="secondary" onClick={this.props.toggleCommentDialog}>{gettext('Cancel')}</Button>
|
||||
</div>
|
||||
<span className="comment-dialog-triangle"></span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
CommentDialog.propTypes = propTypes;
|
||||
|
||||
export default CommentDialog;
|
306
frontend/src/components/markdown-view/comments-list.js
Normal file
306
frontend/src/components/markdown-view/comments-list.js
Normal file
@@ -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 (
|
||||
<div className="seafile-comment">
|
||||
<div className="seafile-comment-title">
|
||||
<div className="seafile-comment-text">{gettext('Show resolved comments')}</div>
|
||||
<div className="d-flex">
|
||||
<label className="custom-switch" id="toggle-resolved-comments">
|
||||
<input type="checkbox" name="option" className="custom-switch-input"
|
||||
onChange={this.toggleResolvedComment}
|
||||
checked={this.state.showResolvedComment && 'checked'}
|
||||
/>
|
||||
<span className="custom-switch-indicator"></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<ul className={'seafile-comment-list'}>
|
||||
{ (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 (
|
||||
<CommentItem
|
||||
item={item} time={time} key={index}
|
||||
editorUtilities={this.props.editorUtilities}
|
||||
editComment={this.editComment}
|
||||
showResolvedComment={this.state.showResolvedComment}
|
||||
deleteComment={this.deleteComment} resolveComment={this.resolveComment}
|
||||
commentsList={this.state.commentsList} scrollToQuote={this.scrollToQuote}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
{(this.state.commentsList.length == 0 && this.props.commentsNumber > 0) && <Loading/>}
|
||||
{ this.props.commentsNumber == 0 &&
|
||||
<li className="comment-vacant">{gettext('No comment yet.')}</li>
|
||||
}
|
||||
</ul>
|
||||
<div className="seafile-comment-footer">
|
||||
<div className="seafile-add-comment">
|
||||
<textarea className="add-comment-input" ref="commentTextarea"
|
||||
placeholder={gettext('Add a comment')}
|
||||
clos="100" rows="3" warp="virtual"></textarea>
|
||||
<Button className="submit-comment" color="success"
|
||||
size="sm" onClick={this.submitComment} >
|
||||
{gettext('Submit')}</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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(
|
||||
<li className="seafile-comment-item" id={item.id}>
|
||||
<div className="seafile-comment-info">
|
||||
<img className="avatar" src={avatar_url} alt=""/>
|
||||
<div className="reviewer-info">
|
||||
<div className="reviewer-name">{user_name}</div>
|
||||
<div className="review-time">{this.props.time}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="seafile-edit-comment">
|
||||
<textarea className="edit-comment-input" value={this.state.newComment} onChange={this.handleCommentChange} clos="100" rows="3" warp="virtual"></textarea>
|
||||
<Button className="comment-btn" color="success" size="sm" onClick={this.updateComment} id={item.id}>{gettext('Update')}</Button>{' '}
|
||||
<Button className="comment-btn" color="secondary" size="sm" onClick={this.toggleEditComment}>{gettext('Cancle')}</Button>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<li className={resolved ? 'seafile-comment-item seafile-comment-item-resolved' : 'seafile-comment-item'} id={id}>
|
||||
<div className="seafile-comment-info">
|
||||
<img className="avatar" src={avatar_url} alt=""/>
|
||||
<div className="reviewer-info">
|
||||
<div className="reviewer-name">{user_name}</div>
|
||||
<div className="review-time">{this.props.time}</div>
|
||||
</div>
|
||||
<Dropdown isOpen={this.state.dropdownOpen} size="sm"
|
||||
className="seafile-comment-dropdown" toggle={this.toggleDropDownMenu}>
|
||||
<DropdownToggle className="seafile-comment-dropdown-btn">
|
||||
<i className="fas fa-ellipsis-v"></i>
|
||||
</DropdownToggle>
|
||||
<DropdownMenu>
|
||||
{(user_email === this.props.editorUtilities.userName) &&
|
||||
<DropdownItem onClick={this.props.deleteComment}
|
||||
className="delete-comment" id={item.id}>{gettext('Delete')}</DropdownItem>
|
||||
}
|
||||
{(user_email === this.props.editorUtilities.userName) &&
|
||||
<DropdownItem onClick={this.toggleEditComment}
|
||||
className="edit-comment" id={item.id}>{gettext('Edit')}</DropdownItem>
|
||||
}
|
||||
{!resolved &&
|
||||
<DropdownItem onClick={this.props.resolveComment} className="seafile-comment-resolved"
|
||||
id={item.id}>{gettext('Mark as resolved')}</DropdownItem>
|
||||
}
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
</div>
|
||||
{item.detail &&
|
||||
<blockquote className="seafile-comment-content">
|
||||
<div onClick={this.scrollToQuote} dangerouslySetInnerHTML={{ __html: this.state.quote }}></div>
|
||||
</blockquote>
|
||||
}
|
||||
<div className="seafile-comment-content" dangerouslySetInnerHTML={{ __html: this.state.comment }}></div>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
CommentsList.propTypes = CommentItempropTypes;
|
||||
|
||||
export default CommentsList;
|
158
frontend/src/components/markdown-view/history-list.js
Normal file
158
frontend/src/components/markdown-view/history-list.js
Normal file
@@ -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 (
|
||||
<div className="seafile-history-side-panel">
|
||||
<ul onScroll={this.onScroll} className={'history-list-container'}>
|
||||
{this.state.historyList ?
|
||||
this.state.historyList.map((item, index = 0, arr) => {
|
||||
let preItemIndex = index + 1;
|
||||
if (preItemIndex === arr.length) {
|
||||
preItemIndex = index;
|
||||
}
|
||||
return (
|
||||
<HistoryItem
|
||||
onClick={this.onClick}
|
||||
ctime={item.ctime}
|
||||
className={this.state.activeItem === index ? 'item-active': ''}
|
||||
currentItem={item}
|
||||
name={item.creator_name}
|
||||
index={index}
|
||||
key={index}
|
||||
preItem={arr[preItemIndex]}
|
||||
/>
|
||||
);
|
||||
}) : <Loading/>
|
||||
}
|
||||
{
|
||||
this.state.loading &&
|
||||
<li className={'reloading-reversion'}><Loading style={{width: '0.5rem', margin: '0 auto', color: '#b0b0b0'}}/></li>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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 (
|
||||
<li onClick={(event) => this.props.onClick(event, this.props.index, this.props.preItem, this.props.currentItem)} className={'history-item-container ' + this.props.className}>
|
||||
<div className="time">{time}</div>
|
||||
<div className="owner"><i className="fa fa-square"/><span>{this.props.name}</span></div>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
HistoryList.propTypes = HistoryItempropTypes;
|
||||
|
||||
|
||||
export default HistoryList;
|
@@ -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 (
|
||||
<NavLink className={classnames({ active: this.props.activeTab === 'outline' })}
|
||||
onClick={() => { this.tabItemClick('outline');}} ><i className="fa fa-list"></i>
|
||||
</NavLink>
|
||||
);
|
||||
case 'comments':
|
||||
return (
|
||||
<NavLink className={classnames({ active: this.props.activeTab === 'comments' })}
|
||||
onClick={() => {this.tabItemClick('comments');}}><i className="fa fa-comments"></i>
|
||||
{this.props.commentsNumber > 0 && <div className='comments-number'>{this.props.commentsNumber}</div>}
|
||||
</NavLink>
|
||||
);
|
||||
case 'history':
|
||||
return (
|
||||
<NavLink className={classnames({ active: this.props.activeTab === 'history' })}
|
||||
onClick={() => { this.tabItemClick('history');}}><i className="fas fa-history"></i>
|
||||
</NavLink>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
renderNavItems = () => {
|
||||
return (
|
||||
<Nav tabs className="md-side-panel-nav">
|
||||
<NavItem className="nav-item">{this.showNavItem('outline')}</NavItem>
|
||||
<NavItem className="nav-item">{this.showNavItem('comments')}</NavItem>
|
||||
<NavItem className="nav-item">{this.showNavItem('history')}</NavItem>
|
||||
</Nav>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<div className="seafile-md-viewer-side-panel">
|
||||
{this.renderNavItems()}
|
||||
<TabContent activeTab={this.props.activeTab}>
|
||||
<TabPane tabId="outline" className="outline">
|
||||
<OutlineView
|
||||
isViewer={true}
|
||||
document={this.props.value.document}
|
||||
editor={this.props.viewer}
|
||||
scrollToNode={this.scrollToNode}
|
||||
/>
|
||||
</TabPane>
|
||||
<TabPane tabId="comments" className="comments">
|
||||
<CommentsList
|
||||
editorUtilities={this.props.editorUtilities}
|
||||
scrollToQuote={this.scrollToQuote}
|
||||
getCommentsNumber={this.props.getCommentsNumber}
|
||||
commentsNumber={this.props.commentsNumber}
|
||||
/>
|
||||
</TabPane>
|
||||
<TabPane tabId="history" className="history">
|
||||
<HistoryList
|
||||
editorUtilities={this.props.editorUtilities}
|
||||
showDiffViewer={this.props.showDiffViewer}
|
||||
setDiffViewerContent={this.props.setDiffViewerContent}
|
||||
reloadDiffContent={this.props.reloadDiffContent}
|
||||
/>
|
||||
</TabPane>
|
||||
</TabContent>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
MarkdownViewerSidePanel.propTypes = propTypes;
|
||||
|
||||
export default MarkdownViewerSidePanel;
|
70
frontend/src/components/markdown-view/outline.js
Normal file
70
frontend/src/components/markdown-view/outline.js
Normal file
@@ -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 (
|
||||
<div className={c} key={node.key} onClick={this.onClick}>{node.text}</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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 (
|
||||
<div className="seafile-editor-outline">
|
||||
{headerList.size > 0 ?
|
||||
headerList.map((node, index) => {
|
||||
let active = (index === this.props.activeTitleIndex) ? ' active' : '';
|
||||
return (
|
||||
<OutlineItem
|
||||
key={node.key}
|
||||
editor={this.props.editor}
|
||||
value={this.props.value}
|
||||
node={node}
|
||||
active={active}
|
||||
scrollToNode={this.props.scrollToNode}
|
||||
/>
|
||||
);
|
||||
}) : <div className={'size-panel-no-content'}>{gettext('No out line.')}</div>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
OutlineView.propTypes = propTypes;
|
||||
|
||||
export default OutlineView;
|
@@ -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 (
|
||||
<CommentItem item={item} showResolvedComment={this.state.showResolvedComment}
|
||||
resolveComment={this.resolveComment} key={index}
|
||||
resolveComment={this.resolveComment} key={index} editComment={this.editComment}
|
||||
scrollToQuote={this.scrollToQuote} deleteComment={this.deleteComment}/>
|
||||
);
|
||||
})
|
||||
@@ -187,7 +194,7 @@ class ReviewComments extends React.Component {
|
||||
<textarea className="add-comment-input" value={this.state.comment}
|
||||
placeholder={gettext('Add a comment.')} onChange={this.handleCommentChange}
|
||||
clos="100" rows="3" warp="virtual"></textarea>
|
||||
<Button className="submit-comment" color="success"
|
||||
<Button className="comment-btn" color="success"
|
||||
size="sm" onClick={this.submitComment}>
|
||||
{gettext('Submit')}</Button>
|
||||
</div>
|
||||
@@ -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(
|
||||
<li className="seafile-comment-item" id={item.id}>
|
||||
<div className="seafile-comment-info">
|
||||
<img className="avatar" src={item.avatarUrl} alt=""/>
|
||||
<div className="reviewer-info">
|
||||
<div className="reviewer-name">{item.name}</div>
|
||||
<div className="review-time">{item.time}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="seafile-edit-comment">
|
||||
<textarea className="edit-comment-input" value={this.state.newComment} onChange={this.handleCommentChange} clos="100" rows="3" warp="virtual"></textarea>
|
||||
<Button className="comment-btn" color="success" size="sm" onClick={this.updateComment} id={item.id}>{gettext('Update')}</Button>{' '}
|
||||
<Button className="comment-btn" color="secondary" size="sm" onClick={this.toggleEditComment}> {gettext('Cancle')}</Button>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<li className={item.resolved ? 'seafile-comment-item seafile-comment-item-resolved'
|
||||
: 'seafile-comment-item'} id={item.id}>
|
||||
@@ -280,6 +328,9 @@ class CommentItem extends React.Component {
|
||||
{ (item.userEmail === username) &&
|
||||
<DropdownItem onClick={this.props.deleteComment}
|
||||
className="delete-comment" id={item.id}>{gettext('Delete')}</DropdownItem>}
|
||||
{ (item.userEmail === username) &&
|
||||
<DropdownItem onClick={this.toggleEditComment}
|
||||
className="edit-comment" id={item.id}>{gettext('Edit')}</DropdownItem>}
|
||||
<DropdownItem onClick={this.props.resolveComment}
|
||||
className="seafile-comment-resolved" id={item.id}>{gettext('Mark as resolved')}</DropdownItem>
|
||||
</DropdownMenu>
|
||||
|
@@ -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,26 +47,12 @@ class MarkdownViewerToolbar extends React.Component {
|
||||
<ButtonGroup>
|
||||
<IconButton id={'shareBtn'} text={gettext('Share')} icon={'fa fa-share-alt'}
|
||||
onMouseDown={this.props.toggleShareLinkDialog}/>
|
||||
{
|
||||
this.props.commentsNumber > 0 ?
|
||||
<button className="btn btn-icon btn-secondary btn-active" id="commentsNumber"
|
||||
type="button" data-active="false" onMouseDown={this.props.toggleCommentList}>
|
||||
<i className="fa fa-comments"></i>{' '}<span>{this.props.commentsNumber}</span>
|
||||
</button>
|
||||
:
|
||||
<IconButton id={'commentsNumber'} text={gettext('Comments')} icon={'fa fa-comments'}
|
||||
onMouseDown={this.props.toggleCommentList}/>
|
||||
}
|
||||
<IconButton text={gettext('Back to parent directory')} id={'parentDirectory'}
|
||||
icon={'fa fa-folder-open'} onMouseDown={this.props.backToParentDirectory}/>
|
||||
{
|
||||
(!this.props.hasDraft && this.props.fileInfo.permission === 'rw')? <IconButton text={gettext('Edit')}
|
||||
id={'editButton'} icon={'fa fa-edit'} onMouseDown={this.props.onEdit}/>: null
|
||||
}
|
||||
{
|
||||
(this.props.showFileHistory) && (!this.props.isShowHistory && <IconButton id={'historyButton'}
|
||||
text={gettext('File history')} onMouseDown={this.props.toggleHistory} icon={'fa fa-history'}/>)
|
||||
}
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -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;
|
||||
}
|
||||
|
45
frontend/src/css/markdown-viewer/comment-dialog.css
Normal file
45
frontend/src/css/markdown-viewer/comment-dialog.css
Normal file
@@ -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;
|
||||
}
|
132
frontend/src/css/markdown-viewer/comments-list.css
Normal file
132
frontend/src/css/markdown-viewer/comments-list.css
Normal file
@@ -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;
|
||||
}
|
105
frontend/src/css/markdown-viewer/history-viewer.css
Normal file
105
frontend/src/css/markdown-viewer/history-viewer.css
Normal file
@@ -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%;
|
||||
}
|
||||
}
|
170
frontend/src/css/markdown-viewer/markdown-editor.css
Normal file
170
frontend/src/css/markdown-viewer/markdown-editor.css
Normal file
@@ -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%;
|
||||
}
|
||||
}
|
@@ -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 {
|
||||
|
@@ -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(
|
||||
<span>
|
||||
{this.props.t('this_file_has_been_updated')}
|
||||
<a href='' >{' '}{this.props.t('refresh')}</a>
|
||||
{gettext('This file has been updated.')}
|
||||
<a href='' >{' '}{gettext('Refresh')}</a>
|
||||
</span>,
|
||||
{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 (
|
||||
<div className="empty-loading-page">
|
||||
@@ -711,43 +878,52 @@ 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}
|
||||
/>
|
||||
<div className="seafile-md-viewer d-flex">
|
||||
<div className="seafile-md-viewer-container d-flex" ref="markdownContainer">
|
||||
{
|
||||
this.state.activeTab === "history" ?
|
||||
<div className="diff-container">
|
||||
<div className="diff-wrapper article">
|
||||
{ this.state.loadingDiff ?
|
||||
<Loading/> :
|
||||
<DiffViewer
|
||||
newMarkdownContent={this.state.markdownContent}
|
||||
oldMarkdownContent={this.state.oldMarkdownContent}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
:
|
||||
<div className='seafile-md-viewer-slate'>
|
||||
<MarkdownViewerSlate
|
||||
fileInfo={this.state.fileInfo}
|
||||
markdownContent={this.state.markdownContent}
|
||||
editorUtilities={editorUtilities}
|
||||
collabUsers={this.state.collabUsers}
|
||||
showFileHistory={true}
|
||||
setFileInfoMtime={this.setFileInfoMtime}
|
||||
toggleStar={this.toggleStar}
|
||||
setEditorMode={this.setEditorMode}
|
||||
draftID={draftID}
|
||||
isDraft={isDraft}
|
||||
emitSwitchEditor={this.emitSwitchEditor}
|
||||
hasDraft={hasDraft}
|
||||
shareLinkExpireDaysMin={shareLinkExpireDaysMin}
|
||||
shareLinkExpireDaysMax={shareLinkExpireDaysMax}
|
||||
relatedFiles={this.state.relatedFiles}
|
||||
siteRoot={siteRoot}
|
||||
openDialogs={this.openDialogs}
|
||||
fileTagList={this.state.fileTagList}
|
||||
showDraftSaved={this.state.showDraftSaved}
|
||||
isShowHistory={this.state.isShowHistory}
|
||||
isShowComments={this.state.isShowComments}
|
||||
onCommentAdded={this.onCommentAdded}
|
||||
value={this.state.value}
|
||||
/>
|
||||
{isShowComments &&
|
||||
<i className="fa fa-plus-square seafile-viewer-comment-btn" ref="commentbtn" onMouseDown={this.addComment}></i>}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<MarkdownViewerSidePanel
|
||||
viewer={this}
|
||||
value={this.state.value}
|
||||
markdownContent={this.state.markdownContent}
|
||||
editorUtilities={editorUtilities}
|
||||
commentsNumber={this.state.commentsNumber}
|
||||
getCommentsNumber={this.getCommentsNumber}
|
||||
toggleHistory={this.toggleHistory}
|
||||
toggleCommentList={this.toggleCommentList}
|
||||
showDiffViewer={this.showDiffViewer}
|
||||
setDiffViewerContent={this.setDiffViewerContent}
|
||||
reloadDiffContent={this.reloadDiffContent}
|
||||
activeTab={this.state.activeTab}
|
||||
tabItemClick={this.tabItemClick}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
} else {
|
||||
component = <SeafileEditor
|
||||
@@ -836,6 +1012,17 @@ class MarkdownEditor extends React.Component {
|
||||
/>
|
||||
</ModalPortal>
|
||||
}
|
||||
{this.state.showCommentDialog &&
|
||||
<ModalPortal>
|
||||
<CommentDialog
|
||||
toggleCommentDialog={this.toggleCancel}
|
||||
editorUtilities={editorUtilities}
|
||||
onCommentAdded={this.onCommentAdded}
|
||||
commentPosition={this.state.commentPosition}
|
||||
quote={this.quote}
|
||||
/>
|
||||
</ModalPortal>
|
||||
}
|
||||
</React.Fragment>
|
||||
)}
|
||||
</React.Fragment>
|
||||
|
@@ -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:
|
||||
|
Reference in New Issue
Block a user