mirror of
https://github.com/haiwen/seahub.git
synced 2025-09-07 18:03:48 +00:00
search view by react (#3403)
* search results react * adjust style * adjust style * update
This commit is contained in:
@@ -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",
|
||||
]
|
||||
},
|
||||
|
||||
|
@@ -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: {
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
||||
|
258
frontend/src/pages/search/advanced-search.js
Normal file
258
frontend/src/pages/search/advanced-search.js
Normal file
@@ -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 (
|
||||
<div className="search-filters">
|
||||
{search_repo && <span className="mr-4">{gettext('Libraries')}{': '}{search_repo}</span>}
|
||||
{typesLength > 0 &&
|
||||
<span className="mr-4">{gettext('File Types')}{': '}
|
||||
{fileTypes.map((type, index) => {
|
||||
return <span key={index}>{type}{index !== (typesLength - 1) && ','}{' '}</span>;
|
||||
})}
|
||||
</span>
|
||||
}
|
||||
{(time_from && time_to ) &&
|
||||
<span className="mr-4">{gettext('Last Update')}{': '}{time_from}{' '}{gettext('to')}{' '}{time_to}</span>
|
||||
}
|
||||
{(size_from && size_to) &&
|
||||
<span className="mr-4">{gettext('Size')}{': '}{size_from}{'MB - '}{size_to}{'MB'}</span>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
else {
|
||||
return (
|
||||
<div className="advanced-search">
|
||||
<Collapse isOpen={stateAndValues.isCollapseOpen}>
|
||||
|
||||
{search_repo !== 'all' &&
|
||||
<div className='search-repo search-catalog'>
|
||||
<Row>
|
||||
<Col md="2" lg="2">{gettext('Libraries')}{': '}</Col>
|
||||
<Col md="4" lg="4">
|
||||
<FormGroup check>
|
||||
<Label check>
|
||||
<Input
|
||||
type="radio" name="repo"
|
||||
checked={stateAndValues.isAllRepoCheck}
|
||||
onChange={() => this.props.handlerRepo(true)}
|
||||
/>{gettext('In all libraries')}
|
||||
</Label>
|
||||
</FormGroup>
|
||||
</Col>
|
||||
<Col md="4" lg="4">
|
||||
<FormGroup check>
|
||||
<Label check>
|
||||
<Input
|
||||
type="radio" name="repo"
|
||||
checked={!stateAndValues.isAllRepoCheck}
|
||||
onChange={() => this.props.handlerRepo(false)}
|
||||
/>{repo_name}
|
||||
</Label>
|
||||
</FormGroup>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div className='search-file-types search-catalog'>
|
||||
<Row>
|
||||
<Col md="2" lg="2">{gettext('File types')}{': '}</Col>
|
||||
<Col md="4" lg="4">
|
||||
<FormGroup check>
|
||||
<Label check>
|
||||
<Input
|
||||
type="radio" name="types"
|
||||
checked={!stateAndValues.isFileTypeCollapseOpen}
|
||||
onChange={this.props.closeFileTypeCollapse}
|
||||
/>{gettext('All file types')}
|
||||
</Label>
|
||||
</FormGroup>
|
||||
</Col>
|
||||
<Col md="4" lg="4">
|
||||
<FormGroup check>
|
||||
<Label check>
|
||||
<Input
|
||||
type="radio" name="types"
|
||||
checked={stateAndValues.isFileTypeCollapseOpen}
|
||||
onChange={this.props.openFileTypeCollapse}
|
||||
/>{gettext('Custom file types')}
|
||||
</Label>
|
||||
</FormGroup>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row>
|
||||
<Col md="2" lg="2"></Col>
|
||||
<Col md="10" lg="10">
|
||||
<Collapse isOpen={stateAndValues.isFileTypeCollapseOpen}>
|
||||
<FormGroup className="search-file-types-form">
|
||||
<Fragment>
|
||||
<CustomInput
|
||||
type="checkbox" id="checkTextFiles" label={gettext('Text files')} inline
|
||||
onChange={() => this.props.handlerFileTypes(0)}
|
||||
checked={stateAndValues.fileTypeItemsStatus[0]}/>
|
||||
<CustomInput
|
||||
type="checkbox" id="checkDocuments" label={gettext('Documents')} inline
|
||||
onChange={() => this.props.handlerFileTypes(1)}
|
||||
checked={stateAndValues.fileTypeItemsStatus[1]}/>
|
||||
<CustomInput
|
||||
type="checkbox" id="checkImages" label={gettext('Images')} inline
|
||||
onChange={() => this.props.handlerFileTypes(2)}
|
||||
checked={stateAndValues.fileTypeItemsStatus[2]}/>
|
||||
<CustomInput
|
||||
type="checkbox" id="checkVideo" label={gettext('Video')} inline
|
||||
onChange={() => this.props.handlerFileTypes(3)}
|
||||
checked={stateAndValues.fileTypeItemsStatus[3]}/>
|
||||
<CustomInput
|
||||
type="checkbox" id="checkAudio" label={gettext('Audio')} inline
|
||||
onChange={() => this.props.handlerFileTypes(4)}
|
||||
checked={stateAndValues.fileTypeItemsStatus[4]}/>
|
||||
<CustomInput
|
||||
type="checkbox" id="checkPdf" label={gettext('pdf')} inline
|
||||
onChange={() => this.props.handlerFileTypes(5)}
|
||||
checked={stateAndValues.fileTypeItemsStatus[5]}/>
|
||||
<CustomInput
|
||||
type="checkbox" id="checkMarkdown" label={gettext('markdown')} inline
|
||||
onChange={() => this.props.handlerFileTypes(6)}
|
||||
checked={stateAndValues.fileTypeItemsStatus[6]}/>
|
||||
</Fragment>
|
||||
</FormGroup>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control search-input"
|
||||
name="query"
|
||||
autoComplete="off"
|
||||
placeholder={gettext("Input file extensions here, separate with ','")}
|
||||
onChange={this.props.handlerFileTypesInput}
|
||||
value={stateAndValues.input_fexts}
|
||||
onKeyDown={this.props.handleKeyDown}
|
||||
/>
|
||||
</Collapse>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
|
||||
<div className='search-date search-catalog'>
|
||||
<Row>
|
||||
<Col md="2" lg="2" className="mt-2">{gettext('Last Update')}{': '}</Col>
|
||||
<Col md="4" lg="4" sm="4" xs="5" className="position-relative">
|
||||
<SelectDate
|
||||
onDateChange={this.props.handleTimeFromInput}
|
||||
value={stateAndValues.time_from}
|
||||
locale={lang}
|
||||
/>
|
||||
<span className="select-data-icon"><i className="fa fa-calendar-alt"></i></span>
|
||||
</Col>
|
||||
<div className="mt-2">-</div>
|
||||
<Col md="4" lg="4" sm="4" xs="5" className="position-relative">
|
||||
<SelectDate
|
||||
onDateChange={this.props.handleTimeToInput}
|
||||
value={stateAndValues.time_to}
|
||||
locale={lang}
|
||||
/>
|
||||
<span className="select-data-icon"><i className="fa fa-calendar-alt"></i></span>
|
||||
</Col>
|
||||
</Row>
|
||||
{errorDateMsg && <Row><Col md="2" lg="2"></Col><Col md="8" className="error mt-2">{errorDateMsg}</Col></Row>}
|
||||
</div>
|
||||
|
||||
<div className='search-size search-catalog'>
|
||||
<Row>
|
||||
<Col md="2" lg="2" className="mt-2">{gettext('Size')}{': '}</Col>
|
||||
<Col md="4" lg="4" sm="4" xs="5">
|
||||
<FormGroup>
|
||||
<InputGroup>
|
||||
<Input
|
||||
type="tel" name="size_from"
|
||||
onKeyDown={this.props.handleKeyDown}
|
||||
onChange={this.props.handleSizeFromInput}
|
||||
value={stateAndValues.size_from}
|
||||
/>
|
||||
<InputGroupAddon addonType="append">{'MB'}</InputGroupAddon>
|
||||
</InputGroup>
|
||||
</FormGroup>
|
||||
<MediaQuery query="(min-width: 768px)">
|
||||
{errorSizeMsg && <div className="error mb-4">{errorSizeMsg}</div>}
|
||||
<Button color="primary" onClick={this.props.handleSubmit}>{gettext('Submit')}</Button>
|
||||
<Button className="ml-2" onClick={this.props.handleReset}>{gettext('Reset')}</Button>
|
||||
</MediaQuery>
|
||||
</Col>
|
||||
<div className="mt-2">-</div>
|
||||
<Col md="4" lg="4" sm="4" xs="5">
|
||||
<FormGroup>
|
||||
<InputGroup>
|
||||
<Input
|
||||
type="tel" name="size_to"
|
||||
onKeyDown={this.props.handleKeyDown}
|
||||
onChange={this.props.handleSizeToInput}
|
||||
value={stateAndValues.size_to}
|
||||
/>
|
||||
<InputGroupAddon addonType="append">{'MB'}</InputGroupAddon>
|
||||
</InputGroup>
|
||||
</FormGroup>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
<MediaQuery query="(max-width: 768px)">
|
||||
{errorSizeMsg && <div className="error mb-4">{errorSizeMsg}</div>}
|
||||
<Button className="ml-3" color="primary" onClick={this.props.handleSubmit}>{gettext('Submit')}</Button>
|
||||
<Button className="ml-2" onClick={this.props.handleReset}>{gettext('Reset')}</Button>
|
||||
</MediaQuery>
|
||||
</Collapse>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
45
frontend/src/pages/search/index.js
Normal file
45
frontend/src/pages/search/index.js
Normal file
@@ -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 (
|
||||
<div className="w-100 h-100">
|
||||
<div className="main-panel-north border-left-show">
|
||||
<Logo/>
|
||||
<CommonToolbar onSearchedClick={this.onSearchedClick}/>
|
||||
</div>
|
||||
<div className="main-panel-south">
|
||||
<SearchViewPanel/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ReactDOM.render(
|
||||
<SearchView/>,
|
||||
document.getElementById('wrapper')
|
||||
);
|
338
frontend/src/pages/search/main-panel.js
Normal file
338
frontend/src/pages/search/main-panel.js
Normal file
@@ -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 (
|
||||
<div className="search-page">
|
||||
<div className="search-page-container">
|
||||
<div className="input-icon align-items-center d-flex">
|
||||
<input
|
||||
type="text"
|
||||
className="form-control search-input"
|
||||
name="query"
|
||||
autoComplete="off"
|
||||
value={this.state.q}
|
||||
placeholder={gettext('Search Files')}
|
||||
onChange={this.handleSearchInput}
|
||||
onKeyDown={this.handleKeyDown}
|
||||
/>
|
||||
<i className="search-icon-right input-icon-addon fas fa-search" onClick={this.handleSubmit}></i>
|
||||
<i className={`fas action-icon fa-angle-double-${isCollapseOpen ? 'up' : 'down'}`} onClick={this.toggleCollapse}></i>
|
||||
</div>
|
||||
{this.state.errorMsg && <div className="error">{this.state.errorMsg}</div>}
|
||||
<AdvancedSearch
|
||||
openFileTypeCollapse={this.openFileTypeCollapse}
|
||||
closeFileTypeCollapse={this.closeFileTypeCollapse}
|
||||
handlerFileTypes={this.handlerFileTypes}
|
||||
handlerFileTypesInput={this.handlerFileTypesInput}
|
||||
handleSubmit={this.handleSubmit}
|
||||
handleReset={this.handleReset}
|
||||
handlerRepo={this.handlerRepo}
|
||||
handleKeyDown={this.handleKeyDown}
|
||||
handleTimeFromInput={this.handleTimeFromInput}
|
||||
handleTimeToInput={this.handleTimeToInput}
|
||||
handleSizeFromInput={this.handleSizeFromInput}
|
||||
handleSizeToInput={this.handleSizeToInput}
|
||||
stateAndValues={this.state}
|
||||
/>
|
||||
</div>
|
||||
{this.state.isLoading && <Loading/>}
|
||||
{(!this.state.isLoading && this.state.isResultGot) && <SearchResults resultItems={this.state.resultItems}/>}
|
||||
{(!this.state.isLoading && this.state.isResultGot) &&
|
||||
<div className="paginator">
|
||||
{this.state.page !== 1 && <a href="#" onClick={() => this.handlePrevious()}>{gettext('Previous')}</a>}
|
||||
{(this.state.page !== 1 && this.state.hasMore) && <span> | </span>}
|
||||
{this.state.hasMore && <a href="#" onClick={() => this.handleNext()}>{gettext('Next')}</a>}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default SearchViewPanel;
|
86
frontend/src/pages/search/search-results.js
Normal file
86
frontend/src/pages/search/search-results.js
Normal file
@@ -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 (
|
||||
<li className="search-result-item">
|
||||
<img className={linkContent ? 'item-img' : 'lib-item-img'} src={fileIconUrl} alt=""/>
|
||||
<div className="item-content">
|
||||
<div className="item-name ellipsis">
|
||||
<a href={this.handlerFileURL(item)} target="_blank" title={item.name}>{item.name}</a>
|
||||
</div>
|
||||
<div className="item-link ellipsis">
|
||||
<a href={this.handlerParentDirURL(item)} target="_blank" >{item.repo_name}{this.handlerParentDirPath(item)}</a>
|
||||
</div>
|
||||
<div className="item-link ellipsis">
|
||||
{Utils.bytesToSize(item.size) + ' ' + moment(item.last_modified * 1000).format('YYYY-MM-DD')}
|
||||
</div>
|
||||
<div className="item-text ellipsis" dangerouslySetInnerHTML={{__html: item.content_highlight}}></div>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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 (
|
||||
<div className="search-result-container position-static mt-4">
|
||||
<ul className="search-result-list">
|
||||
<p className="tip">{total > 0 ? (total + ' ' + (total === 1 ? gettext('result') : gettext('results'))) : gettext('No result')}</p>
|
||||
{resultItems.map((item, index) => {
|
||||
return <ResultsItem key={index} item={item}/>;
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const searchResultsPropTypes = {
|
||||
resultItems: PropTypes.array.isRequired,
|
||||
};
|
||||
|
||||
SearchResults.propTypes = searchResultsPropTypes;
|
||||
|
||||
export default SearchResults;
|
Reference in New Issue
Block a user