1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-08-23 01:07:35 +00:00
seahub/frontend/src/draft-review.js

1007 lines
32 KiB
JavaScript
Raw Normal View History

2018-10-15 07:51:29 +00:00
import React from 'react';
import ReactDOM from 'react-dom';
2018-11-28 04:43:53 +00:00
import PropTypes from 'prop-types';
2018-10-16 10:19:51 +00:00
/* eslint-disable */
2018-10-15 07:51:29 +00:00
import Prism from 'prismjs';
2018-10-16 10:19:51 +00:00
/* eslint-enable */
2018-11-23 02:19:36 +00:00
import { siteRoot, gettext, reviewID, draftOriginFilePath, draftFilePath, draftOriginRepoID,
2018-12-28 15:22:49 +00:00
draftFileName, opStatus, publishFileVersion, originFileVersion, author, authorAvatar,
draftFileExists, originFileExists } from './utils/constants';
2018-10-15 07:51:29 +00:00
import { seafileAPI } from './utils/seafile-api';
import axios from 'axios';
import DiffViewer from '@seafile/seafile-editor/dist/viewer/diff-viewer';
2018-12-03 06:03:21 +00:00
import { serialize } from '@seafile/seafile-editor/dist/utils/slate2markdown/serialize';
2018-10-15 07:51:29 +00:00
import Loading from './components/loading';
2018-12-07 06:58:37 +00:00
import toaster from './components/toast';
2018-10-23 05:13:44 +00:00
import ReviewComments from './components/review-list-view/review-comments';
2018-12-03 06:03:21 +00:00
import ReviewCommentDialog from './components/review-list-view/review-comment-dialog.js';
2018-11-07 03:03:26 +00:00
import { Tooltip } from 'reactstrap';
2018-11-05 02:33:33 +00:00
import AddReviewerDialog from './components/dialog/add-reviewer-dialog.js';
2018-11-23 02:19:36 +00:00
import { findRange } from '@seafile/slate-react';
2018-11-26 06:01:28 +00:00
import { Nav, NavItem, NavLink, TabContent, TabPane } from 'reactstrap';
import classnames from 'classnames';
import HistoryList from './pages/review/history-list';
2018-12-13 06:30:45 +00:00
import { Value, Document, Block } from 'slate';
2018-10-15 07:51:29 +00:00
import './assets/css/fa-solid.css';
import './assets/css/fa-regular.css';
import './assets/css/fontawesome.css';
import './css/layout.css';
import './css/initial-style.css';
import './css/toolbar.css';
2018-10-23 05:13:44 +00:00
import './css/draft-review.css';
2018-10-15 07:51:29 +00:00
2019-01-18 10:40:45 +00:00
const URL = require('url-parse');
2018-10-15 07:51:29 +00:00
require('@seafile/seafile-editor/dist/editor/code-hight-package');
class DraftReview extends React.Component {
constructor(props) {
super(props);
this.state = {
draftContent: '',
draftOriginContent: '',
reviewStatus: opStatus,
isLoading: true,
2018-10-23 05:13:44 +00:00
commentsNumber: null,
2018-10-25 02:39:16 +00:00
inResizing: false,
commentWidth: 30,
2018-10-30 03:07:01 +00:00
isShowDiff: true,
showDiffTip: false,
2018-11-05 02:33:33 +00:00
showReviewerDialog: false,
2018-11-07 03:03:26 +00:00
reviewers: [],
2018-11-26 06:01:28 +00:00
activeTab: 'reviewInfo',
historyList: [],
2018-11-27 02:41:27 +00:00
totalReversionCount: 0,
changedNodes: [],
2018-11-28 09:03:44 +00:00
isShowCommentDialog: false,
2019-01-18 10:40:45 +00:00
activeItem: null,
2018-10-15 07:51:29 +00:00
};
2018-12-08 04:13:02 +00:00
this.quote = '';
2018-11-23 02:19:36 +00:00
this.newIndex = null;
this.oldIndex = null;
2018-11-27 02:41:27 +00:00
this.changeIndex = -1;
2018-12-13 06:30:45 +00:00
this.range = null;
2018-10-15 07:51:29 +00:00
}
componentDidMount() {
2018-11-26 06:01:28 +00:00
this.initialContent();
document.addEventListener('selectionchange', this.setBtnPosition);
}
initialContent = () => {
2018-12-28 15:22:49 +00:00
switch(this.state.reviewStatus) {
case 'closed':
this.setState({
isLoading: false,
isShowDiff: false
})
break;
case "open":
if (!draftFileExists) {
this.setState({
isLoading: false,
isShowDiff: false
})
return;
}
if (!originFileExists) {
seafileAPI.getFileDownloadLink(draftOriginRepoID, draftFilePath)
.then(res => {
seafileAPI.getFileContent(res.data)
.then(res => {
this.setState({
draftContent: res.data,
draftOriginContent: res.data,
isLoading: false,
isShowDiff: false
});
})
})
return;
}
2019-01-18 10:40:45 +00:00
const hash = window.location.hash;
if (hash.indexOf('#history-') === 0) {
const currentCommitID = hash.slice(9, 49);
const preCommitID = hash.slice(50, 90);
this.setState({
isLoading: false,
activeTab: 'history',
});
seafileAPI.listFileHistoryRecords(draftOriginRepoID, draftFilePath, 1, 25).then((res) => {
const historyList = res.data.data;
2018-12-28 15:22:49 +00:00
this.setState({
2019-01-18 10:40:45 +00:00
historyList: historyList,
totalReversionCount: res.data.total_count
});
for (let i = 0, length = historyList.length; i < length; i++) {
if (preCommitID === historyList[i].commit_id) {
this.setState({
activeItem: i
});
break;
}
}
});
axios.all([
seafileAPI.getFileRevision(draftOriginRepoID, currentCommitID, draftFilePath),
seafileAPI.getFileRevision(draftOriginRepoID, preCommitID, draftFilePath)
]).then(axios.spread((res1, res2) => {
axios.all([seafileAPI.getFileContent(res1.data), seafileAPI.getFileContent(res2.data)]).then(axios.spread((content1,content2) => {
this.setDiffViewerContent(content2.data, content1.data);
}));
2018-12-28 15:22:49 +00:00
}));
2019-01-18 10:40:45 +00:00
return;
} else {
axios.all([
seafileAPI.getFileDownloadLink(draftOriginRepoID, draftFilePath),
seafileAPI.getFileDownloadLink(draftOriginRepoID, draftOriginFilePath)
]).then(axios.spread((res1, res2) => {
axios.all([
seafileAPI.getFileContent(res1.data),
seafileAPI.getFileContent(res2.data)
]).then(axios.spread((draftContent, draftOriginContent) => {
this.setState({
draftContent: draftContent.data,
draftOriginContent: draftOriginContent.data,
isLoading: false
});
let that = this;
setTimeout(() => {
that.getChangedNodes();
}, 100);
}));
}));
}
2018-12-28 15:22:49 +00:00
break;
case "finished":
if (!originFileExists) {
this.setState({
isLoading: false,
isShowDiff: false
})
return;
}
let dl0 = siteRoot + 'repo/' + draftOriginRepoID + '/' + publishFileVersion + '/download?' + 'p=' + draftOriginFilePath;
let dl = siteRoot + 'repo/' + draftOriginRepoID + '/' + originFileVersion + '/download?' + 'p=' + draftOriginFilePath;
axios.all([
seafileAPI.getFileContent(dl0),
seafileAPI.getFileContent(dl)
2018-10-15 07:51:29 +00:00
]).then(axios.spread((draftContent, draftOriginContent) => {
this.setState({
draftContent: draftContent.data,
draftOriginContent: draftOriginContent.data,
2018-12-28 15:22:49 +00:00
isLoading: false,
2018-10-15 07:51:29 +00:00
});
}));
2018-12-28 15:22:49 +00:00
break;
}
2018-11-23 02:19:36 +00:00
}
componentWillUnmount() {
document.removeEventListener('selectionchange', this.setBtnPosition);
2018-10-15 07:51:29 +00:00
}
2019-01-18 10:40:45 +00:00
onHistoryItemClick = (currentCommitID, preCommitID, activeItem) => {
const url = 'history-' + preCommitID + '-' + currentCommitID;
this.setURL(url);
this.setState({
activeItem: activeItem
});
axios.all([
seafileAPI.getFileRevision(draftOriginRepoID, currentCommitID, draftFilePath),
seafileAPI.getFileRevision(draftOriginRepoID, preCommitID, draftFilePath)
]).then(axios.spread((res1, res2) => {
axios.all([seafileAPI.getFileContent(res1.data), seafileAPI.getFileContent(res2.data)]).then(axios.spread((content1,content2) => {
this.setDiffViewerContent(content1.data, content2.data);
}));
}));
}
onHistoryListChange = (historyList) => {
this.setState({
historyList: historyList
});
}
setURL = (newurl) => {
let url = new URL(window.location.href);
url.set('hash', newurl);
window.location.href = url.toString();
}
2018-10-15 07:51:29 +00:00
onCloseReview = () => {
seafileAPI.updateReviewStatus(reviewID, 'closed').then(res => {
this.setState({reviewStatus: 'closed'});
2018-10-25 09:50:47 +00:00
let msg_s = gettext('Successfully closed review %(reviewID)s.');
msg_s = msg_s.replace('%(reviewID)s', reviewID);
2018-12-07 06:58:37 +00:00
toaster.success(msg_s);
2018-10-15 07:51:29 +00:00
}).catch(() => {
2018-10-25 09:50:47 +00:00
let msg_s = gettext('Failed to close review %(reviewID)s');
msg_s = msg_s.replace('%(reviewID)s', reviewID);
2018-12-07 06:58:37 +00:00
toaster.danger(msg_s);
2018-10-15 07:51:29 +00:00
});
}
onPublishReview = () => {
seafileAPI.updateReviewStatus(reviewID, 'finished').then(res => {
2018-11-26 06:01:28 +00:00
this.setState({reviewStatus: 'finished', activeTab: 'reviewInfo' });
2018-11-15 10:05:20 +00:00
let msg_s = gettext('Successfully published draft.');
2018-12-07 06:58:37 +00:00
toaster.success(msg_s);
2018-10-15 07:51:29 +00:00
}).catch(() => {
2018-11-15 10:05:20 +00:00
let msg_s = gettext('Failed to publish draft.');
2018-12-07 06:58:37 +00:00
toaster.danger(msg_s);
2018-10-15 07:51:29 +00:00
});
}
2018-11-28 09:03:44 +00:00
toggleCommentDialog = () => {
this.setState({
isShowCommentDialog: !this.state.isShowCommentDialog
});
}
2018-10-15 07:51:29 +00:00
2018-10-23 05:13:44 +00:00
getCommentsNumber = () => {
seafileAPI.listReviewComments(reviewID).then((res) => {
let number = res.data.total_count;
this.setState({
commentsNumber: number,
});
});
}
2018-10-25 02:39:16 +00:00
onResizeMouseUp = () => {
if(this.state.inResizing) {
this.setState({
inResizing: false
});
}
}
onResizeMouseDown = () => {
this.setState({
inResizing: true
});
};
onResizeMouseMove = (e) => {
let rate = 100 - e.nativeEvent.clientX / this.refs.main.clientWidth * 100;
if(rate < 20 || rate > 60) {
this.setState({
inResizing: false
});
return null;
}
this.setState({
commentWidth: rate
});
};
2018-10-30 03:07:01 +00:00
onSwitchShowDiff = () => {
2018-11-27 02:41:27 +00:00
if (!this.state.isShowDiff) {
let that = this;
setTimeout(() => {
that.getChangedNodes();
}, 100);
}
2018-10-30 03:07:01 +00:00
this.setState({
isShowDiff: !this.state.isShowDiff,
2018-11-07 03:03:26 +00:00
});
2018-10-30 03:07:01 +00:00
}
toggleDiffTip = () => {
this.setState({
showDiffTip: !this.state.showDiffTip
});
}
2018-11-05 02:33:33 +00:00
toggleAddReviewerDialog = () => {
2018-11-12 01:28:02 +00:00
if (this.state.showReviewerDialog) {
this.listReviewers();
}
2018-11-05 02:33:33 +00:00
this.setState({
showReviewerDialog: !this.state.showReviewerDialog
});
}
2018-11-07 03:03:26 +00:00
listReviewers = () => {
seafileAPI.listReviewers(reviewID).then((res) => {
this.setState({
reviewers: res.data.reviewers
});
});
}
2018-11-23 02:19:36 +00:00
setBtnPosition = (e) => {
const nativeSelection = window.getSelection();
if (!nativeSelection.rangeCount) {
2018-12-13 06:30:45 +00:00
this.range = null;
2018-11-23 02:19:36 +00:00
return;
}
if (nativeSelection.isCollapsed === false) {
const nativeRange = nativeSelection.getRangeAt(0);
2018-12-20 07:21:14 +00:00
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;
}
2018-12-20 07:21:14 +00:00
let newNativeRange = document.createRange();
newNativeRange.setStartBefore(startNode);
newNativeRange.setEndAfter(startNode);
this.range = findRange(newNativeRange, this.refs.diffViewer.value);
}
else {
this.range = findRange(nativeRange, this.refs.diffViewer.value);
2018-11-23 02:19:36 +00:00
}
if (!this.range) {
return;
}
2018-11-23 02:19:36 +00:00
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;
2018-12-06 09:56:31 +00:00
style.top = `${rect.top - 100 + this.refs.viewContent.scrollTop}px`;
2018-11-23 02:19:36 +00:00
}
else {
let style = this.refs.commentbtn.style;
2018-11-28 09:03:44 +00:00
style.top = '-1000px';
2018-11-23 02:19:36 +00:00
}
}
2018-12-08 04:13:02 +00:00
getQuote = () => {
2018-12-13 06:30:45 +00:00
let range = this.range;
2018-11-23 02:19:36 +00:00
if (!range) {
return;
}
2018-12-20 07:21:14 +00:00
this.quote = '';
2018-11-23 02:19:36 +00:00
const { document } = this.refs.diffViewer.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);
const focusBlock = document.getClosestBlock(focus.key);
const anchorBlock = document.getClosestBlock(anchor.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);
}
}
2018-12-08 04:13:02 +00:00
let fragment = document.getFragmentAtRange(range);
2018-12-13 06:30:45 +00:00
let nodes = this.removeNullNode(fragment.nodes);
let newFragment = Document.create({
nodes: nodes
});
2018-12-08 04:13:02 +00:00
let newValue = Value.create({
2018-12-13 06:30:45 +00:00
document: newFragment
2018-12-08 04:13:02 +00:00
});
this.quote = serialize(newValue.toJSON());
2018-11-23 02:19:36 +00:00
let blockPath = document.createSelection(range).anchor.path.slice(0, 1);
let node = document.getNode(blockPath);
this.newIndex = node.data.get('new_index');
this.oldIndex = node.data.get('old_index');
2018-12-03 06:03:21 +00:00
}
2018-12-13 06:30:45 +00:00
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;
}
2018-12-03 06:03:21 +00:00
addComment = (e) => {
e.stopPropagation();
2018-12-08 04:13:02 +00:00
this.getQuote();
if (!this.quote) {
return;
}
2018-11-23 02:19:36 +00:00
this.setState({
2018-11-28 09:03:44 +00:00
isShowCommentDialog: true
2018-11-23 02:19:36 +00:00
});
}
findScrollContainer = (element, window) => {
let parent = element.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;
}
2018-12-08 04:13:02 +00:00
scrollToQuote = (newIndex, oldIndex, quote) => {
2018-11-23 02:19:36 +00:00
const nodes = this.refs.diffViewer.value.document.nodes;
let key;
nodes.map((node) => {
if (node.data.get('old_index') == oldIndex && node.data.get('new_index') == newIndex) {
key = node.key;
}
});
if (typeof(key) !== 'string') {
nodes.map((node) => {
2018-12-08 04:13:02 +00:00
if (node.text.indexOf(quote) > 0) {
2018-11-23 02:19:36 +00:00
key = node.key;
}
});
}
if (typeof(key) === 'string') {
const win = window;
2019-01-03 00:36:38 +00:00
let element = win.document.querySelector(`[data-key="${key}"]`);
while (element.tagName === "CODE") {
element = element.parentNode;
}
2018-11-23 02:19:36 +00:00
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;
}
}
}
2018-11-26 06:01:28 +00:00
tabItemClick = (tab) => {
if (this.state.activeTab !== tab) {
2019-01-18 10:40:45 +00:00
if (tab !== 'history') {
this.setURL('#');
}
2018-11-28 04:43:53 +00:00
if (tab == 'reviewInfo') {
2018-11-26 06:01:28 +00:00
this.initialContent();
2018-11-28 04:43:53 +00:00
}
else if (tab == 'history'){
2018-11-26 06:01:28 +00:00
this.initialDiffViewerContent();
}
this.setState({
activeTab: tab
2018-11-28 04:43:53 +00:00
});
2018-11-26 06:01:28 +00:00
}
}
2018-11-27 02:41:27 +00:00
getChangedNodes = () => {
const nodes = this.refs.diffViewer.value.document.nodes;
let keys = [];
let lastDiffState = '';
nodes.map((node) => {
2018-11-28 04:43:53 +00:00
if (node.data.get('diff_state') === 'diff-added' && lastDiffState !== 'diff-added') {
2018-11-27 02:41:27 +00:00
keys.push(node.key);
}
2018-11-28 04:43:53 +00:00
else if (node.data.get('diff_state') === 'diff-removed' && lastDiffState !== 'diff-removed') {
2018-11-27 02:41:27 +00:00
keys.push(node.key);
}
2018-12-20 09:45:59 +00:00
else if (node.data.get('diff_state') === 'diff-replaced' && lastDiffState !== 'diff-replaced') {
keys.push(node.key);
}
2018-11-28 04:43:53 +00:00
lastDiffState = node.data.get('diff_state');
2018-11-27 02:41:27 +00:00
});
this.setState({
changedNodes: keys
});
}
scrollToChangedNode = (scroll) => {
if (this.state.changedNodes.length == 0) return;
if (scroll === 'up') {
this.changeIndex++;
}
else {
this.changeIndex--;
}
if (this.changeIndex > this.state.changedNodes.length - 1) {
this.changeIndex = 0;
}
if (this.changeIndex < 0) {
this.changeIndex = this.state.changedNodes.length - 1;
}
const win = window;
let key = this.state.changedNodes[this.changeIndex];
2018-12-28 10:20:50 +00:00
let element = win.document.querySelector(`[data-key="${key}"]`);
// fix code-block or tables
while (element.className.indexOf('diff-') === -1 && element.tagName !== "BODY") {
element = element.parentNode;
}
2018-11-27 02:41:27 +00:00
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;
}
}
2018-10-23 05:13:44 +00:00
componentWillMount() {
this.getCommentsNumber();
2018-11-07 03:03:26 +00:00
this.listReviewers();
2018-10-23 05:13:44 +00:00
}
2018-11-26 06:01:28 +00:00
initialDiffViewerContent = () => {
seafileAPI.listFileHistoryRecords(draftOriginRepoID, draftFilePath, 1, 25).then((res) => {
this.setState({
historyList: res.data.data,
totalReversionCount: res.data.total_count
});
if (res.data.data.length > 1) {
axios.all([
seafileAPI.getFileRevision(draftOriginRepoID, res.data.data[0].commit_id, draftFilePath),
seafileAPI.getFileRevision(draftOriginRepoID, res.data.data[1].commit_id, draftFilePath)
]).then(axios.spread((res1, res2) => {
axios.all([seafileAPI.getFileContent(res1.data), seafileAPI.getFileContent(res2.data)]).then(axios.spread((content1,content2) => {
this.setState({
draftContent: content1.data,
draftOriginContent: content2.data
});
}));
}));
} else {
seafileAPI.getFileRevision(draftOriginRepoID, res.data.data[0].commit_id, draftFilePath).then((res) => {
seafileAPI.getFileContent(res.data).then((content) => {
this.setState({
draftContent: content.data,
draftOriginContent: ''
2018-11-28 04:43:53 +00:00
});
2018-11-26 06:01:28 +00:00
});
});
}
});
}
setDiffViewerContent = (newContent, prevContent) => {
this.setState({
draftContent: newContent,
draftOriginContent: prevContent
2018-11-28 04:43:53 +00:00
});
2018-11-26 06:01:28 +00:00
}
2018-11-28 09:03:44 +00:00
onCommentAdded = () => {
this.getCommentsNumber();
this.toggleCommentDialog();
}
2018-12-28 15:22:49 +00:00
showDiffViewer = () => {
return (
<div>
{this.state.isShowDiff ?
<DiffViewer
newMarkdownContent={this.state.draftContent}
oldMarkdownContent={this.state.draftOriginContent}
ref="diffViewer"
/>
:
<DiffViewer
newMarkdownContent={this.state.draftContent}
oldMarkdownContent={this.state.draftContent}
ref="diffViewer"
/>
}
<i className="fa fa-plus-square review-comment-btn" ref="commentbtn" onMouseDown={this.addComment}></i>
</div>
)
}
renderContent = () => {
switch(this.state.reviewStatus) {
case "closed":
return <p className="error">{gettext('The review has been closed.')}</p>;
case "open":
if (!draftFileExists) {
return <p className="error">{gettext('Draft has been deleted.')}</p>;
}
return this.showDiffViewer();
case "finished":
if (!originFileExists) {
return <p className="error">{gettext('Original file has been deleted.')}</p>
}
return this.showDiffViewer();
}
}
showDiffButton = () => {
return (
<div className={'seafile-toggle-diff'}>
<label className="custom-switch" id="toggle-diff">
<input type="checkbox" checked={this.state.isShowDiff && 'checked'}
name="option" className="custom-switch-input"
onChange={this.onSwitchShowDiff}/>
<span className="custom-switch-indicator"></span>
</label>
<Tooltip placement="bottom" isOpen={this.state.showDiffTip}
target="toggle-diff" toggle={this.toggleDiffTip}>
{gettext('View diff')}</Tooltip>
</div>
)
}
renderDiffButton = () => {
switch(this.state.reviewStatus) {
case "closed":
return;
case "open":
if (!draftFileExists) {
return;
}
if (!originFileExists) {
return;
}
return this.showDiffButton();
case "finished":
if (!originFileExists) {
return;
}
return this.showDiffButton();
}
}
renderGo = (OriginFileLink, draftLink) => {
let viewFile = <a href={OriginFileLink} className="view-file-link">{gettext('View File')}</a>;
let editDraft = <a href={draftLink} className="draft-link">{gettext('Edit draft')}</a>;
switch(this.state.reviewStatus) {
case "closed":
return viewFile;
case "open":
if (!draftFileExists) {
return viewFile;
}
return editDraft;
case "finished":
if (!originFileExists) {
return;
}
return viewFile;
}
}
showNavItem = (showTab) => {
switch(showTab) {
case "info":
return (
<NavItem className="nav-item">
<NavLink
className={classnames({ active: this.state.activeTab === 'reviewInfo' })}
onClick={() => { this.tabItemClick('reviewInfo');}}
>
<i className="fas fa-info-circle"></i>
</NavLink>
</NavItem>
);
case "comments":
return (
<NavItem className="nav-item">
<NavLink
className={classnames({ active: this.state.activeTab === 'comments' })}
onClick={() => {this.tabItemClick('comments');}}
>
<i className="fa fa-comments"></i>
{ this.state.commentsNumber > 0 &&
<div className='comments-number'>{this.state.commentsNumber}</div>}
</NavLink>
</NavItem>
);
case "history":
return (
<NavItem className="nav-item">
<NavLink
className={classnames({ active: this.state.activeTab === 'history' })}
onClick={() => { this.tabItemClick('history');}}
>
<i className="fas fa-history"></i>
</NavLink>
</NavItem>
);
}
}
renderNavItems = () => {
switch(this.state.reviewStatus) {
case "closed":
return (
<Nav tabs className="review-side-panel-nav">
{this.showNavItem("info")}
</Nav>
);
case "open":
if (!draftFileExists) {
return (
<Nav tabs className="review-side-panel-nav">
{this.showNavItem("info")}
</Nav>
);
}
return (
<Nav tabs className="review-side-panel-nav">
{this.showNavItem('info')}
{this.showNavItem('comments')}
{this.showNavItem('history')}
</Nav>
);
case "finished":
if (!originFileExists) {
return (
<Nav tabs className="review-side-panel-nav">
{this.showNavItem("info")}
</Nav>
);
}
return (
<Nav tabs className="review-side-panel-nav">
{this.showNavItem('info')}
{this.showNavItem('comments')}
</Nav>
)
}
}
2018-10-15 07:51:29 +00:00
render() {
2018-10-25 02:39:16 +00:00
const onResizeMove = this.state.inResizing ? this.onResizeMouseMove : null;
2018-10-30 06:20:02 +00:00
const draftLink = siteRoot + 'lib/' + draftOriginRepoID + '/file' + draftFilePath + '?mode=edit';
2018-11-19 08:18:11 +00:00
const OriginFileLink = siteRoot + 'lib/' + draftOriginRepoID + '/file' + draftOriginFilePath + '/';
2018-10-15 07:51:29 +00:00
return(
<div className="wrapper">
<div id="header" className="header review">
<div className="cur-file-info">
<div className="info-item file-feature">
2018-12-25 02:26:05 +00:00
<span className="sf2-icon-review"></span>
2018-10-15 07:51:29 +00:00
</div>
<div className="info-item file-info">
2018-10-30 03:07:01 +00:00
<React.Fragment>
<span className="file-name">{draftFileName}</span>
<span className="file-copywriting">{gettext('review')}</span>
2018-12-28 15:22:49 +00:00
{this.renderGo(OriginFileLink, draftLink)}
2018-10-30 03:07:01 +00:00
</React.Fragment>
2018-11-23 02:19:36 +00:00
</div>
2018-10-15 07:51:29 +00:00
</div>
2018-10-23 05:13:44 +00:00
<div className="button-group">
2018-12-28 15:22:49 +00:00
{this.renderDiffButton()}
2018-10-23 05:13:44 +00:00
{
this.state.reviewStatus === 'open' &&
<div className="cur-file-operation">
2018-11-15 10:05:20 +00:00
<button className='btn btn-secondary file-operation-btn' title={gettext('Close review')}
onClick={this.onCloseReview}>{gettext('Close')}</button>
2018-12-28 15:22:49 +00:00
{ draftFileExists && <button className='btn btn-success file-operation-btn' title={gettext('Publish draft')}
2018-11-15 10:05:20 +00:00
onClick={this.onPublishReview}>{gettext('Publish')}</button>
2018-12-28 15:22:49 +00:00
}
2018-10-23 05:13:44 +00:00
</div>
}
{
this.state.reviewStatus === 'finished' &&
2019-01-02 03:28:39 +00:00
<div className="cur-file-operation">
<button className='btn review-state review-state-finished'
title={gettext('Finished')}>{gettext('Finished')}</button>
</div>
2018-10-23 05:13:44 +00:00
}
{
2019-01-02 03:28:39 +00:00
this.state.reviewStatus === 'closed' &&
<div className="cur-file-operation">
<button className='btn review-state review-state-closed'
title={gettext('Closed')}>{gettext('Closed')}</button>
</div>
2018-10-23 05:13:44 +00:00
}
</div>
2018-10-15 07:51:29 +00:00
</div>
2018-10-25 02:39:16 +00:00
<div id="main" className="main" ref="main">
<div className="cur-view-container content-container"
onMouseMove={onResizeMove} onMouseUp={this.onResizeMouseUp} ref="comment">
<div style={{width:(100-this.state.commentWidth)+'%'}}
2018-12-03 06:03:21 +00:00
className='cur-view-content' ref="viewContent">
2018-11-07 03:03:26 +00:00
{this.state.isLoading ?
2018-10-30 03:07:01 +00:00
<div className="markdown-viewer-render-content article">
<Loading />
</div>
:
2018-11-23 02:19:36 +00:00
<div className="markdown-viewer-render-content article" ref="mainPanel">
2018-12-28 15:22:49 +00:00
{this.renderContent()}
2018-10-30 03:07:01 +00:00
</div>
}
2018-10-15 07:51:29 +00:00
</div>
2018-12-03 06:03:21 +00:00
<div className="cur-view-right-part" style={{width:(this.state.commentWidth)+'%'}}>
<div className="seafile-comment-resize" onMouseDown={this.onResizeMouseDown}></div>
<div className="review-side-panel">
2018-12-28 15:22:49 +00:00
{this.renderNavItems()}
2018-12-03 06:03:21 +00:00
<TabContent activeTab={this.state.activeTab}>
<TabPane tabId="reviewInfo">
<div className="review-side-panel-body">
<SidePanelReviewers
reviewers={this.state.reviewers}
toggleAddReviewerDialog={this.toggleAddReviewerDialog}/>
<SidePanelAuthor/>
{ this.state.isShowDiff &&
<SidePanelChanges
changedNumber={this.state.changedNodes.length}
scrollToChangedNode={this.scrollToChangedNode}/>
2018-11-28 04:43:53 +00:00
}
2018-12-03 06:03:21 +00:00
</div>
</TabPane>
<TabPane tabId="comments" className="comments">
<ReviewComments
scrollToQuote={this.scrollToQuote}
getCommentsNumber={this.getCommentsNumber}
commentsNumber={this.state.commentsNumber}
inResizing={this.state.inResizing}
/>
</TabPane>
{ this.state.reviewStatus == 'finished'? '':
<TabPane tabId="history" className="history">
2019-01-18 10:40:45 +00:00
<HistoryList
activeItem={this.state.activeItem}
2018-12-03 06:03:21 +00:00
historyList={this.state.historyList}
2019-01-18 10:40:45 +00:00
onHistoryItemClick={this.onHistoryItemClick}
onHistoryListChange={this.onHistoryListChange}
totalReversionCount={this.state.totalReversionCount}
/>
2018-11-28 04:43:53 +00:00
</TabPane>
2018-12-03 06:03:21 +00:00
}
</TabContent>
2018-11-07 03:03:26 +00:00
</div>
2018-12-03 06:03:21 +00:00
</div>
2018-10-15 07:51:29 +00:00
</div>
</div>
2018-11-05 02:33:33 +00:00
{ this.state.showReviewerDialog &&
<AddReviewerDialog
showReviewerDialog={this.state.showReviewerDialog}
toggleAddReviewerDialog={this.toggleAddReviewerDialog}
reviewID={reviewID}
2018-11-07 03:03:26 +00:00
reviewers={this.state.reviewers}
2018-11-05 02:33:33 +00:00
/>
}
2018-12-06 09:56:31 +00:00
{this.state.isShowCommentDialog &&
<ReviewCommentDialog
toggleCommentDialog={this.toggleCommentDialog}
onCommentAdded={this.onCommentAdded}
2018-12-08 04:13:02 +00:00
quote={this.quote}
2018-12-06 09:56:31 +00:00
newIndex={this.newIndex}
oldIndex={this.oldIndex}
/>
}
2018-10-15 07:51:29 +00:00
</div>
);
}
}
2018-11-27 02:41:27 +00:00
class SidePanelReviewers extends React.Component {
constructor(props) {
super(props);
}
render() {
return (
<div className="review-side-panel-item">
<div className="review-side-panel-header">{gettext('Reviewers')}
<i className="fa fa-cog" onClick={this.props.toggleAddReviewerDialog}></i>
</div>
{ this.props.reviewers.length > 0 ?
this.props.reviewers.map((item, index = 0, arr) => {
return (
<div className="reviewer-info" key={index}>
<img className="avatar review-side-panel-avatar" src={item.avatar_url} alt=""/>
<span className="reviewer-name">{item.user_name}</span>
</div>
);
})
:
<span>{gettext('No reviewer yet.')}</span>
}
</div>
);
}
}
2018-11-28 04:43:53 +00:00
const sidePanelReviewersPropTypes = {
reviewers: PropTypes.array.isRequired,
toggleAddReviewerDialog: PropTypes.func.isRequired
};
SidePanelReviewers.propTypes = sidePanelReviewersPropTypes;
2018-11-27 02:41:27 +00:00
class SidePanelAuthor extends React.Component {
render() {
return (
<div className="review-side-panel-item">
<div className="review-side-panel-header">{gettext('Author')}</div>
<div className="author-info">
<img className="avatar review-side-panel-avatar" src={authorAvatar} alt=""/>
<span className="author-name">{author}</span>
</div>
</div>
);
}
}
class SidePanelChanges extends React.Component {
constructor(props) {
super(props);
}
render() {
return (
<div className="review-side-panel-item">
<div className="review-side-panel-header">{gettext('Changes')}</div>
<div className="changes-info">
2018-12-25 05:48:13 +00:00
<span>{gettext('Number of changes:')}{' '}{this.props.changedNumber}</span>
2018-11-27 02:41:27 +00:00
{ this.props.changedNumber > 0 &&
<div>
<i className="fa fa-arrow-circle-up" onClick={() => { this.props.scrollToChangedNode('down');}}></i>
<i className="fa fa-arrow-circle-down" onClick={() => { this.props.scrollToChangedNode('up');}}></i>
</div>
}
</div>
</div>
);
}
}
2018-11-28 04:43:53 +00:00
const sidePanelChangesPropTypes = {
changedNumber: PropTypes.number.isRequired,
scrollToChangedNode: PropTypes.func.isRequired
};
SidePanelChanges.propTypes = sidePanelChangesPropTypes;
2018-10-15 07:51:29 +00:00
ReactDOM.render (
<DraftReview />,
document.getElementById('wrapper')
2018-10-25 09:50:47 +00:00
);