aboutsummaryrefslogtreecommitdiff
path: root/src/client/views/search/SearchBox.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'src/client/views/search/SearchBox.tsx')
-rw-r--r--src/client/views/search/SearchBox.tsx274
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 },
+});