diff --git a/frontend/src/components/dialog/search-file-dialog.js b/frontend/src/components/dialog/search-file-dialog.js
new file mode 100644
index 0000000000..fc497529f0
--- /dev/null
+++ b/frontend/src/components/dialog/search-file-dialog.js
@@ -0,0 +1,143 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import moment from 'moment';
+import { Button, Modal, ModalHeader, ModalBody, ModalFooter, Alert } from 'reactstrap';
+import { Utils } from '../../utils/utils';
+import { seafileAPI } from '../../utils/seafile-api.js';
+import { gettext, siteRoot } from '../../utils/constants';
+
+const propTypes = {
+ repoID: PropTypes.string.isRequired,
+ repoName: PropTypes.string.isRequired,
+ toggleDialog: PropTypes.func.isRequired
+};
+
+class SearchFileDialog extends React.Component {
+
+ constructor(props) {
+ super(props);
+ this.state = {
+ isSubmitDisabled: true,
+ q: '',
+ errMessage: '',
+ fileList: []
+ };
+ }
+
+ searchFile = () => {
+ const { q } = this.state;
+ if (!q.trim()) {
+ return false;
+ }
+ seafileAPI.searchFileInRepo(this.props.repoID, q).then((res) => {
+ this.setState({
+ fileList: res.data.data,
+ errMessage: ''
+ });
+ }).catch(error => {
+ let errMessage = Utils.getErrorMsg(error);
+ this.setState({
+ errMessage: errMessage
+ });
+ });
+ }
+
+ handleKeyDown = (e) => {
+ if (e.key == 'Enter') {
+ e.preventDefault();
+ this.searchFile();
+ }
+ }
+
+ toggle = () => {
+ this.props.toggleDialog();
+ }
+
+ handleInputChange = (e) => {
+ const q = e.target.value;
+ this.setState({
+ q: q,
+ isSubmitDisabled: !q.trim()
+ });
+ }
+
+ render() {
+ const { q, errMessage, fileList, isSubmitDisabled } = this.state;
+ return (
+
+ {gettext('Search')}
+
+
+
+
+
+ {errMessage && {errMessage}}
+
+ {fileList.length > 0 &&
+
+
+
+ |
+ {gettext('Name')} |
+ {gettext('Size')} |
+ {gettext('Last Update')} |
+
+
+
+ {fileList.map((item, index) => {
+ return (
+
+ );
+ })
+ }
+
+
}
+
+
+
+
+
+
+ );
+ }
+}
+
+SearchFileDialog.propTypes = propTypes;
+
+const FileItemPropTypes = {
+ repoID: PropTypes.string.isRequired,
+ repoName: PropTypes.string.isRequired,
+ item: PropTypes.object.isRequired
+};
+
+class FileItem extends React.PureComponent {
+
+ render() {
+ const { item, repoID, repoName } = this.props;
+ const name = item.path.substr(item.path.lastIndexOf('/') + 1);
+ const url = item.type == 'file' ?
+ `${siteRoot}lib/${repoID}/file${Utils.encodePath(item.path)}` :
+ `${siteRoot}library/${repoID}/${Utils.encodePath(repoName + item.path)}`;
+
+ return(
+
+  |
+
+ {name}
+ |
+ {item.type == 'file' ? Utils.bytesToSize(item.size) : ''} |
+ {moment(item.mtime).fromNow()} |
+
+ );
+ }
+}
+
+FileItem.propTypes = FileItemPropTypes;
+
+
+export default SearchFileDialog;
diff --git a/frontend/src/components/search/search-by-name.js b/frontend/src/components/search/search-by-name.js
new file mode 100644
index 0000000000..bb69953c14
--- /dev/null
+++ b/frontend/src/components/search/search-by-name.js
@@ -0,0 +1,51 @@
+import React, { Component, Fragment } from 'react';
+import PropTypes from 'prop-types';
+import { gettext } from '../../utils/constants';
+import SearchFileDialog from '../dialog/search-file-dialog.js';
+
+import '../../css/top-search-by-name.css';
+
+const propTypes = {
+ repoID: PropTypes.string.isRequired,
+ repoName: PropTypes.string.isRequired
+};
+
+class SearchByName extends Component {
+
+ constructor(props) {
+ super(props);
+ this.state = {
+ isDialogOpen: false
+ };
+ }
+
+ toggleDialog = () => {
+ this.setState({
+ isDialogOpen: !this.state.isDialogOpen
+ });
+ }
+
+ render() {
+ const { repoID, repoName } = this.props;
+ return (
+
+
+ {this.state.isDialogOpen &&
+
+ }
+
+ );
+ }
+}
+
+SearchByName.propTypes = propTypes;
+
+export default SearchByName;
diff --git a/frontend/src/components/toolbar/common-toolbar.js b/frontend/src/components/toolbar/common-toolbar.js
index 1536efcc5d..0dc8b46f43 100644
--- a/frontend/src/components/toolbar/common-toolbar.js
+++ b/frontend/src/components/toolbar/common-toolbar.js
@@ -2,17 +2,21 @@ import React from 'react';
import PropTypes from 'prop-types';
import { isPro, gettext, showLogoutIcon } from '../../utils/constants';
import Search from '../search/search';
+import SearchByName from '../search/search-by-name';
import Notification from '../common/notification';
import Account from '../common/account';
import Logout from '../common/logout';
const propTypes = {
repoID: PropTypes.string,
+ repoName: PropTypes.string,
+ isLibView: PropTypes.bool,
onSearchedClick: PropTypes.func.isRequired,
searchPlaceholder: PropTypes.string
};
-class CommonToolbar extends React.Component {
+class CommonToolbar extends React.Component {
+
render() {
let searchPlaceholder = this.props.searchPlaceholder || gettext('Search Files');
return (
@@ -24,6 +28,12 @@ class CommonToolbar extends React.Component {
onSearchedClick={this.props.onSearchedClick}
/>
)}
+ {this.props.isLibView && !isPro &&
+
+ }
{showLogoutIcon && ()}
diff --git a/frontend/src/css/top-search-by-name.css b/frontend/src/css/top-search-by-name.css
new file mode 100644
index 0000000000..6f5ca33089
--- /dev/null
+++ b/frontend/src/css/top-search-by-name.css
@@ -0,0 +1,7 @@
+.top-search-file-icon {
+ color: #999;
+ font-size: 20px;
+ align-self: center;
+ font-weight: 800;
+ cursor: pointer;
+}
diff --git a/frontend/src/pages/lib-content-view/lib-content-toolbar.js b/frontend/src/pages/lib-content-view/lib-content-toolbar.js
index e847c53abf..1c0754ba78 100644
--- a/frontend/src/pages/lib-content-view/lib-content-toolbar.js
+++ b/frontend/src/pages/lib-content-view/lib-content-toolbar.js
@@ -82,7 +82,7 @@ class LibContentToolbar extends React.Component {
/>
-
+
);
}
@@ -135,7 +135,7 @@ class LibContentToolbar extends React.Component {
/>
}
-
+
);
}
diff --git a/seahub/api2/endpoints/search_file.py b/seahub/api2/endpoints/search_file.py
new file mode 100644
index 0000000000..a4cc1d72ca
--- /dev/null
+++ b/seahub/api2/endpoints/search_file.py
@@ -0,0 +1,68 @@
+# Copyright (c) 2012-2016 Seafile Ltd.
+
+from rest_framework.authentication import SessionAuthentication
+from rest_framework.permissions import IsAuthenticated
+from rest_framework.response import Response
+from rest_framework.views import APIView
+from rest_framework import status
+
+
+from seaserv import seafile_api
+
+from seahub.api2.authentication import TokenAuthentication
+from seahub.api2.throttling import UserRateThrottle
+from seahub.api2.utils import api_error
+
+from seahub.views import check_folder_permission
+from seahub.utils.timeutils import timestamp_to_isoformat_timestr
+
+try:
+ from seahub.settings import CLOUD_MODE
+except ImportError:
+ CLOUD_MODE = False
+
+
+class SearchFile(APIView):
+
+ authentication_classes = (TokenAuthentication, SessionAuthentication)
+ permission_classes = (IsAuthenticated,)
+ throttle_classes = (UserRateThrottle,)
+
+ def get(self, request, format=None):
+ """ Search file by name.
+ """
+
+ # argument check
+ repo_id = request.GET.get('repo_id', None)
+ if not repo_id:
+ error_msg = 'repo_id invalid.'
+ return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
+
+ q = request.GET.get('q', None)
+ if not q:
+ error_msg = 'q invalid.'
+ return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
+
+ # resource check
+ if not seafile_api.get_repo(repo_id):
+ error_msg = 'Library %s not found.' % repo_id
+ return api_error(status.HTTP_404_NOT_FOUND, error_msg)
+
+ # permission check
+ if not check_folder_permission(request, repo_id, '/'):
+ error_msg = 'Permission denied.'
+ return api_error(status.HTTP_403_FORBIDDEN, error_msg)
+
+ result = []
+ searched_files = seafile_api.search_files(repo_id, q)
+
+ for searched_file in searched_files:
+ # {'path': '/123.docx', 'size': 19446, 'mtime': 1604130882, 'is_dir': False}
+ file_info = {}
+ file_info['path'] = searched_file.path
+ file_info['size'] = searched_file.size
+ file_info['mtime'] = timestamp_to_isoformat_timestr(searched_file.mtime)
+ file_info['type'] = 'folder' if searched_file.is_dir else 'file'
+ result.append(file_info)
+
+ return Response({'data': result})
diff --git a/seahub/urls.py b/seahub/urls.py
index c4f53c1e9c..72308bb865 100644
--- a/seahub/urls.py
+++ b/seahub/urls.py
@@ -20,6 +20,8 @@ from seahub.views.repo import repo_history_view, repo_snapshot, view_shared_dir,
from seahub.dingtalk.views import dingtalk_login, dingtalk_callback, \
dingtalk_connect, dingtalk_connect_callback, dingtalk_disconnect
+from seahub.api2.endpoints.search_file import SearchFile
+
from seahub.api2.endpoints.smart_link import SmartLink, SmartLinkToken
from seahub.api2.endpoints.groups import Groups, Group
from seahub.api2.endpoints.all_groups import AllGroups
@@ -286,6 +288,9 @@ urlpatterns = [
url(r'^api/v2.1/smart-link/$', SmartLink.as_view(), name="api-v2.1-smart-link"),
url(r'^api/v2.1/smart-links/(?P[-0-9a-f]{36})/$', SmartLinkToken.as_view(), name="api-v2.1-smart-links-token"),
+ # search file by name
+ url(r'^api/v2.1/search-file/$', SearchFile.as_view(), name='api-v2.1-search-file'),
+
# departments
url(r'api/v2.1/departments/$', Departments.as_view(), name='api-v2.1-all-departments'),