diff --git a/frontend/src/components/dialog/add-reviewer-dialog.js b/frontend/src/components/dialog/add-reviewer-dialog.js index d2cadd20d7..3bfac5a750 100644 --- a/frontend/src/components/dialog/add-reviewer-dialog.js +++ b/frontend/src/components/dialog/add-reviewer-dialog.js @@ -1,14 +1,14 @@ import React from 'react'; -import AsyncSelect from 'react-select/lib/Async'; import PropTypes from 'prop-types'; import { gettext } from '../../utils/constants'; import { Button, Modal, ModalHeader, ModalBody, ModalFooter } from 'reactstrap'; import { seafileAPI } from '../../utils/seafile-api.js'; +import UserSelect from '../user-select.js'; import '../../css/add-reviewer-dialog.css'; const propTypes = { showReviewerDialog: PropTypes.bool.isRequired, - reviewID: PropTypes.string.isRequired, + draftID: PropTypes.string.isRequired, toggleAddReviewerDialog: PropTypes.func.isRequired, reviewers: PropTypes.array.isRequired }; @@ -27,7 +27,7 @@ class AddReviewerDialog extends React.Component { } listReviewers = () => { - seafileAPI.listReviewers(this.props.reviewID).then((res) => { + seafileAPI.listDraftReviewers(this.props.draftID).then((res) => { this.setState({ reviewers: res.data.reviewers }); @@ -41,29 +41,9 @@ class AddReviewerDialog extends React.Component { this.Options = []; } - loadOptions = (value, callback) => { - if (value.trim().length > 0) { - seafileAPI.searchUsers(value.trim()).then((res) => { - this.Options = []; - for (let i = 0 ; i < res.data.users.length; i++) { - let obj = {}; - obj.value = res.data.users[i].name; - obj.email = res.data.users[i].email; - obj.label = - - - {res.data.users[i].name} - ; - this.Options.push(obj); - } - callback(this.Options); - }); - } - } - addReviewers = () => { if (this.state.selectedOption.length > 0 ) { - this.refs.reviewSelect.select.onChange([], { action: 'clear' }); + this.refs.reviewSelect.clearSelect(); let reviewers = []; for (let i = 0; i < this.state.selectedOption.length; i ++) { reviewers[i] = this.state.selectedOption[i].email; @@ -72,7 +52,7 @@ class AddReviewerDialog extends React.Component { loading: true, errorMsg: [], }); - seafileAPI.addReviewers(this.props.reviewID, reviewers).then((res) => { + seafileAPI.addDraftReviewers(this.props.draftID, reviewers).then((res) => { if (res.data.failed.length > 0) { let errorMsg = []; for (let i = 0 ; i < res.data.failed.length ; i++) { @@ -95,7 +75,7 @@ class AddReviewerDialog extends React.Component { deleteReviewer = (event) => { let reviewer = event.target.getAttribute('name'); - seafileAPI.deleteReviewer(this.props.reviewID, reviewer).then((res) => { + seafileAPI.deleteDraftReviewer(this.props.draftID, reviewer).then((res) => { if (res.data === 200) { let newReviewers = []; for (let i = 0; i < this.state.reviewers.length; i ++) { @@ -116,12 +96,12 @@ class AddReviewerDialog extends React.Component { {gettext('Request a review')}

{gettext('Add new reviewer')}

- {this.state.errorMsg.length > 0 && this.state.errorMsg.map((item, index = 0, arr) => { diff --git a/frontend/src/components/review-list-view/review-comment-dialog.js b/frontend/src/components/review-list-view/review-comment-dialog.js index a483b7bbf0..de2ebdfa78 100644 --- a/frontend/src/components/review-list-view/review-comment-dialog.js +++ b/frontend/src/components/review-list-view/review-comment-dialog.js @@ -2,7 +2,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { Button } from 'reactstrap'; import { seafileAPI } from '../../utils/seafile-api'; -import { reviewID, gettext, name } from '../../utils/constants'; +import { gettext, name, draftRepoID, draftFilePath } from '../../utils/constants'; import { processor } from '../../utils/seafile-markdown2html'; import '../../css/review-comment-dialog.css'; @@ -13,6 +13,7 @@ const commentDialogPropTypes = { quote: PropTypes.string, newIndex: PropTypes.number, oldIndex: PropTypes.number, + draftID: PropTypes.string, }; class ReviewCommentDialog extends React.Component { @@ -42,12 +43,12 @@ class ReviewCommentDialog extends React.Component { oldIndex: this.props.oldIndex }; let detailJSON = JSON.stringify(detail); - seafileAPI.addReviewComment(reviewID, comment, detailJSON).then((response) => { + seafileAPI.postComment(draftRepoID, draftFilePath, comment, detailJSON).then((response) => { this.props.onCommentAdded(); }); } else { - seafileAPI.addReviewComment(reviewID, comment).then((response) => { + seafileAPI.postComment(draftRepoID, draftFilePath, comment).then((response) => { this.props.onCommentAdded(); }); } diff --git a/frontend/src/components/review-list-view/review-comments.js b/frontend/src/components/review-list-view/review-comments.js index 34c7b9474a..b2f7a8be25 100644 --- a/frontend/src/components/review-list-view/review-comments.js +++ b/frontend/src/components/review-list-view/review-comments.js @@ -3,7 +3,7 @@ import PropTypes from 'prop-types'; import { processor } from '../../utils/seafile-markdown2html'; import { Button, Dropdown, DropdownToggle, DropdownMenu, DropdownItem } from 'reactstrap'; import { seafileAPI } from '../../utils/seafile-api'; -import { reviewID, gettext } from '../../utils/constants'; +import { gettext, draftFilePath, draftRepoID } from '../../utils/constants'; import Loading from '../../components/loading.js'; import reviewComment from '../../models/review-comment.js'; import { username } from '../../utils/constants.js'; @@ -24,14 +24,14 @@ class ReviewComments extends React.Component { this.state = { commentsList: [], inResizing: false, - commentFooterHeight: 30, + commentFooterHeight: 25, showResolvedComment: true, comment: '', }; } listComments = (scroll) => { - seafileAPI.listReviewComments(reviewID).then((response) => { + seafileAPI.listComments(draftRepoID, draftFilePath).then((response) => { response.data.comments.reverse(); let commentList = []; response.data.comments.forEach(item => { @@ -56,7 +56,7 @@ class ReviewComments extends React.Component { submitComment = () => { let comment = this.state.comment.trim(); if (comment.length > 0) { - seafileAPI.addReviewComment(reviewID, comment).then((response) => { + seafileAPI.postComment(draftRepoID, draftFilePath, comment).then((response) => { this.listComments(true); this.props.getCommentsNumber(); }); @@ -67,7 +67,7 @@ class ReviewComments extends React.Component { } resolveComment = (event) => { - seafileAPI.updateReviewComment(reviewID, event.target.id, 'true').then((res) => { + seafileAPI.updateComment(draftRepoID, event.target.id, 'true').then((res) => { this.props.getCommentsNumber(); this.listComments(); }); @@ -80,7 +80,7 @@ class ReviewComments extends React.Component { } deleteComment = (event) => { - seafileAPI.deleteReviewComment(reviewID, event.target.id).then((res) => { + seafileAPI.deleteComment(draftRepoID, event.target.id).then((res) => { this.props.getCommentsNumber(); this.listComments(); }); diff --git a/frontend/src/css/draft-review.css b/frontend/src/css/draft.css similarity index 99% rename from frontend/src/css/draft-review.css rename to frontend/src/css/draft.css index 85bbc08930..c773587a93 100644 --- a/frontend/src/css/draft-review.css +++ b/frontend/src/css/draft.css @@ -190,7 +190,7 @@ width: 1rem; } .review-side-panel .tab-content { - height: 100%; + height: calc(100% - 38px); } .review-side-panel .tab-content .comments, .review-side-panel .tab-content .tab-pane { diff --git a/frontend/src/css/file-history.css b/frontend/src/css/file-history.css index 1e9c04374c..f4df686154 100644 --- a/frontend/src/css/file-history.css +++ b/frontend/src/css/file-history.css @@ -56,6 +56,7 @@ flex: 1; overflow: hidden; min-height: 0; + height: 500px; } .history-list-container { diff --git a/frontend/src/css/review-comments.css b/frontend/src/css/review-comments.css index 9837402321..90c0d01a26 100644 --- a/frontend/src/css/review-comments.css +++ b/frontend/src/css/review-comments.css @@ -84,7 +84,7 @@ color: #555; } .seafile-comment-item .seafile-comment-content { - margin-top: 10px; + margin: 10px 0 0 40px; } .seafile-comment-item .seafile-comment-content ol, .seafile-comment-item .seafile-comment-content ul, @@ -115,7 +115,7 @@ .seafile-comment-footer { background-color: #fafaf9; border-top: 1px solid #e5e5e5; - min-height: 170px; + min-height: 120px; position: absolute; bottom: 0; padding-top: 0; @@ -130,7 +130,7 @@ background-color: #fff; border: 1px solid #e6e6dd; padding: 5px; - height: calc(100% - 100px); + height: calc(100% - 50px); min-height: 70px; width: 100%; resize: none; diff --git a/frontend/src/draft.js b/frontend/src/draft.js index bb811c0b8f..7594d43e92 100644 --- a/frontend/src/draft.js +++ b/frontend/src/draft.js @@ -5,13 +5,12 @@ import { Button } from 'reactstrap'; /* eslint-disable */ import Prism from 'prismjs'; /* eslint-enable */ -import { siteRoot, gettext } from './utils/constants'; +import { siteRoot, gettext, draftOriginFilePath, draftFilePath, author, authorAvatar, originFileExists, draftID, draftFileName, draftRepoID } from './utils/constants'; import { seafileAPI } from './utils/seafile-api'; import axios from 'axios'; import DiffViewer from '@seafile/seafile-editor/dist/viewer/diff-viewer'; import { serialize } from '@seafile/seafile-editor/dist/utils/slate2markdown/serialize'; import Loading from './components/loading'; -import toaster from './components/toast'; import ReviewComments from './components/review-list-view/review-comments'; import ReviewCommentDialog from './components/review-list-view/review-comment-dialog.js'; import { Tooltip } from 'reactstrap'; @@ -21,6 +20,7 @@ import { Nav, NavItem, NavLink, TabContent, TabPane } from 'reactstrap'; import classnames from 'classnames'; import HistoryList from './pages/review/history-list'; import { Value, Document, Block } from 'slate'; +import ModalPortal from './components/modal-portal'; import './assets/css/fa-solid.css'; import './assets/css/fa-regular.css'; @@ -28,10 +28,10 @@ import './assets/css/fontawesome.css'; import './css/layout.css'; import './css/toolbar.css'; import './css/dirent-detail.css'; -import './css/draft-review.css'; +import './css/draft.css'; require('@seafile/seafile-editor/dist/editor/code-hight-package'); -const { draftID, draftFileName, draftRepoID, draftFilePath, draftOriginFilePath, originFileExists } = window.draft.config; +const URL = require('url-parse'); class Draft extends React.Component { constructor(props) { @@ -44,11 +44,22 @@ class Draft extends React.Component { showDiffTip: false, activeTab: 'reviewInfo', commentsNumber: null, + changedNodes: [], + originRepoName: '', + isShowCommentDialog: false, + unresolvedComments: 0, + activeItem: null, + historyList: [], + showReviewerDialog: false, + reviewers: [], + inResizing: false, + rightPartWidth: 30, }; - } - - componentDidMount() { - this.initialContent(); + this.quote = ''; + this.newIndex = null; + this.oldIndex = null; + this.changeIndex = -1; + this.range = null; } initialContent = () => { @@ -68,23 +79,232 @@ class Draft extends React.Component { return; } - axios.all([ - seafileAPI.getFileDownloadLink(draftRepoID, draftFilePath), - seafileAPI.getFileDownloadLink(draftRepoID, draftOriginFilePath) - ]).then(axios.spread((res1, res2) => { - axios.all([ - seafileAPI.getFileContent(res1.data), - seafileAPI.getFileContent(res2.data) - ]).then(axios.spread((draftContent, draftOriginContent) => { + const hash = window.location.hash; + if (hash.indexOf('#history-') === 0) { + const currentCommitID = hash.slice(9, 49); + const preCommitID = hash.slice(50, 90); + let preItemFilePath, currentItemFilePath; + this.setState({ + isLoading: false, + activeTab: 'history', + }); + seafileAPI.listFileHistoryRecords(draftRepoID, draftFilePath, 1, 25).then((res) => { + const historyList = res.data.data; this.setState({ - draftContent: draftContent.data, - draftOriginContent: draftOriginContent.data, - isLoading: false - }); + 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 + }); + preItemFilePath = historyList[i].path; + } + if (currentCommitID === historyList[i].commit_id) { + currentItemFilePath = historyList[i].path; + } + if (preItemFilePath && currentItemFilePath) break; + } + axios.all([ + seafileAPI.getFileRevision(draftRepoID, currentCommitID, currentItemFilePath), + seafileAPI.getFileRevision(draftRepoID, preCommitID, preItemFilePath) + ]).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); + })); + })); + return; + }); + } else { + axios.all([ + seafileAPI.getFileDownloadLink(draftRepoID, draftFilePath), + seafileAPI.getFileDownloadLink(draftRepoID, 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); + })); + })); + } + } + + onHistoryItemClick = (currentItem, preItem, activeItem) => { + const preCommitID = preItem.commit_id; + const currentCommitID = currentItem.commit_id; + const url = 'history-' + preCommitID + '-' + currentCommitID; + this.setURL(url); + this.setState({ + activeItem: activeItem + }); + axios.all([ + seafileAPI.getFileRevision(draftRepoID, currentCommitID, currentItem.path), + seafileAPI.getFileRevision(draftRepoID, preCommitID, preItem.path) + ]).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 + }); + } + + getCommentsNumber = () => { + seafileAPI.listComments(draftRepoID, draftFilePath).then((res) => { + let number = res.data.total_count; + let comments = res.data.comments; + let unresolvedComments = 0; + for (let i = 0; i < res.data.total_count; i++) { + if (comments[i].resolved === false) { + unresolvedComments++; + } + } + this.setState({ + commentsNumber: number, + unresolvedComments: unresolvedComments, + }); + }); + } + + addComment = (e) => { + e.stopPropagation(); + this.getQuote(); + if (!this.quote) { + return; + } + this.setState({ + isShowCommentDialog: true + }); + } + + onCommentAdded = () => { + this.getCommentsNumber(); + this.toggleCommentDialog(); + } + + toggleCommentDialog = () => { + this.setState({ + isShowCommentDialog: !this.state.isShowCommentDialog + }); + } + + getOriginRepoInfo = () => { + seafileAPI.getRepoInfo(draftRepoID).then((res) => { + this.setState({ + originRepoName: res.data.repo_name + }); + }); + } + + getChangedNodes = () => { + const nodes = this.refs.diffViewer.value.document.nodes; + let keys = []; + let lastDiffState = ''; + nodes.map((node) => { + if (node.data.get('diff_state') === 'diff-added' && lastDiffState !== 'diff-added') { + keys.push(node.key); + } else if (node.data.get('diff_state') === 'diff-removed' && lastDiffState !== 'diff-removed') { + keys.push(node.key); + } else if (node.data.get('diff_state') === 'diff-replaced' && lastDiffState !== 'diff-replaced') { + keys.push(node.key); + } + lastDiffState = node.data.get('diff_state'); + }); + 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]; + 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; + } + 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; + } + } + + 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; + } + + scrollToQuote = (newIndex, oldIndex, quote) => { + 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) => { + if (node.text.indexOf(quote) > 0) { + key = node.key; + } + }); + } + if (typeof(key) === 'string') { + const win = window; + let element = win.document.querySelector(`[data-key="${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; + } + } + } + showDiffViewer = () => { return (
@@ -100,11 +320,26 @@ class Draft extends React.Component { ref="diffViewer" /> } +
); } + listReviewers = () => { + seafileAPI.listDraftReviewers(draftID).then((res) => { + this.setState({ + reviewers: res.data.reviewers + }); + }); + } + onSwitchShowDiff = () => { + if (!this.state.isShowDiff) { + let that = this; + setTimeout(() => { + that.getChangedNodes(); + }, 100); + } this.setState({ isShowDiff: !this.state.isShowDiff, }); @@ -116,6 +351,14 @@ class Draft extends React.Component { }); } + toggleAddReviewerDialog = () => { + if (this.state.showReviewerDialog) { + this.listReviewers(); + } + this.setState({ + showReviewerDialog: !this.state.showReviewerDialog + }); + } showDiffButton = () => { return ( @@ -137,18 +380,70 @@ class Draft extends React.Component { const OriginFileLink = siteRoot + 'lib/' + draftRepoID + '/file' + draftOriginFilePath + '/'; seafileAPI.publishDraft(draftID).then(res => { window.location.href = OriginFileLink; - }) + }); + } + + initialDiffViewerContent = () => { + seafileAPI.listFileHistoryRecords(draftRepoID, 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(draftRepoID, res.data.data[0].commit_id, draftFilePath), + seafileAPI.getFileRevision(draftRepoID, 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(draftRepoID, res.data.data[0].commit_id, draftFilePath).then((res) => { + seafileAPI.getFileContent(res.data).then((content) => { + this.setState({ + draftContent: content.data, + draftOriginContent: '' + }); + }); + }); + } + }); + } + + setDiffViewerContent = (newContent, prevContent) => { + this.setState({ + draftContent: newContent, + draftOriginContent: prevContent + }); + } + + setURL = (newurl) => { + let url = new URL(window.location.href); + url.set('hash', newurl); + window.location.href = url.toString(); } tabItemClick = (tab) => { if (this.state.activeTab !== tab) { + if (tab !== 'history' && window.location.hash) { + this.setURL('#'); + } + if (tab == 'reviewInfo') { + this.initialContent(); + } + else if (tab == 'history') { + this.initialDiffViewerContent(); + } this.setState({ activeTab: tab }); } } - showNavItem = (showTab) => { switch(showTab) { case 'info': @@ -198,9 +493,174 @@ class Draft extends React.Component { ); } + setBtnPosition = (e) => { + 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.refs.diffViewer.value); + } + else { + this.range = findRange(nativeRange, this.refs.diffViewer.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 - 100 + this.refs.viewContent.scrollTop}px`; + } + else { + let style = this.refs.commentbtn.style; + style.top = '-1000px'; + } + } + + getQuote = () => { + let range = this.range; + if (!range) { + return; + } + this.quote = ''; + 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); + // 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 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'); + } + + 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; + } + + 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({ + rightPartWidth: rate + }); + }; + + componentWillMount() { + this.getCommentsNumber(); + this.listReviewers(); + this.getOriginRepoInfo(); + } + + componentDidMount() { + this.initialContent(); + document.addEventListener('selectionchange', this.setBtnPosition); + } + + componentWillUnmount() { + document.removeEventListener('selectionchange', this.setBtnPosition); + } + render() { + const onResizeMove = this.state.inResizing ? this.onResizeMouseMove : null; const draftLink = siteRoot + 'lib/' + draftRepoID + '/file' + draftFilePath + '?mode=edit'; - const OriginFileLink = siteRoot + 'lib/' + draftRepoID + '/file' + draftOriginFilePath + '/'; return(
- -
-
-
+
+
+
{this.state.isLoading ?
@@ -240,31 +699,213 @@ class Draft extends React.Component {
}
-
-
-
-
- {this.renderNavItems()} - - - review info - - - comments - - - history list - - +
+
+
+ {this.renderNavItems()} + + +
+ + + + {(this.state.isShowDiff === true && this.state.changedNodes.length > 0) && + + } + + +
+
+ + + + + + +
+
+ { this.state.showReviewerDialog && + + + + } + {this.state.isShowCommentDialog && + + + + }
); } } +class SidePanelReviewers extends React.Component { + + constructor(props) { + super(props); + } + + render() { + return ( +
+
{gettext('Reviewers')} + +
+ { this.props.reviewers.length > 0 ? + this.props.reviewers.map((item, index = 0, arr) => { + return ( +
+ + {item.user_name} +
+ ); + }) + : + {gettext('No reviewer yet.')} + } +
+ ); + } +} + +const sidePanelReviewersPropTypes = { + reviewers: PropTypes.array.isRequired, + toggleAddReviewerDialog: PropTypes.func.isRequired +}; + +SidePanelReviewers.propTypes = sidePanelReviewersPropTypes; + +class SidePanelAuthor extends React.Component { + render() { + return ( +
+
{gettext('Author')}
+
+ + {author} +
+
+ ); + } +} + +class SidePanelOrigin extends React.Component { + + constructor(props) { + super(props); + } + + render() { + return ( +
+ + + + + + + +
{gettext('Location')}{this.props.originRepoName}{draftOriginFilePath}
+
+ ); + } +} + +const SidePanelOriginPropTypes = { + originRepoName: PropTypes.string.isRequired +}; + +SidePanelOrigin.propTypes = SidePanelOriginPropTypes; + + +class UnresolvedComments extends React.Component { + + constructor(props) { + super(props); + } + + render() { + return ( +
+
{gettext('Comments')}
+
+ {gettext('Unresolved comments:')}{' '}{this.props.number} +
+
+ ); + } +} + +const UnresolvedCommentsPropTypes = { + number: PropTypes.number.isRequired +}; + +UnresolvedComments.propTypes = UnresolvedCommentsPropTypes; + + +class SidePanelChanges extends React.Component { + + constructor(props) { + super(props); + } + + render() { + return ( +
+
{gettext('Changes')}
+
+ {gettext('Number of changes:')}{' '}{this.props.changedNumber} + { this.props.changedNumber > 0 && +
+ { this.props.scrollToChangedNode('down');}}> + { this.props.scrollToChangedNode('up');}}> +
+ } +
+
+ ); + } +} + +const sidePanelChangesPropTypes = { + changedNumber: PropTypes.number.isRequired, + scrollToChangedNode: PropTypes.func.isRequired +}; + +SidePanelChanges.propTypes = sidePanelChangesPropTypes; + + ReactDOM.render ( , document.getElementById('wrapper') diff --git a/frontend/src/pages/review/history-list.js b/frontend/src/pages/review/history-list.js index f36a38e0ea..eb147e33c6 100644 --- a/frontend/src/pages/review/history-list.js +++ b/frontend/src/pages/review/history-list.js @@ -21,9 +21,9 @@ class HistoryList extends React.Component { }; } - onClick = (event, key, preCommitID, currentCommitID)=> { + onClick = (event, key, preItem, currentItem)=> { if (key === this.state.activeItem) return false; - this.props.onHistoryItemClick(currentCommitID, preCommitID, key); + this.props.onHistoryItemClick(currentItem, preItem, key); } onScroll = (event) => { @@ -51,7 +51,7 @@ class HistoryList extends React.Component { render() { return ( -
+
    { this.props.historyList ? @@ -65,11 +65,11 @@ class HistoryList extends React.Component { onClick={this.onClick} ctime={item.ctime} className={this.props.activeItem === index ? 'item-active': ''} - currentCommitId={item.commit_id} name={item.creator_name} index={index} key={index} - preCommitId={arr[preItemIndex].commit_id} + preItem={arr[preItemIndex]} + currentItem={item} /> ); }) : @@ -88,7 +88,7 @@ class HistoryItem extends React.Component { render() { let time = moment.parseZone(this.props.ctime).format('YYYY-MM-DD HH:mm'); return ( -
  • this.props.onClick(event, this.props.index, this.props.preCommitId, this.props.currentCommitId)} className={'history-list-item ' + this.props.className}> +
  • this.props.onClick(event, this.props.index, this.props.preItem, this.props.currentItem)} className={'history-list-item ' + this.props.className}>
    {time}
    {this.props.name}
    diff --git a/frontend/src/utils/constants.js b/frontend/src/utils/constants.js index 4c3500f183..2bded33e07 100644 --- a/frontend/src/utils/constants.js +++ b/frontend/src/utils/constants.js @@ -61,20 +61,14 @@ export const filePath = window.fileHistory ? window.fileHistory.pageOptions.file export const fileName = window.fileHistory ? window.fileHistory.pageOptions.fileName : ''; // Draft review -export const draftFilePath = window.draftReview ? window.draftReview.config.draftFilePath: ''; -export const draftOriginFilePath = window.draftReview ? window.draftReview.config.draftOriginFilePath: ''; -export const draftOriginRepoID = window.draftReview ? window.draftReview.config.draftOriginRepoID: ''; -export const draftFileName = window.draftReview ? window.draftReview.config.draftFileName: ''; -export const reviewID = window.draftReview ? window.draftReview.config.reviewID : ''; -export const draftID = window.draftReview ? window.draftReview.config.draftID : ''; -export const opStatus = window.draftReview ? window.draftReview.config.opStatus : ''; -export const reviewPerm = window.draftReview ? window.draftReview.config.perm : ''; -export const publishFileVersion = window.draftReview ? window.draftReview.config.publishFileVersion : ''; -export const originFileVersion = window.draftReview ? window.draftReview.config.originFileVersion : ''; -export const author = window.draftReview ? window.draftReview.config.author : ''; -export const authorAvatar = window.draftReview ? window.draftReview.config.authorAvatar : ''; -export const originFileExists = window.draftReview ? window.draftReview.config.originFileExists : ''; -export const draftFileExists = window.draftReview ? window.draftReview.config.draftFileExists : ''; +export const draftFilePath = window.draft ? window.draft.config.draftFilePath: ''; +export const draftOriginFilePath = window.draft ? window.draft.config.draftOriginFilePath: ''; +export const draftFileName = window.draft ? window.draft.config.draftFileName: ''; +export const draftID = window.draft ? window.draft.config.draftID : ''; +export const draftRepoID = window.draft ? window.draft.config.draftRepoID : ''; +export const author = window.draft ? window.draft.config.author : ''; +export const authorAvatar = window.draft ? window.draft.config.authorAvatar : ''; +export const originFileExists = window.draft ? window.draft.config.originFileExists : ''; // org admin export const orgID = window.org ? window.org.pageOptions.orgID : ''; diff --git a/seahub/api2/endpoints/draft_reviewer.py b/seahub/api2/endpoints/draft_reviewer.py index e78bc56595..4d55701f68 100644 --- a/seahub/api2/endpoints/draft_reviewer.py +++ b/seahub/api2/endpoints/draft_reviewer.py @@ -16,6 +16,7 @@ from seahub.api2.utils import api_error, user_to_dict from seahub.base.templatetags.seahub_tags import email2nickname from seahub.base.accounts import User +from seahub.tags.models import FileUUIDMap from seahub.views import check_folder_permission from seahub.utils import is_valid_username from seahub.drafts.models import Draft, DraftReviewer @@ -42,7 +43,7 @@ class DraftReviewerView(APIView): # get reviewer list reviewers = [] - for x in d.reviewreviewer_set.all(): + for x in d.draftreviewer_set.all(): reviewer = user_to_dict(x.reviewer, request=request, avatar_size=avatar_size) reviewers.append(reviewer) @@ -88,7 +89,7 @@ class DraftReviewerView(APIView): }) continue - uuid = d.origin_file_uuid + uuid = FileUUIDMap.objects.get_fileuuidmap_by_uuid(d.origin_file_uuid) origin_file_path = posixpath.join(uuid.parent_path, uuid.filename) # check perm if seafile_api.check_permission_by_path(d.origin_repo_id, origin_file_path, reviewer) != 'rw':