diff options
Diffstat (limited to 'src')
35 files changed, 432 insertions, 263 deletions
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index 177810444..af2b95659 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -235,7 +235,7 @@ export namespace Docs { let title = prototypeId.toUpperCase().replace(upper, `_${upper}`); // synthesize the default options, the type and title from computed values and // whatever options pertain to this specific prototype - let options = { title: title, type: type, ...defaultOptions, ...(template.options || {}) }; + let options = { title: title, type: type, baseProto: true, ...defaultOptions, ...(template.options || {}) }; let primary = layout.view.LayoutString(); let collectionView = layout.collectionView; if (collectionView) { @@ -483,18 +483,20 @@ export namespace Docs { export namespace DocUtils { - export function MakeLink(source: Doc, target: Doc, targetContext?: Doc, title: string = "", description: string = "", tags: string = "Default") { + export function MakeLink(source: Doc, target: Doc, targetContext?: Doc, title: string = "", description: string = "", tags: string = "Default", sourceContext?: Doc) { if (LinkManager.Instance.doesLinkExist(source, target)) return; let sv = DocumentManager.Instance.getDocumentView(source); if (sv && sv.props.ContainingCollectionView && sv.props.ContainingCollectionView.props.Document === target) return; if (target === CurrentUserUtils.UserDocument) return; + let linkDoc; UndoManager.RunInBatch(() => { - let linkDoc = Docs.Create.TextDocument({ width: 100, height: 30, borderRounding: "100%" }); + linkDoc = Docs.Create.TextDocument({ width: 100, height: 30, borderRounding: "100%" }); linkDoc.type = DocumentType.LINK; let linkDocProto = Doc.GetProto(linkDoc); - linkDocProto.context = targetContext; + linkDocProto.targetContext = targetContext; + linkDocProto.sourceContext = sourceContext; linkDocProto.title = title === "" ? source.title + " to " + target.title : title; linkDocProto.linkDescription = description; linkDocProto.linkTags = tags; @@ -509,8 +511,8 @@ export namespace DocUtils { LinkManager.Instance.addLink(linkDoc); - return linkDoc; }, "make link"); + return linkDoc; } } diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts index bb1345044..262194a40 100644 --- a/src/client/util/DocumentManager.ts +++ b/src/client/util/DocumentManager.ts @@ -3,7 +3,7 @@ import { DocumentView } from '../views/nodes/DocumentView'; import { Doc, DocListCast, Opt } from '../../new_fields/Doc'; import { FieldValue, Cast, NumCast, BoolCast, StrCast } from '../../new_fields/Types'; import { listSpec } from '../../new_fields/Schema'; -import { undoBatch } from './UndoManager'; +import { undoBatch, UndoManager } from './UndoManager'; import { CollectionDockingView } from '../views/collections/CollectionDockingView'; import { CollectionView } from '../views/collections/CollectionView'; import { CollectionPDFView } from '../views/collections/CollectionPDFView'; @@ -142,7 +142,9 @@ export class DocumentManager { if (!forceDockFunc && (docView = DocumentManager.Instance.getDocumentView(doc))) { docView.props.Document.libraryBrush = true; if (linkPage !== undefined) docView.props.Document.curPage = linkPage; - docView.props.focus(docView.props.Document, willZoom); + UndoManager.RunInBatch(() => { + docView!.props.focus(docView!.props.Document, willZoom); + }, "focus"); } else { if (!contextDoc) { if (docContext) { diff --git a/src/client/util/RichTextSchema.tsx b/src/client/util/RichTextSchema.tsx index e0ff3074b..269de0f42 100644 --- a/src/client/util/RichTextSchema.tsx +++ b/src/client/util/RichTextSchema.tsx @@ -215,12 +215,13 @@ export const marks: { [index: string]: MarkSpec } = { link: { attrs: { href: {}, + location: { default: null }, title: { default: null } }, inclusive: false, parseDOM: [{ tag: "a[href]", getAttrs(dom: any) { - return { href: dom.getAttribute("href"), title: dom.getAttribute("title") }; + return { href: dom.getAttribute("href"), location: dom.getAttribute("location"), title: dom.getAttribute("title") }; } }], toDOM(node: any) { return ["a", node.attrs, 0]; } diff --git a/src/client/util/SearchUtil.ts b/src/client/util/SearchUtil.ts index 806746496..abf1a7c32 100644 --- a/src/client/util/SearchUtil.ts +++ b/src/client/util/SearchUtil.ts @@ -4,30 +4,41 @@ import { Doc } from '../../new_fields/Doc'; import { Id } from '../../new_fields/FieldSymbols'; export namespace SearchUtil { + export type HighlightingResult = { [id: string]: { [key: string]: string[] } }; + export interface IdSearchResult { ids: string[]; numFound: number; + highlighting: HighlightingResult | undefined; } export interface DocSearchResult { docs: Doc[]; numFound: number; + highlighting: HighlightingResult | undefined; } - export function Search(query: string, filterQuery: string | undefined, returnDocs: true, start?: number, count?: number): Promise<DocSearchResult>; - export function Search(query: string, filterQuery: string | undefined, returnDocs: false, start?: number, count?: number): Promise<IdSearchResult>; - export async function Search(query: string, filterQuery: string | undefined, returnDocs: boolean, start?: number, rows?: number) { + export interface SearchParams { + hl?: boolean; + "hl.fl"?: string; + start?: number; + rows?: number; + fq?: string; + } + export function Search(query: string, returnDocs: true, options?: SearchParams): Promise<DocSearchResult>; + export function Search(query: string, returnDocs: false, options?: SearchParams): Promise<IdSearchResult>; + export async function Search(query: string, returnDocs: boolean, options: SearchParams = {}) { query = query || "*"; //If we just have a filter query, search for * as the query const result: IdSearchResult = JSON.parse(await rp.get(DocServer.prepend("/search"), { - qs: { query, filterQuery, start, rows }, + qs: { ...options, q: query }, })); if (!returnDocs) { return result; } - const { ids, numFound } = result; + const { ids, numFound, highlighting } = result; const docMap = await DocServer.GetRefFields(ids); const docs = ids.map((id: string) => docMap[id]).filter((doc: any) => doc instanceof Doc); - return { docs, numFound }; + return { docs, numFound, highlighting }; } export async function GetAliasesOfDocument(doc: Doc): Promise<Doc[]>; @@ -36,31 +47,31 @@ export namespace SearchUtil { const proto = Doc.GetProto(doc); const protoId = proto[Id]; if (returnDocs) { - return (await Search("", `proto_i:"${protoId}"`, returnDocs)).docs; + return (await Search("", returnDocs, { fq: `proto_i:"${protoId}"` })).docs; } else { - return (await Search("", `proto_i:"${protoId}"`, returnDocs)).ids; + return (await Search("", returnDocs, { fq: `proto_i:"${protoId}"` })).ids; } // return Search(`{!join from=id to=proto_i}id:${protoId}`, true); } export async function GetViewsOfDocument(doc: Doc): Promise<Doc[]> { - const results = await Search("", `proto_i:"${doc[Id]}"`, true); + const results = await Search("", true, { fq: `proto_i:"${doc[Id]}"` }); return results.docs; } export async function GetContextsOfDocument(doc: Doc): Promise<{ contexts: Doc[], aliasContexts: Doc[] }> { - const docContexts = (await Search("", `data_l:"${doc[Id]}"`, true)).docs; + const docContexts = (await Search("", true, { fq: `data_l:"${doc[Id]}"` })).docs; const aliases = await GetAliasesOfDocument(doc, false); - const aliasContexts = (await Promise.all(aliases.map(doc => Search("", `data_l:"${doc}"`, true)))); + const aliasContexts = (await Promise.all(aliases.map(doc => Search("", true, { fq: `data_l:"${doc}"` })))); const contexts = { contexts: docContexts, aliasContexts: [] as Doc[] }; aliasContexts.forEach(result => contexts.aliasContexts.push(...result.docs)); return contexts; } export async function GetContextIdsOfDocument(doc: Doc): Promise<{ contexts: string[], aliasContexts: string[] }> { - const docContexts = (await Search("", `data_l:"${doc[Id]}"`, false)).ids; + const docContexts = (await Search("", false, { fq: `data_l:"${doc[Id]}"` })).ids; const aliases = await GetAliasesOfDocument(doc, false); - const aliasContexts = (await Promise.all(aliases.map(doc => Search("", `data_l:"${doc}"`, false)))); + const aliasContexts = (await Promise.all(aliases.map(doc => Search("", false, { fq: `data_l:"${doc}"` })))); const contexts = { contexts: docContexts, aliasContexts: [] as string[] }; aliasContexts.forEach(result => contexts.aliasContexts.push(...result.ids)); return contexts; diff --git a/src/client/util/TooltipTextMenu.tsx b/src/client/util/TooltipTextMenu.tsx index 3e804651c..309d19016 100644 --- a/src/client/util/TooltipTextMenu.tsx +++ b/src/client/util/TooltipTextMenu.tsx @@ -19,6 +19,7 @@ import { CollectionDockingView } from "../views/collections/CollectionDockingVie import { DocumentManager } from "./DocumentManager"; import { Id } from "../../new_fields/FieldSymbols"; import { FormattedTextBoxProps } from "../views/nodes/FormattedTextBox"; +import { SelectionManager } from "./SelectionManager"; //appears above a selection of text in a RichTextBox to give user options such as Bold, Italics, etc. export class TooltipTextMenu { @@ -239,13 +240,21 @@ export class TooltipTextMenu { this.linkDrag.onpointerdown = (e: PointerEvent) => { let dragData = new DragManager.LinkDragData(this.editorProps.Document); dragData.dontClearTextBox = true; + // hack to get source context -sy + let docView = DocumentManager.Instance.getDocumentView(this.editorProps.Document); e.stopPropagation(); + let ctrlKey = e.ctrlKey; DragManager.StartLinkDrag(this.linkDrag!, dragData, e.clientX, e.clientY, { handlers: { dragComplete: action(() => { - let m = dragData.droppedDocuments; - m.length && this.makeLink(DocServer.prepend("/doc/" + m[0][Id])); + // let m = dragData.droppedDocuments; + let linkDoc = dragData.linkDocument; + let proto = Doc.GetProto(linkDoc); + if (docView && docView.props.ContainingCollectionView) { + proto.sourceContext = docView.props.ContainingCollectionView.props.Document; + } + linkDoc instanceof Doc && this.makeLink(DocServer.prepend("/doc/" + linkDoc[Id]), ctrlKey ? "onRight" : "inTab"); }), }, hideSource: false @@ -263,7 +272,7 @@ export class TooltipTextMenu { this.linkText.onkeydown = (e: KeyboardEvent) => { if (e.key === "Enter") { - this.makeLink(this.linkText!.textContent!); + // this.makeLink(this.linkText!.textContent!); e.stopPropagation(); e.preventDefault(); } @@ -312,9 +321,9 @@ export class TooltipTextMenu { } } - makeLink = (target: string) => { + makeLink = (target: string, location: string) => { let node = this.view.state.selection.$from.nodeAfter; - let link = this.view.state.schema.mark(this.view.state.schema.marks.link, { href: target }); + let link = this.view.state.schema.mark(this.view.state.schema.marks.link, { href: target, location: location }); this.view.dispatch(this.view.state.tr.removeMark(this.view.state.selection.from, this.view.state.selection.to, this.view.state.schema.marks.link)); this.view.dispatch(this.view.state.tr.addMark(this.view.state.selection.from, this.view.state.selection.to, link)); node = this.view.state.selection.$from.nodeAfter; diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index 398974cb6..fb5104915 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -534,7 +534,6 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> doc.width = actualdW; if (fixedAspect) doc.height = nheight / nwidth * doc.width; else doc.height = actualdH; - Doc.SetInPlace(element.props.Document, "nativeHeight", (doc.height || 0) / doc.width * (doc.nativeWidth || 0), true); } else { if (!fixedAspect) { @@ -543,7 +542,6 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> doc.height = actualdH; if (fixedAspect) doc.width = nwidth / nheight * doc.height; else doc.width = actualdW; - Doc.SetInPlace(element.props.Document, "nativeWidth", (doc.width || 0) / doc.height * (doc.nativeHeight || 0), true); } } else { dW && (doc.width = actualdW); diff --git a/src/client/views/InkingCanvas.tsx b/src/client/views/InkingCanvas.tsx index 37a6bbab7..3e0d7b476 100644 --- a/src/client/views/InkingCanvas.tsx +++ b/src/client/views/InkingCanvas.tsx @@ -152,7 +152,7 @@ export class InkingCanvas extends React.Component<InkCanvasProps> { get drawnPaths() { let curPage = NumCast(this.props.Document.curPage, -1); let paths = Array.from(this.inkData).reduce((paths, [id, strokeData]) => { - if (strokeData.page === -1 || Math.round(strokeData.page) === Math.round(curPage)) { + if (strokeData.page === -1 || (Math.abs(Math.round(strokeData.page) - Math.round(curPage)) < 3)) { paths.push(<InkingStroke key={id} id={id} line={strokeData.pathData} count={strokeData.pathData.length} diff --git a/src/client/views/InkingControl.tsx b/src/client/views/InkingControl.tsx index 1910e409b..c7f7bdb66 100644 --- a/src/client/views/InkingControl.tsx +++ b/src/client/views/InkingControl.tsx @@ -1,5 +1,5 @@ import { observable, action, computed, runInAction } from "mobx"; -import { ColorState } from 'react-color'; +import { ColorResult } from 'react-color'; import React = require("react"); import { observer } from "mobx-react"; import "./InkingControl.scss"; @@ -41,7 +41,7 @@ export class InkingControl extends React.Component { } @undoBatch - switchColor = action((color: ColorState): void => { + switchColor = action((color: ColorResult): void => { this._selectedColor = color.hex + (color.rgb.a !== undefined ? this.decimalToHexString(Math.round(color.rgb.a * 255)) : "ff"); if (InkingControl.Instance.selectedTool === InkTool.None) { if (MainOverlayTextBox.Instance.SetColor(color.hex)) return; diff --git a/src/client/views/SearchBox.tsx b/src/client/views/SearchBox.tsx index 6995e3c7d..1b9be841f 100644 --- a/src/client/views/SearchBox.tsx +++ b/src/client/views/SearchBox.tsx @@ -1,27 +1,18 @@ -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, faObjectGroup } 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 { faObjectGroup, faSearch } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { action, observable, runInAction } from 'mobx'; +import { observer } from 'mobx-react'; +import * as React from 'react'; import * as rp from 'request-promise'; -import { SearchItem } from './search/SearchItem'; -import { isString } from 'util'; -import { constant } from 'async'; -import { DocServer } from '../DocServer'; import { Doc } from '../../new_fields/Doc'; import { Id } from '../../new_fields/FieldSymbols'; -import { DocumentManager } from '../util/DocumentManager'; -import { SetupDrag } from '../util/DragManager'; -import { Docs } from '../documents/Documents'; -import { RouteStore } from '../../server/RouteStore'; import { NumCast } from '../../new_fields/Types'; +import { DocServer } from '../DocServer'; +import { Docs } from '../documents/Documents'; +import { SetupDrag } from '../util/DragManager'; +import { SearchItem } from './search/SearchItem'; +import "./SearchBox.scss"; library.add(faSearch); library.add(faObjectGroup); @@ -72,22 +63,6 @@ export class SearchBox extends React.Component { } return docs; } - public static async convertDataUri(imageUri: string, returnedFilename: string) { - try { - let posting = DocServer.prepend(RouteStore.dataUriToImage); - const returnedUri = await rp.post(posting, { - body: { - uri: imageUri, - name: returnedFilename - }, - json: true, - }); - return returnedUri; - - } catch (e) { - console.log(e); - } - } @action handleClickFilter = (e: Event): void => { @@ -188,7 +163,7 @@ export class SearchBox extends React.Component { </div> {this._resultsOpen ? ( <div className="searchBox-results"> - {this._results.map(result => <SearchItem doc={result} key={result[Id]} />)} + {this._results.map(result => <SearchItem doc={result} key={result[Id]} highlighting={[]} />)} </div> ) : null} </div> diff --git a/src/client/views/collections/CollectionPDFView.tsx b/src/client/views/collections/CollectionPDFView.tsx index 8ab360984..9074854d6 100644 --- a/src/client/views/collections/CollectionPDFView.tsx +++ b/src/client/views/collections/CollectionPDFView.tsx @@ -1,6 +1,6 @@ import { action, IReactionDisposer, observable, reaction } from "mobx"; import { observer } from "mobx-react"; -import { WidthSym } from "../../../new_fields/Doc"; +import { WidthSym, HeightSym } from "../../../new_fields/Doc"; import { Id } from "../../../new_fields/FieldSymbols"; import { NumCast } from "../../../new_fields/Types"; import { emptyFunction } from "../../../Utils"; @@ -29,15 +29,7 @@ export class CollectionPDFView extends React.Component<FieldViewProps> { this._reactionDisposer = reaction( () => NumCast(this.props.Document.scrollY), () => { - // let transform = this.props.ScreenToLocalTransform(); - // if (this._buttonTray.current) { - // console.log(this._buttonTray.current.offsetHeight); - // console.log(NumCast(this.props.Document.scrollY)); - let scale = this.nativeWidth() / this.props.Document[WidthSym](); this.props.Document.panY = NumCast(this.props.Document.scrollY); - // console.log(scale); - // } - // console.log(this.props.Document[HeightSym]()); }, { fireImmediately: true } ); diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx index 71f1908f0..5d5189336 100644 --- a/src/client/views/collections/CollectionSubView.tsx +++ b/src/client/views/collections/CollectionSubView.tsx @@ -9,7 +9,7 @@ import { BoolCast, Cast } from "../../../new_fields/Types"; import { CurrentUserUtils } from "../../../server/authentication/models/current_user_utils"; import { RouteStore } from "../../../server/RouteStore"; import { DocServer } from "../../DocServer"; -import { Docs, DocumentOptions } from "../../documents/Documents"; +import { Docs, DocumentOptions, DocumentType } from "../../documents/Documents"; import { DragManager } from "../../util/DragManager"; import { undoBatch, UndoManager } from "../../util/UndoManager"; import { DocComponent } from "../DocComponent"; @@ -74,7 +74,7 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) { return; } // The following conditional detects a recurring bug we've seen on the server - if (proto[Id] === "collectionProto") { + if (proto[Id] === Docs.Prototypes.get(DocumentType.COL)[Id]) { alert("COLLECTION PROTO CURSOR ISSUE DETECTED! Check console for more info..."); console.log(doc); console.log(proto); @@ -179,8 +179,8 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) { } } if (text && text.indexOf("www.youtube.com/watch") !== -1) { - const url = text.replace("youtube.com/watch?v=", "youtube.com/embed/");// + "?enablejsapi=1"; - this.props.addDocument(Docs.Create.VideoDocument(url, { ...options, width: 400, height: 315 })); + const url = text.replace("youtube.com/watch?v=", "youtube.com/embed/"); + this.props.addDocument(Docs.Create.VideoDocument(url, { ...options, title: url, width: 400, height: 315, nativeWidth: 600, nativeHeight: 472.5 })); return; } diff --git a/src/client/views/collections/CollectionVideoView.tsx b/src/client/views/collections/CollectionVideoView.tsx index faf507496..d7d5773ba 100644 --- a/src/client/views/collections/CollectionVideoView.tsx +++ b/src/client/views/collections/CollectionVideoView.tsx @@ -1,20 +1,12 @@ -import { action, observable, trace } from "mobx"; -import * as htmlToImage from "html-to-image"; +import { action } from "mobx"; import { observer } from "mobx-react"; -import { ContextMenu } from "../ContextMenu"; -import { CollectionViewType, CollectionBaseView, CollectionRenderProps } from "./CollectionBaseView"; -import React = require("react"); -import "./CollectionVideoView.scss"; -import { CollectionFreeFormView } from "./collectionFreeForm/CollectionFreeFormView"; +import { NumCast } from "../../../new_fields/Types"; import { FieldView, FieldViewProps } from "../nodes/FieldView"; -import { emptyFunction, Utils } from "../../../Utils"; -import { Id } from "../../../new_fields/FieldSymbols"; import { VideoBox } from "../nodes/VideoBox"; -import { NumCast, Cast, StrCast } from "../../../new_fields/Types"; -import { VideoField } from "../../../new_fields/URLField"; -import { SearchBox } from "../search/SearchBox"; -import { DocServer } from "../../DocServer"; -import { Docs, DocUtils } from "../../documents/Documents"; +import { CollectionBaseView, CollectionRenderProps, CollectionViewType } from "./CollectionBaseView"; +import { CollectionFreeFormView } from "./collectionFreeForm/CollectionFreeFormView"; +import "./CollectionVideoView.scss"; +import React = require("react"); @observer @@ -68,49 +60,6 @@ export class CollectionVideoView extends React.Component<FieldViewProps> { this.props.Document.curPage = 0; } } - - onContextMenu = (e: React.MouseEvent): void => { - if (!e.isPropagationStopped() && this.props.Document[Id] !== "mainDoc") { // need to test this because GoldenLayout causes a parallel hierarchy in the React DOM for its children and the main document view7 - } - - let field = Cast(this.props.Document[this.props.fieldKey], VideoField); - if (field) { - let url = field.url.href; - ContextMenu.Instance.addItem({ - description: "Copy path", event: () => { Utils.CopyText(url); }, icon: "expand-arrows-alt" - }); - } - let width = NumCast(this.props.Document.width); - let height = NumCast(this.props.Document.height); - ContextMenu.Instance.addItem({ - description: "Take Snapshot", event: async () => { - var canvas = document.createElement('canvas'); - canvas.width = 640; - canvas.height = 640 * NumCast(this.props.Document.nativeHeight) / NumCast(this.props.Document.nativeWidth); - var ctx = canvas.getContext('2d');//draw image to canvas. scale to target dimensions - this._videoBox!.player && ctx && ctx.drawImage(this._videoBox!.player!, 0, 0, canvas.width, canvas.height); - - //convert to desired file format - var dataUrl = canvas.toDataURL('image/png'); // can also use 'image/png' - // if you want to preview the captured image, - - let filename = encodeURIComponent("snapshot" + this.props.Document.title + "_" + this.props.Document.curPage).replace(/\./g, ""); - SearchBox.convertDataUri(dataUrl, filename).then(returnedFilename => { - if (returnedFilename) { - let url = DocServer.prepend(returnedFilename); - let imageSummary = Docs.Create.ImageDocument(url, { - x: NumCast(this.props.Document.x) + width, y: NumCast(this.props.Document.y), - width: 150, height: height / width * 150, title: "--snapshot" + NumCast(this.props.Document.curPage) + " image-" - }); - this.props.addDocument && this.props.addDocument(imageSummary, false); - DocUtils.MakeLink(imageSummary, this.props.Document); - } - }); - }, - icon: "expand-arrows-alt" - }); - } - setVideoBox = (videoBox: VideoBox) => { this._videoBox = videoBox; }; private subView = (_type: CollectionViewType, renderProps: CollectionRenderProps) => { @@ -123,7 +72,7 @@ export class CollectionVideoView extends React.Component<FieldViewProps> { render() { return ( - <CollectionBaseView {...this.props} className="collectionVideoView-cont" onContextMenu={this.onContextMenu}> + <CollectionBaseView {...this.props} className="collectionVideoView-cont" > {this.subView} </CollectionBaseView>); } diff --git a/src/client/views/collections/ParentDocumentSelector.tsx b/src/client/views/collections/ParentDocumentSelector.tsx index a97aa4f36..c3e55d825 100644 --- a/src/client/views/collections/ParentDocumentSelector.tsx +++ b/src/client/views/collections/ParentDocumentSelector.tsx @@ -23,9 +23,9 @@ export class SelectorContextMenu extends React.Component<SelectorProps> { async fetchDocuments() { let aliases = (await SearchUtil.GetAliasesOfDocument(this.props.Document)).filter(doc => doc !== this.props.Document); - const { docs } = await SearchUtil.Search("", `data_l:"${this.props.Document[Id]}"`, true); + const { docs } = await SearchUtil.Search("", true, { fq: `data_l:"${this.props.Document[Id]}"` }); const map: Map<Doc, Doc> = new Map; - const allDocs = await Promise.all(aliases.map(doc => SearchUtil.Search("", `data_l:"${doc[Id]}"`, true).then(result => result.docs))); + const allDocs = await Promise.all(aliases.map(doc => SearchUtil.Search("", true, { fq: `data_l:"${doc[Id]}"` }).then(result => result.docs))); allDocs.forEach((docs, index) => docs.forEach(doc => map.set(doc, aliases[index]))); docs.forEach(doc => map.delete(doc)); runInAction(() => { diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index 19e280444..b91aaedee 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -267,7 +267,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { // this.props.Document.panX = panX; // this.props.Document.panY = panY; if (this.props.Document.scrollY) { - this.props.Document.scrollY = panY; + this.props.Document.scrollY = panY - scale * this.props.Document[HeightSym](); } } @@ -429,7 +429,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { let docviews = docs.reduce((prev, doc) => { if (!(doc instanceof Doc)) return prev; var page = NumCast(doc.page, -1); - if (Math.round(page) === Math.round(curPage) || page === -1) { + if ((Math.abs(Math.round(page) - Math.round(curPage)) < 3) || page === -1) { let minim = BoolCast(doc.isMinimized, false); if (minim === undefined || !minim) { const pos = script ? this.getCalculatedPositions(script, { doc, index: prev.length, collection: this.Document, docs, state }) : {}; diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index f7a33466e..245dd319d 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -344,7 +344,8 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu let first = linkedDocs.filter(d => Doc.AreProtosEqual(d.anchor1 as Doc, this.props.Document)); let linkedFwdDocs = first.length ? [first[0].anchor2 as Doc, first[0].anchor1 as Doc] : [expandedDocs[0], expandedDocs[0]]; - let linkedFwdContextDocs = [first.length ? await (first[0].context) as Doc : undefined, undefined]; + // @TODO: shouldn't always follow target context + let linkedFwdContextDocs = [first.length ? await (first[0].targetContext) as Doc : undefined, undefined]; let linkedFwdPage = [first.length ? NumCast(first[0].linkedToPage, undefined) : undefined, undefined]; @@ -454,8 +455,9 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu else { // const docs = await SearchUtil.Search(`data_l:"${destDoc[Id]}"`, true); // const views = docs.map(d => DocumentManager.Instance.getDocumentView(d)).filter(d => d).map(d => d as DocumentView); - DocUtils.MakeLink(sourceDoc, destDoc, this.props.ContainingCollectionView ? this.props.ContainingCollectionView.props.Document : undefined); + let linkDoc = DocUtils.MakeLink(sourceDoc, destDoc, this.props.ContainingCollectionView ? this.props.ContainingCollectionView.props.Document : undefined); de.data.droppedDocuments.push(destDoc); + de.data.linkDocument = linkDoc; } } } @@ -554,6 +556,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu }, icon: "search" }); cm.addItem({ description: "Center View", event: () => this.props.focus(this.props.Document, false), icon: "crosshairs" }); + cm.addItem({ description: "Zoom to Document", event: () => this.props.focus(this.props.Document, true), icon: "search" }); cm.addItem({ description: "Copy URL", event: () => Utils.CopyText(DocServer.prepend("/doc/" + this.props.Document[Id])), icon: "link" }); cm.addItem({ description: "Copy ID", event: () => Utils.CopyText(this.props.Document[Id]), icon: "fingerprint" }); cm.addItem({ description: "Delete", event: this.deleteClicked, icon: "trash" }); diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx index 82c2cef26..066cc40e2 100644 --- a/src/client/views/nodes/FormattedTextBox.tsx +++ b/src/client/views/nodes/FormattedTextBox.tsx @@ -303,6 +303,10 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe let ctrlKey = e.ctrlKey; if (e.button === 0 && ((!this.props.isSelected() && !e.ctrlKey) || (this.props.isSelected() && e.ctrlKey)) && !e.metaKey && e.target) { let href = (e.target as any).href; + let location: string; + if ((e.target as any).attributes.location) { + location = (e.target as any).attributes.location.value; + } for (let parent = (e.target as any).parentNode; !href && parent; parent = parent.parentNode) { href = parent.childNodes[0].href ? parent.childNodes[0].href : parent.href; } @@ -310,8 +314,22 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe if (href.indexOf(DocServer.prepend("/doc/")) === 0) { this._linkClicked = href.replace(DocServer.prepend("/doc/"), "").split("?")[0]; if (this._linkClicked) { - DocServer.GetRefField(this._linkClicked).then(f => { - (f instanceof Doc) && DocumentManager.Instance.jumpToDocument(f, ctrlKey, false, document => this.props.addDocTab(document, undefined, "inTab")); + DocServer.GetRefField(this._linkClicked).then(async linkDoc => { + if (linkDoc instanceof Doc) { + let proto = Doc.GetProto(linkDoc); + let targetContext = await Cast(proto.targetContext, Doc); + let jumpToDoc = await Cast(linkDoc.anchor2, Doc); + if (jumpToDoc) { + if (DocumentManager.Instance.getDocumentView(jumpToDoc)) { + + DocumentManager.Instance.jumpToDocument(jumpToDoc, e.altKey, undefined, undefined, NumCast((jumpToDoc === linkDoc.anchor2 ? linkDoc.anchor2Page : linkDoc.anchor1Page))); + return; + } + } + if (targetContext) { + DocumentManager.Instance.jumpToDocument(targetContext, ctrlKey, false, document => this.props.addDocTab(document, undefined, location ? location : "inTab")); + } + } }); e.stopPropagation(); e.preventDefault(); diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index 1df955f1f..a3e098fd8 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -1,6 +1,6 @@ import { library } from '@fortawesome/fontawesome-svg-core'; -import { faImage } from '@fortawesome/free-solid-svg-icons'; -import { action, observable, computed } from 'mobx'; +import { faImage, faFileAudio } from '@fortawesome/free-solid-svg-icons'; +import { action, observable, computed, runInAction } from 'mobx'; import { observer } from "mobx-react"; import Lightbox from 'react-image-lightbox'; import 'react-image-lightbox/style.css'; // This only needs to be imported once in your app @@ -8,7 +8,7 @@ import { Doc, HeightSym, WidthSym, DocListCast } from '../../../new_fields/Doc'; import { List } from '../../../new_fields/List'; import { createSchema, listSpec, makeInterface } from '../../../new_fields/Schema'; import { Cast, FieldValue, NumCast, StrCast, BoolCast } from '../../../new_fields/Types'; -import { ImageField } from '../../../new_fields/URLField'; +import { ImageField, AudioField } from '../../../new_fields/URLField'; import { Utils } from '../../../Utils'; import { DragManager } from '../../util/DragManager'; import { undoBatch } from '../../util/UndoManager'; @@ -22,11 +22,16 @@ import "./ImageBox.scss"; import React = require("react"); import { RouteStore } from '../../../server/RouteStore'; import { Docs } from '../../documents/Documents'; +import { DocServer } from '../../DocServer'; +import { Font } from '@react-pdf/renderer'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; var requestImageSize = require('../../util/request-image-size'); var path = require('path'); +const { Howl, Howler } = require('howler'); library.add(faImage); +library.add(faFileAudio); export const pageSchema = createSchema({ @@ -157,8 +162,15 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD }).then(function (stream) { gumStream = stream; recorder = new MediaRecorder(stream); - recorder.ondataavailable = function (e: any) { - var url = URL.createObjectURL(e.data); + recorder.ondataavailable = async function (e: any) { + const formData = new FormData(); + formData.append("file", e.data); + const res = await fetch(DocServer.prepend(RouteStore.upload), { + method: 'POST', + body: formData + }); + const files = await res.json(); + const url = DocServer.prepend(files[0]); // upload to server with known URL let audioDoc = Docs.Create.AudioDocument(url, { title: "audio test", x: NumCast(self.props.Document.x), y: NumCast(self.props.Document.y), width: 200, height: 32 }); audioDoc.embed = true; @@ -169,12 +181,13 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD audioAnnos.push(audioDoc); } }; + runInAction(() => self._audioState = 2); recorder.start(); setTimeout(() => { recorder.stop(); - + runInAction(() => self._audioState = 0); gumStream.getAudioTracks()[0].stop(); - }, 1000); + }, 5000); }); } @@ -264,6 +277,46 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD }); } + @observable _audioState = 0; + + @action + onPointerEnter = () => { + let self = this; + let audioAnnos = DocListCast(this.extensionDoc.audioAnnotations); + if (audioAnnos.length && this._audioState === 0) { + audioAnnos.map(anno => anno.data instanceof AudioField && new Howl({ + src: [anno.data.url.href], + autoplay: true, + loop: false, + volume: 0.5, + onend: function () { + runInAction(() => self._audioState = 0); + } + })); + this._audioState = 1; + } + // else { + // if (this._audioState === 0) { + // this._audioState = 1; + // new Howl({ + // src: ["https://www.kozco.com/tech/piano2-CoolEdit.mp3"], + // autoplay: true, + // loop: false, + // volume: 0.5, + // onend: function () { + // runInAction(() => self._audioState = 0); + // } + // }); + // } + // } + } + + @action + audioDown = () => { + this.recordAudioAnnotation(); + } + + render() { // let transform = this.props.ScreenToLocalTransform().inverse(); let pw = typeof this.props.PanelWidth === "function" ? this.props.PanelWidth() : typeof this.props.PanelWidth === "number" ? (this.props.PanelWidth as any) as number : 50; @@ -274,7 +327,7 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD let id = (this.props as any).id; // bcz: used to set id = "isExpander" in templates.tsx let nativeWidth = FieldValue(this.Document.nativeWidth, pw); let nativeHeight = FieldValue(this.Document.nativeHeight, 0); - let paths: string[] = ["http://www.cs.brown.edu/~bcz/noImage.png"]; + let paths: string[] = [window.origin + RouteStore.corsProxy + "/" + "http://www.cs.brown.edu/~bcz/noImage.png"]; // this._curSuffix = ""; // if (w > 20) { Doc.UpdateDocumentExtensionForField(this.dataDoc, this.props.fieldKey); @@ -297,6 +350,7 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD return ( <div id={id} className={`imageBox-cont${interactive}`} style={{ background: "transparent" }} + onPointerEnter={this.onPointerEnter} onPointerDown={this.onPointerDown} onDrop={this.onDrop} ref={this.createDropTarget} onContextMenu={this.specificContextMenu}> <img id={id} @@ -308,6 +362,17 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD ref={this._imgRef} onError={this.onError} /> {paths.length > 1 ? this.dots(paths) : (null)} + <div onPointerDown={this.audioDown} style={{ + display: DocListCast(this.extensionDoc.audioAnnotations).length ? "inline" : "inline", + width: nativeWidth * 0.1, + height: nativeWidth * 0.1, + position: "absolute", + top: 0, + left: 0, + }}> + <FontAwesomeIcon + style={{ width: "100%", height: "100%", color: [DocListCast(this.extensionDoc.audioAnnotations).length ? "blue" : "gray", "green", "red"][this._audioState] }} icon={faFileAudio} size="sm" /> + </div> {/* {this.lightbox(paths)} */} </div>); } diff --git a/src/client/views/nodes/LinkMenuItem.tsx b/src/client/views/nodes/LinkMenuItem.tsx index 6a18a4e7b..a0c37a719 100644 --- a/src/client/views/nodes/LinkMenuItem.tsx +++ b/src/client/views/nodes/LinkMenuItem.tsx @@ -32,16 +32,28 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> { @undoBatch onFollowLink = async (e: React.PointerEvent): Promise<void> => { e.stopPropagation(); + e.persist(); let jumpToDoc = this.props.destinationDoc; let pdfDoc = FieldValue(Cast(this.props.destinationDoc, Doc)); if (pdfDoc) { jumpToDoc = pdfDoc; } + let proto = Doc.GetProto(this.props.linkDoc); + let targetContext = await Cast(proto.targetContext, Doc); + let sourceContext = await Cast(proto.sourceContext, Doc); + let self = this; if (DocumentManager.Instance.getDocumentView(jumpToDoc)) { - let self = this; DocumentManager.Instance.jumpToDocument(jumpToDoc, e.altKey, undefined, undefined, NumCast((this.props.destinationDoc === self.props.linkDoc.anchor2 ? self.props.linkDoc.anchor2Page : self.props.linkDoc.anchor1Page))); + } + else if (!((this.props.destinationDoc === self.props.linkDoc.anchor2 && targetContext) || (this.props.destinationDoc === self.props.linkDoc.anchor1 && sourceContext))) { + DocumentManager.Instance.jumpToDocument(jumpToDoc, e.altKey, false, document => CollectionDockingView.Instance.AddRightSplit(document, undefined)); } else { - CollectionDockingView.Instance.AddRightSplit(jumpToDoc, undefined); + if (this.props.destinationDoc === self.props.linkDoc.anchor2 && targetContext) { + DocumentManager.Instance.jumpToDocument(jumpToDoc, e.altKey, false, document => CollectionDockingView.Instance.AddRightSplit(targetContext!, undefined)); + } + else if (this.props.destinationDoc === self.props.linkDoc.anchor1 && sourceContext) { + DocumentManager.Instance.jumpToDocument(jumpToDoc, e.altKey, false, document => CollectionDockingView.Instance.AddRightSplit(sourceContext!, undefined)); + } } } diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx index 972e6875d..8b8e500c4 100644 --- a/src/client/views/nodes/VideoBox.tsx +++ b/src/client/views/nodes/VideoBox.tsx @@ -1,19 +1,24 @@ import React = require("react"); -import { action, IReactionDisposer, observable, reaction, trace, computed, runInAction, untracked } from "mobx"; +import { action, computed, IReactionDisposer, observable, reaction, runInAction, untracked, trace } from "mobx"; import { observer } from "mobx-react"; +import * as rp from 'request-promise'; +import { InkTool } from "../../../new_fields/InkField"; import { makeInterface } from "../../../new_fields/Schema"; import { Cast, FieldValue, NumCast } from "../../../new_fields/Types"; import { VideoField } from "../../../new_fields/URLField"; +import { RouteStore } from "../../../server/RouteStore"; +import { Utils } from "../../../Utils"; +import { DocServer } from "../../DocServer"; +import { Docs, DocUtils } from "../../documents/Documents"; import { ContextMenu } from "../ContextMenu"; import { ContextMenuProps } from "../ContextMenuItem"; import { DocComponent } from "../DocComponent"; +import { DocumentDecorations } from "../DocumentDecorations"; import { InkingControl } from "../InkingControl"; import { positionSchema } from "./DocumentView"; import { FieldView, FieldViewProps } from './FieldView'; import { pageSchema } from "./ImageBox"; import "./VideoBox.scss"; -import { InkTool } from "../../../new_fields/InkField"; -import { DocumentDecorations } from "../DocumentDecorations"; type VideoDocument = makeInterface<[typeof positionSchema, typeof pageSchema]>; const VideoDocument = makeInterface(positionSchema, pageSchema); @@ -52,21 +57,17 @@ export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoD @action public Play = (update: boolean = true) => { this.Playing = true; update && this.player && this.player.play(); - console.log("PLAYING = " + update); update && this._youtubePlayer && this._youtubePlayer.playVideo(); !this._playTimer && (this._playTimer = setInterval(this.updateTimecode, 500)); this.updateTimecode(); } @action public Seek(time: number) { - console.log("Seeking " + time); - //if (this._youtubePlayer && this._youtubePlayer.getPlayerState() === 5) return; this._youtubePlayer && this._youtubePlayer.seekTo(Math.round(time), true); } @action public Pause = (update: boolean = true) => { this.Playing = false; - console.log("PAUSING = " + update); update && this.player && this.player.pause(); update && this._youtubePlayer && this._youtubePlayer.pauseVideo(); this._playTimer && clearInterval(this._playTimer); @@ -93,7 +94,7 @@ export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoD let youtubeaspect = 400 / 315; var nativeWidth = FieldValue(this.Document.nativeWidth, 0); var nativeHeight = FieldValue(this.Document.nativeHeight, 0); - if (!nativeWidth || !nativeHeight || Math.abs(nativeWidth / nativeHeight - youtubeaspect) > 0.05) { + if (!nativeWidth || !nativeHeight) { if (!this.Document.nativeWidth) this.Document.nativeWidth = 600; this.Document.nativeHeight = this.Document.nativeWidth / youtubeaspect; this.Document.height = FieldValue(this.Document.width, 0) / youtubeaspect; @@ -119,13 +120,62 @@ export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoD } } + public static async convertDataUri(imageUri: string, returnedFilename: string) { + try { + let posting = DocServer.prepend(RouteStore.dataUriToImage); + const returnedUri = await rp.post(posting, { + body: { + uri: imageUri, + name: returnedFilename + }, + json: true, + }); + return returnedUri; + + } catch (e) { + console.log(e); + } + } specificContextMenu = (e: React.MouseEvent): void => { let field = Cast(this.Document[this.props.fieldKey], VideoField); if (field) { + let url = field.url.href; let subitems: ContextMenuProps[] = []; + subitems.push({ description: "Copy path", event: () => { Utils.CopyText(url); }, icon: "expand-arrows-alt" }); subitems.push({ description: "Toggle Show Controls", event: action(() => VideoBox._showControls = !VideoBox._showControls), icon: "expand-arrows-alt" }); - subitems.push({ description: "GOTO 3", event: action(() => this.Seek(3)), icon: "expand-arrows-alt" }); - subitems.push({ description: "PLAY", event: action(() => this.Play()), icon: "expand-arrows-alt" }); + let width = NumCast(this.props.Document.width); + let height = NumCast(this.props.Document.height); + subitems.push({ + description: "Take Snapshot", event: async () => { + var canvas = document.createElement('canvas'); + canvas.width = 640; + canvas.height = 640 * NumCast(this.props.Document.nativeHeight) / NumCast(this.props.Document.nativeWidth); + var ctx = canvas.getContext('2d');//draw image to canvas. scale to target dimensions + if (ctx) { + ctx.rect(0, 0, canvas.width, canvas.height); + ctx.fillStyle = "blue"; + ctx.fill(); + this._videoRef && ctx.drawImage(this._videoRef, 0, 0, canvas.width, canvas.height); + } + + //convert to desired file format + var dataUrl = canvas.toDataURL('image/png'); // can also use 'image/png' + // if you want to preview the captured image, + let filename = encodeURIComponent("snapshot" + this.props.Document.title + "_" + this.props.Document.curPage).replace(/\./g, ""); + VideoBox.convertDataUri(dataUrl, filename).then(returnedFilename => { + if (returnedFilename) { + let url = DocServer.prepend(returnedFilename); + let imageSummary = Docs.Create.ImageDocument(url, { + x: NumCast(this.props.Document.x) + width, y: NumCast(this.props.Document.y), + width: 150, height: height / width * 150, title: "--snapshot" + NumCast(this.props.Document.curPage) + " image-" + }); + this.props.ContainingCollectionView && this.props.ContainingCollectionView.props.addDocument && this.props.ContainingCollectionView.props.addDocument(imageSummary, false); + DocUtils.MakeLink(imageSummary, this.props.Document); + } + }); + }, + icon: "expand-arrows-alt" + }); ContextMenu.Instance.addItem({ description: "Video Funcs...", subitems: subitems }); } } @@ -136,7 +186,7 @@ export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoD let style = "videoBox-content" + (this._fullScreen ? "-fullScreen" : "") + interactive; return !field ? <div>Loading</div> : <video className={`${style}`} ref={this.setVideoRef} onCanPlay={this.videoLoad} controls={VideoBox._showControls} - onPlay={() => this.Play()} onSeeked={this.updateTimecode} onPause={() => this.Pause()}> + onPlay={() => this.Play()} onSeeked={this.updateTimecode} onPause={() => this.Pause()} onClick={e => e.preventDefault()}> <source src={field.url.href} type="video/mp4" /> Not supported. </video>; @@ -155,13 +205,18 @@ export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoD else this._youtubeContentCreated = false; let iframe = e.target; + let started = true; let onYoutubePlayerStateChange = (event: any) => runInAction(() => { - console.log("Event " + event.data); + if (started && event.data === YT.PlayerState.PLAYING) { + started = false; + this._youtubePlayer.unMute(); + this.Pause(); + return; + } if (event.data === YT.PlayerState.PLAYING && !this.Playing) this.Play(false); if (event.data === YT.PlayerState.PAUSED && this.Playing) this.Pause(false); }); let onYoutubePlayerReady = (event: any) => { - console.log("READY!"); this._reactionDisposer && this._reactionDisposer(); this._youtubeReactionDisposer && this._youtubeReactionDisposer(); this._reactionDisposer = reaction(() => this.props.Document.curPage, () => !this.Playing && this.Seek(this.Document.curPage || 0)); @@ -185,8 +240,8 @@ export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoD let style = "videoBox-content-YouTube" + (this._fullScreen ? "-fullScreen" : ""); let start = untracked(() => Math.round(NumCast(this.props.Document.curPage))); return <iframe key={this._youtubeIframeId} id={`${this.youtubeVideoId + this._youtubeIframeId}-player`} - onLoad={this.youtubeIframeLoaded} className={`${style}`} width="640" height="390" - src={`https://www.youtube.com/embed/${this.youtubeVideoId}?enablejsapi=1&rel=0&showinfo=1&autoplay=1&start=${start}&modestbranding=1&controls=${VideoBox._showControls ? 1 : 0}`} + onLoad={this.youtubeIframeLoaded} className={`${style}`} width={NumCast(this.props.Document.nativeWidth, 640)} height={NumCast(this.props.Document.nativeHeight, 390)} + src={`https://www.youtube.com/embed/${this.youtubeVideoId}?enablejsapi=1&rel=0&showinfo=1&autoplay=1&mute=1&start=${start}&modestbranding=1&controls=${VideoBox._showControls ? 1 : 0}`} ></iframe>; } diff --git a/src/client/views/pdf/Annotation.tsx b/src/client/views/pdf/Annotation.tsx index 104241237..ed7081b1d 100644 --- a/src/client/views/pdf/Annotation.tsx +++ b/src/client/views/pdf/Annotation.tsx @@ -75,7 +75,7 @@ class RegionAnnotation extends React.Component<IRegionAnnotationProps> { () => this.props.parent.Index, () => { if (this.props.parent.Index === this.props.index) { - this.props.parent.scrollTo(this.props.y * scale - (NumCast(this.props.parent.props.parent.Document.pdfHeight) / 2)); + this.props.parent.scrollTo(this.props.y * scale); } } ); diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx index c560e581c..699a1ffd7 100644 --- a/src/client/views/pdf/PDFViewer.tsx +++ b/src/client/views/pdf/PDFViewer.tsx @@ -192,7 +192,9 @@ export class Viewer extends React.Component<IViewerProps> { } scrollTo(y: number) { - this.props.parent.scrollTo(y); + if (this.props.mainCont.current) { + this.props.parent.scrollTo(y - this.props.mainCont.current.clientHeight); + } } @action @@ -349,7 +351,7 @@ export class Viewer extends React.Component<IViewerProps> { <img key={res.path} src={res.path} onError={handleError} style={{ width: `${parseInt(res.width) * scale}px`, height: `${parseInt(res.height) * scale}px` }} />); } catch (e) { - + console.log(e); } } } diff --git a/src/client/views/search/FieldFilters.tsx b/src/client/views/search/FieldFilters.tsx index 648aac20a..7a33282d2 100644 --- a/src/client/views/search/FieldFilters.tsx +++ b/src/client/views/search/FieldFilters.tsx @@ -34,7 +34,7 @@ export class FieldFilters extends React.Component<FieldFilterProps> { <div className="field-filters"> <CheckBox default={true} numCount={3} parent={this} originalStatus={this.props.titleFieldStatus} updateStatus={this.props.updateTitleStatus} title={Keys.TITLE} /> <CheckBox default={true} numCount={3} parent={this} originalStatus={this.props.authorFieldStatus} updateStatus={this.props.updateAuthorStatus} title={Keys.AUTHOR} /> - <CheckBox default={true} numCount={3} parent={this} originalStatus={this.props.dataFieldStatus} updateStatus={this.props.updateDataStatus} title={Keys.DATA} /> + <CheckBox default={false} numCount={3} parent={this} originalStatus={this.props.dataFieldStatus} updateStatus={this.props.updateDataStatus} title={"Deleted Docs"} /> </div> ); } diff --git a/src/client/views/search/FilterBox.tsx b/src/client/views/search/FilterBox.tsx index f11fb008c..706d1eb7f 100644 --- a/src/client/views/search/FilterBox.tsx +++ b/src/client/views/search/FilterBox.tsx @@ -40,7 +40,7 @@ export class FilterBox extends React.Component { @observable private _icons: string[] = this._allIcons; @observable private _titleFieldStatus: boolean = true; @observable private _authorFieldStatus: boolean = true; - @observable private _dataFieldStatus: boolean = true; + @observable public _deletedDocsStatus: boolean = false; @observable private _collectionStatus = false; @observable private _collectionSelfStatus = true; @observable private _collectionParentStatus = true; @@ -87,6 +87,9 @@ export class FilterBox extends React.Component { } }); + + let el = acc[i] as HTMLElement; + el.click(); } }); } @@ -161,13 +164,13 @@ export class FilterBox extends React.Component { if (this._authorFieldStatus) { finalQuery = finalQuery + this.basicFieldFilters(query, Keys.AUTHOR); } - if (this._dataFieldStatus) { + if (this._deletedDocsStatus) { finalQuery = finalQuery + this.basicFieldFilters(query, Keys.DATA); } return finalQuery; } - get fieldFiltersApplied() { return !(this._dataFieldStatus && this._authorFieldStatus && this._titleFieldStatus); } + get fieldFiltersApplied() { return !(this._authorFieldStatus && this._titleFieldStatus); } //TODO: basically all of this //gets all of the collections of all the docviews that are selected @@ -305,7 +308,7 @@ export class FilterBox extends React.Component { updateAuthorStatus(newStat: boolean) { this._authorFieldStatus = newStat; } @action.bound - updateDataStatus(newStat: boolean) { this._dataFieldStatus = newStat; } + updateDataStatus(newStat: boolean) { this._deletedDocsStatus = newStat; } @action.bound updateCollectionStatus(newStat: boolean) { this._collectionStatus = newStat; } @@ -321,7 +324,7 @@ export class FilterBox extends React.Component { getParentCollectionStatus() { return this._collectionParentStatus; } getTitleStatus() { return this._titleFieldStatus; } getAuthorStatus() { return this._authorFieldStatus; } - getDataStatus() { return this._dataFieldStatus; } + getDataStatus() { return this._deletedDocsStatus; } // Useful queries: // Delegates of a document: {!join from=id to=proto_i}id:{protoId} @@ -373,7 +376,7 @@ export class FilterBox extends React.Component { <div style={{ marginLeft: "auto" }}><NaviconButton onClick={this.toggleFieldOpen} /></div> </div> <div className="filter-panel"><FieldFilters - titleFieldStatus={this._titleFieldStatus} dataFieldStatus={this._dataFieldStatus} authorFieldStatus={this._authorFieldStatus} + titleFieldStatus={this._titleFieldStatus} dataFieldStatus={this._deletedDocsStatus} authorFieldStatus={this._authorFieldStatus} updateAuthorStatus={this.updateAuthorStatus} updateDataStatus={this.updateDataStatus} updateTitleStatus={this.updateTitleStatus} /> </div> </div> </div> diff --git a/src/client/views/search/SearchBox.scss b/src/client/views/search/SearchBox.scss index 324ba3063..109b88ac9 100644 --- a/src/client/views/search/SearchBox.scss +++ b/src/client/views/search/SearchBox.scss @@ -41,11 +41,10 @@ } .searchBox-results { - margin-right: 142px; + margin-right: 136px; top: 300px; display: flex; flex-direction: column; - margin-right: 72px; max-height: 560px; overflow: hidden; overflow-y: auto; @@ -60,5 +59,6 @@ text-transform: uppercase; text-align: left; font-weight: bold; + margin-left: 28px; } }
\ No newline at end of file diff --git a/src/client/views/search/SearchBox.tsx b/src/client/views/search/SearchBox.tsx index ec778b346..16c44225a 100644 --- a/src/client/views/search/SearchBox.tsx +++ b/src/client/views/search/SearchBox.tsx @@ -6,7 +6,7 @@ import "./FilterBox.scss"; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { SetupDrag } from '../../util/DragManager'; import { Docs } from '../../documents/Documents'; -import { NumCast } from '../../../new_fields/Types'; +import { NumCast, Cast } from '../../../new_fields/Types'; import { Doc } from '../../../new_fields/Doc'; import { SearchItem } from './SearchItem'; import { DocServer } from '../../DocServer'; @@ -22,7 +22,9 @@ export class SearchBox extends React.Component { @observable private _searchString: string = ""; @observable private _resultsOpen: boolean = false; - @observable private _results: Doc[] = []; + @observable private _searchbarOpen: boolean = false; + @observable private _results: [Doc, string[]][] = []; + private _resultsSet = new Map<Doc, number>(); @observable private _openNoResults: boolean = false; @observable private _visibleElements: JSX.Element[] = []; @@ -60,6 +62,7 @@ export class SearchBox extends React.Component { this._openNoResults = false; this._results = []; + this._resultsSet.clear(); this._visibleElements = []; this._numTotalResults = -1; this._endIndex = -1; @@ -91,8 +94,10 @@ export class SearchBox extends React.Component { let query = this._searchString; query = FilterBox.Instance.getFinalQuery(query); this._results = []; + this._resultsSet.clear(); this._isSearch = []; this._visibleElements = []; + FilterBox.Instance.closeFilter(); //if there is no query there should be no result if (query === "") { @@ -107,18 +112,20 @@ export class SearchBox extends React.Component { runInAction(() => { this._resultsOpen = true; + this._searchbarOpen = true; this._openNoResults = true; this.resultsScrolled(); }); } getAllResults = async (query: string) => { - return SearchUtil.Search(query, this.filterQuery, true, 0, 10000000); + return SearchUtil.Search(query, true, { fq: this.filterQuery, start: 0, rows: 10000000 }); } private get filterQuery() { const types = FilterBox.Instance.filterTypes; - return "proto_i:*" + (types ? ` AND (${types.map(type => `({!join from=id to=proto_i}type_t:"${type}" AND NOT type_t:*) OR type_t:"${type}"`).join(" ")})` : ""); + const includeDeleted = FilterBox.Instance.getDataStatus(); + return "NOT baseProto_b:true" + (includeDeleted ? "" : " AND NOT deleted:true") + (types ? ` AND (${types.map(type => `({!join from=id to=proto_i}type_t:"${type}" AND NOT type_t:*) OR type_t:"${type}"`).join(" ")})` : ""); } @@ -129,15 +136,33 @@ export class SearchBox extends React.Component { } this.lockPromise = new Promise(async res => { while (this._results.length <= this._endIndex && (this._numTotalResults === -1 || this._maxSearchIndex < this._numTotalResults)) { - this._curRequest = SearchUtil.Search(query, this.filterQuery, true, this._maxSearchIndex, 10).then(action((res: SearchUtil.DocSearchResult) => { + this._curRequest = SearchUtil.Search(query, true, { fq: this.filterQuery, start: this._maxSearchIndex, rows: 10, hl: true, "hl.fl": "*" }).then(action(async (res: SearchUtil.DocSearchResult) => { // happens at the beginning if (res.numFound !== this._numTotalResults && this._numTotalResults === -1) { this._numTotalResults = res.numFound; } - let filteredDocs = FilterBox.Instance.filterDocsByType(res.docs); - this._results.push(...filteredDocs); + const highlighting = res.highlighting || {}; + const highlightList = res.docs.map(doc => highlighting[doc[Id]]); + const docs = await Promise.all(res.docs.map(async doc => (await Cast(doc.extendsDoc, Doc)) || doc)); + const highlights: typeof res.highlighting = {}; + docs.forEach((doc, index) => highlights[doc[Id]] = highlightList[index]); + let filteredDocs = FilterBox.Instance.filterDocsByType(docs); + runInAction(() => { + // this._results.push(...filteredDocs); + filteredDocs.forEach(doc => { + const index = this._resultsSet.get(doc); + const highlight = highlights[doc[Id]]; + const hlights = highlight ? Object.keys(highlight).map(key => key.substring(0, key.length - 2)) : [] + if (index === undefined) { + this._resultsSet.set(doc, this._results.length); + this._results.push([doc, hlights]); + } else { + this._results[index][1].push(...hlights); + } + }); + }); this._curRequest = undefined; })); @@ -198,6 +223,7 @@ export class SearchBox extends React.Component { this._openNoResults = false; FilterBox.Instance.closeFilter(); this._resultsOpen = true; + this._searchbarOpen = true; FilterBox.Instance._pointerTime = e.timeStamp; } @@ -205,12 +231,14 @@ export class SearchBox extends React.Component { closeSearch = () => { FilterBox.Instance.closeFilter(); this.closeResults(); + this._searchbarOpen = false; } @action.bound closeResults() { this._resultsOpen = false; this._results = []; + this._resultsSet.clear(); this._visibleElements = []; this._numTotalResults = -1; this._endIndex = -1; @@ -255,19 +283,19 @@ export class SearchBox extends React.Component { } else { if (this._isSearch[i] !== "search") { - let result: Doc | undefined = undefined; + let result: [Doc, string[]] | undefined = undefined; if (i >= this._results.length) { this.getResults(this._searchString); if (i < this._results.length) result = this._results[i]; if (result) { - this._visibleElements[i] = <SearchItem doc={result} key={result[Id]} />; + this._visibleElements[i] = <SearchItem doc={result[0]} key={result[0][Id]} highlighting={result[1]} />; this._isSearch[i] = "search"; } } else { result = this._results[i]; if (result) { - this._visibleElements[i] = <SearchItem doc={result} key={result[Id]} />; + this._visibleElements[i] = <SearchItem doc={result[0]} key={result[0][Id]} highlighting={result[1]} />; this._isSearch[i] = "search"; } } @@ -281,15 +309,10 @@ export class SearchBox extends React.Component { } @computed - get resFull() { - console.log(this._numTotalResults); - return this._numTotalResults <= 8; - } + get resFull() { return this._numTotalResults <= 8; } @computed - get resultHeight() { - return this._numTotalResults * 70; - } + get resultHeight() { return this._numTotalResults * 70; } render() { return ( @@ -300,7 +323,7 @@ export class SearchBox extends React.Component { </span> <input value={this._searchString} onChange={this.onChange} type="text" placeholder="Search..." className="searchBox-barChild searchBox-input" onPointerDown={this.openSearch} onKeyPress={this.enter} - style={{ width: this._resultsOpen ? "500px" : "100px" }} /> + style={{ width: this._searchbarOpen ? "500px" : "100px" }} /> <button className="searchBox-barChild searchBox-submit" onClick={this.submitSearch} onPointerDown={FilterBox.Instance.stopProp}>Submit</button> <button className="searchBox-barChild searchBox-filter" onClick={FilterBox.Instance.openFilter} onPointerDown={FilterBox.Instance.stopProp}>Filter</button> </div> diff --git a/src/client/views/search/SearchItem.scss b/src/client/views/search/SearchItem.scss index 24dd2eaa3..273d49349 100644 --- a/src/client/views/search/SearchItem.scss +++ b/src/client/views/search/SearchItem.scss @@ -24,11 +24,15 @@ flex-direction: row; width: 100%; - .search-title { - text-transform: uppercase; - text-align: left; + .search-title-container { width: 100%; - font-weight: bold; + + .search-title { + text-transform: uppercase; + text-align: left; + width: 100%; + font-weight: bold; + } } .search-info { diff --git a/src/client/views/search/SearchItem.tsx b/src/client/views/search/SearchItem.tsx index e34d101a8..a995140e2 100644 --- a/src/client/views/search/SearchItem.tsx +++ b/src/client/views/search/SearchItem.tsx @@ -26,6 +26,7 @@ import { faFile } from '@fortawesome/free-solid-svg-icons'; export interface SearchItemProps { doc: Doc; + highlighting: string[]; } library.add(faCaretUp); @@ -51,9 +52,9 @@ export class SelectorContextMenu extends React.Component<SearchItemProps> { async fetchDocuments() { let aliases = (await SearchUtil.GetViewsOfDocument(this.props.doc)).filter(doc => doc !== this.props.doc); - const { docs } = await SearchUtil.Search("", `data_l:"${this.props.doc[Id]}"`, true); + const { docs } = await SearchUtil.Search("", true, { fq: `data_l:"${this.props.doc[Id]}"` }); const map: Map<Doc, Doc> = new Map; - const allDocs = await Promise.all(aliases.map(doc => SearchUtil.Search("", `data_l:"${doc[Id]}"`, true).then(result => result.docs))); + const allDocs = await Promise.all(aliases.map(doc => SearchUtil.Search("", true, { fq: `data_l:"${doc[Id]}"` }).then(result => result.docs))); allDocs.forEach((docs, index) => docs.forEach(doc => map.set(doc, aliases[index]))); docs.forEach(doc => map.delete(doc)); runInAction(() => { @@ -100,8 +101,8 @@ export class SearchItem extends React.Component<SearchItemProps> { onClick = () => { // I dont think this is the best functionality because clicking the name of the collection does that. Change it back if you'd like - // DocumentManager.Instance.jumpToDocument(this.props.doc, false); - CollectionDockingView.Instance.AddRightSplit(this.props.doc, undefined); + DocumentManager.Instance.jumpToDocument(this.props.doc, false); + // CollectionDockingView.Instance.AddRightSplit(this.props.doc, undefined); } @observable _useIcons = true; @observable _displayDim = 50; @@ -243,7 +244,10 @@ export class SearchItem extends React.Component<SearchItemProps> { onClick={this.onClick} onPointerDown={this.pointerDown} > <div className="main-search-info"> <div title="Drag as document" onPointerDown={this.onPointerDown} style={{ marginRight: "7px" }}> <FontAwesomeIcon icon="file" size="lg" /> </div> - <div className="search-title" id="result" >{StrCast(this.props.doc.title)}</div> + <div className="search-title-container"> + <div className="search-title">{StrCast(this.props.doc.title)}</div> + <div className="search-highlighting">Matched fields: {this.props.highlighting.join(", ")}</div> + </div> <div className="search-info" style={{ width: this._useIcons ? "15%" : "400px" }}> <div className={`icon-${this._useIcons ? "icons" : "live"}`}> <div className="search-type" title="Click to Preview">{this.DocumentIcon}</div> diff --git a/src/new_fields/Doc.ts b/src/new_fields/Doc.ts index 0d9fa540f..1dd721396 100644 --- a/src/new_fields/Doc.ts +++ b/src/new_fields/Doc.ts @@ -371,6 +371,8 @@ export namespace Doc { copy[key] = field; } else if (field instanceof ObjectField) { copy[key] = ObjectField.MakeCopy(field); + } else if (field instanceof Promise) { + field.then(f => (copy[key] === undefined) && (copy[key] = f)); //TODO what should we do here? } else { copy[key] = field; } diff --git a/src/new_fields/Schema.ts b/src/new_fields/Schema.ts index 2355304d5..b1a449e08 100644 --- a/src/new_fields/Schema.ts +++ b/src/new_fields/Schema.ts @@ -104,7 +104,7 @@ export function makeStrictInterface<T extends Interface>(schema: T): (doc: Doc) } export function createSchema<T extends Interface>(schema: T): T & { proto: ToConstructor<Doc> } { - schema.proto = Doc; + (schema as any).proto = Doc; return schema as any; } diff --git a/src/server/GarbageCollector.ts b/src/server/GarbageCollector.ts index 59682e51e..ea5388004 100644 --- a/src/server/GarbageCollector.ts +++ b/src/server/GarbageCollector.ts @@ -59,7 +59,9 @@ function addDoc(doc: any, ids: string[], files: { [name: string]: string[] }) { } } -async function GarbageCollect() { +async function GarbageCollect(full: boolean = true) { + console.log("start GC"); + const start = Date.now(); // await new Promise(res => setTimeout(res, 3000)); const cursor = await Database.Instance.query({}, { userDocumentId: 1 }, 'users'); const users = await cursor.toArray(); @@ -68,7 +70,7 @@ async function GarbageCollect() { const files: { [name: string]: string[] } = {}; while (ids.length) { - const count = Math.min(ids.length, 100); + const count = Math.min(ids.length, 1000); const index = ids.length - count; const fetchIds = ids.splice(index, count).filter(id => !visited.has(id)); if (!fetchIds.length) { @@ -91,43 +93,58 @@ async function GarbageCollect() { cursor.close(); - const toDeleteCursor = await Database.Instance.query({ _id: { $nin: Array.from(visited) } }, { _id: 1 }); + const notToDelete = Array.from(visited); + const toDeleteCursor = await Database.Instance.query({ _id: { $nin: notToDelete } }, { _id: 1 }); const toDelete: string[] = (await toDeleteCursor.toArray()).map(doc => doc._id); toDeleteCursor.close(); - let i = 0; - let deleted = 0; - while (i < toDelete.length) { - const count = Math.min(toDelete.length, 5000); - const toDeleteDocs = toDelete.slice(i, i + count); - i += count; - const result = await Database.Instance.delete({ _id: { $in: toDeleteDocs } }, "newDocuments"); - deleted += result.deletedCount || 0; - } - // const result = await Database.Instance.delete({ _id: { $in: toDelete } }, "newDocuments"); - console.log(`${deleted} documents deleted`); + 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( + notToDelete.map<any>(id => ({ + id, deleted: { set: null } + })) + .concat(toDelete.map(id => ({ + id, deleted: { set: true } + }))))); + console.log("Done with partial GC"); + console.log(`Took ${(Date.now() - start) / 1000} seconds`); + } else { + let i = 0; + let deleted = 0; + while (i < toDelete.length) { + const count = Math.min(toDelete.length, 5000); + const toDeleteDocs = toDelete.slice(i, i + count); + i += count; + const result = await Database.Instance.delete({ _id: { $in: toDeleteDocs } }, "newDocuments"); + deleted += result.deletedCount || 0; + } + // const result = await Database.Instance.delete({ _id: { $in: toDelete } }, "newDocuments"); + console.log(`${deleted} documents deleted`); - await Search.Instance.deleteDocuments(toDelete); - console.log("Cleared search documents"); + await Search.Instance.deleteDocuments(toDelete); + console.log("Cleared search documents"); - const folder = "./src/server/public/files/"; - fs.readdir(folder, (_, fileList) => { - const filesToDelete = fileList.filter(file => { - const ext = path.extname(file); - let base = path.basename(file, ext); - const existsInDb = (base in files || (base = base.substring(0, base.length - 2)) in files) && files[base].includes(ext); - return file !== ".gitignore" && !existsInDb; - }); - console.log(`Deleting ${filesToDelete.length} files`); - filesToDelete.forEach(file => { - console.log(`Deleting file ${file}`); - try { - fs.unlinkSync(folder + file); - } catch { - console.warn(`Couldn't delete file ${file}`); - } + const folder = "./src/server/public/files/"; + fs.readdir(folder, (_, fileList) => { + const filesToDelete = fileList.filter(file => { + const ext = path.extname(file); + let base = path.basename(file, ext); + const existsInDb = (base in files || (base = base.substring(0, base.length - 2)) in files) && files[base].includes(ext); + return file !== ".gitignore" && !existsInDb; + }); + console.log(`Deleting ${filesToDelete.length} files`); + filesToDelete.forEach(file => { + console.log(`Deleting file ${file}`); + try { + fs.unlinkSync(folder + file); + } catch { + console.warn(`Couldn't delete file ${file}`); + } + }); + console.log(`Deleted ${filesToDelete.length} files`); }); - console.log(`Deleted ${filesToDelete.length} files`); - }); + } } -GarbageCollect(); +GarbageCollect(false); diff --git a/src/server/Search.ts b/src/server/Search.ts index 69e327d2d..723dc101b 100644 --- a/src/server/Search.ts +++ b/src/server/Search.ts @@ -30,20 +30,14 @@ export class Search { } } - public async search(query: string, filterQuery: string = "", start: number = 0, rows: number = 10) { + public async search(query: any) { try { const searchResults = JSON.parse(await rp.get(this.url + "dash/select", { - qs: { - q: query, - fq: filterQuery, - fl: "id", - start, - rows, - } + qs: query })); const { docs, numFound } = searchResults.response; const ids = docs.map((field: any) => field.id); - return { ids, numFound }; + return { ids, numFound, highlighting: searchResults.highlighting }; } catch { return { ids: [], numFound: -1 }; } diff --git a/src/server/authentication/controllers/user_controller.ts b/src/server/authentication/controllers/user_controller.ts index 0e431f1e6..f5c6e1610 100644 --- a/src/server/authentication/controllers/user_controller.ts +++ b/src/server/authentication/controllers/user_controller.ts @@ -77,8 +77,9 @@ export let postSignup = (req: Request, res: Response, next: NextFunction) => { let tryRedirectToTarget = (req: Request, res: Response) => { if (req.session && req.session.target) { - res.redirect(req.session.target); + let target = req.session.target; req.session.target = undefined; + res.redirect(target); } else { res.redirect(RouteStore.home); } diff --git a/src/server/database.ts b/src/server/database.ts index 29a8ffafa..7f5331998 100644 --- a/src/server/database.ts +++ b/src/server/database.ts @@ -140,6 +140,17 @@ export class Database { } } + public updateMany(query: any, update: any, collectionName = "newDocuments") { + if (this.db) { + const db = this.db; + return new Promise<mongodb.WriteOpResult>(res => db.collection(collectionName).update(query, update, (_, result) => res(result))); + } else { + return new Promise<mongodb.WriteOpResult>(res => { + this.onConnect.push(() => this.updateMany(query, update, collectionName).then(res)); + }); + } + } + public print() { console.log("db says hi!"); } diff --git a/src/server/index.ts b/src/server/index.ts index 06f8358e1..5703b032e 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -110,7 +110,7 @@ function addSecureRoute(method: Method, if (req.user) { handler(req.user, res, req); } else { - req.session!.target = `http://localhost:${port}${req.originalUrl}`; + req.session!.target = req.originalUrl; onRejection(res, req); } }; @@ -144,12 +144,13 @@ app.get("/pull", (req, res) => // GETTERS app.get("/search", async (req, res) => { - const { query, filterQuery, start, rows } = req.query; - if (query === undefined) { + const solrQuery: any = {}; + ["q", "fq", "start", "rows", "hl", "hl.fl"].forEach(key => solrQuery[key] = req.query[key]); + if (solrQuery.q === undefined) { res.send([]); return; } - let results = await Search.Instance.search(query, filterQuery, start, rows); + let results = await Search.Instance.search(solrQuery); res.send(results); }); @@ -178,11 +179,10 @@ app.get("/whosOnline", (req, res) => { res.send(users); }); - app.get("/thumbnail/:filename", (req, res) => { let filename = req.params.filename; let noExt = filename.substring(0, filename.length - ".png".length); - let pagenumber = parseInt(noExt[noExt.length - 1]); + let pagenumber = parseInt(noExt.split('-')[1]); fs.exists(uploadDir + filename, (exists: boolean) => { console.log(`${uploadDir + filename} ${exists ? "exists" : "does not exist"}`); if (exists) { @@ -190,13 +190,14 @@ app.get("/thumbnail/:filename", (req, res) => { probe(input, (err: any, result: any) => { if (err) { console.log(err); + console.log(`error on ${filename}`); return; } res.send({ path: "/files/" + filename, width: result.width, height: result.height }); }); } else { - LoadPage(uploadDir + filename.substring(0, filename.length - "-n.png".length) + ".pdf", pagenumber, res); + LoadPage(uploadDir + filename.substring(0, filename.length - noExt.split('-')[1].length - ".PNG".length - 1) + ".pdf", pagenumber, res); } }); }); diff --git a/src/server/updateProtos.ts b/src/server/updateProtos.ts new file mode 100644 index 000000000..90490eb45 --- /dev/null +++ b/src/server/updateProtos.ts @@ -0,0 +1,15 @@ +import { Database } from "./database"; + +const protos = + ["text", "histogram", "image", "web", "collection", "kvp", + "video", "audio", "pdf", "icon", "import", "linkdoc"]; + +(async function () { + await Promise.all( + protos.map(protoId => new Promise(res => Database.Instance.update(protoId, { + $set: { "fields.baseProto": true } + }, res))) + ); + + console.log("done"); +})();
\ No newline at end of file |