diff options
author | tschicke-brown <tyler_schicke@brown.edu> | 2019-05-09 20:31:52 -0400 |
---|---|---|
committer | GitHub <noreply@github.com> | 2019-05-09 20:31:52 -0400 |
commit | 30aaf7c496eb4cbebac1e4cf9e0695cd286c63f2 (patch) | |
tree | fe887b7862ed82b7b59d657146ea8e627174ece7 /src | |
parent | 6691c55623fff5194b5fd1a830096e3925281301 (diff) | |
parent | b18aec5d4b07bbc859a5b1b2d22ca3bd92ca53cd (diff) |
Merge pull request #130 from browngraphicslab/new_search
New search
Diffstat (limited to 'src')
-rw-r--r-- | src/client/documents/Documents.ts | 1 | ||||
-rw-r--r-- | src/client/util/DocumentManager.ts | 31 | ||||
-rw-r--r-- | src/client/views/Main.scss | 3 | ||||
-rw-r--r-- | src/client/views/Main.tsx | 6 | ||||
-rw-r--r-- | src/client/views/SearchBox.scss | 99 | ||||
-rw-r--r-- | src/client/views/SearchBox.tsx | 133 | ||||
-rw-r--r-- | src/client/views/SearchItem.tsx | 52 | ||||
-rw-r--r-- | src/client/views/nodes/LinkBox.tsx | 23 | ||||
-rw-r--r-- | src/debug/Test.tsx | 7 | ||||
-rw-r--r-- | src/server/Search.ts | 45 | ||||
-rw-r--r-- | src/server/index.ts | 84 |
11 files changed, 454 insertions, 30 deletions
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index 63ba01b6a..11929455c 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -174,6 +174,7 @@ export namespace Docs { if (!("creationDate" in protoProps)) { protoProps.creationDate = new DateField; } + protoProps.isPrototype = true; return SetDelegateOptions(SetInstanceOptions(proto, protoProps, data), delegateProps); } diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts index 779b07ce5..a8b643d4d 100644 --- a/src/client/util/DocumentManager.ts +++ b/src/client/util/DocumentManager.ts @@ -1,9 +1,10 @@ import { computed, observable } from 'mobx'; import { DocumentView } from '../views/nodes/DocumentView'; import { Doc } from '../../new_fields/Doc'; -import { FieldValue, Cast, BoolCast } from '../../new_fields/Types'; +import { FieldValue, Cast, NumCast, BoolCast } from '../../new_fields/Types'; import { listSpec } from '../../new_fields/Schema'; -import { SelectionManager } from './SelectionManager'; +import { undoBatch } from './UndoManager'; +import { CollectionDockingView } from '../views/collections/CollectionDockingView'; export class DocumentManager { @@ -101,4 +102,30 @@ export class DocumentManager { return pairs; }, [] as { a: DocumentView, b: DocumentView, l: Doc }[]); } + + @undoBatch + public jumpToDocument = async (doc: Doc): Promise<void> => { + let docView = DocumentManager.Instance.getDocumentView(doc); + if (docView) { + docView.props.focus(docView.props.Document); + } else { + const contextDoc = await Cast(doc.annotationOn, Doc); + if (!contextDoc) { + CollectionDockingView.Instance.AddRightSplit(Doc.MakeDelegate(doc)); + } else { + const page = NumCast(doc.page, undefined); + const curPage = NumCast(contextDoc.curPage, undefined); + if (page !== curPage) { + contextDoc.curPage = page; + } + let contextView = DocumentManager.Instance.getDocumentView(contextDoc); + if (contextView) { + contextDoc.panTransformType = "Ease"; + contextView.props.focus(contextDoc); + } else { + CollectionDockingView.Instance.AddRightSplit(contextDoc); + } + } + } + } }
\ No newline at end of file diff --git a/src/client/views/Main.scss b/src/client/views/Main.scss index 5c5c252e9..2430e8f6c 100644 --- a/src/client/views/Main.scss +++ b/src/client/views/Main.scss @@ -47,7 +47,7 @@ h1 { } .jsx-parser { - width:100%; + width: 100%; pointer-events: none; border-radius: inherit; } @@ -184,6 +184,7 @@ button:hover { overflow: scroll; z-index: 1; } + #mainContent-div { width: 100%; height: 100%; diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx index 617580332..c9d5c395c 100644 --- a/src/client/views/Main.tsx +++ b/src/client/views/Main.tsx @@ -31,6 +31,7 @@ import "./Main.scss"; import { MainOverlayTextBox } from './MainOverlayTextBox'; import { DocumentView } from './nodes/DocumentView'; import { PreviewCursor } from './PreviewCursor'; +import { SearchBox } from './SearchBox'; import { SelectionManager } from '../util/SelectionManager'; import { FieldResult, Field, Doc, Opt } from '../../new_fields/Doc'; import { Cast, FieldValue, StrCast } from '../../new_fields/Types'; @@ -38,7 +39,6 @@ import { DocServer } from '../DocServer'; import { listSpec } from '../../new_fields/Schema'; import { Id } from '../../new_fields/RefField'; - @observer export class Main extends React.Component { public static Instance: Main; @@ -266,9 +266,11 @@ export class Main extends React.Component { <button className="toolbar-button round-button" title="Redo" onClick={() => UndoManager.Redo()}><FontAwesomeIcon icon="redo-alt" size="sm" /></button> <button className="toolbar-button round-button" title="Ink" onClick={() => InkingControl.Instance.toggleDisplay()}><FontAwesomeIcon icon="pen-nib" size="sm" /></button> </div >, - <div className="main-buttonDiv" key="logout" style={{ top: '34px', right: '1px', position: 'absolute' }} ref={logoutRef}> + <div className="main-searchDiv" key="search" style={{ top: '34px', right: '1px', position: 'absolute' }} > <SearchBox /> </div>, + <div className="main-buttonDiv" key="logout" style={{ bottom: '0px', right: '1px', position: 'absolute' }} ref={logoutRef}> <button onClick={() => request.get(DocServer.prepend(RouteStore.logout), emptyFunction)}>Log Out</button></div> ]; + } render() { diff --git a/src/client/views/SearchBox.scss b/src/client/views/SearchBox.scss new file mode 100644 index 000000000..792d6dd3c --- /dev/null +++ b/src/client/views/SearchBox.scss @@ -0,0 +1,99 @@ +@import "globalCssVariables"; + +.searchBox { + height: 32px; + //display: flex; + //padding: 4px; + -webkit-transition: width 0.4s ease-in-out; + transition: width 0.4s ease-in-out; + align-items: center; + + + + input[type=text] { + width: 130px; + -webkit-transition: width 0.4s; + transition: width 0.4s; + position: absolute; + right: 100px; + } + + input[type=text]:focus { + width: 500px; + outline: 3px solid lightblue; + } + + .filter-button { + position: absolute; + right: 30px; + } + + .submit-search { + text-align: right; + color: $dark-color; + -webkit-transition: right 0.4s; + transition: right 0.4s; + } + + .submit-search:hover { + color: $main-accent; + transform: scale(1.05); + cursor: pointer; + } +} + +.filter-form { + background: $dark-color; + height: 400px; + width: 400px; + position: relative; + right: 1px; + color: $light-color; + padding: 10px; + flex-direction: column; +} + +#header { + text-transform: uppercase; + letter-spacing: 2px; + font-size: 100%; + height: 40px; +} + +#option { + height: 20px; +} + +.results { + top: 300px; + display: flex; + flex-direction: column; + + .search-item { + width: 500px; + height: 50px; + background: $light-color-secondary; + display: flex; + justify-content: space-between; + align-items: center; + transition: all 0.1s; + border-width: 0.11px; + border-style: none; + border-color: $intermediate-color; + border-bottom-style: solid; + padding: 10px; + white-space: nowrap; + font-size: 13px; + } + + .search-item:hover { + transition: all 0.1s; + background: $lighter-alt-accent; + } + + .search-title { + text-transform: uppercase; + text-align: left; + width: 8vw; + } +}
\ No newline at end of file diff --git a/src/client/views/SearchBox.tsx b/src/client/views/SearchBox.tsx new file mode 100644 index 000000000..7dd1af4e7 --- /dev/null +++ b/src/client/views/SearchBox.tsx @@ -0,0 +1,133 @@ +import * as React from 'react'; +import { observer } from 'mobx-react'; +import { observable, action, runInAction } from 'mobx'; +import { Utils } from '../../Utils'; +import { MessageStore } from '../../server/Message'; +import "./SearchBox.scss"; +import { faSearch } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { library } from '@fortawesome/fontawesome-svg-core'; +// const app = express(); +// import * as express from 'express'; +import { Search } from '../../server/Search'; +import * as rp from 'request-promise'; +import { SearchItem } from './SearchItem'; +import { isString } from 'util'; +import { constant } from 'async'; +import { DocServer } from '../DocServer'; +import { Doc } from '../../new_fields/Doc'; +import { Id } from '../../new_fields/RefField'; +import { DocumentManager } from '../util/DocumentManager'; + + +library.add(faSearch); + +@observer +export class SearchBox extends React.Component { + @observable + searchString: string = ""; + + @observable private _open: boolean = false; + @observable private _resultsOpen: boolean = false; + + @observable + private _results: Doc[] = []; + + @action.bound + onChange(e: React.ChangeEvent<HTMLInputElement>) { + this.searchString = e.target.value; + } + + @action + submitSearch = async () => { + runInAction(() => this._results = []); + let query = this.searchString; + + let response = await rp.get('http://localhost:1050/search', { + qs: { + query + } + }); + let results = JSON.parse(response); + + //gets json result into a list of documents that can be used + this.getResults(results); + + runInAction(() => { this._resultsOpen = true; }); + } + + @action + getResults = async (res: string[]) => { + res.map(async result => { + const doc = await DocServer.GetRefField(result); + if (doc instanceof Doc) { + runInAction(() => this._results.push(doc)); + } + }); + } + + @action + handleClickFilter = (e: Event): void => { + var className = (e.target as any).className; + var id = (e.target as any).id; + if (className !== "filter-button" && className !== "filter-form") { + this._open = false; + } + + } + + @action + handleClickResults = (e: Event): void => { + var className = (e.target as any).className; + var id = (e.target as any).id; + if (id !== "result") { + this._resultsOpen = false; + } + + } + + componentWillMount() { + document.addEventListener('mousedown', this.handleClickFilter, false); + document.addEventListener('mousedown', this.handleClickResults, false); + } + + componentWillUnmount() { + document.removeEventListener('mousedown', this.handleClickFilter, false); + document.removeEventListener('mousedown', this.handleClickResults, false); + } + + @action + toggleFilterDisplay = () => { + this._open = !this._open; + } + + enter = (e: React.KeyboardEvent<HTMLInputElement>) => { + if (e.key === "Enter") { + this.submitSearch(); + } + } + + render() { + return ( + <div id="outer"> + <div className="searchBox" id="outer"> + + <input value={this.searchString} onChange={this.onChange} type="text" placeholder="Search.." className="search" id="input" onKeyPress={this.enter} /> + <button className="filter-button" onClick={this.toggleFilterDisplay}> Filter </button> + <div className="submit-search" id="submit" onClick={this.submitSearch}><FontAwesomeIcon style={{ height: "100%" }} icon="search" size="lg" /></div> + <div className="results" id="results" style={this._resultsOpen ? { display: "flex" } : { display: "none" }}> + {this._results.map(result => <SearchItem doc={result} key={result[Id]} />)} + </div> + </div> + <div className="filter-form" id="filter" style={this._open ? { display: "flex" } : { display: "none" }}> + <div className="filter-form" id="header">Filter Search Results</div> + <div className="filter-form" id="option"> + filter by collection, key, type of node + </div> + + </div> + </div> + + ); + } +}
\ No newline at end of file diff --git a/src/client/views/SearchItem.tsx b/src/client/views/SearchItem.tsx new file mode 100644 index 000000000..d30579907 --- /dev/null +++ b/src/client/views/SearchItem.tsx @@ -0,0 +1,52 @@ +import React = require("react"); +import { Doc } from "../../new_fields/Doc"; +import { DocumentManager } from "../util/DocumentManager"; +import { library } from '@fortawesome/fontawesome-svg-core'; +import { faCaretUp, faFilePdf, faFilm, faImage, faObjectGroup, faStickyNote } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { Cast } from "../../new_fields/Types"; +import { FieldView, FieldViewProps } from './nodes/FieldView'; +import { computed } from "mobx"; +import { IconField } from "../../new_fields/IconField"; + + +export interface SearchProps { + doc: Doc; +} + +library.add(faCaretUp); +library.add(faObjectGroup); +library.add(faStickyNote); +library.add(faFilePdf); +library.add(faFilm); + +export class SearchItem extends React.Component<SearchProps> { + + onClick = () => { + DocumentManager.Instance.jumpToDocument(this.props.doc); + } + + //needs help + // @computed get layout(): string { const field = Cast(this.props.doc[fieldKey], IconField); return field ? field.icon : "<p>Error loading icon data</p>"; } + + + public static DocumentIcon(layout: string) { + let button = layout.indexOf("PDFBox") !== -1 ? faFilePdf : + layout.indexOf("ImageBox") !== -1 ? faImage : + layout.indexOf("Formatted") !== -1 ? faStickyNote : + layout.indexOf("Video") !== -1 ? faFilm : + layout.indexOf("Collection") !== -1 ? faObjectGroup : + faCaretUp; + return <FontAwesomeIcon icon={button} className="documentView-minimizedIcon" />; + } + + render() { + return ( + <div className="search-item" id="result" onClick={this.onClick}> + <div className="search-title" id="result" >title: {this.props.doc.title}</div> + {/* <div className="search-type" id="result" >Type: {this.props.doc.layout}</div> */} + {/* <div className="search-type" >{SearchItem.DocumentIcon(this.layout)}</div> */} + </div> + ); + } +}
\ No newline at end of file diff --git a/src/client/views/nodes/LinkBox.tsx b/src/client/views/nodes/LinkBox.tsx index 08cfa590b..611cb66b6 100644 --- a/src/client/views/nodes/LinkBox.tsx +++ b/src/client/views/nodes/LinkBox.tsx @@ -31,28 +31,7 @@ export class LinkBox extends React.Component<Props> { @undoBatch onViewButtonPressed = async (e: React.PointerEvent): Promise<void> => { e.stopPropagation(); - let docView = DocumentManager.Instance.getDocumentView(this.props.pairedDoc); - if (docView) { - docView.props.focus(docView.props.Document); - } else { - const contextDoc = await Cast(this.props.pairedDoc.annotationOn, Doc); - if (!contextDoc) { - CollectionDockingView.Instance.AddRightSplit(Doc.MakeDelegate(this.props.pairedDoc)); - } else { - const page = NumCast(this.props.pairedDoc.page, undefined); - const curPage = NumCast(contextDoc.curPage, undefined); - if (page !== curPage) { - contextDoc.curPage = page; - } - let contextView = DocumentManager.Instance.getDocumentView(contextDoc); - if (contextView) { - contextDoc.panTransformType = "Ease"; - contextView.props.focus(contextDoc); - } else { - CollectionDockingView.Instance.AddRightSplit(contextDoc); - } - } - } + DocumentManager.Instance.jumpToDocument(this.props.pairedDoc); } onEditButtonPressed = (e: React.PointerEvent): void => { diff --git a/src/debug/Test.tsx b/src/debug/Test.tsx index 04ef00722..57221aa39 100644 --- a/src/debug/Test.tsx +++ b/src/debug/Test.tsx @@ -6,6 +6,7 @@ import { ImageField } from '../new_fields/URLField'; import { Doc } from '../new_fields/Doc'; import { List } from '../new_fields/List'; + const schema1 = createSchema({ hello: "number", test: "string", @@ -41,7 +42,7 @@ class Test extends React.Component { doc.fields = "test"; doc.test = "hello doc"; doc.url = url; - doc.testDoc = doc2; + //doc.testDoc = doc2; const test1: TestDoc = TestDoc(doc); @@ -70,7 +71,9 @@ class Test extends React.Component { } render() { - return <button onClick={this.onClick}>Click me</button>; + return <div><button onClick={this.onClick}>Click me</button> + {/* <input onKeyPress={this.onEnter}></input> */} + </div>; } } diff --git a/src/server/Search.ts b/src/server/Search.ts new file mode 100644 index 000000000..1d8cfdcf0 --- /dev/null +++ b/src/server/Search.ts @@ -0,0 +1,45 @@ +import * as rp from 'request-promise'; +import { Database } from './database'; +import { thisExpression } from 'babel-types'; + +export class Search { + public static Instance = new Search(); + private url = 'http://localhost:8983/solr/'; + + public async updateDocument(document: any) { + try { + return rp.post(this.url + "dash/update", { + headers: { 'content-type': 'application/json' }, + body: JSON.stringify([document]) + }); + } catch { } + } + + public async search(query: string) { + try { + const searchResults = JSON.parse(await rp.get(this.url + "dash/select", { + qs: { + q: query + } + })); + const fields = searchResults.response.docs; + const ids = fields.map((field: any) => field.id); + return ids; + } catch { + return []; + } + } + + public async clear() { + try { + return rp.post(this.url + "dash/update", { + body: { + delete: { + query: "*:*" + } + }, + json: true + }); + } catch { } + } +}
\ No newline at end of file diff --git a/src/server/index.ts b/src/server/index.ts index 6801b3132..93e4cafbf 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -11,6 +11,7 @@ import { ObservableMap } from 'mobx'; import * as passport from 'passport'; import * as path from 'path'; import * as request from 'request'; +import * as rp from 'request-promise'; import * as io from 'socket.io'; import { Socket } from 'socket.io'; import * as webpack from 'webpack'; @@ -21,7 +22,7 @@ import { getForgot, getLogin, getLogout, getReset, getSignup, postForgot, postLo import { DashUserModel } from './authentication/models/user_model'; import { Client } from './Client'; import { Database } from './database'; -import { MessageStore, Transferable, Diff } from "./Message"; +import { MessageStore, Transferable, Types, Diff } from "./Message"; import { RouteStore } from './RouteStore'; const app = express(); const config = require('../../webpack.config'); @@ -31,6 +32,9 @@ const serverPort = 4321; import expressFlash = require('express-flash'); import flash = require('connect-flash'); import c = require("crypto"); +import { Search } from './Search'; +import { debug } from 'util'; +import _ = require('lodash'); const MongoStore = require('connect-mongo')(session); const mongoose = require('mongoose'); @@ -117,8 +121,16 @@ app.get("/pull", (req, res) => res.redirect("/"); })); +// SEARCH + // GETTERS +app.get("/search", async (req, res) => { + let query = req.query.query || "hello"; + let results = await Search.Instance.search(query); + res.send(results); +}); + // anyone attempting to navigate to localhost at this port will // first have to login addSecureRoute( @@ -240,6 +252,7 @@ server.on("connection", function (socket: Socket) { async function deleteFields() { await Database.Instance.deleteAll(); + await Search.Instance.clear(); await Database.Instance.deleteAll('newDocuments'); } @@ -248,6 +261,7 @@ async function deleteAll() { await Database.Instance.deleteAll('newDocuments'); await Database.Instance.deleteAll('sessions'); await Database.Instance.deleteAll('users'); + await Search.Instance.clear(); } function barReceived(guid: String) { @@ -266,6 +280,10 @@ function getFields([ids, callback]: [string[], (result: Transferable[]) => void] function setField(socket: Socket, newValue: Transferable) { Database.Instance.update(newValue.id, newValue, () => socket.broadcast.emit(MessageStore.SetField.Message, newValue)); + if (newValue.type === Types.Text) { + Search.Instance.updateDocument({ id: newValue.id, data: (newValue as any).data }); + console.log("set field"); + } } function GetRefField([id, callback]: [string, (result?: Transferable) => void]) { @@ -276,9 +294,73 @@ function GetRefFields([ids, callback]: [string[], (result?: Transferable[]) => v Database.Instance.getDocuments(ids, callback, "newDocuments"); } + +const suffixMap: { [type: string]: (string | [string, string | ((json: any) => any)]) } = { + "number": "_n", + "string": "_t", + "image": ["_t", "url"], + "video": ["_t", "url"], + "pdf": ["_t", "url"], + "audio": ["_t", "url"], + "web": ["_t", "url"], + "date": ["_d", value => new Date(value.date).toISOString()], + "proxy": ["_i", "fieldId"], + "list": ["_l", list => { + const results = []; + for (const value of list.fields) { + const term = ToSearchTerm(value); + if (term) { + results.push(term.value); + } + } + return results.length ? results : null; + }] +}; + +function ToSearchTerm(val: any): { suffix: string, value: any } | undefined { + const type = val.__type || typeof val; + let suffix = suffixMap[type]; + if (!suffix) { + return; + } + + if (Array.isArray(suffix)) { + const accessor = suffix[1]; + if (typeof accessor === "function") { + val = accessor(val); + } else { + val = val[accessor]; + } + suffix = suffix[0]; + } + + return { suffix, value: val } +} + function UpdateField(socket: Socket, diff: Diff) { Database.Instance.update(diff.id, diff.diff, () => socket.broadcast.emit(MessageStore.UpdateField.Message, diff), false, "newDocuments"); + const docfield = diff.diff.$set; + if (!docfield) { + return; + } + const update: any = { id: diff.id }; + let dynfield = false; + for (let key in docfield) { + if (!key.startsWith("fields.")) continue; + let val = docfield[key]; + let term = ToSearchTerm(val); + if (term !== undefined) { + let { suffix, value } = term; + key = key.substring(7); + Object.values(suffixMap).forEach(suf => update[key + suf] = null); + update[key + suffix] = { set: value }; + dynfield = true; + } + } + if (dynfield) { + Search.Instance.updateDocument(update); + } } function CreateField(newValue: any) { |