1
0
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:
陈钦亮
2019-03-14 10:15:25 +08:00
committed by Daniel Pan
parent 8c81489c51
commit c9773d7b63
16 changed files with 1631 additions and 101 deletions

View File

@@ -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"

View 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;

View 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;

View 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;

View File

@@ -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;

View 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;

View File

@@ -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>

View File

@@ -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>

View File

@@ -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;
}

View 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;
}

View 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;
}

View 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%;
}
}

View 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%;
}
}

View File

@@ -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 {

View File

@@ -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>

View File

@@ -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: