diff --git a/frontend/config/webpack.config.dev.js b/frontend/config/webpack.config.dev.js index 4f292b0fd2..f4b090b052 100644 --- a/frontend/config/webpack.config.dev.js +++ b/frontend/config/webpack.config.dev.js @@ -69,6 +69,11 @@ module.exports = { require.resolve('react-dev-utils/webpackHotDevClient'), paths.appSrc + "/file-history.js", ], + fileHistoryOld: [ + require.resolve('./polyfills'), + require.resolve('react-dev-utils/webpackHotDevClient'), + paths.appSrc + "/file-history-old.js", + ], app: [ require.resolve('./polyfills'), require.resolve('react-dev-utils/webpackHotDevClient'), diff --git a/frontend/config/webpack.config.prod.js b/frontend/config/webpack.config.prod.js index ba1514849f..6fbe09b931 100644 --- a/frontend/config/webpack.config.prod.js +++ b/frontend/config/webpack.config.prod.js @@ -62,6 +62,7 @@ module.exports = { wiki: [require.resolve('./polyfills'), paths.appSrc + "/wiki.js"], repoview: [require.resolve('./polyfills'), paths.appSrc + "/repo-wiki-mode.js"], fileHistory: [require.resolve('./polyfills'), paths.appSrc + "/file-history.js"], + fileHistoryOld: [require.resolve('./polyfills'), paths.appSrc + "/file-history-old.js"], app: [require.resolve('./polyfills'), paths.appSrc + "/app.js"], draft: [require.resolve('./polyfills'), paths.appSrc + "/draft.js"], draw: [require.resolve('./polyfills'), paths.appSrc + "/draw/draw.js"], diff --git a/frontend/src/components/file-view/file-toolbar.js b/frontend/src/components/file-view/file-toolbar.js index b74cb146d4..d035b1e76d 100644 --- a/frontend/src/components/file-view/file-toolbar.js +++ b/frontend/src/components/file-view/file-toolbar.js @@ -100,7 +100,7 @@ class FileToolbar extends React.Component { icon="fa fa-history" text={gettext('History')} tag="a" - href={`${siteRoot}repo/file_revisions/${repoID}/?p=${encodeURIComponent(filePath)}&referer=${encodeURIComponent(location.href)}`} + href={`${siteRoot}repo/file_revisions/${repoID}/?p=${encodeURIComponent(filePath)}`} /> )} {(canEditFile && !err) && diff --git a/frontend/src/components/logo.js b/frontend/src/components/logo.js index f623b8a8da..e027732ef4 100644 --- a/frontend/src/components/logo.js +++ b/frontend/src/components/logo.js @@ -3,7 +3,8 @@ import PropTypes from 'prop-types'; import { siteRoot, mediaUrl, logoPath, logoWidth, logoHeight, siteTitle } from '../utils/constants'; const propTypes = { - onCloseSidePanel: PropTypes.func.isRequired, + onCloseSidePanel: PropTypes.func, + showCloseSidePanelIcon: PropTypes.bool, }; class Logo extends React.Component { @@ -18,13 +19,15 @@ class Logo extends React.Component { - - + {this.props.showCloseSidePanelIcon && + + + } ); } diff --git a/frontend/src/css/file-history-old.css b/frontend/src/css/file-history-old.css new file mode 100644 index 0000000000..bbf5e33745 --- /dev/null +++ b/frontend/src/css/file-history-old.css @@ -0,0 +1,97 @@ +.old-history-header { + display: flex; + justify-content: space-between; + align-items: center; + border-bottom: 1px solid #e8e8e8; + background-color: #f4f4f7; + font-size: 1rem; + padding: 0.5rem 1rem; +} + +.old-history-main { + padding: 16px 10% 0; + display: inline !important; + overflow: auto; + position: relative; + min-height: 1px; +} + +.old-history-main .go-back { + font-size: 25px; + color: #ccc; + float: left; + margin-left: -3rem; +} + +.old-history-main .go-back:hover { + color:#ff9933; + text-decoration: none; +} + +.old-history-main .get-more-btn { + width: 100%; + padding: .5em 0; + background: #efefef; + border: 0; + color: #777; + border-radius: 2px; +} +.old-history-main .get-more-btn:hover { + color: #444; +} + +.old-history-main p { + color: #808080; + font-size: 12px; + margin-top: 0; + margin-bottom: 1rem; +} + +.old-history-main h2 { + font-size: 1.5em; + color: #222; + font-weight: bold; + line-height: 1.5; +} + +.old-history-main .file-name { + color: #ee8204; + word-wrap: break-word; +} + +.old-history-main .commit-list { + width: 100%; + margin: 8px 0 40px; +} + +.old-history-main .commit-list .avatar { + width: 16px; + height: 16px; + border-radius: 2px; +} + +.old-history-main .commit-list .username { + vertical-align: middle; + color: #eb8205; + text-decoration: none; + font-weight: bold; +} + +.old-history-more-operation i { + color: #999; + cursor: pointer; +} + +.old-history-more-operation a:hover { + text-decoration: none; +} + +.old-history-more-operation i:hover { + color: #666; +} + +@media (max-width: 768px) { + .old-history-main .go-back { + margin-left: -2rem; + } +} \ No newline at end of file diff --git a/frontend/src/file-history-old.js b/frontend/src/file-history-old.js new file mode 100644 index 0000000000..e769364dcd --- /dev/null +++ b/frontend/src/file-history-old.js @@ -0,0 +1,255 @@ +import React, { Fragment } from 'react'; +import ReactDOM from 'react-dom'; +import { Button } from 'reactstrap'; +import { Utils } from './utils/utils'; +import { seafileAPI } from './utils/seafile-api'; +import { siteRoot, gettext, PER_PAGE, filePath, fileName, historyRepoID, useNewAPI, canDownload, canCompare } from './utils/constants'; +import editUtilties from './utils/editor-utilties'; +import Loading from './components/loading'; +import Logo from './components/logo'; +import CommonToolbar from './components/toolbar/common-toolbar'; +import HistoryItem from './pages/file-history-old/history-item'; + +import './assets/css/fa-solid.css'; +import './assets/css/fa-regular.css'; +import './assets/css/fontawesome.css'; +import './css/layout.css'; +import './css/toolbar.css'; +import './css/search.css'; +import './css/file-history-old.css'; + +class FileHistory extends React.Component { + + constructor(props) { + super(props); + this.state = { + historyList: [], + currentPage: 1, + hasMore: false, + nextCommit: undefined, + filePath: '', + oldFilePath: '', + isLoading: true, + isReloadingData: false, + }; + } + + componentDidMount() { + if (useNewAPI) { + this.listNewHistoryRecords(filePath, PER_PAGE); + } else { + this.listOldHistoryRecords(historyRepoID, filePath); + } + } + + listNewHistoryRecords = (filePath, PER_PAGE) => { + editUtilties.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) { + 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); + }); + } + } + } + + 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) { + if (useNewAPI) { + let currentPage = this.state.currentPage + 1; + this.setState({ + currentPage: currentPage, + isReloadingData: true, + }); + editUtilties.listFileHistoryRecords(filePath, currentPage, PER_PAGE).then(res => { + this.updateNewRecords(res.data); + this.setState({isReloadingData: false}); + }); + } else { + let commitID = this.state.nextCommit; + let filePath = this.state.filePath; + let oldFilePath = this.state.oldFilePath; + this.setState({isReloadingData: true}); + if (oldFilePath) { + seafileAPI.listOldFileHistoryRecords(historyRepoID, oldFilePath, commitID).then((res) => { + this.updateOldRecords(res.data); + this.setState({isReloadingData: false}); + }); + } else { + seafileAPI.listOldFileHistoryRecords(historyRepoID, filePath, commitID).then((res) => { + this.updateOldRecords(res.data); + this.setState({isReloadingData: false}); + }); + } + } + } + } + + updateNewRecords(result) { + this.setState({ + historyList: [...this.state.historyList, ...result.data], + currentPage: result.page, + hasMore: result.total_count > (PER_PAGE * this.state.currentPage), + isLoading: false, + }); + } + + updateOldRecords(result) { + 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, + isLoading: 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); + }); + } + } + } + + onItemRestore = (item) => { + let commitId = item.commit_id; + let filePath = item.path; + editUtilties.revertFile(filePath, commitId).then(res => { + if (res.data.success) { + this.setState({isLoading: true}); + this.refershFileList(); + } + }); + } + + refershFileList() { + if (useNewAPI) { + editUtilties.listFileHistoryRecords(filePath, 1, PER_PAGE).then((res) => { + this.initNewRecords(res.data); + }); + } else { + seafileAPI.listOldFileHistoryRecords(historyRepoID, filePath).then((res) => { + this.initOldRecords(res.data); + }); + } + } + + onSearchedClick = (searchedItem) => { + Utils.handleSearchedItemClick(searchedItem); + } + + render() { + return ( + + +
+
+
+ + + +

{fileName}{' '}{gettext('History Versions')}

+
+
+ {this.state.isLoading && } + {!this.state.isLoading && + + + + + + + + + + + {this.state.historyList.map((item, index) => { + return ( + + ); + })} + +
{gettext('Time')}{gettext('Modifier')}{gettext('Size')}
+ } + {this.state.isReloadingData && } + {this.state.nextCommit && !this.state.isLoading && !this.state.isReloadingData && + + } +
+
+
+
+ ); + } +} + +ReactDOM.render ( + , + document.getElementById('wrapper') +); diff --git a/frontend/src/file-history.js b/frontend/src/file-history.js index 176b9ba548..4374468357 100644 --- a/frontend/src/file-history.js +++ b/frontend/src/file-history.js @@ -27,16 +27,8 @@ class FileHistory extends React.Component { }; } - onSearchedClick = (selectedItem) => { - if (selectedItem.is_dir === true) { - let url = siteRoot + 'library/' + selectedItem.repo_id + '/' + selectedItem.repo_name + selectedItem.path; - let newWindow = window.open('about:blank'); - newWindow.location.href = url; - } else { - let url = siteRoot + 'lib/' + selectedItem.repo_id + '/file' + Utils.encodePath(selectedItem.path); - let newWindow = window.open('about:blank'); - newWindow.location.href = url; - } + onSearchedClick = (searchedItem) => { + Utils.handleSearchedItemClick(searchedItem); } setDiffContent = (newMarkdownContent, oldMarkdownContent)=> { diff --git a/frontend/src/pages/file-history-old/history-item.js b/frontend/src/pages/file-history-old/history-item.js new file mode 100644 index 0000000000..2c6fe14222 --- /dev/null +++ b/frontend/src/pages/file-history-old/history-item.js @@ -0,0 +1,133 @@ +import React, { Fragment } from 'react'; +import PropTypes from 'prop-types'; +import moment from 'moment'; +import { Utils } from '../../utils/utils'; +import { gettext, siteRoot, filePath, historyRepoID } from '../../utils/constants'; +import URLDecorator from '../../utils/url-decorator'; +import { Dropdown, DropdownToggle, DropdownMenu, DropdownItem } from 'reactstrap'; + +moment.locale(window.app.config.lang); + +const propTypes = { + item: PropTypes.object.isRequired, + index: PropTypes.number.isRequired, + canDownload: PropTypes.bool.isRequired, + canCompare: PropTypes.bool.isRequired, + onItemRestore: PropTypes.func.isRequired, +}; + +class HistoryItem extends React.Component { + + constructor(props) { + super(props); + this.state = { + active: false, + }; + } + + onMouseEnter = () => { + this.setState({ + active: true + }); + } + + onMouseLeave = () => { + this.setState({ + active: false + }); + } + + onItemRestore = (e) => { + e.preventDefault(); + this.props.onItemRestore(this.props.item); + } + + render() { + let item = this.props.item; + let downloadUrl = URLDecorator.getUrl({type: 'download_historic_file', filePath: filePath, objID: item.rev_file_id}); + let userProfileURL = `${siteRoot}profile/${encodeURIComponent(item.creator_email)}/`; + let viewUrl = `${siteRoot}repo/${historyRepoID}/history/files/?obj_id=${item.rev_file_id}&commit_id=${item.commit_id}&p=${filePath}`; + let diffUrl = `${siteRoot}repo/text_diff/${historyRepoID}/?commit=${item.commit_id}&p=${filePath}`; + return ( + + + + + {this.props.index === 0 && gettext('(current version)')} + + + {' '} + {item.creator_name} + + {Utils.bytesToSize(item.size)} + + {this.state.active && + + } + + + + ); + } +} + +HistoryItem.propTypes = propTypes; + + +const MoreMenuPropTypes = { + index: PropTypes.number.isRequired, + downloadUrl: PropTypes.string.isRequired, + viewUrl: PropTypes.string.isRequired, + diffUrl: PropTypes.string.isRequired, + onItemRestore: PropTypes.func.isRequired, + canDownload: PropTypes.bool.isRequired, + canCompare: PropTypes.bool.isRequired, +}; + +class MoreMenu extends React.PureComponent { + + constructor(props) { + super(props); + this.state = { + dropdownOpen: false + }; + } + + dropdownToggle = () => { + this.setState({ dropdownOpen: !this.state.dropdownOpen }); + } + + render() { + const { index, downloadUrl, viewUrl, diffUrl, onItemRestore, canCompare, canDownload } = this.props; + return ( + + + + + {index !== 0 && {gettext('Restore')}} + {canDownload && {gettext('Download')}} + {gettext('View')} + {canCompare && {gettext('Diff')}} + + + ); + } +} + +MoreMenu.propTypes = MoreMenuPropTypes; + +export default HistoryItem; diff --git a/frontend/src/utils/constants.js b/frontend/src/utils/constants.js index 400b5e1cba..1cbe1fc22c 100644 --- a/frontend/src/utils/constants.js +++ b/frontend/src/utils/constants.js @@ -66,6 +66,9 @@ export const historyRepoID = window.fileHistory ? window.fileHistory.pageOptions export const repoName = window.fileHistory ? window.fileHistory.pageOptions.repoName : ''; export const filePath = window.fileHistory ? window.fileHistory.pageOptions.filePath : ''; export const fileName = window.fileHistory ? window.fileHistory.pageOptions.fileName : ''; +export const useNewAPI = window.fileHistory ? window.fileHistory.pageOptions.use_new_api : ''; +export const canDownload = window.fileHistory ? window.fileHistory.pageOptions.can_download_file : ''; +export const canCompare = window.fileHistory ? window.fileHistory.pageOptions.can_compare : ''; // Draft review export const draftFilePath = window.draft ? window.draft.config.draftFilePath: ''; diff --git a/frontend/src/utils/utils.js b/frontend/src/utils/utils.js index 22c33d0e16..2ebc1849cc 100644 --- a/frontend/src/utils/utils.js +++ b/frontend/src/utils/utils.js @@ -1,4 +1,4 @@ -import { mediaUrl, gettext, serviceURL } from './constants'; +import { mediaUrl, gettext, serviceURL, siteRoot } from './constants'; import { strChineseFirstPY } from './pinyin-by-unicode'; export const Utils = { @@ -801,4 +801,16 @@ export const Utils = { return message; }, + handleSearchedItemClick: function(searchedItem) { + if (searchedItem.is_dir === true) { + let url = siteRoot + 'library/' + searchedItem.repo_id + '/' + searchedItem.repo_name + searchedItem.path; + let newWindow = window.open('about:blank'); + newWindow.location.href = url; + } else { + let url = siteRoot + 'lib/' + searchedItem.repo_id + '/file' + Utils.encodePath(searchedItem.path); + let newWindow = window.open('about:blank'); + newWindow.location.href = url; + } + }, + }; diff --git a/seahub/templates/file_revisions_old.html b/seahub/templates/file_revisions_old.html new file mode 100644 index 0000000000..c5fe938bce --- /dev/null +++ b/seahub/templates/file_revisions_old.html @@ -0,0 +1,26 @@ +{% extends "base_for_react.html" %} +{% load render_bundle from webpack_loader %} + +{% block extra_style %} +{% render_bundle 'fileHistoryOld' 'css'%} +{% endblock %} + +{% block extra_script %} + + {% render_bundle 'fileHistoryOld' 'js'%} +{% endblock %} diff --git a/seahub/views/__init__.py b/seahub/views/__init__.py index ee87e2b993..f4065c2b81 100644 --- a/seahub/views/__init__.py +++ b/seahub/views/__init__.py @@ -830,25 +830,31 @@ def file_revisions(request, repo_id): if repo_perm != 'rw' or (is_locked and not locked_by_me): can_revert_file = False - # for 'go back' - referer = request.GET.get('referer', '') + # Whether use new file history API which read file history from db. + suffix_list = seafevents_api.get_file_history_suffix() + if suffix_list and isinstance(suffix_list, list): + suffix_list = [x.lower() for x in suffix_list] + else: + logger.error('Wrong type of suffix_list: %s' % repr(suffix_list)) + suffix_list = [] + use_new_api = True if file_ext in suffix_list else False - # Whether use new file revisions page which read file history from db. if request.GET.get('_new', None) is not None: if request.GET.get('_new') == '0': - use_new_page = False - else: - use_new_page = True - else: - suffix_list = seafevents_api.get_file_history_suffix() - if suffix_list and isinstance(suffix_list, list): - suffix_list = [x.lower() for x in suffix_list] - else: - logger.error('Wrong type of suffix_list: %s' % repr(suffix_list)) - suffix_list = [] - use_new_page = True if file_ext in suffix_list else False + return render(request, 'file_revisions.html', { + 'repo': repo, + 'path': path, + 'u_filename': u_filename, + 'zipped': zipped, + 'is_owner': is_owner, + 'can_compare': can_compare, + 'can_revert_file': can_revert_file, + 'can_download_file': parse_repo_perm(repo_perm).can_download, + }) - if use_new_page: + use_new_style = True if filetype == 'markdown' else False + + if use_new_style: return render(request, 'file_revisions_new.html', { 'repo': repo, 'path': path, @@ -857,10 +863,9 @@ def file_revisions(request, repo_id): 'is_owner': is_owner, 'can_compare': can_compare, 'can_revert_file': can_revert_file, - 'referer': referer, }) - return render(request, 'file_revisions.html', { + return render(request, 'file_revisions_old.html', { 'repo': repo, 'path': path, 'u_filename': u_filename, @@ -869,8 +874,8 @@ def file_revisions(request, repo_id): 'can_compare': can_compare, 'can_revert_file': can_revert_file, 'can_download_file': parse_repo_perm(repo_perm).can_download, - 'referer': referer, - }) + 'use_new_api': use_new_api, + }) def demo(request):