diff options
Diffstat (limited to 'src/client/views/nodes/LinkEditor.tsx')
-rw-r--r-- | src/client/views/nodes/LinkEditor.tsx | 342 |
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 |