diff options
Diffstat (limited to 'src/client/views/collections/CollectionTreeView.tsx')
-rw-r--r-- | src/client/views/collections/CollectionTreeView.tsx | 308 |
1 files changed, 194 insertions, 114 deletions
diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx index 8b06d9ac4..6acef434e 100644 --- a/src/client/views/collections/CollectionTreeView.tsx +++ b/src/client/views/collections/CollectionTreeView.tsx @@ -1,20 +1,32 @@ +import { IconProp, library } from '@fortawesome/fontawesome-svg-core'; +import { faCaretDown, faCaretRight, faTrashAlt, faAngleRight } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { action, observable, trace } from "mobx"; import { observer } from "mobx-react"; -import { CollectionViewBase } from "./CollectionViewBase"; -import { Document } from "../../../fields/Document"; -import { KeyStore } from "../../../fields/KeyStore"; -import { ListField } from "../../../fields/ListField"; -import React = require("react") -import { TextField } from "../../../fields/TextField"; -import { observable, action } from "mobx"; -import "./CollectionTreeView.scss"; +import { DragManager, SetupDrag, dropActionType } from "../../util/DragManager"; import { EditableView } from "../EditableView"; -import { setupDrag } from "../../util/DragManager"; -import { FieldWaiting } from "../../../fields/Field"; -import { COLLECTION_BORDER_WIDTH } from "./CollectionView"; +import { CollectionSubView } from "./CollectionSubView"; +import "./CollectionTreeView.scss"; +import React = require("react"); +import { Document, listSpec } from '../../../new_fields/Schema'; +import { Cast, StrCast, BoolCast, FieldValue } from '../../../new_fields/Types'; +import { Doc, DocListCast } from '../../../new_fields/Doc'; +import { Id } from '../../../new_fields/RefField'; +import { ContextMenu } from '../ContextMenu'; +import { undoBatch } from '../../util/UndoManager'; +import { Main } from '../Main'; +import { CurrentUserUtils } from '../../../server/authentication/models/current_user_utils'; +import { CollectionDockingView } from './CollectionDockingView'; +import { DocumentManager } from '../../util/DocumentManager'; +import { List } from '../../../new_fields/List'; +import { Docs } from '../../documents/Documents'; + export interface TreeViewProps { - document: Document; - deleteDoc: (doc: Document) => void; + document: Doc; + deleteDoc: (doc: Doc) => void; + moveDocument: DragManager.MoveFunction; + dropAction: "alias" | "copy" | undefined; } export enum BulletType { @@ -23,151 +35,219 @@ export enum BulletType { List } +library.add(faTrashAlt); +library.add(faAngleRight); +library.add(faCaretDown); +library.add(faCaretRight); + @observer /** * Component that takes in a document prop and a boolean whether it's collapsed or not. */ class TreeView extends React.Component<TreeViewProps> { - @observable - collapsed: boolean = false; + @observable _collapsed: boolean = true; + + @undoBatch delete = () => this.props.deleteDoc(this.props.document); - delete = () => { - this.props.deleteDoc(this.props.document); + @undoBatch openRight = async () => { + if (this.props.document.dockingConfig) { + Main.Instance.openWorkspace(this.props.document); + } else { + CollectionDockingView.Instance.AddRightSplit(this.props.document); + } } + get children() { + return Cast(this.props.document.data, listSpec(Doc), []); // bcz: needed? .filter(doc => FieldValue(doc)); + } + + onPointerDown = (e: React.PointerEvent) => { + e.stopPropagation(); + } @action - remove = (document: Document) => { - var children = this.props.document.GetT<ListField<Document>>(KeyStore.Data, ListField); - if (children && children !== FieldWaiting) { - children.Data.splice(children.Data.indexOf(document), 1); + remove = (document: Document, key: string) => { + let children = Cast(this.props.document[key], listSpec(Doc), []); + if (children) { + children.splice(children.indexOf(document), 1); } } - renderBullet(type: BulletType) { - let onClicked = action(() => this.collapsed = !this.collapsed); + @action + move: DragManager.MoveFunction = (document, target, addDoc) => { + if (this.props.document === target) { + return true; + } + //TODO This should check if it was removed + this.remove(document, "data"); + return addDoc(document); + } + renderBullet(type: BulletType) { + let onClicked = action(() => this._collapsed = !this._collapsed); + let bullet: IconProp | undefined = undefined; switch (type) { - case BulletType.Collapsed: - return <div className="bullet" onClick={onClicked}>▶</div> - case BulletType.Collapsible: - return <div className="bullet" onClick={onClicked}>▼</div> - case BulletType.List: - return <div className="bullet">—</div> + case BulletType.Collapsed: bullet = "caret-right"; break; + case BulletType.Collapsible: bullet = "caret-down"; break; } + return <div className="bullet" onClick={onClicked}>{bullet ? <FontAwesomeIcon icon={bullet} /> : ""} </div>; } + @action + onMouseEnter = () => { + this._isOver = true; + } + @observable _isOver: boolean = false; + @action + onMouseLeave = () => { + this._isOver = false; + } /** * Renders the EditableView title element for placement into the tree. */ renderTitle() { - let title = this.props.document.GetT<TextField>(KeyStore.Title, TextField); - - // if the title hasn't loaded, immediately return the div - if (!title || title === "<Waiting>") { - return <div key={this.props.document.Id}></div>; - } - - return <div className="docContainer"> <EditableView contents={title.Data} - height={36} GetValue={() => { - let title = this.props.document.GetT<TextField>(KeyStore.Title, TextField); - if (title && title !== "<Waiting>") - return title.Data; - return ""; - }} SetValue={(value: string) => { - this.props.document.SetData(KeyStore.Title, value, TextField); - return true; - }} /> - <div className="delete-button" onClick={this.delete}>x</div> - </div > + let reference = React.createRef<HTMLDivElement>(); + let onItemDown = SetupDrag(reference, () => this.props.document, this.props.moveDocument, this.props.dropAction); + let editableView = (titleString: string) => + (<EditableView + oneLine={!this._isOver ? true : false} + display={"block"} + contents={titleString} + height={36} + GetValue={() => StrCast(this.props.document.title)} + SetValue={(value: string) => { + let target = this.props.document.proto ? this.props.document.proto : this.props.document; + target.title = value; + return true; + }} + />); + let dataDocs = Cast(CollectionDockingView.Instance.props.Document.data, listSpec(Doc), []); + let openRight = dataDocs && dataDocs.indexOf(this.props.document) !== -1 ? (null) : ( + <div className="treeViewItem-openRight" onPointerDown={this.onPointerDown} onClick={this.openRight}> + <FontAwesomeIcon icon="angle-right" size="lg" /> + <FontAwesomeIcon icon="angle-right" size="lg" /> + </div>); + return ( + <div className="docContainer" ref={reference} onPointerDown={onItemDown} onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave} + style={{ background: BoolCast(this.props.document.protoBrush, false) ? "#06123232" : BoolCast(this.props.document.libraryBrush, false) ? "#06121212" : "0" }} + onPointerEnter={this.onPointerEnter} onPointerLeave={this.onPointerLeave}> + {editableView(StrCast(this.props.document.title))} + {openRight} + {/* {<div className="delete-button" onClick={this.delete}><FontAwesomeIcon icon="trash-alt" size="xs" /></div>} */} + </div >); } - render() { - var children = this.props.document.GetT<ListField<Document>>(KeyStore.Data, ListField); - - let reference = React.createRef<HTMLDivElement>(); - let onItemDown = setupDrag(reference, () => this.props.document); - let titleElement = this.renderTitle(); - - // check if this document is a collection - if (children && children !== FieldWaiting) { - let subView; - - // if uncollapsed, then add the children elements - if (!this.collapsed) { - // render all children elements - let childrenElement = (children.Data.map(value => - <TreeView document={value} deleteDoc={this.remove} />) - ) - subView = - <li key={this.props.document.Id} > - {this.renderBullet(BulletType.Collapsible)} - {titleElement} - <ul key={this.props.document.Id}> - {childrenElement} - </ul> - </li> - } else { - subView = <li key={this.props.document.Id}> - {this.renderBullet(BulletType.Collapsed)} - {titleElement} - </li> + onWorkspaceContextMenu = (e: React.MouseEvent): void => { + if (!e.isPropagationStopped() && this.props.document[Id] !== CurrentUserUtils.MainDocId) { // need to test this because GoldenLayout causes a parallel hierarchy in the React DOM for its children and the main document view7 + ContextMenu.Instance.addItem({ description: "Open as Workspace", event: undoBatch(() => Main.Instance.openWorkspace(this.props.document)) }); + ContextMenu.Instance.addItem({ description: "Open Right", event: () => CollectionDockingView.Instance.AddRightSplit(this.props.document) }); + ContextMenu.Instance.addItem({ + description: "Open Fields", event: () => CollectionDockingView.Instance.AddRightSplit(Docs.KVPDocument(this.props.document, + { title: this.props.document.title + ".kvp", width: 300, height: 300 })) + }); + if (DocumentManager.Instance.getDocumentViews(this.props.document).length) { + ContextMenu.Instance.addItem({ description: "Focus", event: () => DocumentManager.Instance.getDocumentViews(this.props.document).map(view => view.props.focus(this.props.document)) }); } - - return <div className="treeViewItem-container" onPointerDown={onItemDown} ref={reference}> - {subView} - </div> + ContextMenu.Instance.addItem({ + description: "Delete", event: undoBatch(() => { + this.props.deleteDoc(this.props.document); + }) + }); + ContextMenu.Instance.displayMenu(e.pageX - 15, e.pageY - 15); + e.stopPropagation(); } + } + + onPointerEnter = (e: React.PointerEvent): void => { this.props.document.libraryBrush = true; }; + onPointerLeave = (e: React.PointerEvent): void => { this.props.document.libraryBrush = false; }; - // otherwise this is a normal leaf node - else { - return <li key={this.props.document.Id}> - {this.renderBullet(BulletType.List)} - {titleElement} - </li>; + render() { + let bulletType = BulletType.List; + let contentElement: (JSX.Element | null)[] = []; + let keys = Array.from(Object.keys(this.props.document)); + if (this.props.document.proto instanceof Doc) { + keys.push(...Array.from(Object.keys(this.props.document.proto))); + while (keys.indexOf("proto") !== -1) keys.splice(keys.indexOf("proto"), 1); } + keys.map(key => { + let docList = DocListCast(this.props.document[key]); + let doc = Cast(this.props.document[key], Doc); + if (doc instanceof Doc || docList.length) { + if (!this._collapsed) { + bulletType = BulletType.Collapsible; + let spacing = (key === "data") ? 0 : -10; + contentElement.push(<ul key={key + "more"}> + {(key === "data") ? (null) : + <span className="collectionTreeView-keyHeader" style={{ display: "block", marginTop: "7px" }} key={key}>{key}</span>} + <div style={{ display: "block", marginTop: `${spacing}px` }}> + {TreeView.GetChildElements(doc instanceof Doc ? [doc] : docList, key !== "data", (doc: Doc) => this.remove(doc, key), this.move, this.props.dropAction)} + </div> + </ul >); + } else { + bulletType = BulletType.Collapsed; + } + } + }); + return <div className="treeViewItem-container" + onContextMenu={this.onWorkspaceContextMenu}> + <li className="collection-child"> + {this.renderBullet(bulletType)} + {this.renderTitle()} + {contentElement} + </li> + </div>; + } + public static GetChildElements(docs: Doc[], allowMinimized: boolean, remove: ((doc: Doc) => void), move: DragManager.MoveFunction, dropAction: dropActionType) { + return docs.filter(child => !child.excludeFromLibrary && (allowMinimized || !child.isMinimized)).map(child => + <TreeView document={child} key={child[Id]} deleteDoc={remove} moveDocument={move} dropAction={dropAction} />); } } - @observer -export class CollectionTreeView extends CollectionViewBase { - +export class CollectionTreeView extends CollectionSubView(Document) { @action remove = (document: Document) => { - var children = this.props.Document.GetT<ListField<Document>>(KeyStore.Data, ListField); - if (children && children !== FieldWaiting) { - children.Data.splice(children.Data.indexOf(document), 1); + let children = Cast(this.props.Document.data, listSpec(Doc), []); + if (children) { + children.splice(children.indexOf(document), 1); + } + } + onContextMenu = (e: React.MouseEvent): void => { + if (!e.isPropagationStopped() && this.props.Document[Id] !== CurrentUserUtils.MainDocId) { // need to test this because GoldenLayout causes a parallel hierarchy in the React DOM for its children and the main document view7 + ContextMenu.Instance.addItem({ description: "Create Workspace", event: undoBatch(() => Main.Instance.createNewWorkspace()) }); + } + if (!ContextMenu.Instance.getItems().some(item => item.description === "Delete")) { + ContextMenu.Instance.addItem({ description: "Delete", event: undoBatch(() => this.remove(this.props.Document)) }); } } - render() { - let titleStr = ""; - let title = this.props.Document.GetT<TextField>(KeyStore.Title, TextField); - if (title && title !== FieldWaiting) { - titleStr = title.Data; + let dropAction = StrCast(this.props.Document.dropAction, "alias") as dropActionType; + if (!this.children) { + return (null); } - - var children = this.props.Document.GetT<ListField<Document>>(KeyStore.Data, ListField); - let childrenElement = !children || children === FieldWaiting ? (null) : - (children.Data.map(value => - <TreeView document={value} key={value.Id} deleteDoc={this.remove} />) - ) + let childElements = TreeView.GetChildElements(this.children, false, this.remove, this.props.moveDocument, dropAction); return ( - <div id="body" className="collectionTreeView-dropTarget" onDrop={(e: React.DragEvent) => this.onDrop(e, {})} ref={this.createDropTarget} style={{ borderWidth: `${COLLECTION_BORDER_WIDTH}px` }}> - <h3> - <EditableView contents={titleStr} - height={72} GetValue={() => { - return this.props.Document.Title; - }} SetValue={(value: string) => { - this.props.Document.SetData(KeyStore.Title, value, TextField); + <div id="body" className="collectionTreeView-dropTarget" + style={{ borderRadius: "inherit" }} + onContextMenu={this.onContextMenu} + onWheel={(e: React.WheelEvent) => e.stopPropagation()} + onDrop={(e: React.DragEvent) => this.onDrop(e, {})} ref={this.createDropTarget}> + <div className="coll-title"> + <EditableView + contents={this.props.Document.title} + display={"inline"} + height={72} + GetValue={() => StrCast(this.props.Document.title)} + SetValue={(value: string) => { + let target = this.props.Document.proto ? this.props.Document.proto : this.props.Document; + target.title = value; return true; }} /> - </h3> + </div> <ul className="no-indent"> - {childrenElement} + {childElements} </ul> </div > ); |