aboutsummaryrefslogtreecommitdiff
path: root/src/client/views/nodes/LinkEditor.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'src/client/views/nodes/LinkEditor.tsx')
-rw-r--r--src/client/views/nodes/LinkEditor.tsx342
1 files changed, 315 insertions, 27 deletions
diff --git a/src/client/views/nodes/LinkEditor.tsx b/src/client/views/nodes/LinkEditor.tsx
index 71a423338..29ead7388 100644
--- a/src/client/views/nodes/LinkEditor.tsx
+++ b/src/client/views/nodes/LinkEditor.tsx
@@ -1,57 +1,345 @@
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, faTrash } from '@fortawesome/free-solid-svg-icons';
+import { library } from "@fortawesome/fontawesome-svg-core";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { SetupDrag } from "../../util/DragManager";
-interface Props {
- linkDoc: Doc;
- showLinks: () => void;
+library.add(faArrowLeft, faEllipsisV, faTable, faTrash);
+
+
+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;
+ deleteLink = (): void => {
+ let index = LinkManager.Instance.allLinks.indexOf(this.props.linkDoc);
+ LinkManager.Instance.allLinks.splice(index, 1);
+ this.props.showLinks();
}
@action
- onDescriptionChanged = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
- this._descriptionInput = 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
+ 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>
+ <div className="linkEditor-info">
+ <p className="linkEditor-linkedTo">editing link to: <b>{destination.proto!.title}</b></p>
+ <button className="linkEditor-delete" onPointerDown={() => this.deleteLink()} title="Delete link"><FontAwesomeIcon icon="trash" size="sm" /></button>
+ </div>
+ <div className="linkEditor-groupsLabel">
+ <b>Relationships:</b>
+ <button onClick={() => this.addGroup()} title="Add Group">+</button>
+ </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