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 && + + + + + + + + + + + {fileList.map((item, index) => { + return ( + + ); + }) + } + +
{gettext('Name')}{gettext('Size')}{gettext('Last Update')}
} +
+
+ + + +
+ ); + } +} + +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'),