From 60ceef0a3a8c11d85434a154e542424d34d9562c Mon Sep 17 00:00:00 2001
From: Tyler Schicke <tyler_schicke@brown.edu>
Date: Sat, 13 Jul 2019 16:46:49 -0400
Subject: Refactored style in DocDecs and added metadata entry to DocDecs

---
 src/client/views/MetadataEntryMenu.tsx | 74 ++++++++++++++++++++++++++++++++++
 1 file changed, 74 insertions(+)
 create mode 100644 src/client/views/MetadataEntryMenu.tsx

(limited to 'src/client/views/MetadataEntryMenu.tsx')

diff --git a/src/client/views/MetadataEntryMenu.tsx b/src/client/views/MetadataEntryMenu.tsx
new file mode 100644
index 000000000..0dc7e0220
--- /dev/null
+++ b/src/client/views/MetadataEntryMenu.tsx
@@ -0,0 +1,74 @@
+import * as React from 'react';
+import "./MetadataEntryMenu.scss";
+import { observer } from 'mobx-react';
+import { observable, action } from 'mobx';
+import { KeyValueBox } from './nodes/KeyValueBox';
+import { Doc } from '../../new_fields/Doc';
+
+export type DocLike = Doc | Doc[] | Promise<Doc> | Promise<Doc[]>;
+export interface MetadataEntryProps {
+    docs: DocLike | (() => DocLike);
+    onError?: () => boolean;
+}
+
+@observer
+export class MetadataEntryMenu extends React.Component<MetadataEntryProps>{
+    @observable private _currentKey: string = "";
+    @observable private _currentValue: string = "";
+
+    @action
+    onKeyChange = (e: React.ChangeEvent<HTMLInputElement>) => {
+        this._currentKey = e.target.value;
+    }
+
+    @action
+    onValueChange = (e: React.ChangeEvent<HTMLInputElement>) => {
+        this._currentValue = e.target.value;
+    }
+
+    onValueKeyDown = async (e: React.KeyboardEvent) => {
+        if (e.key === "Enter") {
+            const script = KeyValueBox.CompileKVPScript(this._currentValue);
+            if (!script) return;
+            let doc = this.props.docs;
+            if (typeof doc === "function") {
+                doc = doc();
+            }
+            doc = await doc;
+            let success: boolean;
+            if (doc instanceof Doc) {
+                success = KeyValueBox.ApplyKVPScript(doc, this._currentKey, script);
+            } else {
+                success = doc.every(d => KeyValueBox.ApplyKVPScript(d, this._currentKey, script));
+            }
+            if (!success) {
+                if (this.props.onError) {
+                    if (this.props.onError()) {
+                        this.clearInputs();
+                    }
+                } else {
+                    this.clearInputs();
+                }
+            } else {
+                this.clearInputs();
+            }
+        }
+    }
+
+    @action
+    clearInputs = () => {
+        this._currentKey = "";
+        this._currentValue = "";
+    }
+
+    render() {
+        return (
+            <div className="metadataEntry-outerDiv">
+                Key:
+                <input className="metadataEntry-input" value={this._currentKey} onChange={this.onKeyChange} />
+                Value:
+                <input className="metadataEntry-input" value={this._currentValue} onChange={this.onValueChange} onKeyDown={this.onValueKeyDown} />
+            </div>
+        );
+    }
+}
\ No newline at end of file
-- 
cgit v1.2.3-70-g09d2


From 8e45625865b20a9e25b120d6938d3ec31aa880e3 Mon Sep 17 00:00:00 2001
From: Tyler Schicke <tyler_schicke@brown.edu>
Date: Sat, 13 Jul 2019 20:12:27 -0400
Subject: Kinda added auto complete

---
 package.json                             |  2 ++
 src/client/views/DocumentDecorations.tsx |  2 +-
 src/client/views/MetadataEntryMenu.tsx   | 49 ++++++++++++++++++++++++++++++--
 3 files changed, 50 insertions(+), 3 deletions(-)

(limited to 'src/client/views/MetadataEntryMenu.tsx')

diff --git a/package.json b/package.json
index 6681c4c1c..22b3a6b21 100644
--- a/package.json
+++ b/package.json
@@ -92,6 +92,7 @@
         "@types/prosemirror-view": "^1.3.1",
         "@types/pug": "^2.0.4",
         "@types/react": "^16.8.7",
+        "@types/react-autosuggest": "^9.3.9",
         "@types/react-color": "^2.14.1",
         "@types/react-measure": "^2.0.4",
         "@types/react-table": "^6.7.22",
@@ -169,6 +170,7 @@
         "raw-loader": "^1.0.0",
         "react": "^16.8.4",
         "react-anime": "^2.2.0",
