From a30cbfd90f3b5207fc790a1c8dc61e58f69f4e38 Mon Sep 17 00:00:00 2001
From: Sam Wilkins <samuel_wilkins@brown.edu>
Date: Wed, 5 Jun 2019 22:13:34 -0400
Subject: tab focus shifting on tab drag over and beginnings of global key
 handling, including splitting with control + rightarrow

---
 .../views/collections/CollectionDockingView.tsx      | 20 ++++++++++++++++++++
 1 file changed, 20 insertions(+)

(limited to 'src/client/views/collections/CollectionDockingView.tsx')

diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx
index dcc1bd95d..4b7868e81 100644
--- a/src/client/views/collections/CollectionDockingView.tsx
+++ b/src/client/views/collections/CollectionDockingView.tsx
@@ -44,6 +44,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
     private _containerRef = React.createRef<HTMLDivElement>();
     private _flush: boolean = false;
     private _ignoreStateChange = "";
+    private _isPointerDown = false;
 
     constructor(props: SubCollectionViewProps) {
         super(props);
@@ -247,6 +248,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
 
     @action
     onPointerUp = (e: React.PointerEvent): void => {
+        this._isPointerDown = false;
         if (this._flush) {
             this._flush = false;
             setTimeout(() => this.stateChanged(), 10);
@@ -254,6 +256,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
     }
     @action
     onPointerDown = (e: React.PointerEvent): void => {
+        this._isPointerDown = true;
         var className = (e.target as any).className;
         if (className === "messageCounter") {
             e.stopPropagation();
@@ -334,6 +337,23 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
                     tab.element.append(counter);
                     let upDiv = document.createElement("span");
                     const stack = tab.contentItem.parent;
+                    // console.log("TAB: ", tab);
+                    tab.element[0].onmouseenter = (e: any) => {
+                        if (!this._isPointerDown) return;
+                        var activeContentItem = tab.header.parent.getActiveContentItem();
+                        if (tab.contentItem !== activeContentItem) {
+                            tab.header.parent.setActiveContentItem(tab.contentItem);
+                        }
+                        tab.setActive(true);
+                    }
+                    // tab.element[0].ondragenter = (e: any) => {
+                    //     console.log("DRAGGING OVER DETECTED!");
+                    //     console.log(e);
+                    // }
+                    // tab.element[0].ondrag = (e: any) => {
+                    //     console.log("DRAGGING!");
+                    //     console.log(e);
+                    // }
                     ReactDOM.render(<ParentDocSelector Document={doc} addDocTab={(doc, location) => CollectionDockingView.Instance.AddTab(stack, doc)} />, upDiv);
                     tab.reactComponents = [upDiv];
                     tab.element.append(upDiv);
-- 
cgit v1.2.3-70-g09d2


From 79b37db46fda36cd779645256b03d9d074141eb6 Mon Sep 17 00:00:00 2001
From: Sam Wilkins <samuel_wilkins@brown.edu>
Date: Wed, 5 Jun 2019 22:13:49 -0400
Subject: cleanup and documentation

---
 src/client/views/collections/CollectionDockingView.tsx | 10 +---------
 1 file changed, 1 insertion(+), 9 deletions(-)

(limited to 'src/client/views/collections/CollectionDockingView.tsx')

diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx
index 4b7868e81..1adb73bcf 100644
--- a/src/client/views/collections/CollectionDockingView.tsx
+++ b/src/client/views/collections/CollectionDockingView.tsx
@@ -337,7 +337,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
                     tab.element.append(counter);
                     let upDiv = document.createElement("span");
                     const stack = tab.contentItem.parent;
-                    // console.log("TAB: ", tab);
+                    // shifts the focus to this tab when another tab is dragged over it
                     tab.element[0].onmouseenter = (e: any) => {
                         if (!this._isPointerDown) return;
                         var activeContentItem = tab.header.parent.getActiveContentItem();
@@ -346,14 +346,6 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
                         }
                         tab.setActive(true);
                     }
-                    // tab.element[0].ondragenter = (e: any) => {
-                    //     console.log("DRAGGING OVER DETECTED!");
-                    //     console.log(e);
-                    // }
-                    // tab.element[0].ondrag = (e: any) => {
-                    //     console.log("DRAGGING!");
-                    //     console.log(e);
-                    // }
                     ReactDOM.render(<ParentDocSelector Document={doc} addDocTab={(doc, location) => CollectionDockingView.Instance.AddTab(stack, doc)} />, upDiv);
                     tab.reactComponents = [upDiv];
                     tab.element.append(upDiv);
-- 
cgit v1.2.3-70-g09d2


From 74dee5f76ffc5bbdd07caafb4273aaf485dec1b9 Mon Sep 17 00:00:00 2001
From: Sam Wilkins <samuel_wilkins@brown.edu>
Date: Wed, 12 Jun 2019 23:22:58 -0400
Subject: beginnings of nested golden layout

---
 src/client/documents/Documents.ts                  |  18 +++
 src/client/util/DocumentManager.ts                 |   4 +-
 src/client/util/DragManager.ts                     |   8 +-
 src/client/util/TooltipTextMenu.tsx                |   2 +-
 src/client/views/MainOverlayTextBox.tsx            |   4 +-
 src/client/views/MainView.tsx                      |  20 ++-
 .../views/collections/CollectionBaseView.tsx       |   2 +-
 .../views/collections/CollectionDockingView.tsx    | 162 ++++++++++++++-------
 .../views/collections/CollectionTreeView.tsx       |   2 +-
 .../views/collections/ParentDocumentSelector.tsx   |   4 +-
 src/client/views/nodes/DocumentView.tsx            |  12 +-
 11 files changed, 158 insertions(+), 80 deletions(-)

(limited to 'src/client/views/collections/CollectionDockingView.tsx')

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index ab61b915c..b346e1570 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -35,6 +35,7 @@ import { dropActionType } from "../util/DragManager";
 import { DateField } from "../../new_fields/DateField";
 import { UndoManager } from "../util/UndoManager";
 import { RouteStore } from "../../server/RouteStore";
+import { CollectionDockingView } from "../views/collections/CollectionDockingView";
 var requestImageSize = require('request-image-size');
 var path = require('path');
 
@@ -315,6 +316,23 @@ export namespace Docs {
     export function DockDocument(documents: Array<Doc>, config: string, options: DocumentOptions, id?: string) {
         return CreateInstance(collProto, new List(documents), { ...options, viewType: CollectionViewType.Docking, dockingConfig: config }, id);
     }
+    export type DocConfig = {
+        doc: Doc,
+        initialWidth?: number
+    }
+    export function StandardCollectionDockingDocument(configs: Array<DocConfig>, options: DocumentOptions, id?: string, type: string = "row") {
+        let layoutConfig = {
+            content: [
+                {
+                    type: type,
+                    content: [
+                        ...configs.map(config => CollectionDockingView.makeDocumentConfig(config.doc, config.initialWidth))
+                    ]
+                }
+            ]
+        };
+        return DockDocument(configs.map(c => c.doc), JSON.stringify(layoutConfig), options, id);
+    }
 
     export function CaptionDocument(doc: Doc) {
         const captionDoc = Doc.MakeAlias(doc);
diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts
index 65c4b9e4b..72d9f7e76 100644
--- a/src/client/util/DocumentManager.ts
+++ b/src/client/util/DocumentManager.ts
@@ -134,7 +134,7 @@ export class DocumentManager {
                 const actualDoc = Doc.MakeAlias(docDelegate);
                 actualDoc.libraryBrush = true;
                 if (linkPage !== undefined) actualDoc.curPage = linkPage;
-                (dockFunc || CollectionDockingView.Instance.AddRightSplit)(actualDoc);
+                (dockFunc || CollectionDockingView.AddRightSplit)(actualDoc);
             } else {
                 let contextView: DocumentView | null;
                 docDelegate.libraryBrush = true;
@@ -142,7 +142,7 @@ export class DocumentManager {
                     contextDoc.panTransformType = "Ease";
                     contextView.props.focus(contextDoc);
                 } else {
-                    (dockFunc || CollectionDockingView.Instance.AddRightSplit)(contextDoc);
+                    (dockFunc || CollectionDockingView.AddRightSplit)(contextDoc);
                 }
             }
         }
diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts
index 1e84a0db0..7625b0463 100644
--- a/src/client/util/DragManager.ts
+++ b/src/client/util/DragManager.ts
@@ -26,8 +26,8 @@ export function SetupDrag(_reference: React.RefObject<HTMLElement>, docFunc: ()
         // if (this.props.isSelected() || this.props.isTopMost) {
         if (e.button === 0) {
             e.stopPropagation();
-            if (e.shiftKey && CollectionDockingView.Instance) {
-                CollectionDockingView.Instance.StartOtherDrag([await docFunc()], e);
+            if (e.shiftKey && CollectionDockingView.TopLevel) {
+                CollectionDockingView.TopLevel.StartOtherDrag([await docFunc()], e);
             } else {
                 document.addEventListener("pointermove", onRowMove);
                 document.addEventListener("pointerup", onRowUp);
@@ -264,9 +264,9 @@ export namespace DragManager {
             if (dragData instanceof DocumentDragData) {
                 dragData.userDropAction = e.ctrlKey || e.altKey ? "alias" : undefined;
             }
-            if (e.shiftKey && CollectionDockingView.Instance) {
+            if (e.shiftKey && CollectionDockingView.TopLevel) {
                 AbortDrag();
-                CollectionDockingView.Instance.StartOtherDrag(docs, {
+                CollectionDockingView.TopLevel.StartOtherDrag(docs, {
                     pageX: e.pageX,
                     pageY: e.pageY,
                     preventDefault: emptyFunction,
diff --git a/src/client/util/TooltipTextMenu.tsx b/src/client/util/TooltipTextMenu.tsx
index f517f757a..fa2483db5 100644
--- a/src/client/util/TooltipTextMenu.tsx
+++ b/src/client/util/TooltipTextMenu.tsx
@@ -194,7 +194,7 @@ export class TooltipTextMenu {
                                 if (DocumentManager.Instance.getDocumentView(f)) {
                                     DocumentManager.Instance.getDocumentView(f)!.props.focus(f);
                                 }
-                                else if (CollectionDockingView.Instance) CollectionDockingView.Instance.AddRightSplit(f);
+                                else if (CollectionDockingView.TopLevel) CollectionDockingView.AddRightSplit(f);
                             }
                         }));
                     }
diff --git a/src/client/views/MainOverlayTextBox.tsx b/src/client/views/MainOverlayTextBox.tsx
index 24327b995..718979123 100644
--- a/src/client/views/MainOverlayTextBox.tsx
+++ b/src/client/views/MainOverlayTextBox.tsx
@@ -86,7 +86,7 @@ export class MainOverlayTextBox extends React.Component<MainOverlayTextBoxProps>
 
     addDocTab = (doc: Doc, location: string) => {
         if (true) { // location === "onRight") { need to figure out stack to add "inTab"
-            CollectionDockingView.Instance.AddRightSplit(doc);
+            CollectionDockingView.AddRightSplit(doc);
         }
     }
     render() {
@@ -102,6 +102,6 @@ export class MainOverlayTextBox extends React.Component<MainOverlayTextBoxProps>
                 </div>
             </ div>;
         }
-        else return (null); Z
+        else return (null);
     }
 }
\ No newline at end of file
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 67a026897..426e2440a 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -1,5 +1,5 @@
 import { IconName, library } from '@fortawesome/fontawesome-svg-core';
-import { faFilePdf, faFilm, faFont, faGlobeAsia, faImage, faMusic, faObjectGroup, faPenNib, faRedoAlt, faTable, faTree, faUndoAlt, faBell } from '@fortawesome/free-solid-svg-icons';
+import { faClone, faFilePdf, faFilm, faFont, faGlobeAsia, faImage, faMusic, faObjectGroup, faPenNib, faRedoAlt, faTable, faTree, faUndoAlt, faBell } from '@fortawesome/free-solid-svg-icons';
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { action, computed, configure, observable, runInAction, trace } from 'mobx';
 import { observer } from 'mobx-react';
@@ -106,6 +106,7 @@ export class MainView extends React.Component {
         library.add(faFilm);
         library.add(faMusic);
         library.add(faTree);
+        library.add(faClone)
         this.initEventListeners();
         this.initAuthenticationRouters();
     }
@@ -151,8 +152,11 @@ export class MainView extends React.Component {
         const list = Cast(CurrentUserUtils.UserDocument.data, listSpec(Doc));
         if (list) {
             let freeformDoc = Docs.FreeformDocument([], { x: 0, y: 400, width: this.pwidth * .7, height: this.pheight, title: `WS collection ${list.length + 1}` });
-            var dockingLayout = { content: [{ type: 'row', content: [CollectionDockingView.makeDocumentConfig(CurrentUserUtils.UserDocument, 150), CollectionDockingView.makeDocumentConfig(freeformDoc, 600)] }] };
-            let mainDoc = Docs.DockDocument([CurrentUserUtils.UserDocument, freeformDoc], JSON.stringify(dockingLayout), { title: `Workspace ${list.length + 1}` }, id);
+            let configs = [
+                { doc: CurrentUserUtils.UserDocument, initialWidth: 150 },
+                { doc: freeformDoc, initialWidth: 600 }
+            ]
+            let mainDoc = Docs.StandardCollectionDockingDocument(configs, { title: `Workspace ${list.length + 1}` }, id);
             list.push(mainDoc);
             // bcz: strangely, we need a timeout to prevent exceptions/issues initializing GoldenLayout (the rendering engine for Main Container)
             setTimeout(() => {
@@ -183,8 +187,8 @@ export class MainView extends React.Component {
     }
 
     openNotifsCol = () => {
-        if (this._notifsCol && CollectionDockingView.Instance) {
-            CollectionDockingView.Instance.AddRightSplit(this._notifsCol);
+        if (this._notifsCol && CollectionDockingView.TopLevel) {
+            CollectionDockingView.AddRightSplit(this._notifsCol);
         }
     }
 
@@ -240,6 +244,7 @@ export class MainView extends React.Component {
 
         let addTextNode = action(() => Docs.TextDocument({ borderRounding: -1, width: 200, height: 200, title: "a text note" }));
         let addColNode = action(() => Docs.FreeformDocument([], { width: this.pwidth * .7, height: this.pheight, title: "a freeform collection" }));
+        let addDockingNode = action(() => Docs.StandardCollectionDockingDocument([{ doc: addColNode(), initialWidth: 200 }], { width: 200, height: 200, title: "a nested docking freeform collection" }));
         let addSchemaNode = action(() => Docs.SchemaDocument(["title"], [], { width: 200, height: 200, title: "a schema collection" }));
         let addTreeNode = action(() => CurrentUserUtils.UserDocument);
         //let addTreeNode = action(() => Docs.TreeDocument([CurrentUserUtils.UserDocument], { width: 250, height: 400, title: "Library:" + CurrentUserUtils.email, dropAction: "alias" }));
@@ -260,6 +265,7 @@ export class MainView extends React.Component {
             [React.createRef<HTMLDivElement>(), "object-group", "Add Collection", addColNode],
             [React.createRef<HTMLDivElement>(), "tree", "Add Tree", addTreeNode],
             [React.createRef<HTMLDivElement>(), "table", "Add Schema", addSchemaNode],
+            [React.createRef<HTMLDivElement>(), "clone", "Add Docking Frame", addDockingNode]
         ];
 
         return < div id="add-nodes-menu" >
@@ -327,12 +333,12 @@ export class MainView extends React.Component {
         switch (e.key) {
             case "ArrowRight":
                 if (this.mainFreeform) {
-                    CollectionDockingView.Instance.AddRightSplit(this.mainFreeform!);
+                    CollectionDockingView.AddRightSplit(this.mainFreeform!);
                 }
                 break;
             case "ArrowLeft":
                 if (this.mainFreeform) {
-                    CollectionDockingView.Instance.CloseRightSplit(this.mainFreeform!);
+                    CollectionDockingView.CloseRightSplit(this.mainFreeform!);
                 }
                 break;
             case "o":
diff --git a/src/client/views/collections/CollectionBaseView.tsx b/src/client/views/collections/CollectionBaseView.tsx
index 4d6721dc1..096c65092 100644
--- a/src/client/views/collections/CollectionBaseView.tsx
+++ b/src/client/views/collections/CollectionBaseView.tsx
@@ -16,7 +16,7 @@ export enum CollectionViewType {
     Schema,
     Docking,
     Tree,
-    Stacking
+    Stacking,
 }
 
 export interface CollectionRenderProps {
diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx
index 1adb73bcf..dfb8fac35 100644
--- a/src/client/views/collections/CollectionDockingView.tsx
+++ b/src/client/views/collections/CollectionDockingView.tsx
@@ -1,9 +1,9 @@
 import 'golden-layout/src/css/goldenlayout-base.css';
 import 'golden-layout/src/css/goldenlayout-dark-theme.css';
-import { action, observable, reaction, Lambda } from "mobx";
+import { action, observable, reaction, Lambda, IReactionDisposer } from "mobx";
 import { observer } from "mobx-react";
 import * as ReactDOM from 'react-dom';
-import Measure from "react-measure";
+import Measure, { ContentRect } from "react-measure";
 import * as GoldenLayout from "../../../client/goldenLayout";
 import { Doc, Field, Opt, DocListCast } from "../../../new_fields/Doc";
 import { FieldId } from "../../../new_fields/RefField";
@@ -26,7 +26,7 @@ import { CurrentUserUtils } from '../../../server/authentication/models/current_
 
 @observer
 export class CollectionDockingView extends React.Component<SubCollectionViewProps> {
-    public static Instance: CollectionDockingView;
+    public static TopLevel: CollectionDockingView;
     public static makeDocumentConfig(document: Doc, width?: number) {
         return {
             type: 'react-component',
@@ -35,11 +35,16 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
             width: width,
             props: {
                 documentId: document[Id],
-                //collectionDockingView: CollectionDockingView.Instance
             }
         };
     }
 
+    private makeDocConfig = (document: Doc, width?: number) => {
+        const config = CollectionDockingView.makeDocumentConfig(document, width);
+        (config.props as any).parent = this;
+        return config;
+    }
+
     private _goldenLayout: any = null;
     private _containerRef = React.createRef<HTMLDivElement>();
     private _flush: boolean = false;
@@ -48,7 +53,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
 
     constructor(props: SubCollectionViewProps) {
         super(props);
-        CollectionDockingView.Instance = this;
+        CollectionDockingView.TopLevel = this;
         (window as any).React = React;
         (window as any).ReactDOM = ReactDOM;
     }
@@ -58,41 +63,69 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
         this.hack = true;
         this.undohack = UndoManager.StartBatch("goldenDrag");
         dragDocs.map(dragDoc =>
-            this.AddRightSplit(dragDoc, true).contentItems[0].tab._dragListener.
+            CollectionDockingView.AddRightSplit(dragDoc, true).contentItems[0].tab._dragListener.
                 onMouseDown({ pageX: e.pageX, pageY: e.pageY, preventDefault: emptyFunction, button: 0 }));
     }
 
-    @action
-    public OpenFullScreen(document: Doc) {
+    private openFullScreen = (document: Doc) => {
         let newItemStackConfig = {
             type: 'stack',
-            content: [CollectionDockingView.makeDocumentConfig(document)]
+            content: [this.makeDocConfig(document)]
         };
         var docconfig = this._goldenLayout.root.layoutManager.createContentItem(newItemStackConfig, this._goldenLayout);
         this._goldenLayout.root.contentItems[0].addChild(docconfig);
         docconfig.callDownwards('_$init');
         this._goldenLayout._$maximiseItem(docconfig);
-        this._ignoreStateChange = JSON.stringify(this._goldenLayout.toConfig());
+        this._ignoreStateChange = this.retrieveConfiguration();
         this.stateChanged();
     }
 
+    @action
+    public static OpenFullScreen(document: Doc, dockingView: CollectionDockingView = CollectionDockingView.TopLevel) {
+        dockingView.openFullScreen(document);
+    }
+
+    initializeConfiguration = (configText: string) => {
+        let configuration: any = JSON.parse(configText);
+        this.injectParentProp(configuration.content);
+        this._goldenLayout = new GoldenLayout(configuration);
+    }
+
+    retrieveConfiguration = () => {
+        let configuration: any = this._goldenLayout.toConfig();
+        this.injectParentProp(configuration.content, true);
+        return JSON.stringify(configuration);
+    }
+
+    injectParentProp = (contentArray: any[], reverse: boolean = false) => {
+        if (!contentArray || contentArray.length == 0) return;
+        contentArray.forEach(member => {
+            let baseCase = Object.keys(member).includes("props");
+            if (!baseCase) {
+                this.injectParentProp(member.content, reverse)
+            } else {
+                reverse ? delete member.props.parent : member.props.parent = this;
+            }
+        });
+    }
+
     @undoBatch
     @action
-    public CloseRightSplit = (document: Doc): boolean => {
+    public static CloseRightSplit = (document: Doc, dockingView: CollectionDockingView = CollectionDockingView.TopLevel): boolean => {
         let retVal = false;
-        if (this._goldenLayout.root.contentItems[0].isRow) {
-            retVal = Array.from(this._goldenLayout.root.contentItems[0].contentItems).some((child: any) => {
+        if (dockingView._goldenLayout.root.contentItems[0].isRow) {
+            retVal = Array.from(dockingView._goldenLayout.root.contentItems[0].contentItems).some((child: any) => {
                 if (child.contentItems.length === 1 && child.contentItems[0].config.component === "DocumentFrameRenderer" &&
                     Doc.AreProtosEqual(DocumentManager.Instance.getDocumentViewById(child.contentItems[0].config.props.documentId)!.Document, document)) {
                     child.contentItems[0].remove();
-                    this.layoutChanged(document);
+                    dockingView.layoutChanged(document);
                     return true;
                 } else {
                     Array.from(child.contentItems).filter((tab: any) => tab.config.component === "DocumentFrameRenderer").some((tab: any, j: number) => {
                         if (Doc.AreProtosEqual(DocumentManager.Instance.getDocumentViewById(tab.config.props.documentId)!.Document, document)) {
                             child.contentItems[j].remove();
                             child.config.activeItemIndex = Math.max(child.contentItems.length - 1, 0);
-                            let docs = Cast(this.props.Document.data, listSpec(Doc));
+                            let docs = Cast(dockingView.props.Document.data, listSpec(Doc));
                             docs && docs.indexOf(document) !== -1 && docs.splice(docs.indexOf(document), 1);
                             return true;
                         }
@@ -103,7 +136,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
             });
         }
         if (retVal) {
-            this.stateChanged();
+            dockingView.stateChanged();
         }
         return retVal;
     }
@@ -112,23 +145,26 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
     layoutChanged(removed?: Doc) {
         this._goldenLayout.root.callDownwards('setSize', [this._goldenLayout.width, this._goldenLayout.height]);
         this._goldenLayout.emit('stateChanged');
-        this._ignoreStateChange = JSON.stringify(this._goldenLayout.toConfig());
-        if (removed) CollectionDockingView.Instance._removedDocs.push(removed);
+        this._ignoreStateChange = this.retrieveConfiguration();
+        if (removed) CollectionDockingView.TopLevel._removedDocs.push(removed);
         this.stateChanged();
     }
 
     //
     //  Creates a vertical split on the right side of the docking view, and then adds the Document to that split
     //
-    @action
-    public AddRightSplit = (document: Doc, minimize: boolean = false) => {
+    public static AddRightSplit = (document: Doc, minimize: boolean = false, dockingView: CollectionDockingView = CollectionDockingView.TopLevel) => {
+        return dockingView.addRightSplit(document, minimize);
+    }
+
+    private addRightSplit(document: Doc, minimize = false) {
         let docs = Cast(this.props.Document.data, listSpec(Doc));
         if (docs) {
             docs.push(document);
         }
         let newItemStackConfig = {
             type: 'stack',
-            content: [CollectionDockingView.makeDocumentConfig(document)]
+            content: [this.makeDocConfig(document)]
         };
 
         var newContentItem = this._goldenLayout.root.layoutManager.createContentItem(newItemStackConfig, this._goldenLayout);
@@ -157,13 +193,17 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
 
         return newContentItem;
     }
-    @action
-    public AddTab = (stack: any, document: Doc) => {
+
+    public static AddTab = (stack: any, document: Doc, dockingView: CollectionDockingView = CollectionDockingView.TopLevel) => {
+        dockingView.addTab(stack, document);
+    }
+
+    private addTab = (stack: any, document: Doc) => {
         let docs = Cast(this.props.Document.data, listSpec(Doc));
         if (docs) {
             docs.push(document);
         }
-        let docContentConfig = CollectionDockingView.makeDocumentConfig(document);
+        let docContentConfig = this.makeDocConfig(document);
         var newContentItem = stack.layoutManager.createContentItem(docContentConfig, this._goldenLayout);
         stack.addChild(newContentItem.contentItems[0], undefined);
         this.layoutChanged();
@@ -173,10 +213,10 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
         var config = StrCast(this.props.Document.dockingConfig);
         if (config) {
             if (!this._goldenLayout) {
-                this._goldenLayout = new GoldenLayout(JSON.parse(config));
+                this.initializeConfiguration(config);
             }
             else {
-                if (config === JSON.stringify(this._goldenLayout.toConfig())) {
+                if (config === this.retrieveConfiguration()) {
                     return;
                 }
                 try {
@@ -186,7 +226,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
                     this._goldenLayout.unbind('stackCreated', this.stackCreated);
                 } catch (e) { }
                 this._goldenLayout.destroy();
-                this._goldenLayout = new GoldenLayout(JSON.parse(config));
+                this.initializeConfiguration(config);
             }
             this._goldenLayout.on('itemDropped', this.itemDropped);
             this._goldenLayout.on('tabCreated', this.tabCreated);
@@ -204,13 +244,13 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
             this._goldenLayout.init();
         }
     }
-    reactionDisposer?: Lambda;
+    reactionDisposer?: IReactionDisposer;
     componentDidMount: () => void = () => {
         if (this._containerRef.current) {
             this.reactionDisposer = reaction(
                 () => StrCast(this.props.Document.dockingConfig),
                 () => {
-                    if (!this._goldenLayout || this._ignoreStateChange !== JSON.stringify(this._goldenLayout.toConfig())) {
+                    if (!this._goldenLayout || this._ignoreStateChange !== this.retrieveConfiguration()) {
                         // Because this is in a set timeout, if this component unmounts right after mounting,
                         // we will leak a GoldenLayout, because we try to destroy it before we ever create it
                         setTimeout(() => this.setupGoldenLayout(), 1);
@@ -218,7 +258,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
                     this._ignoreStateChange = "";
                 }, { fireImmediately: true });
 
-            window.addEventListener('resize', this.onResize); // bcz: would rather add this event to the parent node, but resize events only come from Window
+            // window.addEventListener('resize', this.onResize); // bcz: would rather add this event to the parent node, but resize events only come from Window
         }
     }
     componentWillUnmount: () => void = () => {
@@ -232,18 +272,19 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
         }
         if (this._goldenLayout) this._goldenLayout.destroy();
         this._goldenLayout = null;
-        window.removeEventListener('resize', this.onResize);
+        // window.removeEventListener('resize', this.onResize);
 
         if (this.reactionDisposer) {
             this.reactionDisposer();
         }
     }
     @action
-    onResize = (event: any) => {
-        var cur = this._containerRef.current;
-
+    onResize = (size: ContentRect) => {
         // bcz: since GoldenLayout isn't a React component itself, we need to notify it to resize when its document container's size has changed
-        this._goldenLayout.updateSize(cur!.getBoundingClientRect().width, cur!.getBoundingClientRect().height);
+        // this._goldenLayout.updateSize(cur!.getBoundingClientRect().width, cur!.getBoundingClientRect().height);
+        if (this._goldenLayout) {
+            this._goldenLayout.updateSize(size.offset!.width, size.offset!.height);
+        }
     }
 
     @action
@@ -301,12 +342,12 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
 
     @undoBatch
     stateChanged = () => {
-        let docs = Cast(CollectionDockingView.Instance.props.Document.data, listSpec(Doc));
-        CollectionDockingView.Instance._removedDocs.map(theDoc =>
+        let docs = Cast(CollectionDockingView.TopLevel.props.Document.data, listSpec(Doc));
+        CollectionDockingView.TopLevel._removedDocs.map(theDoc =>
             docs && docs.indexOf(theDoc) !== -1 &&
             docs.splice(docs.indexOf(theDoc), 1));
-        CollectionDockingView.Instance._removedDocs.length = 0;
-        var json = JSON.stringify(this._goldenLayout.toConfig());
+        CollectionDockingView.TopLevel._removedDocs.length = 0;
+        var json = this.retrieveConfiguration();
         this.props.Document.dockingConfig = json;
         if (this.undohack && !this.hack) {
             this.undohack.end();
@@ -315,18 +356,18 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
         this.hack = false;
     }
 
-    itemDropped = () => {
+    private itemDropped = () => {
         this.stateChanged();
     }
 
-    htmlToElement(html: string) {
+    private htmlToElement(html: string) {
         var template = document.createElement('template');
         html = html.trim(); // Never return a text node of whitespace as the result
         template.innerHTML = html;
         return template.content.firstChild;
     }
 
-    tabCreated = async (tab: any) => {
+    private tabCreated = async (tab: any) => {
         if (tab.hasOwnProperty("contentItem") && tab.contentItem.config.type !== "stack") {
             if (tab.contentItem.config.fixed) {
                 tab.contentItem.parent.config.fixed = true;
@@ -346,7 +387,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
                         }
                         tab.setActive(true);
                     }
-                    ReactDOM.render(<ParentDocSelector Document={doc} addDocTab={(doc, location) => CollectionDockingView.Instance.AddTab(stack, doc)} />, upDiv);
+                    ReactDOM.render(<ParentDocSelector Document={doc} addDocTab={(doc, location) => CollectionDockingView.AddTab(stack, doc)} />, upDiv);
                     tab.reactComponents = [upDiv];
                     tab.element.append(upDiv);
                     counter.DashDocId = tab.contentItem.config.props.documentId;
@@ -368,13 +409,13 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
                 let doc = await DocServer.GetRefField(tab.contentItem.config.props.documentId);
                 if (doc instanceof Doc) {
                     let theDoc = doc;
-                    CollectionDockingView.Instance._removedDocs.push(theDoc);
+                    CollectionDockingView.TopLevel._removedDocs.push(theDoc);
                 }
                 tab.contentItem.remove();
             });
     }
 
-    tabDestroyed = (tab: any) => {
+    private tabDestroyed = (tab: any) => {
         if (tab.reactComponents) {
             for (const ele of tab.reactComponents) {
                 ReactDOM.unmountComponentAtNode(ele);
@@ -383,7 +424,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
     }
     _removedDocs: Doc[] = [];
 
-    stackCreated = (stack: any) => {
+    private stackCreated = (stack: any) => {
         //stack.header.controlsContainer.find('.lm_popout').hide();
         stack.header.controlsContainer.find('.lm_close') //get the close icon
             .off('click') //unbind the current click handler
@@ -394,7 +435,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
                     let doc = await DocServer.GetRefField(contentItem.config.props.documentId);
                     if (doc instanceof Doc) {
                         let theDoc = doc;
-                        CollectionDockingView.Instance._removedDocs.push(theDoc);
+                        CollectionDockingView.TopLevel._removedDocs.push(theDoc);
                     }
                 });
                 //}
@@ -410,8 +451,14 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
 
     render() {
         return (
-            <div className="collectiondockingview-container" id="menuContainer"
-                onPointerDown={this.onPointerDown} onPointerUp={this.onPointerUp} ref={this._containerRef} />
+            <Measure onResize={this.onResize} offset>
+                {({ measureRef }) => (
+                    <div ref={measureRef} style={{ width: "100%", height: "100%" }}>
+                        <div className="collectiondockingview-container" id="menuContainer"
+                            onPointerDown={this.onPointerDown} onPointerUp={this.onPointerUp} ref={this._containerRef} />
+                    </div>
+                )}
+            </Measure>
         );
     }
 
@@ -419,16 +466,23 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
 
 interface DockedFrameProps {
     documentId: FieldId;
-    //collectionDockingView: CollectionDockingView
+    glContainer: any;
+    glEventHub: any;
+    parent: CollectionDockingView;
 }
+
 @observer
 export class DockedFrameRenderer extends React.Component<DockedFrameProps> {
     _mainCont = React.createRef<HTMLDivElement>();
     @observable private _panelWidth = 0;
     @observable private _panelHeight = 0;
     @observable private _document: Opt<Doc>;
+    private get parentProps(): SubCollectionViewProps {
+        return this.props.parent.props;
+    }
+
     get _stack(): any {
-        let parent = (this.props as any).glContainer.parent.parent;
+        let parent = this.props.glContainer.parent.parent;
         if (this._document && this._document.excludeFromLibrary && parent.parent && parent.parent.contentItems.length > 1)
             return parent.parent.contentItems[1];
         return parent;
@@ -451,7 +505,7 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> {
         if (this._mainCont.current && this._mainCont.current.children) {
             let { scale, translateX, translateY } = Utils.GetScreenTransform(this._mainCont.current.children[0].firstChild as HTMLElement);
             scale = Utils.GetScreenTransform(this._mainCont.current).scale;
-            return CollectionDockingView.Instance.props.ScreenToLocalTransform().translate(-translateX, -translateY).scale(1 / this.contentScaling() / scale);
+            return this.parentProps.ScreenToLocalTransform().translate(-translateX, -translateY).scale(1 / this.contentScaling() / scale);
         }
         return Transform.Identity();
     }
@@ -469,9 +523,9 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> {
 
     addDocTab = (doc: Doc, location: string) => {
         if (location === "onRight") {
-            CollectionDockingView.Instance.AddRightSplit(doc);
+            CollectionDockingView.AddRightSplit(doc);
         } else {
-            CollectionDockingView.Instance.AddTab(this._stack, doc);
+            CollectionDockingView.AddTab(this._stack, doc);
         }
     }
     get content() {
diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx
index 48da52ffa..b0a310ec1 100644
--- a/src/client/views/collections/CollectionTreeView.tsx
+++ b/src/client/views/collections/CollectionTreeView.tsx
@@ -123,7 +123,7 @@ class TreeView extends React.Component<TreeViewProps> {
                     return true;
                 }}
             />);
-        let dataDocs = CollectionDockingView.Instance ? Cast(CollectionDockingView.Instance.props.Document.data, listSpec(Doc), []) : [];
+        let dataDocs = CollectionDockingView.TopLevel ? Cast(CollectionDockingView.TopLevel.props.Document.data, listSpec(Doc), []) : [];
         let openRight = dataDocs && dataDocs.indexOf(this.props.document) !== -1 ? (null) : (
             <div className="treeViewItem-openRight" onPointerDown={this.onPointerDown} onClick={this.openRight}>
                 <FontAwesomeIcon icon="angle-right" size="lg" />
diff --git a/src/client/views/collections/ParentDocumentSelector.tsx b/src/client/views/collections/ParentDocumentSelector.tsx
index f11af04a3..95183bd2c 100644
--- a/src/client/views/collections/ParentDocumentSelector.tsx
+++ b/src/client/views/collections/ParentDocumentSelector.tsx
@@ -29,8 +29,8 @@ export class SelectorContextMenu extends React.Component<SelectorProps> {
         allDocs.forEach((docs, index) => docs.forEach(doc => map.set(doc, aliases[index])));
         docs.forEach(doc => map.delete(doc));
         runInAction(() => {
-            this._docs = docs.filter(doc => !Doc.AreProtosEqual(doc, CollectionDockingView.Instance.props.Document)).map(doc => ({ col: doc, target: this.props.Document }));
-            this._otherDocs = Array.from(map.entries()).filter(entry => !Doc.AreProtosEqual(entry[0], CollectionDockingView.Instance.props.Document)).map(([col, target]) => ({ col, target }));
+            this._docs = docs.filter(doc => !Doc.AreProtosEqual(doc, CollectionDockingView.TopLevel.props.Document)).map(doc => ({ col: doc, target: this.props.Document }));
+            this._otherDocs = Array.from(map.entries()).filter(entry => !Doc.AreProtosEqual(entry[0], CollectionDockingView.TopLevel.props.Document)).map(([col, target]) => ({ col, target }));
         });
     }
 
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 7750b9173..efba26c2c 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -238,11 +238,11 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
                             expandedProtoDocs.forEach(maxDoc => maxDoc.isMinimized = wasMinimized);
                         }
                     }
-                    if (maxLocation && maxLocation !== "inPlace" && CollectionDockingView.Instance) {
-                        let dataDocs = DocListCast(CollectionDockingView.Instance.props.Document.data);
+                    if (maxLocation && maxLocation !== "inPlace" && CollectionDockingView.TopLevel) {
+                        let dataDocs = DocListCast(CollectionDockingView.TopLevel.props.Document.data);
                         if (dataDocs) {
                             expandedDocs.forEach(maxDoc =>
-                                (!CollectionDockingView.Instance.CloseRightSplit(Doc.GetProto(maxDoc)) &&
+                                (!CollectionDockingView.CloseRightSplit(Doc.GetProto(maxDoc)) &&
                                     this.props.addDocTab(getDispDoc(maxDoc), maxLocation)));
                         }
                     } else {
@@ -270,8 +270,8 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
         this._downX = e.clientX;
         this._downY = e.clientY;
         this._hitExpander = DocListCast(this.props.Document.subBulletDocs).length > 0;
-        if (e.shiftKey && e.buttons === 1 && CollectionDockingView.Instance) {
-            CollectionDockingView.Instance.StartOtherDrag([Doc.MakeAlias(this.props.Document)], e);
+        if (e.shiftKey && e.buttons === 1 && CollectionDockingView.TopLevel) {
+            CollectionDockingView.TopLevel.StartOtherDrag([Doc.MakeAlias(this.props.Document)], e);
             e.stopPropagation();
         } else {
             if (this.active) e.stopPropagation(); // events stop at the lowest document that is active.  
@@ -316,7 +316,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
         }
     }
     fullScreenClicked = (): void => {
-        CollectionDockingView.Instance && CollectionDockingView.Instance.OpenFullScreen(Doc.MakeCopy(this.props.Document, false));
+        CollectionDockingView.TopLevel && CollectionDockingView.OpenFullScreen(Doc.MakeCopy(this.props.Document, false));
         SelectionManager.DeselectAll();
     }
 
-- 
cgit v1.2.3-70-g09d2


From d2c9550f23c4e5654822ac01b973bb965e3f6dec Mon Sep 17 00:00:00 2001
From: Sam Wilkins <samuel_wilkins@brown.edu>
Date: Fri, 14 Jun 2019 20:49:12 -0400
Subject: cleaned up Docs namespace and thoroughly documented
 DocServer.GetRefFields

---
 .vscode/settings.json                              |   3 +-
 src/client/DocServer.ts                            |  80 ++-
 src/client/documents/Documents.ts                  | 758 ++++++++++++---------
 src/client/views/DocumentDecorations.tsx           |   2 +-
 src/client/views/Main.tsx                          |   2 +-
 src/client/views/MainView.tsx                      |  24 +-
 src/client/views/SearchBox.tsx                     |   2 +-
 .../views/collections/CollectionDockingView.tsx    | 386 ++++-------
 .../views/collections/CollectionSchemaView.tsx     |   2 +-
 src/client/views/collections/CollectionSubView.tsx |  16 +-
 .../views/collections/CollectionTreeView.tsx       |   2 +-
 .../views/collections/CollectionVideoView.tsx      |   2 +-
 .../views/collections/DockedFrameRenderer.tsx      | 116 ++++
 .../collections/collectionFreeForm/MarqueeView.tsx |  18 +-
 src/client/views/nodes/DocumentView.tsx            |   4 +-
 src/mobile/ImageUpload.tsx                         |   4 +-
 src/new_fields/Doc.ts                              |  12 +
 src/new_fields/util.ts                             |   1 +
 .../authentication/models/current_user_utils.ts    |   8 +-
 19 files changed, 835 insertions(+), 607 deletions(-)
 create mode 100644 src/client/views/collections/DockedFrameRenderer.tsx

(limited to 'src/client/views/collections/CollectionDockingView.tsx')

diff --git a/.vscode/settings.json b/.vscode/settings.json
index fc315ffaf..5df697fee 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -9,5 +9,6 @@
     "editor.formatOnSave": true,
     "editor.detectIndentation": false,
     "typescript.format.insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets": false,
-    "typescript.format.insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces": true
+    "typescript.format.insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces": true,
+    "search.usePCRE2": true
 }
\ No newline at end of file
diff --git a/src/client/DocServer.ts b/src/client/DocServer.ts
index cbcf751ee..d759b4757 100644
--- a/src/client/DocServer.ts
+++ b/src/client/DocServer.ts
@@ -47,36 +47,81 @@ export namespace DocServer {
         }
     }
 
+    /**
+     * Given a list of Doc GUIDs, this utility function will asynchronously attempt to fetch each document
+     * associated with a given input id, first looking in the RefField cache and then communicating with
+     * the server if the document was not found there.
+     * 
+     * @param ids the ids that map to the reqested documents
+     */
     export async function GetRefFields(ids: string[]): Promise<{ [id: string]: Opt<RefField> }> {
         const requestedIds: string[] = [];
         const waitingIds: string[] = [];
         const promises: Promise<Opt<RefField>>[] = [];
         const map: { [id: string]: Opt<RefField> } = {};
+
+        // 1) An initial pass through the cache to determine which documents need to be fetched,
+        // which are already in the process of being fetched and which already exist in the
+        // cache
         for (const id of ids) {
             const cached = _cache[id];
+
             if (cached === undefined) {
+                // NOT CACHED => we'll have to send a request to the server
                 requestedIds.push(id);
             } else if (cached instanceof Promise) {
+                // BEING CACHED => someone else previously (likely recently) called GetRefFields,
+                // and requested one of the documents I'm looking for. Shouldn't fetch again, just
+                // wait until this promise is resolved (see the second to last line of the function)
                 promises.push(cached);
                 waitingIds.push(id);
             } else {
+                // CACHED => great, let's just add it to the field map
                 map[id] = cached;
             }
         }
-        const prom = Utils.EmitCallback(_socket, MessageStore.GetRefFields, requestedIds).then(fields => {
+
+        // 2) Synchronously, we emit a single callback to the server requesting the documents for the given ids.
+        // This returns a promise, which, when resolved, indicates that all the JSON serialized versions of
+        // the fields have been returned from the server
+        const fieldsReceived: Promise<any> = Utils.EmitCallback(_socket, MessageStore.GetRefFields, requestedIds);
+
+        // 3) When the serialized RefFields have been received, go head and begin deserializing them into objects.
+        // Here, once deserialized, we also invoke .proto to 'load' the documents' prototypes, which ensures that all
+        // future .proto calls won't have to go farther than the cache to get their actual value.
+        const fieldsDeserialized = fieldsReceived.then(async fields => {
             const fieldMap: { [id: string]: RefField } = {};
+            const deserializedFields: any = [];
             for (const field of fields) {
                 if (field !== undefined) {
-                    fieldMap[field.id] = SerializationHelper.Deserialize(field);
+                    // deserialize
+                    let deserialized: any = SerializationHelper.Deserialize(field);
+                    fieldMap[field.id] = deserialized;
+                    deserializedFields.push(deserialized.proto);
                 }
             }
-
+            // this actually handles the loeading of prototypes
+            await Promise.all(deserializedFields);
             return fieldMap;
         });
-        requestedIds.forEach(id => _cache[id] = prom.then(fields => fields[id]));
-        const fields = await prom;
+
+        // 4) Here, for each of the documents we've requested *ourselves* (i.e. weren't promises or found in the cache)
+        // we set the value at the field's id to a promise that will resolve to the field. 
+        // When we find that promises exist at keys in the cache, THIS is where they were set, just by some other caller (method).
+        requestedIds.forEach(id => _cache[id] = fieldsDeserialized.then(fields => fields[id]));
+
+        // 5) At this point, all fields have a) been returned from the server and b) been deserialized into actual Field objects whose
+        // prototype documents, if any, have also been fetched and cached.
+        const fields = await fieldsDeserialized;
+
+        // 6) With this confidence, we can now go through and update the cache at the ids of the fields that
+        // we explicitly had to fetch. To finish it off, we add whatever value we've come up with for a given
+        // id to the soon to be returned field mapping.
         requestedIds.forEach(id => {
             const field = fields[id];
+            // either way, overwrite or delete any promises that we inserted as flags
+            // to indicate that the field was in the process of being fetched. Now everything
+            // should be an actual value within or entirely absent from the cache.
             if (field !== undefined) {
                 _cache[id] = field;
             } else {
@@ -84,14 +129,23 @@ export namespace DocServer {
             }
             map[id] = field;
         });
-        await Promise.all(requestedIds.map(async id => {
-            const field = fields[id];
-            if (field) {
-                await (field as any).proto;
-            }
-        }));
-        const otherFields = await Promise.all(promises);
-        waitingIds.forEach((id, index) => map[id] = otherFields[index]);
+
+        // 7) Those promises we encountered in the else if of 1), which represent
+        // other callers having already submitted a request to the server for (a) document(s)
+        // in which we're interested, must still be awaited so that we can return the proper
+        // values for those as well. 
+        //
+        // Fortunately, those other callers will also hit their own version of 6) and clean up
+        // the shared cache when these promises resolve, so all we have to do is...
+        const otherCallersFetching = await Promise.all(promises);
+        // ...extract the RefFields returned from the resolution of those promises and add them to our
+        // own map.
+        waitingIds.forEach((id, index) => map[id] = otherCallersFetching[index]);
+
+        // Now, we return our completed mapping from all of the ids that were passed into the method
+        // to their actual RefField | undefined values. This return value either becomes the input
+        // argument to the caller's promise (i.e. GetRefFields.then(map => //do something with map...))
+        // or it is the direct return result if the promise is awaited.
         return map;
     }
 
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index b346e1570..b10954636 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -18,7 +18,6 @@ import { action } from "mobx";
 import { ColumnAttributeModel } from "../northstar/core/attribute/AttributeModel";
 import { AttributeTransformationModel } from "../northstar/core/attribute/AttributeTransformationModel";
 import { AggregateFunction } from "../northstar/model/idea/idea";
-import { Template } from "../views/Templates";
 import { MINIMIZED_ICON_SIZE } from "../views/globalCssVariables.scss";
 import { IconBox } from "../views/nodes/IconBox";
 import { Field, Doc, Opt } from "../../new_fields/Doc";
@@ -30,7 +29,7 @@ import { Cast, NumCast } from "../../new_fields/Types";
 import { IconField } from "../../new_fields/IconField";
 import { listSpec } from "../../new_fields/Schema";
 import { DocServer } from "../DocServer";
-import { StrokeData, InkField } from "../../new_fields/InkField";
+import { InkField } from "../../new_fields/InkField";
 import { dropActionType } from "../util/DragManager";
 import { DateField } from "../../new_fields/DateField";
 import { UndoManager } from "../util/UndoManager";
@@ -67,345 +66,486 @@ export interface DocumentOptions {
     dbDoc?: Doc;
     // [key: string]: Opt<Field>;
 }
-const delegateKeys = ["x", "y", "width", "height", "panX", "panY"];
 
-export namespace DocUtils {
-    export function MakeLink(source: Doc, target: Doc) {
-        let protoSrc = source.proto ? source.proto : source;
-        let protoTarg = target.proto ? target.proto : target;
-        UndoManager.RunInBatch(() => {
-            let linkDoc = Docs.TextDocument({ width: 100, height: 30, borderRounding: -1 });
-            //let linkDoc = new Doc;
-            linkDoc.proto!.title = "-link name-";
-            linkDoc.proto!.linkDescription = "";
-            linkDoc.proto!.linkTags = "Default";
+export namespace Docs {
 
-            linkDoc.proto!.linkedTo = target;
-            linkDoc.proto!.linkedToPage = target.curPage;
-            linkDoc.proto!.linkedFrom = source;
-            linkDoc.proto!.linkedFromPage = source.curPage;
+    export namespace Prototypes {
 
-            let linkedFrom = Cast(protoTarg.linkedFromDocs, listSpec(Doc));
-            if (!linkedFrom) {
-                protoTarg.linkedFromDocs = linkedFrom = new List<Doc>();
-            }
-            linkedFrom.push(linkDoc);
+        // the complete list of document prototypes and their ids
+        export let textProto: Doc; const textProtoId = "textProto";
+        export let histoProto: Doc; const histoProtoId = "histoProto";
+        export let imageProto: Doc; const imageProtoId = "imageProto";
+        export let webProto: Doc; const webProtoId = "webProto";
+        export let collProto: Doc; const collProtoId = "collectionProto";
+        export let kvpProto: Doc; const kvpProtoId = "kvpProto";
+        export let videoProto: Doc; const videoProtoId = "videoProto";
+        export let audioProto: Doc; const audioProtoId = "audioProto";
+        export let pdfProto: Doc; const pdfProtoId = "pdfProto";
+        export let iconProto: Doc; const iconProtoId = "iconProto";
 
-            let linkedTo = Cast(protoSrc.linkedToDocs, listSpec(Doc));
-            if (!linkedTo) {
-                protoSrc.linkedToDocs = linkedTo = new List<Doc>();
+        /**
+         * This function loads or initializes the prototype for each docment type.
+         * 
+         * This is an asynchronous function because it has to attempt
+         * to fetch the prototype documents from the server.
+         * 
+         * Once we have this object that maps the prototype ids to a potentially
+         * undefined document, we either initialize our private prototype
+         * variables with the document returned from the server or, if prototypes
+         * haven't been initialized, the newly initialized prototype document.
+         */
+        export async function initialize(): Promise<void> {
+            // non-guid string ids for each document prototype
+            let protoIds = [textProtoId, histoProtoId, collProtoId, imageProtoId, webProtoId, kvpProtoId, videoProtoId, audioProtoId, pdfProtoId, iconProtoId]
+            // fetch the actual prototype documents from the server
+            let actualProtos = await DocServer.GetRefFields(protoIds);
+
+            // initialize prototype documents
+            textProto = actualProtos[textProtoId] as Doc || CreateTextProto();
+            histoProto = actualProtos[histoProtoId] as Doc || CreateHistogramProto();
+            collProto = actualProtos[collProtoId] as Doc || CreateCollectionProto();
+            imageProto = actualProtos[imageProtoId] as Doc || CreateImageProto();
+            webProto = actualProtos[webProtoId] as Doc || CreateWebProto();
+            kvpProto = actualProtos[kvpProtoId] as Doc || CreateKVPProto();
+            videoProto = actualProtos[videoProtoId] as Doc || CreateVideoProto();
+            audioProto = actualProtos[audioProtoId] as Doc || CreateAudioProto();
+            pdfProto = actualProtos[pdfProtoId] as Doc || CreatePdfProto();
+            iconProto = actualProtos[iconProtoId] as Doc || CreateIconProto();
+        }
+
+        /**
+         * This is a convenience method that is used to initialize
+         * prototype documents for the first time.
+         * 
+         * @param protoId the id of the prototype, indicating the specific prototype
+         * to initialize (see the *protoId list at the top of the namespace)
+         * @param title the prototype document's title, follows *-PROTO
+         * @param layout the layout key for this prototype and thus the
+         * layout key that all delegates will inherit
+         * @param options any value specified in the DocumentOptions object likewise
+         * becomes the default value for that key for all delegates
+         */
+        function buildPrototype(protoId: string, title: string, layout: string, options: DocumentOptions): Doc {
+            return Doc.assign(new Doc(protoId, true), { ...options, title: title, layout: layout, baseLayout: layout });
+        }
+
+        // INDIVIDUAL INITIALIZERS
+
+        function CreateImageProto(): Doc {
+            let defaultAttrs = {
+                x: 0,
+                y: 0,
+                nativeWidth: 600,
+                width: 300,
+                backgroundLayout: ImageBox.LayoutString(),
+                curPage: 0
+            };
+            return buildPrototype(imageProtoId, "IMAGE_PROTO", CollectionView.LayoutString("annotations"), defaultAttrs);
+        }
+
+        function CreateHistogramProto(): Doc {
+            let defaultAttrs = {
+                x: 0,
+                y: 0,
+                width: 300,
+                height: 300,
+                backgroundColor: "black",
+                backgroundLayout:
+                    HistogramBox.LayoutString()
+            };
+            return buildPrototype(histoProtoId, "HISTO PROTO", CollectionView.LayoutString("annotations"), defaultAttrs);
+        }
+
+        function CreateIconProto(): Doc {
+            let defaultAttrs = {
+                x: 0,
+                y: 0,
+                width: Number(MINIMIZED_ICON_SIZE),
+                height: Number(MINIMIZED_ICON_SIZE)
+            };
+            return buildPrototype(iconProtoId, "ICON_PROTO", IconBox.LayoutString(), defaultAttrs);
+        }
+
+        function CreateTextProto(): Doc {
+            let defaultAttrs = {
+                x: 0,
+                y: 0,
+                width: 300,
+                height: 150,
+                backgroundColor: "#f1efeb"
+            };
+            return buildPrototype(textProtoId, "TEXT_PROTO", FormattedTextBox.LayoutString(), defaultAttrs);
+        }
+
+        function CreatePdfProto(): Doc {
+            let defaultAttrs = {
+                x: 0,
+                y: 0,
+                nativeWidth: 1200,
+                width: 300,
+                backgroundLayout: PDFBox.LayoutString(),
+                curPage: 1
+            };
+            return buildPrototype(pdfProtoId, "PDF_PROTO", CollectionPDFView.LayoutString("annotations"), defaultAttrs);
+        }
+
+        function CreateWebProto(): Doc {
+            let defaultAttrs = {
+                x: 0,
+                y: 0,
+                width: 300,
+                height: 300
+            };
+            return buildPrototype(webProtoId, "WEB_PROTO", WebBox.LayoutString(), defaultAttrs);
+        }
+
+        function CreateCollectionProto(): Doc {
+            let defaultAttrs = {
+                panX: 0,
+                panY: 0,
+                scale: 1,
+                width: 500,
+                height: 500
+            };
+            return buildPrototype(collProtoId, "COLLECTION_PROTO", CollectionView.LayoutString("data"), defaultAttrs);
+        }
+
+        function CreateKVPProto(): Doc {
+            let defaultAttrs = {
+                x: 0,
+                y: 0,
+                width: 300,
+                height: 150
+            };
+            return buildPrototype(kvpProtoId, "KVP_PROTO", KeyValueBox.LayoutString(), defaultAttrs);
+        }
+
+        function CreateVideoProto(): Doc {
+            let defaultAttrs = {
+                x: 0,
+                y: 0,
+                nativeWidth: 600,
+                width: 300,
+                backgroundLayout: VideoBox.LayoutString(),
+                curPage: 0
+            };
+            return buildPrototype(videoProtoId, "VIDEO_PROTO", CollectionVideoView.LayoutString("annotations"), defaultAttrs);
+        }
+
+        function CreateAudioProto(): Doc {
+            let defaultAttrs = {
+                x: 0,
+                y: 0,
+                width: 300,
+                height: 150
             }
-            linkedTo.push(linkDoc);
-            return linkDoc;
-        }, "make link");
+            return buildPrototype(audioProtoId, "AUDIO_PROTO", AudioBox.LayoutString(), defaultAttrs);
+        }
     }
 
+    /**
+     * Encapsulates the factory used to create new document instances
+     * delegated from top-level prototypes
+     */
+    export namespace Create {
 
-}
+        const delegateKeys = ["x", "y", "width", "height", "panX", "panY"];
 
-export namespace Docs {
-    let textProto: Doc;
-    let histoProto: Doc;
-    let imageProto: Doc;
-    let webProto: Doc;
-    let collProto: Doc;
-    let kvpProto: Doc;
-    let videoProto: Doc;
-    let audioProto: Doc;
-    let pdfProto: Doc;
-    let iconProto: Doc;
-    const textProtoId = "textProto";
-    const histoProtoId = "histoProto";
-    const pdfProtoId = "pdfProto";
-    const imageProtoId = "imageProto";
-    const webProtoId = "webProto";
-    const collProtoId = "collectionProto";
-    const kvpProtoId = "kvpProto";
-    const videoProtoId = "videoProto";
-    const audioProtoId = "audioProto";
-    const iconProtoId = "iconProto";
-
-    export function initProtos(): Promise<void> {
-        return DocServer.GetRefFields([textProtoId, histoProtoId, collProtoId, imageProtoId, webProtoId, kvpProtoId, videoProtoId, audioProtoId, pdfProtoId, iconProtoId]).then(fields => {
-            textProto = fields[textProtoId] as Doc || CreateTextPrototype();
-            histoProto = fields[histoProtoId] as Doc || CreateHistogramPrototype();
-            collProto = fields[collProtoId] as Doc || CreateCollectionPrototype();
-            imageProto = fields[imageProtoId] as Doc || CreateImagePrototype();
-            webProto = fields[webProtoId] as Doc || CreateWebPrototype();
-            kvpProto = fields[kvpProtoId] as Doc || CreateKVPPrototype();
-            videoProto = fields[videoProtoId] as Doc || CreateVideoPrototype();
-            audioProto = fields[audioProtoId] as Doc || CreateAudioPrototype();
-            pdfProto = fields[pdfProtoId] as Doc || CreatePdfPrototype();
-            iconProto = fields[iconProtoId] as Doc || CreateIconPrototype();
-        });
-    }
+        /**
+         * This function receives the relevant document prototype and uses
+         * it to create a new of that base-level prototype, or the
+         * underlying data document, which it then delegates again 
+         * to create the view document.
+         * 
+         * It also takes the opportunity to register the user
+         * that created the document and the time of creation.
+         * 
+         * @param proto the specific document prototype off of which to model
+         * this new instance (textProto, imageProto, etc.)
+         * @param data the Field to store at this new instance's data key
+         * @param options any initial values to provide for this new instance
+         * @param delegId if applicable, an existing document id. If undefined, Doc's
+         * constructor just generates a new GUID. This is currently used
+         * only when creating a DockDocument from the current user's already existing
+         * main document.
+         */
+        function CreateInstanceFromProto(proto: Doc, data: Field, options: DocumentOptions, delegId?: string) {
+            const { omit: protoProps, extract: delegateProps } = OmitKeys(options, delegateKeys);
 
-    function setupPrototypeOptions(protoId: string, title: string, layout: string, options: DocumentOptions): Doc {
-        return Doc.assign(new Doc(protoId, true), { ...options, title: title, layout: layout, baseLayout: layout });
-    }
-    function SetInstanceOptions<U extends Field>(doc: Doc, options: DocumentOptions, value: U) {
-        const deleg = Doc.MakeDelegate(doc);
-        deleg.data = value;
-        return Doc.assign(deleg, options);
-    }
-    function SetDelegateOptions(doc: Doc, options: DocumentOptions, id?: string) {
-        const deleg = Doc.MakeDelegate(doc, id);
-        return Doc.assign(deleg, options);
-    }
+            if (!("author" in protoProps)) {
+                protoProps.author = CurrentUserUtils.email;
+            }
 
-    function CreateImagePrototype(): Doc {
-        let imageProto = setupPrototypeOptions(imageProtoId, "IMAGE_PROTO", CollectionView.LayoutString("annotations"),
-            { x: 0, y: 0, nativeWidth: 600, width: 300, backgroundLayout: ImageBox.LayoutString(), curPage: 0 });
-        return imageProto;
-    }
+            if (!("creationDate" in protoProps)) {
+                protoProps.creationDate = new DateField;
+            }
 
-    function CreateHistogramPrototype(): Doc {
-        let histoProto = setupPrototypeOptions(histoProtoId, "HISTO PROTO", CollectionView.LayoutString("annotations"),
-            { x: 0, y: 0, width: 300, height: 300, backgroundColor: "black", backgroundLayout: HistogramBox.LayoutString() });
-        return histoProto;
-    }
-    function CreateIconPrototype(): Doc {
-        let iconProto = setupPrototypeOptions(iconProtoId, "ICON_PROTO", IconBox.LayoutString(),
-            { x: 0, y: 0, width: Number(MINIMIZED_ICON_SIZE), height: Number(MINIMIZED_ICON_SIZE) });
-        return iconProto;
-    }
-    function CreateTextPrototype(): Doc {
-        let textProto = setupPrototypeOptions(textProtoId, "TEXT_PROTO", FormattedTextBox.LayoutString(),
-            { x: 0, y: 0, width: 300, height: 150, backgroundColor: "#f1efeb" });
-        return textProto;
-    }
-    function CreatePdfPrototype(): Doc {
-        let pdfProto = setupPrototypeOptions(pdfProtoId, "PDF_PROTO", CollectionPDFView.LayoutString("annotations"),
-            { x: 0, y: 0, nativeWidth: 1200, width: 300, backgroundLayout: PDFBox.LayoutString(), curPage: 1 });
-        return pdfProto;
-    }
-    function CreateWebPrototype(): Doc {
-        let webProto = setupPrototypeOptions(webProtoId, "WEB_PROTO", WebBox.LayoutString(),
-            { x: 0, y: 0, width: 300, height: 300 });
-        return webProto;
-    }
-    function CreateCollectionPrototype(): Doc {
-        let collProto = setupPrototypeOptions(collProtoId, "COLLECTION_PROTO", CollectionView.LayoutString("data"),
-            { panX: 0, panY: 0, scale: 1, width: 500, height: 500 });
-        return collProto;
-    }
+            protoProps.isPrototype = true;
 
-    function CreateKVPPrototype(): Doc {
-        let kvpProto = setupPrototypeOptions(kvpProtoId, "KVP_PROTO", KeyValueBox.LayoutString(),
-            { x: 0, y: 0, width: 300, height: 150 });
-        return kvpProto;
-    }
-    function CreateVideoPrototype(): Doc {
-        let videoProto = setupPrototypeOptions(videoProtoId, "VIDEO_PROTO", CollectionVideoView.LayoutString("annotations"),
-            { x: 0, y: 0, nativeWidth: 600, width: 300, backgroundLayout: VideoBox.LayoutString(), curPage: 0 });
-        return videoProto;
-    }
-    function CreateAudioPrototype(): Doc {
-        let audioProto = setupPrototypeOptions(audioProtoId, "AUDIO_PROTO", AudioBox.LayoutString(),
-            { x: 0, y: 0, width: 300, height: 150 });
-        return audioProto;
-    }
+            let dataDoc = MakeDataDelegate(proto, protoProps, data);
+            let viewDoc = Doc.MakeDelegate(dataDoc, delegId);
 
-    function CreateInstance(proto: Doc, data: Field, options: DocumentOptions, delegId?: string) {
-        const { omit: protoProps, extract: delegateProps } = OmitKeys(options, delegateKeys);
-        if (!("author" in protoProps)) {
-            protoProps.author = CurrentUserUtils.email;
+            return Doc.assign(viewDoc, delegateProps);
         }
-        if (!("creationDate" in protoProps)) {
-            protoProps.creationDate = new DateField;
+
+        /**
+         * This function receives the relevant top level document prototype
+         * and models a new instance by delegating from it.
+         * 
+         * Note that it stores the data it recieves at the delegate's data key,
+         * and applies any document options to this new delegate / instance.
+         * @param proto the prototype from which to model this new delegate
+         * @param options initial values to apply to this new delegate
+         * @param value the data to store in this new delegate
+         */
+        function MakeDataDelegate<D extends Field>(proto: Doc, options: DocumentOptions, value: D) {
+            const deleg = Doc.MakeDelegate(proto);
+            deleg.data = value;
+            return Doc.assign(deleg, options);
         }
-        protoProps.isPrototype = true;
 
-        return SetDelegateOptions(SetInstanceOptions(proto, protoProps, data), delegateProps, delegId);
-    }
+        export function ImageDocument(url: string, options: DocumentOptions = {}) {
+            let inst = CreateInstanceFromProto(Prototypes.imageProto, new ImageField(new URL(url)), { title: path.basename(url), ...options });
+            requestImageSize(window.origin + RouteStore.corsProxy + "/" + url)
+                .then((size: any) => {
+                    let aspect = size.height / size.width;
+                    if (!inst.proto!.nativeWidth) {
+                        inst.proto!.nativeWidth = size.width;
+                    }
+                    inst.proto!.nativeHeight = Number(inst.proto!.nativeWidth!) * aspect;
+                    inst.proto!.height = NumCast(inst.proto!.width) * aspect;
+                })
+                .catch((err: any) => console.log(err));
+            return inst;
 
-    export function ImageDocument(url: string, options: DocumentOptions = {}) {
-        let inst = CreateInstance(imageProto, new ImageField(new URL(url)), { title: path.basename(url), ...options });
-        requestImageSize(window.origin + RouteStore.corsProxy + "/" + url)
-            .then((size: any) => {
-                let aspect = size.height / size.width;
-                if (!inst.proto!.nativeWidth) {
-                    inst.proto!.nativeWidth = size.width;
+            // let doc = SetInstanceOptions(GetImagePrototype(), { ...options, layoutKeys: [KeyStore.Data, KeyStore.Annotations, KeyStore.Caption] },
+            //     [new URL(url), ImageField]);
+            // doc.SetText(KeyStore.Caption, "my caption...");
+            // doc.SetText(KeyStore.BackgroundLayout, EmbeddedCaption());
+            // doc.SetText(KeyStore.OverlayLayout, FixedCaption());
+            // return doc;
+        }
+
+        export function VideoDocument(url: string, options: DocumentOptions = {}) {
+            return CreateInstanceFromProto(Prototypes.videoProto, new VideoField(new URL(url)), options);
+        }
+
+        export function AudioDocument(url: string, options: DocumentOptions = {}) {
+            return CreateInstanceFromProto(Prototypes.audioProto, new AudioField(new URL(url)), options);
+        }
+
+        export function HistogramDocument(histoOp: HistogramOperation, options: DocumentOptions = {}) {
+            return CreateInstanceFromProto(Prototypes.histoProto, new HistogramField(histoOp), options);
+        }
+
+        export function TextDocument(options: DocumentOptions = {}) {
+            return CreateInstanceFromProto(Prototypes.textProto, "", options);
+        }
+
+        export function IconDocument(icon: string, options: DocumentOptions = {}) {
+            return CreateInstanceFromProto(Prototypes.iconProto, new IconField(icon), options);
+        }
+
+        export function PdfDocument(url: string, options: DocumentOptions = {}) {
+            return CreateInstanceFromProto(Prototypes.pdfProto, new PdfField(new URL(url)), options);
+        }
+
+        export async function DBDocument(url: string, options: DocumentOptions = {}, columnOptions: DocumentOptions = {}) {
+            let schemaName = options.title ? options.title : "-no schema-";
+            let ctlog = await Gateway.Instance.GetSchema(url, schemaName);
+            if (ctlog && ctlog.schemas) {
+                let schema = ctlog.schemas[0];
+                let schemaDoc = Docs.Create.TreeDocument([], { ...options, nativeWidth: undefined, nativeHeight: undefined, width: 150, height: 100, title: schema.displayName! });
+                let schemaDocuments = Cast(schemaDoc.data, listSpec(Doc), []);
+                if (!schemaDocuments) {
+                    return;
                 }
-                inst.proto!.nativeHeight = Number(inst.proto!.nativeWidth!) * aspect;
-                inst.proto!.height = NumCast(inst.proto!.width) * aspect;
-            })
-            .catch((err: any) => console.log(err));
-        return inst;
-        // let doc = SetInstanceOptions(GetImagePrototype(), { ...options, layoutKeys: [KeyStore.Data, KeyStore.Annotations, KeyStore.Caption] },
-        //     [new URL(url), ImageField]);
-        // doc.SetText(KeyStore.Caption, "my caption...");
-        // doc.SetText(KeyStore.BackgroundLayout, EmbeddedCaption());
-        // doc.SetText(KeyStore.OverlayLayout, FixedCaption());
-        // return doc;
-    }
-    export function VideoDocument(url: string, options: DocumentOptions = {}) {
-        return CreateInstance(videoProto, new VideoField(new URL(url)), options);
-    }
-    export function AudioDocument(url: string, options: DocumentOptions = {}) {
-        return CreateInstance(audioProto, new AudioField(new URL(url)), options);
-    }
+                CurrentUserUtils.AddNorthstarSchema(schema, schemaDoc);
+                const docs = schemaDocuments;
+                CurrentUserUtils.GetAllNorthstarColumnAttributes(schema).map(attr => {
+                    DocServer.GetRefField(attr.displayName! + ".alias").then(action((field: Opt<Field>) => {
+                        if (field instanceof Doc) {
+                            docs.push(field);
+                        } else {
+                            var atmod = new ColumnAttributeModel(attr);
+                            let histoOp = new HistogramOperation(schema.displayName!,
+                                new AttributeTransformationModel(atmod, AggregateFunction.None),
+                                new AttributeTransformationModel(atmod, AggregateFunction.Count),
+                                new AttributeTransformationModel(atmod, AggregateFunction.Count));
+                            docs.push(Docs.Create.HistogramDocument(histoOp, { ...columnOptions, width: 200, height: 200, title: attr.displayName! }));
+                        }
+                    }));
+                });
+                return schemaDoc;
+            }
+            return Docs.Create.TreeDocument([], { width: 50, height: 100, title: schemaName });
+        }
 
-    export function HistogramDocument(histoOp: HistogramOperation, options: DocumentOptions = {}) {
-        return CreateInstance(histoProto, new HistogramField(histoOp), options);
-    }
-    export function TextDocument(options: DocumentOptions = {}) {
-        return CreateInstance(textProto, "", options);
-    }
-    export function IconDocument(icon: string, options: DocumentOptions = {}) {
-        return CreateInstance(iconProto, new IconField(icon), options);
-    }
-    export function PdfDocument(url: string, options: DocumentOptions = {}) {
-        return CreateInstance(pdfProto, new PdfField(new URL(url)), options);
-    }
+        export function WebDocument(url: string, options: DocumentOptions = {}) {
+            return CreateInstanceFromProto(Prototypes.webProto, new WebField(new URL(url)), options);
+        }
+
+        export function HtmlDocument(html: string, options: DocumentOptions = {}) {
+            return CreateInstanceFromProto(Prototypes.webProto, new HtmlField(html), options);
+        }
 
-    export async function DBDocument(url: string, options: DocumentOptions = {}, columnOptions: DocumentOptions = {}) {
-        let schemaName = options.title ? options.title : "-no schema-";
-        let ctlog = await Gateway.Instance.GetSchema(url, schemaName);
-        if (ctlog && ctlog.schemas) {
-            let schema = ctlog.schemas[0];
-            let schemaDoc = Docs.TreeDocument([], { ...options, nativeWidth: undefined, nativeHeight: undefined, width: 150, height: 100, title: schema.displayName! });
-            let schemaDocuments = Cast(schemaDoc.data, listSpec(Doc), []);
-            if (!schemaDocuments) {
-                return;
+        export function KVPDocument(document: Doc, options: DocumentOptions = {}) {
+            return CreateInstanceFromProto(Prototypes.kvpProto, document, { title: document.title + ".kvp", ...options });
+        }
+
+        export function FreeformDocument(documents: Array<Doc>, options: DocumentOptions, makePrototype: boolean = true) {
+            if (!makePrototype) {
+                return MakeDataDelegate(Prototypes.collProto, { ...options, viewType: CollectionViewType.Freeform }, new List(documents));
             }
-            CurrentUserUtils.AddNorthstarSchema(schema, schemaDoc);
-            const docs = schemaDocuments;
-            CurrentUserUtils.GetAllNorthstarColumnAttributes(schema).map(attr => {
-                DocServer.GetRefField(attr.displayName! + ".alias").then(action((field: Opt<Field>) => {
-                    if (field instanceof Doc) {
-                        docs.push(field);
-                    } else {
-                        var atmod = new ColumnAttributeModel(attr);
-                        let histoOp = new HistogramOperation(schema.displayName!,
-                            new AttributeTransformationModel(atmod, AggregateFunction.None),
-                            new AttributeTransformationModel(atmod, AggregateFunction.Count),
-                            new AttributeTransformationModel(atmod, AggregateFunction.Count));
-                        docs.push(Docs.HistogramDocument(histoOp, { ...columnOptions, width: 200, height: 200, title: attr.displayName! }));
+            return CreateInstanceFromProto(Prototypes.collProto, new List(documents), { schemaColumns: new List(["title"]), ...options, viewType: CollectionViewType.Freeform });
+        }
+
+        export function SchemaDocument(schemaColumns: string[], documents: Array<Doc>, options: DocumentOptions) {
+            return CreateInstanceFromProto(Prototypes.collProto, new List(documents), { schemaColumns: new List(schemaColumns), ...options, viewType: CollectionViewType.Schema });
+        }
+
+        export function TreeDocument(documents: Array<Doc>, options: DocumentOptions) {
+            return CreateInstanceFromProto(Prototypes.collProto, new List(documents), { schemaColumns: new List(["title"]), ...options, viewType: CollectionViewType.Tree });
+        }
+
+        export function StackingDocument(documents: Array<Doc>, options: DocumentOptions) {
+            return CreateInstanceFromProto(Prototypes.collProto, new List(documents), { schemaColumns: new List(["title"]), ...options, viewType: CollectionViewType.Stacking });
+        }
+
+        export function DockDocument(documents: Array<Doc>, config: string, options: DocumentOptions, id?: string) {
+            return CreateInstanceFromProto(Prototypes.collProto, new List(documents), { ...options, viewType: CollectionViewType.Docking, dockingConfig: config }, id);
+        }
+
+        export type DocConfig = {
+            doc: Doc,
+            initialWidth?: number
+        }
+
+        export function StandardCollectionDockingDocument(configs: Array<DocConfig>, options: DocumentOptions, id?: string, type: string = "row") {
+            let layoutConfig = {
+                content: [
+                    {
+                        type: type,
+                        content: [
+                            ...configs.map(config => CollectionDockingView.makeDocumentConfig(config.doc, config.initialWidth))
+                        ]
                     }
-                }));
-            });
-            return schemaDoc;
+                ]
+            };
+            return DockDocument(configs.map(c => c.doc), JSON.stringify(layoutConfig), options, id);
         }
-        return Docs.TreeDocument([], { width: 50, height: 100, title: schemaName });
-    }
-    export function WebDocument(url: string, options: DocumentOptions = {}) {
-        return CreateInstance(webProto, new WebField(new URL(url)), options);
-    }
-    export function HtmlDocument(html: string, options: DocumentOptions = {}) {
-        return CreateInstance(webProto, new HtmlField(html), options);
-    }
-    export function KVPDocument(document: Doc, options: DocumentOptions = {}) {
-        return CreateInstance(kvpProto, document, { title: document.title + ".kvp", ...options });
-    }
-    export function FreeformDocument(documents: Array<Doc>, options: DocumentOptions, makePrototype: boolean = true) {
-        if (!makePrototype) {
-            return SetInstanceOptions(collProto, { ...options, viewType: CollectionViewType.Freeform }, new List(documents));
+
+        export function CaptionDocument(doc: Doc) {
+            const captionDoc = Doc.MakeAlias(doc);
+            captionDoc.overlayLayout = Templating.FixedCaption();
+            captionDoc.width = Cast(doc.width, "number", 0);
+            captionDoc.height = Cast(doc.height, "number", 0);
+            return captionDoc;
         }
-        return CreateInstance(collProto, new List(documents), { schemaColumns: new List(["title"]), ...options, viewType: CollectionViewType.Freeform });
-    }
-    export function SchemaDocument(schemaColumns: string[], documents: Array<Doc>, options: DocumentOptions) {
-        return CreateInstance(collProto, new List(documents), { schemaColumns: new List(schemaColumns), ...options, viewType: CollectionViewType.Schema });
-    }
-    export function TreeDocument(documents: Array<Doc>, options: DocumentOptions) {
-        return CreateInstance(collProto, new List(documents), { schemaColumns: new List(["title"]), ...options, viewType: CollectionViewType.Tree });
-    }
-    export function StackingDocument(documents: Array<Doc>, options: DocumentOptions) {
-        return CreateInstance(collProto, new List(documents), { schemaColumns: new List(["title"]), ...options, viewType: CollectionViewType.Stacking });
-    }
-    export function DockDocument(documents: Array<Doc>, config: string, options: DocumentOptions, id?: string) {
-        return CreateInstance(collProto, new List(documents), { ...options, viewType: CollectionViewType.Docking, dockingConfig: config }, id);
-    }
-    export type DocConfig = {
-        doc: Doc,
-        initialWidth?: number
-    }
-    export function StandardCollectionDockingDocument(configs: Array<DocConfig>, options: DocumentOptions, id?: string, type: string = "row") {
-        let layoutConfig = {
-            content: [
-                {
-                    type: type,
-                    content: [
-                        ...configs.map(config => CollectionDockingView.makeDocumentConfig(config.doc, config.initialWidth))
-                    ]
-                }
-            ]
-        };
-        return DockDocument(configs.map(c => c.doc), JSON.stringify(layoutConfig), options, id);
     }
 
-    export function CaptionDocument(doc: Doc) {
-        const captionDoc = Doc.MakeAlias(doc);
-        captionDoc.overlayLayout = FixedCaption();
-        captionDoc.width = Cast(doc.width, "number", 0);
-        captionDoc.height = Cast(doc.height, "number", 0);
-        return captionDoc;
-    }
+    export namespace Templating {
 
-    // example of custom display string for an image that shows a caption.
-    function EmbeddedCaption() {
-        return `<div style="height:100%">
-            <div style="position:relative; margin:auto; height:85%; width:85%;" >`
-            + ImageBox.LayoutString() +
-            `</div>
-            <div style="position:relative; height:15%; text-align:center; ">`
-            + FormattedTextBox.LayoutString("caption") +
-            `</div> 
-        </div>`;
-    }
-    export function FixedCaption(fieldName: string = "caption") {
-        return `<div style="position:absolute; height:30px; bottom:0; width:100%">
-            <div style="position:absolute; width:100%; height:100%; text-align:center;bottom:0;">`
-            + FormattedTextBox.LayoutString(fieldName) +
-            `</div> 
-        </div>`;
-    }
+        // example of custom display string for an image that shows a caption.
+        export function EmbeddedCaption() {
+            return `<div style="height:100%">
+                <div style="position:relative; margin:auto; height:85%; width:85%;" >`
+                + ImageBox.LayoutString() +
+                `</div>
+                <div style="position:relative; height:15%; text-align:center; ">`
+                + FormattedTextBox.LayoutString("caption") +
+                `</div> 
+            </div>`;
+        }
 
-    function OuterCaption() {
-        return (`
-<div>
-    <div style="margin:auto; height:calc(100%); width:100%;">
-        {layout}
-    </div>
-    <div style="height:(100% + 25px); width:100%; position:absolute">
-        <FormattedTextBox doc={Document} DocumentViewForField={DocumentView} bindings={bindings} fieldKey={"caption"} isSelected={isSelected} select={select} selectOnLoad={SelectOnLoad} isTopMost={isTopMost}/>
-    </div>
-</div>       
-        `);
-    }
-    function InnerCaption() {
-        return (`
-    <div>
-        <div style="margin:auto; height:calc(100% - 25px); width:100%;">
-            {layout}
-        </div>
-        <div style="height:25px; width:100%; position:absolute">
-            <FormattedTextBox doc={Document} DocumentViewForField={DocumentView} bindings={bindings} fieldKey={"caption"} isSelected={isSelected} select={select} selectOnLoad={SelectOnLoad} isTopMost={isTopMost}/>
-        </div>
-    </div>       
+        export function FixedCaption(fieldName: string = "caption") {
+            return `<div style="position:absolute; height:30px; bottom:0; width:100%">
+                <div style="position:absolute; width:100%; height:100%; text-align:center;bottom:0;">`
+                + FormattedTextBox.LayoutString(fieldName) +
+                `</div> 
+            </div>`;
+        }
+
+        export function OuterCaption() {
+            return (`
+                <div>
+                    <div style="margin:auto; height:calc(100%); width:100%;">
+                        {layout}
+                    </div>
+                    <div style="height:(100% + 25px); width:100%; position:absolute">
+                        <FormattedTextBox doc={Document} DocumentViewForField={DocumentView} bindings={bindings} fieldKey={"caption"} isSelected={isSelected} select={select} selectOnLoad={SelectOnLoad} isTopMost={isTopMost}/>
+                    </div>
+                </div>       
             `);
-    }
+        }
 
-    /*
+        export function InnerCaption() {
+            return (`
+                <div>
+                    <div style="margin:auto; height:calc(100% - 25px); width:100%;">
+                        {layout}
+                    </div>
+                    <div style="height:25px; width:100%; position:absolute">
+                        <FormattedTextBox doc={Document} DocumentViewForField={DocumentView} bindings={bindings} fieldKey={"caption"} isSelected={isSelected} select={select} selectOnLoad={SelectOnLoad} isTopMost={isTopMost}/>
+                    </div>
+                </div>       
+            `);
+        }
 
-    this template requires an additional style setting on the collectionView-cont to make the layout relative
-    
-.collectionView-cont {
-    position: relative;
-    width: 100%;
-    height: 100%;
-}
-    */
-    function Percentaption() {
-        return (`
-    <div>
-        <div style="margin:auto; height:85%; width:85%;">
-            {layout}
-        </div>
-        <div style="height:15%; width:100%; position:absolute">
-            <FormattedTextBox doc={Document} DocumentViewForField={DocumentView} bindings={bindings} fieldKey={"caption"} isSelected={isSelected} select={select} selectOnLoad={SelectOnLoad} isTopMost={isTopMost}/>
-        </div>
-    </div>       
+        /*
+        this template requires an additional style setting on the collectionView-cont to make the layout relative
+            .collectionView-cont {
+                position: relative;
+                width: 100%;
+                height: 100%;
+            }
+        */
+        export function PercentCaption() {
+            return (`
+                <div>
+                    <div style="margin:auto; height:85%; width:85%;">
+                        {layout}
+                    </div>
+                    <div style="height:15%; width:100%; position:absolute">
+                        <FormattedTextBox doc={Document} DocumentViewForField={DocumentView} bindings={bindings} fieldKey={"caption"} isSelected={isSelected} select={select} selectOnLoad={SelectOnLoad} isTopMost={isTopMost}/>
+                    </div>
+                </div>       
             `);
+        }
+
+    }
+}
+
+export namespace DocUtils {
+
+    export function MakeLink(source: Doc, target: Doc) {
+        let protoSrc = source.proto ? source.proto : source;
+        let protoTarg = target.proto ? target.proto : target;
+        UndoManager.RunInBatch(() => {
+            let linkDoc = Docs.Create.TextDocument({ width: 100, height: 30, borderRounding: -1 });
+            //let linkDoc = new Doc;
+            linkDoc.proto!.title = "-link name-";
+            linkDoc.proto!.linkDescription = "";
+            linkDoc.proto!.linkTags = "Default";
+
+            linkDoc.proto!.linkedTo = target;
+            linkDoc.proto!.linkedToPage = target.curPage;
+            linkDoc.proto!.linkedFrom = source;
+            linkDoc.proto!.linkedFromPage = source.curPage;
+
+            let linkedFrom = Cast(protoTarg.linkedFromDocs, listSpec(Doc));
+            if (!linkedFrom) {
+                protoTarg.linkedFromDocs = linkedFrom = new List<Doc>();
+            }
+            linkedFrom.push(linkDoc);
+
+            let linkedTo = Cast(protoSrc.linkedToDocs, listSpec(Doc));
+            if (!linkedTo) {
+                protoSrc.linkedToDocs = linkedTo = new List<Doc>();
+            }
+            linkedTo.push(linkDoc);
+            return linkDoc;
+        }, "make link");
     }
+
 }
\ No newline at end of file
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index da9b1253e..787033455 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -274,7 +274,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
     @undoBatch
     @action createIcon = (selected: DocumentView[], layoutString: string): Doc => {
         let doc = selected[0].props.Document;
-        let iconDoc = Docs.IconDocument(layoutString);
+        let iconDoc = Docs.Create.IconDocument(layoutString);
         iconDoc.isButton = true;
         iconDoc.proto!.title = selected.length > 1 ? "-multiple-.icon" : StrCast(doc.title) + ".icon";
         iconDoc.labelField = selected.length > 1 ? undefined : this._fieldKey;
diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx
index 3d9750a85..98b14f9c8 100644
--- a/src/client/views/Main.tsx
+++ b/src/client/views/Main.tsx
@@ -5,7 +5,7 @@ import * as ReactDOM from 'react-dom';
 import * as React from 'react';
 
 (async () => {
-    await Docs.initProtos();
+    await Docs.Prototypes.initialize();
     await CurrentUserUtils.loadCurrentUser();
     ReactDOM.render(<MainView />, document.getElementById('root'));
 })();
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 426e2440a..984db0426 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -55,7 +55,7 @@ export class MainView extends React.Component {
     private set mainContainer(doc: Opt<Doc>) {
         if (doc) {
             if (!("presentationView" in doc)) {
-                doc.presentationView = Docs.TreeDocument([], { title: "Presentation" });
+                doc.presentationView = Docs.Create.TreeDocument([], { title: "Presentation" });
             }
             CurrentUserUtils.UserDocument.activeWorkspace = doc;
         }
@@ -151,12 +151,12 @@ export class MainView extends React.Component {
     createNewWorkspace = async (id?: string) => {
         const list = Cast(CurrentUserUtils.UserDocument.data, listSpec(Doc));
         if (list) {
-            let freeformDoc = Docs.FreeformDocument([], { x: 0, y: 400, width: this.pwidth * .7, height: this.pheight, title: `WS collection ${list.length + 1}` });
+            let freeformDoc = Docs.Create.FreeformDocument([], { x: 0, y: 400, width: this.pwidth * .7, height: this.pheight, title: `WS collection ${list.length + 1}` });
             let configs = [
                 { doc: CurrentUserUtils.UserDocument, initialWidth: 150 },
                 { doc: freeformDoc, initialWidth: 600 }
             ]
-            let mainDoc = Docs.StandardCollectionDockingDocument(configs, { title: `Workspace ${list.length + 1}` }, id);
+            let mainDoc = Docs.Create.StandardCollectionDockingDocument(configs, { title: `Workspace ${list.length + 1}` }, id);
             list.push(mainDoc);
             // bcz: strangely, we need a timeout to prevent exceptions/issues initializing GoldenLayout (the rendering engine for Main Container)
             setTimeout(() => {
@@ -242,18 +242,18 @@ export class MainView extends React.Component {
         let audiourl = "http://techslides.com/demos/samples/sample.mp3";
         let videourl = "http://techslides.com/demos/sample-videos/small.mp4";
 
-        let addTextNode = action(() => Docs.TextDocument({ borderRounding: -1, width: 200, height: 200, title: "a text note" }));
-        let addColNode = action(() => Docs.FreeformDocument([], { width: this.pwidth * .7, height: this.pheight, title: "a freeform collection" }));
-        let addDockingNode = action(() => Docs.StandardCollectionDockingDocument([{ doc: addColNode(), initialWidth: 200 }], { width: 200, height: 200, title: "a nested docking freeform collection" }));
-        let addSchemaNode = action(() => Docs.SchemaDocument(["title"], [], { width: 200, height: 200, title: "a schema collection" }));
+        let addTextNode = action(() => Docs.Create.TextDocument({ borderRounding: -1, width: 200, height: 200, title: "a text note" }));
+        let addColNode = action(() => Docs.Create.FreeformDocument([], { width: this.pwidth * .7, height: this.pheight, title: "a freeform collection" }));
+        let addDockingNode = action(() => Docs.Create.StandardCollectionDockingDocument([{ doc: addColNode(), initialWidth: 200 }], { width: 200, height: 200, title: "a nested docking freeform collection" }));
+        let addSchemaNode = action(() => Docs.Create.SchemaDocument(["title"], [], { width: 200, height: 200, title: "a schema collection" }));
         let addTreeNode = action(() => CurrentUserUtils.UserDocument);
         //let addTreeNode = action(() => Docs.TreeDocument([CurrentUserUtils.UserDocument], { width: 250, height: 400, title: "Library:" + CurrentUserUtils.email, dropAction: "alias" }));
         // let addTreeNode = action(() => Docs.TreeDocument(this._northstarSchemas, { width: 250, height: 400, title: "northstar schemas", dropAction: "copy"  }));
-        let addVideoNode = action(() => Docs.VideoDocument(videourl, { width: 200, title: "video node" }));
-        let addPDFNode = action(() => Docs.PdfDocument(pdfurl, { width: 200, height: 200, title: "a pdf doc" }));
-        let addImageNode = action(() => Docs.ImageDocument(imgurl, { width: 200, title: "an image of a cat" }));
-        let addWebNode = action(() => Docs.WebDocument(weburl, { width: 200, height: 200, title: "a sample web page" }));
-        let addAudioNode = action(() => Docs.AudioDocument(audiourl, { width: 200, height: 200, title: "audio node" }));
+        let addVideoNode = action(() => Docs.Create.VideoDocument(videourl, { width: 200, title: "video node" }));
+        let addPDFNode = action(() => Docs.Create.PdfDocument(pdfurl, { width: 200, height: 200, title: "a pdf doc" }));
+        let addImageNode = action(() => Docs.Create.ImageDocument(imgurl, { width: 200, title: "an image of a cat" }));
+        let addWebNode = action(() => Docs.Create.WebDocument(weburl, { width: 200, height: 200, title: "a sample web page" }));
+        let addAudioNode = action(() => Docs.Create.AudioDocument(audiourl, { width: 200, height: 200, title: "audio node" }));
 
         let btns: [React.RefObject<HTMLDivElement>, IconName, string, () => Doc][] = [
             [React.createRef<HTMLDivElement>(), "font", "Add Textbox", addTextNode],
diff --git a/src/client/views/SearchBox.tsx b/src/client/views/SearchBox.tsx
index 63d2065e2..7164d98a4 100644
--- a/src/client/views/SearchBox.tsx
+++ b/src/client/views/SearchBox.tsx
@@ -166,7 +166,7 @@ export class SearchBox extends React.Component {
                 y += 300;
             }
         }
-        return Docs.FreeformDocument(docs, { width: 400, height: 400, panX: 175, panY: 175, backgroundColor: "grey", title: `Search Docs: "${this.searchString}"` });
+        return Docs.Create.FreeformDocument(docs, { width: 400, height: 400, panX: 175, panY: 175, backgroundColor: "grey", title: `Search Docs: "${this.searchString}"` });
     }
 
     // Useful queries:
diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx
index dfb8fac35..e2bcb10ec 100644
--- a/src/client/views/collections/CollectionDockingView.tsx
+++ b/src/client/views/collections/CollectionDockingView.tsx
@@ -6,50 +6,32 @@ import * as ReactDOM from 'react-dom';
 import Measure, { ContentRect } from "react-measure";
 import * as GoldenLayout from "../../../client/goldenLayout";
 import { Doc, Field, Opt, DocListCast } from "../../../new_fields/Doc";
-import { FieldId } from "../../../new_fields/RefField";
 import { listSpec } from "../../../new_fields/Schema";
 import { Cast, NumCast, StrCast } from "../../../new_fields/Types";
 import { emptyFunction, returnTrue, Utils } from "../../../Utils";
 import { DocServer } from "../../DocServer";
 import { DragLinksAsDocuments, DragManager } from "../../util/DragManager";
-import { Transform } from '../../util/Transform';
 import { undoBatch, UndoManager } from "../../util/UndoManager";
-import { DocumentView } from "../nodes/DocumentView";
 import "./CollectionDockingView.scss";
 import { SubCollectionViewProps } from "./CollectionSubView";
 import React = require("react");
 import { ParentDocSelector } from './ParentDocumentSelector';
 import { DocumentManager } from '../../util/DocumentManager';
-import { CollectionViewType } from './CollectionBaseView';
 import { Id } from '../../../new_fields/FieldSymbols';
-import { CurrentUserUtils } from '../../../server/authentication/models/current_user_utils';
+import { DockedFrameRenderer } from './DockedFrameRenderer';
 
 @observer
 export class CollectionDockingView extends React.Component<SubCollectionViewProps> {
     public static TopLevel: CollectionDockingView;
-    public static makeDocumentConfig(document: Doc, width?: number) {
-        return {
-            type: 'react-component',
-            component: 'DocumentFrameRenderer',
-            title: document.title,
-            width: width,
-            props: {
-                documentId: document[Id],
-            }
-        };
-    }
-
-    private makeDocConfig = (document: Doc, width?: number) => {
-        const config = CollectionDockingView.makeDocumentConfig(document, width);
-        (config.props as any).parent = this;
-        return config;
-    }
-
     private _goldenLayout: any = null;
     private _containerRef = React.createRef<HTMLDivElement>();
+    reactionDisposer?: IReactionDisposer;
+    _removedDocs: Doc[] = [];
     private _flush: boolean = false;
     private _ignoreStateChange = "";
     private _isPointerDown = false;
+    hack: boolean = false;
+    undohack: any = null;
 
     constructor(props: SubCollectionViewProps) {
         super(props);
@@ -57,32 +39,93 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
         (window as any).React = React;
         (window as any).ReactDOM = ReactDOM;
     }
-    hack: boolean = false;
-    undohack: any = null;
-    public StartOtherDrag(dragDocs: Doc[], e: any) {
-        this.hack = true;
-        this.undohack = UndoManager.StartBatch("goldenDrag");
-        dragDocs.map(dragDoc =>
-            CollectionDockingView.AddRightSplit(dragDoc, true).contentItems[0].tab._dragListener.
-                onMouseDown({ pageX: e.pageX, pageY: e.pageY, preventDefault: emptyFunction, button: 0 }));
+
+    componentDidMount: () => void = () => {
+        if (this._containerRef.current) {
+            this.reactionDisposer = reaction(
+                () => StrCast(this.props.Document.dockingConfig),
+                () => {
+                    if (!this._goldenLayout || this._ignoreStateChange !== this.retrieveConfiguration()) {
+                        // Because this is in a set timeout, if this component unmounts right after mounting,
+                        // we will leak a GoldenLayout, because we try to destroy it before we ever create it
+                        setTimeout(() => this.setupGoldenLayout(), 1);
+                    }
+                    this._ignoreStateChange = "";
+                }, { fireImmediately: true });
+
+            // window.addEventListener('resize', this.onResize); // bcz: would rather add this event to the parent node, but resize events only come from Window
+        }
     }
 
-    private openFullScreen = (document: Doc) => {
-        let newItemStackConfig = {
-            type: 'stack',
-            content: [this.makeDocConfig(document)]
-        };
-        var docconfig = this._goldenLayout.root.layoutManager.createContentItem(newItemStackConfig, this._goldenLayout);
-        this._goldenLayout.root.contentItems[0].addChild(docconfig);
-        docconfig.callDownwards('_$init');
-        this._goldenLayout._$maximiseItem(docconfig);
-        this._ignoreStateChange = this.retrieveConfiguration();
-        this.stateChanged();
+    componentWillUnmount: () => void = () => {
+        try {
+            this._goldenLayout.unbind('itemDropped', this.itemDropped);
+            this._goldenLayout.unbind('tabCreated', this.tabCreated);
+            this._goldenLayout.unbind('stackCreated', this.stackCreated);
+            this._goldenLayout.unbind('tabDestroyed', this.tabDestroyed);
+        } catch (e) {
+            console.log("Unable to unbind Golden Layout event listener...", e);
+        }
+        if (this._goldenLayout) this._goldenLayout.destroy();
+        this._goldenLayout = null;
+
+        if (this.reactionDisposer) {
+            this.reactionDisposer();
+        }
     }
 
-    @action
-    public static OpenFullScreen(document: Doc, dockingView: CollectionDockingView = CollectionDockingView.TopLevel) {
-        dockingView.openFullScreen(document);
+    setupGoldenLayout() {
+        var config = StrCast(this.props.Document.dockingConfig);
+        if (config) {
+            if (!this._goldenLayout) {
+                this.initializeConfiguration(config);
+            }
+            else {
+                if (config === this.retrieveConfiguration()) {
+                    return;
+                }
+                try {
+                    this._goldenLayout.unbind('itemDropped', this.itemDropped);
+                    this._goldenLayout.unbind('tabCreated', this.tabCreated);
+                    this._goldenLayout.unbind('tabDestroyed', this.tabDestroyed);
+                    this._goldenLayout.unbind('stackCreated', this.stackCreated);
+                } catch (e) { }
+                this._goldenLayout.destroy();
+                this.initializeConfiguration(config);
+            }
+            this._goldenLayout.on('itemDropped', this.itemDropped);
+            this._goldenLayout.on('tabCreated', this.tabCreated);
+            this._goldenLayout.on('tabDestroyed', this.tabDestroyed);
+            this._goldenLayout.on('stackCreated', this.stackCreated);
+            this._goldenLayout.registerComponent('DocumentFrameRenderer', DockedFrameRenderer);
+            this._goldenLayout.container = this._containerRef.current;
+            if (this._goldenLayout.config.maximisedItemId === '__glMaximised') {
+                try {
+                    this._goldenLayout.config.root.getItemsById(this._goldenLayout.config.maximisedItemId)[0].toggleMaximise();
+                } catch (e) {
+                    this._goldenLayout.config.maximisedItemId = null;
+                }
+            }
+            this._goldenLayout.init();
+        }
+    }
+
+    private makeDocConfig = (document: Doc, width?: number) => {
+        const config = CollectionDockingView.makeDocumentConfig(document, width);
+        (config.props as any).parent = this;
+        return config;
+    }
+
+    public static makeDocumentConfig(document: Doc, width?: number) {
+        return {
+            type: 'react-component',
+            component: 'DocumentFrameRenderer',
+            title: document.title,
+            width: width,
+            props: {
+                documentId: document[Id],
+            }
+        };
     }
 
     initializeConfiguration = (configText: string) => {
@@ -109,44 +152,29 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
         });
     }
 
-    @undoBatch
-    @action
-    public static CloseRightSplit = (document: Doc, dockingView: CollectionDockingView = CollectionDockingView.TopLevel): boolean => {
-        let retVal = false;
-        if (dockingView._goldenLayout.root.contentItems[0].isRow) {
-            retVal = Array.from(dockingView._goldenLayout.root.contentItems[0].contentItems).some((child: any) => {
-                if (child.contentItems.length === 1 && child.contentItems[0].config.component === "DocumentFrameRenderer" &&
-                    Doc.AreProtosEqual(DocumentManager.Instance.getDocumentViewById(child.contentItems[0].config.props.documentId)!.Document, document)) {
-                    child.contentItems[0].remove();
-                    dockingView.layoutChanged(document);
-                    return true;
-                } else {
-                    Array.from(child.contentItems).filter((tab: any) => tab.config.component === "DocumentFrameRenderer").some((tab: any, j: number) => {
-                        if (Doc.AreProtosEqual(DocumentManager.Instance.getDocumentViewById(tab.config.props.documentId)!.Document, document)) {
-                            child.contentItems[j].remove();
-                            child.config.activeItemIndex = Math.max(child.contentItems.length - 1, 0);
-                            let docs = Cast(dockingView.props.Document.data, listSpec(Doc));
-                            docs && docs.indexOf(document) !== -1 && docs.splice(docs.indexOf(document), 1);
-                            return true;
-                        }
-                        return false;
-                    });
-                }
-                return false;
-            });
-        }
-        if (retVal) {
-            dockingView.stateChanged();
-        }
-        return retVal;
+    public StartOtherDrag(dragDocs: Doc[], e: any) {
+        this.hack = true;
+        this.undohack = UndoManager.StartBatch("goldenDrag");
+        dragDocs.map(dragDoc =>
+            CollectionDockingView.AddRightSplit(dragDoc, true).contentItems[0].tab._dragListener.
+                onMouseDown({ pageX: e.pageX, pageY: e.pageY, preventDefault: emptyFunction, button: 0 }));
     }
 
     @action
-    layoutChanged(removed?: Doc) {
-        this._goldenLayout.root.callDownwards('setSize', [this._goldenLayout.width, this._goldenLayout.height]);
-        this._goldenLayout.emit('stateChanged');
+    public static OpenFullScreen(document: Doc, dockingView: CollectionDockingView = CollectionDockingView.TopLevel) {
+        dockingView.openFullScreen(document);
+    }
+
+    private openFullScreen = (document: Doc) => {
+        let newItemStackConfig = {
+            type: 'stack',
+            content: [this.makeDocConfig(document)]
+        };
+        var docconfig = this._goldenLayout.root.layoutManager.createContentItem(newItemStackConfig, this._goldenLayout);
+        this._goldenLayout.root.contentItems[0].addChild(docconfig);
+        docconfig.callDownwards('_$init');
+        this._goldenLayout._$maximiseItem(docconfig);
         this._ignoreStateChange = this.retrieveConfiguration();
-        if (removed) CollectionDockingView.TopLevel._removedDocs.push(removed);
         this.stateChanged();
     }
 
@@ -209,75 +237,47 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
         this.layoutChanged();
     }
 
-    setupGoldenLayout() {
-        var config = StrCast(this.props.Document.dockingConfig);
-        if (config) {
-            if (!this._goldenLayout) {
-                this.initializeConfiguration(config);
-            }
-            else {
-                if (config === this.retrieveConfiguration()) {
-                    return;
-                }
-                try {
-                    this._goldenLayout.unbind('itemDropped', this.itemDropped);
-                    this._goldenLayout.unbind('tabCreated', this.tabCreated);
-                    this._goldenLayout.unbind('tabDestroyed', this.tabDestroyed);
-                    this._goldenLayout.unbind('stackCreated', this.stackCreated);
-                } catch (e) { }
-                this._goldenLayout.destroy();
-                this.initializeConfiguration(config);
-            }
-            this._goldenLayout.on('itemDropped', this.itemDropped);
-            this._goldenLayout.on('tabCreated', this.tabCreated);
-            this._goldenLayout.on('tabDestroyed', this.tabDestroyed);
-            this._goldenLayout.on('stackCreated', this.stackCreated);
-            this._goldenLayout.registerComponent('DocumentFrameRenderer', DockedFrameRenderer);
-            this._goldenLayout.container = this._containerRef.current;
-            if (this._goldenLayout.config.maximisedItemId === '__glMaximised') {
-                try {
-                    this._goldenLayout.config.root.getItemsById(this._goldenLayout.config.maximisedItemId)[0].toggleMaximise();
-                } catch (e) {
-                    this._goldenLayout.config.maximisedItemId = null;
+    @undoBatch
+    @action
+    public static CloseRightSplit = (document: Doc, dockingView: CollectionDockingView = CollectionDockingView.TopLevel): boolean => {
+        let retVal = false;
+        if (dockingView._goldenLayout.root.contentItems[0].isRow) {
+            retVal = Array.from(dockingView._goldenLayout.root.contentItems[0].contentItems).some((child: any) => {
+                if (child.contentItems.length === 1 && child.contentItems[0].config.component === "DocumentFrameRenderer" &&
+                    Doc.AreProtosEqual(DocumentManager.Instance.getDocumentViewById(child.contentItems[0].config.props.documentId)!.Document, document)) {
+                    child.contentItems[0].remove();
+                    dockingView.layoutChanged(document);
+                    return true;
+                } else {
+                    Array.from(child.contentItems).filter((tab: any) => tab.config.component === "DocumentFrameRenderer").some((tab: any, j: number) => {
+                        if (Doc.AreProtosEqual(DocumentManager.Instance.getDocumentViewById(tab.config.props.documentId)!.Document, document)) {
+                            child.contentItems[j].remove();
+                            child.config.activeItemIndex = Math.max(child.contentItems.length - 1, 0);
+                            let docs = Cast(dockingView.props.Document.data, listSpec(Doc));
+                            docs && docs.indexOf(document) !== -1 && docs.splice(docs.indexOf(document), 1);
+                            return true;
+                        }
+                        return false;
+                    });
                 }
-            }
-            this._goldenLayout.init();
+                return false;
+            });
         }
-    }
-    reactionDisposer?: IReactionDisposer;
-    componentDidMount: () => void = () => {
-        if (this._containerRef.current) {
-            this.reactionDisposer = reaction(
-                () => StrCast(this.props.Document.dockingConfig),
-                () => {
-                    if (!this._goldenLayout || this._ignoreStateChange !== this.retrieveConfiguration()) {
-                        // Because this is in a set timeout, if this component unmounts right after mounting,
-                        // we will leak a GoldenLayout, because we try to destroy it before we ever create it
-                        setTimeout(() => this.setupGoldenLayout(), 1);
-                    }
-                    this._ignoreStateChange = "";
-                }, { fireImmediately: true });
-
-            // window.addEventListener('resize', this.onResize); // bcz: would rather add this event to the parent node, but resize events only come from Window
+        if (retVal) {
+            dockingView.stateChanged();
         }
+        return retVal;
     }
-    componentWillUnmount: () => void = () => {
-        try {
-            this._goldenLayout.unbind('itemDropped', this.itemDropped);
-            this._goldenLayout.unbind('tabCreated', this.tabCreated);
-            this._goldenLayout.unbind('stackCreated', this.stackCreated);
-            this._goldenLayout.unbind('tabDestroyed', this.tabDestroyed);
-        } catch (e) {
 
-        }
-        if (this._goldenLayout) this._goldenLayout.destroy();
-        this._goldenLayout = null;
-        // window.removeEventListener('resize', this.onResize);
-
-        if (this.reactionDisposer) {
-            this.reactionDisposer();
-        }
+    @action
+    layoutChanged(removed?: Doc) {
+        this._goldenLayout.root.callDownwards('setSize', [this._goldenLayout.width, this._goldenLayout.height]);
+        this._goldenLayout.emit('stateChanged');
+        this._ignoreStateChange = this.retrieveConfiguration();
+        if (removed) CollectionDockingView.TopLevel._removedDocs.push(removed);
+        this.stateChanged();
     }
+
     @action
     onResize = (size: ContentRect) => {
         // bcz: since GoldenLayout isn't a React component itself, we need to notify it to resize when its document container's size has changed
@@ -387,6 +387,9 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
                         }
                         tab.setActive(true);
                     }
+                    tab.header.element[0].ondrop = (e: any) => {
+                        console.log("DROPPPP THE BASS!", e);
+                    }
                     ReactDOM.render(<ParentDocSelector Document={doc} addDocTab={(doc, location) => CollectionDockingView.AddTab(stack, doc)} />, upDiv);
                     tab.reactComponents = [upDiv];
                     tab.element.append(upDiv);
@@ -422,7 +425,6 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
             }
         }
     }
-    _removedDocs: Doc[] = [];
 
     private stackCreated = (stack: any) => {
         //stack.header.controlsContainer.find('.lm_popout').hide();
@@ -462,102 +464,4 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
         );
     }
 
-}
-
-interface DockedFrameProps {
-    documentId: FieldId;
-    glContainer: any;
-    glEventHub: any;
-    parent: CollectionDockingView;
-}
-
-@observer
-export class DockedFrameRenderer extends React.Component<DockedFrameProps> {
-    _mainCont = React.createRef<HTMLDivElement>();
-    @observable private _panelWidth = 0;
-    @observable private _panelHeight = 0;
-    @observable private _document: Opt<Doc>;
-    private get parentProps(): SubCollectionViewProps {
-        return this.props.parent.props;
-    }
-
-    get _stack(): any {
-        let parent = this.props.glContainer.parent.parent;
-        if (this._document && this._document.excludeFromLibrary && parent.parent && parent.parent.contentItems.length > 1)
-            return parent.parent.contentItems[1];
-        return parent;
-    }
-    constructor(props: any) {
-        super(props);
-        DocServer.GetRefField(this.props.documentId).then(action((f: Opt<Field>) => this._document = f as Doc));
-    }
-
-    nativeWidth = () => NumCast(this._document!.nativeWidth, this._panelWidth);
-    nativeHeight = () => NumCast(this._document!.nativeHeight, this._panelHeight);
-    contentScaling = () => {
-        const nativeH = this.nativeHeight();
-        const nativeW = this.nativeWidth();
-        let wscale = this._panelWidth / nativeW;
-        return wscale * nativeH > this._panelHeight ? this._panelHeight / nativeH : wscale;
-    }
-
-    ScreenToLocalTransform = () => {
-        if (this._mainCont.current && this._mainCont.current.children) {
-            let { scale, translateX, translateY } = Utils.GetScreenTransform(this._mainCont.current.children[0].firstChild as HTMLElement);
-            scale = Utils.GetScreenTransform(this._mainCont.current).scale;
-            return this.parentProps.ScreenToLocalTransform().translate(-translateX, -translateY).scale(1 / this.contentScaling() / scale);
-        }
-        return Transform.Identity();
-    }
-    get scaleToFitMultiplier() {
-        let docWidth = NumCast(this._document!.width);
-        let docHeight = NumCast(this._document!.height);
-        if (NumCast(this._document!.nativeWidth) || !docWidth || !this._panelWidth || !this._panelHeight) return 1;
-        if (StrCast(this._document!.layout).indexOf("Collection") === -1 ||
-            NumCast(this._document!.viewType) !== CollectionViewType.Freeform) return 1;
-        let scaling = Math.max(1, this._panelWidth / docWidth * docHeight > this._panelHeight ?
-            this._panelHeight / docHeight : this._panelWidth / docWidth);
-        return scaling;
-    }
-    get previewPanelCenteringOffset() { return (this._panelWidth - this.nativeWidth() * this.contentScaling()) / 2; }
-
-    addDocTab = (doc: Doc, location: string) => {
-        if (location === "onRight") {
-            CollectionDockingView.AddRightSplit(doc);
-        } else {
-            CollectionDockingView.AddTab(this._stack, doc);
-        }
-    }
-    get content() {
-        if (!this._document) {
-            return (null);
-        }
-        return (
-            <div className="collectionDockingView-content" ref={this._mainCont}
-                style={{ transform: `translate(${this.previewPanelCenteringOffset}px, 0px) scale(${this.scaleToFitMultiplier}, ${this.scaleToFitMultiplier})` }}>
-                <DocumentView key={this._document[Id]} Document={this._document}
-                    bringToFront={emptyFunction}
-                    addDocument={undefined}
-                    removeDocument={undefined}
-                    ContentScaling={this.contentScaling}
-                    PanelWidth={this.nativeWidth}
-                    PanelHeight={this.nativeHeight}
-                    ScreenToLocalTransform={this.ScreenToLocalTransform}
-                    isTopMost={true}
-                    selectOnLoad={false}
-                    parentActive={returnTrue}
-                    whenActiveChanged={emptyFunction}
-                    focus={emptyFunction}
-                    addDocTab={this.addDocTab}
-                    ContainingCollectionView={undefined} />
-            </div >);
-    }
-
-    render() {
-        let theContent = this.content;
-        return !this._document ? (null) :
-            <Measure offset onResize={action((r: any) => { this._panelWidth = r.offset.width; this._panelHeight = r.offset.height; })}>
-                {({ measureRef }) => <div ref={measureRef}>  {theContent} </div>}
-            </Measure>;
-    }
 }
\ No newline at end of file
diff --git a/src/client/views/collections/CollectionSchemaView.tsx b/src/client/views/collections/CollectionSchemaView.tsx
index 11d71d023..477879b79 100644
--- a/src/client/views/collections/CollectionSchemaView.tsx
+++ b/src/client/views/collections/CollectionSchemaView.tsx
@@ -261,7 +261,7 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) {
         let dbName = StrCast(this.props.Document.title);
         let res = await Gateway.Instance.PostSchema(csv, dbName);
         if (self.props.CollectionView.props.addDocument) {
-            let schemaDoc = await Docs.DBDocument("https://www.cs.brown.edu/" + dbName, { title: dbName }, { dbDoc: self.props.Document });
+            let schemaDoc = await Docs.Create.DBDocument("https://www.cs.brown.edu/" + dbName, { title: dbName }, { dbDoc: self.props.Document });
             if (schemaDoc) {
                 //self.props.CollectionView.props.addDocument(schemaDoc, false);
                 self.props.Document.schemaDoc = schemaDoc;
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index be37efd3d..440a2410b 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -113,20 +113,20 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
         protected async getDocumentFromType(type: string, path: string, options: DocumentOptions): Promise<Opt<Doc>> {
             let ctor: ((path: string, options: DocumentOptions) => (Doc | Promise<Doc | undefined>)) | undefined = undefined;
             if (type.indexOf("image") !== -1) {
-                ctor = Docs.ImageDocument;
+                ctor = Docs.Create.ImageDocument;
             }
             if (type.indexOf("video") !== -1) {
-                ctor = Docs.VideoDocument;
+                ctor = Docs.Create.VideoDocument;
             }
             if (type.indexOf("audio") !== -1) {
-                ctor = Docs.AudioDocument;
+                ctor = Docs.Create.AudioDocument;
             }
             if (type.indexOf("pdf") !== -1) {
-                ctor = Docs.PdfDocument;
+                ctor = Docs.Create.PdfDocument;
                 options.nativeWidth = 1200;
             }
             if (type.indexOf("excel") !== -1) {
-                ctor = Docs.DBDocument;
+                ctor = Docs.Create.DBDocument;
                 options.dropAction = "copy";
             }
             if (type.indexOf("html") !== -1) {
@@ -145,7 +145,7 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
                     });
                     return undefined;
                 }
-                ctor = Docs.WebDocument;
+                ctor = Docs.Create.WebDocument;
                 options = { height: options.width, ...options, title: path, nativeWidth: undefined };
             }
             return ctor ? ctor(path, options) : undefined;
@@ -175,13 +175,13 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
                 return;
             }
             if (html && html.indexOf("<img") !== 0 && !html.startsWith("<a")) {
-                let htmlDoc = Docs.HtmlDocument(html, { ...options, width: 300, height: 300, documentText: text });
+                let htmlDoc = Docs.Create.HtmlDocument(html, { ...options, width: 300, height: 300, documentText: text });
                 this.props.addDocument(htmlDoc, false);
                 return;
             }
             if (text && text.indexOf("www.youtube.com/watch") !== -1) {
                 const url = text.replace("youtube.com/watch?v=", "youtube.com/embed/");
-                this.props.addDocument(Docs.WebDocument(url, { ...options, width: 300, height: 300 }));
+                this.props.addDocument(Docs.Create.WebDocument(url, { ...options, width: 300, height: 300 }));
                 return;
             }
 
diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx
index b0a310ec1..424042fee 100644
--- a/src/client/views/collections/CollectionTreeView.tsx
+++ b/src/client/views/collections/CollectionTreeView.tsx
@@ -142,7 +142,7 @@ class TreeView extends React.Component<TreeViewProps> {
     onWorkspaceContextMenu = (e: React.MouseEvent): void => {
         if (!e.isPropagationStopped()) { // need to test this because GoldenLayout causes a parallel hierarchy in the React DOM for its children and the main document view7
             ContextMenu.Instance.addItem({ description: "Open as Workspace", event: undoBatch(() => MainView.Instance.openWorkspace(this.props.document)) });
-            ContextMenu.Instance.addItem({ description: "Open Fields", event: () => this.props.addDocTab(Docs.KVPDocument(this.props.document, { width: 300, height: 300 }), "onRight"), icon: "layer-group" });
+            ContextMenu.Instance.addItem({ description: "Open Fields", event: () => this.props.addDocTab(Docs.Create.KVPDocument(this.props.document, { width: 300, height: 300 }), "onRight"), icon: "layer-group" });
             if (NumCast(this.props.document.viewType) !== CollectionViewType.Docking) {
                 ContextMenu.Instance.addItem({ description: "Open Tab", event: () => this.props.addDocTab(this.props.document, "inTab"), icon: "folder" });
                 ContextMenu.Instance.addItem({ description: "Open Right", event: () => this.props.addDocTab(this.props.document, "onRight"), icon: "caret-square-right" });
diff --git a/src/client/views/collections/CollectionVideoView.tsx b/src/client/views/collections/CollectionVideoView.tsx
index 7853544d5..bd5cd5450 100644
--- a/src/client/views/collections/CollectionVideoView.tsx
+++ b/src/client/views/collections/CollectionVideoView.tsx
@@ -98,7 +98,7 @@ export class CollectionVideoView extends React.Component<FieldViewProps> {
                 SearchBox.convertDataUri(dataUrl, filename).then((returnedFilename) => {
                     if (returnedFilename) {
                         let url = DocServer.prepend(returnedFilename);
-                        let imageSummary = Docs.ImageDocument(url, {
+                        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-"
                         });
diff --git a/src/client/views/collections/DockedFrameRenderer.tsx b/src/client/views/collections/DockedFrameRenderer.tsx
new file mode 100644
index 000000000..25d4b2a49
--- /dev/null
+++ b/src/client/views/collections/DockedFrameRenderer.tsx
@@ -0,0 +1,116 @@
+import 'golden-layout/src/css/goldenlayout-base.css';
+import 'golden-layout/src/css/goldenlayout-dark-theme.css';
+import { action, observable, reaction, Lambda, IReactionDisposer } from "mobx";
+import { observer } from "mobx-react";
+import Measure, { ContentRect } from "react-measure";
+import { Doc, Field, Opt, DocListCast } from "../../../new_fields/Doc";
+import { FieldId } from "../../../new_fields/RefField";
+import { Cast, NumCast, StrCast } from "../../../new_fields/Types";
+import { emptyFunction, returnTrue, Utils } from "../../../Utils";
+import { DocServer } from "../../DocServer";
+import { Transform } from '../../util/Transform';
+import { DocumentView } from "../nodes/DocumentView";
+import "./CollectionDockingView.scss";
+import { SubCollectionViewProps } from "./CollectionSubView";
+import React = require("react");
+import { CollectionViewType } from './CollectionBaseView';
+import { Id } from '../../../new_fields/FieldSymbols';
+import { CollectionDockingView } from './CollectionDockingView';
+
+interface DockedFrameProps {
+    documentId: FieldId;
+    glContainer: any;
+    glEventHub: any;
+    parent: CollectionDockingView;
+}
+
+@observer
+export class DockedFrameRenderer extends React.Component<DockedFrameProps> {
+    _mainCont = React.createRef<HTMLDivElement>();
+    @observable private _panelWidth = 0;
+    @observable private _panelHeight = 0;
+    @observable private _document: Opt<Doc>;
+    private get parentProps(): SubCollectionViewProps {
+        return this.props.parent.props;
+    }
+
+    get _stack(): any {
+        let parent = this.props.glContainer.parent.parent;
+        if (this._document && this._document.excludeFromLibrary && parent.parent && parent.parent.contentItems.length > 1)
+            return parent.parent.contentItems[1];
+        return parent;
+    }
+    constructor(props: any) {
+        super(props);
+        DocServer.GetRefField(this.props.documentId).then(action((f: Opt<Field>) => this._document = f as Doc));
+    }
+
+    nativeWidth = () => NumCast(this._document!.nativeWidth, this._panelWidth);
+    nativeHeight = () => NumCast(this._document!.nativeHeight, this._panelHeight);
+    contentScaling = () => {
+        const nativeH = this.nativeHeight();
+        const nativeW = this.nativeWidth();
+        let wscale = this._panelWidth / nativeW;
+        return wscale * nativeH > this._panelHeight ? this._panelHeight / nativeH : wscale;
+    }
+
+    ScreenToLocalTransform = () => {
+        if (this._mainCont.current && this._mainCont.current.children) {
+            let { scale, translateX, translateY } = Utils.GetScreenTransform(this._mainCont.current.children[0].firstChild as HTMLElement);
+            scale = Utils.GetScreenTransform(this._mainCont.current).scale;
+            return this.parentProps.ScreenToLocalTransform().translate(-translateX, -translateY).scale(1 / this.contentScaling() / scale);
+        }
+        return Transform.Identity();
+    }
+    get scaleToFitMultiplier() {
+        let docWidth = NumCast(this._document!.width);
+        let docHeight = NumCast(this._document!.height);
+        if (NumCast(this._document!.nativeWidth) || !docWidth || !this._panelWidth || !this._panelHeight) return 1;
+        if (StrCast(this._document!.layout).indexOf("Collection") === -1 ||
+            NumCast(this._document!.viewType) !== CollectionViewType.Freeform) return 1;
+        let scaling = Math.max(1, this._panelWidth / docWidth * docHeight > this._panelHeight ?
+            this._panelHeight / docHeight : this._panelWidth / docWidth);
+        return scaling;
+    }
+    get previewPanelCenteringOffset() { return (this._panelWidth - this.nativeWidth() * this.contentScaling()) / 2; }
+
+    addDocTab = (doc: Doc, location: string) => {
+        if (location === "onRight") {
+            CollectionDockingView.AddRightSplit(doc);
+        } else {
+            CollectionDockingView.AddTab(this._stack, doc);
+        }
+    }
+    get content() {
+        if (!this._document) {
+            return (null);
+        }
+        return (
+            <div className="collectionDockingView-content" ref={this._mainCont}
+                style={{ transform: `translate(${this.previewPanelCenteringOffset}px, 0px) scale(${this.scaleToFitMultiplier}, ${this.scaleToFitMultiplier})` }}>
+                <DocumentView key={this._document[Id]} Document={this._document}
+                    bringToFront={emptyFunction}
+                    addDocument={undefined}
+                    removeDocument={undefined}
+                    ContentScaling={this.contentScaling}
+                    PanelWidth={this.nativeWidth}
+                    PanelHeight={this.nativeHeight}
+                    ScreenToLocalTransform={this.ScreenToLocalTransform}
+                    isTopMost={true}
+                    selectOnLoad={false}
+                    parentActive={returnTrue}
+                    whenActiveChanged={emptyFunction}
+                    focus={emptyFunction}
+                    addDocTab={this.addDocTab}
+                    ContainingCollectionView={undefined} />
+            </div >);
+    }
+
+    render() {
+        let theContent = this.content;
+        return !this._document ? (null) :
+            <Measure offset onResize={action((r: any) => { this._panelWidth = r.offset.width; this._panelHeight = r.offset.height; })}>
+                {({ measureRef }) => <div ref={measureRef}>  {theContent} </div>}
+            </Measure>;
+    }
+}
\ No newline at end of file
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index 29734fa19..cd386abfa 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -79,7 +79,7 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
                 }
                 ns.map(line => {
                     let indent = line.search(/\S|$/);
-                    let newBox = Docs.TextDocument({ width: 200, height: 35, x: x + indent / 3 * 10, y: y, documentText: "@@@" + line, title: line });
+                    let newBox = Docs.Create.TextDocument({ width: 200, height: 35, x: x + indent / 3 * 10, y: y, documentText: "@@@" + line, title: line });
                     this.props.addDocument(newBox, false);
                     y += 40 * this.props.getTransform().Scale;
                 });
@@ -89,13 +89,13 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
             navigator.clipboard.readText().then(text => {
                 let ns = text.split("\n").filter(t => t.trim() !== "\r" && t.trim() !== "");
                 if (ns.length === 1 && text.startsWith("http")) {
-                    this.props.addDocument(Docs.ImageDocument(text, { nativeWidth: 300, width: 300, x: x, y: y }), false);// paste an image from its URL in the paste buffer
+                    this.props.addDocument(Docs.Create.ImageDocument(text, { nativeWidth: 300, width: 300, x: x, y: y }), false);// paste an image from its URL in the paste buffer
                 } else {
                     this.pasteTable(ns, x, y);
                 }
             });
         } else {
-            let newBox = Docs.TextDocument({ width: 200, height: 100, x: x, y: y, title: "-typed text-" });
+            let newBox = Docs.Create.TextDocument({ width: 200, height: 100, x: x, y: y, title: "-typed text-" });
             this.props.addLiveTextDocument(newBox);
         }
         e.stopPropagation();
@@ -136,7 +136,7 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
                 doc.width = 200;
                 docList.push(doc);
             }
-            let newCol = Docs.SchemaDocument([...(groupAttr ? ["_group"] : []), ...columns.filter(c => c)], docList, { x: x, y: y, title: "droppedTable", width: 300, height: 100 });
+            let newCol = Docs.Create.SchemaDocument([...(groupAttr ? ["_group"] : []), ...columns.filter(c => c)], docList, { x: x, y: y, title: "droppedTable", width: 300, height: 100 });
 
             this.props.addDocument(newCol, false);
         }
@@ -259,7 +259,7 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
             let ink = Cast(this.props.container.props.Document.ink, InkField);
             let inkData = ink ? ink.inkData : undefined;
             let zoomBasis = NumCast(this.props.container.props.Document.scale, 1);
-            let newCollection = Docs.FreeformDocument(selected, {
+            let newCollection = Docs.Create.FreeformDocument(selected, {
                 x: bounds.left,
                 y: bounds.top,
                 panX: 0,
@@ -283,14 +283,14 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
                     d.page = -1;
                     return d;
                 });
-                let summary = Docs.TextDocument({ x: bounds.left, y: bounds.top, width: 300, height: 100, backgroundColor: "#e2ad32" /* yellow */, title: "-summary-" });
+                let summary = Docs.Create.TextDocument({ x: bounds.left, y: bounds.top, width: 300, height: 100, backgroundColor: "#e2ad32" /* yellow */, title: "-summary-" });
                 newCollection.proto!.summaryDoc = summary;
                 selected = [newCollection];
                 newCollection.x = bounds.left + bounds.width;
                 summary.proto!.subBulletDocs = new List<Doc>(selected);
                 //summary.proto!.maximizeLocation = "inTab";  // or "inPlace", or "onRight"
                 summary.templates = new List<string>([Templates.Bullet.Layout]);
-                let container = Docs.FreeformDocument([summary, newCollection], { x: bounds.left, y: bounds.top, width: 300, height: 200, title: "-summary-" });
+                let container = Docs.Create.FreeformDocument([summary, newCollection], { x: bounds.left, y: bounds.top, width: 300, height: 200, title: "-summary-" });
                 container.viewType = CollectionViewType.Stacking;
                 this.props.addLiveTextDocument(container);
                 // });
@@ -303,11 +303,11 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
                         d.page = -1;
                         return d;
                     });
-                    let summary = Docs.TextDocument({ x: bounds.left, y: bounds.top, width: 300, height: 100, backgroundColor: "#e2ad32" /* yellow */, title: "-summary-" });
+                    let summary = Docs.Create.TextDocument({ x: bounds.left, y: bounds.top, width: 300, height: 100, backgroundColor: "#e2ad32" /* yellow */, title: "-summary-" });
                     SearchBox.convertDataUri(dataUrl, "icon" + summary[Id] + "_image").then((returnedFilename) => {
                         if (returnedFilename) {
                             let url = DocServer.prepend(returnedFilename);
-                            let imageSummary = Docs.ImageDocument(url, {
+                            let imageSummary = Docs.Create.ImageDocument(url, {
                                 x: bounds.left, y: bounds.top + 100 / zoomBasis,
                                 width: 150, height: bounds.height / bounds.width * 150, title: "-summary image-"
                             });
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index efba26c2c..16e40000d 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -302,7 +302,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
     }
 
     deleteClicked = (): void => { this.props.removeDocument && this.props.removeDocument(this.props.Document); }
-    fieldsClicked = (): void => { this.props.addDocTab(Docs.KVPDocument(this.props.Document, { width: 300, height: 300 }), "onRight") };
+    fieldsClicked = (): void => { this.props.addDocTab(Docs.Create.KVPDocument(this.props.Document, { width: 300, height: 300 }), "onRight") };
     makeBtnClicked = (): void => {
         let doc = Doc.GetProto(this.props.Document);
         doc.isButton = !BoolCast(doc.isButton, false);
@@ -418,7 +418,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
         cm.addItem({
             description: "Find aliases", event: async () => {
                 const aliases = await SearchUtil.GetAliasesOfDocument(this.props.Document);
-                this.props.addDocTab && this.props.addDocTab(Docs.SchemaDocument(["title"], aliases, {}), "onRight");
+                this.props.addDocTab && this.props.addDocTab(Docs.Create.SchemaDocument(["title"], aliases, {}), "onRight");
             }, icon: "search"
         });
         cm.addItem({ description: "Center View", event: () => this.props.focus(this.props.Document), icon: "crosshairs" });
diff --git a/src/mobile/ImageUpload.tsx b/src/mobile/ImageUpload.tsx
index bfc1738fc..a8f94b746 100644
--- a/src/mobile/ImageUpload.tsx
+++ b/src/mobile/ImageUpload.tsx
@@ -33,7 +33,7 @@ class Uploader extends React.Component {
     onClick = async () => {
         try {
             this.status = "initializing protos";
-            await Docs.initProtos();
+            await Docs.Prototypes.initialize();
             let imgPrev = document.getElementById("img_preview");
             if (imgPrev) {
                 let files: FileList | null = inputRef.current!.files;
@@ -53,7 +53,7 @@ class Uploader extends React.Component {
                     const json = await res.json();
                     json.map(async (file: any) => {
                         let path = window.location.origin + file;
-                        var doc = Docs.ImageDocument(path, { nativeWidth: 200, width: 200, title: name });
+                        var doc = Docs.Create.ImageDocument(path, { nativeWidth: 200, width: 200, title: name });
 
                         this.status = "getting user document";
 
diff --git a/src/new_fields/Doc.ts b/src/new_fields/Doc.ts
index 7f7263cf1..af65f5482 100644
--- a/src/new_fields/Doc.ts
+++ b/src/new_fields/Doc.ts
@@ -172,6 +172,18 @@ export namespace Doc {
         }
         return protos;
     }
+
+    /**
+     * This function is intended to model Object.assign({}, {}) [https://mzl.la/1Mo3l21], which copies
+     * the values of the properties of a source object into the target.
+     * 
+     * This is just a specific, Dash-authored version that serves the same role for our
+     * Doc class.
+     * 
+     * @param doc the target document into which you'd like to insert the new fields 
+     * @param fields the fields to project onto the target. Its type signature defines a mapping from some string key
+     * to a potentially undefined field, where each entry in this mapping is optional. 
+     */
     export function assign<K extends string>(doc: Doc, fields: Partial<Record<K, Opt<Field>>>) {
         for (const key in fields) {
             if (fields.hasOwnProperty(key)) {
diff --git a/src/new_fields/util.ts b/src/new_fields/util.ts
index 2b304c373..8caceb063 100644
--- a/src/new_fields/util.ts
+++ b/src/new_fields/util.ts
@@ -60,6 +60,7 @@ export function getter(target: any, prop: string | symbol | number, receiver: an
     }
     return getField(target, prop);
 }
+
 function getProtoField(protoField: Doc | undefined, prop: string | number, cb?: (field: Field | undefined) => void) {
     if (!protoField) return undefined;
     let field = protoField[prop];
diff --git a/src/server/authentication/models/current_user_utils.ts b/src/server/authentication/models/current_user_utils.ts
index e5b7a025b..169be3b99 100644
--- a/src/server/authentication/models/current_user_utils.ts
+++ b/src/server/authentication/models/current_user_utils.ts
@@ -33,8 +33,8 @@ export class CurrentUserUtils {
         doc.title = this.email;
         doc.data = new List<Doc>();
         doc.excludeFromLibrary = true;
-        doc.optionalRightCollection = Docs.StackingDocument([], { title: "New mobile uploads" });
-        // doc.library = Docs.TreeDocument([doc], { title: `Library: ${CurrentUserUtils.email}` });
+        doc.optionalRightCollection = Docs.Create.StackingDocument([], { title: "New mobile uploads" });
+        // doc.library = Docs.Create.TreeDocument([doc], { title: `Library: ${CurrentUserUtils.email}` });
         // (doc.library as Doc).excludeFromLibrary = true;
         return doc;
     }
@@ -94,12 +94,12 @@ export class CurrentUserUtils {
         //                         new AttributeTransformationModel(atmod, AggregateFunction.None),
         //                         new AttributeTransformationModel(atmod, AggregateFunction.Count),
         //                         new AttributeTransformationModel(atmod, AggregateFunction.Count));
-        //                     schemaDocuments.push(Docs.HistogramDocument(histoOp, { width: 200, height: 200, title: attr.displayName! }));
+        //                     schemaDocuments.push(Docs.Create.HistogramDocument(histoOp, { width: 200, height: 200, title: attr.displayName! }));
         //                 }
         //             })));
         //             return promises;
         //         }, [] as Promise<void>[]));
-        //         return CurrentUserUtils._northstarSchemas.push(Docs.TreeDocument(schemaDocuments, { width: 50, height: 100, title: schema.displayName! }));
+        //         return CurrentUserUtils._northstarSchemas.push(Docs.Create.TreeDocument(schemaDocuments, { width: 50, height: 100, title: schema.displayName! }));
         //     });
         // }
     }
-- 
cgit v1.2.3-70-g09d2


From de0304b2966ebdede9d9db8c510e19020046115c Mon Sep 17 00:00:00 2001
From: Sam Wilkins <samuel_wilkins@brown.edu>
Date: Mon, 17 Jun 2019 13:38:15 -0400
Subject: peripheral renaming fixes

---
 src/client/documents/Documents.ts                              |  4 ++--
 src/client/util/History.ts                                     |  6 +++---
 src/client/util/SearchUtil.ts                                  |  4 ++--
 src/client/util/TooltipTextMenu.tsx                            |  8 ++++----
 src/client/views/MainView.tsx                                  | 10 +++++-----
 src/client/views/SearchBox.tsx                                 |  6 +++---
 src/client/views/collections/CollectionDockingView.tsx         | 10 +++++-----
 src/client/views/collections/CollectionSubView.tsx             |  8 ++++----
 src/client/views/collections/CollectionVideoView.tsx           |  2 +-
 src/client/views/collections/DockedFrameRenderer.tsx           |  2 +-
 .../collections/collectionFreeForm/CollectionFreeFormView.tsx  |  4 ++--
 .../views/collections/collectionFreeForm/MarqueeView.tsx       |  2 +-
 src/client/views/nodes/DocumentView.tsx                        |  2 +-
 src/client/views/nodes/FormattedTextBox.tsx                    |  6 +++---
 src/client/views/nodes/PDFBox.tsx                              |  2 +-
 src/client/views/nodes/VideoBox.tsx                            |  2 +-
 src/debug/Viewer.tsx                                           |  2 +-
 src/mobile/ImageUpload.tsx                                     |  4 ++--
 src/new_fields/Proxy.ts                                        |  2 +-
 src/server/authentication/models/current_user_utils.ts         |  6 +++---
 20 files changed, 46 insertions(+), 46 deletions(-)

(limited to 'src/client/views/collections/CollectionDockingView.tsx')

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index b10954636..758291b9b 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -98,7 +98,7 @@ export namespace Docs {
             // non-guid string ids for each document prototype
             let protoIds = [textProtoId, histoProtoId, collProtoId, imageProtoId, webProtoId, kvpProtoId, videoProtoId, audioProtoId, pdfProtoId, iconProtoId]
             // fetch the actual prototype documents from the server
-            let actualProtos = await DocServer.GetRefFields(protoIds);
+            let actualProtos = await DocServer.getRefFields(protoIds);
 
             // initialize prototype documents
             textProto = actualProtos[textProtoId] as Doc || CreateTextProto();
@@ -363,7 +363,7 @@ export namespace Docs {
                 CurrentUserUtils.AddNorthstarSchema(schema, schemaDoc);
                 const docs = schemaDocuments;
                 CurrentUserUtils.GetAllNorthstarColumnAttributes(schema).map(attr => {
-                    DocServer.GetRefField(attr.displayName! + ".alias").then(action((field: Opt<Field>) => {
+                    DocServer.getRefField(attr.displayName! + ".alias").then(action((field: Opt<Field>) => {
                         if (field instanceof Doc) {
                             docs.push(field);
                         } else {
diff --git a/src/client/util/History.ts b/src/client/util/History.ts
index 545ea8629..94bfcbe09 100644
--- a/src/client/util/History.ts
+++ b/src/client/util/History.ts
@@ -88,7 +88,7 @@ export namespace HistoryUtil {
     }
 
     export function createUrl(params: ParsedUrl): string {
-        let baseUrl = DocServer.prepend(`/${params.type}`);
+        let baseUrl = DocServer.Util.prepend(`/${params.type}`);
         switch (params.type) {
             case "doc":
                 const initializers = encodeURIComponent(JSON.stringify(params.initializers));
@@ -103,7 +103,7 @@ export namespace HistoryUtil {
     }
 
     export async function initDoc(id: string, initializer: DocInitializerList) {
-        const doc = await DocServer.GetRefField(id);
+        const doc = await DocServer.getRefField(id);
         if (!(doc instanceof Doc)) {
             return;
         }
@@ -111,7 +111,7 @@ export namespace HistoryUtil {
     }
 
     async function onDocUrl(url: DocUrl) {
-        const field = await DocServer.GetRefField(url.docId);
+        const field = await DocServer.getRefField(url.docId);
         await Promise.all(Object.keys(url.initializers).map(id => initDoc(id, url.initializers[id])));
         if (field instanceof Doc) {
             MainView.Instance.openWorkspace(field, true);
diff --git a/src/client/util/SearchUtil.ts b/src/client/util/SearchUtil.ts
index 28ec8ca14..9dd9acbb7 100644
--- a/src/client/util/SearchUtil.ts
+++ b/src/client/util/SearchUtil.ts
@@ -7,13 +7,13 @@ export namespace SearchUtil {
     export function Search(query: string, returnDocs: true): Promise<Doc[]>;
     export function Search(query: string, returnDocs: false): Promise<string[]>;
     export async function Search(query: string, returnDocs: boolean) {
-        const ids = JSON.parse(await rp.get(DocServer.prepend("/search"), {
+        const ids = JSON.parse(await rp.get(DocServer.Util.prepend("/search"), {
             qs: { query }
         }));
         if (!returnDocs) {
             return ids;
         }
-        const docMap = await DocServer.GetRefFields(ids);
+        const docMap = await DocServer.getRefFields(ids);
         return ids.map((id: string) => docMap[id]).filter((doc: any) => doc instanceof Doc);
     }
 
diff --git a/src/client/util/TooltipTextMenu.tsx b/src/client/util/TooltipTextMenu.tsx
index fa2483db5..36219a99e 100644
--- a/src/client/util/TooltipTextMenu.tsx
+++ b/src/client/util/TooltipTextMenu.tsx
@@ -187,9 +187,9 @@ export class TooltipTextMenu {
                 let link = node && node.marks.find(m => m.type.name === "link");
                 if (link) {
                     let href: string = link.attrs.href;
-                    if (href.indexOf(DocServer.prepend("/doc/")) === 0) {
-                        let docid = href.replace(DocServer.prepend("/doc/"), "");
-                        DocServer.GetRefField(docid).then(action((f: Opt<Field>) => {
+                    if (href.indexOf(DocServer.Util.prepend("/doc/")) === 0) {
+                        let docid = href.replace(DocServer.Util.prepend("/doc/"), "");
+                        DocServer.getRefField(docid).then(action((f: Opt<Field>) => {
                             if (f instanceof Doc) {
                                 if (DocumentManager.Instance.getDocumentView(f)) {
                                     DocumentManager.Instance.getDocumentView(f)!.props.focus(f);
@@ -218,7 +218,7 @@ export class TooltipTextMenu {
                         handlers: {
                             dragComplete: action(() => {
                                 let m = dragData.droppedDocuments;
-                                this.makeLink(DocServer.prepend("/doc/" + m[0][Id]));
+                                this.makeLink(DocServer.Util.prepend("/doc/" + m[0][Id]));
                             }),
                         },
                         hideSource: false
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 984db0426..734961b56 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -76,11 +76,11 @@ export class MainView extends React.Component {
         // causes errors to be generated when modifying an observable outside of an action
         configure({ enforceActions: "observed" });
         if (window.location.search.includes("readonly")) {
-            DocServer.makeReadOnly();
+            DocServer.Util.makeReadOnly();
         }
         if (window.location.search.includes("safe")) {
             if (!window.location.search.includes("nro")) {
-                DocServer.makeReadOnly();
+                DocServer.Util.makeReadOnly();
             }
             CollectionBaseView.SetSafeMode(true);
         }
@@ -141,7 +141,7 @@ export class MainView extends React.Component {
                 this.createNewWorkspace();
             }
         } else {
-            DocServer.GetRefField(CurrentUserUtils.MainDocId).then(field =>
+            DocServer.getRefField(CurrentUserUtils.MainDocId).then(field =>
                 field instanceof Doc ? this.openWorkspace(field) :
                     this.createNewWorkspace(CurrentUserUtils.MainDocId));
         }
@@ -294,7 +294,7 @@ export class MainView extends React.Component {
         let logoutRef = React.createRef<HTMLDivElement>();
 
         return [
-            <button className="clear-db-button" key="clear-db" onClick={e => e.shiftKey && e.altKey && DocServer.DeleteDatabase()}>Clear Database</button>,
+            <button className="clear-db-button" key="clear-db" onClick={e => e.shiftKey && e.altKey && DocServer.Util.deleteDatabase()}>Clear Database</button>,
             <div id="toolbar" key="toolbar">
                 <div ref={notifsRef}>
                     <button className="toolbar-button round-button" title="Notifs"
@@ -312,7 +312,7 @@ export class MainView extends React.Component {
             </div >,
             this.isSearchVisible ? <div className="main-searchDiv" key="search" style={{ top: '34px', right: '1px', position: 'absolute' }} > <SearchBox /> </div> : null,
             <div className="main-buttonDiv" key="logout" style={{ bottom: '0px', right: '1px', position: 'absolute' }} ref={logoutRef}>
-                <button onClick={() => request.get(DocServer.prepend(RouteStore.logout), emptyFunction)}>Log Out</button></div>
+                <button onClick={() => request.get(DocServer.Util.prepend(RouteStore.logout), emptyFunction)}>Log Out</button></div>
         ];
 
     }
diff --git a/src/client/views/SearchBox.tsx b/src/client/views/SearchBox.tsx
index 7164d98a4..973715876 100644
--- a/src/client/views/SearchBox.tsx
+++ b/src/client/views/SearchBox.tsx
@@ -56,13 +56,13 @@ export class SearchBox extends React.Component {
 
     @action
     getResults = async (query: string) => {
-        let response = await rp.get(DocServer.prepend('/search'), {
+        let response = await rp.get(DocServer.Util.prepend('/search'), {
             qs: {
                 query
             }
         });
         let res: string[] = JSON.parse(response);
-        const fields = await DocServer.GetRefFields(res);
+        const fields = await DocServer.getRefFields(res);
         const docs: Doc[] = [];
         for (const id of res) {
             const field = fields[id];
@@ -74,7 +74,7 @@ export class SearchBox extends React.Component {
     }
     public static async convertDataUri(imageUri: string, returnedFilename: string) {
         try {
-            let posting = DocServer.prepend(RouteStore.dataUriToImage);
+            let posting = DocServer.Util.prepend(RouteStore.dataUriToImage);
             const returnedUri = await rp.post(posting, {
                 body: {
                     uri: imageUri,
diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx
index e2bcb10ec..4f5837590 100644
--- a/src/client/views/collections/CollectionDockingView.tsx
+++ b/src/client/views/collections/CollectionDockingView.tsx
@@ -306,7 +306,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
             let y = e.clientY;
             let docid = (e.target as any).DashDocId;
             let tab = (e.target as any).parentElement as HTMLElement;
-            DocServer.GetRefField(docid).then(action(async (sourceDoc: Opt<Field>) =>
+            DocServer.getRefField(docid).then(action(async (sourceDoc: Opt<Field>) =>
                 (sourceDoc instanceof Doc) && DragLinksAsDocuments(tab, x, y, sourceDoc)));
         } else
             if ((className === "lm_title" || className === "lm_tab lm_active") && !e.shiftKey) {
@@ -320,7 +320,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
                 if (glTab && glTab.contentItem && glTab.contentItem.parent) {
                     glTab.contentItem.parent.setActiveContentItem(glTab.contentItem);
                 }
-                DocServer.GetRefField(docid).then(action((f: Opt<Field>) => {
+                DocServer.getRefField(docid).then(action((f: Opt<Field>) => {
                     if (f instanceof Doc) {
                         DragManager.StartDocumentDrag([tab], new DragManager.DocumentDragData([f]), x, y,
                             {
@@ -372,7 +372,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
             if (tab.contentItem.config.fixed) {
                 tab.contentItem.parent.config.fixed = true;
             }
-            DocServer.GetRefField(tab.contentItem.config.props.documentId).then(async doc => {
+            DocServer.getRefField(tab.contentItem.config.props.documentId).then(async doc => {
                 if (doc instanceof Doc) {
                     let counter: any = this.htmlToElement(`<span class="messageCounter">0</div>`);
                     tab.element.append(counter);
@@ -409,7 +409,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
                 if (tab.reactionDisposer) {
                     tab.reactionDisposer();
                 }
-                let doc = await DocServer.GetRefField(tab.contentItem.config.props.documentId);
+                let doc = await DocServer.getRefField(tab.contentItem.config.props.documentId);
                 if (doc instanceof Doc) {
                     let theDoc = doc;
                     CollectionDockingView.TopLevel._removedDocs.push(theDoc);
@@ -434,7 +434,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
                 //if (confirm('really close this?')) {
                 stack.remove();
                 stack.contentItems.map(async (contentItem: any) => {
-                    let doc = await DocServer.GetRefField(contentItem.config.props.documentId);
+                    let doc = await DocServer.getRefField(contentItem.config.props.documentId);
                     if (doc instanceof Doc) {
                         let theDoc = doc;
                         CollectionDockingView.TopLevel._removedDocs.push(theDoc);
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index 440a2410b..36e276d13 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -133,7 +133,7 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
                 if (path.includes(window.location.hostname)) {
                     let s = path.split('/');
                     let id = s[s.length - 1];
-                    DocServer.GetRefField(id).then(field => {
+                    DocServer.getRefField(id).then(field => {
                         if (field instanceof Doc) {
                             let alias = Doc.MakeAlias(field);
                             alias.x = options.x || 0;
@@ -170,8 +170,8 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
             if (html && html.indexOf(document.location.origin)) { // prosemirror text containing link to dash document
                 let start = html.indexOf(window.location.origin);
                 let path = html.substr(start, html.length - start);
-                let docid = path.substr(0, path.indexOf("\">")).replace(DocServer.prepend("/doc/"), "").split("?")[0];
-                DocServer.GetRefField(docid).then(f => (f instanceof Doc) && this.props.addDocument(f, false));
+                let docid = path.substr(0, path.indexOf("\">")).replace(DocServer.Util.prepend("/doc/"), "").split("?")[0];
+                DocServer.getRefField(docid).then(f => (f instanceof Doc) && this.props.addDocument(f, false));
                 return;
             }
             if (html && html.indexOf("<img") !== 0 && !html.startsWith("<a")) {
@@ -194,7 +194,7 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
                 if (item.kind === "string" && item.type.indexOf("uri") !== -1) {
                     let str: string;
                     let prom = new Promise<string>(resolve => e.dataTransfer.items[i].getAsString(resolve))
-                        .then(action((s: string) => rp.head(DocServer.prepend(RouteStore.corsProxy + "/" + (str = s)))))
+                        .then(action((s: string) => rp.head(DocServer.Util.prepend(RouteStore.corsProxy + "/" + (str = s)))))
                         .then(result => {
                             let type = result["content-type"];
                             if (type) {
diff --git a/src/client/views/collections/CollectionVideoView.tsx b/src/client/views/collections/CollectionVideoView.tsx
index bd5cd5450..ccbac9915 100644
--- a/src/client/views/collections/CollectionVideoView.tsx
+++ b/src/client/views/collections/CollectionVideoView.tsx
@@ -97,7 +97,7 @@ export class CollectionVideoView extends React.Component<FieldViewProps> {
                 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 url = DocServer.Util.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-"
diff --git a/src/client/views/collections/DockedFrameRenderer.tsx b/src/client/views/collections/DockedFrameRenderer.tsx
index 25d4b2a49..1e7c5661b 100644
--- a/src/client/views/collections/DockedFrameRenderer.tsx
+++ b/src/client/views/collections/DockedFrameRenderer.tsx
@@ -42,7 +42,7 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> {
     }
     constructor(props: any) {
         super(props);
-        DocServer.GetRefField(this.props.documentId).then(action((f: Opt<Field>) => this._document = f as Doc));
+        DocServer.getRefField(this.props.documentId).then(action((f: Opt<Field>) => this._document = f as Doc));
     }
 
     nativeWidth = () => NumCast(this._document!.nativeWidth, this._panelWidth);
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 9d19df540..cd613e6ab 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -235,8 +235,8 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
             e.preventDefault();
             let start = html.indexOf(window.location.origin);
             let path = html.substr(start, html.length - start);
-            let docid = path.substr(0, path.indexOf("\">")).replace(DocServer.prepend("/doc/"), "").split("?")[0];
-            DocServer.GetRefField(docid).then(f => {
+            let docid = path.substr(0, path.indexOf("\">")).replace(DocServer.Util.prepend("/doc/"), "").split("?")[0];
+            DocServer.getRefField(docid).then(f => {
                 if (f instanceof Doc) {
                     f.x = pt[0];
                     f.y = pt[1];
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index cd386abfa..07a58ed64 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -306,7 +306,7 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
                     let summary = Docs.Create.TextDocument({ x: bounds.left, y: bounds.top, width: 300, height: 100, backgroundColor: "#e2ad32" /* yellow */, title: "-summary-" });
                     SearchBox.convertDataUri(dataUrl, "icon" + summary[Id] + "_image").then((returnedFilename) => {
                         if (returnedFilename) {
-                            let url = DocServer.prepend(returnedFilename);
+                            let url = DocServer.Util.prepend(returnedFilename);
                             let imageSummary = Docs.Create.ImageDocument(url, {
                                 x: bounds.left, y: bounds.top + 100 / zoomBasis,
                                 width: 150, height: bounds.height / bounds.width * 150, title: "-summary image-"
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 16e40000d..fdcb20e9a 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -422,7 +422,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
             }, icon: "search"
         });
         cm.addItem({ description: "Center View", event: () => this.props.focus(this.props.Document), icon: "crosshairs" });
-        cm.addItem({ description: "Copy URL", event: () => Utils.CopyText(DocServer.prepend("/doc/" + this.props.Document[Id])), icon: "link" });
+        cm.addItem({ description: "Copy URL", event: () => Utils.CopyText(DocServer.Util.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" });
         if (!this.topMost) {
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index d00a4b928..6a14a04f7 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -237,9 +237,9 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
                 href = parent.childNodes[0].href;
             }
             if (href) {
-                if (href.indexOf(DocServer.prepend("/doc/")) === 0) {
-                    let docid = href.replace(DocServer.prepend("/doc/"), "").split("?")[0];
-                    DocServer.GetRefField(docid).then(f => {
+                if (href.indexOf(DocServer.Util.prepend("/doc/")) === 0) {
+                    let docid = href.replace(DocServer.Util.prepend("/doc/"), "").split("?")[0];
+                    DocServer.getRefField(docid).then(f => {
                         (f instanceof Doc) && DocumentManager.Instance.jumpToDocument(f, ctrlKey, document => this.props.addDocTab(document, "inTab"))
                     });
                 }
diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx
index aa29a7170..df9e49b64 100644
--- a/src/client/views/nodes/PDFBox.tsx
+++ b/src/client/views/nodes/PDFBox.tsx
@@ -257,7 +257,7 @@ export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocumen
                 .then(action((dataUrl: string) => {
                     SearchBox.convertDataUri(dataUrl, "icon" + this.Document[Id] + "_" + this.curPage).then((returnedFilename) => {
                         if (returnedFilename) {
-                            let url = DocServer.prepend(returnedFilename);
+                            let url = DocServer.Util.prepend(returnedFilename);
                             this.props.Document.thumbnail = new ImageField(new URL(url));
                         }
                         runInAction(() => this._renderAsSvg = true);
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index 35ecf12f6..9ab607e91 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -97,7 +97,7 @@ export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoD
             };
             try {
                 let responseSchema: any = {};
-                const videoInfoResponse = await rp.get(DocServer.prepend(RouteStore.corsProxy + "/" + `https://www.youtube.com/watch?v=${videoId}`), videoInfoRequestConfig);
+                const videoInfoResponse = await rp.get(DocServer.Util.prepend(RouteStore.corsProxy + "/" + `https://www.youtube.com/watch?v=${videoId}`), videoInfoRequestConfig);
                 const dataHtml = videoInfoResponse;
                 const start = dataHtml.indexOf('ytplayer.config = ') + 18;
                 const end = dataHtml.indexOf(';ytplayer.load');
diff --git a/src/debug/Viewer.tsx b/src/debug/Viewer.tsx
index b22300d0b..753149756 100644
--- a/src/debug/Viewer.tsx
+++ b/src/debug/Viewer.tsx
@@ -146,7 +146,7 @@ class Viewer extends React.Component {
     @action
     onKeyPress = (e: React.KeyboardEvent<HTMLDivElement>) => {
         if (e.key === "Enter") {
-            DocServer.GetRefField(this.idToAdd).then(action((field: any) => {
+            DocServer.getRefField(this.idToAdd).then(action((field: any) => {
                 if (field !== undefined) {
                     this.fields.push(field);
                 }
diff --git a/src/mobile/ImageUpload.tsx b/src/mobile/ImageUpload.tsx
index a8f94b746..df597e0a9 100644
--- a/src/mobile/ImageUpload.tsx
+++ b/src/mobile/ImageUpload.tsx
@@ -57,11 +57,11 @@ class Uploader extends React.Component {
 
                         this.status = "getting user document";
 
-                        const res = await rp.get(DocServer.prepend(RouteStore.getUserDocumentId));
+                        const res = await rp.get(DocServer.Util.prepend(RouteStore.getUserDocumentId));
                         if (!res) {
                             throw new Error("No user id returned");
                         }
-                        const field = await DocServer.GetRefField(res);
+                        const field = await DocServer.getRefField(res);
                         let pending: Opt<Doc>;
                         if (field instanceof Doc) {
                             pending = await Cast(field.optionalRightCollection, Doc);
diff --git a/src/new_fields/Proxy.ts b/src/new_fields/Proxy.ts
index 130ec066e..230e4ab8b 100644
--- a/src/new_fields/Proxy.ts
+++ b/src/new_fields/Proxy.ts
@@ -57,7 +57,7 @@ export class ProxyField<T extends RefField> extends ObjectField {
             return undefined;
         }
         if (!this.promise) {
-            this.promise = DocServer.GetRefField(this.fieldId).then(action((field: any) => {
+            this.promise = DocServer.getRefField(this.fieldId).then(action((field: any) => {
                 this.promise = undefined;
                 this.cache = field;
                 if (field === undefined) this.failed = true;
diff --git a/src/server/authentication/models/current_user_utils.ts b/src/server/authentication/models/current_user_utils.ts
index 169be3b99..95c20d2db 100644
--- a/src/server/authentication/models/current_user_utils.ts
+++ b/src/server/authentication/models/current_user_utils.ts
@@ -40,7 +40,7 @@ export class CurrentUserUtils {
     }
 
     public static async loadCurrentUser(): Promise<any> {
-        let userPromise = rp.get(DocServer.prepend(RouteStore.getCurrUser)).then(response => {
+        let userPromise = rp.get(DocServer.Util.prepend(RouteStore.getCurrUser)).then(response => {
             if (response) {
                 let obj = JSON.parse(response);
                 CurrentUserUtils.curr_id = obj.id as string;
@@ -49,9 +49,9 @@ export class CurrentUserUtils {
                 throw new Error("There should be a user! Why does Dash think there isn't one?");
             }
         });
-        let userDocPromise = await rp.get(DocServer.prepend(RouteStore.getUserDocumentId)).then(id => {
+        let userDocPromise = await rp.get(DocServer.Util.prepend(RouteStore.getUserDocumentId)).then(id => {
             if (id) {
-                return DocServer.GetRefField(id).then(field =>
+                return DocServer.getRefField(id).then(field =>
                     runInAction(() => this.user_document = field instanceof Doc ? field : this.createUserDocument(id)));
             } else {
                 throw new Error("There should be a user id! Why does Dash think there isn't one?");
-- 
cgit v1.2.3-70-g09d2


From 892608273cdfeba4cfb55c5c604bee4361b3be0e Mon Sep 17 00:00:00 2001
From: Sam Wilkins <samwilkins333@gmail.com>
Date: Wed, 10 Jul 2019 11:53:00 -0400
Subject: documentation tweaks

---
 src/client/DocServer.ts                                | 13 +++++++------
 src/client/documents/Documents.ts                      |  6 +++---
 src/client/views/collections/CollectionDockingView.tsx |  8 ++++----
 3 files changed, 14 insertions(+), 13 deletions(-)

(limited to 'src/client/views/collections/CollectionDockingView.tsx')

diff --git a/src/client/DocServer.ts b/src/client/DocServer.ts
index 3b33657eb..067d89c51 100644
--- a/src/client/DocServer.ts
+++ b/src/client/DocServer.ts
@@ -146,9 +146,10 @@ export namespace DocServer {
         const promises: Promise<Opt<RefField>>[] = [];
         const map: { [id: string]: Opt<RefField> } = {};
 
-        // 1) an initial pass through the cache to determine which documents need to be fetched,
-        // which are already in the process of being fetched and which already exist in the
-        // cache
+        // 1) an initial pass through the cache to determine
+        // i) which documents need to be fetched
+        // ii) which are already in the process of being fetched
+        // iii) which already exist in the cache
         for (const id of ids) {
             const cached = _cache[id];
             if (cached === undefined) {
@@ -204,7 +205,7 @@ export namespace DocServer {
 
         // 6) with this confidence, we can now go through and update the cache at the ids of the fields that
         // we explicitly had to fetch. To finish it off, we add whatever value we've come up with for a given
-        // id to the soon to be returned field mapping.
+        // id to the soon-to-be-returned field mapping.
         requestedIds.forEach(id => {
             const field = fields[id];
             // either way, overwrite or delete any promises (that we inserted as flags
@@ -232,8 +233,8 @@ export namespace DocServer {
 
         // now, we return our completed mapping from all of the ids that were passed into the method
         // to their actual RefField | undefined values. This return value either becomes the input
-        // argument to the caller's promise (i.e. GetRefFields.then(map => //do something with map...))
-        // or it is the direct return result if the promise is awaited.
+        // argument to the caller's promise (i.e. GetRefFields(["_id1_", "_id2_", "_id3_"]).then(map => //do something with map...))
+        // or it is the direct return result if the promise is awaited (i.e. let fields = await GetRefFields(["_id1_", "_id2_", "_id3_"])).
         return map;
     }
 
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 758291b9b..782c133ff 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -96,7 +96,7 @@ export namespace Docs {
          */
         export async function initialize(): Promise<void> {
             // non-guid string ids for each document prototype
-            let protoIds = [textProtoId, histoProtoId, collProtoId, imageProtoId, webProtoId, kvpProtoId, videoProtoId, audioProtoId, pdfProtoId, iconProtoId]
+            let protoIds = [textProtoId, histoProtoId, collProtoId, imageProtoId, webProtoId, kvpProtoId, videoProtoId, audioProtoId, pdfProtoId, iconProtoId];
             // fetch the actual prototype documents from the server
             let actualProtos = await DocServer.getRefFields(protoIds);
 
@@ -238,7 +238,7 @@ export namespace Docs {
                 y: 0,
                 width: 300,
                 height: 150
-            }
+            };
             return buildPrototype(audioProtoId, "AUDIO_PROTO", AudioBox.LayoutString(), defaultAttrs);
         }
     }
@@ -419,7 +419,7 @@ export namespace Docs {
         export type DocConfig = {
             doc: Doc,
             initialWidth?: number
-        }
+        };
 
         export function StandardCollectionDockingDocument(configs: Array<DocConfig>, options: DocumentOptions, id?: string, type: string = "row") {
             let layoutConfig = {
diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx
index 4f5837590..5270a4624 100644
--- a/src/client/views/collections/CollectionDockingView.tsx
+++ b/src/client/views/collections/CollectionDockingView.tsx
@@ -141,11 +141,11 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
     }
 
     injectParentProp = (contentArray: any[], reverse: boolean = false) => {
-        if (!contentArray || contentArray.length == 0) return;
+        if (!contentArray || contentArray.length === 0) return;
         contentArray.forEach(member => {
             let baseCase = Object.keys(member).includes("props");
             if (!baseCase) {
-                this.injectParentProp(member.content, reverse)
+                this.injectParentProp(member.content, reverse);
             } else {
                 reverse ? delete member.props.parent : member.props.parent = this;
             }
@@ -386,10 +386,10 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
                             tab.header.parent.setActiveContentItem(tab.contentItem);
                         }
                         tab.setActive(true);
-                    }
+                    };
                     tab.header.element[0].ondrop = (e: any) => {
                         console.log("DROPPPP THE BASS!", e);
-                    }
+                    };
                     ReactDOM.render(<ParentDocSelector Document={doc} addDocTab={(doc, location) => CollectionDockingView.AddTab(stack, doc)} />, upDiv);
                     tab.reactComponents = [upDiv];
                     tab.element.append(upDiv);
-- 
cgit v1.2.3-70-g09d2


From abb53ad8a5cbf2439aac420e9ec16a3a461dec1e Mon Sep 17 00:00:00 2001
From: Tyler Schicke <tyler_schicke@brown.edu>
Date: Wed, 10 Jul 2019 15:58:04 -0400
Subject: Added color for ReadOnly mode

---
 src/client/DocServer.ts                                | 2 ++
 src/client/views/collections/CollectionDockingView.tsx | 1 +
 2 files changed, 3 insertions(+)

(limited to 'src/client/views/collections/CollectionDockingView.tsx')

diff --git a/src/client/DocServer.ts b/src/client/DocServer.ts
index 443f70bb1..97147f5e4 100644
--- a/src/client/DocServer.ts
+++ b/src/client/DocServer.ts
@@ -32,6 +32,8 @@ export namespace DocServer {
         // _cache = {};
     }
 
+    export function isReadOnly() { return _isReadOnly; }
+
     export function prepend(extension: string): string {
         return window.location.origin + extension;
     }
diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx
index d477f96f0..57964442d 100644
--- a/src/client/views/collections/CollectionDockingView.tsx
+++ b/src/client/views/collections/CollectionDockingView.tsx
@@ -426,6 +426,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
 
     stackCreated = (stack: any) => {
         //stack.header.controlsContainer.find('.lm_popout').hide();
+        stack.header.element[0].style.backgroundColor = DocServer.isReadOnly() ? "#228540" : undefined;
         stack.header.controlsContainer.find('.lm_close') //get the close icon
             .off('click') //unbind the current click handler
             .click(action(function () {
-- 
cgit v1.2.3-70-g09d2


From aeb0521e8ad8f1efc8c3a39af9eb5443c5368453 Mon Sep 17 00:00:00 2001
From: Tyler Schicke <tyler_schicke@brown.edu>
Date: Thu, 11 Jul 2019 16:47:22 -0400
Subject: Removed rendering of collapsed tabs

---
 .../views/collections/CollectionDockingView.tsx    | 26 +++++++++++++++++++++-
 src/client/views/nodes/VideoBox.tsx                | 11 +++------
 2 files changed, 28 insertions(+), 9 deletions(-)

(limited to 'src/client/views/collections/CollectionDockingView.tsx')

diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx
index 57964442d..abc9bbeba 100644
--- a/src/client/views/collections/CollectionDockingView.tsx
+++ b/src/client/views/collections/CollectionDockingView.tsx
@@ -26,7 +26,7 @@ import React = require("react");
 import { MainView } from '../MainView';
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { library } from '@fortawesome/fontawesome-svg-core';
-import { faFile } from '@fortawesome/free-solid-svg-icons';
+import { faFile, faUnlockAlt } from '@fortawesome/free-solid-svg-icons';
 import { CurrentUserUtils } from '../../../server/authentication/models/current_user_utils';
 library.add(faFile);
 
@@ -471,6 +471,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
 interface DockedFrameProps {
     documentId: FieldId;
     dataDocumentId: FieldId;
+    glContainer: any;
     //collectionDockingView: CollectionDockingView
 }
 @observer
@@ -480,6 +481,9 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> {
     @observable private _panelHeight = 0;
     @observable private _document: Opt<Doc>;
     @observable private _dataDoc: Opt<Doc>;
+
+    @observable private _isActive: boolean = false;
+
     get _stack(): any {
         let parent = (this.props as any).glContainer.parent.parent;
         if (this._document && this._document.excludeFromLibrary && parent.parent && parent.parent.contentItems.length > 1) {
@@ -497,6 +501,25 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> {
         }));
     }
 
+    componentDidMount() {
+        this.props.glContainer.layoutManager.on("activeContentItemChanged", this.onActiveContentItemChanged);
+        this.props.glContainer.on("tab", this.onActiveContentItemChanged);
+        this.onActiveContentItemChanged();
+    }
+
+    componentWillUnmount() {
+        this.props.glContainer.layoutManager.off("activeContentItemChanged", this.onActiveContentItemChanged);
+        this.props.glContainer.off("tab", this.onActiveContentItemChanged);
+    }
+
+    @action.bound
+    private onActiveContentItemChanged() {
+        if (this.props.glContainer.tab) {
+            this._isActive = this.props.glContainer.tab.isActive;
+        }
+    }
+
+
     nativeWidth = () => NumCast(this._document!.nativeWidth, this._panelWidth);
     nativeHeight = () => {
         let nh = NumCast(this._document!.nativeHeight, this._panelHeight);
@@ -571,6 +594,7 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> {
     }
 
     render() {
+        if (!this._isActive) return null;
         let theContent = this.content;
         return !this._document ? (null) :
             <Measure offset onResize={action((r: any) => { this._panelWidth = r.offset.width; this._panelHeight = r.offset.height; })}>
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index a5e145856..c65dfe0bd 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -148,14 +148,9 @@ export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoD
         let interactive = InkingControl.Instance.selectedTool ? "" : "-interactive";
         let style = "videoBox-cont" + (this._fullScreen ? "-fullScreen" : interactive);
         return !field ? <div>Loading</div> :
-            VideoBox._showControls ?
-                <video className={`${style}`} ref={this.setVideoRef} onCanPlay={this.videoLoad} onPointerDown={this.onPointerDown} onContextMenu={this.specificContextMenu} controls>
-                    <source src={field.url.href} type="video/mp4" />
-                    Not supported.
-            </video> :
-                <video className={`${style}`} ref={this.setVideoRef} onCanPlay={this.videoLoad} onPointerDown={this.onPointerDown} onContextMenu={this.specificContextMenu}>
-                    <source src={field.url.href} type="video/mp4" />
-                    Not supported.
+            <video className={`${style}`} ref={this.setVideoRef} onCanPlay={this.videoLoad} onPointerDown={this.onPointerDown} onContextMenu={this.specificContextMenu} controls={VideoBox._showControls}>
+                <source src={field.url.href} type="video/mp4" />
+                Not supported.
             </video>;
     }
 }
\ No newline at end of file
-- 
cgit v1.2.3-70-g09d2


From a7fa75a9b8e5811f81d6cdf92d680ab2cfb38fed Mon Sep 17 00:00:00 2001
From: Tyler Schicke <tyler_schicke@brown.edu>
Date: Thu, 11 Jul 2019 17:07:33 -0400
Subject: Added middle click on stack to add new tab

---
 src/client/views/collections/CollectionDockingView.tsx | 6 ++++++
 1 file changed, 6 insertions(+)

(limited to 'src/client/views/collections/CollectionDockingView.tsx')

diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx
index abc9bbeba..48aedc187 100644
--- a/src/client/views/collections/CollectionDockingView.tsx
+++ b/src/client/views/collections/CollectionDockingView.tsx
@@ -28,6 +28,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { library } from '@fortawesome/fontawesome-svg-core';
 import { faFile, faUnlockAlt } from '@fortawesome/free-solid-svg-icons';
 import { CurrentUserUtils } from '../../../server/authentication/models/current_user_utils';
+import { Docs } from '../../documents/Documents';
 library.add(faFile);
 
 @observer
@@ -427,6 +428,11 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
     stackCreated = (stack: any) => {
         //stack.header.controlsContainer.find('.lm_popout').hide();
         stack.header.element[0].style.backgroundColor = DocServer.isReadOnly() ? "#228540" : undefined;
+        stack.header.element.on('mousedown', (e: any) => {
+            if (e.target === stack.header.element[0] && e.button === 1) {
+                this.AddTab(stack, Docs.FreeformDocument([], { width: this.props.PanelWidth(), height: this.props.PanelHeight(), title: "Untitled Collection" }), undefined);
+            }
+        });
         stack.header.controlsContainer.find('.lm_close') //get the close icon
             .off('click') //unbind the current click handler
             .click(action(function () {
-- 
cgit v1.2.3-70-g09d2


From 956bef7a41ff4a01c517dbe9304bf4b84a9d809d Mon Sep 17 00:00:00 2001
From: Tyler Schicke <tyler_schicke@brown.edu>
Date: Fri, 12 Jul 2019 13:44:57 -0400
Subject: fixed middle click to create new tab

---
 src/client/views/collections/CollectionDockingView.tsx | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

(limited to 'src/client/views/collections/CollectionDockingView.tsx')

diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx
index 1a7ccffa5..e5cee188f 100644
--- a/src/client/views/collections/CollectionDockingView.tsx
+++ b/src/client/views/collections/CollectionDockingView.tsx
@@ -430,7 +430,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
         stack.header.element[0].style.backgroundColor = DocServer.Control.isReadOnly() ? "#228540" : undefined;
         stack.header.element.on('mousedown', (e: any) => {
             if (e.target === stack.header.element[0] && e.button === 1) {
-                this.AddTab(stack, Docs.FreeformDocument([], { width: this.props.PanelWidth(), height: this.props.PanelHeight(), title: "Untitled Collection" }), undefined);
+                this.AddTab(stack, Docs.Create.FreeformDocument([], { width: this.props.PanelWidth(), height: this.props.PanelHeight(), title: "Untitled Collection" }), undefined);
             }
         });
         stack.header.controlsContainer.find('.lm_close') //get the close icon
-- 
cgit v1.2.3-70-g09d2


From 4db507d08249ccddf664798ab59c3b729c3d1065 Mon Sep 17 00:00:00 2001
From: Bob Zeleznik <zzzman@gmail.com>
Date: Fri, 12 Jul 2019 22:19:03 -0400
Subject: fixed a few youtube video things.  changed title of library doc.

---
 .../views/collections/CollectionDockingView.tsx    |  55 ++++++----
 .../views/collections/CollectionStackingView.tsx   |   2 +-
 src/client/views/nodes/VideoBox.scss               |  15 ++-
 src/client/views/nodes/VideoBox.tsx                | 116 +++++++++++++--------
 .../authentication/models/current_user_utils.ts    |   3 +-
 5 files changed, 116 insertions(+), 75 deletions(-)

(limited to 'src/client/views/collections/CollectionDockingView.tsx')

diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx
index e5cee188f..34b8eb8e1 100644
--- a/src/client/views/collections/CollectionDockingView.tsx
+++ b/src/client/views/collections/CollectionDockingView.tsx
@@ -1,6 +1,6 @@
 import 'golden-layout/src/css/goldenlayout-base.css';
 import 'golden-layout/src/css/goldenlayout-dark-theme.css';
-import { action, Lambda, observable, reaction } from "mobx";
+import { action, Lambda, observable, reaction, trace, computed } from "mobx";
 import { observer } from "mobx-react";
 import * as ReactDOM from 'react-dom';
 import Measure from "react-measure";
@@ -412,7 +412,9 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
                     }
                     SelectionManager.DeselectAll();
                 }
+                CollectionDockingView.Instance._ignoreStateChange = JSON.stringify(CollectionDockingView.Instance._goldenLayout.toConfig());
                 tab.contentItem.remove();
+                CollectionDockingView.Instance._ignoreStateChange = JSON.stringify(CollectionDockingView.Instance._goldenLayout.toConfig());
             });
     }
 
@@ -569,33 +571,40 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> {
             CollectionDockingView.Instance.AddTab(this._stack, doc, dataDoc);
         }
     }
-    get content() {
+    @computed get docView() {
         if (!this._document) {
             return (null);
         }
         let resolvedDataDoc = this._document.layout instanceof Doc ? this._document : this._dataDoc;
+        return <DocumentView key={this._document[Id]}
+            Document={this._document}
+            DataDoc={resolvedDataDoc}
+            bringToFront={emptyFunction}
+            addDocument={undefined}
+            removeDocument={undefined}
+            ContentScaling={this.contentScaling}
+            PanelWidth={this.nativeWidth}
+            PanelHeight={this.nativeHeight}
+            ScreenToLocalTransform={this.ScreenToLocalTransform}
+            renderDepth={0}
+            selectOnLoad={false}
+            parentActive={returnTrue}
+            whenActiveChanged={emptyFunction}
+            focus={emptyFunction}
+            addDocTab={this.addDocTab}
+            ContainingCollectionView={undefined}
+            zoomToScale={emptyFunction}
+            getScale={returnOne} />
+    }
+
+    @computed get content() {
+        if (!this._document) {
+            return (null);
+        }
         return (
             <div className="collectionDockingView-content" ref={this._mainCont}
                 style={{ transform: `translate(${this.previewPanelCenteringOffset}px, 0px) scale(${this.scaleToFitMultiplier})` }}>
-                <DocumentView key={this._document[Id]}
-                    Document={this._document}
-                    DataDoc={resolvedDataDoc}
-                    bringToFront={emptyFunction}
-                    addDocument={undefined}
-                    removeDocument={undefined}
-                    ContentScaling={this.contentScaling}
-                    PanelWidth={this.nativeWidth}
-                    PanelHeight={this.nativeHeight}
-                    ScreenToLocalTransform={this.ScreenToLocalTransform}
-                    renderDepth={0}
-                    selectOnLoad={false}
-                    parentActive={returnTrue}
-                    whenActiveChanged={emptyFunction}
-                    focus={emptyFunction}
-                    addDocTab={this.addDocTab}
-                    ContainingCollectionView={undefined}
-                    zoomToScale={emptyFunction}
-                    getScale={returnOne} />
+                {this.docView}
             </div >);
     }
 
@@ -604,7 +613,9 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> {
         let theContent = this.content;
         return !this._document ? (null) :
             <Measure offset onResize={action((r: any) => { this._panelWidth = r.offset.width; this._panelHeight = r.offset.height; })}>
-                {({ measureRef }) => <div ref={measureRef}>  {theContent} </div>}
+                {({ measureRef }) => <div ref={measureRef}>
+                    {theContent}
+                </div>}
             </Measure>;
     }
 }
\ No newline at end of file
diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx
index a84fd9cfe..fe01103d6 100644
--- a/src/client/views/collections/CollectionStackingView.tsx
+++ b/src/client/views/collections/CollectionStackingView.tsx
@@ -50,7 +50,7 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
     }
 
     overlays = (doc: Doc) => {
-        return doc.type === DocumentType.IMG ? { title: "title", caption: "caption" } : {};
+        return doc.type === DocumentType.IMG || doc.type === DocumentType.VID ? { title: "title", caption: "caption" } : {};
     }
 
     getDisplayDoc(layoutDoc: Doc, d: Doc, dxf: () => Transform) {
diff --git a/src/client/views/nodes/VideoBox.scss b/src/client/views/nodes/VideoBox.scss
index 55f2fe7c5..d651a8621 100644
--- a/src/client/views/nodes/VideoBox.scss
+++ b/src/client/views/nodes/VideoBox.scss
@@ -1,12 +1,17 @@
-.videoBox-cont, .videoBox-cont-interactive, .videoBox-cont-fullScreen {
-    width: 100%; 
+.videoBox-content-YouTube, .videoBox-content-YouTube-fullScreen,
+.videoBox-content, .videoBox-content-interactive, .videoBox-cont-fullScreen {
+    width: 100%;
+}
+
+.videoBox-content, .videoBox-content-interactive, .videoBox-content-fullScreen {
     height: Auto; 
 }
 
-.videoBox-cont-interactive {
-    pointer-events: all;
+.videoBox-content-YouTube, .videoBox-content-YouTube-fullScreen  {
+    height: 100%;
 }
 
-.videoBox-cont-fullScreen {
+.videoBox-content-interactive, .videoBox-content-fullScreen,
+.videoBox-content-YouTube-fullScreen  {
     pointer-events: all;
 }
\ No newline at end of file
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index 1b9138cfd..c9179db47 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -1,21 +1,18 @@
 import React = require("react");
-import { action, IReactionDisposer, observable, reaction } from "mobx";
+import { action, IReactionDisposer, observable, reaction, trace, computed } from "mobx";
 import { observer } from "mobx-react";
-import * as rp from "request-promise";
 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 { DocServer } from "../../DocServer";
+import { ContextMenu } from "../ContextMenu";
+import { ContextMenuProps } from "../ContextMenuItem";
 import { DocComponent } from "../DocComponent";
+import { InkingControl } from "../InkingControl";
 import { positionSchema } from "./DocumentView";
 import { FieldView, FieldViewProps } from './FieldView';
 import { pageSchema } from "./ImageBox";
 import "./VideoBox.scss";
-import { ContextMenu } from "../ContextMenu";
-import { ContextMenuProps } from "../ContextMenuItem";
-import { InkingControl } from "../InkingControl";
-import * as $ from "jquery";
+import { InkTool } from "../../../new_fields/InkField";
 
 type VideoDocument = makeInterface<[typeof positionSchema, typeof pageSchema]>;
 const VideoDocument = makeInterface(positionSchema, pageSchema);
@@ -23,6 +20,7 @@ const VideoDocument = makeInterface(positionSchema, pageSchema);
 @observer
 export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoDocument) {
     private _reactionDisposer?: IReactionDisposer;
+    private _youtubeReactionDisposer?: IReactionDisposer;
     private _youtubePlayer: any = undefined;
     private _videoRef: HTMLVideoElement | null = null;
     @observable _playTimer?: NodeJS.Timeout = undefined;
@@ -47,45 +45,37 @@ export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoD
 
     @action public Play() {
         this.Playing = true;
-        if (this.player) {
-            this.player.play();
-            if (!this._playTimer) this._playTimer = setInterval(this.updateTimecode, 500);
-        } else if (this._youtubePlayer) {
-            this._youtubePlayer.playVideo();
-            if (!this._playTimer) this._playTimer = setInterval(this.updateYoutubeTimecode, 1000);
+        this.player && this.player.play();
+        this._youtubePlayer && this._youtubePlayer.playVideo();
+        !this._playTimer && (this._playTimer = setInterval(this.updateTimecode, 500));
+        this._youtubeSeekTo = false;
+    }
+
+    @action public Seek(time: number) {
+        if (this._youtubePlayer && !this.Playing) {
+            this._youtubeSeekTo = true;
+            this._youtubePlayer.seekTo(time);
         }
     }
 
     @action public Pause() {
         this.Playing = false;
-        if (this.player) {
-            this.player.pause();
-            if (this._playTimer) {
-                clearInterval(this._playTimer);
-                this._playTimer = undefined;
-            }
-        } else if (this._youtubePlayer) {
-            // let interactive = InkingControl.Instance.selectedTool || !this.props.isSelected() ? "" : "-interactive";
-            // this._youtubePlayer.getIframe().style.pointerEvents = interactive ? "all" : "none";
-            this._youtubePlayer.pauseVideo();
-            if (this._playTimer) {
-                clearInterval(this._playTimer);
-                this._playTimer = undefined;
-            }
-        }
+        this.player && this.player.pause();
+        this._youtubePlayer && this._youtubePlayer.pauseVideo();
+        this._playTimer && clearInterval(this._playTimer);
+        this._playTimer = undefined;
+        this._youtubeSeekTo = false;
     }
 
     @action public FullScreen() {
         this._fullScreen = true;
         this.player && this.player.requestFullscreen();
+        this._youtubePlayer && this.props.addDocTab(this.props.Document, this.props.DataDoc, "inTab");
     }
 
     @action
     updateTimecode = () => {
         this.player && (this.props.Document.curPage = this.player.currentTime);
-    }
-    @action
-    updateYoutubeTimecode = () => {
         this._youtubePlayer && (this.props.Document.curPage = this._youtubePlayer.getCurrentTime());
     }
     componentDidMount() {
@@ -108,9 +98,15 @@ export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoD
                 videoId: videoid.toString(),
                 playerVars: { 'controls': VideoBox._showControls ? 1 : 0 },
                 events: {
-                    'onStateChange': this.onPlayerStateChange,
+                    'onStateChange': this.onYoutubePlayerStateChange,
+                    'onReady': this.onYoutubePlayerReady,
                 }
             });
+            this._reactionDisposer = reaction(() => this.props.Document.curPage, () => this.Seek(this.Document.curPage || 0), { fireImmediately: true });
+            this._youtubeReactionDisposer = reaction(() => [this.props.isSelected(), InkingControl.Instance.selectedTool], () => {
+                let interactive = InkingControl.Instance.selectedTool === InkTool.None && this.props.isSelected();
+                this._youtubePlayer.getIframe().style.pointerEvents = interactive ? "all" : "none";
+            }, { fireImmediately: true })
             // let iframe = $(document.getElementById(`${videoid}-player`)!);
             // iframe.on("load", function () {
             //     iframe.contents().find("head")
@@ -120,13 +116,30 @@ export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoD
     }
 
     @action
-    onPlayerStateChange = (event: any) => {
+    onYoutubePlayerStateChange = (event: any) => {
+        console.log("event.data = " + event.data);
         this.Playing = event.data == YT.PlayerState.PLAYING;
+        if (this._youtubeSeekTo && this.Playing) {
+            this._youtubePlayer.pauseVideo();
+            this._youtubeSeekTo = false;
+        } else this.Playing && !this._playTimer && (this._playTimer = setInterval(this.updateTimecode, 500));
+        event.data === YT.PlayerState.PAUSED && this._playTimer && clearInterval(this._playTimer);
+    }
+    _youtubeSeekTo = false;
+    @action
+    onYoutubePlayerReady = (event: any) => {
+        this.Playing = false;
+        this._youtubePlayer && (this._youtubePlayer.getIframe().style.pointerEvents = "none");
+        if (this.Document.curPage) {
+            this.Seek(this.Document.curPage);
+            this._youtubeSeekTo = true;
+        }
     }
 
     componentWillUnmount() {
         this.Pause();
-        if (this._reactionDisposer) this._reactionDisposer();
+        this._reactionDisposer && this._reactionDisposer();
+        this._youtubeReactionDisposer && this._youtubeReactionDisposer();
     }
 
     @action
@@ -143,6 +156,11 @@ export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoD
 
     @observable static _showControls: boolean = false;
 
+    @computed get youtubeVideoId() {
+        let field = Cast(this.Document[this.props.fieldKey], VideoField);
+        return field && field.url.href.indexOf("youtube") !== -1 ? ((arr: string[]) => arr[arr.length - 1])(field.url.href.split("/")) : "";
+    }
+
     specificContextMenu = (e: React.MouseEvent): void => {
         let field = Cast(this.Document[this.props.fieldKey], VideoField);
         if (field) {
@@ -152,19 +170,25 @@ export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoD
         }
     }
 
-    render() {
+    @computed get content() {
         let field = Cast(this.Document[this.props.fieldKey], VideoField);
         let interactive = InkingControl.Instance.selectedTool || !this.props.isSelected() ? "" : "-interactive";
-        let style = "videoBox-cont" + (this._fullScreen ? "-fullScreen" : interactive);
-        let videoid = field && field.url.href.indexOf("youtube") !== -1 ? ((arr: string[]) => arr[arr.length - 1])(field.url.href.split("/")) : "";
-
-        if (this._youtubePlayer) this._youtubePlayer.getIframe().style.pointerEvents = interactive ? "all" : "none";
+        let style = "videoBox-content" + (this._fullScreen ? "-fullScreen" : "") + interactive;
         return !field ? <div>Loading</div> :
-            videoid ?
-                <div id={`${videoid}-player`} className={`${style}`} style={{ height: "100%" }} /> :
-                <video className={`${style}`} ref={this.setVideoRef} onCanPlay={this.videoLoad} onContextMenu={this.specificContextMenu} controls={VideoBox._showControls}>
-                    <source src={field.url.href} type="video/mp4" />
-                    Not supported.
-                </video>;
+            <video className={`${style}`} ref={this.setVideoRef} onCanPlay={this.videoLoad} controls={VideoBox._showControls}>
+                <source src={field.url.href} type="video/mp4" />
+                Not supported.
+            </video>;
+    }
+
+    @computed get youtubeContent() {
+        let style = "videoBox-content-YouTube" + (this._fullScreen ? "-fullScreen" : "");
+        return <div id={`${this.youtubeVideoId}-player`} className={`${style}`} />;
+    }
+
+    render() {
+        return <div style={{ pointerEvents: "all", width: "100%", height: "100%" }} onContextMenu={this.specificContextMenu}>
+            {this.youtubeVideoId ? this.youtubeContent : this.content}
+        </div>;
     }
 }
\ No newline at end of file
diff --git a/src/server/authentication/models/current_user_utils.ts b/src/server/authentication/models/current_user_utils.ts
index 39a973e08..384c579de 100644
--- a/src/server/authentication/models/current_user_utils.ts
+++ b/src/server/authentication/models/current_user_utils.ts
@@ -10,7 +10,7 @@ import { CollectionView } from "../../../client/views/collections/CollectionView
 import { Doc } from "../../../new_fields/Doc";
 import { List } from "../../../new_fields/List";
 import { listSpec } from "../../../new_fields/Schema";
-import { Cast, FieldValue } from "../../../new_fields/Types";
+import { Cast, FieldValue, StrCast } from "../../../new_fields/Types";
 import { RouteStore } from "../../RouteStore";
 
 export class CurrentUserUtils {
@@ -69,6 +69,7 @@ export class CurrentUserUtils {
             sidebar.boxShadow = "1 1 3";
             doc.sidebar = sidebar;
         }
+        StrCast(doc.title).indexOf("@") !== -1 && (doc.title = StrCast(doc.title).split("@")[0] + "'s Library");
 
     }
 
-- 
cgit v1.2.3-70-g09d2


From 5ee47fa3533c769364c91561a0be5ee388f855d3 Mon Sep 17 00:00:00 2001
From: Bob Zeleznik <zzzman@gmail.com>
Date: Fri, 12 Jul 2019 23:49:37 -0400
Subject: fixed resizing youtube videos.  changed dockingview to not call
 setupGoldenLayout on  dropping or closing a tab.

---
 src/client/views/collections/CollectionDockingView.tsx | 7 ++++++-
 src/client/views/nodes/VideoBox.tsx                    | 5 +++--
 2 files changed, 9 insertions(+), 3 deletions(-)

(limited to 'src/client/views/collections/CollectionDockingView.tsx')

diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx
index 34b8eb8e1..fe8288b28 100644
--- a/src/client/views/collections/CollectionDockingView.tsx
+++ b/src/client/views/collections/CollectionDockingView.tsx
@@ -298,7 +298,10 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
     onPointerUp = (e: React.PointerEvent): void => {
         if (this._flush) {
             this._flush = false;
-            setTimeout(() => this.stateChanged(), 10);
+            setTimeout(() => {
+                CollectionDockingView.Instance._ignoreStateChange = JSON.stringify(CollectionDockingView.Instance._goldenLayout.toConfig());
+                this.stateChanged()
+            }, 10);
         }
     }
     @action
@@ -342,6 +345,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
     }
 
     itemDropped = () => {
+        CollectionDockingView.Instance._ignoreStateChange = JSON.stringify(CollectionDockingView.Instance._goldenLayout.toConfig());
         this.stateChanged();
     }
 
@@ -357,6 +361,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
             if (tab.contentItem.config.fixed) {
                 tab.contentItem.parent.config.fixed = true;
             }
+
             let doc = await DocServer.GetRefField(tab.contentItem.config.props.documentId) as Doc;
             let dataDoc = await DocServer.GetRefField(tab.contentItem.config.props.dataDocumentId) as Doc;
             if (doc instanceof Doc) {
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index 861e91e41..e86348241 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -13,6 +13,7 @@ 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);
@@ -103,8 +104,8 @@ export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoD
                 }
             });
             this._reactionDisposer = reaction(() => this.props.Document.curPage, () => this.Seek(this.Document.curPage || 0), { fireImmediately: true });
-            this._youtubeReactionDisposer = reaction(() => [this.props.isSelected(), InkingControl.Instance.selectedTool], () => {
-                let interactive = InkingControl.Instance.selectedTool === InkTool.None && this.props.isSelected();
+            this._youtubeReactionDisposer = reaction(() => [this.props.isSelected(), DocumentDecorations.Instance.Interacting, InkingControl.Instance.selectedTool], () => {
+                let interactive = InkingControl.Instance.selectedTool === InkTool.None && this.props.isSelected() && !DocumentDecorations.Instance.Interacting;
                 this._youtubePlayer.getIframe().style.pointerEvents = interactive ? "all" : "none";
             }, { fireImmediately: true })
             // let iframe = $(document.getElementById(`${videoid}-player`)!);
-- 
cgit v1.2.3-70-g09d2


From 66f0d72f07d3cf28e01abd0d9b2659c951defcd9 Mon Sep 17 00:00:00 2001
From: Tyler Schicke <tyler_schicke@brown.edu>
Date: Sun, 14 Jul 2019 20:16:09 -0400
Subject: Added closed stack's tabs to recently closed docs

---
 src/client/views/collections/CollectionDockingView.tsx | 14 ++++++++++----
 1 file changed, 10 insertions(+), 4 deletions(-)

(limited to 'src/client/views/collections/CollectionDockingView.tsx')

diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx
index fe8288b28..781bafec0 100644
--- a/src/client/views/collections/CollectionDockingView.tsx
+++ b/src/client/views/collections/CollectionDockingView.tsx
@@ -412,8 +412,10 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
                 if (doc instanceof Doc) {
                     let theDoc = doc;
                     CollectionDockingView.Instance._removedDocs.push(theDoc);
-                    if (CurrentUserUtils.UserDocument.recentlyClosed instanceof Doc) {
-                        Doc.AddDocToList(CurrentUserUtils.UserDocument.recentlyClosed, "data", doc, undefined, true, true);
+
+                    const recent = await Cast(CurrentUserUtils.UserDocument.recentlyClosed, Doc);
+                    if (recent) {
+                        Doc.AddDocToList(recent, "data", doc, undefined, true, true);
                     }
                     SelectionManager.DeselectAll();
                 }
@@ -442,12 +444,16 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
         });
         stack.header.controlsContainer.find('.lm_close') //get the close icon
             .off('click') //unbind the current click handler
-            .click(action(function () {
+            .click(action(async function () {
                 //if (confirm('really close this?')) {
+                const recent = await Cast(CurrentUserUtils.UserDocument.recentlyClosed, Doc);
                 stack.remove();
-                stack.contentItems.map(async (contentItem: any) => {
+                stack.contentItems.forEach(async (contentItem: any) => {
                     let doc = await DocServer.GetRefField(contentItem.config.props.documentId);
                     if (doc instanceof Doc) {
+                        if (recent) {
+                            Doc.AddDocToList(recent, "data", doc, undefined, true, true);
+                        }
                         let theDoc = doc;
                         CollectionDockingView.Instance._removedDocs.push(theDoc);
                     }
-- 
cgit v1.2.3-70-g09d2


From c9bf8852a77298e1e958cfc72559418b6cb5981a Mon Sep 17 00:00:00 2001
From: bob <bcz@cs.brown.edu>
Date: Mon, 15 Jul 2019 11:57:23 -0400
Subject: tweaks

---
 src/client/util/TooltipTextMenu.tsx                    | 2 +-
 src/client/views/collections/CollectionDockingView.tsx | 1 +
 src/server/Search.ts                                   | 2 +-
 3 files changed, 3 insertions(+), 2 deletions(-)

(limited to 'src/client/views/collections/CollectionDockingView.tsx')

diff --git a/src/client/util/TooltipTextMenu.tsx b/src/client/util/TooltipTextMenu.tsx
index 0d3adef2e..9ba339285 100644
--- a/src/client/util/TooltipTextMenu.tsx
+++ b/src/client/util/TooltipTextMenu.tsx
@@ -243,7 +243,7 @@ export class TooltipTextMenu {
                         handlers: {
                             dragComplete: action(() => {
                                 let m = dragData.droppedDocuments;
-                                this.makeLink(DocServer.prepend("/doc/" + m[0][Id]));
+                                m.length && this.makeLink(DocServer.prepend("/doc/" + m[0][Id]));
                             }),
                         },
                         hideSource: false
diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx
index 781bafec0..1069ebbdb 100644
--- a/src/client/views/collections/CollectionDockingView.tsx
+++ b/src/client/views/collections/CollectionDockingView.tsx
@@ -260,6 +260,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
                         // Because this is in a set timeout, if this component unmounts right after mounting,
                         // we will leak a GoldenLayout, because we try to destroy it before we ever create it
                         setTimeout(() => this.setupGoldenLayout(), 1);
+                        DocListCast((CurrentUserUtils.UserDocument.workspaces as Doc).data).map(d => d.workspaceBrush = false);
                         this.props.Document.workspaceBrush = true;
                     }
                     this._ignoreStateChange = "";
diff --git a/src/server/Search.ts b/src/server/Search.ts
index 98f421937..11092c5be 100644
--- a/src/server/Search.ts
+++ b/src/server/Search.ts
@@ -26,7 +26,7 @@ export class Search {
             });
             return res;
         } catch (e) {
-            // console.warn("Search error: " + e + document);
+            // console.warn("Search error: ", e, documents);
         }
     }
 
-- 
cgit v1.2.3-70-g09d2


From 533c48010ca668a944962bd61cd65115fc5a6fa6 Mon Sep 17 00:00:00 2001
From: Sam Wilkins <samuel_wilkins@brown.edu>
Date: Tue, 16 Jul 2019 12:04:21 -0400
Subject: linter and error fixes

---
 src/client/documents/Documents.ts                      | 2 +-
 src/client/views/InkingControl.tsx                     | 4 ++--
 src/client/views/Main.tsx                              | 2 +-
 src/client/views/collections/CollectionDockingView.tsx | 4 ++--
 src/client/views/collections/CollectionTreeView.tsx    | 2 +-
 src/client/views/nodes/ImageBox.tsx                    | 2 +-
 src/client/views/nodes/VideoBox.tsx                    | 4 ++--
 src/client/views/nodes/WebBox.tsx                      | 2 +-
 src/client/views/search/SearchBox.tsx                  | 2 +-
 9 files changed, 12 insertions(+), 12 deletions(-)

(limited to 'src/client/views/collections/CollectionDockingView.tsx')

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index ada9f3610..177810444 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -98,7 +98,7 @@ export namespace Docs {
 
         type LayoutSource = { LayoutString: () => string };
         type CollectionLayoutSource = { LayoutString: (fieldStr: string, fieldExt?: string) => string };
-        type CollectionViewType = [CollectionLayoutSource, string, string?]
+        type CollectionViewType = [CollectionLayoutSource, string, string?];
         type PrototypeTemplate = {
             layout: {
                 view: LayoutSource,
diff --git a/src/client/views/InkingControl.tsx b/src/client/views/InkingControl.tsx
index c7f7bdb66..1910e409b 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 { ColorResult } from 'react-color';
+import { ColorState } 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: ColorResult): void => {
+    switchColor = action((color: ColorState): 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/Main.tsx b/src/client/views/Main.tsx
index 80399e24b..86578af3e 100644
--- a/src/client/views/Main.tsx
+++ b/src/client/views/Main.tsx
@@ -26,7 +26,7 @@ let swapDocs = async () => {
         }
         CurrentUserUtils.UserDocument.linkManagerDoc = undefined;
     }
-}
+};
 
 (async () => {
     const info = await CurrentUserUtils.loadCurrentUser();
diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx
index 1069ebbdb..a193ff677 100644
--- a/src/client/views/collections/CollectionDockingView.tsx
+++ b/src/client/views/collections/CollectionDockingView.tsx
@@ -301,7 +301,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
             this._flush = false;
             setTimeout(() => {
                 CollectionDockingView.Instance._ignoreStateChange = JSON.stringify(CollectionDockingView.Instance._goldenLayout.toConfig());
-                this.stateChanged()
+                this.stateChanged();
             }, 10);
         }
     }
@@ -606,7 +606,7 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> {
             addDocTab={this.addDocTab}
             ContainingCollectionView={undefined}
             zoomToScale={emptyFunction}
-            getScale={returnOne} />
+            getScale={returnOne} />;
     }
 
     @computed get content() {
diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx
index 0196fecff..c212cc97c 100644
--- a/src/client/views/collections/CollectionTreeView.tsx
+++ b/src/client/views/collections/CollectionTreeView.tsx
@@ -153,7 +153,7 @@ class TreeView extends React.Component<TreeViewProps> {
         let docList = Cast(this.resolvedDataDoc[this.fieldKey], listSpec(Doc));
         let doc = Cast(this.resolvedDataDoc[this.fieldKey], Doc);
         let isDoc = doc instanceof Doc || docList;
-        let c
+        let c;
         return <div className="bullet" onClick={action(() => this._collapsed = !this._collapsed)} style={{ color: StrCast(this.props.document.color, "black"), opacity: 0.4 }}>
             {<FontAwesomeIcon icon={this._collapsed ? (isDoc ? "caret-square-right" : "caret-right") : (isDoc ? "caret-square-down" : "caret-down")} />}
         </div>;
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 73ae8955d..1df955f1f 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -33,7 +33,7 @@ export const pageSchema = createSchema({
     curPage: "number",
 });
 
-interface window {
+interface Window {
     MediaRecorder: MediaRecorder;
 }
 
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index 66844cdd6..e90efb31c 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -108,7 +108,7 @@ export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoD
             this._youtubeReactionDisposer = reaction(() => [this.props.isSelected(), DocumentDecorations.Instance.Interacting, InkingControl.Instance.selectedTool], () => {
                 let interactive = InkingControl.Instance.selectedTool === InkTool.None && this.props.isSelected() && !DocumentDecorations.Instance.Interacting;
                 this._youtubePlayer.getIframe().style.pointerEvents = interactive ? "all" : "none";
-            }, { fireImmediately: true })
+            }, { fireImmediately: true });
             // let iframe = $(document.getElementById(`${videoid}-player`)!);
             // iframe.on("load", function () {
             //     iframe.contents().find("head")
@@ -120,7 +120,7 @@ export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoD
     @action
     onYoutubePlayerStateChange = (event: any) => {
         console.log("event.data = " + event.data);
-        this.Playing = event.data == YT.PlayerState.PLAYING;
+        this.Playing = event.data === YT.PlayerState.PLAYING;
         if (this._youtubeSeekTo && this.Playing) {
             this._youtubePlayer.pauseVideo();
             this._youtubeSeekTo = false;
diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx
index 96b972a1c..f0a9ec6d8 100644
--- a/src/client/views/nodes/WebBox.tsx
+++ b/src/client/views/nodes/WebBox.tsx
@@ -19,7 +19,7 @@ export function onYouTubeIframeAPIReady() {
     });
 }
 // must cast as any to set property on window
-const _global = (window /* browser */ || global /* node */) as any
+const _global = (window /* browser */ || global /* node */) as any;
 _global.onYouTubeIframeAPIReady = onYouTubeIframeAPIReady;
 
 function onPlayerReady(event: any) {
diff --git a/src/client/views/search/SearchBox.tsx b/src/client/views/search/SearchBox.tsx
index 3830a6d16..ec778b346 100644
--- a/src/client/views/search/SearchBox.tsx
+++ b/src/client/views/search/SearchBox.tsx
@@ -282,7 +282,7 @@ export class SearchBox extends React.Component {
 
     @computed
     get resFull() {
-        console.log(this._numTotalResults)
+        console.log(this._numTotalResults);
         return this._numTotalResults <= 8;
     }
 
-- 
cgit v1.2.3-70-g09d2


From 5aa6bcdd4e23bb9e9d05181d0dc6b638e45e397b Mon Sep 17 00:00:00 2001
From: Tyler Schicke <tyler_schicke@brown.edu>
Date: Wed, 17 Jul 2019 20:48:14 -0400
Subject: Moved DocServer.prepend and changed how corsProxy works

---
 src/Utils.ts                                       | 13 +++++
 src/client/DocServer.ts                            |  8 ----
 src/client/util/History.ts                         |  2 +-
 .../util/Import & Export/DirectoryImportBox.tsx    |  4 +-
 src/client/util/SearchUtil.ts                      |  3 +-
 src/client/util/TooltipTextMenu.tsx                |  8 ++--
 src/client/views/MainView.tsx                      |  4 +-
 src/client/views/SearchBox.tsx                     |  3 +-
 .../views/collections/CollectionDockingView.tsx    |  2 +-
 src/client/views/collections/CollectionSubView.tsx |  7 +--
 src/client/views/nodes/DocumentView.tsx            |  4 +-
 src/client/views/nodes/FormattedTextBox.tsx        |  5 +-
 src/client/views/nodes/ImageBox.tsx                |  6 +--
 src/client/views/nodes/VideoBox.tsx                |  4 +-
 src/client/views/pdf/PDFViewer.tsx                 | 55 +++++-----------------
 src/client/views/search/SearchBox.tsx              |  6 +--
 src/mobile/ImageUpload.tsx                         |  5 +-
 .../authentication/models/current_user_utils.ts    |  5 +-
 src/server/index.ts                                |  2 +-
 19 files changed, 65 insertions(+), 81 deletions(-)

(limited to 'src/client/views/collections/CollectionDockingView.tsx')

diff --git a/src/Utils.ts b/src/Utils.ts
index e8a80bdc3..ac6d127cc 100644
--- a/src/Utils.ts
+++ b/src/Utils.ts
@@ -2,6 +2,7 @@ import v4 = require('uuid/v4');
 import v5 = require("uuid/v5");
 import { Socket } from 'socket.io';
 import { Message } from './server/Message';
+import { RouteStore } from './server/RouteStore';
 
 export class Utils {
 
@@ -27,6 +28,18 @@ export class Utils {
         return { scale, translateX, translateY };
     }
 
+    /**
+     * A convenience method. Prepends the full path (i.e. http://localhost:1050) to the
+     * requested extension
+     * @param extension the specified sub-path to append to the window origin
+     */
+    public static prepend(extension: string): string {
+        return window.location.origin + extension;
+    }
+    public static CorsProxy(url: string): string {
+        return this.prepend(RouteStore.corsProxy + "/") + encodeURIComponent(url);
+    }
+
     public static CopyText(text: string) {
         var textArea = document.createElement("textarea");
         textArea.value = text;
diff --git a/src/client/DocServer.ts b/src/client/DocServer.ts
index 6737657c8..8c64d2b2f 100644
--- a/src/client/DocServer.ts
+++ b/src/client/DocServer.ts
@@ -47,14 +47,6 @@ export namespace DocServer {
         Utils.AddServerHandler(_socket, MessageStore.DeleteField, respondToDelete);
         Utils.AddServerHandler(_socket, MessageStore.DeleteFields, respondToDelete);
     }
-    /**
-     * A convenience method. Prepends the full path (i.e. http://localhost:1050) to the
-     * requested extension
-     * @param extension the specified sub-path to append to the window origin
-     */
-    export function prepend(extension: string): string {
-        return window.location.origin + extension;
-    }
 
     function errorFunc(): never {
         throw new Error("Can't use DocServer without calling init first");
diff --git a/src/client/util/History.ts b/src/client/util/History.ts
index cbf5b3fc8..e9ff21b22 100644
--- a/src/client/util/History.ts
+++ b/src/client/util/History.ts
@@ -129,7 +129,7 @@ export namespace HistoryUtil {
 
     function addStringifier(type: string, keys: string[], customStringifier?: (state: ParsedUrl, current: string) => string) {
         stringifiers[type] = state => {
-            let path = DocServer.prepend(`/${type}`);
+            let path = Utils.prepend(`/${type}`);
             if (customStringifier) {
                 path = customStringifier(state, path);
             }
diff --git a/src/client/util/Import & Export/DirectoryImportBox.tsx b/src/client/util/Import & Export/DirectoryImportBox.tsx
index c096e9ceb..75b0b52a7 100644
--- a/src/client/util/Import & Export/DirectoryImportBox.tsx	
+++ b/src/client/util/Import & Export/DirectoryImportBox.tsx	
@@ -98,12 +98,12 @@ export default class DirectoryImportBox extends React.Component<FieldViewProps>
 
             runInAction(() => this.remaining++);
 
-            let prom = fetch(DocServer.prepend(RouteStore.upload), {
+            let prom = fetch(Utils.prepend(RouteStore.upload), {
                 method: 'POST',
                 body: formData
             }).then(async (res: Response) => {
                 (await res.json()).map(action((file: any) => {
-                    let docPromise = Docs.Get.DocumentFromType(type, DocServer.prepend(file), { nativeWidth: 300, width: 300, title: dropFileName });
+                    let docPromise = Docs.Get.DocumentFromType(type, Utils.prepend(file), { nativeWidth: 300, width: 300, title: dropFileName });
                     docPromise.then(doc => {
                         doc && docs.push(doc) && runInAction(() => this.remaining--);
                     });
diff --git a/src/client/util/SearchUtil.ts b/src/client/util/SearchUtil.ts
index abf1a7c32..ee5a83710 100644
--- a/src/client/util/SearchUtil.ts
+++ b/src/client/util/SearchUtil.ts
@@ -2,6 +2,7 @@ import * as rp from 'request-promise';
 import { DocServer } from '../DocServer';
 import { Doc } from '../../new_fields/Doc';
 import { Id } from '../../new_fields/FieldSymbols';
+import { Utils } from '../../Utils';
 
 export namespace SearchUtil {
     export type HighlightingResult = { [id: string]: { [key: string]: string[] } };
@@ -29,7 +30,7 @@ export namespace SearchUtil {
     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"), {
+        const result: IdSearchResult = JSON.parse(await rp.get(Utils.prepend("/search"), {
             qs: { ...options, q: query },
         }));
         if (!returnDocs) {
diff --git a/src/client/util/TooltipTextMenu.tsx b/src/client/util/TooltipTextMenu.tsx
index 309d19016..a4c053de2 100644
--- a/src/client/util/TooltipTextMenu.tsx
+++ b/src/client/util/TooltipTextMenu.tsx
@@ -19,7 +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";
+import { Utils } from "../../Utils";
 
 //appears above a selection of text in a RichTextBox to give user options such as Bold, Italics, etc.
 export class TooltipTextMenu {
@@ -213,8 +213,8 @@ export class TooltipTextMenu {
                 let link = node && node.marks.find(m => m.type.name === "link");
                 if (link) {
                     let href: string = link.attrs.href;
-                    if (href.indexOf(DocServer.prepend("/doc/")) === 0) {
-                        let docid = href.replace(DocServer.prepend("/doc/"), "");
+                    if (href.indexOf(Utils.prepend("/doc/")) === 0) {
+                        let docid = href.replace(Utils.prepend("/doc/"), "");
                         DocServer.GetRefField(docid).then(action((f: Opt<Field>) => {
                             if (f instanceof Doc) {
                                 if (DocumentManager.Instance.getDocumentView(f)) {
@@ -254,7 +254,7 @@ export class TooltipTextMenu {
                                 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");
+                                linkDoc instanceof Doc && this.makeLink(Utils.prepend("/doc/" + linkDoc[Id]), ctrlKey ? "onRight" : "inTab");
                             }),
                         },
                         hideSource: false
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index ce7369220..94a4835a1 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -16,7 +16,7 @@ import { listSpec } from '../../new_fields/Schema';
 import { Cast, FieldValue, NumCast, BoolCast, StrCast } from '../../new_fields/Types';
 import { CurrentUserUtils } from '../../server/authentication/models/current_user_utils';
 import { RouteStore } from '../../server/RouteStore';
-import { emptyFunction, returnOne, returnTrue } from '../../Utils';
+import { emptyFunction, returnOne, returnTrue, Utils } from '../../Utils';
 import { DocServer } from '../DocServer';
 import { Docs } from '../documents/Documents';
 import { SetupDrag } from '../util/DragManager';
@@ -433,7 +433,7 @@ export class MainView extends React.Component {
         return [
             this.isSearchVisible ? <div className="main-searchDiv" key="search" style={{ top: '34px', right: '1px', position: 'absolute' }} > <FilterBox /> </div> : null,
             <div className="main-buttonDiv" key="logout" style={{ bottom: '0px', right: '1px', position: 'absolute' }} ref={logoutRef}>
-                <button onClick={() => window.location.assign(DocServer.prepend(RouteStore.logout))}>Log Out</button></div>
+                <button onClick={() => window.location.assign(Utils.prepend(RouteStore.logout))}>Log Out</button></div>
         ];
 
     }
diff --git a/src/client/views/SearchBox.tsx b/src/client/views/SearchBox.tsx
index 1b9be841f..33cb63df5 100644
--- a/src/client/views/SearchBox.tsx
+++ b/src/client/views/SearchBox.tsx
@@ -13,6 +13,7 @@ import { Docs } from '../documents/Documents';
 import { SetupDrag } from '../util/DragManager';
 import { SearchItem } from './search/SearchItem';
 import "./SearchBox.scss";
+import { Utils } from '../../Utils';
 
 library.add(faSearch);
 library.add(faObjectGroup);
@@ -47,7 +48,7 @@ export class SearchBox extends React.Component {
 
     @action
     getResults = async (query: string) => {
-        let response = await rp.get(DocServer.prepend('/search'), {
+        let response = await rp.get(Utils.prepend('/search'), {
             qs: {
                 query
             }
diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx
index a193ff677..ba7903419 100644
--- a/src/client/views/collections/CollectionDockingView.tsx
+++ b/src/client/views/collections/CollectionDockingView.tsx
@@ -465,7 +465,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
             .off('click') //unbind the current click handler
             .click(action(function () {
                 stack.config.fixed = !stack.config.fixed;
-                // var url = DocServer.prepend("/doc/" + stack.contentItems[0].tab.contentItem.config.props.documentId);
+                // var url = Utils.prepend("/doc/" + stack.contentItems[0].tab.contentItem.config.props.documentId);
                 // let win = window.open(url, stack.contentItems[0].tab.title, "width=300,height=400");
             }));
     }
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index 5d5189336..2ddefb3c0 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -20,6 +20,7 @@ import { CollectionVideoView } from "./CollectionVideoView";
 import { CollectionView } from "./CollectionView";
 import React = require("react");
 import { MainView } from "../MainView";
+import { Utils } from "../../../Utils";
 
 export interface CollectionViewProps extends FieldViewProps {
     addDocument: (document: Doc, allowDuplicates?: boolean) => boolean;
@@ -164,7 +165,7 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
                 } else {
                     let path = window.location.origin + "/doc/";
                     if (text.startsWith(path)) {
-                        let docid = text.replace(DocServer.prepend("/doc/"), "").split("?")[0];
+                        let docid = text.replace(Utils.prepend("/doc/"), "").split("?")[0];
                         DocServer.GetRefField(docid).then(f => {
                             if (f instanceof Doc) {
                                 if (options.x || options.y) { f.x = options.x; f.y = options.y; } // should be in CollectionFreeFormView
@@ -193,7 +194,7 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
                 if (item.kind === "string" && item.type.indexOf("uri") !== -1) {
                     let str: string;
                     let prom = new Promise<string>(resolve => e.dataTransfer.items[i].getAsString(resolve))
-                        .then(action((s: string) => rp.head(DocServer.prepend(RouteStore.corsProxy + "/" + (str = s)))))
+                        .then(action((s: string) => rp.head(Utils.CorsProxy(str = s))))
                         .then(result => {
                             let type = result["content-type"];
                             if (type) {
@@ -219,7 +220,7 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
                     }).then(async (res: Response) => {
                         (await res.json()).map(action((file: any) => {
                             let full = { ...options, nativeWidth: type.indexOf("video") !== -1 ? 600 : 300, width: 300, title: dropFileName };
-                            let path = DocServer.prepend(file);
+                            let path = Utils.prepend(file);
                             Docs.Get.DocumentFromType(type, path, full).then(doc => doc && this.props.addDocument(doc));
                         }));
                     });
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 245dd319d..af1cc079b 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -557,13 +557,13 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
         });
         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 URL", event: () => Utils.CopyText(Utils.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" });
         type User = { email: string, userDocumentId: string };
         let usersMenu: ContextMenuProps[] = [];
         try {
-            let stuff = await rp.get(DocServer.prepend(RouteStore.getUsers));
+            let stuff = await rp.get(Utils.prepend(RouteStore.getUsers));
             const users: User[] = JSON.parse(stuff);
             usersMenu = users.filter(({ email }) => email !== CurrentUserUtils.email).map(({ email, userDocumentId }) => ({
                 description: email, event: async () => {
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index 066cc40e2..99801ecff 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -35,6 +35,7 @@ import "./FormattedTextBox.scss";
 import React = require("react");
 import { DateField } from '../../../new_fields/DateField';
 import { thisExpression } from 'babel-types';
+import { Utils } from '../../../Utils';
 
 library.add(faEdit);
 library.add(faSmile);
@@ -311,8 +312,8 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
                 href = parent.childNodes[0].href ? parent.childNodes[0].href : parent.href;
             }
             if (href) {
-                if (href.indexOf(DocServer.prepend("/doc/")) === 0) {
-                    this._linkClicked = href.replace(DocServer.prepend("/doc/"), "").split("?")[0];
+                if (href.indexOf(Utils.prepend("/doc/")) === 0) {
+                    this._linkClicked = href.replace(Utils.prepend("/doc/"), "").split("?")[0];
                     if (this._linkClicked) {
                         DocServer.GetRefField(this._linkClicked).then(async linkDoc => {
                             if (linkDoc instanceof Doc) {
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index ebe3732e6..4ffd265d7 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -165,12 +165,12 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD
             recorder.ondataavailable = async function (e: any) {
                 const formData = new FormData();
                 formData.append("file", e.data);
-                const res = await fetch(DocServer.prepend(RouteStore.upload), {
+                const res = await fetch(Utils.prepend(RouteStore.upload), {
                     method: 'POST',
                     body: formData
                 });
                 const files = await res.json();
-                const url = DocServer.prepend(files[0]);
+                const url = Utils.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;
@@ -327,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[] = [window.origin + RouteStore.corsProxy + "/" + "http://www.cs.brown.edu/~bcz/noImage.png"];
+        let paths: string[] = [Utils.CorsProxy("http://www.cs.brown.edu/~bcz/noImage.png")];
         // this._curSuffix = "";
         // if (w > 20) {
         Doc.UpdateDocumentExtensionForField(this.dataDoc, this.props.fieldKey);
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index 8b8e500c4..0f2d18f6b 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -122,7 +122,7 @@ export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoD
 
     public static async convertDataUri(imageUri: string, returnedFilename: string) {
         try {
-            let posting = DocServer.prepend(RouteStore.dataUriToImage);
+            let posting = Utils.prepend(RouteStore.dataUriToImage);
             const returnedUri = await rp.post(posting, {
                 body: {
                     uri: imageUri,
@@ -164,7 +164,7 @@ export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoD
                     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 url = Utils.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-"
diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx
index 699a1ffd7..b7ded7e06 100644
--- a/src/client/views/pdf/PDFViewer.tsx
+++ b/src/client/views/pdf/PDFViewer.tsx
@@ -4,23 +4,18 @@ import * as Pdfjs from "pdfjs-dist";
 import "pdfjs-dist/web/pdf_viewer.css";
 import * as rp from "request-promise";
 import { Dictionary } from "typescript-collections";
-import { Doc, DocListCast, HeightSym, Opt, WidthSym } from "../../../new_fields/Doc";
+import { Doc, DocListCast, Opt } from "../../../new_fields/Doc";
 import { Id } from "../../../new_fields/FieldSymbols";
 import { List } from "../../../new_fields/List";
-import { BoolCast, Cast, NumCast, StrCast, FieldValue } from "../../../new_fields/Types";
-import { emptyFunction } from "../../../Utils";
-import { DocServer } from "../../DocServer";
-import { Docs, DocUtils, DocumentOptions } from "../../documents/Documents";
-import { DocumentManager } from "../../util/DocumentManager";
+import { Cast, NumCast, StrCast } from "../../../new_fields/Types";
+import { emptyFunction, Utils } from "../../../Utils";
+import { Docs, DocUtils } from "../../documents/Documents";
 import { DragManager } from "../../util/DragManager";
-import { DocumentView } from "../nodes/DocumentView";
-import { PDFBox, handleBackspace } from "../nodes/PDFBox";
+import { PDFBox } from "../nodes/PDFBox";
 import Page from "./Page";
 import "./PDFViewer.scss";
 import React = require("react");
-import PDFMenu from "./PDFMenu";
-import { UndoManager } from "../../util/UndoManager";
-import { CompileScript, CompiledScript, CompileResult } from "../../util/Scripting";
+import { CompileScript, CompileResult } from "../../util/Scripting";
 import { ScriptField } from "../../../new_fields/ScriptField";
 import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
 import Annotation from "./Annotation";
@@ -90,16 +85,12 @@ export class Viewer extends React.Component<IViewerProps> {
     private _annotationReactionDisposer?: IReactionDisposer;
     private _dropDisposer?: DragManager.DragDropDisposer;
     private _filterReactionDisposer?: IReactionDisposer;
-    private _activeReactionDisposer?: IReactionDisposer;
     private _viewer: React.RefObject<HTMLDivElement>;
     private _mainCont: React.RefObject<HTMLDivElement>;
     private _pdfViewer: any;
     // private _textContent: Pdfjs.TextContent[] = [];
     private _pdfFindController: any;
     private _searchString: string = "";
-    private _rendered: boolean = false;
-    private _pageIndex: number = -1;
-    private _matchIndex: number = 0;
 
     constructor(props: IViewerProps) {
         super(props);
@@ -135,23 +126,6 @@ export class Viewer extends React.Component<IViewerProps> {
             },
             { fireImmediately: true });
 
-        this._activeReactionDisposer = reaction(
-            () => this.props.parent.props.active(),
-            () => {
-                runInAction(() => {
-                    if (!this.props.parent.props.active()) {
-                        this._searching = false;
-                        this._pdfFindController = null;
-                        if (this._viewer.current) {
-                            let cns = this._viewer.current.childNodes;
-                            for (let i = cns.length - 1; i >= 0; i--) {
-                                cns.item(i).remove();
-                            }
-                        }
-                    }
-                });
-            }
-        );
 
         if (this.props.parent.props.ContainingCollectionView) {
             this._filterReactionDisposer = reaction(
@@ -346,7 +320,7 @@ export class Viewer extends React.Component<IViewerProps> {
             this._isPage[page] = "image";
             const address = this.props.url;
             try {
-                let res = JSON.parse(await rp.get(DocServer.prepend(`/thumbnail${address.substring("files/".length, address.length - ".pdf".length)}-${page + 1}.PNG`)));
+                let res = JSON.parse(await rp.get(Utils.prepend(`/thumbnail${address.substring("files/".length, address.length - ".pdf".length)}-${page + 1}.PNG`)));
                 runInAction(() => this._visibleElements[page] =
                     <img key={res.path} src={res.path} onError={handleError}
                         style={{ width: `${parseInt(res.width) * scale}px`, height: `${parseInt(res.height) * scale}px` }} />);
@@ -476,7 +450,6 @@ export class Viewer extends React.Component<IViewerProps> {
                             phraseSearch: true,
                             query: searchString
                         });
-                    this._rendered = true;
                 });
                 container.addEventListener("pagerendered", () => {
                     console.log("rendered");
@@ -488,7 +461,6 @@ export class Viewer extends React.Component<IViewerProps> {
                             phraseSearch: true,
                             query: searchString
                         });
-                    this._rendered = true;
                 });
             }
         }
@@ -563,7 +535,6 @@ export class Viewer extends React.Component<IViewerProps> {
                     });
                     container.addEventListener("pagerendered", () => {
                         console.log("rendered");
-                        this._rendered = true;
                     });
                     this._pdfViewer.setDocument(this.props.pdf);
                     this._pdfFindController = new PDFJSViewer.PDFFindController(this._pdfViewer);
@@ -703,17 +674,17 @@ class SimpleLinkService {
     externalLinkRel: any = null;
     pdf: any = null;
 
-    navigateTo(dest: any) { }
+    navigateTo() { }
 
-    getDestinationHash(dest: any) { return "#"; }
+    getDestinationHash() { return "#"; }
 
-    getAnchorUrl(hash: any) { return "#"; }
+    getAnchorUrl() { return "#"; }
 
-    setHash(hash: any) { }
+    setHash() { }
 
-    executeNamedAction(action: any) { }
+    executeNamedAction() { }
 
-    cachePageRef(pageNum: any, pageRef: any) { }
+    cachePageRef() { }
 
     get pagesCount() {
         return this.pdf ? this.pdf.numPages : 0;
diff --git a/src/client/views/search/SearchBox.tsx b/src/client/views/search/SearchBox.tsx
index 16c44225a..2214ac8af 100644
--- a/src/client/views/search/SearchBox.tsx
+++ b/src/client/views/search/SearchBox.tsx
@@ -9,12 +9,12 @@ import { Docs } from '../../documents/Documents';
 import { NumCast, Cast } from '../../../new_fields/Types';
 import { Doc } from '../../../new_fields/Doc';
 import { SearchItem } from './SearchItem';
-import { DocServer } from '../../DocServer';
 import * as rp from 'request-promise';
 import { Id } from '../../../new_fields/FieldSymbols';
 import { SearchUtil } from '../../util/SearchUtil';
 import { RouteStore } from '../../../server/RouteStore';
 import { FilterBox } from './FilterBox';
+import { Utils } from '../../../Utils';
 
 
 @observer
@@ -74,7 +74,7 @@ export class SearchBox extends React.Component {
 
     public static async convertDataUri(imageUri: string, returnedFilename: string) {
         try {
-            let posting = DocServer.prepend(RouteStore.dataUriToImage);
+            let posting = Utils.prepend(RouteStore.dataUriToImage);
             const returnedUri = await rp.post(posting, {
                 body: {
                     uri: imageUri,
@@ -154,7 +154,7 @@ export class SearchBox extends React.Component {
                         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)) : []
+                            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]);
diff --git a/src/mobile/ImageUpload.tsx b/src/mobile/ImageUpload.tsx
index a8f94b746..33a615cbf 100644
--- a/src/mobile/ImageUpload.tsx
+++ b/src/mobile/ImageUpload.tsx
@@ -11,6 +11,7 @@ import { listSpec } from '../new_fields/Schema';
 import { List } from '../new_fields/List';
 import { observer } from 'mobx-react';
 import { observable } from 'mobx';
+import { Utils } from '../Utils';
 
 
 
@@ -57,7 +58,7 @@ class Uploader extends React.Component {
 
                         this.status = "getting user document";
 
-                        const res = await rp.get(DocServer.prepend(RouteStore.getUserDocumentId));
+                        const res = await rp.get(Utils.prepend(RouteStore.getUserDocumentId));
                         if (!res) {
                             throw new Error("No user id returned");
                         }
@@ -104,6 +105,8 @@ class Uploader extends React.Component {
 }
 
 
+DocServer.init(window.location.protocol, window.location.hostname, 4321, "image upload");
+
 ReactDOM.render((
     <Uploader />
 ),
diff --git a/src/server/authentication/models/current_user_utils.ts b/src/server/authentication/models/current_user_utils.ts
index e796ccb43..1c52a3f11 100644
--- a/src/server/authentication/models/current_user_utils.ts
+++ b/src/server/authentication/models/current_user_utils.ts
@@ -12,6 +12,7 @@ import { List } from "../../../new_fields/List";
 import { listSpec } from "../../../new_fields/Schema";
 import { Cast, FieldValue, StrCast } from "../../../new_fields/Types";
 import { RouteStore } from "../../RouteStore";
+import { Utils } from "../../../Utils";
 
 export class CurrentUserUtils {
     private static curr_email: string;
@@ -74,7 +75,7 @@ export class CurrentUserUtils {
     }
 
     public static loadCurrentUser() {
-        return rp.get(DocServer.prepend(RouteStore.getCurrUser)).then(response => {
+        return rp.get(Utils.prepend(RouteStore.getCurrUser)).then(response => {
             if (response) {
                 const result: { id: string, email: string } = JSON.parse(response);
                 return result;
@@ -87,7 +88,7 @@ export class CurrentUserUtils {
     public static async loadUserDocument({ id, email }: { id: string, email: string }) {
         this.curr_id = id;
         this.curr_email = email;
-        await rp.get(DocServer.prepend(RouteStore.getUserDocumentId)).then(id => {
+        await rp.get(Utils.prepend(RouteStore.getUserDocumentId)).then(id => {
             if (id) {
                 return DocServer.GetRefField(id).then(async field => {
                     if (field instanceof Doc) {
diff --git a/src/server/index.ts b/src/server/index.ts
index b3859aa4f..6b4e59bfc 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -417,7 +417,7 @@ app.get(RouteStore.reset, getReset);
 app.post(RouteStore.reset, postReset);
 
 app.use(RouteStore.corsProxy, (req, res) =>
-    req.pipe(request(req.url.substring(1))).pipe(res));
+    req.pipe(request(decodeURIComponent(req.url.substring(1)))).pipe(res));
 
 app.get(RouteStore.delete, (req, res) => {
     if (release) {
-- 
cgit v1.2.3-70-g09d2

