diff options
Diffstat (limited to 'src/client/views/search/SearchBox.tsx')
-rw-r--r-- | src/client/views/search/SearchBox.tsx | 909 |
1 files changed, 316 insertions, 593 deletions
diff --git a/src/client/views/search/SearchBox.tsx b/src/client/views/search/SearchBox.tsx index f7b817aa1..ed1dc6de6 100644 --- a/src/client/views/search/SearchBox.tsx +++ b/src/client/views/search/SearchBox.tsx @@ -1,145 +1,108 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { Tooltip } from '@material-ui/core'; -import { action, computed, observable, runInAction } from 'mobx'; +import { action, computed, IReactionDisposer, observable, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; -import * as rp from 'request-promise'; -import { Doc, DocListCast } from '../../../fields/Doc'; +import { Doc, DocListCast, Field, Opt } from '../../../fields/Doc'; import { documentSchema } from "../../../fields/documentSchemas"; import { Id } from '../../../fields/FieldSymbols'; import { List } from '../../../fields/List'; import { createSchema, listSpec, makeInterface } from '../../../fields/Schema'; import { SchemaHeaderField } from '../../../fields/SchemaHeaderField'; import { Cast, NumCast, StrCast } from '../../../fields/Types'; -import { returnFalse, Utils } from '../../../Utils'; +import { returnFalse, returnZero } from '../../../Utils'; import { Docs } from '../../documents/Documents'; import { DocumentType } from "../../documents/DocumentTypes"; -import { CurrentUserUtils } from '../../util/CurrentUserUtils'; import { SetupDrag } from '../../util/DragManager'; import { SearchUtil } from '../../util/SearchUtil'; import { SelectionManager } from '../../util/SelectionManager'; import { Transform } from '../../util/Transform'; +import { ColumnType } from "../collections/CollectionSchemaView"; import { CollectionView, CollectionViewType } from '../collections/CollectionView'; import { ViewBoxBaseComponent } from "../DocComponent"; import { DocumentView } from '../nodes/DocumentView'; import { FieldView, FieldViewProps } from '../nodes/FieldView'; import "./SearchBox.scss"; -export const searchSchema = createSchema({ - id: "string", - Document: Doc, - searchQuery: "string", -}); - -export enum Keys { - TITLE = "title", - AUTHOR = "author", - DATA = "data", - TEXT = "text" -} +export const searchSchema = createSchema({ Document: Doc }); type SearchBoxDocument = makeInterface<[typeof documentSchema, typeof searchSchema]>; const SearchBoxDocument = makeInterface(documentSchema, searchSchema); -//React.Component<SearchProps> @observer export class SearchBox extends ViewBoxBaseComponent<FieldViewProps, SearchBoxDocument>(SearchBoxDocument) { + public static LayoutString(fieldKey: string) { return FieldView.LayoutString(SearchBox, fieldKey); } + public static Instance: SearchBox; - get _searchString() { return this.layoutDoc.searchQuery; } - @computed set _searchString(value) { this.layoutDoc.searchQuery = (value); } - @observable private _resultsOpen: boolean = false; - @observable _searchbarOpen: boolean = false; - @observable private _results: [Doc, string[], string[]][] = []; - @observable private _openNoResults: boolean = false; - @observable private _visibleElements: JSX.Element[] = []; - @observable private _visibleDocuments: Doc[] = []; - - private _resultsSet = new Map<Doc, number>(); - private _resultsRef = React.createRef<HTMLDivElement>(); - public inputRef = React.createRef<HTMLInputElement>(); - - private _isSearch: ("search" | "placeholder" | undefined)[] = []; - private _isSorted: ("sorted" | "placeholder" | undefined)[] = []; - + private _allIcons: string[] = [DocumentType.INK, DocumentType.AUDIO, DocumentType.COL, DocumentType.IMG, DocumentType.LINK, DocumentType.PDF, DocumentType.RTF, DocumentType.VID, DocumentType.WEB]; + private _numResultsPerPage = 500; private _numTotalResults = -1; private _endIndex = -1; - - static Instance: SearchBox; - + private _lockPromise?: Promise<void>; + private _resultsSet = new Map<Doc, number>(); + private _inputRef = React.createRef<HTMLInputElement>(); private _maxSearchIndex: number = 0; private _curRequest?: Promise<any> = undefined; - public static LayoutString(fieldKey: string) { return FieldView.LayoutString(SearchBox, fieldKey); } + private _disposers: { [name: string]: IReactionDisposer } = {}; + private _blockedTypes = [DocumentType.PRESELEMENT, DocumentType.KVP, DocumentType.DOCHOLDER, DocumentType.SEARCH, DocumentType.SEARCHITEM, DocumentType.FONTICON, DocumentType.BUTTON, DocumentType.SCRIPTING]; - private new_buckets: { [characterName: string]: number } = {}; - //if true, any keywords can be used. if false, all keywords are required. - //this also serves as an indicator if the word status filter is applied - @observable private _basicWordStatus: boolean = false; - @observable private _nodeStatus: boolean = false; - @observable private _keyStatus: boolean = false; + private currentSelectedCollection: DocumentView | undefined = undefined; + private docsforfilter: Doc[] = []; + private realTotalResults: number = 0; + private collectionRef = React.createRef<HTMLSpanElement>(); - @observable private newAssign: boolean = true; + @observable _icons: string[] = this._allIcons; + @observable _results: [Doc, string[], string[]][] = []; + @observable _visibleElements: JSX.Element[] = []; + @observable _visibleDocuments: Doc[] = []; + @observable _deletedDocsStatus: boolean = false; + @observable _onlyAliases: boolean = true; + @observable _searchbarOpen = false; + @observable _searchFullDB = "DB"; + @observable _noResults = ""; + @observable _pageStart = 0; + @observable open = false; + @observable children = 0; + @observable newsearchstring = ""; + @observable headercount: number = 0; + @observable headerscale: number = 0; + @observable filter = false; constructor(props: any) { super(props); SearchBox.Instance = this; - this.resultsScrolled = this.resultsScrolled.bind(this); - } - @observable setupButtons = false; - componentDidMount = () => { - if (this.setupButtons === false) { - runInAction(() => this.setupButtons = true); - } - if (this.inputRef.current) { - this.inputRef.current.focus(); - runInAction(() => { this._searchbarOpen = true; }); + componentDidMount = action(() => { + if (this._inputRef.current) { + this._inputRef.current.focus(); + this._searchbarOpen = true; } - if (this.rootDoc.searchQuery && this.newAssign) { - const sq = this.rootDoc.searchQuery; - runInAction(() => { + }) - // this._deletedDocsStatus=this.props.filterQuery!.deletedDocsStatus; - // this._authorFieldStatus=this.props.filterQuery!.authorFieldStatus - // this._titleFieldStatus=this.props.filterQuery!.titleFieldStatus; - // this._basicWordStatus=this.props.filterQuery!.basicWordStatus; - // this._icons=this.props.filterQuery!.icons; - this.newAssign = false; - }); - runInAction(() => { - this.layoutDoc._searchString = StrCast(sq); - this.submitSearch(); - }); - } + componentWillUnmount() { + Object.values(this._disposers).forEach(disposer => disposer?.()); } - @action getViews = (doc: Doc) => SearchUtil.GetViewsOfDocument(doc) - - @observable newsearchstring: string = ""; @action.bound onChange(e: React.ChangeEvent<HTMLInputElement>) { this.layoutDoc._searchString = e.target.value; this.newsearchstring = e.target.value; - - if (e.target.value === "") { - this._results.forEach(result => { - Doc.UnBrushDoc(result[0]); - result[0].searchMatch = undefined; - }); + if (this.currentSelectedCollection) { + this.doLoop(this.currentSelectedCollection, undefined); + } + this.closeSearch(false); - this.props.Document._schemaHeaders = new List<SchemaHeaderField>([]); if (this.currentSelectedCollection !== undefined) { this.currentSelectedCollection.props.Document._searchDocs = new List<Doc>([]); this.currentSelectedCollection = undefined; this.props.Document.selectedDoc = undefined; - } - runInAction(() => { this.open = false; }); - this._openNoResults = false; + this.open = false; this._results = []; this._resultsSet.clear(); this._visibleElements = []; @@ -150,124 +113,93 @@ export class SearchBox extends ViewBoxBaseComponent<FieldViewProps, SearchBoxDoc } } - enter = (e: React.KeyboardEvent) => { - if (e.key === "Enter") { + enter = action((e: React.KeyboardEvent | undefined) => { + if (!e || e.key === "Enter") { this.layoutDoc._searchString = this.newsearchstring; - - if (StrCast(this.layoutDoc._searchString) !== "" || !this.searchFullDB) { - runInAction(() => this.open = true); - } - else { - runInAction(() => this.open = false); - - } + this._pageStart = 0; + this.open = StrCast(this.layoutDoc._searchString) !== "" || this._searchFullDB !== "DB"; this.submitSearch(); } - } - - @observable open: boolean = false; - - - public static async convertDataUri(imageUri: string, returnedFilename: string) { - try { - const posting = Utils.prepend("/uploadURI"); - const returnedUri = await rp.post(posting, { - body: { - uri: imageUri, - name: returnedFilename - }, - json: true, - }); - return returnedUri; - - } catch (e) { - console.log("SearchBox:" + e); - } - } - - public _allIcons: string[] = [DocumentType.INK, DocumentType.AUDIO, DocumentType.COL, DocumentType.IMG, DocumentType.LINK, DocumentType.PDF, DocumentType.RTF, DocumentType.VID, DocumentType.WEB]; - //if true, any keywords can be used. if false, all keywords are required. - //this also serves as an indicator if the word status filter is applied - @observable private _filterOpen: boolean = false; - //if icons = all icons, then no icon filter is applied - // get _icons() { return this.props.searchFileTypes; } - // set _icons(value) { - // this.props.setSearchFileTypes(value); - // } - @observable _icons: string[] = this._allIcons; - //if all of these are true, no key filter is applied - @observable private _titleFieldStatus: boolean = true; - @observable private _authorFieldStatus: boolean = true; - //this also serves as an indicator if the collection status filter is applied - @observable public _deletedDocsStatus: boolean = false; - @observable private _collectionStatus = false; - + }); getFinalQuery(query: string): string { //alters the query so it looks in the correct fields - //if this is true, then not all of the field boxes are checked + //if this is true, th`en not all of the field boxes are checked //TODO: data - if (this.fieldFiltersApplied) { - query = this.applyBasicFieldFilters(query); - query = query.replace(/\s+/g, ' ').trim(); - } + const initialfilters = Cast(this.props.Document._docFilters, listSpec("string"), []); + + const filters: string[] = []; - //alters the query based on if all words or any words are required - //if this._wordstatus is false, all words are required and a + is added before each - if (!this._basicWordStatus) { - query = this.basicRequireWords(query); - query = query.replace(/\s+/g, ' ').trim(); + for (let i = 0; i < initialfilters.length; i = i + 3) { + if (initialfilters[i + 2] !== undefined) { + filters.push(initialfilters[i]); + filters.push(initialfilters[i + 1]); + filters.push(initialfilters[i + 2]); + } } - // if should be searched in a specific collection - if (this._collectionStatus) { - query = this.addCollectionFilter(query); - query = query.replace(/\s+/g, ' ').trim(); + const finalfilters: { [key: string]: string[] } = {}; + for (let i = 0; i < filters.length; i = i + 3) { + if (finalfilters[filters[i]] !== undefined) { + finalfilters[filters[i]].push(filters[i + 1]); + } + else { + finalfilters[filters[i]] = [filters[i + 1]]; + } } - return query; - } - basicRequireWords(query: string): string { - return query.split(" ").join(" + ").replace(/ + /, ""); + for (const key in finalfilters) { + const values = finalfilters[key]; + if (values.length === 1) { + const mod = "_t:"; + const newWords: string[] = []; + const oldWords = values[0].split(" "); + oldWords.forEach((word, i) => { + i === 0 ? newWords.push(key + mod + "\"" + word + "\"") : newWords.push("AND " + key + mod + "\"" + word + "\""); + }); + query = `(${query}) AND (${newWords.join(" ")})`; + } + else { + for (let i = 0; i < values.length; i++) { + const mod = "_t:"; + const newWords: string[] = []; + const oldWords = values[i].split(" "); + oldWords.forEach((word, i) => { + i === 0 ? newWords.push(key + mod + "\"" + word + "\"") : newWords.push("AND " + key + mod + "\"" + word + "\""); + }); + const v = "(" + newWords.join(" ") + ")"; + if (i === 0) { + query = `(${query}) AND (${v}`; + if (values.length === 1) { + query = query + ")"; + } + } + else if (i === values.length - 1) { + query = query + " OR " + v + ")"; + } + else { + query = query + " OR " + v; + } + } + } + } + + return query.replace(/-\s+/g, ''); } @action filterDocsByType(docs: Doc[]) { const finalDocs: Doc[] = []; - const blockedTypes: string[] = ["preselement", "docholder", "collection", "search", "searchitem", "script", "fonticonbox", "button", "label"]; docs.forEach(doc => { - const layoutresult = Cast(doc.type, "string"); - if (layoutresult && !blockedTypes.includes(layoutresult)) { - if (layoutresult && this._icons.includes(layoutresult)) { - finalDocs.push(doc); - } + const layoutresult = StrCast(doc.type, "string") as DocumentType; + if (layoutresult && !this._blockedTypes.includes(layoutresult) && this._icons.includes(layoutresult)) { + finalDocs.push(doc); } }); return finalDocs; } - addCollectionFilter(query: string): string { - const collections: Doc[] = this.getCurCollections(); - const oldWords = query.split(" "); - - const collectionString: string[] = []; - collections.forEach(doc => { - const proto = doc.proto; - const protoId = (proto || doc)[Id]; - const colString: string = "{!join from=data_l to=id}id:" + protoId + " "; - collectionString.push(colString); - }); - - let finalColString = collectionString.join(" "); - finalColString = finalColString.trim(); - return "+(" + finalColString + ")" + query; - } - - get filterTypes() { - return this._icons.length === this._allIcons.length ? undefined : this._icons; - } - //TODO: basically all of this //gets all of the collections of all the docviews that are selected //if a collection is the only thing selected, search only in that collection (not its container) @@ -293,12 +225,10 @@ export class SearchBox extends ViewBoxBaseComponent<FieldViewProps, SearchBoxDoc } - currentSelectedCollection: DocumentView | undefined = undefined; - docsforfilter: Doc[] = []; - searchCollection(query: string) { - const selectedCollection: DocumentView = SelectionManager.SelectedDocuments()[0]; + const selectedCollection = SelectionManager.SelectedDocuments()[0]; query = query.toLowerCase(); + if (selectedCollection !== undefined) { this.currentSelectedCollection = selectedCollection; if (this.filter === true) { @@ -316,14 +246,8 @@ export class SearchBox extends ViewBoxBaseComponent<FieldViewProps, SearchBoxDoc newarray.push(...DocListCast(d.data)); } const hlights: string[] = []; - const protos = Doc.GetAllPrototypes(d); - protos.forEach(proto => { - Object.keys(proto).forEach(key => { - if (StrCast(d[key]).toLowerCase().includes(query) && !hlights.includes(key)) { - hlights.push(key); - } - }); - }); + this.documentKeys(d).forEach(key => + Field.toString(d[key] as Field).toLowerCase().includes(query) && !hlights.includes(key) && hlights.push(key)); if (hlights.length > 0) { found.push([d, hlights, []]); docsforFilter.push(d); @@ -335,30 +259,17 @@ export class SearchBox extends ViewBoxBaseComponent<FieldViewProps, SearchBoxDoc this.docsforfilter = docsforFilter; if (this.filter === true) { selectedCollection.props.Document._searchDocs = new List<Doc>(docsforFilter); - docs = DocListCast(selectedCollection.dataDoc[Doc.LayoutFieldKey(selectedCollection.dataDoc)]); - while (docs.length > 0) { - newarray = []; - docs.forEach((d) => { - if (d.data !== undefined) { - d._searchDocs = new List<Doc>(docsforFilter); - const newdocs = DocListCast(d.data); - newdocs.forEach((newdoc) => { - newarray.push(newdoc); - }); - } - }); - docs = newarray; - } + this.doLoop(selectedCollection, docsforFilter); } this._numTotalResults = found.length; + this.realTotalResults = found.length; } else { - this.noresults = "No collection selected :("; + this._noResults = "No collection selected :("; } } - documentKeys(doc: Doc) { const keys: { [key: string]: boolean } = {}; // bcz: ugh. this is untracked since otherwise a large collection of documents will blast the server for all their fields. @@ -367,135 +278,79 @@ export class SearchBox extends ViewBoxBaseComponent<FieldViewProps, SearchBoxDoc // then by the time the options button is clicked, all of the fields should be in place. If a new field is added while this menu // is displayed (unlikely) it won't show up until something else changes. //TODO Types - Doc.GetAllPrototypes(doc).map - (proto => Object.keys(proto).forEach(key => keys[key] = false)); + Doc.GetAllPrototypes(doc).map(proto => Object.keys(proto).forEach(key => keys[key] = false)); return Array.from(Object.keys(keys)); } - applyBasicFieldFilters(query: string) { - let finalQuery = ""; - - if (this._titleFieldStatus) { - finalQuery = finalQuery + this.basicFieldFilters(query, Keys.TITLE); - } - if (this._authorFieldStatus) { - finalQuery = finalQuery + this.basicFieldFilters(query, Keys.AUTHOR); - } - if (this._deletedDocsStatus) { - finalQuery = finalQuery + this.basicFieldFilters(query, Keys.DATA); - } - if (this._deletedDocsStatus) { - finalQuery = finalQuery + this.basicFieldFilters(query, Keys.TEXT); - } - return finalQuery; - } - - basicFieldFilters(query: string, type: string): string { - let mod = ""; - switch (type) { - case Keys.AUTHOR: mod = " author_t:"; break; - case Keys.DATA: break; // TODO - case Keys.TITLE: mod = " _title_t:"; break; - case Keys.TEXT: mod = " text_t:"; break; - } - - const newWords: string[] = []; - const oldWords = query.split(" "); - oldWords.forEach(word => newWords.push(mod + word)); - - query = newWords.join(" "); - - return query; - } - - get fieldFiltersApplied() { return !(this._authorFieldStatus && this._titleFieldStatus); } - @action submitSearch = async (reset?: boolean) => { + if (this.currentSelectedCollection !== undefined) { + this.doLoop(this.currentSelectedCollection, undefined); + } if (reset) { this.layoutDoc._searchString = ""; } - this.props.Document._docFilters = undefined; - this.noresults = ""; + //this.props.Document._docFilters = new List(); + this._noResults = ""; this.dataDoc[this.fieldKey] = new List<Doc>([]); this.headercount = 0; this.children = 0; - this.buckets = []; - this.new_buckets = {}; - const query = StrCast(this.layoutDoc._searchString); + let query = StrCast(this.layoutDoc._searchString); Doc.SetSearchQuery(query); - this.getFinalQuery(query); - this._results.forEach(result => { - Doc.UnBrushDoc(result[0]); - result[0].searchMatch = undefined; - }); + this._searchFullDB ? query = this.getFinalQuery(query) : console.log("local"); + this.closeSearch(false); this._results = []; this._resultsSet.clear(); - this._isSearch = []; - this._isSorted = []; this._visibleElements = []; this._visibleDocuments = []; - if (StrCast(this.props.Document.searchQuery)) { - if (this._timeout) { clearTimeout(this._timeout); this._timeout = undefined; } - this._timeout = setTimeout(() => { - console.log("Resubmitting search"); - }, 60000); - } - if (query !== "") { + if (query !== "" || this._searchFullDB === "My Stuff") { this._endIndex = 12; this._maxSearchIndex = 0; this._numTotalResults = -1; - this.searchFullDB ? await this.getResults(query) : this.searchCollection(query); + this._searchFullDB ? await this.getResults(query) : this.searchCollection(query); runInAction(() => { - this._resultsOpen = true; this._searchbarOpen = true; - this._openNoResults = true; this.resultsScrolled(); - }); } } - @observable searchFullDB = true; - - @observable _timeout: any = undefined; - - @observable firststring: string = ""; - @observable secondstring: string = ""; - - @observable bucketcount: number[] = []; - @observable buckets: Doc[] | undefined; - getAllResults = async (query: string) => { return SearchUtil.Search(query, true, { fq: this.filterQuery, start: 0, rows: 10000000 }); } private get filterQuery() { - const types = ["preselement", "docholder", "collection", "search", "searchitem", "script", "fonticonbox", "button", "label"]; // this.filterTypes; - const baseExpr = "NOT baseProto_b:true AND NOT system_b:true"; - const includeDeleted = this.getDataStatus() ? "" : " NOT deleted_b:true"; - const includeIcons = this.getDataStatus() ? "" : " NOT type_t:fonticonbox"; - const typeExpr = !types ? "" : ` ${types.map(type => `NOT ({!join from=id to=proto_i}type_t:${type}) AND NOT type_t:${type}`).join(" AND ")}`; + const baseExpr = "NOT system_b:true"; + const authorExpr = this._searchFullDB === "My Stuff" ? ` author_t:${Doc.CurrentUserEmail}` : undefined; + const includeDeleted = this._deletedDocsStatus ? "" : " NOT deleted_b:true"; + const typeExpr = this._onlyAliases ? "NOT {!join from=id to=proto_i}type_t:*" : `(type_t:* OR {!join from=id to=proto_i}type_t:*) ${this._blockedTypes.map(type => `NOT ({!join from=id to=proto_i}type_t:${type}) AND NOT type_t:${type}`).join(" AND ")}`; // fq: type_t:collection OR {!join from=id to=proto_i}type_t:collection q:text_t:hello - const query = [baseExpr, includeDeleted, includeIcons, typeExpr].join(" AND ").replace(/AND $/, ""); + const query = [baseExpr, authorExpr, includeDeleted, typeExpr].filter(q => q).join(" AND ").replace(/AND $/, ""); return query; } - getDataStatus() { return this._deletedDocsStatus; } + @computed get primarySort() { + const suffixMap = (type: ColumnType) => { + switch (type) { + case ColumnType.Date: return "_d"; + case ColumnType.String: return "_t"; + case ColumnType.Boolean: return "_b"; + case ColumnType.Number: return "_n"; + } + }; + const headers = Cast(this.props.Document._schemaHeaders, listSpec(SchemaHeaderField), []); + return headers.reduce((p: Opt<string>, header: SchemaHeaderField) => p || (header.desc !== undefined && suffixMap(header.type) ? (header.heading + suffixMap(header.type) + (header.desc ? " desc" : " asc")) : undefined), undefined); + } - private NumResults = 25; - private lockPromise?: Promise<void>; getResults = async (query: string) => { - console.log("Get"); - if (this.lockPromise) { - await this.lockPromise; - } - this.lockPromise = new Promise(async res => { + 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: this.NumResults, hl: true, "hl.fl": "*", }).then(action(async (res: SearchUtil.DocSearchResult) => { + this._curRequest = SearchUtil.Search(query, true, { onlyAliases: true, allowAliases: true, /*sort: this.primarySort,*/ fq: this.filterQuery, start: 0, rows: this._numResultsPerPage, hl: true, "hl.fl": "*", }).then(action(async (res: SearchUtil.DocSearchResult) => { // happens at the beginning + this.realTotalResults = res.numFound <= 0 ? 0 : res.numFound; if (res.numFound !== this._numTotalResults && this._numTotalResults === -1) { this._numTotalResults = res.numFound; } @@ -503,44 +358,31 @@ export class SearchBox extends ViewBoxBaseComponent<FieldViewProps, SearchBoxDoc const highlightList = res.docs.map(doc => highlighting[doc[Id]]); const lines = new Map<string, string[]>(); res.docs.map((doc, i) => lines.set(doc[Id], res.lines[i])); - const docs = await Promise.all(res.docs.map(async doc => (await Cast(doc.extendsDoc, Doc)) || doc)); + const docs = res.docs; const highlights: typeof res.highlighting = {}; docs.forEach((doc, index) => highlights[doc[Id]] = highlightList[index]); const filteredDocs = this.filterDocsByType(docs); - runInAction(() => { - filteredDocs.forEach((doc, i) => { - const index = this._resultsSet.get(doc); - const highlight = highlights[doc[Id]]; - const line = lines.get(doc[Id]) || []; - const hlights = highlight ? Object.keys(highlight).map(key => key.substring(0, key.length - 2)).filter(k => k) : []; - doc ? console.log(Cast(doc.context, Doc)) : null; - if (this.findCommonElements(hlights)) { - } - else { - const layoutresult = Cast(doc.type, "string"); - if (layoutresult) { - if (this.new_buckets[layoutresult] === undefined) { - this.new_buckets[layoutresult] = 1; - } - else { - this.new_buckets[layoutresult] = this.new_buckets[layoutresult] + 1; - } - } - if (index === undefined) { - this._resultsSet.set(doc, this._results.length); - this._results.push([doc, hlights, line]); - } else { - this._results[index][1].push(...hlights); - this._results[index][2].push(...line); - } - } - }); - }); + runInAction(() => filteredDocs.forEach((doc, i) => { + const index = this._resultsSet.get(doc); + const highlight = highlights[doc[Id]]; + const line = lines.get(doc[Id]) || []; + const hlights = highlight ? Object.keys(highlight).map(key => key.substring(0, key.length - 2)).filter(k => k) : []; + // if (this.findCommonElements(hlights)) { + // } + if (index === undefined) { + this._resultsSet.set(doc, this._results.length); + this._results.push([doc, hlights, line]); + } else { + this._results[index][1].push(...hlights); + this._results[index][2].push(...line); + } + + })); this._curRequest = undefined; })); - this._maxSearchIndex += this.NumResults; + this._maxSearchIndex += this._numResultsPerPage; await this._curRequest; } @@ -548,21 +390,13 @@ export class SearchBox extends ViewBoxBaseComponent<FieldViewProps, SearchBoxDoc this.resultsScrolled(); res(); }); - return this.lockPromise; + return this._lockPromise; } - @observable noresults = ""; - collectionRef = React.createRef<HTMLSpanElement>(); + startDragCollection = async () => { const res = await this.getAllResults(this.getFinalQuery(StrCast(this.layoutDoc._searchString))); const filtered = this.filterDocsByType(res.docs); - const docs = filtered.map(doc => { - const isProto = Doc.GetT(doc, "isPrototype", "boolean", true); - if (isProto) { - return Doc.MakeDelegate(doc); - } else { - return Doc.MakeAlias(doc); - } - }); + const docs = filtered.map(doc => Doc.GetT(doc, "isPrototype", "boolean", true) ? Doc.MakeDelegate(doc) : Doc.MakeAlias(doc)); let x = 0; let y = 0; for (const doc of docs.map(d => Doc.Layout(d))) { @@ -586,26 +420,27 @@ export class SearchBox extends ViewBoxBaseComponent<FieldViewProps, SearchBoxDoc y += 300; } } - return Docs.Create.SearchDocument({ _autoHeight: true, _viewType: CollectionViewType.Schema, title: StrCast(this.layoutDoc._searchString), searchQuery: StrCast(this.layoutDoc._searchString) }); + return Docs.Create.SchemaDocument(Cast(this.props.Document._schemaHeaders, listSpec(SchemaHeaderField), []), DocListCast(this.dataDoc[this.fieldKey]), { _autoHeight: true, _viewType: CollectionViewType.Schema, title: StrCast(this.layoutDoc._searchString) }); } @action.bound openSearch(e: React.SyntheticEvent) { e.stopPropagation(); - this._openNoResults = false; - this._resultsOpen = true; + this._results.forEach(result => Doc.BrushDoc(result[0])); this._searchbarOpen = true; } @action.bound - closeSearch = () => { - //this.closeResults(); - this._searchbarOpen = false; + closeSearch = (closesearchbar = true) => { + this._results.forEach(result => { + Doc.UnBrushDoc(result[0]); + result[0].searchMatch = undefined; + }); + closesearchbar && (this._searchbarOpen = false); } @action.bound closeResults() { - this._resultsOpen = false; this._results = []; this._resultsSet.clear(); this._visibleElements = []; @@ -615,26 +450,10 @@ export class SearchBox extends ViewBoxBaseComponent<FieldViewProps, SearchBoxDoc this._curRequest = undefined; } - @observable children: number = 0; @action resultsScrolled = (e?: React.UIEvent<HTMLDivElement>) => { - if (!this._resultsRef.current) return; - this.props.Document._schemaHeaders = new List<SchemaHeaderField>([]); - - const scrollY = e ? e.currentTarget.scrollTop : this._resultsRef.current ? this._resultsRef.current.scrollTop : 0; - const itemHght = 53; - const startIndex = Math.floor(Math.max(0, scrollY / itemHght)); - //const endIndex = Math.ceil(Math.min(this._numTotalResults - 1, startIndex + (this._resultsRef.current.getBoundingClientRect().height / itemHght))); - const endIndex = 30; - //this._endIndex = endIndex === -1 ? 12 : endIndex; this._endIndex = 30; - const headers = new Set<string>(["title", "author", "*lastModified", "text"]); - if ((this._numTotalResults === 0 || this._results.length === 0) && this._openNoResults) { - if (this.noresults === "") { - this.noresults = "No search results :("; - } - return; - } + const headers = new Set<string>(["title", "author", "text", "type", "data", "*lastModified", "context"]); if (this._numTotalResults <= this._maxSearchIndex) { this._numTotalResults = this._results.length; @@ -646,278 +465,182 @@ export class SearchBox extends ViewBoxBaseComponent<FieldViewProps, SearchBoxDoc // undefined until a searchitem is put in there this._visibleElements = Array<JSX.Element>(this._numTotalResults === -1 ? 0 : this._numTotalResults); this._visibleDocuments = Array<Doc>(this._numTotalResults === -1 ? 0 : this._numTotalResults); - // indicates if things are placeholders - this._isSearch = Array<undefined>(this._numTotalResults === -1 ? 0 : this._numTotalResults); - this._isSorted = Array<undefined>(this._numTotalResults === -1 ? 0 : this._numTotalResults); - } - for (let i = 0; i < this._numTotalResults; i++) { + let max = this._numResultsPerPage; + max > this._results.length ? max = this._results.length : console.log(""); + for (let i = this._pageStart; i < max; 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._isSorted[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[], string[]] | undefined = undefined; - if (i >= this._results.length) { - this.getResults(StrCast(this.layoutDoc._searchString)); - if (i < this._results.length) result = this._results[i]; - if (result) { - const highlights = Array.from([...Array.from(new Set(result[1]).values())]); - const lines = new List<string>(result[2]); - result[0].lines = lines; - result[0].highlighting = highlights.join(", "); - highlights.forEach((item) => headers.add(item)); - this._visibleDocuments[i] = result[0]; - this._isSearch[i] = "search"; - Doc.BrushDoc(result[0]); - result[0].searchMatch = true; - Doc.AddDocToList(this.dataDoc, this.props.fieldKey, result[0]); - this.children++; - } - } - else { - result = this._results[i]; - if (result) { - const highlights = Array.from([...Array.from(new Set(result[1]).values())]); - const lines = new List<string>(result[2]); - highlights.forEach((item) => headers.add(item)); - result[0].lines = lines; - result[0].highlighting = highlights.join(", "); - result[0].searchMatch = true; - if (i < this._visibleDocuments.length) { - this._visibleDocuments[i] = result[0]; - this._isSearch[i] = "search"; - Doc.BrushDoc(result[0]); - Doc.AddDocToList(this.dataDoc, this.props.fieldKey, result[0]); - this.children++; - } - } - } + + let result: [Doc, string[], string[]] | undefined = undefined; + + result = this._results[i]; + if (result) { + const highlights = Array.from([...Array.from(new Set(result[1]).values())]); + const lines = new List<string>(result[2]); + highlights.forEach((item) => headers.add(item)); + result[0].lines = lines; + result[0].highlighting = highlights.join(", "); + result[0].searchMatch = true; + if (i < this._visibleDocuments.length) { + this._visibleDocuments[i] = result[0]; + Doc.BrushDoc(result[0]); + Doc.AddDocToList(this.dataDoc, this.props.fieldKey, result[0]); + this.children++; } } } - const schemaheaders: SchemaHeaderField[] = []; this.headerscale = headers.size; - headers.forEach((item) => schemaheaders.push(new SchemaHeaderField(item, "#f1efeb"))); - this.headercount = schemaheaders.length; - this.props.Document._schemaHeaders = new List<SchemaHeaderField>(schemaheaders); + const oldSchemaHeaders = Cast(this.props.Document._schemaHeaders, listSpec("string"), []); + if (oldSchemaHeaders?.length && typeof oldSchemaHeaders[0] !== "object") { + const newSchemaHeaders = oldSchemaHeaders.map(i => typeof i === "string" ? new SchemaHeaderField(i, "#f1efeb") : i); + headers.forEach(header => { + if (oldSchemaHeaders.includes(header) === false) { + newSchemaHeaders.push(new SchemaHeaderField(header, "#f1efeb")); + } + }); + this.headercount = newSchemaHeaders.length; + this.props.Document._schemaHeaders = new List<SchemaHeaderField>(newSchemaHeaders); + } else if (this.props.Document._schemaHeaders === undefined) { + this.props.Document._schemaHeaders = new List<SchemaHeaderField>([new SchemaHeaderField("title", "#f1efeb")]); + } if (this._maxSearchIndex >= this._numTotalResults) { this._visibleElements.length = this._results.length; this._visibleDocuments.length = this._results.length; - this._isSearch.length = this._results.length; } } - @observable headercount: number = 0; - @observable headerscale: number = 0; findCommonElements(arr2: string[]) { const arr1 = ["layout", "data"]; return arr1.some(item => arr2.includes(item)); } - @computed - get resFull() { return this._numTotalResults <= 8; } - - @computed - get resultHeight() { return this._numTotalResults * 70; } + getTransform = () => this.props.ScreenToLocalTransform().translate(-5, -65);// listBox padding-left and pres-box-cont minHeight + panelHeight = () => this.props.PanelHeight(); - addButtonDoc = (doc: Doc) => Doc.AddDocToList(CurrentUserUtils.UserDocument.expandingButtons as Doc, "data", doc); - remButtonDoc = (doc: Doc) => Doc.RemoveDocFromList(CurrentUserUtils.UserDocument.expandingButtons as Doc, "data", doc); - moveButtonDoc = (doc: Doc, targetCollection: Doc | undefined, addDocument: (document: Doc) => boolean) => this.remButtonDoc(doc) && addDocument(doc); - - @computed get searchItemTemplate() { return Cast(Doc.UserDoc().searchItemTemplate, Doc, null); } - - getTransform = () => { - return this.props.ScreenToLocalTransform().translate(-5, -65);// listBox padding-left and pres-box-cont minHeight - } - panelHeight = () => { - return this.props.PanelHeight(); - } selectElement = (doc: Doc) => { //this.gotoDocument(this.childDocs.indexOf(doc), NumCasst(this.layoutDoc._itemIndex)); } + returnHeight = () => 31 + 31 * 6; + returnLength = () => Math.min(window.innerWidth, 51 + 205 * Cast(this.props.Document._schemaHeaders, listSpec(SchemaHeaderField), []).length); + + @action + changeSearchScope = (scope: string) => { + scope && (this.filter = false); + this._searchFullDB = scope; + this.dataDoc[this.fieldKey] = new List<Doc>([]); + if (this.currentSelectedCollection !== undefined) { + this.doLoop(this.currentSelectedCollection, undefined); + } + this.submitSearch(); + } - addDocument = (doc: Doc) => { - return null; + @computed get scopeButtons() { + return <div style={{ + height: 25, + paddingLeft: "4px", + paddingRight: "4px", + border: "1px solid gray", + borderRadius: "0.3em", + borderBottom: !this.open ? "1px solid" : "none", + }}> + <form className="beta" style={{ justifyContent: "space-evenly", display: "flex" }}> + <div style={{ display: "contents" }}> + <div className="radio" style={{ margin: 0 }}> + <label style={{ fontSize: 12, marginTop: 6 }} > + <input type="radio" style={{ marginLeft: -16, marginTop: -1 }} checked={!this._searchFullDB} onChange={() => this.changeSearchScope("")} /> + Collection + </label> + </div> + <div className="radio" style={{ margin: 0 }}> + <label style={{ fontSize: 12, marginTop: 6 }} > + <input type="radio" style={{ marginLeft: -16, marginTop: -1 }} checked={this._searchFullDB?.length ? true : false} onChange={() => this.changeSearchScope("DB")} /> + DB + <span onClick={action(() => this._searchFullDB = this._searchFullDB === "My Stuff" ? "DB" : "My Stuff")}> + {this._searchFullDB === "My Stuff" ? "(me)" : "(full)"} + </span> + </label> + </div> + </div> + </form> + </div>; } - @observable filter = false; + doLoop = (collectionView: DocumentView, filter: Doc[] | undefined) => { + let docs = DocListCast(collectionView.dataDoc[Doc.LayoutFieldKey(collectionView.dataDoc)]); + let newarray: Doc[] = []; + while (docs.length > 0) { + newarray = []; + docs.forEach(d => { + const subDocs = DocListCast(d.data); + if (subDocs.length) { + d._searchDocs = filter ? new List<Doc>(filter) : undefined; + DocListCast(d.data).forEach(newdoc => newarray.push(newdoc)); + } + }); + docs = newarray; + } + collectionView.props.Document._searchDocs = filter ? new List<Doc>(filter) : undefined; + this.props.Document.selectedDoc = filter ? collectionView.props.Document : undefined; + } - //Make id layour document render() { this.props.Document._chromeStatus === "disabled"; this.props.Document._searchDoc = true; - const cols = Cast(this.props.Document._schemaHeaders, listSpec(SchemaHeaderField), []).length; - let length = 0; - cols > 5 ? length = 1076 : length = cols * 205 + 51; - let height = 0; const rows = this.children; - rows > 8 ? height = 31 + 31 * 8 : height = 31 * rows + 31; return ( <div style={{ pointerEvents: "all" }} className="searchBox-container"> + <div style={{ position: "absolute", left: 15, height: 32, alignItems: "center", display: "flex" }}>{Doc.CurrentUserEmail}</div> <div className="searchBox-bar"> - <div style={{ position: "absolute", left: 15 }}>{Doc.CurrentUserEmail}</div> - <div style={{ display: "flex", alignItems: "center" }}> - <FontAwesomeIcon onPointerDown={SetupDrag(this.collectionRef, () => StrCast(this.layoutDoc._searchString) ? this.startDragCollection() : undefined)} icon={"search"} size="lg" - style={{ color: "black", padding: 1, left: 35, position: "relative" }} /> - - <div style={{ cursor: "default", left: 250, position: "relative", }}> - <Tooltip title={<div className="dash-tooltip" >only display documents matching search</div>} ><div> - <FontAwesomeIcon icon={"filter"} size="lg" - style={{ padding: 1, backgroundColor: this.filter ? "white" : "lightgray", color: this.filter ? "black" : "white" }} - onPointerDown={e => { e.stopPropagation(); SetupDrag(this.collectionRef, () => StrCast(this.layoutDoc._searchString) ? this.startDragCollection() : undefined); }} - onClick={action(() => { - const dofilter = (currentSelectedCollection: DocumentView) => { - let docs = DocListCast(currentSelectedCollection.dataDoc[Doc.LayoutFieldKey(currentSelectedCollection.dataDoc)]); - while (docs.length > 0) { - const newarray: Doc[] = []; - docs.filter(d => d.data !== undefined).forEach((d) => { - d._searchDocs = new List<Doc>(this.docsforfilter); - newarray.push(...DocListCast(d.data)); - }); - docs = newarray; - } - }; - this.filter = !this.filter && !this.searchFullDB; - if (this.filter === true && this.currentSelectedCollection !== undefined) { - this.currentSelectedCollection.props.Document._searchDocs = new List<Doc>(this.docsforfilter); - - dofilter(this.currentSelectedCollection); - - this.currentSelectedCollection.props.Document._docFilters = new List<string>(Cast(this.props.Document._docFilters, listSpec("string"), [])); - this.props.Document.selectedDoc = this.currentSelectedCollection.props.Document; - } - else if (this.filter === false && this.currentSelectedCollection !== undefined) { - - dofilter(this.currentSelectedCollection); - - this.currentSelectedCollection.props.Document._searchDocs = new List<Doc>([]); - this.currentSelectedCollection.props.Document._docFilters = undefined; - this.props.Document.selectedDoc = undefined; - } - } - )} /> - </div></Tooltip></div> - <input value={this.newsearchstring} autoComplete="off" onChange={this.onChange} type="text" placeholder="Search..." id="search-input" ref={this.inputRef} + <div style={{ position: "relative", display: "flex", width: 450 }}> + <input value={this.newsearchstring} autoComplete="off" onChange={this.onChange} type="text" placeholder="Search..." id="search-input" ref={this._inputRef} className="searchBox-barChild searchBox-input" onPointerDown={this.openSearch} onKeyPress={this.enter} onFocus={this.openSearch} - style={{ padding: 1, paddingLeft: 20, paddingRight: 20, color: "black", height: 20, width: 250 }} /> - <div style={{ - height: 25, - paddingLeft: "4px", - paddingRight: "4px", - border: "1px solid gray", - borderRadius: "0.3em", - borderBottom: this.open === false ? "1px solid" : "none", - }}> - <form className="beta" style={{ justifyContent: "space-evenly", display: "flex" }}> - <div style={{ display: "contents" }}> - <div className="radio" style={{ margin: 0 }}> - <label style={{ fontSize: 12, marginTop: 6 }} > - <input type="radio" style={{ marginLeft: -16, marginTop: -1 }} checked={!this.searchFullDB} onChange={() => { - runInAction(() => { - this.searchFullDB = !this.searchFullDB; - this.dataDoc[this.fieldKey] = new List<Doc>([]); - if (this.currentSelectedCollection !== undefined) { - let newarray: Doc[] = []; - let docs: Doc[] = []; - docs = DocListCast(this.currentSelectedCollection.dataDoc[Doc.LayoutFieldKey(this.currentSelectedCollection.dataDoc)]); - while (docs.length > 0) { - newarray = []; - docs.forEach((d) => { - if (d.data !== undefined) { - d._searchDocs = new List<Doc>(); - const newdocs = DocListCast(d.data); - newdocs.forEach((newdoc) => { - newarray.push(newdoc); - }); - } - }); - docs = newarray; - } - this.currentSelectedCollection.props.Document._docFilters = undefined; - this.currentSelectedCollection.props.Document._searchDocs = undefined; - this.currentSelectedCollection = undefined; - } - this.submitSearch(); - }); - }} /> - Collection - </label> - </div> - <div className="radio" style={{ margin: 0 }}> - <label style={{ fontSize: 12, marginTop: 6 }} > - <input style={{ marginLeft: -16, marginTop: -1 }} type="radio" checked={this.searchFullDB} onChange={() => { - runInAction(() => { - this.searchFullDB = !this.searchFullDB; - this.dataDoc[this.fieldKey] = new List<Doc>([]); - this.filter = false; - if (this.currentSelectedCollection !== undefined) { - let newarray: Doc[] = []; - let docs: Doc[] = []; - docs = DocListCast(this.currentSelectedCollection.dataDoc[Doc.LayoutFieldKey(this.currentSelectedCollection.dataDoc)]); - while (docs.length > 0) { - newarray = []; - docs.forEach((d) => { - if (d.data !== undefined) { - d._searchDocs = new List<Doc>(); - const newdocs = DocListCast(d.data); - newdocs.forEach((newdoc) => { - newarray.push(newdoc); - }); - } - }); - docs = newarray; - } - this.currentSelectedCollection.props.Document._docFilters = undefined; - this.currentSelectedCollection.props.Document._searchDocs = undefined; - this.currentSelectedCollection = undefined; - } - this.submitSearch(); - }); - }} /> - DB - </label> + style={{ padding: 1, paddingLeft: 20, paddingRight: 60, color: "black", height: 20, width: 250 }} /> + <div style={{ display: "flex", alignItems: "center" }}> + <div style={{ position: "absolute", left: 10 }}> + <Tooltip title={<div className="dash-tooltip" >drag search results as collection</div>}> + <div><FontAwesomeIcon onPointerDown={SetupDrag(this.collectionRef, () => StrCast(this.layoutDoc._searchString) ? this.startDragCollection() : undefined)} icon={"search"} size="lg" + style={{ cursor: "hand", color: "black", padding: 1, position: "relative" }} /></div> + </Tooltip> + </div> + <div style={{ position: "absolute", left: 200, width: 30, zIndex: 9000, color: "grey", background: "white", }}> + {`${this._results.length}` + " of " + `${this.realTotalResults}`} + </div> + <div style={{ cursor: "default", left: 235, position: "absolute", }}> + <Tooltip title={<div className="dash-tooltip" >only display documents matching search</div>} > + <div> + <FontAwesomeIcon icon={"filter"} size="lg" + style={{ cursor: "hand", padding: 1, backgroundColor: this.filter ? "white" : "lightgray", color: this.filter ? "black" : "white" }} + onPointerDown={e => { e.stopPropagation(); SetupDrag(this.collectionRef, () => this.layoutDoc._searchString ? this.startDragCollection() : undefined); }} + onClick={action(() => { + this.filter = !this.filter && !this._searchFullDB; + this.currentSelectedCollection && this.doLoop(this.currentSelectedCollection, this.filter ? this.docsforfilter : undefined); + })} /> </div> - </div> - </form> + </Tooltip> + </div> + {this.scopeButtons} </div> - </div> - - </div> - <div style={{ zIndex: 20000, color: "black" }}> - {this._searchbarOpen === true ? - <div style={{ display: "flex", justifyContent: "center", }}> - {this.noresults === "" ? <div style={{ display: this.open === true ? "flex" : "none", overflow: "auto", }}> - <CollectionView {...this.props} - Document={this.props.Document} - moveDocument={returnFalse} - removeDocument={returnFalse} - PanelHeight={this.open === true ? () => height : () => 0} - PanelWidth={this.open === true ? () => length : () => 0} - overflow={cols > 5 || rows > 8 ? true : false} - focus={this.selectElement} - ScreenToLocalTransform={Transform.Identity} - /> - </div> : - <div style={{ display: "flex", justifyContent: "center" }}><div style={{ height: 200, top: 54, minWidth: 400, position: "absolute", backgroundColor: "rgb(241, 239, 235)", display: "flex", justifyContent: "center", alignItems: "center", border: "black 1px solid", }}> - <div>{this.noresults}</div> - </div></div>} - </div> : undefined} - </div> - <div className="searchBox-results" onScroll={this.resultsScrolled} style={{ - display: this._resultsOpen ? "flex" : "none", - height: this.resFull ? "auto" : this.resultHeight, - overflow: "visibile" // this.resFull ? "auto" : "visible" - }} ref={this._resultsRef}> + </div> </div> + {!this._searchbarOpen ? (null) : <div style={{ zIndex: 20000, color: "black" }}> + <div style={{ display: "flex", justifyContent: "center", }}> + <div style={{ display: this.open ? "flex" : "none", overflow: "auto", }}> + <CollectionView {...this.props} + Document={this.props.Document} + moveDocument={returnFalse} + removeDocument={returnFalse} + PanelHeight={this.open ? this.returnHeight : returnZero} + PanelWidth={this.open ? this.returnLength : returnZero} + overflow={length > window.innerWidth || rows > 6 ? true : false} + focus={this.selectElement} + ScreenToLocalTransform={Transform.Identity} + /> + </div> + </div> + </div>} </div > ); } -} +}
\ No newline at end of file |