aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authortschicke-brown <tyler_schicke@brown.edu>2019-05-09 20:31:52 -0400
committerGitHub <noreply@github.com>2019-05-09 20:31:52 -0400
commit30aaf7c496eb4cbebac1e4cf9e0695cd286c63f2 (patch)
treefe887b7862ed82b7b59d657146ea8e627174ece7 /src
parent6691c55623fff5194b5fd1a830096e3925281301 (diff)
parentb18aec5d4b07bbc859a5b1b2d22ca3bd92ca53cd (diff)
Merge pull request #130 from browngraphicslab/new_search
New search
Diffstat (limited to 'src')
-rw-r--r--src/client/documents/Documents.ts1
-rw-r--r--src/client/util/DocumentManager.ts31
-rw-r--r--src/client/views/Main.scss3
-rw-r--r--src/client/views/Main.tsx6
-rw-r--r--src/client/views/SearchBox.scss99
-rw-r--r--src/client/views/SearchBox.tsx133
-rw-r--r--src/client/views/SearchItem.tsx52
-rw-r--r--src/client/views/nodes/LinkBox.tsx23
-rw-r--r--src/debug/Test.tsx7
-rw-r--r--src/server/Search.ts45
-rw-r--r--src/server/index.ts84
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) {