diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/client/util/SearchUtil.ts | 8 | ||||
-rw-r--r-- | src/client/views/pdf/PDFMenu.tsx | 10 | ||||
-rw-r--r-- | src/client/views/pdf/PDFViewer.tsx | 2 | ||||
-rw-r--r-- | src/client/views/search/FilterBox.tsx | 5 | ||||
-rw-r--r-- | src/client/views/search/SearchBox.scss | 3 | ||||
-rw-r--r-- | src/client/views/search/SearchBox.tsx | 183 | ||||
-rw-r--r-- | src/client/views/search/SearchItem.scss | 4 | ||||
-rw-r--r-- | src/client/views/search/SearchItem.tsx | 6 | ||||
-rw-r--r-- | src/server/Search.ts | 5 | ||||
-rw-r--r-- | src/server/index.ts | 8 |
10 files changed, 183 insertions, 51 deletions
diff --git a/src/client/util/SearchUtil.ts b/src/client/util/SearchUtil.ts index 338628960..674eeb1a8 100644 --- a/src/client/util/SearchUtil.ts +++ b/src/client/util/SearchUtil.ts @@ -14,11 +14,11 @@ export namespace SearchUtil { numFound: number; } - export function Search(query: string, returnDocs: true): Promise<DocSearchResult>; - export function Search(query: string, returnDocs: false): Promise<IdSearchResult>; - export async function Search(query: string, returnDocs: boolean) { + export function Search(query: string, returnDocs: true, start?: number, count?: number): Promise<DocSearchResult>; + export function Search(query: string, returnDocs: false, start?: number, count?: number): Promise<IdSearchResult>; + export async function Search(query: string, returnDocs: boolean, start?: number, rows?: number) { const result: IdSearchResult = JSON.parse(await rp.get(DocServer.prepend("/search"), { - qs: { query } + qs: { query, start, rows } })); if (!returnDocs) { return result; diff --git a/src/client/views/pdf/PDFMenu.tsx b/src/client/views/pdf/PDFMenu.tsx index feaa1f652..e73b759df 100644 --- a/src/client/views/pdf/PDFMenu.tsx +++ b/src/client/views/pdf/PDFMenu.tsx @@ -238,24 +238,24 @@ export default class PDFMenu extends React.Component { render() { let buttons = this.Status === "pdf" || this.Status === "snippet" ? [ - <button className="pdfMenu-button" title="Click to Highlight" onClick={this.highlightClicked} key="1" + <button key="1" className="pdfMenu-button" title="Click to Highlight" onClick={this.highlightClicked} style={this.Highlighting ? { backgroundColor: "#121212" } : {}}> <FontAwesomeIcon icon="highlighter" size="lg" style={{ transition: "transform 0.1s", transform: this.Highlighting ? "" : "rotate(-45deg)" }} /> </button>, <button className="pdfMenu-button" title="Drag to Annotate" ref={this._commentCont} onPointerDown={this.pointerDown}><FontAwesomeIcon icon="comment-alt" size="lg" key="2" /></button>, this.Status === "snippet" ? <button className="pdfMenu-button" title="Drag to Snippetize Selection" onPointerDown={this.snippetStart} ref={this._snippetButton}><FontAwesomeIcon icon="cut" size="lg" /></button> : undefined, - <button className="pdfMenu-button" title="Pin Menu" onClick={this.togglePin} key="3" + <button key="3" className="pdfMenu-button" title="Pin Menu" onClick={this.togglePin} style={this.Pinned ? { backgroundColor: "#121212" } : {}}> <FontAwesomeIcon icon="thumbtack" size="lg" style={{ transition: "transform 0.1s", transform: this.Pinned ? "rotate(45deg)" : "" }} /> </button> ] : [ - <button className="pdfMenu-button" title="Delete Anchor" onPointerDown={this.deleteClicked}><FontAwesomeIcon icon="trash-alt" size="lg" key="1" /></button>, - <button className="pdfMenu-button" title="Pin to Presentation" onPointerDown={this.PinToPres}><FontAwesomeIcon icon="map-pin" size="lg" key="2" /></button>, + <button key="4" className="pdfMenu-button" title="Delete Anchor" onPointerDown={this.deleteClicked}><FontAwesomeIcon icon="trash-alt" size="lg" key="1" /></button>, + <button key="5" className="pdfMenu-button" title="Pin to Presentation" onPointerDown={this.PinToPres}><FontAwesomeIcon icon="map-pin" size="lg" key="2" /></button>, <div className="pdfMenu-addTag" key="3"> <input onKeyDown={handleBackspace} onChange={this.keyChanged} placeholder="Key" style={{ gridColumn: 1 }} /> <input onKeyDown={handleBackspace} onChange={this.valueChanged} placeholder="Value" style={{ gridColumn: 3 }} /> </div>, - <button className="pdfMenu-button" title={`Add tag: ${this._keyValue} with value: ${this._valueValue}`} onPointerDown={this.addTag}><FontAwesomeIcon style={{ transition: "all .2s" }} color={this._added ? "#42f560" : "white"} icon="check" size="lg" key="4" /></button>, + <button key="6" className="pdfMenu-button" title={`Add tag: ${this._keyValue} with value: ${this._valueValue}`} onPointerDown={this.addTag}><FontAwesomeIcon style={{ transition: "all .2s" }} color={this._added ? "#42f560" : "white"} icon="check" size="lg" key="4" /></button>, ]; return ( diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx index 943454c33..01fd1c247 100644 --- a/src/client/views/pdf/PDFViewer.tsx +++ b/src/client/views/pdf/PDFViewer.tsx @@ -242,7 +242,7 @@ export class Viewer extends React.Component<IViewerProps> { let mainAnnoDoc = Docs.Create.InstanceFromProto(new Doc(), "", {}); mainAnnoDoc.title = "Annotation on " + StrCast(this.props.parent.Document.title); - mainAnnoDoc.pdfDoc = this.props.parent.Document; + mainAnnoDoc.pdfDoc = this.props.parent.props.Document; let minY = Number.MAX_VALUE; this._savedAnnotations.forEach((key: number, value: HTMLDivElement[]) => { for (let anno of value) { diff --git a/src/client/views/search/FilterBox.tsx b/src/client/views/search/FilterBox.tsx index 58a873ced..435ca86e3 100644 --- a/src/client/views/search/FilterBox.tsx +++ b/src/client/views/search/FilterBox.tsx @@ -239,10 +239,13 @@ export class FilterBox extends React.Component { @action filterDocsByType(docs: Doc[]) { + if (this._icons.length === 9) { + return docs; + } let finalDocs: Doc[] = []; docs.forEach(doc => { let layoutresult = Cast(doc.type, "string"); - if (!layoutresult || this._icons.includes(layoutresult)) { + if (layoutresult && this._icons.includes(layoutresult)) { finalDocs.push(doc); } }); diff --git a/src/client/views/search/SearchBox.scss b/src/client/views/search/SearchBox.scss index 08d3a65f1..324ba3063 100644 --- a/src/client/views/search/SearchBox.scss +++ b/src/client/views/search/SearchBox.scss @@ -45,7 +45,8 @@ top: 300px; display: flex; flex-direction: column; - height: 560px; + margin-right: 72px; + max-height: 560px; overflow: hidden; overflow-y: auto; diff --git a/src/client/views/search/SearchBox.tsx b/src/client/views/search/SearchBox.tsx index 121abf973..c02db528a 100644 --- a/src/client/views/search/SearchBox.tsx +++ b/src/client/views/search/SearchBox.tsx @@ -1,6 +1,6 @@ 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'; @@ -16,6 +16,7 @@ import { SearchUtil } from '../../util/SearchUtil'; import { RouteStore } from '../../../server/RouteStore'; import { FilterBox } from './FilterBox'; + @observer export class SearchBox extends React.Component { @@ -23,16 +24,22 @@ export class SearchBox extends React.Component { @observable private _resultsOpen: boolean = false; @observable private _results: Doc[] = []; @observable private _openNoResults: boolean = false; - @observable public _pageNum: number = 0; - //temp - @observable public _maxNum: number = 10; + @observable private _visibleElements: JSX.Element[] = []; + + 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,15 +56,16 @@ 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._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 { @@ -78,38 +86,73 @@ 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._isSearch = []; + this._visibleElements = []; //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._openNoResults = true; + this.resultsScrolled(); }); } - @action + getAllResults = async (query: string) => { + return SearchUtil.Search(query, true, 0, 10000000); + } + getResults = async (query: string) => { - const { docs } = await SearchUtil.Search(query, true); - return FilterBox.Instance.filterDocsByType(docs); + + while (this._results.length <= this._endIndex && (this._numTotalResults === -1 || this._maxSearchIndex < this._numTotalResults)) { + + let prom: Promise<any>; + if (this._curRequest) { + prom = this._curRequest; + return; + } else { + prom = SearchUtil.Search(query, true, this._maxSearchIndex, 10); + this._maxSearchIndex += 10; + } + prom.then(action((res: SearchUtil.DocSearchResult) => { + + // happens at the beginning + if (res.numFound !== this._numTotalResults && this._numTotalResults === -1) { + this._numTotalResults = res.numFound; + } + + let filteredDocs = FilterBox.Instance.filterDocsByType(res.docs); + this._results.push(...filteredDocs); + + if (prom === this._curRequest) { + this._curRequest = undefined; + } + })); + + this._curRequest = prom; + + await prom; + } } 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); @@ -142,6 +185,7 @@ export class SearchBox extends React.Component { } } return Docs.Create.FreeformDocument(docs, { width: 400, height: 400, panX: 175, panY: 175, backgroundColor: "grey", title: `Search Docs: "${this._searchString}"` }); + } @action.bound @@ -155,7 +199,6 @@ export class SearchBox extends React.Component { @action.bound closeSearch = () => { - console.log("closing search"); FilterBox.Instance.closeFilter(); this.closeResults(); } @@ -164,6 +207,83 @@ export class SearchBox extends React.Component { closeResults() { this._resultsOpen = false; this._results = []; + this._visibleElements = []; + this._numTotalResults = -1; + this._endIndex = -1; + this._curRequest = undefined; + } + + resultsScrolled = flow(function* (this: SearchBox, e?: React.UIEvent<HTMLDivElement>) { + let scrollY = e ? e.currentTarget.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}`}></div>; + } + } + else { + if (this._isSearch[i] !== "search") { + let result: Doc | 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} key={result[Id]} />; + this._isSearch[i] = "search"; + } + } + else { + result = this._results[i]; + if (result) { + this._visibleElements[i] = <SearchItem doc={result} key={result[Id]} />; + this._isSearch[i] = "search"; + } + } + } + } + } + if (this._maxSearchIndex >= this._numTotalResults) { + this._visibleElements.length = this._results.length; + this._isSearch.length = this._results.length; + } + }); + + @computed + get resFull() { + console.log(this._numTotalResults) + return this._numTotalResults <= 8; + } + + @computed + get resultHeight() { + return this._numTotalResults * 70; } render() { @@ -179,15 +299,12 @@ export class SearchBox extends React.Component { <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} key={result[Id]} />) - ) : - this._openNoResults ? (<div className="no-result">No Search Results</div>) : null} + <div className="searchBox-results" onScroll={this.resultsScrolled} style={{ + display: this._resultsOpen ? "flex" : "none", + height: this.resFull ? "560px" : this.resultHeight, overflow: this.resFull ? "auto" : "visible" + }}> + {this._visibleElements} </div> - {/* <div style={this._results.length !== 0 ? { display: "flex" } : { display: "none" }}> - <Pager /> - </div> */} </div> ); } diff --git a/src/client/views/search/SearchItem.scss b/src/client/views/search/SearchItem.scss index 827cb7567..0fb93daad 100644 --- a/src/client/views/search/SearchItem.scss +++ b/src/client/views/search/SearchItem.scss @@ -193,6 +193,10 @@ z-index: 1; } +.searchBox-placeholder { + min-height: 70px; +} + .collection { display: flex; } diff --git a/src/client/views/search/SearchItem.tsx b/src/client/views/search/SearchItem.tsx index b3ba4c3dd..16ad71d16 100644 --- a/src/client/views/search/SearchItem.tsx +++ b/src/client/views/search/SearchItem.tsx @@ -99,7 +99,9 @@ export class SearchItem extends React.Component<SearchItemProps> { @observable _selected: boolean = false; onClick = () => { - DocumentManager.Instance.jumpToDocument(this.props.doc, false); + // I dont think this is the best functionality because clicking the name of the collection does that. Change it back if you'd like + // DocumentManager.Instance.jumpToDocument(this.props.doc, false); + CollectionDockingView.Instance.AddRightSplit(this.props.doc, undefined); } @observable _useIcons = true; @observable _displayDim = 50; @@ -240,7 +242,7 @@ export class SearchItem extends React.Component<SearchItemProps> { <div className="search-item" onPointerEnter={this.highlightDoc} onPointerLeave={this.unHighlightDoc} id="result" onClick={this.onClick} onPointerDown={this.pointerDown} > <div className="main-search-info"> - <div title="Drag as document" onPointerDown={this.onPointerDown}> <FontAwesomeIcon icon="file" size="lg" /> </div> + <div title="Drag as document" onPointerDown={this.onPointerDown} style={{ marginRight: "7px" }}> <FontAwesomeIcon icon="file" size="lg" /> </div> <div className="search-title" id="result" >{StrCast(this.props.doc.title)}</div> <div className="search-info" style={{ width: this._useIcons ? "15%" : "400px" }}> <div className={`icon-${this._useIcons ? "icons" : "live"}`}> diff --git a/src/server/Search.ts b/src/server/Search.ts index 8591f8857..98f421937 100644 --- a/src/server/Search.ts +++ b/src/server/Search.ts @@ -30,13 +30,14 @@ export class Search { } } - public async search(query: string, start: number = 0) { + public async search(query: string, start: number = 0, rows: number = 10) { try { const searchResults = JSON.parse(await rp.get(this.url + "dash/select", { qs: { q: query, fl: "id", - start: start + start, + rows, } })); const { docs, numFound } = searchResults.response; diff --git a/src/server/index.ts b/src/server/index.ts index 9cb43bf4e..58af074aa 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -144,8 +144,12 @@ app.get("/pull", (req, res) => // GETTERS app.get("/search", async (req, res) => { - let query = req.query.query || "hello"; - let results = await Search.Instance.search(query); + const { query, start, rows } = req.query; + if (query === undefined) { + res.send([]); + return; + } + let results = await Search.Instance.search(query, start, rows); res.send(results); }); |