diff --git a/frontend/config/webpack.config.dev.js b/frontend/config/webpack.config.dev.js
index b15617522c..324c862b4d 100644
--- a/frontend/config/webpack.config.dev.js
+++ b/frontend/config/webpack.config.dev.js
@@ -218,6 +218,11 @@ module.exports = {
require.resolve('./polyfills'),
require.resolve('react-dev-utils/webpackHotDevClient'),
paths.appSrc + "/view-file-cdoc.js",
+ ],
+ search: [
+ require.resolve('./polyfills'),
+ require.resolve('react-dev-utils/webpackHotDevClient'),
+ paths.appSrc + "/pages/search",
]
},
diff --git a/frontend/config/webpack.config.prod.js b/frontend/config/webpack.config.prod.js
index a41ff61334..3e53ec13d1 100644
--- a/frontend/config/webpack.config.prod.js
+++ b/frontend/config/webpack.config.prod.js
@@ -91,7 +91,8 @@ module.exports = {
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"],
- viewCdoc: [require.resolve('./polyfills'), paths.appSrc + "/view-file-cdoc.js"]
+ viewCdoc: [require.resolve('./polyfills'), paths.appSrc + "/view-file-cdoc.js"],
+ search: [require.resolve('./polyfills'), paths.appSrc + "/pages/search"]
},
output: {
diff --git a/frontend/src/css/search.css b/frontend/src/css/search.css
index 6ed899ffa5..a70a6dedac 100644
--- a/frontend/src/css/search.css
+++ b/frontend/src/css/search.css
@@ -97,6 +97,82 @@
font-weight: bold;
}
+.main-panel-south {
+ flex: auto;
+ overflow: auto;
+ height: calc(100% - 50px);
+}
+.search-page {
+ margin: 30px auto;
+ width: 65%;
+}
+.search-page .search-result-container {
+ border-radius: 0;
+ box-shadow: none;
+}
+.search-page .search-page-container {
+ padding: 10px;
+ background: #f7f7f8;
+}
+.search-page .search-page-container .search-input {
+ padding-left: 0.5rem;
+ width: 30rem;
+}
+.search-page .search-page-container .fa-angle-double-up,
+.search-page .search-page-container .fa-angle-double-down {
+ font-size: 1rem;
+}
+.search-page .advanced-search .search-file-types .search-input {
+ padding-left: 0.5rem;
+ width: 30rem;
+ max-width: 100%;
+}
+.search-page .search-page-container .search-icon-right {
+ left: 28rem;
+}
+.search-page .paginator {
+ text-align: center;
+ margin: 1rem 0;
+}
+.search-page .advanced-search, .search-page .search-filters {
+ color: #747474;
+}
+.search-page .search-filters {
+ padding: 10px 10px 0;
+}
+.search-page .advanced-search .search-repo,
+.search-page .advanced-search .search-file-types {
+ padding: 5px 0;
+}
+.search-file-types .search-file-types-form {
+ top: 10px;
+}
+.search-page .advanced-search .search-catalog {
+ margin: 0 10px;
+ border-top: 1px dashed #e2e2e2;
+ padding: 10px 0;
+}
+.search-page .advanced-search .search-catalog:first-child {
+ border: none;
+}
+.search-page .custom-checkbox .custom-control-input:checked ~ .custom-control-label::before {
+ background-color: #3B88FD;
+}
+.search-date .ant-input {
+ height: 2.375rem;
+ padding: 0.375rem 0.75rem;
+ line-height: 1.6;
+ border: 1px solid rgba(0, 40, 100, 0.12);
+ border-radius: 3px;
+ transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
+}
+.search-date .select-data-icon {
+ position: absolute;
+ right: 1.5rem;
+ top: 0.5rem;
+ color: #b2b2b2;
+}
+
@media (max-width: 767px) {
.common-toolbar .search {
margin: 0;
@@ -137,4 +213,26 @@
left: auto;
width: 20rem;
}
+
+ .search-page {
+ margin: 0;
+ width: 100%;
+ height: 100%;
+ }
+
+ .search-page .search-page-container .search-input {
+ box-shadow: none;
+ width: 95% !important;
+ }
+
+ .search-page .search-page-container .search-icon-right {
+ left: 85%;
+ }
+
+ .search-page .search-result-container {
+ top: 0;
+ left: 0;
+ width: 100%;
+ padding: 10px;
+ }
}
diff --git a/frontend/src/pages/search/advanced-search.js b/frontend/src/pages/search/advanced-search.js
new file mode 100644
index 0000000000..9028046d89
--- /dev/null
+++ b/frontend/src/pages/search/advanced-search.js
@@ -0,0 +1,258 @@
+import React, { Fragment } from 'react';
+import PropTypes from 'prop-types';
+import MediaQuery from 'react-responsive';
+import { gettext, lang } from '../../utils/constants';
+import { Button, Col, Collapse, CustomInput, FormGroup, Input, Label, Row, InputGroupAddon, InputGroup } from 'reactstrap';
+import SelectDate from '@seafile/seafile-editor/dist/components/calander';
+
+const { repo_name, search_repo } = window.search.pageOptions;
+
+class AdvancedSearch extends React.Component {
+
+ constructor(props) {
+ super(props);
+ }
+
+ getFileTypesList = (fileTypes) => {
+ const fileTypeItems = [gettext('Text'), gettext('Document'), gettext('Image'), gettext('Video'), gettext('Audio'), 'PDF', 'Markdown'];
+ let ftype = [];
+ for (let i = 0, len = fileTypes.length; i < len; i++) {
+ if (fileTypes[i]) {
+ ftype.push(fileTypeItems[i]);
+ }
+ }
+ return ftype;
+ };
+
+ render() {
+ const { stateAndValues } = this.props;
+ const { errorDateMsg, errorSizeMsg } = stateAndValues;
+
+ if (stateAndValues.isShowSearchFilter) {
+ const { size_from, size_to, time_from, time_to, search_repo, fileTypeItemsStatus } = stateAndValues;
+ const fileTypes = this.getFileTypesList(fileTypeItemsStatus);
+ const typesLength = fileTypes.length;
+ return (
+
+ {search_repo && {gettext('Libraries')}{': '}{search_repo}}
+ {typesLength > 0 &&
+ {gettext('File Types')}{': '}
+ {fileTypes.map((type, index) => {
+ return {type}{index !== (typesLength - 1) && ','}{' '};
+ })}
+
+ }
+ {(time_from && time_to ) &&
+ {gettext('Last Update')}{': '}{time_from}{' '}{gettext('to')}{' '}{time_to}
+ }
+ {(size_from && size_to) &&
+ {gettext('Size')}{': '}{size_from}{'MB - '}{size_to}{'MB'}
+ }
+
+ );
+ }
+ else {
+ return (
+
+ );
+ }
+ }
+}
+
+const advancedSearchPropTypes = {
+ openFileTypeCollapse: PropTypes.func.isRequired,
+ closeFileTypeCollapse: PropTypes.func.isRequired,
+ handlerFileTypes: PropTypes.func.isRequired,
+ handlerFileTypesInput: PropTypes.func.isRequired,
+ handleSubmit: PropTypes.func.isRequired,
+ handleReset: PropTypes.func.isRequired,
+ handlerRepo: PropTypes.func.isRequired,
+ handleKeyDown: PropTypes.func.isRequired,
+ handleTimeFromInput: PropTypes.func.isRequired,
+ handleTimeToInput: PropTypes.func.isRequired,
+ handleSizeFromInput: PropTypes.func.isRequired,
+ handleSizeToInput: PropTypes.func.isRequired,
+ stateAndValues: PropTypes.object.isRequired,
+};
+
+AdvancedSearch.propTypes = advancedSearchPropTypes;
+
+export default AdvancedSearch;
diff --git a/frontend/src/pages/search/index.js b/frontend/src/pages/search/index.js
new file mode 100644
index 0000000000..c7ce73025e
--- /dev/null
+++ b/frontend/src/pages/search/index.js
@@ -0,0 +1,45 @@
+import React from 'react';
+import ReactDOM from 'react-dom';
+import CommonToolbar from '../../components/toolbar/common-toolbar';
+import Logo from '../../components/logo';
+import SearchViewPanel from './main-panel';
+import { siteRoot } from '../../utils/constants';
+import { Utils } from '../../utils/utils';
+
+import '../../css/layout.css';
+import '../../css/toolbar.css';
+import '../../css/search.css';
+
+class SearchView extends React.Component {
+
+ constructor(props) {
+ super(props);
+ }
+
+ onSearchedClick = (selectedItem) => {
+ let url = selectedItem.is_dir ?
+ siteRoot + 'library/' + selectedItem.repo_id + '/' + selectedItem.repo_name + selectedItem.path :
+ siteRoot + 'lib/' + selectedItem.repo_id + '/file' + Utils.encodePath(selectedItem.path);
+ let newWindow = window.open('about:blank');
+ newWindow.location.href = url;
+ };
+
+ render() {
+ return (
+
+ );
+ }
+}
+
+ReactDOM.render(
+ ,
+ document.getElementById('wrapper')
+);
diff --git a/frontend/src/pages/search/main-panel.js b/frontend/src/pages/search/main-panel.js
new file mode 100644
index 0000000000..055c75e2b3
--- /dev/null
+++ b/frontend/src/pages/search/main-panel.js
@@ -0,0 +1,338 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import moment from 'moment';
+import { gettext } from '../../utils/constants';
+import { seafileAPI } from '../../utils/seafile-api';
+import SearchResults from './search-results';
+import AdvancedSearch from './advanced-search';
+import toaster from '../../components/toast';
+import Loading from '../../components/loading';
+
+import '../../css/search.css';
+
+const _ = require('lodash');
+const { q, repo_name, search_repo, search_ftypes } = window.search.pageOptions;
+
+class SearchViewPanel extends React.Component {
+
+ constructor(props) {
+ super(props);
+ this.stateHistory = null;
+ this.state = {
+ isCollapseOpen: search_repo !== 'all',
+ isFileTypeCollapseOpen: false,
+ isResultGot: false,
+ isLoading: true,
+ isAllRepoCheck: search_repo === 'all',
+ isShowSearchFilter: false,
+ // advanced search index
+ q: q.trim(),
+ search_repo: search_repo,
+ search_ftypes: search_ftypes,
+ fileTypeItemsStatus: [false, false, false, false, false, false, false],
+ input_fexts: '',
+ time_from: '',
+ time_to: '',
+ size_from: '',
+ size_to: '',
+ // search result
+ hasMore: false,
+ resultItems: [],
+ page: 1,
+ per_page: 20,
+ errorMsg: '',
+ errorDateMsg: '',
+ errorSizeMsg: '',
+ };
+ }
+
+ getSearchResults(params) {
+ this.setState({
+ isLoading: true,
+ isResultGot: false,
+ });
+ const stateHistory = _.cloneDeep(this.state);
+ seafileAPI.searchFiles(params, null).then(res => {
+ this.setState({
+ isLoading: false,
+ isResultGot: true,
+ resultItems: res.data.results,
+ hasMore: res.data.has_more,
+ page: params.page,
+ isShowSearchFilter: true,
+ });
+ this.stateHistory = stateHistory;
+ this.stateHistory.resultItems = res.data.results;
+ this.stateHistory.hasMore = res.data.has_more;
+ this.stateHistory.page = params.page;
+ }).catch((error) => {
+ this.setState({ isLoading: false });
+ if (error.response) {
+ toaster.danger(error.response.data.detail || error.response.data.error_msg || gettext('Error'), {duration: 3});
+ } else {
+ toaster.danger(gettext('Please check the network.'), {duration: 3});
+ }
+ });
+ }
+
+ handleSearchParams = (page) => {
+ let params = { q: this.state.q.trim(), page: page };
+ const ftype = this.getFileTypesList();
+ if (this.state.search_repo) {params.search_repo = this.state.search_repo;}
+ if (this.state.search_ftypes) {params.search_ftypes = this.state.search_ftypes;}
+ if (this.state.per_page) {params.per_page = this.state.per_page;}
+ if (this.state.input_fexts) {params.input_fexts = this.state.input_fexts;}
+ if (this.state.time_from) {params.time_from = moment(this.state.time_from).valueOf() / 1000;}
+ if (this.state.time_to) {params.time_to = moment(this.state.time_to).valueOf() / 1000;}
+ if (this.state.size_from) {params.size_from = this.state.size_from * 1000 *1000;}
+ if (this.state.size_to) {params.size_to = this.state.size_to * 1000 * 1000;}
+ if (ftype.length !== 0) {params.ftype = ftype;}
+ return params;
+ };
+
+ handleSubmit = () => {
+ if (this.compareNumber(this.state.time_from, this.state.time_to)) {
+ this.setState({ errorDateMsg: gettext('Start date should be earlier than end date.') });
+ return;
+ }
+ if (this.compareNumber(this.state.size_from, this.state.size_to)) {
+ this.setState({ errorSizeMsg: gettext('Invalid file size range.') });
+ return;
+ }
+ if (this.getValueLength(this.state.q.trim()) < 3) {
+ if (this.state.q.trim().length === 0) {
+ this.setState({ errorMsg: gettext('It is required.') });
+ } else {
+ this.setState({ errorMsg: gettext('Required at least three letters.') });
+ }
+ if (this.state.isLoading) {
+ this.setState({ isLoading: false });
+ }
+ } else {
+ const params = this.handleSearchParams(1);
+ this.getSearchResults(params);
+ }
+ };
+
+ compareNumber = (num1, num2) => {
+ if (!num1 || !num2) return false;
+ if (parseInt(num1.replace(/\-/g, '')) >= parseInt(num2.replace(/\-/g, ''))) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ showSearchFilter = () => {
+ this.setState({ isShowSearchFilter: true });
+ }
+
+ hideSearchFilter = () => {
+ this.setState({ isShowSearchFilter: false });
+ }
+
+ handleReset = () => {
+ this.setState({
+ q: q.trim(),
+ search_repo: search_repo,
+ search_ftypes: search_ftypes,
+ fileTypeItemsStatus: [false, false, false, false, false, false, false],
+ input_fexts: '',
+ time_from: '',
+ time_to: '',
+ size_from: '',
+ size_to: '',
+ errorMsg: '',
+ errorDateMsg: '',
+ errorSizeMsg: '',
+ });
+ }
+
+ handlePrevious = () => {
+ if (this.stateHistory && this.state.page > 1) {
+ this.setState(this.stateHistory,() => {
+ const params = this.handleSearchParams(this.state.page - 1);
+ this.getSearchResults(params);
+ });
+ } else {
+ toaster.danger(gettext('Error'), {duration: 3});
+ }
+ };
+
+ handleNext = () => {
+ if (this.stateHistory && this.state.hasMore) {
+ this.setState(this.stateHistory,() => {
+ const params = this.handleSearchParams(this.state.page + 1);
+ this.getSearchResults(params);
+ });
+ } else {
+ toaster.danger(gettext('Error'), {duration: 3});
+ }
+ };
+
+ getValueLength(str) {
+ let code, len = 0;
+ for (let i = 0, length = str.length; i < length; i++) {
+ code = str.charCodeAt(i);
+ if (code === 10) { //solve enter problem
+ len += 2;
+ } else if (code < 0x007f) {
+ len += 1;
+ } else if (code >= 0x0080 && code <= 0x07ff) {
+ len += 2;
+ } else if (code >= 0x0800 && code <= 0xffff) {
+ len += 3;
+ }
+ }
+ return len;
+ }
+
+ toggleCollapse = () => {
+ this.setState({isCollapseOpen: !this.state.isCollapseOpen});
+ this.hideSearchFilter();
+ };
+
+ openFileTypeCollapse = () => {
+ this.setState({
+ isFileTypeCollapseOpen: true,
+ search_ftypes: 'custom',
+ });
+ };
+
+ closeFileTypeCollapse = () => {
+ this.setState({
+ isFileTypeCollapseOpen: false,
+ fileTypeItemsStatus: Array(7).fill(false),
+ search_ftypes: 'all',
+ input_fexts: '',
+ });
+ };
+
+ handleSearchInput = (event) => {
+ this.setState({ q: event.target.value });
+ if (this.state.errorMsg) this.setState({ errorMsg: ''});
+ if (this.state.errorSizeMsg) this.setState({ errorSizeMsg: '' });
+ if (this.state.errorDateMsg) this.setState({ errorDateMsg: '' });
+ };
+
+ handleKeyDown = (e) => {
+ if (e.keyCode === 13) {
+ e.preventDefault();
+ this.handleSubmit();
+ }
+ };
+
+ handlerRepo = (isAll) => {
+ if (isAll) {
+ this.setState({
+ isAllRepoCheck: true,
+ search_repo: 'all',
+ });
+ } else {
+ this.setState({
+ isAllRepoCheck: false,
+ search_repo: search_repo !== 'all' ? search_repo : '',
+ });
+ }
+ };
+
+ handlerFileTypes = (i) => {
+ let newFileTypeItemsStatus = this.state.fileTypeItemsStatus;
+ newFileTypeItemsStatus[i] = !this.state.fileTypeItemsStatus[i];
+ this.setState({ fileTypeItemsStatus: newFileTypeItemsStatus });
+ };
+
+ getFileTypesList = () => {
+ const fileTypeItems = ['Text', 'Document', 'Image', 'Video', 'Audio', 'PDF', 'Markdown'];
+ let ftype = [];
+ for (let i = 0, len = this.state.fileTypeItemsStatus.length; i < len; i++){
+ if (this.state.fileTypeItemsStatus[i]) {
+ ftype.push(fileTypeItems[i]);
+ }
+ }
+ return ftype;
+ };
+
+ handlerFileTypesInput = (event) => {
+ this.setState({ input_fexts: event.target.value.trim() });
+ };
+
+ handleTimeFromInput = (value) => {
+ this.setState({ time_from: value });
+ if (this.state.errorDateMsg) this.setState({ errorDateMsg: '' });
+ };
+
+ handleTimeToInput = (value) => {
+ this.setState({ time_to: value });
+ if (this.state.errorDateMsg) this.setState({ errorDateMsg: '' });
+ };
+
+ handleSizeFromInput = (event) => {
+ this.setState({ size_from: event.target.value >= 0 ? event.target.value : 0 });
+ if (this.state.errorSizeMsg) this.setState({ errorSizeMsg: '' });
+ };
+
+ handleSizeToInput = (event) => {
+ this.setState({ size_to: event.target.value >= 0 ? event.target.value : 0 });
+ if (this.state.errorSizeMsg) this.setState({ errorSizeMsg: '' });
+ };
+
+ componentDidMount() {
+ if (this.state.q) {
+ this.handleSubmit();
+ } else {
+ this.setState({ isLoading: false });
+ }
+ }
+
+ render() {
+ let { isCollapseOpen } = this.state;
+ return (
+
+
+
+
+
+
+
+ {this.state.errorMsg &&
{this.state.errorMsg}
}
+
+
+ {this.state.isLoading &&
}
+ {(!this.state.isLoading && this.state.isResultGot) &&
}
+ {(!this.state.isLoading && this.state.isResultGot) &&
+
+ }
+
+ );
+ }
+}
+
+export default SearchViewPanel;
diff --git a/frontend/src/pages/search/search-results.js b/frontend/src/pages/search/search-results.js
new file mode 100644
index 0000000000..57a37bbf20
--- /dev/null
+++ b/frontend/src/pages/search/search-results.js
@@ -0,0 +1,86 @@
+import React from 'react';
+import moment from 'moment';
+import PropTypes from 'prop-types';
+import { Utils } from '../../utils/utils';
+import { siteRoot, gettext } from '../../utils/constants';
+
+class ResultsItem extends React.Component {
+
+ constructor(props) {
+ super(props);
+ }
+
+ handlerFileURL= (item) => {
+ return item.is_dir ? siteRoot + 'library/' + item.repo_id + '/' + item.repo_name + item.fullpath :
+ siteRoot + 'lib/' + item.repo_id + '/file' + Utils.encodePath(item.fullpath);
+ };
+
+ handlerParentDirPath= (item) => {
+ let index = item.is_dir ? item.fullpath.length - item.name.length - 1 : item.fullpath.length - item.name.length;
+ return item.fullpath.substring(0, index);
+ };
+
+ handlerParentDirURL= (item) => {
+ return siteRoot + 'library/' + item.repo_id + '/' + item.repo_name + this.handlerParentDirPath(item);
+ };
+
+ render() {
+ let item = this.props.item;
+ let linkContent = decodeURI(item.fullpath).substring(1);
+ let folderIconUrl = linkContent ? Utils.getFolderIconUrl(false, 192) : Utils.getDefaultLibIconUrl(true);
+ let fileIconUrl = item.is_dir ? folderIconUrl : Utils.getFileIconUrl(item.name, 192);
+ return (
+
+
+
+
+
+
+ {Utils.bytesToSize(item.size) + ' ' + moment(item.last_modified * 1000).format('YYYY-MM-DD')}
+
+
+
+
+ );
+ }
+}
+
+const resultsItemPropTypes = {
+ item: PropTypes.object.isRequired,
+};
+
+ResultsItem.propTypes = resultsItemPropTypes;
+
+class SearchResults extends React.Component {
+
+ constructor(props) {
+ super(props);
+ }
+
+ render() {
+ const { resultItems } = this.props;
+ const total = resultItems.length;
+ return (
+
+
+ {total > 0 ? (total + ' ' + (total === 1 ? gettext('result') : gettext('results'))) : gettext('No result')}
+ {resultItems.map((item, index) => {
+ return ;
+ })}
+
+
+ );
+ }
+}
+
+const searchResultsPropTypes = {
+ resultItems: PropTypes.array.isRequired,
+};
+
+SearchResults.propTypes = searchResultsPropTypes;
+
+export default SearchResults;