+        "react-autosuggest": "^9.4.3",
         "react-bootstrap": "^1.0.0-beta.5",
         "react-bootstrap-dropdown-menu": "^1.1.15",
         "react-color": "^2.17.0",
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 56fbd75a0..2cb3de50f 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -639,7 +639,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
         return (
             <div className="linkButtonWrapper">
                 <Flyout anchorPoint={anchorPoints.TOP_LEFT}
-                    content={<MetadataEntryMenu docs={() => SelectionManager.SelectedDocuments().map(dv => dv.props.Document)} />}>{/* tfs: @bcz This might need to be the data document? */}
+                    content={<MetadataEntryMenu docs={() => SelectionManager.SelectedDocuments().map(dv => dv.props.Document)} suggestWithFunction />}>{/* tfs: @bcz This might need to be the data document? */}
                     <div className="docDecs-tagButton" title="Add fields"><FontAwesomeIcon className="documentdecorations-icon" icon="tag" size="sm" /></div>
                 </Flyout>
             </div>
diff --git a/src/client/views/MetadataEntryMenu.tsx b/src/client/views/MetadataEntryMenu.tsx
index 0dc7e0220..0c8fd3909 100644
--- a/src/client/views/MetadataEntryMenu.tsx
+++ b/src/client/views/MetadataEntryMenu.tsx
@@ -1,20 +1,23 @@
 import * as React from 'react';
 import "./MetadataEntryMenu.scss";
 import { observer } from 'mobx-react';
-import { observable, action } from 'mobx';
+import { observable, action, runInAction } from 'mobx';
 import { KeyValueBox } from './nodes/KeyValueBox';
 import { Doc } from '../../new_fields/Doc';
+import * as Autosuggest from 'react-autosuggest';
 
 export type DocLike = Doc | Doc[] | Promise<Doc> | Promise<Doc[]>;
 export interface MetadataEntryProps {
     docs: DocLike | (() => DocLike);
     onError?: () => boolean;
+    suggestWithFunction?: boolean;
 }
 
 @observer
 export class MetadataEntryMenu extends React.Component<MetadataEntryProps>{
     @observable private _currentKey: string = "";
     @observable private _currentValue: string = "";
+    @observable private suggestions: string[] = [];
 
     @action
     onKeyChange = (e: React.ChangeEvent<HTMLInputElement>) => {
@@ -61,11 +64,53 @@ export class MetadataEntryMenu extends React.Component<MetadataEntryProps>{
         this._currentValue = "";
     }
 
+    getKeySuggestions = async (value: string): Promise<string[]> => {
+        value = value.toLowerCase();
+        let docs = this.props.docs;
+        if (typeof docs === "function") {
+            if (this.props.suggestWithFunction) {
+                docs = docs();
+            } else {
+                return [];
+            }
+        }
+        docs = await docs;
+        if (docs instanceof Doc) {
+            return Object.keys(docs).filter(key => key.toLowerCase().startsWith(value));
+        } else {
+            const keys = new Set<string>();
+            docs.forEach(doc => Doc.allKeys(doc).forEach(key => keys.add(key)));
+            return Array.from(keys).filter(key => key.toLowerCase().startsWith(value));
+        }
+    }
+    getSuggestionValue = (suggestion: string) => suggestion;
+
+    renderSuggestion = (suggestion: string) => {
+        return <p>{suggestion}</p>;
+    }
+
+    onSuggestionFetch = async ({ value }: { value: string }) => {
+        const sugg = await this.getKeySuggestions(value);
+        runInAction(() => {
+            this.suggestions = sugg;
+        });
+    }
+
+    @action
+    onSuggestionClear = () => {
+        this.suggestions = [];
+    }
+
     render() {
         return (
             <div className="metadataEntry-outerDiv">
                 Key:
-                <input className="metadataEntry-input" value={this._currentKey} onChange={this.onKeyChange} />
+                <Autosuggest inputProps={{ value: this._currentKey, onChange: this.onKeyChange, className: "metadataEntry-input" }}
+                    getSuggestionValue={this.getSuggestionValue}
+                    suggestions={this.suggestions}
+                    renderSuggestion={this.renderSuggestion}
+                    onSuggestionsFetchRequested={this.onSuggestionFetch}
+                    onSuggestionsClearRequested={this.onSuggestionClear} />
                 Value:
                 <input className="metadataEntry-input" value={this._currentValue} onChange={this.onValueChange} onKeyDown={this.onValueKeyDown} />
             </div>
-- 
cgit v1.2.3-70-g09d2


From 86bf6857e98743502fe6bc85e7de6807808c41c4 Mon Sep 17 00:00:00 2001
From: Tyler Schicke <tyler_schicke@brown.edu>
Date: Sun, 14 Jul 2019 12:36:46 -0400
Subject: Fixed some of the auto suggest stuff

---
 src/client/views/MetadataEntryMenu.scss | 57 +++++++++++++++++++++++++++++++++
 src/client/views/MetadataEntryMenu.tsx  |  7 ++--
 2 files changed, 61 insertions(+), 3 deletions(-)

(limited to 'src/client/views/MetadataEntryMenu.tsx')

diff --git a/src/client/views/MetadataEntryMenu.scss b/src/client/views/MetadataEntryMenu.scss
index 73e5b6a73..469843350 100644
--- a/src/client/views/MetadataEntryMenu.scss
+++ b/src/client/views/MetadataEntryMenu.scss
@@ -7,4 +7,61 @@
 .metadataEntry-outerDiv {
     display: flex;
     width: 300px;
+}
+
+.react-autosuggest__container {
+    position: relative;
+}
+
+.react-autosuggest__input {
+    width: 240px;
+    height: 30px;
+    padding: 10px 20px;
+    font-family: Helvetica, sans-serif;
+    font-weight: 300;
+    font-size: 16px;
+    border: 1px solid #aaa;
+    border-radius: 4px;
+}
+
+.react-autosuggest__input--focused {
+    outline: none;
+}
+
+.react-autosuggest__input--open {
+    border-bottom-left-radius: 0;
+    border-bottom-right-radius: 0;
+}
+
+.react-autosuggest__suggestions-container {
+    display: none;
+}
+
+.react-autosuggest__suggestions-container--open {
+    display: block;
+    position: fixed;
+    width: 280px;
+    border: 1px solid #aaa;
+    background-color: #fff;
+    font-family: Helvetica, sans-serif;
+    font-weight: 300;
+    font-size: 16px;
+    border-bottom-left-radius: 4px;
+    border-bottom-right-radius: 4px;
+    z-index: 2;
+}
+
+.react-autosuggest__suggestions-list {
+    margin: 0;
+    padding: 0;
+    list-style-type: none;
+}
+
+.react-autosuggest__suggestion {
+    cursor: pointer;
+    padding: 10px 20px;
+}
+
+.react-autosuggest__suggestion--highlighted {
+    background-color: #ddd;
 }
\ No newline at end of file
diff --git a/src/client/views/MetadataEntryMenu.tsx b/src/client/views/MetadataEntryMenu.tsx
index 0c8fd3909..cb574aa96 100644
--- a/src/client/views/MetadataEntryMenu.tsx
+++ b/src/client/views/MetadataEntryMenu.tsx
@@ -1,7 +1,7 @@
 import * as React from 'react';
 import "./MetadataEntryMenu.scss";
 import { observer } from 'mobx-react';
-import { observable, action, runInAction } from 'mobx';
+import { observable, action, runInAction, trace } from 'mobx';
 import { KeyValueBox } from './nodes/KeyValueBox';
 import { Doc } from '../../new_fields/Doc';
 import * as Autosuggest from 'react-autosuggest';
@@ -20,8 +20,8 @@ export class MetadataEntryMenu extends React.Component<MetadataEntryProps>{
     @observable private suggestions: string[] = [];
 
     @action
-    onKeyChange = (e: React.ChangeEvent<HTMLInputElement>) => {
-        this._currentKey = e.target.value;
+    onKeyChange = (e: React.ChangeEvent, { newValue }: { newValue: string }) => {
+        this._currentKey = newValue;
     }
 
     @action
@@ -102,6 +102,7 @@ export class MetadataEntryMenu extends React.Component<MetadataEntryProps>{
     }
 
     render() {
+        trace();
         return (
             <div className="metadataEntry-outerDiv">
                 Key:
-- 
cgit v1.2.3-70-g09d2


From d09679619e7cf0fbae60e1f9220a6dbeb9de1bd7 Mon Sep 17 00:00:00 2001
From: Tyler Schicke <tyler_schicke@brown.edu>
Date: Sun, 14 Jul 2019 14:35:50 -0400
Subject: Fixed metadata entry css

---
 src/client/views/MetadataEntryMenu.scss | 23 ++++++++++-------------
 src/client/views/MetadataEntryMenu.tsx  | 11 +++++++++--
 2 files changed, 19 insertions(+), 15 deletions(-)

(limited to 'src/client/views/MetadataEntryMenu.tsx')

diff --git a/src/client/views/MetadataEntryMenu.scss b/src/client/views/MetadataEntryMenu.scss
index 469843350..a6df3cd1e 100644
--- a/src/client/views/MetadataEntryMenu.scss
+++ b/src/client/views/MetadataEntryMenu.scss
@@ -1,9 +1,3 @@
-.metadataEntry-input {
-    width: 40%;
-    margin-left: 5px;
-    margin-right: 5px;
-}
-
 .metadataEntry-outerDiv {
     display: flex;
     width: 300px;
@@ -13,15 +7,18 @@
     position: relative;
 }
 
+.react-autosuggest__container,
+.metadataEntry-input {
+    width: 100%;
+    margin-left: 5px;
+    margin-right: 5px;
+}
+
+.metadataEntry-input,
 .react-autosuggest__input {
-    width: 240px;
-    height: 30px;
-    padding: 10px 20px;
-    font-family: Helvetica, sans-serif;
-    font-weight: 300;
-    font-size: 16px;
     border: 1px solid #aaa;
     border-radius: 4px;
+    width: 100%;
 }
 
 .react-autosuggest__input--focused {
@@ -40,7 +37,7 @@
 .react-autosuggest__suggestions-container--open {
     display: block;
     position: fixed;
-    width: 280px;
+    width: 180px;
     border: 1px solid #aaa;
     background-color: #fff;
     font-family: Helvetica, sans-serif;
diff --git a/src/client/views/MetadataEntryMenu.tsx b/src/client/views/MetadataEntryMenu.tsx
index cb574aa96..59de0e2b4 100644
--- a/src/client/views/MetadataEntryMenu.tsx
+++ b/src/client/views/MetadataEntryMenu.tsx
@@ -19,6 +19,8 @@ export class MetadataEntryMenu extends React.Component<MetadataEntryProps>{
     @observable private _currentValue: string = "";
     @observable private suggestions: string[] = [];
 
+    private autosuggestRef = React.createRef<Autosuggest>();
+
     @action
     onKeyChange = (e: React.ChangeEvent, { newValue }: { newValue: string }) => {
         this._currentKey = newValue;
@@ -62,6 +64,10 @@ export class MetadataEntryMenu extends React.Component<MetadataEntryProps>{
     clearInputs = () => {
         this._currentKey = "";
         this._currentValue = "";
+        if (this.autosuggestRef.current) {
+            const input: HTMLInputElement = (this.autosuggestRef.current as any).input;
+            input && input.focus();
+        }
     }
 
     getKeySuggestions = async (value: string): Promise<string[]> => {
@@ -106,12 +112,13 @@ export class MetadataEntryMenu extends React.Component<MetadataEntryProps>{
         return (
             <div className="metadataEntry-outerDiv">
                 Key:
-                <Autosuggest inputProps={{ value: this._currentKey, onChange: this.onKeyChange, className: "metadataEntry-input" }}
+                <Autosuggest inputProps={{ value: this._currentKey, onChange: this.onKeyChange }}
                     getSuggestionValue={this.getSuggestionValue}
                     suggestions={this.suggestions}
                     renderSuggestion={this.renderSuggestion}
                     onSuggestionsFetchRequested={this.onSuggestionFetch}
-                    onSuggestionsClearRequested={this.onSuggestionClear} />
+                    onSuggestionsClearRequested={this.onSuggestionClear}
+                    ref={this.autosuggestRef} />
                 Value:
                 <input className="metadataEntry-input" value={this._currentValue} onChange={this.onValueChange} onKeyDown={this.onValueKeyDown} />
             </div>
-- 
cgit v1.2.3-70-g09d2


From 7d9e29690956327d1ed9981cd2882d08b72b5c86 Mon Sep 17 00:00:00 2001
From: Tyler Schicke <tyler_schicke@brown.edu>
Date: Sun, 14 Jul 2019 16:02:08 -0400
Subject: Added value preview to MetadataEntry

---
 src/client/views/MetadataEntryMenu.tsx | 46 +++++++++++++++++++++++++++++++++-
 1 file changed, 45 insertions(+), 1 deletion(-)

(limited to 'src/client/views/MetadataEntryMenu.tsx')

diff --git a/src/client/views/MetadataEntryMenu.tsx b/src/client/views/MetadataEntryMenu.tsx
index 59de0e2b4..08abb9887 100644
--- a/src/client/views/MetadataEntryMenu.tsx
+++ b/src/client/views/MetadataEntryMenu.tsx
@@ -3,7 +3,7 @@ import "./MetadataEntryMenu.scss";
 import { observer } from 'mobx-react';
 import { observable, action, runInAction, trace } from 'mobx';
 import { KeyValueBox } from './nodes/KeyValueBox';
-import { Doc } from '../../new_fields/Doc';
+import { Doc, Field } from '../../new_fields/Doc';
 import * as Autosuggest from 'react-autosuggest';
 
 export type DocLike = Doc | Doc[] | Promise<Doc> | Promise<Doc[]>;
@@ -18,17 +18,60 @@ export class MetadataEntryMenu extends React.Component<MetadataEntryProps>{
     @observable private _currentKey: string = "";
     @observable private _currentValue: string = "";
     @observable private suggestions: string[] = [];
+    private userModified = false;
 
     private autosuggestRef = React.createRef<Autosuggest>();
 
     @action
     onKeyChange = (e: React.ChangeEvent, { newValue }: { newValue: string }) => {
         this._currentKey = newValue;
+        if (!this.userModified) {
+            this.previewValue();
+        }
+    }
+
+    previewValue = async () => {
+        let field: Field | undefined | null = null;
+        let onProto: boolean = false;
+        let value: string | undefined = undefined;
+        let docs = this.props.docs;
+        if (typeof docs === "function") {
+            if (this.props.suggestWithFunction) {
+                docs = docs();
+            } else {
+                return;
+            }
+        }
+        docs = await docs;
+        if (docs instanceof Doc) {
+            await docs[this._currentKey];
+            value = Field.toKeyValueString(docs, this._currentKey);
+        } else {
+            for (const doc of docs) {
+                const v = await doc[this._currentKey];
+                onProto = onProto || !Object.keys(doc).includes(this._currentKey);
+                if (field === null) {
+                    field = v;
+                } else if (v !== field) {
+                    value = "multiple values";
+                }
+            }
+        }
+        if (value === undefined) {
+            if (field !== null && field !== undefined) {
+                value = (onProto ? "" : "= ") + Field.toScriptString(field);
+            } else {
+                value = "";
+            }
+        }
+        const s = value;
+        runInAction(() => this._currentValue = s);
     }
 
     @action
     onValueChange = (e: React.ChangeEvent<HTMLInputElement>) => {
         this._currentValue = e.target.value;
+        this.userModified = e.target.value.trim() !== "";
     }
 
     onValueKeyDown = async (e: React.KeyboardEvent) => {
@@ -64,6 +107,7 @@ export class MetadataEntryMenu extends React.Component<MetadataEntryProps>{
     clearInputs = () => {
         this._currentKey = "";
         this._currentValue = "";
+        this.userModified = false;
         if (this.autosuggestRef.current) {
             const input: HTMLInputElement = (this.autosuggestRef.current as any).input;
             input && input.focus();
-- 
cgit v1.2.3-70-g09d2


From e302a00b20ae0f44393548c7da27af60fd56c92b Mon Sep 17 00:00:00 2001
From: Tyler Schicke <tyler_schicke@brown.edu>
Date: Sun, 14 Jul 2019 18:49:42 -0400
Subject: Added whosOnline route to server

---
 src/client/DocServer.ts                            | 97 +++++++++++++---------
 src/client/views/Main.tsx                          |  5 +-
 src/client/views/MetadataEntryMenu.tsx             |  1 -
 src/debug/Repl.tsx                                 |  6 +-
 src/debug/Viewer.tsx                               | 15 ++--
 .../authentication/models/current_user_utils.ts    | 17 ++--
 src/server/index.ts                                | 45 +++++++++-
 7 files changed, 128 insertions(+), 58 deletions(-)

(limited to 'src/client/views/MetadataEntryMenu.tsx')

diff --git a/src/client/DocServer.ts b/src/client/DocServer.ts
index d05793ea2..6737657c8 100644
--- a/src/client/DocServer.ts
+++ b/src/client/DocServer.ts
@@ -5,6 +5,7 @@ import { Utils, emptyFunction } from '../Utils';
 import { SerializationHelper } from './util/SerializationHelper';
 import { RefField } from '../new_fields/RefField';
 import { Id, HandleUpdate } from '../new_fields/FieldSymbols';
+import { CurrentUserUtils } from '../server/authentication/models/current_user_utils';
 
 /**
  * This class encapsulates the transfer and cross-client synchronization of
@@ -21,12 +22,31 @@ import { Id, HandleUpdate } from '../new_fields/FieldSymbols';
  */
 export namespace DocServer {
     let _cache: { [id: string]: RefField | Promise<Opt<RefField>> } = {};
-    const _socket = OpenSocket(`${window.location.protocol}//${window.location.hostname}:4321`);
+    let _socket: SocketIOClient.Socket;
     // this client's distinct GUID created at initialization
-    const GUID: string = Utils.GenerateGuid();
+    let GUID: string;
     // indicates whether or not a document is currently being udpated, and, if so, its id
     let updatingId: string | undefined;
 
+    export function init(protocol: string, hostname: string, port: number, identifier: string) {
+        _cache = {};
+        GUID = identifier;
+        _socket = OpenSocket(`${protocol}//${hostname}:${port}`);
+
+        _GetRefField = _GetRefFieldImpl;
+        _GetRefFields = _GetRefFieldsImpl;
+        _CreateField = _CreateFieldImpl;
+        _UpdateField = _UpdateFieldImpl;
+
+        /**
+         * Whenever the server sends us its handshake message on our
+         * websocket, we use the above function to return the handshake.
+         */
+        Utils.AddServerHandler(_socket, MessageStore.Foo, onConnection);
+        Utils.AddServerHandler(_socket, MessageStore.UpdateField, respondToUpdate);
+        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
@@ -36,6 +56,10 @@ export namespace DocServer {
         return window.location.origin + extension;
     }
 
+    function errorFunc(): never {
+        throw new Error("Can't use DocServer without calling init first");
+    }
+
     export namespace Control {
 
         let _isReadOnly = false;
@@ -63,22 +87,16 @@ export namespace DocServer {
 
     }
 
-    export namespace Util {
-
-        /**
-         * Whenever the server sends us its handshake message on our
-         * websocket, we use the above function to return the handshake.
-         */
-        Utils.AddServerHandler(_socket, MessageStore.Foo, onConnection);
+    /**
+     * This function emits a message (with this client's
+     * unique GUID) to the server
+     * indicating that this client has connected
+     */
+    function onConnection() {
+        _socket.emit(MessageStore.Bar.Message, GUID);
+    }
 
-        /**
-         * This function emits a message (with this client's
-         * unique GUID) to the server
-         * indicating that this client has connected
-         */
-        function onConnection() {
-            _socket.emit(MessageStore.Bar.Message, GUID);
-        }
+    export namespace Util {
 
         /**
          * Emits a message to the server that wipes
@@ -98,7 +116,7 @@ export namespace DocServer {
      * the server if the document has not been cached.
      * @param id the id of the requested document
      */
-    export async function GetRefField(id: string): Promise<Opt<RefField>> {
+    const _GetRefFieldImpl = (id: string): Promise<Opt<RefField>> => {
         // an initial pass through the cache to determine whether the document needs to be fetched,
         // is already in the process of being fetched or already exists in the
         // cache
@@ -139,8 +157,14 @@ export namespace DocServer {
             return cached;
         } else {
             // CACHED => great, let's just return the cached field we have
-            return cached;
+            return Promise.resolve(cached);
         }
+    };
+
+    let _GetRefField: (id: string) => Promise<Opt<RefField>> = errorFunc;
+
+    export function GetRefField(id: string): Promise<Opt<RefField>> {
+        return _GetRefField(id);
     }
 
     /**
@@ -149,7 +173,7 @@ export namespace DocServer {
      * the server if the document has not been cached.
      * @param ids the ids that map to the reqested documents
      */
-    export async function GetRefFields(ids: string[]): Promise<{ [id: string]: Opt<RefField> }> {
+    const _GetRefFieldsImpl = async (ids: string[]): Promise<{ [id: string]: Opt<RefField> }> => {
         const requestedIds: string[] = [];
         const waitingIds: string[] = [];
         const promises: Promise<Opt<RefField>>[] = [];
@@ -245,16 +269,13 @@ export namespace DocServer {
         // 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;
-    }
+    };
 
-    function _UpdateFieldImpl(id: string, diff: any) {
-        if (id === updatingId) {
-            return;
-        }
-        Utils.Emit(_socket, MessageStore.UpdateField, { id, diff });
-    }
+    let _GetRefFields: (ids: string[]) => Promise<{ [id: string]: Opt<RefField> }> = errorFunc;
 
-    let _UpdateField = _UpdateFieldImpl;
+    export function GetRefFields(ids: string[]) {
+        return _GetRefFields(ids);
+    }
 
     // WRITE A NEW DOCUMENT TO THE SERVER
 
@@ -274,7 +295,7 @@ export namespace DocServer {
         Utils.Emit(_socket, MessageStore.CreateField, initialState);
     }
 
-    let _CreateField = _CreateFieldImpl;
+    let _CreateField: (field: RefField) => void = errorFunc;
 
     // NOTIFY THE SERVER OF AN UPDATE TO A DOC'S STATE
 
@@ -290,6 +311,15 @@ export namespace DocServer {
         _UpdateField(id, updatedState);
     }
 
+    function _UpdateFieldImpl(id: string, diff: any) {
+        if (id === updatingId) {
+            return;
+        }
+        Utils.Emit(_socket, MessageStore.UpdateField, { id, diff });
+    }
+
+    let _UpdateField: (id: string, diff: any) => void = errorFunc;
+
     function _respondToUpdateImpl(diff: any) {
         const id = diff.id;
         // to be valid, the Diff object must reference
@@ -355,13 +385,4 @@ export namespace DocServer {
     function respondToDelete(ids: string | string[]) {
         _respondToDelete(ids);
     }
-
-    function connected() {
-        _socket.emit(MessageStore.Bar.Message, GUID);
-    }
-
-    Utils.AddServerHandler(_socket, MessageStore.Foo, connected);
-    Utils.AddServerHandler(_socket, MessageStore.UpdateField, respondToUpdate);
-    Utils.AddServerHandler(_socket, MessageStore.DeleteField, respondToDelete);
-    Utils.AddServerHandler(_socket, MessageStore.DeleteFields, respondToDelete);
 }
\ No newline at end of file
diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx
index 589542806..80399e24b 100644
--- a/src/client/views/Main.tsx
+++ b/src/client/views/Main.tsx
@@ -6,6 +6,7 @@ import * as React from 'react';
 import { Cast } from "../../new_fields/Types";
 import { Doc, DocListCastAsync } from "../../new_fields/Doc";
 import { List } from "../../new_fields/List";
+import { DocServer } from "../DocServer";
 
 let swapDocs = async () => {
     let oldDoc = await Cast(CurrentUserUtils.UserDocument.linkManagerDoc, Doc);
@@ -28,8 +29,10 @@ let swapDocs = async () => {
 }
 
 (async () => {
+    const info = await CurrentUserUtils.loadCurrentUser();
+    DocServer.init(window.location.protocol, window.location.hostname, 4321, info.email);
     await Docs.Prototypes.initialize();
-    await CurrentUserUtils.loadCurrentUser();
+    await CurrentUserUtils.loadUserDocument(info);
     await swapDocs();
     ReactDOM.render(<MainView />, document.getElementById('root'));
 })();
\ No newline at end of file
diff --git a/src/client/views/MetadataEntryMenu.tsx b/src/client/views/MetadataEntryMenu.tsx
index 08abb9887..5ee661944 100644
--- a/src/client/views/MetadataEntryMenu.tsx
+++ b/src/client/views/MetadataEntryMenu.tsx
@@ -152,7 +152,6 @@ export class MetadataEntryMenu extends React.Component<MetadataEntryProps>{
     }
 
     render() {
-        trace();
         return (
             <div className="metadataEntry-outerDiv">
                 Key:
diff --git a/src/debug/Repl.tsx b/src/debug/Repl.tsx
index 91b711c79..4f4db13d2 100644
--- a/src/debug/Repl.tsx
+++ b/src/debug/Repl.tsx
@@ -6,6 +6,7 @@ import { CompileScript } from '../client/util/Scripting';
 import { makeInterface } from '../new_fields/Schema';
 import { ObjectField } from '../new_fields/ObjectField';
 import { RefField } from '../new_fields/RefField';
+import { DocServer } from '../client/DocServer';
 
 @observer
 class Repl extends React.Component {
@@ -63,4 +64,7 @@ class Repl extends React.Component {
     }
 }
 
-ReactDOM.render(<Repl />, document.getElementById("root"));
\ No newline at end of file
+(async function () {
+    DocServer.init(window.location.protocol, window.location.hostname, 4321, "repl");
+    ReactDOM.render(<Repl />, document.getElementById("root"));
+})();
\ No newline at end of file
diff --git a/src/debug/Viewer.tsx b/src/debug/Viewer.tsx
index f48eb696c..2b3eed154 100644
--- a/src/debug/Viewer.tsx
+++ b/src/debug/Viewer.tsx
@@ -178,9 +178,12 @@ class Viewer extends React.Component {
     }
 }
 
-ReactDOM.render((
-    <div style={{ position: "absolute", width: "100%", height: "100%" }}>
-        <Viewer />
-    </div>),
-    document.getElementById('root')
-);
\ No newline at end of file
+(async function () {
+    await DocServer.init(window.location.protocol, window.location.hostname, 4321, "viewer");
+    ReactDOM.render((
+        <div style={{ position: "absolute", width: "100%", height: "100%" }}>
+            <Viewer />
+        </div>),
+        document.getElementById('root')
+    );
+})();
\ 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 384c579de..e796ccb43 100644
--- a/src/server/authentication/models/current_user_utils.ts
+++ b/src/server/authentication/models/current_user_utils.ts
@@ -73,17 +73,21 @@ export class CurrentUserUtils {
 
     }
 
-    public static async loadCurrentUser(): Promise<any> {
-        let userPromise = rp.get(DocServer.prepend(RouteStore.getCurrUser)).then(response => {
+    public static loadCurrentUser() {
+        return rp.get(DocServer.prepend(RouteStore.getCurrUser)).then(response => {
             if (response) {
-                let obj = JSON.parse(response);
-                CurrentUserUtils.curr_id = obj.id as string;
-                CurrentUserUtils.curr_email = obj.email as string;
+                const result: { id: string, email: string } = JSON.parse(response);
+                return result;
             } else {
                 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 => {
+    }
+
+    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 => {
             if (id) {
                 return DocServer.GetRefField(id).then(async field => {
                     if (field instanceof Doc) {
@@ -108,7 +112,6 @@ export class CurrentUserUtils {
         } catch (e) {
 
         }
-        return Promise.all([userPromise, userDocPromise]);
     }
 
     /* Northstar catalog ... really just for testing so this should eventually go away */
diff --git a/src/server/index.ts b/src/server/index.ts
index 21adff9e5..9cb43bf4e 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -149,6 +149,32 @@ app.get("/search", async (req, res) => {
     res.send(results);
 });
 
+function msToTime(duration: number) {
+    let milliseconds = Math.floor((duration % 1000) / 100),
+        seconds = Math.floor((duration / 1000) % 60),
+        minutes = Math.floor((duration / (1000 * 60)) % 60),
+        hours = Math.floor((duration / (1000 * 60 * 60)) % 24);
+
+    let hoursS = (hours < 10) ? "0" + hours : hours;
+    let minutesS = (minutes < 10) ? "0" + minutes : minutes;
+    let secondsS = (seconds < 10) ? "0" + seconds : seconds;
+
+    return hoursS + ":" + minutesS + ":" + secondsS + "." + milliseconds;
+}
+
+app.get("/whosOnline", (req, res) => {
+    let users: any = { active: {}, inactive: {} };
+    const now = Date.now();
+
+    for (const user in timeMap) {
+        const time = timeMap[user];
+        const key = ((now - time) / 1000) < (60 * 5) ? "active" : "inactive";
+        users[key][user] = `Last active ${msToTime(now - time)} ago`;
+    }
+
+    res.send(users);
+});
+
 app.get("/thumbnail/:filename", (req, res) => {
     let filename = req.params.filename;
     let noExt = filename.substring(0, filename.length - ".png".length);
@@ -450,12 +476,21 @@ interface Map {
 }
 let clients: Map = {};
 
+let socketMap = new Map<SocketIO.Socket, string>();
+let timeMap: { [id: string]: number } = {};
+
 server.on("connection", function (socket: Socket) {
-    console.log("a user has connected");
+    socket.use((packet, next) => {
+        let id = socketMap.get(socket);
+        if (id) {
+            timeMap[id] = Date.now();
+        }
+        next();
+    });
 
     Utils.Emit(socket, MessageStore.Foo, "handshooken");
 
-    Utils.AddServerHandler(socket, MessageStore.Bar, barReceived);
+    Utils.AddServerHandler(socket, MessageStore.Bar, guid => barReceived(socket, guid));
     Utils.AddServerHandler(socket, MessageStore.SetField, (args) => setField(socket, args));
     Utils.AddServerHandlerCallback(socket, MessageStore.GetField, getField);
     Utils.AddServerHandlerCallback(socket, MessageStore.GetFields, getFields);
@@ -485,8 +520,10 @@ async function deleteAll() {
     await Search.Instance.clear();
 }
 
-function barReceived(guid: String) {
-    clients[guid.toString()] = new Client(guid.toString());
+function barReceived(socket: SocketIO.Socket, guid: string) {
+    clients[guid] = new Client(guid.toString());
+    console.log(`User ${guid} has connected`);
+    socketMap.set(socket, guid);
 }
 
 function getField([id, callback]: [string, (result?: Transferable) => void]) {
-- 
cgit v1.2.3-70-g09d2


From 68c135f23c54cc44a84fb17223bc49e3b7bff1a8 Mon Sep 17 00:00:00 2001
From: Tyler Schicke <tyler_schicke@brown.edu>
Date: Sun, 14 Jul 2019 19:20:03 -0400
Subject: Fixed a couple issues

---
 src/client/views/MetadataEntryMenu.scss                                | 2 ++
 src/client/views/MetadataEntryMenu.tsx                                 | 1 +
 .../views/collections/collectionFreeForm/CollectionFreeFormView.tsx    | 3 ++-
 3 files changed, 5 insertions(+), 1 deletion(-)

(limited to 'src/client/views/MetadataEntryMenu.tsx')

diff --git a/src/client/views/MetadataEntryMenu.scss b/src/client/views/MetadataEntryMenu.scss
index a6df3cd1e..bcfc9a82d 100644
--- a/src/client/views/MetadataEntryMenu.scss
+++ b/src/client/views/MetadataEntryMenu.scss
@@ -37,6 +37,8 @@
 .react-autosuggest__suggestions-container--open {
     display: block;
     position: fixed;
+    overflow-y: auto;
+    max-height: 400px;
     width: 180px;
     border: 1px solid #aaa;
     background-color: #fff;
diff --git a/src/client/views/MetadataEntryMenu.tsx b/src/client/views/MetadataEntryMenu.tsx
index 5ee661944..bd5a307b3 100644
--- a/src/client/views/MetadataEntryMenu.tsx
+++ b/src/client/views/MetadataEntryMenu.tsx
@@ -158,6 +158,7 @@ export class MetadataEntryMenu extends React.Component<MetadataEntryProps>{
                 <Autosuggest inputProps={{ value: this._currentKey, onChange: this.onKeyChange }}
                     getSuggestionValue={this.getSuggestionValue}
                     suggestions={this.suggestions}
+                    alwaysRenderSuggestions
                     renderSuggestion={this.renderSuggestion}
                     onSuggestionsFetchRequested={this.onSuggestionFetch}
                     onSuggestionsClearRequested={this.onSuggestionClear}
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index b75cf7d5e..19e280444 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -485,7 +485,8 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
                     const script = this.Document[key];
                     let originalText: string | undefined = undefined;
                     if (script) originalText = script.script.originalScript;
-                    let scriptingBox = <ScriptBox initialText={originalText} onCancel={overlayDisposer} onSave={(text, onError) => {
+                    // tslint:disable-next-line: no-unnecessary-callback-wrapper
+                    let scriptingBox = <ScriptBox initialText={originalText} onCancel={() => overlayDisposer()} onSave={(text, onError) => {
                         const script = CompileScript(text, {
                             params,
                             requiredType,
-- 
cgit v1.2.3-70-g09d2

