aboutsummaryrefslogtreecommitdiff
path: root/src/client/views/search/SearchBox.tsx
diff options
context:
space:
mode:
authorab <abdullah_ahmed@brown.edu>2019-07-23 11:38:10 -0400
committerab <abdullah_ahmed@brown.edu>2019-07-23 11:38:10 -0400
commit13c016d7f7765acda7f6ce2d69c14597469f55d7 (patch)
tree6fcf409ee4f0035443aa4fb67a05d24aae09689d /src/client/views/search/SearchBox.tsx
parentbd841fe56540e1a9177d2872310b10fefcb4acd1 (diff)
parentd880e4b2fcb4e7bab3ee25d63209b173efcf37c0 (diff)
merged
Diffstat (limited to 'src/client/views/search/SearchBox.tsx')
-rw-r--r--src/client/views/search/SearchBox.tsx225
1 files changed, 186 insertions, 39 deletions
diff --git a/src/client/views/search/SearchBox.tsx b/src/client/views/search/SearchBox.tsx
index 393a5a924..2214ac8af 100644
--- a/src/client/views/search/SearchBox.tsx
+++ b/src/client/views/search/SearchBox.tsx
@@ -1,38 +1,49 @@
import * as React from 'react';
import { observer } from 'mobx-react';
-import { observable, action, runInAction } from 'mobx';
+import { observable, action, runInAction, flow, computed } from 'mobx';
import "./SearchBox.scss";
import "./FilterBox.scss";
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { SetupDrag } from '../../util/DragManager';
import { Docs } from '../../documents/Documents';
-import { NumCast } from '../../../new_fields/Types';
+import { NumCast, Cast } from '../../../new_fields/Types';
import { Doc } from '../../../new_fields/Doc';
import { SearchItem } from './SearchItem';
-import { DocServer } from '../../DocServer';
import * as rp from 'request-promise';
import { Id } from '../../../new_fields/FieldSymbols';
import { SearchUtil } from '../../util/SearchUtil';
import { RouteStore } from '../../../server/RouteStore';
import { FilterBox } from './FilterBox';
+import { Utils } from '../../../Utils';
+
@observer
export class SearchBox extends React.Component {
@observable private _searchString: string = "";
@observable private _resultsOpen: boolean = false;
- @observable private _results: Doc[] = [];
+ @observable private _searchbarOpen: boolean = false;
+ @observable private _results: [Doc, string[]][] = [];
+ private _resultsSet = new Map<Doc, number>();
@observable private _openNoResults: boolean = false;
- @observable public _pageNum: number = 0;
- //temp
- @observable public _maxNum: number = 10;
+ @observable private _visibleElements: JSX.Element[] = [];
+
+ private resultsRef = React.createRef<HTMLDivElement>();
+
+ private _isSearch: ("search" | "placeholder" | undefined)[] = [];
+ private _numTotalResults = -1;
+ private _endIndex = -1;
static Instance: SearchBox;
+ private _maxSearchIndex: number = 0;
+ private _curRequest?: Promise<any> = undefined;
+
constructor(props: any) {
super(props);
SearchBox.Instance = this;
+ this.resultsScrolled = this.resultsScrolled.bind(this);
}
@action
@@ -49,19 +60,21 @@ export class SearchBox extends React.Component {
onChange(e: React.ChangeEvent<HTMLInputElement>) {
this._searchString = e.target.value;
- if (this._searchString === "") {
- this._results = [];
- this._openNoResults = false;
- }
+ this._openNoResults = false;
+ this._results = [];
+ this._resultsSet.clear();
+ this._visibleElements = [];
+ this._numTotalResults = -1;
+ this._endIndex = -1;
+ this._curRequest = undefined;
+ this._maxSearchIndex = 0;
}
- enter = (e: React.KeyboardEvent) => {
- if (e.key === "Enter") { this.submitSearch(); }
- }
+ enter = (e: React.KeyboardEvent) => { if (e.key === "Enter") { this.submitSearch(); } };
public static async convertDataUri(imageUri: string, returnedFilename: string) {
try {
- let posting = DocServer.prepend(RouteStore.dataUriToImage);
+ let posting = Utils.prepend(RouteStore.dataUriToImage);
const returnedUri = await rp.post(posting, {
body: {
uri: imageUri,
@@ -78,38 +91,97 @@ export class SearchBox extends React.Component {
@action
submitSearch = async () => {
- let query = this._searchString; // searchbox gets query
- let results: Doc[];
-
+ let query = this._searchString;
query = FilterBox.Instance.getFinalQuery(query);
+ this._results = [];
+ this._resultsSet.clear();
+ this._isSearch = [];
+ this._visibleElements = [];
+ FilterBox.Instance.closeFilter();
//if there is no query there should be no result
if (query === "") {
- results = [];
+ return;
}
else {
- //gets json result into a list of documents that can be used
- //these are filtered by type
- results = await this.getResults(query);
+ this._endIndex = 12;
+ this._maxSearchIndex = 0;
+ this._numTotalResults = -1;
+ await this.getResults(query);
}
runInAction(() => {
this._resultsOpen = true;
- this._results = results;
+ this._searchbarOpen = true;
this._openNoResults = true;
+ this.resultsScrolled();
});
}
- @action
+ getAllResults = async (query: string) => {
+ return SearchUtil.Search(query, true, { fq: this.filterQuery, start: 0, rows: 10000000 });
+ }
+
+ private get filterQuery() {
+ const types = FilterBox.Instance.filterTypes;
+ const includeDeleted = FilterBox.Instance.getDataStatus();
+ return "NOT baseProto_b:true" + (includeDeleted ? "" : " AND NOT deleted:true") + (types ? ` AND (${types.map(type => `({!join from=id to=proto_i}type_t:"${type}" AND NOT type_t:*) OR type_t:"${type}"`).join(" ")})` : "");
+ }
+
+
+ private lockPromise?: Promise<void>;
getResults = async (query: string) => {
- const { docs } = await SearchUtil.Search(query, true);
- return FilterBox.Instance.filterDocsByType(docs);
+ if (this.lockPromise) {
+ await this.lockPromise;
+ }
+ this.lockPromise = new Promise(async res => {
+ while (this._results.length <= this._endIndex && (this._numTotalResults === -1 || this._maxSearchIndex < this._numTotalResults)) {
+ this._curRequest = SearchUtil.Search(query, true, { fq: this.filterQuery, start: this._maxSearchIndex, rows: 10, hl: true, "hl.fl": "*" }).then(action(async (res: SearchUtil.DocSearchResult) => {
+
+ // happens at the beginning
+ if (res.numFound !== this._numTotalResults && this._numTotalResults === -1) {
+ this._numTotalResults = res.numFound;
+ }
+
+ const highlighting = res.highlighting || {};
+ const highlightList = res.docs.map(doc => highlighting[doc[Id]]);
+ const docs = await Promise.all(res.docs.map(async doc => (await Cast(doc.extendsDoc, Doc)) || doc));
+ const highlights: typeof res.highlighting = {};
+ docs.forEach((doc, index) => highlights[doc[Id]] = highlightList[index]);
+ let filteredDocs = FilterBox.Instance.filterDocsByType(docs);
+ runInAction(() => {
+ // this._results.push(...filteredDocs);
+ filteredDocs.forEach(doc => {
+ const index = this._resultsSet.get(doc);
+ const highlight = highlights[doc[Id]];
+ const hlights = highlight ? Object.keys(highlight).map(key => key.substring(0, key.length - 2)) : [];
+ if (index === undefined) {
+ this._resultsSet.set(doc, this._results.length);
+ this._results.push([doc, hlights]);
+ } else {
+ this._results[index][1].push(...hlights);
+ }
+ });
+ });
+
+ this._curRequest = undefined;
+ }));
+ this._maxSearchIndex += 10;
+
+ await this._curRequest;
+ }
+ this.resultsScrolled();
+ res();
+ });
+ return this.lockPromise;
}
collectionRef = React.createRef<HTMLSpanElement>();
startDragCollection = async () => {
- const results = await this.getResults(FilterBox.Instance.getFinalQuery(this._searchString));
- const docs = results.map(doc => {
+ let res = await this.getAllResults(FilterBox.Instance.getFinalQuery(this._searchString));
+ let filtered = FilterBox.Instance.filterDocsByType(res.docs);
+ // console.log(this._results)
+ const docs = filtered.map(doc => {
const isProto = Doc.GetT(doc, "isPrototype", "boolean", true);
if (isProto) {
return Doc.MakeDelegate(doc);
@@ -141,7 +213,8 @@ export class SearchBox extends React.Component {
y += 300;
}
}
- return Docs.FreeformDocument(docs, { width: 400, height: 400, panX: 175, panY: 175, backgroundColor: "grey", title: `Search Docs: "${this._searchString}"` });
+ return Docs.Create.FreeformDocument(docs, { width: 400, height: 400, panX: 175, panY: 175, backgroundColor: "grey", title: `Search Docs: "${this._searchString}"` });
+
}
@action.bound
@@ -150,6 +223,7 @@ export class SearchBox extends React.Component {
this._openNoResults = false;
FilterBox.Instance.closeFilter();
this._resultsOpen = true;
+ this._searchbarOpen = true;
FilterBox.Instance._pointerTime = e.timeStamp;
}
@@ -157,34 +231,107 @@ export class SearchBox extends React.Component {
closeSearch = () => {
FilterBox.Instance.closeFilter();
this.closeResults();
+ this._searchbarOpen = false;
}
@action.bound
closeResults() {
this._resultsOpen = false;
this._results = [];
+ this._resultsSet.clear();
+ this._visibleElements = [];
+ this._numTotalResults = -1;
+ this._endIndex = -1;
+ this._curRequest = undefined;
}
+ @action
+ resultsScrolled = (e?: React.UIEvent<HTMLDivElement>) => {
+ let scrollY = e ? e.currentTarget.scrollTop : this.resultsRef.current ? this.resultsRef.current.scrollTop : 0;
+ let buffer = 4;
+ let startIndex = Math.floor(Math.max(0, scrollY / 70 - buffer));
+ let endIndex = Math.ceil(Math.min(this._numTotalResults - 1, startIndex + (560 / 70) + buffer));
+
+ this._endIndex = endIndex === -1 ? 12 : endIndex;
+
+ if ((this._numTotalResults === 0 || this._results.length === 0) && this._openNoResults) {
+ this._visibleElements = [<div className="no-result">No Search Results</div>];
+ return;
+ }
+
+ if (this._numTotalResults <= this._maxSearchIndex) {
+ this._numTotalResults = this._results.length;
+ }
+
+ // only hit right at the beginning
+ // visibleElements is all of the elements (even the ones you can't see)
+ else if (this._visibleElements.length !== this._numTotalResults) {
+ // undefined until a searchitem is put in there
+ this._visibleElements = Array<JSX.Element>(this._numTotalResults === -1 ? 0 : this._numTotalResults);
+ // indicates if things are placeholders
+ this._isSearch = Array<undefined>(this._numTotalResults === -1 ? 0 : this._numTotalResults);
+ }
+
+ for (let i = 0; i < this._numTotalResults; i++) {
+ //if the index is out of the window then put a placeholder in
+ //should ones that have already been found get set to placeholders?
+ if (i < startIndex || i > endIndex) {
+ if (this._isSearch[i] !== "placeholder") {
+ this._isSearch[i] = "placeholder";
+ this._visibleElements[i] = <div className="searchBox-placeholder" key={`searchBox-placeholder-${i}`}>Loading...</div>;
+ }
+ }
+ else {
+ if (this._isSearch[i] !== "search") {
+ let result: [Doc, string[]] | undefined = undefined;
+ if (i >= this._results.length) {
+ this.getResults(this._searchString);
+ if (i < this._results.length) result = this._results[i];
+ if (result) {
+ this._visibleElements[i] = <SearchItem doc={result[0]} key={result[0][Id]} highlighting={result[1]} />;
+ this._isSearch[i] = "search";
+ }
+ }
+ else {
+ result = this._results[i];
+ if (result) {
+ this._visibleElements[i] = <SearchItem doc={result[0]} key={result[0][Id]} highlighting={result[1]} />;
+ this._isSearch[i] = "search";
+ }
+ }
+ }
+ }
+ }
+ if (this._maxSearchIndex >= this._numTotalResults) {
+ this._visibleElements.length = this._results.length;
+ this._isSearch.length = this._results.length;
+ }
+ }
+
+ @computed
+ get resFull() { return this._numTotalResults <= 8; }
+
+ @computed
+ get resultHeight() { return this._numTotalResults * 70; }
+
render() {
return (
<div className="searchBox-container">
<div className="searchBox-bar">
- <span className="searchBox-barChild searchBox-collection" onPointerDown={SetupDrag(this.collectionRef, this.startDragCollection)} ref={this.collectionRef}>
+ <span className="searchBox-barChild searchBox-collection" onPointerDown={SetupDrag(this.collectionRef, this.startDragCollection)} ref={this.collectionRef} title="Drag Results as Collection">
<FontAwesomeIcon icon="object-group" size="lg" />
</span>
<input value={this._searchString} onChange={this.onChange} type="text" placeholder="Search..."
className="searchBox-barChild searchBox-input" onPointerDown={this.openSearch} onKeyPress={this.enter}
- style={{ width: this._resultsOpen ? "500px" : "100px" }} />
+ style={{ width: this._searchbarOpen ? "500px" : "100px" }} />
+ <button className="searchBox-barChild searchBox-submit" onClick={this.submitSearch} onPointerDown={FilterBox.Instance.stopProp}>Submit</button>
<button className="searchBox-barChild searchBox-filter" onClick={FilterBox.Instance.openFilter} onPointerDown={FilterBox.Instance.stopProp}>Filter</button>
</div>
- <div className="searchBox-results" style={this._resultsOpen ? { display: "flex" } : { display: "none" }}>
- {(this._results.length !== 0) ? (
- this._results.map(result => <SearchItem doc={result} query={this._searchString} key={result[Id]} />)
- ) :
- this._openNoResults ? (<div className="no-result">No Search Results</div>) : null}
- </div>
- <div style={this._results.length !== 0 ? { display: "flex" } : { display: "none" }}>
- {/* <Pager /> */}
+ <div className="searchBox-results" onScroll={this.resultsScrolled} style={{
+ display: this._resultsOpen ? "flex" : "none",
+ height: this.resFull ? "560px" : this.resultHeight, overflow: this.resFull ? "auto" : "visible"
+ }} ref={this.resultsRef}>
+ {this._visibleElements}
</div>
</div>
);