From a37faf30652c6c593923198ad34e54377e670caf Mon Sep 17 00:00:00 2001
From: Bob Zeleznik <zzzman@gmail.com>
Date: Wed, 5 Jun 2019 08:09:10 -0400
Subject: fixed full screen text bug.  cleaned up imports

---
 .../collections/collectionFreeForm/CollectionFreeFormLinkView.tsx  | 7 +++----
 1 file changed, 3 insertions(+), 4 deletions(-)

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

diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
index 301b769af..ca7c99f28 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
@@ -1,11 +1,10 @@
 import { observer } from "mobx-react";
-import { Utils } from "../../../../Utils";
+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 { StrCast, NumCast, BoolCast } from "../../../../new_fields/Types";
-import { Doc, WidthSym, HeightSym } from "../../../../new_fields/Doc";
-import { InkingControl } from "../../InkingControl";
 
 export interface CollectionFreeFormLinkViewProps {
     A: Doc;
-- 
cgit v1.2.3-70-g09d2


From 06d5bb5c65da6f4a115ebf8118989115420bccef Mon Sep 17 00:00:00 2001
From: bob <bcz@cs.brown.edu>
Date: Tue, 11 Jun 2019 10:21:42 -0400
Subject: cleaned up some drop() code with text.  fixed linkview titling. 
 changed dragging to not deselect source collection.

---
 src/client/util/SelectionManager.ts                | 23 ++++++++++++++++------
 .../views/collections/CollectionBaseView.tsx       |  5 +++--
 .../CollectionFreeFormLinkView.tsx                 |  5 +++--
 src/client/views/nodes/FormattedTextBox.tsx        | 22 ---------------------
 4 files changed, 23 insertions(+), 32 deletions(-)

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

diff --git a/src/client/util/SelectionManager.ts b/src/client/util/SelectionManager.ts
index ddad4b818..b26032b04 100644
--- a/src/client/util/SelectionManager.ts
+++ b/src/client/util/SelectionManager.ts
@@ -10,15 +10,23 @@ export namespace SelectionManager {
         @observable SelectedDocuments: Array<DocumentView> = [];
 
         @action
-        SelectDoc(doc: DocumentView, ctrlPressed: boolean): void {
+        SelectDoc(docView: DocumentView, ctrlPressed: boolean): void {
             // if doc is not in SelectedDocuments, add it
             if (!ctrlPressed) {
                 this.DeselectAll();
             }
 
-            if (manager.SelectedDocuments.indexOf(doc) === -1) {
-                manager.SelectedDocuments.push(doc);
-                doc.props.whenActiveChanged(true);
+            if (manager.SelectedDocuments.indexOf(docView) === -1) {
+                manager.SelectedDocuments.push(docView);
+                docView.props.whenActiveChanged(true);
+            }
+        }
+        @action
+        DeselectDoc(docView: DocumentView): void {
+            let ind = manager.SelectedDocuments.indexOf(docView);
+            if (ind !== -1) {
+                manager.SelectedDocuments.splice(ind, 1);
+                docView.props.whenActiveChanged(false);
             }
         }
         @action
@@ -31,8 +39,11 @@ export namespace SelectionManager {
 
     const manager = new Manager();
 
-    export function SelectDoc(doc: DocumentView, ctrlPressed: boolean): void {
-        manager.SelectDoc(doc, ctrlPressed);
+    export function DeselectDoc(docView: DocumentView): void {
+        manager.DeselectDoc(docView);
+    }
+    export function SelectDoc(docView: DocumentView, ctrlPressed: boolean): void {
+        manager.SelectDoc(docView, ctrlPressed);
     }
 
     export function IsSelected(doc: DocumentView): boolean {
diff --git a/src/client/views/collections/CollectionBaseView.tsx b/src/client/views/collections/CollectionBaseView.tsx
index d120c3a0c..5238ad114 100644
--- a/src/client/views/collections/CollectionBaseView.tsx
+++ b/src/client/views/collections/CollectionBaseView.tsx
@@ -10,6 +10,7 @@ import { SelectionManager } from '../../util/SelectionManager';
 import { ContextMenu } from '../ContextMenu';
 import { FieldViewProps } from '../nodes/FieldView';
 import './CollectionBaseView.scss';
+import { DocumentManager } from '../../util/DocumentManager';
 
 export enum CollectionViewType {
     Invalid,
@@ -129,7 +130,8 @@ export class CollectionBaseView extends React.Component<CollectionViewProps> {
 
     @action.bound
     removeDocument(doc: Doc): boolean {
-        SelectionManager.DeselectAll();
+        let docView = DocumentManager.Instance.getDocumentView(doc, this.props.ContainingCollectionView)
+        docView && SelectionManager.DeselectDoc(docView);
         const props = this.props;
         //TODO This won't create the field if it doesn't already exist
         const value = Cast(props.Document[props.fieldKey], listSpec(Doc), []);
@@ -163,7 +165,6 @@ export class CollectionBaseView extends React.Component<CollectionViewProps> {
             return true;
         }
         if (this.removeDocument(doc)) {
-            SelectionManager.DeselectAll();
             return addDocument(doc);
         }
         return false;
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
index ca7c99f28..ba7e6cf9e 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
@@ -44,8 +44,9 @@ 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 = "";
-        this.props.LinkDocs.map(l => text += StrCast(l.title) + "(" + StrCast(l.linkDescription) + "), ");
-        text = text.substr(0, text.length - 2);
+        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"
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index 4325fd48c..2923fd378 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -75,7 +75,6 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
     private _toolTipTextMenu: TooltipTextMenu | undefined = undefined;
     private _lastState: any = undefined;
     private _applyingChange: boolean = false;
-    private _dropDisposer?: DragManager.DragDropDisposer;
     private _linkClicked = "";
     private _reactionDisposer: Opt<IReactionDisposer>;
     private _inputReactionDisposer: Opt<IReactionDisposer>;
@@ -136,25 +135,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
         }
     }
 
-    @undoBatch
-    @action
-    drop = async (e: Event, de: DragManager.DropEvent) => {
-        if (de.data instanceof DragManager.LinkDragData) {
-            let sourceDoc = de.data.linkSourceDocument;
-            let destDoc = this.props.Document;
-
-            DocUtils.MakeLink(sourceDoc, destDoc);
-            de.data.droppedDocuments.push(destDoc);
-            e.stopPropagation();
-        }
-    }
-
     componentDidMount() {
-        if (this._ref.current) {
-            this._dropDisposer = DragManager.MakeDropTarget(this._ref.current, {
-                handlers: { drop: this.drop.bind(this) }
-            });
-        }
         const config = {
             schema,
             inpRules, //these currently don't do anything, but could eventually be helpful
@@ -250,9 +231,6 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
         if (this._proxyReactionDisposer) {
             this._proxyReactionDisposer();
         }
-        if (this._dropDisposer) {
-            this._dropDisposer();
-        }
     }
 
     onPointerDown = (e: React.PointerEvent): void => {
-- 
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/collections/collectionFreeForm/CollectionFreeFormLinkView.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 d91e7eec9a62363b383b929166cdf600b124334c Mon Sep 17 00:00:00 2001
From: Fawn <fangrui_tong@brown.edu>
Date: Tue, 18 Jun 2019 15:09:21 -0400
Subject: links to nodes in different contexts render as a circle

---
 src/client/util/DocumentManager.ts                 |  16 ++-
 .../CollectionFreeFormLinkView.tsx                 |  97 +++++++++------
 .../CollectionFreeFormLinksView.tsx                | 135 +++++++++++++++++----
 src/client/views/nodes/DocumentView.tsx            |   4 +-
 4 files changed, 181 insertions(+), 71 deletions(-)

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

diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts
index fc78993b8..85f8bf751 100644
--- a/src/client/util/DocumentManager.ts
+++ b/src/client/util/DocumentManager.ts
@@ -1,7 +1,7 @@
 import { computed, observable } from 'mobx';
 import { DocumentView } from '../views/nodes/DocumentView';
 import { Doc, DocListCast, Opt } from '../../new_fields/Doc';
-import { FieldValue, Cast, NumCast, BoolCast } from '../../new_fields/Types';
+import { FieldValue, Cast, NumCast, BoolCast, StrCast } from '../../new_fields/Types';
 import { listSpec } from '../../new_fields/Schema';
 import { undoBatch } from './UndoManager';
 import { CollectionDockingView } from '../views/collections/CollectionDockingView';
@@ -85,22 +85,26 @@ export class DocumentManager {
     @computed
     public get LinkedDocumentViews() {
         let linked = DocumentManager.Instance.DocumentViews.filter(dv => dv.isSelected() || BoolCast(dv.props.Document.libraryBrush, false)).reduce((pairs, dv) => {
-
+            // console.log("FINDING LINKED DVs FOR", StrCast(dv.props.Document.title));
             let linksList = LinkManager.Instance.findAllRelatedLinks(dv.props.Document);
             if (linksList && linksList.length) {
                 pairs.push(...linksList.reduce((pairs, link) => {
                     if (link) {
                         let destination = LinkManager.Instance.findOppositeAnchor(link, dv.props.Document);
                         if (destination) {
-                            DocumentManager.Instance.getDocumentViews(destination).map(docView1 =>
-                                pairs.push({ a: dv, b: docView1, l: link }));
+                            DocumentManager.Instance.getDocumentViews(destination).map(docView1 => {
+                                // console.log("PUSHING LINK BETWEEN", StrCast(dv.props.Document.title), StrCast(docView1.props.Document.title));
+                                // TODO: if any docviews are not in the same context, draw a proxy
+                                // let sameContent = dv.props.ContainingCollectionView === docView1.props.ContainingCollectionView;
+                                pairs.push({ anchor1View: dv, anchor2View: docView1, linkDoc: link });
+                            });
                         }
                     }
                     return pairs;
-                }, [] as { a: DocumentView, b: DocumentView, l: Doc }[]));
+                }, [] as { anchor1View: DocumentView, anchor2View: DocumentView, linkDoc: Doc }[]));
             }
             return pairs;
-        }, [] as { a: DocumentView, b: DocumentView, l: Doc }[]);
+        }, [] as { anchor1View: DocumentView, anchor2View: DocumentView, linkDoc: Doc }[]);
         return linked;
     }
 
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
index ddde8ece8..36ffac9c8 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
@@ -5,58 +5,81 @@ import { InkingControl } from "../../InkingControl";
 import "./CollectionFreeFormLinkView.scss";
 import React = require("react");
 import v5 = require("uuid/v5");
+import { DocumentView } from "../../nodes/DocumentView";
 
 export interface CollectionFreeFormLinkViewProps {
-    A: Doc;
-    B: Doc;
-    LinkDocs: Doc[];
-    addDocument: (document: Doc, allowDuplicates?: boolean) => boolean;
-    removeDocument: (document: Doc) => boolean;
+    // anchor1: Doc;
+    // anchor2: Doc;
+    // LinkDocs: Doc[];
+    // addDocument: (document: Doc, allowDuplicates?: boolean) => boolean;
+    // removeDocument: (document: Doc) => boolean;
+    // sameContext: boolean;
+
+    sourceView: DocumentView;
+    targetView: DocumentView;
+    sameContext: boolean;
 }
 
 @observer
 export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFormLinkViewProps> {
 
-    onPointerDown = (e: React.PointerEvent) => {
-        if (e.button === 0 && !InkingControl.Instance.selectedTool) {
-            let a = this.props.A;
-            let b = this.props.B;
-            let x1 = NumCast(a.x) + (BoolCast(a.isMinimized, false) ? 5 : a[WidthSym]() / 2);
-            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);
-            });
-            e.stopPropagation();
-            e.preventDefault();
-        }
-    }
+    // onPointerDown = (e: React.PointerEvent) => {
+    //     if (e.button === 0 && !InkingControl.Instance.selectedTool) {
+    //         let a = this.props.A;
+    //         let b = this.props.B;
+    //         let x1 = NumCast(a.x) + (BoolCast(a.isMinimized, false) ? 5 : a[WidthSym]() / 2);
+    //         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);
+    //         });
+    //         e.stopPropagation();
+    //         e.preventDefault();
+    //     }
+    // }
+
     render() {
-        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);
-        let y1 = NumCast(a.y) + (BoolCast(a.isMinimized, false) ? 5 : NumCast(a.height) / NumCast(a.zoomBasis, 1) / 2);
-        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 = "";
-        this.props.LinkDocs.map(l => text += StrCast(l.title) + "(" + StrCast(l.linkDescription) + "), ");
-        text = "";
+        // let l = this.props.LinkDocs;
+        // let a = this.props.A;
+        // let b = this.props.B;
+        let a1 = this.props.sourceView;
+        let a2 = this.props.targetView;
+        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 x2 = NumCast(a2.Document.x) + (BoolCast(a2.Document.isMinimized, false) ? 5 : NumCast(a2.Document.width) / NumCast(a2.Document.zoomBasis, 1) / 2);
+        let y2 = NumCast(a2.Document.y) + (BoolCast(a2.Document.isMinimized, false) ? 5 : NumCast(a2.Document.height) / NumCast(a2.Document.zoomBasis, 1) / 2);
+        if (!this.props.sameContext) {
+            x2 = x1 + 300;
+            y2 = y1 - 300;
+        }
+
+        // if (!this.props.sameContext) {
+        //     console.log("not same context", StrCast(a1.title), StrCast(a2.title));
+        //     x2 = x1 + 300;
+        //     y2 = y2 + 300;
+        // } else {
+        //     console.log("same context", StrCast(a1.title), StrCast(a2.title));
+        // }
+        // let text = "";
+        // this.props.LinkDocs.map(l => text += StrCast(l.title) + "(" + StrCast(l.linkDescription) + "), ");
+        // text = "";
         return (
             <>
-                <line key="linkLine" className="collectionfreeformlinkview-linkLine"
-                    style={{ strokeWidth: `${2 * l.length / 2}` }}
+                <line className="collectionfreeformlinkview-linkLine"
+                    style={{ strokeWidth: `${2 * 1 / 2}` }}
                     x1={`${x1}`} y1={`${y1}`}
                     x2={`${x2}`} y2={`${y2}`} />
+                {!this.props.sameContext ? <circle className="collectionfreeformlinkview-linkCircle" cx={x2} cy={y2} r={10}></circle> : <></>}
                 {/* <circle key="linkCircle" className="collectionfreeformlinkview-linkCircle"
                     cx={(x1 + x2) / 2} cy={(y1 + y2) / 2} r={8} onPointerDown={this.onPointerDown} /> */}
-                <text key="linkText" textAnchor="middle" className="collectionfreeformlinkview-linkText" x={`${(x1 + x2) / 2}`} y={`${(y1 + y2) / 2}`}>
+                {/* <text key="linkText" textAnchor="middle" className="collectionfreeformlinkview-linkText" x={`${(x1 + x2) / 2}`} y={`${(y1 + y2) / 2}`}>
                     {text}
-                </text>
+                </text> */}
             </>
         );
     }
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
index c4dd534ed..fc92c81d5 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
@@ -92,40 +92,123 @@ export class CollectionFreeFormLinksView extends React.Component<CollectionViewP
         return equalViews.filter(sv => sv.props.ContainingCollectionView && sv.props.ContainingCollectionView.props.Document === this.props.Document);
     }
 
-    @computed
-    get uniqueConnections() {
-        let connections = DocumentManager.Instance.LinkedDocumentViews.reduce((drawnPairs, connection) => {
-            let srcViews = this.documentAnchors(connection.a);
-            let targetViews = this.documentAnchors(connection.b);
-            let possiblePairs: { a: Doc, b: Doc, }[] = [];
-            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));
-                    let match2 = (Doc.AreProtosEqual(possiblePair.a, drawnPair.b) && Doc.AreProtosEqual(possiblePair.b, drawnPair.a));
-                    let match = match1 || match2;
-                    if (match && !drawnPair.l.reduce((found, link) => found || link[Id] === connection.l[Id], false)) {
-                        drawnPair.l.push(connection.l);
-                    }
-                    return match || found;
-                }, false)) {
-                    drawnPairs.push({ a: possiblePair.a, b: possiblePair.b, l: [connection.l] })
+    // @computed
+    // get uniqueConnections() {
+    //     // console.log("\n");
+    //     let connections = DocumentManager.Instance.LinkedDocumentViews.reduce((drawnPairs, connection) => {
+    //         // console.log("CONNECTION BETWEEN", StrCast(connection.anchor1View.props.Document.title), StrCast(connection.anchor2View.props.Document.title));
+    //         let srcViews = this.documentAnchors(connection.anchor1View);
+    //         // srcViews.forEach(sv => {
+    //         //     console.log("DOCANCHORS SRC", StrCast(connection.anchor1View.Document.title), StrCast(sv.Document.title));
+    //         // });
+
+    //         let targetViews = this.documentAnchors(connection.anchor2View);
+    //         // targetViews.forEach(sv => {
+    //         //     console.log("DOCANCHORS TARG", StrCast(connection.anchor2View.Document.title), StrCast(sv.Document.title));
+    //         // });
+
+    //         // console.log("lengths", srcViews.length, targetViews.length);
+
+    //         // srcViews.forEach(v => {
+    //         //     console.log("SOURCE VIEW", StrCast(v.props.Document.title));
+    //         // });
+    //         // targetViews.forEach(v => {
+    //         //     console.log("TARGET VIEW", StrCast(v.Document.title));
+    //         // });
+
+    //         let possiblePairs: { anchor1: Doc, anchor2: Doc }[] = [];
+    //         // srcViews.map(sv => {
+    //         //     console.log("SOURCE VIEW", StrCast(sv.props.Document.title));
+    //         //     targetViews.map(tv => {
+    //         //         console.log("TARGET VIEW", StrCast(tv.props.Document.title));
+    //         //         // console.log("PUSHING PAIR", StrCast(sv.props.Document.title), StrCast(tv.props.Document.title));
+    //         //         possiblePairs.push({ anchor1: sv.props.Document, anchor2: tv.props.Document });
+    //         //     });
+    //         //     console.log("END\n");
+    //         // });
+    //         srcViews.forEach(sv => {
+    //             // console.log("SOURCE VIEW", StrCast(sv.props.Document.title));
+    //             targetViews.forEach(tv => {
+    //                 // console.log("TARGET VIEW", StrCast(tv.props.Document.title));
+    //                 // console.log("PUSHING PAIR", StrCast(sv.props.Document.title), StrCast(tv.props.Document.title));
+    //                 possiblePairs.push({ anchor1: sv.props.Document, anchor2: tv.props.Document });
+    //             });
+    //             // console.log("END\n");
+    //         });
+    //         // console.log("POSSIBLE PAIRS LENGTH", possiblePairs.length);
+    //         possiblePairs.map(possiblePair => {
+    //             // console.log("POSSIBLEPAIR", StrCast(possiblePair.anchor1.title), StrCast(possiblePair.anchor2.title));
+    //             if (!drawnPairs.reduce((found, drawnPair) => {
+    //                 let match1 = (Doc.AreProtosEqual(possiblePair.anchor1, drawnPair.anchor1) && Doc.AreProtosEqual(possiblePair.anchor2, drawnPair.anchor2));
+    //                 let match2 = (Doc.AreProtosEqual(possiblePair.anchor1, drawnPair.anchor2) && Doc.AreProtosEqual(possiblePair.anchor2, drawnPair.anchor1));
+    //                 let match = match1 || match2;
+    //                 if (match && !drawnPair.linkDocs.reduce((found, link) => found || link[Id] === connection.linkDoc[Id], false)) {
+    //                     drawnPair.linkDocs.push(connection.linkDoc);
+    //                 }
+    //                 return match || found;
+    //             }, false)) {
+    //                 drawnPairs.push({ anchor1: possiblePair.anchor1, anchor2: possiblePair.anchor2, linkDocs: [connection.linkDoc] });
+    //             }
+    //         });
+    //         return drawnPairs;
+    //     }, [] as { anchor1: Doc, anchor2: Doc, linkDocs: Doc[] }[]);
+    //     return connections.map(c => {
+    //         let x = c.linkDocs.reduce((p, l) => p + l[Id], "");
+    //         return <CollectionFreeFormLinkView key={x} anchor1={c.anchor1} anchor2={c.anchor2} />;
+    //     });
+    // }
+
+    findUniquePairs = (): JSX.Element[] => {
+        // console.log("FIND UNIQUE PAIRS");
+        let connections = DocumentManager.Instance.LinkedDocumentViews;
+
+        let unique: Array<{ sourceView: DocumentView, targetView: DocumentView, sameContext: boolean }> = [];
+        connections.forEach(c => {
+            let match1Index = unique.findIndex(u => (c.anchor1View === u.sourceView) && (c.anchor2View === u.targetView));
+            let match2Index = unique.findIndex(u => (c.anchor1View === u.targetView) && (c.anchor2View === u.sourceView));
+            let sameContext = c.anchor1View.props.ContainingCollectionView === c.anchor2View.props.ContainingCollectionView;
+
+            if (!(match1Index > -1 || match2Index > -1)) {
+                // if docview pair does not already exist in unique, push
+                unique.push({ sourceView: c.anchor1View, targetView: c.anchor2View, sameContext: sameContext });
+            } else {
+                // if docview pair exists in unique, push if not in same context
+                if (!sameContext) {
+                    match1Index > -1 ? unique.push({ sourceView: c.anchor2View, targetView: c.anchor1View, sameContext: sameContext })
+                        : unique.push({ sourceView: c.anchor1View, targetView: c.anchor2View, sameContext: sameContext });
                 }
-            });
-            return drawnPairs;
-        }, [] as { a: Doc, b: Doc, l: Doc[] }[]);
-        return connections.map(c => {
-            let x = c.l.reduce((p, l) => p + l[Id], "");
-            return <CollectionFreeFormLinkView key={x} A={c.a} B={c.b} LinkDocs={c.l}
-                removeDocument={this.props.removeDocument} addDocument={this.props.addDocument} />;
+            }
+        });
+
+        console.log("\n UNIQUE");
+        unique.forEach(u => {
+            console.log(StrCast(u.sourceView.Document.title), StrCast(u.targetView.Document.title), u.sameContext);
+        });
+
+        // console.log("\n");
+
+        return unique.map(u => {
+            // TODO: make better key
+            let key = StrCast(u.sourceView.Document[Id]) + "-link-" + StrCast(u.targetView.Document[Id]) + "-" + Date.now() + Math.random();
+            let sourceIn = u.sourceView.props.ContainingCollectionView!.props.Document === this.props.Document;
+            let targetIn = u.targetView.props.ContainingCollectionView!.props.Document === this.props.Document;
+            let inContainer = u.sameContext ? sourceIn || targetIn : sourceIn;
+            if (inContainer) {
+                // console.log("key", key, StrCast(u.sourceView.Document.title), StrCast(u.targetView.Document.title));
+                return <CollectionFreeFormLinkView key={key} sourceView={u.sourceView} targetView={u.targetView} sameContext={u.sameContext} />;
+            } else {
+                return <div key={key}></div>;
+            }
         });
     }
 
     render() {
+        this.findUniquePairs();
         return (
             <div className="collectionfreeformlinksview-container">
                 <svg className="collectionfreeformlinksview-svgCanvas">
-                    {this.uniqueConnections}
+                    {/* {this.uniqueConnections} */}
+                    {this.findUniquePairs()}
                 </svg>
                 {this.props.children}
             </div>
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index c998b8ea6..e98392a18 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -319,8 +319,8 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
         this._lastTap = Date.now();
     }
 
-    deleteClicked = (): void => { this.props.removeDocument && this.props.removeDocument(this.props.Document); }
-    fieldsClicked = (): void => { this.props.addDocTab(Docs.KVPDocument(this.props.Document, { width: 300, height: 300 }), "onRight") };
+    deleteClicked = (): void => { this.props.removeDocument && this.props.removeDocument(this.props.Document); };
+    fieldsClicked = (): void => { this.props.addDocTab(Docs.KVPDocument(this.props.Document, { width: 300, height: 300 }), "onRight"); };
     makeBtnClicked = (): void => {
         let doc = Doc.GetProto(this.props.Document);
         doc.isButton = !BoolCast(doc.isButton, false);
-- 
cgit v1.2.3-70-g09d2


From 8e5afb5bbb47324a381b5184254e77eba7bd8536 Mon Sep 17 00:00:00 2001
From: Fawn <fangrui_tong@brown.edu>
Date: Tue, 18 Jun 2019 16:30:24 -0400
Subject: can click on button link to node in different context than source

---
 .../CollectionFreeFormLinkView.scss                |  1 +
 .../CollectionFreeFormLinkView.tsx                 | 28 +++++++++++++---------
 .../CollectionFreeFormLinksView.tsx                |  6 ++---
 3 files changed, 21 insertions(+), 14 deletions(-)

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

diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss
index 7a0fd2b31..fc5212edd 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss
@@ -9,6 +9,7 @@
     opacity: 0.5;
     transform: translate(10000px,10000px);
     pointer-events: all;
+    cursor: pointer;
 }
 .collectionfreeformlinkview-linkText {
     stroke: rgb(0,0,0);
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
index 36ffac9c8..4d477454b 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
@@ -6,6 +6,7 @@ import "./CollectionFreeFormLinkView.scss";
 import React = require("react");
 import v5 = require("uuid/v5");
 import { DocumentView } from "../../nodes/DocumentView";
+import { Docs } from "../../../documents/Documents";
 
 export interface CollectionFreeFormLinkViewProps {
     // anchor1: Doc;
@@ -18,6 +19,7 @@ export interface CollectionFreeFormLinkViewProps {
     sourceView: DocumentView;
     targetView: DocumentView;
     sameContext: boolean;
+    addDocTab: (document: Doc, where: string) => void;
 }
 
 @observer
@@ -42,6 +44,13 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo
     //     }
     // }
 
+    followButton = (e: React.PointerEvent): void => {
+        // TODO: would be nicer to open docview in context
+        e.stopPropagation();
+        console.log("follow");
+        this.props.addDocTab(this.props.targetView.props.Document, "onRight");
+    }
+
     render() {
         // let l = this.props.LinkDocs;
         // let a = this.props.A;
@@ -58,23 +67,20 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo
             y2 = y1 - 300;
         }
 
-        // if (!this.props.sameContext) {
-        //     console.log("not same context", StrCast(a1.title), StrCast(a2.title));
-        //     x2 = x1 + 300;
-        //     y2 = y2 + 300;
-        // } else {
-        //     console.log("same context", StrCast(a1.title), StrCast(a2.title));
-        // }
-        // let text = "";
-        // this.props.LinkDocs.map(l => text += StrCast(l.title) + "(" + StrCast(l.linkDescription) + "), ");
-        // text = "";
+        let containing = "";
+        if (this.props.targetView.props.ContainingCollectionView) {
+            containing = StrCast(this.props.targetView.props.ContainingCollectionView.props.Document.title);
+        }
+
+        let text = "link to " + StrCast(this.props.targetView.props.Document.title) + (containing === "" ? "" : (" in the context of " + containing));
         return (
             <>
                 <line className="collectionfreeformlinkview-linkLine"
                     style={{ strokeWidth: `${2 * 1 / 2}` }}
                     x1={`${x1}`} y1={`${y1}`}
                     x2={`${x2}`} y2={`${y2}`} />
-                {!this.props.sameContext ? <circle className="collectionfreeformlinkview-linkCircle" cx={x2} cy={y2} r={10}></circle> : <></>}
+                {!this.props.sameContext ? <circle className="collectionfreeformlinkview-linkCircle" cx={x2} cy={y2} r={20} onPointerDown={this.followButton}></circle> : <></>}
+                {!this.props.sameContext ? <text textAnchor="middle" className="collectionfreeformlinkview-linkText" x={`${x2}`} y={`${y2}`}> {text}</text> : <></>}
                 {/* <circle key="linkCircle" className="collectionfreeformlinkview-linkCircle"
                     cx={(x1 + x2) / 2} cy={(y1 + y2) / 2} r={8} onPointerDown={this.onPointerDown} /> */}
                 {/* <text key="linkText" textAnchor="middle" className="collectionfreeformlinkview-linkText" x={`${(x1 + x2) / 2}`} y={`${(y1 + y2) / 2}`}>
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
index fc92c81d5..86625863f 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
@@ -190,12 +190,12 @@ export class CollectionFreeFormLinksView extends React.Component<CollectionViewP
         return unique.map(u => {
             // TODO: make better key
             let key = StrCast(u.sourceView.Document[Id]) + "-link-" + StrCast(u.targetView.Document[Id]) + "-" + Date.now() + Math.random();
-            let sourceIn = u.sourceView.props.ContainingCollectionView!.props.Document === this.props.Document;
-            let targetIn = u.targetView.props.ContainingCollectionView!.props.Document === this.props.Document;
+            let sourceIn = u.sourceView.props.ContainingCollectionView ? u.sourceView.props.ContainingCollectionView.props.Document === this.props.Document : false;
+            let targetIn = u.targetView.props.ContainingCollectionView ? u.targetView.props.ContainingCollectionView.props.Document === this.props.Document : false;
             let inContainer = u.sameContext ? sourceIn || targetIn : sourceIn;
             if (inContainer) {
                 // console.log("key", key, StrCast(u.sourceView.Document.title), StrCast(u.targetView.Document.title));
-                return <CollectionFreeFormLinkView key={key} sourceView={u.sourceView} targetView={u.targetView} sameContext={u.sameContext} />;
+                return <CollectionFreeFormLinkView key={key} sourceView={u.sourceView} targetView={u.targetView} sameContext={u.sameContext} addDocTab={this.props.addDocTab} />;
             } else {
                 return <div key={key}></div>;
             }
-- 
cgit v1.2.3-70-g09d2


From f362dbfc237536c6c4a8c6d088c3dc818080f7c2 Mon Sep 17 00:00:00 2001
From: Fawn <fangrui_tong@brown.edu>
Date: Wed, 19 Jun 2019 12:50:58 -0400
Subject: both tail ends of a cut link appear on hover/focus of an anchor

---
 src/client/documents/Documents.ts                  | 12 ++++-
 src/client/util/DragManager.ts                     |  9 ++++
 .../CollectionFreeFormLinkView.tsx                 | 21 +-------
 .../CollectionFreeFormLinkWithProxyView.tsx        | 60 ++++++++++++++++++++++
 .../CollectionFreeFormLinksView.tsx                | 59 ++++++++++++---------
 5 files changed, 116 insertions(+), 45 deletions(-)
 create mode 100644 src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkWithProxyView.tsx

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

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 002de8b5f..79ba433c8 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -99,8 +99,18 @@ export namespace DocUtils {
             linkDocProto.anchor2 = target;
             linkDocProto.anchor2Page = target.curPage;
             linkDocProto.anchor2Groups = new List<Doc>([]);
+
             linkDocProto.context = targetContext;
 
+            let proxy1 = Docs.TextDocument({ width: 300, height: 100, borderRounding: 0 });
+            let proxy1Proto = Doc.GetProto(proxy1);
+            let proxy2 = Docs.TextDocument({ width: 300, height: 100, borderRounding: 0 });
+            let proxy2Proto = Doc.GetProto(proxy2);
+
+            linkDocProto.proxy1 = proxy1; // src: 1 targ: 2
+            linkDocProto.proxy2 = proxy2; // src: 2 targ: 1
+
+
             LinkManager.Instance.allLinks.push(linkDoc);
 
             return linkDoc;
@@ -377,7 +387,7 @@ export namespace Docs {
     }
 
     /*
-
+ 
     this template requires an additional style setting on the collectionView-cont to make the layout relative
     
 .collectionView-cont {
diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts
index 82d30e0e6..4787ac40f 100644
--- a/src/client/util/DragManager.ts
+++ b/src/client/util/DragManager.ts
@@ -45,6 +45,9 @@ 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);
+
+    // TODO: if not in same context then don't drag
+
     let moddrag = await Cast(draggeddoc.annotationOn, Doc);
     let dragData = new DragManager.DocumentDragData(moddrag ? [moddrag] : [draggeddoc]);
     DragManager.StartDocumentDrag([dragEle], dragData, x, y, {
@@ -58,6 +61,9 @@ export async function DragLinkAsDocument(dragEle: HTMLElement, x: number, y: num
 export async function DragLinksAsDocuments(dragEle: HTMLElement, x: number, y: number, sourceDoc: Doc) {
     let srcTarg = sourceDoc.proto;
     let draggedDocs: Doc[] = [];
+
+    // TODO: if not in same context then don't drag
+
     if (srcTarg) {
         let linkDocs = LinkManager.Instance.findAllRelatedLinks(srcTarg);
         if (linkDocs) {
@@ -74,6 +80,9 @@ 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;
+        // };
         DragManager.StartDocumentDrag([dragEle], dragData, x, y, {
             handlers: {
                 dragComplete: action(emptyFunction),
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
index 4d477454b..f995a35e3 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
@@ -18,8 +18,6 @@ export interface CollectionFreeFormLinkViewProps {
 
     sourceView: DocumentView;
     targetView: DocumentView;
-    sameContext: boolean;
-    addDocTab: (document: Doc, where: string) => void;
 }
 
 @observer
@@ -44,12 +42,6 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo
     //     }
     // }
 
-    followButton = (e: React.PointerEvent): void => {
-        // TODO: would be nicer to open docview in context
-        e.stopPropagation();
-        console.log("follow");
-        this.props.addDocTab(this.props.targetView.props.Document, "onRight");
-    }
 
     render() {
         // let l = this.props.LinkDocs;
@@ -62,25 +54,14 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo
 
         let x2 = NumCast(a2.Document.x) + (BoolCast(a2.Document.isMinimized, false) ? 5 : NumCast(a2.Document.width) / NumCast(a2.Document.zoomBasis, 1) / 2);
         let y2 = NumCast(a2.Document.y) + (BoolCast(a2.Document.isMinimized, false) ? 5 : NumCast(a2.Document.height) / NumCast(a2.Document.zoomBasis, 1) / 2);
-        if (!this.props.sameContext) {
-            x2 = x1 + 300;
-            y2 = y1 - 300;
-        }
-
-        let containing = "";
-        if (this.props.targetView.props.ContainingCollectionView) {
-            containing = StrCast(this.props.targetView.props.ContainingCollectionView.props.Document.title);
-        }
 
-        let text = "link to " + StrCast(this.props.targetView.props.Document.title) + (containing === "" ? "" : (" in the context of " + containing));
         return (
             <>
                 <line className="collectionfreeformlinkview-linkLine"
                     style={{ strokeWidth: `${2 * 1 / 2}` }}
                     x1={`${x1}`} y1={`${y1}`}
                     x2={`${x2}`} y2={`${y2}`} />
-                {!this.props.sameContext ? <circle className="collectionfreeformlinkview-linkCircle" cx={x2} cy={y2} r={20} onPointerDown={this.followButton}></circle> : <></>}
-                {!this.props.sameContext ? <text textAnchor="middle" className="collectionfreeformlinkview-linkText" x={`${x2}`} y={`${y2}`}> {text}</text> : <></>}
+
                 {/* <circle key="linkCircle" className="collectionfreeformlinkview-linkCircle"
                     cx={(x1 + x2) / 2} cy={(y1 + y2) / 2} r={8} onPointerDown={this.onPointerDown} /> */}
                 {/* <text key="linkText" textAnchor="middle" className="collectionfreeformlinkview-linkText" x={`${(x1 + x2) / 2}`} y={`${(y1 + y2) / 2}`}>
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkWithProxyView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkWithProxyView.tsx
new file mode 100644
index 000000000..81a00ba95
--- /dev/null
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkWithProxyView.tsx
@@ -0,0 +1,60 @@
+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 } from "mobx";
+
+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);
+
+    followButton = (e: React.PointerEvent): void => {
+        // TODO: would be nicer to open docview in context
+        e.stopPropagation();
+        console.log("follow");
+        this.props.addDocTab(this.props.targetView.props.Document, "onRight");
+    }
+
+    render() {
+        let a1 = this.props.sourceView;
+        let a2 = this.props.proxyDoc;
+        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 x2 = NumCast(a2.x) + (BoolCast(a2.isMinimized, false) ? 5 : NumCast(a2.width) / NumCast(a2.zoomBasis, 1) / 2);
+        let y2 = NumCast(a2.y) + (BoolCast(a2.isMinimized, false) ? 5 : NumCast(a2.height) / NumCast(a2.zoomBasis, 1) / 2);
+
+        // let containing = "";
+        // if (this.props.targetView.props.ContainingCollectionView) {
+        //     containing = StrCast(this.props.targetView.props.ContainingCollectionView.props.Document.title);
+        // }
+
+        // let text = "link to " + StrCast(this.props.targetView.props.Document.title) + (containing === "" ? "" : (" in the context of " + containing));
+        return (
+            <>
+                <line className="collectionfreeformlinkview-linkLine"
+                    style={{ strokeWidth: `${2 * 1 / 2}` }}
+                    x1={`${x1}`} y1={`${y1}`}
+                    x2={`${x2}`} y2={`${y2}`} />
+                {/* <circle className="collectionfreeformlinkview-linkCircle" cx={x2} cy={y2} r={20} ></circle>
+                <text textAnchor="middle" className="collectionfreeformlinkview-linkText" x={`${x2}`} y={`${y2}`}> {text}</text> */}
+            </>
+        );
+    }
+}
+
+//onPointerDown={this.followButton}
\ 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 86625863f..eaef1f32a 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
@@ -11,6 +11,8 @@ import { CollectionViewProps } from "../CollectionSubView";
 import "./CollectionFreeFormLinksView.scss";
 import { CollectionFreeFormLinkView } from "./CollectionFreeFormLinkView";
 import React = require("react");
+import { CollectionFreeFormLinkWithProxyView } from "./CollectionFreeFormLinkWithProxyView";
+import { Docs } from "../../../documents/Documents";
 
 @observer
 export class CollectionFreeFormLinksView extends React.Component<CollectionViewProps> {
@@ -159,47 +161,56 @@ export class CollectionFreeFormLinksView extends React.Component<CollectionViewP
     // }
 
     findUniquePairs = (): JSX.Element[] => {
-        // console.log("FIND UNIQUE PAIRS");
         let connections = DocumentManager.Instance.LinkedDocumentViews;
 
-        let unique: Array<{ sourceView: DocumentView, targetView: DocumentView, sameContext: boolean }> = [];
+        let unique: Set<{ sourceView: DocumentView, targetView: DocumentView, linkDoc: Doc }> = new Set();
         connections.forEach(c => {
-            let match1Index = unique.findIndex(u => (c.anchor1View === u.sourceView) && (c.anchor2View === u.targetView));
-            let match2Index = unique.findIndex(u => (c.anchor1View === u.targetView) && (c.anchor2View === u.sourceView));
+            // let match1Index = unique.findIndex(u => (c.anchor1View === u.sourceView) && (c.anchor2View === u.targetView));
+            // let match2Index = unique.findIndex(u => (c.anchor1View === u.targetView) && (c.anchor2View === u.sourceView));
+            let match1 = unique.has({ sourceView: c.anchor1View, targetView: c.anchor2View, linkDoc: c.linkDoc });
+            let match2 = unique.has({ sourceView: c.anchor2View, targetView: c.anchor1View, linkDoc: c.linkDoc });
             let sameContext = c.anchor1View.props.ContainingCollectionView === c.anchor2View.props.ContainingCollectionView;
 
-            if (!(match1Index > -1 || match2Index > -1)) {
-                // if docview pair does not already exist in unique, push
-                unique.push({ sourceView: c.anchor1View, targetView: c.anchor2View, sameContext: sameContext });
+            // if in same context, push if docview pair does not already exist
+            // else push both directions of pair
+            if (sameContext) {
+                if (!(match1 || match2)) unique.add({ sourceView: c.anchor1View, targetView: c.anchor2View, linkDoc: c.linkDoc });
             } else {
-                // if docview pair exists in unique, push if not in same context
-                if (!sameContext) {
-                    match1Index > -1 ? unique.push({ sourceView: c.anchor2View, targetView: c.anchor1View, sameContext: sameContext })
-                        : unique.push({ sourceView: c.anchor1View, targetView: c.anchor2View, sameContext: sameContext });
-                }
+                unique.add({ sourceView: c.anchor1View, targetView: c.anchor2View, linkDoc: c.linkDoc });
+                unique.add({ sourceView: c.anchor2View, targetView: c.anchor1View, linkDoc: c.linkDoc });
             }
         });
 
-        console.log("\n UNIQUE");
+        let uniqueList: JSX.Element[] = [];
         unique.forEach(u => {
-            console.log(StrCast(u.sourceView.Document.title), StrCast(u.targetView.Document.title), u.sameContext);
-        });
-
-        // console.log("\n");
-
-        return unique.map(u => {
             // TODO: make better key
             let key = StrCast(u.sourceView.Document[Id]) + "-link-" + StrCast(u.targetView.Document[Id]) + "-" + Date.now() + Math.random();
             let sourceIn = u.sourceView.props.ContainingCollectionView ? u.sourceView.props.ContainingCollectionView.props.Document === this.props.Document : false;
             let targetIn = u.targetView.props.ContainingCollectionView ? u.targetView.props.ContainingCollectionView.props.Document === this.props.Document : false;
-            let inContainer = u.sameContext ? sourceIn || targetIn : sourceIn;
+            let sameContext = u.sourceView.props.ContainingCollectionView === u.targetView.props.ContainingCollectionView;
+            let inContainer = sameContext ? sourceIn || targetIn : sourceIn;
+
             if (inContainer) {
-                // console.log("key", key, StrCast(u.sourceView.Document.title), StrCast(u.targetView.Document.title));
-                return <CollectionFreeFormLinkView key={key} sourceView={u.sourceView} targetView={u.targetView} sameContext={u.sameContext} addDocTab={this.props.addDocTab} />;
-            } else {
-                return <div key={key}></div>;
+                // let alias = Doc.MakeAlias(proxy);
+                if (sameContext) {
+                    uniqueList.push(<CollectionFreeFormLinkView key={key} sourceView={u.sourceView} targetView={u.targetView} />);
+                } else {
+                    let proxyKey = Doc.AreProtosEqual(u.sourceView.Document, Cast(u.linkDoc.anchor1, Doc, new Doc)) ? "proxy1" : "proxy2";
+                    let proxy = Cast(u.linkDoc[proxyKey], Doc, new Doc);
+
+                    let context = u.targetView.props.ContainingCollectionView ? (" in the context of " + StrCast(u.targetView.props.ContainingCollectionView.props.Document.title)) : "";
+                    let text = proxyKey + " link to " + StrCast(u.targetView.props.Document.title) + context;
+
+                    let proxyProto = Doc.GetProto(proxy);
+                    proxyProto.data = text;
+
+                    this.props.addDocument(proxy, false);
+                    uniqueList.push(<CollectionFreeFormLinkWithProxyView key={key} sourceView={u.sourceView} targetView={u.targetView}
+                        proxyDoc={proxy} addDocTab={this.props.addDocTab} />);
+                }
             }
         });
+        return uniqueList;
     }
 
     render() {
-- 
cgit v1.2.3-70-g09d2


From c5e401cb0a7fec2279ceecbc8d1429dcdd2f04b9 Mon Sep 17 00:00:00 2001
From: Fawn <fangrui_tong@brown.edu>
Date: Wed, 19 Jun 2019 22:27:21 -0400
Subject: buttons on cut links functional except for when dragged from link
 menu

---
 src/client/documents/Documents.ts                  |  47 +++++++--
 src/client/util/DragManager.ts                     |   7 ++
 src/client/util/LinkManager.ts                     |  15 ++-
 .../CollectionFreeFormLinkView.scss                |  59 ++++++++---
 .../CollectionFreeFormLinkView.tsx                 |   2 +-
 .../CollectionFreeFormLinkWithProxyView.tsx        | 109 +++++++++++++++++----
 .../CollectionFreeFormLinksView.tsx                |  42 +++++---
 src/client/views/nodes/DocumentContentsView.tsx    |   3 +-
 src/client/views/nodes/DocumentView.tsx            |  14 ++-
 src/client/views/nodes/FieldView.tsx               |   6 ++
 src/client/views/nodes/LinkBox.scss                |  76 --------------
 src/client/views/nodes/LinkBox.tsx                 | 107 --------------------
 src/client/views/nodes/LinkButtonBox.scss          |  18 ++++
 src/client/views/nodes/LinkButtonBox.tsx           |  63 ++++++++++++
 src/client/views/nodes/LinkMenu.tsx                |   4 +-
 src/client/views/nodes/LinkMenuItem.scss           |  76 ++++++++++++++
 src/client/views/nodes/LinkMenuItem.tsx            | 107 ++++++++++++++++++++
 src/new_fields/LinkButtonField.ts                  |  35 +++++++
 18 files changed, 548 insertions(+), 242 deletions(-)
 delete mode 100644 src/client/views/nodes/LinkBox.scss
 delete mode 100644 src/client/views/nodes/LinkBox.tsx
 create mode 100644 src/client/views/nodes/LinkButtonBox.scss
 create mode 100644 src/client/views/nodes/LinkButtonBox.tsx
 create mode 100644 src/client/views/nodes/LinkMenuItem.scss
 create mode 100644 src/client/views/nodes/LinkMenuItem.tsx
 create mode 100644 src/new_fields/LinkButtonField.ts

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

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 79ba433c8..4d4fa2645 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -26,7 +26,7 @@ import { OmitKeys } from "../../Utils";
 import { ImageField, VideoField, AudioField, PdfField, WebField } from "../../new_fields/URLField";
 import { HtmlField } from "../../new_fields/HtmlField";
 import { List } from "../../new_fields/List";
-import { Cast, NumCast } from "../../new_fields/Types";
+import { Cast, NumCast, StrCast } from "../../new_fields/Types";
 import { IconField } from "../../new_fields/IconField";
 import { listSpec } from "../../new_fields/Schema";
 import { DocServer } from "../DocServer";
@@ -36,6 +36,10 @@ 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 { DocumentManager } from "../util/DocumentManager";
+import { Id } from "../../new_fields/FieldSymbols";
 var requestImageSize = require('request-image-size');
 var path = require('path');
 
@@ -102,14 +106,32 @@ export namespace DocUtils {
 
             linkDocProto.context = targetContext;
 
-            let proxy1 = Docs.TextDocument({ width: 300, height: 100, borderRounding: 0 });
-            let proxy1Proto = Doc.GetProto(proxy1);
-            let proxy2 = Docs.TextDocument({ width: 300, height: 100, borderRounding: 0 });
-            let proxy2Proto = Doc.GetProto(proxy2);
+            let sourceViews = DocumentManager.Instance.getDocumentViews(source);
+            let targetViews = DocumentManager.Instance.getDocumentViews(target);
+            sourceViews.forEach(sv => {
+                targetViews.forEach(tv => {
 
-            linkDocProto.proxy1 = proxy1; // src: 1 targ: 2
-            linkDocProto.proxy2 = proxy2; // src: 2 targ: 1
+                    // 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);
 
@@ -131,6 +153,7 @@ export namespace Docs {
     let audioProto: Doc;
     let pdfProto: Doc;
     let iconProto: Doc;
+    let linkProto: Doc;
     const textProtoId = "textProto";
     const histoProtoId = "histoProto";
     const pdfProtoId = "pdfProto";
@@ -141,6 +164,7 @@ export namespace Docs {
     const videoProtoId = "videoProto";
     const audioProtoId = "audioProto";
     const iconProtoId = "iconProto";
+    const linkProtoId = "linkProto";
 
     export function initProtos(): Promise<void> {
         return DocServer.GetRefFields([textProtoId, histoProtoId, collProtoId, imageProtoId, webProtoId, kvpProtoId, videoProtoId, audioProtoId, pdfProtoId, iconProtoId]).then(fields => {
@@ -154,6 +178,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();
         });
     }
 
@@ -186,6 +211,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 CreateTextPrototype(): Doc {
         let textProto = setupPrototypeOptions(textProtoId, "TEXT_PROTO", FormattedTextBox.LayoutString(),
             { x: 0, y: 0, width: 300, backgroundColor: "#f1efeb" });
@@ -272,6 +302,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 PdfDocument(url: string, options: DocumentOptions = {}) {
         return CreateInstance(pdfProto, new PdfField(new URL(url)), options);
     }
diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts
index 4787ac40f..be00778e7 100644
--- a/src/client/util/DragManager.ts
+++ b/src/client/util/DragManager.ts
@@ -7,6 +7,8 @@ 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 { DocumentManager } from "./DocumentManager";
 
 export type dropActionType = "alias" | "copy" | undefined;
 export function SetupDrag(_reference: React.RefObject<HTMLElement>, docFunc: () => Doc | Promise<Doc>, moveFunc?: DragManager.MoveFunction, dropAction?: dropActionType, options?: any, dontHideOnDrop?: boolean) {
@@ -223,6 +225,11 @@ 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/util/LinkManager.ts b/src/client/util/LinkManager.ts
index 23ba9d2e4..544f2edda 100644
--- a/src/client/util/LinkManager.ts
+++ b/src/client/util/LinkManager.ts
@@ -1,4 +1,4 @@
-import { observable } from "mobx";
+import { observable, action } from "mobx";
 import { StrCast, Cast } from "../../new_fields/Types";
 import { Doc, DocListCast } from "../../new_fields/Doc";
 import { listSpec } from "../../new_fields/Schema";
@@ -32,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; 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
 
     // finds all links that contain the given anchor
     public findAllRelatedLinks(anchor: Doc): Array<Doc> {
@@ -134,4 +135,16 @@ export class LinkManager {
         }
     }
 
+    @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/CollectionFreeFormLinkView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss
index fc5212edd..d8d518147 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss
@@ -1,19 +1,50 @@
-.collectionfreeformlinkview-linkLine {
-    stroke: black;
+// .collectionfreeformlinkview-linkLine {
+//     stroke: black;
+//     transform: translate(10000px,10000px);
+//     opacity: 0.5;
+//     pointer-events: all;
+// }
+// .collectionfreeformlinkview-linkCircle {
+//     stroke: rgb(0,0,0);
+//     opacity: 0.5;
+//     transform: translate(10000px,10000px);
+//     pointer-events: all;
+//     cursor: pointer;
+// }
+// .collectionfreeformlinkview-linkText {
+//     stroke: rgb(0,0,0);
+//     opacity: 0.5;
+//     transform: translate(10000px,10000px);
+//     pointer-events: all;
+// }
+
+.linkview-ele {
     transform: translate(10000px,10000px);
-    opacity: 0.5;
     pointer-events: all;
+
+    &.linkview-line {
+        stroke: black;
+        stroke-width: 2px;
+        opacity: 0.5;
+    }
 }
-.collectionfreeformlinkview-linkCircle {
-    stroke: rgb(0,0,0);
-    opacity: 0.5;
-    transform: translate(10000px,10000px);
-    pointer-events: all;
+
+.linkview-button {
+    width: 200px;
+    height: 100px;
+    border-radius: 5px;
+    padding: 10px;
+    position: relative;
+    background-color: black;
     cursor: pointer;
-}
-.collectionfreeformlinkview-linkText {
-    stroke: rgb(0,0,0);
-    opacity: 0.5;
-    transform: translate(10000px,10000px);
-    pointer-events: all;
+
+    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 f995a35e3..13b5dc575 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
@@ -57,7 +57,7 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo
 
         return (
             <>
-                <line className="collectionfreeformlinkview-linkLine"
+                <line className="linkview-line linkview-ele"
                     style={{ strokeWidth: `${2 * 1 / 2}` }}
                     x1={`${x1}`} y1={`${y1}`}
                     x2={`${x2}`} y2={`${y2}`} />
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkWithProxyView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkWithProxyView.tsx
index 81a00ba95..a4d122af2 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkWithProxyView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkWithProxyView.tsx
@@ -7,13 +7,17 @@ import React = require("react");
 import v5 = require("uuid/v5");
 import { DocumentView } from "../../nodes/DocumentView";
 import { Docs } from "../../../documents/Documents";
-import { observable } from "mobx";
+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;
+    // addDocTab: (document: Doc, where: string) => void;
 }
 
 @observer
@@ -21,37 +25,104 @@ export class CollectionFreeFormLinkWithProxyView extends React.Component<Collect
 
     // @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 => {
-        // TODO: would be nicer to open docview in context
         e.stopPropagation();
-        console.log("follow");
-        this.props.addDocTab(this.props.targetView.props.Document, "onRight");
+        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 a2 = this.props.proxyDoc;
         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 x2 = NumCast(a2.x) + (BoolCast(a2.isMinimized, false) ? 5 : NumCast(a2.width) / NumCast(a2.zoomBasis, 1) / 2);
-        let y2 = NumCast(a2.y) + (BoolCast(a2.isMinimized, false) ? 5 : NumCast(a2.height) / NumCast(a2.zoomBasis, 1) / 2);
-
-        // let containing = "";
-        // if (this.props.targetView.props.ContainingCollectionView) {
-        //     containing = StrCast(this.props.targetView.props.ContainingCollectionView.props.Document.title);
-        // }
+        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;
 
-        // let text = "link to " + StrCast(this.props.targetView.props.Document.title) + (containing === "" ? "" : (" in the context of " + containing));
         return (
             <>
-                <line className="collectionfreeformlinkview-linkLine"
-                    style={{ strokeWidth: `${2 * 1 / 2}` }}
+                <line className="linkview-line linkview-ele"
+                    // style={{ strokeWidth: `${2 * 1 / 2}` }}
                     x1={`${x1}`} y1={`${y1}`}
-                    x2={`${x2}`} y2={`${y2}`} />
-                {/* <circle className="collectionfreeformlinkview-linkCircle" cx={x2} cy={y2} r={20} ></circle>
-                <text textAnchor="middle" className="collectionfreeformlinkview-linkText" x={`${x2}`} y={`${y2}`}> {text}</text> */}
+                    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>
             </>
         );
     }
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
index eaef1f32a..bde68001b 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
@@ -1,4 +1,4 @@
-import { computed, IReactionDisposer, reaction } from "mobx";
+import { computed, IReactionDisposer, reaction, action } from "mobx";
 import { observer } from "mobx-react";
 import { Doc, DocListCast } from "../../../../new_fields/Doc";
 import { Id } from "../../../../new_fields/FieldSymbols";
@@ -13,6 +13,8 @@ import { CollectionFreeFormLinkView } from "./CollectionFreeFormLinkView";
 import React = require("react");
 import { CollectionFreeFormLinkWithProxyView } from "./CollectionFreeFormLinkWithProxyView";
 import { Docs } from "../../../documents/Documents";
+import { LinkButtonField } from "../../../../new_fields/LinkButtonField";
+import { LinkManager } from "../../../util/LinkManager";
 
 @observer
 export class CollectionFreeFormLinksView extends React.Component<CollectionViewProps> {
@@ -195,18 +197,32 @@ export class CollectionFreeFormLinksView extends React.Component<CollectionViewP
                 if (sameContext) {
                     uniqueList.push(<CollectionFreeFormLinkView key={key} sourceView={u.sourceView} targetView={u.targetView} />);
                 } else {
-                    let proxyKey = Doc.AreProtosEqual(u.sourceView.Document, Cast(u.linkDoc.anchor1, Doc, new Doc)) ? "proxy1" : "proxy2";
-                    let proxy = Cast(u.linkDoc[proxyKey], Doc, new Doc);
-
-                    let context = u.targetView.props.ContainingCollectionView ? (" in the context of " + StrCast(u.targetView.props.ContainingCollectionView.props.Document.title)) : "";
-                    let text = proxyKey + " link to " + StrCast(u.targetView.props.Document.title) + context;
-
-                    let proxyProto = Doc.GetProto(proxy);
-                    proxyProto.data = text;
-
-                    this.props.addDocument(proxy, false);
-                    uniqueList.push(<CollectionFreeFormLinkWithProxyView key={key} sourceView={u.sourceView} targetView={u.targetView}
-                        proxyDoc={proxy} addDocTab={this.props.addDocTab} />);
+                    let proxy = LinkManager.Instance.findLinkProxy(StrCast(u.sourceView.props.Document[Id]), StrCast(u.targetView.props.Document[Id]));
+                    if (!proxy) {
+                        proxy = Docs.LinkButtonDocument(
+                            { sourceViewId: StrCast(u.sourceView.props.Document[Id]), targetViewId: StrCast(u.targetView.props.Document[Id]) },
+                            { width: 200, height: 100, borderRounding: 0 });
+                        let proxy1Proto = Doc.GetProto(proxy);
+                        proxy1Proto.sourceViewId = StrCast(u.sourceView.props.Document[Id]);
+                        proxy1Proto.targetViewId = StrCast(u.targetView.props.Document[Id]);
+                        proxy1Proto.isLinkButton = true;
+
+                        // LinkManager.Instance.linkProxies.push(proxy);
+                        LinkManager.Instance.addLinkProxy(proxy);
+                    }
+                    uniqueList.push(<CollectionFreeFormLinkWithProxyView key={key} sourceView={u.sourceView} targetView={u.targetView} proxyDoc={proxy} />);
+
+                    // let proxy = LinkManager.Instance.findLinkProxy(StrCast(u.sourceView.props.Document[Id]), StrCast(u.targetView.props.Document[Id]));
+                    // if (proxy) {
+                    //     this.props.addDocument(proxy, false);
+                    //     uniqueList.push(<CollectionFreeFormLinkWithProxyView key={key} sourceView={u.sourceView} targetView={u.targetView} />);
+                    // }
+                    // let proxyKey = Doc.AreProtosEqual(u.sourceView.Document, Cast(u.linkDoc.anchor1, Doc, new Doc)) ? "proxy1" : "proxy2";
+                    // let proxy = Cast(u.linkDoc[proxyKey], Doc, new Doc);
+                    // this.props.addDocument(proxy, false);
+
+                    // uniqueList.push(<CollectionFreeFormLinkWithProxyView key={key} sourceView={u.sourceView} targetView={u.targetView}
+                    //     proxyDoc={proxy} addDocTab={this.props.addDocTab} />);
                 }
             }
         });
diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx
index 02396c3af..940b00a90 100644
--- a/src/client/views/nodes/DocumentContentsView.tsx
+++ b/src/client/views/nodes/DocumentContentsView.tsx
@@ -12,6 +12,7 @@ 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";
@@ -103,7 +104,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 }}
+            components={{ FormattedTextBox, ImageBox, IconBox, FieldView, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, CollectionPDFView, CollectionVideoView, WebBox, KeyValueBox, PDFBox, VideoBox, AudioBox, HistogramBox, LinkButtonBox }}
             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 e98392a18..c424384a4 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -462,6 +462,17 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
         var scaling = this.props.ContentScaling();
         var nativeHeight = this.nativeHeight > 0 ? `${this.nativeHeight}px` : "100%";
         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;
+
         return (
             <div className={`documentView-node${this.props.isTopMost ? "-topmost" : ""}`}
                 ref={this._mainCont}
@@ -476,7 +487,8 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
                     background: this.Document.backgroundColor || "",
                     width: nativeWidth,
                     height: nativeHeight,
-                    transform: `scale(${scaling}, ${scaling})`
+                    transform: `scale(${scaling}, ${scaling})`,
+                    // display: display ? "block" : "none"
                 }}
                 onDrop={this.onDrop} onContextMenu={this.onContextMenu} onPointerDown={this.onPointerDown} onClick={this.onClick}
 
diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx
index 5a83de8e3..6fecb34d7 100644
--- a/src/client/views/nodes/FieldView.tsx
+++ b/src/client/views/nodes/FieldView.tsx
@@ -18,6 +18,8 @@ import { FormattedTextBox } from "./FormattedTextBox";
 import { IconBox } from "./IconBox";
 import { ImageBox } from "./ImageBox";
 import { VideoBox } from "./VideoBox";
+import { LinkButtonBox } from "./LinkButtonBox";
+import { LinkButtonField } from "../../../new_fields/LinkButtonField";
 
 
 //
@@ -49,6 +51,7 @@ export interface FieldViewProps {
 @observer
 export class FieldView extends React.Component<FieldViewProps> {
     public static LayoutString(fieldType: { name: string }, fieldStr: string = "data") {
+        console.log("LAYOUT STRING", fieldType.name, fieldStr);
         return `<${fieldType.name} {...props} fieldKey={"${fieldStr}"} />`;
     }
 
@@ -74,6 +77,9 @@ export class FieldView extends React.Component<FieldViewProps> {
         else if (field instanceof IconField) {
             return <IconBox {...this.props} />;
         }
+        else if (field instanceof LinkButtonField) {
+            return <LinkButtonBox {...this.props} />;
+        }
         else if (field instanceof VideoField) {
             return <VideoBox {...this.props} />;
         }
diff --git a/src/client/views/nodes/LinkBox.scss b/src/client/views/nodes/LinkBox.scss
deleted file mode 100644
index 77462f611..000000000
--- a/src/client/views/nodes/LinkBox.scss
+++ /dev/null
@@ -1,76 +0,0 @@
-@import "../globalCssVariables";
-.linkMenu-item {
-    border-top: 0.5px solid $main-accent; 
-    padding: 6px;
-    position: relative;
-    display: flex;
-    font-size: 12px;
-
-    .linkMenu-item-content {
-        width: 100%;
-    }
-    
-    .link-metadata {
-        margin-top: 6px;
-        padding: 3px;
-        border-top: 0.5px solid $light-color-secondary;
-        .link-metadata-row {
-            margin-left: 6px;
-        }
-    }
-
-    &:last-child {
-        border-bottom: 0.5px solid $main-accent;
-    }
-    &:hover {
-        .linkMenu-item-buttons {
-            display: flex;
-        }
-        .linkMenu-item-content {
-            &.expand-two {
-                width: calc(100% - 46px);
-            }
-            &.expand-three {
-                width: calc(100% - 78px);
-            }
-        }
-    }
-}
-
-.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/LinkBox.tsx b/src/client/views/nodes/LinkBox.tsx
deleted file mode 100644
index 8d07547ed..000000000
--- a/src/client/views/nodes/LinkBox.tsx
+++ /dev/null
@@ -1,107 +0,0 @@
-import { library } from '@fortawesome/fontawesome-svg-core';
-import { faEdit, faEye, faTimes, faArrowRight, faChevronDown, faChevronUp } from '@fortawesome/free-solid-svg-icons';
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { observer } from "mobx-react";
-import { DocumentManager } from "../../util/DocumentManager";
-import { undoBatch } from "../../util/UndoManager";
-import './LinkBox.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';
-library.add(faEye, faEdit, faTimes, faArrowRight, faChevronDown, faChevronUp);
-
-
-interface Props {
-    groupType: string;
-    linkDoc: Doc;
-    sourceDoc: Doc;
-    destinationDoc: Doc;
-    showEditor: () => void;
-}
-
-@observer
-export class LinkBox extends React.Component<Props> {
-    private _drag = React.createRef<HTMLDivElement>();
-    @observable private _showMore: boolean = false;
-    @action toggleShowMore() { this._showMore = !this._showMore; }
-
-    @undoBatch
-    onFollowLink = async (e: React.PointerEvent): Promise<void> => {
-        e.stopPropagation();
-        DocumentManager.Instance.jumpToDocument(this.props.destinationDoc, e.altKey);
-    }
-
-    onEdit = (e: React.PointerEvent): void => {
-        e.stopPropagation();
-        this.props.showEditor();
-    }
-
-    renderMetadata = (): JSX.Element => {
-        let groups = LinkManager.Instance.getAnchorGroups(this.props.linkDoc, this.props.sourceDoc);
-        let index = groups.findIndex(groupDoc => StrCast(groupDoc.type).toUpperCase() === this.props.groupType.toUpperCase());
-        let groupDoc = index > -1 ? groups[index] : undefined;
-
-        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 => {
-                return (<div key={key} className="link-metadata-row"><b>{key}</b>: {StrCast(mdDoc[key])}</div>);
-            });
-        }
-
-        return (<div className="link-metadata">{mdRows}</div>);
-    }
-
-    onLinkButtonDown = (e: React.PointerEvent): void => {
-        e.stopPropagation();
-        document.removeEventListener("pointermove", this.onLinkButtonMoved);
-        document.addEventListener("pointermove", this.onLinkButtonMoved);
-        document.removeEventListener("pointerup", this.onLinkButtonUp);
-        document.addEventListener("pointerup", this.onLinkButtonUp);
-    }
-
-    onLinkButtonUp = (e: PointerEvent): void => {
-        document.removeEventListener("pointermove", this.onLinkButtonMoved);
-        document.removeEventListener("pointerup", this.onLinkButtonUp);
-        e.stopPropagation();
-    }
-
-    onLinkButtonMoved = async (e: PointerEvent) => {
-        if (this._drag.current !== null && (e.movementX > 1 || e.movementY > 1)) {
-            document.removeEventListener("pointermove", this.onLinkButtonMoved);
-            document.removeEventListener("pointerup", this.onLinkButtonUp);
-
-            DragLinkAsDocument(this._drag.current, e.x, e.y, this.props.linkDoc, this.props.sourceDoc);
-        }
-        e.stopPropagation();
-    }
-
-    render() {
-
-        let keys = LinkManager.Instance.groupMetadataKeys.get(this.props.groupType);
-        let canExpand = keys ? keys.length > 0 : false;
-
-        return (
-            <div className="linkMenu-item">
-                <div className={canExpand ? "linkMenu-item-content expand-three" : "linkMenu-item-content expand-two"}>
-                    <div className="link-name">
-                        <p>{StrCast(this.props.destinationDoc.title)}</p>
-                        <div className="linkMenu-item-buttons">
-                            {canExpand ? <div title="Show more" className="button" onPointerDown={() => this.toggleShowMore()}>
-                                <FontAwesomeIcon className="fa-icon" icon={this._showMore ? "chevron-up" : "chevron-down"} 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" ref={this._drag} onPointerDown={this.onLinkButtonDown} onPointerUp={this.onFollowLink}><FontAwesomeIcon className="fa-icon" icon="arrow-right" size="sm" /></div>
-                        </div>
-                    </div>
-                    {this._showMore ? this.renderMetadata() : <></>}
-                </div>
-
-            </div >
-        );
-    }
-}
\ No newline at end of file
diff --git a/src/client/views/nodes/LinkButtonBox.scss b/src/client/views/nodes/LinkButtonBox.scss
new file mode 100644
index 000000000..24bfd2c9f
--- /dev/null
+++ b/src/client/views/nodes/LinkButtonBox.scss
@@ -0,0 +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-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
new file mode 100644
index 000000000..8a7c1ed8b
--- /dev/null
+++ b/src/client/views/nodes/LinkButtonBox.tsx
@@ -0,0 +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
diff --git a/src/client/views/nodes/LinkMenu.tsx b/src/client/views/nodes/LinkMenu.tsx
index aef56df67..7e4c15312 100644
--- a/src/client/views/nodes/LinkMenu.tsx
+++ b/src/client/views/nodes/LinkMenu.tsx
@@ -1,7 +1,7 @@
 import { action, observable } from "mobx";
 import { observer } from "mobx-react";
 import { DocumentView } from "./DocumentView";
-import { LinkBox } from "./LinkBox";
+import { LinkMenuItem } from "./LinkMenuItem";
 import { LinkEditor } from "./LinkEditor";
 import './LinkMenu.scss';
 import React = require("react");
@@ -23,7 +23,7 @@ export class LinkMenu extends React.Component<Props> {
         let source = this.props.docView.Document;
         return group.map(linkDoc => {
             let destination = LinkManager.Instance.findOppositeAnchor(linkDoc, source);
-            return <LinkBox key={destination[Id] + source[Id]} groupType={groupType} linkDoc={linkDoc} sourceDoc={source} destinationDoc={destination} showEditor={action(() => this._editingLink = linkDoc)} />;
+            return <LinkMenuItem key={destination[Id] + source[Id]} groupType={groupType} linkDoc={linkDoc} sourceDoc={source} destinationDoc={destination} showEditor={action(() => this._editingLink = linkDoc)} />;
         });
     }
 
diff --git a/src/client/views/nodes/LinkMenuItem.scss b/src/client/views/nodes/LinkMenuItem.scss
new file mode 100644
index 000000000..77462f611
--- /dev/null
+++ b/src/client/views/nodes/LinkMenuItem.scss
@@ -0,0 +1,76 @@
+@import "../globalCssVariables";
+.linkMenu-item {
+    border-top: 0.5px solid $main-accent; 
+    padding: 6px;
+    position: relative;
+    display: flex;
+    font-size: 12px;
+
+    .linkMenu-item-content {
+        width: 100%;
+    }
+    
+    .link-metadata {
+        margin-top: 6px;
+        padding: 3px;
+        border-top: 0.5px solid $light-color-secondary;
+        .link-metadata-row {
+            margin-left: 6px;
+        }
+    }
+
+    &:last-child {
+        border-bottom: 0.5px solid $main-accent;
+    }
+    &:hover {
+        .linkMenu-item-buttons {
+            display: flex;
+        }
+        .linkMenu-item-content {
+            &.expand-two {
+                width: calc(100% - 46px);
+            }
+            &.expand-three {
+                width: calc(100% - 78px);
+            }
+        }
+    }
+}
+
+.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
new file mode 100644
index 000000000..c68365584
--- /dev/null
+++ b/src/client/views/nodes/LinkMenuItem.tsx
@@ -0,0 +1,107 @@
+import { library } from '@fortawesome/fontawesome-svg-core';
+import { faEdit, faEye, faTimes, faArrowRight, faChevronDown, faChevronUp } from '@fortawesome/free-solid-svg-icons';
+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 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';
+library.add(faEye, faEdit, faTimes, faArrowRight, faChevronDown, faChevronUp);
+
+
+interface LinkMenuItemProps {
+    groupType: string;
+    linkDoc: Doc;
+    sourceDoc: Doc;
+    destinationDoc: Doc;
+    showEditor: () => void;
+}
+
+@observer
+export class LinkMenuItem extends React.Component<LinkMenuItemProps> {
+    private _drag = React.createRef<HTMLDivElement>();
+    @observable private _showMore: boolean = false;
+    @action toggleShowMore() { this._showMore = !this._showMore; }
+
+    @undoBatch
+    onFollowLink = async (e: React.PointerEvent): Promise<void> => {
+        e.stopPropagation();
+        DocumentManager.Instance.jumpToDocument(this.props.destinationDoc, e.altKey);
+    }
+
+    onEdit = (e: React.PointerEvent): void => {
+        e.stopPropagation();
+        this.props.showEditor();
+    }
+
+    renderMetadata = (): JSX.Element => {
+        let groups = LinkManager.Instance.getAnchorGroups(this.props.linkDoc, this.props.sourceDoc);
+        let index = groups.findIndex(groupDoc => StrCast(groupDoc.type).toUpperCase() === this.props.groupType.toUpperCase());
+        let groupDoc = index > -1 ? groups[index] : undefined;
+
+        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 => {
+                return (<div key={key} className="link-metadata-row"><b>{key}</b>: {StrCast(mdDoc[key])}</div>);
+            });
+        }
+
+        return (<div className="link-metadata">{mdRows}</div>);
+    }
+
+    onLinkButtonDown = (e: React.PointerEvent): void => {
+        e.stopPropagation();
+        document.removeEventListener("pointermove", this.onLinkButtonMoved);
+        document.addEventListener("pointermove", this.onLinkButtonMoved);
+        document.removeEventListener("pointerup", this.onLinkButtonUp);
+        document.addEventListener("pointerup", this.onLinkButtonUp);
+    }
+
+    onLinkButtonUp = (e: PointerEvent): void => {
+        document.removeEventListener("pointermove", this.onLinkButtonMoved);
+        document.removeEventListener("pointerup", this.onLinkButtonUp);
+        e.stopPropagation();
+    }
+
+    onLinkButtonMoved = async (e: PointerEvent) => {
+        if (this._drag.current !== null && (e.movementX > 1 || e.movementY > 1)) {
+            document.removeEventListener("pointermove", this.onLinkButtonMoved);
+            document.removeEventListener("pointerup", this.onLinkButtonUp);
+
+            DragLinkAsDocument(this._drag.current, e.x, e.y, this.props.linkDoc, this.props.sourceDoc);
+        }
+        e.stopPropagation();
+    }
+
+    render() {
+
+        let keys = LinkManager.Instance.groupMetadataKeys.get(this.props.groupType);
+        let canExpand = keys ? keys.length > 0 : false;
+
+        return (
+            <div className="linkMenu-item">
+                <div className={canExpand ? "linkMenu-item-content expand-three" : "linkMenu-item-content expand-two"}>
+                    <div className="link-name">
+                        <p>{StrCast(this.props.destinationDoc.title)}</p>
+                        <div className="linkMenu-item-buttons">
+                            {canExpand ? <div title="Show more" className="button" onPointerDown={() => this.toggleShowMore()}>
+                                <FontAwesomeIcon className="fa-icon" icon={this._showMore ? "chevron-up" : "chevron-down"} 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" ref={this._drag} onPointerDown={this.onLinkButtonDown} onPointerUp={this.onFollowLink}><FontAwesomeIcon className="fa-icon" icon="arrow-right" size="sm" /></div>
+                        </div>
+                    </div>
+                    {this._showMore ? this.renderMetadata() : <></>}
+                </div>
+
+            </div >
+        );
+    }
+}
\ No newline at end of file
diff --git a/src/new_fields/LinkButtonField.ts b/src/new_fields/LinkButtonField.ts
new file mode 100644
index 000000000..92e1ed922
--- /dev/null
+++ b/src/new_fields/LinkButtonField.ts
@@ -0,0 +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";
+
+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 e6ebed17e6ddb2ccee81d65fcb451a9b54302762 Mon Sep 17 00:00:00 2001
From: Fawn <fangrui_tong@brown.edu>
Date: Thu, 20 Jun 2019 17:12:48 -0400
Subject: links can be made from freeform view to treeview

---
 src/client/views/collections/CollectionTreeView.tsx               | 8 +++++++-
 .../collectionFreeForm/CollectionFreeFormLinkView.scss            | 6 ++----
 .../collections/collectionFreeForm/CollectionFreeFormLinkView.tsx | 2 +-
 .../collectionFreeForm/CollectionFreeFormLinksView.tsx            | 3 +++
 src/client/views/nodes/LinkMenuItem.scss                          | 8 ++++++--
 5 files changed, 19 insertions(+), 8 deletions(-)

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

diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx
index c51c16883..ba2dac1c3 100644
--- a/src/client/views/collections/CollectionTreeView.tsx
+++ b/src/client/views/collections/CollectionTreeView.tsx
@@ -9,7 +9,7 @@ import { List } from '../../../new_fields/List';
 import { Document, listSpec } from '../../../new_fields/Schema';
 import { BoolCast, Cast, NumCast, StrCast } from '../../../new_fields/Types';
 import { emptyFunction, Utils } from '../../../Utils';
-import { Docs } from '../../documents/Documents';
+import { Docs, DocUtils } from '../../documents/Documents';
 import { DocumentManager } from '../../util/DocumentManager';
 import { DragManager, dropActionType, SetupDrag } from "../../util/DragManager";
 import { SelectionManager } from '../../util/SelectionManager';
@@ -232,6 +232,12 @@ class TreeView extends React.Component<TreeViewProps> {
         let bounds = this.props.ScreenToLocalTransform().transformPoint(rect.left, rect.top + rect.height / 2);
         let before = x[1] < bounds[1];
         let inside = x[0] > bounds[0] + 75 || (!before && !this._collapsed);
+        if (de.data instanceof DragManager.LinkDragData) {
+            let sourceDoc = de.data.linkSourceDocument;
+            let destDoc = this.props.document;
+            DocUtils.MakeLink(sourceDoc, destDoc);
+            e.stopPropagation();
+        }
         if (de.data instanceof DragManager.DocumentDragData) {
             let addDoc = (doc: Doc) => this.props.addDocument(doc, this.props.document, before);
             if (inside) {
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss
index 0cd4338e5..d8d518147 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss
@@ -33,16 +33,14 @@
     width: 200px;
     height: 100px;
     border-radius: 5px;
-    border: 2px solid black;
     padding: 10px;
     position: relative;
-    background-color: white;
-    text-align: center;
+    background-color: black;
     cursor: pointer;
 
     p {
         width: calc(100% - 20px);
-        color: black;
+        color: white;
         position: absolute;
         top: 50%;
         left: 50%;
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
index 13b5dc575..ca55b0ff0 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
@@ -58,7 +58,7 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo
         return (
             <>
                 <line className="linkview-line linkview-ele"
-                    style={{ strokeWidth: `${2 * 1 / 2}` }}
+                    // style={{ strokeWidth: `${2 * 1 / 2}` }}
                     x1={`${x1}`} y1={`${y1}`}
                     x2={`${x2}`} y2={`${y2}`} />
 
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
index 3c2ccecfc..6868e8187 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
@@ -165,6 +165,9 @@ export class CollectionFreeFormLinksView extends React.Component<CollectionViewP
     findUniquePairs = (): JSX.Element[] => {
         let connections = DocumentManager.Instance.LinkedDocumentViews;
 
+        // console.log("CONNECTIONS");
+        // connections.forEach(c => console.log(StrCast(c.anchor1View.Document.title), StrCast(c.anchor2View.Document.title)));
+
         let unique: Set<{ sourceView: DocumentView, targetView: DocumentView, linkDoc: Doc }> = new Set();
         connections.forEach(c => {
 
diff --git a/src/client/views/nodes/LinkMenuItem.scss b/src/client/views/nodes/LinkMenuItem.scss
index 77462f611..16a1df331 100644
--- a/src/client/views/nodes/LinkMenuItem.scss
+++ b/src/client/views/nodes/LinkMenuItem.scss
@@ -6,6 +6,10 @@
     display: flex;
     font-size: 12px;
 
+    .link-name {
+        position: relative;
+    }
+
     .linkMenu-item-content {
         width: 100%;
     }
@@ -27,10 +31,10 @@
             display: flex;
         }
         .linkMenu-item-content {
-            &.expand-two {
+            &.expand-two p {
                 width: calc(100% - 46px);
             }
-            &.expand-three {
+            &.expand-three p {
                 width: calc(100% - 78px);
             }
         }
-- 
cgit v1.2.3-70-g09d2


From d78c651322ad228152b862eaa378946fe65cc9f9 Mon Sep 17 00:00:00 2001
From: Fawn <fangrui_tong@brown.edu>
Date: Fri, 21 Jun 2019 13:32:23 -0400
Subject: dragged links from menu are aliases

---
 src/client/util/DocumentManager.ts                 |  13 +
 src/client/util/DragManager.ts                     |  12 +-
 .../CollectionFreeFormLinkView.tsx                 |  14 +-
 .../CollectionFreeFormLinksView.tsx                | 267 ++++++++++-----------
 4 files changed, 163 insertions(+), 143 deletions(-)

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

diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts
index 84f4d7fa2..325f4894d 100644
--- a/src/client/util/DocumentManager.ts
+++ b/src/client/util/DocumentManager.ts
@@ -10,6 +10,7 @@ import { CollectionPDFView } from '../views/collections/CollectionPDFView';
 import { CollectionVideoView } from '../views/collections/CollectionVideoView';
 import { Id } from '../../new_fields/FieldSymbols';
 import { LinkManager } from './LinkManager';
+import { CurrentUserUtils } from '../../server/authentication/models/current_user_utils';
 
 
 export class DocumentManager {
@@ -84,6 +85,18 @@ export class DocumentManager {
 
     @computed
     public get LinkedDocumentViews() {
+        // console.log("linked");
+        // let docs = DocListCast(CurrentUserUtils.UserDocument.data);
+        // docs.forEach(d => {
+        //     console.log("d", StrCast(d.title));
+
+        // });
+
+        // let d = Cast(CurrentUserUtils.UserDocument.activeWorkspace, Doc, new Doc);
+        // console.log("DOC", StrCast(d.title));
+
+
+
         let linked = DocumentManager.Instance.DocumentViews.filter(dv => dv.isSelected() || BoolCast(dv.props.Document.libraryBrush, false)).reduce((pairs, dv) => {
             // console.log("FINDING LINKED DVs FOR", StrCast(dv.props.Document.title));
             let linksList = LinkManager.Instance.findAllRelatedLinks(dv.props.Document);
diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts
index 01193cab5..78cae4ff7 100644
--- a/src/client/util/DragManager.ts
+++ b/src/client/util/DragManager.ts
@@ -1,6 +1,6 @@
 import { action, runInAction, observable } from "mobx";
 import { Doc, DocListCastAsync } from "../../new_fields/Doc";
-import { Cast } from "../../new_fields/Types";
+import { Cast, StrCast } from "../../new_fields/Types";
 import { emptyFunction } from "../../Utils";
 import { CollectionDockingView } from "../views/collections/CollectionDockingView";
 import * as globalCssVariables from "../views/globalCssVariables.scss";
@@ -52,6 +52,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.StartDocumentDrag([dragEle], dragData, x, y, {
         handlers: {
             dragComplete: action(emptyFunction),
@@ -82,6 +83,7 @@ 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.dropAction = "alias" as dropActionType;
         // dragData.moveDocument = (document, targetCollection, addDocument) => {
         //     return false;
         // };
@@ -201,14 +203,18 @@ export namespace DragManager {
     export let StartDragFunctions: (() => void)[] = [];
 
     export function StartDocumentDrag(eles: HTMLElement[], dragData: DocumentDragData, downX: number, downY: number, options?: DragOptions) {
+        console.log("outside", dragData.userDropAction, dragData.dropAction);
         runInAction(() => StartDragFunctions.map(func => func()));
         StartDrag(eles, dragData, downX, downY, options,
-            (dropData: { [id: string]: any }) =>
+            (dropData: { [id: string]: any }) => {
+                console.log("DRAG", dragData.userDropAction, dragData.dropAction);
                 (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));
+                        dragData.draggedDocuments
+                );
+            });
     }
 
     export function StartAnnotationDrag(eles: HTMLElement[], dragData: AnnotationDragData, downX: number, downY: number, options?: DragOptions) {
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
index ca55b0ff0..65d5ac474 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
@@ -16,8 +16,10 @@ export interface CollectionFreeFormLinkViewProps {
     // removeDocument: (document: Doc) => boolean;
     // sameContext: boolean;
 
-    sourceView: DocumentView;
-    targetView: DocumentView;
+    // sourceView: DocumentView;
+    // targetView: DocumentView;
+    sourceView: Doc;
+    targetView: Doc;
 }
 
 @observer
@@ -49,11 +51,11 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo
         // let b = this.props.B;
         let a1 = this.props.sourceView;
         let a2 = this.props.targetView;
-        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 x1 = NumCast(a1.x) + (BoolCast(a1.isMinimized, false) ? 5 : NumCast(a1.width) / NumCast(a1.zoomBasis, 1) / 2);
+        let y1 = NumCast(a1.y) + (BoolCast(a1.isMinimized, false) ? 5 : NumCast(a1.height) / NumCast(a1.zoomBasis, 1) / 2);
 
-        let x2 = NumCast(a2.Document.x) + (BoolCast(a2.Document.isMinimized, false) ? 5 : NumCast(a2.Document.width) / NumCast(a2.Document.zoomBasis, 1) / 2);
-        let y2 = NumCast(a2.Document.y) + (BoolCast(a2.Document.isMinimized, false) ? 5 : NumCast(a2.Document.height) / NumCast(a2.Document.zoomBasis, 1) / 2);
+        let x2 = NumCast(a2.x) + (BoolCast(a2.isMinimized, false) ? 5 : NumCast(a2.width) / NumCast(a2.zoomBasis, 1) / 2);
+        let y2 = NumCast(a2.y) + (BoolCast(a2.isMinimized, false) ? 5 : NumCast(a2.height) / NumCast(a2.zoomBasis, 1) / 2);
 
         return (
             <>
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
index 6868e8187..9d2f8946b 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
@@ -96,153 +96,152 @@ export class CollectionFreeFormLinksView extends React.Component<CollectionViewP
         return equalViews.filter(sv => sv.props.ContainingCollectionView && sv.props.ContainingCollectionView.props.Document === this.props.Document);
     }
 
-    // @computed
-    // get uniqueConnections() {
-    //     // console.log("\n");
-    //     let connections = DocumentManager.Instance.LinkedDocumentViews.reduce((drawnPairs, connection) => {
-    //         // console.log("CONNECTION BETWEEN", StrCast(connection.anchor1View.props.Document.title), StrCast(connection.anchor2View.props.Document.title));
-    //         let srcViews = this.documentAnchors(connection.anchor1View);
-    //         // srcViews.forEach(sv => {
-    //         //     console.log("DOCANCHORS SRC", StrCast(connection.anchor1View.Document.title), StrCast(sv.Document.title));
-    //         // });
-
-    //         let targetViews = this.documentAnchors(connection.anchor2View);
-    //         // targetViews.forEach(sv => {
-    //         //     console.log("DOCANCHORS TARG", StrCast(connection.anchor2View.Document.title), StrCast(sv.Document.title));
-    //         // });
-
-    //         // console.log("lengths", srcViews.length, targetViews.length);
-
-    //         // srcViews.forEach(v => {
-    //         //     console.log("SOURCE VIEW", StrCast(v.props.Document.title));
-    //         // });
-    //         // targetViews.forEach(v => {
-    //         //     console.log("TARGET VIEW", StrCast(v.Document.title));
-    //         // });
-
-    //         let possiblePairs: { anchor1: Doc, anchor2: Doc }[] = [];
-    //         // srcViews.map(sv => {
-    //         //     console.log("SOURCE VIEW", StrCast(sv.props.Document.title));
-    //         //     targetViews.map(tv => {
-    //         //         console.log("TARGET VIEW", StrCast(tv.props.Document.title));
-    //         //         // console.log("PUSHING PAIR", StrCast(sv.props.Document.title), StrCast(tv.props.Document.title));
-    //         //         possiblePairs.push({ anchor1: sv.props.Document, anchor2: tv.props.Document });
-    //         //     });
-    //         //     console.log("END\n");
-    //         // });
-    //         srcViews.forEach(sv => {
-    //             // console.log("SOURCE VIEW", StrCast(sv.props.Document.title));
-    //             targetViews.forEach(tv => {
-    //                 // console.log("TARGET VIEW", StrCast(tv.props.Document.title));
-    //                 // console.log("PUSHING PAIR", StrCast(sv.props.Document.title), StrCast(tv.props.Document.title));
-    //                 possiblePairs.push({ anchor1: sv.props.Document, anchor2: tv.props.Document });
-    //             });
-    //             // console.log("END\n");
-    //         });
-    //         // console.log("POSSIBLE PAIRS LENGTH", possiblePairs.length);
-    //         possiblePairs.map(possiblePair => {
-    //             // console.log("POSSIBLEPAIR", StrCast(possiblePair.anchor1.title), StrCast(possiblePair.anchor2.title));
-    //             if (!drawnPairs.reduce((found, drawnPair) => {
-    //                 let match1 = (Doc.AreProtosEqual(possiblePair.anchor1, drawnPair.anchor1) && Doc.AreProtosEqual(possiblePair.anchor2, drawnPair.anchor2));
-    //                 let match2 = (Doc.AreProtosEqual(possiblePair.anchor1, drawnPair.anchor2) && Doc.AreProtosEqual(possiblePair.anchor2, drawnPair.anchor1));
-    //                 let match = match1 || match2;
-    //                 if (match && !drawnPair.linkDocs.reduce((found, link) => found || link[Id] === connection.linkDoc[Id], false)) {
-    //                     drawnPair.linkDocs.push(connection.linkDoc);
-    //                 }
-    //                 return match || found;
-    //             }, false)) {
-    //                 drawnPairs.push({ anchor1: possiblePair.anchor1, anchor2: possiblePair.anchor2, linkDocs: [connection.linkDoc] });
-    //             }
-    //         });
-    //         return drawnPairs;
-    //     }, [] as { anchor1: Doc, anchor2: Doc, linkDocs: Doc[] }[]);
-    //     return connections.map(c => {
-    //         let x = c.linkDocs.reduce((p, l) => p + l[Id], "");
-    //         return <CollectionFreeFormLinkView key={x} anchor1={c.anchor1} anchor2={c.anchor2} />;
-    //     });
-    // }
+    @computed
+    get uniqueConnections() {
+        // console.log("\n");
+        let connections = DocumentManager.Instance.LinkedDocumentViews.reduce((drawnPairs, connection) => {
+            // console.log("CONNECTION BETWEEN", StrCast(connection.anchor1View.props.Document.title), StrCast(connection.anchor2View.props.Document.title));
+            let srcViews = this.documentAnchors(connection.anchor1View);
+            // srcViews.forEach(sv => {
+            //     console.log("DOCANCHORS SRC", StrCast(connection.anchor1View.Document.title), StrCast(sv.Document.title));
+            // });
+
+            let targetViews = this.documentAnchors(connection.anchor2View);
+            // targetViews.forEach(sv => {
+            //     console.log("DOCANCHORS TARG", StrCast(connection.anchor2View.Document.title), StrCast(sv.Document.title));
+            // });
+
+            // console.log("lengths", srcViews.length, targetViews.length);
+
+            // srcViews.forEach(v => {
+            //     console.log("SOURCE VIEW", StrCast(v.props.Document.title));
+            // });
+            // targetViews.forEach(v => {
+            //     console.log("TARGET VIEW", StrCast(v.Document.title));
+            // });
+
+            let possiblePairs: { anchor1: Doc, anchor2: Doc }[] = [];
+            // srcViews.map(sv => {
+            //     console.log("SOURCE VIEW", StrCast(sv.props.Document.title));
+            //     targetViews.map(tv => {
+            //         console.log("TARGET VIEW", StrCast(tv.props.Document.title));
+            //         // console.log("PUSHING PAIR", StrCast(sv.props.Document.title), StrCast(tv.props.Document.title));
+            //         possiblePairs.push({ anchor1: sv.props.Document, anchor2: tv.props.Document });
+            //     });
+            //     console.log("END\n");
+            // });
+            srcViews.forEach(sv => {
+                // console.log("SOURCE VIEW", StrCast(sv.props.Document.title));
+                targetViews.forEach(tv => {
+                    // console.log("TARGET VIEW", StrCast(tv.props.Document.title));
+                    // console.log("PUSHING PAIR", StrCast(sv.props.Document.title), StrCast(tv.props.Document.title));
+                    possiblePairs.push({ anchor1: sv.props.Document, anchor2: tv.props.Document });
+                });
+                // console.log("END\n");
+            });
+            // console.log("POSSIBLE PAIRS LENGTH", possiblePairs.length);
+            possiblePairs.map(possiblePair => {
+                // console.log("POSSIBLEPAIR", StrCast(possiblePair.anchor1.title), StrCast(possiblePair.anchor2.title));
+                if (!drawnPairs.reduce((found, drawnPair) => {
+                    let match1 = (Doc.AreProtosEqual(possiblePair.anchor1, drawnPair.anchor1) && Doc.AreProtosEqual(possiblePair.anchor2, drawnPair.anchor2));
+                    let match2 = (Doc.AreProtosEqual(possiblePair.anchor1, drawnPair.anchor2) && Doc.AreProtosEqual(possiblePair.anchor2, drawnPair.anchor1));
+                    let match = match1 || match2;
+                    if (match && !drawnPair.linkDocs.reduce((found, link) => found || link[Id] === connection.linkDoc[Id], false)) {
+                        drawnPair.linkDocs.push(connection.linkDoc);
+                    }
+                    return match || found;
+                }, false)) {
+                    drawnPairs.push({ anchor1: possiblePair.anchor1, anchor2: possiblePair.anchor2, linkDocs: [connection.linkDoc] });
+                }
+            });
+            return drawnPairs;
+        }, [] as { anchor1: Doc, anchor2: Doc, linkDocs: Doc[] }[]);
+        return connections.map(c => {
+            let x = c.linkDocs.reduce((p, l) => p + l[Id], "");
+            return <CollectionFreeFormLinkView key={x} sourceView={c.anchor1} targetView={c.anchor2} />;
+        });
+    }
 
-    findUniquePairs = (): JSX.Element[] => {
-        let connections = DocumentManager.Instance.LinkedDocumentViews;
+    // findUniquePairs = (): JSX.Element[] => {
+    //     let connections = DocumentManager.Instance.LinkedDocumentViews;
 
-        // console.log("CONNECTIONS");
-        // connections.forEach(c => console.log(StrCast(c.anchor1View.Document.title), StrCast(c.anchor2View.Document.title)));
+    //     // console.log("CONNECTIONS");
+    //     // connections.forEach(c => console.log(StrCast(c.anchor1View.Document.title), StrCast(c.anchor2View.Document.title)));
 
-        let unique: Set<{ sourceView: DocumentView, targetView: DocumentView, linkDoc: Doc }> = new Set();
-        connections.forEach(c => {
+    //     let unique: Set<{ sourceView: DocumentView, targetView: DocumentView, linkDoc: Doc }> = new Set();
+    //     connections.forEach(c => {
 
-            // let match1Index = unique.findIndex(u => (c.anchor1View === u.sourceView) && (c.anchor2View === u.targetView));
-            // let match2Index = unique.findIndex(u => (c.anchor1View === u.targetView) && (c.anchor2View === u.sourceView));
-            let match1 = unique.has({ sourceView: c.anchor1View, targetView: c.anchor2View, linkDoc: c.linkDoc });
-            let match2 = unique.has({ sourceView: c.anchor2View, targetView: c.anchor1View, linkDoc: c.linkDoc });
-            let sameContext = c.anchor1View.props.ContainingCollectionView === c.anchor2View.props.ContainingCollectionView;
+    //         // let match1Index = unique.findIndex(u => (c.anchor1View === u.sourceView) && (c.anchor2View === u.targetView));
+    //         // let match2Index = unique.findIndex(u => (c.anchor1View === u.targetView) && (c.anchor2View === u.sourceView));
+    //         let match1 = unique.has({ sourceView: c.anchor1View, targetView: c.anchor2View, linkDoc: c.linkDoc });
+    //         let match2 = unique.has({ sourceView: c.anchor2View, targetView: c.anchor1View, linkDoc: c.linkDoc });
+    //         let sameContext = c.anchor1View.props.ContainingCollectionView === c.anchor2View.props.ContainingCollectionView;
 
-            // console.log("CONNECTION", StrCast(c.anchor1View.props.Document.title), StrCast(c.anchor2View.props.Document.title), match1, match2);
+    //         // console.log("CONNECTION", StrCast(c.anchor1View.props.Document.title), StrCast(c.anchor2View.props.Document.title), match1, match2);
 
 
-            // if in same context, push if docview pair does not already exist
-            // else push both directions of pair
-            if (sameContext) {
-                if (!(match1 || match2)) unique.add({ sourceView: c.anchor1View, targetView: c.anchor2View, linkDoc: c.linkDoc });
-            } else {
-                unique.add({ sourceView: c.anchor1View, targetView: c.anchor2View, linkDoc: c.linkDoc });
-                unique.add({ sourceView: c.anchor2View, targetView: c.anchor1View, linkDoc: c.linkDoc });
-            }
-        });
+    //         // if in same context, push if docview pair does not already exist
+    //         // else push both directions of pair
+    //         if (sameContext) {
+    //             if (!(match1 || match2)) unique.add({ sourceView: c.anchor1View, targetView: c.anchor2View, linkDoc: c.linkDoc });
+    //         } else {
+    //             unique.add({ sourceView: c.anchor1View, targetView: c.anchor2View, linkDoc: c.linkDoc });
+    //             unique.add({ sourceView: c.anchor2View, targetView: c.anchor1View, linkDoc: c.linkDoc });
+    //         }
+    //     });
 
-        let uniqueList: JSX.Element[] = [];
-        unique.forEach(u => {
-            // TODO: make better key
-            let key = StrCast(u.sourceView.Document[Id]) + "-link-" + StrCast(u.targetView.Document[Id]) + "-" + Date.now() + Math.random();
-            let sourceIn = u.sourceView.props.ContainingCollectionView ? u.sourceView.props.ContainingCollectionView.props.Document === this.props.Document : false;
-            let targetIn = u.targetView.props.ContainingCollectionView ? u.targetView.props.ContainingCollectionView.props.Document === this.props.Document : false;
-            let sameContext = u.sourceView.props.ContainingCollectionView === u.targetView.props.ContainingCollectionView;
-            let inContainer = sameContext ? sourceIn || targetIn : sourceIn;
-
-            if (inContainer) {
-                // let alias = Doc.MakeAlias(proxy);
-                if (sameContext) {
-                    uniqueList.push(<CollectionFreeFormLinkView key={key} sourceView={u.sourceView} targetView={u.targetView} />);
-                } else {
-                    let proxy = LinkManager.Instance.findLinkProxy(StrCast(u.sourceView.props.Document[Id]), StrCast(u.targetView.props.Document[Id]));
-                    if (!proxy) {
-                        proxy = Docs.LinkButtonDocument(
-                            { sourceViewId: StrCast(u.sourceView.props.Document[Id]), targetViewId: StrCast(u.targetView.props.Document[Id]) },
-                            { width: 200, height: 100, borderRounding: 0 });
-                        let proxy1Proto = Doc.GetProto(proxy);
-                        proxy1Proto.sourceViewId = StrCast(u.sourceView.props.Document[Id]);
-                        proxy1Proto.targetViewId = StrCast(u.targetView.props.Document[Id]);
-                        proxy1Proto.isLinkButton = true;
-
-                        // LinkManager.Instance.linkProxies.push(proxy);
-                        LinkManager.Instance.addLinkProxy(proxy);
-                    }
-                    uniqueList.push(<CollectionFreeFormLinkWithProxyView key={key} sourceView={u.sourceView} targetView={u.targetView} proxyDoc={proxy} />);
-
-                    // let proxy = LinkManager.Instance.findLinkProxy(StrCast(u.sourceView.props.Document[Id]), StrCast(u.targetView.props.Document[Id]));
-                    // if (proxy) {
-                    //     this.props.addDocument(proxy, false);
-                    //     uniqueList.push(<CollectionFreeFormLinkWithProxyView key={key} sourceView={u.sourceView} targetView={u.targetView} />);
-                    // }
-                    // let proxyKey = Doc.AreProtosEqual(u.sourceView.Document, Cast(u.linkDoc.anchor1, Doc, new Doc)) ? "proxy1" : "proxy2";
-                    // let proxy = Cast(u.linkDoc[proxyKey], Doc, new Doc);
-                    // this.props.addDocument(proxy, false);
-
-                    // uniqueList.push(<CollectionFreeFormLinkWithProxyView key={key} sourceView={u.sourceView} targetView={u.targetView}
-                    //     proxyDoc={proxy} addDocTab={this.props.addDocTab} />);
-                }
-            }
-        });
-        return uniqueList;
-    }
+    //     let uniqueList: JSX.Element[] = [];
+    //     unique.forEach(u => {
+    //         // TODO: make better key
+    //         let key = StrCast(u.sourceView.Document[Id]) + "-link-" + StrCast(u.targetView.Document[Id]) + "-" + Date.now() + Math.random();
+    //         let sourceIn = u.sourceView.props.ContainingCollectionView ? u.sourceView.props.ContainingCollectionView.props.Document === this.props.Document : false;
+    //         let targetIn = u.targetView.props.ContainingCollectionView ? u.targetView.props.ContainingCollectionView.props.Document === this.props.Document : false;
+    //         let sameContext = u.sourceView.props.ContainingCollectionView === u.targetView.props.ContainingCollectionView;
+    //         let inContainer = sameContext ? sourceIn || targetIn : sourceIn;
+
+    //         if (inContainer) {
+    //             // let alias = Doc.MakeAlias(proxy);
+    //             if (sameContext) {
+    //                 uniqueList.push(<CollectionFreeFormLinkView key={key} sourceView={u.sourceView} targetView={u.targetView} />);
+    //             } else {
+    //                 let proxy = LinkManager.Instance.findLinkProxy(StrCast(u.sourceView.props.Document[Id]), StrCast(u.targetView.props.Document[Id]));
+    //                 if (!proxy) {
+    //                     proxy = Docs.LinkButtonDocument(
+    //                         { sourceViewId: StrCast(u.sourceView.props.Document[Id]), targetViewId: StrCast(u.targetView.props.Document[Id]) },
+    //                         { width: 200, height: 100, borderRounding: 0 });
+    //                     let proxy1Proto = Doc.GetProto(proxy);
+    //                     proxy1Proto.sourceViewId = StrCast(u.sourceView.props.Document[Id]);
+    //                     proxy1Proto.targetViewId = StrCast(u.targetView.props.Document[Id]);
+    //                     proxy1Proto.isLinkButton = true;
+
+    //                     // LinkManager.Instance.linkProxies.push(proxy);
+    //                     LinkManager.Instance.addLinkProxy(proxy);
+    //                 }
+    //                 uniqueList.push(<CollectionFreeFormLinkWithProxyView key={key} sourceView={u.sourceView} targetView={u.targetView} proxyDoc={proxy} />);
+
+    //                 // let proxy = LinkManager.Instance.findLinkProxy(StrCast(u.sourceView.props.Document[Id]), StrCast(u.targetView.props.Document[Id]));
+    //                 // if (proxy) {
+    //                 //     this.props.addDocument(proxy, false);
+    //                 //     uniqueList.push(<CollectionFreeFormLinkWithProxyView key={key} sourceView={u.sourceView} targetView={u.targetView} />);
+    //                 // }
+    //                 // let proxyKey = Doc.AreProtosEqual(u.sourceView.Document, Cast(u.linkDoc.anchor1, Doc, new Doc)) ? "proxy1" : "proxy2";
+    //                 // let proxy = Cast(u.linkDoc[proxyKey], Doc, new Doc);
+    //                 // this.props.addDocument(proxy, false);
+
+    //                 // uniqueList.push(<CollectionFreeFormLinkWithProxyView key={key} sourceView={u.sourceView} targetView={u.targetView}
+    //                 //     proxyDoc={proxy} addDocTab={this.props.addDocTab} />);
+    //             }
+    //         }
+    //     });
+    //     return uniqueList;
+    // }
 
     render() {
-        this.findUniquePairs();
         return (
             <div className="collectionfreeformlinksview-container">
                 <svg className="collectionfreeformlinksview-svgCanvas">
-                    {/* {this.uniqueConnections} */}
-                    {this.findUniquePairs()}
+                    {this.uniqueConnections}
+                    {/* {this.findUniquePairs()} */}
                 </svg>
                 {this.props.children}
             </div>
-- 
cgit v1.2.3-70-g09d2


From a4b34adcb34184728be0b69b33a561f6d10f0a98 Mon Sep 17 00:00:00 2001
From: Fawn <fangrui_tong@brown.edu>
Date: Fri, 21 Jun 2019 16:27:03 -0400
Subject: can drag just a group of links on a doc

---
 src/client/documents/Documents.ts                  |   4 +-
 src/client/util/DocumentManager.ts                 |  46 ++-----
 src/client/util/DragManager.ts                     |  47 ++++++-
 .../CollectionFreeFormLinkView.scss                |  38 +++---
 .../CollectionFreeFormLinkView.tsx                 |  88 ++++++------
 .../CollectionFreeFormLinksView.tsx                | 148 +++------------------
 src/client/views/nodes/DocumentView.tsx            |   2 +
 src/client/views/nodes/LinkMenu.scss               |  15 ++-
 src/client/views/nodes/LinkMenu.tsx                |  18 +--
 src/client/views/nodes/LinkMenuGroup.tsx           |  74 +++++++++++
 src/client/views/nodes/LinkMenuItem.tsx            |   8 +-
 src/new_fields/Doc.ts                              |  16 ++-
 12 files changed, 243 insertions(+), 261 deletions(-)
 create mode 100644 src/client/views/nodes/LinkMenuGroup.tsx

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

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index df5c39562..7cef48b98 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -91,7 +91,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) {
-        if (LinkManager.Instance.doesLinkExist(source, target)) return;
+        if (LinkManager.Instance.doesLinkExist(source, target)) {
+            console.log("LINK EXISTS"); return;
+        }
 
         UndoManager.RunInBatch(() => {
 
diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts
index 325f4894d..c4cb6721a 100644
--- a/src/client/util/DocumentManager.ts
+++ b/src/client/util/DocumentManager.ts
@@ -85,51 +85,25 @@ export class DocumentManager {
 
     @computed
     public get LinkedDocumentViews() {
-        // console.log("linked");
-        // let docs = DocListCast(CurrentUserUtils.UserDocument.data);
-        // docs.forEach(d => {
-        //     console.log("d", StrCast(d.title));
-
-        // });
-
-        // let d = Cast(CurrentUserUtils.UserDocument.activeWorkspace, Doc, new Doc);
-        // console.log("DOC", StrCast(d.title));
-
-
-
-        let linked = DocumentManager.Instance.DocumentViews.filter(dv => dv.isSelected() || BoolCast(dv.props.Document.libraryBrush, false)).reduce((pairs, dv) => {
-            // console.log("FINDING LINKED DVs FOR", StrCast(dv.props.Document.title));
+        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 destination = LinkManager.Instance.findOppositeAnchor(link, dv.props.Document);
-                    // console.log("FINDING FOR", StrCast(dv.Document.title), StrCast(destination.title));
-
-                    if (destination) {
-                        let dvs = DocumentManager.Instance.getDocumentViews(destination);
-                        if (dvs.length > 0) {
-                            dvs.map(docView1 => {
-                                // console.log("PUSHING LINK BETWEEN", StrCast(dv.props.Document.title), StrCast(docView1.props.Document.title));
-                                // TODO: if any docviews are not in the same context, draw a proxy
-                                // let sameContent = dv.props.ContainingCollectionView === docView1.props.ContainingCollectionView;
-                                pairs.push({ anchor1View: dv, anchor2View: docView1, linkDoc: link });
-                                // console.log("PUSHED", StrCast(dv.props.Document.title), StrCast(docView1.Document.title));
-                            });
-                        } else {
-                            let dv = DocumentManager.Instance.getDocumentView(destination);
-                            dv ? console.log(StrCast(dv.props.Document.title)) : console.log("cant find");
-                        }
+                    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 { anchor1View: DocumentView, anchor2View: DocumentView, linkDoc: Doc }[]));
+                }, [] as { a: DocumentView, b: DocumentView, l: Doc }[]));
             }
             return pairs;
-        }, [] as { anchor1View: DocumentView, anchor2View: DocumentView, linkDoc: Doc }[]);
-        return linked;
+        }, [] as { a: DocumentView, b: DocumentView, l: Doc }[]);
     }
 
+
     @undoBatch
     public jumpToDocument = async (docDelegate: Doc, forceDockFunc: boolean = false, dockFunc?: (doc: Doc) => void, linkPage?: number, docContext?: Doc): Promise<void> => {
         let doc = Doc.GetProto(docDelegate);
diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts
index 11530ef09..2abcff4f7 100644
--- a/src/client/util/DragManager.ts
+++ b/src/client/util/DragManager.ts
@@ -48,12 +48,10 @@ 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);
 
-    // TODO: if not in same context then don't drag
-
     let moddrag = await Cast(draggeddoc.annotationOn, Doc);
     let dragData = new DragManager.DocumentDragData(moddrag ? [moddrag] : [draggeddoc]);
     dragData.dropAction = "alias" as dropActionType;
-    DragManager.StartDocumentDrag([dragEle], dragData, x, y, {
+    DragManager.StartLinkedDocumentDrag([dragEle], dragData, x, y, {
         handlers: {
             dragComplete: action(emptyFunction),
         },
@@ -83,11 +81,21 @@ 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.dropAction = "alias" as dropActionType;
         // dragData.moveDocument = (document, targetCollection, addDocument) => {
         //     return false;
         // };
-        DragManager.StartDocumentDrag([dragEle], dragData, x, y, {
+
+        // 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], dragData, x, y, {
             handlers: {
                 dragComplete: action(emptyFunction),
             },
@@ -215,6 +223,35 @@ export namespace DragManager {
             });
     }
 
+    export function StartLinkedDocumentDrag(eles: HTMLElement[], dragData: DocumentDragData, downX: number, downY: number, options?: DragOptions) {
+
+        runInAction(() => StartDragFunctions.map(func => func()));
+        StartDrag(eles, dragData, downX, downY, options,
+            (dropData: { [id: string]: any }) => {
+                dropData.droppedDocuments = dragData.draggedDocuments.map(d => {
+                    let dv = DocumentManager.Instance.getDocumentView(d);
+                    // 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 ?
+                    //     Doc.MakeAlias(d) : d;
+                });
+
+            });
+    }
+
     export function StartAnnotationDrag(eles: HTMLElement[], dragData: AnnotationDragData, downX: number, downY: number, options?: DragOptions) {
         StartDrag(eles, dragData, downX, downY, options);
     }
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss
index d8d518147..239c2ce56 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss
@@ -1,22 +1,22 @@
-// .collectionfreeformlinkview-linkLine {
-//     stroke: black;
-//     transform: translate(10000px,10000px);
-//     opacity: 0.5;
-//     pointer-events: all;
-// }
-// .collectionfreeformlinkview-linkCircle {
-//     stroke: rgb(0,0,0);
-//     opacity: 0.5;
-//     transform: translate(10000px,10000px);
-//     pointer-events: all;
-//     cursor: pointer;
-// }
-// .collectionfreeformlinkview-linkText {
-//     stroke: rgb(0,0,0);
-//     opacity: 0.5;
-//     transform: translate(10000px,10000px);
-//     pointer-events: all;
-// }
+.collectionfreeformlinkview-linkLine {
+    stroke: black;
+    transform: translate(10000px,10000px);
+    opacity: 0.5;
+    pointer-events: all;
+}
+.collectionfreeformlinkview-linkCircle {
+    stroke: rgb(0,0,0);
+    opacity: 0.5;
+    transform: translate(10000px,10000px);
+    pointer-events: all;
+    cursor: pointer;
+}
+.collectionfreeformlinkview-linkText {
+    stroke: rgb(0,0,0);
+    opacity: 0.5;
+    transform: translate(10000px,10000px);
+    pointer-events: all;
+}
 
 .linkview-ele {
     transform: translate(10000px,10000px);
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
index 65d5ac474..5c7f080e0 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
@@ -5,70 +5,60 @@ 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";
 
 export interface CollectionFreeFormLinkViewProps {
-    // anchor1: Doc;
-    // anchor2: Doc;
-    // LinkDocs: Doc[];
-    // addDocument: (document: Doc, allowDuplicates?: boolean) => boolean;
-    // removeDocument: (document: Doc) => boolean;
-    // sameContext: boolean;
-
-    // sourceView: DocumentView;
-    // targetView: DocumentView;
-    sourceView: Doc;
-    targetView: Doc;
+    A: Doc;
+    B: Doc;
+    LinkDocs: Doc[];
+    addDocument: (document: Doc, allowDuplicates?: boolean) => boolean;
+    removeDocument: (document: Doc) => boolean;
 }
 
 @observer
 export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFormLinkViewProps> {
 
-    // onPointerDown = (e: React.PointerEvent) => {
-    //     if (e.button === 0 && !InkingControl.Instance.selectedTool) {
-    //         let a = this.props.A;
-    //         let b = this.props.B;
-    //         let x1 = NumCast(a.x) + (BoolCast(a.isMinimized, false) ? 5 : a[WidthSym]() / 2);
-    //         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);
-    //         });
-    //         e.stopPropagation();
-    //         e.preventDefault();
-    //     }
-    // }
-
-
+    onPointerDown = (e: React.PointerEvent) => {
+        if (e.button === 0 && !InkingControl.Instance.selectedTool) {
+            let a = this.props.A;
+            let b = this.props.B;
+            let x1 = NumCast(a.x) + (BoolCast(a.isMinimized, false) ? 5 : a[WidthSym]() / 2);
+            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);
+            });
+            e.stopPropagation();
+            e.preventDefault();
+        }
+    }
     render() {
-        // let l = this.props.LinkDocs;
-        // let a = this.props.A;
-        // let b = this.props.B;
-        let a1 = this.props.sourceView;
-        let a2 = this.props.targetView;
-        let x1 = NumCast(a1.x) + (BoolCast(a1.isMinimized, false) ? 5 : NumCast(a1.width) / NumCast(a1.zoomBasis, 1) / 2);
-        let y1 = NumCast(a1.y) + (BoolCast(a1.isMinimized, false) ? 5 : NumCast(a1.height) / NumCast(a1.zoomBasis, 1) / 2);
-
-        let x2 = NumCast(a2.x) + (BoolCast(a2.isMinimized, false) ? 5 : NumCast(a2.width) / NumCast(a2.zoomBasis, 1) / 2);
-        let y2 = NumCast(a2.y) + (BoolCast(a2.isMinimized, false) ? 5 : NumCast(a2.height) / NumCast(a2.zoomBasis, 1) / 2);
-
+        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);
+        let y1 = NumCast(a.y) + (BoolCast(a.isMinimized, false) ? 5 : NumCast(a.height) / NumCast(a.zoomBasis, 1) / 2);
+        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 = "";
         return (
             <>
-                <line className="linkview-line linkview-ele"
-                    // style={{ strokeWidth: `${2 * 1 / 2}` }}
+                <line key="linkLine" className="collectionfreeformlinkview-linkLine"
+                    style={{ strokeWidth: `${2 * l.length / 2}` }}
                     x1={`${x1}`} y1={`${y1}`}
                     x2={`${x2}`} y2={`${y2}`} />
-
                 {/* <circle key="linkCircle" className="collectionfreeformlinkview-linkCircle"
                     cx={(x1 + x2) / 2} cy={(y1 + y2) / 2} r={8} onPointerDown={this.onPointerDown} /> */}
-                {/* <text key="linkText" textAnchor="middle" className="collectionfreeformlinkview-linkText" x={`${(x1 + x2) / 2}`} y={`${(y1 + y2) / 2}`}>
+                <text key="linkText" textAnchor="middle" className="collectionfreeformlinkview-linkText" x={`${(x1 + x2) / 2}`} y={`${(y1 + y2) / 2}`}>
                     {text}
-                </text> */}
+                </text>
             </>
         );
     }
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
index 9d2f8946b..854122592 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
@@ -1,4 +1,4 @@
-import { computed, IReactionDisposer, reaction, action } from "mobx";
+import { computed, IReactionDisposer, reaction } from "mobx";
 import { observer } from "mobx-react";
 import { Doc, DocListCast } from "../../../../new_fields/Doc";
 import { Id } from "../../../../new_fields/FieldSymbols";
@@ -11,10 +11,6 @@ import { CollectionViewProps } from "../CollectionSubView";
 import "./CollectionFreeFormLinksView.scss";
 import { CollectionFreeFormLinkView } from "./CollectionFreeFormLinkView";
 import React = require("react");
-import { CollectionFreeFormLinkWithProxyView } from "./CollectionFreeFormLinkWithProxyView";
-import { Docs } from "../../../documents/Documents";
-import { LinkButtonField } from "../../../../new_fields/LinkButtonField";
-import { LinkManager } from "../../../util/LinkManager";
 
 @observer
 export class CollectionFreeFormLinksView extends React.Component<CollectionViewProps> {
@@ -98,150 +94,42 @@ export class CollectionFreeFormLinksView extends React.Component<CollectionViewP
 
     @computed
     get uniqueConnections() {
-        // console.log("\n");
-        let connections = DocumentManager.Instance.LinkedDocumentViews.reduce((drawnPairs, connection) => {
-            // console.log("CONNECTION BETWEEN", StrCast(connection.anchor1View.props.Document.title), StrCast(connection.anchor2View.props.Document.title));
-            let srcViews = this.documentAnchors(connection.anchor1View);
-            // srcViews.forEach(sv => {
-            //     console.log("DOCANCHORS SRC", StrCast(connection.anchor1View.Document.title), StrCast(sv.Document.title));
-            // });
-
-            let targetViews = this.documentAnchors(connection.anchor2View);
-            // targetViews.forEach(sv => {
-            //     console.log("DOCANCHORS TARG", StrCast(connection.anchor2View.Document.title), StrCast(sv.Document.title));
-            // });
-
-            // console.log("lengths", srcViews.length, targetViews.length);
+        // DocumentManager.Instance.LinkedDocumentViews.forEach(d => {
+        //     console.log("CONNECTION", StrCast(d.a.props.Document.title), StrCast(d.b.props.Document.title));
+        // });
 
-            // srcViews.forEach(v => {
-            //     console.log("SOURCE VIEW", StrCast(v.props.Document.title));
-            // });
-            // targetViews.forEach(v => {
-            //     console.log("TARGET VIEW", StrCast(v.Document.title));
-            // });
-
-            let possiblePairs: { anchor1: Doc, anchor2: Doc }[] = [];
-            // srcViews.map(sv => {
-            //     console.log("SOURCE VIEW", StrCast(sv.props.Document.title));
-            //     targetViews.map(tv => {
-            //         console.log("TARGET VIEW", StrCast(tv.props.Document.title));
-            //         // console.log("PUSHING PAIR", StrCast(sv.props.Document.title), StrCast(tv.props.Document.title));
-            //         possiblePairs.push({ anchor1: sv.props.Document, anchor2: tv.props.Document });
-            //     });
-            //     console.log("END\n");
-            // });
-            srcViews.forEach(sv => {
-                // console.log("SOURCE VIEW", StrCast(sv.props.Document.title));
-                targetViews.forEach(tv => {
-                    // console.log("TARGET VIEW", StrCast(tv.props.Document.title));
-                    // console.log("PUSHING PAIR", StrCast(sv.props.Document.title), StrCast(tv.props.Document.title));
-                    possiblePairs.push({ anchor1: sv.props.Document, anchor2: tv.props.Document });
-                });
-                // console.log("END\n");
-            });
-            // console.log("POSSIBLE PAIRS LENGTH", possiblePairs.length);
+        let connections = DocumentManager.Instance.LinkedDocumentViews.reduce((drawnPairs, connection) => {
+            let srcViews = this.documentAnchors(connection.a);
+            let targetViews = this.documentAnchors(connection.b);
+            let possiblePairs: { a: Doc, b: Doc, }[] = [];
+            srcViews.map(sv => targetViews.map(tv => possiblePairs.push({ a: sv.props.Document, b: tv.props.Document })));
             possiblePairs.map(possiblePair => {
-                // console.log("POSSIBLEPAIR", StrCast(possiblePair.anchor1.title), StrCast(possiblePair.anchor2.title));
                 if (!drawnPairs.reduce((found, drawnPair) => {
-                    let match1 = (Doc.AreProtosEqual(possiblePair.anchor1, drawnPair.anchor1) && Doc.AreProtosEqual(possiblePair.anchor2, drawnPair.anchor2));
-                    let match2 = (Doc.AreProtosEqual(possiblePair.anchor1, drawnPair.anchor2) && Doc.AreProtosEqual(possiblePair.anchor2, drawnPair.anchor1));
+                    let match1 = (Doc.AreProtosEqual(possiblePair.a, drawnPair.a) && Doc.AreProtosEqual(possiblePair.b, drawnPair.b));
+                    let match2 = (Doc.AreProtosEqual(possiblePair.a, drawnPair.b) && Doc.AreProtosEqual(possiblePair.b, drawnPair.a));
                     let match = match1 || match2;
-                    if (match && !drawnPair.linkDocs.reduce((found, link) => found || link[Id] === connection.linkDoc[Id], false)) {
-                        drawnPair.linkDocs.push(connection.linkDoc);
+                    if (match && !drawnPair.l.reduce((found, link) => found || link[Id] === connection.l[Id], false)) {
+                        drawnPair.l.push(connection.l);
                     }
                     return match || found;
                 }, false)) {
-                    drawnPairs.push({ anchor1: possiblePair.anchor1, anchor2: possiblePair.anchor2, linkDocs: [connection.linkDoc] });
+                    drawnPairs.push({ a: possiblePair.a, b: possiblePair.b, l: [connection.l] });
                 }
             });
             return drawnPairs;
-        }, [] as { anchor1: Doc, anchor2: Doc, linkDocs: Doc[] }[]);
+        }, [] as { a: Doc, b: Doc, l: Doc[] }[]);
         return connections.map(c => {
-            let x = c.linkDocs.reduce((p, l) => p + l[Id], "");
-            return <CollectionFreeFormLinkView key={x} sourceView={c.anchor1} targetView={c.anchor2} />;
+            let x = c.l.reduce((p, l) => p + l[Id], "");
+            return <CollectionFreeFormLinkView key={x} A={c.a} B={c.b} LinkDocs={c.l}
+                removeDocument={this.props.removeDocument} addDocument={this.props.addDocument} />;
         });
     }
 
-    // findUniquePairs = (): JSX.Element[] => {
-    //     let connections = DocumentManager.Instance.LinkedDocumentViews;
-
-    //     // console.log("CONNECTIONS");
-    //     // connections.forEach(c => console.log(StrCast(c.anchor1View.Document.title), StrCast(c.anchor2View.Document.title)));
-
-    //     let unique: Set<{ sourceView: DocumentView, targetView: DocumentView, linkDoc: Doc }> = new Set();
-    //     connections.forEach(c => {
-
-    //         // let match1Index = unique.findIndex(u => (c.anchor1View === u.sourceView) && (c.anchor2View === u.targetView));
-    //         // let match2Index = unique.findIndex(u => (c.anchor1View === u.targetView) && (c.anchor2View === u.sourceView));
-    //         let match1 = unique.has({ sourceView: c.anchor1View, targetView: c.anchor2View, linkDoc: c.linkDoc });
-    //         let match2 = unique.has({ sourceView: c.anchor2View, targetView: c.anchor1View, linkDoc: c.linkDoc });
-    //         let sameContext = c.anchor1View.props.ContainingCollectionView === c.anchor2View.props.ContainingCollectionView;
-
-    //         // console.log("CONNECTION", StrCast(c.anchor1View.props.Document.title), StrCast(c.anchor2View.props.Document.title), match1, match2);
-
-
-    //         // if in same context, push if docview pair does not already exist
-    //         // else push both directions of pair
-    //         if (sameContext) {
-    //             if (!(match1 || match2)) unique.add({ sourceView: c.anchor1View, targetView: c.anchor2View, linkDoc: c.linkDoc });
-    //         } else {
-    //             unique.add({ sourceView: c.anchor1View, targetView: c.anchor2View, linkDoc: c.linkDoc });
-    //             unique.add({ sourceView: c.anchor2View, targetView: c.anchor1View, linkDoc: c.linkDoc });
-    //         }
-    //     });
-
-    //     let uniqueList: JSX.Element[] = [];
-    //     unique.forEach(u => {
-    //         // TODO: make better key
-    //         let key = StrCast(u.sourceView.Document[Id]) + "-link-" + StrCast(u.targetView.Document[Id]) + "-" + Date.now() + Math.random();
-    //         let sourceIn = u.sourceView.props.ContainingCollectionView ? u.sourceView.props.ContainingCollectionView.props.Document === this.props.Document : false;
-    //         let targetIn = u.targetView.props.ContainingCollectionView ? u.targetView.props.ContainingCollectionView.props.Document === this.props.Document : false;
-    //         let sameContext = u.sourceView.props.ContainingCollectionView === u.targetView.props.ContainingCollectionView;
-    //         let inContainer = sameContext ? sourceIn || targetIn : sourceIn;
-
-    //         if (inContainer) {
-    //             // let alias = Doc.MakeAlias(proxy);
-    //             if (sameContext) {
-    //                 uniqueList.push(<CollectionFreeFormLinkView key={key} sourceView={u.sourceView} targetView={u.targetView} />);
-    //             } else {
-    //                 let proxy = LinkManager.Instance.findLinkProxy(StrCast(u.sourceView.props.Document[Id]), StrCast(u.targetView.props.Document[Id]));
-    //                 if (!proxy) {
-    //                     proxy = Docs.LinkButtonDocument(
-    //                         { sourceViewId: StrCast(u.sourceView.props.Document[Id]), targetViewId: StrCast(u.targetView.props.Document[Id]) },
-    //                         { width: 200, height: 100, borderRounding: 0 });
-    //                     let proxy1Proto = Doc.GetProto(proxy);
-    //                     proxy1Proto.sourceViewId = StrCast(u.sourceView.props.Document[Id]);
-    //                     proxy1Proto.targetViewId = StrCast(u.targetView.props.Document[Id]);
-    //                     proxy1Proto.isLinkButton = true;
-
-    //                     // LinkManager.Instance.linkProxies.push(proxy);
-    //                     LinkManager.Instance.addLinkProxy(proxy);
-    //                 }
-    //                 uniqueList.push(<CollectionFreeFormLinkWithProxyView key={key} sourceView={u.sourceView} targetView={u.targetView} proxyDoc={proxy} />);
-
-    //                 // let proxy = LinkManager.Instance.findLinkProxy(StrCast(u.sourceView.props.Document[Id]), StrCast(u.targetView.props.Document[Id]));
-    //                 // if (proxy) {
-    //                 //     this.props.addDocument(proxy, false);
-    //                 //     uniqueList.push(<CollectionFreeFormLinkWithProxyView key={key} sourceView={u.sourceView} targetView={u.targetView} />);
-    //                 // }
-    //                 // let proxyKey = Doc.AreProtosEqual(u.sourceView.Document, Cast(u.linkDoc.anchor1, Doc, new Doc)) ? "proxy1" : "proxy2";
-    //                 // let proxy = Cast(u.linkDoc[proxyKey], Doc, new Doc);
-    //                 // this.props.addDocument(proxy, false);
-
-    //                 // uniqueList.push(<CollectionFreeFormLinkWithProxyView key={key} sourceView={u.sourceView} targetView={u.targetView}
-    //                 //     proxyDoc={proxy} addDocTab={this.props.addDocTab} />);
-    //             }
-    //         }
-    //     });
-    //     return uniqueList;
-    // }
-
     render() {
         return (
             <div className="collectionfreeformlinksview-container">
                 <svg className="collectionfreeformlinksview-svgCanvas">
                     {this.uniqueConnections}
-                    {/* {this.findUniquePairs()} */}
                 </svg>
                 {this.props.children}
             </div>
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index dbbae2d59..1fc2cf770 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -161,7 +161,9 @@ 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/LinkMenu.scss b/src/client/views/nodes/LinkMenu.scss
index d2e411c3d..c01ed23c4 100644
--- a/src/client/views/nodes/LinkMenu.scss
+++ b/src/client/views/nodes/LinkMenu.scss
@@ -18,8 +18,19 @@
     }
 
     .linkMenu-group-name {
-        border-bottom: 0.5px solid lightgray; 
-        margin-bottom: 4px;
+        padding: 4px 6px;
+        line-height: 12px;
+        border-radius: 5px;
+        margin-bottom: 2px;
+
+        &:hover {
+            background-color: lightgray;
+        }
+    }
+
+    .linkMenu-group-wrapper {
+        border-top: 0.5px solid lightgray; 
+        padding-top: 4px;
     }
 }
 
diff --git a/src/client/views/nodes/LinkMenu.tsx b/src/client/views/nodes/LinkMenu.tsx
index 8aca130a5..f96c7d2e4 100644
--- a/src/client/views/nodes/LinkMenu.tsx
+++ b/src/client/views/nodes/LinkMenu.tsx
@@ -8,6 +8,9 @@ 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 { emptyFunction } from "../../../Utils";
+import { LinkMenuGroup } from "./LinkMenuGroup";
 
 interface Props {
     docView: DocumentView;
@@ -19,24 +22,11 @@ export class LinkMenu extends React.Component<Props> {
 
     @observable private _editingLink?: Doc;
 
-    renderGroup = (group: Doc[], groupType: string): Array<JSX.Element> => {
-        let source = this.props.docView.Document;
-        return group.map(linkDoc => {
-            let destination = LinkManager.Instance.findOppositeAnchor(linkDoc, source);
-            return <LinkMenuItem key={destination[Id] + source[Id]} groupType={groupType} linkDoc={linkDoc} sourceDoc={source} destinationDoc={destination} showEditor={action(() => this._editingLink = linkDoc)} />;
-        });
-    }
-
     renderAllGroups = (groups: Map<string, Array<Doc>>): Array<JSX.Element> => {
         let linkItems: Array<JSX.Element> = [];
         groups.forEach((group, groupType) => {
             linkItems.push(
-                <div key={groupType} className="linkMenu-group">
-                    <p className="linkMenu-group-name">{groupType}:</p>
-                    <div className="linkMenu-group-wrapper">
-                        {this.renderGroup(group, groupType)}
-                    </div>
-                </div>
+                <LinkMenuGroup key={groupType} sourceDoc={this.props.docView.props.Document} group={group} groupType={groupType} showEditor={action((linkDoc: Doc) => this._editingLink = linkDoc)} />
             );
         });
 
diff --git a/src/client/views/nodes/LinkMenuGroup.tsx b/src/client/views/nodes/LinkMenuGroup.tsx
new file mode 100644
index 000000000..229143d99
--- /dev/null
+++ b/src/client/views/nodes/LinkMenuGroup.tsx
@@ -0,0 +1,74 @@
+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 { LinkManager } from "../../util/LinkManager";
+import { DragLinksAsDocuments, DragManager } from "../../util/DragManager";
+import { emptyFunction } from "../../../Utils";
+
+interface LinkMenuGroupProps {
+    sourceDoc: Doc;
+    group: Doc[];
+    groupType: string;
+    showEditor: (linkDoc: Doc) => void;
+}
+
+@observer
+export class LinkMenuGroup extends React.Component<LinkMenuGroupProps> {
+
+    private _drag = React.createRef<HTMLDivElement>();
+
+    onLinkButtonDown = (e: React.PointerEvent): void => {
+        e.stopPropagation();
+        document.removeEventListener("pointermove", this.onLinkButtonMoved);
+        document.addEventListener("pointermove", this.onLinkButtonMoved);
+        document.removeEventListener("pointerup", this.onLinkButtonUp);
+        document.addEventListener("pointerup", this.onLinkButtonUp);
+    }
+
+    onLinkButtonUp = (e: PointerEvent): void => {
+        document.removeEventListener("pointermove", this.onLinkButtonMoved);
+        document.removeEventListener("pointerup", this.onLinkButtonUp);
+        e.stopPropagation();
+    }
+
+    onLinkButtonMoved = async (e: PointerEvent) => {
+        if (this._drag.current !== null && (e.movementX > 1 || e.movementY > 1)) {
+            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 dragData = new DragManager.DocumentDragData(draggedDocs);
+
+            DragManager.StartLinkedDocumentDrag([this._drag.current], dragData, e.x, e.y, {
+                handlers: {
+                    dragComplete: action(emptyFunction),
+                },
+                hideSource: false
+            });
+        }
+        e.stopPropagation();
+    }
+
+    render() {
+        let groupItems = this.props.group.map(linkDoc => {
+            let destination = LinkManager.Instance.findOppositeAnchor(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} />;
+        });
+
+        return (
+            <div className="linkMenu-group">
+                <p className="linkMenu-group-name" ref={this._drag} onPointerDown={this.onLinkButtonDown} >{this.props.groupType}:</p>
+                <div className="linkMenu-group-wrapper">
+                    {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 1ef4690b0..42ef353b7 100644
--- a/src/client/views/nodes/LinkMenuItem.tsx
+++ b/src/client/views/nodes/LinkMenuItem.tsx
@@ -21,7 +21,7 @@ interface LinkMenuItemProps {
     linkDoc: Doc;
     sourceDoc: Doc;
     destinationDoc: Doc;
-    showEditor: () => void;
+    showEditor: (linkDoc: Doc) => void;
 }
 
 @observer
@@ -42,7 +42,7 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> {
 
     onEdit = (e: React.PointerEvent): void => {
         e.stopPropagation();
-        this.props.showEditor();
+        this.props.showEditor(this.props.linkDoc);
     }
 
     renderMetadata = (): JSX.Element => {
@@ -95,12 +95,12 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> {
             <div className="linkMenu-item">
                 <div className={canExpand ? "linkMenu-item-content expand-three" : "linkMenu-item-content expand-two"}>
                     <div className="link-name">
-                        <p>{StrCast(this.props.destinationDoc.title)}</p>
+                        <p ref={this._drag} onPointerDown={this.onLinkButtonDown}>{StrCast(this.props.destinationDoc.title)}</p>
                         <div className="linkMenu-item-buttons">
                             {canExpand ? <div title="Show more" className="button" onPointerDown={() => this.toggleShowMore()}>
                                 <FontAwesomeIcon className="fa-icon" icon={this._showMore ? "chevron-up" : "chevron-down"} 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" ref={this._drag} onPointerDown={this.onLinkButtonDown} onPointerUp={this.onFollowLink}><FontAwesomeIcon className="fa-icon" icon="arrow-right" size="sm" /></div>
+                            <div title="Follow link" className="button" onPointerDown={this.onFollowLink}><FontAwesomeIcon className="fa-icon" icon="arrow-right" size="sm" /></div>
                         </div>
                     </div>
                     {this._showMore ? this.renderMetadata() : <></>}
diff --git a/src/new_fields/Doc.ts b/src/new_fields/Doc.ts
index 1b0ff812f..5ce47fc2f 100644
--- a/src/new_fields/Doc.ts
+++ b/src/new_fields/Doc.ts
@@ -3,11 +3,13 @@ import { serializable, primitive, map, alias, list } from "serializr";
 import { autoObject, SerializationHelper, Deserializable } from "../client/util/SerializationHelper";
 import { DocServer } from "../client/DocServer";
 import { setter, getter, getField, updateFunction, deleteProperty } from "./util";
-import { Cast, ToConstructor, PromiseValue, FieldValue, NumCast } from "./Types";
+import { Cast, ToConstructor, PromiseValue, FieldValue, NumCast, StrCast } from "./Types";
 import { listSpec } from "./Schema";
 import { ObjectField } from "./ObjectField";
 import { RefField, FieldId } from "./RefField";
 import { ToScriptString, SelfProxy, Parent, OnUpdate, Self, HandleUpdate, Update, Id } from "./FieldSymbols";
+import { LinkManager } from "../client/util/LinkManager";
+import { DocUtils } from "../client/documents/Documents";
 
 export namespace Field {
     export function toScriptString(field: Field): string {
@@ -247,6 +249,18 @@ export namespace Doc {
                 }
             }
         });
+        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/collections/collectionFreeForm/CollectionFreeFormLinkView.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 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/collections/collectionFreeForm/CollectionFreeFormLinkView.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

