diff options
author | Bob Zeleznik <zzzman@gmail.com> | 2019-12-10 18:49:03 -0500 |
---|---|---|
committer | Bob Zeleznik <zzzman@gmail.com> | 2019-12-10 18:49:03 -0500 |
commit | 1dbb45826d4414ed7a1acb5daff730b6e79e97c2 (patch) | |
tree | 9258a346834abccc9ce4881664ccb956f53ae9f7 /src | |
parent | 4ab742c54d600fb62b02268f48e711258558924b (diff) | |
parent | 68ccde3251622fdb51ef3d21282fddd8207da3c1 (diff) |
Merge branch 'master' of https://github.com/browngraphicslab/Dash-Web
Diffstat (limited to 'src')
-rw-r--r-- | src/client/util/ParagraphNodeSpec.ts | 6 | ||||
-rw-r--r-- | src/client/util/RichTextRules.ts | 18 | ||||
-rw-r--r-- | src/client/util/RichTextSchema.tsx | 16 | ||||
-rw-r--r-- | src/client/views/EditableView.tsx | 2 | ||||
-rw-r--r-- | src/client/views/OverlayView.tsx | 4 | ||||
-rw-r--r-- | src/client/views/collections/CollectionTreeView.scss | 1 | ||||
-rw-r--r-- | src/client/views/collections/CollectionTreeView.tsx | 10 | ||||
-rw-r--r-- | src/client/views/nodes/DocumentView.tsx | 22 | ||||
-rw-r--r-- | src/client/views/nodes/FormattedTextBox.tsx | 28 | ||||
-rw-r--r-- | src/client/views/nodes/FormattedTextBoxComment.tsx | 5 | ||||
-rw-r--r-- | src/client/views/presentationview/PresElementBox.tsx | 4 | ||||
-rw-r--r-- | src/client/views/search/SearchItem.tsx | 4 | ||||
-rw-r--r-- | src/server/ActionUtilities.ts | 27 | ||||
-rw-r--r-- | src/server/ApiManagers/SearchManager.ts | 2 | ||||
-rw-r--r-- | src/server/GarbageCollector.ts | 4 | ||||
-rw-r--r-- | src/server/Search.ts | 28 | ||||
-rw-r--r-- | src/server/Websocket/Websocket.ts | 15 | ||||
-rw-r--r-- | src/server/authentication/models/user_model.ts | 4 | ||||
-rw-r--r-- | src/server/remapUrl.ts | 2 | ||||
-rw-r--r-- | src/server/updateSearch.ts | 114 |
20 files changed, 235 insertions, 81 deletions
diff --git a/src/client/util/ParagraphNodeSpec.ts b/src/client/util/ParagraphNodeSpec.ts index 593aec498..fceb8c00f 100644 --- a/src/client/util/ParagraphNodeSpec.ts +++ b/src/client/util/ParagraphNodeSpec.ts @@ -105,6 +105,10 @@ function toDOM(node: Node): DOMOutputSpec { style += `padding-bottom: ${paddingBottom};`; } + if (indent) { + style += `text-indent: ${indent}; padding-left: ${indent < 0 ? -indent : undefined};`; + } + style && (attrs.style = style); if (indent) { @@ -115,7 +119,7 @@ function toDOM(node: Node): DOMOutputSpec { attrs.id = id; } - return ['p', { ...attrs, ...{ style: `text-indent: ${indent}; padding-left: ${indent < 0 ? -indent : undefined};` } }, 0]; + return ['p', attrs, 0]; } export const toParagraphDOM = toDOM; diff --git a/src/client/util/RichTextRules.ts b/src/client/util/RichTextRules.ts index 4e60976d5..5f2d67a3e 100644 --- a/src/client/util/RichTextRules.ts +++ b/src/client/util/RichTextRules.ts @@ -64,10 +64,10 @@ export const inpRules = { new RegExp(/^#([0-9]+)\s$/), (state, match, start, end) => { const size = Number(match[1]); - const ruleProvider = FormattedTextBox.InputBoxOverlay!.props.ruleProvider; - const heading = NumCast(FormattedTextBox.InputBoxOverlay!.props.Document.heading); + const ruleProvider = FormattedTextBox.FocusedBox!.props.ruleProvider; + const heading = NumCast(FormattedTextBox.FocusedBox!.props.Document.heading); if (ruleProvider && heading) { - (Cast(FormattedTextBox.InputBoxOverlay!.props.Document, Doc) as Doc).heading = size; + (Cast(FormattedTextBox.FocusedBox!.props.Document, Doc) as Doc).heading = size; return state.tr.deleteRange(start, end); } return state.tr.deleteRange(start, end).addStoredMark(schema.marks.pFontSize.create({ fontSize: size })); @@ -163,8 +163,8 @@ export const inpRules = { (state, match, start, end) => { const node = (state.doc.resolve(start) as any).nodeAfter; const sm = state.storedMarks || undefined; - const ruleProvider = FormattedTextBox.InputBoxOverlay!.props.ruleProvider; - const heading = NumCast(FormattedTextBox.InputBoxOverlay!.props.Document.heading); + const ruleProvider = FormattedTextBox.FocusedBox!.props.ruleProvider; + const heading = NumCast(FormattedTextBox.FocusedBox!.props.Document.heading); if (ruleProvider && heading) { ruleProvider["ruleAlign_" + heading] = "center"; return node ? state.tr.deleteRange(start, end).setStoredMarks([...node.marks, ...(sm ? sm : [])]) : state.tr; @@ -178,8 +178,8 @@ export const inpRules = { (state, match, start, end) => { const node = (state.doc.resolve(start) as any).nodeAfter; const sm = state.storedMarks || undefined; - const ruleProvider = FormattedTextBox.InputBoxOverlay!.props.ruleProvider; - const heading = NumCast(FormattedTextBox.InputBoxOverlay!.props.Document.heading); + const ruleProvider = FormattedTextBox.FocusedBox!.props.ruleProvider; + const heading = NumCast(FormattedTextBox.FocusedBox!.props.Document.heading); if (ruleProvider && heading) { ruleProvider["ruleAlign_" + heading] = "left"; return node ? state.tr.deleteRange(start, end).setStoredMarks([...node.marks, ...(sm ? sm : [])]) : state.tr; @@ -193,8 +193,8 @@ export const inpRules = { (state, match, start, end) => { const node = (state.doc.resolve(start) as any).nodeAfter; const sm = state.storedMarks || undefined; - const ruleProvider = FormattedTextBox.InputBoxOverlay!.props.ruleProvider; - const heading = NumCast(FormattedTextBox.InputBoxOverlay!.props.Document.heading); + const ruleProvider = FormattedTextBox.FocusedBox!.props.ruleProvider; + const heading = NumCast(FormattedTextBox.FocusedBox!.props.Document.heading); if (ruleProvider && heading) { ruleProvider["ruleAlign_" + heading] = "right"; return node ? state.tr.deleteRange(start, end).setStoredMarks([...node.marks, ...(sm ? sm : [])]) : state.tr; diff --git a/src/client/util/RichTextSchema.tsx b/src/client/util/RichTextSchema.tsx index 4369be1ee..fac8f4027 100644 --- a/src/client/util/RichTextSchema.tsx +++ b/src/client/util/RichTextSchema.tsx @@ -177,18 +177,7 @@ export const nodes: { [index: string]: NodeSpec } = { docid: { default: "" }, }, group: "inline", - draggable: true, - // parseDOM: [{ - // tag: "img[src]", getAttrs(dom: any) { - // return { - // src: dom.getAttribute("src"), - // title: dom.getAttribute("title"), - // alt: dom.getAttribute("alt"), - // width: Math.min(100, Number(dom.getAttribute("width"))), - // }; - // } - // }], - // TODO if we don't define toDom, dragging the image crashes. Why? + draggable: false, toDOM(node) { const attrs = { style: `width: ${node.attrs.width}, height: ${node.attrs.height}` }; return ["div", { ...node.attrs, ...attrs }]; @@ -704,6 +693,7 @@ export class DashDocView { this._dashSpan = document.createElement("div"); this._outer = document.createElement("span"); this._outer.style.position = "relative"; + this._outer.style.textIndent = "0"; this._outer.style.width = node.attrs.width; this._outer.style.height = node.attrs.height; this._outer.style.display = node.attrs.hidden ? "none" : "inline-block"; @@ -772,7 +762,7 @@ export class DashDocView { } }); const self = this; - this._dashSpan.onkeydown = function (e: any) { e.stopPropagation(); }; + this._dashSpan.onkeydown = function (e: any) { }; this._dashSpan.onkeypress = function (e: any) { e.stopPropagation(); }; this._dashSpan.onwheel = function (e: any) { e.preventDefault(); }; this._dashSpan.onkeyup = function (e: any) { e.stopPropagation(); }; diff --git a/src/client/views/EditableView.tsx b/src/client/views/EditableView.tsx index ea9d548a1..d0cecf03d 100644 --- a/src/client/views/EditableView.tsx +++ b/src/client/views/EditableView.tsx @@ -120,7 +120,9 @@ export class EditableView extends React.Component<EditableProps> { @action setIsFocused = (value: boolean) => { + let wasFocused = this._editing; this._editing = value; + return wasFocused !== this._editing; } render() { diff --git a/src/client/views/OverlayView.tsx b/src/client/views/OverlayView.tsx index 9b42d199c..cd330d492 100644 --- a/src/client/views/OverlayView.tsx +++ b/src/client/views/OverlayView.tsx @@ -1,7 +1,7 @@ import * as React from "react"; import { observer } from "mobx-react"; import { observable, action, trace, computed } from "mobx"; -import { Utils, emptyFunction, returnOne, returnTrue, returnEmptyString, returnZero, returnFalse } from "../../Utils"; +import { Utils, emptyFunction, returnOne, returnTrue, returnEmptyString, returnZero, returnFalse, emptyPath } from "../../Utils"; import './OverlayView.scss'; import { CurrentUserUtils } from "../../server/authentication/models/current_user_utils"; @@ -174,7 +174,7 @@ export class OverlayView extends React.Component { return <div className="overlayView-doc" key={d[Id]} onPointerDown={onPointerDown} style={{ transform: `translate(${d.x}px, ${d.y}px)`, display: d.isMinimized ? "none" : "" }}> <DocumentView Document={d} - LibraryPath={[]} + LibraryPath={emptyPath} ChromeHeight={returnZero} // isSelected={returnFalse} // select={emptyFunction} diff --git a/src/client/views/collections/CollectionTreeView.scss b/src/client/views/collections/CollectionTreeView.scss index 8b12395a7..0b9dc2eb2 100644 --- a/src/client/views/collections/CollectionTreeView.scss +++ b/src/client/views/collections/CollectionTreeView.scss @@ -15,6 +15,7 @@ background: $light-color-secondary; font-size: 13px; overflow: auto; + user-select: none; cursor: default; ul { diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx index bf612e4f1..a0ddc8884 100644 --- a/src/client/views/collections/CollectionTreeView.tsx +++ b/src/client/views/collections/CollectionTreeView.tsx @@ -9,7 +9,7 @@ import { List } from '../../../new_fields/List'; import { Document, listSpec } from '../../../new_fields/Schema'; import { ComputedField, ScriptField } from '../../../new_fields/ScriptField'; import { BoolCast, Cast, NumCast, StrCast } from '../../../new_fields/Types'; -import { emptyFunction, Utils, returnFalse } from '../../../Utils'; +import { emptyFunction, Utils, returnFalse, emptyPath } from '../../../Utils'; import { Docs, DocUtils } from '../../documents/Documents'; import { DocumentType } from "../../documents/DocumentTypes"; import { DocumentManager } from '../../util/DocumentManager'; @@ -344,8 +344,8 @@ class TreeView extends React.Component<TreeViewProps> { <ContentFittingDocumentView Document={layoutDoc} DataDocument={this.templateDataDoc} - LibraryPath={[]} - renderDepth={this.props.renderDepth} + LibraryPath={emptyPath} + renderDepth={this.props.renderDepth + 1} showOverlays={this.noOverlays} ruleProvider={this.props.document.isRuleProvider && layoutDoc.type !== DocumentType.TEXT ? this.props.document : this.props.ruleProvider} fitToBox={this.boundsOfCollectionDocument !== undefined} @@ -626,8 +626,8 @@ export class CollectionTreeView extends CollectionSubView(Document) { const addDoc = (doc: Doc, relativeTo?: Doc, before?: boolean) => Doc.AddDocToList(this.props.Document, this.props.fieldKey, doc, relativeTo, before, false, false, false); const moveDoc = (d: Doc, target: Doc, addDoc: (doc: Doc) => boolean) => this.props.moveDocument(d, target, addDoc); return !this.childDocs ? (null) : ( - <div id="body" className="collectionTreeView-dropTarget" - style={{ overflow: "auto", background: StrCast(this.props.Document.backgroundColor, "lightgray"), paddingTop: `${NumCast(this.props.Document.yMargin, 20)}px` }} + <div className="collectionTreeView-dropTarget" id="body" + style={{ background: StrCast(this.props.Document.backgroundColor, "lightgray"), paddingTop: `${NumCast(this.props.Document.yMargin, 20)}px` }} onContextMenu={this.onContextMenu} onWheel={(e: React.WheelEvent) => this._mainEle && this._mainEle.scrollHeight > this._mainEle.clientHeight && e.stopPropagation()} onDrop={this.onTreeDrop} diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index c6b134d6c..9219da80b 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -92,6 +92,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu private _hitTemplateDrag = false; private _mainCont = React.createRef<HTMLDivElement>(); private _dropDisposer?: DragManager.DragDropDisposer; + private _titleRef = React.createRef<EditableView>(); public get displayName() { return "DocumentView(" + this.props.Document.title + ")"; } // this makes mobx trace() statements more descriptive public get ContentDiv() { return this._mainCont.current; } @@ -138,6 +139,23 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu } } + onKeyDown = (e: React.KeyboardEvent) => { + if (e.altKey && e.key === "t" && !(e.nativeEvent as any).StopPropagationForReal) { + (e.nativeEvent as any).StopPropagationForReal = true; // e.stopPropagation() doesn't seem to work... + e.stopPropagation(); + if (!StrCast(this.Document.showTitle)) this.Document.showTitle = "title"; + if (!this._titleRef.current) setTimeout(() => this._titleRef.current?.setIsFocused(true), 0); + else if (!this._titleRef.current.setIsFocused(true)) { // if focus didn't change, focus on interior text... + { + this._titleRef.current?.setIsFocused(false); + let any = (this._mainCont.current?.getElementsByClassName("ProseMirror")?.[0] as any); + any.keeplocation = true; + any?.focus(); + } + } + } + } + onClick = async (e: React.MouseEvent) => { if (!e.nativeEvent.cancelBubble && !this.Document.ignoreClick && CurrentUserUtils.MainDocId !== this.props.Document[Id] && (Math.abs(e.clientX - this._downX) < Utils.DRAG_THRESHOLD && Math.abs(e.clientY - this._downY) < Utils.DRAG_THRESHOLD)) { @@ -622,7 +640,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu position: showTextTitle ? "relative" : "absolute", pointerEvents: SelectionManager.GetIsDragging() ? "none" : "all", }}> - <EditableView + <EditableView ref={this._titleRef} contents={this.Document[showTitle]} display={"block"} height={72} fontSize={12} GetValue={() => StrCast(this.Document[showTitle])} @@ -694,7 +712,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu const highlightColors = ["transparent", "maroon", "maroon", "yellow", "magenta", "cyan", "orange"]; const highlightStyles = ["solid", "dashed", "solid", "solid", "solid", "solid", "solid"]; const highlighting = fullDegree && this.layoutDoc.type !== DocumentType.FONTICON && this.layoutDoc.viewType !== CollectionViewType.Linear; - return <div className={`documentView-node${this.topMost ? "-topmost" : ""}`} ref={this._mainCont} + return <div className={`documentView-node${this.topMost ? "-topmost" : ""}`} ref={this._mainCont} onKeyDown={this.onKeyDown} onDrop={this.onDrop} onContextMenu={this.onContextMenu} onPointerDown={this.onPointerDown} onClick={this.onClick} onPointerEnter={e => Doc.BrushDoc(this.props.Document)} onPointerLeave={e => Doc.UnBrushDoc(this.props.Document)} style={{ diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx index 6d3c1434a..70fa4974d 100644 --- a/src/client/views/nodes/FormattedTextBox.tsx +++ b/src/client/views/nodes/FormattedTextBox.tsx @@ -80,7 +80,6 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps & private _proseRef?: HTMLDivElement; private _editorView: Opt<EditorView>; private _applyingChange: boolean = false; - private _nodeClicked: any; private _searchIndex = 0; private _sidebarMovement = 0; private _lastX = 0; @@ -101,6 +100,8 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps & @observable private _ruleFontFamily = "Arial"; @observable private _fontAlign = ""; @observable private _entered = false; + + public static FocusedBox: FormattedTextBox | undefined; public static SelectOnLoad = ""; public static IsFragment(html: string) { return html.indexOf("data-pm-slice") !== -1; @@ -828,7 +829,7 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps & clipboardTextSerializer: this.clipboardTextSerializer, handlePaste: this.handlePaste, }); - if (startup) { + if (startup && this._editorView) { Doc.GetProto(doc).documentText = undefined; this._editorView.dispatch(this._editorView.state.tr.insertText(startup)); } @@ -869,10 +870,11 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps & this._buttonBarReactionDisposer && this._buttonBarReactionDisposer(); this._editorView && this._editorView.destroy(); } + + static _downEvent: any; onPointerDown = (e: React.PointerEvent): void => { + FormattedTextBox._downEvent = true; FormattedTextBoxComment.textBox = this; - const pos = this._editorView!.posAtCoords({ left: e.clientX, top: e.clientY }); - pos && (this._nodeClicked = this._editorView!.state.doc.nodeAt(pos.pos)); if (this.props.onClick && e.button === 0) { e.preventDefault(); } @@ -885,6 +887,8 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps & } onPointerUp = (e: React.PointerEvent): void => { + if (!FormattedTextBox._downEvent) return; + FormattedTextBox._downEvent = false; if (!(e.nativeEvent as any).formattedHandled) { FormattedTextBoxComment.textBox = this; FormattedTextBoxComment.update(this._editorView!); @@ -896,11 +900,17 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps & } } - static InputBoxOverlay: FormattedTextBox | undefined; @action onFocused = (e: React.FocusEvent): void => { - FormattedTextBox.InputBoxOverlay = this; + FormattedTextBox.FocusedBox = this; this.tryUpdateHeight(); + + // see if we need to preserve the insertion point + let prosediv = this._proseRef?.children?.[0] as any; + let keeplocation = prosediv?.keeplocation; + prosediv && (prosediv.keeplocation = undefined); + let pos = this._editorView?.state.selection.$from.pos || 1; + keeplocation && setTimeout(() => this._editorView?.dispatch(this._editorView?.state.tr.setSelection(TextSelection.create(this._editorView.state.doc, pos)))); } onPointerWheel = (e: React.WheelEvent): void => { // if a text note is not selected and scrollable, this prevents us from being able to scroll and zoom out at the same time @@ -1043,10 +1053,14 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps & this._undoTyping.end(); this._undoTyping = undefined; } - this.doLinkOnDeselect(); + this.doLinkOnDeselect(); 6 } onKeyPress = (e: React.KeyboardEvent) => { + if (e.altKey) { + e.preventDefault(); + return; + } if (!this._editorView!.state.selection.empty && e.key === "%") { (this._editorView!.state as any).EnteringStyle = true; e.preventDefault(); diff --git a/src/client/views/nodes/FormattedTextBoxComment.tsx b/src/client/views/nodes/FormattedTextBoxComment.tsx index 9f1dd4aec..409229c1a 100644 --- a/src/client/views/nodes/FormattedTextBoxComment.tsx +++ b/src/client/views/nodes/FormattedTextBoxComment.tsx @@ -4,7 +4,7 @@ import { EditorView } from "prosemirror-view"; import * as ReactDOM from 'react-dom'; import { Doc } from "../../../new_fields/Doc"; import { Cast, FieldValue, NumCast } from "../../../new_fields/Types"; -import { emptyFunction, returnEmptyString, returnFalse, Utils } from "../../../Utils"; +import { emptyFunction, returnEmptyString, returnFalse, Utils, emptyPath } from "../../../Utils"; import { DocServer } from "../../DocServer"; import { DocumentManager } from "../../util/DocumentManager"; import { schema } from "../../util/RichTextSchema"; @@ -94,6 +94,7 @@ export class FormattedTextBoxComment { FormattedTextBoxComment.start, FormattedTextBoxComment.end, FormattedTextBoxComment.mark, FormattedTextBoxComment.opened, keep); e.stopPropagation(); + e.preventDefault(); }; root && root.appendChild(FormattedTextBoxComment.tooltip); } @@ -178,7 +179,7 @@ export class FormattedTextBoxComment { if (target) { ReactDOM.render(<ContentFittingDocumentView Document={target} - LibraryPath={[]} + LibraryPath={emptyPath} fitToBox={true} moveDocument={returnFalse} getTransform={Transform.Identity} diff --git a/src/client/views/presentationview/PresElementBox.tsx b/src/client/views/presentationview/PresElementBox.tsx index cfb9319bb..37c837414 100644 --- a/src/client/views/presentationview/PresElementBox.tsx +++ b/src/client/views/presentationview/PresElementBox.tsx @@ -9,7 +9,7 @@ import { documentSchema } from '../../../new_fields/documentSchemas'; import { Id } from "../../../new_fields/FieldSymbols"; import { createSchema, makeInterface } from '../../../new_fields/Schema'; import { Cast, NumCast, StrCast } from "../../../new_fields/Types"; -import { emptyFunction, returnFalse } from "../../../Utils"; +import { emptyFunction, returnFalse, emptyPath } from "../../../Utils"; import { DocumentType } from "../../documents/DocumentTypes"; import { Transform } from "../../util/Transform"; import { CollectionViewType } from '../collections/CollectionView'; @@ -171,7 +171,7 @@ export class PresElementBox extends DocComponent<FieldViewProps, PresDocument>(P }}> <ContentFittingDocumentView Document={this.targetDoc} - LibraryPath={[]} + LibraryPath={emptyPath} fitToBox={StrCast(this.targetDoc.type).indexOf(DocumentType.COL) !== -1} addDocument={returnFalse} removeDocument={returnFalse} diff --git a/src/client/views/search/SearchItem.tsx b/src/client/views/search/SearchItem.tsx index 0be583358..1007102f6 100644 --- a/src/client/views/search/SearchItem.tsx +++ b/src/client/views/search/SearchItem.tsx @@ -7,7 +7,7 @@ import { observer } from "mobx-react"; import { Doc } from "../../../new_fields/Doc"; import { Id } from "../../../new_fields/FieldSymbols"; import { Cast, NumCast, StrCast } from "../../../new_fields/Types"; -import { emptyFunction, returnEmptyString, returnFalse, returnOne, Utils } from "../../../Utils"; +import { emptyFunction, returnEmptyString, returnFalse, returnOne, Utils, emptyPath } from "../../../Utils"; import { DocumentType } from "../../documents/DocumentTypes"; import { DocumentManager } from "../../util/DocumentManager"; import { DragManager, SetupDrag } from "../../util/DragManager"; @@ -160,7 +160,7 @@ export class SearchItem extends React.Component<SearchItemProps> { onPointerLeave={action(() => this._displayDim = 50)} > <DocumentView Document={this.props.doc} - LibraryPath={[]} + LibraryPath={emptyPath} fitToBox={StrCast(this.props.doc.type).indexOf(DocumentType.COL) !== -1} addDocument={returnFalse} removeDocument={returnFalse} diff --git a/src/server/ActionUtilities.ts b/src/server/ActionUtilities.ts index 94008e171..4667254d8 100644 --- a/src/server/ActionUtilities.ts +++ b/src/server/ActionUtilities.ts @@ -3,7 +3,7 @@ import { ExecOptions } from 'shelljs'; import { exec } from 'child_process'; import * as path from 'path'; import * as rimraf from "rimraf"; -import { yellow } from 'colors'; +import { yellow, Color } from 'colors'; export const command_line = (command: string, fromDirectory?: string) => { return new Promise<string>((resolve, reject) => { @@ -29,18 +29,29 @@ export const write_text_file = (relativePath: string, contents: any) => { }); }; -export interface LogData { +export interface LogData<T> { startMessage: string; endMessage: string; - action: () => void | Promise<void>; + action: () => T | Promise<T>; + color?: Color; } let current = Math.ceil(Math.random() * 20); -export async function log_execution({ startMessage, endMessage, action }: LogData) { - const color = `\x1b[${31 + current++ % 6}m%s\x1b[0m`; - console.log(color, `${startMessage}...`); - await action(); - console.log(color, endMessage); +export async function log_execution<T>({ startMessage, endMessage, action, color }: LogData<T>): Promise<T> { + let result: T; + const formattedStart = `${startMessage}...`; + const formattedEnd = `${endMessage}.`; + if (color) { + console.log(color(formattedStart)); + result = await action(); + console.log(color(formattedEnd)); + } else { + const color = `\x1b[${31 + current++ % 6}m%s\x1b[0m`; + console.log(color, formattedStart); + result = await action(); + console.log(color, formattedEnd); + } + return result; } export function logPort(listener: string, port: number) { diff --git a/src/server/ApiManagers/SearchManager.ts b/src/server/ApiManagers/SearchManager.ts index ccd0896bd..ccfd570b8 100644 --- a/src/server/ApiManagers/SearchManager.ts +++ b/src/server/ApiManagers/SearchManager.ts @@ -57,7 +57,7 @@ export class SearchManager extends ApiManager { res.send([]); return; } - const results = await Search.Instance.search(solrQuery); + const results = await Search.search(solrQuery); res.send(results); } }); diff --git a/src/server/GarbageCollector.ts b/src/server/GarbageCollector.ts index 09b52eadf..5729c3ee5 100644 --- a/src/server/GarbageCollector.ts +++ b/src/server/GarbageCollector.ts @@ -100,7 +100,7 @@ async function GarbageCollect(full: boolean = true) { if (!full) { await Database.Instance.updateMany({ _id: { $nin: notToDelete } }, { $set: { "deleted": true } }); await Database.Instance.updateMany({ _id: { $in: notToDelete } }, { $unset: { "deleted": true } }); - console.log(await Search.Instance.updateDocuments( + console.log(await Search.updateDocuments( notToDelete.map<any>(id => ({ id, deleted: { set: null } })) @@ -122,7 +122,7 @@ async function GarbageCollect(full: boolean = true) { // const result = await Database.Instance.delete({ _id: { $in: toDelete } }, "newDocuments"); console.log(`${deleted} documents deleted`); - await Search.Instance.deleteDocuments(toDelete); + await Search.deleteDocuments(toDelete); console.log("Cleared search documents"); const folder = "./src/server/public/files/"; diff --git a/src/server/Search.ts b/src/server/Search.ts index 723dc101b..2b59c14b1 100644 --- a/src/server/Search.ts +++ b/src/server/Search.ts @@ -1,14 +1,12 @@ 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/'; +const pathTo = (relative: string) => `http://localhost:8983/solr/dash/${relative}`; - public async updateDocument(document: any) { +export namespace Search { + + export async function updateDocument(document: any) { try { - const res = await rp.post(this.url + "dash/update", { + const res = await rp.post(pathTo("update"), { headers: { 'content-type': 'application/json' }, body: JSON.stringify([document]) }); @@ -18,9 +16,9 @@ export class Search { } } - public async updateDocuments(documents: any[]) { + export async function updateDocuments(documents: any[]) { try { - const res = await rp.post(this.url + "dash/update", { + const res = await rp.post(pathTo("update"), { headers: { 'content-type': 'application/json' }, body: JSON.stringify(documents) }); @@ -30,9 +28,9 @@ export class Search { } } - public async search(query: any) { + export async function search(query: any) { try { - const searchResults = JSON.parse(await rp.get(this.url + "dash/select", { + const searchResults = JSON.parse(await rp.get(pathTo("select"), { qs: query })); const { docs, numFound } = searchResults.response; @@ -43,9 +41,9 @@ export class Search { } } - public async clear() { + export async function clear() { try { - return await rp.post(this.url + "dash/update", { + return rp.post(pathTo("update"), { body: { delete: { query: "*:*" @@ -56,7 +54,7 @@ export class Search { } catch { } } - public deleteDocuments(docs: string[]) { + export async function deleteDocuments(docs: string[]) { const promises: rp.RequestPromise[] = []; const nToDelete = 1000; let index = 0; @@ -64,7 +62,7 @@ export class Search { const count = Math.min(docs.length - index, nToDelete); const deleteIds = docs.slice(index, index + count); index += count; - promises.push(rp.post(this.url + "dash/update", { + promises.push(rp.post(pathTo("update"), { body: { delete: { query: deleteIds.map(id => `id:"${id}"`).join(" ") diff --git a/src/server/Websocket/Websocket.ts b/src/server/Websocket/Websocket.ts index 5c0bb508b..76e02122b 100644 --- a/src/server/Websocket/Websocket.ts +++ b/src/server/Websocket/Websocket.ts @@ -10,7 +10,6 @@ import { GoogleCredentialsLoader } from "../credentials/CredentialsLoader"; import { logPort } from "../ActionUtilities"; import { timeMap } from "../ApiManagers/UserManager"; import { green } from "colors"; -import { SolrManager } from "../ApiManagers/SearchManager"; export namespace WebSocket { @@ -80,7 +79,7 @@ export namespace WebSocket { export async function deleteFields() { await Database.Instance.deleteAll(); - await Search.Instance.clear(); + await Search.clear(); await Database.Instance.deleteAll('newDocuments'); } @@ -89,7 +88,7 @@ export namespace WebSocket { await Database.Instance.deleteAll('newDocuments'); await Database.Instance.deleteAll('sessions'); await Database.Instance.deleteAll('users'); - await Search.Instance.clear(); + await Search.clear(); } function barReceived(socket: SocketIO.Socket, userEmail: string) { @@ -111,7 +110,7 @@ export namespace WebSocket { 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 }); + Search.updateDocument({ id: newValue.id, data: (newValue as any).data }); console.log("set field"); console.log("checking in"); } @@ -197,7 +196,7 @@ export namespace WebSocket { } } if (dynfield) { - Search.Instance.updateDocument(update); + Search.updateDocument(update); } } @@ -206,16 +205,14 @@ export namespace WebSocket { socket.broadcast.emit(MessageStore.DeleteField.Message, id); }); - Search.Instance.deleteDocuments([id]); + Search.deleteDocuments([id]); } function DeleteFields(socket: Socket, ids: string[]) { Database.Instance.delete({ _id: { $in: ids } }, "newDocuments").then(() => { socket.broadcast.emit(MessageStore.DeleteFields.Message, ids); }); - - Search.Instance.deleteDocuments(ids); - + Search.deleteDocuments(ids); } function CreateField(newValue: any) { diff --git a/src/server/authentication/models/user_model.ts b/src/server/authentication/models/user_model.ts index cc670a03a..78e39dbc1 100644 --- a/src/server/authentication/models/user_model.ts +++ b/src/server/authentication/models/user_model.ts @@ -73,7 +73,11 @@ userSchema.pre("save", function save(next) { }); const comparePassword: comparePasswordFunction = function (this: DashUserModel, candidatePassword, cb) { + // Choose one of the following bodies for authentication logic. + // secure bcrypt.compare(candidatePassword, this.password, cb); + // bypass password + // cb(undefined, true); }; userSchema.methods.comparePassword = comparePassword; diff --git a/src/server/remapUrl.ts b/src/server/remapUrl.ts index 5218a239a..45d2fdd33 100644 --- a/src/server/remapUrl.ts +++ b/src/server/remapUrl.ts @@ -54,7 +54,7 @@ async function update() { })); console.log("Done"); // await Promise.all(updates.map(update => { - // return limit(() => Search.Instance.updateDocument(update)); + // return limit(() => Search.updateDocument(update)); // })); cursor.close(); } diff --git a/src/server/updateSearch.ts b/src/server/updateSearch.ts new file mode 100644 index 000000000..5ae6885c5 --- /dev/null +++ b/src/server/updateSearch.ts @@ -0,0 +1,114 @@ +import { Database } from "./database"; +import { Search } from "./Search"; +import { log_execution } from "./ActionUtilities"; +import { cyan, green, yellow, red } from "colors"; + +const suffixMap: { [type: string]: (string | [string, string | ((json: any) => any)]) } = { + "number": "_n", + "string": "_t", + "boolean": "_b", + "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 { + if (val === null || val === undefined) { + return; + } + 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 }; +} + +async function update() { + console.log(green("Beginning update...")); + await log_execution<void>({ + startMessage: "Clearing existing Solr information...", + endMessage: "Solr information successfully cleared", + action: Search.clear, + color: cyan + }); + const cursor = await log_execution({ + startMessage: "Connecting to and querying for all documents from database...", + endMessage: "Connection successful and query complete", + action: () => Database.Instance.query({}), + color: yellow + }); + const updates: any[] = []; + let numDocs = 0; + function updateDoc(doc: any) { + numDocs++; + if ((numDocs % 50) === 0) { + console.log(`Batch of 50 complete, total of ${numDocs}`); + } + if (doc.__type !== "Doc") { + return; + } + const fields = doc.fields; + if (!fields) { + return; + } + const update: any = { id: doc._id }; + let dynfield = false; + for (const key in fields) { + const value = fields[key]; + const term = ToSearchTerm(value); + if (term !== undefined) { + const { suffix, value } = term; + update[key + suffix] = value; + dynfield = true; + } + } + if (dynfield) { + updates.push(update); + } + } + await cursor.forEach(updateDoc); + const result = await log_execution({ + startMessage: `Dispatching updates for ${updates.length} documents`, + endMessage: "Dispatched updates complete", + action: () => Search.updateDocuments(updates), + color: cyan + }); + try { + const { status } = JSON.parse(result).responseHeader; + console.log(status ? red(`Failed with status code (${status})`) : green("Success!")); + } catch { + console.log(red("Error:")); + console.log(result); + console.log("\n"); + } + await cursor.close(); + process.exit(0); +} + +update();
\ No newline at end of file |