From c9f77d5aab98e6e7865cdcad957d5c937631775d Mon Sep 17 00:00:00 2001
From: Tyler Schicke <tyler_schicke@brown.edu>
Date: Mon, 24 Jun 2019 13:41:39 -0400
Subject: Added ReadOnly mode for docs and changed computed values a bit

---
 src/client/views/nodes/KeyValueBox.tsx | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

(limited to 'src/client/views/nodes/KeyValueBox.tsx')

diff --git a/src/client/views/nodes/KeyValueBox.tsx b/src/client/views/nodes/KeyValueBox.tsx
index 3d626eef0..a4c14ae38 100644
--- a/src/client/views/nodes/KeyValueBox.tsx
+++ b/src/client/views/nodes/KeyValueBox.tsx
@@ -38,10 +38,11 @@ export class KeyValueBox extends React.Component<FieldViewProps> {
     }
     public static SetField(doc: Doc, key: string, value: string) {
         let eq = value.startsWith("=");
+        let target = eq ? doc : Doc.GetProto(doc);
         value = eq ? value.substr(1) : value;
         let dubEq = value.startsWith(":=");
         value = dubEq ? value.substr(2) : value;
-        let options: ScriptOptions = { addReturn: true };
+        let options: ScriptOptions = { addReturn: true, params: { this: "Doc" } };
         if (dubEq) options.typecheck = false;
         let script = CompileScript(value, options);
         if (!script.compiled) {
@@ -49,12 +50,11 @@ export class KeyValueBox extends React.Component<FieldViewProps> {
         }
         let field = new ComputedField(script);
         if (!dubEq) {
-            let res = script.run();
+            let res = script.run({ this: target });
             if (!res.success) return false;
             field = res.result;
         }
         if (Field.IsField(field, true)) {
-            let target = eq ? doc : Doc.GetProto(doc);
             target[key] = field;
             return true;
         }
-- 
cgit v1.2.3-70-g09d2


From 18b568ce20b66c4e16521c043df804279a5cd163 Mon Sep 17 00:00:00 2001
From: Sam Wilkins <samuel_wilkins@brown.edu>
Date: Wed, 26 Jun 2019 23:19:12 -0400
Subject: implemented drag drop template from key value key selection

---
 src/client/views/nodes/KeyValueBox.scss  |  6 +--
 src/client/views/nodes/KeyValueBox.tsx   | 72 ++++++++++++++++++++++++++++++--
 src/client/views/nodes/KeyValuePair.scss | 39 +++++++++++++----
 src/client/views/nodes/KeyValuePair.tsx  | 62 ++++++++++++++++++++-------
 src/scraping/buxton/scraper.py           |  8 ++--
 5 files changed, 153 insertions(+), 34 deletions(-)

(limited to 'src/client/views/nodes/KeyValueBox.tsx')

diff --git a/src/client/views/nodes/KeyValueBox.scss b/src/client/views/nodes/KeyValueBox.scss
index 20cae03d4..87a9565e8 100644
--- a/src/client/views/nodes/KeyValueBox.scss
+++ b/src/client/views/nodes/KeyValueBox.scss
@@ -91,12 +91,12 @@ $header-height: 30px;
     width: 4px;
     float: left; 
     height: 30px;
-    width: 10px;
+    width: 5px;
     z-index: 20;
     right: 0;
     top: 0;
-    border-radius: 10px;
-    background: gray;
+    border-radius: 0;
+    background: black;
     pointer-events: all;
 }
 .keyValueBox-dividerDragger{
diff --git a/src/client/views/nodes/KeyValueBox.tsx b/src/client/views/nodes/KeyValueBox.tsx
index cd65c42bc..4beb70284 100644
--- a/src/client/views/nodes/KeyValueBox.tsx
+++ b/src/client/views/nodes/KeyValueBox.tsx
@@ -7,13 +7,23 @@ import { FieldView, FieldViewProps } from './FieldView';
 import "./KeyValueBox.scss";
 import { KeyValuePair } from "./KeyValuePair";
 import React = require("react");
-import { NumCast, Cast, FieldValue } from "../../../new_fields/Types";
-import { Doc, Field } from "../../../new_fields/Doc";
+import { NumCast, Cast, FieldValue, StrCast } from "../../../new_fields/Types";
+import { Doc, Field, FieldResult } from "../../../new_fields/Doc";
 import { ComputedField } from "../../../new_fields/ScriptField";
+import { SetupDrag } from "../../util/DragManager";
+import { Docs } from "../../documents/Documents";
+import { RawDataOperationParameters } from "../../northstar/model/idea/idea";
+import { Templates } from "../Templates";
+import { List } from "../../../new_fields/List";
+import { TextField } from "../../util/ProsemirrorCopy/prompt";
+import { RichTextField } from "../../../new_fields/RichTextField";
+import { ImageField } from "../../../new_fields/URLField";
 
 @observer
 export class KeyValueBox extends React.Component<FieldViewProps> {
     private _mainCont = React.createRef<HTMLDivElement>();
+    private _keyHeader = React.createRef<HTMLTableHeaderCellElement>();
+    @observable private rows: KeyValuePair[] = [];
 
     public static LayoutString(fieldStr: string = "data") { return FieldView.LayoutString(KeyValueBox, fieldStr); }
     @observable private _keyInput: string = "";
@@ -90,7 +100,7 @@ export class KeyValueBox extends React.Component<FieldViewProps> {
         let rows: JSX.Element[] = [];
         let i = 0;
         for (let key of Object.keys(ids).sort()) {
-            rows.push(<KeyValuePair doc={realDoc} keyWidth={100 - this.splitPercentage} rowStyle={"keyValueBox-" + (i++ % 2 ? "oddRow" : "evenRow")} key={key} keyName={key} />);
+            rows.push(<KeyValuePair doc={realDoc} ref={(el) => { if (el) this.rows.push(el); }} keyWidth={100 - this.splitPercentage} rowStyle={"keyValueBox-" + (i++ % 2 ? "oddRow" : "evenRow")} key={key} keyName={key} />);
         }
         return rows;
     }
@@ -134,6 +144,58 @@ export class KeyValueBox extends React.Component<FieldViewProps> {
         document.addEventListener('pointerup', this.onDividerUp);
     }
 
+    getTemplate = async () => {
+        let parent = Docs.FreeformDocument([], { width: 800, height: 800, title: "Template" });
+        for (let row of this.rows.filter(row => row.isChecked)) {
+            await this.createTemplateField(parent, row);
+            row.uncheck();
+        }
+        return parent;
+    }
+
+    createTemplateField = async (parent: Doc, row: KeyValuePair) => {
+        let collectionKeyProp = `fieldKey={"data"}`;
+        let metaKey = row.props.keyName;
+        let metaKeyProp = `fieldKey={"${metaKey}"}`;
+
+        let sourceDoc = await Cast(this.props.Document.data, Doc);
+        if (!sourceDoc) {
+            return;
+        }
+        let target = this.inferType(sourceDoc[metaKey], metaKey);
+
+        let template = Doc.MakeAlias(target);
+        template.proto = parent;
+        template.title = metaKey;
+        template.nativeWidth = 300;
+        template.nativeHeight = 300;
+        template.embed = true;
+        template.isTemplate = true;
+        template.templates = new List<string>([Templates.TitleBar(metaKey)]);
+        if (target.backgroundLayout) {
+            let metaAnoKeyProp = `fieldKey={"${metaKey}"} fieldExt={"annotations"}`;
+            let collectionAnoKeyProp = `fieldKey={"annotations"}`;
+            template.layout = StrCast(target.layout).replace(collectionAnoKeyProp, metaAnoKeyProp);
+            template.backgroundLayout = StrCast(target.backgroundLayout).replace(collectionKeyProp, metaKeyProp);
+        } else {
+            template.layout = StrCast(target.layout).replace(collectionKeyProp, metaKeyProp);
+        }
+        Doc.AddDocToList(parent, "data", template);
+        row.uncheck();
+    }
+
+    inferType = (field: FieldResult, metaKey: string) => {
+        let options = { width: 300, height: 300, title: metaKey };
+        if (field instanceof RichTextField || typeof field === "string" || typeof field === "number") {
+            return Docs.TextDocument(options);
+        } else if (field instanceof List) {
+            return Docs.FreeformDocument([], options);
+        } else if (field instanceof ImageField) {
+            return Docs.ImageDocument("https://www.freepik.com/free-icon/picture-frame-with-mountain-image_748687.htm", options);
+        }
+        return new Doc;
+    }
+
     render() {
         let dividerDragger = this.splitPercentage === 0 ? (null) :
             <div className="keyValueBox-dividerDragger" style={{ transform: `translate(calc(${100 - this.splitPercentage}% - 5px), 0px)` }}>
@@ -144,7 +206,9 @@ export class KeyValueBox extends React.Component<FieldViewProps> {
             <table className="keyValueBox-table">
                 <tbody className="keyValueBox-tbody">
                     <tr className="keyValueBox-header">
-                        <th className="keyValueBox-key" style={{ width: `${100 - this.splitPercentage}%` }}>Key</th>
+                        <th className="keyValueBox-key" style={{ width: `${100 - this.splitPercentage}%` }} ref={this._keyHeader}
+                            onPointerDown={SetupDrag(this._keyHeader, this.getTemplate)}
+                        >Key</th>
                         <th className="keyValueBox-fields" style={{ width: `${this.splitPercentage}%` }}>Fields</th>
                     </tr>
                     {this.createTable()}
diff --git a/src/client/views/nodes/KeyValuePair.scss b/src/client/views/nodes/KeyValuePair.scss
index a1c5d5537..f78767234 100644
--- a/src/client/views/nodes/KeyValuePair.scss
+++ b/src/client/views/nodes/KeyValuePair.scss
@@ -3,6 +3,7 @@
 
 .keyValuePair-td-key {
     display:inline-block;
+
     .keyValuePair-td-key-container{
         width:100%;
         height:100%;
@@ -10,14 +11,23 @@
         flex-direction: row;           
         flex-wrap: nowrap;              
         justify-content: space-between; 
+        align-items: center;
         .keyValuePair-td-key-delete{
             position: relative;
             background-color: transparent;
             color:red;
         }
+        .keyValuePair-td-key-check {
+            position: relative;
+            margin: 0;
+        }
         .keyValuePair-keyField {
             width:100%;
-            text-align: center;
+            margin-left: 20px;
+            margin-top: -1px;
+            font-family: monospace;
+            // text-align: center;
+            align-self: center;
             position: relative;
             overflow: auto;
         }
@@ -26,12 +36,25 @@
 .keyValuePair-td-value {
     display:inline-block;
     overflow: scroll;
-    img { 
-        max-height: 36px;
-        width: auto;
-    }
-    .videoBox-cont{
-        width: auto; 
-        max-height: 36px; 
+    font-family: monospace;
+    height: 30px;
+        .keyValuePair-td-value-container {
+            display: flex;
+            align-items: center;
+            align-content: center;
+            flex-direction: row;
+            justify-content: space-between;
+            flex-wrap: nowrap;
+            width: 100%;
+            height: 100%;
+
+            img { 
+                max-height: 36px;
+                width: auto;
+            }
+            .videoBox-cont{
+                width: auto; 
+                max-height: 36px; 
+            }
     }
 }
\ No newline at end of file
diff --git a/src/client/views/nodes/KeyValuePair.tsx b/src/client/views/nodes/KeyValuePair.tsx
index ede4e3858..b5db52ac7 100644
--- a/src/client/views/nodes/KeyValuePair.tsx
+++ b/src/client/views/nodes/KeyValuePair.tsx
@@ -12,6 +12,7 @@ import React = require("react");
 import { Doc, Opt, Field } from '../../../new_fields/Doc';
 import { FieldValue } from '../../../new_fields/Types';
 import { KeyValueBox } from './KeyValueBox';
+import { DragManager, SetupDrag } from '../../util/DragManager';
 
 // Represents one row in a key value plane
 
@@ -23,6 +24,20 @@ export interface KeyValuePairProps {
 }
 @observer
 export class KeyValuePair extends React.Component<KeyValuePairProps> {
+    @observable private isPointerOver = false;
+    @observable public isChecked = false;
+    private checkbox = React.createRef<HTMLInputElement>();
+
+    @action
+    handleCheck = (e: React.ChangeEvent<HTMLInputElement>) => {
+        this.isChecked = e.currentTarget.checked;
+    }
+
+    @action
+    uncheck = () => {
+        this.checkbox.current!.checked = false;
+        this.isChecked = false;
+    }
 
     render() {
         let props: FieldViewProps = {
@@ -44,12 +59,16 @@ export class KeyValuePair extends React.Component<KeyValuePairProps> {
             addDocTab: returnZero,
         };
         let contents = <FieldView {...props} />;
-        let fieldKey = Object.keys(props.Document).indexOf(props.fieldKey) !== -1 ? props.fieldKey : "(" + props.fieldKey + ")";
+        // let fieldKey = Object.keys(props.Document).indexOf(props.fieldKey) !== -1 ? props.fieldKey : "(" + props.fieldKey + ")";
+        let keyStyle = Object.keys(props.Document).indexOf(props.fieldKey) !== -1 ? "black" : "blue";
+
+        let hover = { transition: "0.3s ease opacity", opacity: this.isPointerOver || this.isChecked ? 1 : 0 };
+
         return (
-            <tr className={this.props.rowStyle}>
+            <tr className={this.props.rowStyle} onPointerEnter={action(() => this.isPointerOver = true)} onPointerLeave={action(() => this.isPointerOver = false)}>
                 <td className="keyValuePair-td-key" style={{ width: `${this.props.keyWidth}%` }}>
                     <div className="keyValuePair-td-key-container">
-                        <button className="keyValuePair-td-key-delete" onClick={() => {
+                        <button style={hover} className="keyValuePair-td-key-delete" onClick={() => {
                             if (Object.keys(props.Document).indexOf(props.fieldKey) !== -1) {
                                 props.Document[props.fieldKey] = undefined;
                             }
@@ -57,22 +76,35 @@ export class KeyValuePair extends React.Component<KeyValuePairProps> {
                         }}>
                             X
                         </button>
-                        <div className="keyValuePair-keyField">{fieldKey}</div>
+                        <input
+                            className={"keyValuePair-td-key-check"}
+                            type="checkbox"
+                            style={hover}
+                            onChange={this.handleCheck}
+                            ref={this.checkbox}
+                        />
+                        <div className="keyValuePair-keyField" style={{ color: keyStyle }}>{props.fieldKey}</div>
                     </div>
                 </td>
                 <td className="keyValuePair-td-value" style={{ width: `${100 - this.props.keyWidth}%` }}>
-                    <EditableView contents={contents} height={36} GetValue={() => {
-                        const onDelegate = Object.keys(props.Document).includes(props.fieldKey);
+                    <div className="keyValuePair-td-value-container">
+                        <EditableView
+                            contents={contents}
+                            height={36}
+                            GetValue={() => {
+                                const onDelegate = Object.keys(props.Document).includes(props.fieldKey);
 
-                        let field = FieldValue(props.Document[props.fieldKey]);
-                        if (Field.IsField(field)) {
-                            return (onDelegate ? "=" : "") + Field.toScriptString(field);
-                        }
-                        return "";
-                    }}
-                        SetValue={(value: string) =>
-                            KeyValueBox.SetField(props.Document, props.fieldKey, value)}>
-                    </EditableView></td>
+                                let field = FieldValue(props.Document[props.fieldKey]);
+                                if (Field.IsField(field)) {
+                                    return (onDelegate ? "=" : "") + Field.toScriptString(field);
+                                }
+                                return "";
+                            }}
+                            SetValue={(value: string) =>
+                                KeyValueBox.SetField(props.Document, props.fieldKey, value)}>
+                        </EditableView>
+                    </div>
+                </td>
             </tr>
         );
     }
diff --git a/src/scraping/buxton/scraper.py b/src/scraping/buxton/scraper.py
index 97af10519..fcffeac13 100644
--- a/src/scraping/buxton/scraper.py
+++ b/src/scraping/buxton/scraper.py
@@ -60,7 +60,7 @@ def protofy(fieldId):
     }
 
 
-def write_schema(parse_results, display_fields):
+def write_schema(parse_results, display_fields, storage_key):
     view_guids = parse_results["child_guids"]
 
     data_doc = parse_results["schema"]
@@ -87,7 +87,7 @@ def write_schema(parse_results, display_fields):
     }
 
     fields["proto"] = protofy("collectionProto")
-    fields["data"] = listify(proxify_guids(view_guids))
+    fields[storage_key] = listify(proxify_guids(view_guids))
     fields["schemaColumns"] = listify(display_fields)
     fields["backgroundColor"] = "white"
     fields["scale"] = 0.5
@@ -304,7 +304,7 @@ for file_name in os.listdir(source):
     if file_name.endswith('.docx'):
         candidates += 1
         schema_guids.append(write_schema(
-            parse_document(file_name), ["title", "data"]))
+            parse_document(file_name), ["title", "data"], "image_data"))
 
 print("writing parent schema...")
 parent_guid = write_schema({
@@ -314,7 +314,7 @@ parent_guid = write_schema({
         "__type": "Doc"
     },
     "child_guids": schema_guids
-}, ["title", "short_description", "original_price"])
+}, ["title", "short_description", "original_price"], "data")
 
 print("appending parent schema to main workspace...\n")
 db.newDocuments.update_one(
-- 
cgit v1.2.3-70-g09d2


From 5b2ee61fd04f18478e3b5355ae19cd06a0840fa5 Mon Sep 17 00:00:00 2001
From: Sam Wilkins <35748010+samwilkins333@users.noreply.github.com>
Date: Thu, 27 Jun 2019 12:39:36 -0400
Subject: templating workflow improvements

---
 src/client/views/nodes/KeyValueBox.tsx | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

(limited to 'src/client/views/nodes/KeyValueBox.tsx')

diff --git a/src/client/views/nodes/KeyValueBox.tsx b/src/client/views/nodes/KeyValueBox.tsx
index 4beb70284..af22f0a48 100644
--- a/src/client/views/nodes/KeyValueBox.tsx
+++ b/src/client/views/nodes/KeyValueBox.tsx
@@ -145,7 +145,9 @@ export class KeyValueBox extends React.Component<FieldViewProps> {
     }
 
     getTemplate = async () => {
-        let parent = Docs.FreeformDocument([], { width: 800, height: 800, title: "Template" });
+        let parent = Docs.StackingDocument([], { width: 800, height: 800, title: "Template" });
+        parent.singleColumn = false;
+        parent.columnWidth = 50;
         for (let row of this.rows.filter(row => row.isChecked)) {
             await this.createTemplateField(parent, row);
             row.uncheck();
@@ -167,8 +169,8 @@ export class KeyValueBox extends React.Component<FieldViewProps> {
         let template = Doc.MakeAlias(target);
         template.proto = parent;
         template.title = metaKey;
-        template.nativeWidth = 300;
-        template.nativeHeight = 300;
+        template.nativeWidth = 0;
+        template.nativeHeight = 0;
         template.embed = true;
         template.isTemplate = true;
         template.templates = new List<string>([Templates.TitleBar(metaKey)]);
@@ -187,7 +189,7 @@ export class KeyValueBox extends React.Component<FieldViewProps> {
     inferType = (field: FieldResult, metaKey: string) => {
         let options = { width: 300, height: 300, title: metaKey };
         if (field instanceof RichTextField || typeof field === "string" || typeof field === "number") {
-            return Docs.TextDocument(options);
+            return Docs.StackingDocument(options);
         } else if (field instanceof List) {
             return Docs.FreeformDocument([], options);
         } else if (field instanceof ImageField) {
@@ -218,4 +220,4 @@ export class KeyValueBox extends React.Component<FieldViewProps> {
             {dividerDragger}
         </div>);
     }
-}
\ No newline at end of file
+}
-- 
cgit v1.2.3-70-g09d2


From 2ca24a234817495c3330b93b504b13c0c6db91f3 Mon Sep 17 00:00:00 2001
From: Tyler Schicke <tyler_schicke@brown.edu>
Date: Thu, 27 Jun 2019 14:22:45 -0400
Subject: fixed template stuff

---
 src/client/views/nodes/KeyValueBox.tsx | 14 +++++++++++---
 1 file changed, 11 insertions(+), 3 deletions(-)

(limited to 'src/client/views/nodes/KeyValueBox.tsx')

diff --git a/src/client/views/nodes/KeyValueBox.tsx b/src/client/views/nodes/KeyValueBox.tsx
index af22f0a48..0e798d291 100644
--- a/src/client/views/nodes/KeyValueBox.tsx
+++ b/src/client/views/nodes/KeyValueBox.tsx
@@ -99,8 +99,16 @@ export class KeyValueBox extends React.Component<FieldViewProps> {
 
         let rows: JSX.Element[] = [];
         let i = 0;
+        const self = this;
         for (let key of Object.keys(ids).sort()) {
-            rows.push(<KeyValuePair doc={realDoc} ref={(el) => { if (el) this.rows.push(el); }} keyWidth={100 - this.splitPercentage} rowStyle={"keyValueBox-" + (i++ % 2 ? "oddRow" : "evenRow")} key={key} keyName={key} />);
+            rows.push(<KeyValuePair doc={realDoc} ref={(function () {
+                let oldEl: KeyValuePair | undefined;
+                return (el: KeyValuePair) => {
+                    if (oldEl) self.rows.splice(self.rows.indexOf(oldEl), 1);
+                    oldEl = el;
+                    if (el) self.rows.push(el);
+                };
+            })()} keyWidth={100 - this.splitPercentage} rowStyle={"keyValueBox-" + (i++ % 2 ? "oddRow" : "evenRow")} key={key} keyName={key} />);
         }
         return rows;
     }
@@ -189,9 +197,9 @@ export class KeyValueBox extends React.Component<FieldViewProps> {
     inferType = (field: FieldResult, metaKey: string) => {
         let options = { width: 300, height: 300, title: metaKey };
         if (field instanceof RichTextField || typeof field === "string" || typeof field === "number") {
-            return Docs.StackingDocument(options);
+            return Docs.TextDocument(options);
         } else if (field instanceof List) {
-            return Docs.FreeformDocument([], options);
+            return Docs.StackingDocument([], options);
         } else if (field instanceof ImageField) {
             return Docs.ImageDocument("https://www.freepik.com/free-icon/picture-frame-with-mountain-image_748687.htm", options);
         }
-- 
cgit v1.2.3-70-g09d2


From f745ee91930c2b559098c5c6d75d5108edca2f01 Mon Sep 17 00:00:00 2001
From: Tyler Schicke <tyler_schicke@brown.edu>
Date: Fri, 28 Jun 2019 16:31:24 -0400
Subject: test3

---
 src/client/views/nodes/KeyValueBox.tsx | 1 +
 1 file changed, 1 insertion(+)

(limited to 'src/client/views/nodes/KeyValueBox.tsx')

diff --git a/src/client/views/nodes/KeyValueBox.tsx b/src/client/views/nodes/KeyValueBox.tsx
index 0e798d291..eee4ec670 100644
--- a/src/client/views/nodes/KeyValueBox.tsx
+++ b/src/client/views/nodes/KeyValueBox.tsx
@@ -207,6 +207,7 @@ export class KeyValueBox extends React.Component<FieldViewProps> {
     }
 
     render() {
+        return null;
         let dividerDragger = this.splitPercentage === 0 ? (null) :
             <div className="keyValueBox-dividerDragger" style={{ transform: `translate(calc(${100 - this.splitPercentage}% - 5px), 0px)` }}>
                 <div className="keyValueBox-dividerDraggerThumb" onPointerDown={this.onDividerDown} />
-- 
cgit v1.2.3-70-g09d2


From 8ef971485492e3dc461cd93b2ae89e01b9995741 Mon Sep 17 00:00:00 2001
From: Tyler Schicke <tyler_schicke@brown.edu>
Date: Fri, 28 Jun 2019 16:57:46 -0400
Subject: undo tests

---
 src/client/views/nodes/DocumentContentsView.tsx | 3 +--
 src/client/views/nodes/KeyValueBox.tsx          | 1 -
 src/client/views/pdf/PDFViewer.tsx              | 3 +--
 src/new_fields/Doc.ts                           | 1 -
 4 files changed, 2 insertions(+), 6 deletions(-)

(limited to 'src/client/views/nodes/KeyValueBox.tsx')

diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx
index 240b21714..0da4888a1 100644
--- a/src/client/views/nodes/DocumentContentsView.tsx
+++ b/src/client/views/nodes/DocumentContentsView.tsx
@@ -52,8 +52,7 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & {
         const layout = Cast(layoutDoc[this.props.layoutKey], "string");
         if (layout === undefined) {
             return this.props.Document.data ?
-                "null" :
-                // "<FieldView {...props} fieldKey='data' />" :
+                "<FieldView {...props} fieldKey='data' />" :
                 KeyValueBox.LayoutString(layoutDoc.proto ? "proto" : "");
         } else if (typeof layout === "string") {
             return layout;
diff --git a/src/client/views/nodes/KeyValueBox.tsx b/src/client/views/nodes/KeyValueBox.tsx
index eee4ec670..0e798d291 100644
--- a/src/client/views/nodes/KeyValueBox.tsx
+++ b/src/client/views/nodes/KeyValueBox.tsx
@@ -207,7 +207,6 @@ export class KeyValueBox extends React.Component<FieldViewProps> {
     }
 
     render() {
-        return null;
         let dividerDragger = this.splitPercentage === 0 ? (null) :
             <div className="keyValueBox-dividerDragger" style={{ transform: `translate(calc(${100 - this.splitPercentage}% - 5px), 0px)` }}>
                 <div className="keyValueBox-dividerDraggerThumb" onPointerDown={this.onDividerDown} />
diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx
index 380ba3c45..a645b0041 100644
--- a/src/client/views/pdf/PDFViewer.tsx
+++ b/src/client/views/pdf/PDFViewer.tsx
@@ -1,4 +1,4 @@
-import { action, computed, IReactionDisposer, observable, reaction, runInAction, trace } from "mobx";
+import { action, computed, IReactionDisposer, observable, reaction, runInAction } from "mobx";
 import { observer } from "mobx-react";
 import * as Pdfjs from "pdfjs-dist";
 import "pdfjs-dist/web/pdf_viewer.css";
@@ -620,7 +620,6 @@ class Viewer extends React.Component<IViewerProps> {
     }
 
     render() {
-        trace();
         let compiled = this._script;
         return (
             <div ref={this._mainCont} style={{ pointerEvents: "all" }} onPointerDown={this.pointerDown}>
diff --git a/src/new_fields/Doc.ts b/src/new_fields/Doc.ts
index 0340806ef..27dcfba08 100644
--- a/src/new_fields/Doc.ts
+++ b/src/new_fields/Doc.ts
@@ -281,7 +281,6 @@ export namespace Doc {
     }
 
     export function expandTemplateLayout(templateLayoutDoc: Doc, dataDoc?: Doc) {
-        return templateLayoutDoc;
         let resolvedDataDoc = (templateLayoutDoc !== dataDoc) ? dataDoc : undefined;
         if (!dataDoc || !(templateLayoutDoc && !(Cast(templateLayoutDoc.layout, Doc) instanceof Doc) && resolvedDataDoc && resolvedDataDoc !== templateLayoutDoc)) {
             return templateLayoutDoc;
-- 
cgit v1.2.3-70-g09d2


From a8664face6f19cd2373bd928c2a2b3043bb6278d Mon Sep 17 00:00:00 2001
From: Tyler Schicke <tyler_schicke@brown.edu>
Date: Tue, 2 Jul 2019 19:02:51 -0400
Subject: Added arrange script to freeform views

---
 src/client/views/MainView.tsx                      |  4 +-
 src/client/views/OverlayView.tsx                   | 46 ++++++++++++++++++++++
 src/client/views/ScriptBox.tsx                     | 36 +++++++++++++++++
 .../collectionFreeForm/CollectionFreeFormView.tsx  | 41 ++++++++++++++++++-
 .../views/nodes/CollectionFreeFormDocumentView.tsx | 12 ++++--
 src/client/views/nodes/KeyValueBox.tsx             | 12 ++++--
 6 files changed, 141 insertions(+), 10 deletions(-)
 create mode 100644 src/client/views/OverlayView.tsx
 create mode 100644 src/client/views/ScriptBox.tsx

(limited to 'src/client/views/nodes/KeyValueBox.tsx')

diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 04ecc47cf..cc39dc04c 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -38,6 +38,7 @@ import PDFMenu from './pdf/PDFMenu';
 import { InkTool } from '../../new_fields/InkField';
 import _ from "lodash";
 import KeyManager from './GlobalKeyHandler';
+import { OverlayView } from './OverlayView';
 
 @observer
 export class MainView extends React.Component {
@@ -291,7 +292,7 @@ export class MainView extends React.Component {
             } else {
                 CollectionDockingView.Instance.AddRightSplit(doc, dataDoc);
             }
-        }
+        };
         let flyout = <DocumentView
             Document={CurrentUserUtils.UserDocument}
             DataDoc={undefined}
@@ -441,6 +442,7 @@ export class MainView extends React.Component {
                 {this.miscButtons}
                 <PDFMenu />
                 <MainOverlayTextBox />
+                <OverlayView />
             </div>
         );
     }
diff --git a/src/client/views/OverlayView.tsx b/src/client/views/OverlayView.tsx
new file mode 100644
index 000000000..72f1068ce
--- /dev/null
+++ b/src/client/views/OverlayView.tsx
@@ -0,0 +1,46 @@
+import * as React from "react";
+import { observer } from "mobx-react";
+import { observable, action } from "mobx";
+
+export type OverlayDisposer = () => void;
+
+export type OverlayElementOptions = {
+    x: number;
+    y: number;
+    width?: number;
+    height?: number;
+};
+
+@observer
+export class OverlayView extends React.Component {
+    public static Instance: OverlayView;
+    @observable.shallow
+    private _elements: { ele: JSX.Element, options: OverlayElementOptions }[] = [];
+
+    constructor(props: any) {
+        super(props);
+        if (!OverlayView.Instance) {
+            OverlayView.Instance = this;
+        }
+    }
+
+    @action
+    addElement(ele: JSX.Element, options: OverlayElementOptions): OverlayDisposer {
+        const eleWithPosition = { ele, options };
+        this._elements.push(eleWithPosition);
+        return action(() => {
+            const index = this._elements.indexOf(eleWithPosition);
+            if (index !== -1) this._elements.splice(index, 1);
+        });
+    }
+
+    render() {
+        return (
+            <div>
+                {this._elements.map(({ ele, options: { x, y, width, height } }) => (
+                    <div style={{ position: "absolute", transform: `translate(${x}px, ${y}px)`, width, height }}>{ele}</div>
+                ))}
+            </div>
+        );
+    }
+}
\ No newline at end of file
diff --git a/src/client/views/ScriptBox.tsx b/src/client/views/ScriptBox.tsx
new file mode 100644
index 000000000..aea9d52a4
--- /dev/null
+++ b/src/client/views/ScriptBox.tsx
@@ -0,0 +1,36 @@
+import * as React from "react";
+import { observer } from "mobx-react";
+import { observable, action } from "mobx";
+
+export interface ScriptBoxProps {
+    onSave: (text: string, onError: (error: string) => void) => void;
+    onCancel?: () => void;
+}
+
+@observer
+export class ScriptBox extends React.Component<ScriptBoxProps> {
+    @observable
+    private _scriptText: string = "";
+
+    @action
+    onChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
+        this._scriptText = e.target.value;
+    }
+
+    @action
+    onError = (error: string) => {
+        console.log(error);
+    }
+
+    render() {
+        return (
+            <div className="scriptBox-outerDiv">
+                <div className="scriptBox-toolbar">
+                    <button onClick={e => { this.props.onSave(this._scriptText, this.onError); e.stopPropagation(); }}>Save</button>
+                    <button onClick={e => { this.props.onCancel && this.props.onCancel(); e.stopPropagation(); }}>Cancel</button>
+                </div>
+                <textarea className="scriptBox-textarea" onChange={this.onChange} value={this._scriptText}></textarea>
+            </div>
+        );
+    }
+}
\ No newline at end of file
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 169dbe540..23c0fdd0d 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -27,6 +27,10 @@ import "./CollectionFreeFormView.scss";
 import { MarqueeView } from "./MarqueeView";
 import React = require("react");
 import v5 = require("uuid/v5");
+import { ScriptField } from "../../../../new_fields/ScriptField";
+import { OverlayView } from "../../OverlayView";
+import { ScriptBox } from "../../ScriptBox";
+import { CompileScript } from "../../../util/Scripting";
 
 
 export const panZoomSchema = createSchema({
@@ -388,6 +392,18 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
         };
     }
 
+    getCalculatedPositions(doc: Doc, index: number, collection: Doc): { x?: number, y?: number, width?: number, height?: number } | undefined {
+        const script = Cast(this.props.Document.arrangeScript, ScriptField);
+        if (!script) {
+            return undefined;
+        }
+        const result = script.script.run({ doc, index, collection });
+        if (!result.success) {
+            return undefined;
+        }
+        return result.result;
+    }
+
     @computed.struct
     get views() {
         let curPage = FieldValue(this.Document.curPage, -1);
@@ -397,7 +413,8 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
             if (Math.round(page) === Math.round(curPage) || page === -1) {
                 let minim = BoolCast(doc.isMinimized, false);
                 if (minim === undefined || !minim) {
-                    prev.push(<CollectionFreeFormDocumentView key={doc[Id]} {...this.getChildDocumentViewProps(doc)} />);
+                    const pos = this.getCalculatedPositions(doc, prev.length, this.Document) || {};
+                    prev.push(<CollectionFreeFormDocumentView key={doc[Id]} x={pos.x} y={pos.y} width={pos.width} height={pos.height} {...this.getChildDocumentViewProps(doc)} />);
                 }
             }
             return prev;
@@ -438,6 +455,28 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
                 }
             }
         });
+        ContextMenu.Instance.addItem({
+            description: "Add freeform arrangement",
+            event: () => {
+                let overlayDisposer: () => void;
+                let scriptingBox = <ScriptBox onCancel={() => overlayDisposer()} onSave={(text, onError) => {
+                    const script = CompileScript(text, {
+                        params: {
+                            doc: "Doc", index: "number", collection: "Doc"
+                        },
+                        requiredType: "{x: number, y: number, width?: number, height?: number}",
+                        typecheck: false
+                    });
+                    if (!script.compiled) {
+                        onError(script.errors.map(error => error.messageText).join("\n"));
+                        return;
+                    }
+                    this.props.Document.arrangeScript = new ScriptField(script);
+                    overlayDisposer();
+                }} />;
+                overlayDisposer = OverlayView.Instance.addElement(scriptingBox, { x: 100, y: 100, width: 200, height: 200 });
+            }
+        });
     }
 
     private childViews = () => [
diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
index 858959d81..8556817cc 100644
--- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
+++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
@@ -9,6 +9,10 @@ import "./DocumentView.scss";
 import React = require("react");
 
 export interface CollectionFreeFormDocumentViewProps extends DocumentViewProps {
+    x?: number;
+    y?: number;
+    width?: number;
+    height?: number;
 }
 
 const schema = createSchema({
@@ -23,13 +27,13 @@ const FreeformDocument = makeInterface(schema, positionSchema);
 @observer
 export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeFormDocumentViewProps, FreeformDocument>(FreeformDocument) {
     @computed get transform() { return `scale(${this.props.ContentScaling()}) translate(${this.X}px, ${this.Y}px) scale(${this.zoom}) `; }
-    @computed get X() { return FieldValue(this.Document.x, 0); }
-    @computed get Y() { return FieldValue(this.Document.y, 0); }
+    @computed get X() { return this.props.x !== undefined ? this.props.x : this.Document.x || 0; }
+    @computed get Y() { return this.props.y !== undefined ? this.props.y : this.Document.y || 0; }
+    @computed get width(): number { return BoolCast(this.props.Document.willMaximize) ? 0 : this.props.width !== undefined ? this.props.width : this.Document.width || 0; }
+    @computed get height(): number { return BoolCast(this.props.Document.willMaximize) ? 0 : this.props.height !== undefined ? this.props.height : this.Document.height || 0; }
     @computed get zoom(): number { return 1 / FieldValue(this.Document.zoomBasis, 1); }
     @computed get nativeWidth(): number { return FieldValue(this.Document.nativeWidth, 0); }
     @computed get nativeHeight(): number { return FieldValue(this.Document.nativeHeight, 0); }
-    @computed get width(): number { return BoolCast(this.props.Document.willMaximize) ? 0 : FieldValue(this.Document.width, 0); }
-    @computed get height(): number { return BoolCast(this.props.Document.willMaximize) ? 0 : FieldValue(this.Document.height, 0); }
 
     set width(w: number) {
         this.Document.width = w;
diff --git a/src/client/views/nodes/KeyValueBox.tsx b/src/client/views/nodes/KeyValueBox.tsx
index 0e798d291..9407d742c 100644
--- a/src/client/views/nodes/KeyValueBox.tsx
+++ b/src/client/views/nodes/KeyValueBox.tsx
@@ -9,7 +9,7 @@ import { KeyValuePair } from "./KeyValuePair";
 import React = require("react");
 import { NumCast, Cast, FieldValue, StrCast } from "../../../new_fields/Types";
 import { Doc, Field, FieldResult } from "../../../new_fields/Doc";
-import { ComputedField } from "../../../new_fields/ScriptField";
+import { ComputedField, ScriptField } from "../../../new_fields/ScriptField";
 import { SetupDrag } from "../../util/DragManager";
 import { Docs } from "../../documents/Documents";
 import { RawDataOperationParameters } from "../../northstar/model/idea/idea";
@@ -50,7 +50,7 @@ export class KeyValueBox extends React.Component<FieldViewProps> {
         let eq = value.startsWith("=");
         let target = eq ? doc : Doc.GetProto(doc);
         value = eq ? value.substr(1) : value;
-        let dubEq = value.startsWith(":=");
+        let dubEq = value.startsWith(":=") ? 1 : value.startsWith(";=") ? 2 : 0;
         value = dubEq ? value.substr(2) : value;
         let options: ScriptOptions = { addReturn: true, params: { this: "Doc" } };
         if (dubEq) options.typecheck = false;
@@ -58,8 +58,12 @@ export class KeyValueBox extends React.Component<FieldViewProps> {
         if (!script.compiled) {
             return false;
         }
-        let field = new ComputedField(script);
-        if (!dubEq) {
+        let field: Field;
+        if (dubEq === 1) {
+            field = new ComputedField(script);
+        } else if (dubEq === 2) {
+            field = new ScriptField(script);
+        } else {
             let res = script.run({ this: target });
             if (!res.success) return false;
             field = res.result;
-- 
cgit v1.2.3-70-g09d2


From 04887c8a578147015421d3909bd100c82ac5e31d Mon Sep 17 00:00:00 2001
From: Sam Wilkins <samwilkins333@gmail.com>
Date: Mon, 8 Jul 2019 19:55:15 -0400
Subject: fixed readonly and hopefully cursor proto bugs

---
 src/client/DocServer.ts                            |  4 +-
 .../util/Import & Export/DirectoryImportBox.tsx    |  1 -
 src/client/views/collections/CollectionSubView.tsx |  9 ++-
 src/client/views/nodes/KeyValueBox.tsx             | 81 +++++++++++++++-------
 4 files changed, 66 insertions(+), 29 deletions(-)

(limited to 'src/client/views/nodes/KeyValueBox.tsx')

diff --git a/src/client/DocServer.ts b/src/client/DocServer.ts
index cbcf751ee..652a9b701 100644
--- a/src/client/DocServer.ts
+++ b/src/client/DocServer.ts
@@ -12,7 +12,9 @@ export namespace DocServer {
     const GUID: string = Utils.GenerateGuid();
 
     export function makeReadOnly() {
-        _CreateField = emptyFunction;
+        _CreateField = field => {
+            _cache[field[Id]] = field;
+        };
         _UpdateField = emptyFunction;
         _respondToUpdate = emptyFunction;
     }
diff --git a/src/client/util/Import & Export/DirectoryImportBox.tsx b/src/client/util/Import & Export/DirectoryImportBox.tsx
index 1a939882c..ce95ba90e 100644
--- a/src/client/util/Import & Export/DirectoryImportBox.tsx	
+++ b/src/client/util/Import & Export/DirectoryImportBox.tsx	
@@ -79,7 +79,6 @@ export default class DirectoryImportBox extends React.Component<FieldViewProps>
         let validated: File[] = [];
         for (let i = 0; i < files.length; i++) {
             let file = files.item(i);
-            console.log(file);
             file && !unsupported.includes(file.type) && validated.push(file);
         }
 
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index bf17088ae..a8810f336 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -19,6 +19,7 @@ import { CollectionPDFView } from "./CollectionPDFView";
 import { CollectionVideoView } from "./CollectionVideoView";
 import { CollectionView } from "./CollectionView";
 import React = require("react");
+import { MainView } from "../MainView";
 
 export interface CollectionViewProps extends FieldViewProps {
     addDocument: (document: Doc, allowDuplicates?: boolean) => boolean;
@@ -67,10 +68,16 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
             let email = CurrentUserUtils.email;
             let pos = { x: position[0], y: position[1] };
             if (id && email) {
-                const proto = await doc.proto;
+                const proto = Doc.GetProto(doc);
                 if (!proto) {
                     return;
                 }
+                if (proto[Id] === "collectionProto") {
+                    alert("COLLECTION PROTO CURSOR ISSUE DETECTED! Check console for more info...");
+                    console.log(doc);
+                    console.log(proto);
+                    throw new Error(`AHA! You were trying to set a cursor on a collection's proto, which is the original collection proto! Look at the two previously printed lines for document values!`);
+                }
                 let cursors = Cast(proto.cursors, listSpec(CursorField));
                 if (!cursors) {
                     proto.cursors = cursors = new List<CursorField>();
diff --git a/src/client/views/nodes/KeyValueBox.tsx b/src/client/views/nodes/KeyValueBox.tsx
index 9407d742c..fbabe224e 100644
--- a/src/client/views/nodes/KeyValueBox.tsx
+++ b/src/client/views/nodes/KeyValueBox.tsx
@@ -18,6 +18,7 @@ import { List } from "../../../new_fields/List";
 import { TextField } from "../../util/ProsemirrorCopy/prompt";
 import { RichTextField } from "../../../new_fields/RichTextField";
 import { ImageField } from "../../../new_fields/URLField";
+import { SelectionManager } from "../../util/SelectionManager";
 
 @observer
 export class KeyValueBox extends React.Component<FieldViewProps> {
@@ -167,44 +168,72 @@ export class KeyValueBox extends React.Component<FieldViewProps> {
         return parent;
     }
 
-    createTemplateField = async (parent: Doc, row: KeyValuePair) => {
-        let collectionKeyProp = `fieldKey={"data"}`;
-        let metaKey = row.props.keyName;
-        let metaKeyProp = `fieldKey={"${metaKey}"}`;
+    createTemplateField = async (parentStackingDoc: Doc, row: KeyValuePair) => {
+        // let collectionKeyProp = `fieldKey={"data"}`;
+        // let metaKey = row.props.keyName;
+        // let metaKeyProp = `fieldKey={"${metaKey}"}`;
+
+        // let sourceDoc = await Cast(this.props.Document.data, Doc);
+        // if (!sourceDoc) {
+        //     return;
+        // }
+        // let target = this.inferType(sourceDoc[metaKey], metaKey);
 
+        // let template = Doc.MakeAlias(target);
+        // template.proto = parent;
+        // template.title = metaKey;
+        // template.nativeWidth = 0;
+        // template.nativeHeight = 0;
+        // template.embed = true;
+        // template.isTemplate = true;
+        // template.templates = new List<string>([Templates.TitleBar(metaKey)]);
+        // if (target.backgroundLayout) {
+        //     let metaAnoKeyProp = `fieldKey={"${metaKey}"} fieldExt={"annotations"}`;
+        //     let collectionAnoKeyProp = `fieldKey={"annotations"}`;
+        //     template.layout = StrCast(target.layout).replace(collectionAnoKeyProp, metaAnoKeyProp);
+        //     template.backgroundLayout = StrCast(target.backgroundLayout).replace(collectionKeyProp, metaKeyProp);
+        // } else {
+        //     template.layout = StrCast(target.layout).replace(collectionKeyProp, metaKeyProp);
+        // }
+
+        let metaKey = row.props.keyName;
         let sourceDoc = await Cast(this.props.Document.data, Doc);
         if (!sourceDoc) {
             return;
         }
-        let target = this.inferType(sourceDoc[metaKey], metaKey);
-
-        let template = Doc.MakeAlias(target);
-        template.proto = parent;
-        template.title = metaKey;
-        template.nativeWidth = 0;
-        template.nativeHeight = 0;
-        template.embed = true;
-        template.isTemplate = true;
-        template.templates = new List<string>([Templates.TitleBar(metaKey)]);
-        if (target.backgroundLayout) {
-            let metaAnoKeyProp = `fieldKey={"${metaKey}"} fieldExt={"annotations"}`;
-            let collectionAnoKeyProp = `fieldKey={"annotations"}`;
-            template.layout = StrCast(target.layout).replace(collectionAnoKeyProp, metaAnoKeyProp);
-            template.backgroundLayout = StrCast(target.backgroundLayout).replace(collectionKeyProp, metaKeyProp);
-        } else {
-            template.layout = StrCast(target.layout).replace(collectionKeyProp, metaKeyProp);
+        let fieldTemplate = this.inferType(sourceDoc[metaKey], metaKey);
+
+        // move data doc fields to layout doc as needed (nativeWidth/nativeHeight, data, ??)
+        let backgroundLayout = StrCast(fieldTemplate.backgroundLayout);
+        let layout = StrCast(fieldTemplate.layout).replace(/fieldKey={"[^"]*"}/, `fieldKey={"${metaKey}"}`);
+        if (backgroundLayout) {
+            layout = StrCast(fieldTemplate.layout).replace(/fieldKey={"annotations"}/, `fieldKey={"${metaKey}"} fieldExt={"annotations"}`);
+            backgroundLayout = backgroundLayout.replace(/fieldKey={"[^"]*"}/, `fieldKey={"${metaKey}"}`);
         }
-        Doc.AddDocToList(parent, "data", template);
+        let nw = NumCast(fieldTemplate.nativeWidth);
+        let nh = NumCast(fieldTemplate.nativeHeight);
+
+        fieldTemplate.title = metaKey;
+        fieldTemplate.layout = layout;
+        fieldTemplate.backgroundLayout = backgroundLayout;
+        fieldTemplate.nativeWidth = nw;
+        fieldTemplate.nativeHeight = nh;
+        fieldTemplate.embed = true;
+        fieldTemplate.isTemplate = true;
+        fieldTemplate.templates = new List<string>([Templates.TitleBar(metaKey)]);
+        fieldTemplate.proto = Doc.GetProto(parentStackingDoc);
+
+        Doc.AddDocToList(parentStackingDoc, "data", fieldTemplate);
         row.uncheck();
     }
 
-    inferType = (field: FieldResult, metaKey: string) => {
+    inferType = (data: FieldResult, metaKey: string) => {
         let options = { width: 300, height: 300, title: metaKey };
-        if (field instanceof RichTextField || typeof field === "string" || typeof field === "number") {
+        if (data instanceof RichTextField || typeof data === "string" || typeof data === "number") {
             return Docs.TextDocument(options);
-        } else if (field instanceof List) {
+        } else if (data instanceof List) {
             return Docs.StackingDocument([], options);
-        } else if (field instanceof ImageField) {
+        } else if (data instanceof ImageField) {
             return Docs.ImageDocument("https://www.freepik.com/free-icon/picture-frame-with-mountain-image_748687.htm", options);
         }
         return new Doc;
-- 
cgit v1.2.3-70-g09d2


From 7fdb33cafaa3e8593d648ddba994356a9625ff56 Mon Sep 17 00:00:00 2001
From: Sam Wilkins <samwilkins333@gmail.com>
Date: Tue, 9 Jul 2019 13:52:35 -0400
Subject: string list to list of text doc map in scraper and cleaned up kv pane
 templating interaction code

---
 src/client/views/nodes/DocumentContentsView.tsx |  7 +--
 src/client/views/nodes/KeyValueBox.tsx          | 20 ++++++--
 src/scraping/buxton/scraper.py                  | 61 +++++++++++++++++++++++--
 3 files changed, 75 insertions(+), 13 deletions(-)

(limited to 'src/client/views/nodes/KeyValueBox.tsx')

diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx
index b4c0e844f..eb786d537 100644
--- a/src/client/views/nodes/DocumentContentsView.tsx
+++ b/src/client/views/nodes/DocumentContentsView.tsx
@@ -69,7 +69,7 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & {
             // this document as the data document for the layout.
             return this.props.Document;
         }
-        return this.props.DataDoc
+        return this.props.DataDoc;
     }
     get layoutDoc() {
         // if this document's layout field contains a document (ie, a rendering template), then we will use that
@@ -124,13 +124,8 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & {
         if (this.props.renderDepth > 7) return (null);
         if (!this.layout && (this.props.layoutKey !== "overlayLayout" || !this.templates.length)) return (null);
         return <ObserverJsxParser
-<<<<<<< HEAD
             components={{ FormattedTextBox, ImageBox, IconBox, DirectoryImportBox, FieldView, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, CollectionPDFView, CollectionVideoView, WebBox, KeyValueBox, PDFBox, VideoBox, AudioBox, HistogramBox }}
-            bindings={this.CreateBindings(this.props.Document.layout instanceof Doc ? this.props.Document.layout : this.props.Document)}
-=======
-            components={{ FormattedTextBox, ImageBox, IconBox, FieldView, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, CollectionPDFView, CollectionVideoView, WebBox, KeyValueBox, PDFBox, VideoBox, AudioBox, HistogramBox }}
             bindings={this.CreateBindings()}
->>>>>>> b49fdb1c42b9758e006521e0f404634ba396a2ac
             jsx={this.finalLayout}
             showWarnings={true}
             onError={(test: any) => { console.log(test); }}
diff --git a/src/client/views/nodes/KeyValueBox.tsx b/src/client/views/nodes/KeyValueBox.tsx
index e27ab1589..e8619584e 100644
--- a/src/client/views/nodes/KeyValueBox.tsx
+++ b/src/client/views/nodes/KeyValueBox.tsx
@@ -161,7 +161,7 @@ export class KeyValueBox extends React.Component<FieldViewProps> {
     getTemplate = async () => {
         let parent = Docs.StackingDocument([], { width: 800, height: 800, title: "Template" });
         parent.singleColumn = false;
-        parent.columnWidth = 50;
+        parent.columnWidth = 100;
         for (let row of this.rows.filter(row => row.isChecked)) {
             await this.createTemplateField(parent, row);
             row.uncheck();
@@ -175,7 +175,7 @@ export class KeyValueBox extends React.Component<FieldViewProps> {
         if (!sourceDoc) {
             return;
         }
-        let fieldTemplate = this.inferType(sourceDoc[metaKey], metaKey);
+        let fieldTemplate = await this.inferType(sourceDoc[metaKey], metaKey);
 
         // move data doc fields to layout doc as needed (nativeWidth/nativeHeight, data, ??)
         let backgroundLayout = StrCast(fieldTemplate.backgroundLayout);
@@ -200,12 +200,24 @@ export class KeyValueBox extends React.Component<FieldViewProps> {
         Cast(parentStackingDoc.data, listSpec(Doc))!.push(fieldTemplate);
     }
 
-    inferType = (data: FieldResult, metaKey: string) => {
+    inferType = async (data: FieldResult, metaKey: string) => {
         let options = { width: 300, height: 300, title: metaKey };
         if (data instanceof RichTextField || typeof data === "string" || typeof data === "number") {
             return Docs.TextDocument(options);
         } else if (data instanceof List) {
-            return Docs.StackingDocument([], options);
+            if (data.length === 0) {
+                return Docs.StackingDocument([], options);
+            }
+            let first = await Cast(data[0], Doc);
+            if (!first) {
+                return Docs.StackingDocument([], options);
+            }
+            switch (first.type) {
+                case "image":
+                    return Docs.StackingDocument([], options);
+                case "text":
+                    return Docs.TreeDocument([], options);
+            }
         } else if (data instanceof ImageField) {
             return Docs.ImageDocument("https://www.freepik.com/free-icon/picture-frame-with-mountain-image_748687.htm", options);
         }
diff --git a/src/scraping/buxton/scraper.py b/src/scraping/buxton/scraper.py
index 02c6d8b74..700269727 100644
--- a/src/scraping/buxton/scraper.py
+++ b/src/scraping/buxton/scraper.py
@@ -26,7 +26,7 @@ def extract_links(fileName):
         item = rels[rel]
         if item.reltype == RT.HYPERLINK and ".aspx" not in item._target:
             links.append(item._target)
-    return listify(links)
+    return text_doc_map(links)
 
 
 def extract_value(kv_string):
@@ -60,6 +60,12 @@ def protofy(fieldId):
     }
 
 
+def text_doc_map(string_list):
+    def guid_map(caption):
+        return write_text_doc(caption)
+    return listify(proxify_guids(list(map(guid_map, string_list))))
+
+
 def write_schema(parse_results, display_fields, storage_key):
     view_guids = parse_results["child_guids"]
 
@@ -110,6 +116,54 @@ def write_schema(parse_results, display_fields, storage_key):
     return view_doc_guid
 
 
+def write_text_doc(content):
+    data_doc_guid = guid()
+    view_doc_guid = guid()
+
+    view_doc = {
+        "_id": view_doc_guid,
+        "fields": {
+            "proto": protofy(data_doc_guid),
+            "x": 10,
+            "y": 10,
+            "width": 400,
+            "zIndex": 2,
+            "libraryBrush": False
+        },
+        "__type": "Doc"
+    }
+
+    data_doc = {
+        "_id": data_doc_guid,
+        "fields": {
+            "proto": protofy("textProto"),
+            "data": {
+                "Data": '{"doc":{"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","text":"' + content + '"}]}]},"selection":{"type":"text","anchor":1,"head":1}' + '}',
+                "__type": "RichTextField"
+            },
+            "title": content,
+            "nativeWidth": 200,
+            "author": "Bill Buxton",
+            "creationDate": {
+                "date": datetime.datetime.utcnow().microsecond,
+                "__type": "date"
+            },
+            "isPrototype": True,
+            "autoHeight": True,
+            "page": -1,
+            "nativeHeight": 200,
+            "height": 200,
+            "data_text": content
+        },
+        "__type": "Doc"
+    }
+
+    db.newDocuments.insert_one(view_doc)
+    db.newDocuments.insert_one(data_doc)
+
+    return view_doc_guid
+
+
 def write_image(folder, name):
     path = f"http://localhost:1050/files/{folder}/{name}"
 
@@ -253,7 +307,7 @@ def parse_document(file_name: str):
     while lines[cur] != "Image":
         link_descriptions.append(lines[cur].strip())
         cur += 1
-    result["link_descriptions"] = listify(link_descriptions)
+    result["link_descriptions"] = text_doc_map(link_descriptions)
 
     result["hyperlinks"] = extract_links(source + "/" + file_name)
 
@@ -265,7 +319,8 @@ def parse_document(file_name: str):
         captions.append(lines[cur + 1])
         cur += 2
     result["images"] = listify(images)
-    result["captions"] = listify(captions)
+
+    result["captions"] = text_doc_map(captions)
 
     notes = []
     if (cur < len(lines) and lines[cur] == "NOTES:"):
-- 
cgit v1.2.3-70-g09d2


From 9d9c16939bb296a9deb38bc34cbea9870aee4f76 Mon Sep 17 00:00:00 2001
From: Sam Wilkins <samwilkins333@gmail.com>
Date: Tue, 9 Jul 2019 14:33:56 -0400
Subject: collection view fix

---
 src/client/views/nodes/KeyValueBox.tsx | 5 +++++
 1 file changed, 5 insertions(+)

(limited to 'src/client/views/nodes/KeyValueBox.tsx')

diff --git a/src/client/views/nodes/KeyValueBox.tsx b/src/client/views/nodes/KeyValueBox.tsx
index e8619584e..b5c47f138 100644
--- a/src/client/views/nodes/KeyValueBox.tsx
+++ b/src/client/views/nodes/KeyValueBox.tsx
@@ -175,7 +175,9 @@ export class KeyValueBox extends React.Component<FieldViewProps> {
         if (!sourceDoc) {
             return;
         }
+
         let fieldTemplate = await this.inferType(sourceDoc[metaKey], metaKey);
+        let previousViewType = fieldTemplate.viewType;
 
         // move data doc fields to layout doc as needed (nativeWidth/nativeHeight, data, ??)
         let backgroundLayout = StrCast(fieldTemplate.backgroundLayout);
@@ -196,6 +198,7 @@ export class KeyValueBox extends React.Component<FieldViewProps> {
         fieldTemplate.isTemplate = true;
         fieldTemplate.templates = new List<string>([Templates.TitleBar(metaKey)]);
         fieldTemplate.proto = Doc.GetProto(parentStackingDoc);
+        previousViewType && (fieldTemplate.viewType = previousViewType);
 
         Cast(parentStackingDoc.data, listSpec(Doc))!.push(fieldTemplate);
     }
@@ -214,8 +217,10 @@ export class KeyValueBox extends React.Component<FieldViewProps> {
             }
             switch (first.type) {
                 case "image":
+                    console.log("STACKING VIEW CREATED for ", data);
                     return Docs.StackingDocument([], options);
                 case "text":
+                    console.log("TREE VIEW CREATED for ", data);
                     return Docs.TreeDocument([], options);
             }
         } else if (data instanceof ImageField) {
-- 
cgit v1.2.3-70-g09d2


From 7a33f0b56f1182c19cc7ea1b276da3a674438957 Mon Sep 17 00:00:00 2001
From: Sam Wilkins <samwilkins333@gmail.com>
Date: Tue, 9 Jul 2019 14:39:23 -0400
Subject: cleanup

---
 src/client/views/nodes/KeyValueBox.tsx | 4 +---
 1 file changed, 1 insertion(+), 3 deletions(-)

(limited to 'src/client/views/nodes/KeyValueBox.tsx')

diff --git a/src/client/views/nodes/KeyValueBox.tsx b/src/client/views/nodes/KeyValueBox.tsx
index b5c47f138..2f5a0f963 100644
--- a/src/client/views/nodes/KeyValueBox.tsx
+++ b/src/client/views/nodes/KeyValueBox.tsx
@@ -217,14 +217,12 @@ export class KeyValueBox extends React.Component<FieldViewProps> {
             }
             switch (first.type) {
                 case "image":
-                    console.log("STACKING VIEW CREATED for ", data);
                     return Docs.StackingDocument([], options);
                 case "text":
-                    console.log("TREE VIEW CREATED for ", data);
                     return Docs.TreeDocument([], options);
             }
         } else if (data instanceof ImageField) {
-            return Docs.ImageDocument("https://www.freepik.com/free-icon/picture-frame-with-mountain-image_748687.htm", options);
+            return Docs.ImageDocument("https://image.flaticon.com/icons/png/512/23/23765.png", options);
         }
         return new Doc;
     }
-- 
cgit v1.2.3-70-g09d2


From b1b69f8a4f9e34f0c8e667ec95f9fe16ebc8b2e4 Mon Sep 17 00:00:00 2001
From: Bob Zeleznik <zzzman@gmail.com>
Date: Wed, 10 Jul 2019 21:32:31 -0400
Subject: factored template creation code into Doc.ts

---
 src/client/views/DocumentDecorations.tsx           | 28 +-------------------
 .../views/collections/CollectionTreeView.tsx       |  1 -
 src/client/views/nodes/KeyValueBox.tsx             | 21 +--------------
 src/new_fields/Doc.ts                              | 30 +++++++++++++++++++++-
 4 files changed, 31 insertions(+), 49 deletions(-)

(limited to 'src/client/views/nodes/KeyValueBox.tsx')

diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 07ecc6b0f..95335020f 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -84,33 +84,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
                 let fieldTemplate = fieldTemplateView.props.Document;
                 let docTemplate = fieldTemplateView.props.ContainingCollectionView!.props.Document;
                 let metaKey = text.slice(1, text.length);
-
-                // move data doc fields to layout doc as needed (nativeWidth/nativeHeight, data, ??)
-                let backgroundLayout = StrCast(fieldTemplate.backgroundLayout);
-                let fieldLayoutDoc = fieldTemplate;
-                if (fieldTemplate.layout instanceof Doc) {
-                    fieldLayoutDoc = Doc.MakeDelegate(fieldTemplate.layout);
-                }
-                let layout = StrCast(fieldLayoutDoc.layout).replace(/fieldKey={"[^"]*"}/, `fieldKey={"${metaKey}"}`);
-                if (backgroundLayout) {
-                    layout = StrCast(fieldLayoutDoc.layout).replace(/fieldKey={"annotations"}/, `fieldKey={"${metaKey}"} fieldExt={"annotations"}`);
-                    backgroundLayout = backgroundLayout.replace(/fieldKey={"[^"]*"}/, `fieldKey={"${metaKey}"}`);
-                }
-                let nw = Cast(fieldTemplate.nativeWidth, "number");
-                let nh = Cast(fieldTemplate.nativeHeight, "number");
-
-                let layoutDelegate = fieldTemplate.layout instanceof Doc ? fieldLayoutDoc : fieldTemplate;
-                layoutDelegate.layout = layout;
-
-                fieldTemplate.title = metaKey;
-                fieldTemplate.layout = layoutDelegate !== fieldTemplate ? layoutDelegate : layout;
-                fieldTemplate.backgroundLayout = backgroundLayout;
-                fieldTemplate.nativeWidth = nw;
-                fieldTemplate.nativeHeight = nh;
-                fieldTemplate.embed = true;
-                fieldTemplate.isTemplate = true;
-                fieldTemplate.showTitle = "title";
-                fieldTemplate.proto = Doc.GetProto(docTemplate);
+                Doc.MakeTemplate(fieldTemplate, metaKey, Doc.GetProto(docTemplate));
             }
             else {
                 if (SelectionManager.SelectedDocuments().length > 0) {
diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx
index d90c78f85..3674a743a 100644
--- a/src/client/views/collections/CollectionTreeView.tsx
+++ b/src/client/views/collections/CollectionTreeView.tsx
@@ -73,7 +73,6 @@ class TreeView extends React.Component<TreeViewProps> {
     @observable _collapsed: boolean = true;
 
     @computed get fieldKey() {
-        trace();
         let keys = Array.from(Object.keys(this.resolvedDataDoc));  // bcz: Argh -- make untracked to avoid this rerunning whenever 'libraryBrush' is set
         if (this.resolvedDataDoc.proto instanceof Doc) {
             let arr = Array.from(Object.keys(this.resolvedDataDoc.proto!));// bcz: Argh -- make untracked to avoid this rerunning whenever 'libraryBrush' is set
diff --git a/src/client/views/nodes/KeyValueBox.tsx b/src/client/views/nodes/KeyValueBox.tsx
index 2f5a0f963..467cb2b6b 100644
--- a/src/client/views/nodes/KeyValueBox.tsx
+++ b/src/client/views/nodes/KeyValueBox.tsx
@@ -178,26 +178,7 @@ export class KeyValueBox extends React.Component<FieldViewProps> {
 
         let fieldTemplate = await this.inferType(sourceDoc[metaKey], metaKey);
         let previousViewType = fieldTemplate.viewType;
-
-        // move data doc fields to layout doc as needed (nativeWidth/nativeHeight, data, ??)
-        let backgroundLayout = StrCast(fieldTemplate.backgroundLayout);
-        let layout = StrCast(fieldTemplate.layout).replace(/fieldKey={"[^"]*"}/, `fieldKey={"${metaKey}"}`);
-        if (backgroundLayout) {
-            layout = StrCast(fieldTemplate.layout).replace(/fieldKey={"annotations"}/, `fieldKey={"${metaKey}"} fieldExt={"annotations"}`);
-            backgroundLayout = backgroundLayout.replace(/fieldKey={"[^"]*"}/, `fieldKey={"${metaKey}"}`);
-        }
-        let nw = NumCast(fieldTemplate.nativeWidth);
-        let nh = NumCast(fieldTemplate.nativeHeight);
-
-        fieldTemplate.title = metaKey;
-        fieldTemplate.layout = layout;
-        fieldTemplate.backgroundLayout = backgroundLayout;
-        fieldTemplate.nativeWidth = nw;
-        fieldTemplate.nativeHeight = nh;
-        fieldTemplate.embed = true;
-        fieldTemplate.isTemplate = true;
-        fieldTemplate.templates = new List<string>([Templates.TitleBar(metaKey)]);
-        fieldTemplate.proto = Doc.GetProto(parentStackingDoc);
+        Doc.MakeTemplate(fieldTemplate, metaKey, Doc.GetProto(parentStackingDoc));
         previousViewType && (fieldTemplate.viewType = previousViewType);
 
         Cast(parentStackingDoc.data, listSpec(Doc))!.push(fieldTemplate);
diff --git a/src/new_fields/Doc.ts b/src/new_fields/Doc.ts
index c63c9ef93..3d46243e0 100644
--- a/src/new_fields/Doc.ts
+++ b/src/new_fields/Doc.ts
@@ -3,7 +3,7 @@ import { serializable, primitive, map, alias, list } from "serializr";
 import { autoObject, SerializationHelper, Deserializable } from "../client/util/SerializationHelper";
 import { DocServer } from "../client/DocServer";
 import { setter, getter, getField, updateFunction, deleteProperty, makeEditable, makeReadOnly } from "./util";
-import { Cast, ToConstructor, PromiseValue, FieldValue, NumCast, BoolCast } from "./Types";
+import { Cast, ToConstructor, PromiseValue, FieldValue, NumCast, BoolCast, StrCast } from "./Types";
 import { listSpec } from "./Schema";
 import { ObjectField } from "./ObjectField";
 import { RefField, FieldId } from "./RefField";
@@ -362,4 +362,32 @@ export namespace Doc {
         delegate.proto = doc;
         return delegate;
     }
+
+    export function MakeTemplate(fieldTemplate: Doc, metaKey: string, proto: Doc) {
+        // move data doc fields to layout doc as needed (nativeWidth/nativeHeight, data, ??)
+        let backgroundLayout = StrCast(fieldTemplate.backgroundLayout);
+        let fieldLayoutDoc = fieldTemplate;
+        if (fieldTemplate.layout instanceof Doc) {
+            fieldLayoutDoc = Doc.MakeDelegate(fieldTemplate.layout);
+        }
+        let layout = StrCast(fieldLayoutDoc.layout).replace(/fieldKey={"[^"]*"}/, `fieldKey={"${metaKey}"}`);
+        if (backgroundLayout) {
+            layout = StrCast(fieldLayoutDoc.layout).replace(/fieldKey={"annotations"}/, `fieldKey={"${metaKey}"} fieldExt={"annotations"}`);
+            backgroundLayout = backgroundLayout.replace(/fieldKey={"[^"]*"}/, `fieldKey={"${metaKey}"}`);
+        }
+        let nw = Cast(fieldTemplate.nativeWidth, "number");
+        let nh = Cast(fieldTemplate.nativeHeight, "number");
+
+        let layoutDelegate = fieldTemplate.layout instanceof Doc ? fieldLayoutDoc : fieldTemplate;
+        layoutDelegate.layout = layout;
+
+        fieldTemplate.title = metaKey;
+        fieldTemplate.layout = layoutDelegate !== fieldTemplate ? layoutDelegate : layout;
+        fieldTemplate.backgroundLayout = backgroundLayout;
+        fieldTemplate.nativeWidth = nw;
+        fieldTemplate.nativeHeight = nh;
+        fieldTemplate.isTemplate = true;
+        fieldTemplate.showTitle = "title";
+        fieldTemplate.proto = proto;
+    }
 }
\ No newline at end of file
-- 
cgit v1.2.3-70-g09d2


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/util/Scripting.ts              |  3 +-
 src/client/views/DocumentDecorations.scss | 44 ++++++++++--------
 src/client/views/DocumentDecorations.tsx  | 31 +++++++++----
 src/client/views/MetadataEntryMenu.scss   | 10 +++++
 src/client/views/MetadataEntryMenu.tsx    | 74 +++++++++++++++++++++++++++++++
 src/client/views/nodes/KeyValueBox.tsx    | 31 ++++++++++---
 src/client/views/nodes/KeyValuePair.tsx   |  8 +---
 src/new_fields/Doc.ts                     |  9 ++++
 8 files changed, 169 insertions(+), 41 deletions(-)
 create mode 100644 src/client/views/MetadataEntryMenu.scss
 create mode 100644 src/client/views/MetadataEntryMenu.tsx

(limited to 'src/client/views/nodes/KeyValueBox.tsx')

diff --git a/src/client/util/Scripting.ts b/src/client/util/Scripting.ts
index 3156c4f43..62c2cfe85 100644
--- a/src/client/util/Scripting.ts
+++ b/src/client/util/Scripting.ts
@@ -33,6 +33,8 @@ export interface CompileError {
     errors: any[];
 }
 
+export type CompileResult = CompiledScript | CompileError;
+
 export namespace Scripting {
     export function addGlobal(global: { name: string }): void;
     export function addGlobal(name: string, global: any): void;
@@ -61,7 +63,6 @@ export function scriptingGlobal(constructor: { new(...args: any[]): any }) {
 
 const scriptingGlobals: { [name: string]: any } = {};
 
-export type CompileResult = CompiledScript | CompileError;
 function Run(script: string | undefined, customParams: string[], diagnostics: any[], originalScript: string, options: ScriptOptions): CompileResult {
     const errors = diagnostics.some(diag => diag.category === ts.DiagnosticCategory.Error);
     if ((options.typecheck !== false && errors) || !script) {
diff --git a/src/client/views/DocumentDecorations.scss b/src/client/views/DocumentDecorations.scss
index 2d430512b..1afc5c147 100644
--- a/src/client/views/DocumentDecorations.scss
+++ b/src/client/views/DocumentDecorations.scss
@@ -1,6 +1,7 @@
 @import "globalCssVariables";
 
 $linkGap : 3px;
+
 .documentDecorations {
     position: absolute;
 }
@@ -52,15 +53,16 @@ $linkGap : 3px;
         grid-column-start: 5;
         grid-column-end: 7;
     }
-    
-    #documentDecorations-borderRadius{
+
+    #documentDecorations-borderRadius {
         grid-column-start: 5;
         grid-column-end: 7;
         border-radius: 100%;
-        .borderRadiusTooltip{
-            width:10px;
-            height:10px;
-            position:absolute;
+
+        .borderRadiusTooltip {
+            width: 10px;
+            height: 10px;
+            position: absolute;
         }
     }
 
@@ -68,8 +70,9 @@ $linkGap : 3px;
     #documentDecorations-bottomRightResizer {
         cursor: nwse-resize;
     }
+
     #documentDecorations-bottomRightResizer {
-        grid-row:4;
+        grid-row: 4;
     }
 
     #documentDecorations-topRightResizer,
@@ -86,7 +89,8 @@ $linkGap : 3px;
     #documentDecorations-rightResizer {
         cursor: ew-resize;
     }
-    .title{
+
+    .title {
         background: $alt-accent;
         grid-column-start: 3;
         grid-column-end: 4;
@@ -129,7 +133,6 @@ $linkGap : 3px;
 
 .linkFlyout {
     grid-column: 2/4;
-    margin-top: $linkGap;
 }
 
 .linkButton-empty:hover {
@@ -145,6 +148,7 @@ $linkGap : 3px;
 }
 
 .link-button-container {
+    margin-top: $linkGap;
     grid-column: 1/4;
     width: auto;
     height: auto;
@@ -152,9 +156,12 @@ $linkGap : 3px;
     flex-direction: row;
 }
 
+.linkButtonWrapper {
+    pointer-events: auto;
+    padding-right: 5px;
+}
+
 .linkButton-linker {
-    margin-left: 5px;
-    margin-top: $linkGap;
     height: 20px;
     width: 20px;
     text-align: center;
@@ -169,7 +176,8 @@ $linkGap : 3px;
     transform: scale(1.05);
 }
 
-.linkButton-empty, .linkButton-nonempty {
+.linkButton-empty,
+.linkButton-nonempty {
     height: 20px;
     width: 20px;
     border-radius: 50%;
@@ -194,9 +202,6 @@ $linkGap : 3px;
 }
 
 .templating-menu {
-    position: absolute;
-    bottom: 0;
-    left: 50px;
     pointer-events: auto;
     text-transform: uppercase;
     letter-spacing: 2px;
@@ -208,15 +213,17 @@ $linkGap : 3px;
     align-items: center;
 }
 
-.fa-icon-link {
+.documentdecorations-icon {
     margin-top: 3px;
 }
-.templating-button {
+
+.templating-button,
+.docDecs-tagButton {
     width: 20px;
     height: 20px;
     border-radius: 50%;
     opacity: 0.9;
-    font-size:14;
+    font-size: 14;
     background-color: $dark-color;
     color: $light-color;
     text-align: center;
@@ -238,6 +245,7 @@ $linkGap : 3px;
     background-color: $light-color-secondary;
     padding: 2px 12px;
     list-style: none;
+
     .templateToggle {
         text-align: left;
     }
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index c24d66ee4..56fbd75a0 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -1,5 +1,5 @@
 import { library } from '@fortawesome/fontawesome-svg-core';
-import { faLink } from '@fortawesome/free-solid-svg-icons';
+import { faLink, faTag } from '@fortawesome/free-solid-svg-icons';
 import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
 import { action, computed, observable, reaction, runInAction } from "mobx";
 import { observer } from "mobx-react";
@@ -27,11 +27,13 @@ import React = require("react");
 import { RichTextField } from '../../new_fields/RichTextField';
 import { LinkManager } from '../util/LinkManager';
 import { ObjectField } from '../../new_fields/ObjectField';
+import { MetadataEntryMenu } from './MetadataEntryMenu';
 const higflyout = require("@hig/flyout");
 export const { anchorPoints } = higflyout;
 export const Flyout = higflyout.default;
 
 library.add(faLink);
+library.add(faTag);
 
 @observer
 export class DocumentDecorations extends React.Component<{}, { value: string }> {
@@ -596,8 +598,8 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
         if (!canEmbed) return (null);
         return (
             <div className="linkButtonWrapper">
-                <div style={{ paddingTop: 3, marginLeft: 30 }} title="Drag Embed" className="linkButton-linker" ref={this._embedButton} onPointerDown={this.onEmbedButtonDown}>
-                    <FontAwesomeIcon className="fa-image" icon="image" size="sm" />
+                <div title="Drag Embed" className="linkButton-linker" ref={this._embedButton} onPointerDown={this.onEmbedButtonDown}>
+                    <FontAwesomeIcon className="documentdecorations-icon" icon="image" size="sm" />
                 </div>
             </div>
         );
@@ -610,10 +612,9 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
         this._textDoc = thisDoc;
         return (
             <div className="tooltipwrapper">
-                <div style={{ paddingTop: 3, marginLeft: 30 }} title="Hide Tooltip" className="linkButton-linker" ref={this._tooltipoff} onPointerDown={this.onTooltipOff}>
+                <div title="Hide Tooltip" className="linkButton-linker" ref={this._tooltipoff} onPointerDown={this.onTooltipOff}>
                     {/* <FontAwesomeIcon className="fa-image" icon="image" size="sm" /> */}
-                    T
-                    </div>
+                </div>
             </div>
 
         );
@@ -634,6 +635,17 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
         }
     }
 
+    get metadataMenu() {
+        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? */}
+                    <div className="docDecs-tagButton" title="Add fields"><FontAwesomeIcon className="documentdecorations-icon" icon="tag" size="sm" /></div>
+                </Flyout>
+            </div>
+        );
+    }
+
     render() {
         var bounds = this.Bounds;
         let seldoc = SelectionManager.SelectedDocuments().length ? SelectionManager.SelectedDocuments()[0] : undefined;
@@ -723,10 +735,13 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
                     </div>
                     <div className="linkButtonWrapper">
                         <div title="Drag Link" className="linkButton-linker" ref={this._linkerButton} onPointerDown={this.onLinkerButtonDown}>
-                            <FontAwesomeIcon className="fa-icon-link" icon="link" size="sm" />
+                            <FontAwesomeIcon className="documentdecorations-icon" icon="link" size="sm" />
                         </div>
                     </div>
-                    <TemplateMenu docs={SelectionManager.ViewsSortedVertically()} templates={templates} />
+                    <div className="linkButtonWrapper">
+                        <TemplateMenu docs={SelectionManager.ViewsSortedVertically()} templates={templates} />
+                    </div>
+                    {this.metadataMenu}
                     {this.considerEmbed()}
                     {/* {this.considerTooltip()} */}
                 </div>
diff --git a/src/client/views/MetadataEntryMenu.scss b/src/client/views/MetadataEntryMenu.scss
new file mode 100644
index 000000000..73e5b6a73
--- /dev/null
+++ b/src/client/views/MetadataEntryMenu.scss
@@ -0,0 +1,10 @@
+.metadataEntry-input {
+    width: 40%;
+    margin-left: 5px;
+    margin-right: 5px;
+}
+
+.metadataEntry-outerDiv {
+    display: flex;
+    width: 300px;
+}
\ No newline at end of file
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
diff --git a/src/client/views/nodes/KeyValueBox.tsx b/src/client/views/nodes/KeyValueBox.tsx
index eb8abf2c7..c9dd9a64e 100644
--- a/src/client/views/nodes/KeyValueBox.tsx
+++ b/src/client/views/nodes/KeyValueBox.tsx
@@ -2,7 +2,7 @@
 import { action, computed, observable } from "mobx";
 import { observer } from "mobx-react";
 import 'react-image-lightbox/style.css'; // This only needs to be imported once in your app
-import { CompileScript, ScriptOptions } from "../../util/Scripting";
+import { CompileScript, ScriptOptions, CompiledScript } from "../../util/Scripting";
 import { FieldView, FieldViewProps } from './FieldView';
 import "./KeyValueBox.scss";
 import { KeyValuePair } from "./KeyValuePair";
@@ -21,6 +21,12 @@ import { ImageField } from "../../../new_fields/URLField";
 import { SelectionManager } from "../../util/SelectionManager";
 import { listSpec } from "../../../new_fields/Schema";
 
+export type KVPScript = {
+    script: CompiledScript;
+    type: "computed" | "script" | false;
+    onDelegate: boolean;
+};
+
 @observer
 export class KeyValueBox extends React.Component<FieldViewProps> {
     private _mainCont = React.createRef<HTMLDivElement>();
@@ -48,22 +54,27 @@ export class KeyValueBox extends React.Component<FieldViewProps> {
             }
         }
     }
-    public static SetField(doc: Doc, key: string, value: string) {
+    public static CompileKVPScript(value: string): KVPScript | undefined {
         let eq = value.startsWith("=");
-        let target = eq ? doc : Doc.GetProto(doc);
         value = eq ? value.substr(1) : value;
-        let dubEq = value.startsWith(":=") ? 1 : value.startsWith(";=") ? 2 : 0;
+        const dubEq = value.startsWith(":=") ? "computed" : value.startsWith(";=") ? "script" : false;
         value = dubEq ? value.substr(2) : value;
         let options: ScriptOptions = { addReturn: true, params: { this: "Doc" } };
         if (dubEq) options.typecheck = false;
         let script = CompileScript(value, options);
         if (!script.compiled) {
-            return false;
+            return undefined;
         }
+        return { script, type: dubEq, onDelegate: eq };
+    }
+
+    public static ApplyKVPScript(doc: Doc, key: string, kvpScript: KVPScript): boolean {
+        const { script, type, onDelegate } = kvpScript;
+        const target = onDelegate ? doc : Doc.GetProto(doc);
         let field: Field;
-        if (dubEq === 1) {
+        if (type === "computed") {
             field = new ComputedField(script);
-        } else if (dubEq === 2) {
+        } else if (type === "script") {
             field = new ScriptField(script);
         } else {
             let res = script.run({ this: target });
@@ -77,6 +88,12 @@ export class KeyValueBox extends React.Component<FieldViewProps> {
         return false;
     }
 
+    public static SetField(doc: Doc, key: string, value: string) {
+        const script = this.CompileKVPScript(value);
+        if (!script) return false;
+        return this.ApplyKVPScript(doc, key, script);
+    }
+
     onPointerDown = (e: React.PointerEvent): void => {
         if (e.buttons === 1 && this.props.isSelected()) {
             e.stopPropagation();
diff --git a/src/client/views/nodes/KeyValuePair.tsx b/src/client/views/nodes/KeyValuePair.tsx
index b5db52ac7..209782242 100644
--- a/src/client/views/nodes/KeyValuePair.tsx
+++ b/src/client/views/nodes/KeyValuePair.tsx
@@ -92,13 +92,7 @@ export class KeyValuePair extends React.Component<KeyValuePairProps> {
                             contents={contents}
                             height={36}
                             GetValue={() => {
-                                const onDelegate = Object.keys(props.Document).includes(props.fieldKey);
-
-                                let field = FieldValue(props.Document[props.fieldKey]);
-                                if (Field.IsField(field)) {
-                                    return (onDelegate ? "=" : "") + Field.toScriptString(field);
-                                }
-                                return "";
+                                return Field.toKeyValueString(props.Document, props.fieldKey);
                             }}
                             SetValue={(value: string) =>
                                 KeyValueBox.SetField(props.Document, props.fieldKey, value)}>
diff --git a/src/new_fields/Doc.ts b/src/new_fields/Doc.ts
index 0e5f8b4d9..c5f9e7adf 100644
--- a/src/new_fields/Doc.ts
+++ b/src/new_fields/Doc.ts
@@ -12,6 +12,15 @@ import { scriptingGlobal } from "../client/util/Scripting";
 import { List } from "./List";
 
 export namespace Field {
+    export function toKeyValueString(doc: Doc, key: string): string {
+        const onDelegate = Object.keys(doc).includes(key);
+
+        let field = FieldValue(doc[key]);
+        if (Field.IsField(field)) {
+            return (onDelegate ? "=" : "") + Field.toScriptString(field);
+        }
+        return "";
+    }
     export function toScriptString(field: Field): string {
         if (typeof field === "string") {
             return `"${field}"`;
-- 
cgit v1.2.3-70-g09d2

