diff options
Diffstat (limited to 'src/client/views/Main.tsx')
-rw-r--r-- | src/client/views/Main.tsx | 439 |
1 files changed, 353 insertions, 86 deletions
diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx index 6e5b84075..6f66f8f38 100644 --- a/src/client/views/Main.tsx +++ b/src/client/views/Main.tsx @@ -1,4 +1,4 @@ -import { action, configure } from 'mobx'; +import { action, configure, observable, runInAction, trace, computed, reaction } from 'mobx'; import "normalize.css"; import * as React from 'react'; import * as ReactDOM from 'react-dom'; @@ -7,106 +7,373 @@ import { KeyStore } from '../../fields/KeyStore'; import "./Main.scss"; import { MessageStore } from '../../server/Message'; import { Utils } from '../../Utils'; +import * as request from 'request' +import * as rp from 'request-promise' import { Documents } from '../documents/Documents'; import { Server } from '../Server'; import { setupDrag } from '../util/DragManager'; import { Transform } from '../util/Transform'; import { UndoManager } from '../util/UndoManager'; +import { WorkspacesMenu } from '../../server/authentication/controllers/WorkspacesMenu'; import { CollectionDockingView } from './collections/CollectionDockingView'; import { ContextMenu } from './ContextMenu'; import { DocumentDecorations } from './DocumentDecorations'; import { DocumentView } from './nodes/DocumentView'; import "./Main.scss"; +import { observer } from 'mobx-react'; import { InkingControl } from './InkingControl'; -import { NumberField } from '../../fields/NumberField'; -import { TextField } from '../../fields/TextField'; +import { RouteStore } from '../../server/RouteStore'; +import { library, IconName } from '@fortawesome/fontawesome-svg-core'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faFont } from '@fortawesome/free-solid-svg-icons'; +import { faImage } from '@fortawesome/free-solid-svg-icons'; +import { faFilePdf } from '@fortawesome/free-solid-svg-icons'; +import { faObjectGroup } from '@fortawesome/free-solid-svg-icons'; +import { faTable } from '@fortawesome/free-solid-svg-icons'; +import { faGlobeAsia } from '@fortawesome/free-solid-svg-icons'; +import { faUndoAlt } from '@fortawesome/free-solid-svg-icons'; +import { faRedoAlt } from '@fortawesome/free-solid-svg-icons'; +import { faPenNib } from '@fortawesome/free-solid-svg-icons'; +import { faFilm } from '@fortawesome/free-solid-svg-icons'; +import { faMusic } from '@fortawesome/free-solid-svg-icons'; +import { faTree } from '@fortawesome/free-solid-svg-icons'; +import Measure from 'react-measure'; +import { DashUserModel } from '../../server/authentication/models/user_model'; +import { ServerUtils } from '../../server/ServerUtil'; +import { CurrentUserUtils } from '../../server/authentication/models/current_user_utils'; +import { Field, Opt, FieldWaiting } from '../../fields/Field'; import { ListField } from '../../fields/ListField'; +import { Gateway, Settings } from '../northstar/manager/Gateway'; +import { Catalog, Schema, Attribute, AttributeGroup, AggregateFunction } from '../northstar/model/idea/idea'; +import { ArrayUtil } from '../northstar/utils/ArrayUtil'; +import '../northstar/model/ModelExtensions' +import '../northstar/utils/Extensions' +import { HistogramOperation } from '../northstar/operations/HistogramOperation'; +import { AttributeTransformationModel } from '../northstar/core/attribute/AttributeTransformationModel'; +import { ColumnAttributeModel } from '../northstar/core/attribute/AttributeModel'; +@observer +export class Main extends React.Component { + // dummy initializations keep the compiler happy + @observable private mainfreeform?: Document; + @observable public pwidth: number = 0; + @observable public pheight: number = 0; + private _northstarSchemas: Document[] = []; -configure({ enforceActions: "observed" }); // causes errors to be generated when modifying an observable outside of an action -window.addEventListener("drop", (e) => e.preventDefault(), false) -window.addEventListener("dragover", (e) => e.preventDefault(), false) -document.addEventListener("pointerdown", action(function (e: PointerEvent) { - if (!ContextMenu.Instance.intersects(e.pageX, e.pageY)) { - ContextMenu.Instance.clearItems() - } -}), true) - - -const mainDocId = "mainDoc"; -let mainContainer: Document; -let mainfreeform: Document; -Documents.initProtos(mainDocId, (res?: Document) => { - if (res instanceof Document) { - mainContainer = res; - mainContainer.GetAsync(KeyStore.ActiveFrame, field => mainfreeform = field as Document); - } - else { - mainContainer = Documents.DockDocument(JSON.stringify({ content: [{ type: 'row', content: [] }] }), { title: "main container" }, mainDocId); - - // bcz: strangely, we need a timeout to prevent exceptions/issues initializing GoldenLayout (the rendering engine for Main Container) - setTimeout(() => { - mainfreeform = Documents.FreeformDocument([], { x: 0, y: 400, title: "mini collection" }); - - var dockingLayout = { content: [{ type: 'row', content: [CollectionDockingView.makeDocumentConfig(mainfreeform)] }] }; - mainContainer.SetText(KeyStore.Data, JSON.stringify(dockingLayout)); - mainContainer.Set(KeyStore.ActiveFrame, mainfreeform); - }, 0); - } - - let imgurl = "https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg"; - let pdfurl = "http://www.adobe.com/support/products/enterprise/knowledgecenter/media/c4611_sample_explain.pdf" - let weburl = "https://cs.brown.edu/courses/cs166/"; - let clearDatabase = action(() => Utils.Emit(Server.Socket, MessageStore.DeleteAll, {})) - let addTextNode = action(() => Documents.TextDocument({ width: 200, height: 200, title: "a text note" })) - let addColNode = action(() => Documents.FreeformDocument([], { width: 200, height: 200, title: "a freeform collection" })); - let addSchemaNode = action(() => Documents.SchemaDocument([Documents.TextDocument()], { width: 200, height: 200, title: "a schema collection" })); - let addPDFNode = action(() => Documents.PdfDocument(pdfurl, { width: 200, height: 200, title: "a schema collection" })); - let addImageNode = action(() => Documents.ImageDocument(imgurl, { width: 200, height: 200, title: "an image of a cat" })); - let addWebNode = action(() => Documents.WebDocument(weburl, { width: 200, height: 200, title: "a sample web page" })); - - let addClick = (creator: () => Document) => action(() => - mainfreeform.GetList<Document>(KeyStore.Data, []).push(creator()) - ); - - let imgRef = React.createRef<HTMLDivElement>(); - let pdfRef = React.createRef<HTMLDivElement>(); - let webRef = React.createRef<HTMLDivElement>(); - let textRef = React.createRef<HTMLDivElement>(); - let schemaRef = React.createRef<HTMLDivElement>(); - let colRef = React.createRef<HTMLDivElement>(); - - ReactDOM.render(( - <div style={{ position: "absolute", width: "100%", height: "100%" }}> - <DocumentView Document={mainContainer} - AddDocument={undefined} RemoveDocument={undefined} ScreenToLocalTransform={() => Transform.Identity} - ContentScaling={() => 1} - PanelWidth={() => 0} - PanelHeight={() => 0} + @computed private get mainContainer(): Document | undefined { + let doc = this.userDocument.GetT(KeyStore.ActiveWorkspace, Document); + return doc == FieldWaiting ? undefined : doc; + } + + private set mainContainer(doc: Document | undefined) { + if (doc) { + this.userDocument.Set(KeyStore.ActiveWorkspace, doc); + } + } + + private get userDocument(): Document { + return CurrentUserUtils.UserDocument; + } + + public static Instance: Main; + + constructor(props: Readonly<{}>) { + super(props); + Main.Instance = this; + // causes errors to be generated when modifying an observable outside of an action + configure({ enforceActions: "observed" }); + if (window.location.pathname !== RouteStore.home) { + let pathname = window.location.pathname.split("/"); + if (pathname.length > 1 && pathname[pathname.length - 2] == 'doc') { + CurrentUserUtils.MainDocId = pathname[pathname.length - 1]; + } + }; + + CurrentUserUtils.loadCurrentUser(); + + library.add(faFont); + library.add(faImage); + library.add(faFilePdf); + library.add(faObjectGroup); + library.add(faTable); + library.add(faGlobeAsia); + library.add(faUndoAlt); + library.add(faRedoAlt); + library.add(faPenNib); + library.add(faFilm); + library.add(faMusic); + library.add(faTree); + + this.initEventListeners(); + this.initAuthenticationRouters(); + + this.initializeNorthstar(); + } + + onHistory = () => { + if (window.location.pathname !== RouteStore.home) { + let pathname = window.location.pathname.split("/"); + CurrentUserUtils.MainDocId = pathname[pathname.length - 1]; + Server.GetField(CurrentUserUtils.MainDocId, action((field: Opt<Field>) => { + if (field instanceof Document) { + this.openWorkspace(field, true); + } + })); + } + } + + componentDidMount() { + window.onpopstate = this.onHistory; + } + + componentWillUnmount() { + window.onpopstate = null; + } + + initEventListeners = () => { + // window.addEventListener("pointermove", (e) => this.reportLocation(e)) + window.addEventListener("drop", (e) => e.preventDefault(), false) // drop event handler + window.addEventListener("dragover", (e) => e.preventDefault(), false) // drag event handler + // click interactions for the context menu + document.addEventListener("pointerdown", action(function (e: PointerEvent) { + if (!ContextMenu.Instance.intersects(e.pageX, e.pageY)) { + ContextMenu.Instance.clearItems(); + } + }), true); + } + + initAuthenticationRouters = () => { + // Load the user's active workspace, or create a new one if initial session after signup + if (!CurrentUserUtils.MainDocId) { + this.userDocument.GetTAsync(KeyStore.ActiveWorkspace, Document).then(doc => { + if (doc) { + CurrentUserUtils.MainDocId = doc.Id; + this.openWorkspace(doc); + } else { + this.createNewWorkspace(); + } + }) + } else { + Server.GetField(CurrentUserUtils.MainDocId).then(field => { + if (field instanceof Document) { + this.openWorkspace(field) + } else { + this.createNewWorkspace(CurrentUserUtils.MainDocId); + } + }) + } + } + + @action + createNewWorkspace = (id?: string): void => { + this.userDocument.GetTAsync<ListField<Document>>(KeyStore.Workspaces, ListField).then(action((list: Opt<ListField<Document>>) => { + if (list) { + let freeformDoc = Documents.FreeformDocument([], { x: 0, y: 400, title: "mini collection" }); + var dockingLayout = { content: [{ type: 'row', content: [CollectionDockingView.makeDocumentConfig(freeformDoc)] }] }; + let mainDoc = Documents.DockDocument(JSON.stringify(dockingLayout), { title: `Main Container ${list.Data.length + 1}` }, id); + list.Data.push(mainDoc); + CurrentUserUtils.MainDocId = mainDoc.Id; + // bcz: strangely, we need a timeout to prevent exceptions/issues initializing GoldenLayout (the rendering engine for Main Container) + setTimeout(() => { + this.openWorkspace(mainDoc); + let pendingDocument = Documents.SchemaDocument([], { title: "New Mobile Uploads" }) + mainDoc.Set(KeyStore.OptionalRightCollection, pendingDocument); + }, 0); + } + })); + } + + @action + openWorkspace = (doc: Document, fromHistory = false): void => { + this.mainContainer = doc; + fromHistory || window.history.pushState(null, doc.Title, "/doc/" + doc.Id); + this.userDocument.GetTAsync(KeyStore.OptionalRightCollection, Document).then(col => { + // if there is a pending doc, and it has new data, show it (syip: we use a timeout to prevent collection docking view from being uninitialized) + setTimeout(() => { + if (col) { + col.GetTAsync<ListField<Document>>(KeyStore.Data, ListField, (f: Opt<ListField<Document>>) => { + if (f && f.Data.length > 0) { + CollectionDockingView.Instance.AddRightSplit(col); + } + }) + } + }, 100); + }); + } + + @observable + workspacesShown: boolean = false; + + areWorkspacesShown = () => { + return this.workspacesShown; + } + @action + toggleWorkspaces = () => { + this.workspacesShown = !this.workspacesShown; + } + + screenToLocalTransform = () => Transform.Identity + pwidthFunc = () => this.pwidth; + pheightFunc = () => this.pheight; + focusDocument = (doc: Document) => { } + noScaling = () => 1; + + @computed + get mainContent() { + return !this.mainContainer ? (null) : + <DocumentView Document={this.mainContainer} + AddDocument={undefined} + RemoveDocument={undefined} + ScreenToLocalTransform={this.screenToLocalTransform} + ContentScaling={this.noScaling} + PanelWidth={this.pwidthFunc} + PanelHeight={this.pheightFunc} isTopMost={true} SelectOnLoad={false} - focus={() => { }} + focus={this.focusDocument} ContainingCollectionView={undefined} /> - <DocumentDecorations /> - <ContextMenu /> - <div className="main-buttonDiv" style={{ bottom: '0px' }} ref={imgRef} > - <button onPointerDown={setupDrag(imgRef, addImageNode)} onClick={addClick(addImageNode)}>Add Image</button></div> - <div className="main-buttonDiv" style={{ bottom: '25px' }} ref={webRef} > - <button onPointerDown={setupDrag(webRef, addWebNode)} onClick={addClick(addWebNode)}>Add Web</button></div> - <div className="main-buttonDiv" style={{ bottom: '50px' }} ref={textRef}> - <button onPointerDown={setupDrag(textRef, addTextNode)} onClick={addClick(addTextNode)}>Add Text</button></div> - <div className="main-buttonDiv" style={{ bottom: '75px' }} ref={colRef}> - <button onPointerDown={setupDrag(colRef, addColNode)} onClick={addClick(addColNode)}>Add Collection</button></div> - <div className="main-buttonDiv" style={{ bottom: '100px' }} ref={schemaRef}> - <button onPointerDown={setupDrag(schemaRef, addSchemaNode)} onClick={addClick(addSchemaNode)}>Add Schema</button></div> - <div className="main-buttonDiv" style={{ bottom: '125px' }} > - <button onClick={clearDatabase}>Clear Database</button></div> - <div className="main-buttonDiv" style={{ bottom: '150px' }} ref={pdfRef}> - <button onPointerDown={setupDrag(pdfRef, addPDFNode)} onClick={addClick(addPDFNode)}>Add PDF</button></div> - <button className="main-undoButtons" style={{ bottom: '25px' }} onClick={() => UndoManager.Undo()}>Undo</button> - <button className="main-undoButtons" style={{ bottom: '0px' }} onClick={() => UndoManager.Redo()}>Redo</button> - <InkingControl /> - </div>), - document.getElementById('root')); -}) + } + + /* for the expandable add nodes menu. Not included with the miscbuttons because once it expands it expands the whole div with it, making canvas interactions limited. */ + @computed + get nodesMenu() { + let imgurl = "https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg"; + let pdfurl = "http://www.adobe.com/support/products/enterprise/knowledgecenter/media/c4611_sample_explain.pdf" + let weburl = "https://cs.brown.edu/courses/cs166/"; + let audiourl = "http://techslides.com/demos/samples/sample.mp3"; + let videourl = "http://techslides.com/demos/sample-videos/small.mp4"; + + let addTextNode = action(() => Documents.TextDocument({ width: 200, height: 200, title: "a text note" })) + let addColNode = action(() => Documents.FreeformDocument([], { width: 200, height: 200, title: "a freeform collection" })); + let addSchemaNode = action(() => Documents.SchemaDocument([], { width: 200, height: 200, title: "a schema collection" })); + let addTreeNode = action(() => Documents.TreeDocument(this._northstarSchemas, { width: 250, height: 400, title: "northstar schemas" })); + let addVideoNode = action(() => Documents.VideoDocument(videourl, { width: 200, height: 200, title: "video node" })); + let addPDFNode = action(() => Documents.PdfDocument(pdfurl, { width: 200, height: 200, title: "a schema collection" })); + let addImageNode = action(() => Documents.ImageDocument(imgurl, { width: 200, height: 200, title: "an image of a cat" })); + let addWebNode = action(() => Documents.WebDocument(weburl, { width: 200, height: 200, title: "a sample web page" })); + let addAudioNode = action(() => Documents.AudioDocument(audiourl, { width: 200, height: 200, title: "audio node" })) + + let btns: [React.RefObject<HTMLDivElement>, IconName, string, () => Document][] = [ + [React.createRef<HTMLDivElement>(), "font", "Add Textbox", addTextNode], + [React.createRef<HTMLDivElement>(), "image", "Add Image", addImageNode], + [React.createRef<HTMLDivElement>(), "file-pdf", "Add PDF", addPDFNode], + [React.createRef<HTMLDivElement>(), "film", "Add Video", addVideoNode], + [React.createRef<HTMLDivElement>(), "music", "Add Audio", addAudioNode], + [React.createRef<HTMLDivElement>(), "globe-asia", "Add Web Clipping", addWebNode], + [React.createRef<HTMLDivElement>(), "object-group", "Add Collection", addColNode], + [React.createRef<HTMLDivElement>(), "tree", "Add Tree", addTreeNode], + [React.createRef<HTMLDivElement>(), "table", "Add Schema", addSchemaNode], + ] + + return < div id="add-nodes-menu" > + <input type="checkbox" id="add-menu-toggle" /> + <label htmlFor="add-menu-toggle" title="Add Node"><p>+</p></label> + + <div id="add-options-content"> + <ul id="add-options-list"> + {btns.map(btn => + <li key={btn[1]} ><div ref={btn[0]}> + <button className="round-button add-button" title={btn[2]} onPointerDown={setupDrag(btn[0], btn[3])}> + <FontAwesomeIcon icon={btn[1]} size="sm" /> + </button> + </div></li>)} + </ul> + </div> + </div > + } + + /* @TODO this should really be moved into a moveable toolbar component, but for now let's put it here to meet the deadline */ + @computed + get miscButtons() { + let workspacesRef = React.createRef<HTMLDivElement>(); + let logoutRef = React.createRef<HTMLDivElement>(); + + let clearDatabase = action(() => Utils.Emit(Server.Socket, MessageStore.DeleteAll, {})) + return [ + <button className="clear-db-button" key="clear-db" onClick={clearDatabase}>Clear Database</button>, + <div id="toolbar" key="toolbar"> + <button className="toolbar-button round-button" title="Undo" onClick={() => UndoManager.Undo()}><FontAwesomeIcon icon="undo-alt" size="sm" /></button> + <button className="toolbar-button round-button" title="Redo" onClick={() => UndoManager.Redo()}><FontAwesomeIcon icon="redo-alt" size="sm" /></button> + <button className="toolbar-button round-button" title="Ink" onClick={() => InkingControl.Instance.toggleDisplay()}><FontAwesomeIcon icon="pen-nib" size="sm" /></button> + </div >, + <div className="main-buttonDiv" key="workspaces" style={{ top: '34px', left: '2px', position: 'absolute' }} ref={workspacesRef}> + <button onClick={this.toggleWorkspaces}>Workspaces</button></div>, + <div className="main-buttonDiv" key="logout" style={{ top: '34px', right: '1px', position: 'absolute' }} ref={logoutRef}> + <button onClick={() => request.get(ServerUtils.prepend(RouteStore.logout), () => { })}>Log Out</button></div> + ] + } + + render() { + let workspaceMenu: any = null; + let workspaces = this.userDocument.GetT<ListField<Document>>(KeyStore.Workspaces, ListField); + if (workspaces && workspaces !== FieldWaiting) { + workspaceMenu = <WorkspacesMenu active={this.mainContainer} open={this.openWorkspace} new={this.createNewWorkspace} allWorkspaces={workspaces.Data} + isShown={this.areWorkspacesShown} toggle={this.toggleWorkspaces} /> + } + return ( + <div id="main-div"> + <DocumentDecorations /> + <Measure onResize={(r: any) => runInAction(() => { + this.pwidth = r.entry.width; + this.pheight = r.entry.height; + })}> + {({ measureRef }) => + <div ref={measureRef} id="mainContent-div"> + {this.mainContent} + </div> + } + </Measure> + <ContextMenu /> + {this.nodesMenu} + {this.miscButtons} + {workspaceMenu} + <InkingControl /> + </div> + ); + } + + // --------------- Northstar hooks ------------- / + + @action SetNorthstarCatalog(ctlog: Catalog) { + CurrentUserUtils.NorthstarDBCatalog = ctlog; + if (ctlog && ctlog.schemas) { + this._northstarSchemas = ctlog.schemas.map(schema => { + let schemaDoc = Documents.TreeDocument([], { width: 50, height: 100, title: schema.displayName! }); + let schemaDocuments = schemaDoc.GetList(KeyStore.Data, [] as Document[]); + CurrentUserUtils.GetAllNorthstarColumnAttributes(schema).map(attr => { + Server.GetField(attr.displayName! + ".alias", action((field: Opt<Field>) => { + if (field instanceof Document) { + schemaDocuments.push(field); + } else { + var atmod = new ColumnAttributeModel(attr); + let histoOp = new HistogramOperation(schema!.displayName!, + new AttributeTransformationModel(atmod, AggregateFunction.None), + new AttributeTransformationModel(atmod, AggregateFunction.Count), + new AttributeTransformationModel(atmod, AggregateFunction.Count)); + schemaDocuments.push(Documents.HistogramDocument(histoOp, { width: 200, height: 200, title: attr.displayName! }, undefined, attr.displayName! + ".alias")); + } + })); + }); + return schemaDoc; + }) + } + } + async initializeNorthstar(): Promise<void> { + let envPath = "/assets/env.json"; + const response = await fetch(envPath, { + redirect: "follow", + method: "GET", + credentials: "include" + }); + const env = await response.json(); + Settings.Instance.Update(env); + let cat = Gateway.Instance.ClearCatalog(); + cat.then(async () => this.SetNorthstarCatalog(await Gateway.Instance.GetCatalog())); + } +} +Documents.initProtos().then(() => { + return CurrentUserUtils.loadCurrentUser() +}).then(() => { + ReactDOM.render(<Main />, document.getElementById('root')); +}); |