diff options
Diffstat (limited to 'src/client/views/search/SearchBox.tsx')
-rw-r--r-- | src/client/views/search/SearchBox.tsx | 274 |
1 files changed, 160 insertions, 114 deletions
diff --git a/src/client/views/search/SearchBox.tsx b/src/client/views/search/SearchBox.tsx index 9f153e86d..56552c952 100644 --- a/src/client/views/search/SearchBox.tsx +++ b/src/client/views/search/SearchBox.tsx @@ -1,22 +1,26 @@ +/* eslint-disable jsx-a11y/no-static-element-interactions */ +/* eslint-disable jsx-a11y/click-events-have-key-events */ import { Tooltip } from '@mui/material'; import { action, computed, makeObservable, observable } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; -import { Doc, DocListCastAsync, Field } from '../../../fields/Doc'; +import { ClientUtils } from '../../../ClientUtils'; +import { Doc, DocListCastAsync, Field, FieldType } from '../../../fields/Doc'; import { DirectLinks, DocData } from '../../../fields/DocSymbols'; import { Id } from '../../../fields/FieldSymbols'; import { DocCast, StrCast } from '../../../fields/Types'; +import { DocUtils } from '../../documents/DocUtils'; import { DocumentType } from '../../documents/DocumentTypes'; -import { DocUtils } from '../../documents/Documents'; -import { DocumentManager } from '../../util/DocumentManager'; -import { LinkManager } from '../../util/LinkManager'; +import { Docs } from '../../documents/Documents'; import { SearchUtil } from '../../util/SearchUtil'; -import { SettingsManager } from '../../util/SettingsManager'; +import { SnappingManager } from '../../util/SnappingManager'; import { undoBatch } from '../../util/UndoManager'; import { ViewBoxBaseComponent } from '../DocComponent'; +import { ObservableReactComponent } from '../ObservableReactComponent'; import { CollectionDockingView } from '../collections/CollectionDockingView'; import { IRecommendation, Recommendation } from '../newlightbox/components'; import { fetchRecommendations } from '../newlightbox/utils'; +import { DocumentView } from '../nodes/DocumentView'; import { FieldView, FieldViewProps } from '../nodes/FieldView'; import './SearchBox.scss'; @@ -24,6 +28,96 @@ const DAMPENING_FACTOR = 0.9; const MAX_ITERATIONS = 25; const ERROR = 0.03; +export interface SearchBoxItemProps { + Document: Doc; + searchString: string; + isLinkSearch: boolean; + matchedKeys: string[]; + className: string; + linkFrom: Doc | undefined; + selectItem: (doc: Doc) => void; + linkCreateAnchor?: () => Doc | undefined; + linkCreated?: (link: Doc) => void; +} +@observer +export class SearchBoxItem extends ObservableReactComponent<SearchBoxItemProps> { + constructor(props: SearchBoxItemProps) { + super(props); + makeObservable(this); + } + + /** + * @param {Doc} doc - doc to be selected + * + * This method selects a doc by either jumping to it (centering/zooming in on it) + * or opening it in a new tab. + */ + selectElement = async (doc: Doc, finishFunc: () => void) => { + await DocumentView.showDocument(doc, { willZoomCentered: true }, finishFunc); + }; + + /** + * @param {Doc} doc - doc of the search result that has been clicked on + * + * This method is called when the user clicks on a search result. The _selectedResult is + * updated accordingly and the doc is highlighted with the selectElement method. + */ + onResultClick = action(async (doc: Doc) => { + this._props.selectItem(doc); + this.selectElement(doc, () => DocumentView.getFirstDocumentView(doc)?.ComponentView?.search?.(this._props.searchString, undefined, false)); + }); + + componentWillUnmount(): void { + const doc = this._props.Document; + DocumentView.getFirstDocumentView(doc)?.ComponentView?.search?.('', undefined, true); + } + + @undoBatch + makeLink = action((linkTo: Doc) => { + const linkFrom = this._props.linkCreateAnchor?.(); + if (linkFrom) { + const link = DocUtils.MakeLink(linkFrom, linkTo, {}); + link && this._props.linkCreated?.(link); + } + }); + + render() { + // eslint-disable-next-line no-use-before-define + const formattedType = SearchBox.formatType(StrCast(this._props.Document.type), StrCast(this._props.Document.type_collection)); + const { title } = this._props.Document; + + return ( + <Tooltip placement="right" title={<div className="dash-tooltip">{title as string}</div>}> + <div + onClick={ + this._props.isLinkSearch + ? () => this.makeLink(this._props.Document) + : e => { + this.onResultClick(this._props.Document); + e.stopPropagation(); + } + } + style={{ + fontWeight: Doc.Links(this._props.linkFrom).find( + link => Doc.AreProtosEqual(Doc.getOppositeAnchor(link, this._props.linkFrom!), this._props.Document) || Doc.AreProtosEqual(DocCast(Doc.getOppositeAnchor(link, this._props.linkFrom!)?.annotationOn), this._props.Document) + ) + ? 'bold' + : '', + }} + className={this._props.className}> + <div className="searchBox-result-title">{title as string}</div> + <div className="searchBox-result-type" style={{ color: SnappingManager.userVariantColor }}> + {formattedType} + </div> + <div className="searchBox-result-keys" style={{ color: SnappingManager.userVariantColor }}> + {this._props.matchedKeys.join(', ')} + </div> + </div> + </Tooltip> + ); + } +} + export interface SearchBoxProps extends FieldViewProps { linkSearch: boolean; linkFrom?: (() => Doc | undefined) | undefined; @@ -40,6 +134,7 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps>() { public static LayoutString(fieldKey: string) { return FieldView.LayoutString(SearchBox, fieldKey); } + // eslint-disable-next-line no-use-before-define public static Instance: SearchBox; private _inputRef = React.createRef<HTMLInputElement>(); @@ -109,46 +204,29 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps>() { }); /** - * @param {Doc} doc - doc of the search result that has been clicked on - * - * This method is called when the user clicks on a search result. The _selectedResult is - * updated accordingly and the doc is highlighted with the selectElement method. - */ - onResultClick = action(async (doc: Doc) => { - this._selectedResult = doc; - this.selectElement(doc, () => DocumentManager.Instance.getFirstDocumentView(doc)?.ComponentView?.search?.(this._searchString, undefined, false)); - }); - - @undoBatch - makeLink = action((linkTo: Doc) => { - const linkFrom = this._props.linkCreateAnchor?.(); - if (linkFrom) { - const link = DocUtils.MakeLink(linkFrom, linkTo, {}); - link && this._props.linkCreated?.(link); - } - }); - - /** * @param {Doc[]} docs - docs to be searched through recursively * @param {number, Doc => void} func - function to be called on each doc * * This method iterates asynchronously through an array of docs and all docs within those * docs, calling the function func on each doc. */ - static async foreachRecursiveDocAsync(docs: Doc[], func: (depth: number, doc: Doc) => void) { + static async foreachRecursiveDocAsync(docsIn: Doc[], func: (depth: number, doc: Doc) => void) { + let docs = docsIn; let newarray: Doc[] = []; - var depth = 0; + let depth = 0; while (docs.length > 0) { newarray = []; + // eslint-disable-next-line no-await-in-loop await Promise.all( docs .filter(d => d) + // eslint-disable-next-line no-loop-func .map(async d => { const fieldKey = Doc.LayoutFieldKey(d); - const annos = !Field.toString(Doc.LayoutField(d) as Field).includes('CollectionView'); + const annos = !Field.toString(Doc.LayoutField(d) as FieldType).includes('CollectionView'); const data = d[annos ? fieldKey + '_annotations' : fieldKey]; - const docs = await DocListCastAsync(data); - docs && newarray.push(...docs); + const dataDocs = await DocListCastAsync(data); + dataDocs && newarray.push(...dataDocs); func(depth, d); }) ); @@ -170,6 +248,7 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps>() { case DocumentType.IMG : return 'Img'; case DocumentType.RTF : return 'Rtf'; case DocumentType.COL : return 'Col:'+colType.substring(0,3); + default: } // prettier-ignore return type.charAt(0).toUpperCase() + type.substring(1, 3); @@ -210,7 +289,7 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps>() { if (doc[DocData][DirectLinks].size === 0) { this._linkedDocsOut.set(doc, new Set(this._results.keys())); - this._results.forEach((_, linkedDoc) => { + this._results.forEach((__, linkedDoc) => { this._linkedDocsIn.get(linkedDoc)?.add(doc); }); } else { @@ -244,7 +323,6 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps>() { pageRankIteration(): boolean { let converged = true; const pageRankFromAll = (1 - DAMPENING_FACTOR) / this._results.size; - const nextPageRanks = new Map<Doc, number>(); this._results.forEach((_, doc) => { @@ -292,41 +370,33 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps>() { const query = StrCast(this._searchString); Doc.SetSearchQuery(query); - if (!this._props.linkSearch) Array.from(this._results.keys()).forEach(doc => DocumentManager.Instance.getFirstDocumentView(doc)?.ComponentView?.search?.(this._searchString, undefined, true)); + if (!this._props.linkSearch) Array.from(this._results.keys()).forEach(doc => DocumentView.getFirstDocumentView(doc)?.ComponentView?.search?.(this._searchString, undefined, true)); this._results.clear(); if (query) { this.searchCollection(query); const response = await fetchRecommendations('', query, [], true); - const recs = response.recommendations; + const recs = response.recommendations as any[]; const recommendations: IRecommendation[] = []; - for (const key in recs) { - const title = recs[key].title; - const url = recs[key].url; - const type = recs[key].type; - const text = recs[key].text; - const transcript = recs[key].transcript; - const previewUrl = recs[key].previewUrl; - const embedding = recs[key].embedding; - const distance = recs[key].distance; - const source = recs[key].source; - const related_concepts = recs[key].related_concepts; - const docId = recs[key].doc_id; + recs.forEach(rec => { + const { title, url, type, text, transcript, previewUrl, embedding, distance, source, related_concepts: relatedConcepts, doc_id: docId } = rec; recommendations.push({ - title: title, + title, data: url, - type: type, - text: text, - transcript: transcript, - previewUrl: previewUrl, - embedding: embedding, + type, + text, + transcript, + previewUrl, + embedding, distance: Math.round(distance * 100) / 100, source: source, - related_concepts: related_concepts, - docId: docId, + related_concepts: relatedConcepts, + docId, }); - } - const setRecommendations = action(() => (this._recommendations = recommendations)); + }); + const setRecommendations = action(() => { + this._recommendations = recommendations; + }); setRecommendations(); } }; @@ -339,7 +409,7 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps>() { this._timeout && clearTimeout(this._timeout); this._timeout = undefined; this._results.forEach((_, doc) => { - DocumentManager.Instance.getFirstDocumentView(doc)?.ComponentView?.search?.('', undefined, true); + DocumentView.getFirstDocumentView(doc)?.ComponentView?.search?.('', undefined, true); Doc.UnBrushDoc(doc); Doc.UnHighlightDoc(doc); Doc.ClearSearchMatches(); @@ -347,16 +417,6 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps>() { }); /** - * @param {Doc} doc - doc to be selected - * - * This method selects a doc by either jumping to it (centering/zooming in on it) - * or opening it in a new tab. - */ - selectElement = async (doc: Doc, finishFunc: () => void) => { - await DocumentManager.Instance.showDocument(doc, { willZoomCentered: true }, finishFunc); - }; - - /** * This method returns a JSX list of the options in the select drop-down menu, which * is used to filter the types of documents that appear in the search results. */ @@ -366,7 +426,7 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps>() { return selectValues.map(value => ( <option key={value} value={value}> - {SearchBox.formatType(value, '')} + {ClientUtils.cleanDocumentTypeExt(value as DocumentType)} </option> )); } @@ -375,64 +435,45 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps>() { * This method renders the search input box, select drop-down menu, and search results. */ render() { - var validResults = 0; - const isLinkSearch: boolean = this._props.linkSearch; - const sortedResults = Array.from(this._results.entries()).sort((a, b) => (this._pageRanks.get(b[0]) ?? 0) - (this._pageRanks.get(a[0]) ?? 0)); // sorted by page rank + const resultsJSX = [] as any[]; + const linkFrom = this._props.linkFrom?.(); - const resultsJSX = Array(); - - const fromDoc = this._props.linkFrom?.(); + let validResults = 0; + sortedResults.forEach(([Document, matchedKeys]) => { + let className = 'searchBox-results-scroll-view-result'; - sortedResults.forEach(result => { - var className = 'searchBox-results-scroll-view-result'; - - if (this._selectedResult === result[0]) { + if (this._selectedResult === Document) { className += ' searchBox-results-scroll-view-result-selected'; } - const formattedType = SearchBox.formatType(StrCast(result[0].type), StrCast(result[0].type_collection)); - const title = result[0].title; - - if (this._docTypeString === 'keys' || this._docTypeString === 'all' || this._docTypeString === result[0].type) { + if (this._docTypeString === 'keys' || this._docTypeString === 'all' || this._docTypeString === Document.type) { validResults++; resultsJSX.push( - <Tooltip key={result[0][Id]} placement={'right'} title={<div className="dash-tooltip">{title as string}</div>}> - <div - onClick={ - isLinkSearch - ? () => this.makeLink(result[0]) - : e => { - this.onResultClick(result[0]); - e.stopPropagation(); - } - } - style={{ - fontWeight: LinkManager.Links(fromDoc).find( - link => Doc.AreProtosEqual(LinkManager.getOppositeAnchor(link, fromDoc!), result[0] as Doc) || Doc.AreProtosEqual(DocCast(LinkManager.getOppositeAnchor(link, fromDoc!)?.annotationOn), result[0] as Doc) - ) - ? 'bold' - : '', - }} - className={className}> - <div className="searchBox-result-title">{title as string}</div> - <div className="searchBox-result-type" style={{ color: SettingsManager.userVariantColor }}> - {formattedType} - </div> - <div className="searchBox-result-keys" style={{ color: SettingsManager.userVariantColor }}> - {result[1].join(', ')} - </div> - </div> - </Tooltip> + <SearchBoxItem + key={Document[Id]} + Document={Document} + selectItem={action((doc: Doc) => { + this._selectedResult = doc; + })} + isLinkSearch={isLinkSearch} + searchString={this._searchString} + matchedKeys={matchedKeys} + linkFrom={linkFrom} + className={className} + linkCreateAnchor={this._props.linkCreateAnchor} + linkCreated={this._props.linkCreated} + /> ); } }); + // eslint-disable-next-line react/jsx-props-no-spreading const recommendationsJSX: JSX.Element[] = this._recommendations.map(props => <Recommendation {...props} />); return ( - <div className="searchBox-container" style={{ pointerEvents: 'all', color: SettingsManager.userColor, background: SettingsManager.userBackgroundColor }}> + <div className="searchBox-container" style={{ pointerEvents: 'all', color: SnappingManager.userColor, background: SnappingManager.userBackgroundColor }}> <div className="searchBox-bar"> {isLinkSearch ? null : ( <select name="type" id="searchBox-type" className="searchBox-type" onChange={this.onSelectChange}> @@ -457,18 +498,18 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps>() { </div> {resultsJSX.length > 0 && ( <div className="searchBox-results-container"> - <div className="section-header" style={{ background: SettingsManager.userVariantColor }}> + <div className="section-header" style={{ background: SnappingManager.userVariantColor }}> <div className="section-title">Results</div> - <div className="section-subtitle">{`${validResults}` + ' result' + (validResults === 1 ? '' : 's')}</div> + <div className="section-subtitle">{`${validResults} result` + (validResults === 1 ? '' : 's')}</div> </div> <div className="searchBox-results-view">{resultsJSX}</div> </div> )} {recommendationsJSX.length > 0 && ( <div className="searchBox-recommendations-container"> - <div className="section-header" style={{ background: SettingsManager.userVariantColor }}> + <div className="section-header" style={{ background: SnappingManager.userVariantColor }}> <div className="section-title">Recommendations</div> - <div className="section-subtitle">{`${validResults}` + ' result' + (validResults === 1 ? '' : 's')}</div> + <div className="section-subtitle">{`${validResults} result` + (validResults === 1 ? '' : 's')}</div> </div> <div className="searchBox-recommendations-view">{recommendationsJSX}</div> </div> @@ -477,3 +518,8 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps>() { ); } } + +Docs.Prototypes.TemplateMap.set(DocumentType.SEARCH, { + layout: { view: SearchBox, dataField: 'data' }, + options: { acl: '', _width: 400 }, +}); |