From 70996f3f19d408e819e081ed03bd7ccf0de44503 Mon Sep 17 00:00:00 2001
From: Vellichora <fangrui_tong@brown.edu>
Date: Fri, 7 Jun 2019 17:52:31 -0400
Subject: links can be categorized under a group

---
 src/client/views/nodes/LinkEditor.tsx | 103 +++++++++++++++++++++++++++-------
 1 file changed, 83 insertions(+), 20 deletions(-)

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

diff --git a/src/client/views/nodes/LinkEditor.tsx b/src/client/views/nodes/LinkEditor.tsx
index 71a423338..2ab8c3460 100644
--- a/src/client/views/nodes/LinkEditor.tsx
+++ b/src/client/views/nodes/LinkEditor.tsx
@@ -6,52 +6,115 @@ import './LinkEditor.scss';
 import { props } from "bluebird";
 import { DocumentView } from "./DocumentView";
 import { link } from "fs";
-import { StrCast } from "../../../new_fields/Types";
+import { StrCast, Cast } from "../../../new_fields/Types";
 import { Doc } from "../../../new_fields/Doc";
+import { List } from "../../../new_fields/List";
+import { listSpec } from "../../../new_fields/Schema";
+import { LinkManager } from "../../util/LinkManager";
 
 interface Props {
+    sourceDoc: Doc;
     linkDoc: Doc;
+    groups: Map<number, Doc>;
     showLinks: () => void;
 }
 
 @observer
 export class LinkEditor extends React.Component<Props> {
 
-    @observable private _nameInput: string = StrCast(this.props.linkDoc.title);
-    @observable private _descriptionInput: string = StrCast(this.props.linkDoc.linkDescription);
+    // @observable private _title: string = StrCast(this.props.linkDoc.title);
+    // @observable private _description: string = StrCast(this.props.linkDoc.linkDescription);
+    // @observable private _tags: Array<string> = Cast(this.props.linkDoc.linkTags, List);
 
+    // @action
+    // onTitleChanged = (e: React.ChangeEvent<HTMLInputElement>) => {
+    //     this._title = e.target.value;
+    // }
 
-    onSaveButtonPressed = (e: React.PointerEvent): void => {
-        e.stopPropagation();
+    // @action
+    // onDescriptionChanged = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
+    //     this._description = e.target.value;
+    // }
+
+    // renderTags() {
+    //     return this._tags.map(tag => {
+    //         if (tag === "") {
+    //             return <input type="text" placeholder="Tag"></input>;
+    //         } else {
+    //             return <input type="text" value={tag}></input>;
+    //         }
+    //     })
+    // }
+
+    // addTag = (): void => {
+    //     this._tags.push("");
+    // }
 
+    @action
+    editGroup(groupId: number, value: string) {
         let linkDoc = this.props.linkDoc.proto ? this.props.linkDoc.proto : this.props.linkDoc;
-        linkDoc.title = this._nameInput;
-        linkDoc.linkDescription = this._descriptionInput;
+        let groupDoc = this.props.groups.get(groupId);
+        if (groupDoc) {
+            groupDoc.proto!.type = value;
+            if (Doc.AreProtosEqual(this.props.sourceDoc, Cast(linkDoc.anchor1, Doc, new Doc))) {
+                // let groups = Cast(linkDoc.anchor1Groups, listSpec(Doc), []);
+                // groups.push(groupDoc);
+                linkDoc.anchor1Groups = new List<Doc>([groupDoc]);
+
+            } else {
+                linkDoc.anchor2Groups = new List<Doc>([groupDoc]);
+            }
+        }
 
-        this.props.showLinks();
     }
 
+    renderGroup(groupId: number, groupDoc: Doc) {
+        return (
+            <div>
+                <p>type:</p>
+                <input type="text" value={StrCast(groupDoc.proto!.type)} onChange={e => this.editGroup(groupId, e.target.value)}></input>
+            </div>
+        )
+    }
 
+    renderGroups() {
+        let groups: Array<JSX.Element> = [];
+        this.props.groups.forEach((groupDoc, groupId) => {
+            groups.push(
+                <div>
+                    {this.renderGroup(groupId, groupDoc)}
+                </div>
+            )
+        });
+        return groups;
+    }
+
+    onSaveButtonPressed = (e: React.PointerEvent): void => {
+        e.stopPropagation();
+
+        // let linkDoc = this.props.linkDoc.proto ? this.props.linkDoc.proto : this.props.linkDoc;
+        // // linkDoc.title = this._title;
+        // // linkDoc.linkDescription = this._description;
+
+        this.props.showLinks();
+    }
 
     render() {
+        let destination = LinkManager.Instance.findOppositeAnchor(this.props.linkDoc, this.props.sourceDoc);
 
         return (
             <div className="edit-container">
-                <input onChange={this.onNameChanged} className="name-input" type="text" value={this._nameInput} placeholder="Name . . ."></input>
-                <textarea onChange={this.onDescriptionChanged} className="description-input" value={this._descriptionInput} placeholder="Description . . ."></textarea>
+                <p>linked to: {destination.proto!.title}</p>
+                <b>Groups:</b>
+                {this.renderGroups()}
+
+                {/* <input onChange={this.onTitleChanged} className="name-input" type="text" value={this._title} placeholder="Name . . ."></input>
+                <textarea onChange={this.onDescriptionChanged} className="description-input" value={this._description} placeholder="Description . . ."></textarea> */}
+                {/* {this.renderTags()}
+                <button onClick={this.addTag}>+</button> */}
                 <div className="save-button" onPointerDown={this.onSaveButtonPressed}>SAVE</div>
             </div>
 
         );
     }
-
-    @action
-    onNameChanged = (e: React.ChangeEvent<HTMLInputElement>) => {
-        this._nameInput = e.target.value;
-    }
-
-    @action
-    onDescriptionChanged = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
-        this._descriptionInput = e.target.value;
-    }
 }
\ No newline at end of file
-- 
cgit v1.2.3-70-g09d2


From d429898b5337331450e46c223380e5d00967b2d6 Mon Sep 17 00:00:00 2001
From: Fawn <fangrui_tong@brown.edu>
Date: Mon, 10 Jun 2019 20:02:32 -0400
Subject: added set up for metadata on links

---
 src/client/documents/Documents.ts      |   1 +
 src/client/util/DocumentManager.ts     |   5 +-
 src/client/util/DragManager.ts         |   4 +-
 src/client/util/LinkManager.ts         |  81 ++++++++----
 src/client/views/nodes/LinkEditor.scss |  42 ++++++
 src/client/views/nodes/LinkEditor.tsx  | 229 +++++++++++++++++++++++++++------
 src/client/views/nodes/LinkMenu.tsx    |  34 +++--
 7 files changed, 313 insertions(+), 83 deletions(-)

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

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 5ec19f987..731490d97 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -91,6 +91,7 @@ export namespace DocUtils {
         UndoManager.RunInBatch(() => {
             let groupDoc = Docs.TextDocument();
             groupDoc.proto!.type = "*";
+            groupDoc.proto!.metadata = new List<Doc>([]);
 
             let linkDoc = Docs.TextDocument({ width: 100, height: 30, borderRounding: -1 });
             //let linkDoc = new Doc;
diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts
index 52f0fe3f6..2acbb3ad4 100644
--- a/src/client/util/DocumentManager.ts
+++ b/src/client/util/DocumentManager.ts
@@ -9,7 +9,7 @@ import { CollectionView } from '../views/collections/CollectionView';
 import { CollectionPDFView } from '../views/collections/CollectionPDFView';
 import { CollectionVideoView } from '../views/collections/CollectionVideoView';
 import { Id } from '../../new_fields/FieldSymbols';
-import { LinkManager } from './LinkManager';
+import { LinkManager, LinkUtils } from './LinkManager';
 
 
 export class DocumentManager {
@@ -92,7 +92,8 @@ export class DocumentManager {
                 pairs.push(...linksList.reduce((pairs, link) => {
                     if (link) {
                         // let destination = (link["linkedTo"] === dv.props.Document) ? link["linkedFrom"] : link["linkedTo"];
-                        let destination = LinkManager.Instance.findOppositeAnchor(link, dv.props.Document);
+
+                        let destination = LinkUtils.findOppositeAnchor(link, dv.props.Document);
                         let linkToDoc = FieldValue(Cast(destination, Doc));
                         // let linkToDoc = FieldValue(Cast(link.linkedTo, Doc));
                         if (linkToDoc) {
diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts
index 809368aff..9ac421fbf 100644
--- a/src/client/util/DragManager.ts
+++ b/src/client/util/DragManager.ts
@@ -4,7 +4,7 @@ import { Cast } from "../../new_fields/Types";
 import { emptyFunction } from "../../Utils";
 import { CollectionDockingView } from "../views/collections/CollectionDockingView";
 import * as globalCssVariables from "../views/globalCssVariables.scss";
-import { LinkManager } from "./LinkManager";
+import { LinkManager, LinkUtils } from "./LinkManager";
 
 export type dropActionType = "alias" | "copy" | undefined;
 export function SetupDrag(_reference: React.RefObject<HTMLElement>, docFunc: () => Doc | Promise<Doc>, moveFunc?: DragManager.MoveFunction, dropAction?: dropActionType) {
@@ -48,7 +48,7 @@ export async function DragLinksAsDocuments(dragEle: HTMLElement, x: number, y: n
         // let linkFromDocs = await DocListCastAsync(srcTarg.linkedFromDocs);
         let linkDocs = LinkManager.Instance.findAllRelatedLinks(srcTarg);
         if (linkDocs) draggedDocs = linkDocs.map(link => {
-            return LinkManager.Instance.findOppositeAnchor(link, sourceDoc);
+            return LinkUtils.findOppositeAnchor(link, sourceDoc);
         });
 
 
diff --git a/src/client/util/LinkManager.ts b/src/client/util/LinkManager.ts
index a6d649395..5e8e0475b 100644
--- a/src/client/util/LinkManager.ts
+++ b/src/client/util/LinkManager.ts
@@ -10,6 +10,43 @@ import { Doc } from "../../new_fields/Doc";
 import { listSpec } from "../../new_fields/Schema";
 import { List } from "../../new_fields/List";
 
+export namespace LinkUtils {
+    export function findOppositeAnchor(link: Doc, anchor: Doc): Doc {
+        if (Doc.AreProtosEqual(anchor, Cast(link.anchor1, Doc, new Doc))) {
+            return Cast(link.anchor2, Doc, new Doc);
+        } else {
+            return Cast(link.anchor1, Doc, new Doc);
+        }
+    }
+
+    // export function getAnchorGroups(link: Doc, anchor: Doc): Doc[] {
+    //     let groups;
+    //     if (Doc.AreProtosEqual(anchor, Cast(link.anchor1, Doc, new Doc))) {
+    //         groups = Cast(link.anchor1Groups, listSpec(Doc), []);
+    //     } else {
+    //         groups = Cast(link.anchor2Groups, listSpec(Doc), []);
+    //     }
+
+    //     if (groups instanceof Doc[]) {
+    //         return groups;
+    //     } else {
+    //         return [];
+    //     }
+    //     // if (Doc.AreProtosEqual(anchor, Cast(link.anchor1, Doc, new Doc))) {
+    //     //     returnCast(link.anchor1Groups, listSpec(Doc), []);
+    //     // } else {
+    //     //     return Cast(link.anchor2Groups, listSpec(Doc), []);
+    //     // }
+    // }
+
+    export function setAnchorGroups(link: Doc, anchor: Doc, groups: Doc[]) {
+        if (Doc.AreProtosEqual(anchor, Cast(link.anchor1, Doc, new Doc))) {
+            link.anchor1Groups = new List<Doc>(groups);
+        } else {
+            link.anchor2Groups = new List<Doc>(groups);
+        }
+    }
+}
 
 export class LinkManager {
     private static _instance: LinkManager;
@@ -19,31 +56,30 @@ export class LinkManager {
     private constructor() {
     }
 
-    @observable
-    public allLinks: Array<Doc> = [];
+    @observable public allLinks: Array<Doc> = [];
+    @observable public allGroups: Map<string, Doc> = new Map();
 
-    public findAllRelatedLinks(source: Doc): Array<Doc> {
-        let related = LinkManager.Instance.allLinks.filter(
-            link => Doc.AreProtosEqual(source, Cast(link.anchor1, Doc, new Doc)) || Doc.AreProtosEqual(source, Cast(link.anchor2, Doc, new Doc)));
-        return related;
+    public findAllRelatedLinks(anchor: Doc): Array<Doc> {
+        return LinkManager.Instance.allLinks.filter(
+            link => Doc.AreProtosEqual(anchor, Cast(link.anchor1, Doc, new Doc)) || Doc.AreProtosEqual(anchor, Cast(link.anchor2, Doc, new Doc)));
     }
 
-    public findRelatedGroupedLinks(source: Doc): Map<string, Array<Doc>> {
-        let related = this.findAllRelatedLinks(source);
+    public findRelatedGroupedLinks(anchor: Doc): Map<string, Array<Doc>> {
+        let related = this.findAllRelatedLinks(anchor);
 
-        let categories = new Map();
+        let anchorGroups = new Map();
         related.forEach(link => {
             // get groups of given doc
-            let groups = (Doc.AreProtosEqual(source, Cast(link.anchor1, Doc, new Doc))) ? Cast(link.anchor1Groups, listSpec(Doc), []) : Cast(link.anchor2Groups, listSpec(Doc), []);
-            if (groups) {
-                if (groups.length > 0) {
-                    groups.forEach(groupDoc => {
+            let oppGroups = (Doc.AreProtosEqual(anchor, Cast(link.anchor1, Doc, new Doc))) ? Cast(link.anchor1Groups, listSpec(Doc), []) : Cast(link.anchor2Groups, listSpec(Doc), []);
+            if (oppGroups) {
+                if (oppGroups.length > 0) {
+                    oppGroups.forEach(groupDoc => {
                         if (groupDoc instanceof Doc) {
                             let groupType = StrCast(groupDoc.proto!.type);
-                            let group = categories.get(groupType); // TODO: clean this up lol
+                            let group = anchorGroups.get(groupType); // TODO: clean this up lol
                             if (group) group.push(link);
                             else group = [link];
-                            categories.set(groupType, group);
+                            anchorGroups.set(groupType, group);
                         } else {
                             // promise doc
                         }
@@ -52,10 +88,10 @@ export class LinkManager {
                 }
                 else {
                     // if link is in no groups then put it in default group
-                    let group = categories.get("*");
+                    let group = anchorGroups.get("*");
                     if (group) group.push(link);
                     else group = [link];
-                    categories.set("*", group);
+                    anchorGroups.set("*", group);
                 }
             }
 
@@ -66,16 +102,11 @@ export class LinkManager {
             //     else group = [link];
             //     categories.set(link.linkTags, group);
         })
-        return categories;
+        return anchorGroups;
     }
 
-    public findOppositeAnchor(link: Doc, source: Doc): Doc {
-        if (Doc.AreProtosEqual(source, Cast(link.anchor1, Doc, new Doc))) {
-            return Cast(link.anchor2, Doc, new Doc);
-        } else {
-            return Cast(link.anchor1, Doc, new Doc);
-        }
-    }
+
+
 
     // public findAnchorTags(link: Doc, source: Doc): Doc[] {
     //     if (source)
diff --git a/src/client/views/nodes/LinkEditor.scss b/src/client/views/nodes/LinkEditor.scss
index 9629585d7..52ed26442 100644
--- a/src/client/views/nodes/LinkEditor.scss
+++ b/src/client/views/nodes/LinkEditor.scss
@@ -39,4 +39,46 @@
 .save-button:hover {
     background: $main-accent;
     cursor: pointer;
+}
+
+.linkEditor {
+    font-size: 12px; // TODO
+
+    .linkEditor-back {
+        // background-color: $dark-color;
+        // color: $light-color;
+        margin-bottom: 6px;
+    }
+
+    .linkEditor-groupsLabel {
+        display: flex;
+        justify-content: space-between;
+        button {
+            width: 20px;
+            height: 20px;
+            margin-left: 6px;
+            padding: 0;
+            font-size: 14px;
+        }
+    }
+    .linkEditor-linkedTo {
+        border-bottom: 0.5px solid $light-color-secondary; 
+        padding-bottom: 6px;
+        margin-bottom: 6px;
+    }
+    .linkEditor-group {
+        background-color: $light-color-secondary; 
+        padding: 6px;
+        margin: 3px 0;
+        border-radius: 3px;
+    }
+    .linkEditor-group-row {
+        display: flex;
+        .linkEditor-group-row-label {
+            margin-right: 6px;
+        }
+    }
+    .linkEditor-metadata-row {
+        display: flex;
+    }
 }
\ No newline at end of file
diff --git a/src/client/views/nodes/LinkEditor.tsx b/src/client/views/nodes/LinkEditor.tsx
index 2ab8c3460..14fdd26df 100644
--- a/src/client/views/nodes/LinkEditor.tsx
+++ b/src/client/views/nodes/LinkEditor.tsx
@@ -10,17 +10,57 @@ import { StrCast, Cast } from "../../../new_fields/Types";
 import { Doc } from "../../../new_fields/Doc";
 import { List } from "../../../new_fields/List";
 import { listSpec } from "../../../new_fields/Schema";
-import { LinkManager } from "../../util/LinkManager";
+import { LinkManager, LinkUtils } from "../../util/LinkManager";
+import { Docs } from "../../documents/Documents";
+import { Utils } from "../../../Utils";
+import { faArrowLeft, faEllipsisV } from '@fortawesome/free-solid-svg-icons';
+import { library } from "@fortawesome/fontawesome-svg-core";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { string } from "prop-types";
 
-interface Props {
+library.add(faArrowLeft);
+library.add(faEllipsisV);
+
+interface LinkEditorProps {
     sourceDoc: Doc;
     linkDoc: Doc;
-    groups: Map<number, Doc>;
+    groups: Map<string, Doc>;
+    metadata: Map<string, Map<string, Doc>>;
     showLinks: () => void;
 }
 
 @observer
-export class LinkEditor extends React.Component<Props> {
+export class LinkEditor extends React.Component<LinkEditorProps> {
+
+    // @observable private _groups: Map<string, Doc> = new Map();
+    // @observable private _metadata: Map<string, Map<string, Doc>> = new Map();
+
+    // // componentDidMount() {
+
+    // // }
+    // constructor(props: LinkEditorProps) {
+    //     super(props);
+
+    //     let groups = new Map<string, Doc>();
+    //     let metadata: Map<string, Map<string, Doc>> = new Map();
+    //     let groupList = (Doc.AreProtosEqual(props.docView.props.Document, Cast(this._editingLink.anchor1, Doc, new Doc))) ?
+    //         Cast(this._editingLink.anchor1Groups, listSpec(Doc), []) : Cast(this._editingLink.anchor2Groups, listSpec(Doc), []);
+    //     groupList.forEach(groupDoc => {
+    //         if (groupDoc instanceof Doc) {
+    //             let id = Utils.GenerateGuid();
+    //             groups.set(id, groupDoc);
+
+    //             let metadataMap = new Map<string, Doc>();
+    //             let metadataDocs = Cast(groupDoc.proto!.metadata, listSpec(Doc), []);
+    //             metadataDocs.forEach(mdDoc => {
+    //                 if (mdDoc && mdDoc instanceof Doc) { // TODO: handle promise doc
+    //                     metadataMap.set(Utils.GenerateGuid(), mdDoc);
+    //                 }
+    //             })
+    //             metadata.set(id, metadataMap);
+    //         }
+    //     })
+    // }
 
     // @observable private _title: string = StrCast(this.props.linkDoc.title);
     // @observable private _description: string = StrCast(this.props.linkDoc.linkDescription);
@@ -51,68 +91,175 @@ export class LinkEditor extends React.Component<Props> {
     // }
 
     @action
-    editGroup(groupId: number, value: string) {
+    editGroup(groupId: string, value: string) {
         let linkDoc = this.props.linkDoc.proto ? this.props.linkDoc.proto : this.props.linkDoc;
         let groupDoc = this.props.groups.get(groupId);
         if (groupDoc) {
             groupDoc.proto!.type = value;
-            if (Doc.AreProtosEqual(this.props.sourceDoc, Cast(linkDoc.anchor1, Doc, new Doc))) {
-                // let groups = Cast(linkDoc.anchor1Groups, listSpec(Doc), []);
-                // groups.push(groupDoc);
-                linkDoc.anchor1Groups = new List<Doc>([groupDoc]);
-
-            } else {
-                linkDoc.anchor2Groups = new List<Doc>([groupDoc]);
-            }
+            LinkUtils.setAnchorGroups(linkDoc, this.props.sourceDoc, [groupDoc]);
         }
+    }
+
+    @action
+    addGroup = (e: React.MouseEvent): void => {
+        // create new document for group
+        let groupDoc = Docs.TextDocument();
+        groupDoc.proto!.title = "";
+        groupDoc.proto!.metadata = new List<Doc>([]);
 
+        this.props.groups.set(Utils.GenerateGuid(), groupDoc);
+
+        let linkDoc = this.props.linkDoc.proto ? this.props.linkDoc.proto : this.props.linkDoc;
+        LinkUtils.setAnchorGroups(linkDoc, this.props.sourceDoc, Array.from(this.props.groups.values()));
     }
 
-    renderGroup(groupId: number, groupDoc: Doc) {
+    renderGroup(groupId: string, groupDoc: Doc) {
+        // let metadata = this.props.metadata.get(groupId);
+        // if (!metadata) {
+        //     metadata = new Map<string, Doc>();
+        // }
         return (
-            <div>
-                <p>type:</p>
-                <input type="text" value={StrCast(groupDoc.proto!.type)} onChange={e => this.editGroup(groupId, e.target.value)}></input>
+            // <div key={groupId} className="linkEditor-group">
+            <div key={groupId} className="linkEditor-group-row">
+                <p className="linkEditor-group-row-label">type:</p>
+                <input key={groupId + "-type"} type="text" value={StrCast(groupDoc.proto!.type)} onChange={e => this.editGroup(groupId, e.target.value)}></input>
             </div>
+            //         {/* {this.renderMetadata(groupId)} */ }
+            // {/* <button onPointerDown={() => this.addMetadata(groupId)}>+</button> */ }
+            //     // </div>
         )
     }
 
+    @action
+    addMetadata = (groupId: string): void => {
+        // create new metadata doc
+        let mdDoc = Docs.TextDocument();
+        mdDoc.proto!.title = "";
+        mdDoc.proto!.value = "";
+
+        // append to map
+        let mdMap = this.props.metadata.get(groupId);
+        if (mdMap) {
+            mdMap.set(Utils.GenerateGuid(), mdDoc);
+        } else {
+            mdMap = new Map();
+            mdMap.set(Utils.GenerateGuid(), mdDoc);
+        }
+
+        // add to internal representation of metadata
+        this.props.metadata.set(groupId, mdMap);
+
+        // add to internatal representation of group
+        let groupDoc = this.props.groups.get(groupId);
+        if (groupDoc) {
+            groupDoc.proto!.metadata = new List<Doc>(Array.from(mdMap.values()));
+            this.props.groups.set(groupId, groupDoc);
+        }
+
+        // add to link doc
+        let linkDoc = this.props.linkDoc.proto ? this.props.linkDoc.proto : this.props.linkDoc;
+        LinkUtils.setAnchorGroups(linkDoc, this.props.sourceDoc, Array.from(this.props.groups.values()));
+
+    }
+
+    // @action
+    // addMetadata = (groupId: string): void => {
+    //     let groupDoc = this.props.groups.get(groupId);
+    //     if (groupDoc) {
+    //         // create new document for metadata row
+    //         let metadata = Cast(groupDoc.metadata, listSpec(Doc), []);
+    //         let metadataDoc = Docs.TextDocument();
+    //         metadataDoc.proto!.title = "";
+    //         metadataDoc.proto!.value = "";
+    //         let metadataMap = new Map<string, 
+
+    //         this.props.metadata.set(groupId, new Map)
+
+    //         groupDoc.proto!.metadata = new List<Doc>([metadataDoc]); // TODO: append to metadata
+    //     }
+    // }
+
+    // @action
+    // editMetadataTitle = (groupId: string, mdId: string, value: string) => {
+    //     let group = this.props.metadata.get(groupId);
+    //     if (group) {
+    //         let mdDoc = group.get(mdId);
+    //         if (mdDoc) {
+    //             mdDoc.proto!.title = value;
+    //         }
+    //     }
+    // }
+
+    // @action
+    // editMetadataValue = (groupId: string, mdId: string, value: string) => {
+    //     let group = this.props.metadata.get(groupId);
+    //     if (group) {
+    //         let mdDoc = group.get(mdId);
+    //         if (mdDoc) {
+    //             mdDoc.proto!.value = value;
+    //         }
+    //     }
+    // }
+
+    @action
+    editMetadataTitle(groupId: string, mdId: string, value: string) {
+
+    }
+
+    @action
+    editMetadataValue(groupId: string, mdId: string, value: string) {
+
+    }
+
+    renderMetadata(groupId: string) {
+        let metadata: Array<JSX.Element> = [];
+        let metadataMap = this.props.metadata.get(groupId);
+        if (metadataMap) {
+            metadataMap.forEach((mdDoc, mdId) => {
+                metadata.push(
+                    <div key={mdId} className="linkEditor-metadata-row">
+                        <input type="text" value={StrCast(mdDoc.proto!.title)} onChange={e => this.editMetadataTitle(groupId, mdId, e.target.value)}></input>
+                        :
+                        <input type="text" value={StrCast(mdDoc.proto!.value)} onChange={e => this.editMetadataValue(groupId, mdId, e.target.value)}></input>
+                    </div>
+                )
+            })
+        }
+
+        return metadata;
+
+        // let metadataList: Array<JSX.Element> = [];
+        // metadata.forEach((mdDoc, mdId) => {
+        //     metadataList.push(
+        //         <div key={mdId} className="linkEditor-metadata-row">
+        //             <input type="text" value={StrCast(mdDoc.proto!.title)} onChange={e => this.editMetadataTitle(groupId, mdId, e.target.value)}></input>:
+        //             <input type="text" value={StrCast(mdDoc.proto!.value)} onChange={e => this.editMetadataValue(groupId, mdId, e.target.value)}></input>
+        //         </div>
+        //     )
+        // })
+    }
+
     renderGroups() {
         let groups: Array<JSX.Element> = [];
         this.props.groups.forEach((groupDoc, groupId) => {
-            groups.push(
-                <div>
-                    {this.renderGroup(groupId, groupDoc)}
-                </div>
-            )
+            groups.push(this.renderGroup(groupId, groupDoc))
         });
         return groups;
     }
 
-    onSaveButtonPressed = (e: React.PointerEvent): void => {
-        e.stopPropagation();
-
-        // let linkDoc = this.props.linkDoc.proto ? this.props.linkDoc.proto : this.props.linkDoc;
-        // // linkDoc.title = this._title;
-        // // linkDoc.linkDescription = this._description;
-
-        this.props.showLinks();
-    }
-
     render() {
-        let destination = LinkManager.Instance.findOppositeAnchor(this.props.linkDoc, this.props.sourceDoc);
+        let destination = LinkUtils.findOppositeAnchor(this.props.linkDoc, this.props.sourceDoc);
 
         return (
-            <div className="edit-container">
-                <p>linked to: {destination.proto!.title}</p>
-                <b>Groups:</b>
+            <div className="linkEditor">
+                <button className="linkEditor-back" onPointerDown={() => this.props.showLinks()}><FontAwesomeIcon icon="arrow-left" size="sm" /></button>
+                <p className="linkEditor-linkedTo">editing link to: <b>{destination.proto!.title}</b></p>
+                <div className="linkEditor-groupsLabel">
+                    <b>Groups:</b>
+                    <button onClick={this.addGroup} title="Add Group">+</button>
+                </div>
                 {this.renderGroups()}
 
-                {/* <input onChange={this.onTitleChanged} className="name-input" type="text" value={this._title} placeholder="Name . . ."></input>
-                <textarea onChange={this.onDescriptionChanged} className="description-input" value={this._description} placeholder="Description . . ."></textarea> */}
-                {/* {this.renderTags()}
-                <button onClick={this.addTag}>+</button> */}
-                <div className="save-button" onPointerDown={this.onSaveButtonPressed}>SAVE</div>
             </div>
 
         );
diff --git a/src/client/views/nodes/LinkMenu.tsx b/src/client/views/nodes/LinkMenu.tsx
index affe35e2a..ab478feae 100644
--- a/src/client/views/nodes/LinkMenu.tsx
+++ b/src/client/views/nodes/LinkMenu.tsx
@@ -8,9 +8,10 @@ import React = require("react");
 import { Doc, DocListCast } from "../../../new_fields/Doc";
 import { Cast, FieldValue, StrCast } from "../../../new_fields/Types";
 import { Id } from "../../../new_fields/FieldSymbols";
-import { LinkManager } from "../../util/LinkManager";
-import { number } from "prop-types";
+import { LinkManager, LinkUtils } from "../../util/LinkManager";
+import { number, string } from "prop-types";
 import { listSpec } from "../../../new_fields/Schema";
+import { Utils } from "../../../Utils";
 
 interface Props {
     docView: DocumentView;
@@ -34,12 +35,11 @@ export class LinkMenu extends React.Component<Props> {
     renderLinkGroupItems(links: Doc[]) {
         let source = this.props.docView.Document;
         return links.map(link => {
-            // let destination = (link["linkedTo"] === source) ? link["linkedFrom"] : link["linkedTo"];
-            let destination = LinkManager.Instance.findOppositeAnchor(link, source);
+            let destination = LinkUtils.findOppositeAnchor(link, source);
             let doc = FieldValue(Cast(destination, Doc));
             if (doc) {
                 console.log(doc[Id] + source[Id], "source is", source[Id]);
-                return <LinkBox key={doc[Id] + source[Id]} linkDoc={link} linkName={"link"} pairedDoc={doc} showEditor={action(() => this._editingLink = link)} type={""} />;
+                return <LinkBox key={doc[Id] + source[Id]} linkDoc={link} linkName={StrCast(destination.title)} pairedDoc={doc} showEditor={action(() => this._editingLink = link)} type={""} />;
             }
         });
     }
@@ -79,20 +79,28 @@ export class LinkMenu extends React.Component<Props> {
                 </div>
             );
         } else {
-            let counter = 0;
-            let groups = new Map<number, Doc>();
+            let groups = new Map<string, Doc>();
+            let metadata: Map<string, Map<string, Doc>> = new Map();
             let groupList = (Doc.AreProtosEqual(this.props.docView.props.Document, Cast(this._editingLink.anchor1, Doc, new Doc))) ?
                 Cast(this._editingLink.anchor1Groups, listSpec(Doc), []) : Cast(this._editingLink.anchor2Groups, listSpec(Doc), []);
-            groupList.forEach(group => {
-                if (group instanceof Doc) {
-                    console.log(counter);
-                    groups.set(counter, group);
-                    counter++;
+            groupList.forEach(groupDoc => {
+                if (groupDoc instanceof Doc) {
+                    let id = Utils.GenerateGuid();
+                    groups.set(id, groupDoc);
+
+                    let metadataMap = new Map<string, Doc>();
+                    let metadataDocs = Cast(groupDoc.proto!.metadata, listSpec(Doc), []);
+                    metadataDocs.forEach(mdDoc => {
+                        if (mdDoc && mdDoc instanceof Doc) { // TODO: handle promise doc
+                            metadataMap.set(Utils.GenerateGuid(), mdDoc);
+                        }
+                    })
+                    metadata.set(id, metadataMap);
                 }
             })
 
             return (
-                <LinkEditor groups={groups} sourceDoc={this.props.docView.props.Document} linkDoc={this._editingLink} showLinks={action(() => this._editingLink = undefined)}></LinkEditor>
+                <LinkEditor groups={groups} metadata={metadata} sourceDoc={this.props.docView.props.Document} linkDoc={this._editingLink} showLinks={action(() => this._editingLink = undefined)}></LinkEditor>
             );
         }
 
-- 
cgit v1.2.3-70-g09d2


From 074b4b2a3246ae86fda334629d40540dd2bbf633 Mon Sep 17 00:00:00 2001
From: Fawn <fangrui_tong@brown.edu>
Date: Tue, 11 Jun 2019 15:27:02 -0400
Subject: links can choose group type from preexisting group types

---
 src/client/util/LinkManager.ts         |   2 +-
 src/client/views/nodes/LinkEditor.scss |  26 +++
 src/client/views/nodes/LinkEditor.tsx  | 370 ++++++++++++++++++---------------
 src/client/views/nodes/LinkMenu.tsx    |  43 ++--
 4 files changed, 252 insertions(+), 189 deletions(-)

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

diff --git a/src/client/util/LinkManager.ts b/src/client/util/LinkManager.ts
index 5e8e0475b..99fb4c6c0 100644
--- a/src/client/util/LinkManager.ts
+++ b/src/client/util/LinkManager.ts
@@ -57,7 +57,7 @@ export class LinkManager {
     }
 
     @observable public allLinks: Array<Doc> = [];
-    @observable public allGroups: Map<string, Doc> = new Map();
+    @observable public allGroups: Map<string, Array<string>> = new Map();
 
     public findAllRelatedLinks(anchor: Doc): Array<Doc> {
         return LinkManager.Instance.allLinks.filter(
diff --git a/src/client/views/nodes/LinkEditor.scss b/src/client/views/nodes/LinkEditor.scss
index 52ed26442..d3ac8cf19 100644
--- a/src/client/views/nodes/LinkEditor.scss
+++ b/src/client/views/nodes/LinkEditor.scss
@@ -80,5 +80,31 @@
     }
     .linkEditor-metadata-row {
         display: flex;
+        justify-content: space-between;
+        input {
+            width: calc(50% - 2px);
+        }
+    }
+}
+
+.linkEditor-dropdown {
+    width: 100%;
+    position: relative;
+    .linkEditor-options-wrapper {
+        width: 100%;
+        position: absolute;
+        top: 19px;
+        left: 0;
+        display: flex;
+        flex-direction: column;
+    }
+    .linkEditor-option {
+        background-color: red;
+        border: 1px solid $intermediate-color;
+        border-top: 0;
+        cursor: pointer;
+        &:hover {
+            background-color:  $light-color-secondary;
+        }
     }
 }
\ No newline at end of file
diff --git a/src/client/views/nodes/LinkEditor.tsx b/src/client/views/nodes/LinkEditor.tsx
index 14fdd26df..481d3bf37 100644
--- a/src/client/views/nodes/LinkEditor.tsx
+++ b/src/client/views/nodes/LinkEditor.tsx
@@ -21,85 +21,119 @@ import { string } from "prop-types";
 library.add(faArrowLeft);
 library.add(faEllipsisV);
 
+// this dropdown could be generalizeds
+@observer
+class LinkGroupsDropdown extends React.Component<{ groupId: string, groupType: string, setGroup: (groupId: string, group: string) => void }> {
+    @observable private _searchTerm: string = "";
+    @observable private _groupType: string = this.props.groupType;
+
+    @action
+    setSearchTerm(value: string) {
+        this._searchTerm = value;
+    }
+
+    @action
+    setGroupType(value: string) {
+        this._groupType = value;
+    }
+
+    createGroup(value: string) {
+        LinkManager.Instance.allGroups.set(value, []);
+        this.props.setGroup(this.props.groupId, value);
+    }
+
+    renderOptions = (): JSX.Element[] => {
+        let allGroups: string[], searchTerm: string, results: string[], exactFound: boolean;
+        if (this._searchTerm !== "") {
+            allGroups = Array.from(LinkManager.Instance.allGroups.keys());
+            searchTerm = this._searchTerm.toUpperCase();
+            results = allGroups.filter(group => group.toUpperCase().indexOf(searchTerm) > -1);
+            exactFound = results.findIndex(group => group.toUpperCase() === searchTerm) > -1;
+        } else {
+            results = [];
+            exactFound = false;
+        }
+
+        let options = [];
+        results.forEach(result => {
+            options.push(<div key={result} className="linkEditor-option"
+                onClick={() => { this.props.setGroup(this.props.groupId, result); this.setGroupType(result); this.setSearchTerm("") }}>{result}</div>)
+        });
+
+        if (!exactFound && this._searchTerm !== "") {
+            options.push(<div key={""} className="linkEditor-option"
+                onClick={() => { this.createGroup(this._searchTerm); this.setGroupType(this._searchTerm); this.setSearchTerm("") }}>Create new "{this._searchTerm}" group</div>)
+        }
+
+        return options;
+    }
+
+    render() {
+        return (
+            <div className="linkEditor-dropdown">
+                <input type="text" value={this._groupType} placeholder="Search for a group or create a new group"
+                    onChange={e => { this.setSearchTerm(e.target.value); this.setGroupType(e.target.value) }}></input>
+                <div className="linkEditor-options-wrapper">
+                    {this.renderOptions()}
+                </div>
+            </div>
+        )
+    }
+}
+
+
 interface LinkEditorProps {
     sourceDoc: Doc;
     linkDoc: Doc;
-    groups: Map<string, Doc>;
-    metadata: Map<string, Map<string, Doc>>;
+    // groups: Map<string, Doc>;
+    // metadata: Map<string, Map<string, Doc>>;
     showLinks: () => void;
 }
 
 @observer
 export class LinkEditor extends React.Component<LinkEditorProps> {
 
-    // @observable private _groups: Map<string, Doc> = new Map();
-    // @observable private _metadata: Map<string, Map<string, Doc>> = new Map();
-
-    // // componentDidMount() {
-
-    // // }
-    // constructor(props: LinkEditorProps) {
-    //     super(props);
-
-    //     let groups = new Map<string, Doc>();
-    //     let metadata: Map<string, Map<string, Doc>> = new Map();
-    //     let groupList = (Doc.AreProtosEqual(props.docView.props.Document, Cast(this._editingLink.anchor1, Doc, new Doc))) ?
-    //         Cast(this._editingLink.anchor1Groups, listSpec(Doc), []) : Cast(this._editingLink.anchor2Groups, listSpec(Doc), []);
-    //     groupList.forEach(groupDoc => {
-    //         if (groupDoc instanceof Doc) {
-    //             let id = Utils.GenerateGuid();
-    //             groups.set(id, groupDoc);
-
-    //             let metadataMap = new Map<string, Doc>();
-    //             let metadataDocs = Cast(groupDoc.proto!.metadata, listSpec(Doc), []);
-    //             metadataDocs.forEach(mdDoc => {
-    //                 if (mdDoc && mdDoc instanceof Doc) { // TODO: handle promise doc
-    //                     metadataMap.set(Utils.GenerateGuid(), mdDoc);
-    //                 }
-    //             })
-    //             metadata.set(id, metadataMap);
-    //         }
-    //     })
-    // }
-
-    // @observable private _title: string = StrCast(this.props.linkDoc.title);
-    // @observable private _description: string = StrCast(this.props.linkDoc.linkDescription);
-    // @observable private _tags: Array<string> = Cast(this.props.linkDoc.linkTags, List);
-
-    // @action
-    // onTitleChanged = (e: React.ChangeEvent<HTMLInputElement>) => {
-    //     this._title = e.target.value;
-    // }
+    @observable private _groups: Map<string, Doc> = new Map();
+    @observable private _metadata: Map<string, Map<string, Doc>> = new Map();
+
+    constructor(props: LinkEditorProps) {
+        super(props);
+
+        let groups = new Map<string, Doc>();
+        let metadata: Map<string, Map<string, Doc>> = new Map();
+        let groupList = (Doc.AreProtosEqual(props.sourceDoc, Cast(props.linkDoc.anchor1, Doc, new Doc))) ?
+            Cast(props.linkDoc.anchor1Groups, listSpec(Doc), []) : Cast(props.linkDoc.anchor2Groups, listSpec(Doc), []);
+        groupList.forEach(groupDoc => {
+            if (groupDoc instanceof Doc) {
+                let id = Utils.GenerateGuid();
+                groups.set(id, groupDoc);
+
+                let metadataMap = new Map<string, Doc>();
+                let metadataDocs = Cast(groupDoc.proto!.metadata, listSpec(Doc), []);
+                metadataDocs.forEach(mdDoc => {
+                    if (mdDoc && mdDoc instanceof Doc) { // TODO: handle promise doc
+                        metadataMap.set(Utils.GenerateGuid(), mdDoc);
+                    }
+                })
+                metadata.set(id, metadataMap);
+            } else {
+                // promise doc
+            }
+        })
+        this._groups = groups;
+        this._metadata = metadata;
+    }
 
     // @action
-    // onDescriptionChanged = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
-    //     this._description = e.target.value;
-    // }
-
-    // renderTags() {
-    //     return this._tags.map(tag => {
-    //         if (tag === "") {
-    //             return <input type="text" placeholder="Tag"></input>;
-    //         } else {
-    //             return <input type="text" value={tag}></input>;
-    //         }
-    //     })
-    // }
-
-    // addTag = (): void => {
-    //     this._tags.push("");
+    // editGroup(groupId: string, value: string) {
+    //     let linkDoc = this.props.linkDoc.proto ? this.props.linkDoc.proto : this.props.linkDoc;
+    //     let groupDoc = this._groups.get(groupId);
+    //     if (groupDoc) {
+    //         groupDoc.proto!.type = value;
+    //         LinkUtils.setAnchorGroups(linkDoc, this.props.sourceDoc, [groupDoc]);
+    //     }
     // }
 
-    @action
-    editGroup(groupId: string, value: string) {
-        let linkDoc = this.props.linkDoc.proto ? this.props.linkDoc.proto : this.props.linkDoc;
-        let groupDoc = this.props.groups.get(groupId);
-        if (groupDoc) {
-            groupDoc.proto!.type = value;
-            LinkUtils.setAnchorGroups(linkDoc, this.props.sourceDoc, [groupDoc]);
-        }
-    }
-
     @action
     addGroup = (e: React.MouseEvent): void => {
         // create new document for group
@@ -107,149 +141,150 @@ export class LinkEditor extends React.Component<LinkEditorProps> {
         groupDoc.proto!.title = "";
         groupDoc.proto!.metadata = new List<Doc>([]);
 
-        this.props.groups.set(Utils.GenerateGuid(), groupDoc);
+        this._groups.set(Utils.GenerateGuid(), groupDoc);
+
+        let linkDoc = this.props.linkDoc.proto ? this.props.linkDoc.proto : this.props.linkDoc;
+        LinkUtils.setAnchorGroups(linkDoc, this.props.sourceDoc, Array.from(this._groups.values()));
+    }
 
+    @action
+    setGroup = (groupId: string, group: string): void => {
         let linkDoc = this.props.linkDoc.proto ? this.props.linkDoc.proto : this.props.linkDoc;
-        LinkUtils.setAnchorGroups(linkDoc, this.props.sourceDoc, Array.from(this.props.groups.values()));
+        let groupDoc = this._groups.get(groupId);
+        if (groupDoc) {
+            groupDoc.proto!.type = group;
+            LinkUtils.setAnchorGroups(linkDoc, this.props.sourceDoc, [groupDoc]);
+        }
     }
 
     renderGroup(groupId: string, groupDoc: Doc) {
-        // let metadata = this.props.metadata.get(groupId);
-        // if (!metadata) {
-        //     metadata = new Map<string, Doc>();
-        // }
+        console.log("testing", groupDoc["strawberry"], groupDoc["type"]);
         return (
-            // <div key={groupId} className="linkEditor-group">
-            <div key={groupId} className="linkEditor-group-row">
-                <p className="linkEditor-group-row-label">type:</p>
-                <input key={groupId + "-type"} type="text" value={StrCast(groupDoc.proto!.type)} onChange={e => this.editGroup(groupId, e.target.value)}></input>
+            <div key={groupId} className="linkEditor-group">
+                <div className="linkEditor-group-row">
+                    <p className="linkEditor-group-row-label">type:</p>
+                    <LinkGroupsDropdown groupId={groupId} groupType={StrCast(groupDoc.proto!.type)} setGroup={this.setGroup} />
+                    {/* <input key={groupId + "-type"} type="text" value={StrCast(groupDoc.proto!.type)} onChange={e => this.editGroup(groupId, e.target.value)}></input> */}
+                    {/* <input key={groupId + "-type"} type="text" value={StrCast(groupDoc.proto!.type)} onChange={e => this.editGroup(groupId, e.target.value)}></input> */}
+                </div>
+                {this.renderMetadata(groupId)}
+                <button onClick={() => this.addMetadata(groupId)}>+</button>
             </div>
-            //         {/* {this.renderMetadata(groupId)} */ }
-            // {/* <button onPointerDown={() => this.addMetadata(groupId)}>+</button> */ }
-            //     // </div>
         )
     }
 
     @action
-    addMetadata = (groupId: string): void => {
-        // create new metadata doc
-        let mdDoc = Docs.TextDocument();
-        mdDoc.proto!.title = "";
-        mdDoc.proto!.value = "";
-
-        // append to map
-        let mdMap = this.props.metadata.get(groupId);
-        if (mdMap) {
-            mdMap.set(Utils.GenerateGuid(), mdDoc);
+    addMetadata = (groupType: string): void => {
+        let mdKeys = LinkManager.Instance.allGroups.get(groupType);
+        if (mdKeys) {
+            if (mdKeys.indexOf("new key") > -1) {
+                mdKeys.push("new key");
+            }
         } else {
-            mdMap = new Map();
-            mdMap.set(Utils.GenerateGuid(), mdDoc);
+            mdKeys = ["new key"];
         }
+        LinkManager.Instance.allGroups.set(groupType, mdKeys);
+        console.log("md added", groupType, LinkManager.Instance.allGroups.get(groupType), LinkManager.Instance.allGroups);
+
+        // // create new metadata doc
+        // let mdDoc = Docs.TextDocument();
+        // mdDoc.proto!.title = "";
+        // mdDoc.proto!.value = "";
+
+        // // append to map
+        // let mdMap = this._metadata.get(groupId);
+        // if (mdMap) {
+        //     mdMap.set(Utils.GenerateGuid(), mdDoc);
+        // } else {
+        //     mdMap = new Map();
+        //     mdMap.set(Utils.GenerateGuid(), mdDoc);
+        // }
 
-        // add to internal representation of metadata
-        this.props.metadata.set(groupId, mdMap);
-
-        // add to internatal representation of group
-        let groupDoc = this.props.groups.get(groupId);
-        if (groupDoc) {
-            groupDoc.proto!.metadata = new List<Doc>(Array.from(mdMap.values()));
-            this.props.groups.set(groupId, groupDoc);
-        }
+        // // add to internal representation of metadata
+        // this._metadata.set(groupId, mdMap);
 
-        // add to link doc
-        let linkDoc = this.props.linkDoc.proto ? this.props.linkDoc.proto : this.props.linkDoc;
-        LinkUtils.setAnchorGroups(linkDoc, this.props.sourceDoc, Array.from(this.props.groups.values()));
+        // // add to internatal representation of group
+        // let groupDoc = this._groups.get(groupId);
+        // if (groupDoc) {
+        //     groupDoc.proto!.metadata = new List<Doc>(Array.from(mdMap.values()));
+        //     this._groups.set(groupId, groupDoc);
+        // }
 
+        // // add to link doc
+        // let linkDoc = this.props.linkDoc.proto ? this.props.linkDoc.proto : this.props.linkDoc;
+        // LinkUtils.setAnchorGroups(linkDoc, this.props.sourceDoc, Array.from(this._groups.values()));
     }
 
     // @action
-    // addMetadata = (groupId: string): void => {
-    //     let groupDoc = this.props.groups.get(groupId);
-    //     if (groupDoc) {
-    //         // create new document for metadata row
-    //         let metadata = Cast(groupDoc.metadata, listSpec(Doc), []);
-    //         let metadataDoc = Docs.TextDocument();
-    //         metadataDoc.proto!.title = "";
-    //         metadataDoc.proto!.value = "";
-    //         let metadataMap = new Map<string, 
-
-    //         this.props.metadata.set(groupId, new Map)
-
-    //         groupDoc.proto!.metadata = new List<Doc>([metadataDoc]); // TODO: append to metadata
-    //     }
-    // }
-
-    // @action
-    // editMetadataTitle = (groupId: string, mdId: string, value: string) => {
-    //     let group = this.props.metadata.get(groupId);
-    //     if (group) {
-    //         let mdDoc = group.get(mdId);
+    // editMetadataTitle(groupId: string, mdId: string, value: string) {
+    //     let groupMd = this._metadata.get(groupId);
+    //     if (groupMd) {
+    //         let mdDoc = groupMd.get(mdId);
     //         if (mdDoc) {
     //             mdDoc.proto!.title = value;
     //         }
     //     }
+    //     // set group and link?
     // }
 
     // @action
-    // editMetadataValue = (groupId: string, mdId: string, value: string) => {
-    //     let group = this.props.metadata.get(groupId);
-    //     if (group) {
-    //         let mdDoc = group.get(mdId);
+    // editMetadataValue(groupId: string, mdId: string, value: string) {
+    //     let groupMd = this._metadata.get(groupId);
+    //     if (groupMd) {
+    //         let mdDoc = groupMd.get(mdId);
     //         if (mdDoc) {
     //             mdDoc.proto!.value = value;
     //         }
     //     }
+    //     // set group and link?
     // }
 
-    @action
-    editMetadataTitle(groupId: string, mdId: string, value: string) {
-
-    }
-
-    @action
-    editMetadataValue(groupId: string, mdId: string, value: string) {
-
-    }
-
     renderMetadata(groupId: string) {
         let metadata: Array<JSX.Element> = [];
-        let metadataMap = this.props.metadata.get(groupId);
-        if (metadataMap) {
-            metadataMap.forEach((mdDoc, mdId) => {
-                metadata.push(
-                    <div key={mdId} className="linkEditor-metadata-row">
-                        <input type="text" value={StrCast(mdDoc.proto!.title)} onChange={e => this.editMetadataTitle(groupId, mdId, e.target.value)}></input>
-                        :
-                        <input type="text" value={StrCast(mdDoc.proto!.value)} onChange={e => this.editMetadataValue(groupId, mdId, e.target.value)}></input>
-                    </div>
-                )
-            })
+        let groupDoc = this._groups.get(groupId);
+        if (groupDoc) {
+            let mdDoc = Cast(groupDoc.proto!.metadata, Doc, new Doc);
+            let groupType = StrCast(groupDoc.proto!.type);
+            let groupMdKeys = LinkManager.Instance.allGroups.get(groupType);
+            console.log("rendering md", groupType, groupMdKeys, LinkManager.Instance.allGroups);
+            if (groupMdKeys) {
+                groupMdKeys.forEach(key => {
+                    metadata.push(
+                        <div key={key} className="linkEditor-metadata-row">
+                            <input type="text" value={key} placeholder="key"></input>
+                            :
+                            <input type="text" value={(mdDoc[key] === undefined) ? "" : StrCast(mdDoc[key])} placeholder="value"></input>
+                        </div>
+                    )
+                })
+            }
         }
 
-        return metadata;
 
-        // let metadataList: Array<JSX.Element> = [];
-        // metadata.forEach((mdDoc, mdId) => {
-        //     metadataList.push(
-        //         <div key={mdId} className="linkEditor-metadata-row">
-        //             <input type="text" value={StrCast(mdDoc.proto!.title)} onChange={e => this.editMetadataTitle(groupId, mdId, e.target.value)}></input>:
-        //             <input type="text" value={StrCast(mdDoc.proto!.value)} onChange={e => this.editMetadataValue(groupId, mdId, e.target.value)}></input>
-        //         </div>
-        //     )
-        // })
-    }
+        // let metadataMap = this._metadata.get(groupId);
+        // if (metadataMap) {
+        //     metadataMap.forEach((mdDoc, mdId) => {
+        //         metadata.push(
+        //             <div key={mdId} className="linkEditor-metadata-row">
+        //                 <input type="text" value={StrCast(mdDoc.proto!.title)} placeholder="key" onChange={e => this.editMetadataTitle(groupId, mdId, e.target.value)}></input>
+        //                 :
+        //                 <input type="text" value={StrCast(mdDoc.proto!.value)} placeholder="value" onChange={e => this.editMetadataValue(groupId, mdId, e.target.value)}></input>
+        //             </div>
+        //         )
+        //     })
+        // }
 
-    renderGroups() {
-        let groups: Array<JSX.Element> = [];
-        this.props.groups.forEach((groupDoc, groupId) => {
-            groups.push(this.renderGroup(groupId, groupDoc))
-        });
-        return groups;
+        return metadata;
     }
 
     render() {
         let destination = LinkUtils.findOppositeAnchor(this.props.linkDoc, this.props.sourceDoc);
 
+        let groups: Array<JSX.Element> = [];
+        this._groups.forEach((groupDoc, groupId) => {
+            groups.push(this.renderGroup(groupId, groupDoc))
+        });
+
         return (
             <div className="linkEditor">
                 <button className="linkEditor-back" onPointerDown={() => this.props.showLinks()}><FontAwesomeIcon icon="arrow-left" size="sm" /></button>
@@ -258,8 +293,7 @@ export class LinkEditor extends React.Component<LinkEditorProps> {
                     <b>Groups:</b>
                     <button onClick={this.addGroup} title="Add Group">+</button>
                 </div>
-                {this.renderGroups()}
-
+                {groups}
             </div>
 
         );
diff --git a/src/client/views/nodes/LinkMenu.tsx b/src/client/views/nodes/LinkMenu.tsx
index ab478feae..e378ee1cb 100644
--- a/src/client/views/nodes/LinkMenu.tsx
+++ b/src/client/views/nodes/LinkMenu.tsx
@@ -48,7 +48,6 @@ export class LinkMenu extends React.Component<Props> {
         let linkItems: Array<JSX.Element> = [];
 
         links.forEach((links, group) => {
-            console.log("category is ", group);
             linkItems.push(
                 <div key={group} className="link-menu-group">
                     <p className="link-menu-group-name">{group}:</p>
@@ -59,6 +58,10 @@ export class LinkMenu extends React.Component<Props> {
             )
         });
 
+        if (linkItems.length === 0) {
+            linkItems.push(<p key="">no links have been created yet</p>);
+        }
+
         return linkItems;
     }
 
@@ -79,28 +82,28 @@ export class LinkMenu extends React.Component<Props> {
                 </div>
             );
         } else {
-            let groups = new Map<string, Doc>();
-            let metadata: Map<string, Map<string, Doc>> = new Map();
-            let groupList = (Doc.AreProtosEqual(this.props.docView.props.Document, Cast(this._editingLink.anchor1, Doc, new Doc))) ?
-                Cast(this._editingLink.anchor1Groups, listSpec(Doc), []) : Cast(this._editingLink.anchor2Groups, listSpec(Doc), []);
-            groupList.forEach(groupDoc => {
-                if (groupDoc instanceof Doc) {
-                    let id = Utils.GenerateGuid();
-                    groups.set(id, groupDoc);
+            // let groups = new Map<string, Doc>();
+            // let metadata: Map<string, Map<string, Doc>> = new Map();
+            // let groupList = (Doc.AreProtosEqual(this.props.docView.props.Document, Cast(this._editingLink.anchor1, Doc, new Doc))) ?
+            //     Cast(this._editingLink.anchor1Groups, listSpec(Doc), []) : Cast(this._editingLink.anchor2Groups, listSpec(Doc), []);
+            // groupList.forEach(groupDoc => {
+            //     if (groupDoc instanceof Doc) {
+            //         let id = Utils.GenerateGuid();
+            //         groups.set(id, groupDoc);
 
-                    let metadataMap = new Map<string, Doc>();
-                    let metadataDocs = Cast(groupDoc.proto!.metadata, listSpec(Doc), []);
-                    metadataDocs.forEach(mdDoc => {
-                        if (mdDoc && mdDoc instanceof Doc) { // TODO: handle promise doc
-                            metadataMap.set(Utils.GenerateGuid(), mdDoc);
-                        }
-                    })
-                    metadata.set(id, metadataMap);
-                }
-            })
+            //         let metadataMap = new Map<string, Doc>();
+            //         let metadataDocs = Cast(groupDoc.proto!.metadata, listSpec(Doc), []);
+            //         metadataDocs.forEach(mdDoc => {
+            //             if (mdDoc && mdDoc instanceof Doc) { // TODO: handle promise doc
+            //                 metadataMap.set(Utils.GenerateGuid(), mdDoc);
+            //             }
+            //         })
+            //         metadata.set(id, metadataMap);
+            //     }
+            // })
 
             return (
-                <LinkEditor groups={groups} metadata={metadata} sourceDoc={this.props.docView.props.Document} linkDoc={this._editingLink} showLinks={action(() => this._editingLink = undefined)}></LinkEditor>
+                <LinkEditor sourceDoc={this.props.docView.props.Document} linkDoc={this._editingLink} showLinks={action(() => this._editingLink = undefined)}></LinkEditor>
             );
         }
 
-- 
cgit v1.2.3-70-g09d2


From 2c3b54d6e07c37a4f5fd52a49d6d60e7a8e5d2d2 Mon Sep 17 00:00:00 2001
From: Fawn <fangrui_tong@brown.edu>
Date: Tue, 11 Jun 2019 18:17:28 -0400
Subject: metadata types can be assigned to groups of links

---
 src/client/documents/Documents.ts     |  2 +-
 src/client/views/nodes/LinkEditor.tsx | 76 +++++++++++++++++++++++++++++------
 src/client/views/nodes/LinkMenu.tsx   |  1 -
 3 files changed, 64 insertions(+), 15 deletions(-)

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

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 731490d97..9f1501265 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -91,7 +91,7 @@ export namespace DocUtils {
         UndoManager.RunInBatch(() => {
             let groupDoc = Docs.TextDocument();
             groupDoc.proto!.type = "*";
-            groupDoc.proto!.metadata = new List<Doc>([]);
+            groupDoc.proto!.metadata = Docs.TextDocument();
 
             let linkDoc = Docs.TextDocument({ width: 100, height: 30, borderRounding: -1 });
             //let linkDoc = new Doc;
diff --git a/src/client/views/nodes/LinkEditor.tsx b/src/client/views/nodes/LinkEditor.tsx
index 481d3bf37..8d12bc30f 100644
--- a/src/client/views/nodes/LinkEditor.tsx
+++ b/src/client/views/nodes/LinkEditor.tsx
@@ -37,6 +37,7 @@ class LinkGroupsDropdown extends React.Component<{ groupId: string, groupType: s
         this._groupType = value;
     }
 
+    @action
     createGroup(value: string) {
         LinkManager.Instance.allGroups.set(value, []);
         this.props.setGroup(this.props.groupId, value);
@@ -81,12 +82,49 @@ class LinkGroupsDropdown extends React.Component<{ groupId: string, groupType: s
     }
 }
 
+@observer
+class LinkMetadataEditor extends React.Component<{ groupType: string, mdDoc: Doc, mdKey: string, mdValue: string }> {
+    @observable private _key: string = this.props.mdKey;
+    @observable private _value: string = this.props.mdValue;
+
+    @action
+    editMetadataKey = (value: string): void => {
+        // TODO: check that metadata doesnt already exist in group
+        let groupMdKeys = new Array(...LinkManager.Instance.allGroups.get(this.props.groupType)!);
+        if (groupMdKeys) {
+            let index = groupMdKeys.indexOf(this._key);
+            if (index > -1) {
+                groupMdKeys[index] = value;
+            }
+            else {
+                console.log("OLD KEY WAS NOT FOUND", ...groupMdKeys)
+            }
+        }
+
+        this._key = value;
+        LinkManager.Instance.allGroups.set(this.props.groupType, groupMdKeys);
+    }
+
+    @action
+    editMetadataValue = (value: string): void => {
+        this.props.mdDoc[this._key] = value;
+        this._value = value;
+    }
+
+    render() {
+        return (
+            <div className="linkEditor-metadata-row">
+                <input type="text" value={this._key} placeholder="key" onChange={e => this.editMetadataKey(e.target.value)}></input>:
+                <input type="text" value={this._value} placeholder="value" onChange={e => this.editMetadataValue(e.target.value)}></input>
+            </div>
+        )
+    }
+}
+
 
 interface LinkEditorProps {
     sourceDoc: Doc;
     linkDoc: Doc;
-    // groups: Map<string, Doc>;
-    // metadata: Map<string, Map<string, Doc>>;
     showLinks: () => void;
 }
 
@@ -158,7 +196,6 @@ export class LinkEditor extends React.Component<LinkEditorProps> {
     }
 
     renderGroup(groupId: string, groupDoc: Doc) {
-        console.log("testing", groupDoc["strawberry"], groupDoc["type"]);
         return (
             <div key={groupId} className="linkEditor-group">
                 <div className="linkEditor-group-row">
@@ -168,23 +205,28 @@ export class LinkEditor extends React.Component<LinkEditorProps> {
                     {/* <input key={groupId + "-type"} type="text" value={StrCast(groupDoc.proto!.type)} onChange={e => this.editGroup(groupId, e.target.value)}></input> */}
                 </div>
                 {this.renderMetadata(groupId)}
-                <button onClick={() => this.addMetadata(groupId)}>+</button>
+                {groupDoc["type"] === "*" ? <></> : <button onClick={() => this.addMetadata(StrCast(groupDoc.proto!.type))}>add kvp</button>}
+                <button onClick={this.viewGroupAsTable}>view group as table</button>
+                {/* <button onClick={() => this.addMetadata(StrCast(groupDoc.proto!.type))}>+</button> */}
             </div>
         )
     }
 
+    viewGroupAsTable = (): void => {
+
+    }
+
     @action
     addMetadata = (groupType: string): void => {
         let mdKeys = LinkManager.Instance.allGroups.get(groupType);
         if (mdKeys) {
-            if (mdKeys.indexOf("new key") > -1) {
+            if (mdKeys.indexOf("new key") === -1) {
                 mdKeys.push("new key");
             }
         } else {
             mdKeys = ["new key"];
         }
         LinkManager.Instance.allGroups.set(groupType, mdKeys);
-        console.log("md added", groupType, LinkManager.Instance.allGroups.get(groupType), LinkManager.Instance.allGroups);
 
         // // create new metadata doc
         // let mdDoc = Docs.TextDocument();
@@ -239,6 +281,14 @@ export class LinkEditor extends React.Component<LinkEditorProps> {
     //     // set group and link?
     // }
 
+    @action
+    editMetadataKey(groupId: string, value: string) {
+        let groupDoc = this._groups.get(groupId);
+        if (groupDoc) {
+
+        }
+    }
+
     renderMetadata(groupId: string) {
         let metadata: Array<JSX.Element> = [];
         let groupDoc = this._groups.get(groupId);
@@ -246,15 +296,15 @@ export class LinkEditor extends React.Component<LinkEditorProps> {
             let mdDoc = Cast(groupDoc.proto!.metadata, Doc, new Doc);
             let groupType = StrCast(groupDoc.proto!.type);
             let groupMdKeys = LinkManager.Instance.allGroups.get(groupType);
-            console.log("rendering md", groupType, groupMdKeys, LinkManager.Instance.allGroups);
             if (groupMdKeys) {
-                groupMdKeys.forEach(key => {
+                groupMdKeys.forEach((key, index) => {
                     metadata.push(
-                        <div key={key} className="linkEditor-metadata-row">
-                            <input type="text" value={key} placeholder="key"></input>
-                            :
-                            <input type="text" value={(mdDoc[key] === undefined) ? "" : StrCast(mdDoc[key])} placeholder="value"></input>
-                        </div>
+                        <LinkMetadataEditor key={"mded-" + index} groupType={groupType} mdDoc={mdDoc} mdKey={key} mdValue={(mdDoc[key] === undefined) ? "" : StrCast(mdDoc[key])} />
+                        // <div key={key} className="linkEditor-metadata-row">
+                        //     <input type="text" value={key} placeholder="key"></input>
+                        //     :
+                        //     <input type="text" value={(mdDoc[key] === undefined) ? "" : StrCast(mdDoc[key])} placeholder="value"></input>
+                        // </div>
                     )
                 })
             }
diff --git a/src/client/views/nodes/LinkMenu.tsx b/src/client/views/nodes/LinkMenu.tsx
index e378ee1cb..21b5807ae 100644
--- a/src/client/views/nodes/LinkMenu.tsx
+++ b/src/client/views/nodes/LinkMenu.tsx
@@ -38,7 +38,6 @@ export class LinkMenu extends React.Component<Props> {
             let destination = LinkUtils.findOppositeAnchor(link, source);
             let doc = FieldValue(Cast(destination, Doc));
             if (doc) {
-                console.log(doc[Id] + source[Id], "source is", source[Id]);
                 return <LinkBox key={doc[Id] + source[Id]} linkDoc={link} linkName={StrCast(destination.title)} pairedDoc={doc} showEditor={action(() => this._editingLink = link)} type={""} />;
             }
         });
-- 
cgit v1.2.3-70-g09d2


From e1fd270f1806ffd51174c835b335ceb4ebb2fe56 Mon Sep 17 00:00:00 2001
From: Fawn <fangrui_tong@brown.edu>
Date: Wed, 12 Jun 2019 16:20:46 -0400
Subject: created remove handlers for link metadata and groups

---
 src/client/documents/Documents.ts      |  14 +-
 src/client/util/LinkManager.ts         | 110 ++++++++++-----
 src/client/views/nodes/LinkBox.tsx     |  38 ++---
 src/client/views/nodes/LinkEditor.scss |  32 ++++-
 src/client/views/nodes/LinkEditor.tsx  | 250 +++++++++++++++------------------
 src/client/views/nodes/LinkMenu.scss   |   2 +-
 src/client/views/nodes/LinkMenu.tsx    |  33 +----
 7 files changed, 251 insertions(+), 228 deletions(-)

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

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 9f1501265..9517cbbda 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -89,9 +89,13 @@ export namespace DocUtils {
         // let protoSrc = source.proto ? source.proto : source;
         // let protoTarg = target.proto ? target.proto : target;
         UndoManager.RunInBatch(() => {
-            let groupDoc = Docs.TextDocument();
-            groupDoc.proto!.type = "*";
-            groupDoc.proto!.metadata = Docs.TextDocument();
+            // let groupDoc1 = Docs.TextDocument();
+            // groupDoc1.proto!.type = "*";
+            // groupDoc1.proto!.metadata = Docs.TextDocument();
+
+            // let groupDoc2 = Docs.TextDocument();
+            // groupDoc2.proto!.type = "*";
+            // groupDoc2.proto!.metadata = Docs.TextDocument();
 
             let linkDoc = Docs.TextDocument({ width: 100, height: 30, borderRounding: -1 });
             //let linkDoc = new Doc;
@@ -99,11 +103,11 @@ export namespace DocUtils {
             // linkDoc.proto!.linkDescription = "";
             linkDoc.proto!.anchor1 = source;
             linkDoc.proto!.anchor1Page = source.curPage;
-            linkDoc.proto!.anchor1Groups = new List<Doc>([groupDoc]);
+            linkDoc.proto!.anchor1Groups = new List<Doc>([]);
 
             linkDoc.proto!.anchor2 = target;
             linkDoc.proto!.anchor2Page = target.curPage;
-            linkDoc.proto!.anchor2Groups = new List<Doc>([groupDoc]);
+            linkDoc.proto!.anchor2Groups = new List<Doc>([]);
 
             // linkDoc.proto!.linkedTo = target;
             // linkDoc.proto!.linkedToPage = target.curPage;
diff --git a/src/client/util/LinkManager.ts b/src/client/util/LinkManager.ts
index 99fb4c6c0..cc8617052 100644
--- a/src/client/util/LinkManager.ts
+++ b/src/client/util/LinkManager.ts
@@ -9,6 +9,7 @@ import { StrCast, Cast } from "../../new_fields/Types";
 import { Doc } from "../../new_fields/Doc";
 import { listSpec } from "../../new_fields/Schema";
 import { List } from "../../new_fields/List";
+import { string } from "prop-types";
 
 export namespace LinkUtils {
     export function findOppositeAnchor(link: Doc, anchor: Doc): Doc {
@@ -40,14 +41,48 @@ export namespace LinkUtils {
     // }
 
     export function setAnchorGroups(link: Doc, anchor: Doc, groups: Doc[]) {
+        // console.log("setting groups for anchor", anchor["title"]);
         if (Doc.AreProtosEqual(anchor, Cast(link.anchor1, Doc, new Doc))) {
             link.anchor1Groups = new List<Doc>(groups);
         } else {
             link.anchor2Groups = new List<Doc>(groups);
         }
     }
+
+    export function removeGroupFromAnchor(link: Doc, anchor: Doc, groupType: string) {
+        let groups = [];
+        if (Doc.AreProtosEqual(anchor, Cast(link.anchor1, Doc, new Doc))) {
+            groups = Cast(link["anchor1Groups"], listSpec(Doc), []);
+        } else {
+            groups = Cast(link["anchor2Groups"], listSpec(Doc), []);
+        }
+
+        let newGroups: Doc[] = [];
+        groups.forEach(groupDoc => {
+            if (groupDoc instanceof Doc && StrCast(groupDoc["type"]) !== groupType) {
+                newGroups.push(groupDoc);
+            }
+        })
+        LinkUtils.setAnchorGroups(link, anchor, newGroups);
+    }
 }
 
+/* 
+ * link doc: 
+ * - anchor1: doc
+ * - anchor1page: number
+ * - anchor1groups: list of group docs representing the groups anchor1 categorizes this link/anchor2 in 
+ * - anchor2: doc
+ * - anchor2page: number
+ * - anchor2groups: list of group docs representing the groups anchor2 categorizes this link/anchor1 in 
+ * 
+ * group doc:
+ * - type: string representing the group type/name/category
+ * - metadata: doc representing the metadata kvps
+ * 
+ * metadata doc:
+ * - user defined kvps 
+ */
 export class LinkManager {
     private static _instance: LinkManager;
     public static get Instance(): LinkManager {
@@ -56,60 +91,57 @@ export class LinkManager {
     private constructor() {
     }
 
-    @observable public allLinks: Array<Doc> = [];
-    @observable public allGroups: Map<string, Array<string>> = new Map();
+    @observable public allLinks: Array<Doc> = []; // list of link docs
+    @observable public allGroups: Map<string, Array<string>> = new Map(); // map of group type to list of its metadata keys
 
     public findAllRelatedLinks(anchor: Doc): Array<Doc> {
         return LinkManager.Instance.allLinks.filter(
             link => Doc.AreProtosEqual(anchor, Cast(link.anchor1, Doc, new Doc)) || Doc.AreProtosEqual(anchor, Cast(link.anchor2, Doc, new Doc)));
     }
 
+    // returns map of group type to anchor's links in that group type
     public findRelatedGroupedLinks(anchor: Doc): Map<string, Array<Doc>> {
         let related = this.findAllRelatedLinks(anchor);
 
-        let anchorGroups = new Map();
+        let anchorGroups = new Map<string, Array<Doc>>();
         related.forEach(link => {
-            // get groups of given doc
-            let oppGroups = (Doc.AreProtosEqual(anchor, Cast(link.anchor1, Doc, new Doc))) ? Cast(link.anchor1Groups, listSpec(Doc), []) : Cast(link.anchor2Groups, listSpec(Doc), []);
-            if (oppGroups) {
-                if (oppGroups.length > 0) {
-                    oppGroups.forEach(groupDoc => {
-                        if (groupDoc instanceof Doc) {
-                            let groupType = StrCast(groupDoc.proto!.type);
-                            let group = anchorGroups.get(groupType); // TODO: clean this up lol
-                            if (group) group.push(link);
-                            else group = [link];
-                            anchorGroups.set(groupType, group);
-                        } else {
-                            // promise doc
-                        }
-
-                    })
-                }
-                else {
-                    // if link is in no groups then put it in default group
-                    let group = anchorGroups.get("*");
-                    if (group) group.push(link);
-                    else group = [link];
-                    anchorGroups.set("*", group);
-                }
-            }
 
+            // get groups of given anchor categorizes this link/opposite anchor in
+            let groups = (Doc.AreProtosEqual(anchor, Cast(link.anchor1, Doc, new Doc))) ? Cast(link.anchor1Groups, listSpec(Doc), []) : Cast(link.anchor2Groups, listSpec(Doc), []);
+            if (groups.length > 0) {
+                groups.forEach(groupDoc => {
+                    if (groupDoc instanceof Doc) {
+                        let groupType = StrCast(groupDoc["type"]);
+                        let group = anchorGroups.get(groupType); // TODO: clean this up lol
+                        if (group) group.push(link);
+                        else group = [link];
+                        anchorGroups.set(groupType, group);
+                    } else {
+                        // promise doc
+                    }
+
+                })
+            }
+            else {
+                // if link is in no groups then put it in default group
+                let group = anchorGroups.get("*");
+                if (group) group.push(link);
+                else group = [link];
+                anchorGroups.set("*", group);
+            }
 
-            //     let anchor = this.findOppositeAnchor(link, source);
-            //     let group = categories.get(link.linkTags);
-            //     if (group) group.push(link);
-            //     else group = [link];
-            //     categories.set(link.linkTags, group);
         })
         return anchorGroups;
     }
 
-
-
-
-    // public findAnchorTags(link: Doc, source: Doc): Doc[] {
-    //     if (source)
-    // }
+    public deleteGroup(groupType: string) {
+        let deleted = LinkManager.Instance.allGroups.delete(groupType);
+        if (deleted) {
+            LinkManager.Instance.allLinks.forEach(linkDoc => {
+                LinkUtils.removeGroupFromAnchor(linkDoc, Cast(linkDoc["anchor1"], Doc, new Doc), groupType);
+                LinkUtils.removeGroupFromAnchor(linkDoc, Cast(linkDoc["anchor2"], Doc, new Doc), groupType);
+            })
+        }
+    }
 
 }
\ No newline at end of file
diff --git a/src/client/views/nodes/LinkBox.tsx b/src/client/views/nodes/LinkBox.tsx
index ebca1dc69..5597bb1aa 100644
--- a/src/client/views/nodes/LinkBox.tsx
+++ b/src/client/views/nodes/LinkBox.tsx
@@ -41,23 +41,23 @@ export class LinkBox extends React.Component<Props> {
         this.props.showEditor();
     }
 
-    @action
-    onDeleteButtonPressed = async (e: React.PointerEvent): Promise<void> => {
-        e.stopPropagation();
-        const [linkedFrom, linkedTo] = await Promise.all([Cast(this.props.linkDoc.linkedFrom, Doc), Cast(this.props.linkDoc.linkedTo, Doc)]);
-        if (linkedFrom) {
-            const linkedToDocs = Cast(linkedFrom.linkedToDocs, listSpec(Doc));
-            if (linkedToDocs) {
-                linkedToDocs.splice(linkedToDocs.indexOf(this.props.linkDoc), 1);
-            }
-        }
-        if (linkedTo) {
-            const linkedFromDocs = Cast(linkedTo.linkedFromDocs, listSpec(Doc));
-            if (linkedFromDocs) {
-                linkedFromDocs.splice(linkedFromDocs.indexOf(this.props.linkDoc), 1);
-            }
-        }
-    }
+    // @action
+    // onDeleteButtonPressed = async (e: React.PointerEvent): Promise<void> => {
+    //     e.stopPropagation();
+    //     const [linkedFrom, linkedTo] = await Promise.all([Cast(this.props.linkDoc.linkedFrom, Doc), Cast(this.props.linkDoc.linkedTo, Doc)]);
+    //     if (linkedFrom) {
+    //         const linkedToDocs = Cast(linkedFrom.linkedToDocs, listSpec(Doc));
+    //         if (linkedToDocs) {
+    //             linkedToDocs.splice(linkedToDocs.indexOf(this.props.linkDoc), 1);
+    //         }
+    //     }
+    //     if (linkedTo) {
+    //         const linkedFromDocs = Cast(linkedTo.linkedFromDocs, listSpec(Doc));
+    //         if (linkedFromDocs) {
+    //             linkedFromDocs.splice(linkedFromDocs.indexOf(this.props.linkDoc), 1);
+    //         }
+    //     }
+    // }
 
     render() {
 
@@ -80,8 +80,8 @@ export class LinkBox extends React.Component<Props> {
                         <FontAwesomeIcon className="fa-icon" icon="arrow-right" size="sm" /></div>
                     <div title="Edit Link" className="button" onPointerDown={this.onEditButtonPressed}>
                         <FontAwesomeIcon className="fa-icon" icon="edit" size="sm" /></div>
-                    <div title="Delete Link" className="button" onPointerDown={this.onDeleteButtonPressed}>
-                        <FontAwesomeIcon className="fa-icon" icon="times" size="sm" /></div>
+                    {/* <div title="Delete Link" className="button" onPointerDown={this.onDeleteButtonPressed}>
+                        <FontAwesomeIcon className="fa-icon" icon="times" size="sm" /></div> */}
                 </div>
             </div>
         );
diff --git a/src/client/views/nodes/LinkEditor.scss b/src/client/views/nodes/LinkEditor.scss
index d3ac8cf19..b56f3046a 100644
--- a/src/client/views/nodes/LinkEditor.scss
+++ b/src/client/views/nodes/LinkEditor.scss
@@ -74,6 +74,7 @@
     }
     .linkEditor-group-row {
         display: flex;
+        margin-bottom: 6px;
         .linkEditor-group-row-label {
             margin-right: 6px;
         }
@@ -81,8 +82,17 @@
     .linkEditor-metadata-row {
         display: flex;
         justify-content: space-between;
+        margin-bottom: 6px;
         input {
-            width: calc(50% - 2px);
+            width: calc(50% - 18px);
+            height: 20px;
+        }
+        button {
+            width: 20px;
+            height: 20px;
+            margin-left: 6px;
+            padding: 0;
+            font-size: 14px;
         }
     }
 }
@@ -90,6 +100,7 @@
 .linkEditor-dropdown {
     width: 100%;
     position: relative;
+    z-index: 999;
     .linkEditor-options-wrapper {
         width: 100%;
         position: absolute;
@@ -107,4 +118,23 @@
             background-color:  $light-color-secondary;
         }
     }
+}
+
+.linkEditor-group-buttons {
+    height: 20px;
+    display: flex;
+    justify-content: space-between;
+    button {
+        width: 31%;
+        height: 20px;
+        margin-left: 6px;
+        padding: 0;
+        font-size: 10px;
+        &:first-child { // delete
+            font-size: 14px;
+        }
+        &:disabled { // delete
+            background-color: gray;
+        }
+    }
 }
\ No newline at end of file
diff --git a/src/client/views/nodes/LinkEditor.tsx b/src/client/views/nodes/LinkEditor.tsx
index 8d12bc30f..05e05c2ee 100644
--- a/src/client/views/nodes/LinkEditor.tsx
+++ b/src/client/views/nodes/LinkEditor.tsx
@@ -1,11 +1,7 @@
 import { observable, computed, action } from "mobx";
 import React = require("react");
-import { SelectionManager } from "../../util/SelectionManager";
 import { observer } from "mobx-react";
 import './LinkEditor.scss';
-import { props } from "bluebird";
-import { DocumentView } from "./DocumentView";
-import { link } from "fs";
 import { StrCast, Cast } from "../../../new_fields/Types";
 import { Doc } from "../../../new_fields/Doc";
 import { List } from "../../../new_fields/List";
@@ -21,7 +17,7 @@ import { string } from "prop-types";
 library.add(faArrowLeft);
 library.add(faEllipsisV);
 
-// this dropdown could be generalizeds
+// this dropdown could be generalized
 @observer
 class LinkGroupsDropdown extends React.Component<{ groupId: string, groupType: string, setGroup: (groupId: string, group: string) => void }> {
     @observable private _searchTerm: string = "";
@@ -63,7 +59,7 @@ class LinkGroupsDropdown extends React.Component<{ groupId: string, groupType: s
 
         if (!exactFound && this._searchTerm !== "") {
             options.push(<div key={""} className="linkEditor-option"
-                onClick={() => { this.createGroup(this._searchTerm); this.setGroupType(this._searchTerm); this.setSearchTerm("") }}>Create new "{this._searchTerm}" group</div>)
+                onClick={() => { this.createGroup(this._searchTerm); this.setGroupType(this._searchTerm); this.setSearchTerm("") }}>Define new "{this._searchTerm}" relationship</div>)
         }
 
         return options;
@@ -82,6 +78,8 @@ class LinkGroupsDropdown extends React.Component<{ groupId: string, groupType: s
     }
 }
 
+
+
 @observer
 class LinkMetadataEditor extends React.Component<{ groupType: string, mdDoc: Doc, mdKey: string, mdValue: string }> {
     @observable private _key: string = this.props.mdKey;
@@ -111,11 +109,28 @@ class LinkMetadataEditor extends React.Component<{ groupType: string, mdDoc: Doc
         this._value = value;
     }
 
+    @action
+    removeMetadata = (): void => {
+        let groupMdKeys = new Array(...LinkManager.Instance.allGroups.get(this.props.groupType)!);
+        if (groupMdKeys) {
+            let index = groupMdKeys.indexOf(this._key);
+            if (index > -1) {
+                groupMdKeys.splice(index, 1);
+            }
+            else {
+                console.log("OLD KEY WAS NOT FOUND", ...groupMdKeys)
+            }
+        }
+        this._key = "";
+        LinkManager.Instance.allGroups.set(this.props.groupType, groupMdKeys);
+    }
+
     render() {
         return (
             <div className="linkEditor-metadata-row">
                 <input type="text" value={this._key} placeholder="key" onChange={e => this.editMetadataKey(e.target.value)}></input>:
                 <input type="text" value={this._value} placeholder="value" onChange={e => this.editMetadataValue(e.target.value)}></input>
+                <button onClick={() => this.removeMetadata()}>X</button>
             </div>
         )
     }
@@ -131,85 +146,129 @@ interface LinkEditorProps {
 @observer
 export class LinkEditor extends React.Component<LinkEditorProps> {
 
-    @observable private _groups: Map<string, Doc> = new Map();
-    @observable private _metadata: Map<string, Map<string, Doc>> = new Map();
+    @observable private _groups: Map<string, Doc> = new Map(); // map of temp group id to the corresponding group doc
 
     constructor(props: LinkEditorProps) {
         super(props);
 
         let groups = new Map<string, Doc>();
-        let metadata: Map<string, Map<string, Doc>> = new Map();
         let groupList = (Doc.AreProtosEqual(props.sourceDoc, Cast(props.linkDoc.anchor1, Doc, new Doc))) ?
             Cast(props.linkDoc.anchor1Groups, listSpec(Doc), []) : Cast(props.linkDoc.anchor2Groups, listSpec(Doc), []);
         groupList.forEach(groupDoc => {
             if (groupDoc instanceof Doc) {
                 let id = Utils.GenerateGuid();
                 groups.set(id, groupDoc);
-
-                let metadataMap = new Map<string, Doc>();
-                let metadataDocs = Cast(groupDoc.proto!.metadata, listSpec(Doc), []);
-                metadataDocs.forEach(mdDoc => {
-                    if (mdDoc && mdDoc instanceof Doc) { // TODO: handle promise doc
-                        metadataMap.set(Utils.GenerateGuid(), mdDoc);
-                    }
-                })
-                metadata.set(id, metadataMap);
             } else {
                 // promise doc
             }
         })
         this._groups = groups;
-        this._metadata = metadata;
     }
 
-    // @action
-    // editGroup(groupId: string, value: string) {
-    //     let linkDoc = this.props.linkDoc.proto ? this.props.linkDoc.proto : this.props.linkDoc;
-    //     let groupDoc = this._groups.get(groupId);
-    //     if (groupDoc) {
-    //         groupDoc.proto!.type = value;
-    //         LinkUtils.setAnchorGroups(linkDoc, this.props.sourceDoc, [groupDoc]);
-    //     }
-    // }
-
     @action
-    addGroup = (e: React.MouseEvent): void => {
-        // create new document for group
-        let groupDoc = Docs.TextDocument();
-        groupDoc.proto!.title = "";
-        groupDoc.proto!.metadata = new List<Doc>([]);
+    addGroup = (): void => {
+        console.log("before adding", ...Array.from(this._groups.keys()));
+
+        let index = Array.from(this._groups.values()).findIndex(groupDoc => {
+            return groupDoc["type"] === "New Group";
+        })
+        if (index === -1) {
+            // create new document for group
+            let groupDoc = Docs.TextDocument();
+            groupDoc.proto!.type = "New Group";
+            groupDoc.proto!.metadata = Docs.TextDocument();
 
-        this._groups.set(Utils.GenerateGuid(), groupDoc);
+            this._groups.set(Utils.GenerateGuid(), groupDoc);
 
-        let linkDoc = this.props.linkDoc.proto ? this.props.linkDoc.proto : this.props.linkDoc;
-        LinkUtils.setAnchorGroups(linkDoc, this.props.sourceDoc, Array.from(this._groups.values()));
+            let linkDoc = this.props.linkDoc.proto ? this.props.linkDoc.proto : this.props.linkDoc;
+            LinkUtils.setAnchorGroups(linkDoc, this.props.sourceDoc, Array.from(this._groups.values()));
+        }
+
+
+        // console.log("set anchor groups for", this.props.sourceDoc["title"]);
+        console.log("after adding", ...Array.from(this._groups.keys()));
     }
 
     @action
-    setGroup = (groupId: string, group: string): void => {
+    setGroupType = (groupId: string, groupType: string): void => {
+        console.log("setting for ", groupId);
         let linkDoc = this.props.linkDoc.proto ? this.props.linkDoc.proto : this.props.linkDoc;
         let groupDoc = this._groups.get(groupId);
         if (groupDoc) {
-            groupDoc.proto!.type = group;
-            LinkUtils.setAnchorGroups(linkDoc, this.props.sourceDoc, [groupDoc]);
+            console.log("found group doc");
+            groupDoc.proto!.type = groupType;
+
+            this._groups.set(groupId, groupDoc);
+
+            let gd = this._groups.get(groupId);
+            if (gd)
+                console.log("just created", StrCast(gd["type"]));
+
+            LinkUtils.setAnchorGroups(linkDoc, this.props.sourceDoc, Array.from(this._groups.values()));
+            console.log("set", Array.from(this._groups.values()).length)
+        }
+
+        let anchor1groups: string[] = [];
+        Cast(this.props.linkDoc.anchor1Groups, listSpec(Doc), []).forEach(doc => {
+            if (doc instanceof Doc) {
+                anchor1groups.push(StrCast(doc.proto!.type));
+            } else {
+                console.log("promise");
+            }
+        })
+        let anchor2groups: string[] = [];
+        Cast(this.props.linkDoc.anchor2Groups, listSpec(Doc), []).forEach(doc => {
+            if (doc instanceof Doc) {
+                anchor2groups.push(StrCast(doc.proto!.type));
+            } else {
+                console.log("promise");
+            }
+        })
+        console.log("groups for anchors; anchor1: [", ...anchor1groups, "] ; anchor2: [", ...anchor2groups, "]")
+    }
+
+    removeGroupFromLink = (groupId: string, groupType: string) => {
+        // this._groups.delete(groupId);
+        let groupDoc = this._groups.get(groupId);
+        if (groupDoc) {
+            LinkUtils.removeGroupFromAnchor(this.props.linkDoc, this.props.sourceDoc, groupType);
+            this._groups.delete(groupId);
+        }
+        // LinkUtils.setAnchorGroups(this.props.linkDoc, this.props.sourceDoc, Array.from(this._groups.values()));
+        console.log("\nremoved", groupId, "remaining", ...Array.from(this._groups.keys()), "\n");
+    }
+
+    deleteGroup = (groupId: string, groupType: string) => {
+        let groupDoc = this._groups.get(groupId);
+        if (groupDoc) {
+            LinkManager.Instance.deleteGroup(groupType);
+            this._groups.delete(groupId);
         }
     }
 
     renderGroup(groupId: string, groupDoc: Doc) {
-        return (
-            <div key={groupId} className="linkEditor-group">
-                <div className="linkEditor-group-row">
-                    <p className="linkEditor-group-row-label">type:</p>
-                    <LinkGroupsDropdown groupId={groupId} groupType={StrCast(groupDoc.proto!.type)} setGroup={this.setGroup} />
-                    {/* <input key={groupId + "-type"} type="text" value={StrCast(groupDoc.proto!.type)} onChange={e => this.editGroup(groupId, e.target.value)}></input> */}
-                    {/* <input key={groupId + "-type"} type="text" value={StrCast(groupDoc.proto!.type)} onChange={e => this.editGroup(groupId, e.target.value)}></input> */}
+        let type = StrCast(groupDoc["type"]);
+        if ((type && LinkManager.Instance.allGroups.get(type)) || type === "New Group") {
+            return (
+                <div key={groupId} className="linkEditor-group">
+                    <div className="linkEditor-group-row">
+                        <p className="linkEditor-group-row-label">type:</p>
+                        <LinkGroupsDropdown groupId={groupId} groupType={StrCast(groupDoc.proto!.type)} setGroup={this.setGroupType} />
+                    </div>
+                    {this.renderMetadata(groupId)}
+                    <div className="linkEditor-group-buttons">
+                        {groupDoc["type"] === "New Group" ? <button disabled={true} title="Add KVP">+</button> :
+                            <button onClick={() => this.addMetadata(StrCast(groupDoc.proto!.type))} title="Add KVP">+</button>}
+                        {/* <button onClick={this.viewGroupAsTable}>view group as table</button> */}
+                        <button onClick={() => this.copyGroup(groupId, type)} title="Copy group to opposite anchor">↔</button>
+                        <button onClick={() => this.removeGroupFromLink(groupId, type)} title="Remove group from link">x</button>
+                        <button onClick={() => this.deleteGroup(groupId, type)} title="Delete group">xx</button>
+                    </div>
                 </div>
-                {this.renderMetadata(groupId)}
-                {groupDoc["type"] === "*" ? <></> : <button onClick={() => this.addMetadata(StrCast(groupDoc.proto!.type))}>add kvp</button>}
-                <button onClick={this.viewGroupAsTable}>view group as table</button>
-                {/* <button onClick={() => this.addMetadata(StrCast(groupDoc.proto!.type))}>+</button> */}
-            </div>
-        )
+            )
+        } else {
+            return <></>
+        }
     }
 
     viewGroupAsTable = (): void => {
@@ -228,65 +287,6 @@ export class LinkEditor extends React.Component<LinkEditorProps> {
         }
         LinkManager.Instance.allGroups.set(groupType, mdKeys);
 
-        // // create new metadata doc
-        // let mdDoc = Docs.TextDocument();
-        // mdDoc.proto!.title = "";
-        // mdDoc.proto!.value = "";
-
-        // // append to map
-        // let mdMap = this._metadata.get(groupId);
-        // if (mdMap) {
-        //     mdMap.set(Utils.GenerateGuid(), mdDoc);
-        // } else {
-        //     mdMap = new Map();
-        //     mdMap.set(Utils.GenerateGuid(), mdDoc);
-        // }
-
-        // // add to internal representation of metadata
-        // this._metadata.set(groupId, mdMap);
-
-        // // add to internatal representation of group
-        // let groupDoc = this._groups.get(groupId);
-        // if (groupDoc) {
-        //     groupDoc.proto!.metadata = new List<Doc>(Array.from(mdMap.values()));
-        //     this._groups.set(groupId, groupDoc);
-        // }
-
-        // // add to link doc
-        // let linkDoc = this.props.linkDoc.proto ? this.props.linkDoc.proto : this.props.linkDoc;
-        // LinkUtils.setAnchorGroups(linkDoc, this.props.sourceDoc, Array.from(this._groups.values()));
-    }
-
-    // @action
-    // editMetadataTitle(groupId: string, mdId: string, value: string) {
-    //     let groupMd = this._metadata.get(groupId);
-    //     if (groupMd) {
-    //         let mdDoc = groupMd.get(mdId);
-    //         if (mdDoc) {
-    //             mdDoc.proto!.title = value;
-    //         }
-    //     }
-    //     // set group and link?
-    // }
-
-    // @action
-    // editMetadataValue(groupId: string, mdId: string, value: string) {
-    //     let groupMd = this._metadata.get(groupId);
-    //     if (groupMd) {
-    //         let mdDoc = groupMd.get(mdId);
-    //         if (mdDoc) {
-    //             mdDoc.proto!.value = value;
-    //         }
-    //     }
-    //     // set group and link?
-    // }
-
-    @action
-    editMetadataKey(groupId: string, value: string) {
-        let groupDoc = this._groups.get(groupId);
-        if (groupDoc) {
-
-        }
     }
 
     renderMetadata(groupId: string) {
@@ -300,30 +300,10 @@ export class LinkEditor extends React.Component<LinkEditorProps> {
                 groupMdKeys.forEach((key, index) => {
                     metadata.push(
                         <LinkMetadataEditor key={"mded-" + index} groupType={groupType} mdDoc={mdDoc} mdKey={key} mdValue={(mdDoc[key] === undefined) ? "" : StrCast(mdDoc[key])} />
-                        // <div key={key} className="linkEditor-metadata-row">
-                        //     <input type="text" value={key} placeholder="key"></input>
-                        //     :
-                        //     <input type="text" value={(mdDoc[key] === undefined) ? "" : StrCast(mdDoc[key])} placeholder="value"></input>
-                        // </div>
                     )
                 })
             }
         }
-
-
-        // let metadataMap = this._metadata.get(groupId);
-        // if (metadataMap) {
-        //     metadataMap.forEach((mdDoc, mdId) => {
-        //         metadata.push(
-        //             <div key={mdId} className="linkEditor-metadata-row">
-        //                 <input type="text" value={StrCast(mdDoc.proto!.title)} placeholder="key" onChange={e => this.editMetadataTitle(groupId, mdId, e.target.value)}></input>
-        //                 :
-        //                 <input type="text" value={StrCast(mdDoc.proto!.value)} placeholder="value" onChange={e => this.editMetadataValue(groupId, mdId, e.target.value)}></input>
-        //             </div>
-        //         )
-        //     })
-        // }
-
         return metadata;
     }
 
@@ -340,10 +320,10 @@ export class LinkEditor extends React.Component<LinkEditorProps> {
                 <button className="linkEditor-back" onPointerDown={() => this.props.showLinks()}><FontAwesomeIcon icon="arrow-left" size="sm" /></button>
                 <p className="linkEditor-linkedTo">editing link to: <b>{destination.proto!.title}</b></p>
                 <div className="linkEditor-groupsLabel">
-                    <b>Groups:</b>
-                    <button onClick={this.addGroup} title="Add Group">+</button>
+                    <b>Relationships:</b>
+                    <button onClick={() => this.addGroup()} title="Add Group">+</button>
                 </div>
-                {groups}
+                {groups.length > 0 ? groups : <div className="linkEditor-group">There are currently no relationships associated with this link.</div>}
             </div>
 
         );
diff --git a/src/client/views/nodes/LinkMenu.scss b/src/client/views/nodes/LinkMenu.scss
index 860f31d8a..7e031b897 100644
--- a/src/client/views/nodes/LinkMenu.scss
+++ b/src/client/views/nodes/LinkMenu.scss
@@ -40,7 +40,7 @@
             display: flex;
         }
         &:hover .link-menu-item-content {
-            width: calc(100% - 72px);
+            width: calc(100% - 42px);
         }
     }
     .link-menu-item-buttons {
diff --git a/src/client/views/nodes/LinkMenu.tsx b/src/client/views/nodes/LinkMenu.tsx
index 21b5807ae..47bd6c3eb 100644
--- a/src/client/views/nodes/LinkMenu.tsx
+++ b/src/client/views/nodes/LinkMenu.tsx
@@ -32,7 +32,7 @@ export class LinkMenu extends React.Component<Props> {
     //     });
     // }
 
-    renderLinkGroupItems(links: Doc[]) {
+    renderGroup(links: Doc[]) {
         let source = this.props.docView.Document;
         return links.map(link => {
             let destination = LinkUtils.findOppositeAnchor(link, source);
@@ -43,7 +43,7 @@ export class LinkMenu extends React.Component<Props> {
         });
     }
 
-    renderLinkItems = (links: Map<string, Array<Doc>>): Array<JSX.Element> => {
+    renderLinks = (links: Map<string, Array<Doc>>): Array<JSX.Element> => {
         let linkItems: Array<JSX.Element> = [];
 
         links.forEach((links, group) => {
@@ -51,7 +51,7 @@ export class LinkMenu extends React.Component<Props> {
                 <div key={group} className="link-menu-group">
                     <p className="link-menu-group-name">{group}:</p>
                     <div className="link-menu-group-wrapper">
-                        {this.renderLinkGroupItems(links)}
+                        {this.renderGroup(links)}
                     </div>
                 </div>
             )
@@ -65,10 +65,7 @@ export class LinkMenu extends React.Component<Props> {
     }
 
     render() {
-        //get list of links from document
-        // let linkFrom = DocListCast(this.props.docView.props.Document.linkedFromDocs);
-        // let linkTo = DocListCast(this.props.docView.props.Document.linkedToDocs);
-        let related = LinkManager.Instance.findRelatedGroupedLinks(this.props.docView.props.Document);
+        let related: Map<string, Doc[]> = LinkManager.Instance.findRelatedGroupedLinks(this.props.docView.props.Document);
         if (this._editingLink === undefined) {
             return (
                 <div id="linkMenu-container">
@@ -76,31 +73,11 @@ export class LinkMenu extends React.Component<Props> {
                     <div id="linkMenu-list">
                         {/* {this.renderLinkItems(linkTo, "linkedTo", "Destination: ")}
                         {this.renderLinkItems(linkFrom, "linkedFrom", "Source: ")} */}
-                        {this.renderLinkItems(related)}
+                        {this.renderLinks(related)}
                     </div>
                 </div>
             );
         } else {
-            // let groups = new Map<string, Doc>();
-            // let metadata: Map<string, Map<string, Doc>> = new Map();
-            // let groupList = (Doc.AreProtosEqual(this.props.docView.props.Document, Cast(this._editingLink.anchor1, Doc, new Doc))) ?
-            //     Cast(this._editingLink.anchor1Groups, listSpec(Doc), []) : Cast(this._editingLink.anchor2Groups, listSpec(Doc), []);
-            // groupList.forEach(groupDoc => {
-            //     if (groupDoc instanceof Doc) {
-            //         let id = Utils.GenerateGuid();
-            //         groups.set(id, groupDoc);
-
-            //         let metadataMap = new Map<string, Doc>();
-            //         let metadataDocs = Cast(groupDoc.proto!.metadata, listSpec(Doc), []);
-            //         metadataDocs.forEach(mdDoc => {
-            //             if (mdDoc && mdDoc instanceof Doc) { // TODO: handle promise doc
-            //                 metadataMap.set(Utils.GenerateGuid(), mdDoc);
-            //             }
-            //         })
-            //         metadata.set(id, metadataMap);
-            //     }
-            // })
-
             return (
                 <LinkEditor sourceDoc={this.props.docView.props.Document} linkDoc={this._editingLink} showLinks={action(() => this._editingLink = undefined)}></LinkEditor>
             );
-- 
cgit v1.2.3-70-g09d2


From 2843baddfa1fe4f46762c63b732eedb5ea7c6e41 Mon Sep 17 00:00:00 2001
From: Fawn <fangrui_tong@brown.edu>
Date: Wed, 12 Jun 2019 18:23:22 -0400
Subject: link group can be viewed as a table

---
 src/client/views/nodes/LinkEditor.tsx | 69 ++++++++++++++++++++++++++++++++---
 1 file changed, 64 insertions(+), 5 deletions(-)

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

diff --git a/src/client/views/nodes/LinkEditor.tsx b/src/client/views/nodes/LinkEditor.tsx
index 05e05c2ee..3886687f1 100644
--- a/src/client/views/nodes/LinkEditor.tsx
+++ b/src/client/views/nodes/LinkEditor.tsx
@@ -13,6 +13,7 @@ import { faArrowLeft, faEllipsisV } from '@fortawesome/free-solid-svg-icons';
 import { library } from "@fortawesome/fontawesome-svg-core";
 import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
 import { string } from "prop-types";
+import { SetupDrag } from "../../util/DragManager";
 
 library.add(faArrowLeft);
 library.add(faEllipsisV);
@@ -176,7 +177,7 @@ export class LinkEditor extends React.Component<LinkEditorProps> {
             // create new document for group
             let groupDoc = Docs.TextDocument();
             groupDoc.proto!.type = "New Group";
-            groupDoc.proto!.metadata = Docs.TextDocument();
+            // groupDoc.proto!.metadata = Docs.TextDocument();
 
             this._groups.set(Utils.GenerateGuid(), groupDoc);
 
@@ -246,6 +247,55 @@ export class LinkEditor extends React.Component<LinkEditorProps> {
         }
     }
 
+    copyGroup = (groupId: string, groupType: string) => {
+        let oppAnchor = LinkUtils.findOppositeAnchor(this.props.linkDoc, this.props.sourceDoc);
+        let groupList = (Doc.AreProtosEqual(oppAnchor, Cast(this.props.linkDoc.anchor1, Doc, new Doc))) ?
+            Cast(this.props.linkDoc.anchor1Groups, listSpec(Doc), []) : Cast(this.props.linkDoc.anchor2Groups, listSpec(Doc), []);
+        // if group already exists on opposite anchor, copy value
+        let index = groupList.findIndex(groupDoc => {
+            if (groupDoc instanceof Doc) {
+                return StrCast(groupDoc["type"]) === groupType;
+            } else {
+                return false;
+            }
+        })
+        // TODO: clean
+        // if (index > 0) {
+        //     let thisGroupDoc = this._groups.get(groupId);
+        //     let oppGroupDoc = groupList[index];
+        //     let keys = LinkManager.Instance.allGroups.get(groupType);
+        //     if (keys) {
+        //         keys.forEach(key => {
+        //             if (thisGroupDoc && oppGroupDoc instanceof Doc) { // TODO: clean
+        //                 let val = thisGroupDoc[key] === undefined ? "" : StrCast(thisGroupDoc[key]);
+        //                 oppGroupDoc[key] = val;
+        //             }
+        //             //     mdDoc[key] === undefined) ? "" : StrCast(mdDoc[key])
+        //             // oppGroupDoc[key] = thisGroupDoc[key];
+        //         })
+        //     }
+        //     // LinkUtils.setAnchorGroups(this.props.linkDoc, oppAnchor, [oppGroupDoc]);
+        // } else {
+        let thisGroupDoc = this._groups.get(groupId);
+        let newGroupDoc = Docs.TextDocument();
+        newGroupDoc.proto!.type = groupType;
+        let keys = LinkManager.Instance.allGroups.get(groupType);
+        if (keys) {
+            keys.forEach(key => {
+                if (thisGroupDoc) { // TODO: clean
+                    let val = thisGroupDoc[key] === undefined ? "" : StrCast(thisGroupDoc[key]);
+                    newGroupDoc[key] = val;
+                }
+                //     mdDoc[key] === undefined) ? "" : StrCast(mdDoc[key])
+                // oppGroupDoc[key] = thisGroupDoc[key];
+            })
+        }
+        LinkUtils.setAnchorGroups(this.props.linkDoc, oppAnchor, [newGroupDoc]); // TODO: fix to append to list
+        // }
+
+        // else create group on opposite anchor
+    }
+
     renderGroup(groupId: string, groupDoc: Doc) {
         let type = StrCast(groupDoc["type"]);
         if ((type && LinkManager.Instance.allGroups.get(type)) || type === "New Group") {
@@ -259,10 +309,10 @@ export class LinkEditor extends React.Component<LinkEditorProps> {
                     <div className="linkEditor-group-buttons">
                         {groupDoc["type"] === "New Group" ? <button disabled={true} title="Add KVP">+</button> :
                             <button onClick={() => this.addMetadata(StrCast(groupDoc.proto!.type))} title="Add KVP">+</button>}
-                        {/* <button onClick={this.viewGroupAsTable}>view group as table</button> */}
                         <button onClick={() => this.copyGroup(groupId, type)} title="Copy group to opposite anchor">↔</button>
                         <button onClick={() => this.removeGroupFromLink(groupId, type)} title="Remove group from link">x</button>
                         <button onClick={() => this.deleteGroup(groupId, type)} title="Delete group">xx</button>
+                        {this.viewGroupAsTable(groupId, type)}
                     </div>
                 </div>
             )
@@ -271,10 +321,20 @@ export class LinkEditor extends React.Component<LinkEditorProps> {
         }
     }
 
-    viewGroupAsTable = (): void => {
-
+    viewGroupAsTable(groupId: string, groupType: string) {
+        let keys = LinkManager.Instance.allGroups.get(groupType);
+        let groupDoc = this._groups.get(groupId);
+        if (keys && groupDoc) {
+            console.log("keys:", ...keys);
+            let createTable = action(() => Docs.SchemaDocument(keys!, [Cast(groupDoc!["metadata"], Doc, new Doc)], { width: 200, height: 200, title: groupType + " table" }));
+            let ref = React.createRef<HTMLDivElement>();
+            return <div ref={ref}><button onPointerDown={SetupDrag(ref, createTable)}>:)</button></div>
+        } else {
+            return <></>
+        }
     }
 
+
     @action
     addMetadata = (groupType: string): void => {
         let mdKeys = LinkManager.Instance.allGroups.get(groupType);
@@ -286,7 +346,6 @@ export class LinkEditor extends React.Component<LinkEditorProps> {
             mdKeys = ["new key"];
         }
         LinkManager.Instance.allGroups.set(groupType, mdKeys);
-
     }
 
     renderMetadata(groupId: string) {
-- 
cgit v1.2.3-70-g09d2


From bd829aa067912baa08c18c09f5dcfcd3853e45ad Mon Sep 17 00:00:00 2001
From: Fawn <fangrui_tong@brown.edu>
Date: Thu, 13 Jun 2019 11:47:50 -0400
Subject: anchors show up in link group table and metadata values get copied on
 transfer

---
 src/client/util/LinkManager.ts        | 27 +++++++++++++++++++++++++++
 src/client/views/nodes/LinkEditor.tsx | 20 +++++++++++++++-----
 2 files changed, 42 insertions(+), 5 deletions(-)

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

diff --git a/src/client/util/LinkManager.ts b/src/client/util/LinkManager.ts
index cc8617052..02ecec88a 100644
--- a/src/client/util/LinkManager.ts
+++ b/src/client/util/LinkManager.ts
@@ -134,6 +134,33 @@ export class LinkManager {
         return anchorGroups;
     }
 
+    public findMetadataInGroup(groupType: string) {
+        let md: Doc[] = [];
+        let allLinks = LinkManager.Instance.allLinks;
+        // for every link find its groups
+        // allLinks.forEach(linkDoc => {
+        //     let anchor1groups = LinkManager.Instance.findRelatedGroupedLinks(Cast(linkDoc["anchor1"], Doc, new Doc));
+        //     if (anchor1groups.get(groupType)) {
+        //         md.push(linkDoc["anchor1"]["group"])
+        //     }
+        // })
+        allLinks.forEach(linkDoc => {
+            let anchor1Groups = Cast(linkDoc["anchor1Groups"], listSpec(Doc), []);
+            let anchor2Groups = Cast(linkDoc["anchor2Groups"], listSpec(Doc), []);
+            [...anchor1Groups, ...anchor2Groups].forEach(groupDoc => {
+                if (groupDoc instanceof Doc) {
+                    if (StrCast(groupDoc["type"]) === groupType) {
+                        md.push(Cast(groupDoc["metadata"], Doc, new Doc));
+                    }
+                } else {
+                    // TODO: promise
+                }
+            })
+
+        })
+        return md;
+    }
+
     public deleteGroup(groupType: string) {
         let deleted = LinkManager.Instance.allGroups.delete(groupType);
         if (deleted) {
diff --git a/src/client/views/nodes/LinkEditor.tsx b/src/client/views/nodes/LinkEditor.tsx
index 3886687f1..8860ac582 100644
--- a/src/client/views/nodes/LinkEditor.tsx
+++ b/src/client/views/nodes/LinkEditor.tsx
@@ -175,9 +175,14 @@ export class LinkEditor extends React.Component<LinkEditorProps> {
         })
         if (index === -1) {
             // create new document for group
+            let mdDoc = Docs.TextDocument();
+            mdDoc.proto!.anchor1 = this.props.sourceDoc["title"];
+            mdDoc.proto!.anchor2 = LinkUtils.findOppositeAnchor(this.props.linkDoc, this.props.sourceDoc)["title"];
+
             let groupDoc = Docs.TextDocument();
             groupDoc.proto!.type = "New Group";
-            // groupDoc.proto!.metadata = Docs.TextDocument();
+            groupDoc.proto!.metadata = mdDoc;
+
 
             this._groups.set(Utils.GenerateGuid(), groupDoc);
 
@@ -277,19 +282,23 @@ export class LinkEditor extends React.Component<LinkEditorProps> {
         //     // LinkUtils.setAnchorGroups(this.props.linkDoc, oppAnchor, [oppGroupDoc]);
         // } else {
         let thisGroupDoc = this._groups.get(groupId);
+        let thisMdDoc = Cast(thisGroupDoc!["metadata"], Doc, new Doc);
         let newGroupDoc = Docs.TextDocument();
-        newGroupDoc.proto!.type = groupType;
+        let newMdDoc = Docs.TextDocument();
         let keys = LinkManager.Instance.allGroups.get(groupType);
         if (keys) {
             keys.forEach(key => {
                 if (thisGroupDoc) { // TODO: clean
-                    let val = thisGroupDoc[key] === undefined ? "" : StrCast(thisGroupDoc[key]);
-                    newGroupDoc[key] = val;
+                    let val = thisMdDoc[key] === undefined ? "" : StrCast(thisMdDoc[key]);
+                    newMdDoc[key] = val;
                 }
                 //     mdDoc[key] === undefined) ? "" : StrCast(mdDoc[key])
                 // oppGroupDoc[key] = thisGroupDoc[key];
             })
         }
+        newGroupDoc.proto!.type = groupType;
+        newGroupDoc.proto!.metadata = newMdDoc;
+
         LinkUtils.setAnchorGroups(this.props.linkDoc, oppAnchor, [newGroupDoc]); // TODO: fix to append to list
         // }
 
@@ -326,7 +335,8 @@ export class LinkEditor extends React.Component<LinkEditorProps> {
         let groupDoc = this._groups.get(groupId);
         if (keys && groupDoc) {
             console.log("keys:", ...keys);
-            let createTable = action(() => Docs.SchemaDocument(keys!, [Cast(groupDoc!["metadata"], Doc, new Doc)], { width: 200, height: 200, title: groupType + " table" }));
+            let docs: Doc[] = LinkManager.Instance.findMetadataInGroup(groupType);
+            let createTable = action(() => Docs.SchemaDocument(["anchor1", "anchor2", ...keys!], docs, { width: 200, height: 200, title: groupType + " table" }));
             let ref = React.createRef<HTMLDivElement>();
             return <div ref={ref}><button onPointerDown={SetupDrag(ref, createTable)}>:)</button></div>
         } else {
-- 
cgit v1.2.3-70-g09d2


From 8c70b822ddd6f2a92d3f3d30cd54c47efad38605 Mon Sep 17 00:00:00 2001
From: Fawn <fangrui_tong@brown.edu>
Date: Thu, 13 Jun 2019 12:05:06 -0400
Subject: updated styling on link editor

---
 .../collectionFreeForm/CollectionFreeFormLinkView.tsx  |  3 ++-
 src/client/views/nodes/LinkEditor.scss                 | 18 +++++++++++++-----
 src/client/views/nodes/LinkEditor.tsx                  | 17 +++++++++--------
 3 files changed, 24 insertions(+), 14 deletions(-)

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

diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
index 301b769af..a1a222675 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
@@ -46,7 +46,8 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo
         let y2 = NumCast(b.y) + (BoolCast(b.isMinimized, false) ? 5 : NumCast(b.height) / NumCast(b.zoomBasis, 1) / 2);
         let text = "";
         this.props.LinkDocs.map(l => text += StrCast(l.title) + "(" + StrCast(l.linkDescription) + "), ");
-        text = text.substr(0, text.length - 2);
+        // text = text.substr(0, text.length - 2);
+        text = "";
         return (
             <>
                 <line key="linkLine" className="collectionfreeformlinkview-linkLine"
diff --git a/src/client/views/nodes/LinkEditor.scss b/src/client/views/nodes/LinkEditor.scss
index b56f3046a..df7bd6086 100644
--- a/src/client/views/nodes/LinkEditor.scss
+++ b/src/client/views/nodes/LinkEditor.scss
@@ -110,12 +110,14 @@
         flex-direction: column;
     }
     .linkEditor-option {
-        background-color: red;
+        background-color: $light-color-secondary;
         border: 1px solid $intermediate-color;
         border-top: 0;
+        padding: 3px;
         cursor: pointer;
         &:hover {
-            background-color:  $light-color-secondary;
+            background-color: $intermediate-color;
+            font-weight: bold;
         }
     }
 }
@@ -124,10 +126,10 @@
     height: 20px;
     display: flex;
     justify-content: space-between;
-    button {
-        width: 31%;
+    .linkEditor-groupOpts {
+        width: calc(33% - 3px);
         height: 20px;
-        margin-left: 6px;
+        // margin-left: 6px;
         padding: 0;
         font-size: 10px;
         &:first-child { // delete
@@ -137,4 +139,10 @@
             background-color: gray;
         }
     }
+    .linkEditor-groupOpts button {
+        width: 100%;
+        height: 20px;
+        font-size: 10px;
+        padding: 0;
+    }
 }
\ No newline at end of file
diff --git a/src/client/views/nodes/LinkEditor.tsx b/src/client/views/nodes/LinkEditor.tsx
index 8860ac582..bab87fc12 100644
--- a/src/client/views/nodes/LinkEditor.tsx
+++ b/src/client/views/nodes/LinkEditor.tsx
@@ -9,7 +9,7 @@ import { listSpec } from "../../../new_fields/Schema";
 import { LinkManager, LinkUtils } from "../../util/LinkManager";
 import { Docs } from "../../documents/Documents";
 import { Utils } from "../../../Utils";
-import { faArrowLeft, faEllipsisV } from '@fortawesome/free-solid-svg-icons';
+import { faArrowLeft, faEllipsisV, faTable } from '@fortawesome/free-solid-svg-icons';
 import { library } from "@fortawesome/fontawesome-svg-core";
 import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
 import { string } from "prop-types";
@@ -17,6 +17,7 @@ import { SetupDrag } from "../../util/DragManager";
 
 library.add(faArrowLeft);
 library.add(faEllipsisV);
+library.add(faTable);
 
 // this dropdown could be generalized
 @observer
@@ -316,11 +317,11 @@ export class LinkEditor extends React.Component<LinkEditorProps> {
                     </div>
                     {this.renderMetadata(groupId)}
                     <div className="linkEditor-group-buttons">
-                        {groupDoc["type"] === "New Group" ? <button disabled={true} title="Add KVP">+</button> :
-                            <button onClick={() => this.addMetadata(StrCast(groupDoc.proto!.type))} title="Add KVP">+</button>}
-                        <button onClick={() => this.copyGroup(groupId, type)} title="Copy group to opposite anchor">↔</button>
-                        <button onClick={() => this.removeGroupFromLink(groupId, type)} title="Remove group from link">x</button>
-                        <button onClick={() => this.deleteGroup(groupId, type)} title="Delete group">xx</button>
+                        {groupDoc["type"] === "New Group" ? <button className="linkEditor-groupOpts" disabled={true} title="Add KVP">+</button> :
+                            <button className="linkEditor-groupOpts" onClick={() => this.addMetadata(StrCast(groupDoc.proto!.type))} title="Add KVP">+</button>}
+                        <button className="linkEditor-groupOpts" onClick={() => this.copyGroup(groupId, type)} title="Copy group to opposite anchor">↔</button>
+                        {/* <button className="linkEditor-groupOpts" onClick={() => this.removeGroupFromLink(groupId, type)} title="Remove group from link">x</button>
+                        <button className="linkEditor-groupOpts" onClick={() => this.deleteGroup(groupId, type)} title="Delete group">xx</button> */}
                         {this.viewGroupAsTable(groupId, type)}
                     </div>
                 </div>
@@ -338,9 +339,9 @@ export class LinkEditor extends React.Component<LinkEditorProps> {
             let docs: Doc[] = LinkManager.Instance.findMetadataInGroup(groupType);
             let createTable = action(() => Docs.SchemaDocument(["anchor1", "anchor2", ...keys!], docs, { width: 200, height: 200, title: groupType + " table" }));
             let ref = React.createRef<HTMLDivElement>();
-            return <div ref={ref}><button onPointerDown={SetupDrag(ref, createTable)}>:)</button></div>
+            return <div className="linkEditor-groupOpts" ref={ref}><button onPointerDown={SetupDrag(ref, createTable)}><FontAwesomeIcon icon="table" size="sm" /></button></div>
         } else {
-            return <></>
+            return <button className="linkEditor-groupOpts" disabled><FontAwesomeIcon icon="table" size="sm" /></button>
         }
     }
 
-- 
cgit v1.2.3-70-g09d2


From 15d2dbc6935df7667733c267de07961e51df5f00 Mon Sep 17 00:00:00 2001
From: Fawn <fangrui_tong@brown.edu>
Date: Thu, 13 Jun 2019 12:37:47 -0400
Subject: link table minor fixes

---
 src/client/util/LinkManager.ts        | 11 ++++++++++-
 src/client/views/nodes/LinkEditor.tsx |  2 ++
 2 files changed, 12 insertions(+), 1 deletion(-)

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

diff --git a/src/client/util/LinkManager.ts b/src/client/util/LinkManager.ts
index 02ecec88a..fab0efb4e 100644
--- a/src/client/util/LinkManager.ts
+++ b/src/client/util/LinkManager.ts
@@ -147,7 +147,16 @@ export class LinkManager {
         allLinks.forEach(linkDoc => {
             let anchor1Groups = Cast(linkDoc["anchor1Groups"], listSpec(Doc), []);
             let anchor2Groups = Cast(linkDoc["anchor2Groups"], listSpec(Doc), []);
-            [...anchor1Groups, ...anchor2Groups].forEach(groupDoc => {
+            anchor1Groups.forEach(groupDoc => {
+                if (groupDoc instanceof Doc) {
+                    if (StrCast(groupDoc["type"]) === groupType) {
+                        md.push(Cast(groupDoc["metadata"], Doc, new Doc));
+                    }
+                } else {
+                    // TODO: promise
+                }
+            })
+            anchor2Groups.forEach(groupDoc => {
                 if (groupDoc instanceof Doc) {
                     if (StrCast(groupDoc["type"]) === groupType) {
                         md.push(Cast(groupDoc["metadata"], Doc, new Doc));
diff --git a/src/client/views/nodes/LinkEditor.tsx b/src/client/views/nodes/LinkEditor.tsx
index bab87fc12..370fdee1d 100644
--- a/src/client/views/nodes/LinkEditor.tsx
+++ b/src/client/views/nodes/LinkEditor.tsx
@@ -286,6 +286,8 @@ export class LinkEditor extends React.Component<LinkEditorProps> {
         let thisMdDoc = Cast(thisGroupDoc!["metadata"], Doc, new Doc);
         let newGroupDoc = Docs.TextDocument();
         let newMdDoc = Docs.TextDocument();
+        newMdDoc.proto!.anchor1 = StrCast(thisMdDoc["anchor2"]);
+        newMdDoc.proto!.anchor2 = StrCast(thisMdDoc["anchor1"]);
         let keys = LinkManager.Instance.allGroups.get(groupType);
         if (keys) {
             keys.forEach(key => {
-- 
cgit v1.2.3-70-g09d2


From bd864051d6bbec3f1ac09ab6c66f9bb62e02411b Mon Sep 17 00:00:00 2001
From: Fawn <fangrui_tong@brown.edu>
Date: Thu, 13 Jun 2019 15:41:08 -0400
Subject: fixed lint issues

---
 src/client/util/LinkManager.ts        | 38 ++++++++++----------
 src/client/views/nodes/LinkEditor.tsx | 67 +++++++++++++++++------------------
 2 files changed, 51 insertions(+), 54 deletions(-)

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

diff --git a/src/client/util/LinkManager.ts b/src/client/util/LinkManager.ts
index fab0efb4e..c0c607b01 100644
--- a/src/client/util/LinkManager.ts
+++ b/src/client/util/LinkManager.ts
@@ -52,17 +52,17 @@ export namespace LinkUtils {
     export function removeGroupFromAnchor(link: Doc, anchor: Doc, groupType: string) {
         let groups = [];
         if (Doc.AreProtosEqual(anchor, Cast(link.anchor1, Doc, new Doc))) {
-            groups = Cast(link["anchor1Groups"], listSpec(Doc), []);
+            groups = Cast(link.proto!.anchor1Groups, listSpec(Doc), []);
         } else {
-            groups = Cast(link["anchor2Groups"], listSpec(Doc), []);
+            groups = Cast(link.proto!.anchor2Groups, listSpec(Doc), []);
         }
 
         let newGroups: Doc[] = [];
         groups.forEach(groupDoc => {
-            if (groupDoc instanceof Doc && StrCast(groupDoc["type"]) !== groupType) {
+            if (groupDoc instanceof Doc && StrCast(groupDoc.type) !== groupType) {
                 newGroups.push(groupDoc);
             }
-        })
+        });
         LinkUtils.setAnchorGroups(link, anchor, newGroups);
     }
 }
@@ -111,7 +111,7 @@ export class LinkManager {
             if (groups.length > 0) {
                 groups.forEach(groupDoc => {
                     if (groupDoc instanceof Doc) {
-                        let groupType = StrCast(groupDoc["type"]);
+                        let groupType = StrCast(groupDoc.type);
                         let group = anchorGroups.get(groupType); // TODO: clean this up lol
                         if (group) group.push(link);
                         else group = [link];
@@ -120,7 +120,7 @@ export class LinkManager {
                         // promise doc
                     }
 
-                })
+                });
             }
             else {
                 // if link is in no groups then put it in default group
@@ -130,7 +130,7 @@ export class LinkManager {
                 anchorGroups.set("*", group);
             }
 
-        })
+        });
         return anchorGroups;
     }
 
@@ -145,28 +145,28 @@ export class LinkManager {
         //     }
         // })
         allLinks.forEach(linkDoc => {
-            let anchor1Groups = Cast(linkDoc["anchor1Groups"], listSpec(Doc), []);
-            let anchor2Groups = Cast(linkDoc["anchor2Groups"], listSpec(Doc), []);
+            let anchor1Groups = Cast(linkDoc.anchor1Groups, listSpec(Doc), []);
+            let anchor2Groups = Cast(linkDoc.anchor2Groups, listSpec(Doc), []);
             anchor1Groups.forEach(groupDoc => {
                 if (groupDoc instanceof Doc) {
-                    if (StrCast(groupDoc["type"]) === groupType) {
-                        md.push(Cast(groupDoc["metadata"], Doc, new Doc));
+                    if (StrCast(groupDoc.type) === groupType) {
+                        md.push(Cast(groupDoc.metadata, Doc, new Doc));
                     }
                 } else {
                     // TODO: promise
                 }
-            })
+            });
             anchor2Groups.forEach(groupDoc => {
                 if (groupDoc instanceof Doc) {
-                    if (StrCast(groupDoc["type"]) === groupType) {
-                        md.push(Cast(groupDoc["metadata"], Doc, new Doc));
+                    if (StrCast(groupDoc.type) === groupType) {
+                        md.push(Cast(groupDoc.metadata, Doc, new Doc));
                     }
                 } else {
                     // TODO: promise
                 }
-            })
+            });
 
-        })
+        });
         return md;
     }
 
@@ -174,9 +174,9 @@ export class LinkManager {
         let deleted = LinkManager.Instance.allGroups.delete(groupType);
         if (deleted) {
             LinkManager.Instance.allLinks.forEach(linkDoc => {
-                LinkUtils.removeGroupFromAnchor(linkDoc, Cast(linkDoc["anchor1"], Doc, new Doc), groupType);
-                LinkUtils.removeGroupFromAnchor(linkDoc, Cast(linkDoc["anchor2"], Doc, new Doc), groupType);
-            })
+                LinkUtils.removeGroupFromAnchor(linkDoc, Cast(linkDoc.anchor1, Doc, new Doc), groupType);
+                LinkUtils.removeGroupFromAnchor(linkDoc, Cast(linkDoc.anchor2, Doc, new Doc), groupType);
+            });
         }
     }
 
diff --git a/src/client/views/nodes/LinkEditor.tsx b/src/client/views/nodes/LinkEditor.tsx
index 370fdee1d..386d0cc3e 100644
--- a/src/client/views/nodes/LinkEditor.tsx
+++ b/src/client/views/nodes/LinkEditor.tsx
@@ -56,12 +56,12 @@ class LinkGroupsDropdown extends React.Component<{ groupId: string, groupType: s
         let options = [];
         results.forEach(result => {
             options.push(<div key={result} className="linkEditor-option"
-                onClick={() => { this.props.setGroup(this.props.groupId, result); this.setGroupType(result); this.setSearchTerm("") }}>{result}</div>)
+                onClick={() => { this.props.setGroup(this.props.groupId, result); this.setGroupType(result); this.setSearchTerm(""); }}>{result}</div>);
         });
 
         if (!exactFound && this._searchTerm !== "") {
             options.push(<div key={""} className="linkEditor-option"
-                onClick={() => { this.createGroup(this._searchTerm); this.setGroupType(this._searchTerm); this.setSearchTerm("") }}>Define new "{this._searchTerm}" relationship</div>)
+                onClick={() => { this.createGroup(this._searchTerm); this.setGroupType(this._searchTerm); this.setSearchTerm(""); }}>Define new "{this._searchTerm}" relationship</div>);
         }
 
         return options;
@@ -71,12 +71,12 @@ class LinkGroupsDropdown extends React.Component<{ groupId: string, groupType: s
         return (
             <div className="linkEditor-dropdown">
                 <input type="text" value={this._groupType} placeholder="Search for a group or create a new group"
-                    onChange={e => { this.setSearchTerm(e.target.value); this.setGroupType(e.target.value) }}></input>
+                    onChange={e => { this.setSearchTerm(e.target.value); this.setGroupType(e.target.value); }}></input>
                 <div className="linkEditor-options-wrapper">
                     {this.renderOptions()}
                 </div>
             </div>
-        )
+        );
     }
 }
 
@@ -97,7 +97,7 @@ class LinkMetadataEditor extends React.Component<{ groupType: string, mdDoc: Doc
                 groupMdKeys[index] = value;
             }
             else {
-                console.log("OLD KEY WAS NOT FOUND", ...groupMdKeys)
+                console.log("OLD KEY WAS NOT FOUND", ...groupMdKeys);
             }
         }
 
@@ -120,7 +120,7 @@ class LinkMetadataEditor extends React.Component<{ groupType: string, mdDoc: Doc
                 groupMdKeys.splice(index, 1);
             }
             else {
-                console.log("OLD KEY WAS NOT FOUND", ...groupMdKeys)
+                console.log("OLD KEY WAS NOT FOUND", ...groupMdKeys);
             }
         }
         this._key = "";
@@ -134,7 +134,7 @@ class LinkMetadataEditor extends React.Component<{ groupType: string, mdDoc: Doc
                 <input type="text" value={this._value} placeholder="value" onChange={e => this.editMetadataValue(e.target.value)}></input>
                 <button onClick={() => this.removeMetadata()}>X</button>
             </div>
-        )
+        );
     }
 }
 
@@ -163,7 +163,7 @@ export class LinkEditor extends React.Component<LinkEditorProps> {
             } else {
                 // promise doc
             }
-        })
+        });
         this._groups = groups;
     }
 
@@ -171,20 +171,20 @@ export class LinkEditor extends React.Component<LinkEditorProps> {
     addGroup = (): void => {
         console.log("before adding", ...Array.from(this._groups.keys()));
 
+        // new group only gets added if there is not already a group with type "new group"
         let index = Array.from(this._groups.values()).findIndex(groupDoc => {
-            return groupDoc["type"] === "New Group";
-        })
+            return groupDoc.type === "New Group";
+        });
         if (index === -1) {
             // create new document for group
             let mdDoc = Docs.TextDocument();
-            mdDoc.proto!.anchor1 = this.props.sourceDoc["title"];
-            mdDoc.proto!.anchor2 = LinkUtils.findOppositeAnchor(this.props.linkDoc, this.props.sourceDoc)["title"];
+            mdDoc.proto!.anchor1 = this.props.sourceDoc.title;
+            mdDoc.proto!.anchor2 = LinkUtils.findOppositeAnchor(this.props.linkDoc, this.props.sourceDoc).title;
 
             let groupDoc = Docs.TextDocument();
             groupDoc.proto!.type = "New Group";
             groupDoc.proto!.metadata = mdDoc;
 
-
             this._groups.set(Utils.GenerateGuid(), groupDoc);
 
             let linkDoc = this.props.linkDoc.proto ? this.props.linkDoc.proto : this.props.linkDoc;
@@ -207,12 +207,9 @@ export class LinkEditor extends React.Component<LinkEditorProps> {
 
             this._groups.set(groupId, groupDoc);
 
-            let gd = this._groups.get(groupId);
-            if (gd)
-                console.log("just created", StrCast(gd["type"]));
 
             LinkUtils.setAnchorGroups(linkDoc, this.props.sourceDoc, Array.from(this._groups.values()));
-            console.log("set", Array.from(this._groups.values()).length)
+            console.log("set", Array.from(this._groups.values()).length);
         }
 
         let anchor1groups: string[] = [];
@@ -222,7 +219,7 @@ export class LinkEditor extends React.Component<LinkEditorProps> {
             } else {
                 console.log("promise");
             }
-        })
+        });
         let anchor2groups: string[] = [];
         Cast(this.props.linkDoc.anchor2Groups, listSpec(Doc), []).forEach(doc => {
             if (doc instanceof Doc) {
@@ -230,8 +227,8 @@ export class LinkEditor extends React.Component<LinkEditorProps> {
             } else {
                 console.log("promise");
             }
-        })
-        console.log("groups for anchors; anchor1: [", ...anchor1groups, "] ; anchor2: [", ...anchor2groups, "]")
+        });
+        console.log("groups for anchors; anchor1: [", ...anchor1groups, "] ; anchor2: [", ...anchor2groups, "]");
     }
 
     removeGroupFromLink = (groupId: string, groupType: string) => {
@@ -260,11 +257,11 @@ export class LinkEditor extends React.Component<LinkEditorProps> {
         // if group already exists on opposite anchor, copy value
         let index = groupList.findIndex(groupDoc => {
             if (groupDoc instanceof Doc) {
-                return StrCast(groupDoc["type"]) === groupType;
+                return StrCast(groupDoc.type) === groupType;
             } else {
                 return false;
             }
-        })
+        });
         // TODO: clean
         // if (index > 0) {
         //     let thisGroupDoc = this._groups.get(groupId);
@@ -283,11 +280,11 @@ export class LinkEditor extends React.Component<LinkEditorProps> {
         //     // LinkUtils.setAnchorGroups(this.props.linkDoc, oppAnchor, [oppGroupDoc]);
         // } else {
         let thisGroupDoc = this._groups.get(groupId);
-        let thisMdDoc = Cast(thisGroupDoc!["metadata"], Doc, new Doc);
+        let thisMdDoc = Cast(thisGroupDoc!.metadata, Doc, new Doc);
         let newGroupDoc = Docs.TextDocument();
         let newMdDoc = Docs.TextDocument();
-        newMdDoc.proto!.anchor1 = StrCast(thisMdDoc["anchor2"]);
-        newMdDoc.proto!.anchor2 = StrCast(thisMdDoc["anchor1"]);
+        newMdDoc.proto!.anchor1 = StrCast(thisMdDoc.anchor2);
+        newMdDoc.proto!.anchor2 = StrCast(thisMdDoc.anchor1);
         let keys = LinkManager.Instance.allGroups.get(groupType);
         if (keys) {
             keys.forEach(key => {
@@ -297,7 +294,7 @@ export class LinkEditor extends React.Component<LinkEditorProps> {
                 }
                 //     mdDoc[key] === undefined) ? "" : StrCast(mdDoc[key])
                 // oppGroupDoc[key] = thisGroupDoc[key];
-            })
+            });
         }
         newGroupDoc.proto!.type = groupType;
         newGroupDoc.proto!.metadata = newMdDoc;
@@ -309,7 +306,7 @@ export class LinkEditor extends React.Component<LinkEditorProps> {
     }
 
     renderGroup(groupId: string, groupDoc: Doc) {
-        let type = StrCast(groupDoc["type"]);
+        let type = StrCast(groupDoc.type);
         if ((type && LinkManager.Instance.allGroups.get(type)) || type === "New Group") {
             return (
                 <div key={groupId} className="linkEditor-group">
@@ -319,7 +316,7 @@ export class LinkEditor extends React.Component<LinkEditorProps> {
                     </div>
                     {this.renderMetadata(groupId)}
                     <div className="linkEditor-group-buttons">
-                        {groupDoc["type"] === "New Group" ? <button className="linkEditor-groupOpts" disabled={true} title="Add KVP">+</button> :
+                        {groupDoc.type === "New Group" ? <button className="linkEditor-groupOpts" disabled={true} title="Add KVP">+</button> :
                             <button className="linkEditor-groupOpts" onClick={() => this.addMetadata(StrCast(groupDoc.proto!.type))} title="Add KVP">+</button>}
                         <button className="linkEditor-groupOpts" onClick={() => this.copyGroup(groupId, type)} title="Copy group to opposite anchor">↔</button>
                         {/* <button className="linkEditor-groupOpts" onClick={() => this.removeGroupFromLink(groupId, type)} title="Remove group from link">x</button>
@@ -327,9 +324,9 @@ export class LinkEditor extends React.Component<LinkEditorProps> {
                         {this.viewGroupAsTable(groupId, type)}
                     </div>
                 </div>
-            )
+            );
         } else {
-            return <></>
+            return <></>;
         }
     }
 
@@ -341,9 +338,9 @@ export class LinkEditor extends React.Component<LinkEditorProps> {
             let docs: Doc[] = LinkManager.Instance.findMetadataInGroup(groupType);
             let createTable = action(() => Docs.SchemaDocument(["anchor1", "anchor2", ...keys!], docs, { width: 200, height: 200, title: groupType + " table" }));
             let ref = React.createRef<HTMLDivElement>();
-            return <div className="linkEditor-groupOpts" ref={ref}><button onPointerDown={SetupDrag(ref, createTable)}><FontAwesomeIcon icon="table" size="sm" /></button></div>
+            return <div className="linkEditor-groupOpts" ref={ref}><button onPointerDown={SetupDrag(ref, createTable)}><FontAwesomeIcon icon="table" size="sm" /></button></div>;
         } else {
-            return <button className="linkEditor-groupOpts" disabled><FontAwesomeIcon icon="table" size="sm" /></button>
+            return <button className="linkEditor-groupOpts" disabled><FontAwesomeIcon icon="table" size="sm" /></button>;
         }
     }
 
@@ -372,8 +369,8 @@ export class LinkEditor extends React.Component<LinkEditorProps> {
                 groupMdKeys.forEach((key, index) => {
                     metadata.push(
                         <LinkMetadataEditor key={"mded-" + index} groupType={groupType} mdDoc={mdDoc} mdKey={key} mdValue={(mdDoc[key] === undefined) ? "" : StrCast(mdDoc[key])} />
-                    )
-                })
+                    );
+                });
             }
         }
         return metadata;
@@ -384,7 +381,7 @@ export class LinkEditor extends React.Component<LinkEditorProps> {
 
         let groups: Array<JSX.Element> = [];
         this._groups.forEach((groupDoc, groupId) => {
-            groups.push(this.renderGroup(groupId, groupDoc))
+            groups.push(this.renderGroup(groupId, groupDoc));
         });
 
         return (
-- 
cgit v1.2.3-70-g09d2


From 0d0782362d4549b80c27c3ce5d8439c2f6fa4d7b Mon Sep 17 00:00:00 2001
From: Fawn <fangrui_tong@brown.edu>
Date: Thu, 13 Jun 2019 16:05:26 -0400
Subject: fixed delete handlers for link groups

---
 src/client/util/LinkManager.ts         | 57 +++++++++++++++++-----------------
 src/client/views/nodes/LinkEditor.scss |  2 +-
 src/client/views/nodes/LinkEditor.tsx  | 34 ++++++++++----------
 3 files changed, 46 insertions(+), 47 deletions(-)

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

diff --git a/src/client/util/LinkManager.ts b/src/client/util/LinkManager.ts
index c0c607b01..2f40c77e7 100644
--- a/src/client/util/LinkManager.ts
+++ b/src/client/util/LinkManager.ts
@@ -20,49 +20,48 @@ export namespace LinkUtils {
         }
     }
 
-    // export function getAnchorGroups(link: Doc, anchor: Doc): Doc[] {
-    //     let groups;
-    //     if (Doc.AreProtosEqual(anchor, Cast(link.anchor1, Doc, new Doc))) {
-    //         groups = Cast(link.anchor1Groups, listSpec(Doc), []);
-    //     } else {
-    //         groups = Cast(link.anchor2Groups, listSpec(Doc), []);
-    //     }
-
-    //     if (groups instanceof Doc[]) {
-    //         return groups;
-    //     } else {
-    //         return [];
-    //     }
-    //     // if (Doc.AreProtosEqual(anchor, Cast(link.anchor1, Doc, new Doc))) {
-    //     //     returnCast(link.anchor1Groups, listSpec(Doc), []);
-    //     // } else {
-    //     //     return Cast(link.anchor2Groups, listSpec(Doc), []);
-    //     // }
-    // }
-
     export function setAnchorGroups(link: Doc, anchor: Doc, groups: Doc[]) {
         // console.log("setting groups for anchor", anchor["title"]);
         if (Doc.AreProtosEqual(anchor, Cast(link.anchor1, Doc, new Doc))) {
             link.anchor1Groups = new List<Doc>(groups);
+
+            let print: string[] = [];
+            Cast(link.anchor1Groups, listSpec(Doc), []).forEach(doc => {
+                if (doc instanceof Doc) {
+                    print.push(StrCast(doc.type));
+                }
+            });
+            console.log("set anchor's groups as", print);
         } else {
             link.anchor2Groups = new List<Doc>(groups);
+
+            let print: string[] = [];
+            Cast(link.anchor2Groups, listSpec(Doc), []).forEach(doc => {
+                if (doc instanceof Doc) {
+                    print.push(StrCast(doc.type));
+                }
+            });
+            console.log("set anchor's groups as", print);
         }
     }
 
     export function removeGroupFromAnchor(link: Doc, anchor: Doc, groupType: string) {
-        let groups = [];
-        if (Doc.AreProtosEqual(anchor, Cast(link.anchor1, Doc, new Doc))) {
-            groups = Cast(link.proto!.anchor1Groups, listSpec(Doc), []);
-        } else {
-            groups = Cast(link.proto!.anchor2Groups, listSpec(Doc), []);
-        }
+        let groups = Doc.AreProtosEqual(anchor, Cast(link.anchor1, Doc, new Doc)) ?
+            Cast(link.proto!.anchor1Groups, listSpec(Doc), []) : Cast(link.proto!.anchor2Groups, listSpec(Doc), []);
 
         let newGroups: Doc[] = [];
         groups.forEach(groupDoc => {
             if (groupDoc instanceof Doc && StrCast(groupDoc.type) !== groupType) {
                 newGroups.push(groupDoc);
-            }
+            } // TODO: promise
         });
+
+        // let grouptypes: string[] = [];
+        // newGroups.forEach(groupDoc => {
+        //     grouptypes.push(StrCast(groupDoc.type));
+        // });
+        // console.log("remove anchor's groups as", grouptypes);
+
         LinkUtils.setAnchorGroups(link, anchor, newGroups);
     }
 }
@@ -92,7 +91,7 @@ export class LinkManager {
     }
 
     @observable public allLinks: Array<Doc> = []; // list of link docs
-    @observable public allGroups: Map<string, Array<string>> = new Map(); // map of group type to list of its metadata keys
+    @observable public groupMetadataKeys: Map<string, Array<string>> = new Map(); // map of group type to list of its metadata keys
 
     public findAllRelatedLinks(anchor: Doc): Array<Doc> {
         return LinkManager.Instance.allLinks.filter(
@@ -171,7 +170,7 @@ export class LinkManager {
     }
 
     public deleteGroup(groupType: string) {
-        let deleted = LinkManager.Instance.allGroups.delete(groupType);
+        let deleted = LinkManager.Instance.groupMetadataKeys.delete(groupType);
         if (deleted) {
             LinkManager.Instance.allLinks.forEach(linkDoc => {
                 LinkUtils.removeGroupFromAnchor(linkDoc, Cast(linkDoc.anchor1, Doc, new Doc), groupType);
diff --git a/src/client/views/nodes/LinkEditor.scss b/src/client/views/nodes/LinkEditor.scss
index df7bd6086..01f0cb82a 100644
--- a/src/client/views/nodes/LinkEditor.scss
+++ b/src/client/views/nodes/LinkEditor.scss
@@ -127,7 +127,7 @@
     display: flex;
     justify-content: space-between;
     .linkEditor-groupOpts {
-        width: calc(33% - 3px);
+        width: calc(20% - 3px);
         height: 20px;
         // margin-left: 6px;
         padding: 0;
diff --git a/src/client/views/nodes/LinkEditor.tsx b/src/client/views/nodes/LinkEditor.tsx
index 386d0cc3e..5097d625e 100644
--- a/src/client/views/nodes/LinkEditor.tsx
+++ b/src/client/views/nodes/LinkEditor.tsx
@@ -37,14 +37,14 @@ class LinkGroupsDropdown extends React.Component<{ groupId: string, groupType: s
 
     @action
     createGroup(value: string) {
-        LinkManager.Instance.allGroups.set(value, []);
+        LinkManager.Instance.groupMetadataKeys.set(value, []);
         this.props.setGroup(this.props.groupId, value);
     }
 
     renderOptions = (): JSX.Element[] => {
         let allGroups: string[], searchTerm: string, results: string[], exactFound: boolean;
         if (this._searchTerm !== "") {
-            allGroups = Array.from(LinkManager.Instance.allGroups.keys());
+            allGroups = Array.from(LinkManager.Instance.groupMetadataKeys.keys());
             searchTerm = this._searchTerm.toUpperCase();
             results = allGroups.filter(group => group.toUpperCase().indexOf(searchTerm) > -1);
             exactFound = results.findIndex(group => group.toUpperCase() === searchTerm) > -1;
@@ -90,7 +90,7 @@ class LinkMetadataEditor extends React.Component<{ groupType: string, mdDoc: Doc
     @action
     editMetadataKey = (value: string): void => {
         // TODO: check that metadata doesnt already exist in group
-        let groupMdKeys = new Array(...LinkManager.Instance.allGroups.get(this.props.groupType)!);
+        let groupMdKeys = new Array(...LinkManager.Instance.groupMetadataKeys.get(this.props.groupType)!);
         if (groupMdKeys) {
             let index = groupMdKeys.indexOf(this._key);
             if (index > -1) {
@@ -102,7 +102,7 @@ class LinkMetadataEditor extends React.Component<{ groupType: string, mdDoc: Doc
         }
 
         this._key = value;
-        LinkManager.Instance.allGroups.set(this.props.groupType, groupMdKeys);
+        LinkManager.Instance.groupMetadataKeys.set(this.props.groupType, groupMdKeys);
     }
 
     @action
@@ -113,7 +113,7 @@ class LinkMetadataEditor extends React.Component<{ groupType: string, mdDoc: Doc
 
     @action
     removeMetadata = (): void => {
-        let groupMdKeys = new Array(...LinkManager.Instance.allGroups.get(this.props.groupType)!);
+        let groupMdKeys = new Array(...LinkManager.Instance.groupMetadataKeys.get(this.props.groupType)!);
         if (groupMdKeys) {
             let index = groupMdKeys.indexOf(this._key);
             if (index > -1) {
@@ -124,7 +124,7 @@ class LinkMetadataEditor extends React.Component<{ groupType: string, mdDoc: Doc
             }
         }
         this._key = "";
-        LinkManager.Instance.allGroups.set(this.props.groupType, groupMdKeys);
+        LinkManager.Instance.groupMetadataKeys.set(this.props.groupType, groupMdKeys);
     }
 
     render() {
@@ -132,7 +132,7 @@ class LinkMetadataEditor extends React.Component<{ groupType: string, mdDoc: Doc
             <div className="linkEditor-metadata-row">
                 <input type="text" value={this._key} placeholder="key" onChange={e => this.editMetadataKey(e.target.value)}></input>:
                 <input type="text" value={this._value} placeholder="value" onChange={e => this.editMetadataValue(e.target.value)}></input>
-                <button onClick={() => this.removeMetadata()}>X</button>
+                <button onClick={() => this.removeMetadata()}>x</button>
             </div>
         );
     }
@@ -199,7 +199,7 @@ export class LinkEditor extends React.Component<LinkEditorProps> {
     @action
     setGroupType = (groupId: string, groupType: string): void => {
         console.log("setting for ", groupId);
-        let linkDoc = this.props.linkDoc.proto ? this.props.linkDoc.proto : this.props.linkDoc;
+        // let linkDoc = this.props.linkDoc.proto ? this.props.linkDoc.proto : this.props.linkDoc;
         let groupDoc = this._groups.get(groupId);
         if (groupDoc) {
             console.log("found group doc");
@@ -208,7 +208,7 @@ export class LinkEditor extends React.Component<LinkEditorProps> {
             this._groups.set(groupId, groupDoc);
 
 
-            LinkUtils.setAnchorGroups(linkDoc, this.props.sourceDoc, Array.from(this._groups.values()));
+            LinkUtils.setAnchorGroups(this.props.linkDoc, this.props.sourceDoc, Array.from(this._groups.values()));
             console.log("set", Array.from(this._groups.values()).length);
         }
 
@@ -285,7 +285,7 @@ export class LinkEditor extends React.Component<LinkEditorProps> {
         let newMdDoc = Docs.TextDocument();
         newMdDoc.proto!.anchor1 = StrCast(thisMdDoc.anchor2);
         newMdDoc.proto!.anchor2 = StrCast(thisMdDoc.anchor1);
-        let keys = LinkManager.Instance.allGroups.get(groupType);
+        let keys = LinkManager.Instance.groupMetadataKeys.get(groupType);
         if (keys) {
             keys.forEach(key => {
                 if (thisGroupDoc) { // TODO: clean
@@ -307,7 +307,7 @@ export class LinkEditor extends React.Component<LinkEditorProps> {
 
     renderGroup(groupId: string, groupDoc: Doc) {
         let type = StrCast(groupDoc.type);
-        if ((type && LinkManager.Instance.allGroups.get(type)) || type === "New Group") {
+        if ((type && LinkManager.Instance.groupMetadataKeys.get(type)) || type === "New Group") {
             return (
                 <div key={groupId} className="linkEditor-group">
                     <div className="linkEditor-group-row">
@@ -319,8 +319,8 @@ export class LinkEditor extends React.Component<LinkEditorProps> {
                         {groupDoc.type === "New Group" ? <button className="linkEditor-groupOpts" disabled={true} title="Add KVP">+</button> :
                             <button className="linkEditor-groupOpts" onClick={() => this.addMetadata(StrCast(groupDoc.proto!.type))} title="Add KVP">+</button>}
                         <button className="linkEditor-groupOpts" onClick={() => this.copyGroup(groupId, type)} title="Copy group to opposite anchor">↔</button>
-                        {/* <button className="linkEditor-groupOpts" onClick={() => this.removeGroupFromLink(groupId, type)} title="Remove group from link">x</button>
-                        <button className="linkEditor-groupOpts" onClick={() => this.deleteGroup(groupId, type)} title="Delete group">xx</button> */}
+                        <button className="linkEditor-groupOpts" onClick={() => this.removeGroupFromLink(groupId, type)} title="Remove group from link">x</button>
+                        <button className="linkEditor-groupOpts" onClick={() => this.deleteGroup(groupId, type)} title="Delete group">xx</button>
                         {this.viewGroupAsTable(groupId, type)}
                     </div>
                 </div>
@@ -331,7 +331,7 @@ export class LinkEditor extends React.Component<LinkEditorProps> {
     }
 
     viewGroupAsTable(groupId: string, groupType: string) {
-        let keys = LinkManager.Instance.allGroups.get(groupType);
+        let keys = LinkManager.Instance.groupMetadataKeys.get(groupType);
         let groupDoc = this._groups.get(groupId);
         if (keys && groupDoc) {
             console.log("keys:", ...keys);
@@ -347,7 +347,7 @@ export class LinkEditor extends React.Component<LinkEditorProps> {
 
     @action
     addMetadata = (groupType: string): void => {
-        let mdKeys = LinkManager.Instance.allGroups.get(groupType);
+        let mdKeys = LinkManager.Instance.groupMetadataKeys.get(groupType);
         if (mdKeys) {
             if (mdKeys.indexOf("new key") === -1) {
                 mdKeys.push("new key");
@@ -355,7 +355,7 @@ export class LinkEditor extends React.Component<LinkEditorProps> {
         } else {
             mdKeys = ["new key"];
         }
-        LinkManager.Instance.allGroups.set(groupType, mdKeys);
+        LinkManager.Instance.groupMetadataKeys.set(groupType, mdKeys);
     }
 
     renderMetadata(groupId: string) {
@@ -364,7 +364,7 @@ export class LinkEditor extends React.Component<LinkEditorProps> {
         if (groupDoc) {
             let mdDoc = Cast(groupDoc.proto!.metadata, Doc, new Doc);
             let groupType = StrCast(groupDoc.proto!.type);
-            let groupMdKeys = LinkManager.Instance.allGroups.get(groupType);
+            let groupMdKeys = LinkManager.Instance.groupMetadataKeys.get(groupType);
             if (groupMdKeys) {
                 groupMdKeys.forEach((key, index) => {
                     metadata.push(
-- 
cgit v1.2.3-70-g09d2


From f54496c7aa930e385e77aaf37df8d51db733e3a2 Mon Sep 17 00:00:00 2001
From: Fawn <fangrui_tong@brown.edu>
Date: Fri, 14 Jun 2019 15:24:35 -0400
Subject: cleaned up link code

---
 src/client/util/LinkManager.ts         | 166 ++++++-----------
 src/client/views/nodes/LinkBox.scss    | 122 ++++++-------
 src/client/views/nodes/LinkBox.tsx     |  52 +-----
 src/client/views/nodes/LinkEditor.scss | 106 +++++------
 src/client/views/nodes/LinkEditor.tsx  | 319 +++++++++++++--------------------
 src/client/views/nodes/LinkMenu.scss   |  76 +-------
 src/client/views/nodes/LinkMenu.tsx    |  54 ++----
 7 files changed, 311 insertions(+), 584 deletions(-)

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

diff --git a/src/client/util/LinkManager.ts b/src/client/util/LinkManager.ts
index 929fcbf21..aaed7388d 100644
--- a/src/client/util/LinkManager.ts
+++ b/src/client/util/LinkManager.ts
@@ -1,71 +1,9 @@
-import { observable, computed, action } from "mobx";
-import React = require("react");
-import { SelectionManager } from "./SelectionManager";
-import { observer } from "mobx-react";
-import { props } from "bluebird";
-import { DocumentView } from "../views/nodes/DocumentView";
-import { link } from "fs";
+import { observable } from "mobx";
 import { StrCast, Cast } from "../../new_fields/Types";
-import { Doc } from "../../new_fields/Doc";
+import { Doc, DocListCast } from "../../new_fields/Doc";
 import { listSpec } from "../../new_fields/Schema";
 import { List } from "../../new_fields/List";
-import { string } from "prop-types";
-import { Docs } from "../documents/Documents";
 
-export namespace LinkUtils {
-    export function findOppositeAnchor(link: Doc, anchor: Doc): Doc {
-        if (Doc.AreProtosEqual(anchor, Cast(link.anchor1, Doc, new Doc))) {
-            return Cast(link.anchor2, Doc, new Doc);
-        } else {
-            return Cast(link.anchor1, Doc, new Doc);
-        }
-    }
-
-    export function setAnchorGroups(link: Doc, anchor: Doc, groups: Doc[]) {
-        // console.log("setting groups for anchor", anchor["title"]);
-        if (Doc.AreProtosEqual(anchor, Cast(link.anchor1, Doc, new Doc))) {
-            link.anchor1Groups = new List<Doc>(groups);
-
-            let print: string[] = [];
-            Cast(link.anchor1Groups, listSpec(Doc), []).forEach(doc => {
-                if (doc instanceof Doc) {
-                    print.push(StrCast(doc.type));
-                }
-            });
-            console.log("set anchor's groups as", print);
-        } else {
-            link.anchor2Groups = new List<Doc>(groups);
-
-            let print: string[] = [];
-            Cast(link.anchor2Groups, listSpec(Doc), []).forEach(doc => {
-                if (doc instanceof Doc) {
-                    print.push(StrCast(doc.type));
-                }
-            });
-            console.log("set anchor's groups as", print);
-        }
-    }
-
-    export function removeGroupFromAnchor(link: Doc, anchor: Doc, groupType: string) {
-        let groups = Doc.AreProtosEqual(anchor, Cast(link.anchor1, Doc, new Doc)) ?
-            Cast(link.proto!.anchor1Groups, listSpec(Doc), []) : Cast(link.proto!.anchor2Groups, listSpec(Doc), []);
-
-        let newGroups: Doc[] = [];
-        groups.forEach(groupDoc => {
-            if (groupDoc instanceof Doc && StrCast(groupDoc.type) !== groupType) {
-                newGroups.push(groupDoc);
-            } // TODO: promise
-        });
-
-        // let grouptypes: string[] = [];
-        // newGroups.forEach(groupDoc => {
-        //     grouptypes.push(StrCast(groupDoc.type));
-        // });
-        // console.log("remove anchor's groups as", grouptypes);
-
-        LinkUtils.setAnchorGroups(link, anchor, newGroups);
-    }
-}
 
 /* 
  * link doc: 
@@ -94,6 +32,7 @@ export class LinkManager {
     @observable public allLinks: Array<Doc> = []; // list of link docs
     @observable public groupMetadataKeys: Map<string, Array<string>> = new Map(); // map of group type to list of its metadata keys
 
+    // finds all links that contain the given anchor
     public findAllRelatedLinks(anchor: Doc): Array<Doc> {
         return LinkManager.Instance.allLinks.filter(
             link => Doc.AreProtosEqual(anchor, Cast(link.anchor1, Doc, new Doc)) || Doc.AreProtosEqual(anchor, Cast(link.anchor2, Doc, new Doc)));
@@ -102,27 +41,19 @@ export class LinkManager {
     // returns map of group type to anchor's links in that group type
     public findRelatedGroupedLinks(anchor: Doc): Map<string, Array<Doc>> {
         let related = this.findAllRelatedLinks(anchor);
-
         let anchorGroups = new Map<string, Array<Doc>>();
         related.forEach(link => {
+            let groups = LinkManager.Instance.getAnchorGroups(link, anchor);
 
-            // get groups of given anchor categorizes this link/opposite anchor in
-            let groups = (Doc.AreProtosEqual(anchor, Cast(link.anchor1, Doc, new Doc))) ? Cast(link.anchor1Groups, listSpec(Doc), []) : Cast(link.anchor2Groups, listSpec(Doc), []);
             if (groups.length > 0) {
                 groups.forEach(groupDoc => {
-                    if (groupDoc instanceof Doc) {
-                        let groupType = StrCast(groupDoc.type);
-                        let group = anchorGroups.get(groupType); // TODO: clean this up lol
-                        if (group) group.push(link);
-                        else group = [link];
-                        anchorGroups.set(groupType, group);
-                    } else {
-                        // promise doc
-                    }
-
+                    let groupType = StrCast(groupDoc.type);
+                    let group = anchorGroups.get(groupType);
+                    if (group) group.push(link);
+                    else group = [link];
+                    anchorGroups.set(groupType, group);
                 });
-            }
-            else {
+            } else {
                 // if link is in no groups then put it in default group
                 let group = anchorGroups.get("*");
                 if (group) group.push(link);
@@ -134,52 +65,38 @@ export class LinkManager {
         return anchorGroups;
     }
 
-    public findMetadataInGroup(groupType: string) {
+    // returns a list of all metadata docs associated with the given group type
+    public findAllMetadataDocsInGroup(groupType: string): Array<Doc> {
         let md: Doc[] = [];
         let allLinks = LinkManager.Instance.allLinks;
-        // for every link find its groups
-        // allLinks.forEach(linkDoc => {
-        //     let anchor1groups = LinkManager.Instance.findRelatedGroupedLinks(Cast(linkDoc["anchor1"], Doc, new Doc));
-        //     if (anchor1groups.get(groupType)) {
-        //         md.push(linkDoc["anchor1"]["group"])
-        //     }
-        // })
         allLinks.forEach(linkDoc => {
-            let anchor1Groups = Cast(linkDoc.anchor1Groups, listSpec(Doc), []);
-            let anchor2Groups = Cast(linkDoc.anchor2Groups, listSpec(Doc), []);
-            anchor1Groups.forEach(groupDoc => {
-                if (groupDoc instanceof Doc) {
-                    if (StrCast(groupDoc.type) === groupType) {
-                        md.push(Cast(groupDoc.metadata, Doc, new Doc));
-                    }
-                } else {
-                    // TODO: promise
-                }
-            });
-            anchor2Groups.forEach(groupDoc => {
-                if (groupDoc instanceof Doc) {
-                    if (StrCast(groupDoc.type) === groupType) {
-                        md.push(Cast(groupDoc.metadata, Doc, new Doc));
-                    }
-                } else {
-                    // TODO: promise
-                }
-            });
-
+            let anchor1Groups = LinkManager.Instance.getAnchorGroups(linkDoc, Cast(linkDoc.anchor1, Doc, new Doc));
+            let anchor2Groups = LinkManager.Instance.getAnchorGroups(linkDoc, Cast(linkDoc.anchor2, Doc, new Doc));
+            anchor1Groups.forEach(groupDoc => { if (StrCast(groupDoc.type).toUpperCase() === groupType.toUpperCase()) md.push(Cast(groupDoc.metadata, Doc, new Doc)); });
+            anchor2Groups.forEach(groupDoc => { if (StrCast(groupDoc.type).toUpperCase() === groupType.toUpperCase()) md.push(Cast(groupDoc.metadata, Doc, new Doc)); });
         });
         return md;
     }
 
-    public deleteGroup(groupType: string) {
+    // removes all group docs from all links with the given group type
+    public deleteGroup(groupType: string): void {
         let deleted = LinkManager.Instance.groupMetadataKeys.delete(groupType);
         if (deleted) {
             LinkManager.Instance.allLinks.forEach(linkDoc => {
-                LinkUtils.removeGroupFromAnchor(linkDoc, Cast(linkDoc.anchor1, Doc, new Doc), groupType);
-                LinkUtils.removeGroupFromAnchor(linkDoc, Cast(linkDoc.anchor2, Doc, new Doc), groupType);
+                LinkManager.Instance.removeGroupFromAnchor(linkDoc, Cast(linkDoc.anchor1, Doc, new Doc), groupType);
+                LinkManager.Instance.removeGroupFromAnchor(linkDoc, Cast(linkDoc.anchor2, Doc, new Doc), groupType);
             });
         }
     }
 
+    // removes group doc of given group type only from given anchor on given link
+    public removeGroupFromAnchor(linkDoc: Doc, anchor: Doc, groupType: string) {
+        let groups = LinkManager.Instance.getAnchorGroups(linkDoc, anchor);
+        let newGroups = groups.filter(groupDoc => StrCast(groupDoc.type).toUpperCase() !== groupType.toUpperCase());
+        LinkManager.Instance.setAnchorGroups(linkDoc, anchor, newGroups);
+    }
+
+    // checks if a link with the given anchors exists
     public doesLinkExist(anchor1: Doc, anchor2: Doc) {
         let allLinks = LinkManager.Instance.allLinks;
         let index = allLinks.findIndex(linkDoc => {
@@ -189,4 +106,31 @@ export class LinkManager {
         return index !== -1;
     }
 
+    // finds the opposite anchor of a given anchor in a link
+    public findOppositeAnchor(linkDoc: Doc, anchor: Doc): Doc {
+        if (Doc.AreProtosEqual(anchor, Cast(linkDoc.anchor1, Doc, new Doc))) {
+            return Cast(linkDoc.anchor2, Doc, new Doc);
+        } else {
+            return Cast(linkDoc.anchor1, Doc, new Doc);
+        }
+    }
+
+    // gets the groups associates with an anchor in a link
+    public getAnchorGroups(linkDoc: Doc, anchor: Doc): Array<Doc> {
+        if (Doc.AreProtosEqual(anchor, Cast(linkDoc.anchor1, Doc, new Doc))) {
+            return DocListCast(linkDoc.anchor1Groups);
+        } else {
+            return DocListCast(linkDoc.anchor2Groups);
+        }
+    }
+
+    // sets the groups of the given anchor in the given link
+    public setAnchorGroups(linkDoc: Doc, anchor: Doc, groups: Doc[]) {
+        if (Doc.AreProtosEqual(anchor, Cast(linkDoc.anchor1, Doc, new Doc))) {
+            linkDoc.anchor1Groups = new List<Doc>(groups);
+        } else {
+            linkDoc.anchor2Groups = new List<Doc>(groups);
+        }
+    }
+
 }
\ No newline at end of file
diff --git a/src/client/views/nodes/LinkBox.scss b/src/client/views/nodes/LinkBox.scss
index 639f83b38..08fefaf4d 100644
--- a/src/client/views/nodes/LinkBox.scss
+++ b/src/client/views/nodes/LinkBox.scss
@@ -1,66 +1,62 @@
 @import "../globalCssVariables";
-.link-container {
-    width: 100%;
-    height: 50px;
+.link-menu-item {
+    border-top: 0.5px solid $light-color-secondary; 
+    padding: 6px;
+    position: relative;
     display: flex;
-    flex-direction: row;
-    border-top: 0.5px solid #bababa;
-}
-
-.info-container {
-    width: 65%;
-    padding-top: 5px;
-    padding-left: 5px;
-    display: flex;
-    flex-direction: column
-}
-
-.link-name {
-    font-size: 11px;
-}
-
-.doc-name {
-    font-size: 8px;
-}
-
-.button-container {
-    width: 35%;
-    padding-top: 8px;
-    display: flex;
-    flex-direction: row;
-}
-
-.button {
-    height: 20px;
-    width: 20px;
-    margin: 8px 4px;
-    border-radius: 50%;
-    opacity: 0.9;
-    pointer-events: auto;
-    background-color: $dark-color;
-    color: $light-color;
-    text-transform: uppercase;
-    letter-spacing: 2px;
-    font-size: 60%;
-    transition: transform 0.2s;
-}
-
-.button:hover {
-    background: $main-accent;
-    cursor: pointer;
-}
-
-// .fa-icon-view {
-//     margin-left: 3px;
-//     margin-top: 5px;
-// }
-
-.fa-icon-edit {
-    margin-left: 6px;
-    margin-top: 6px;
-}
-
-.fa-icon-delete {
-    margin-left: 7px;
-    margin-top: 6px;
+    font-size: 12px;
+
+    .link-menu-item-content {
+        width: 100%;
+    }
+
+    &:last-child {
+        border-bottom: 0.5px solid $light-color-secondary;
+    }
+    &:hover {
+        .link-menu-item-buttons {
+            display: flex;
+        }
+        .link-menu-item-content {
+            width: calc(100% - 42px);
+        }
+    }
+}
+
+.link-menu-item-buttons {
+    display: none;
+    position: absolute;
+    top: 50%;
+    right: 0;
+    transform: translateY(-50%);
+
+    .button {
+        width: 20px;
+        height: 20px;
+        margin: 0;
+        margin-right: 6px;
+        border-radius: 50%;
+        cursor: pointer;
+        pointer-events: auto;
+        background-color: $dark-color;
+        color: $light-color;
+        font-size: 65%;
+        transition: transform 0.2s;
+        text-align: center;
+        position: relative;
+
+        .fa-icon {
+            position: absolute;
+            top: 50%;
+            left: 50%;
+            transform: translate(-50%, -50%);
+        }
+
+        &:last-child {
+            margin-right: 0;
+        }
+        &:hover {
+            background: $main-accent;
+        }
+    }
 }
\ No newline at end of file
diff --git a/src/client/views/nodes/LinkBox.tsx b/src/client/views/nodes/LinkBox.tsx
index 5597bb1aa..1a7cce4a3 100644
--- a/src/client/views/nodes/LinkBox.tsx
+++ b/src/client/views/nodes/LinkBox.tsx
@@ -4,13 +4,10 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { observer } from "mobx-react";
 import { DocumentManager } from "../../util/DocumentManager";
 import { undoBatch } from "../../util/UndoManager";
-import { CollectionDockingView } from "../collections/CollectionDockingView";
 import './LinkBox.scss';
 import React = require("react");
 import { Doc } from '../../../new_fields/Doc';
-import { Cast, NumCast } from '../../../new_fields/Types';
-import { listSpec } from '../../../new_fields/Schema';
-import { action } from 'mobx';
+import { StrCast } from '../../../new_fields/Types';
 
 
 library.add(faEye);
@@ -20,9 +17,8 @@ library.add(faArrowRight);
 
 interface Props {
     linkDoc: Doc;
-    linkName: String;
-    pairedDoc: Doc;
-    type: String;
+    sourceDoc: Doc;
+    destinationDoc: Doc;
     showEditor: () => void;
 }
 
@@ -30,58 +26,28 @@ interface Props {
 export class LinkBox extends React.Component<Props> {
 
     @undoBatch
-    followLink = async (e: React.PointerEvent): Promise<void> => {
+    onFollowLink = async (e: React.PointerEvent): Promise<void> => {
         e.stopPropagation();
-        DocumentManager.Instance.jumpToDocument(this.props.pairedDoc, e.altKey);
+        DocumentManager.Instance.jumpToDocument(this.props.destinationDoc, e.altKey);
     }
 
-    onEditButtonPressed = (e: React.PointerEvent): void => {
+    onEdit = (e: React.PointerEvent): void => {
         e.stopPropagation();
-
         this.props.showEditor();
     }
 
-    // @action
-    // onDeleteButtonPressed = async (e: React.PointerEvent): Promise<void> => {
-    //     e.stopPropagation();
-    //     const [linkedFrom, linkedTo] = await Promise.all([Cast(this.props.linkDoc.linkedFrom, Doc), Cast(this.props.linkDoc.linkedTo, Doc)]);
-    //     if (linkedFrom) {
-    //         const linkedToDocs = Cast(linkedFrom.linkedToDocs, listSpec(Doc));
-    //         if (linkedToDocs) {
-    //             linkedToDocs.splice(linkedToDocs.indexOf(this.props.linkDoc), 1);
-    //         }
-    //     }
-    //     if (linkedTo) {
-    //         const linkedFromDocs = Cast(linkedTo.linkedFromDocs, listSpec(Doc));
-    //         if (linkedFromDocs) {
-    //             linkedFromDocs.splice(linkedFromDocs.indexOf(this.props.linkDoc), 1);
-    //         }
-    //     }
-    // }
-
     render() {
-
         return (
-            //<LinkEditor linkBox={this} linkDoc={this.props.linkDoc} />
             <div className="link-menu-item">
                 <div className="link-menu-item-content">
                     <div className="link-name">
-                        <p>{this.props.linkName}</p>
-                    </div>
-                    <div className="doc-name">
-                        <p>{this.props.type}{this.props.pairedDoc.Title}</p>
+                        <p>{StrCast(this.props.destinationDoc.title)}</p>
                     </div>
                 </div>
 
                 <div className="link-menu-item-buttons">
-                    {/* <div title="Follow Link" className="button" onPointerDown={this.onViewButtonPressed}>
-                        <FontAwesomeIcon className="fa-icon-view" icon="eye" size="sm" /></div> */}
-                    <div title="Follow Link" className="button" onPointerDown={this.followLink}>
-                        <FontAwesomeIcon className="fa-icon" icon="arrow-right" size="sm" /></div>
-                    <div title="Edit Link" className="button" onPointerDown={this.onEditButtonPressed}>
-                        <FontAwesomeIcon className="fa-icon" icon="edit" size="sm" /></div>
-                    {/* <div title="Delete Link" className="button" onPointerDown={this.onDeleteButtonPressed}>
-                        <FontAwesomeIcon className="fa-icon" icon="times" size="sm" /></div> */}
+                    <div title="Edit link" className="button" onPointerDown={this.onEdit}><FontAwesomeIcon className="fa-icon" icon="edit" size="sm" /></div>
+                    <div title="Follow link" className="button" onPointerDown={this.onFollowLink}><FontAwesomeIcon className="fa-icon" icon="arrow-right" size="sm" /></div>
                 </div>
             </div>
         );
diff --git a/src/client/views/nodes/LinkEditor.scss b/src/client/views/nodes/LinkEditor.scss
index 01f0cb82a..6aac39f45 100644
--- a/src/client/views/nodes/LinkEditor.scss
+++ b/src/client/views/nodes/LinkEditor.scss
@@ -1,92 +1,63 @@
 @import "../globalCssVariables";
-.edit-container {
+
+.linkEditor {
     width: 100%;
     height: auto;
-    display: flex;
-    flex-direction: column;
+    font-size: 12px; // TODO
 }
 
-.name-input {
-    margin-bottom: 10px;
-    padding: 5px;
-    font-size: 12px;
-    border: 1px solid #bababa;
+.linkEditor-back {
+    margin-bottom: 6px;
 }
 
-.description-input {
-    font-size: 11px;
-    padding: 5px;
-    margin-bottom: 10px;
-    border: 1px solid #bababa;
-}
+.linkEditor-groupsLabel {
+    display: flex;
+    justify-content: space-between;
 
-.save-button {
-    width: 50px;
-    height: 22px;
-    pointer-events: auto;
-    background-color: $dark-color;
-    color: $light-color;
-    text-transform: uppercase;
-    letter-spacing: 2px;
-    padding: 2px;
-    font-size: 10px;
-    margin: 0 auto;
-    transition: transform 0.2s;
-    text-align: center;
-    line-height: 20px;
+    button {
+        width: 20px;
+        height: 20px;
+        margin-left: 6px;
+        padding: 0;
+        font-size: 14px;
+    }
 }
 
-.save-button:hover {
-    background: $main-accent;
-    cursor: pointer;
+.linkEditor-linkedTo {
+    border-bottom: 0.5px solid $light-color-secondary; 
+    padding-bottom: 6px;
+    margin-bottom: 6px;
 }
 
-.linkEditor {
-    font-size: 12px; // TODO
-
-    .linkEditor-back {
-        // background-color: $dark-color;
-        // color: $light-color;
-        margin-bottom: 6px;
-    }
+.linkEditor-group {
+    background-color: $light-color-secondary; 
+    padding: 6px;
+    margin: 3px 0;
+    border-radius: 3px;
 
-    .linkEditor-groupsLabel {
-        display: flex;
-        justify-content: space-between;
-        button {
-            width: 20px;
-            height: 20px;
-            margin-left: 6px;
-            padding: 0;
-            font-size: 14px;
-        }
-    }
-    .linkEditor-linkedTo {
-        border-bottom: 0.5px solid $light-color-secondary; 
-        padding-bottom: 6px;
-        margin-bottom: 6px;
-    }
-    .linkEditor-group {
-        background-color: $light-color-secondary; 
-        padding: 6px;
-        margin: 3px 0;
-        border-radius: 3px;
-    }
     .linkEditor-group-row {
         display: flex;
         margin-bottom: 6px;
+
         .linkEditor-group-row-label {
             margin-right: 6px;
         }
     }
+
     .linkEditor-metadata-row {
         display: flex;
         justify-content: space-between;
         margin-bottom: 6px;
+
+        .linkEditor-error {
+            border-color: red;
+        }
+
         input {
             width: calc(50% - 18px);
             height: 20px;
         }
+
         button {
             width: 20px;
             height: 20px;
@@ -97,10 +68,12 @@
     }
 }
 
+
 .linkEditor-dropdown {
     width: 100%;
     position: relative;
     z-index: 999;
+
     .linkEditor-options-wrapper {
         width: 100%;
         position: absolute;
@@ -109,12 +82,14 @@
         display: flex;
         flex-direction: column;
     }
+
     .linkEditor-option {
         background-color: $light-color-secondary;
         border: 1px solid $intermediate-color;
         border-top: 0;
         padding: 3px;
         cursor: pointer;
+
         &:hover {
             background-color: $intermediate-color;
             font-weight: bold;
@@ -126,19 +101,18 @@
     height: 20px;
     display: flex;
     justify-content: space-between;
+
     .linkEditor-groupOpts {
         width: calc(20% - 3px);
         height: 20px;
-        // margin-left: 6px;
         padding: 0;
         font-size: 10px;
-        &:first-child { // delete
-            font-size: 14px;
-        }
-        &:disabled { // delete
+
+        &:disabled {
             background-color: gray;
         }
     }
+
     .linkEditor-groupOpts button {
         width: 100%;
         height: 20px;
diff --git a/src/client/views/nodes/LinkEditor.tsx b/src/client/views/nodes/LinkEditor.tsx
index 5097d625e..484682c22 100644
--- a/src/client/views/nodes/LinkEditor.tsx
+++ b/src/client/views/nodes/LinkEditor.tsx
@@ -4,61 +4,52 @@ import { observer } from "mobx-react";
 import './LinkEditor.scss';
 import { StrCast, Cast } from "../../../new_fields/Types";
 import { Doc } from "../../../new_fields/Doc";
-import { List } from "../../../new_fields/List";
-import { listSpec } from "../../../new_fields/Schema";
-import { LinkManager, LinkUtils } from "../../util/LinkManager";
+import { LinkManager } from "../../util/LinkManager";
 import { Docs } from "../../documents/Documents";
 import { Utils } from "../../../Utils";
 import { faArrowLeft, faEllipsisV, faTable } from '@fortawesome/free-solid-svg-icons';
 import { library } from "@fortawesome/fontawesome-svg-core";
 import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { string } from "prop-types";
 import { SetupDrag } from "../../util/DragManager";
 
 library.add(faArrowLeft);
 library.add(faEllipsisV);
 library.add(faTable);
 
+
+interface GroupTypesDropdownProps {
+    groupId: string;
+    groupType: string;
+    setGroup: (groupId: string, group: string) => void;
+}
 // this dropdown could be generalized
 @observer
-class LinkGroupsDropdown extends React.Component<{ groupId: string, groupType: string, setGroup: (groupId: string, group: string) => void }> {
+class GroupTypesDropdown extends React.Component<GroupTypesDropdownProps> {
     @observable private _searchTerm: string = "";
     @observable private _groupType: string = this.props.groupType;
 
-    @action
-    setSearchTerm(value: string) {
-        this._searchTerm = value;
-    }
+    @action setSearchTerm = (value: string): void => { this._searchTerm = value; };
+    @action setGroupType = (value: string): void => { this._groupType = value; };
 
     @action
-    setGroupType(value: string) {
-        this._groupType = value;
+    createGroup = (groupType: string): void => {
+        this.props.setGroup(this.props.groupId, groupType);
+        LinkManager.Instance.groupMetadataKeys.set(groupType, []);
     }
 
-    @action
-    createGroup(value: string) {
-        LinkManager.Instance.groupMetadataKeys.set(value, []);
-        this.props.setGroup(this.props.groupId, value);
-    }
+    renderOptions = (): JSX.Element[] | JSX.Element => {
+        if (this._searchTerm === "") return <></>;
 
-    renderOptions = (): JSX.Element[] => {
-        let allGroups: string[], searchTerm: string, results: string[], exactFound: boolean;
-        if (this._searchTerm !== "") {
-            allGroups = Array.from(LinkManager.Instance.groupMetadataKeys.keys());
-            searchTerm = this._searchTerm.toUpperCase();
-            results = allGroups.filter(group => group.toUpperCase().indexOf(searchTerm) > -1);
-            exactFound = results.findIndex(group => group.toUpperCase() === searchTerm) > -1;
-        } else {
-            results = [];
-            exactFound = false;
-        }
+        let allGroupTypes = Array.from(LinkManager.Instance.groupMetadataKeys.keys());
+        let groupOptions = allGroupTypes.filter(groupType => groupType.toUpperCase().indexOf(this._searchTerm.toUpperCase()) > -1);
+        let exactFound = groupOptions.findIndex(groupType => groupType.toUpperCase() === this._searchTerm.toUpperCase()) > -1;
 
-        let options = [];
-        results.forEach(result => {
-            options.push(<div key={result} className="linkEditor-option"
-                onClick={() => { this.props.setGroup(this.props.groupId, result); this.setGroupType(result); this.setSearchTerm(""); }}>{result}</div>);
+        let options = groupOptions.map(groupType => {
+            return <div key={groupType} className="linkEditor-option"
+                onClick={() => { this.props.setGroup(this.props.groupId, groupType); this.setGroupType(groupType); this.setSearchTerm(""); }}>{groupType}</div>;
         });
 
+        // if search term does not already exist as a group type, give option to create new group type
         if (!exactFound && this._searchTerm !== "") {
             options.push(<div key={""} className="linkEditor-option"
                 onClick={() => { this.createGroup(this._searchTerm); this.setGroupType(this._searchTerm); this.setSearchTerm(""); }}>Define new "{this._searchTerm}" relationship</div>);
@@ -81,57 +72,66 @@ class LinkGroupsDropdown extends React.Component<{ groupId: string, groupType: s
 }
 
 
-
+interface LinkMetadataEditorProps {
+    groupType: string;
+    mdDoc: Doc;
+    mdKey: string;
+    mdValue: string;
+}
 @observer
-class LinkMetadataEditor extends React.Component<{ groupType: string, mdDoc: Doc, mdKey: string, mdValue: string }> {
+class LinkMetadataEditor extends React.Component<LinkMetadataEditorProps> {
     @observable private _key: string = this.props.mdKey;
     @observable private _value: string = this.props.mdValue;
+    @observable private _keyError: boolean = false;
 
     @action
-    editMetadataKey = (value: string): void => {
-        // TODO: check that metadata doesnt already exist in group
+    setMetadataKey = (value: string): void => {
         let groupMdKeys = new Array(...LinkManager.Instance.groupMetadataKeys.get(this.props.groupType)!);
-        if (groupMdKeys) {
-            let index = groupMdKeys.indexOf(this._key);
-            if (index > -1) {
-                groupMdKeys[index] = value;
-            }
-            else {
-                console.log("OLD KEY WAS NOT FOUND", ...groupMdKeys);
-            }
+
+        // don't allow user to create existing key
+        let newIndex = groupMdKeys.findIndex(key => key.toUpperCase() === value.toUpperCase());
+        if (newIndex > -1) {
+            this._keyError = true;
+            this._key = value;
+            return;
+        } else {
+            this._keyError = false;
         }
 
+        // set new value for key
+        let currIndex = groupMdKeys.findIndex(key => key.toUpperCase() === this._key.toUpperCase());
+        if (currIndex === -1) console.error("LinkMetadataEditor: key was not found");
+        groupMdKeys[currIndex] = value;
+
         this._key = value;
         LinkManager.Instance.groupMetadataKeys.set(this.props.groupType, groupMdKeys);
     }
 
     @action
-    editMetadataValue = (value: string): void => {
-        this.props.mdDoc[this._key] = value;
-        this._value = value;
+    setMetadataValue = (value: string): void => {
+        if (!this._keyError) {
+            this._value = value;
+            this.props.mdDoc[this._key] = value;
+        }
     }
 
     @action
     removeMetadata = (): void => {
         let groupMdKeys = new Array(...LinkManager.Instance.groupMetadataKeys.get(this.props.groupType)!);
-        if (groupMdKeys) {
-            let index = groupMdKeys.indexOf(this._key);
-            if (index > -1) {
-                groupMdKeys.splice(index, 1);
-            }
-            else {
-                console.log("OLD KEY WAS NOT FOUND", ...groupMdKeys);
-            }
-        }
-        this._key = "";
+
+        let index = groupMdKeys.findIndex(key => key.toUpperCase() === this._key.toUpperCase());
+        if (index === -1) console.error("LinkMetadataEditor: key was not found");
+        groupMdKeys.splice(index, 1);
+
         LinkManager.Instance.groupMetadataKeys.set(this.props.groupType, groupMdKeys);
+        this._key = "";
     }
 
     render() {
         return (
             <div className="linkEditor-metadata-row">
-                <input type="text" value={this._key} placeholder="key" onChange={e => this.editMetadataKey(e.target.value)}></input>:
-                <input type="text" value={this._value} placeholder="value" onChange={e => this.editMetadataValue(e.target.value)}></input>
+                <input className={this._keyError ? "linkEditor-error" : ""} type="text" value={this._key} placeholder="key" onChange={e => this.setMetadataKey(e.target.value)}></input>:
+                <input type="text" value={this._value} placeholder="value" onChange={e => this.setMetadataValue(e.target.value)}></input>
                 <button onClick={() => this.removeMetadata()}>x</button>
             </div>
         );
@@ -144,175 +144,127 @@ interface LinkEditorProps {
     linkDoc: Doc;
     showLinks: () => void;
 }
-
 @observer
 export class LinkEditor extends React.Component<LinkEditorProps> {
 
-    @observable private _groups: Map<string, Doc> = new Map(); // map of temp group id to the corresponding group doc
+    // map of temporary group id to the corresponding group doc
+    @observable private _groups: Map<string, Doc> = new Map();
 
     constructor(props: LinkEditorProps) {
         super(props);
 
         let groups = new Map<string, Doc>();
-        let groupList = (Doc.AreProtosEqual(props.sourceDoc, Cast(props.linkDoc.anchor1, Doc, new Doc))) ?
-            Cast(props.linkDoc.anchor1Groups, listSpec(Doc), []) : Cast(props.linkDoc.anchor2Groups, listSpec(Doc), []);
+        let groupList = LinkManager.Instance.getAnchorGroups(props.linkDoc, props.sourceDoc);
         groupList.forEach(groupDoc => {
-            if (groupDoc instanceof Doc) {
-                let id = Utils.GenerateGuid();
-                groups.set(id, groupDoc);
-            } else {
-                // promise doc
-            }
+            let id = Utils.GenerateGuid();
+            groups.set(id, groupDoc);
         });
         this._groups = groups;
     }
 
     @action
     addGroup = (): void => {
-        console.log("before adding", ...Array.from(this._groups.keys()));
-
         // new group only gets added if there is not already a group with type "new group"
         let index = Array.from(this._groups.values()).findIndex(groupDoc => {
             return groupDoc.type === "New Group";
         });
-        if (index === -1) {
-            // create new document for group
-            let mdDoc = Docs.TextDocument();
-            mdDoc.proto!.anchor1 = this.props.sourceDoc.title;
-            mdDoc.proto!.anchor2 = LinkUtils.findOppositeAnchor(this.props.linkDoc, this.props.sourceDoc).title;
+        if (index > -1) return;
 
-            let groupDoc = Docs.TextDocument();
-            groupDoc.proto!.type = "New Group";
-            groupDoc.proto!.metadata = mdDoc;
+        // create new metadata document for group
+        let mdDoc = Docs.TextDocument();
+        mdDoc.proto!.anchor1 = this.props.sourceDoc.title;
+        mdDoc.proto!.anchor2 = LinkManager.Instance.findOppositeAnchor(this.props.linkDoc, this.props.sourceDoc).title;
 
-            this._groups.set(Utils.GenerateGuid(), groupDoc);
+        // create new group document
+        let groupDoc = Docs.TextDocument();
+        groupDoc.proto!.type = "New Group";
+        groupDoc.proto!.metadata = mdDoc;
 
-            let linkDoc = this.props.linkDoc.proto ? this.props.linkDoc.proto : this.props.linkDoc;
-            LinkUtils.setAnchorGroups(linkDoc, this.props.sourceDoc, Array.from(this._groups.values()));
-        }
+        this._groups.set(Utils.GenerateGuid(), groupDoc);
 
-
-        // console.log("set anchor groups for", this.props.sourceDoc["title"]);
-        console.log("after adding", ...Array.from(this._groups.keys()));
+        let linkDoc = this.props.linkDoc.proto ? this.props.linkDoc.proto : this.props.linkDoc;
+        LinkManager.Instance.setAnchorGroups(linkDoc, this.props.sourceDoc, Array.from(this._groups.values()));
     }
 
     @action
     setGroupType = (groupId: string, groupType: string): void => {
-        console.log("setting for ", groupId);
-        // let linkDoc = this.props.linkDoc.proto ? this.props.linkDoc.proto : this.props.linkDoc;
         let groupDoc = this._groups.get(groupId);
         if (groupDoc) {
-            console.log("found group doc");
             groupDoc.proto!.type = groupType;
-
             this._groups.set(groupId, groupDoc);
-
-
-            LinkUtils.setAnchorGroups(this.props.linkDoc, this.props.sourceDoc, Array.from(this._groups.values()));
-            console.log("set", Array.from(this._groups.values()).length);
+            LinkManager.Instance.setAnchorGroups(this.props.linkDoc, this.props.sourceDoc, Array.from(this._groups.values()));
         }
-
-        let anchor1groups: string[] = [];
-        Cast(this.props.linkDoc.anchor1Groups, listSpec(Doc), []).forEach(doc => {
-            if (doc instanceof Doc) {
-                anchor1groups.push(StrCast(doc.proto!.type));
-            } else {
-                console.log("promise");
-            }
-        });
-        let anchor2groups: string[] = [];
-        Cast(this.props.linkDoc.anchor2Groups, listSpec(Doc), []).forEach(doc => {
-            if (doc instanceof Doc) {
-                anchor2groups.push(StrCast(doc.proto!.type));
-            } else {
-                console.log("promise");
-            }
-        });
-        console.log("groups for anchors; anchor1: [", ...anchor1groups, "] ; anchor2: [", ...anchor2groups, "]");
     }
 
-    removeGroupFromLink = (groupId: string, groupType: string) => {
-        // this._groups.delete(groupId);
+    removeGroupFromLink = (groupId: string, groupType: string): void => {
         let groupDoc = this._groups.get(groupId);
-        if (groupDoc) {
-            LinkUtils.removeGroupFromAnchor(this.props.linkDoc, this.props.sourceDoc, groupType);
-            this._groups.delete(groupId);
-        }
-        // LinkUtils.setAnchorGroups(this.props.linkDoc, this.props.sourceDoc, Array.from(this._groups.values()));
-        console.log("\nremoved", groupId, "remaining", ...Array.from(this._groups.keys()), "\n");
+        if (!groupDoc) console.error("LinkEditor: group not found");
+        LinkManager.Instance.removeGroupFromAnchor(this.props.linkDoc, this.props.sourceDoc, groupType);
+        this._groups.delete(groupId);
     }
 
-    deleteGroup = (groupId: string, groupType: string) => {
+    deleteGroup = (groupId: string, groupType: string): void => {
         let groupDoc = this._groups.get(groupId);
-        if (groupDoc) {
-            LinkManager.Instance.deleteGroup(groupType);
-            this._groups.delete(groupId);
-        }
+        if (!groupDoc) console.error("LinkEditor: group not found");
+        LinkManager.Instance.deleteGroup(groupType);
+        this._groups.delete(groupId);
     }
 
-    copyGroup = (groupId: string, groupType: string) => {
-        let oppAnchor = LinkUtils.findOppositeAnchor(this.props.linkDoc, this.props.sourceDoc);
-        let groupList = (Doc.AreProtosEqual(oppAnchor, Cast(this.props.linkDoc.anchor1, Doc, new Doc))) ?
-            Cast(this.props.linkDoc.anchor1Groups, listSpec(Doc), []) : Cast(this.props.linkDoc.anchor2Groups, listSpec(Doc), []);
-        // if group already exists on opposite anchor, copy value
-        let index = groupList.findIndex(groupDoc => {
-            if (groupDoc instanceof Doc) {
-                return StrCast(groupDoc.type) === groupType;
-            } else {
-                return false;
-            }
-        });
-        // TODO: clean
-        // if (index > 0) {
-        //     let thisGroupDoc = this._groups.get(groupId);
-        //     let oppGroupDoc = groupList[index];
-        //     let keys = LinkManager.Instance.allGroups.get(groupType);
-        //     if (keys) {
-        //         keys.forEach(key => {
-        //             if (thisGroupDoc && oppGroupDoc instanceof Doc) { // TODO: clean
-        //                 let val = thisGroupDoc[key] === undefined ? "" : StrCast(thisGroupDoc[key]);
-        //                 oppGroupDoc[key] = val;
-        //             }
-        //             //     mdDoc[key] === undefined) ? "" : StrCast(mdDoc[key])
-        //             // oppGroupDoc[key] = thisGroupDoc[key];
-        //         })
-        //     }
-        //     // LinkUtils.setAnchorGroups(this.props.linkDoc, oppAnchor, [oppGroupDoc]);
-        // } else {
-        let thisGroupDoc = this._groups.get(groupId);
-        let thisMdDoc = Cast(thisGroupDoc!.metadata, Doc, new Doc);
-        let newGroupDoc = Docs.TextDocument();
-        let newMdDoc = Docs.TextDocument();
-        newMdDoc.proto!.anchor1 = StrCast(thisMdDoc.anchor2);
-        newMdDoc.proto!.anchor2 = StrCast(thisMdDoc.anchor1);
+    copyGroup = (groupId: string, groupType: string): void => {
+        let sourceGroupDoc = this._groups.get(groupId);
+        let sourceMdDoc = Cast(sourceGroupDoc!.metadata, Doc, new Doc);
+        let destDoc = LinkManager.Instance.findOppositeAnchor(this.props.linkDoc, this.props.sourceDoc);
+        let destGroupList = LinkManager.Instance.getAnchorGroups(this.props.linkDoc, destDoc);
         let keys = LinkManager.Instance.groupMetadataKeys.get(groupType);
+
+        // create new metadata doc with copied kvp
+        let destMdDoc = Docs.TextDocument();
+        destMdDoc.proto!.anchor1 = StrCast(sourceMdDoc.anchor2);
+        destMdDoc.proto!.anchor2 = StrCast(sourceMdDoc.anchor1);
         if (keys) {
             keys.forEach(key => {
-                if (thisGroupDoc) { // TODO: clean
-                    let val = thisMdDoc[key] === undefined ? "" : StrCast(thisMdDoc[key]);
-                    newMdDoc[key] = val;
-                }
-                //     mdDoc[key] === undefined) ? "" : StrCast(mdDoc[key])
-                // oppGroupDoc[key] = thisGroupDoc[key];
+                let val = sourceMdDoc[key] === undefined ? "" : StrCast(sourceMdDoc[key]);
+                destMdDoc[key] = val;
             });
         }
-        newGroupDoc.proto!.type = groupType;
-        newGroupDoc.proto!.metadata = newMdDoc;
 
-        LinkUtils.setAnchorGroups(this.props.linkDoc, oppAnchor, [newGroupDoc]); // TODO: fix to append to list
-        // }
+        // create new group doc with new metadata doc
+        let destGroupDoc = Docs.TextDocument();
+        destGroupDoc.proto!.type = groupType;
+        destGroupDoc.proto!.metadata = destMdDoc;
 
-        // else create group on opposite anchor
+        // if group does not already exist on opposite anchor, create group doc
+        let index = destGroupList.findIndex(groupDoc => { StrCast(groupDoc.type).toUpperCase() === groupType.toUpperCase(); });
+        if (index > -1) {
+            destGroupList[index] = destGroupDoc;
+        } else {
+            destGroupList.push(destGroupDoc);
+        }
+
+        LinkManager.Instance.setAnchorGroups(this.props.linkDoc, destDoc, destGroupList);
+    }
+
+    viewGroupAsTable = (groupId: string, groupType: string): JSX.Element => {
+        let keys = LinkManager.Instance.groupMetadataKeys.get(groupType);
+        let groupDoc = this._groups.get(groupId);
+        if (keys && groupDoc) {
+            let docs: Doc[] = LinkManager.Instance.findAllMetadataDocsInGroup(groupType);
+            let createTable = action(() => Docs.SchemaDocument(["anchor1", "anchor2", ...keys!], docs, { width: 200, height: 200, title: groupType + " table" }));
+            let ref = React.createRef<HTMLDivElement>();
+            return <div className="linkEditor-groupOpts" ref={ref}><button onPointerDown={SetupDrag(ref, createTable)}><FontAwesomeIcon icon="table" size="sm" /></button></div>;
+        } else {
+            return <button className="linkEditor-groupOpts" disabled><FontAwesomeIcon icon="table" size="sm" /></button>;
+        }
     }
 
-    renderGroup(groupId: string, groupDoc: Doc) {
+    renderGroup = (groupId: string, groupDoc: Doc): JSX.Element => {
         let type = StrCast(groupDoc.type);
         if ((type && LinkManager.Instance.groupMetadataKeys.get(type)) || type === "New Group") {
             return (
                 <div key={groupId} className="linkEditor-group">
                     <div className="linkEditor-group-row">
                         <p className="linkEditor-group-row-label">type:</p>
-                        <LinkGroupsDropdown groupId={groupId} groupType={StrCast(groupDoc.proto!.type)} setGroup={this.setGroupType} />
+                        <GroupTypesDropdown groupId={groupId} groupType={StrCast(groupDoc.proto!.type)} setGroup={this.setGroupType} />
                     </div>
                     {this.renderMetadata(groupId)}
                     <div className="linkEditor-group-buttons">
@@ -330,35 +282,20 @@ export class LinkEditor extends React.Component<LinkEditorProps> {
         }
     }
 
-    viewGroupAsTable(groupId: string, groupType: string) {
-        let keys = LinkManager.Instance.groupMetadataKeys.get(groupType);
-        let groupDoc = this._groups.get(groupId);
-        if (keys && groupDoc) {
-            console.log("keys:", ...keys);
-            let docs: Doc[] = LinkManager.Instance.findMetadataInGroup(groupType);
-            let createTable = action(() => Docs.SchemaDocument(["anchor1", "anchor2", ...keys!], docs, { width: 200, height: 200, title: groupType + " table" }));
-            let ref = React.createRef<HTMLDivElement>();
-            return <div className="linkEditor-groupOpts" ref={ref}><button onPointerDown={SetupDrag(ref, createTable)}><FontAwesomeIcon icon="table" size="sm" /></button></div>;
-        } else {
-            return <button className="linkEditor-groupOpts" disabled><FontAwesomeIcon icon="table" size="sm" /></button>;
-        }
-    }
-
 
     @action
     addMetadata = (groupType: string): void => {
         let mdKeys = LinkManager.Instance.groupMetadataKeys.get(groupType);
         if (mdKeys) {
-            if (mdKeys.indexOf("new key") === -1) {
-                mdKeys.push("new key");
-            }
+            // only add "new key" if there is no other key with value "new key"; prevents spamming
+            if (mdKeys.indexOf("new key") === -1) mdKeys.push("new key");
         } else {
             mdKeys = ["new key"];
         }
         LinkManager.Instance.groupMetadataKeys.set(groupType, mdKeys);
     }
 
-    renderMetadata(groupId: string) {
+    renderMetadata = (groupId: string): JSX.Element[] => {
         let metadata: Array<JSX.Element> = [];
         let groupDoc = this._groups.get(groupId);
         if (groupDoc) {
@@ -377,7 +314,7 @@ export class LinkEditor extends React.Component<LinkEditorProps> {
     }
 
     render() {
-        let destination = LinkUtils.findOppositeAnchor(this.props.linkDoc, this.props.sourceDoc);
+        let destination = LinkManager.Instance.findOppositeAnchor(this.props.linkDoc, this.props.sourceDoc);
 
         let groups: Array<JSX.Element> = [];
         this._groups.forEach((groupDoc, groupId) => {
diff --git a/src/client/views/nodes/LinkMenu.scss b/src/client/views/nodes/LinkMenu.scss
index 7e031b897..09a830c9e 100644
--- a/src/client/views/nodes/LinkMenu.scss
+++ b/src/client/views/nodes/LinkMenu.scss
@@ -1,84 +1,14 @@
 @import "../globalCssVariables";
 
-#linkMenu-container {
+.linkMenu {
     width: 100%;
     height: auto;
-    display: flex;
-    flex-direction: column;
 }
 
-#linkMenu-searchBar {
-    width: 100%;
-    padding: 5px;
-    margin-bottom: 10px;
-    font-size: 12px;
-    border: 1px solid #bababa;
-}
-
-#linkMenu-list {
-    margin-top: 5px;
-    width: 100%;
-    height: 100px;
+.linkMenu-list {
+    max-height: 200px;
     overflow-y: scroll;
 }
 
-.link-menu-group {
-    .link-menu-item {
-        border-top: 0.5px solid $light-color-secondary; 
-        padding: 6px;
-        position: relative;
-        display: flex;
-
-        .link-menu-item-content {
-            width: 100%;
-        }
 
-        &:last-child {
-            border-bottom: 0.5px solid $light-color-secondary;
-        }
-        &:hover .link-menu-item-buttons {
-            display: flex;
-        }
-        &:hover .link-menu-item-content {
-            width: calc(100% - 42px);
-        }
-    }
-    .link-menu-item-buttons {
-        display: none;
-        position: absolute;
-        top: 50%;
-        right: 0;
-        transform: translateY(-50%);
-
-        .button {
-            width: 20px;
-            height: 20px;
-            margin: 0;
-            margin-right: 6px;
-            border-radius: 50%;
-            cursor: pointer;
-            pointer-events: auto;
-            background-color: $dark-color;
-            color: $light-color;
-            font-size: 65%;
-            transition: transform 0.2s;
-            text-align: center;
-            position: relative;
-
-            .fa-icon {
-                position: absolute;
-                top: 50%;
-                left: 50%;
-                transform: translate(-50%, -50%);
-            }
-
-            &:last-child {
-                margin-right: 0;
-            }
-            &:hover {
-                background: $main-accent;
-            }
-        }
-    }
-}
 
diff --git a/src/client/views/nodes/LinkMenu.tsx b/src/client/views/nodes/LinkMenu.tsx
index 47bd6c3eb..ebca54c92 100644
--- a/src/client/views/nodes/LinkMenu.tsx
+++ b/src/client/views/nodes/LinkMenu.tsx
@@ -6,12 +6,8 @@ import { LinkEditor } from "./LinkEditor";
 import './LinkMenu.scss';
 import React = require("react");
 import { Doc, DocListCast } from "../../../new_fields/Doc";
-import { Cast, FieldValue, StrCast } from "../../../new_fields/Types";
 import { Id } from "../../../new_fields/FieldSymbols";
 import { LinkManager, LinkUtils } from "../../util/LinkManager";
-import { number, string } from "prop-types";
-import { listSpec } from "../../../new_fields/Schema";
-import { Utils } from "../../../Utils";
 
 interface Props {
     docView: DocumentView;
@@ -23,57 +19,42 @@ export class LinkMenu extends React.Component<Props> {
 
     @observable private _editingLink?: Doc;
 
-    // renderLinkItems(links: Doc[], key: string, type: string) {
-    //     return links.map(link => {
-    //         let doc = FieldValue(Cast(link[key], Doc));
-    //         if (doc) {
-    //             return <LinkBox key={doc[Id]} linkDoc={link} linkName={StrCast(link.title)} pairedDoc={doc} showEditor={action(() => this._editingLink = link)} type={type} />;
-    //         }
-    //     });
-    // }
-
-    renderGroup(links: Doc[]) {
+    renderGroup = (group: Doc[]): Array<JSX.Element> => {
         let source = this.props.docView.Document;
-        return links.map(link => {
-            let destination = LinkUtils.findOppositeAnchor(link, source);
-            let doc = FieldValue(Cast(destination, Doc));
-            if (doc) {
-                return <LinkBox key={doc[Id] + source[Id]} linkDoc={link} linkName={StrCast(destination.title)} pairedDoc={doc} showEditor={action(() => this._editingLink = link)} type={""} />;
-            }
+        return group.map(linkDoc => {
+            let destination = LinkUtils.findOppositeAnchor(linkDoc, source);
+            return <LinkBox key={destination[Id] + source[Id]} linkDoc={linkDoc} sourceDoc={source} destinationDoc={destination} showEditor={action(() => this._editingLink = linkDoc)} />;
         });
     }
 
-    renderLinks = (links: Map<string, Array<Doc>>): Array<JSX.Element> => {
+    renderAllGroups = (groups: Map<string, Array<Doc>>): Array<JSX.Element> => {
         let linkItems: Array<JSX.Element> = [];
-
-        links.forEach((links, group) => {
+        groups.forEach((group, groupType) => {
             linkItems.push(
-                <div key={group} className="link-menu-group">
-                    <p className="link-menu-group-name">{group}:</p>
+                <div key={groupType} className="link-menu-group">
+                    <p className="link-menu-group-name">{groupType}:</p>
                     <div className="link-menu-group-wrapper">
-                        {this.renderGroup(links)}
+                        {this.renderGroup(group)}
                     </div>
                 </div>
-            )
+            );
         });
 
-        if (linkItems.length === 0) {
-            linkItems.push(<p key="">no links have been created yet</p>);
-        }
+        // source doc has no links
+        if (linkItems.length === 0) linkItems.push(<p key="">No links have been created yet. Drag the linking button onto another document to create a link.</p>);
 
         return linkItems;
     }
 
     render() {
-        let related: Map<string, Doc[]> = LinkManager.Instance.findRelatedGroupedLinks(this.props.docView.props.Document);
+        let sourceDoc = this.props.docView.props.Document;
+        let groups: Map<string, Doc[]> = LinkManager.Instance.findRelatedGroupedLinks(sourceDoc);
         if (this._editingLink === undefined) {
             return (
-                <div id="linkMenu-container">
+                <div className="linkMenu">
                     {/* <input id="linkMenu-searchBar" type="text" placeholder="Search..."></input> */}
-                    <div id="linkMenu-list">
-                        {/* {this.renderLinkItems(linkTo, "linkedTo", "Destination: ")}
-                        {this.renderLinkItems(linkFrom, "linkedFrom", "Source: ")} */}
-                        {this.renderLinks(related)}
+                    <div className="linkMenu-list">
+                        {this.renderAllGroups(groups)}
                     </div>
                 </div>
             );
@@ -82,6 +63,5 @@ export class LinkMenu extends React.Component<Props> {
                 <LinkEditor sourceDoc={this.props.docView.props.Document} linkDoc={this._editingLink} showLinks={action(() => this._editingLink = undefined)}></LinkEditor>
             );
         }
-
     }
 }
\ No newline at end of file
-- 
cgit v1.2.3-70-g09d2


From 6fcd0d8d6fb1471b8af460f6d80bdf0d0e681566 Mon Sep 17 00:00:00 2001
From: Fawn <fangrui_tong@brown.edu>
Date: Tue, 18 Jun 2019 15:17:27 -0400
Subject: added button to delete a link

---
 src/client/views/nodes/LinkEditor.scss | 21 +++++++++++++++------
 src/client/views/nodes/LinkEditor.tsx  | 18 +++++++++++++-----
 2 files changed, 28 insertions(+), 11 deletions(-)

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

diff --git a/src/client/views/nodes/LinkEditor.scss b/src/client/views/nodes/LinkEditor.scss
index 6aac39f45..154b4abe3 100644
--- a/src/client/views/nodes/LinkEditor.scss
+++ b/src/client/views/nodes/LinkEditor.scss
@@ -10,6 +10,21 @@
     margin-bottom: 6px;
 }
 
+.linKEditor-info {
+    border-bottom: 0.5px solid $light-color-secondary; 
+    padding-bottom: 6px;
+    margin-bottom: 6px;
+    display: flex;
+    justify-content: space-between;
+
+    .linkEditor-delete {
+        width: 20px;
+        height: 20px;
+        margin-left: 6px;
+        padding: 0;
+    }
+}
+
 .linkEditor-groupsLabel {
     display: flex;
     justify-content: space-between;
@@ -23,12 +38,6 @@
     }
 }
 
-.linkEditor-linkedTo {
-    border-bottom: 0.5px solid $light-color-secondary; 
-    padding-bottom: 6px;
-    margin-bottom: 6px;
-}
-
 .linkEditor-group {
     background-color: $light-color-secondary; 
     padding: 6px;
diff --git a/src/client/views/nodes/LinkEditor.tsx b/src/client/views/nodes/LinkEditor.tsx
index 484682c22..29ead7388 100644
--- a/src/client/views/nodes/LinkEditor.tsx
+++ b/src/client/views/nodes/LinkEditor.tsx
@@ -7,14 +7,12 @@ import { Doc } from "../../../new_fields/Doc";
 import { LinkManager } from "../../util/LinkManager";
 import { Docs } from "../../documents/Documents";
 import { Utils } from "../../../Utils";
-import { faArrowLeft, faEllipsisV, faTable } from '@fortawesome/free-solid-svg-icons';
+import { faArrowLeft, faEllipsisV, faTable, faTrash } from '@fortawesome/free-solid-svg-icons';
 import { library } from "@fortawesome/fontawesome-svg-core";
 import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
 import { SetupDrag } from "../../util/DragManager";
 
-library.add(faArrowLeft);
-library.add(faEllipsisV);
-library.add(faTable);
+library.add(faArrowLeft, faEllipsisV, faTable, faTrash);
 
 
 interface GroupTypesDropdownProps {
@@ -162,6 +160,13 @@ export class LinkEditor extends React.Component<LinkEditorProps> {
         this._groups = groups;
     }
 
+    @action
+    deleteLink = (): void => {
+        let index = LinkManager.Instance.allLinks.indexOf(this.props.linkDoc);
+        LinkManager.Instance.allLinks.splice(index, 1);
+        this.props.showLinks();
+    }
+
     @action
     addGroup = (): void => {
         // new group only gets added if there is not already a group with type "new group"
@@ -324,7 +329,10 @@ export class LinkEditor extends React.Component<LinkEditorProps> {
         return (
             <div className="linkEditor">
                 <button className="linkEditor-back" onPointerDown={() => this.props.showLinks()}><FontAwesomeIcon icon="arrow-left" size="sm" /></button>
-                <p className="linkEditor-linkedTo">editing link to: <b>{destination.proto!.title}</b></p>
+                <div className="linkEditor-info">
+                    <p className="linkEditor-linkedTo">editing link to: <b>{destination.proto!.title}</b></p>
+                    <button className="linkEditor-delete" onPointerDown={() => this.deleteLink()} title="Delete link"><FontAwesomeIcon icon="trash" size="sm" /></button>
+                </div>
                 <div className="linkEditor-groupsLabel">
                     <b>Relationships:</b>
                     <button onClick={() => this.addGroup()} title="Add Group">+</button>
-- 
cgit v1.2.3-70-g09d2


From 32ef8d83d5829e2faadbebaf6f9b694df5d7ea02 Mon Sep 17 00:00:00 2001
From: Fawn <fangrui_tong@brown.edu>
Date: Fri, 21 Jun 2019 17:41:20 -0400
Subject: link menu styling

---
 src/client/documents/Documents.ts      |  9 +++---
 src/client/util/LinkManager.ts         | 10 +++++--
 src/client/views/nodes/LinkEditor.scss | 50 ++++++++++++++++++----------------
 src/client/views/nodes/LinkEditor.tsx  | 32 ++++++++++++++--------
 src/client/views/nodes/LinkMenu.scss   |  5 ----
 src/new_fields/Doc.ts                  | 17 ++++--------
 6 files changed, 66 insertions(+), 57 deletions(-)

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

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 7cef48b98..64032e096 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -90,7 +90,7 @@ export namespace DocUtils {
     // export function MakeLink(source: Doc, target: Doc, targetContext?: Doc, title: string = "", description: string = "", tags: string = "Default") {
     //     let protoSrc = source.proto ? source.proto : source;
     //     let protoTarg = target.proto ? target.proto : target;
-    export function MakeLink(source: Doc, target: Doc, targetContext?: Doc) {
+    export function MakeLink(source: Doc, target: Doc, targetContext?: Doc, title: string = "", description: string = "", tags: string = "Default") {
         if (LinkManager.Instance.doesLinkExist(source, target)) {
             console.log("LINK EXISTS"); return;
         }
@@ -99,9 +99,10 @@ export namespace DocUtils {
 
             let linkDoc = Docs.TextDocument({ width: 100, height: 30, borderRounding: -1 });
             let linkDocProto = Doc.GetProto(linkDoc);
-            // linkDocProto.title = title === "" ? source.title + " to " + target.title : title;
-            // linkDocProto.linkDescription = description;
-            // linkDocProto.linkTags = tags;
+
+            linkDocProto.title = title; //=== "" ? source.title + " to " + target.title : title;
+            linkDocProto.linkDescription = description;
+            linkDocProto.linkTags = tags;
 
             linkDocProto.anchor1 = source;
             linkDocProto.anchor1Page = source.curPage;
diff --git a/src/client/util/LinkManager.ts b/src/client/util/LinkManager.ts
index 544f2edda..fef996bb9 100644
--- a/src/client/util/LinkManager.ts
+++ b/src/client/util/LinkManager.ts
@@ -3,6 +3,7 @@ import { StrCast, Cast } from "../../new_fields/Types";
 import { Doc, DocListCast } from "../../new_fields/Doc";
 import { listSpec } from "../../new_fields/Schema";
 import { List } from "../../new_fields/List";
+import { Id } from "../../new_fields/FieldSymbols";
 
 
 /* 
@@ -36,8 +37,13 @@ export class LinkManager {
 
     // finds all links that contain the given anchor
     public findAllRelatedLinks(anchor: Doc): Array<Doc> {
-        return LinkManager.Instance.allLinks.filter(
-            link => Doc.AreProtosEqual(anchor, Cast(link.anchor1, Doc, new Doc)) || Doc.AreProtosEqual(anchor, Cast(link.anchor2, Doc, new Doc)));
+        return LinkManager.Instance.allLinks.filter(link => {
+            let protomatch1 = Doc.AreProtosEqual(anchor, Cast(link.anchor1, Doc, new Doc));
+            let protomatch2 = Doc.AreProtosEqual(anchor, Cast(link.anchor2, Doc, new Doc));
+            let idmatch1 = StrCast(anchor[Id]) === StrCast(Cast(link.anchor1, Doc, new Doc)[Id]);
+            let idmatch2 = StrCast(anchor[Id]) === StrCast(Cast(link.anchor2, Doc, new Doc)[Id]);
+            return protomatch1 || protomatch2 || idmatch1 || idmatch2;
+        });
     }
 
     // returns map of group type to anchor's links in that group type
diff --git a/src/client/views/nodes/LinkEditor.scss b/src/client/views/nodes/LinkEditor.scss
index 154b4abe3..760887fa2 100644
--- a/src/client/views/nodes/LinkEditor.scss
+++ b/src/client/views/nodes/LinkEditor.scss
@@ -25,17 +25,22 @@
     }
 }
 
+.linkEditor-button {
+    width: 20px;
+    height: 20px;
+    margin-left: 6px;
+    padding: 0;
+    font-size: 12px;
+    border-radius: 10px;
+
+    &:disabled {
+        background-color: gray;
+    }
+}
+
 .linkEditor-groupsLabel {
     display: flex;
     justify-content: space-between;
-
-    button {
-        width: 20px;
-        height: 20px;
-        margin-left: 6px;
-        padding: 0;
-        font-size: 14px;
-    }
 }
 
 .linkEditor-group {
@@ -109,23 +114,20 @@
 .linkEditor-group-buttons {
     height: 20px;
     display: flex;
-    justify-content: space-between;
-
-    .linkEditor-groupOpts {
-        width: calc(20% - 3px);
-        height: 20px;
-        padding: 0;
-        font-size: 10px;
+    justify-content: flex-end;
 
-        &:disabled {
-            background-color: gray;
-        }
+    .linkEditor-button {
+        margin-left: 6px;
     }
 
-    .linkEditor-groupOpts button {
-        width: 100%;
-        height: 20px;
-        font-size: 10px;
-        padding: 0;
-    }
+    // .linkEditor-groupOpts {
+    //     width: calc(20% - 3px);
+    //     height: 20px;
+    //     padding: 0;
+    //     font-size: 10px;
+
+    //     &:disabled {
+    //         background-color: gray;
+    //     }
+    // }
 }
\ No newline at end of file
diff --git a/src/client/views/nodes/LinkEditor.tsx b/src/client/views/nodes/LinkEditor.tsx
index 29ead7388..0d13c6cc8 100644
--- a/src/client/views/nodes/LinkEditor.tsx
+++ b/src/client/views/nodes/LinkEditor.tsx
@@ -7,12 +7,13 @@ import { Doc } from "../../../new_fields/Doc";
 import { LinkManager } from "../../util/LinkManager";
 import { Docs } from "../../documents/Documents";
 import { Utils } from "../../../Utils";
-import { faArrowLeft, faEllipsisV, faTable, faTrash } from '@fortawesome/free-solid-svg-icons';
+import { faArrowLeft, faEllipsisV, faTable, faTrash, faCog } from '@fortawesome/free-solid-svg-icons';
 import { library } from "@fortawesome/fontawesome-svg-core";
 import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
 import { SetupDrag } from "../../util/DragManager";
+import { anchorPoints, Flyout } from "../DocumentDecorations";
 
-library.add(faArrowLeft, faEllipsisV, faTable, faTrash);
+library.add(faArrowLeft, faEllipsisV, faTable, faTrash, faCog);
 
 
 interface GroupTypesDropdownProps {
@@ -256,9 +257,9 @@ export class LinkEditor extends React.Component<LinkEditorProps> {
             let docs: Doc[] = LinkManager.Instance.findAllMetadataDocsInGroup(groupType);
             let createTable = action(() => Docs.SchemaDocument(["anchor1", "anchor2", ...keys!], docs, { width: 200, height: 200, title: groupType + " table" }));
             let ref = React.createRef<HTMLDivElement>();
-            return <div className="linkEditor-groupOpts" ref={ref}><button onPointerDown={SetupDrag(ref, createTable)}><FontAwesomeIcon icon="table" size="sm" /></button></div>;
+            return <div ref={ref}><button className="linkEditor-button" onPointerDown={SetupDrag(ref, createTable)}><FontAwesomeIcon icon="table" size="sm" /></button></div>;
         } else {
-            return <button className="linkEditor-groupOpts" disabled><FontAwesomeIcon icon="table" size="sm" /></button>;
+            return <button className="linkEditor-button" disabled><FontAwesomeIcon icon="table" size="sm" /></button>;
         }
     }
 
@@ -273,12 +274,20 @@ export class LinkEditor extends React.Component<LinkEditorProps> {
                     </div>
                     {this.renderMetadata(groupId)}
                     <div className="linkEditor-group-buttons">
-                        {groupDoc.type === "New Group" ? <button className="linkEditor-groupOpts" disabled={true} title="Add KVP">+</button> :
-                            <button className="linkEditor-groupOpts" onClick={() => this.addMetadata(StrCast(groupDoc.proto!.type))} title="Add KVP">+</button>}
-                        <button className="linkEditor-groupOpts" onClick={() => this.copyGroup(groupId, type)} title="Copy group to opposite anchor">↔</button>
-                        <button className="linkEditor-groupOpts" onClick={() => this.removeGroupFromLink(groupId, type)} title="Remove group from link">x</button>
-                        <button className="linkEditor-groupOpts" onClick={() => this.deleteGroup(groupId, type)} title="Delete group">xx</button>
+                        {groupDoc.type === "New Group" ? <button className="linkEditor-button" disabled={true} title="Add KVP">+</button> :
+                            <button className="linkEditor-button" onClick={() => this.addMetadata(StrCast(groupDoc.proto!.type))} title="Add KVP">+</button>}
                         {this.viewGroupAsTable(groupId, type)}
+                        <Flyout
+                            anchorPoint={anchorPoints.LEFT_TOP}
+                            content={
+                                <>
+                                    <button className="linkEditor-groupOpts" onClick={() => this.copyGroup(groupId, type)} title="Copy group to opposite anchor">↔</button>
+                                    <button className="linkEditor-groupOpts" onClick={() => this.removeGroupFromLink(groupId, type)} title="Remove group from link">x</button>
+                                    <button className="linkEditor-groupOpts" onClick={() => this.deleteGroup(groupId, type)} title="Delete group">xx</button>
+                                </>
+                            }>
+                            <button className="linkEditor-button" title="Delete group"><FontAwesomeIcon icon="cog" size="sm" /></button>
+                        </Flyout >
                     </div>
                 </div>
             );
@@ -290,6 +299,7 @@ export class LinkEditor extends React.Component<LinkEditorProps> {
 
     @action
     addMetadata = (groupType: string): void => {
+        console.log("ADD MD");
         let mdKeys = LinkManager.Instance.groupMetadataKeys.get(groupType);
         if (mdKeys) {
             // only add "new key" if there is no other key with value "new key"; prevents spamming
@@ -331,11 +341,11 @@ export class LinkEditor extends React.Component<LinkEditorProps> {
                 <button className="linkEditor-back" onPointerDown={() => this.props.showLinks()}><FontAwesomeIcon icon="arrow-left" size="sm" /></button>
                 <div className="linkEditor-info">
                     <p className="linkEditor-linkedTo">editing link to: <b>{destination.proto!.title}</b></p>
-                    <button className="linkEditor-delete" onPointerDown={() => this.deleteLink()} title="Delete link"><FontAwesomeIcon icon="trash" size="sm" /></button>
+                    <button className="linkEditor-delete linkEditor-button" onPointerDown={() => this.deleteLink()} title="Delete link"><FontAwesomeIcon icon="trash" size="sm" /></button>
                 </div>
                 <div className="linkEditor-groupsLabel">
                     <b>Relationships:</b>
-                    <button onClick={() => this.addGroup()} title="Add Group">+</button>
+                    <button className="linkEditor-button" onClick={() => this.addGroup()} title=" Add Group">+</button>
                 </div>
                 {groups.length > 0 ? groups : <div className="linkEditor-group">There are currently no relationships associated with this link.</div>}
             </div>
diff --git a/src/client/views/nodes/LinkMenu.scss b/src/client/views/nodes/LinkMenu.scss
index 1ca669d00..ae3446e25 100644
--- a/src/client/views/nodes/LinkMenu.scss
+++ b/src/client/views/nodes/LinkMenu.scss
@@ -11,7 +11,6 @@
 }
 
 .linkMenu-group {
-    // margin-bottom: 10px;
     border-bottom: 0.5px solid lightgray; 
     padding: 5px 0;
 
@@ -30,10 +29,6 @@
             background-color: lightgray;
         }
     }
-
-    // .linkMenu-group-wrapper {
-    //     padding-top: 4px;
-    // }
 }
 
 
diff --git a/src/new_fields/Doc.ts b/src/new_fields/Doc.ts
index 5ce47fc2f..fda788f2d 100644
--- a/src/new_fields/Doc.ts
+++ b/src/new_fields/Doc.ts
@@ -234,32 +234,27 @@ export namespace Doc {
     export function MakeCopy(doc: Doc, copyProto: boolean = false): Doc {
         const copy = new Doc;
         Object.keys(doc).forEach(key => {
+            console.log(key);
             const field = doc[key];
             if (key === "proto" && copyProto) {
+                console.log(1);
                 if (field instanceof Doc) {
+                    console.log(2);
                     copy[key] = Doc.MakeCopy(field);
                 }
             } else {
                 if (field instanceof RefField) {
+                    console.log(3);
                     copy[key] = field;
                 } else if (field instanceof ObjectField) {
+                    console.log(4);
                     copy[key] = ObjectField.MakeCopy(field);
                 } else {
+                    console.log(5);
                     copy[key] = field;
                 }
             }
         });
-        console.log("COPY", StrCast(doc.title));
-        let links = LinkManager.Instance.findAllRelatedLinks(doc);
-        links.forEach(linkDoc => {
-            let opp = LinkManager.Instance.findOppositeAnchor(linkDoc, doc);
-            console.log("OPP", StrCast(opp.title));
-            DocUtils.MakeLink(opp, copy);
-        });
-
-        LinkManager.Instance.allLinks.forEach(l => {
-            console.log("LINK", StrCast(Cast(l.anchor1, Doc, new Doc).title), StrCast(Cast(l.anchor2, Doc, new Doc).title));
-        });
 
         return copy;
     }
-- 
cgit v1.2.3-70-g09d2


From 522970375fe0227f9221a7e8be02875afd74ca63 Mon Sep 17 00:00:00 2001
From: Fawn <fangrui_tong@brown.edu>
Date: Mon, 24 Jun 2019 14:01:29 -0400
Subject: link menu styling

---
 src/client/util/DocumentManager.ts                 | 23 ++++++-----
 src/client/util/DragManager.ts                     | 12 ++----
 src/client/util/LinkManager.ts                     |  6 +--
 .../CollectionFreeFormLinkView.tsx                 | 24 +++++------
 .../CollectionFreeFormLinksView.tsx                | 19 ++++++++-
 src/client/views/nodes/LinkEditor.scss             |  2 +-
 src/client/views/nodes/LinkEditor.tsx              | 46 +++++++++++++---------
 7 files changed, 77 insertions(+), 55 deletions(-)

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

diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts
index c4cb6721a..89e6183d6 100644
--- a/src/client/util/DocumentManager.ts
+++ b/src/client/util/DocumentManager.ts
@@ -85,20 +85,19 @@ export class DocumentManager {
 
     @computed
     public get LinkedDocumentViews() {
+        console.log("link");
         return DocumentManager.Instance.DocumentViews.filter(dv => dv.isSelected() || BoolCast(dv.props.Document.libraryBrush, false)).reduce((pairs, dv) => {
             let linksList = LinkManager.Instance.findAllRelatedLinks(dv.props.Document);
-            // let linksList = DocListCast(dv.props.Document.linkedToDocs);
-            if (linksList && linksList.length) {
-                pairs.push(...linksList.reduce((pairs, link) => {
-                    if (link) {
-                        let linkToDoc = LinkManager.Instance.findOppositeAnchor(link, dv.props.Document);
-                        // console.log("FOUND ", DocumentManager.Instance.getDocumentViews(linkToDoc).length, "DOCVIEWS FOR", StrCast(linkToDoc.title), "WITH SOURCE", StrCast(dv.props.Document.title));
-                        DocumentManager.Instance.getDocumentViews(linkToDoc).map(docView1 =>
-                            pairs.push({ a: dv, b: docView1, l: link }));
-                    }
-                    return pairs;
-                }, [] as { a: DocumentView, b: DocumentView, l: Doc }[]));
-            }
+            pairs.push(...linksList.reduce((pairs, link) => {
+                if (link) {
+                    let linkToDoc = LinkManager.Instance.findOppositeAnchor(link, dv.props.Document);
+                    DocumentManager.Instance.getDocumentViews(linkToDoc).map(docView1 => {
+                        pairs.push({ a: dv, b: docView1, l: link });
+                    });
+                }
+                return pairs;
+            }, [] as { a: DocumentView, b: DocumentView, l: Doc }[]));
+            // }
             return pairs;
         }, [] as { a: DocumentView, b: DocumentView, l: Doc }[]);
     }
diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts
index 2abcff4f7..f4c8adc8e 100644
--- a/src/client/util/DragManager.ts
+++ b/src/client/util/DragManager.ts
@@ -7,7 +7,7 @@ import * as globalCssVariables from "../views/globalCssVariables.scss";
 import { LinkManager } from "./LinkManager";
 import { URLField } from "../../new_fields/URLField";
 import { SelectionManager } from "./SelectionManager";
-import { Docs } from "../documents/Documents";
+import { Docs, DocUtils } from "../documents/Documents";
 import { DocumentManager } from "./DocumentManager";
 
 export type dropActionType = "alias" | "copy" | undefined;
@@ -51,7 +51,7 @@ export async function DragLinkAsDocument(dragEle: HTMLElement, x: number, y: num
     let moddrag = await Cast(draggeddoc.annotationOn, Doc);
     let dragData = new DragManager.DocumentDragData(moddrag ? [moddrag] : [draggeddoc]);
     dragData.dropAction = "alias" as dropActionType;
-    DragManager.StartLinkedDocumentDrag([dragEle], dragData, x, y, {
+    DragManager.StartLinkedDocumentDrag([dragEle], sourceDoc, dragData, x, y, {
         handlers: {
             dragComplete: action(emptyFunction),
         },
@@ -95,7 +95,7 @@ export async function DragLinksAsDocuments(dragEle: HTMLElement, x: number, y: n
         //                 dragData.draggedDocuments
         //         );
         //     });
-        DragManager.StartLinkedDocumentDrag([dragEle], dragData, x, y, {
+        DragManager.StartLinkedDocumentDrag([dragEle], sourceDoc, dragData, x, y, {
             handlers: {
                 dragComplete: action(emptyFunction),
             },
@@ -223,7 +223,7 @@ export namespace DragManager {
             });
     }
 
-    export function StartLinkedDocumentDrag(eles: HTMLElement[], dragData: DocumentDragData, downX: number, downY: number, options?: DragOptions) {
+    export function StartLinkedDocumentDrag(eles: HTMLElement[], sourceDoc: Doc, dragData: DocumentDragData, downX: number, downY: number, options?: DragOptions) {
 
         runInAction(() => StartDragFunctions.map(func => func()));
         StartDrag(eles, dragData, downX, downY, options,
@@ -233,16 +233,12 @@ export namespace DragManager {
                     // console.log("DRAG", StrCast(d.title));
 
                     if (dv) {
-                        console.log("DRAG", StrCast(d.title), "has view");
                         if (dv.props.ContainingCollectionView === SelectionManager.SelectedDocuments()[0].props.ContainingCollectionView) {
-                            console.log("DRAG", StrCast(d.title), "same");
                             return d;
                         } else {
-                            console.log("DRAG", StrCast(d.title), "diff");
                             return Doc.MakeAlias(d);
                         }
                     } else {
-                        console.log("DRAG", StrCast(d.title), "has no view");
                         return Doc.MakeAlias(d);
                     }
                     // return (dv && dv.props.ContainingCollectionView !== SelectionManager.SelectedDocuments()[0].props.ContainingCollectionView) || !dv ?
diff --git a/src/client/util/LinkManager.ts b/src/client/util/LinkManager.ts
index fef996bb9..745255f31 100644
--- a/src/client/util/LinkManager.ts
+++ b/src/client/util/LinkManager.ts
@@ -40,9 +40,9 @@ export class LinkManager {
         return LinkManager.Instance.allLinks.filter(link => {
             let protomatch1 = Doc.AreProtosEqual(anchor, Cast(link.anchor1, Doc, new Doc));
             let protomatch2 = Doc.AreProtosEqual(anchor, Cast(link.anchor2, Doc, new Doc));
-            let idmatch1 = StrCast(anchor[Id]) === StrCast(Cast(link.anchor1, Doc, new Doc)[Id]);
-            let idmatch2 = StrCast(anchor[Id]) === StrCast(Cast(link.anchor2, Doc, new Doc)[Id]);
-            return protomatch1 || protomatch2 || idmatch1 || idmatch2;
+            // let idmatch1 = StrCast(anchor[Id]) === StrCast(Cast(link.anchor1, Doc, new Doc)[Id]);
+            // let idmatch2 = StrCast(anchor[Id]) === StrCast(Cast(link.anchor2, Doc, new Doc)[Id]);
+            return protomatch1 || protomatch2;// || idmatch1 || idmatch2;
         });
     }
 
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
index 5c7f080e0..5dc3b5c16 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
@@ -9,6 +9,7 @@ import v5 = require("uuid/v5");
 export interface CollectionFreeFormLinkViewProps {
     A: Doc;
     B: Doc;
+    // LinkDoc: Doc;
     LinkDocs: Doc[];
     addDocument: (document: Doc, allowDuplicates?: boolean) => boolean;
     removeDocument: (document: Doc) => boolean;
@@ -25,18 +26,18 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo
             let y1 = NumCast(a.y) + (BoolCast(a.isMinimized, false) ? 5 : a[HeightSym]() / 2);
             let x2 = NumCast(b.x) + (BoolCast(b.isMinimized, false) ? 5 : b[WidthSym]() / 2);
             let y2 = NumCast(b.y) + (BoolCast(b.isMinimized, false) ? 5 : b[HeightSym]() / 2);
-            this.props.LinkDocs.map(l => {
-                let width = l[WidthSym]();
-                l.x = (x1 + x2) / 2 - width / 2;
-                l.y = (y1 + y2) / 2 + 10;
-                if (!this.props.removeDocument(l)) this.props.addDocument(l, false);
-            });
+            // this.props.LinkDocs.map(l => {
+            //     let width = l[WidthSym]();
+            //     l.x = (x1 + x2) / 2 - width / 2;
+            //     l.y = (y1 + y2) / 2 + 10;
+            //     if (!this.props.removeDocument(l)) this.props.addDocument(l, false);
+            // });
             e.stopPropagation();
             e.preventDefault();
         }
     }
     render() {
-        let l = this.props.LinkDocs;
+        // let l = this.props.LinkDocs;
         let a = this.props.A;
         let b = this.props.B;
         let x1 = NumCast(a.x) + (BoolCast(a.isMinimized, false) ? 5 : NumCast(a.width) / NumCast(a.zoomBasis, 1) / 2);
@@ -44,14 +45,13 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo
         let x2 = NumCast(b.x) + (BoolCast(b.isMinimized, false) ? 5 : NumCast(b.width) / NumCast(b.zoomBasis, 1) / 2);
         let y2 = NumCast(b.y) + (BoolCast(b.isMinimized, false) ? 5 : NumCast(b.height) / NumCast(b.zoomBasis, 1) / 2);
         let text = "";
-        let first = this.props.LinkDocs[0];
-        if (this.props.LinkDocs.length === 1) text += first.title + (first.linkDescription ? "(" + StrCast(first.linkDescription) + ")" : "");
-        else text = "-multiple-";
-        text = "";
+        // let first = this.props.LinkDocs[0];
+        // if (this.props.LinkDocs.length === 1) text += first.title + (first.linkDescription ? "(" + StrCast(first.linkDescription) + ")" : "");
+        // else text = "-multiple-";
         return (
             <>
                 <line key="linkLine" className="collectionfreeformlinkview-linkLine"
-                    style={{ strokeWidth: `${2 * l.length / 2}` }}
+                    style={{ strokeWidth: `${2 * 1 / 2}` }}
                     x1={`${x1}`} y1={`${y1}`}
                     x2={`${x2}`} y2={`${y2}`} />
                 {/* <circle key="linkCircle" className="collectionfreeformlinkview-linkCircle"
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
index 854122592..76e089f08 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
@@ -98,11 +98,20 @@ export class CollectionFreeFormLinksView extends React.Component<CollectionViewP
         //     console.log("CONNECTION", StrCast(d.a.props.Document.title), StrCast(d.b.props.Document.title));
         // });
 
+        console.log("CONNECTIONS");
+
         let connections = DocumentManager.Instance.LinkedDocumentViews.reduce((drawnPairs, connection) => {
             let srcViews = this.documentAnchors(connection.a);
             let targetViews = this.documentAnchors(connection.b);
+
+            console.log(srcViews.length, targetViews.length);
             let possiblePairs: { a: Doc, b: Doc, }[] = [];
-            srcViews.map(sv => targetViews.map(tv => possiblePairs.push({ a: sv.props.Document, b: tv.props.Document })));
+            srcViews.map(sv => {
+                targetViews.map(tv => {
+                    console.log("PUSH", StrCast(sv.props.Document.title), StrCast(sv.props.Document.id), StrCast(tv.props.Document.title), StrCast(tv.props.Document.id));
+                    possiblePairs.push({ a: sv.props.Document, b: tv.props.Document });
+                });
+            });
             possiblePairs.map(possiblePair => {
                 if (!drawnPairs.reduce((found, drawnPair) => {
                     let match1 = (Doc.AreProtosEqual(possiblePair.a, drawnPair.a) && Doc.AreProtosEqual(possiblePair.b, drawnPair.b));
@@ -123,9 +132,17 @@ export class CollectionFreeFormLinksView extends React.Component<CollectionViewP
             return <CollectionFreeFormLinkView key={x} A={c.a} B={c.b} LinkDocs={c.l}
                 removeDocument={this.props.removeDocument} addDocument={this.props.addDocument} />;
         });
+
+        // return DocumentManager.Instance.LinkedDocumentViews.map(c => {
+        //     // let x = c.l.reduce((p, l) => p + l[Id], "");
+        //     let x = c.a.Document[Id] + c.b.Document[Id];
+        //     return <CollectionFreeFormLinkView key={x} A={c.a.props.Document} B={c.b.props.Document} LinkDoc={c.l}
+        //         removeDocument={this.props.removeDocument} addDocument={this.props.addDocument} />;
+        // });
     }
 
     render() {
+        console.log("\n");
         return (
             <div className="collectionfreeformlinksview-container">
                 <svg className="collectionfreeformlinksview-svgCanvas">
diff --git a/src/client/views/nodes/LinkEditor.scss b/src/client/views/nodes/LinkEditor.scss
index 760887fa2..2602b8816 100644
--- a/src/client/views/nodes/LinkEditor.scss
+++ b/src/client/views/nodes/LinkEditor.scss
@@ -30,7 +30,7 @@
     height: 20px;
     margin-left: 6px;
     padding: 0;
-    font-size: 12px;
+    // font-size: 12px;
     border-radius: 10px;
 
     &:disabled {
diff --git a/src/client/views/nodes/LinkEditor.tsx b/src/client/views/nodes/LinkEditor.tsx
index 0d13c6cc8..95199bae2 100644
--- a/src/client/views/nodes/LinkEditor.tsx
+++ b/src/client/views/nodes/LinkEditor.tsx
@@ -7,13 +7,13 @@ import { Doc } from "../../../new_fields/Doc";
 import { LinkManager } from "../../util/LinkManager";
 import { Docs } from "../../documents/Documents";
 import { Utils } from "../../../Utils";
-import { faArrowLeft, faEllipsisV, faTable, faTrash, faCog } from '@fortawesome/free-solid-svg-icons';
+import { faArrowLeft, faEllipsisV, faTable, faTrash, faCog, faExchangeAlt, faTimes, faPlus } from '@fortawesome/free-solid-svg-icons';
 import { library } from "@fortawesome/fontawesome-svg-core";
 import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
 import { SetupDrag } from "../../util/DragManager";
 import { anchorPoints, Flyout } from "../DocumentDecorations";
 
-library.add(faArrowLeft, faEllipsisV, faTable, faTrash, faCog);
+library.add(faArrowLeft, faEllipsisV, faTable, faTrash, faCog, faExchangeAlt, faTimes, faPlus);
 
 
 interface GroupTypesDropdownProps {
@@ -131,7 +131,7 @@ class LinkMetadataEditor extends React.Component<LinkMetadataEditorProps> {
             <div className="linkEditor-metadata-row">
                 <input className={this._keyError ? "linkEditor-error" : ""} type="text" value={this._key} placeholder="key" onChange={e => this.setMetadataKey(e.target.value)}></input>:
                 <input type="text" value={this._value} placeholder="value" onChange={e => this.setMetadataValue(e.target.value)}></input>
-                <button onClick={() => this.removeMetadata()}>x</button>
+                <button onClick={() => this.removeMetadata()}><FontAwesomeIcon icon="times" size="sm" /></button>
             </div>
         );
     }
@@ -266,6 +266,29 @@ export class LinkEditor extends React.Component<LinkEditorProps> {
     renderGroup = (groupId: string, groupDoc: Doc): JSX.Element => {
         let type = StrCast(groupDoc.type);
         if ((type && LinkManager.Instance.groupMetadataKeys.get(type)) || type === "New Group") {
+            let buttons;
+            if (type === "New Group") {
+                buttons = (
+                    <>
+                        <button className="linkEditor-button" disabled={true} title="Add KVP"><FontAwesomeIcon icon="plus" size="sm" /></button>
+                        <button className="linkEditor-button" disabled title="Copy group to opposite anchor"><FontAwesomeIcon icon="exchange-alt" size="sm" /></button>
+                        <button className="linkEditor-button" onClick={() => this.removeGroupFromLink(groupId, type)} title="Remove group from link"><FontAwesomeIcon icon="times" size="sm" /></button>
+                        <button className="linkEditor-button" disabled title="Delete group"><FontAwesomeIcon icon="trash" size="sm" /></button>
+                        <button className="linkEditor-button" disabled><FontAwesomeIcon icon="table" size="sm" /></button>
+                    </>
+                );
+            } else {
+                buttons = (
+                    <>
+                        <button className="linkEditor-button" onClick={() => this.addMetadata(StrCast(groupDoc.proto!.type))} title="Add KVP"><FontAwesomeIcon icon="plus" size="sm" /></button>
+                        <button className="linkEditor-button" onClick={() => this.copyGroup(groupId, type)} title="Copy group to opposite anchor"><FontAwesomeIcon icon="exchange-alt" size="sm" /></button>
+                        <button className="linkEditor-button" onClick={() => this.removeGroupFromLink(groupId, type)} title="Remove group from link"><FontAwesomeIcon icon="times" size="sm" /></button>
+                        <button className="linkEditor-button" onClick={() => this.deleteGroup(groupId, type)} title="Delete group"><FontAwesomeIcon icon="trash" size="sm" /></button>
+                        {this.viewGroupAsTable(groupId, type)}
+                    </>
+                );
+            }
+
             return (
                 <div key={groupId} className="linkEditor-group">
                     <div className="linkEditor-group-row">
@@ -274,20 +297,7 @@ export class LinkEditor extends React.Component<LinkEditorProps> {
                     </div>
                     {this.renderMetadata(groupId)}
                     <div className="linkEditor-group-buttons">
-                        {groupDoc.type === "New Group" ? <button className="linkEditor-button" disabled={true} title="Add KVP">+</button> :
-                            <button className="linkEditor-button" onClick={() => this.addMetadata(StrCast(groupDoc.proto!.type))} title="Add KVP">+</button>}
-                        {this.viewGroupAsTable(groupId, type)}
-                        <Flyout
-                            anchorPoint={anchorPoints.LEFT_TOP}
-                            content={
-                                <>
-                                    <button className="linkEditor-groupOpts" onClick={() => this.copyGroup(groupId, type)} title="Copy group to opposite anchor">↔</button>
-                                    <button className="linkEditor-groupOpts" onClick={() => this.removeGroupFromLink(groupId, type)} title="Remove group from link">x</button>
-                                    <button className="linkEditor-groupOpts" onClick={() => this.deleteGroup(groupId, type)} title="Delete group">xx</button>
-                                </>
-                            }>
-                            <button className="linkEditor-button" title="Delete group"><FontAwesomeIcon icon="cog" size="sm" /></button>
-                        </Flyout >
+                        {buttons}
                     </div>
                 </div>
             );
@@ -345,7 +355,7 @@ export class LinkEditor extends React.Component<LinkEditorProps> {
                 </div>
                 <div className="linkEditor-groupsLabel">
                     <b>Relationships:</b>
-                    <button className="linkEditor-button" onClick={() => this.addGroup()} title=" Add Group">+</button>
+                    <button className="linkEditor-button" onClick={() => this.addGroup()} title=" Add Group"><FontAwesomeIcon icon="plus" size="sm" /></button>
                 </div>
                 {groups.length > 0 ? groups : <div className="linkEditor-group">There are currently no relationships associated with this link.</div>}
             </div>
-- 
cgit v1.2.3-70-g09d2


From 41cf1e8536964764f18ab752140e484e36cbe464 Mon Sep 17 00:00:00 2001
From: Fawn <fangrui_tong@brown.edu>
Date: Tue, 25 Jun 2019 17:09:36 -0400
Subject: links can save

---
 src/client/documents/Documents.ts                  |  58 +---
 src/client/util/DocumentManager.ts                 |  14 +-
 src/client/util/DragManager.ts                     |  24 +-
 src/client/util/LinkManager.ts                     | 239 +++++++++++----
 src/client/views/DocumentDecorations.tsx           |   2 +-
 src/client/views/MainView.tsx                      |   7 +
 .../views/collections/CollectionDockingView.tsx    |   4 +-
 .../views/collections/CollectionTreeView.tsx       |  18 +-
 .../CollectionFreeFormLinksView.tsx                |   7 +-
 src/client/views/nodes/DocumentView.tsx            |   8 +-
 src/client/views/nodes/LinkButtonBox.scss          |  34 +--
 src/client/views/nodes/LinkButtonBox.tsx           | 126 ++++----
 src/client/views/nodes/LinkEditor.tsx              | 323 +++++++++------------
 src/client/views/nodes/LinkMenu.tsx                |   2 +-
 src/client/views/nodes/LinkMenuGroup.tsx           |   8 +-
 src/client/views/nodes/LinkMenuItem.tsx            |   6 +-
 src/new_fields/Doc.ts                              |   9 +-
 src/new_fields/LinkButtonField.ts                  |  58 ++--
 18 files changed, 503 insertions(+), 444 deletions(-)

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

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 64032e096..fbd96fb66 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -35,8 +35,8 @@ import { DateField } from "../../new_fields/DateField";
 import { UndoManager } from "../util/UndoManager";
 import { RouteStore } from "../../server/RouteStore";
 import { LinkManager } from "../util/LinkManager";
-import { LinkButtonBox } from "../views/nodes/LinkButtonBox";
-import { LinkButtonField, LinkButtonData } from "../../new_fields/LinkButtonField";
+// import { LinkButtonBox } from "../views/nodes/LinkButtonBox";
+// import { LinkButtonField, LinkButtonData } from "../../new_fields/LinkButtonField";
 import { DocumentManager } from "../util/DocumentManager";
 import { Id } from "../../new_fields/FieldSymbols";
 var requestImageSize = require('request-image-size');
@@ -100,6 +100,7 @@ export namespace DocUtils {
             let linkDoc = Docs.TextDocument({ width: 100, height: 30, borderRounding: -1 });
             let linkDocProto = Doc.GetProto(linkDoc);
 
+            linkDocProto.context = targetContext;
             linkDocProto.title = title; //=== "" ? source.title + " to " + target.title : title;
             linkDocProto.linkDescription = description;
             linkDocProto.linkTags = tags;
@@ -111,36 +112,7 @@ export namespace DocUtils {
             linkDocProto.anchor2Page = target.curPage;
             linkDocProto.anchor2Groups = new List<Doc>([]);
 
-            linkDocProto.context = targetContext;
-
-            let sourceViews = DocumentManager.Instance.getDocumentViews(source);
-            let targetViews = DocumentManager.Instance.getDocumentViews(target);
-            sourceViews.forEach(sv => {
-                targetViews.forEach(tv => {
-
-                    // TODO: do only for when diff contexts
-                    let proxy1 = Docs.LinkButtonDocument(
-                        { sourceViewId: StrCast(sv.props.Document[Id]), targetViewId: StrCast(tv.props.Document[Id]) },
-                        { width: 200, height: 100, borderRounding: 0 });
-                    let proxy1Proto = Doc.GetProto(proxy1);
-                    proxy1Proto.sourceViewId = StrCast(sv.props.Document[Id]);
-                    proxy1Proto.targetViewId = StrCast(tv.props.Document[Id]);
-                    proxy1Proto.isLinkButton = true;
-
-                    let proxy2 = Docs.LinkButtonDocument(
-                        { sourceViewId: StrCast(tv.props.Document[Id]), targetViewId: StrCast(sv.props.Document[Id]) },
-                        { width: 200, height: 100, borderRounding: 0 });
-                    let proxy2Proto = Doc.GetProto(proxy2);
-                    proxy2Proto.sourceViewId = StrCast(tv.props.Document[Id]);
-                    proxy2Proto.targetViewId = StrCast(sv.props.Document[Id]);
-                    proxy2Proto.isLinkButton = true;
-
-                    LinkManager.Instance.linkProxies.push(proxy1);
-                    LinkManager.Instance.linkProxies.push(proxy2);
-                });
-            });
-
-            LinkManager.Instance.allLinks.push(linkDoc);
+            LinkManager.Instance.addLink(linkDoc);
 
             return linkDoc;
         }, "make link");
@@ -160,7 +132,7 @@ export namespace Docs {
     let audioProto: Doc;
     let pdfProto: Doc;
     let iconProto: Doc;
-    let linkProto: Doc;
+    // let linkProto: Doc;
     const textProtoId = "textProto";
     const histoProtoId = "histoProto";
     const pdfProtoId = "pdfProto";
@@ -171,7 +143,7 @@ export namespace Docs {
     const videoProtoId = "videoProto";
     const audioProtoId = "audioProto";
     const iconProtoId = "iconProto";
-    const linkProtoId = "linkProto";
+    // const linkProtoId = "linkProto";
 
     export function initProtos(): Promise<void> {
         return DocServer.GetRefFields([textProtoId, histoProtoId, collProtoId, imageProtoId, webProtoId, kvpProtoId, videoProtoId, audioProtoId, pdfProtoId, iconProtoId]).then(fields => {
@@ -185,7 +157,7 @@ export namespace Docs {
             audioProto = fields[audioProtoId] as Doc || CreateAudioPrototype();
             pdfProto = fields[pdfProtoId] as Doc || CreatePdfPrototype();
             iconProto = fields[iconProtoId] as Doc || CreateIconPrototype();
-            linkProto = fields[linkProtoId] as Doc || CreateLinkPrototype();
+            // linkProto = fields[linkProtoId] as Doc || CreateLinkPrototype();
         });
     }
 
@@ -218,11 +190,11 @@ export namespace Docs {
             { x: 0, y: 0, width: Number(MINIMIZED_ICON_SIZE), height: Number(MINIMIZED_ICON_SIZE) });
         return iconProto;
     }
-    function CreateLinkPrototype(): Doc {
-        let linkProto = setupPrototypeOptions(linkProtoId, "LINK_PROTO", LinkButtonBox.LayoutString(),
-            { x: 0, y: 0, width: 300 });
-        return linkProto;
-    }
+    // function CreateLinkPrototype(): Doc {
+    //     let linkProto = setupPrototypeOptions(linkProtoId, "LINK_PROTO", LinkButtonBox.LayoutString(),
+    //         { x: 0, y: 0, width: 300 });
+    //     return linkProto;
+    // }
     function CreateTextPrototype(): Doc {
         let textProto = setupPrototypeOptions(textProtoId, "TEXT_PROTO", FormattedTextBox.LayoutString(),
             { x: 0, y: 0, width: 300, backgroundColor: "#f1efeb" });
@@ -309,9 +281,9 @@ export namespace Docs {
     export function IconDocument(icon: string, options: DocumentOptions = {}) {
         return CreateInstance(iconProto, new IconField(icon), options);
     }
-    export function LinkButtonDocument(data: LinkButtonData, options: DocumentOptions = {}) {
-        return CreateInstance(linkProto, new LinkButtonField(data), options);
-    }
+    // export function LinkButtonDocument(data: LinkButtonData, options: DocumentOptions = {}) {
+    //     return CreateInstance(linkProto, new LinkButtonField(data), options);
+    // }
     export function PdfDocument(url: string, options: DocumentOptions = {}) {
         return CreateInstance(pdfProto, new PdfField(new URL(url)), options);
     }
diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts
index 89e6183d6..767abe63f 100644
--- a/src/client/util/DocumentManager.ts
+++ b/src/client/util/DocumentManager.ts
@@ -85,12 +85,11 @@ export class DocumentManager {
 
     @computed
     public get LinkedDocumentViews() {
-        console.log("link");
-        return DocumentManager.Instance.DocumentViews.filter(dv => dv.isSelected() || BoolCast(dv.props.Document.libraryBrush, false)).reduce((pairs, dv) => {
-            let linksList = LinkManager.Instance.findAllRelatedLinks(dv.props.Document);
+        let pairs = DocumentManager.Instance.DocumentViews.filter(dv => dv.isSelected() || BoolCast(dv.props.Document.libraryBrush, false)).reduce((pairs, dv) => {
+            let linksList = LinkManager.Instance.getAllRelatedLinks(dv.props.Document);
             pairs.push(...linksList.reduce((pairs, link) => {
                 if (link) {
-                    let linkToDoc = LinkManager.Instance.findOppositeAnchor(link, dv.props.Document);
+                    let linkToDoc = LinkManager.Instance.getOppositeAnchor(link, dv.props.Document);
                     DocumentManager.Instance.getDocumentViews(linkToDoc).map(docView1 => {
                         pairs.push({ a: dv, b: docView1, l: link });
                     });
@@ -100,6 +99,13 @@ export class DocumentManager {
             // }
             return pairs;
         }, [] as { a: DocumentView, b: DocumentView, l: Doc }[]);
+
+        // console.log("LINKED DOCUMENT VIEWS");
+        // pairs.forEach(p => {
+        //     console.log(StrCast(p.a.Document.title), p.a.props.Document[Id], StrCast(p.b.Document.title), p.b.props.Document[Id]);
+        // });
+
+        return pairs;
     }
 
 
diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts
index f4c8adc8e..1aacf2c53 100644
--- a/src/client/util/DragManager.ts
+++ b/src/client/util/DragManager.ts
@@ -46,7 +46,7 @@ export function SetupDrag(_reference: React.RefObject<HTMLElement>, docFunc: ()
 }
 
 export async function DragLinkAsDocument(dragEle: HTMLElement, x: number, y: number, linkDoc: Doc, sourceDoc: Doc) {
-    let draggeddoc = LinkManager.Instance.findOppositeAnchor(linkDoc, sourceDoc);
+    let draggeddoc = LinkManager.Instance.getOppositeAnchor(linkDoc, sourceDoc);
 
     let moddrag = await Cast(draggeddoc.annotationOn, Doc);
     let dragData = new DragManager.DocumentDragData(moddrag ? [moddrag] : [draggeddoc]);
@@ -66,10 +66,10 @@ export async function DragLinksAsDocuments(dragEle: HTMLElement, x: number, y: n
     // TODO: if not in same context then don't drag
 
     if (srcTarg) {
-        let linkDocs = LinkManager.Instance.findAllRelatedLinks(srcTarg);
+        let linkDocs = LinkManager.Instance.getAllRelatedLinks(srcTarg);
         if (linkDocs) {
             draggedDocs = linkDocs.map(link => {
-                return LinkManager.Instance.findOppositeAnchor(link, sourceDoc);
+                return LinkManager.Instance.getOppositeAnchor(link, sourceDoc);
             });
         }
     }
@@ -236,10 +236,16 @@ export namespace DragManager {
                         if (dv.props.ContainingCollectionView === SelectionManager.SelectedDocuments()[0].props.ContainingCollectionView) {
                             return d;
                         } else {
-                            return Doc.MakeAlias(d);
+                            // return d;
+                            let r = Doc.MakeAlias(d);
+                            // DocUtils.MakeLink(sourceDoc, r);
+                            return r;
                         }
                     } else {
-                        return Doc.MakeAlias(d);
+                        // return d;
+                        let r = Doc.MakeAlias(d);
+                        // DocUtils.MakeLink(sourceDoc, r);
+                        return r;
                     }
                     // return (dv && dv.props.ContainingCollectionView !== SelectionManager.SelectedDocuments()[0].props.ContainingCollectionView) || !dv ?
                     //     Doc.MakeAlias(d) : d;
@@ -282,10 +288,10 @@ export namespace DragManager {
         StartDrag([ele], dragData, downX, downY, options);
     }
 
-    export function StartLinkProxyDrag(ele: HTMLElement, dragData: DocumentDragData, downX: number, downY: number, options?: DragOptions) {
-        runInAction(() => StartDragFunctions.map(func => func()));
-        StartDrag([ele], dragData, downX, downY, options);
-    }
+    // export function StartLinkProxyDrag(ele: HTMLElement, dragData: DocumentDragData, downX: number, downY: number, options?: DragOptions) {
+    //     runInAction(() => StartDragFunctions.map(func => func()));
+    //     StartDrag([ele], dragData, downX, downY, options);
+    // }
 
     export let AbortDrag: () => void = emptyFunction;
 
diff --git a/src/client/util/LinkManager.ts b/src/client/util/LinkManager.ts
index 745255f31..82c3a9acd 100644
--- a/src/client/util/LinkManager.ts
+++ b/src/client/util/LinkManager.ts
@@ -1,9 +1,10 @@
 import { observable, action } from "mobx";
-import { StrCast, Cast } from "../../new_fields/Types";
+import { StrCast, Cast, FieldValue } from "../../new_fields/Types";
 import { Doc, DocListCast } from "../../new_fields/Doc";
 import { listSpec } from "../../new_fields/Schema";
 import { List } from "../../new_fields/List";
 import { Id } from "../../new_fields/FieldSymbols";
+import { CurrentUserUtils } from "../../server/authentication/models/current_user_utils";
 
 
 /* 
@@ -23,6 +24,11 @@ import { Id } from "../../new_fields/FieldSymbols";
  * - user defined kvps 
  */
 export class LinkManager {
+    // static Instance: LinkManager;
+    // private constructor() {
+    //     LinkManager.Instance = this;
+    // }
+
     private static _instance: LinkManager;
     public static get Instance(): LinkManager {
         return this._instance || (this._instance = new this());
@@ -30,25 +36,138 @@ export class LinkManager {
     private constructor() {
     }
 
-    @observable public allLinks: Array<Doc> = []; // list of link docs
-    @observable public groupMetadataKeys: Map<string, Array<string>> = new Map();
-    // map of group type to list of its metadata keys; serves as a dictionary of groups to what kind of metadata it hodls
-    @observable public linkProxies: Array<Doc> = []; // list of linkbutton docs - used to visualize link when an anchors are not in the same context
+    public get LinkManagerDoc(): Doc | undefined {
+        return FieldValue(Cast(CurrentUserUtils.UserDocument.linkManagerDoc, Doc));
+    }
+    // @observable public allLinks: Array<Doc> = []; //List<Doc> = new List<Doc>([]); // list of link docs
+    // @observable public groupMetadataKeys: Map<string, Array<string>> = new Map();
+    // map of group type to list of its metadata keys; serves as a dictionary of groups to what kind of metadata it holds
+
+    public getAllLinks(): Doc[] {
+        return LinkManager.Instance.LinkManagerDoc ? LinkManager.Instance.LinkManagerDoc.allLinks ? DocListCast(LinkManager.Instance.LinkManagerDoc.allLinks) : [] : [];
+    }
+
+    public addLink(linkDoc: Doc): boolean {
+        let linkList = LinkManager.Instance.getAllLinks();
+        linkList.push(linkDoc);
+        console.log("link man doc", LinkManager.Instance.LinkManagerDoc);
+        if (LinkManager.Instance.LinkManagerDoc) {
+            LinkManager.Instance.LinkManagerDoc.allLinks = new List<Doc>(linkList);
+            return true;
+        }
+        return false;
+    }
+
+    public deleteLink(linkDoc: Doc): boolean {
+        let linkList = LinkManager.Instance.getAllLinks();
+        let index = LinkManager.Instance.getAllLinks().indexOf(linkDoc);
+        if (index > -1) {
+            linkList.splice(index, 1);
+            if (LinkManager.Instance.LinkManagerDoc) {
+                LinkManager.Instance.LinkManagerDoc.allLinks = new List<Doc>(linkList);
+                return true;
+            }
+        }
+        return false;
+    }
 
     // finds all links that contain the given anchor
-    public findAllRelatedLinks(anchor: Doc): Array<Doc> {
-        return LinkManager.Instance.allLinks.filter(link => {
+    public getAllRelatedLinks(anchor: Doc): Doc[] {//List<Doc> {
+        let related = LinkManager.Instance.getAllLinks().filter(link => {
             let protomatch1 = Doc.AreProtosEqual(anchor, Cast(link.anchor1, Doc, new Doc));
             let protomatch2 = Doc.AreProtosEqual(anchor, Cast(link.anchor2, Doc, new Doc));
-            // let idmatch1 = StrCast(anchor[Id]) === StrCast(Cast(link.anchor1, Doc, new Doc)[Id]);
-            // let idmatch2 = StrCast(anchor[Id]) === StrCast(Cast(link.anchor2, Doc, new Doc)[Id]);
-            return protomatch1 || protomatch2;// || idmatch1 || idmatch2;
+            return protomatch1 || protomatch2;
         });
+        return related;
+    }
+
+    public addGroupType(groupType: string): boolean {
+        if (LinkManager.Instance.LinkManagerDoc) {
+            LinkManager.Instance.LinkManagerDoc[groupType] = new List<string>([]);
+            let groupTypes = LinkManager.Instance.getAllGroupTypes();
+            groupTypes.push(groupType);
+            LinkManager.Instance.LinkManagerDoc.allGroupTypes = new List<string>(groupTypes);
+            return true;
+        }
+        return false;
+    }
+
+    // removes all group docs from all links with the given group type
+    public deleteGroupType(groupType: string): boolean {
+        if (LinkManager.Instance.LinkManagerDoc) {
+            if (LinkManager.Instance.LinkManagerDoc[groupType]) {
+                LinkManager.Instance.LinkManagerDoc[groupType] = undefined;
+                LinkManager.Instance.getAllLinks().forEach(linkDoc => {
+                    LinkManager.Instance.removeGroupFromAnchor(linkDoc, Cast(linkDoc.anchor1, Doc, new Doc), groupType);
+                    LinkManager.Instance.removeGroupFromAnchor(linkDoc, Cast(linkDoc.anchor2, Doc, new Doc), groupType);
+                });
+            }
+            return true;
+        } else return false;
+    }
+
+    public getAllGroupTypes(): string[] {
+        if (LinkManager.Instance.LinkManagerDoc) {
+            if (LinkManager.Instance.LinkManagerDoc.allGroupTypes) {
+                return Cast(LinkManager.Instance.LinkManagerDoc.allGroupTypes, listSpec("string"), []);
+            } else {
+                LinkManager.Instance.LinkManagerDoc.allGroupTypes = new List<string>([]);
+                return [];
+            }
+        }
+        return [];
+    }
+
+    // gets the groups associates with an anchor in a link
+    public getAnchorGroups(linkDoc: Doc, anchor: Doc): Array<Doc> {
+        if (Doc.AreProtosEqual(anchor, Cast(linkDoc.anchor1, Doc, new Doc))) {
+            return DocListCast(linkDoc.anchor1Groups);
+        } else {
+            return DocListCast(linkDoc.anchor2Groups);
+        }
+    }
+
+    // sets the groups of the given anchor in the given link
+    public setAnchorGroups(linkDoc: Doc, anchor: Doc, groups: Doc[]) {
+        if (Doc.AreProtosEqual(anchor, Cast(linkDoc.anchor1, Doc, new Doc))) {
+            linkDoc.anchor1Groups = new List<Doc>(groups);
+        } else {
+            linkDoc.anchor2Groups = new List<Doc>(groups);
+        }
+    }
+
+    public addGroupToAnchor(linkDoc: Doc, anchor: Doc, groupDoc: Doc, replace: boolean = false) {
+        let groups = LinkManager.Instance.getAnchorGroups(linkDoc, anchor);
+        let index = groups.findIndex(gDoc => {
+            return StrCast(groupDoc.type).toUpperCase() === StrCast(gDoc.type).toUpperCase();
+        });
+        if (index > -1 && replace) {
+            groups[index] = groupDoc;
+        }
+        if (index === -1) {
+            groups.push(groupDoc);
+        }
+        LinkManager.Instance.setAnchorGroups(linkDoc, anchor, groups);
+    }
+
+    // removes group doc of given group type only from given anchor on given link
+    public removeGroupFromAnchor(linkDoc: Doc, anchor: Doc, groupType: string) {
+        let groups = LinkManager.Instance.getAnchorGroups(linkDoc, anchor);
+        let newGroups = groups.filter(groupDoc => StrCast(groupDoc.type).toUpperCase() !== groupType.toUpperCase());
+        LinkManager.Instance.setAnchorGroups(linkDoc, anchor, newGroups);
     }
 
+    // public doesAnchorHaveGroup(linkDoc: Doc, anchor: Doc, groupDoc: Doc): boolean {
+    //     let groups = LinkManager.Instance.getAnchorGroups(linkDoc, anchor);
+    //     let index = groups.findIndex(gDoc => {
+    //         return StrCast(groupDoc.type).toUpperCase() === StrCast(gDoc.type).toUpperCase();
+    //     });
+    //     return index > -1;
+    // }
+
     // returns map of group type to anchor's links in that group type
-    public findRelatedGroupedLinks(anchor: Doc): Map<string, Array<Doc>> {
-        let related = this.findAllRelatedLinks(anchor);
+    public getRelatedGroupedLinks(anchor: Doc): Map<string, Array<Doc>> {
+        let related = this.getAllRelatedLinks(anchor);
         let anchorGroups = new Map<string, Array<Doc>>();
         related.forEach(link => {
             let groups = LinkManager.Instance.getAnchorGroups(link, anchor);
@@ -73,10 +192,41 @@ export class LinkManager {
         return anchorGroups;
     }
 
+    // public addMetadataKeyToGroup(groupType: string, key: string): boolean {
+    //     if (LinkManager.Instance.LinkManagerDoc) {
+    //         if (LinkManager.Instance.LinkManagerDoc[groupType]) {
+    //             let keyList = LinkManager.Instance.findMetadataKeysInGroup(groupType);
+    //             keyList.push(key);
+    //             LinkManager.Instance.LinkManagerDoc[groupType] = new List<string>(keyList);
+    //             return true;
+    //         }
+    //         return false;
+    //     }
+    //     return false;
+    // }
+
+    public getMetadataKeysInGroup(groupType: string): string[] {
+        if (LinkManager.Instance.LinkManagerDoc) {
+            return LinkManager.Instance.LinkManagerDoc[groupType] ? Cast(LinkManager.Instance.LinkManagerDoc[groupType], listSpec("string"), []) : [];
+        }
+        return [];
+    }
+
+    public setMetadataKeysForGroup(groupType: string, keys: string[]): boolean {
+        if (LinkManager.Instance.LinkManagerDoc) {
+            // if (LinkManager.Instance.LinkManagerDoc[groupType]) {
+            LinkManager.Instance.LinkManagerDoc[groupType] = new List<string>(keys);
+            return true;
+            // }
+            // return false;
+        }
+        return false;
+    }
+
     // returns a list of all metadata docs associated with the given group type
-    public findAllMetadataDocsInGroup(groupType: string): Array<Doc> {
+    public getAllMetadataDocsInGroup(groupType: string): Array<Doc> {
         let md: Doc[] = [];
-        let allLinks = LinkManager.Instance.allLinks;
+        let allLinks = LinkManager.Instance.getAllLinks();
         allLinks.forEach(linkDoc => {
             let anchor1Groups = LinkManager.Instance.getAnchorGroups(linkDoc, Cast(linkDoc.anchor1, Doc, new Doc));
             let anchor2Groups = LinkManager.Instance.getAnchorGroups(linkDoc, Cast(linkDoc.anchor2, Doc, new Doc));
@@ -86,27 +236,9 @@ export class LinkManager {
         return md;
     }
 
-    // removes all group docs from all links with the given group type
-    public deleteGroup(groupType: string): void {
-        let deleted = LinkManager.Instance.groupMetadataKeys.delete(groupType);
-        if (deleted) {
-            LinkManager.Instance.allLinks.forEach(linkDoc => {
-                LinkManager.Instance.removeGroupFromAnchor(linkDoc, Cast(linkDoc.anchor1, Doc, new Doc), groupType);
-                LinkManager.Instance.removeGroupFromAnchor(linkDoc, Cast(linkDoc.anchor2, Doc, new Doc), groupType);
-            });
-        }
-    }
-
-    // removes group doc of given group type only from given anchor on given link
-    public removeGroupFromAnchor(linkDoc: Doc, anchor: Doc, groupType: string) {
-        let groups = LinkManager.Instance.getAnchorGroups(linkDoc, anchor);
-        let newGroups = groups.filter(groupDoc => StrCast(groupDoc.type).toUpperCase() !== groupType.toUpperCase());
-        LinkManager.Instance.setAnchorGroups(linkDoc, anchor, newGroups);
-    }
-
     // checks if a link with the given anchors exists
-    public doesLinkExist(anchor1: Doc, anchor2: Doc) {
-        let allLinks = LinkManager.Instance.allLinks;
+    public doesLinkExist(anchor1: Doc, anchor2: Doc): boolean {
+        let allLinks = LinkManager.Instance.getAllLinks();
         let index = allLinks.findIndex(linkDoc => {
             return (Doc.AreProtosEqual(Cast(linkDoc.anchor1, Doc, new Doc), anchor1) && Doc.AreProtosEqual(Cast(linkDoc.anchor2, Doc, new Doc), anchor2)) ||
                 (Doc.AreProtosEqual(Cast(linkDoc.anchor1, Doc, new Doc), anchor2) && Doc.AreProtosEqual(Cast(linkDoc.anchor2, Doc, new Doc), anchor1));
@@ -115,7 +247,7 @@ export class LinkManager {
     }
 
     // finds the opposite anchor of a given anchor in a link
-    public findOppositeAnchor(linkDoc: Doc, anchor: Doc): Doc {
+    public getOppositeAnchor(linkDoc: Doc, anchor: Doc): Doc {
         if (Doc.AreProtosEqual(anchor, Cast(linkDoc.anchor1, Doc, new Doc))) {
             return Cast(linkDoc.anchor2, Doc, new Doc);
         } else {
@@ -123,34 +255,17 @@ export class LinkManager {
         }
     }
 
-    // gets the groups associates with an anchor in a link
-    public getAnchorGroups(linkDoc: Doc, anchor: Doc): Array<Doc> {
-        if (Doc.AreProtosEqual(anchor, Cast(linkDoc.anchor1, Doc, new Doc))) {
-            return DocListCast(linkDoc.anchor1Groups);
-        } else {
-            return DocListCast(linkDoc.anchor2Groups);
-        }
-    }
 
-    // sets the groups of the given anchor in the given link
-    public setAnchorGroups(linkDoc: Doc, anchor: Doc, groups: Doc[]) {
-        if (Doc.AreProtosEqual(anchor, Cast(linkDoc.anchor1, Doc, new Doc))) {
-            linkDoc.anchor1Groups = new List<Doc>(groups);
-        } else {
-            linkDoc.anchor2Groups = new List<Doc>(groups);
-        }
-    }
-
-    @action
-    public addLinkProxy(proxy: Doc) {
-        LinkManager.Instance.linkProxies.push(proxy);
-    }
+    // @action
+    // public addLinkProxy(proxy: Doc) {
+    //     LinkManager.Instance.linkProxies.push(proxy);
+    // }
 
-    public findLinkProxy(sourceViewId: string, targetViewId: string): Doc | undefined {
-        let index = LinkManager.Instance.linkProxies.findIndex(p => {
-            return StrCast(p.sourceViewId) === sourceViewId && StrCast(p.targetViewId) === targetViewId;
-        });
-        return index > -1 ? LinkManager.Instance.linkProxies[index] : undefined;
-    }
+    // public findLinkProxy(sourceViewId: string, targetViewId: string): Doc | undefined {
+    //     let index = LinkManager.Instance.linkProxies.findIndex(p => {
+    //         return StrCast(p.sourceViewId) === sourceViewId && StrCast(p.targetViewId) === targetViewId;
+    //     });
+    //     return index > -1 ? LinkManager.Instance.linkProxies[index] : undefined;
+    // }
 
 }
\ No newline at end of file
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 926273633..3a2752d7e 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -573,7 +573,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
         let linkButton = null;
         if (SelectionManager.SelectedDocuments().length > 0) {
             let selFirst = SelectionManager.SelectedDocuments()[0];
-            let linkCount = LinkManager.Instance.findAllRelatedLinks(selFirst.props.Document).length;
+            let linkCount = LinkManager.Instance.getAllRelatedLinks(selFirst.props.Document).length;
             linkButton = (<Flyout
                 anchorPoint={anchorPoints.RIGHT_TOP}
                 content={<LinkMenu docView={selFirst}
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 51630c29b..d90e9d23e 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -35,6 +35,8 @@ import { HistoryUtil } from '../util/History';
 import { CollectionBaseView } from './collections/CollectionBaseView';
 import PDFMenu from './pdf/PDFMenu';
 import { InkTool } from '../../new_fields/InkField';
+import { LinkManager } from '../util/LinkManager';
+import { List } from '../../new_fields/List';
 
 
 @observer
@@ -146,6 +148,11 @@ export class MainView extends React.Component {
             let freeformDoc = Docs.FreeformDocument([], { x: 0, y: 400, width: this.pwidth * .7, height: this.pheight, title: `WS collection ${list.length + 1}` });
             var dockingLayout = { content: [{ type: 'row', content: [CollectionDockingView.makeDocumentConfig(CurrentUserUtils.UserDocument, 150), CollectionDockingView.makeDocumentConfig(freeformDoc, 600)] }] };
             let mainDoc = Docs.DockDocument([CurrentUserUtils.UserDocument, freeformDoc], JSON.stringify(dockingLayout), { title: `Workspace ${list.length + 1}` }, id);
+            if (!CurrentUserUtils.UserDocument.linkManagerDoc) {
+                let linkManagerDoc = new Doc();
+                linkManagerDoc.allLinks = new List<Doc>([]);
+                CurrentUserUtils.UserDocument.linkManagerDoc = linkManagerDoc;
+            }
             list.push(mainDoc);
             // bcz: strangely, we need a timeout to prevent exceptions/issues initializing GoldenLayout (the rendering engine for Main Container)
             setTimeout(() => {
diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx
index c82027da5..4140f8029 100644
--- a/src/client/views/collections/CollectionDockingView.tsx
+++ b/src/client/views/collections/CollectionDockingView.tsx
@@ -354,9 +354,9 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
                     tab.reactComponents = [upDiv];
                     tab.element.append(upDiv);
                     counter.DashDocId = tab.contentItem.config.props.documentId;
-                    tab.reactionDisposer = reaction(() => [LinkManager.Instance.findAllRelatedLinks(doc), doc.title],
+                    tab.reactionDisposer = reaction(() => [LinkManager.Instance.getAllRelatedLinks(doc), doc.title],
                         () => {
-                            counter.innerHTML = LinkManager.Instance.findAllRelatedLinks(doc).length;
+                            counter.innerHTML = LinkManager.Instance.getAllRelatedLinks(doc).length;
                             tab.titleElement[0].textContent = doc.title;
                         }, { fireImmediately: true });
                     tab.titleElement[0].DashDocId = tab.contentItem.config.props.documentId;
diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx
index 0b922b3c4..7bc3ad124 100644
--- a/src/client/views/collections/CollectionTreeView.tsx
+++ b/src/client/views/collections/CollectionTreeView.tsx
@@ -73,7 +73,7 @@ class TreeView extends React.Component<TreeViewProps> {
     @undoBatch delete = () => this.props.deleteDoc(this.props.document);
     @undoBatch openRight = async () => this.props.addDocTab(this.props.document, "openRight");
 
-    onPointerDown = (e: React.PointerEvent) => e.stopPropagation()
+    onPointerDown = (e: React.PointerEvent) => e.stopPropagation();
     onPointerEnter = (e: React.PointerEvent): void => {
         this.props.active() && (this.props.document.libraryBrush = true);
         if (e.buttons === 1 && SelectionManager.GetIsDragging()) {
@@ -115,7 +115,7 @@ class TreeView extends React.Component<TreeViewProps> {
         return this.props.document !== target && this.props.deleteDoc(doc) && addDoc(doc);
     }
     @action
-    indent = () => this.props.addDocument(this.props.document) && this.delete();
+    indent = () => this.props.addDocument(this.props.document) && this.delete()
 
     renderBullet() {
         let docList = Cast(this.props.document["data"], listSpec(Doc));
@@ -167,7 +167,7 @@ class TreeView extends React.Component<TreeViewProps> {
                 keyList.push(key);
             }
         });
-        if (LinkManager.Instance.findAllRelatedLinks(this.props.document).length > 0) keyList.push("links");
+        if (LinkManager.Instance.getAllRelatedLinks(this.props.document).length > 0) keyList.push("links");
         if (keyList.indexOf("data") !== -1) {
             keyList.splice(keyList.indexOf("data"), 1);
         }
@@ -281,9 +281,9 @@ class TreeView extends React.Component<TreeViewProps> {
         let ele: JSX.Element[] = [];
         let remDoc = (doc: Doc) => this.remove(doc, this._chosenKey);
         let addDoc = (doc: Doc, addBefore?: Doc, before?: boolean) => TreeView.AddDocToList(this.props.document, this._chosenKey, doc, addBefore, before);
-        let groups = LinkManager.Instance.findRelatedGroupedLinks(this.props.document);
+        let groups = LinkManager.Instance.getRelatedGroupedLinks(this.props.document);
         groups.forEach((groupLinkDocs, groupType) => {
-            let destLinks = groupLinkDocs.map(d => LinkManager.Instance.findOppositeAnchor(d, this.props.document));
+            let destLinks = groupLinkDocs.map(d => LinkManager.Instance.getOppositeAnchor(d, this.props.document));
             ele.push(
                 <div key={"treeviewlink-" + groupType + "subtitle"}>
                     <div className="collectionTreeView-subtitle">{groupType}:</div>
@@ -325,7 +325,7 @@ class TreeView extends React.Component<TreeViewProps> {
                         addDocTab={this.props.addDocTab}
                         setPreviewScript={emptyFunction}>
                     </CollectionSchemaPreview>
-                </div>
+                </div>;
             }
         }
         return <div className="treeViewItem-container" ref={this.createTreeDropTarget} onContextMenu={this.onWorkspaceContextMenu}>
@@ -364,14 +364,14 @@ class TreeView extends React.Component<TreeViewProps> {
                     TreeView.AddDocToList(docList[i - 1], fieldKey, child);
                     remove(child);
                 }
-            }
+            };
             let addDocument = (doc: Doc, relativeTo?: Doc, before?: boolean) => {
                 return add(doc, relativeTo ? relativeTo : docList[i], before !== undefined ? before : false);
-            }
+            };
             let rowHeight = () => {
                 let aspect = NumCast(child.nativeWidth, 0) / NumCast(child.nativeHeight, 0);
                 return aspect ? Math.min(child[WidthSym](), rowWidth()) / aspect : child[HeightSym]();
-            }
+            };
             return <TreeView
                 document={child}
                 treeViewId={treeViewId}
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
index 76e089f08..bb8e8a5c2 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
@@ -98,17 +98,17 @@ export class CollectionFreeFormLinksView extends React.Component<CollectionViewP
         //     console.log("CONNECTION", StrCast(d.a.props.Document.title), StrCast(d.b.props.Document.title));
         // });
 
-        console.log("CONNECTIONS");
+        // console.log("CONNECTIONS");
 
         let connections = DocumentManager.Instance.LinkedDocumentViews.reduce((drawnPairs, connection) => {
             let srcViews = this.documentAnchors(connection.a);
             let targetViews = this.documentAnchors(connection.b);
 
-            console.log(srcViews.length, targetViews.length);
+            // console.log(srcViews.length, targetViews.length);
             let possiblePairs: { a: Doc, b: Doc, }[] = [];
             srcViews.map(sv => {
                 targetViews.map(tv => {
-                    console.log("PUSH", StrCast(sv.props.Document.title), StrCast(sv.props.Document.id), StrCast(tv.props.Document.title), StrCast(tv.props.Document.id));
+                    // console.log("PUSH", StrCast(sv.props.Document.title), StrCast(sv.props.Document.id), StrCast(tv.props.Document.title), StrCast(tv.props.Document.id));
                     possiblePairs.push({ a: sv.props.Document, b: tv.props.Document });
                 });
             });
@@ -142,7 +142,6 @@ export class CollectionFreeFormLinksView extends React.Component<CollectionViewP
     }
 
     render() {
-        console.log("\n");
         return (
             <div className="collectionfreeformlinksview-container">
                 <svg className="collectionfreeformlinksview-svgCanvas">
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 1fc2cf770..7b185336b 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -285,7 +285,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
                 let subBulletDocs = await DocListCastAsync(this.props.Document.subBulletDocs);
                 let maximizedDocs = await DocListCastAsync(this.props.Document.maximizedDocs);
                 let summarizedDocs = await DocListCastAsync(this.props.Document.summarizedDocs);
-                let linkedDocs = LinkManager.Instance.findAllRelatedLinks(this.props.Document);
+                let linkedDocs = LinkManager.Instance.getAllRelatedLinks(this.props.Document);
                 let expandedDocs: Doc[] = [];
                 expandedDocs = subBulletDocs ? [...subBulletDocs, ...expandedDocs] : expandedDocs;
                 expandedDocs = maximizedDocs ? [...maximizedDocs, ...expandedDocs] : expandedDocs;
@@ -536,11 +536,6 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
 
     onPointerEnter = (e: React.PointerEvent): void => { this.props.Document.libraryBrush = true; };
     onPointerLeave = (e: React.PointerEvent): void => { this.props.Document.libraryBrush = false; };
-    onDragOver = (e: React.DragEvent): void => {
-        this.props.Document.libraryBrush = true;
-        console.log("dragOver");
-    };
-    onDragLeave = (e: React.DragEvent): void => { this.props.Document.libraryBrush = false; };
 
     isSelected = () => SelectionManager.IsSelected(this);
     @action select = (ctrlPressed: boolean) => { SelectionManager.SelectDoc(this, ctrlPressed); };
@@ -585,7 +580,6 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
                     // display: display ? "block" : "none"
                 }}
                 onDrop={this.onDrop} onContextMenu={this.onContextMenu} onPointerDown={this.onPointerDown} onClick={this.onClick}
-                onDragOver={this.onDragOver} onDragLeave={this.onDragLeave}
                 onPointerEnter={this.onPointerEnter} onPointerLeave={this.onPointerLeave}
             >
                 {this.contents}
diff --git a/src/client/views/nodes/LinkButtonBox.scss b/src/client/views/nodes/LinkButtonBox.scss
index 24bfd2c9f..6be2dcf60 100644
--- a/src/client/views/nodes/LinkButtonBox.scss
+++ b/src/client/views/nodes/LinkButtonBox.scss
@@ -1,18 +1,18 @@
-.linkBox-cont {
-    width: 200px;
-    height: 100px;
-    background-color: black;
-    text-align: center;
-    color: white;
-    padding: 10px;
-    border-radius: 5px;
-    position: relative;
+// .linkBox-cont {
+//     width: 200px;
+//     height: 100px;
+//     background-color: black;
+//     text-align: center;
+//     color: white;
+//     padding: 10px;
+//     border-radius: 5px;
+//     position: relative;
 
-    .linkBox-cont-wrapper {
-        width: calc(100% - 20px);
-        position: absolute;
-        left: 50%;
-        top: 50%;
-        transform: translate(-50%, -50%);
-    }
-}
\ No newline at end of file
+//     .linkBox-cont-wrapper {
+//         width: calc(100% - 20px);
+//         position: absolute;
+//         left: 50%;
+//         top: 50%;
+//         transform: translate(-50%, -50%);
+//     }
+// }
\ No newline at end of file
diff --git a/src/client/views/nodes/LinkButtonBox.tsx b/src/client/views/nodes/LinkButtonBox.tsx
index 8a7c1ed8b..440847ead 100644
--- a/src/client/views/nodes/LinkButtonBox.tsx
+++ b/src/client/views/nodes/LinkButtonBox.tsx
@@ -1,63 +1,63 @@
-import React = require("react");
-import { library } from '@fortawesome/fontawesome-svg-core';
-import { faCaretUp, faFilePdf, faFilm, faImage, faObjectGroup, faStickyNote } from '@fortawesome/free-solid-svg-icons';
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { computed, observable, runInAction } from "mobx";
-import { observer } from "mobx-react";
-import { FieldView, FieldViewProps } from './FieldView';
-import "./LinkButtonBox.scss";
-import { DocumentView } from "./DocumentView";
-import { Doc } from "../../../new_fields/Doc";
-import { LinkButtonField } from "../../../new_fields/LinkButtonField";
-import { Cast, StrCast, BoolCast } from "../../../new_fields/Types";
-import { CollectionDockingView } from "../collections/CollectionDockingView";
-import { DocumentManager } from "../../util/DocumentManager";
-import { Id } from "../../../new_fields/FieldSymbols";
-
-library.add(faCaretUp);
-library.add(faObjectGroup);
-library.add(faStickyNote);
-library.add(faFilePdf);
-library.add(faFilm);
-
-@observer
-export class LinkButtonBox extends React.Component<FieldViewProps> {
-    public static LayoutString() { return FieldView.LayoutString(LinkButtonBox); }
-
-    followLink = (): void => {
-        console.log("follow link???");
-        let field = Cast(this.props.Document[this.props.fieldKey], LinkButtonField, new LinkButtonField({ sourceViewId: "-1", targetViewId: "-1" }));
-        let targetView = DocumentManager.Instance.getDocumentViewById(field.data.targetViewId);
-        if (targetView && targetView.props.ContainingCollectionView) {
-            CollectionDockingView.Instance.AddRightSplit(targetView.props.ContainingCollectionView.props.Document);
-        }
-    }
-
-    render() {
-
-        let field = Cast(this.props.Document[this.props.fieldKey], LinkButtonField, new LinkButtonField({ sourceViewId: "-1", targetViewId: "-1" }));
-        let targetView = DocumentManager.Instance.getDocumentViewById(field.data.targetViewId);
-
-        let text = "Could not find link";
-        if (targetView) {
-            let context = targetView.props.ContainingCollectionView ? (" in the context of " + StrCast(targetView.props.ContainingCollectionView.props.Document.title)) : "";
-            text = "Link to " + StrCast(targetView.props.Document.title) + context;
-        }
-
-        let activeDvs = DocumentManager.Instance.DocumentViews.filter(dv => dv.isSelected() || BoolCast(dv.props.Document.libraryBrush, false));
-        let display = activeDvs.reduce((found, dv) => {
-            let matchSv = field.data.sourceViewId === StrCast(dv.props.Document[Id]);
-            let matchTv = field.data.targetViewId === StrCast(dv.props.Document[Id]);
-            let match = matchSv || matchTv;
-            return match || found;
-        }, false);
-
-        return (
-            <div className="linkBox-cont" style={{ display: display ? "block" : "none" }}>
-                <div className="linkBox-cont-wrapper">
-                    <p>{text}</p>
-                </div>
-            </div >
-        );
-    }
-}
\ No newline at end of file
+// import React = require("react");
+// import { library } from '@fortawesome/fontawesome-svg-core';
+// import { faCaretUp, faFilePdf, faFilm, faImage, faObjectGroup, faStickyNote } from '@fortawesome/free-solid-svg-icons';
+// import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+// import { computed, observable, runInAction } from "mobx";
+// import { observer } from "mobx-react";
+// import { FieldView, FieldViewProps } from './FieldView';
+// import "./LinkButtonBox.scss";
+// import { DocumentView } from "./DocumentView";
+// import { Doc } from "../../../new_fields/Doc";
+// import { LinkButtonField } from "../../../new_fields/LinkButtonField";
+// import { Cast, StrCast, BoolCast } from "../../../new_fields/Types";
+// import { CollectionDockingView } from "../collections/CollectionDockingView";
+// import { DocumentManager } from "../../util/DocumentManager";
+// import { Id } from "../../../new_fields/FieldSymbols";
+
+// library.add(faCaretUp);
+// library.add(faObjectGroup);
+// library.add(faStickyNote);
+// library.add(faFilePdf);
+// library.add(faFilm);
+
+// @observer
+// export class LinkButtonBox extends React.Component<FieldViewProps> {
+//     public static LayoutString() { return FieldView.LayoutString(LinkButtonBox); }
+
+//     followLink = (): void => {
+//         console.log("follow link???");
+//         let field = Cast(this.props.Document[this.props.fieldKey], LinkButtonField, new LinkButtonField({ sourceViewId: "-1", targetViewId: "-1" }));
+//         let targetView = DocumentManager.Instance.getDocumentViewById(field.data.targetViewId);
+//         if (targetView && targetView.props.ContainingCollectionView) {
+//             CollectionDockingView.Instance.AddRightSplit(targetView.props.ContainingCollectionView.props.Document);
+//         }
+//     }
+
+//     render() {
+
+//         let field = Cast(this.props.Document[this.props.fieldKey], LinkButtonField, new LinkButtonField({ sourceViewId: "-1", targetViewId: "-1" }));
+//         let targetView = DocumentManager.Instance.getDocumentViewById(field.data.targetViewId);
+
+//         let text = "Could not find link";
+//         if (targetView) {
+//             let context = targetView.props.ContainingCollectionView ? (" in the context of " + StrCast(targetView.props.ContainingCollectionView.props.Document.title)) : "";
+//             text = "Link to " + StrCast(targetView.props.Document.title) + context;
+//         }
+
+//         let activeDvs = DocumentManager.Instance.DocumentViews.filter(dv => dv.isSelected() || BoolCast(dv.props.Document.libraryBrush, false));
+//         let display = activeDvs.reduce((found, dv) => {
+//             let matchSv = field.data.sourceViewId === StrCast(dv.props.Document[Id]);
+//             let matchTv = field.data.targetViewId === StrCast(dv.props.Document[Id]);
+//             let match = matchSv || matchTv;
+//             return match || found;
+//         }, false);
+
+//         return (
+//             <div className="linkBox-cont" style={{ display: display ? "block" : "none" }}>
+//                 <div className="linkBox-cont-wrapper">
+//                     <p>{text}</p>
+//                 </div>
+//             </div >
+//         );
+//     }
+// }
\ No newline at end of file
diff --git a/src/client/views/nodes/LinkEditor.tsx b/src/client/views/nodes/LinkEditor.tsx
index 95199bae2..5f4f7d4f0 100644
--- a/src/client/views/nodes/LinkEditor.tsx
+++ b/src/client/views/nodes/LinkEditor.tsx
@@ -17,9 +17,8 @@ library.add(faArrowLeft, faEllipsisV, faTable, faTrash, faCog, faExchangeAlt, fa
 
 
 interface GroupTypesDropdownProps {
-    groupId: string;
     groupType: string;
-    setGroup: (groupId: string, group: string) => void;
+    setGroupType: (group: string) => void;
 }
 // this dropdown could be generalized
 @observer
@@ -32,20 +31,20 @@ class GroupTypesDropdown extends React.Component<GroupTypesDropdownProps> {
 
     @action
     createGroup = (groupType: string): void => {
-        this.props.setGroup(this.props.groupId, groupType);
-        LinkManager.Instance.groupMetadataKeys.set(groupType, []);
+        this.props.setGroupType(groupType);
+        LinkManager.Instance.addGroupType(groupType);
     }
 
     renderOptions = (): JSX.Element[] | JSX.Element => {
         if (this._searchTerm === "") return <></>;
 
-        let allGroupTypes = Array.from(LinkManager.Instance.groupMetadataKeys.keys());
+        let allGroupTypes = Array.from(LinkManager.Instance.getAllGroupTypes());
         let groupOptions = allGroupTypes.filter(groupType => groupType.toUpperCase().indexOf(this._searchTerm.toUpperCase()) > -1);
         let exactFound = groupOptions.findIndex(groupType => groupType.toUpperCase() === this._searchTerm.toUpperCase()) > -1;
 
         let options = groupOptions.map(groupType => {
             return <div key={groupType} className="linkEditor-option"
-                onClick={() => { this.props.setGroup(this.props.groupId, groupType); this.setGroupType(groupType); this.setSearchTerm(""); }}>{groupType}</div>;
+                onClick={() => { this.props.setGroupType(groupType); this.setGroupType(groupType); this.setSearchTerm(""); }}>{groupType}</div>;
         });
 
         // if search term does not already exist as a group type, give option to create new group type
@@ -85,7 +84,7 @@ class LinkMetadataEditor extends React.Component<LinkMetadataEditorProps> {
 
     @action
     setMetadataKey = (value: string): void => {
-        let groupMdKeys = new Array(...LinkManager.Instance.groupMetadataKeys.get(this.props.groupType)!);
+        let groupMdKeys = LinkManager.Instance.getMetadataKeysInGroup(this.props.groupType);
 
         // don't allow user to create existing key
         let newIndex = groupMdKeys.findIndex(key => key.toUpperCase() === value.toUpperCase());
@@ -98,12 +97,15 @@ class LinkMetadataEditor extends React.Component<LinkMetadataEditorProps> {
         }
 
         // set new value for key
-        let currIndex = groupMdKeys.findIndex(key => key.toUpperCase() === this._key.toUpperCase());
+        let currIndex = groupMdKeys.findIndex(key => {
+            console.log("finding index this", key.toUpperCase(), "that", this._key.toUpperCase());
+            return StrCast(key).toUpperCase() === this._key.toUpperCase();
+        });
         if (currIndex === -1) console.error("LinkMetadataEditor: key was not found");
         groupMdKeys[currIndex] = value;
 
         this._key = value;
-        LinkManager.Instance.groupMetadataKeys.set(this.props.groupType, groupMdKeys);
+        LinkManager.Instance.setMetadataKeysForGroup(this.props.groupType, groupMdKeys);
     }
 
     @action
@@ -116,13 +118,13 @@ class LinkMetadataEditor extends React.Component<LinkMetadataEditorProps> {
 
     @action
     removeMetadata = (): void => {
-        let groupMdKeys = new Array(...LinkManager.Instance.groupMetadataKeys.get(this.props.groupType)!);
+        let groupMdKeys = LinkManager.Instance.getMetadataKeysInGroup(this.props.groupType);
 
         let index = groupMdKeys.findIndex(key => key.toUpperCase() === this._key.toUpperCase());
         if (index === -1) console.error("LinkMetadataEditor: key was not found");
         groupMdKeys.splice(index, 1);
 
-        LinkManager.Instance.groupMetadataKeys.set(this.props.groupType, groupMdKeys);
+        LinkManager.Instance.setMetadataKeysForGroup(this.props.groupType, groupMdKeys);
         this._key = "";
     }
 
@@ -137,215 +139,176 @@ class LinkMetadataEditor extends React.Component<LinkMetadataEditorProps> {
     }
 }
 
-
-interface LinkEditorProps {
+interface LinkGroupEditorProps {
     sourceDoc: Doc;
     linkDoc: Doc;
-    showLinks: () => void;
+    groupDoc: Doc;
 }
 @observer
-export class LinkEditor extends React.Component<LinkEditorProps> {
+export class LinkGroupEditor extends React.Component<LinkGroupEditorProps> {
 
-    // map of temporary group id to the corresponding group doc
-    @observable private _groups: Map<string, Doc> = new Map();
-
-    constructor(props: LinkEditorProps) {
-        super(props);
+    @action
+    setGroupType = (groupType: string): void => {
+        console.log("SET GROUP TYPE TO", groupType);
+        this.props.groupDoc.type = groupType;
+        console.log("GROUP TYPE HAS BEEN SET TO ", StrCast(this.props.groupDoc.type));
+    }
 
-        let groups = new Map<string, Doc>();
-        let groupList = LinkManager.Instance.getAnchorGroups(props.linkDoc, props.sourceDoc);
-        groupList.forEach(groupDoc => {
-            let id = Utils.GenerateGuid();
-            groups.set(id, groupDoc);
-        });
-        this._groups = groups;
+    removeGroupFromLink = (groupType: string): void => {
+        LinkManager.Instance.removeGroupFromAnchor(this.props.linkDoc, this.props.sourceDoc, groupType);
     }
 
-    @action
-    deleteLink = (): void => {
-        let index = LinkManager.Instance.allLinks.indexOf(this.props.linkDoc);
-        LinkManager.Instance.allLinks.splice(index, 1);
-        this.props.showLinks();
+    deleteGroup = (groupType: string): void => {
+        LinkManager.Instance.deleteGroupType(groupType);
     }
 
-    @action
-    addGroup = (): void => {
-        // new group only gets added if there is not already a group with type "new group"
-        let index = Array.from(this._groups.values()).findIndex(groupDoc => {
-            return groupDoc.type === "New Group";
-        });
-        if (index > -1) return;
+    copyGroup = (groupType: string): void => {
+        let sourceGroupDoc = this.props.groupDoc;
+        let sourceMdDoc = Cast(sourceGroupDoc.metadata, Doc, new Doc);
 
-        // create new metadata document for group
-        let mdDoc = Docs.TextDocument();
-        mdDoc.proto!.anchor1 = this.props.sourceDoc.title;
-        mdDoc.proto!.anchor2 = LinkManager.Instance.findOppositeAnchor(this.props.linkDoc, this.props.sourceDoc).title;
+        let destDoc = LinkManager.Instance.getOppositeAnchor(this.props.linkDoc, this.props.sourceDoc);
+        // let destGroupList = LinkManager.Instance.getAnchorGroups(this.props.linkDoc, destDoc);
+        let keys = LinkManager.Instance.getMetadataKeysInGroup(groupType);
 
-        // create new group document
-        let groupDoc = Docs.TextDocument();
-        groupDoc.proto!.type = "New Group";
-        groupDoc.proto!.metadata = mdDoc;
+        // create new metadata doc with copied kvp
+        let destMdDoc = new Doc();
+        destMdDoc.anchor1 = StrCast(sourceMdDoc.anchor2);
+        destMdDoc.anchor2 = StrCast(sourceMdDoc.anchor1);
+        keys.forEach(key => {
+            let val = sourceMdDoc[key] === undefined ? "" : StrCast(sourceMdDoc[key]);
+            destMdDoc[key] = val;
+        });
 
-        this._groups.set(Utils.GenerateGuid(), groupDoc);
+        // create new group doc with new metadata doc
+        let destGroupDoc = new Doc();
+        destGroupDoc.type = groupType;
+        destGroupDoc.metadata = destMdDoc;
 
-        let linkDoc = this.props.linkDoc.proto ? this.props.linkDoc.proto : this.props.linkDoc;
-        LinkManager.Instance.setAnchorGroups(linkDoc, this.props.sourceDoc, Array.from(this._groups.values()));
+        LinkManager.Instance.addGroupToAnchor(this.props.linkDoc, destDoc, destGroupDoc, true);
     }
 
     @action
-    setGroupType = (groupId: string, groupType: string): void => {
-        let groupDoc = this._groups.get(groupId);
-        if (groupDoc) {
-            groupDoc.proto!.type = groupType;
-            this._groups.set(groupId, groupDoc);
-            LinkManager.Instance.setAnchorGroups(this.props.linkDoc, this.props.sourceDoc, Array.from(this._groups.values()));
-        }
-    }
-
-    removeGroupFromLink = (groupId: string, groupType: string): void => {
-        let groupDoc = this._groups.get(groupId);
-        if (!groupDoc) console.error("LinkEditor: group not found");
-        LinkManager.Instance.removeGroupFromAnchor(this.props.linkDoc, this.props.sourceDoc, groupType);
-        this._groups.delete(groupId);
-    }
-
-    deleteGroup = (groupId: string, groupType: string): void => {
-        let groupDoc = this._groups.get(groupId);
-        if (!groupDoc) console.error("LinkEditor: group not found");
-        LinkManager.Instance.deleteGroup(groupType);
-        this._groups.delete(groupId);
+    addMetadata = (groupType: string): void => {
+        let mdKeys = LinkManager.Instance.getMetadataKeysInGroup(groupType);
+        // only add "new key" if there is no other key with value "new key"; prevents spamming
+        if (mdKeys.indexOf("new key") === -1) mdKeys.push("new key");
+        LinkManager.Instance.setMetadataKeysForGroup(groupType, mdKeys);
     }
 
-    copyGroup = (groupId: string, groupType: string): void => {
-        let sourceGroupDoc = this._groups.get(groupId);
-        let sourceMdDoc = Cast(sourceGroupDoc!.metadata, Doc, new Doc);
-        let destDoc = LinkManager.Instance.findOppositeAnchor(this.props.linkDoc, this.props.sourceDoc);
-        let destGroupList = LinkManager.Instance.getAnchorGroups(this.props.linkDoc, destDoc);
-        let keys = LinkManager.Instance.groupMetadataKeys.get(groupType);
-
-        // create new metadata doc with copied kvp
-        let destMdDoc = Docs.TextDocument();
-        destMdDoc.proto!.anchor1 = StrCast(sourceMdDoc.anchor2);
-        destMdDoc.proto!.anchor2 = StrCast(sourceMdDoc.anchor1);
-        if (keys) {
-            keys.forEach(key => {
-                let val = sourceMdDoc[key] === undefined ? "" : StrCast(sourceMdDoc[key]);
-                destMdDoc[key] = val;
+    renderMetadata = (): JSX.Element[] => {
+        let metadata: Array<JSX.Element> = [];
+        let groupDoc = this.props.groupDoc;
+        let mdDoc = Cast(groupDoc.metadata, Doc, new Doc);
+        let groupType = StrCast(groupDoc.type);
+        let groupMdKeys = LinkManager.Instance.getMetadataKeysInGroup(groupType);
+        if (groupMdKeys) {
+            groupMdKeys.forEach((key, index) => {
+                metadata.push(
+                    <LinkMetadataEditor key={"mded-" + index} groupType={groupType} mdDoc={mdDoc} mdKey={key} mdValue={(mdDoc[key] === undefined) ? "" : StrCast(mdDoc[key])} />
+                );
             });
         }
-
-        // create new group doc with new metadata doc
-        let destGroupDoc = Docs.TextDocument();
-        destGroupDoc.proto!.type = groupType;
-        destGroupDoc.proto!.metadata = destMdDoc;
-
-        // if group does not already exist on opposite anchor, create group doc
-        let index = destGroupList.findIndex(groupDoc => { StrCast(groupDoc.type).toUpperCase() === groupType.toUpperCase(); });
-        if (index > -1) {
-            destGroupList[index] = destGroupDoc;
-        } else {
-            destGroupList.push(destGroupDoc);
-        }
-
-        LinkManager.Instance.setAnchorGroups(this.props.linkDoc, destDoc, destGroupList);
+        return metadata;
     }
 
-    viewGroupAsTable = (groupId: string, groupType: string): JSX.Element => {
-        let keys = LinkManager.Instance.groupMetadataKeys.get(groupType);
-        let groupDoc = this._groups.get(groupId);
-        if (keys && groupDoc) {
-            let docs: Doc[] = LinkManager.Instance.findAllMetadataDocsInGroup(groupType);
-            let createTable = action(() => Docs.SchemaDocument(["anchor1", "anchor2", ...keys!], docs, { width: 200, height: 200, title: groupType + " table" }));
-            let ref = React.createRef<HTMLDivElement>();
-            return <div ref={ref}><button className="linkEditor-button" onPointerDown={SetupDrag(ref, createTable)}><FontAwesomeIcon icon="table" size="sm" /></button></div>;
-        } else {
-            return <button className="linkEditor-button" disabled><FontAwesomeIcon icon="table" size="sm" /></button>;
-        }
+    viewGroupAsTable = (groupType: string): JSX.Element => {
+        let keys = LinkManager.Instance.getMetadataKeysInGroup(groupType);
+        let docs: Doc[] = LinkManager.Instance.getAllMetadataDocsInGroup(groupType);
+        let createTable = action(() => Docs.SchemaDocument(["anchor1", "anchor2", ...keys], docs, { width: 500, height: 300, title: groupType + " table" }));
+        let ref = React.createRef<HTMLDivElement>();
+        return <div ref={ref}><button className="linkEditor-button" onPointerDown={SetupDrag(ref, createTable)}><FontAwesomeIcon icon="table" size="sm" /></button></div>;
     }
 
-    renderGroup = (groupId: string, groupDoc: Doc): JSX.Element => {
-        let type = StrCast(groupDoc.type);
-        if ((type && LinkManager.Instance.groupMetadataKeys.get(type)) || type === "New Group") {
-            let buttons;
-            if (type === "New Group") {
-                buttons = (
-                    <>
-                        <button className="linkEditor-button" disabled={true} title="Add KVP"><FontAwesomeIcon icon="plus" size="sm" /></button>
-                        <button className="linkEditor-button" disabled title="Copy group to opposite anchor"><FontAwesomeIcon icon="exchange-alt" size="sm" /></button>
-                        <button className="linkEditor-button" onClick={() => this.removeGroupFromLink(groupId, type)} title="Remove group from link"><FontAwesomeIcon icon="times" size="sm" /></button>
-                        <button className="linkEditor-button" disabled title="Delete group"><FontAwesomeIcon icon="trash" size="sm" /></button>
-                        <button className="linkEditor-button" disabled><FontAwesomeIcon icon="table" size="sm" /></button>
-                    </>
-                );
-            } else {
-                buttons = (
-                    <>
-                        <button className="linkEditor-button" onClick={() => this.addMetadata(StrCast(groupDoc.proto!.type))} title="Add KVP"><FontAwesomeIcon icon="plus" size="sm" /></button>
-                        <button className="linkEditor-button" onClick={() => this.copyGroup(groupId, type)} title="Copy group to opposite anchor"><FontAwesomeIcon icon="exchange-alt" size="sm" /></button>
-                        <button className="linkEditor-button" onClick={() => this.removeGroupFromLink(groupId, type)} title="Remove group from link"><FontAwesomeIcon icon="times" size="sm" /></button>
-                        <button className="linkEditor-button" onClick={() => this.deleteGroup(groupId, type)} title="Delete group"><FontAwesomeIcon icon="trash" size="sm" /></button>
-                        {this.viewGroupAsTable(groupId, type)}
-                    </>
-                );
-            }
-
-            return (
-                <div key={groupId} className="linkEditor-group">
-                    <div className="linkEditor-group-row">
-                        <p className="linkEditor-group-row-label">type:</p>
-                        <GroupTypesDropdown groupId={groupId} groupType={StrCast(groupDoc.proto!.type)} setGroup={this.setGroupType} />
-                    </div>
-                    {this.renderMetadata(groupId)}
-                    <div className="linkEditor-group-buttons">
-                        {buttons}
-                    </div>
-                </div>
+    render() {
+        let groupType = StrCast(this.props.groupDoc.type);
+        // if ((groupType && LinkManager.Instance.getMetadataKeysInGroup(groupType).length > 0) || groupType === "") {
+        let buttons;
+        if (groupType === "") {
+            buttons = (
+                <>
+                    <button className="linkEditor-button" disabled={true} title="Add KVP"><FontAwesomeIcon icon="plus" size="sm" /></button>
+                    <button className="linkEditor-button" disabled title="Copy group to opposite anchor"><FontAwesomeIcon icon="exchange-alt" size="sm" /></button>
+                    <button className="linkEditor-button" onClick={() => this.removeGroupFromLink(groupType)} title="Remove group from link"><FontAwesomeIcon icon="times" size="sm" /></button>
+                    <button className="linkEditor-button" disabled title="Delete group"><FontAwesomeIcon icon="trash" size="sm" /></button>
+                    <button className="linkEditor-button" disabled><FontAwesomeIcon icon="table" size="sm" /></button>
+                </>
             );
         } else {
-            return <></>;
+            buttons = (
+                <>
+                    <button className="linkEditor-button" onClick={() => this.addMetadata(groupType)} title="Add KVP"><FontAwesomeIcon icon="plus" size="sm" /></button>
+                    <button className="linkEditor-button" onClick={() => this.copyGroup(groupType)} title="Copy group to opposite anchor"><FontAwesomeIcon icon="exchange-alt" size="sm" /></button>
+                    <button className="linkEditor-button" onClick={() => this.removeGroupFromLink(groupType)} title="Remove group from link"><FontAwesomeIcon icon="times" size="sm" /></button>
+                    <button className="linkEditor-button" onClick={() => this.deleteGroup(groupType)} title="Delete group"><FontAwesomeIcon icon="trash" size="sm" /></button>
+                    {this.viewGroupAsTable(groupType)}
+                </>
+            );
         }
+
+        return (
+            <div className="linkEditor-group">
+                <div className="linkEditor-group-row">
+                    <p className="linkEditor-group-row-label">type:</p>
+                    <GroupTypesDropdown groupType={groupType} setGroupType={this.setGroupType} />
+                </div>
+                {this.renderMetadata()}
+                <div className="linkEditor-group-buttons">
+                    {buttons}
+                </div>
+            </div>
+        );
     }
+    // else {
+    //     return <></>;
+    // }
+    // }
+}
+
 
+interface LinkEditorProps {
+    sourceDoc: Doc;
+    linkDoc: Doc;
+    showLinks: () => void;
+}
+@observer
+export class LinkEditor extends React.Component<LinkEditorProps> {
 
     @action
-    addMetadata = (groupType: string): void => {
-        console.log("ADD MD");
-        let mdKeys = LinkManager.Instance.groupMetadataKeys.get(groupType);
-        if (mdKeys) {
-            // only add "new key" if there is no other key with value "new key"; prevents spamming
-            if (mdKeys.indexOf("new key") === -1) mdKeys.push("new key");
-        } else {
-            mdKeys = ["new key"];
-        }
-        LinkManager.Instance.groupMetadataKeys.set(groupType, mdKeys);
+    deleteLink = (): void => {
+        LinkManager.Instance.deleteLink(this.props.linkDoc);
+        this.props.showLinks();
     }
 
-    renderMetadata = (groupId: string): JSX.Element[] => {
-        let metadata: Array<JSX.Element> = [];
-        let groupDoc = this._groups.get(groupId);
-        if (groupDoc) {
-            let mdDoc = Cast(groupDoc.proto!.metadata, Doc, new Doc);
-            let groupType = StrCast(groupDoc.proto!.type);
-            let groupMdKeys = LinkManager.Instance.groupMetadataKeys.get(groupType);
-            if (groupMdKeys) {
-                groupMdKeys.forEach((key, index) => {
-                    metadata.push(
-                        <LinkMetadataEditor key={"mded-" + index} groupType={groupType} mdDoc={mdDoc} mdKey={key} mdValue={(mdDoc[key] === undefined) ? "" : StrCast(mdDoc[key])} />
-                    );
-                });
-            }
-        }
-        return metadata;
+    @action
+    addGroup = (): void => {
+        // create new metadata document for group
+        let mdDoc = new Doc();
+        mdDoc.anchor1 = this.props.sourceDoc.title;
+        mdDoc.anchor2 = LinkManager.Instance.getOppositeAnchor(this.props.linkDoc, this.props.sourceDoc).title;
+
+        // create new group document
+        let groupDoc = new Doc();
+        groupDoc.type = "";
+        groupDoc.metadata = mdDoc;
+
+        LinkManager.Instance.addGroupToAnchor(this.props.linkDoc, this.props.sourceDoc, groupDoc);
     }
 
     render() {
-        let destination = LinkManager.Instance.findOppositeAnchor(this.props.linkDoc, this.props.sourceDoc);
+        let destination = LinkManager.Instance.getOppositeAnchor(this.props.linkDoc, this.props.sourceDoc);
 
-        let groups: Array<JSX.Element> = [];
-        this._groups.forEach((groupDoc, groupId) => {
-            groups.push(this.renderGroup(groupId, groupDoc));
+        let groupList = LinkManager.Instance.getAnchorGroups(this.props.linkDoc, this.props.sourceDoc);
+        console.log("NUM GROUPS", groupList.length);
+        let groups = groupList.map(groupDoc => {
+            return <LinkGroupEditor key={"gred-" + StrCast(groupDoc.type)} linkDoc={this.props.linkDoc} sourceDoc={this.props.sourceDoc} groupDoc={groupDoc} />;
         });
 
+
+        // let groups: Array<JSX.Element> = [];
+        // this._groups.forEach((groupDoc, groupId) => {
+        //     groups.push(this.renderGroup(groupId, groupDoc));
+        // });
+
         return (
             <div className="linkEditor">
                 <button className="linkEditor-back" onPointerDown={() => this.props.showLinks()}><FontAwesomeIcon icon="arrow-left" size="sm" /></button>
diff --git a/src/client/views/nodes/LinkMenu.tsx b/src/client/views/nodes/LinkMenu.tsx
index f96c7d2e4..04ca47db3 100644
--- a/src/client/views/nodes/LinkMenu.tsx
+++ b/src/client/views/nodes/LinkMenu.tsx
@@ -38,7 +38,7 @@ export class LinkMenu extends React.Component<Props> {
 
     render() {
         let sourceDoc = this.props.docView.props.Document;
-        let groups: Map<string, Doc[]> = LinkManager.Instance.findRelatedGroupedLinks(sourceDoc);
+        let groups: Map<string, Doc[]> = LinkManager.Instance.getRelatedGroupedLinks(sourceDoc);
         if (this._editingLink === undefined) {
             return (
                 <div className="linkMenu">
diff --git a/src/client/views/nodes/LinkMenuGroup.tsx b/src/client/views/nodes/LinkMenuGroup.tsx
index 229143d99..71326f703 100644
--- a/src/client/views/nodes/LinkMenuGroup.tsx
+++ b/src/client/views/nodes/LinkMenuGroup.tsx
@@ -42,10 +42,10 @@ export class LinkMenuGroup extends React.Component<LinkMenuGroupProps> {
             document.removeEventListener("pointermove", this.onLinkButtonMoved);
             document.removeEventListener("pointerup", this.onLinkButtonUp);
 
-            let draggedDocs = this.props.group.map(linkDoc => LinkManager.Instance.findOppositeAnchor(linkDoc, this.props.sourceDoc));
+            let draggedDocs = this.props.group.map(linkDoc => LinkManager.Instance.getOppositeAnchor(linkDoc, this.props.sourceDoc));
             let dragData = new DragManager.DocumentDragData(draggedDocs);
 
-            DragManager.StartLinkedDocumentDrag([this._drag.current], dragData, e.x, e.y, {
+            DragManager.StartLinkedDocumentDrag([this._drag.current], this.props.sourceDoc, dragData, e.x, e.y, {
                 handlers: {
                     dragComplete: action(emptyFunction),
                 },
@@ -57,7 +57,7 @@ export class LinkMenuGroup extends React.Component<LinkMenuGroupProps> {
 
     render() {
         let groupItems = this.props.group.map(linkDoc => {
-            let destination = LinkManager.Instance.findOppositeAnchor(linkDoc, this.props.sourceDoc);
+            let destination = LinkManager.Instance.getOppositeAnchor(linkDoc, this.props.sourceDoc);
             return <LinkMenuItem key={destination[Id] + this.props.sourceDoc[Id]} groupType={this.props.groupType}
                 linkDoc={linkDoc} sourceDoc={this.props.sourceDoc} destinationDoc={destination} showEditor={this.props.showEditor} />;
         });
@@ -69,6 +69,6 @@ export class LinkMenuGroup extends React.Component<LinkMenuGroupProps> {
                     {groupItems}
                 </div>
             </div>
-        )
+        );
     }
 }
\ No newline at end of file
diff --git a/src/client/views/nodes/LinkMenuItem.tsx b/src/client/views/nodes/LinkMenuItem.tsx
index 42ef353b7..28694721d 100644
--- a/src/client/views/nodes/LinkMenuItem.tsx
+++ b/src/client/views/nodes/LinkMenuItem.tsx
@@ -53,8 +53,8 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> {
         let mdRows: Array<JSX.Element> = [];
         if (groupDoc) {
             let mdDoc = Cast(groupDoc.metadata, Doc, new Doc);
-            let keys = LinkManager.Instance.groupMetadataKeys.get(this.props.groupType);
-            mdRows = keys!.map(key => {
+            let keys = LinkManager.Instance.getMetadataKeysInGroup(this.props.groupType);//groupMetadataKeys.get(this.props.groupType);
+            mdRows = keys.map(key => {
                 return (<div key={key} className="link-metadata-row"><b>{key}</b>: {StrCast(mdDoc[key])}</div>);
             });
         }
@@ -88,7 +88,7 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> {
 
     render() {
 
-        let keys = LinkManager.Instance.groupMetadataKeys.get(this.props.groupType);
+        let keys = LinkManager.Instance.getMetadataKeysInGroup(this.props.groupType);//groupMetadataKeys.get(this.props.groupType);
         let canExpand = keys ? keys.length > 0 : false;
 
         return (
diff --git a/src/new_fields/Doc.ts b/src/new_fields/Doc.ts
index fda788f2d..9b104184f 100644
--- a/src/new_fields/Doc.ts
+++ b/src/new_fields/Doc.ts
@@ -234,23 +234,20 @@ export namespace Doc {
     export function MakeCopy(doc: Doc, copyProto: boolean = false): Doc {
         const copy = new Doc;
         Object.keys(doc).forEach(key => {
-            console.log(key);
             const field = doc[key];
             if (key === "proto" && copyProto) {
-                console.log(1);
                 if (field instanceof Doc) {
-                    console.log(2);
                     copy[key] = Doc.MakeCopy(field);
                 }
             } else {
                 if (field instanceof RefField) {
-                    console.log(3);
+                    console.log("equals field, ref", key);
                     copy[key] = field;
                 } else if (field instanceof ObjectField) {
-                    console.log(4);
+                    console.log("copy field, object", key);
                     copy[key] = ObjectField.MakeCopy(field);
                 } else {
-                    console.log(5);
+                    console.log("equals field", key);
                     copy[key] = field;
                 }
             }
diff --git a/src/new_fields/LinkButtonField.ts b/src/new_fields/LinkButtonField.ts
index 92e1ed922..e6d1de749 100644
--- a/src/new_fields/LinkButtonField.ts
+++ b/src/new_fields/LinkButtonField.ts
@@ -1,35 +1,35 @@
-import { Deserializable } from "../client/util/SerializationHelper";
-import { serializable, primitive, createSimpleSchema, object } from "serializr";
-import { ObjectField } from "./ObjectField";
-import { Copy, ToScriptString } from "./FieldSymbols";
-import { Doc } from "./Doc";
-import { DocumentView } from "../client/views/nodes/DocumentView";
+// import { Deserializable } from "../client/util/SerializationHelper";
+// import { serializable, primitive, createSimpleSchema, object } from "serializr";
+// import { ObjectField } from "./ObjectField";
+// import { Copy, ToScriptString } from "./FieldSymbols";
+// import { Doc } from "./Doc";
+// import { DocumentView } from "../client/views/nodes/DocumentView";
 
-export type LinkButtonData = {
-    sourceViewId: string,
-    targetViewId: string
-};
+// export type LinkButtonData = {
+//     sourceViewId: string,
+//     targetViewId: string
+// };
 
-const LinkButtonSchema = createSimpleSchema({
-    sourceViewId: true,
-    targetViewId: true
-});
+// const LinkButtonSchema = createSimpleSchema({
+//     sourceViewId: true,
+//     targetViewId: true
+// });
 
-@Deserializable("linkButton")
-export class LinkButtonField extends ObjectField {
-    @serializable(object(LinkButtonSchema))
-    readonly data: LinkButtonData;
+// @Deserializable("linkButton")
+// export class LinkButtonField extends ObjectField {
+//     @serializable(object(LinkButtonSchema))
+//     readonly data: LinkButtonData;
 
-    constructor(data: LinkButtonData) {
-        super();
-        this.data = data;
-    }
+//     constructor(data: LinkButtonData) {
+//         super();
+//         this.data = data;
+//     }
 
-    [Copy]() {
-        return new LinkButtonField(this.data);
-    }
+//     [Copy]() {
+//         return new LinkButtonField(this.data);
+//     }
 
-    [ToScriptString]() {
-        return "invalid";
-    }
-}
+//     [ToScriptString]() {
+//         return "invalid";
+//     }
+// }
-- 
cgit v1.2.3-70-g09d2


From 2d300b0cd3d02c900865c61eacd539efed5289e6 Mon Sep 17 00:00:00 2001
From: Fawn <fangrui_tong@brown.edu>
Date: Tue, 25 Jun 2019 20:18:14 -0400
Subject: fixed link metadata rendering bug

---
 src/client/documents/Documents.ts                  |  11 +-
 src/client/util/LinkManager.ts                     |   2 +-
 .../CollectionFreeFormLinkView.scss                |  31 -----
 .../CollectionFreeFormLinkView.tsx                 |   1 -
 .../CollectionFreeFormLinkWithProxyView.tsx        | 131 ---------------------
 src/client/views/nodes/DocumentContentsView.tsx    |   3 +-
 src/client/views/nodes/DocumentView.tsx            |  11 --
 src/client/views/nodes/LinkButtonBox.scss          |  18 ---
 src/client/views/nodes/LinkButtonBox.tsx           |  63 ----------
 src/client/views/nodes/LinkEditor.tsx              |  45 ++++---
 src/new_fields/LinkButtonField.ts                  |  35 ------
 11 files changed, 35 insertions(+), 316 deletions(-)
 delete mode 100644 src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkWithProxyView.tsx
 delete mode 100644 src/client/views/nodes/LinkButtonBox.scss
 delete mode 100644 src/client/views/nodes/LinkButtonBox.tsx
 delete mode 100644 src/new_fields/LinkButtonField.ts

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

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index d2300e4d2..547687921 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -88,7 +88,7 @@ export namespace DocUtils {
     //     let protoTarg = target.proto ? target.proto : target;
     export function MakeLink(source: Doc, target: Doc, targetContext?: Doc, title: string = "", description: string = "", tags: string = "Default") {
         if (LinkManager.Instance.doesLinkExist(source, target)) {
-            console.log("LINK EXISTS"); return;
+            return;
         }
 
         UndoManager.RunInBatch(() => {
@@ -186,11 +186,6 @@ export namespace Docs {
             { x: 0, y: 0, width: Number(MINIMIZED_ICON_SIZE), height: Number(MINIMIZED_ICON_SIZE) });
         return iconProto;
     }
-    // function CreateLinkPrototype(): Doc {
-    //     let linkProto = setupPrototypeOptions(linkProtoId, "LINK_PROTO", LinkButtonBox.LayoutString(),
-    //         { x: 0, y: 0, width: 300 });
-    //     return linkProto;
-    // }
     function CreateTextPrototype(): Doc {
         let textProto = setupPrototypeOptions(textProtoId, "TEXT_PROTO", FormattedTextBox.LayoutString(),
             { x: 0, y: 0, width: 300, backgroundColor: "#f1efeb" });
@@ -277,9 +272,7 @@ export namespace Docs {
     export function IconDocument(icon: string, options: DocumentOptions = {}) {
         return CreateInstance(iconProto, new IconField(icon), options);
     }
-    // export function LinkButtonDocument(data: LinkButtonData, options: DocumentOptions = {}) {
-    //     return CreateInstance(linkProto, new LinkButtonField(data), options);
-    // }
+
     export function PdfDocument(url: string, options: DocumentOptions = {}) {
         return CreateInstance(pdfProto, new PdfField(new URL(url)), options);
     }
diff --git a/src/client/util/LinkManager.ts b/src/client/util/LinkManager.ts
index 60de7fc52..db814082f 100644
--- a/src/client/util/LinkManager.ts
+++ b/src/client/util/LinkManager.ts
@@ -156,7 +156,7 @@ export class LinkManager {
     // removes group doc of given group type only from given anchor on given link
     public removeGroupFromAnchor(linkDoc: Doc, anchor: Doc, groupType: string) {
         let groups = LinkManager.Instance.getAnchorGroups(linkDoc, anchor);
-        let newGroups = groups.filter(groupDoc => StrCast(groupDoc.type).toUpperCase() !== groupType.toUpperCase());
+        let newGroups = groups.filter(groupDoc => { StrCast(groupDoc.type).toUpperCase() !== groupType.toUpperCase() });
         LinkManager.Instance.setAnchorGroups(linkDoc, anchor, newGroups);
     }
 
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss
index 239c2ce56..fc5212edd 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss
@@ -17,34 +17,3 @@
     transform: translate(10000px,10000px);
     pointer-events: all;
 }
-
-.linkview-ele {
-    transform: translate(10000px,10000px);
-    pointer-events: all;
-
-    &.linkview-line {
-        stroke: black;
-        stroke-width: 2px;
-        opacity: 0.5;
-    }
-}
-
-.linkview-button {
-    width: 200px;
-    height: 100px;
-    border-radius: 5px;
-    padding: 10px;
-    position: relative;
-    background-color: black;
-    cursor: pointer;
-
-    p {
-        width: calc(100% - 20px);
-        color: white;
-        position: absolute;
-        top: 50%;
-        left: 50%;
-        transform: translate(-50%, -50%);
-    }
-
-}
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
index 5dc3b5c16..b546d1b78 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
@@ -9,7 +9,6 @@ import v5 = require("uuid/v5");
 export interface CollectionFreeFormLinkViewProps {
     A: Doc;
     B: Doc;
-    // LinkDoc: Doc;
     LinkDocs: Doc[];
     addDocument: (document: Doc, allowDuplicates?: boolean) => boolean;
     removeDocument: (document: Doc) => boolean;
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkWithProxyView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkWithProxyView.tsx
deleted file mode 100644
index a4d122af2..000000000
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkWithProxyView.tsx
+++ /dev/null
@@ -1,131 +0,0 @@
-import { observer } from "mobx-react";
-import { Doc, HeightSym, WidthSym } from "../../../../new_fields/Doc";
-import { BoolCast, NumCast, StrCast } from "../../../../new_fields/Types";
-import { InkingControl } from "../../InkingControl";
-import "./CollectionFreeFormLinkView.scss";
-import React = require("react");
-import v5 = require("uuid/v5");
-import { DocumentView } from "../../nodes/DocumentView";
-import { Docs } from "../../../documents/Documents";
-import { observable, action } from "mobx";
-import { CollectionDockingView } from "../CollectionDockingView";
-import { dropActionType, DragManager } from "../../../util/DragManager";
-import { emptyFunction } from "../../../../Utils";
-import { DocumentManager } from "../../../util/DocumentManager";
-
-export interface CollectionFreeFormLinkViewProps {
-    sourceView: DocumentView;
-    targetView: DocumentView;
-    proxyDoc: Doc;
-    // addDocTab: (document: Doc, where: string) => void;
-}
-
-@observer
-export class CollectionFreeFormLinkWithProxyView extends React.Component<CollectionFreeFormLinkViewProps> {
-
-    // @observable private _proxyX: number = NumCast(this.props.proxyDoc.x);
-    // @observable private _proxyY: number = NumCast(this.props.proxyDoc.y);
-    private _ref = React.createRef<HTMLDivElement>();
-    private _downX: number = 0;
-    private _downY: number = 0;
-    @observable _x: number = 0;
-    @observable _y: number = 0;
-    // @observable private _proxyDoc: Doc = Docs.TextDocument(); // used for positioning
-
-    @action
-    componentDidMount() {
-        let a2 = this.props.proxyDoc;
-        this._x = NumCast(a2.x) + (BoolCast(a2.isMinimized, false) ? 5 : NumCast(a2.width) / NumCast(a2.zoomBasis, 1) / 2);
-        this._y = NumCast(a2.y) + (BoolCast(a2.isMinimized, false) ? 5 : NumCast(a2.height) / NumCast(a2.zoomBasis, 1) / 2);
-    }
-
-
-    followButton = (e: React.PointerEvent): void => {
-        e.stopPropagation();
-        let open = this.props.targetView.props.ContainingCollectionView ? this.props.targetView.props.ContainingCollectionView.props.Document : this.props.targetView.props.Document;
-        CollectionDockingView.Instance.AddRightSplit(open);
-        DocumentManager.Instance.jumpToDocument(this.props.targetView.props.Document, e.altKey);
-    }
-
-    @action
-    setPosition(x: number, y: number) {
-        this._x = x;
-        this._y = y;
-    }
-
-    startDragging(x: number, y: number) {
-        if (this._ref.current) {
-            let dragData = new DragManager.DocumentDragData([this.props.proxyDoc]);
-
-            DragManager.StartLinkProxyDrag(this._ref.current, dragData, x, y, {
-                handlers: {
-                    dragComplete: action(() => {
-                        let a2 = this.props.proxyDoc;
-                        let offset = NumCast(a2.width) / NumCast(a2.zoomBasis, 1) / 2;
-                        let x = NumCast(a2.x);// + NumCast(a2.width) / NumCast(a2.zoomBasis, 1) / 2;
-                        let y = NumCast(a2.y);// + NumCast(a2.height) / NumCast(a2.zoomBasis, 1) / 2;
-                        this.setPosition(x, y);
-
-                        // this is a hack :'( theres prob a better way to make the input doc not render
-                        let views = DocumentManager.Instance.getDocumentViews(this.props.proxyDoc);
-                        views.forEach(dv => {
-                            dv.props.removeDocument && dv.props.removeDocument(dv.props.Document);
-                        });
-                    }),
-                },
-                hideSource: true //?
-            });
-        }
-    }
-
-    onPointerDown = (e: React.PointerEvent): void => {
-        this._downX = e.clientX;
-        this._downY = e.clientY;
-
-        e.stopPropagation();
-        document.removeEventListener("pointermove", this.onPointerMove);
-        document.addEventListener("pointermove", this.onPointerMove);
-        document.removeEventListener("pointerup", this.onPointerUp);
-        document.addEventListener("pointerup", this.onPointerUp);
-    }
-
-    onPointerMove = (e: PointerEvent): void => {
-        if (Math.abs(this._downX - e.clientX) > 3 || Math.abs(this._downY - e.clientY) > 3) {
-            document.removeEventListener("pointermove", this.onPointerMove);
-            document.removeEventListener("pointerup", this.onPointerUp);
-            this.startDragging(this._downX, this._downY);
-        }
-        e.stopPropagation();
-        e.preventDefault();
-    }
-    onPointerUp = (e: PointerEvent): void => {
-        document.removeEventListener("pointermove", this.onPointerMove);
-        document.removeEventListener("pointerup", this.onPointerUp);
-    }
-
-    render() {
-        let a1 = this.props.sourceView;
-        let x1 = NumCast(a1.Document.x) + (BoolCast(a1.Document.isMinimized, false) ? 5 : NumCast(a1.Document.width) / NumCast(a1.Document.zoomBasis, 1) / 2);
-        let y1 = NumCast(a1.Document.y) + (BoolCast(a1.Document.isMinimized, false) ? 5 : NumCast(a1.Document.height) / NumCast(a1.Document.zoomBasis, 1) / 2);
-
-        let context = this.props.targetView.props.ContainingCollectionView ?
-            (" in the context of " + StrCast(this.props.targetView.props.ContainingCollectionView.props.Document.title)) : "";
-        let text = "link to " + StrCast(this.props.targetView.props.Document.title) + context;
-
-        return (
-            <>
-                <line className="linkview-line linkview-ele"
-                    // style={{ strokeWidth: `${2 * 1 / 2}` }}
-                    x1={`${x1}`} y1={`${y1}`}
-                    x2={`${this._x}`} y2={`${this._y}`} />
-                <foreignObject className="linkview-button-wrapper linkview-ele" width={200} height={100} x={this._x - 100} y={this._y - 50}>
-                    <div className="linkview-button" onPointerDown={this.onPointerDown} onPointerUp={this.followButton} ref={this._ref}>
-                        <p>{text}</p>
-                    </div>
-                </foreignObject>
-            </>
-        );
-    }
-}
-
-//onPointerDown={this.followButton}
\ No newline at end of file
diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx
index 940b00a90..02396c3af 100644
--- a/src/client/views/nodes/DocumentContentsView.tsx
+++ b/src/client/views/nodes/DocumentContentsView.tsx
@@ -12,7 +12,6 @@ import "./DocumentView.scss";
 import { FormattedTextBox } from "./FormattedTextBox";
 import { ImageBox } from "./ImageBox";
 import { IconBox } from "./IconBox";
-import { LinkButtonBox } from "./LinkButtonBox";
 import { KeyValueBox } from "./KeyValueBox";
 import { PDFBox } from "./PDFBox";
 import { VideoBox } from "./VideoBox";
@@ -104,7 +103,7 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & {
     render() {
         if (!this.layout && (this.props.layoutKey !== "overlayLayout" || !this.templates.length)) return (null);
         return <ObserverJsxParser
-            components={{ FormattedTextBox, ImageBox, IconBox, FieldView, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, CollectionPDFView, CollectionVideoView, WebBox, KeyValueBox, PDFBox, VideoBox, AudioBox, HistogramBox, LinkButtonBox }}
+            components={{ FormattedTextBox, ImageBox, IconBox, FieldView, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, CollectionPDFView, CollectionVideoView, WebBox, KeyValueBox, PDFBox, VideoBox, AudioBox, HistogramBox }}
             bindings={this.CreateBindings()}
             jsx={this.finalLayout}
             showWarnings={true}
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index b705276a6..c71d7ed68 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -555,16 +555,6 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
         var scaling = this.props.ContentScaling();
         var nativeWidth = this.nativeWidth > 0 ? `${this.nativeWidth}px` : "100%";
 
-        // // for linkbutton docs
-        // let isLinkButton = BoolCast(this.props.Document.isLinkButton);
-        // let activeDvs = DocumentManager.Instance.DocumentViews.filter(dv => dv.isSelected() || BoolCast(dv.props.Document.libraryBrush, false));
-        // let display = isLinkButton ? activeDvs.reduce((found, dv) => {
-        //     let matchSv = this.props.Document.sourceViewId === StrCast(dv.props.Document[Id]);
-        //     let matchTv = this.props.Document.targetViewId === StrCast(dv.props.Document[Id]);
-        //     let match = matchSv || matchTv;
-        //     return match || found;
-        // }, false) : true;
-
         var nativeHeight = BoolCast(this.props.Document.ignoreAspect) ? this.props.PanelHeight() / this.props.ContentScaling() : this.nativeHeight > 0 ? `${this.nativeHeight}px` : "100%";
         return (
             <div className={`documentView-node${this.props.isTopMost ? "-topmost" : ""}`}
@@ -581,7 +571,6 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
                     width: nativeWidth,
                     height: nativeHeight,
                     transform: `scale(${scaling}, ${scaling})`,
-                    // display: display ? "block" : "none"
                 }}
                 onDrop={this.onDrop} onContextMenu={this.onContextMenu} onPointerDown={this.onPointerDown} onClick={this.onClick}
                 onPointerEnter={this.onPointerEnter} onPointerLeave={this.onPointerLeave}
diff --git a/src/client/views/nodes/LinkButtonBox.scss b/src/client/views/nodes/LinkButtonBox.scss
deleted file mode 100644
index 6be2dcf60..000000000
--- a/src/client/views/nodes/LinkButtonBox.scss
+++ /dev/null
@@ -1,18 +0,0 @@
-// .linkBox-cont {
-//     width: 200px;
-//     height: 100px;
-//     background-color: black;
-//     text-align: center;
-//     color: white;
-//     padding: 10px;
-//     border-radius: 5px;
-//     position: relative;
-
-//     .linkBox-cont-wrapper {
-//         width: calc(100% - 20px);
-//         position: absolute;
-//         left: 50%;
-//         top: 50%;
-//         transform: translate(-50%, -50%);
-//     }
-// }
\ No newline at end of file
diff --git a/src/client/views/nodes/LinkButtonBox.tsx b/src/client/views/nodes/LinkButtonBox.tsx
deleted file mode 100644
index 440847ead..000000000
--- a/src/client/views/nodes/LinkButtonBox.tsx
+++ /dev/null
@@ -1,63 +0,0 @@
-// import React = require("react");
-// import { library } from '@fortawesome/fontawesome-svg-core';
-// import { faCaretUp, faFilePdf, faFilm, faImage, faObjectGroup, faStickyNote } from '@fortawesome/free-solid-svg-icons';
-// import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-// import { computed, observable, runInAction } from "mobx";
-// import { observer } from "mobx-react";
-// import { FieldView, FieldViewProps } from './FieldView';
-// import "./LinkButtonBox.scss";
-// import { DocumentView } from "./DocumentView";
-// import { Doc } from "../../../new_fields/Doc";
-// import { LinkButtonField } from "../../../new_fields/LinkButtonField";
-// import { Cast, StrCast, BoolCast } from "../../../new_fields/Types";
-// import { CollectionDockingView } from "../collections/CollectionDockingView";
-// import { DocumentManager } from "../../util/DocumentManager";
-// import { Id } from "../../../new_fields/FieldSymbols";
-
-// library.add(faCaretUp);
-// library.add(faObjectGroup);
-// library.add(faStickyNote);
-// library.add(faFilePdf);
-// library.add(faFilm);
-
-// @observer
-// export class LinkButtonBox extends React.Component<FieldViewProps> {
-//     public static LayoutString() { return FieldView.LayoutString(LinkButtonBox); }
-
-//     followLink = (): void => {
-//         console.log("follow link???");
-//         let field = Cast(this.props.Document[this.props.fieldKey], LinkButtonField, new LinkButtonField({ sourceViewId: "-1", targetViewId: "-1" }));
-//         let targetView = DocumentManager.Instance.getDocumentViewById(field.data.targetViewId);
-//         if (targetView && targetView.props.ContainingCollectionView) {
-//             CollectionDockingView.Instance.AddRightSplit(targetView.props.ContainingCollectionView.props.Document);
-//         }
-//     }
-
-//     render() {
-
-//         let field = Cast(this.props.Document[this.props.fieldKey], LinkButtonField, new LinkButtonField({ sourceViewId: "-1", targetViewId: "-1" }));
-//         let targetView = DocumentManager.Instance.getDocumentViewById(field.data.targetViewId);
-
-//         let text = "Could not find link";
-//         if (targetView) {
-//             let context = targetView.props.ContainingCollectionView ? (" in the context of " + StrCast(targetView.props.ContainingCollectionView.props.Document.title)) : "";
-//             text = "Link to " + StrCast(targetView.props.Document.title) + context;
-//         }
-
-//         let activeDvs = DocumentManager.Instance.DocumentViews.filter(dv => dv.isSelected() || BoolCast(dv.props.Document.libraryBrush, false));
-//         let display = activeDvs.reduce((found, dv) => {
-//             let matchSv = field.data.sourceViewId === StrCast(dv.props.Document[Id]);
-//             let matchTv = field.data.targetViewId === StrCast(dv.props.Document[Id]);
-//             let match = matchSv || matchTv;
-//             return match || found;
-//         }, false);
-
-//         return (
-//             <div className="linkBox-cont" style={{ display: display ? "block" : "none" }}>
-//                 <div className="linkBox-cont-wrapper">
-//                     <p>{text}</p>
-//                 </div>
-//             </div >
-//         );
-//     }
-// }
\ No newline at end of file
diff --git a/src/client/views/nodes/LinkEditor.tsx b/src/client/views/nodes/LinkEditor.tsx
index a6511c3fe..eed34b21f 100644
--- a/src/client/views/nodes/LinkEditor.tsx
+++ b/src/client/views/nodes/LinkEditor.tsx
@@ -71,10 +71,12 @@ class GroupTypesDropdown extends React.Component<GroupTypesDropdownProps> {
 
 
 interface LinkMetadataEditorProps {
+    id: string;
     groupType: string;
     mdDoc: Doc;
     mdKey: string;
     mdValue: string;
+    changeMdIdKey: (id: string, newKey: string) => void;
 }
 @observer
 class LinkMetadataEditor extends React.Component<LinkMetadataEditorProps> {
@@ -86,8 +88,6 @@ class LinkMetadataEditor extends React.Component<LinkMetadataEditorProps> {
     setMetadataKey = (value: string): void => {
         let groupMdKeys = LinkManager.Instance.getMetadataKeysInGroup(this.props.groupType);
 
-        // console.log("set", ...groupMdKeys, typeof (groupMdKeys[0]));
-
         // don't allow user to create existing key
         let newIndex = groupMdKeys.findIndex(key => key.toUpperCase() === value.toUpperCase());
         if (newIndex > -1) {
@@ -105,6 +105,7 @@ class LinkMetadataEditor extends React.Component<LinkMetadataEditorProps> {
         if (currIndex === -1) console.error("LinkMetadataEditor: key was not found");
         groupMdKeys[currIndex] = value;
 
+        this.props.changeMdIdKey(this.props.id, value);
         this._key = value;
         LinkManager.Instance.setMetadataKeysForGroup(this.props.groupType, [...groupMdKeys]);
     }
@@ -133,7 +134,7 @@ class LinkMetadataEditor extends React.Component<LinkMetadataEditorProps> {
         return (
             <div className="linkEditor-metadata-row">
                 <input className={this._keyError ? "linkEditor-error" : ""} type="text" value={this._key} placeholder="key" onChange={e => this.setMetadataKey(e.target.value)}></input>:
-                <input type="text" value={this._value} placeholder="value" onChange={e => this.setMetadataValue(e.target.value)}></input>
+                <input type="text" value={this._value === "new key" ? "" : this._value} placeholder="value" onChange={e => this.setMetadataValue(e.target.value)}></input>
                 <button onClick={() => this.removeMetadata()}><FontAwesomeIcon icon="times" size="sm" /></button>
             </div>
         );
@@ -148,6 +149,17 @@ interface LinkGroupEditorProps {
 @observer
 export class LinkGroupEditor extends React.Component<LinkGroupEditorProps> {
 
+    private _metadataIds: Map<string, string> = new Map();
+
+    constructor(props: LinkGroupEditorProps) {
+        super(props);
+
+        let groupMdKeys = LinkManager.Instance.getMetadataKeysInGroup(StrCast(props.groupDoc.type));
+        groupMdKeys.forEach(key => {
+            this._metadataIds.set(key, Utils.GenerateGuid());
+        });
+    }
+
     @action
     setGroupType = (groupType: string): void => {
         this.props.groupDoc.type = groupType;
@@ -188,12 +200,18 @@ export class LinkGroupEditor extends React.Component<LinkGroupEditorProps> {
 
     @action
     addMetadata = (groupType: string): void => {
+        this._metadataIds.set("new key", Utils.GenerateGuid());
         let mdKeys = LinkManager.Instance.getMetadataKeysInGroup(groupType);
         // only add "new key" if there is no other key with value "new key"; prevents spamming
         if (mdKeys.indexOf("new key") === -1) mdKeys.push("new key");
         LinkManager.Instance.setMetadataKeysForGroup(groupType, mdKeys);
     }
 
+    // for key rendering purposes
+    changeMdIdKey = (id: string, newKey: string) => {
+        this._metadataIds.set(newKey, id);
+    }
+
     renderMetadata = (): JSX.Element[] => {
         let metadata: Array<JSX.Element> = [];
         let groupDoc = this.props.groupDoc;
@@ -201,25 +219,24 @@ export class LinkGroupEditor extends React.Component<LinkGroupEditorProps> {
         let groupType = StrCast(groupDoc.type);
         let groupMdKeys = LinkManager.Instance.getMetadataKeysInGroup(groupType);
 
-        if (groupMdKeys) {
-            groupMdKeys.forEach((key, index) => {
-                metadata.push(
-                    <LinkMetadataEditor key={"mded-" + index} groupType={groupType} mdDoc={mdDoc} mdKey={key} mdValue={(mdDoc[key] === undefined) ? "" : StrCast(mdDoc[key])} />
-                );
-            });
-        }
+        groupMdKeys.forEach((key) => {
+            let val = StrCast(mdDoc[key]);
+            metadata.push(
+                <LinkMetadataEditor key={"mded-" + this._metadataIds.get(key)} id={this._metadataIds.get(key)!} groupType={groupType} mdDoc={mdDoc} mdKey={key} mdValue={val} changeMdIdKey={this.changeMdIdKey} />
+            );
+        });
         return metadata;
     }
 
     viewGroupAsTable = (groupType: string): JSX.Element => {
         let keys = LinkManager.Instance.getMetadataKeysInGroup(groupType);
+        let index = keys.indexOf("");
+        if (index > -1) keys.splice(index, 1);
         let cols = ["anchor1", "anchor2", ...[...keys]];
-        // keys.forEach(k => cols.push(k));
-        // console.log("COLS", ...cols);
         let docs: Doc[] = LinkManager.Instance.getAllMetadataDocsInGroup(groupType);
         let createTable = action(() => Docs.SchemaDocument(cols, docs, { width: 500, height: 300, title: groupType + " table" }));
         let ref = React.createRef<HTMLDivElement>();
-        return <div ref={ref}><button className="linkEditor-button" onPointerDown={SetupDrag(ref, createTable)}><FontAwesomeIcon icon="table" size="sm" /></button></div>;
+        return <div ref={ref}><button className="linkEditor-button" onPointerDown={SetupDrag(ref, createTable)} title="Drag to view relationship table"><FontAwesomeIcon icon="table" size="sm" /></button></div>;
     }
 
     render() {
@@ -233,7 +250,7 @@ export class LinkGroupEditor extends React.Component<LinkGroupEditorProps> {
                     <button className="linkEditor-button" disabled title="Copy group to opposite anchor"><FontAwesomeIcon icon="exchange-alt" size="sm" /></button>
                     <button className="linkEditor-button" onClick={() => this.removeGroupFromLink(groupType)} title="Remove group from link"><FontAwesomeIcon icon="times" size="sm" /></button>
                     <button className="linkEditor-button" disabled title="Delete group"><FontAwesomeIcon icon="trash" size="sm" /></button>
-                    <button className="linkEditor-button" disabled><FontAwesomeIcon icon="table" size="sm" /></button>
+                    <button className="linkEditor-button" disabled title="Drag to view relationship table"><FontAwesomeIcon icon="table" size="sm" /></button>
                 </>
             );
         } else {
diff --git a/src/new_fields/LinkButtonField.ts b/src/new_fields/LinkButtonField.ts
deleted file mode 100644
index e6d1de749..000000000
--- a/src/new_fields/LinkButtonField.ts
+++ /dev/null
@@ -1,35 +0,0 @@
-// import { Deserializable } from "../client/util/SerializationHelper";
-// import { serializable, primitive, createSimpleSchema, object } from "serializr";
-// import { ObjectField } from "./ObjectField";
-// import { Copy, ToScriptString } from "./FieldSymbols";
-// import { Doc } from "./Doc";
-// import { DocumentView } from "../client/views/nodes/DocumentView";
-
-// export type LinkButtonData = {
-//     sourceViewId: string,
-//     targetViewId: string
-// };
-
-// const LinkButtonSchema = createSimpleSchema({
-//     sourceViewId: true,
-//     targetViewId: true
-// });
-
-// @Deserializable("linkButton")
-// export class LinkButtonField extends ObjectField {
-//     @serializable(object(LinkButtonSchema))
-//     readonly data: LinkButtonData;
-
-//     constructor(data: LinkButtonData) {
-//         super();
-//         this.data = data;
-//     }
-
-//     [Copy]() {
-//         return new LinkButtonField(this.data);
-//     }
-
-//     [ToScriptString]() {
-//         return "invalid";
-//     }
-// }
-- 
cgit v1.2.3-70-g09d2


From ca8a78de9957ad27d345ad51fdaee9dae3f096bd Mon Sep 17 00:00:00 2001
From: Fawn <fangrui_tong@brown.edu>
Date: Tue, 25 Jun 2019 20:44:34 -0400
Subject: can't link to containing collection

---
 src/client/documents/Documents.ts     |  7 ++++---
 src/client/util/DragManager.ts        | 16 ++--------------
 src/client/views/nodes/LinkEditor.tsx |  5 +++--
 src/new_fields/Doc.ts                 |  3 ---
 4 files changed, 9 insertions(+), 22 deletions(-)

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

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 547687921..b11b5fdf2 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -35,6 +35,7 @@ import { DateField } from "../../new_fields/DateField";
 import { UndoManager } from "../util/UndoManager";
 import { RouteStore } from "../../server/RouteStore";
 import { LinkManager } from "../util/LinkManager";
+import { DocumentManager } from "../util/DocumentManager";
 var requestImageSize = require('../util/request-image-size');
 var path = require('path');
 
@@ -87,9 +88,9 @@ export namespace DocUtils {
     //     let protoSrc = source.proto ? source.proto : source;
     //     let protoTarg = target.proto ? target.proto : target;
     export function MakeLink(source: Doc, target: Doc, targetContext?: Doc, title: string = "", description: string = "", tags: string = "Default") {
-        if (LinkManager.Instance.doesLinkExist(source, target)) {
-            return;
-        }
+        if (LinkManager.Instance.doesLinkExist(source, target)) return;
+        let sv = DocumentManager.Instance.getDocumentView(source);
+        if (sv && sv.props.ContainingCollectionView && sv.props.ContainingCollectionView.props.Document === target) return;
 
         UndoManager.RunInBatch(() => {
 
diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts
index 4be3d82d3..55d8c570f 100644
--- a/src/client/util/DragManager.ts
+++ b/src/client/util/DragManager.ts
@@ -63,8 +63,6 @@ export async function DragLinksAsDocuments(dragEle: HTMLElement, x: number, y: n
     let srcTarg = sourceDoc.proto;
     let draggedDocs: Doc[] = [];
 
-    // TODO: if not in same context then don't drag
-
     if (srcTarg) {
         let linkDocs = LinkManager.Instance.getAllRelatedLinks(srcTarg);
         if (linkDocs) {
@@ -232,25 +230,20 @@ export namespace DragManager {
             (dropData: { [id: string]: any }) => {
                 dropData.droppedDocuments = dragData.draggedDocuments.map(d => {
                     let dv = DocumentManager.Instance.getDocumentView(d);
-                    // console.log("DRAG", StrCast(d.title));
 
                     if (dv) {
                         if (dv.props.ContainingCollectionView === SelectionManager.SelectedDocuments()[0].props.ContainingCollectionView) {
                             return d;
                         } else {
-                            // return d;
                             let r = Doc.MakeAlias(d);
-                            // DocUtils.MakeLink(sourceDoc, r);
+                            // DocUtils.MakeLink(r, sourceDoc);
                             return r;
                         }
                     } else {
-                        // return d;
                         let r = Doc.MakeAlias(d);
-                        // DocUtils.MakeLink(sourceDoc, r);
+                        // DocUtils.MakeLink(r, sourceDoc);
                         return r;
                     }
-                    // return (dv && dv.props.ContainingCollectionView !== SelectionManager.SelectedDocuments()[0].props.ContainingCollectionView) || !dv ?
-                    //     Doc.MakeAlias(d) : d;
                 });
 
             });
@@ -290,11 +283,6 @@ export namespace DragManager {
         StartDrag([ele], dragData, downX, downY, options);
     }
 
-    // export function StartLinkProxyDrag(ele: HTMLElement, dragData: DocumentDragData, downX: number, downY: number, options?: DragOptions) {
-    //     runInAction(() => StartDragFunctions.map(func => func()));
-    //     StartDrag([ele], dragData, downX, downY, options);
-    // }
-
     export let AbortDrag: () => void = emptyFunction;
 
     function StartDrag(eles: HTMLElement[], dragData: { [id: string]: any }, downX: number, downY: number, options?: DragOptions, finishDrag?: (dropData: { [id: string]: any }) => void) {
diff --git a/src/client/views/nodes/LinkEditor.tsx b/src/client/views/nodes/LinkEditor.tsx
index eed34b21f..232331204 100644
--- a/src/client/views/nodes/LinkEditor.tsx
+++ b/src/client/views/nodes/LinkEditor.tsx
@@ -133,8 +133,8 @@ class LinkMetadataEditor extends React.Component<LinkMetadataEditorProps> {
     render() {
         return (
             <div className="linkEditor-metadata-row">
-                <input className={this._keyError ? "linkEditor-error" : ""} type="text" value={this._key} placeholder="key" onChange={e => this.setMetadataKey(e.target.value)}></input>:
-                <input type="text" value={this._value === "new key" ? "" : this._value} placeholder="value" onChange={e => this.setMetadataValue(e.target.value)}></input>
+                <input className={this._keyError ? "linkEditor-error" : ""} type="text" value={this._key === "new key" ? "" : this._key} placeholder="key" onChange={e => this.setMetadataKey(e.target.value)}></input>:
+                <input type="text" value={this._value} placeholder="value" onChange={e => this.setMetadataValue(e.target.value)}></input>
                 <button onClick={() => this.removeMetadata()}><FontAwesomeIcon icon="times" size="sm" /></button>
             </div>
         );
@@ -221,6 +221,7 @@ export class LinkGroupEditor extends React.Component<LinkGroupEditorProps> {
 
         groupMdKeys.forEach((key) => {
             let val = StrCast(mdDoc[key]);
+            console.log(key, val);
             metadata.push(
                 <LinkMetadataEditor key={"mded-" + this._metadataIds.get(key)} id={this._metadataIds.get(key)!} groupType={groupType} mdDoc={mdDoc} mdKey={key} mdValue={val} changeMdIdKey={this.changeMdIdKey} />
             );
diff --git a/src/new_fields/Doc.ts b/src/new_fields/Doc.ts
index f6fd44d61..d7411e63e 100644
--- a/src/new_fields/Doc.ts
+++ b/src/new_fields/Doc.ts
@@ -260,13 +260,10 @@ export namespace Doc {
                 }
             } else {
                 if (field instanceof RefField) {
-                    console.log("equals field, ref", key);
                     copy[key] = field;
                 } else if (field instanceof ObjectField) {
-                    console.log("copy field, object", key);
                     copy[key] = ObjectField.MakeCopy(field);
                 } else {
-                    console.log("equals field", key);
                     copy[key] = field;
                 }
             }
-- 
cgit v1.2.3-70-g09d2


From a3c4aa24a9e9074da8f2421954f610c8178e10b1 Mon Sep 17 00:00:00 2001
From: Fawn <fangrui_tong@brown.edu>
Date: Tue, 25 Jun 2019 21:28:15 -0400
Subject: link metadata values appear on first load

---
 src/client/documents/Documents.ts     | 19 -------------------
 src/client/util/DocumentManager.ts    |  8 +++-----
 src/client/util/DragManager.ts        |  2 +-
 src/client/views/nodes/LinkEditor.tsx | 11 +++++++----
 4 files changed, 11 insertions(+), 29 deletions(-)

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

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index b11b5fdf2..ddbf8f753 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -68,25 +68,7 @@ export interface DocumentOptions {
 }
 const delegateKeys = ["x", "y", "width", "height", "panX", "panY"];
 
-// export interface LinkData {
-//     anchor1: Doc;
-//     anchor1Page: number;
-//     anchor1Tags: Array<{ tag: string, name: string, description: string }>;
-//     anchor2: Doc;
-//     anchor2Page: number;
-//     anchor2Tags: Array<{ tag: string, name: string, description: string }>;
-// }
-
-// export interface TagData {
-//     tag: string;
-//     name: string;
-//     description: string;
-// }
-
 export namespace DocUtils {
-    // export function MakeLink(source: Doc, target: Doc, targetContext?: Doc, title: string = "", description: string = "", tags: string = "Default") {
-    //     let protoSrc = source.proto ? source.proto : source;
-    //     let protoTarg = target.proto ? target.proto : target;
     export function MakeLink(source: Doc, target: Doc, targetContext?: Doc, title: string = "", description: string = "", tags: string = "Default") {
         if (LinkManager.Instance.doesLinkExist(source, target)) return;
         let sv = DocumentManager.Instance.getDocumentView(source);
@@ -154,7 +136,6 @@ export namespace Docs {
             audioProto = fields[audioProtoId] as Doc || CreateAudioPrototype();
             pdfProto = fields[pdfProtoId] as Doc || CreatePdfPrototype();
             iconProto = fields[iconProtoId] as Doc || CreateIconPrototype();
-            // linkProto = fields[linkProtoId] as Doc || CreateLinkPrototype();
         });
     }
 
diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts
index 767abe63f..d7798ebfd 100644
--- a/src/client/util/DocumentManager.ts
+++ b/src/client/util/DocumentManager.ts
@@ -73,6 +73,9 @@ export class DocumentManager {
             if (doc === toFind) {
                 toReturn.push(view);
             } else {
+                // if (Doc.AreProtosEqual(doc, toFind)) {
+                //     toReturn.push(view);
+
                 let docSrc = FieldValue(doc.proto);
                 if (docSrc && Object.is(docSrc, toFind)) {
                     toReturn.push(view);
@@ -100,11 +103,6 @@ export class DocumentManager {
             return pairs;
         }, [] as { a: DocumentView, b: DocumentView, l: Doc }[]);
 
-        // console.log("LINKED DOCUMENT VIEWS");
-        // pairs.forEach(p => {
-        //     console.log(StrCast(p.a.Document.title), p.a.props.Document[Id], StrCast(p.b.Document.title), p.b.props.Document[Id]);
-        // });
-
         return pairs;
     }
 
diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts
index 55d8c570f..27063d1c2 100644
--- a/src/client/util/DragManager.ts
+++ b/src/client/util/DragManager.ts
@@ -230,7 +230,7 @@ export namespace DragManager {
             (dropData: { [id: string]: any }) => {
                 dropData.droppedDocuments = dragData.draggedDocuments.map(d => {
                     let dv = DocumentManager.Instance.getDocumentView(d);
-
+                    // return d;
                     if (dv) {
                         if (dv.props.ContainingCollectionView === SelectionManager.SelectedDocuments()[0].props.ContainingCollectionView) {
                             return d;
diff --git a/src/client/views/nodes/LinkEditor.tsx b/src/client/views/nodes/LinkEditor.tsx
index 232331204..80eadf668 100644
--- a/src/client/views/nodes/LinkEditor.tsx
+++ b/src/client/views/nodes/LinkEditor.tsx
@@ -1,8 +1,8 @@
-import { observable, computed, action } from "mobx";
+import { observable, computed, action, trace } from "mobx";
 import React = require("react");
 import { observer } from "mobx-react";
 import './LinkEditor.scss';
-import { StrCast, Cast } from "../../../new_fields/Types";
+import { StrCast, Cast, FieldValue } from "../../../new_fields/Types";
 import { Doc } from "../../../new_fields/Doc";
 import { LinkManager } from "../../util/LinkManager";
 import { Docs } from "../../documents/Documents";
@@ -215,7 +215,10 @@ export class LinkGroupEditor extends React.Component<LinkGroupEditorProps> {
     renderMetadata = (): JSX.Element[] => {
         let metadata: Array<JSX.Element> = [];
         let groupDoc = this.props.groupDoc;
-        let mdDoc = Cast(groupDoc.metadata, Doc, new Doc);
+        const mdDoc = FieldValue(Cast(groupDoc.metadata, Doc));
+        if (!mdDoc) {
+            return [];
+        }
         let groupType = StrCast(groupDoc.type);
         let groupMdKeys = LinkManager.Instance.getMetadataKeysInGroup(groupType);
 
@@ -265,7 +268,7 @@ export class LinkGroupEditor extends React.Component<LinkGroupEditorProps> {
                 </>
             );
         }
-
+        trace();
         return (
             <div className="linkEditor-group">
                 <div className="linkEditor-group-row">
-- 
cgit v1.2.3-70-g09d2


From d0ff42632f8a155303e11945a1a974a15052f0db Mon Sep 17 00:00:00 2001
From: Fawn <fangrui_tong@brown.edu>
Date: Wed, 26 Jun 2019 11:40:36 -0400
Subject: link menu styling

---
 src/client/util/LinkManager.ts                     | 44 +---------------------
 .../CollectionFreeFormLinksView.tsx                | 21 +----------
 src/client/views/nodes/DocumentView.tsx            |  2 -
 src/client/views/nodes/LinkEditor.tsx              |  1 -
 src/client/views/nodes/LinkMenu.scss               | 25 +++++++++---
 src/client/views/nodes/LinkMenuGroup.tsx           | 21 ++++++++++-
 src/client/views/nodes/LinkMenuItem.scss           |  2 +
 7 files changed, 44 insertions(+), 72 deletions(-)

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

diff --git a/src/client/util/LinkManager.ts b/src/client/util/LinkManager.ts
index db814082f..97c816001 100644
--- a/src/client/util/LinkManager.ts
+++ b/src/client/util/LinkManager.ts
@@ -24,10 +24,6 @@ import { CurrentUserUtils } from "../../server/authentication/models/current_use
  * - user defined kvps 
  */
 export class LinkManager {
-    // static Instance: LinkManager;
-    // private constructor() {
-    //     LinkManager.Instance = this;
-    // }
 
     private static _instance: LinkManager;
     public static get Instance(): LinkManager {
@@ -36,12 +32,11 @@ export class LinkManager {
     private constructor() {
     }
 
+    // the linkmanagerdoc stores a list of docs representing all linkdocs in 'allLinks' and a list of strings representing all group types in 'allGroupTypes'
+    // lists of strings representing the metadata keys for each group type is stored under a key that is the same as the group type 
     public get LinkManagerDoc(): Doc | undefined {
         return FieldValue(Cast(CurrentUserUtils.UserDocument.linkManagerDoc, Doc));
     }
-    // @observable public allLinks: Array<Doc> = []; //List<Doc> = new List<Doc>([]); // list of link docs
-    // @observable public groupMetadataKeys: Map<string, Array<string>> = new Map();
-    // map of group type to list of its metadata keys; serves as a dictionary of groups to what kind of metadata it holds
 
     public getAllLinks(): Doc[] {
         return LinkManager.Instance.LinkManagerDoc ? LinkManager.Instance.LinkManagerDoc.allLinks ? DocListCast(LinkManager.Instance.LinkManagerDoc.allLinks) : [] : [];
@@ -160,14 +155,6 @@ export class LinkManager {
         LinkManager.Instance.setAnchorGroups(linkDoc, anchor, newGroups);
     }
 
-    // public doesAnchorHaveGroup(linkDoc: Doc, anchor: Doc, groupDoc: Doc): boolean {
-    //     let groups = LinkManager.Instance.getAnchorGroups(linkDoc, anchor);
-    //     let index = groups.findIndex(gDoc => {
-    //         return StrCast(groupDoc.type).toUpperCase() === StrCast(gDoc.type).toUpperCase();
-    //     });
-    //     return index > -1;
-    // }
-
     // returns map of group type to anchor's links in that group type
     public getRelatedGroupedLinks(anchor: Doc): Map<string, Array<Doc>> {
         let related = this.getAllRelatedLinks(anchor);
@@ -195,19 +182,6 @@ export class LinkManager {
         return anchorGroups;
     }
 
-    // public addMetadataKeyToGroup(groupType: string, key: string): boolean {
-    //     if (LinkManager.Instance.LinkManagerDoc) {
-    //         if (LinkManager.Instance.LinkManagerDoc[groupType]) {
-    //             let keyList = LinkManager.Instance.findMetadataKeysInGroup(groupType);
-    //             keyList.push(key);
-    //             LinkManager.Instance.LinkManagerDoc[groupType] = new List<string>(keyList);
-    //             return true;
-    //         }
-    //         return false;
-    //     }
-    //     return false;
-    // }
-
     public getMetadataKeysInGroup(groupType: string): string[] {
         if (LinkManager.Instance.LinkManagerDoc) {
             return LinkManager.Instance.LinkManagerDoc[groupType] ? Cast(LinkManager.Instance.LinkManagerDoc[groupType], listSpec("string"), []) : [];
@@ -254,18 +228,4 @@ export class LinkManager {
             return Cast(linkDoc.anchor1, Doc, new Doc);
         }
     }
-
-
-    // @action
-    // public addLinkProxy(proxy: Doc) {
-    //     LinkManager.Instance.linkProxies.push(proxy);
-    // }
-
-    // public findLinkProxy(sourceViewId: string, targetViewId: string): Doc | undefined {
-    //     let index = LinkManager.Instance.linkProxies.findIndex(p => {
-    //         return StrCast(p.sourceViewId) === sourceViewId && StrCast(p.targetViewId) === targetViewId;
-    //     });
-    //     return index > -1 ? LinkManager.Instance.linkProxies[index] : undefined;
-    // }
-
 }
\ No newline at end of file
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
index bb8e8a5c2..ebeb1fcee 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
@@ -94,24 +94,12 @@ export class CollectionFreeFormLinksView extends React.Component<CollectionViewP
 
     @computed
     get uniqueConnections() {
-        // DocumentManager.Instance.LinkedDocumentViews.forEach(d => {
-        //     console.log("CONNECTION", StrCast(d.a.props.Document.title), StrCast(d.b.props.Document.title));
-        // });
-
-        // console.log("CONNECTIONS");
-
         let connections = DocumentManager.Instance.LinkedDocumentViews.reduce((drawnPairs, connection) => {
             let srcViews = this.documentAnchors(connection.a);
             let targetViews = this.documentAnchors(connection.b);
 
-            // console.log(srcViews.length, targetViews.length);
             let possiblePairs: { a: Doc, b: Doc, }[] = [];
-            srcViews.map(sv => {
-                targetViews.map(tv => {
-                    // console.log("PUSH", StrCast(sv.props.Document.title), StrCast(sv.props.Document.id), StrCast(tv.props.Document.title), StrCast(tv.props.Document.id));
-                    possiblePairs.push({ a: sv.props.Document, b: tv.props.Document });
-                });
-            });
+            srcViews.map(sv => targetViews.map(tv => possiblePairs.push({ a: sv.props.Document, b: tv.props.Document })));
             possiblePairs.map(possiblePair => {
                 if (!drawnPairs.reduce((found, drawnPair) => {
                     let match1 = (Doc.AreProtosEqual(possiblePair.a, drawnPair.a) && Doc.AreProtosEqual(possiblePair.b, drawnPair.b));
@@ -132,13 +120,6 @@ export class CollectionFreeFormLinksView extends React.Component<CollectionViewP
             return <CollectionFreeFormLinkView key={x} A={c.a} B={c.b} LinkDocs={c.l}
                 removeDocument={this.props.removeDocument} addDocument={this.props.addDocument} />;
         });
-
-        // return DocumentManager.Instance.LinkedDocumentViews.map(c => {
-        //     // let x = c.l.reduce((p, l) => p + l[Id], "");
-        //     let x = c.a.Document[Id] + c.b.Document[Id];
-        //     return <CollectionFreeFormLinkView key={x} A={c.a.props.Document} B={c.b.props.Document} LinkDoc={c.l}
-        //         removeDocument={this.props.removeDocument} addDocument={this.props.addDocument} />;
-        // });
     }
 
     render() {
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index c71d7ed68..d77e08089 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -162,9 +162,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
         this._animateToIconDisposer = reaction(() => this.props.Document.isIconAnimating, (values) =>
             (values instanceof List) && this.animateBetweenIcon(values, values[2], values[3] ? true : false)
             , { fireImmediately: true });
-        // console.log("CREATED NEW DOC VIEW", StrCast(this.props.Document.title), DocumentManager.Instance.DocumentViews.length);
         DocumentManager.Instance.DocumentViews.push(this);
-        // console.log("ADDED TO DOC MAN", StrCast(this.props.Document.title), DocumentManager.Instance.DocumentViews.length);
     }
 
     animateBetweenIcon = (iconPos: number[], startTime: number, maximizing: boolean) => {
diff --git a/src/client/views/nodes/LinkEditor.tsx b/src/client/views/nodes/LinkEditor.tsx
index 80eadf668..87ebeefdb 100644
--- a/src/client/views/nodes/LinkEditor.tsx
+++ b/src/client/views/nodes/LinkEditor.tsx
@@ -224,7 +224,6 @@ export class LinkGroupEditor extends React.Component<LinkGroupEditorProps> {
 
         groupMdKeys.forEach((key) => {
             let val = StrCast(mdDoc[key]);
-            console.log(key, val);
             metadata.push(
                 <LinkMetadataEditor key={"mded-" + this._metadataIds.get(key)} id={this._metadataIds.get(key)!} groupType={groupType} mdDoc={mdDoc} mdKey={key} mdValue={val} changeMdIdKey={this.changeMdIdKey} />
             );
diff --git a/src/client/views/nodes/LinkMenu.scss b/src/client/views/nodes/LinkMenu.scss
index ae3446e25..1dd933c32 100644
--- a/src/client/views/nodes/LinkMenu.scss
+++ b/src/client/views/nodes/LinkMenu.scss
@@ -20,13 +20,28 @@
     }
 
     .linkMenu-group-name {
-        padding: 4px 6px;
-        line-height: 12px;
-        border-radius: 5px;
-        font-weight: bold;
+        display: flex;
 
         &:hover {
-            background-color: lightgray;
+            p {
+                background-color: lightgray;
+                width: calc(100% - 26px);
+            }
+            .linkEditor-tableButton {
+                display: block;
+            }
+        }
+
+        p {
+            width: 100%;
+            padding: 4px 6px;
+            line-height: 12px;
+            border-radius: 5px;
+            font-weight: bold;
+        }
+
+        .linkEditor-tableButton {
+            display: none;
         }
     }
 }
diff --git a/src/client/views/nodes/LinkMenuGroup.tsx b/src/client/views/nodes/LinkMenuGroup.tsx
index 71326f703..732e76997 100644
--- a/src/client/views/nodes/LinkMenuGroup.tsx
+++ b/src/client/views/nodes/LinkMenuGroup.tsx
@@ -8,8 +8,10 @@ import React = require("react");
 import { Doc, DocListCast } from "../../../new_fields/Doc";
 import { Id } from "../../../new_fields/FieldSymbols";
 import { LinkManager } from "../../util/LinkManager";
-import { DragLinksAsDocuments, DragManager } from "../../util/DragManager";
+import { DragLinksAsDocuments, DragManager, SetupDrag } from "../../util/DragManager";
 import { emptyFunction } from "../../../Utils";
+import { Docs } from "../../documents/Documents";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
 
 interface LinkMenuGroupProps {
     sourceDoc: Doc;
@@ -22,6 +24,7 @@ interface LinkMenuGroupProps {
 export class LinkMenuGroup extends React.Component<LinkMenuGroupProps> {
 
     private _drag = React.createRef<HTMLDivElement>();
+    private _table = React.createRef<HTMLDivElement>();
 
     onLinkButtonDown = (e: React.PointerEvent): void => {
         e.stopPropagation();
@@ -55,6 +58,17 @@ export class LinkMenuGroup extends React.Component<LinkMenuGroupProps> {
         e.stopPropagation();
     }
 
+    viewGroupAsTable = (groupType: string): JSX.Element => {
+        let keys = LinkManager.Instance.getMetadataKeysInGroup(groupType);
+        let index = keys.indexOf("");
+        if (index > -1) keys.splice(index, 1);
+        let cols = ["anchor1", "anchor2", ...[...keys]];
+        let docs: Doc[] = LinkManager.Instance.getAllMetadataDocsInGroup(groupType);
+        let createTable = action(() => Docs.SchemaDocument(cols, docs, { width: 500, height: 300, title: groupType + " table" }));
+        let ref = React.createRef<HTMLDivElement>();
+        return <div ref={ref}><button className="linkEditor-button linkEditor-tableButton" onPointerDown={SetupDrag(ref, createTable)} title="Drag to view relationship table"><FontAwesomeIcon icon="table" size="sm" /></button></div>;
+    }
+
     render() {
         let groupItems = this.props.group.map(linkDoc => {
             let destination = LinkManager.Instance.getOppositeAnchor(linkDoc, this.props.sourceDoc);
@@ -64,7 +78,10 @@ export class LinkMenuGroup extends React.Component<LinkMenuGroupProps> {
 
         return (
             <div className="linkMenu-group">
-                <p className="linkMenu-group-name" ref={this._drag} onPointerDown={this.onLinkButtonDown} >{this.props.groupType}:</p>
+                <div className="linkMenu-group-name">
+                    <p ref={this._drag} onPointerDown={this.onLinkButtonDown}>{this.props.groupType}:</p>
+                    {this.viewGroupAsTable(this.props.groupType)}
+                </div>
                 <div className="linkMenu-group-wrapper">
                     {groupItems}
                 </div>
diff --git a/src/client/views/nodes/LinkMenuItem.scss b/src/client/views/nodes/LinkMenuItem.scss
index 25d167231..175a93cb2 100644
--- a/src/client/views/nodes/LinkMenuItem.scss
+++ b/src/client/views/nodes/LinkMenuItem.scss
@@ -1,4 +1,5 @@
 @import "../globalCssVariables";
+
 .linkMenu-item {
     // border-top: 0.5px solid $main-accent; 
     position: relative;
@@ -13,6 +14,7 @@
             padding: 4px 6px;
             line-height: 12px;
             border-radius: 5px;
+            overflow-wrap: break-word;
         }
     }
 
-- 
cgit v1.2.3-70-g09d2


From 69e37491908b5c189b94f780994c1f142c69be2e Mon Sep 17 00:00:00 2001
From: Fawn <fangrui_tong@brown.edu>
Date: Wed, 26 Jun 2019 14:15:40 -0400
Subject: minor changes

---
 src/client/util/DocumentManager.ts       |  1 -
 src/client/util/DragManager.ts           | 24 +--------
 src/client/util/LinkManager.ts           | 18 ++++---
 src/client/views/nodes/LinkEditor.scss   | 12 ++---
 src/client/views/nodes/LinkEditor.tsx    | 14 ++++--
 src/client/views/nodes/LinkMenu.scss     | 84 +++++++++++++++++++++++++++++++
 src/client/views/nodes/LinkMenu.tsx      | 11 ++--
 src/client/views/nodes/LinkMenuItem.scss | 86 --------------------------------
 src/client/views/nodes/LinkMenuItem.tsx  |  5 +-
 9 files changed, 117 insertions(+), 138 deletions(-)
 delete mode 100644 src/client/views/nodes/LinkMenuItem.scss

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

diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts
index d7798ebfd..877475347 100644
--- a/src/client/util/DocumentManager.ts
+++ b/src/client/util/DocumentManager.ts
@@ -68,7 +68,6 @@ export class DocumentManager {
         //gets document view that is in a freeform canvas collection
         DocumentManager.Instance.DocumentViews.map(view => {
             let doc = view.props.Document;
-            // if (view.props.ContainingCollectionView instanceof CollectionFreeFormView) {
 
             if (doc === toFind) {
                 toReturn.push(view);
diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts
index 27063d1c2..8e6abe18e 100644
--- a/src/client/util/DragManager.ts
+++ b/src/client/util/DragManager.ts
@@ -71,7 +71,6 @@ export async function DragLinksAsDocuments(dragEle: HTMLElement, x: number, y: n
             });
         }
     }
-    // draggedDocs.push(...draggedFromDocs);
     if (draggedDocs.length) {
         let moddrag: Doc[] = [];
         for (const draggedDoc of draggedDocs) {
@@ -79,20 +78,6 @@ export async function DragLinksAsDocuments(dragEle: HTMLElement, x: number, y: n
             if (doc) moddrag.push(doc);
         }
         let dragData = new DragManager.DocumentDragData(moddrag.length ? moddrag : draggedDocs);
-        // dragData.moveDocument = (document, targetCollection, addDocument) => {
-        //     return false;
-        // };
-
-        // runInAction(() => StartDragFunctions.map(func => func()));
-        // (eles, dragData, downX, downY, options,
-        //     (dropData: { [id: string]: any }) => {
-        //         (dropData.droppedDocuments = dragData.userDropAction === "alias" || (!dragData.userDropAction && dragData.dropAction === "alias") ?
-        //             dragData.draggedDocuments.map(d => Doc.MakeAlias(d)) :
-        //             dragData.userDropAction === "copy" || (!dragData.userDropAction && dragData.dropAction === "copy") ?
-        //                 dragData.draggedDocuments.map(d => Doc.MakeCopy(d, true)) :
-        //                 dragData.draggedDocuments
-        //         );
-        //     });
         DragManager.StartLinkedDocumentDrag([dragEle], sourceDoc, dragData, x, y, {
             handlers: {
                 dragComplete: action(emptyFunction),
@@ -230,19 +215,14 @@ export namespace DragManager {
             (dropData: { [id: string]: any }) => {
                 dropData.droppedDocuments = dragData.draggedDocuments.map(d => {
                     let dv = DocumentManager.Instance.getDocumentView(d);
-                    // return d;
                     if (dv) {
                         if (dv.props.ContainingCollectionView === SelectionManager.SelectedDocuments()[0].props.ContainingCollectionView) {
                             return d;
                         } else {
-                            let r = Doc.MakeAlias(d);
-                            // DocUtils.MakeLink(r, sourceDoc);
-                            return r;
+                            return Doc.MakeAlias(d);
                         }
                     } else {
-                        let r = Doc.MakeAlias(d);
-                        // DocUtils.MakeLink(r, sourceDoc);
-                        return r;
+                        return Doc.MakeAlias(d);
                     }
                 });
 
diff --git a/src/client/util/LinkManager.ts b/src/client/util/LinkManager.ts
index 97c816001..1db686751 100644
--- a/src/client/util/LinkManager.ts
+++ b/src/client/util/LinkManager.ts
@@ -151,7 +151,7 @@ export class LinkManager {
     // removes group doc of given group type only from given anchor on given link
     public removeGroupFromAnchor(linkDoc: Doc, anchor: Doc, groupType: string) {
         let groups = LinkManager.Instance.getAnchorGroups(linkDoc, anchor);
-        let newGroups = groups.filter(groupDoc => { StrCast(groupDoc.type).toUpperCase() !== groupType.toUpperCase() });
+        let newGroups = groups.filter(groupDoc => StrCast(groupDoc.type).toUpperCase() !== groupType.toUpperCase());
         LinkManager.Instance.setAnchorGroups(linkDoc, anchor, newGroups);
     }
 
@@ -165,23 +165,25 @@ export class LinkManager {
             if (groups.length > 0) {
                 groups.forEach(groupDoc => {
                     let groupType = StrCast(groupDoc.type);
-                    let group = anchorGroups.get(groupType);
-                    if (group) group.push(link);
-                    else group = [link];
-                    anchorGroups.set(groupType, group);
+                    if (groupType === "") {
+                        let group = anchorGroups.get("*");
+                        anchorGroups.set("*", group ? [...group, link] : [link]);
+                    } else {
+                        let group = anchorGroups.get(groupType);
+                        anchorGroups.set(groupType, group ? [...group, link] : [link]);
+                    }
                 });
             } else {
                 // if link is in no groups then put it in default group
                 let group = anchorGroups.get("*");
-                if (group) group.push(link);
-                else group = [link];
-                anchorGroups.set("*", group);
+                anchorGroups.set("*", group ? [...group, link] : [link]);
             }
 
         });
         return anchorGroups;
     }
 
+    // gets a list of strings representing the keys of the metadata associated with the given group type
     public getMetadataKeysInGroup(groupType: string): string[] {
         if (LinkManager.Instance.LinkManagerDoc) {
             return LinkManager.Instance.LinkManagerDoc[groupType] ? Cast(LinkManager.Instance.LinkManagerDoc[groupType], listSpec("string"), []) : [];
diff --git a/src/client/views/nodes/LinkEditor.scss b/src/client/views/nodes/LinkEditor.scss
index 2602b8816..1424d7633 100644
--- a/src/client/views/nodes/LinkEditor.scss
+++ b/src/client/views/nodes/LinkEditor.scss
@@ -10,18 +10,15 @@
     margin-bottom: 6px;
 }
 
-.linKEditor-info {
+.linkEditor-info {
     border-bottom: 0.5px solid $light-color-secondary; 
     padding-bottom: 6px;
     margin-bottom: 6px;
     display: flex;
     justify-content: space-between;
 
-    .linkEditor-delete {
-        width: 20px;
-        height: 20px;
-        margin-left: 6px;
-        padding: 0;
+    .linkEditor-linkedTo {
+        width: calc(100% - 26px);
     }
 }
 
@@ -105,8 +102,7 @@
         cursor: pointer;
 
         &:hover {
-            background-color: $intermediate-color;
-            font-weight: bold;
+            background-color: lightgray;
         }
     }
 }
diff --git a/src/client/views/nodes/LinkEditor.tsx b/src/client/views/nodes/LinkEditor.tsx
index 87ebeefdb..51efcc36d 100644
--- a/src/client/views/nodes/LinkEditor.tsx
+++ b/src/client/views/nodes/LinkEditor.tsx
@@ -11,7 +11,6 @@ import { faArrowLeft, faEllipsisV, faTable, faTrash, faCog, faExchangeAlt, faTim
 import { library } from "@fortawesome/fontawesome-svg-core";
 import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
 import { SetupDrag } from "../../util/DragManager";
-import { anchorPoints, Flyout } from "../DocumentDecorations";
 
 library.add(faArrowLeft, faEllipsisV, faTable, faTrash, faCog, faExchangeAlt, faTimes, faPlus);
 
@@ -35,6 +34,11 @@ class GroupTypesDropdown extends React.Component<GroupTypesDropdownProps> {
         LinkManager.Instance.addGroupType(groupType);
     }
 
+    onChange = (val: string): void => {
+        this.setSearchTerm(val);
+        this.setGroupType(val);
+    }
+
     renderOptions = (): JSX.Element[] | JSX.Element => {
         if (this._searchTerm === "") return <></>;
 
@@ -59,12 +63,12 @@ class GroupTypesDropdown extends React.Component<GroupTypesDropdownProps> {
     render() {
         return (
             <div className="linkEditor-dropdown">
-                <input type="text" value={this._groupType} placeholder="Search for a group or create a new group"
-                    onChange={e => { this.setSearchTerm(e.target.value); this.setGroupType(e.target.value); }}></input>
+                <input type="text" value={this._groupType} placeholder="Search for or create a new group"
+                    onChange={e => this.onChange(e.target.value)}></input>
                 <div className="linkEditor-options-wrapper">
                     {this.renderOptions()}
                 </div>
-            </div>
+            </div >
         );
     }
 }
@@ -326,7 +330,7 @@ export class LinkEditor extends React.Component<LinkEditorProps> {
                 <button className="linkEditor-back" onPointerDown={() => this.props.showLinks()}><FontAwesomeIcon icon="arrow-left" size="sm" /></button>
                 <div className="linkEditor-info">
                     <p className="linkEditor-linkedTo">editing link to: <b>{destination.proto!.title}</b></p>
-                    <button className="linkEditor-delete linkEditor-button" onPointerDown={() => this.deleteLink()} title="Delete link"><FontAwesomeIcon icon="trash" size="sm" /></button>
+                    <button className="linkEditor-button" onPointerDown={() => this.deleteLink()} title="Delete link"><FontAwesomeIcon icon="trash" size="sm" /></button>
                 </div>
                 <div className="linkEditor-groupsLabel">
                     <b>Relationships:</b>
diff --git a/src/client/views/nodes/LinkMenu.scss b/src/client/views/nodes/LinkMenu.scss
index 1dd933c32..429977326 100644
--- a/src/client/views/nodes/LinkMenu.scss
+++ b/src/client/views/nodes/LinkMenu.scss
@@ -46,5 +46,89 @@
     }
 }
 
+.linkMenu-item {
+    // border-top: 0.5px solid $main-accent; 
+    position: relative;
+    display: flex;
+    font-size: 12px;
+    
+
+    .link-name {
+        position: relative;
+
+        p {
+            padding: 4px 6px;
+            line-height: 12px;
+            border-radius: 5px;
+            overflow-wrap: break-word;
+        }
+    }
+
+    .linkMenu-item-content {
+        width: 100%;
+    }
+    
+    .link-metadata {
+        padding: 0 10px 0 16px;
+        margin-bottom: 4px;
+        color: $main-accent;
+        font-style: italic;
+        font-size: 10.5px;
+    }
+
+    &:hover {
+        .linkMenu-item-buttons {
+            display: flex;
+        }
+        .linkMenu-item-content {
+            &.expand-two p {
+                width: calc(100% - 52px);
+                background-color: lightgray;
+            }
+            &.expand-three p {
+                width: calc(100% - 84px);
+                background-color: lightgray;
+            }
+        }
+    }
+}
+
+.linkMenu-item-buttons {
+    display: none;
+    position: absolute;
+    top: 50%;
+    right: 0;
+    transform: translateY(-50%);
+
+    .button {
+        width: 20px;
+        height: 20px;
+        margin: 0;
+        margin-right: 6px;
+        border-radius: 50%;
+        cursor: pointer;
+        pointer-events: auto;
+        background-color: $dark-color;
+        color: $light-color;
+        font-size: 65%;
+        transition: transform 0.2s;
+        text-align: center;
+        position: relative;
+
+        .fa-icon {
+            position: absolute;
+            top: 50%;
+            left: 50%;
+            transform: translate(-50%, -50%);
+        }
+
+        &:last-child {
+            margin-right: 0;
+        }
+        &:hover {
+            background: $main-accent;
+        }
+    }
+}
 
 
diff --git a/src/client/views/nodes/LinkMenu.tsx b/src/client/views/nodes/LinkMenu.tsx
index 04ca47db3..8ef899cfc 100644
--- a/src/client/views/nodes/LinkMenu.tsx
+++ b/src/client/views/nodes/LinkMenu.tsx
@@ -1,15 +1,11 @@
 import { action, observable } from "mobx";
 import { observer } from "mobx-react";
 import { DocumentView } from "./DocumentView";
-import { LinkMenuItem } from "./LinkMenuItem";
 import { LinkEditor } from "./LinkEditor";
 import './LinkMenu.scss';
 import React = require("react");
-import { Doc, DocListCast } from "../../../new_fields/Doc";
-import { Id } from "../../../new_fields/FieldSymbols";
+import { Doc } from "../../../new_fields/Doc";
 import { LinkManager } from "../../util/LinkManager";
-import { DragLinksAsDocuments, DragManager } from "../../util/DragManager";
-import { emptyFunction } from "../../../Utils";
 import { LinkMenuGroup } from "./LinkMenuGroup";
 
 interface Props {
@@ -22,6 +18,11 @@ export class LinkMenu extends React.Component<Props> {
 
     @observable private _editingLink?: Doc;
 
+    @action
+    componentWillReceiveProps() {
+        this._editingLink = undefined;
+    }
+
     renderAllGroups = (groups: Map<string, Array<Doc>>): Array<JSX.Element> => {
         let linkItems: Array<JSX.Element> = [];
         groups.forEach((group, groupType) => {
diff --git a/src/client/views/nodes/LinkMenuItem.scss b/src/client/views/nodes/LinkMenuItem.scss
deleted file mode 100644
index 175a93cb2..000000000
--- a/src/client/views/nodes/LinkMenuItem.scss
+++ /dev/null
@@ -1,86 +0,0 @@
-@import "../globalCssVariables";
-
-.linkMenu-item {
-    // border-top: 0.5px solid $main-accent; 
-    position: relative;
-    display: flex;
-    font-size: 12px;
-    
-
-    .link-name {
-        position: relative;
-
-        p {
-            padding: 4px 6px;
-            line-height: 12px;
-            border-radius: 5px;
-            overflow-wrap: break-word;
-        }
-    }
-
-    .linkMenu-item-content {
-        width: 100%;
-    }
-    
-    .link-metadata {
-        padding: 0 10px 0 16px;
-        margin-bottom: 4px;
-        color: $main-accent;
-        font-style: italic;
-        font-size: 10.5px;
-    }
-
-    &:hover {
-        .linkMenu-item-buttons {
-            display: flex;
-        }
-        .linkMenu-item-content {
-            &.expand-two p {
-                width: calc(100% - 52px);
-                background-color: lightgray;
-            }
-            &.expand-three p {
-                width: calc(100% - 84px);
-                background-color: lightgray;
-            }
-        }
-    }
-}
-
-.linkMenu-item-buttons {
-    display: none;
-    position: absolute;
-    top: 50%;
-    right: 0;
-    transform: translateY(-50%);
-
-    .button {
-        width: 20px;
-        height: 20px;
-        margin: 0;
-        margin-right: 6px;
-        border-radius: 50%;
-        cursor: pointer;
-        pointer-events: auto;
-        background-color: $dark-color;
-        color: $light-color;
-        font-size: 65%;
-        transition: transform 0.2s;
-        text-align: center;
-        position: relative;
-
-        .fa-icon {
-            position: absolute;
-            top: 50%;
-            left: 50%;
-            transform: translate(-50%, -50%);
-        }
-
-        &:last-child {
-            margin-right: 0;
-        }
-        &:hover {
-            background: $main-accent;
-        }
-    }
-}
\ No newline at end of file
diff --git a/src/client/views/nodes/LinkMenuItem.tsx b/src/client/views/nodes/LinkMenuItem.tsx
index 28694721d..486e3dc9b 100644
--- a/src/client/views/nodes/LinkMenuItem.tsx
+++ b/src/client/views/nodes/LinkMenuItem.tsx
@@ -4,14 +4,13 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { observer } from "mobx-react";
 import { DocumentManager } from "../../util/DocumentManager";
 import { undoBatch } from "../../util/UndoManager";
-import './LinkMenuItem.scss';
+import './LinkMenu.scss';
 import React = require("react");
 import { Doc } from '../../../new_fields/Doc';
 import { StrCast, Cast } from '../../../new_fields/Types';
 import { observable, action } from 'mobx';
 import { LinkManager } from '../../util/LinkManager';
-import { DragLinksAsDocuments, DragLinkAsDocument } from '../../util/DragManager';
-import { SelectionManager } from '../../util/SelectionManager';
+import { DragLinkAsDocument } from '../../util/DragManager';
 import { CollectionDockingView } from '../collections/CollectionDockingView';
 library.add(faEye, faEdit, faTimes, faArrowRight, faChevronDown, faChevronUp);
 
-- 
cgit v1.2.3-70-g09d2


From a81677c7dffafa5134d4c5cbe893f7a886eaab63 Mon Sep 17 00:00:00 2001
From: Fawn <fangrui_tong@brown.edu>
Date: Wed, 26 Jun 2019 14:48:16 -0400
Subject: can clear links on a doc

---
 src/client/util/LinkManager.ts         |  5 +++++
 src/client/views/nodes/LinkEditor.scss | 14 +++++++++-----
 src/client/views/nodes/LinkEditor.tsx  |  1 +
 src/client/views/nodes/LinkMenu.scss   |  4 ++++
 src/client/views/nodes/LinkMenu.tsx    | 10 ++++++++++
 5 files changed, 29 insertions(+), 5 deletions(-)

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

diff --git a/src/client/util/LinkManager.ts b/src/client/util/LinkManager.ts
index 1db686751..f2f3e51dd 100644
--- a/src/client/util/LinkManager.ts
+++ b/src/client/util/LinkManager.ts
@@ -75,6 +75,11 @@ export class LinkManager {
         return related;
     }
 
+    public deleteAllLinksOnAnchor(anchor: Doc) {
+        let related = LinkManager.Instance.getAllRelatedLinks(anchor);
+        related.forEach(linkDoc => LinkManager.Instance.deleteLink(linkDoc));
+    }
+
     public addGroupType(groupType: string): boolean {
         if (LinkManager.Instance.LinkManagerDoc) {
             LinkManager.Instance.LinkManagerDoc[groupType] = new List<string>([]);
diff --git a/src/client/views/nodes/LinkEditor.scss b/src/client/views/nodes/LinkEditor.scss
index 1424d7633..3c49c2212 100644
--- a/src/client/views/nodes/LinkEditor.scss
+++ b/src/client/views/nodes/LinkEditor.scss
@@ -47,8 +47,8 @@
     border-radius: 3px;
 
     .linkEditor-group-row {
-        display: flex;
-        margin-bottom: 6px;
+        // display: flex;
+        margin-bottom: 3px;
 
         .linkEditor-group-row-label {
             margin-right: 6px;
@@ -65,16 +65,16 @@
         }
 
         input {
-            width: calc(50% - 18px);
+            width: calc(50% - 16px);
             height: 20px;
         }
 
         button {
             width: 20px;
             height: 20px;
-            margin-left: 6px;
+            margin-left: 3px;
             padding: 0;
-            font-size: 14px;
+            font-size: 10px;
         }
     }
 }
@@ -85,6 +85,10 @@
     position: relative;
     z-index: 999;
 
+    input {
+        width: 100%;
+    }
+
     .linkEditor-options-wrapper {
         width: 100%;
         position: absolute;
diff --git a/src/client/views/nodes/LinkEditor.tsx b/src/client/views/nodes/LinkEditor.tsx
index 51efcc36d..22da732cf 100644
--- a/src/client/views/nodes/LinkEditor.tsx
+++ b/src/client/views/nodes/LinkEditor.tsx
@@ -278,6 +278,7 @@ export class LinkGroupEditor extends React.Component<LinkGroupEditorProps> {
                     <p className="linkEditor-group-row-label">type:</p>
                     <GroupTypesDropdown groupType={groupType} setGroupType={this.setGroupType} />
                 </div>
+                {this.renderMetadata().length > 0 ? <p className="linkEditor-group-row-label">metadata:</p> : <></>}
                 {this.renderMetadata()}
                 <div className="linkEditor-group-buttons">
                     {buttons}
diff --git a/src/client/views/nodes/LinkMenu.scss b/src/client/views/nodes/LinkMenu.scss
index 429977326..7cc11172b 100644
--- a/src/client/views/nodes/LinkMenu.scss
+++ b/src/client/views/nodes/LinkMenu.scss
@@ -131,4 +131,8 @@
     }
 }
 
+.linkEditor-clearButton {
+    float: right;
+}
+
 
diff --git a/src/client/views/nodes/LinkMenu.tsx b/src/client/views/nodes/LinkMenu.tsx
index 8ef899cfc..71384c368 100644
--- a/src/client/views/nodes/LinkMenu.tsx
+++ b/src/client/views/nodes/LinkMenu.tsx
@@ -7,6 +7,11 @@ import React = require("react");
 import { Doc } from "../../../new_fields/Doc";
 import { LinkManager } from "../../util/LinkManager";
 import { LinkMenuGroup } from "./LinkMenuGroup";
+import { faTrash } from '@fortawesome/free-solid-svg-icons';
+import { library } from "@fortawesome/fontawesome-svg-core";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+
+library.add(faTrash)
 
 interface Props {
     docView: DocumentView;
@@ -23,6 +28,10 @@ export class LinkMenu extends React.Component<Props> {
         this._editingLink = undefined;
     }
 
+    clearAllLinks = () => {
+        LinkManager.Instance.deleteAllLinksOnAnchor(this.props.docView.props.Document);
+    }
+
     renderAllGroups = (groups: Map<string, Array<Doc>>): Array<JSX.Element> => {
         let linkItems: Array<JSX.Element> = [];
         groups.forEach((group, groupType) => {
@@ -43,6 +52,7 @@ export class LinkMenu extends React.Component<Props> {
         if (this._editingLink === undefined) {
             return (
                 <div className="linkMenu">
+                    <button className="linkEditor-button linkEditor-clearButton" onClick={() => this.clearAllLinks()} title="Clear all links"><FontAwesomeIcon icon="trash" size="sm" /></button>
                     {/* <input id="linkMenu-searchBar" type="text" placeholder="Search..."></input> */}
                     <div className="linkMenu-list">
                         {this.renderAllGroups(groups)}
-- 
cgit v1.2.3-70-g09d2

