mirror of
https://github.com/haiwen/seahub.git
synced 2025-09-05 08:53:14 +00:00
change viewer (#3115)
This commit is contained in:
@@ -1,305 +0,0 @@
|
||||
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,
|
||||
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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
CommentItem.propTypes = CommentItempropTypes;
|
||||
|
||||
export default CommentsList;
|
@@ -4,6 +4,7 @@ import PropTypes from 'prop-types';
|
||||
import axios from 'axios';
|
||||
import Loading from '../loading';
|
||||
import moment from 'moment';
|
||||
import { gettext } from '../../utils/constants';
|
||||
import '../../css/markdown-viewer/history-viewer.css';
|
||||
|
||||
const propTypes = {
|
||||
@@ -97,6 +98,12 @@ class HistoryList extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<div className="seafile-history-side-panel">
|
||||
<div className="seafile-history-title">
|
||||
<div onClick={this.props.toggleHistoryPanel} className={'seafile-history-title-close'}>
|
||||
<i className={'fa fa-times-circle'}/>
|
||||
</div>
|
||||
<div className={'seafile-history-title-text'}>{gettext('History Versions')}</div>
|
||||
</div>
|
||||
<ul onScroll={this.onScroll} className={'history-list-container'}>
|
||||
{this.state.historyList ?
|
||||
this.state.historyList.map((item, index = 0, arr) => {
|
||||
|
@@ -1,164 +0,0 @@
|
||||
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;
|
@@ -2,7 +2,6 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { gettext } from '../../utils/constants';
|
||||
|
||||
|
||||
const OutlineItempropTypes = {
|
||||
scrollToNode: PropTypes.func.isRequired,
|
||||
node: PropTypes.object.isRequired,
|
||||
@@ -41,7 +40,6 @@ const propTypes = {
|
||||
scrollToNode: PropTypes.func.isRequired,
|
||||
isViewer: PropTypes.bool.isRequired,
|
||||
document: PropTypes.object.isRequired,
|
||||
editor: PropTypes.object.isRequired,
|
||||
activeTitleIndex: PropTypes.number,
|
||||
value: PropTypes.object,
|
||||
};
|
||||
@@ -56,13 +54,13 @@ class OutlineView extends React.PureComponent {
|
||||
|
||||
return (
|
||||
<div className="seafile-editor-outline">
|
||||
<div className="seafile-editor-outline-heading">{gettext('Contents')}</div>
|
||||
{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}
|
||||
|
Reference in New Issue
Block a user