import React from 'react'; import ReactDOM from 'react-dom'; import PropTypes from 'prop-types'; /* eslint-disable */ import Prism from 'prismjs'; /* eslint-enable */ import { siteRoot, gettext, reviewID, draftOriginFilePath, draftFilePath, draftOriginRepoID, draftFileName, opStatus, publishFileVersion, originFileVersion, author, authorAvatar } 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'; import AddReviewerDialog from './components/dialog/add-reviewer-dialog.js'; import { findRange } from '@seafile/slate-react'; 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 './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'; import './css/draft-review.css'; 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, commentsNumber: null, inResizing: false, commentWidth: 30, isShowDiff: true, showDiffTip: false, showReviewerDialog: false, reviewers: [], activeTab: 'reviewInfo', historyList: [], totalReversionCount: 0, changedNodes: [], isShowCommentDialog: false, }; this.quote = ''; this.newIndex = null; this.oldIndex = null; this.changeIndex = -1; this.range = null; } componentDidMount() { this.initialContent(); document.addEventListener('selectionchange', this.setBtnPosition); let that = this; setTimeout(() => { that.getChangedNodes(); }, 1000); } initialContent = () => { if (publishFileVersion == 'None') { 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 }); })); })); } else { 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) ]).then(axios.spread((draftContent, draftOriginContent) => { this.setState({ draftContent: draftContent.data, draftOriginContent: draftOriginContent.data, isLoading: false, }); })); } } componentWillUnmount() { document.removeEventListener('selectionchange', this.setBtnPosition); } onCloseReview = () => { seafileAPI.updateReviewStatus(reviewID, 'closed').then(res => { this.setState({reviewStatus: 'closed'}); let msg_s = gettext('Successfully closed review %(reviewID)s.'); msg_s = msg_s.replace('%(reviewID)s', reviewID); toaster.success(msg_s); }).catch(() => { let msg_s = gettext('Failed to close review %(reviewID)s'); msg_s = msg_s.replace('%(reviewID)s', reviewID); toaster.danger(msg_s); }); } onPublishReview = () => { seafileAPI.updateReviewStatus(reviewID, 'finished').then(res => { this.setState({reviewStatus: 'finished', activeTab: 'reviewInfo' }); let msg_s = gettext('Successfully published draft.'); toaster.success(msg_s); }).catch(() => { let msg_s = gettext('Failed to publish draft.'); toaster.danger(msg_s); }); } toggleCommentDialog = () => { this.setState({ isShowCommentDialog: !this.state.isShowCommentDialog }); } getCommentsNumber = () => { seafileAPI.listReviewComments(reviewID).then((res) => { let number = res.data.total_count; this.setState({ commentsNumber: number, }); }); } 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 }); }; onSwitchShowDiff = () => { if (!this.state.isShowDiff) { let that = this; setTimeout(() => { that.getChangedNodes(); }, 100); } this.setState({ isShowDiff: !this.state.isShowDiff, }); } toggleDiffTip = () => { this.setState({ showDiffTip: !this.state.showDiffTip }); } toggleAddReviewerDialog = () => { if (this.state.showReviewerDialog) { this.listReviewers(); } this.setState({ showReviewerDialog: !this.state.showReviewerDialog }); } listReviewers = () => { seafileAPI.listReviewers(reviewID).then((res) => { this.setState({ reviewers: res.data.reviewers }); }); } setBtnPosition = (e) => { const nativeSelection = window.getSelection(); if (!nativeSelection.rangeCount) { this.range = null; return; } if (nativeSelection.isCollapsed === false) { const nativeRange = nativeSelection.getRangeAt(0); let range = findRange(nativeRange, this.refs.diffViewer.value); if (!range) { this.range = null; return; } this.range = range; 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 = () => { this.quote = ''; let range = this.range; if (!range) { return; } 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); if (anchorBlock && anchor.offset == 0 && focusBlock && focus.offset != 0) { range = range.setFocus(focus.setOffset(0)); } // 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; } addComment = (e) => { e.stopPropagation(); this.getQuote(); if (!this.quote) { return; } this.setState({ isShowCommentDialog: true }); } 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; const element = win.document.querySelector(`[data-key="${key}"]`); 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; } } } tabItemClick = (tab) => { if (this.state.activeTab !== tab) { if (tab == 'reviewInfo') { this.initialContent(); } else if (tab == 'history'){ this.initialDiffViewerContent(); } this.setState({ activeTab: tab }); } } 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); } 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]; const element = win.document.querySelector(`[data-key="${key}"]`); 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; } } componentWillMount() { this.getCommentsNumber(); this.listReviewers(); } 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: '' }); }); }); } }); } setDiffViewerContent = (newContent, prevContent) => { this.setState({ draftContent: newContent, draftOriginContent: prevContent }); } onCommentAdded = () => { this.getCommentsNumber(); this.toggleCommentDialog(); } render() { const onResizeMove = this.state.inResizing ? this.onResizeMouseMove : null; const draftLink = siteRoot + 'lib/' + draftOriginRepoID + '/file' + draftFilePath + '?mode=edit'; const OriginFileLink = siteRoot + 'lib/' + draftOriginRepoID + '/file' + draftOriginFilePath + '/'; return(