diff options
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/util/TooltipTextMenu.tsx | 32 | ||||
-rw-r--r-- | src/client/views/Main.scss | 3 | ||||
-rw-r--r-- | src/client/views/Main.tsx | 6 | ||||
-rw-r--r-- | src/client/views/PresentationView.tsx | 9 | ||||
-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/Templates.tsx | 15 | ||||
-rw-r--r-- | src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx | 4 | ||||
-rw-r--r-- | src/client/views/nodes/DocumentView.tsx | 2 | ||||
-rw-r--r-- | src/client/views/nodes/FormattedTextBox.tsx | 5 | ||||
-rw-r--r-- | src/client/views/nodes/LinkBox.tsx | 23 | ||||
-rw-r--r-- | src/debug/Test.tsx | 6 | ||||
-rw-r--r-- | src/new_fields/util.ts | 1 | ||||
-rw-r--r-- | src/server/Search.ts | 45 | ||||
-rw-r--r-- | src/server/index.ts | 85 |
18 files changed, 484 insertions, 68 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/util/TooltipTextMenu.tsx b/src/client/util/TooltipTextMenu.tsx index 358fdc81b..b860f6758 100644 --- a/src/client/util/TooltipTextMenu.tsx +++ b/src/client/util/TooltipTextMenu.tsx @@ -49,15 +49,14 @@ export class TooltipTextMenu { private fontSizeIndicator: HTMLSpanElement = document.createElement("span"); private link: HTMLAnchorElement; + private linkEditor?: HTMLDivElement; + private linkText?: HTMLDivElement; + private linkDrag?: HTMLImageElement; //dropdown doms private fontSizeDom?: Node; private fontStyleDom?: Node; private listTypeBtnDom?: Node; - private linkEditor?: HTMLDivElement; - private linkText?: HTMLDivElement; - private linkDrag?: HTMLImageElement; - constructor(view: EditorView, editorProps: FieldViewProps) { this.view = view; this.state = view.state; @@ -186,7 +185,7 @@ export class TooltipTextMenu { this.linkText.style.width = "150px"; this.linkText.style.overflow = "hidden"; this.linkText.style.color = "white"; - this.linkText.onpointerdown = (e: PointerEvent) => { e.stopPropagation(); } + this.linkText.onpointerdown = (e: PointerEvent) => { e.stopPropagation(); }; let linkBtn = document.createElement("div"); linkBtn.textContent = ">>"; linkBtn.style.width = "20px"; @@ -203,8 +202,9 @@ export class TooltipTextMenu { let docid = href.replace(DocServer.prepend("/doc/"), ""); DocServer.GetRefField(docid).then(action((f: Opt<Field>) => { if (f instanceof Doc) { - if (DocumentManager.Instance.getDocumentView(f)) + if (DocumentManager.Instance.getDocumentView(f)) { DocumentManager.Instance.getDocumentView(f)!.props.focus(f); + } else CollectionDockingView.Instance.AddRightSplit(f); } })); @@ -212,7 +212,7 @@ export class TooltipTextMenu { e.stopPropagation(); e.preventDefault(); } - } + }; this.linkDrag = document.createElement("img"); this.linkDrag.src = "https://seogurusnyc.com/wp-content/uploads/2016/12/link-1.png"; this.linkDrag.style.width = "20px"; @@ -232,7 +232,7 @@ export class TooltipTextMenu { }), }, hideSource: false - }) + }); }; this.linkEditor.appendChild(this.linkDrag); this.linkEditor.appendChild(this.linkText); @@ -250,7 +250,7 @@ export class TooltipTextMenu { e.stopPropagation(); e.preventDefault(); } - } + }; this.tooltip.appendChild(this.linkEditor); } @@ -351,7 +351,7 @@ export class TooltipTextMenu { icon: icons.link, css: "color:white;", class: "menuicon", - enable(state) { return !state.selection.empty }, + enable(state) { return !state.selection.empty; }, run: (state, dispatch, view) => { // to remove link if (this.markActive(state, markType)) { @@ -396,10 +396,10 @@ export class TooltipTextMenu { } markActive = function (state: EditorState<any>, type: MarkType<Schema<string, string>>) { - let { from, $from, to, empty } = state.selection - if (empty) return type.isInSet(state.storedMarks || $from.marks()) - else return state.doc.rangeHasMark(from, to, type) - } + let { from, $from, to, empty } = state.selection; + if (empty) return type.isInSet(state.storedMarks || $from.marks()); + else return state.doc.rangeHasMark(from, to, type); + }; // Helper function to create menu icons icon(text: string, name: string) { @@ -480,8 +480,8 @@ export class TooltipTextMenu { let attributes = this.getMarksInSelection(this.view.state, [schema.marks.link])[0].attrs; this.link.href = attributes.href; this.link.textContent = attributes.title; - this.link.style.visibility = "visible" - } else this.link.style.visibility = "hidden" + this.link.style.visibility = "visible"; + } else this.link.style.visibility = "hidden"; // Otherwise, reposition it and update its content this.tooltip.style.display = ""; 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/PresentationView.tsx b/src/client/views/PresentationView.tsx index 94867b813..2a456324c 100644 --- a/src/client/views/PresentationView.tsx +++ b/src/client/views/PresentationView.tsx @@ -1,7 +1,7 @@ import { observer } from "mobx-react"; -import React = require("react") +import React = require("react"); import { observable, action, runInAction, reaction } from "mobx"; -import "./PresentationView.scss" +import "./PresentationView.scss"; import "./Main.tsx"; import { DocumentManager } from "../util/DocumentManager"; import { Utils } from "../../Utils"; @@ -141,7 +141,7 @@ export class PresentationView extends React.Component<PresViewProps> { if (activeW && activeW instanceof Doc) { PromiseValue(Cast(activeW.presentationView, Doc)). then(pv => runInAction(() => - self.Document = pv ? pv : (activeW.presentationView = new Doc()))) + self.Document = pv ? pv : (activeW.presentationView = new Doc()))); } }, { fireImmediately: true }); @@ -165,8 +165,9 @@ export class PresentationView extends React.Component<PresViewProps> { } render() { - if (!this.Document) + if (!this.Document) { return (null); + } let titleStr = this.Document.Title; let width = NumCast(this.Document.width); 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/Templates.tsx b/src/client/views/Templates.tsx index 02f9aa510..bbedc95f1 100644 --- a/src/client/views/Templates.tsx +++ b/src/client/views/Templates.tsx @@ -38,18 +38,9 @@ export class Template { export namespace Templates { // export const BasicLayout = new Template("Basic layout", "{layout}"); - export const OuterCaption = new Template("Outer caption", TemplatePosition.OutterBottom, - `<div id="screenSpace" style="margin-top: 100%; font-size:14px; background:yellow; width:100%; position:absolute"><FormattedTextBox {...props} fieldKey={"caption"} /></div>` + export const Caption = new Template("Caption", TemplatePosition.OutterBottom, + `<div id="screenSpace" style="top: 100%; font-size:14px; background:yellow; width:100%; position:absolute"><FormattedTextBox {...props} fieldKey={"caption"} /></div>` ); - - export const InnerCaption = new Template("Inner caption", TemplatePosition.InnerBottom, - `<div><div style="margin:auto; height:calc(100% - 50px); width:100%;">{layout}</div><div style="height:50px; width:100%; position:absolute"><FormattedTextBox {...props} fieldKey={"caption"}/></div></div>` - ); - - export const SideCaption = new Template("Side caption", TemplatePosition.OutterRight, - `<div><div style="margin:auto; height:100%; width:100%;">{layout}</div><div style="height:100%; width:300px; position:absolute; top: 0; right: -300px;"><FormattedTextBox {...props} fieldKey={"caption"}/></div> </div>` - ); - export const TitleOverlay = new Template("TitleOverlay", TemplatePosition.InnerTop, `<div><div style="height:100%; width:100%;position:absolute;">{layout}</div> <div style="height:25px; width:100%; position:absolute; top: 0; background-color: rgba(0, 0, 0, .4); color: white; "> @@ -72,7 +63,7 @@ export namespace Templates { </div>` ); - export const TemplateList: Template[] = [Title, TitleOverlay, OuterCaption, InnerCaption, SideCaption, Bullet]; + export const TemplateList: Template[] = [Title, TitleOverlay, Caption, Bullet]; export function sortTemplates(a: Template, b: Template) { if (a.Position < b.Position) { return -1; } diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx index 1d4584cfe..62b1456cf 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx @@ -67,8 +67,8 @@ export class CollectionFreeFormLinksView extends React.Component<CollectionViewP if (srcBrushDocs === undefined) srcTarg.brushingDocs = srcBrushDocs = new List<Doc>(); else brushAction(srcBrushDocs); } - }) - }) + }); + }); }); } componentWillUnmount() { diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 25a75904b..5aa74c703 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -176,7 +176,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu if (CollectionFreeFormView.RIGHT_BTN_DRAG && (e.button === 2 || (e.button === 0 && e.altKey)) && !this.isSelected()) { return; } - this._hitIsBullet = (e.target && (e.target as any).id === "isBullet"); + this._hitIsBullet = (e.target && (e.target as any).id === "isBullet") || Cast(this.props.Document.subBulletDocs, listSpec(Doc), []).filter(d => d).map(d => d as Doc).length > 0; if (e.shiftKey && e.buttons === 1) { if (this.props.isTopMost) { this.startDragging(e.pageX, e.pageY, e.altKey || e.ctrlKey ? "alias" : undefined, this._hitIsBullet); diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx index fc58e513b..df3eb159b 100644 --- a/src/client/views/nodes/FormattedTextBox.tsx +++ b/src/client/views/nodes/FormattedTextBox.tsx @@ -184,7 +184,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe state: field && field.Data ? EditorState.fromJSON(config, JSON.parse(field.Data)) : EditorState.create(config), dispatchTransaction: this.dispatchTransaction, nodeViews: { - image(node, view, getPos) { return new ImageResizeView(node, view, getPos) } + image(node, view, getPos) { return new ImageResizeView(node, view, getPos); } } }); let text = StrCast(this.props.Document.documentText); @@ -232,8 +232,9 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe let docid = href.replace(DocServer.prepend("/doc/"), ""); DocServer.GetRefField(docid).then(action((f: Opt<Field>) => { if (f instanceof Doc) { - if (DocumentManager.Instance.getDocumentView(f)) + if (DocumentManager.Instance.getDocumentView(f)) { DocumentManager.Instance.getDocumentView(f)!.props.focus(f); + } else CollectionDockingView.Instance.AddRightSplit(f); } })); 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..0dca4b4b1 100644 --- a/src/debug/Test.tsx +++ b/src/debug/Test.tsx @@ -41,7 +41,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 +70,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/new_fields/util.ts b/src/new_fields/util.ts index bbd8157f6..a5f5e368b 100644 --- a/src/new_fields/util.ts +++ b/src/new_fields/util.ts @@ -56,6 +56,7 @@ export function getter(target: any, prop: string | symbol | number, receiver: an return getField(target, prop); } +//TODO The callback parameter is never being passed in currently, so we should be able to get rid of it. export function getField(target: any, prop: string | number, ignoreProto: boolean = false, callback?: (field: Field | undefined) => void): any { const field = target.__fields[prop]; if (field instanceof ProxyField) { diff --git a/src/server/Search.ts b/src/server/Search.ts new file mode 100644 index 000000000..c3cb3c3e6 --- /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 await 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 await 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..f816af48a 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -7,10 +7,10 @@ import * as expressValidator from 'express-validator'; import * as formidable from 'formidable'; import * as fs from 'fs'; import * as mobileDetect from 'mobile-detect'; -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 +21,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 +31,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 +120,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 +251,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 +260,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 +279,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 +293,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) { |