diff --git a/frontend/config/webpack.config.dev.js b/frontend/config/webpack.config.dev.js
index bb41f2e6b9..afa1a26b88 100644
--- a/frontend/config/webpack.config.dev.js
+++ b/frontend/config/webpack.config.dev.js
@@ -204,6 +204,11 @@ module.exports = {
require.resolve('react-dev-utils/webpackHotDevClient'),
paths.appSrc + "/repo-history.js",
],
+ repoFolderTrash: [
+ require.resolve('./polyfills'),
+ require.resolve('react-dev-utils/webpackHotDevClient'),
+ paths.appSrc + "/repo-folder-trash.js",
+ ],
orgAdmin: [
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 b93f5c08f5..e9a5b9d1c9 100644
--- a/frontend/config/webpack.config.prod.js
+++ b/frontend/config/webpack.config.prod.js
@@ -89,6 +89,7 @@ module.exports = {
viewFileUnknown: [require.resolve('./polyfills'), paths.appSrc + "/view-file-unknown.js"],
settings: [require.resolve('./polyfills'), paths.appSrc + "/settings.js"],
repoHistory: [require.resolve('./polyfills'), paths.appSrc + "/repo-history.js"],
+ repoFolderTrash: [require.resolve('./polyfills'), paths.appSrc + "/repo-folder-trash.js"],
orgAdmin: [require.resolve('./polyfills'), paths.appSrc + "/pages/org-admin"],
sysAdmin: [require.resolve('./polyfills'), paths.appSrc + "/pages/sys-admin"],
viewDataGrid: [require.resolve('./polyfills'), paths.appSrc + "/view-file-ctable.js"],
diff --git a/frontend/src/components/cur-dir-path/dir-tool.js b/frontend/src/components/cur-dir-path/dir-tool.js
index 07728ed105..03a918d409 100644
--- a/frontend/src/components/cur-dir-path/dir-tool.js
+++ b/frontend/src/components/cur-dir-path/dir-tool.js
@@ -84,7 +84,7 @@ class DirTool extends React.Component {
let { repoID, repoName, permission, currentPath } = this.props;
let isFile = this.isMarkdownFile(currentPath);
let name = Utils.getFileName(currentPath);
- let trashUrl = siteRoot + 'repo/recycle/' + repoID + '/?referer=' + encodeURIComponent(location.href);
+ let trashUrl = siteRoot + 'repo/' + repoID + '/trash/';
let historyUrl = siteRoot + 'repo/history/' + repoID + '/';
if (permission) {
if (isFile) { // isFile
@@ -96,7 +96,7 @@ class DirTool extends React.Component {
);
} else {
if (name) { // name not '' is not root path
- trashUrl = siteRoot + 'dir/recycle/' + repoID + '/?dir_path=' + encodeURIComponent(currentPath) + '&referer=' + encodeURIComponent(location.href);
+ trashUrl = siteRoot + 'repo/' + repoID + '/trash/?path=' + encodeURIComponent(currentPath);
return (
diff --git a/frontend/src/components/dialog/clean-trash.js b/frontend/src/components/dialog/clean-trash.js
new file mode 100644
index 0000000000..0816b16801
--- /dev/null
+++ b/frontend/src/components/dialog/clean-trash.js
@@ -0,0 +1,90 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { Modal, ModalHeader, ModalBody, ModalFooter } from 'reactstrap';
+import Select from 'react-select/lib/Creatable';
+import { gettext } from '../../utils/constants';
+import { seafileAPI } from '../../utils/seafile-api';
+import toaster from '../toast';
+
+const propTypes = {
+ repoID: PropTypes.string.isRequired,
+ refreshTrash: PropTypes.func.isRequired,
+ toggleDialog: PropTypes.func.isRequired
+};
+
+class CleanTrash extends React.Component {
+ constructor(props) {
+ super(props);
+ this.options = [
+ {label: gettext('3 days ago'), value: 3},
+ {label: gettext('1 week ago'), value: 7},
+ {label: gettext('1 month ago'), value: 30},
+ {label: gettext('all'), value: 0}
+ ];
+ this.state = {
+ inputValue: this.options[0],
+ submitBtnDisabled: false
+ };
+ }
+
+ handleInputChange = (value) => {
+ this.setState({
+ inputValue: value
+ });
+ }
+
+ formSubmit = () => {
+ const inputValue = this.state.inputValue;
+ const { repoID } = this.props;
+
+ this.setState({
+ submitBtnDisabled: true
+ });
+
+ seafileAPI.deleteRepoTrash(repoID, inputValue.value).then((res) => {
+ toaster.success(gettext('Clean succeeded.'));
+ this.props.refreshTrash();
+ this.props.toggleDialog();
+ }).catch((error) => {
+ let errorMsg = '';
+ if (error.response) {
+ errorMsg = error.response.data.error_msg || gettext('Error');
+ } else {
+ errorMsg = gettext('Please check the network.');
+ }
+ this.setState({
+ formErrorMsg: errorMsg,
+ submitBtnDisabled: false
+ });
+ });
+ }
+
+ render() {
+ const { formErrorMsg } = this.state;
+ return (
+
+ {gettext('Clean')}
+
+
+ {gettext('Clear files in trash and history:')}
+
+ {formErrorMsg && {formErrorMsg}
}
+
+
+
+
+
+
+ );
+ }
+}
+
+CleanTrash.propTypes = propTypes;
+
+export default CleanTrash;
diff --git a/frontend/src/css/repo-folder-trash.css b/frontend/src/css/repo-folder-trash.css
new file mode 100644
index 0000000000..3c06c251d4
--- /dev/null
+++ b/frontend/src/css/repo-folder-trash.css
@@ -0,0 +1,33 @@
+body {
+ overflow: hidden;
+}
+#wrapper {
+ height: 100%;
+}
+.top-header {
+ background: #f4f4f7;
+ border-bottom: 1px solid #e8e8e8;
+ padding: .5rem 1rem;
+ flex-shrink: 0;
+}
+.go-back {
+ color: #c0c0c0;
+ font-size: 1.75rem;
+ position: absolute;
+ left: -40px;
+ top: -5px;
+}
+.op-bar {
+ padding: 9px 10px;
+ background: #f2f2f2;
+ border-radius: 2px;
+}
+.more {
+ background: #efefef;
+ border: 0;
+ color: #777;
+}
+.more:hover {
+ color: #000;
+ background: #dfdfdf;
+}
diff --git a/frontend/src/repo-folder-trash.js b/frontend/src/repo-folder-trash.js
new file mode 100644
index 0000000000..6ca4c4ce4c
--- /dev/null
+++ b/frontend/src/repo-folder-trash.js
@@ -0,0 +1,439 @@
+import React from 'react';
+import ReactDOM from 'react-dom';
+import { navigate } from '@reach/router';
+import moment from 'moment';
+import { Utils } from './utils/utils';
+import { gettext, loginUrl, siteRoot, mediaUrl, logoPath, logoWidth, logoHeight, siteTitle } from './utils/constants';
+import { seafileAPI } from './utils/seafile-api';
+import Loading from './components/loading';
+import ModalPortal from './components/modal-portal';
+import toaster from './components/toast';
+import CommonToolbar from './components/toolbar/common-toolbar';
+import CleanTrash from './components/dialog/clean-trash';
+
+import './css/toolbar.css';
+import './css/search.css';
+
+import './css/repo-folder-trash.css';
+
+const {
+ repoID,
+ repoFolderName,
+ path,
+ enableClean
+} = window.app.pageOptions;
+
+class RepoFolderTrash extends React.Component {
+
+ constructor(props) {
+ super(props);
+ this.state = {
+ isLoading: true,
+ errorMsg: '',
+ items: [],
+ scanStat: null,
+ more: false,
+ isCleanTrashDialogOpen: false
+ };
+ }
+
+ componentDidMount() {
+ this.getItems();
+ }
+
+ getItems = (scanStat) => {
+ seafileAPI.getRepoFolderTrash(repoID, path, scanStat).then((res) => {
+ const { data, more, scan_stat } = res.data;
+ if (!data.length && more) {
+ this.getItems(scan_stat);
+ } else {
+ this.setState({
+ isLoading: false,
+ items: this.state.items.concat(data),
+ more: more,
+ scanStat: scan_stat
+ });
+ }
+ }).catch((error) => {
+ if (error.response) {
+ if (error.response.status == 403) {
+ this.setState({
+ isLoading: false,
+ errorMsg: gettext('Permission denied')
+ });
+ location.href = `${loginUrl}?next=${encodeURIComponent(location.href)}`;
+ } else {
+ this.setState({
+ isLoading: false,
+ errorMsg: gettext('Error')
+ });
+ }
+ } else {
+ this.setState({
+ isLoading: false,
+ errorMsg: gettext('Please check the network.')
+ });
+ }
+ });
+ }
+
+ getMore = () => {
+ this.getItems(this.state.scanStat);
+ }
+
+ onSearchedClick = (selectedItem) => {
+ if (selectedItem.is_dir === true) {
+ let url = siteRoot + 'library/' + selectedItem.repo_id + '/' + selectedItem.repo_name + selectedItem.path;
+ navigate(url, {repalce: true});
+ } else {
+ let url = siteRoot + 'lib/' + selectedItem.repo_id + '/file' + Utils.encodePath(selectedItem.path);
+ let newWindow = window.open('about:blank');
+ newWindow.location.href = url;
+ }
+ }
+
+ goBack = (e) => {
+ e.preventDefault();
+ window.history.back();
+ }
+
+ cleanTrash = () => {
+ this.toggleCleanTrashDialog();
+ }
+
+ toggleCleanTrashDialog = () => {
+ this.setState({
+ isCleanTrashDialogOpen: !this.state.isCleanTrashDialogOpen
+ });
+ }
+
+ refreshTrash = () => {
+ this.setState({
+ isLoading: true,
+ errorMsg: '',
+ items: [],
+ scanStat: null,
+ more: false,
+ showFolder: false
+ });
+ this.getItems();
+ }
+
+ renderFolder = (commitID, baseDir, folderPath) => {
+ this.setState({
+ showFolder: true,
+ commitID: commitID,
+ baseDir: baseDir,
+ folderPath: folderPath,
+ folderItems: [],
+ isLoading: true
+ });
+
+ seafileAPI.listCommitDir(repoID, commitID, `${baseDir.substr(0, baseDir.length - 1)}${folderPath}`).then((res) => {
+ this.setState({
+ isLoading: false,
+ folderItems: res.data.dirent_list
+ });
+ }).catch((error) => {
+ if (error.response) {
+ if (error.response.status == 403) {
+ this.setState({
+ isLoading: false,
+ errorMsg: gettext('Permission denied')
+ });
+ location.href = `${loginUrl}?next=${encodeURIComponent(location.href)}`;
+ } else {
+ this.setState({
+ isLoading: false,
+ errorMsg: gettext('Error')
+ });
+ }
+ } else {
+ this.setState({
+ isLoading: false,
+ errorMsg: gettext('Please check the network.')
+ });
+ }
+ });
+ }
+
+ clickRoot = (e) => {
+ e.preventDefault();
+ this.refreshTrash();
+ }
+
+ clickFolderPath = (folderPath, e) => {
+ e.preventDefault();
+ const { commitID, baseDir } = this.state;
+ this.renderFolder(commitID, baseDir, folderPath);
+ }
+
+ renderFolderPath = () => {
+ const pathList = this.state.folderPath.split('/');
+ return (
+
+ {repoFolderName}
+ /
+ {pathList.map((item, index) => {
+ if (index > 0 && index != pathList.length - 1) {
+ return (
+
+ {pathList[index]}
+ /
+
+ );
+ }
+ }
+ )}
+ {pathList[pathList.length - 1]}
+
+ );
+ }
+
+ render() {
+ const { isCleanTrashDialogOpen, showFolder } = this.state;
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
{gettext('Current path: ')}{showFolder ? this.renderFolderPath() : repoFolderName}
+ {(path == '/' && enableClean && !showFolder) &&
+
+ }
+
+
+
+
+
+
+ {isCleanTrashDialogOpen &&
+
+
+
+ }
+
+ );
+ }
+}
+
+class Content extends React.Component {
+
+ constructor(props) {
+ super(props);
+ this.theadData = [
+ {width: '5%', text: ''},
+ {width: '45%', text: gettext('Name')},
+ {width: '20%', text: gettext('Delete Time')},
+ {width: '15%', text: gettext('Size')},
+ {width: '15%', text: ''}
+ ];
+ }
+
+ render() {
+ const { isLoading, errorMsg, items, more, showFolder, commitID, baseDir, folderPath, folderItems } = this.props.data;
+
+ return (
+
+
+
+
+ {this.theadData.map((item, index) => {
+ return {item.text} | ;
+ })}
+
+
+
+ {showFolder ?
+ folderItems.map((item, index) => {
+ return ;
+ }) :
+ items.map((item, index) => {
+ return ;
+ })}
+
+
+ {isLoading && }
+ {errorMsg && {errorMsg}
}
+ {(more && !isLoading && !showFolder) && (
+
+ )}
+
+ );
+ }
+}
+
+class Item extends React.Component {
+
+ constructor(props) {
+ super(props);
+ this.state = {
+ restored: false,
+ isIconShown: false
+ };
+ }
+
+ handleMouseOver = () => {
+ this.setState({isIconShown: true});
+ }
+
+ handleMouseOut = () => {
+ this.setState({isIconShown: false});
+ }
+
+ restoreItem = (e) => {
+ e.preventDefault();
+
+ const item = this.props.item;
+ const { commit_id, parent_dir, obj_name } = item;
+ const path = parent_dir + obj_name;
+ const request = item.is_dir ?
+ seafileAPI.restoreFolder(repoID, commit_id, path) :
+ seafileAPI.restoreFile(repoID, commit_id, path);
+ request.then((res) => {
+ this.setState({
+ restored: true
+ }); toaster.success(gettext('Successfully restored 1 item.'));
+ }).catch((error) => {
+ let errorMsg = '';
+ if (error.response) {
+ errorMsg = error.response.data.error_msg || gettext('Error');
+ } else {
+ errorMsg = gettext('Please check the network.');
+ }
+ toaster.danger(errorMsg);
+ });
+ }
+
+ renderFolder = (e) => {
+ e.preventDefault();
+ const item = this.props.item;
+ this.props.renderFolder(item.commit_id, item.parent_dir, Utils.joinPath('/', item.obj_name));
+ }
+
+ render() {
+ const item = this.props.item;
+ const { restored, isIconShown } = this.state;
+
+ if (restored) {
+ return null;
+ }
+
+ return item.is_dir ? (
+
+
+ }) |
+ {item.obj_name} |
+ {moment(item.deleted_time).format('YYYY-MM-DD')} |
+ |
+
+ {gettext('Restore')}
+ |
+
+
+ ) : (
+
+
+ }) |
+ {item.obj_name} |
+ {moment(item.deleted_time).format('YYYY-MM-DD')} |
+ {Utils.bytesToSize(item.size)} |
+
+ {gettext('Restore')}
+ |
+
+
+ );
+ }
+}
+
+class FolderItem extends React.Component {
+
+ constructor(props) {
+ super(props);
+ this.state = {
+ isIconShown: false
+ };
+ }
+
+ handleMouseOver = () => {
+ this.setState({isIconShown: true});
+ }
+
+ handleMouseOut = () => {
+ this.setState({isIconShown: false});
+ }
+
+ renderFolder = (e) => {
+ e.preventDefault();
+
+ const item = this.props.item;
+ const { commitID, baseDir, folderPath } = this.props;
+ this.props.renderFolder(commitID, baseDir, Utils.joinPath(folderPath, item.name));
+ }
+
+ render() {
+ const item = this.props.item;
+ const { isIconShown } = this.state;
+ const { commitID, baseDir, folderPath } = this.props;
+
+ return item.type == 'dir' ? (
+
+
+ }) |
+ {item.name} |
+ |
+ |
+ |
+
+
+ ) : (
+
+
+ }) |
+ {item.name} |
+ |
+ {Utils.bytesToSize(item.size)} |
+ |
+
+
+ );
+ }
+}
+
+ReactDOM.render(
+ ,
+ document.getElementById('wrapper')
+);
diff --git a/seahub/templates/repo_folder_trash_react.html b/seahub/templates/repo_folder_trash_react.html
new file mode 100644
index 0000000000..a098242c15
--- /dev/null
+++ b/seahub/templates/repo_folder_trash_react.html
@@ -0,0 +1,22 @@
+{% extends 'base_for_react.html' %}
+{% load seahub_tags i18n %}
+{% load render_bundle from webpack_loader %}
+
+{% block sub_title %}{% trans "Trash" %} - {% endblock %}
+
+{% block extra_style %}
+{% render_bundle 'repoFolderTrash' 'css' %}
+{% endblock %}
+
+{% block extra_script %}
+
+{% render_bundle 'repoFolderTrash' 'js' %}
+{% endblock %}
diff --git a/seahub/urls.py b/seahub/urls.py
index e9fd491d5a..0bddd33018 100644
--- a/seahub/urls.py
+++ b/seahub/urls.py
@@ -169,6 +169,7 @@ urlpatterns = [
url(r'^repo/history/view/(?P[-0-9a-f]{36})/$', repo_history_view, name='repo_history_view'),
url(r'^repo/recycle/(?P[-0-9a-f]{36})/$', repo_recycle_view, name='repo_recycle_view'),
url(r'^dir/recycle/(?P[-0-9a-f]{36})/$', dir_recycle_view, name='dir_recycle_view'),
+ url(r'^repo/(?P[-0-9a-f]{36})/trash/$', repo_folder_trash, name="repo_folder_trash"),
url(r'^repo/(?P[-0-9a-f]{36})/raw/(?P.*)$', view_raw_file, name="view_raw_file"),
url(r'^repo/(?P[-0-9a-f]{36})/history/files/$', view_history_file, name="view_history_file"),
url(r'^repo/(?P[-0-9a-f]{36})/trash/files/$', view_trash_file, name="view_trash_file"),
diff --git a/seahub/views/__init__.py b/seahub/views/__init__.py
index 0fddeb08cb..11c3bf5bca 100644
--- a/seahub/views/__init__.py
+++ b/seahub/views/__init__.py
@@ -409,7 +409,6 @@ def dir_recycle_view(request, repo_id):
if not seafile_api.get_dir_id_by_path(repo_id, dir_path) or \
check_folder_permission(request, repo_id, dir_path) != 'rw':
-
return render_permission_error(request, _(u'Unable to view recycle page'))
commit_id = request.GET.get('commit_id', '')
@@ -419,6 +418,30 @@ def dir_recycle_view(request, repo_id):
else:
return render_dir_recycle_dir(request, repo_id, commit_id, dir_path, referer)
+@login_required
+def repo_folder_trash(request, repo_id):
+ path = request.GET.get('path', '/')
+
+ if not seafile_api.get_dir_id_by_path(repo_id, path) or \
+ check_folder_permission(request, repo_id, path) != 'rw':
+ return render_permission_error(request, _(u'Unable to view recycle page'))
+
+ repo = get_repo(repo_id)
+ if not repo:
+ raise Http404
+
+ if path == '/':
+ name = repo.name
+ else:
+ name = os.path.basename(path.rstrip('/'))
+
+ return render(request, 'repo_folder_trash_react.html', {
+ 'repo': repo,
+ 'repo_folder_name': name,
+ 'path': path,
+ 'enable_clean': config.ENABLE_USER_CLEAN_TRASH,
+ })
+
def can_access_repo_setting(request, repo_id, username):
repo = seafile_api.get_repo(repo_id)
if not repo: