diff --git a/frontend/src/components/common/go-back/index.css b/frontend/src/components/common/go-back/index.css new file mode 100644 index 0000000000..e3a8c2f9cc --- /dev/null +++ b/frontend/src/components/common/go-back/index.css @@ -0,0 +1,9 @@ +.go-back { + margin-right: 0.75rem; + color: #c0c0c0; +} + +.go-back:hover { + color: #ff9933; + cursor: pointer; +} diff --git a/frontend/src/components/common/go-back/index.js b/frontend/src/components/common/go-back/index.js new file mode 100644 index 0000000000..93bb45e009 --- /dev/null +++ b/frontend/src/components/common/go-back/index.js @@ -0,0 +1,21 @@ +import React, { Component } from 'react'; + +import './index.css'; + +class GoBack extends Component { + + onBackClick = (event) => { + event.preventDefault(); + window.history.back(); + } + + render() { + return ( +
+ +
+ ); + } +} + +export default GoBack; diff --git a/frontend/src/components/common/switch/index.js b/frontend/src/components/common/switch/index.js new file mode 100644 index 0000000000..7628ba018e --- /dev/null +++ b/frontend/src/components/common/switch/index.js @@ -0,0 +1,37 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import classnames from 'classnames'; + +import '../../../css/switch.css'; + +function Switch(props) { + const { onChange, checked, placeholder, disabled, className, size } = props; + + return( +
+ +
+ ); +} + +Switch.propTypes = { + checked: PropTypes.bool, + disabled: PropTypes.bool, + placeholder: PropTypes.oneOfType([PropTypes.string, PropTypes.node]), + className: PropTypes.string, + size: PropTypes.oneOf(['large', 'small', undefined]), + onChange: PropTypes.func.isRequired, +}; + +export default Switch; diff --git a/frontend/src/components/history-list-view/history-list-item.js b/frontend/src/components/history-list-view/history-list-item.js index 2fc09a7ca3..976b4f372c 100644 --- a/frontend/src/components/history-list-view/history-list-item.js +++ b/frontend/src/components/history-list-view/history-list-item.js @@ -5,6 +5,8 @@ import { Dropdown, DropdownToggle, DropdownMenu, DropdownItem} from 'reactstrap' import { gettext, filePath } from '../../utils/constants'; import URLDecorator from '../../utils/url-decorator'; +import '../../css/history-record-item.css'; + moment.locale(window.app.config.lang); const propTypes = { diff --git a/frontend/src/css/file-history.css b/frontend/src/css/file-history.css index a9880391d1..e3c5f8f444 100644 --- a/frontend/src/css/file-history.css +++ b/frontend/src/css/file-history.css @@ -76,54 +76,6 @@ background-color: #ffe7d5; } -.item-active { - color: #fff; - background-color: #fdc297 !important; -} - -.item-active i { - color: #fff; -} - -.history-list-item .history-info { - flex: 1; - padding: 0 0.5rem; -} - -.history-list-item .history-operation { - width: 1.5rem; - display: flex; - align-items: center; - justify-content: center; -} - -.history-info .time { - color: #000; -} - -.history-info .owner { - margin-top: 0.25rem; - color: #888; - display: flex; - align-items: center; -} - -.owner .squire-icon { - width: 0.5rem; - height: 0.5rem; - background-color: #549b5a; - margin-right: 0.25rem; -} - -.history-body .dropdown-menu { - min-width: 8rem; -} - -.history-body .dropdown-menu a { - text-decoration: none; - color: #6e7687; -} - .history-content .main-panel { flex: 1 1 auto; } diff --git a/frontend/src/css/history-record-item.css b/frontend/src/css/history-record-item.css new file mode 100644 index 0000000000..64332da8bd --- /dev/null +++ b/frontend/src/css/history-record-item.css @@ -0,0 +1,47 @@ +.item-active { + color: #fff; + background-color: #fdc297 !important; +} + +.item-active i { + color: #fff; +} + +.history-list-item .history-info { + flex: 1; + padding: 0 0.5rem; +} + +.history-list-item .history-operation { + width: 1.5rem; + display: flex; + align-items: center; + justify-content: center; +} + +.history-info .time { + color: #000; +} + +.history-info .owner { + margin-top: 0.25rem; + color: #888; + display: flex; + align-items: center; +} + +.owner .squire-icon { + width: 0.5rem; + height: 0.5rem; + background-color: #549b5a; + margin-right: 0.25rem; +} + +.history-body .dropdown-menu { + min-width: 8rem; +} + +.history-body .dropdown-menu a { + text-decoration: none; + color: #6e7687; +} diff --git a/frontend/src/css/sdoc-file-history.css b/frontend/src/css/sdoc-file-history.css new file mode 100644 index 0000000000..32e408e1a7 --- /dev/null +++ b/frontend/src/css/sdoc-file-history.css @@ -0,0 +1,206 @@ +.sdoc-file-history .sdoc-file-history-container { + flex: 1; + overflow-x: hidden; +} + +.sdoc-file-history .sdoc-file-history-header { + height: 50px; + border-bottom: 1px solid #e5e5e5; + background-color: #f4f4f7; +} + +.sdoc-file-history .sdoc-file-history-header .sdoc-file-history-header-left { + font-size: 1.25rem; + flex: 1; +} + +.sdoc-file-history .sdoc-file-history-header .file-name { + flex: 1; +} + +.sdoc-file-history .sdoc-file-history-header .sdoc-file-history-header-right { + height: 100%; + min-width: 100px; +} + +.sdoc-file-history .sdoc-file-history-header .sdoc-file-changes-container { + height: 32px; + border: 1px solid #e5e5e5; + border-radius: 3px; +} + +.sdoc-file-history .sdoc-file-history-header .sdoc-file-changes-divider { + border-right: 1px solid #e5e5e5; + height: 100%; + width: 1px; +} + +.sdoc-file-history .sdoc-file-history-header .sdoc-file-changes-last, +.sdoc-file-history .sdoc-file-history-header .sdoc-file-changes-next { + padding: 0 8px; + color: #000; + opacity: .5; + height: 100%; +} + +.sdoc-file-history .sdoc-file-history-header .sdoc-file-changes-last:hover, +.sdoc-file-history .sdoc-file-history-header .sdoc-file-changes-next:hover { + cursor: pointer; + opacity: .75; +} + +.sdoc-file-history .sdoc-file-history-content { + flex: 1; + min-height: 0; + padding: 20px 40px; + background-color: #fafaf9; + overflow-y: scroll; +} + +.sdoc-file-history .sdoc-file-history-content .sdoc-file-history-viewer { + width: 100%; + min-height: 120px; + flex: 1; + background-color: #fff; + word-break: break-word; + border: 1px solid #e6e6dd; +} + +.sdoc-file-history .sdoc-file-history-content .sdoc-editor-content { + background-color: #fff; +} + +.sdoc-file-history .sdoc-file-history-content .article { + width: 100%; + margin: 0; +} + +/* panel */ +.sdoc-file-history .sdoc-file-history-panel { + width: 260px; + border-left: 1px solid #e5e5e5; +} + +.sdoc-file-history .sdoc-file-history-panel .sdoc-file-history-select-range { + padding: 10px 18px; + height: 50px;; + border-bottom: 1px solid #e5e5e5; + background-color: rgb(250, 250, 249); +} + +.sdoc-file-history .sdoc-file-history-panel .sdoc-file-history-select-range-title { + height: 100%; + width: 100%; + font-size: 1rem; + font-weight: bolder; + line-height: 29px; +} + +.sdoc-file-history .sdoc-file-history-panel .sdoc-file-history-diff-switch { + padding: 0 18px; + height: 50px;; + border-top: 1px solid #e5e5e5; +} + +.sdoc-file-history .sdoc-file-history-diff-switch .custom-switch { + width: 100%; + padding-left: 0; + display: flex; + align-items: center; + justify-content: space-between; + cursor: pointer; +} + +.sdoc-file-history .sdoc-file-history-diff-switch .custom-switch-description { + margin-left: 0; + flex: 1; + padding-right: 8px; +} + +/* history versions */ +.sdoc-file-history-versions { + flex: 1; + flex-direction: column; + min-height: 0; + overflow: auto; +} + +.sdoc-file-history-versions .history-list-item { + padding: 5px 10px; + display: flex; + flex: 1; + border-bottom: 1px solid #e5e5e5; +} + +.sdoc-file-history-versions .history-list-item:last-child { + border-bottom: none; +} + +.sdoc-file-history-versions .history-list-item:hover { + background-color: #ffe7d5; + cursor: pointer; +} + +.history-info .owner { + min-height: 22.5px; +} + +.sdoc-file-history-versions .dropdown-menu { + min-width: 8rem; +} + +.sdoc-file-history-versions .dropdown-menu a { + text-decoration: none; + color: #6e7687; +} + +.history-content .main-panel { + flex: 1 1 auto; +} + +.history-content .history-side-panel { + flex: 0 0 auto; + user-select: none; + border-left: 1px solid #e5e5e5; + background-color: #fff; + display: flex; + flex-direction: column; +} + +.history-content .history-side-panel .history-side-panel-title { + height: 50px; + border-bottom: 1px solid #e5e5e5; + line-height: 50px; + font-size: 1rem; + padding: 0 10px; + background-color: rgb(250,250,249); + font-weight: bolder; +} + +@media (min-width:992px) { + + .history-side-panel { + width: 260px; + } + +} + +@media (max-width:768px) { + + .sdoc-file-history .sdoc-file-history-content { + padding: 0; + } + + .sdoc-file-history .sdoc-file-history-content .sdoc-file-history-viewer { + border: none; + } + + .markdown-viewer-render-content { + margin: 20px; + } + + .markdown-viewer-render-content .diff-view { + padding: 20px; + } + +} diff --git a/frontend/src/css/switch.css b/frontend/src/css/switch.css new file mode 100644 index 0000000000..8b8e83852e --- /dev/null +++ b/frontend/src/css/switch.css @@ -0,0 +1,14 @@ +.seahub-switch.small .custom-switch-indicator { + width: 22px; + height: 12px; + border-radius: 6px; +} + +.seahub-switch.small .custom-switch-indicator:before { + height: 8px; + width: 8px; +} + +.seahub-switch.small .custom-switch-input:checked~.custom-switch-indicator:before { + left: 12px; +} diff --git a/frontend/src/models/file-history.js b/frontend/src/models/file-history.js new file mode 100644 index 0000000000..ed9206bf27 --- /dev/null +++ b/frontend/src/models/file-history.js @@ -0,0 +1,17 @@ +import moment from 'moment'; +moment.locale(window.app.config.lang); + +export default class FileHistory { + + constructor(object) { + this.commitId = object.commit_id || undefined; + this.ctime = object.ctime ? moment(object.ctime).format('YYYY-MM-DD HH:mm') : ''; + this.creatorName = object.creator_name || ''; + this.size = object.size || 0; + this.revRenamedOldPath = object.rev_renamed_old_path || ''; + this.revFileId = object.rev_file_id || ''; + this.path = object.path || ''; + this.description = object.description || ''; + } + +} diff --git a/frontend/src/pages/sdoc-file-history/history-version.js b/frontend/src/pages/sdoc-file-history/history-version.js new file mode 100644 index 0000000000..fb89ca4cfe --- /dev/null +++ b/frontend/src/pages/sdoc-file-history/history-version.js @@ -0,0 +1,99 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Dropdown, DropdownToggle, DropdownMenu, DropdownItem} from 'reactstrap'; +import { gettext, filePath } from '../../utils/constants'; +import URLDecorator from '../../utils/url-decorator'; + +import '../../css/history-record-item.css'; + +class HistoryVersion extends React.Component { + + constructor(props) { + super(props); + this.state = { + isShowOperationIcon: false, + isMenuShow: false, + }; + } + + onMouseEnter = () => { + const { currentVersion, historyVersion } = this.props; + if (currentVersion.commitId === historyVersion.commitId) return; + this.setState({ isShowOperationIcon: true }); + } + + onMouseLeave = () => { + const { currentVersion, historyVersion } = this.props; + if (currentVersion.commitId === historyVersion.commitId) return; + this.setState({ isShowOperationIcon: false }); + } + + onToggleClick = (e) => { + this.setState({ isMenuShow: !this.state.isMenuShow }); + } + + onClick = () => { + this.setState({ isShowOperationIcon: false }); + const { currentVersion, historyVersion } = this.props; + if (currentVersion.commitId === historyVersion.commitId) return; + this.props.onSelectHistoryVersion(historyVersion); + } + + onRestore = () => { + const { historyVersion } = this.props; + this.props.onRestore(historyVersion); + } + + onItemDownload = () => { + // nothing todo + } + + render() { + const { currentVersion, historyVersion } = this.props; + if (!currentVersion || !historyVersion) return null; + const { ctime, commitId, creatorName, revFileId } = historyVersion; + const isHighlightItem = commitId === currentVersion.commitId; + const url = URLDecorator.getUrl({ type: 'download_historic_file', filePath: filePath, objID: revFileId }); + return ( +
  • +
    +
    {ctime}
    +
    + + {creatorName} +
    +
    +
    + + + + {(this.props.index !== 0) && {gettext('Restore')}} + {gettext('Download')} + + +
    +
  • + ); + } +} + +HistoryVersion.propTypes = { + index: PropTypes.number, + currentVersion: PropTypes.object.isRequired, + historyVersion: PropTypes.object, + onSelectHistoryVersion: PropTypes.func.isRequired, + onRestore: PropTypes.func.isRequired, +}; + +export default HistoryVersion; diff --git a/frontend/src/pages/sdoc-file-history/index.js b/frontend/src/pages/sdoc-file-history/index.js index 1454b25c22..bd9e00da9e 100644 --- a/frontend/src/pages/sdoc-file-history/index.js +++ b/frontend/src/pages/sdoc-file-history/index.js @@ -1,259 +1,173 @@ -import React, { Fragment } from 'react'; +import React from 'react'; import ReactDom from 'react-dom'; -import { Button } from 'reactstrap'; -import { Utils } from '../../utils/utils'; +import { UncontrolledTooltip } from 'reactstrap'; +import classnames from 'classnames'; +import DiffViewer from '@seafile/sdoc-editor/dist/pages/diff-viewer'; import { seafileAPI } from '../../utils/seafile-api'; -import { gettext, PER_PAGE, filePath, fileName, historyRepoID, canDownload, canCompare } from '../../utils/constants'; -import editUtilities from '../../utils/editor-utilities'; +import { gettext, fileName, historyRepoID } from '../../utils/constants'; import Loading from '../../components/loading'; -import Logo from '../../components/logo'; -import CommonToolbar from '../../components/toolbar/common-toolbar'; -import HistoryItem from '../file-history-old/history-item'; +import GoBack from '../../components/common/go-back'; +import SidePanel from './side-panel'; +import { Utils } from '../../utils/utils'; +import toaster from '../../components/toast'; import '../../css/layout.css'; -import '../../css/toolbar.css'; -import '../../css/search.css'; -import '../../css/file-history-old.css'; +import '../../css/sdoc-file-history.css'; class SdocFileHistory extends React.Component { constructor(props) { super(props); + const isShowChanges = localStorage.getItem('seahub-sdoc-history-show-changes') === 'false' ? false : true; this.state = { - historyList: [], - currentPage: 1, - hasMore: false, - nextCommit: undefined, - filePath: '', - oldFilePath: '', isLoading: true, - isReloadingData: false, + isShowChanges, + currentVersion: {}, + currentVersionContent: '', + lastVersionContent: '', + changes: [], + currentDiffIndex: 0, }; } - componentDidMount() { - this.listOldHistoryRecords(historyRepoID, filePath); - } - - listNewHistoryRecords = (filePath, PER_PAGE) => { - editUtilities.listFileHistoryRecords(filePath, 1, PER_PAGE).then(res => { - let historyData = res.data; - if (!historyData) { - this.setState({isLoading: false}); - throw Error('There is an error in server.'); - } - this.initNewRecords(res.data); - }); - } - - listOldHistoryRecords = (repoID, filePath) => { - seafileAPI.listOldFileHistoryRecords(repoID, filePath).then((res) => { - let historyData = res.data; - if (!historyData) { - this.setState({isLoading: false}); - throw Error('There is an error in server.'); - } - this.initOldRecords(res.data); - }); - } - - initNewRecords(result) { - if (result.total_count < 5) { - if (result.data.length) { - let commitID = result.data[result.data.length-1].commit_id; - let path = result.data[result.data.length-1].path; - let oldPath = result.data[result.data.length-1].old_path; - path = oldPath ? oldPath : path; - seafileAPI.listOldFileHistoryRecords(historyRepoID, path, commitID).then((res) => { - if (!res.data) { - this.setState({isLoading: false}); - throw Error('There is an error in server.'); - } - this.setState({ - historyList: result.data.concat(res.data.data.slice(1, res.data.data.length)), - isLoading: false, - }); + onSelectHistoryVersion = (currentVersion, lastVersion) => { + this.setState({ isLoading: true, currentVersion }); + seafileAPI.getFileRevision(historyRepoID, currentVersion.commitId, currentVersion.path).then(res => { + return seafileAPI.getFileContent(res.data); + }).then(res => { + const currentVersionContent = res.data; + if (lastVersion) { + seafileAPI.getFileRevision(historyRepoID, lastVersion.commitId, lastVersion.path).then(res => { + return seafileAPI.getFileContent(res.data); + }).then(res => { + const lastVersionContent = res.data; + this.setContent(currentVersionContent, lastVersionContent); + }).catch(error => { + const errorMessage = Utils.getErrorMsg(error, true); + toaster.danger(gettext(errorMessage)); + this.setContent(currentVersionContent, ''); }); } else { - seafileAPI.listOldFileHistoryRecords(historyRepoID, filePath).then((res) => { - if (!res.data) { - this.setState({isLoading: false}); - throw Error('There is an error in server.'); - } - this.setState({ - historyList: res.data.data, - isLoading: false, - }); - }); + this.setContent(currentVersionContent, ''); } - } else { - this.setState({ - historyList: result.data, - currentPage: result.page, - hasMore: result.total_count > (PER_PAGE * this.state.currentPage), - isLoading: false, - }); - } - } - - initOldRecords(result) { - if (result.data.length) { - this.setState({ - historyList: result.data, - nextCommit: result.next_start_commit, - filePath: result.data[result.data.length-1].path, - oldFilePath: result.data[result.data.length-1].rev_renamed_old_path, - isLoading: false, - }); - } else { - this.setState({nextCommit: result.next_start_commit,}); - if (this.state.nextCommit) { - seafileAPI.listOldFileHistoryRecords(historyRepoID, filePath, this.state.nextCommit).then((res) => { - this.initOldRecords(res.data); - }); - } else { - this.setState({isLoading: false}); - } - } - } - - onScrollHandler = (event) => { - const clientHeight = event.target.clientHeight; - const scrollHeight = event.target.scrollHeight; - const scrollTop = event.target.scrollTop; - const isBottom = (clientHeight + scrollTop + 1 >= scrollHeight); - let hasMore = this.state.hasMore; - if (isBottom && hasMore) { - this.reloadMore(); - } - } - - reloadMore = () => { - if (!this.state.isReloadingData) { - const commitID = this.state.nextCommit; - const filePath = this.state.filePath; - const oldFilePath = this.state.oldFilePath; - this.setState({ isReloadingData: true }); - if (oldFilePath) { - seafileAPI.listOldFileHistoryRecords(historyRepoID, oldFilePath, commitID).then((res) => { - this.updateOldRecords(res.data, oldFilePath); - }); - } else { - seafileAPI.listOldFileHistoryRecords(historyRepoID, filePath, commitID).then((res) => { - this.updateOldRecords(res.data, filePath); - }); - } - } - } - - updateNewRecords(result) { - this.setState({ - historyList: [...this.state.historyList, ...result.data], - currentPage: result.page, - hasMore: result.total_count > (PER_PAGE * this.state.currentPage), - isReloadingData: false, + }).catch(error => { + const errorMessage = Utils.getErrorMsg(error, true); + toaster.danger(gettext(errorMessage)); + this.setContent('', ''); }); } - updateOldRecords(result, filePath) { - if (result.data.length) { - this.setState({ - historyList: [...this.state.historyList, ...result.data], - nextCommit: result.next_start_commit, - filePath: result.data[result.data.length-1].path, - oldFilePath: result.data[result.data.length-1].rev_renamed_old_path, - isReloadingData: false, - }); - } else { - this.setState({nextCommit: result.next_start_commit,}); - if (this.state.nextCommit) { - seafileAPI.listOldFileHistoryRecords(historyRepoID, filePath, this.state.nextCommit).then((res) => { - this.updateOldRecords(res.data, filePath); - }); - } - } + setContent = (currentVersionContent = '', lastVersionContent = '') => { + this.setState({ currentVersionContent, lastVersionContent, isLoading: false, changes: [], currentDiffIndex: 0 }); } - onItemRestore = (item) => { - let commitId = item.commit_id; - let filePath = item.path; - editUtilities.revertFile(filePath, commitId).then(res => { - if (res.data.success) { - this.setState({ isLoading: true }); - this.refreshFileList(); + onShowChanges = (isShowChanges) => { + this.setState({ isShowChanges }, () => { + localStorage.setItem('seahub-sdoc-history-show-changes', isShowChanges + ''); + }); + } + + setDiffCount = (diff = { value: [], changes: [] }) => { + const { changes } = diff; + this.setState({ changes, currentDiffIndex: 0 }); + } + + jumpToElement = (currentDiffIndex) => { + this.setState({ currentDiffIndex }, () => { + const { currentDiffIndex, changes } = this.state; + const change = changes[currentDiffIndex]; + const changeElement = document.querySelectorAll(`[data-id=${change}]`)[0]; + if (changeElement) { + this.historyContentRef.scrollTop = changeElement.offsetTop - 10; } }); } - refreshFileList() { - seafileAPI.listOldFileHistoryRecords(historyRepoID, filePath).then((res) => { - this.initOldRecords(res.data); - }); + lastChange = () => { + const { currentDiffIndex, changes } = this.state; + if (currentDiffIndex === 0) { + this.jumpToElement(changes.length - 1); + return; + } + this.jumpToElement(currentDiffIndex - 1); } - onSearchedClick = (searchedItem) => { - Utils.handleSearchedItemClick(searchedItem); - } - - onBackClick = (event) => { - event.preventDefault(); - window.history.back(); + nextChange = () => { + const { currentDiffIndex, changes } = this.state; + if (currentDiffIndex === changes.length - 1) { + this.jumpToElement(0); + return; + } + this.jumpToElement(currentDiffIndex + 1); } render() { + const { currentVersion, isShowChanges, currentVersionContent, lastVersionContent, isLoading, changes, currentDiffIndex } = this.state; + const changesCount = changes ? changes.length : 0; + const isShowChangesTips = isShowChanges && changesCount > 0; + return ( - -