aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/client/documents/Documents.ts44
-rw-r--r--src/client/util/DocumentManager.ts26
-rw-r--r--src/client/util/DragManager.ts14
-rw-r--r--src/client/util/LinkManager.ts137
-rw-r--r--src/client/views/DocumentDecorations.tsx5
-rw-r--r--src/client/views/collections/CollectionDockingView.tsx6
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx5
-rw-r--r--src/client/views/nodes/DocumentView.tsx66
-rw-r--r--src/client/views/nodes/LinkBox.scss122
-rw-r--r--src/client/views/nodes/LinkBox.tsx113
-rw-r--r--src/client/views/nodes/LinkEditor.scss138
-rw-r--r--src/client/views/nodes/LinkEditor.tsx334
-rw-r--r--src/client/views/nodes/LinkMenu.scss23
-rw-r--r--src/client/views/nodes/LinkMenu.tsx47
14 files changed, 814 insertions, 266 deletions
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index b1df89dbd..002de8b5f 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -35,6 +35,7 @@ import { dropActionType } from "../util/DragManager";
import { DateField } from "../../new_fields/DateField";
import { UndoManager } from "../util/UndoManager";
import { RouteStore } from "../../server/RouteStore";
+import { LinkManager } from "../util/LinkManager";
var requestImageSize = require('request-image-size');
var path = require('path');
@@ -68,29 +69,40 @@ export interface DocumentOptions {
}
const delegateKeys = ["x", "y", "width", "height", "panX", "panY"];
+// export interface LinkData {
+// anchor1: Doc;
+// anchor1Page: number;
+// anchor1Tags: Array<{ tag: string, name: string, description: string }>;
+// anchor2: Doc;
+// anchor2Page: number;
+// anchor2Tags: Array<{ tag: string, name: string, description: string }>;
+// }
+
+// export interface TagData {
+// tag: string;
+// name: string;
+// description: string;
+// }
+
export namespace DocUtils {
export function MakeLink(source: Doc, target: Doc, targetContext?: Doc) {
- let protoSrc = source.proto ? source.proto : source;
- let protoTarg = target.proto ? target.proto : target;
+ if (LinkManager.Instance.doesLinkExist(source, target)) return;
+
UndoManager.RunInBatch(() => {
+
let linkDoc = Docs.TextDocument({ width: 100, height: 30, borderRounding: -1 });
let linkDocProto = Doc.GetProto(linkDoc);
- linkDocProto.title = source.title + " to " + target.title;
- linkDocProto.linkDescription = "";
- linkDocProto.linkTags = "Default";
- linkDocProto.linkedTo = target;
- linkDocProto.linkedFrom = source;
- linkDocProto.linkedToPage = target.curPage;
- linkDocProto.linkedFromPage = source.curPage;
- linkDocProto.linkedToContext = targetContext;
+ linkDocProto.anchor1 = source;
+ linkDocProto.anchor1Page = source.curPage;
+ linkDocProto.anchor1Groups = new List<Doc>([]);
+ linkDocProto.anchor2 = target;
+ linkDocProto.anchor2Page = target.curPage;
+ linkDocProto.anchor2Groups = new List<Doc>([]);
+ linkDocProto.context = targetContext;
+
+ LinkManager.Instance.allLinks.push(linkDoc);
- let linkedFrom = Cast(protoTarg.linkedFromDocs, listSpec(Doc));
- let linkedTo = Cast(protoSrc.linkedToDocs, listSpec(Doc));
- !linkedFrom && (protoTarg.linkedFromDocs = linkedFrom = new List<Doc>());
- !linkedTo && (protoSrc.linkedToDocs = linkedTo = new List<Doc>());
- linkedFrom.push(linkDoc);
- linkedTo.push(linkDoc);
return linkDoc;
}, "make link");
}
diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts
index ff0c1560b..fc78993b8 100644
--- a/src/client/util/DocumentManager.ts
+++ b/src/client/util/DocumentManager.ts
@@ -9,6 +9,7 @@ import { CollectionView } from '../views/collections/CollectionView';
import { CollectionPDFView } from '../views/collections/CollectionPDFView';
import { CollectionVideoView } from '../views/collections/CollectionVideoView';
import { Id } from '../../new_fields/FieldSymbols';
+import { LinkManager } from './LinkManager';
export class DocumentManager {
@@ -83,35 +84,24 @@ export class DocumentManager {
@computed
public get LinkedDocumentViews() {
- return DocumentManager.Instance.DocumentViews.filter(dv => dv.isSelected() || BoolCast(dv.props.Document.libraryBrush, false)).reduce((pairs, dv) => {
- let linksList = DocListCast(dv.props.Document.linkedToDocs);
+ let linked = DocumentManager.Instance.DocumentViews.filter(dv => dv.isSelected() || BoolCast(dv.props.Document.libraryBrush, false)).reduce((pairs, dv) => {
+
+ let linksList = LinkManager.Instance.findAllRelatedLinks(dv.props.Document);
if (linksList && linksList.length) {
pairs.push(...linksList.reduce((pairs, link) => {
if (link) {
- let linkToDoc = FieldValue(Cast(link.linkedTo, Doc));
- if (linkToDoc) {
- DocumentManager.Instance.getDocumentViews(linkToDoc).map(docView1 =>
+ 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 }));
}
}
return pairs;
}, [] as { a: DocumentView, b: DocumentView, l: Doc }[]));
}
- linksList = DocListCast(dv.props.Document.linkedFromDocs);
- if (linksList && linksList.length) {
- pairs.push(...linksList.reduce((pairs, link) => {
- if (link) {
- let linkFromDoc = FieldValue(Cast(link.linkedFrom, Doc));
- if (linkFromDoc) {
- DocumentManager.Instance.getDocumentViews(linkFromDoc).map(docView1 =>
- pairs.push({ a: dv, b: docView1, l: link }));
- }
- }
- return pairs;
- }, pairs));
- }
return pairs;
}, [] as { a: DocumentView, b: DocumentView, l: Doc }[]);
+ return linked;
}
@undoBatch
diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts
index ab00fabe1..b32c25b0a 100644
--- a/src/client/util/DragManager.ts
+++ b/src/client/util/DragManager.ts
@@ -4,6 +4,7 @@ import { Cast } from "../../new_fields/Types";
import { emptyFunction } from "../../Utils";
import { CollectionDockingView } from "../views/collections/CollectionDockingView";
import * as globalCssVariables from "../views/globalCssVariables.scss";
+import { LinkManager } from "./LinkManager";
import { URLField } from "../../new_fields/URLField";
import { SelectionManager } from "./SelectionManager";
@@ -45,14 +46,15 @@ export function SetupDrag(_reference: React.RefObject<HTMLElement>, docFunc: ()
export async function DragLinksAsDocuments(dragEle: HTMLElement, x: number, y: number, sourceDoc: Doc) {
let srcTarg = sourceDoc.proto;
let draggedDocs: Doc[] = [];
- let draggedFromDocs: Doc[] = [];
if (srcTarg) {
- let linkToDocs = await DocListCastAsync(srcTarg.linkedToDocs);
- let linkFromDocs = await DocListCastAsync(srcTarg.linkedFromDocs);
- if (linkToDocs) draggedDocs = linkToDocs.map(linkDoc => Cast(linkDoc.linkedTo, Doc) as Doc);
- if (linkFromDocs) draggedFromDocs = linkFromDocs.map(linkDoc => Cast(linkDoc.linkedFrom, Doc) as Doc);
+ let linkDocs = LinkManager.Instance.findAllRelatedLinks(srcTarg);
+ if (linkDocs) {
+ draggedDocs = linkDocs.map(link => {
+ return LinkManager.Instance.findOppositeAnchor(link, sourceDoc);
+ });
+ }
}
- draggedDocs.push(...draggedFromDocs);
+ // draggedDocs.push(...draggedFromDocs);
if (draggedDocs.length) {
let moddrag: Doc[] = [];
for (const draggedDoc of draggedDocs) {
diff --git a/src/client/util/LinkManager.ts b/src/client/util/LinkManager.ts
new file mode 100644
index 000000000..23ba9d2e4
--- /dev/null
+++ b/src/client/util/LinkManager.ts
@@ -0,0 +1,137 @@
+import { observable } from "mobx";
+import { StrCast, Cast } from "../../new_fields/Types";
+import { Doc, DocListCast } from "../../new_fields/Doc";
+import { listSpec } from "../../new_fields/Schema";
+import { List } from "../../new_fields/List";
+
+
+/*
+ * link doc:
+ * - anchor1: doc
+ * - anchor1page: number
+ * - anchor1groups: list of group docs representing the groups anchor1 categorizes this link/anchor2 in
+ * - anchor2: doc
+ * - anchor2page: number
+ * - anchor2groups: list of group docs representing the groups anchor2 categorizes this link/anchor1 in
+ *
+ * group doc:
+ * - type: string representing the group type/name/category
+ * - metadata: doc representing the metadata kvps
+ *
+ * metadata doc:
+ * - user defined kvps
+ */
+export class LinkManager {
+ private static _instance: LinkManager;
+ public static get Instance(): LinkManager {
+ return this._instance || (this._instance = new this());
+ }
+ private constructor() {
+ }
+
+ @observable public allLinks: Array<Doc> = []; // list of link docs
+ @observable public groupMetadataKeys: Map<string, Array<string>> = new Map();
+ // map of group type to list of its metadata keys; serves as a dictionary of groups to what kind of metadata it hodls
+
+ // finds all links that contain the given anchor
+ public findAllRelatedLinks(anchor: Doc): Array<Doc> {
+ return LinkManager.Instance.allLinks.filter(
+ link => Doc.AreProtosEqual(anchor, Cast(link.anchor1, Doc, new Doc)) || Doc.AreProtosEqual(anchor, Cast(link.anchor2, Doc, new Doc)));
+ }
+
+ // returns map of group type to anchor's links in that group type
+ public findRelatedGroupedLinks(anchor: Doc): Map<string, Array<Doc>> {
+ let related = this.findAllRelatedLinks(anchor);
+ let anchorGroups = new Map<string, Array<Doc>>();
+ related.forEach(link => {
+ let groups = LinkManager.Instance.getAnchorGroups(link, anchor);
+
+ if (groups.length > 0) {
+ groups.forEach(groupDoc => {
+ let groupType = StrCast(groupDoc.type);
+ let group = anchorGroups.get(groupType);
+ if (group) group.push(link);
+ else group = [link];
+ anchorGroups.set(groupType, group);
+ });
+ } else {
+ // if link is in no groups then put it in default group
+ let group = anchorGroups.get("*");
+ if (group) group.push(link);
+ else group = [link];
+ anchorGroups.set("*", group);
+ }
+
+ });
+ return anchorGroups;
+ }
+
+ // returns a list of all metadata docs associated with the given group type
+ public findAllMetadataDocsInGroup(groupType: string): Array<Doc> {
+ let md: Doc[] = [];
+ let allLinks = LinkManager.Instance.allLinks;
+ allLinks.forEach(linkDoc => {
+ let anchor1Groups = LinkManager.Instance.getAnchorGroups(linkDoc, Cast(linkDoc.anchor1, Doc, new Doc));
+ let anchor2Groups = LinkManager.Instance.getAnchorGroups(linkDoc, Cast(linkDoc.anchor2, Doc, new Doc));
+ anchor1Groups.forEach(groupDoc => { if (StrCast(groupDoc.type).toUpperCase() === groupType.toUpperCase()) md.push(Cast(groupDoc.metadata, Doc, new Doc)); });
+ anchor2Groups.forEach(groupDoc => { if (StrCast(groupDoc.type).toUpperCase() === groupType.toUpperCase()) md.push(Cast(groupDoc.metadata, Doc, new Doc)); });
+ });
+ return md;
+ }
+
+ // removes all group docs from all links with the given group type
+ public deleteGroup(groupType: string): void {
+ let deleted = LinkManager.Instance.groupMetadataKeys.delete(groupType);
+ if (deleted) {
+ LinkManager.Instance.allLinks.forEach(linkDoc => {
+ LinkManager.Instance.removeGroupFromAnchor(linkDoc, Cast(linkDoc.anchor1, Doc, new Doc), groupType);
+ LinkManager.Instance.removeGroupFromAnchor(linkDoc, Cast(linkDoc.anchor2, Doc, new Doc), groupType);
+ });
+ }
+ }
+
+ // removes group doc of given group type only from given anchor on given link
+ public removeGroupFromAnchor(linkDoc: Doc, anchor: Doc, groupType: string) {
+ let groups = LinkManager.Instance.getAnchorGroups(linkDoc, anchor);
+ let newGroups = groups.filter(groupDoc => StrCast(groupDoc.type).toUpperCase() !== groupType.toUpperCase());
+ LinkManager.Instance.setAnchorGroups(linkDoc, anchor, newGroups);
+ }
+
+ // checks if a link with the given anchors exists
+ public doesLinkExist(anchor1: Doc, anchor2: Doc) {
+ let allLinks = LinkManager.Instance.allLinks;
+ let index = allLinks.findIndex(linkDoc => {
+ return (Doc.AreProtosEqual(Cast(linkDoc.anchor1, Doc, new Doc), anchor1) && Doc.AreProtosEqual(Cast(linkDoc.anchor2, Doc, new Doc), anchor2)) ||
+ (Doc.AreProtosEqual(Cast(linkDoc.anchor1, Doc, new Doc), anchor2) && Doc.AreProtosEqual(Cast(linkDoc.anchor2, Doc, new Doc), anchor1));
+ });
+ return index !== -1;
+ }
+
+ // finds the opposite anchor of a given anchor in a link
+ public findOppositeAnchor(linkDoc: Doc, anchor: Doc): Doc {
+ if (Doc.AreProtosEqual(anchor, Cast(linkDoc.anchor1, Doc, new Doc))) {
+ return Cast(linkDoc.anchor2, Doc, new Doc);
+ } else {
+ return Cast(linkDoc.anchor1, Doc, new Doc);
+ }
+ }
+
+ // gets the groups associates with an anchor in a link
+ public getAnchorGroups(linkDoc: Doc, anchor: Doc): Array<Doc> {
+ if (Doc.AreProtosEqual(anchor, Cast(linkDoc.anchor1, Doc, new Doc))) {
+ return DocListCast(linkDoc.anchor1Groups);
+ } else {
+ return DocListCast(linkDoc.anchor2Groups);
+ }
+ }
+
+ // sets the groups of the given anchor in the given link
+ public setAnchorGroups(linkDoc: Doc, anchor: Doc, groups: Doc[]) {
+ if (Doc.AreProtosEqual(anchor, Cast(linkDoc.anchor1, Doc, new Doc))) {
+ linkDoc.anchor1Groups = new List<Doc>(groups);
+ } else {
+ linkDoc.anchor2Groups = new List<Doc>(groups);
+ }
+ }
+
+} \ No newline at end of file
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index ceca940b6..b636a6bae 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -25,6 +25,7 @@ import { TemplateMenu } from "./TemplateMenu";
import { Template, Templates } from "./Templates";
import React = require("react");
import { URLField } from '../../new_fields/URLField';
+import { LinkManager } from '../util/LinkManager';
const higflyout = require("@hig/flyout");
export const { anchorPoints } = higflyout;
export const Flyout = higflyout.default;
@@ -574,9 +575,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
let linkButton = null;
if (SelectionManager.SelectedDocuments().length > 0) {
let selFirst = SelectionManager.SelectedDocuments()[0];
- let linkToSize = Cast(selFirst.props.Document.linkedToDocs, listSpec(Doc), []).length;
- let linkFromSize = Cast(selFirst.props.Document.linkedFromDocs, listSpec(Doc), []).length;
- let linkCount = linkToSize + linkFromSize;
+ let linkCount = LinkManager.Instance.findAllRelatedLinks(selFirst.props.Document).length;
linkButton = (<Flyout
anchorPoint={anchorPoints.RIGHT_TOP}
content={<LinkMenu docView={selFirst}
diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx
index 51e29cb54..8a5342411 100644
--- a/src/client/views/collections/CollectionDockingView.tsx
+++ b/src/client/views/collections/CollectionDockingView.tsx
@@ -24,6 +24,8 @@ import { SubCollectionViewProps } from "./CollectionSubView";
import { ParentDocSelector } from './ParentDocumentSelector';
import React = require("react");
import { MainView } from '../MainView';
+import { LinkManager } from '../../util/LinkManager';
+
@observer
export class CollectionDockingView extends React.Component<SubCollectionViewProps> {
@@ -339,9 +341,9 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
tab.reactComponents = [upDiv];
tab.element.append(upDiv);
counter.DashDocId = tab.contentItem.config.props.documentId;
- tab.reactionDisposer = reaction(() => [doc.linkedFromDocs, doc.LinkedToDocs, doc.title],
+ tab.reactionDisposer = reaction(() => [LinkManager.Instance.findAllRelatedLinks(doc), doc.title],
() => {
- counter.innerHTML = DocListCast(doc.linkedFromDocs).length + DocListCast(doc.linkedToDocs).length;
+ counter.innerHTML = LinkManager.Instance.findAllRelatedLinks(doc).length;
tab.titleElement[0].textContent = doc.title;
}, { fireImmediately: true });
tab.titleElement[0].DashDocId = tab.contentItem.config.props.documentId;
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
index ba7e6cf9e..ddde8ece8 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
@@ -44,9 +44,8 @@ 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-";
+ this.props.LinkDocs.map(l => text += StrCast(l.title) + "(" + StrCast(l.linkDescription) + "), ");
+ text = "";
return (
<>
<line key="linkLine" className="collectionfreeformlinkview-linkLine"
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 6fe01963a..c998b8ea6 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -30,6 +30,8 @@ import "./DocumentView.scss";
import React = require("react");
import { Id, Copy } from '../../../new_fields/FieldSymbols';
import { ContextMenuProps } from '../ContextMenuItem';
+import { list, object, createSimpleSchema } from 'serializr';
+import { LinkManager } from '../../util/LinkManager';
const JsxParser = require('react-jsx-parser').default; //TODO Why does this need to be imported like this?
library.add(faTrash);
@@ -47,16 +49,17 @@ library.add(faFingerprint);
library.add(faCrosshairs);
library.add(faDesktop);
-const linkSchema = createSchema({
- title: "string",
- linkDescription: "string",
- linkTags: "string",
- linkedTo: Doc,
- linkedFrom: Doc
-});
-type LinkDoc = makeInterface<[typeof linkSchema]>;
-const LinkDoc = makeInterface(linkSchema);
+// const linkSchema = createSchema({
+// title: "string",
+// linkDescription: "string",
+// linkTags: "string",
+// linkedTo: Doc,
+// linkedFrom: Doc
+// });
+
+// type LinkDoc = makeInterface<[typeof linkSchema]>;
+// const LinkDoc = makeInterface(linkSchema);
export interface DocumentViewProps {
ContainingCollectionView: Opt<CollectionView | CollectionPDFView | CollectionVideoView>;
@@ -218,8 +221,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
let subBulletDocs = await DocListCastAsync(this.props.Document.subBulletDocs);
let maximizedDocs = await DocListCastAsync(this.props.Document.maximizedDocs);
let summarizedDocs = await DocListCastAsync(this.props.Document.summarizedDocs);
- let linkedToDocs = await DocListCastAsync(this.props.Document.linkedToDocs, []);
- let linkedFromDocs = await DocListCastAsync(this.props.Document.linkedFromDocs, []);
+ let linkedDocs = LinkManager.Instance.findAllRelatedLinks(this.props.Document);
let expandedDocs: Doc[] = [];
expandedDocs = subBulletDocs ? [...subBulletDocs, ...expandedDocs] : expandedDocs;
expandedDocs = maximizedDocs ? [...maximizedDocs, ...expandedDocs] : expandedDocs;
@@ -254,24 +256,30 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
this.props.collapseToPoint && this.props.collapseToPoint(scrpt, expandedProtoDocs);
}
}
- else if (linkedToDocs.length || linkedFromDocs.length) {
- let linkedFwdDocs = [
- linkedToDocs.length ? linkedToDocs[0].linkedTo as Doc : linkedFromDocs.length ? linkedFromDocs[0].linkedFrom as Doc : expandedDocs[0],
- linkedFromDocs.length ? linkedFromDocs[0].linkedFrom as Doc : linkedToDocs.length ? linkedToDocs[0].linkedTo as Doc : expandedDocs[0]];
-
- let linkedFwdContextDocs = [
- linkedToDocs.length ? await (linkedToDocs[0].linkedToContext) as Doc : linkedFromDocs.length ? await PromiseValue(linkedFromDocs[0].linkedFromContext) as Doc : undefined,
- linkedFromDocs.length ? await (linkedFromDocs[0].linkedFromContext) as Doc : linkedToDocs.length ? await PromiseValue(linkedToDocs[0].linkedToContext) as Doc : undefined];
-
- let linkedFwdPage = [
- linkedToDocs.length ? NumCast(linkedToDocs[0].linkedToPage, undefined) : linkedFromDocs.length ? NumCast(linkedFromDocs[0].linkedFromPage, undefined) : undefined,
- linkedFromDocs.length ? NumCast(linkedFromDocs[0].linkedFromPage, undefined) : linkedToDocs.length ? NumCast(linkedToDocs[0].linkedToPage, undefined) : undefined];
-
- if (!linkedFwdDocs.some(l => l instanceof Promise)) {
- let maxLocation = StrCast(linkedFwdDocs[altKey ? 1 : 0].maximizeLocation, "inTab");
- let targetContext = !Doc.AreProtosEqual(linkedFwdContextDocs[altKey ? 1 : 0], this.props.ContainingCollectionView && this.props.ContainingCollectionView.props.Document) ? linkedFwdContextDocs[altKey ? 1 : 0] : undefined;
- DocumentManager.Instance.jumpToDocument(linkedFwdDocs[altKey ? 1 : 0], ctrlKey, document => this.props.addDocTab(document, maxLocation), linkedFwdPage[altKey ? 1 : 0], targetContext);
- }
+ else if (linkedDocs.length) {
+ let linkedDoc = linkedDocs.length ? linkedDocs[0] : expandedDocs[0];
+ let linkedPages = [linkedDocs.length ? NumCast(linkedDocs[0].anchor1Page, undefined) : NumCast(linkedDocs[0].anchor2Page, undefined),
+ linkedDocs.length ? NumCast(linkedDocs[0].anchor2Page, undefined) : NumCast(linkedDocs[0].anchor1Page, undefined)];
+ let maxLocation = StrCast(linkedDoc.maximizeLocation, "inTab");
+ DocumentManager.Instance.jumpToDocument(linkedDoc, ctrlKey, document => this.props.addDocTab(document, maxLocation), linkedPages[altKey ? 1 : 0]);
+ // else if (linkedToDocs.length || linkedFromDocs.length) {
+ // let linkedFwdDocs = [
+ // linkedToDocs.length ? linkedToDocs[0].linkedTo as Doc : linkedFromDocs.length ? linkedFromDocs[0].linkedFrom as Doc : expandedDocs[0],
+ // linkedFromDocs.length ? linkedFromDocs[0].linkedFrom as Doc : linkedToDocs.length ? linkedToDocs[0].linkedTo as Doc : expandedDocs[0]];
+
+ // let linkedFwdContextDocs = [
+ // linkedToDocs.length ? await (linkedToDocs[0].linkedToContext) as Doc : linkedFromDocs.length ? await PromiseValue(linkedFromDocs[0].linkedFromContext) as Doc : undefined,
+ // linkedFromDocs.length ? await (linkedFromDocs[0].linkedFromContext) as Doc : linkedToDocs.length ? await PromiseValue(linkedToDocs[0].linkedToContext) as Doc : undefined];
+
+ // let linkedFwdPage = [
+ // linkedToDocs.length ? NumCast(linkedToDocs[0].linkedToPage, undefined) : linkedFromDocs.length ? NumCast(linkedFromDocs[0].linkedFromPage, undefined) : undefined,
+ // linkedFromDocs.length ? NumCast(linkedFromDocs[0].linkedFromPage, undefined) : linkedToDocs.length ? NumCast(linkedToDocs[0].linkedToPage, undefined) : undefined];
+
+ // if (!linkedFwdDocs.some(l => l instanceof Promise)) {
+ // let maxLocation = StrCast(linkedFwdDocs[altKey ? 1 : 0].maximizeLocation, "inTab");
+ // let targetContext = !Doc.AreProtosEqual(linkedFwdContextDocs[altKey ? 1 : 0], this.props.ContainingCollectionView && this.props.ContainingCollectionView.props.Document) ? linkedFwdContextDocs[altKey ? 1 : 0] : undefined;
+ // DocumentManager.Instance.jumpToDocument(linkedFwdDocs[altKey ? 1 : 0], ctrlKey, document => this.props.addDocTab(document, maxLocation), linkedFwdPage[altKey ? 1 : 0], targetContext);
+ // }
}
}
}
diff --git a/src/client/views/nodes/LinkBox.scss b/src/client/views/nodes/LinkBox.scss
index 639f83b38..77462f611 100644
--- a/src/client/views/nodes/LinkBox.scss
+++ b/src/client/views/nodes/LinkBox.scss
@@ -1,66 +1,76 @@
@import "../globalCssVariables";
-.link-container {
- width: 100%;
- height: 50px;
+.linkMenu-item {
+ border-top: 0.5px solid $main-accent;
+ padding: 6px;
+ position: relative;
display: flex;
- flex-direction: row;
- border-top: 0.5px solid #bababa;
-}
-
-.info-container {
- width: 65%;
- padding-top: 5px;
- padding-left: 5px;
- display: flex;
- flex-direction: column
-}
-
-.link-name {
- font-size: 11px;
-}
+ font-size: 12px;
-.doc-name {
- font-size: 8px;
-}
-
-.button-container {
- width: 35%;
- padding-top: 8px;
- display: flex;
- flex-direction: row;
-}
+ .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;
+ }
+ }
-.button {
- height: 20px;
- width: 20px;
- margin: 8px 4px;
- border-radius: 50%;
- opacity: 0.9;
- pointer-events: auto;
- background-color: $dark-color;
- color: $light-color;
- text-transform: uppercase;
- letter-spacing: 2px;
- font-size: 60%;
- transition: transform 0.2s;
+ &: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);
+ }
+ }
+ }
}
-.button:hover {
- background: $main-accent;
- cursor: pointer;
-}
+.linkMenu-item-buttons {
+ display: none;
+ position: absolute;
+ top: 50%;
+ right: 0;
+ transform: translateY(-50%);
-// .fa-icon-view {
-// margin-left: 3px;
-// margin-top: 5px;
-// }
+ .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-edit {
- margin-left: 6px;
- margin-top: 6px;
-}
+ .fa-icon {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ }
-.fa-icon-delete {
- margin-left: 7px;
- margin-top: 6px;
+ &:last-child {
+ margin-right: 0;
+ }
+ &:hover {
+ background: $main-accent;
+ }
+ }
} \ No newline at end of file
diff --git a/src/client/views/nodes/LinkBox.tsx b/src/client/views/nodes/LinkBox.tsx
index 68b692aad..09df1eac5 100644
--- a/src/client/views/nodes/LinkBox.tsx
+++ b/src/client/views/nodes/LinkBox.tsx
@@ -1,86 +1,107 @@
import { library } from '@fortawesome/fontawesome-svg-core';
-import { faEdit, faEye, faTimes } from '@fortawesome/free-solid-svg-icons';
+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 { CollectionDockingView } from "../collections/CollectionDockingView";
import './LinkBox.scss';
import React = require("react");
import { Doc } from '../../../new_fields/Doc';
-import { Cast, NumCast } from '../../../new_fields/Types';
-import { listSpec } from '../../../new_fields/Schema';
-import { action } from 'mobx';
+import { StrCast, Cast } from '../../../new_fields/Types';
+import { observable, action } from 'mobx';
+import { LinkManager } from '../../util/LinkManager';
+import { DragLinksAsDocuments } from '../../util/DragManager';
+import { SelectionManager } from '../../util/SelectionManager';
+library.add(faEye, faEdit, faTimes, faArrowRight, faChevronDown, faChevronUp);
-library.add(faEye);
-library.add(faEdit);
-library.add(faTimes);
-
interface Props {
+ groupType: string;
linkDoc: Doc;
- linkName: String;
- pairedDoc: Doc;
- type: String;
+ 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
- onViewButtonPressed = async (e: React.PointerEvent): Promise<void> => {
+ onFollowLink = async (e: React.PointerEvent): Promise<void> => {
e.stopPropagation();
- DocumentManager.Instance.jumpToDocument(this.props.pairedDoc, e.altKey);
+ DocumentManager.Instance.jumpToDocument(this.props.destinationDoc, e.altKey);
}
- onEditButtonPressed = (e: React.PointerEvent): void => {
+ onEdit = (e: React.PointerEvent): void => {
e.stopPropagation();
-
this.props.showEditor();
}
- @action
- onDeleteButtonPressed = async (e: React.PointerEvent): Promise<void> => {
- e.stopPropagation();
- const [linkedFrom, linkedTo] = await Promise.all([Cast(this.props.linkDoc.linkedFrom, Doc), Cast(this.props.linkDoc.linkedTo, Doc)]);
- if (linkedFrom) {
- const linkedToDocs = Cast(linkedFrom.linkedToDocs, listSpec(Doc));
- if (linkedToDocs) {
- linkedToDocs.splice(linkedToDocs.indexOf(this.props.linkDoc), 1);
- }
+ 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>);
+ });
}
- if (linkedTo) {
- const linkedFromDocs = Cast(linkedTo.linkedFromDocs, listSpec(Doc));
- if (linkedFromDocs) {
- linkedFromDocs.splice(linkedFromDocs.indexOf(this.props.linkDoc), 1);
- }
+
+ 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);
+
+ DragLinksAsDocuments(this._drag.current, e.x, e.y, SelectionManager.SelectedDocuments()[0].props.Document);
}
+ e.stopPropagation();
}
render() {
+ let keys = LinkManager.Instance.groupMetadataKeys.get(this.props.groupType);
+ let canExpand = keys ? keys.length > 0 : false;
+
return (
- //<LinkEditor linkBox={this} linkDoc={this.props.linkDoc} />
- <div className="link-container">
- <div className="info-container" onPointerDown={this.onViewButtonPressed}>
+ <div className="linkMenu-item">
+ <div className={canExpand ? "linkMenu-item-content expand-three" : "linkMenu-item-content expand-two"}>
<div className="link-name">
- <p>{this.props.linkName}</p>
- </div>
- <div className="doc-name">
- <p>{this.props.type}{this.props.pairedDoc.Title}</p>
+ <p>{StrCast(this.props.destinationDoc.title)}</p>
+ <div 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 className="button-container">
- {/* <div title="Follow Link" className="button" onPointerDown={this.onViewButtonPressed}>
- <FontAwesomeIcon className="fa-icon-view" icon="eye" size="sm" /></div> */}
- <div title="Edit Link" className="button" onPointerDown={this.onEditButtonPressed}>
- <FontAwesomeIcon className="fa-icon-edit" icon="edit" size="sm" /></div>
- <div title="Delete Link" className="button" onPointerDown={this.onDeleteButtonPressed}>
- <FontAwesomeIcon className="fa-icon-delete" icon="times" size="sm" /></div>
- </div>
- </div>
+ </div >
);
}
} \ No newline at end of file
diff --git a/src/client/views/nodes/LinkEditor.scss b/src/client/views/nodes/LinkEditor.scss
index 9629585d7..6aac39f45 100644
--- a/src/client/views/nodes/LinkEditor.scss
+++ b/src/client/views/nodes/LinkEditor.scss
@@ -1,42 +1,122 @@
@import "../globalCssVariables";
-.edit-container {
+
+.linkEditor {
width: 100%;
height: auto;
+ font-size: 12px; // TODO
+}
+
+.linkEditor-back {
+ margin-bottom: 6px;
+}
+
+.linkEditor-groupsLabel {
display: flex;
- flex-direction: column;
+ justify-content: space-between;
+
+ button {
+ width: 20px;
+ height: 20px;
+ margin-left: 6px;
+ padding: 0;
+ font-size: 14px;
+ }
}
-.name-input {
- margin-bottom: 10px;
- padding: 5px;
- font-size: 12px;
- border: 1px solid #bababa;
+.linkEditor-linkedTo {
+ border-bottom: 0.5px solid $light-color-secondary;
+ padding-bottom: 6px;
+ margin-bottom: 6px;
}
-.description-input {
- font-size: 11px;
- padding: 5px;
- margin-bottom: 10px;
- border: 1px solid #bababa;
+.linkEditor-group {
+ background-color: $light-color-secondary;
+ padding: 6px;
+ margin: 3px 0;
+ border-radius: 3px;
+
+ .linkEditor-group-row {
+ display: flex;
+ margin-bottom: 6px;
+
+ .linkEditor-group-row-label {
+ margin-right: 6px;
+ }
+ }
+
+ .linkEditor-metadata-row {
+ display: flex;
+ justify-content: space-between;
+ margin-bottom: 6px;
+
+ .linkEditor-error {
+ border-color: red;
+ }
+
+ input {
+ width: calc(50% - 18px);
+ height: 20px;
+ }
+
+ button {
+ width: 20px;
+ height: 20px;
+ margin-left: 6px;
+ padding: 0;
+ font-size: 14px;
+ }
+ }
}
-.save-button {
- width: 50px;
- height: 22px;
- pointer-events: auto;
- background-color: $dark-color;
- color: $light-color;
- text-transform: uppercase;
- letter-spacing: 2px;
- padding: 2px;
- font-size: 10px;
- margin: 0 auto;
- transition: transform 0.2s;
- text-align: center;
- line-height: 20px;
+
+.linkEditor-dropdown {
+ width: 100%;
+ position: relative;
+ z-index: 999;
+
+ .linkEditor-options-wrapper {
+ width: 100%;
+ position: absolute;
+ top: 19px;
+ left: 0;
+ display: flex;
+ flex-direction: column;
+ }
+
+ .linkEditor-option {
+ background-color: $light-color-secondary;
+ border: 1px solid $intermediate-color;
+ border-top: 0;
+ padding: 3px;
+ cursor: pointer;
+
+ &:hover {
+ background-color: $intermediate-color;
+ font-weight: bold;
+ }
+ }
}
-.save-button:hover {
- background: $main-accent;
- cursor: pointer;
+.linkEditor-group-buttons {
+ height: 20px;
+ display: flex;
+ justify-content: space-between;
+
+ .linkEditor-groupOpts {
+ width: calc(20% - 3px);
+ height: 20px;
+ padding: 0;
+ font-size: 10px;
+
+ &:disabled {
+ 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 71a423338..484682c22 100644
--- a/src/client/views/nodes/LinkEditor.tsx
+++ b/src/client/views/nodes/LinkEditor.tsx
@@ -1,57 +1,337 @@
import { observable, computed, action } from "mobx";
import React = require("react");
-import { SelectionManager } from "../../util/SelectionManager";
import { observer } from "mobx-react";
import './LinkEditor.scss';
-import { props } from "bluebird";
-import { DocumentView } from "./DocumentView";
-import { link } from "fs";
-import { StrCast } from "../../../new_fields/Types";
+import { StrCast, Cast } from "../../../new_fields/Types";
import { Doc } from "../../../new_fields/Doc";
+import { LinkManager } from "../../util/LinkManager";
+import { Docs } from "../../documents/Documents";
+import { Utils } from "../../../Utils";
+import { faArrowLeft, faEllipsisV, faTable } from '@fortawesome/free-solid-svg-icons';
+import { library } from "@fortawesome/fontawesome-svg-core";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { SetupDrag } from "../../util/DragManager";
-interface Props {
- linkDoc: Doc;
- showLinks: () => void;
+library.add(faArrowLeft);
+library.add(faEllipsisV);
+library.add(faTable);
+
+
+interface GroupTypesDropdownProps {
+ groupId: string;
+ groupType: string;
+ setGroup: (groupId: string, group: string) => void;
}
+// this dropdown could be generalized
+@observer
+class GroupTypesDropdown extends React.Component<GroupTypesDropdownProps> {
+ @observable private _searchTerm: string = "";
+ @observable private _groupType: string = this.props.groupType;
+
+ @action setSearchTerm = (value: string): void => { this._searchTerm = value; };
+ @action setGroupType = (value: string): void => { this._groupType = value; };
+
+ @action
+ createGroup = (groupType: string): void => {
+ this.props.setGroup(this.props.groupId, groupType);
+ LinkManager.Instance.groupMetadataKeys.set(groupType, []);
+ }
+
+ renderOptions = (): JSX.Element[] | JSX.Element => {
+ if (this._searchTerm === "") return <></>;
+ let allGroupTypes = Array.from(LinkManager.Instance.groupMetadataKeys.keys());
+ let groupOptions = allGroupTypes.filter(groupType => groupType.toUpperCase().indexOf(this._searchTerm.toUpperCase()) > -1);
+ let exactFound = groupOptions.findIndex(groupType => groupType.toUpperCase() === this._searchTerm.toUpperCase()) > -1;
+
+ let options = groupOptions.map(groupType => {
+ return <div key={groupType} className="linkEditor-option"
+ onClick={() => { this.props.setGroup(this.props.groupId, groupType); this.setGroupType(groupType); this.setSearchTerm(""); }}>{groupType}</div>;
+ });
+
+ // if search term does not already exist as a group type, give option to create new group type
+ if (!exactFound && this._searchTerm !== "") {
+ options.push(<div key={""} className="linkEditor-option"
+ onClick={() => { this.createGroup(this._searchTerm); this.setGroupType(this._searchTerm); this.setSearchTerm(""); }}>Define new "{this._searchTerm}" relationship</div>);
+ }
+
+ return options;
+ }
+
+ render() {
+ return (
+ <div className="linkEditor-dropdown">
+ <input type="text" value={this._groupType} placeholder="Search for a group or create a new group"
+ onChange={e => { this.setSearchTerm(e.target.value); this.setGroupType(e.target.value); }}></input>
+ <div className="linkEditor-options-wrapper">
+ {this.renderOptions()}
+ </div>
+ </div>
+ );
+ }
+}
+
+
+interface LinkMetadataEditorProps {
+ groupType: string;
+ mdDoc: Doc;
+ mdKey: string;
+ mdValue: string;
+}
@observer
-export class LinkEditor extends React.Component<Props> {
+class LinkMetadataEditor extends React.Component<LinkMetadataEditorProps> {
+ @observable private _key: string = this.props.mdKey;
+ @observable private _value: string = this.props.mdValue;
+ @observable private _keyError: boolean = false;
- @observable private _nameInput: string = StrCast(this.props.linkDoc.title);
- @observable private _descriptionInput: string = StrCast(this.props.linkDoc.linkDescription);
+ @action
+ setMetadataKey = (value: string): void => {
+ let groupMdKeys = new Array(...LinkManager.Instance.groupMetadataKeys.get(this.props.groupType)!);
+ // don't allow user to create existing key
+ let newIndex = groupMdKeys.findIndex(key => key.toUpperCase() === value.toUpperCase());
+ if (newIndex > -1) {
+ this._keyError = true;
+ this._key = value;
+ return;
+ } else {
+ this._keyError = false;
+ }
- onSaveButtonPressed = (e: React.PointerEvent): void => {
- e.stopPropagation();
+ // set new value for key
+ let currIndex = groupMdKeys.findIndex(key => key.toUpperCase() === this._key.toUpperCase());
+ if (currIndex === -1) console.error("LinkMetadataEditor: key was not found");
+ groupMdKeys[currIndex] = value;
- let linkDoc = this.props.linkDoc.proto ? this.props.linkDoc.proto : this.props.linkDoc;
- linkDoc.title = this._nameInput;
- linkDoc.linkDescription = this._descriptionInput;
+ this._key = value;
+ LinkManager.Instance.groupMetadataKeys.set(this.props.groupType, groupMdKeys);
+ }
- this.props.showLinks();
+ @action
+ setMetadataValue = (value: string): void => {
+ if (!this._keyError) {
+ this._value = value;
+ this.props.mdDoc[this._key] = value;
+ }
}
+ @action
+ removeMetadata = (): void => {
+ let groupMdKeys = new Array(...LinkManager.Instance.groupMetadataKeys.get(this.props.groupType)!);
+ let index = groupMdKeys.findIndex(key => key.toUpperCase() === this._key.toUpperCase());
+ if (index === -1) console.error("LinkMetadataEditor: key was not found");
+ groupMdKeys.splice(index, 1);
- render() {
+ LinkManager.Instance.groupMetadataKeys.set(this.props.groupType, groupMdKeys);
+ this._key = "";
+ }
+ render() {
return (
- <div className="edit-container">
- <input onChange={this.onNameChanged} className="name-input" type="text" value={this._nameInput} placeholder="Name . . ."></input>
- <textarea onChange={this.onDescriptionChanged} className="description-input" value={this._descriptionInput} placeholder="Description . . ."></textarea>
- <div className="save-button" onPointerDown={this.onSaveButtonPressed}>SAVE</div>
+ <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>
</div>
-
);
}
+}
+
+
+interface LinkEditorProps {
+ sourceDoc: Doc;
+ linkDoc: Doc;
+ showLinks: () => void;
+}
+@observer
+export class LinkEditor extends React.Component<LinkEditorProps> {
+
+ // map of temporary group id to the corresponding group doc
+ @observable private _groups: Map<string, Doc> = new Map();
+
+ constructor(props: LinkEditorProps) {
+ super(props);
+
+ let groups = new Map<string, Doc>();
+ let groupList = LinkManager.Instance.getAnchorGroups(props.linkDoc, props.sourceDoc);
+ groupList.forEach(groupDoc => {
+ let id = Utils.GenerateGuid();
+ groups.set(id, groupDoc);
+ });
+ this._groups = groups;
+ }
@action
- onNameChanged = (e: React.ChangeEvent<HTMLInputElement>) => {
- this._nameInput = e.target.value;
+ addGroup = (): void => {
+ // new group only gets added if there is not already a group with type "new group"
+ let index = Array.from(this._groups.values()).findIndex(groupDoc => {
+ return groupDoc.type === "New Group";
+ });
+ if (index > -1) return;
+
+ // create new metadata document for group
+ let mdDoc = Docs.TextDocument();
+ mdDoc.proto!.anchor1 = this.props.sourceDoc.title;
+ mdDoc.proto!.anchor2 = LinkManager.Instance.findOppositeAnchor(this.props.linkDoc, this.props.sourceDoc).title;
+
+ // create new group document
+ let groupDoc = Docs.TextDocument();
+ groupDoc.proto!.type = "New Group";
+ groupDoc.proto!.metadata = mdDoc;
+
+ this._groups.set(Utils.GenerateGuid(), groupDoc);
+
+ let linkDoc = this.props.linkDoc.proto ? this.props.linkDoc.proto : this.props.linkDoc;
+ LinkManager.Instance.setAnchorGroups(linkDoc, this.props.sourceDoc, Array.from(this._groups.values()));
}
@action
- onDescriptionChanged = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
- this._descriptionInput = e.target.value;
+ setGroupType = (groupId: string, groupType: string): void => {
+ let groupDoc = this._groups.get(groupId);
+ if (groupDoc) {
+ groupDoc.proto!.type = groupType;
+ this._groups.set(groupId, groupDoc);
+ LinkManager.Instance.setAnchorGroups(this.props.linkDoc, this.props.sourceDoc, Array.from(this._groups.values()));
+ }
+ }
+
+ removeGroupFromLink = (groupId: string, groupType: string): void => {
+ let groupDoc = this._groups.get(groupId);
+ if (!groupDoc) console.error("LinkEditor: group not found");
+ LinkManager.Instance.removeGroupFromAnchor(this.props.linkDoc, this.props.sourceDoc, groupType);
+ this._groups.delete(groupId);
+ }
+
+ deleteGroup = (groupId: string, groupType: string): void => {
+ let groupDoc = this._groups.get(groupId);
+ if (!groupDoc) console.error("LinkEditor: group not found");
+ LinkManager.Instance.deleteGroup(groupType);
+ this._groups.delete(groupId);
+ }
+
+ copyGroup = (groupId: string, groupType: string): void => {
+ let sourceGroupDoc = this._groups.get(groupId);
+ let sourceMdDoc = Cast(sourceGroupDoc!.metadata, Doc, new Doc);
+ let destDoc = LinkManager.Instance.findOppositeAnchor(this.props.linkDoc, this.props.sourceDoc);
+ let destGroupList = LinkManager.Instance.getAnchorGroups(this.props.linkDoc, destDoc);
+ let keys = LinkManager.Instance.groupMetadataKeys.get(groupType);
+
+ // create new metadata doc with copied kvp
+ let destMdDoc = Docs.TextDocument();
+ destMdDoc.proto!.anchor1 = StrCast(sourceMdDoc.anchor2);
+ destMdDoc.proto!.anchor2 = StrCast(sourceMdDoc.anchor1);
+ if (keys) {
+ keys.forEach(key => {
+ let val = sourceMdDoc[key] === undefined ? "" : StrCast(sourceMdDoc[key]);
+ destMdDoc[key] = val;
+ });
+ }
+
+ // create new group doc with new metadata doc
+ let destGroupDoc = Docs.TextDocument();
+ destGroupDoc.proto!.type = groupType;
+ destGroupDoc.proto!.metadata = destMdDoc;
+
+ // if group does not already exist on opposite anchor, create group doc
+ let index = destGroupList.findIndex(groupDoc => { StrCast(groupDoc.type).toUpperCase() === groupType.toUpperCase(); });
+ if (index > -1) {
+ destGroupList[index] = destGroupDoc;
+ } else {
+ destGroupList.push(destGroupDoc);
+ }
+
+ LinkManager.Instance.setAnchorGroups(this.props.linkDoc, destDoc, destGroupList);
+ }
+
+ viewGroupAsTable = (groupId: string, groupType: string): JSX.Element => {
+ let keys = LinkManager.Instance.groupMetadataKeys.get(groupType);
+ let groupDoc = this._groups.get(groupId);
+ if (keys && groupDoc) {
+ let docs: Doc[] = LinkManager.Instance.findAllMetadataDocsInGroup(groupType);
+ let createTable = action(() => Docs.SchemaDocument(["anchor1", "anchor2", ...keys!], docs, { width: 200, height: 200, title: groupType + " table" }));
+ let ref = React.createRef<HTMLDivElement>();
+ return <div className="linkEditor-groupOpts" ref={ref}><button onPointerDown={SetupDrag(ref, createTable)}><FontAwesomeIcon icon="table" size="sm" /></button></div>;
+ } else {
+ return <button className="linkEditor-groupOpts" disabled><FontAwesomeIcon icon="table" size="sm" /></button>;
+ }
+ }
+
+ renderGroup = (groupId: string, groupDoc: Doc): JSX.Element => {
+ let type = StrCast(groupDoc.type);
+ if ((type && LinkManager.Instance.groupMetadataKeys.get(type)) || type === "New Group") {
+ return (
+ <div key={groupId} className="linkEditor-group">
+ <div className="linkEditor-group-row">
+ <p className="linkEditor-group-row-label">type:</p>
+ <GroupTypesDropdown groupId={groupId} groupType={StrCast(groupDoc.proto!.type)} setGroup={this.setGroupType} />
+ </div>
+ {this.renderMetadata(groupId)}
+ <div className="linkEditor-group-buttons">
+ {groupDoc.type === "New Group" ? <button className="linkEditor-groupOpts" disabled={true} title="Add KVP">+</button> :
+ <button className="linkEditor-groupOpts" onClick={() => this.addMetadata(StrCast(groupDoc.proto!.type))} title="Add KVP">+</button>}
+ <button className="linkEditor-groupOpts" onClick={() => this.copyGroup(groupId, type)} title="Copy group to opposite anchor">↔</button>
+ <button className="linkEditor-groupOpts" onClick={() => this.removeGroupFromLink(groupId, type)} title="Remove group from link">x</button>
+ <button className="linkEditor-groupOpts" onClick={() => this.deleteGroup(groupId, type)} title="Delete group">xx</button>
+ {this.viewGroupAsTable(groupId, type)}
+ </div>
+ </div>
+ );
+ } else {
+ return <></>;
+ }
+ }
+
+
+ @action
+ addMetadata = (groupType: string): void => {
+ let mdKeys = LinkManager.Instance.groupMetadataKeys.get(groupType);
+ if (mdKeys) {
+ // only add "new key" if there is no other key with value "new key"; prevents spamming
+ if (mdKeys.indexOf("new key") === -1) mdKeys.push("new key");
+ } else {
+ mdKeys = ["new key"];
+ }
+ LinkManager.Instance.groupMetadataKeys.set(groupType, mdKeys);
+ }
+
+ renderMetadata = (groupId: string): JSX.Element[] => {
+ let metadata: Array<JSX.Element> = [];
+ let groupDoc = this._groups.get(groupId);
+ if (groupDoc) {
+ let mdDoc = Cast(groupDoc.proto!.metadata, Doc, new Doc);
+ let groupType = StrCast(groupDoc.proto!.type);
+ let groupMdKeys = LinkManager.Instance.groupMetadataKeys.get(groupType);
+ if (groupMdKeys) {
+ groupMdKeys.forEach((key, index) => {
+ metadata.push(
+ <LinkMetadataEditor key={"mded-" + index} groupType={groupType} mdDoc={mdDoc} mdKey={key} mdValue={(mdDoc[key] === undefined) ? "" : StrCast(mdDoc[key])} />
+ );
+ });
+ }
+ }
+ return metadata;
+ }
+
+ render() {
+ let destination = LinkManager.Instance.findOppositeAnchor(this.props.linkDoc, this.props.sourceDoc);
+
+ let groups: Array<JSX.Element> = [];
+ this._groups.forEach((groupDoc, groupId) => {
+ groups.push(this.renderGroup(groupId, groupDoc));
+ });
+
+ return (
+ <div className="linkEditor">
+ <button className="linkEditor-back" onPointerDown={() => this.props.showLinks()}><FontAwesomeIcon icon="arrow-left" size="sm" /></button>
+ <p className="linkEditor-linkedTo">editing link to: <b>{destination.proto!.title}</b></p>
+ <div className="linkEditor-groupsLabel">
+ <b>Relationships:</b>
+ <button onClick={() => this.addGroup()} title="Add Group">+</button>
+ </div>
+ {groups.length > 0 ? groups : <div className="linkEditor-group">There are currently no relationships associated with this link.</div>}
+ </div>
+
+ );
}
} \ No newline at end of file
diff --git a/src/client/views/nodes/LinkMenu.scss b/src/client/views/nodes/LinkMenu.scss
index dedcce6ef..09a830c9e 100644
--- a/src/client/views/nodes/LinkMenu.scss
+++ b/src/client/views/nodes/LinkMenu.scss
@@ -1,21 +1,14 @@
-#linkMenu-container {
+@import "../globalCssVariables";
+
+.linkMenu {
width: 100%;
height: auto;
- display: flex;
- flex-direction: column;
}
-#linkMenu-searchBar {
- width: 100%;
- padding: 5px;
- margin-bottom: 10px;
- font-size: 12px;
- border: 1px solid #bababa;
+.linkMenu-list {
+ max-height: 200px;
+ overflow-y: scroll;
}
-#linkMenu-list {
- margin-top: 5px;
- width: 100%;
- height: 100px;
- overflow-y: scroll;
-} \ No newline at end of file
+
+
diff --git a/src/client/views/nodes/LinkMenu.tsx b/src/client/views/nodes/LinkMenu.tsx
index 3f09d6214..aef56df67 100644
--- a/src/client/views/nodes/LinkMenu.tsx
+++ b/src/client/views/nodes/LinkMenu.tsx
@@ -6,8 +6,8 @@ import { LinkEditor } from "./LinkEditor";
import './LinkMenu.scss';
import React = require("react");
import { Doc, DocListCast } from "../../../new_fields/Doc";
-import { Cast, FieldValue, StrCast } from "../../../new_fields/Types";
import { Id } from "../../../new_fields/FieldSymbols";
+import { LinkManager } from "../../util/LinkManager";
interface Props {
docView: DocumentView;
@@ -19,34 +19,49 @@ export class LinkMenu extends React.Component<Props> {
@observable private _editingLink?: Doc;
- renderLinkItems(links: Doc[], key: string, type: string) {
- return links.map(link => {
- let doc = FieldValue(Cast(link[key], Doc));
- if (doc) {
- return <LinkBox key={doc[Id]} linkDoc={link} linkName={StrCast(link.title)} pairedDoc={doc} showEditor={action(() => this._editingLink = link)} type={type} />;
- }
+ renderGroup = (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 <LinkBox 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="link-menu-group">
+ <p className="link-menu-group-name">{groupType}:</p>
+ <div className="link-menu-group-wrapper">
+ {this.renderGroup(group, groupType)}
+ </div>
+ </div>
+ );
+ });
+
+ // if source doc has no links push message
+ if (linkItems.length === 0) linkItems.push(<p key="">No links have been created yet. Drag the linking button onto another document to create a link.</p>);
+
+ return linkItems;
+ }
+
render() {
- //get list of links from document
- let linkFrom = DocListCast(this.props.docView.props.Document.linkedFromDocs);
- let linkTo = DocListCast(this.props.docView.props.Document.linkedToDocs);
+ let sourceDoc = this.props.docView.props.Document;
+ let groups: Map<string, Doc[]> = LinkManager.Instance.findRelatedGroupedLinks(sourceDoc);
if (this._editingLink === undefined) {
return (
- <div id="linkMenu-container">
+ <div className="linkMenu">
{/* <input id="linkMenu-searchBar" type="text" placeholder="Search..."></input> */}
- <div id="linkMenu-list">
- {this.renderLinkItems(linkTo, "linkedTo", "Destination: ")}
- {this.renderLinkItems(linkFrom, "linkedFrom", "Source: ")}
+ <div className="linkMenu-list">
+ {this.renderAllGroups(groups)}
</div>
</div>
);
} else {
return (
- <LinkEditor linkDoc={this._editingLink} showLinks={action(() => this._editingLink = undefined)}></LinkEditor>
+ <LinkEditor sourceDoc={this.props.docView.props.Document} linkDoc={this._editingLink} showLinks={action(() => this._editingLink = undefined)}></LinkEditor>
);
}
-
}
} \ No newline at end of file