mirror of
https://github.com/haiwen/seahub.git
synced 2025-05-10 00:47:19 +00:00
Remove search single page (#7339)
* 01 remove jquery-ui css * 02 remove search page * 03 remove pubuser_search
This commit is contained in:
parent
14e56ee8b9
commit
f7f69aa910
frontend
config
src
components/search
css
pages/search
media/css
seahub
@ -40,7 +40,6 @@ const entryFiles = {
|
||||
repoFolderTrash: '/repo-folder-trash.js',
|
||||
orgAdmin: '/pages/org-admin',
|
||||
sysAdmin: '/pages/sys-admin',
|
||||
search: '/pages/search',
|
||||
uploadLink: '/pages/upload-link',
|
||||
subscription: '/subscription.js',
|
||||
institutionAdmin: '/pages/institution-admin/index.js'
|
||||
|
@ -5,7 +5,7 @@ import classnames from 'classnames';
|
||||
import MediaQuery from 'react-responsive';
|
||||
import { seafileAPI } from '../../utils/seafile-api';
|
||||
import searchAPI from '../../utils/search-api';
|
||||
import { gettext, siteRoot } from '../../utils/constants';
|
||||
import { gettext } from '../../utils/constants';
|
||||
import SearchResultItem from './search-result-item';
|
||||
import SearchResultLibrary from './search-result-library';
|
||||
import { Utils } from '../../utils/utils';
|
||||
@ -13,7 +13,6 @@ import toaster from '../toast';
|
||||
import Loading from '../loading';
|
||||
import { SEARCH_MASK, SEARCH_CONTAINER } from '../../constants/zIndexes';
|
||||
import { PRIVATE_FILE_TYPE } from '../../constants';
|
||||
import Icon from '../icon';
|
||||
|
||||
const propTypes = {
|
||||
repoID: PropTypes.string,
|
||||
@ -35,7 +34,6 @@ class Search extends Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.baseSearchPageURL = `${siteRoot}search/`;
|
||||
this.state = {
|
||||
width: 'default',
|
||||
value: '',
|
||||
@ -44,13 +42,11 @@ class Search extends Component {
|
||||
highlightIndex: 0,
|
||||
page: 0,
|
||||
isLoading: false,
|
||||
hasMore: false,
|
||||
isMaskShow: false,
|
||||
showRecent: true,
|
||||
isResultGotten: false,
|
||||
isCloseShow: false,
|
||||
isSearchInputShow: false, // for mobile
|
||||
searchPageUrl: this.baseSearchPageURL,
|
||||
searchTypesMax: 0,
|
||||
highlightSearchTypesIndex: 0,
|
||||
};
|
||||
@ -367,7 +363,6 @@ class Search extends Component {
|
||||
inputValue: newValue,
|
||||
isLoading: false,
|
||||
highlightIndex: 0,
|
||||
// resultItems: [],
|
||||
isResultGotten: false,
|
||||
}, () => {
|
||||
if (!isInRepo && trimmedValue !== '') {
|
||||
@ -392,7 +387,6 @@ class Search extends Component {
|
||||
this.setState({
|
||||
resultItems: results,
|
||||
isLoading: false,
|
||||
hasMore: false,
|
||||
});
|
||||
}).catch(error => {
|
||||
// eslint-disable-next-line no-console
|
||||
@ -428,7 +422,6 @@ class Search extends Component {
|
||||
isResultGotten: true,
|
||||
page: page + 1,
|
||||
isLoading: false,
|
||||
hasMore: res.data.has_more,
|
||||
});
|
||||
} else {
|
||||
this.setState({
|
||||
@ -436,7 +429,6 @@ class Search extends Component {
|
||||
resultItems: [],
|
||||
isLoading: false,
|
||||
isResultGotten: true,
|
||||
hasMore: res.data.has_more,
|
||||
});
|
||||
}
|
||||
}).catch(error => {
|
||||
@ -450,7 +442,6 @@ class Search extends Component {
|
||||
};
|
||||
|
||||
onNormalSearch = (queryData, cancelToken, page) => {
|
||||
this.updateSearchPageURL(queryData);
|
||||
queryData['per_page'] = PER_PAGE;
|
||||
queryData['page'] = page;
|
||||
seafileAPI.searchFiles(queryData, cancelToken).then(res => {
|
||||
@ -461,7 +452,6 @@ class Search extends Component {
|
||||
isResultGotten: true,
|
||||
isLoading: false,
|
||||
page: page + 1,
|
||||
hasMore: res.data.has_more,
|
||||
});
|
||||
return;
|
||||
}
|
||||
@ -470,7 +460,6 @@ class Search extends Component {
|
||||
resultItems: [],
|
||||
isLoading: false,
|
||||
isResultGotten: true,
|
||||
hasMore: res.data.has_more,
|
||||
});
|
||||
}).catch(error => {
|
||||
/* eslint-disable */
|
||||
@ -479,14 +468,6 @@ class Search extends Component {
|
||||
});
|
||||
};
|
||||
|
||||
updateSearchPageURL(queryData) {
|
||||
let params = '';
|
||||
for (let key in queryData) {
|
||||
params += key + '=' + encodeURIComponent(queryData[key]) + '&';
|
||||
}
|
||||
this.setState({searchPageUrl: `${this.baseSearchPageURL}?${params.substring(0, params.length - 1)}`});
|
||||
}
|
||||
|
||||
formatResultItems(data) {
|
||||
let items = [];
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
@ -681,7 +662,7 @@ class Search extends Component {
|
||||
};
|
||||
|
||||
renderResults = (resultItems, isVisited) => {
|
||||
const { highlightIndex, hasMore, searchPageUrl } = this.state;
|
||||
const { highlightIndex } = this.state;
|
||||
|
||||
const results = (
|
||||
<>
|
||||
@ -700,12 +681,6 @@ class Search extends Component {
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
{(!this.props.isPublic && hasMore) &&
|
||||
<div className="more-search-result mb-1 pl-2 d-flex align-items-center">
|
||||
<Icon symbol="more-level" className="more-search-result-icon" />
|
||||
<a href={searchPageUrl} className="more-search-result-text ml-1">{gettext('More')}</a>
|
||||
</div>
|
||||
}
|
||||
</>
|
||||
);
|
||||
|
||||
|
@ -451,18 +451,3 @@
|
||||
font-size: 14px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.search-result-container .more-search-result {
|
||||
margin-top: -4px;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.search-result-container .more-search-result-icon {
|
||||
width: 1rem;
|
||||
color: #ec8000;
|
||||
margin: 0 10px;
|
||||
}
|
||||
|
||||
.search-result-container .more-search-result-text {
|
||||
font-size: .875rem;
|
||||
}
|
||||
|
@ -1,291 +0,0 @@
|
||||
import React, { Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import MediaQuery from 'react-responsive';
|
||||
import dayjs from 'dayjs';
|
||||
import { Button, Col, Collapse, CustomInput, FormGroup, Input, Label, Row, InputGroupAddon, InputGroup } from 'reactstrap';
|
||||
import { gettext } from '../../utils/constants';
|
||||
import DateTimePicker from '../../components/date-and-time-picker';
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
disabledStartDate = (startValue) => {
|
||||
if (!startValue) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const isAfterToday = startValue.isAfter(dayjs(), 'day');
|
||||
const { time_to } = this.props.stateAndValues;
|
||||
const endValue = time_to;
|
||||
if (!endValue) {
|
||||
return isAfterToday;
|
||||
}
|
||||
return startValue.isAfter(endValue) || isAfterToday;
|
||||
};
|
||||
|
||||
disabledEndDate = (endValue) => {
|
||||
if (!endValue) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const isAfterToday = endValue.isAfter(dayjs(), 'day');
|
||||
const { time_from } = this.props.stateAndValues;
|
||||
const startValue = time_from;
|
||||
if (!startValue) {
|
||||
return isAfterToday;
|
||||
}
|
||||
return endValue.isBefore(startValue) || isAfterToday;
|
||||
};
|
||||
|
||||
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 == 'all' ? gettext('All') : repo_name}</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.format('YYYY-MM-DD')}{' '}{gettext('to')}{' '}{time_to.format('YYYY-MM-DD')}</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="PDF" inline
|
||||
onChange={() => this.props.handlerFileTypes(5)}
|
||||
checked={stateAndValues.fileTypeItemsStatus[5]}/>
|
||||
<CustomInput
|
||||
type="checkbox" id="checkMarkdown" label="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">
|
||||
<DateTimePicker
|
||||
inputWidth={'100%'}
|
||||
disabledDate={this.disabledStartDate}
|
||||
value={stateAndValues.time_from}
|
||||
onChange={this.props.handleTimeFromInput}
|
||||
showHourAndMinute={false}
|
||||
/>
|
||||
<span className="select-data-icon"><i className="sf3-font sf3-font-calendar-alt"></i></span>
|
||||
</Col>
|
||||
<div className="mt-2">-</div>
|
||||
<Col md="4" lg="4" sm="4" xs="5" className="position-relative">
|
||||
<DateTimePicker
|
||||
inputWidth={'100%'}
|
||||
disabledDate={this.disabledEndDate}
|
||||
value={stateAndValues.time_to}
|
||||
onChange={this.props.handleTimeToInput}
|
||||
showHourAndMinute={false}
|
||||
/>
|
||||
<span className="select-data-icon"><i className="sf3-font sf3-font-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: 767.8px)">
|
||||
{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>
|
||||
</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;
|
@ -1,42 +0,0 @@
|
||||
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'));
|
@ -1,351 +0,0 @@
|
||||
import React from 'react';
|
||||
import deepCopy from 'deep-copy';
|
||||
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 { q, 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: null,
|
||||
time_to: null,
|
||||
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,
|
||||
});
|
||||
this.onNormalSearch(params);
|
||||
}
|
||||
|
||||
onNormalSearch = (params) => {
|
||||
const stateHistory = deepCopy(this.state);
|
||||
seafileAPI.searchFiles(params, null).then(res => {
|
||||
const { results, has_more, total } = res.data;
|
||||
this.setState({
|
||||
isLoading: false,
|
||||
isResultGot: true,
|
||||
resultItems: results,
|
||||
hasMore: has_more,
|
||||
total: total,
|
||||
page: params.page,
|
||||
isShowSearchFilter: true,
|
||||
});
|
||||
this.stateHistory = stateHistory;
|
||||
this.stateHistory.resultItems = results;
|
||||
this.stateHistory.hasMore = 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;}
|
||||
const { time_from, time_to } = this.state;
|
||||
if (time_from) {params.time_from = parseInt(time_from.valueOf() / 1000);}
|
||||
if (time_to) {params.time_to = parseInt(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.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);
|
||||
}
|
||||
if (this.state.isCollapseOpen) this.setState({ isCollapseOpen: false });
|
||||
};
|
||||
|
||||
compareNumber = (num1, num2) => {
|
||||
if (!num1 || !num2) return false;
|
||||
// eslint-disable-next-line
|
||||
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: null,
|
||||
time_to: null,
|
||||
size_from: '',
|
||||
size_to: '',
|
||||
errorMsg: '',
|
||||
errorDateMsg: '',
|
||||
errorSizeMsg: '',
|
||||
});
|
||||
};
|
||||
|
||||
handlePrevious = (e) => {
|
||||
e.preventDefault();
|
||||
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 = (e) => {
|
||||
e.preventDefault();
|
||||
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 = 0;
|
||||
let 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) => {
|
||||
// set the time to be '00:00:00'
|
||||
this.setState({ time_from: value ? value.hours(0).minutes(0).seconds(0) : value });
|
||||
if (this.state.errorDateMsg) this.setState({ errorDateMsg: '' });
|
||||
};
|
||||
|
||||
handleTimeToInput = (value) => {
|
||||
// set the time to be '23:59:59'
|
||||
this.setState({ time_to: value ? value.hours(23).minutes(59).seconds(59) : 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 sf3-font sf3-font-search" onClick={this.handleSubmit}></i>
|
||||
<i className={`action-icon sf3-font sf3-font-angles-${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}
|
||||
total={this.state.total}
|
||||
/>
|
||||
}
|
||||
{(!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;
|
@ -1,91 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import dayjs from 'dayjs';
|
||||
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();
|
||||
let fileIconUrl = item.is_dir ? folderIconUrl : Utils.getFileIconUrl(item.name);
|
||||
|
||||
if (item.thumbnail_url !== '') {
|
||||
fileIconUrl = item.thumbnail_url;
|
||||
}
|
||||
|
||||
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} rel="noreferrer">{item.name}</a>
|
||||
</div>
|
||||
<div className="item-link ellipsis">
|
||||
<a href={this.handlerParentDirURL(item)} target="_blank" rel="noreferrer" >{item.repo_name}{this.handlerParentDirPath(item)}</a>
|
||||
</div>
|
||||
<div className="item-link ellipsis">
|
||||
{Utils.bytesToSize(item.size) + ' ' + dayjs(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, total } = this.props;
|
||||
return (
|
||||
<div className="search-result-container position-static">
|
||||
<p className="tip">{total > 0 ? (total + ' ' + (total === 1 ? gettext('result') : gettext('results'))) : gettext('No result')}</p>
|
||||
<ul className="search-result-list">
|
||||
{resultItems.map((item, index) => {
|
||||
return <ResultsItem key={index} item={item}/>;
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const searchResultsPropTypes = {
|
||||
resultItems: PropTypes.array.isRequired,
|
||||
total: PropTypes.number.isRequired
|
||||
};
|
||||
|
||||
SearchResults.propTypes = searchResultsPropTypes;
|
||||
|
||||
export default SearchResults;
|
18
media/css/jquery-ui.datepicker.min.css
vendored
18
media/css/jquery-ui.datepicker.min.css
vendored
File diff suppressed because one or more lines are too long
@ -1,266 +0,0 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% load seahub_tags avatar_tags i18n static %}
|
||||
|
||||
|
||||
{% block extra_style %}
|
||||
<link rel="stylesheet" type="text/css" href="{{MEDIA_URL}}css/jquery-ui.datepicker.min.css" />
|
||||
<style type="text/css">
|
||||
.search-results-file-icon-container {
|
||||
width:36px;
|
||||
text-align:center;
|
||||
}
|
||||
.search-results-file-icon {
|
||||
max-width:36px;
|
||||
max-height:36px;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block main_content %}
|
||||
<div class="row">
|
||||
<div class="col-sm-12 col-md-8 col-md-offset-2">
|
||||
<form method="get" action="{% url 'search' %}" class="search-form" id="search-form">
|
||||
<div class="input_and_submit">
|
||||
<input type="text" class="search-input" name="q" placeholder="{% trans "Search Files" %}" title="{% trans "Search Files" %}" aria-label="{% trans "Search Files" %}" value="{{ keyword }}" />
|
||||
<button type="submit" class="search-submit" aria-label="{% trans "Submit" %}"><span class="icon-search"></span></button>
|
||||
</div>
|
||||
<span class="advanced-search vam icon-double-angle-{% if custom_search %}down{% else %}up{% endif %}" title="{% trans "advanced" %}"></span>
|
||||
<div class="advs hide">
|
||||
<div class="search-time-range">
|
||||
<span>{% trans "Last Update" %}</span><br />
|
||||
<input type="text" name="date_from" value="{{date_from}}" class="input" placeholder="yyyy-mm-dd" />
|
||||
<input type="hidden" name="time_from" value="" />
|
||||
<span> - </span>
|
||||
<input type="text" name="date_to" value="{{date_to}}" class="input" placeholder="yyyy-mm-dd" />
|
||||
<input type="hidden" name="time_to" value="" />
|
||||
</div>
|
||||
<div class="search-size-range">
|
||||
<span>{% trans "Size" %}</span><br />
|
||||
<input type="text" name="size_from_mb" value="{{size_from_mb}}" class="input" />
|
||||
<input type="hidden" name="size_from" />
|
||||
<span> - </span>
|
||||
<input type="text" name="size_to_mb" value="{{size_to_mb}}" class="input" /> MB
|
||||
<input type="hidden" name="size_to" class="input" />
|
||||
</div>
|
||||
<p class="error form-error hide"></p>
|
||||
<input type="submit" value="{% trans "Submit" %}" class="submit" />
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div id="search-results">
|
||||
{% if error %}
|
||||
<p class="error">{{error_msg}}</p>
|
||||
{% else %}
|
||||
{% if not results %}
|
||||
<p>{% trans 'No result found' %}</p>
|
||||
{% else %}
|
||||
<p class="tip">{% blocktrans count counter=total %}{{ total }} result{% plural %}{{ total }} results{% endblocktrans%}</p>
|
||||
<ul id="search-results-list">
|
||||
{% for file in results %}
|
||||
{% if file.is_dir %}
|
||||
<li class="search-results-item ovhd">
|
||||
<img src="{{ MEDIA_URL }}img/folder-192.png" width="36" height="36" alt="{% trans "Folder icon"%}" class="fleft" />
|
||||
{% else %}
|
||||
<li class="search-results-item search-results-file-item ovhd" data-repoid="{{file.repo.id}}" data-path="{{file.fullpath}}" data-name="{{file.name}}">
|
||||
<div class="fleft search-results-file-icon-container">
|
||||
<img src="{{ MEDIA_URL }}img/file/{{ file.name|file_icon_filter }}" width="36" height="36" alt="{% trans "File"%}" class="search-results-file-icon" />
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="main-con">
|
||||
{% if file.is_dir %}
|
||||
<a href="{{ SITE_ROOT }}library/{{ file.repo.id }}/{{ file.repo.name|urlencode }}/{{ file.fullpath|strip_slash|urlencode }}" target="_blank" title="{{ file.fullpath|slice:'1:'}}">{{ file.name }}</a>
|
||||
{% else %}
|
||||
<a href="{% url 'view_lib_file' file.repo.id file.fullpath %}" target="_blank" title="{{ file.fullpath|slice:'1:'}}">{{ file.name }}</a>
|
||||
{% endif %}
|
||||
<br />
|
||||
{% if file.parent_dir == '/' %}
|
||||
<a href="{{ SITE_ROOT }}library/{{ file.repo.id }}/{{ file.repo.name|urlencode }}" target="_blank" class="parent-dir-link">{{ file.repo.name }}/</a>
|
||||
{% else %}
|
||||
<a href="{{ SITE_ROOT }}library/{{ file.repo.id }}/{{ file.repo.name|urlencode }}{{ file.parent_dir|urlencode }}" target="_blank" class="parent-dir-link">{{ file.repo.name }}{{ file.parent_dir }}</a>
|
||||
{% endif %}
|
||||
<br />
|
||||
{% if file.is_dir %}
|
||||
<span class="time">{{ file.last_modified|translate_seahub_time }}</span>
|
||||
{% else %}
|
||||
<span class="time">{{ file.size|filesizeformat }} {{ file.last_modified|translate_seahub_time }}</span>
|
||||
{% endif %}
|
||||
{% if file.content_highlight %}
|
||||
<p class="highlight-content">{{ file.content_highlight|safe }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% if total > per_page %}
|
||||
<div id="paginator">
|
||||
{% if current_page != 1 %}
|
||||
<a href="#" data-page="{{ prev_page }}" class="prev">{% trans "Previous"%}</a>
|
||||
{% endif %}
|
||||
{% if has_more %}
|
||||
<a href="#" data-page="{{ next_page }}" class="next">{% trans "Next"%}</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_script %}
|
||||
<script type="text/javascript" src="{% static "scripts/lib/jquery-ui.min.js" %}"></script>
|
||||
<script type="text/javascript">
|
||||
(function() {
|
||||
var form = $('#search-form'),
|
||||
advs = $('.advs', form);
|
||||
advs.prepend($('#advanced-search-form .search-scales')
|
||||
.clone(true)
|
||||
.append($('.search-time-range, .search-size-range')));
|
||||
|
||||
{% if custom_search %}
|
||||
advs.removeClass('hide');
|
||||
{% endif %}
|
||||
|
||||
// date picker for date-custom
|
||||
$.datepicker.setDefaults({
|
||||
hideIfNoPrevNext: true,
|
||||
maxDate: 0, // today (The maximum selectable date)
|
||||
dateFormat: 'yy-mm-dd'
|
||||
});
|
||||
$('.search-time-range .input').datepicker();
|
||||
|
||||
form.find('.advanced-search').on('click', function() {
|
||||
advs.toggleClass('hide');
|
||||
var it = $(this),
|
||||
str = 'icon-double-angle-';
|
||||
if (it.hasClass(str + 'down')) {
|
||||
it.removeClass(str + 'down').addClass(str + 'up');
|
||||
} else {
|
||||
it.removeClass(str + 'up').addClass(str + 'down');
|
||||
}
|
||||
return false;
|
||||
});
|
||||
{% if custom_ftypes %}
|
||||
var custom_ftypes = [];
|
||||
{% for f in custom_ftypes %}
|
||||
custom_ftypes.push("{{f}}");
|
||||
{% endfor %}
|
||||
var ftype_options = form.find('[name="ftype"]');
|
||||
ftype_options.each(function() {
|
||||
if (custom_ftypes.indexOf($(this).val()) != -1) {
|
||||
$(this).attr('checked', true);
|
||||
$(this).parent().addClass('checkbox-checked');
|
||||
}
|
||||
});
|
||||
{% endif %}
|
||||
|
||||
form.on('submit', function() {
|
||||
var $error = $('.form-error', form);
|
||||
|
||||
// for time range
|
||||
var date_from = $.trim($('[name="date_from"]', form).val());
|
||||
var date_to = $.trim($('[name="date_to"]', form).val());
|
||||
// 'date' can be picked from datepicker, also directly input
|
||||
var date_pattern = /^([012]\d{3})\-(0[1-9]|1[012])\-(0[1-9]|[12]\d|3[01])$/;
|
||||
if (date_from && !date_pattern.test(date_from)) {
|
||||
$error.html("{% trans "Invalid start date, should be yyyy-mm-dd" %}").removeClass('hide');
|
||||
return false;
|
||||
}
|
||||
if (date_to && !date_pattern.test(date_to)) {
|
||||
$error.html("{% trans "Invalid end date, should be yyyy-mm-dd" %}").removeClass('hide');
|
||||
return false;
|
||||
}
|
||||
if (date_from && date_to && date_from > date_to) {
|
||||
$error.html("{% trans "Start date should be earlier than end date." %}").removeClass('hide');
|
||||
return false;
|
||||
}
|
||||
if (date_from) {
|
||||
$('[name="time_from"]', form).val(Date.parse(date_from)/1000); // in second
|
||||
}
|
||||
if (date_to) {
|
||||
$('[name="time_to"]', form).val(Date.parse(date_to)/1000 + (24*60*60 - 1)); // till 23:59:59
|
||||
}
|
||||
|
||||
// for size range
|
||||
var size_from_mb = $.trim($('[name="size_from_mb"]', form).val());
|
||||
var size_to_mb = $.trim($('[name="size_to_mb"]', form).val());
|
||||
var $size_from = $('[name="size_from"]', form);
|
||||
var $size_to = $('[name="size_to"]', form);
|
||||
if (size_from_mb) {
|
||||
$size_from.val(size_from_mb * 1000 * 1000); // turn MB to B
|
||||
}
|
||||
if (size_to_mb) {
|
||||
$size_to.val(size_to_mb * 1000 * 1000); // turn MB to B
|
||||
}
|
||||
});
|
||||
|
||||
// modify pagination link urls
|
||||
var $paginator = $('#paginator');
|
||||
var $prev_next = $('.prev, .next', $paginator);
|
||||
var url_params = location.search.substr(1).split('&');
|
||||
var url_param_names = [];
|
||||
for (var i = 0, len = url_params.length; i < len; i++) {
|
||||
url_param_names.push(url_params[i].split('=')[0]);
|
||||
}
|
||||
if (url_param_names.indexOf('page') != -1) {
|
||||
$prev_next.each(function() {
|
||||
$(this).attr('href', location.search.replace('page={{current_page}}', 'page=' + $(this).attr('data-page')));
|
||||
});
|
||||
} else {
|
||||
$prev_next.each(function() {
|
||||
$(this).attr('href', location.search + '&page=' + $(this).attr('data-page'));
|
||||
});
|
||||
}
|
||||
})();
|
||||
|
||||
function imageCheck(filename) {
|
||||
// no file ext
|
||||
if (filename.lastIndexOf('.') == -1) {
|
||||
return false;
|
||||
}
|
||||
var file_ext = filename.substr(filename.lastIndexOf('.') + 1).toLowerCase();
|
||||
var image_exts = ['gif', 'jpeg', 'jpg', 'png', 'ico', 'bmp', 'tif', 'tiff', 'jfif', 'heic', 'webp'];
|
||||
if (image_exts.indexOf(file_ext) != -1) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
function getThumbnail(i) {
|
||||
var $item = $(img_file_items[i]);
|
||||
$.ajax({
|
||||
url: '{{SITE_ROOT}}thumbnail/' + $item.attr('data-repoid') + '/create/',
|
||||
data: {
|
||||
'path': $item.attr('data-path'),
|
||||
'size': '{{thumbnail_size}}'
|
||||
},
|
||||
cache: false,
|
||||
dataType: 'json',
|
||||
success: function(data) {
|
||||
$('.search-results-file-icon', $item)
|
||||
.attr('src', '{{SITE_ROOT}}' + data.encoded_thumbnail_src)
|
||||
.removeAttr('width height');
|
||||
},
|
||||
complete: function() {
|
||||
if (i < img_file_items.length - 1) {
|
||||
getThumbnail(++i);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
var img_file_items = [];
|
||||
$('.search-results-file-item').each(function(index, item) {
|
||||
var file_name = $(item).attr('data-name');
|
||||
var file_is_img = imageCheck(file_name);
|
||||
if (file_is_img) {
|
||||
img_file_items.push(item);
|
||||
}
|
||||
});
|
||||
if (img_file_items.length) {
|
||||
getThumbnail(0);
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
@ -1,20 +0,0 @@
|
||||
{% extends "base_for_react.html" %}
|
||||
{% load render_bundle from webpack_loader %}
|
||||
|
||||
{% block extra_style %}
|
||||
{% render_bundle 'search' 'css' %}
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_script %}
|
||||
<script type="text/javascript">
|
||||
window.search = {
|
||||
pageOptions: {
|
||||
q: '{{ keyword|escapejs }}',
|
||||
search_repo: '{{ search_repo|escapejs }}',
|
||||
search_ftypes: '{{ search_ftypes|escapejs }}',
|
||||
repo_name:'{{ repo.name|escapejs }}',
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% render_bundle 'search' 'js' %}
|
||||
{% endblock %}
|
@ -238,25 +238,6 @@ def ai_search_wikis(params):
|
||||
resp = requests.post(url, json=params, headers=headers)
|
||||
return resp
|
||||
|
||||
|
||||
def is_valid_date_type(data):
|
||||
try:
|
||||
data = int(data)
|
||||
except:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
def is_valid_size_type(data):
|
||||
try:
|
||||
data = int(data)
|
||||
if data < 0:
|
||||
raise
|
||||
except:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
SEARCH_REPOS_LIMIT = 200
|
||||
RELATED_REPOS_PREFIX = 'RELATED_REPOS_'
|
||||
RELATED_REPOS_CACHE_TIMEOUT = 2 * 60 * 60
|
||||
|
@ -1,305 +0,0 @@
|
||||
# Copyright (c) 2012-2016 Seafile Ltd.
|
||||
import os
|
||||
import logging
|
||||
|
||||
from django.urls import reverse
|
||||
from django.http import HttpResponseRedirect, Http404
|
||||
from django.shortcuts import render
|
||||
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from seaserv import seafile_api, ccnet_api
|
||||
|
||||
from seahub.auth.decorators import login_required
|
||||
from seahub.contacts.models import Contact
|
||||
from seahub.profile.models import Profile
|
||||
from seahub.utils import is_org_context
|
||||
from seahub.utils.repo import is_valid_repo_id_format
|
||||
from seahub.views import check_folder_permission
|
||||
from seahub.settings import THUMBNAIL_SIZE_FOR_GRID
|
||||
from seahub.search.utils import search_files, get_search_repos_map, \
|
||||
SEARCH_FILEEXT, is_valid_date_type, is_valid_size_type
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@login_required
|
||||
def search(request):
|
||||
|
||||
custom_search = False
|
||||
invalid_argument = False
|
||||
need_return_custom_search = False
|
||||
invalid_info = {
|
||||
'error': True,
|
||||
'error_msg': _('Invalid argument.'),
|
||||
}
|
||||
|
||||
# argument check
|
||||
username = request.user.username
|
||||
org_id = request.user.org.org_id if is_org_context(request) else None
|
||||
keyword = request.GET.get('q', None)
|
||||
|
||||
try:
|
||||
current_page = int(request.GET.get('page', '1'))
|
||||
per_page = int(request.GET.get('per_page', '25'))
|
||||
except ValueError:
|
||||
current_page = 1
|
||||
per_page = 25
|
||||
|
||||
start = (current_page - 1) * per_page
|
||||
size = per_page
|
||||
if start < 0 or size < 0:
|
||||
invalid_argument = True
|
||||
|
||||
search_repo = request.GET.get('search_repo', 'all') # val: 'all' or 'repo_id'
|
||||
search_repo = search_repo.lower()
|
||||
if not is_valid_repo_id_format(search_repo) and search_repo != 'all':
|
||||
invalid_argument = True
|
||||
|
||||
search_path = request.GET.get('search_path', None)
|
||||
if search_path is not None and search_path[0] != '/':
|
||||
search_path = "/{0}".format(search_path)
|
||||
|
||||
search_ftypes = request.GET.get('search_ftypes', 'all') # val: 'all' or 'custom'
|
||||
search_ftypes = search_ftypes.lower()
|
||||
if search_ftypes not in ('all', 'custom'):
|
||||
invalid_argument = True
|
||||
|
||||
time_from = request.GET.get('time_from', '')
|
||||
time_to = request.GET.get('time_to', '')
|
||||
size_from = request.GET.get('size_from', '')
|
||||
size_to = request.GET.get('size_to', '')
|
||||
|
||||
date_from = request.GET.get('date_from', '')
|
||||
date_to = request.GET.get('date_to', '')
|
||||
size_from_mb = request.GET.get('size_from_mb', '')
|
||||
size_to_mb = request.GET.get('size_to_mb', '')
|
||||
|
||||
if time_from:
|
||||
if not is_valid_date_type(time_from) and not invalid_argument:
|
||||
need_return_custom_search = True
|
||||
invalid_argument = True
|
||||
invalid_info['error_msg'] = _('Invalid date.')
|
||||
else:
|
||||
time_from = None
|
||||
|
||||
if time_to:
|
||||
if not is_valid_date_type(time_to) and not invalid_argument:
|
||||
need_return_custom_search = True
|
||||
invalid_argument = True
|
||||
invalid_info['error_msg'] = _('Invalid date.')
|
||||
else:
|
||||
time_to = None
|
||||
|
||||
if size_from:
|
||||
if not is_valid_size_type(size_from) and not invalid_argument:
|
||||
need_return_custom_search = True
|
||||
invalid_argument = True
|
||||
invalid_info['error_msg'] = _('Invalid file size.')
|
||||
else:
|
||||
size_from = None
|
||||
|
||||
if size_to:
|
||||
if not is_valid_size_type(size_to) and not invalid_argument:
|
||||
need_return_custom_search = True
|
||||
invalid_argument = True
|
||||
invalid_info['error_msg'] = _('Invalid file size.')
|
||||
else:
|
||||
size_to = None
|
||||
|
||||
if size_to and size_from and size_to < size_from and not invalid_argument:
|
||||
invalid_argument = True
|
||||
need_return_custom_search = True
|
||||
invalid_info['error_msg'] = _('Invalid file size range.')
|
||||
|
||||
if time_to and time_from and time_to < time_from and not invalid_argument:
|
||||
invalid_argument = True
|
||||
need_return_custom_search = True
|
||||
invalid_info['error_msg'] = _('Invalid date range.')
|
||||
|
||||
time_range = (time_from, time_to)
|
||||
size_range = (size_from, size_to)
|
||||
suffixes = None
|
||||
custom_ftypes = request.GET.getlist('ftype') # types like 'Image', 'Video'... same in utils/file_types.py
|
||||
input_fileexts = request.GET.get('input_fexts', '') # file extension input by the user
|
||||
if search_ftypes == 'custom':
|
||||
suffixes = []
|
||||
if len(custom_ftypes) > 0:
|
||||
for ftp in custom_ftypes:
|
||||
if ftp in SEARCH_FILEEXT:
|
||||
for ext in SEARCH_FILEEXT[ftp]:
|
||||
suffixes.append(ext)
|
||||
|
||||
if input_fileexts:
|
||||
input_fexts = input_fileexts.split(',')
|
||||
for i_ext in input_fexts:
|
||||
i_ext = i_ext.strip()
|
||||
if i_ext:
|
||||
suffixes.append(i_ext)
|
||||
|
||||
range_args = [time_from, time_to, size_from, size_to]
|
||||
if search_repo != 'all' or search_ftypes == 'custom' or any(e for e in range_args):
|
||||
custom_search = True
|
||||
|
||||
if invalid_argument:
|
||||
if need_return_custom_search:
|
||||
invalid_info['keyword'] = keyword
|
||||
invalid_info['search_repo'] = search_repo
|
||||
invalid_info['search_ftypes'] = search_ftypes
|
||||
invalid_info['custom_ftypes'] = custom_ftypes
|
||||
invalid_info['input_fileexts'] = input_fileexts
|
||||
invalid_info['custom_search'] = custom_search
|
||||
invalid_info['date_from'] = date_from
|
||||
invalid_info['date_to'] = date_to
|
||||
invalid_info['size_from_mb'] = size_from_mb
|
||||
invalid_info['size_to_mb'] = size_to_mb
|
||||
return render(request, 'search_results.html', invalid_info)
|
||||
|
||||
repo_id_map = {}
|
||||
# check recourse and permissin when search in a single repo
|
||||
if is_valid_repo_id_format(search_repo):
|
||||
repo_id = search_repo
|
||||
repo = seafile_api.get_repo(repo_id)
|
||||
# recourse check
|
||||
if not repo:
|
||||
data = {
|
||||
'error': True,
|
||||
'error_msg': _('Library %s not found.') % repo_id
|
||||
}
|
||||
return render(request, 'search_results.html', data)
|
||||
|
||||
# permission check
|
||||
if not check_folder_permission(request, repo_id, '/'):
|
||||
data = {
|
||||
'error': True,
|
||||
'error_msg': _('Permission denied.')
|
||||
}
|
||||
return render(request, 'search_results.html', data)
|
||||
map_id = repo.origin_repo_id if repo.origin_repo_id else repo_id
|
||||
repo_id_map[map_id] = repo
|
||||
else:
|
||||
shared_from = request.GET.get('shared_from', None)
|
||||
not_shared_from = request.GET.get('not_shared_from', None)
|
||||
repo_id_map, repo_type_map = get_search_repos_map(search_repo,
|
||||
username, org_id, shared_from, not_shared_from)
|
||||
|
||||
obj_desc = {
|
||||
'suffixes': suffixes,
|
||||
'time_range': time_range,
|
||||
'size_range': size_range
|
||||
}
|
||||
# search file
|
||||
try:
|
||||
if keyword:
|
||||
results, total = search_files(repo_id_map, search_path, keyword, obj_desc, start, size, org_id)
|
||||
else:
|
||||
results, total, keyword = [], 0, ''
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
data = {
|
||||
'error': True,
|
||||
'error_msg': _('Internal Server Error')
|
||||
}
|
||||
return render(request, 'search_results.html', data)
|
||||
|
||||
has_more = True if total > current_page * per_page else False
|
||||
for r in results:
|
||||
r['parent_dir'] = os.path.dirname(r['fullpath'].rstrip('/'))
|
||||
|
||||
response_dict = {
|
||||
'repo': repo if is_valid_repo_id_format(search_repo) else None,
|
||||
'keyword': keyword,
|
||||
'results': results,
|
||||
'total': total,
|
||||
'has_more': has_more,
|
||||
'current_page': current_page,
|
||||
'prev_page': current_page - 1,
|
||||
'next_page': current_page + 1,
|
||||
'per_page': per_page,
|
||||
'search_repo': search_repo,
|
||||
'search_ftypes': search_ftypes,
|
||||
'custom_ftypes': custom_ftypes,
|
||||
'input_fileexts': input_fileexts,
|
||||
'error': False,
|
||||
'thumbnail_size': THUMBNAIL_SIZE_FOR_GRID,
|
||||
'date_from': date_from,
|
||||
'date_to': date_to,
|
||||
'size_from_mb': size_from_mb,
|
||||
'size_to_mb': size_to_mb,
|
||||
'custom_search': custom_search
|
||||
}
|
||||
|
||||
# Whether use new index page
|
||||
use_new_page = True
|
||||
if request.GET.get('_old', None):
|
||||
use_new_page = False
|
||||
|
||||
if use_new_page:
|
||||
return render(request, 'search_results_react.html', response_dict)
|
||||
|
||||
return render(request, 'search_results.html', response_dict)
|
||||
|
||||
@login_required
|
||||
def pubuser_search(request):
|
||||
can_search = False
|
||||
if is_org_context(request):
|
||||
can_search = True
|
||||
elif request.cloud_mode:
|
||||
# Users are not allowed to search public user when in cloud mode.
|
||||
can_search = False
|
||||
else:
|
||||
can_search = True
|
||||
|
||||
if can_search is False:
|
||||
raise Http404
|
||||
|
||||
email_or_nickname = request.GET.get('search', '')
|
||||
if not email_or_nickname:
|
||||
return HttpResponseRedirect(reverse('pubuser'))
|
||||
|
||||
# Get user's contacts, used in show "add to contacts" button.
|
||||
username = request.user.username
|
||||
contacts = Contact.objects.get_contacts_by_user(username)
|
||||
contact_emails = [request.user.username]
|
||||
for c in contacts:
|
||||
contact_emails.append(c.contact_email)
|
||||
|
||||
search_result = []
|
||||
# search by username
|
||||
if is_org_context(request):
|
||||
url_prefix = request.user.org.url_prefix
|
||||
org_users = ccnet_api.get_org_users_by_url_prefix(url_prefix, -1, -1)
|
||||
users = []
|
||||
for u in org_users:
|
||||
if email_or_nickname in u.email:
|
||||
users.append(u)
|
||||
else:
|
||||
users = ccnet_api.search_emailusers(email_or_nickname, -1, -1)
|
||||
for u in users:
|
||||
can_be_contact = True if u.email not in contact_emails else False
|
||||
search_result.append({'email': u.email,
|
||||
'can_be_contact': can_be_contact})
|
||||
|
||||
# search by nickname
|
||||
if is_org_context(request):
|
||||
url_prefix = request.user.org.url_prefix
|
||||
org_users = ccnet_api.get_org_users_by_url_prefix(url_prefix, -1, -1)
|
||||
profile_all = Profile.objects.filter(user__in=[u.email for u in org_users]).values('user', 'nickname')
|
||||
else:
|
||||
profile_all = Profile.objects.all().values('user', 'nickname')
|
||||
for p in profile_all:
|
||||
if email_or_nickname in p['nickname']:
|
||||
can_be_contact = True if p['user'] not in contact_emails else False
|
||||
search_result.append({'email': p['user'],
|
||||
'can_be_contact': can_be_contact})
|
||||
|
||||
uniq_usernames = []
|
||||
for res in search_result:
|
||||
if res['email'] not in uniq_usernames:
|
||||
uniq_usernames.append(res['email'])
|
||||
else:
|
||||
search_result.remove(res)
|
||||
|
||||
return render(request, 'pubuser.html', {
|
||||
'search': email_or_nickname,
|
||||
'users': search_result,
|
||||
})
|
@ -910,14 +910,6 @@ urlpatterns += [
|
||||
re_path(r'^demo/', demo),
|
||||
]
|
||||
|
||||
from seahub.utils import HAS_FILE_SEARCH, HAS_FILE_SEASEARCH
|
||||
if HAS_FILE_SEARCH or HAS_FILE_SEASEARCH:
|
||||
from seahub.search.views import search, pubuser_search
|
||||
urlpatterns += [
|
||||
path('search/', search, name='search'),
|
||||
path('pubinfo/users/search/', pubuser_search, name='pubuser_search'),
|
||||
]
|
||||
|
||||
from seahub.utils import is_pro_version
|
||||
if is_pro_version():
|
||||
urlpatterns += [
|
||||
|
Loading…
Reference in New Issue
Block a user