diff options
| author | Naafiyan Ahmed <naafiyan@gmail.com> | 2022-07-12 12:08:49 -0400 |
|---|---|---|
| committer | Naafiyan Ahmed <naafiyan@gmail.com> | 2022-07-12 12:08:49 -0400 |
| commit | 9ad978eac113cf3559d885c62a9368a68f6333ec (patch) | |
| tree | 8daa3a5663379b29d8121aaa39e80cfd5e7fd9ed /src/client/util | |
| parent | 31041d7a5b2c3699518ebb33ccab016af0acd579 (diff) | |
| parent | 5628b585fa6356d66cf2e7454be20e3b847ad22e (diff) | |
merged master
Diffstat (limited to 'src/client/util')
| -rw-r--r-- | src/client/util/CaptureManager.tsx | 157 | ||||
| -rw-r--r-- | src/client/util/CurrentUserUtils.ts | 395 | ||||
| -rw-r--r-- | src/client/util/DocumentManager.ts | 186 | ||||
| -rw-r--r-- | src/client/util/GroupManager.tsx | 253 | ||||
| -rw-r--r-- | src/client/util/History.ts | 45 | ||||
| -rw-r--r-- | src/client/util/LinkFollower.ts | 112 | ||||
| -rw-r--r-- | src/client/util/LinkManager.ts | 259 | ||||
| -rw-r--r-- | src/client/util/RecordingApi.ts | 269 | ||||
| -rw-r--r-- | src/client/util/ScrollBox.tsx | 23 | ||||
| -rw-r--r-- | src/client/util/SelectionManager.ts | 35 | ||||
| -rw-r--r-- | src/client/util/SettingsManager.tsx | 471 | ||||
| -rw-r--r-- | src/client/util/SharingManager.tsx | 575 | ||||
| -rw-r--r-- | src/client/util/SnappingManager.ts | 49 |
13 files changed, 1483 insertions, 1346 deletions
diff --git a/src/client/util/CaptureManager.tsx b/src/client/util/CaptureManager.tsx index c247afa26..0b5957fac 100644 --- a/src/client/util/CaptureManager.tsx +++ b/src/client/util/CaptureManager.tsx @@ -1,17 +1,17 @@ -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { action, computed, observable, runInAction } from "mobx"; -import { observer } from "mobx-react"; -import * as React from "react"; -import { convertToObject } from "typescript"; -import { Doc, DocListCast } from "../../fields/Doc"; -import { BoolCast, StrCast, Cast } from "../../fields/Types"; -import { addStyleSheet, addStyleSheetRule, Utils } from "../../Utils"; -import { LightboxView } from "../views/LightboxView"; -import { MainViewModal } from "../views/MainViewModal"; -import "./CaptureManager.scss"; -import { SelectionManager } from "./SelectionManager"; -import { undoBatch } from "./UndoManager"; -const higflyout = require("@hig/flyout"); +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { action, computed, observable, runInAction } from 'mobx'; +import { observer } from 'mobx-react'; +import * as React from 'react'; +import { convertToObject } from 'typescript'; +import { Doc, DocListCast } from '../../fields/Doc'; +import { BoolCast, StrCast, Cast } from '../../fields/Types'; +import { addStyleSheet, addStyleSheetRule, Utils } from '../../Utils'; +import { LightboxView } from '../views/LightboxView'; +import { MainViewModal } from '../views/MainViewModal'; +import './CaptureManager.scss'; +import { SelectionManager } from './SelectionManager'; +import { undoBatch } from './UndoManager'; +const higflyout = require('@hig/flyout'); export const { anchorPoints } = higflyout; export const Flyout = higflyout.default; @@ -22,32 +22,31 @@ export class CaptureManager extends React.Component<{}> { @observable _document: any; @observable isOpen: boolean = false; // whether the CaptureManager is to be displayed or not. - constructor(props: {}) { super(props); CaptureManager.Instance = this; } - public close = action(() => this.isOpen = false); + public close = action(() => (this.isOpen = false)); public open = action((doc: Doc) => { this.isOpen = true; this._document = doc; }); - @computed get visibilityContent() { - - return <div className="capture-block"> - <div className="capture-block-title">Visibility</div> - <div className="capture-block-radio"> - <div className="radio-container"> - <input type="radio" value="private" name="visibility" style={{ margin: 0, marginRight: 5 }} /> Private - </div> - <div className="radio-container"> - <input type="radio" value="public" name="visibility" style={{ margin: 0, marginRight: 5 }} /> Public + return ( + <div className="capture-block"> + <div className="capture-block-title">Visibility</div> + <div className="capture-block-radio"> + <div className="radio-container"> + <input type="radio" value="private" name="visibility" style={{ margin: 0, marginRight: 5 }} /> Private + </div> + <div className="radio-container"> + <input type="radio" value="public" name="visibility" style={{ margin: 0, marginRight: 5 }} /> Public + </div> </div> </div> - </div>; + ); } @computed get linksContent() { @@ -66,75 +65,79 @@ export class CaptureManager extends React.Component<{}> { order.push( <div className="list-item"> <div className="number">{i}</div> - {(l.anchor1 as Doc).title} + {StrCast((l.anchor1 as Doc).title)} </div> ); } }); } - return <div className="capture-block"> - <div className="capture-block-title">Links</div> - <div className="capture-block-list"> - {order} + return ( + <div className="capture-block"> + <div className="capture-block-title">Links</div> + <div className="capture-block-list">{order}</div> </div> - </div>; + ); } @computed get closeButtons() { - return <div className="capture-block"> - <div className="buttons"> - <div className="save" onClick={() => { - LightboxView.SetLightboxDoc(this._document); - this.close(); - }}> - Save - </div> - <div className="cancel" onClick={() => { - const selected = SelectionManager.Views().slice(); - SelectionManager.DeselectAll(); - selected.map(dv => dv.props.removeDocument?.(dv.props.Document)); - this.close(); - }}> - Cancel + return ( + <div className="capture-block"> + <div className="buttons"> + <div + className="save" + onClick={() => { + LightboxView.SetLightboxDoc(this._document); + this.close(); + }}> + Save + </div> + <div + className="cancel" + onClick={() => { + const selected = SelectionManager.Views().slice(); + SelectionManager.DeselectAll(); + selected.map(dv => dv.props.removeDocument?.(dv.props.Document)); + this.close(); + }}> + Cancel + </div> </div> </div> - </div>; + ); } - - private get captureInterface() { - return <div className="capture-interface"> - <div className="capture-t1"> - <div className="recordButtonOutline" style={{}}> - <div className="recordButtonInner" style={{}}> + return ( + <div className="capture-interface"> + <div className="capture-t1"> + <div className="recordButtonOutline" style={{}}> + <div className="recordButtonInner" style={{}}></div> </div> + Conversation Capture </div> - Conversation Capture - </div> - <div className="capture-t2"> - - </div> - {this.visibilityContent} - {this.linksContent} - <div className="close-button" onClick={this.close}> - <FontAwesomeIcon icon={"times"} color="black" size={"lg"} /> + <div className="capture-t2"></div> + {this.visibilityContent} + {this.linksContent} + <div className="close-button" onClick={this.close}> + <FontAwesomeIcon icon={'times'} color="black" size={'lg'} /> + </div> + {this.closeButtons} </div> - {this.closeButtons} - </div>; - + ); } render() { - return <MainViewModal - contents={this.captureInterface} - isDisplayed={this.isOpen} - interactive={true} - closeOnExternalClick={this.close} - dialogueBoxStyle={{ width: "500px", height: "350px", border: "none", background: "whitesmoke" }} - overlayStyle={{ background: "black" }} - overlayDisplayedOpacity={0.6} - />; + return ( + <MainViewModal + contents={this.captureInterface} + isDisplayed={this.isOpen} + interactive={true} + closeOnExternalClick={this.close} + dialogueBoxStyle={{ width: '500px', height: '350px', border: 'none', background: 'whitesmoke' }} + overlayStyle={{ background: 'black' }} + overlayDisplayedOpacity={0.6} + /> + ); } -}
\ No newline at end of file +} diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts index dca77250c..6c80cf0f4 100644 --- a/src/client/util/CurrentUserUtils.ts +++ b/src/client/util/CurrentUserUtils.ts @@ -1,37 +1,28 @@ -import { computed, observable, reaction } from "mobx"; +import { reaction } from "mobx"; import * as rp from 'request-promise'; -import { DataSym, Doc, DocListCast, DocListCastAsync, Opt } from "../../fields/Doc"; -import { Id } from "../../fields/FieldSymbols"; -import { InkTool } from "../../fields/InkField"; +import { Doc, DocListCast, DocListCastAsync, Opt } from "../../fields/Doc"; import { List } from "../../fields/List"; import { PrefetchProxy } from "../../fields/Proxy"; import { RichTextField } from "../../fields/RichTextField"; import { listSpec } from "../../fields/Schema"; -import { ComputedField, ScriptField } from "../../fields/ScriptField"; -import { Cast, DateCast, DocCast, FieldValue, NumCast, PromiseValue, ScriptCast, StrCast } from "../../fields/Types"; +import { ScriptField } from "../../fields/ScriptField"; +import { Cast, DateCast, DocCast, PromiseValue, StrCast } from "../../fields/Types"; import { nullAudio } from "../../fields/URLField"; -import { SharingPermissions } from "../../fields/util"; +import { SetCachedGroups, SharingPermissions } from "../../fields/util"; import { OmitKeys, Utils } from "../../Utils"; import { DocServer } from "../DocServer"; import { Docs, DocumentOptions, DocUtils, FInfo } from "../documents/Documents"; -import { DocumentType } from "../documents/DocumentTypes"; -import { CollectionDockingView } from "../views/collections/CollectionDockingView"; +import { CollectionViewType, DocumentType } from "../documents/DocumentTypes"; import { TreeViewType } from "../views/collections/CollectionTreeView"; -import { CollectionView, CollectionViewType } from "../views/collections/CollectionView"; -import { TreeView } from "../views/collections/TreeView"; import { Colors } from "../views/global/globalEnums"; import { MainView } from "../views/MainView"; import { ButtonType, NumButtonType } from "../views/nodes/button/FontIconBox"; import { OverlayView } from "../views/OverlayView"; import { DragManager } from "./DragManager"; import { MakeTemplate } from "./DropConverter"; -import { HistoryUtil } from "./History"; import { LinkManager } from "./LinkManager"; import { ScriptingGlobals } from "./ScriptingGlobals"; -import { SelectionManager } from "./SelectionManager"; import { ColorScheme } from "./SettingsManager"; -import { SharingManager } from "./SharingManager"; -import { SnappingManager } from "./SnappingManager"; import { UndoManager } from "./UndoManager"; interface Button { @@ -57,59 +48,6 @@ interface Button { export let resolvedPorts: { server: number, socket: number }; export class CurrentUserUtils { - private static curr_id: string; - //TODO tfs: these should be temporary... - private static mainDocId: string | undefined; - - public static get id() { return this.curr_id; } - public static get MainDocId() { return this.mainDocId; } - public static set MainDocId(id: string | undefined) { this.mainDocId = id; } - @computed public static get UserDocument() { return Doc.UserDoc(); } - - @observable public static GuestTarget: Doc | undefined; - @observable public static GuestDashboard: Doc | undefined; - @observable public static GuestMobile: Doc | undefined; - @observable public static propertiesWidth: number = 0; - @observable public static headerBarHeight: number = 0; - @observable public static searchPanelWidth: number = 0; - - static AssignScripts(doc:Doc, scripts?:{ [key: string]: string;}, funcs?:{[key:string]: string}) { - scripts && Object.keys(scripts).map(key => { - if (ScriptCast(doc[key])?.script.originalScript !== scripts[key] && scripts[key]) { - doc[key] = ScriptField.MakeScript(scripts[key], { dragData: DragManager.DocumentDragData.name, value:"any", scriptContext: "any", documentView:Doc.name}, {"_readOnly_": true}); - } - }); - funcs && Object.keys(funcs).map(key => { - const cfield = ComputedField.WithoutComputed(() => FieldValue(doc[key])); - if (ScriptCast(cfield)?.script.originalScript !== funcs[key] && funcs[key]) { - doc[key] = ComputedField.MakeFunction(funcs[key], { dragData: DragManager.DocumentDragData.name }, {"_readOnly_": true}); - } - }); - return doc; - } - static AssignOpts(doc:Doc|undefined, reqdOpts:DocumentOptions, items?:Doc[]) { - if (doc) { - const compareValues = (val1:any, val2:any) => { - if (val1 instanceof List && val2 instanceof List && val1.length === val2.length) { - return !val1.some(v => !val2.includes(v)) || !val2.some(v => val1.includes(v)); - } - return val1 === val2; - } - Object.entries(reqdOpts).forEach(pair => { - const targetDoc = pair[0].startsWith("_") ? doc : Doc.GetProto(doc as Doc); - if (!Object.getOwnPropertyNames(targetDoc).includes(pair[0].replace(/^_/,"")) || - !compareValues(pair[1], targetDoc[pair[0]])) { - targetDoc[pair[0]] = pair[1]; - } - }); - items?.forEach(item => !DocListCast(doc.data).includes(item) && Doc.AddDocToList(Doc.GetProto(doc), "data", item)); - items && DocListCast(doc.data).forEach(item => !items.includes(item) && Doc.RemoveDocFromList(Doc.GetProto(doc), "data", item)); - } - return doc; - } - static AssignDocField(doc:Doc, field:string, creator:(reqdOpts:DocumentOptions, items?:Doc[]) => Doc, reqdOpts:DocumentOptions, items?: Doc[], scripts?:{[key:string]:string}, funcs?:{[key:string]:string}) { - return this.AssignScripts(this.AssignOpts(DocCast(doc[field]), reqdOpts, items) ?? (doc[field] = creator(reqdOpts, items)), scripts, funcs); - } // initializes experimental advanced template views - slideView, headerView static setupExperimentalTemplateButtons(doc: Doc, tempDocs?:Doc) { @@ -139,12 +77,12 @@ export class CurrentUserUtils { const reqdScripts = { onDragStart: '{ return copyDragFactory(this.dragFactory); }' }; const assignBtnAndTempOpts = (templateBtn:Opt<Doc>, btnOpts:DocumentOptions, templateOptions:DocumentOptions) => { if (templateBtn) { - this.AssignOpts(templateBtn,btnOpts); - this.AssignDocField(templateBtn, "dragFactory", opts => template(opts), templateOptions); + DocUtils.AssignOpts(templateBtn,btnOpts); + DocUtils.AssignDocField(templateBtn, "dragFactory", opts => template(opts), templateOptions); } return templateBtn; }; - return this.AssignScripts(assignBtnAndTempOpts(tempBtn, btnOpts, templateOpts) ?? this.createToolButton( {...btnOpts, dragFactory: MakeTemplate(template(templateOpts))}), reqdScripts); + return DocUtils.AssignScripts(assignBtnAndTempOpts(tempBtn, btnOpts, templateOpts) ?? this.createToolButton( {...btnOpts, dragFactory: MakeTemplate(template(templateOpts))}), reqdScripts); }); const reqdOpts:DocumentOptions = { @@ -154,7 +92,7 @@ export class CurrentUserUtils { }; const reqdScripts = { dropConverter : "convertToButtons(dragData)" }; const reqdFuncs = { hidden: "IsNoviceMode()" }; - return this.AssignScripts(this.AssignOpts(tempDocs, reqdOpts, requiredTypes) ?? Docs.Create.MasonryDocument(requiredTypes, reqdOpts), reqdScripts, reqdFuncs); + return DocUtils.AssignScripts(DocUtils.AssignOpts(tempDocs, reqdOpts, requiredTypes) ?? Docs.Create.MasonryDocument(requiredTypes, reqdOpts), reqdScripts, reqdFuncs); } /// Initializes templates that can be applied to notes @@ -167,16 +105,16 @@ export class CurrentUserUtils { const reqdNoteList = reqdTempOpts.map(opts => { const reqdOpts = {...opts, title: "text", system: true}; const noteType = tempNotes ? DocListCast(tempNotes.data).find(doc => doc.noteType === opts.noteType): undefined; - return this.AssignOpts(noteType, reqdOpts) ?? MakeTemplate(Docs.Create.TextDocument("",reqdOpts), true, opts.noteType??"Note"); + return DocUtils.AssignOpts(noteType, reqdOpts) ?? MakeTemplate(Docs.Create.TextDocument("",reqdOpts), true, opts.noteType??"Note"); }); const reqdOpts:DocumentOptions = { title: "Note Layouts", _height: 75, system: true }; - return this.AssignOpts(tempNotes, reqdOpts, reqdNoteList) ?? (doc[field] = Docs.Create.TreeDocument(reqdNoteList, reqdOpts)); + return DocUtils.AssignOpts(tempNotes, reqdOpts, reqdNoteList) ?? (doc[field] = Docs.Create.TreeDocument(reqdNoteList, reqdOpts)); } /// Initializes collection of templates for notes and click functions static setupDocTemplates(doc: Doc, field="myTemplates") { - this.AssignDocField(doc, "presElement", opts => Docs.Create.PresElementBoxDocument(opts), { title: "pres element template", type: DocumentType.PRESELEMENT, _fitWidth: true, _xMargin: 0, isTemplateDoc: true, isTemplateForField: "data"}); + DocUtils.AssignDocField(doc, "presElement", opts => Docs.Create.PresElementBoxDocument(opts), { title: "pres element template", type: DocumentType.PRESELEMENT, _fitWidth: true, _xMargin: 0, isTemplateDoc: true, isTemplateForField: "data"}); const templates = [ DocCast(doc.presElement), CurrentUserUtils.setupNoteTemplates(doc), @@ -184,13 +122,13 @@ export class CurrentUserUtils { ]; const reqdOpts = { title: "template layouts", _xMargin: 0, system: true, }; const reqdScripts = { dropConverter: "convertToButtons(dragData)" }; - return this.AssignDocField(doc, field, (opts,items) => Docs.Create.TreeDocument(items??[], opts), reqdOpts, templates, reqdScripts); + return DocUtils.AssignDocField(doc, field, (opts,items) => Docs.Create.TreeDocument(items??[], opts), reqdOpts, templates, reqdScripts); } // setup templates for different document types when they are iconified from Document Decorations static setupDefaultIconTemplates(doc: Doc, field="template-icons") { const reqdOpts = { title: "icon templates", _height: 75, system: true }; - const templateIconsDoc = this.AssignOpts(DocCast(doc[field]), reqdOpts) ?? (doc[field] = Docs.Create.TreeDocument([], reqdOpts)); + const templateIconsDoc = DocUtils.AssignOpts(DocCast(doc[field]), reqdOpts) ?? (doc[field] = Docs.Create.TreeDocument([], reqdOpts)); const makeIconTemplate = (type: DocumentType | undefined, templateField: string, opts:DocumentOptions) => { const iconFieldName = "icon" + (type ? "_" + type : ""); @@ -201,8 +139,8 @@ export class CurrentUserUtils { case DocumentType.FONTICON: creator = fontBox; break; } const allopts = {system: true, ...opts}; - return this.AssignScripts( (curIcon?.iconTemplate === opts.iconTemplate ? - this.AssignOpts(curIcon, allopts):undefined) ?? ((templateIconsDoc[iconFieldName] = MakeTemplate(creator(allopts), true, iconFieldName, templateField))), + return DocUtils.AssignScripts( (curIcon?.iconTemplate === opts.iconTemplate ? + DocUtils.AssignOpts(curIcon, allopts):undefined) ?? ((templateIconsDoc[iconFieldName] = MakeTemplate(creator(allopts), true, iconFieldName, templateField))), {onClick:"deiconifyView(documentView)"}); }; const labelBox = (opts: DocumentOptions, data?:string) => Docs.Create.LabelDocument({ @@ -224,7 +162,7 @@ export class CurrentUserUtils { makeIconTemplate("transcription" as any, "transcription", { iconTemplate:DocumentType.LABEL, backgroundColor: "orange" }), //makeIconTemplate(DocumentType.PDF, "icon", {iconTemplate:DocumentType.IMG}, (opts) => imageBox("http://www.cs.brown.edu/~bcz/noImage.png", opts)) ].filter(d => d).map(d => d!); - this.AssignOpts(DocCast(doc[field]), {}, iconTemplates); + DocUtils.AssignOpts(DocCast(doc[field]), {}, iconTemplates); } /// initalizes the set of "empty<DocType>" versions of each document type with default fields. e.g.,. emptyNote, emptyPresentation @@ -297,7 +235,7 @@ export class CurrentUserUtils { }, funcs: {title: 'self.text?.Text'}}, ]; - emptyThings.forEach(thing => this.AssignDocField(doc, "empty"+thing.key, (opts) => thing.creator(opts), {...standardOps(thing.key), ...thing.opts}, undefined, undefined, thing.funcs)); + emptyThings.forEach(thing => DocUtils.AssignDocField(doc, "empty"+thing.key, (opts) => thing.creator(opts), {...standardOps(thing.key), ...thing.opts}, undefined, undefined, thing.funcs)); return [ { toolTip: "Tap or drag to create a note", title: "Note", icon: "sticky-note", dragFactory: doc.emptyNote as Doc, }, @@ -326,7 +264,7 @@ export class CurrentUserUtils { btnType: ButtonType.ToolButton, backgroundColor: reqdOpts.backgroundColor ?? Colors.DARK_GRAY, color: Colors.WHITE, system: true, _removeDropProperties: new List<string>(["_stayInCollection"]), }; - return this.AssignScripts(this.AssignOpts(btn, opts) ?? Docs.Create.FontIconDocument(opts), reqdOpts.scripts, reqdOpts.funcs); + return DocUtils.AssignScripts(DocUtils.AssignOpts(btn, opts) ?? Docs.Create.FontIconDocument(opts), reqdOpts.scripts, reqdOpts.funcs); }); const reqdOpts:DocumentOptions = { @@ -335,7 +273,7 @@ export class CurrentUserUtils { childDocumentsActive: true }; const reqdScripts = { dropConverter: "convertToButtons(dragData)" }; - return this.AssignScripts(this.AssignOpts(dragCreatorDoc, reqdOpts, creatorBtns) ?? Docs.Create.MasonryDocument(creatorBtns, reqdOpts), reqdScripts); + return DocUtils.AssignScripts(DocUtils.AssignOpts(dragCreatorDoc, reqdOpts, creatorBtns) ?? Docs.Create.MasonryDocument(creatorBtns, reqdOpts), reqdScripts); } /// returns descriptions needed to buttons for the left sidebar to open up panes displaying different collections of documents @@ -348,7 +286,7 @@ export class CurrentUserUtils { { title: "Tools", target: this.setupToolsBtnPanel(doc, "myTools"), icon: "wrench", funcs: {hidden: "IsNoviceMode()"} }, { title: "Imports", target: this.setupImportSidebar(doc, "myImports"), icon: "upload", }, { title: "Recently Closed", target: this.setupRecentlyClosed(doc, "myRecentlyClosed"), icon: "archive", }, - { title: "Shared Docs", target: this.MySharedDocs, icon: "users", funcs:{badgeValue:badgeValue}}, + { title: "Shared Docs", target: Doc.MySharedDocs, icon: "users", funcs:{badgeValue:badgeValue}}, { title: "Trails", target: this.setupTrails(doc, "myTrails"), icon: "pres-trail", }, { title: "User Doc View", target: this.setupUserDocView(doc, "myUserDocView"), icon: "address-card",funcs: {hidden: "IsNoviceMode()"} }, ].map(tuple => ({...tuple, scripts:{onClick: 'selectMainMenu(self)'}})); @@ -356,7 +294,7 @@ export class CurrentUserUtils { /// the empty panel that is filled with whichever left menu button's panel has been selected static setupLeftSidebarPanel(doc: Doc, field="myLeftSidebarPanel") { - this.AssignDocField(doc, field, (opts) => ((doc:Doc) => {doc.system = true; return doc;})(new Doc()), {system:true}); + DocUtils.AssignDocField(doc, field, (opts) => ((doc:Doc) => {doc.system = true; return doc;})(new Doc()), {system:true}); } /// Initializes the left sidebar menu buttons and the panels they open up @@ -370,20 +308,20 @@ export class CurrentUserUtils { _width: 60, _height: 60, _stayInCollection: true, _hideContextMenu: true, _chromeHidden: true, _dropAction: "alias", _removeDropProperties: new List<string>(["dropAction", "_stayInCollection"]), }; - return this.AssignScripts(this.AssignOpts(btnDoc, reqdBtnOpts) ?? Docs.Create.FontIconDocument(reqdBtnOpts), scripts, funcs); + return DocUtils.AssignScripts(DocUtils.AssignOpts(btnDoc, reqdBtnOpts) ?? Docs.Create.FontIconDocument(reqdBtnOpts), scripts, funcs); }); const reqdStackOpts:DocumentOptions ={ title: "menuItemPanel", childDropAction: "alias", backgroundColor: Colors.DARK_GRAY, boxShadow: "rgba(0,0,0,0)", dontRegisterView: true, ignoreClick: true, _chromeHidden: true, _gridGap: 0, _yMargin: 0, _yPadding: 0, _xMargin: 0, _autoHeight: false, _width: 60, _columnWidth: 60, _lockedPosition: true, system: true }; - return this.AssignDocField(doc, field, (opts, items) => Docs.Create.StackingDocument(items??[], opts), reqdStackOpts, menuBtns, { dropConverter: "convertToButtons(dragData)" }); + return DocUtils.AssignDocField(doc, field, (opts, items) => Docs.Create.StackingDocument(items??[], opts), reqdStackOpts, menuBtns, { dropConverter: "convertToButtons(dragData)" }); } // Sets up mobile menu if it is undefined creates a new one, otherwise returns existing menu static setupActiveMobileMenu(doc: Doc, field="activeMobileMenu") { const reqdOpts = { _width: 980, ignoreClick: true, _lockedPosition: false, title: "home", _yMargin: 100, system: true, _chromeHidden: true,}; - this.AssignDocField(doc, field, (opts, items) => Docs.Create.StackingDocument(this.setupMobileButtons(), opts), reqdOpts); + DocUtils.AssignDocField(doc, field, (opts, items) => Docs.Create.StackingDocument(this.setupMobileButtons(), opts), reqdOpts); } // Sets up mobile buttons for inside mobile menu @@ -436,42 +374,6 @@ export class CurrentUserUtils { }) as any as Doc - static setupThumbButtons(doc: Doc) { - const docProtoData: { title: string, icon: string, drag?: string, ignoreClick?: boolean, pointerDown?: string, pointerUp?: string, clipboard?: Doc, backgroundColor?: string, dragFactory?: Doc }[] = [ - { title: "use pen", icon: "pen-nib", pointerUp: "resetPen()", pointerDown: 'setPen(2, this.backgroundColor)', backgroundColor: "blue" }, - { title: "use highlighter", icon: "highlighter", pointerUp: "resetPen()", pointerDown: 'setPen(20, this.backgroundColor)', backgroundColor: "yellow" }, - { title: "notepad", icon: "clipboard", pointerUp: "GestureOverlay.Instance.closeFloatingDoc()", pointerDown: 'GestureOverlay.Instance.openFloatingDoc(this.clipboard)', clipboard: Docs.Create.FreeformDocument([], { _width: 300, _height: 300, system: true }), backgroundColor: "orange" }, - { title: "interpret text", icon: "font", pointerUp: "setToolglass('none')", pointerDown: "setToolglass('inktotext')", backgroundColor: "orange" }, - { title: "ignore gestures", icon: "signature", pointerUp: "setToolglass('none')", pointerDown: "setToolglass('ignoregesture')", backgroundColor: "green" }, - ]; - return docProtoData.map(data => Docs.Create.FontIconDocument({ - _nativeWidth: 10, _nativeHeight: 10, _width: 10, _height: 10, title: data.title, icon: data.icon, - _dropAction: data.pointerDown ? "copy" : undefined, - ignoreClick: data.ignoreClick, - onDragStart: data.drag ? ScriptField.MakeFunction(data.drag) : undefined, - clipboard: data.clipboard, - onPointerUp: data.pointerUp ? ScriptField.MakeScript(data.pointerUp) : undefined, - onPointerDown: data.pointerDown ? ScriptField.MakeScript(data.pointerDown) : undefined, - backgroundColor: data.backgroundColor, - _removeDropProperties: new List<string>(["dropAction"]), - dragFactory: data.dragFactory, - system: true - })); - } - - static setupThumbDoc(userDoc: Doc) { - if (!userDoc.thumbDoc) { - const thumbDoc = Docs.Create.LinearDocument(CurrentUserUtils.setupThumbButtons(userDoc), { - _width: 100, _height: 50, ignoreClick: true, _lockedPosition: true, title: "buttons", - _autoHeight: true, _yMargin: 5, linearViewIsExpanded: true, backgroundColor: "white", system: true - }); - thumbDoc.inkToTextDoc = Docs.Create.LinearDocument([], { - _width: 300, _height: 25, _autoHeight: true, linearViewIsExpanded: true, flexDirection: "column", system: true - }); - userDoc.thumbDoc = thumbDoc; - } - return Cast(userDoc.thumbDoc, Doc); - } static setupMobileInkingDoc(userDoc: Doc) { return Docs.Create.FreeformDocument([], { title: "Mobile Inking", backgroundColor: "white", system: true }); @@ -492,7 +394,7 @@ export class CurrentUserUtils { /// Search option on the left side button panel static setupSearcher(doc: Doc, field:string) { - return this.AssignDocField(doc, field, (opts, items) => Docs.Create.SearchDocument(opts), { + return DocUtils.AssignDocField(doc, field, (opts, items) => Docs.Create.SearchDocument(opts), { dontRegisterView: true, backgroundColor: "dimgray", ignoreClick: true, title: "Search Panel", system: true, childDropAction: "alias", _lockedPosition: true, _viewType: CollectionViewType.Schema, _searchDoc: true, }); } @@ -506,7 +408,7 @@ export class CurrentUserUtils { title: "My Tools", system: true, ignoreClick: true, boxShadow: "0 0", _showTitle: "title", _width: 500, _yMargin: 20, _lockedPosition: true, _forceActive: true, _stayInCollection: true, _hideContextMenu: true, _chromeHidden: true, }; - return this.AssignDocField(doc, field, (opts, items) => Docs.Create.StackingDocument(items??[], opts), reqdToolOps, [creatorBtns, templateBtns]); + return DocUtils.AssignDocField(doc, field, (opts, items) => Docs.Create.StackingDocument(items??[], opts), reqdToolOps, [creatorBtns, templateBtns]); } /// initializes the left sidebar dashboard pane @@ -517,7 +419,7 @@ export class CurrentUserUtils { const reqdBtnOpts:DocumentOptions = { _forceActive: true, _width: 30, _height: 30, _stayInCollection: true, _hideContextMenu: true, title: "new dashboard", btnType: ButtonType.ClickButton, toolTip: "Create new dashboard", buttonText: "New trail", icon: "plus", system: true }; const reqdBtnScript = {onClick: newDashboard,} - const newDashboardButton = this.AssignScripts(this.AssignOpts(DocCast(myDashboards?.buttonMenuDoc), reqdBtnOpts) ?? Docs.Create.FontIconDocument(reqdBtnOpts), reqdBtnScript); + const newDashboardButton = DocUtils.AssignScripts(DocUtils.AssignOpts(DocCast(myDashboards?.buttonMenuDoc), reqdBtnOpts) ?? Docs.Create.FontIconDocument(reqdBtnOpts), reqdBtnScript); const reqdOpts:DocumentOptions = { title: "My Dashboards", childHideLinkButton: true, freezeChildren: "remove|add", treeViewHideTitle: true, boxShadow: "0 0", childDontRegisterViews: true, @@ -530,7 +432,7 @@ export class CurrentUserUtils { childContextMenuIcons: new List<string>(["chalkboard", "tv", "camera", "users", "times"]), // entries must be kept in synch with childContextMenuScripts, childContextMenuLabels, and childContextMenuFilters explainer: "This is your collection of dashboards. A dashboard represents the tab configuration of your workspace. To manage documents as folders, go to the Files." }; - myDashboards = this.AssignDocField(doc, field, (opts) => Docs.Create.TreeDocument([], opts), reqdOpts); + myDashboards = DocUtils.AssignDocField(doc, field, (opts) => Docs.Create.TreeDocument([], opts), reqdOpts); const toggleDarkTheme = `this.colorScheme = this.colorScheme ? undefined : "${ColorScheme.Dark}"`; const contextMenuScripts = [newDashboard]; const childContextMenuScripts = [toggleDarkTheme, `toggleComicMode()`, `snapshotDashboard()`, `shareDashboard(self)`, 'removeDashboard(self)']; // entries must be kept in synch with childContextMenuLabels, childContextMenuIcons, and childContextMenuFilters @@ -553,7 +455,7 @@ export class CurrentUserUtils { const reqdBtnOpts:DocumentOptions = { _forceActive: true, _width: 30, _height: 30, _stayInCollection: true, _hideContextMenu: true, title: "New trail", toolTip: "Create new trail", btnType: ButtonType.ClickButton, buttonText: "New trail", icon: "plus", system: true }; const reqdBtnScript = {onClick: `createNewPresentation()`}; - const newTrailButton = this.AssignScripts(this.AssignOpts(DocCast(myTrails?.buttonMenuDoc), reqdBtnOpts) ?? Docs.Create.FontIconDocument(reqdBtnOpts), reqdBtnScript); + const newTrailButton = DocUtils.AssignScripts(DocUtils.AssignOpts(DocCast(myTrails?.buttonMenuDoc), reqdBtnOpts) ?? Docs.Create.FontIconDocument(reqdBtnOpts), reqdBtnScript); const reqdOpts:DocumentOptions = { title: "My Trails", _showTitle: "title", _height: 100, @@ -564,7 +466,7 @@ export class CurrentUserUtils { _lockedPosition: true, boxShadow: "0 0", childDontRegisterViews: true, targetDropAction: "same", system: true, explainer: "All of the trails that you have created will appear here." }; - myTrails = this.AssignDocField(doc, field, (opts) => Docs.Create.TreeDocument([], opts), reqdOpts); + myTrails = DocUtils.AssignDocField(doc, field, (opts) => Docs.Create.TreeDocument([], opts), reqdOpts); const contextMenuScripts = [reqdBtnScript.onClick]; if (Cast(myTrails.contextMenuScripts, listSpec(ScriptField), null)?.length !== contextMenuScripts.length) { myTrails.contextMenuScripts = new List<ScriptField>(contextMenuScripts.map(script => ScriptField.MakeFunction(script)!)); @@ -575,15 +477,15 @@ export class CurrentUserUtils { /// initializes the left sidebar File system pane static setupFilesystem(doc: Doc, field:string) { var myFilesystem = DocCast(doc[field]); - const myFileOrphans = this.AssignDocField(doc, "myFileOrphans", (opts) => Docs.Create.TreeDocument([], opts), { title: "Unfiled", _stayInCollection: true, system: true, isFolder: true }); + const myFileOrphans = DocUtils.AssignDocField(doc, "myFileOrphans", (opts) => Docs.Create.TreeDocument([], opts), { title: "Unfiled", _stayInCollection: true, system: true, isFolder: true }); - const newFolder = `makeTopLevelFolder()`; + const newFolder = `TreeView_addNewFolder()`; const newFolderOpts: DocumentOptions = { _forceActive: true, _stayInCollection: true, _hideContextMenu: true, _width: 30, _height: 30, title: "New folder", btnType: ButtonType.ClickButton, toolTip: "Create new folder", buttonText: "New folder", icon: "folder-plus", system: true }; const newFolderScript = { onClick: newFolder}; - const newFolderButton = this.AssignScripts(this.AssignOpts(DocCast(myFilesystem?.buttonMenuDoc), newFolderOpts) ?? Docs.Create.FontIconDocument(newFolderOpts), newFolderScript); + const newFolderButton = DocUtils.AssignScripts(DocUtils.AssignOpts(DocCast(myFilesystem?.buttonMenuDoc), newFolderOpts) ?? Docs.Create.FontIconDocument(newFolderOpts), newFolderScript); const reqdOpts:DocumentOptions = { _showTitle: "title", _height: 100, _gridGap: 5, _forceActive: true, _lockedPosition: true, title: "My Documents", buttonMenu: true, buttonMenuDoc: newFolderButton, treeViewHideTitle: true, targetDropAction: "proto", system: true, @@ -593,7 +495,7 @@ export class CurrentUserUtils { childContextMenuIcons: new List<string>(["plus"]), explainer: "This is your file manager where you can create folders to keep track of documents independently of your dashboard." }; - myFilesystem = this.AssignDocField(doc, field, (opts, items) => Docs.Create.TreeDocument(items??[], opts), reqdOpts, [myFileOrphans]); + myFilesystem = DocUtils.AssignDocField(doc, field, (opts, items) => Docs.Create.TreeDocument(items??[], opts), reqdOpts, [myFileOrphans]); const childContextMenuScripts = [newFolder]; if (Cast(myFilesystem.childContextMenuScripts, listSpec(ScriptField), null)?.length !== childContextMenuScripts.length) { myFilesystem.childContextMenuScripts = new List<ScriptField>(childContextMenuScripts.map(script => ScriptField.MakeFunction(script)!)); @@ -610,13 +512,13 @@ export class CurrentUserUtils { contextMenuIcons:new List<string>(["trash"]), explainer: "Recently closed documents appear in this menu. They will only be deleted if you explicity empty this list." }; - const recentlyClosed = this.AssignDocField(doc, field, (opts) => Docs.Create.TreeDocument([], opts), reqdOpts); + const recentlyClosed = DocUtils.AssignDocField(doc, field, (opts) => Docs.Create.TreeDocument([], opts), reqdOpts); const clearAll = (target:string) => `getProto(${target}).data = new List([])`; const clearBtnsOpts:DocumentOptions = { _width: 30, _height: 30, _forceActive: true, _stayInCollection: true, _hideContextMenu: true, title: "Empty", target: recentlyClosed, btnType: ButtonType.ClickButton, buttonText: "Empty", icon: "trash", system: true, toolTip: "Empty recently closed",}; - const clearDocsButton = this.AssignDocField(recentlyClosed, "clearDocsBtn", (opts) => Docs.Create.FontIconDocument(opts), clearBtnsOpts, undefined, {onClick: clearAll("self.target")}); + const clearDocsButton = DocUtils.AssignDocField(recentlyClosed, "clearDocsBtn", (opts) => Docs.Create.FontIconDocument(opts), clearBtnsOpts, undefined, {onClick: clearAll("self.target")}); if (recentlyClosed.buttonMenuDoc !== clearDocsButton) Doc.GetProto(recentlyClosed).buttonMenuDoc = clearDocsButton; @@ -626,19 +528,6 @@ export class CurrentUserUtils { return recentlyClosed; } - /// creates a new, empty filter doc - static createFilterDoc() { - const clearAll = `getProto(self).data = new List([])`; - const reqdOpts:DocumentOptions = { - _lockedPosition: true, _autoHeight: true, _fitWidth: true, _height: 150, _xPadding: 5, _yPadding: 5, _gridGap: 5, _forceActive: true, - title: "Unnamed Filter", filterBoolean: "AND", boxShadow: "0 0", childDontRegisterViews: true, targetDropAction: "same", ignoreClick: true, system: true, - childDropAction: "none", treeViewHideTitle: true, treeViewTruncateTitleWidth: 150, - childContextMenuLabels: new List<string>(["Clear All"]), - childContextMenuScripts: new List<ScriptField>([ScriptField.MakeFunction(clearAll)!]), - }; - return Docs.Create.FilterDocument(reqdOpts); - } - /// initializes the left sidebar panel view of the UserDoc static setupUserDocView(doc: Doc, field:string) { const reqdOpts:DocumentOptions = { @@ -646,8 +535,8 @@ export class CurrentUserUtils { boxShadow: "0 0", childDontRegisterViews: true, targetDropAction: "same", ignoreClick: true, system: true, treeViewHideTitle: true, treeViewTruncateTitleWidth: 150 }; - if (!doc[field]) this.AssignOpts(doc, {treeViewOpen: true, treeViewExpandedView: "fields" }); - return this.AssignDocField(doc, field, (opts, items) => Docs.Create.TreeDocument(items??[], opts), reqdOpts, [doc]); + if (!doc[field]) DocUtils.AssignOpts(doc, {treeViewOpen: true, treeViewExpandedView: "fields" }); + return DocUtils.AssignDocField(doc, field, (opts, items) => Docs.Create.TreeDocument(items??[], opts), reqdOpts, [doc]); } static linearButtonList = (opts: DocumentOptions, docs: Doc[]) => Docs.Create.LinearDocument(docs, { @@ -666,7 +555,7 @@ export class CurrentUserUtils { static setupDockedButtons(doc: Doc, field="myDockedBtns") { const dockedBtns = DocCast(doc[field]); const dockBtn = (opts: DocumentOptions, scripts: {[key:string]:string}) => - this.AssignScripts(this.AssignOpts(DocListCast(dockedBtns?.data)?.find(doc => doc.title === opts.title), opts) ?? + DocUtils.AssignScripts(DocUtils.AssignOpts(DocListCast(dockedBtns?.data)?.find(doc => doc.title === opts.title), opts) ?? CurrentUserUtils.createToolButton(opts), scripts); const btnDescs = [// setup reactions to change the highlights on the undo/redo buttons -- would be better to encode this in the undo/redo buttons, but the undo/redo stacks are not wired up that way yet @@ -680,7 +569,7 @@ export class CurrentUserUtils { }; reaction(() => UndoManager.redoStack.slice(), () => Doc.GetProto(btns.find(btn => btn.title === "redo")!).opacity = UndoManager.CanRedo() ? 1 : 0.4, { fireImmediately: true }); reaction(() => UndoManager.undoStack.slice(), () => Doc.GetProto(btns.find(btn => btn.title === "undo")!).opacity = UndoManager.CanUndo() ? 1 : 0.4, { fireImmediately: true }); - return this.AssignDocField(doc, field, (opts, items) => this.linearButtonList(opts, items??[]), dockBtnsReqdOpts, btns); + return DocUtils.AssignDocField(doc, field, (opts, items) => this.linearButtonList(opts, items??[]), dockBtnsReqdOpts, btns); } static textTools():Button[] { @@ -740,16 +629,16 @@ export class CurrentUserUtils { CollectionViewType.Multirow, CollectionViewType.Time, CollectionViewType.Carousel, CollectionViewType.Carousel3D, CollectionViewType.Linear, CollectionViewType.Map, CollectionViewType.Grid]), - title: "Perspective", toolTip: "View", width: 100,btnType: ButtonType.DropdownList,ignoreClick: true, scripts: { script: 'setView(value, _readOnly_)'}}, - { title: "Back", icon: "chevron-left", toolTip: "Prev Animation Frame", width: 20, btnType: ButtonType.ClickButton, scripts: { onClick: 'prevKeyFrame(_readOnly_)'}, funcs: {hidden: 'IsNoviceMode() || !selectedDocumentType(undefined, "freeform")'}}, - { title: "Fwd", icon: "chevron-right", toolTip: "Next Animation Frame", width: 20, btnType: ButtonType.ClickButton, scripts: { onClick: 'nextKeyFrame(_readOnly_)'}, funcs: {hidden: 'IsNoviceMode() || !selectedDocumentType(undefined, "freeform")'}}, - { title: "Fill", icon: "fill-drip", toolTip: "Background Fill Color",width: 20, btnType: ButtonType.ColorButton, ignoreClick: true, scripts: { script: 'setBackgroundColor(value, _readOnly_)'},funcs: {hidden: '!selectedDocumentType()'}}, // Only when a document is selected - { title: "Header", icon: "heading", toolTip: "Header Color", btnType: ButtonType.ColorButton, ignoreClick: true, scripts: { script: 'setHeaderColor(value, _readOnly_)'}, funcs: {hidden: '!selectedDocumentType()'}}, - { title: "Overlay", icon: "layer-group", toolTip: "Overlay", btnType: ButtonType.ToggleButton, scripts: { onClick: 'toggleOverlay(_readOnly_)'}, funcs: {hidden: '!selectedDocumentType(undefined, "freeform", true)'}}, // Only when floating document is selected in freeform - { title: "Text", icon: "text", subMenu: CurrentUserUtils.textTools(), funcs: {linearViewIsExpanded: `selectedDocumentType("${DocumentType.RTF}")`} }, // Always available - { title: "Ink", icon: "ink", subMenu: CurrentUserUtils.inkTools(), funcs: {linearViewIsExpanded: `selectedDocumentType("${DocumentType.INK}")`} }, // Always available - { title: "Web", icon: "web", subMenu: CurrentUserUtils.webTools(), funcs: {linearViewIsExpanded: `selectedDocumentType("${DocumentType.WEB}")`, hidden: `!selectedDocumentType("${DocumentType.WEB}")`} }, // Only when Web is selected - { title: "Schema", icon: "schema", subMenu: CurrentUserUtils.schemaTools(), funcs: {linearViewIsExpanded: `selectedDocumentType(undefined, "${CollectionViewType.Schema}")`, hidden: `!selectedDocumentType(undefined, "${CollectionViewType.Schema}")`} } // Only when Schema is selected + title: "Perspective", toolTip: "View", btnType: ButtonType.DropdownList, ignoreClick: true, width: 100, scripts: { script: 'setView(value, _readOnly_)'}}, + { title: "Back", icon: "chevron-left", toolTip: "Prev Animation Frame", btnType: ButtonType.ClickButton, funcs: {hidden: '!SelectionManager_selectedDocType(undefined, "freeform") || IsNoviceMode()'}, width: 20, scripts: { onClick: 'prevKeyFrame(_readOnly_)'}}, + { title: "Fwd", icon: "chevron-right", toolTip: "Next Animation Frame", btnType: ButtonType.ClickButton, funcs: {hidden: '!SelectionManager_selectedDocType(undefined, "freeform") || IsNoviceMode()'}, width: 20, scripts: { onClick: 'nextKeyFrame(_readOnly_)'}}, + { title: "Fill", icon: "fill-drip", toolTip: "Background Fill Color",btnType: ButtonType.ColorButton, funcs: {hidden: '!SelectionManager_selectedDocType()'}, ignoreClick: true, width: 20, scripts: { script: 'setBackgroundColor(value, _readOnly_)'}}, // Only when a document is selected + { title: "Header", icon: "heading", toolTip: "Header Color", btnType: ButtonType.ColorButton, funcs: {hidden: '!SelectionManager_selectedDocType()'}, ignoreClick: true, scripts: { script: 'setHeaderColor(value, _readOnly_)'}}, + { title: "Overlay", icon: "layer-group", toolTip: "Overlay", btnType: ButtonType.ToggleButton, funcs: {hidden: '!SelectionManager_selectedDocType(undefined, "freeform", true)'}, scripts: { onClick: 'toggleOverlay(_readOnly_)'}}, // Only when floating document is selected in freeform + { title: "Text", icon: "text", toolTip: "Text functions", subMenu: CurrentUserUtils.textTools(), funcs: {hidden: 'false', linearViewIsExpanded: `SelectionManager_selectedDocType("${DocumentType.RTF}")`} }, // Always available + { title: "Ink", icon: "ink", toolTip: "Ink functions", subMenu: CurrentUserUtils.inkTools(), funcs: {hidden: 'false', inearViewIsExpanded: `SelectionManager_selectedDocType("${DocumentType.INK}")`} }, // Always available + { title: "Web", icon: "web", toolTip: "Web functions", subMenu: CurrentUserUtils.webTools(), funcs: {hidden: `!SelectionManager_selectedDocType("${DocumentType.WEB}")`, linearViewIsExpanded: `SelectionManager_selectedDocType("${DocumentType.WEB}")`, } }, // Only when Web is selected + { title: "Schema", icon: "schema", toolTip: "Schema functions", subMenu: CurrentUserUtils.schemaTools(), funcs: {hidden: `!SelectionManager_selectedDocType(undefined, "${CollectionViewType.Schema}")`, linearViewIsExpanded: `SelectionManager_selectedDocType(undefined, "${CollectionViewType.Schema}")`} } // Only when Schema is selected ]; } @@ -768,13 +657,13 @@ export class CurrentUserUtils { ...params.funcs, backgroundColor: params.scripts?.onClick /// a bit hacky. if onClick is set, then we assume it returns a color value when queried with '_readOnly_'. This will be true for toggle buttons, but not generally } - return this.AssignScripts(this.AssignOpts(btnDoc, reqdOpts) ?? Docs.Create.FontIconDocument(reqdOpts), params.scripts, reqdFuncs); + return DocUtils.AssignScripts(DocUtils.AssignOpts(btnDoc, reqdOpts) ?? Docs.Create.FontIconDocument(reqdOpts), params.scripts, reqdFuncs); } /// Initializes all the default buttons for the top bar context menu static setupContextMenuButtons(doc: Doc, field="myContextMenuBtns") { const reqdCtxtOpts = { title: "context menu buttons", flexGap: 0, childDontRegisterViews: true, linearViewIsExpanded: true, ignoreClick: true, linearViewExpandable: false, _height: 35 }; - const ctxtMenuBtnsDoc = this.AssignDocField(doc, field, (opts, items) => this.linearButtonList(opts, items??[]), reqdCtxtOpts, undefined); + const ctxtMenuBtnsDoc = DocUtils.AssignDocField(doc, field, (opts, items) => this.linearButtonList(opts, items??[]), reqdCtxtOpts, undefined); const ctxtMenuBtns = CurrentUserUtils.contextMenuTools().map(params => { const menuBtnDoc = DocListCast(ctxtMenuBtnsDoc?.data).find(doc => doc.title === params.title); if (!params.subMenu) { @@ -786,20 +675,20 @@ export class CurrentUserUtils { const items = params.subMenu?.map(sub => this.setupContextMenuButton(sub, DocListCast(menuBtnDoc?.data).find(doc => doc.title === sub.title)) ); - return this.AssignScripts( - this.AssignDocField(ctxtMenuBtnsDoc, StrCast(params.title), (opts) => this.linearButtonList(opts, items??[]), reqdSubMenuOpts, items), undefined, params.funcs); + return DocUtils.AssignScripts( + DocUtils.AssignDocField(ctxtMenuBtnsDoc, StrCast(params.title), (opts) => this.linearButtonList(opts, items??[]), reqdSubMenuOpts, items), undefined, params.funcs); } }); - return this.AssignOpts(ctxtMenuBtnsDoc, reqdCtxtOpts, ctxtMenuBtns); + return DocUtils.AssignOpts(ctxtMenuBtnsDoc, reqdCtxtOpts, ctxtMenuBtns); } /// collection of documents rendered in the overlay layer above all tabs and other UI static setupOverlays(doc: Doc, field = "myOverlayDocs") { - return this.AssignDocField(doc, field, (opts) => Docs.Create.FreeformDocument([], opts), { title: "overlay documents", backgroundColor: "#aca3a6", system: true }); + return DocUtils.AssignDocField(doc, field, (opts) => Docs.Create.FreeformDocument([], opts), { title: "overlay documents", backgroundColor: "#aca3a6", system: true }); } static setupPublished(doc:Doc, field = "myPublishedDocs") { - return this.AssignDocField(doc, field, (opts) => Docs.Create.TreeDocument([], opts), { title: "published docs", backgroundColor: "#aca3a6", system: true }); + return DocUtils.AssignDocField(doc, field, (opts) => Docs.Create.TreeDocument([], opts), { title: "published docs", backgroundColor: "#aca3a6", system: true }); } /// The database of all links on all documents @@ -838,7 +727,7 @@ export class CurrentUserUtils { explainer: "This is where documents or dashboards that other users have shared with you will appear. To share a document or dashboard right click and select 'Share'" }; - this.AssignDocField(doc, "mySharedDocs", opts => Docs.Create.TreeDocument([], opts, sharingDocumentId + "layout", sharingDocumentId), sharedDocOpts, undefined, sharedScripts); + DocUtils.AssignDocField(doc, "mySharedDocs", opts => Docs.Create.TreeDocument([], opts, sharingDocumentId + "layout", sharingDocumentId), sharedDocOpts, undefined, sharedScripts); } /// Import option on the left side button panel @@ -849,12 +738,12 @@ export class CurrentUserUtils { childDropAction: "copy", _autoHeight: true, _yMargin: 50, _gridGap: 15, boxShadow: "0 0", _lockedPosition: true, system: true, _chromeHidden: true, dontRegisterView: true, explainer: "This is where documents that are Imported into Dash will go." }; - const myImports = this.AssignDocField(doc, field, (opts) => Docs.Create.StackingDocument([], opts), reqdOpts); + const myImports = DocUtils.AssignDocField(doc, field, (opts) => Docs.Create.StackingDocument([], opts), reqdOpts); const reqdBtnOpts:DocumentOptions = { _forceActive: true, toolTip: "Import from computer", _width: 30, _height: 30, _stayInCollection: true, _hideContextMenu: true, title: "Import", btnType: ButtonType.ClickButton, buttonText: "Import", icon: "upload", system: true }; - this.AssignDocField(myImports, "buttonMenuDoc", (opts) => Docs.Create.FontIconDocument(opts), reqdBtnOpts, undefined, { onClick: "importDocument()" }); + DocUtils.AssignDocField(myImports, "buttonMenuDoc", (opts) => Docs.Create.FontIconDocument(opts), reqdBtnOpts, undefined, { onClick: "importDocument()" }); return myImports; } @@ -905,17 +794,16 @@ export class CurrentUserUtils { return doc.clickFuncs as Doc; } - /// Updates the UserDoc to have all required fields, docs, etc. No changes should need to be /// written to the server if the code hasn't changed. However, choices need to be made for each Doc/field /// whether to revert to "default" values, or to leave them as the user/system last set them. static updateUserDocument(doc: Doc, sharingDocumentId: string, linkDatabaseId: string) { - this.AssignDocField(doc, "globalGroupDatabase", () => Docs.Prototypes.MainGroupDocument(), {}); + DocUtils.AssignDocField(doc, "globalGroupDatabase", () => Docs.Prototypes.MainGroupDocument(), {}); reaction(() => DateCast(DocCast(doc.globalGroupDatabase)["data-lastModified"]), async () => { const groups = await DocListCastAsync(DocCast(doc.globalGroupDatabase).data); const mygroups = groups?.filter(group => JSON.parse(StrCast(group.members)).includes(Doc.CurrentUserEmail)) || []; - SnappingManager.SetCachedGroups(["Public", ...mygroups?.map(g => StrCast(g.title))]); + SetCachedGroups(["Public", ...mygroups?.map(g => StrCast(g.title))]); }, { fireImmediately: true }); doc.system ?? (doc.system = true); doc.title ?? (doc.title = Doc.CurrentUserEmail); @@ -949,8 +837,8 @@ export class CurrentUserUtils { this.setupLeftSidebarMenu(doc); // the left-side column of buttons that open their contents in a flyout panel on the left this.setupDocTemplates(doc); // sets up the template menu of templates this.setupFieldInfos(doc); // sets up the collection of field info descriptions for each possible DocumentOption - this.AssignDocField(doc, "globalScriptDatabase", (opts) => Docs.Prototypes.MainScriptDocument(), {}); - this.AssignDocField(doc, "myHeaderBar", (opts) => Docs.Create.MulticolumnDocument([], opts), { title: "header bar", system: true }); // drop down panel at top of dashboard for stashing documents + DocUtils.AssignDocField(doc, "globalScriptDatabase", (opts) => Docs.Prototypes.MainScriptDocument(), {}); + DocUtils.AssignDocField(doc, "myHeaderBar", (opts) => Docs.Create.MulticolumnDocument([], opts), { title: "header bar", system: true }); // drop down panel at top of dashboard for stashing documents if (doc.activeDashboard instanceof Doc) { // undefined means ColorScheme.Light until all CSS is updated with values for each color scheme (e.g., see MainView.scss, DocumentDecorations.scss) @@ -958,12 +846,12 @@ export class CurrentUserUtils { } new LinkManager(); - DocServer.UPDATE_SERVER_CACHE(); + setTimeout(DocServer.UPDATE_SERVER_CACHE, 2500); return doc; } static setupFieldInfos(doc:Doc, field="fieldInfos") { const fieldInfoOpts = { title: "Field Infos", system: true}; // bcz: all possible document options have associated field infos which are stored onn the FieldInfos document **except for title and system which are used as part of the definition of the fieldInfos object - const infos = this.AssignDocField(doc, field, opts => Doc.assign(new Doc(), opts as any), fieldInfoOpts); + const infos = DocUtils.AssignDocField(doc, field, opts => Doc.assign(new Doc(), opts as any), fieldInfoOpts); const entries = Object.entries(new DocumentOptions()); entries.forEach(pair => { if (!Array.from(Object.keys(fieldInfoOpts)).includes(pair[0])) { @@ -975,7 +863,7 @@ export class CurrentUserUtils { case Doc.name: opts.fieldValues = new List<Doc>(options.values as any); break; default: opts.fieldValues = new List<string>(options.values as any); break;// string, pointerEvents, dimUnit, dropActionType } - this.AssignDocField(infos, pair[0], opts => Doc.assign(new Doc(), OmitKeys(opts,["values"]).omit), opts); + DocUtils.AssignDocField(infos, pair[0], opts => Doc.assign(new Doc(), OmitKeys(opts,["values"]).omit), opts); } }); } @@ -996,7 +884,6 @@ export class CurrentUserUtils { } public static async loadUserDocument(id: string) { - this.curr_id = id; await rp.get(Utils.prepend("/getUserDocumentIds")).then(ids => { const { userDocumentId, sharingDocumentId, linkDatabaseId } = JSON.parse(ids); if (userDocumentId !== "guest") { @@ -1013,46 +900,6 @@ export class CurrentUserUtils { }); } - public static _urlState: HistoryUtil.DocUrl; - - /// opens a dashboard as the ActiveDashboard (and adds the dashboard to the users list of dashboards if it's not already there). - /// this also sets the readonly state of the dashboard based on the current mode of dash (from its url) - public static openDashboard = (doc: Doc|undefined, fromHistory = false) => { - if (!doc) return false; - CurrentUserUtils.MainDocId = doc[Id]; - Doc.AddDocToList(CurrentUserUtils.MyDashboards, "data", doc); - - // this has the side-effect of setting the main container since we're assigning the active/guest dashboard - Doc.UserDoc() ? (CurrentUserUtils.ActiveDashboard = doc) : (CurrentUserUtils.GuestDashboard = doc); - - const state = CurrentUserUtils._urlState; - if (state.sharing === true && !Doc.UserDoc()) { - DocServer.Control.makeReadOnly(); - } else { - fromHistory || HistoryUtil.pushState({ - type: "doc", - docId: doc[Id], - readonly: state.readonly, - nro: state.nro, - sharing: false, - }); - if (state.readonly === true || state.readonly === null) { - DocServer.Control.makeReadOnly(); - } else if (state.safe) { - if (!state.nro) { - DocServer.Control.makeReadOnly(); - } - CollectionView.SetSafeMode(true); - } else if (state.nro || state.nro === null || state.readonly === false) { - } else if (doc.readOnly) { - DocServer.Control.makeReadOnly(); - } else { - DocServer.Control.makeEditable(); - } - } - - return true; - } public static importDocument = () => { const input = document.createElement("input"); @@ -1069,7 +916,7 @@ export class CurrentUserUtils { // setTimeout(() => SearchUtil.Search(`{!join from=id to=proto_i}id:link*`, true, {}).then(docs => // docs.docs.forEach(d => LinkManager.Instance.addLink(d))), 2000); // need to give solr some time to update so that this query will find any link docs we've added. // } - const list = Cast(CurrentUserUtils.MyImports.data, listSpec(Doc), null); + const list = Cast(Doc.MyImports.data, listSpec(Doc), null); doc instanceof Doc && list?.splice(0, 0, doc); } else if (input.files && input.files.length !== 0) { const disposer = OverlayView.ShowSpinner(); @@ -1077,7 +924,7 @@ export class CurrentUserUtils { if (results.length !== input.files?.length) { alert("Error uploading files - possibly due to unsupported file types"); } - const list = Cast(CurrentUserUtils.MyImports.data, listSpec(Doc), null); + const list = Cast(Doc.MyImports.data, listSpec(Doc), null); list?.splice(0, 0, ...results); disposer(); } else { @@ -1086,110 +933,12 @@ export class CurrentUserUtils { }; input.click(); } - - public static snapshotDashboard() { return CollectionDockingView.TakeSnapshot(CurrentUserUtils.ActiveDashboard); } - - public static closeActiveDashboard = () => { CurrentUserUtils.ActiveDashboard = undefined; } - - public static removeDashboard = async (dashboard:Doc) => { - const dashboards = await DocListCastAsync(CurrentUserUtils.MyDashboards.data); - if (dashboards?.length) { - if (dashboard === CurrentUserUtils.ActiveDashboard) CurrentUserUtils.openDashboard(dashboards.find(doc => doc !== dashboard)); - Doc.RemoveDocFromList(CurrentUserUtils.MyDashboards, "data", dashboard); - if (!dashboards.length) CurrentUserUtils.ActivePage = "home"; - } - } - public static createNewDashboard = (id?: string, name?: string) => { - const presentation = Doc.MakeCopy(Doc.UserDoc().emptyPresentation as Doc, true); - const dashboards = CurrentUserUtils.MyDashboards; - const dashboardCount = DocListCast(dashboards.data).length + 1; - const freeformOptions: DocumentOptions = { - x: 0, - y: 400, - _width: 1500, - _height: 1000, - _fitWidth: true, - _backgroundGridShow: true, - title: `Untitled Tab 1`, - }; - const title = name ? name : `Dashboard ${dashboardCount}` - const freeformDoc = CurrentUserUtils.GuestTarget || Docs.Create.FreeformDocument([], freeformOptions); - const dashboardDoc = Docs.Create.StandardCollectionDockingDocument([{ doc: freeformDoc, initialWidth: 600 }], { title: title }, id, "row"); - freeformDoc.context = dashboardDoc; - - // switching the tabs from the datadoc to the regular doc - const dashboardTabs = DocListCast(dashboardDoc[DataSym].data); - dashboardDoc.data = new List<Doc>(dashboardTabs); - dashboardDoc["pane-count"] = 1; - - CurrentUserUtils.ActivePresentation = presentation; - - Doc.AddDocToList(dashboards, "data", dashboardDoc); - // open this new dashboard - CurrentUserUtils.ActiveDashboard = dashboardDoc; - CurrentUserUtils.ActivePage = "dashboard"; - } - - public static GetNewTextDoc(title: string, x: number, y: number, width?: number, height?: number, noMargins?: boolean, annotationOn?: Doc, maxHeight?: number, backgroundColor?: string) { - const tbox = Docs.Create.TextDocument("", { - _xMargin: noMargins ? 0 : undefined, _yMargin: noMargins ? 0 : undefined, annotationOn, docMaxAutoHeight: maxHeight, backgroundColor: backgroundColor, - _width: width || 200, _height: 35, x: x, y: y, _fitWidth: true, _autoHeight: true, title - }); - const template = Doc.UserDoc().defaultTextLayout; - if (template instanceof Doc) { - tbox._width = NumCast(template._width); - tbox.layoutKey = "layout_" + StrCast(template.title); - Doc.GetProto(tbox)[StrCast(tbox.layoutKey)] = template; - } - return tbox; - } - - public static get MyUserDocView() { return DocCast(Doc.UserDoc().myUserDocView); } - public static get MyDockedBtns() { return DocCast(Doc.UserDoc().myDockedBtns); } - public static get MySearcher() { return DocCast(Doc.UserDoc().mySearcher); } - public static get MyFilesystem() { return DocCast(Doc.UserDoc().myFilesystem); } - public static get MyHeaderBar() { return DocCast(Doc.UserDoc().myHeaderBar); } - public static get MyTools() { return DocCast(Doc.UserDoc().myTools); } - public static get MyDashboards() { return DocCast(Doc.UserDoc().myDashboards); } - public static get MyFileOrphans() { return DocCast(Doc.UserDoc().myFileOrphans); } - public static get MyTemplates() { return DocCast(Doc.UserDoc().myTemplates); } - public static get MyLeftSidebarMenu() { return DocCast(Doc.UserDoc().myLeftSidebarMenu); } - public static get MyLeftSidebarPanel() { return DocCast(Doc.UserDoc().myLeftSidebarPanel); } - public static get MySharedDocs() { return DocCast(Doc.UserDoc().mySharedDocs); } - public static get MyTrails() { return DocCast(Doc.UserDoc().myTrails); } - public static get MyImports() { return DocCast(Doc.UserDoc().myImports); } - public static get MyContextMenuBtns() { return DocCast(Doc.UserDoc().myContextMenuBtns); } - public static get MyRecentlyClosed() { return DocCast(Doc.UserDoc().myRecentlyClosed); } - public static get MyOverlayDocs() { return DocCast(Doc.UserDoc().myOverlayDocs); } - public static get MyPublishedDocs() { return DocCast(Doc.UserDoc().myPublishedDocs); } - public static get ActiveDashboard() { return DocCast(Doc.UserDoc().activeDashboard); } - public static set ActiveDashboard(val:Doc|undefined) { Doc.UserDoc().activeDashboard = val; } - public static get ActivePresentation() { return DocCast(Doc.UserDoc().activePresentation); } - public static set ActivePresentation(val) { Doc.UserDoc().activePresentation = val; } - public static get ActivePage() { return StrCast(Doc.UserDoc().activePage); } - public static set ActivePage(val) { Doc.UserDoc().activePage = val; } - public static set ActiveTool(tool: InkTool) { Doc.UserDoc().activeTool = tool; } - public static get ActiveTool(): InkTool { return StrCast(Doc.UserDoc().activeTool, InkTool.None) as InkTool; } } -ScriptingGlobals.add(function MySharedDocs() { return CurrentUserUtils.MySharedDocs; }, "document containing all shared Docs"); +ScriptingGlobals.add(function MySharedDocs() { return Doc.MySharedDocs; }, "document containing all shared Docs"); ScriptingGlobals.add(function IsNoviceMode() { return Doc.noviceMode; }, "is Dash in novice mode"); ScriptingGlobals.add(function toggleComicMode() { Doc.UserDoc().renderStyle = Doc.UserDoc().renderStyle === "comic" ? undefined : "comic"; }, "switches between comic and normal document rendering"); -ScriptingGlobals.add(function snapshotDashboard() { CurrentUserUtils.snapshotDashboard(); }, "creates a snapshot copy of a dashboard"); -ScriptingGlobals.add(function createNewDashboard() { return CurrentUserUtils.createNewDashboard(); }, "creates a new dashboard when called"); ScriptingGlobals.add(function createNewPresentation() { return MainView.Instance.createNewPresentation(); }, "creates a new presentation when called"); ScriptingGlobals.add(function createNewFolder() { return MainView.Instance.createNewFolder(); }, "creates a new folder in myFiles when called"); ScriptingGlobals.add(function links(doc: any) { return new List(LinkManager.Instance.getAllRelatedLinks(doc)); }, "returns all the links to the document or its annotations", "(doc: any)"); ScriptingGlobals.add(function importDocument() { return CurrentUserUtils.importDocument(); }, "imports files from device directly into the import sidebar"); -ScriptingGlobals.add(function shareDashboard(dashboard: Doc) { SharingManager.Instance.open(undefined, dashboard); }, "opens sharing dialog for Dashboard"); -ScriptingGlobals.add(function removeDashboard(dashboard: Doc) { CurrentUserUtils.removeDashboard(dashboard); }, "Remove Dashboard from Dashboards"); -ScriptingGlobals.add(function addToDashboards(dashboard: Doc) { CurrentUserUtils.openDashboard( Doc.MakeAlias(dashboard)); }, "adds Dashboard to set of Dashboards"); -ScriptingGlobals.add(function selectedDocumentType(docType?: DocumentType, colType?: CollectionViewType, checkContext?: boolean) { - let selected = (sel => checkContext ? DocCast(sel?.context) : sel)(SelectionManager.SelectedSchemaDoc() ?? SelectionManager.Docs().lastElement()); - return docType ? selected?.type === docType : colType ? selected?.viewType === colType : true; -}); -ScriptingGlobals.add(function makeTopLevelFolder() { - TreeView._editTitleOnLoad = { id: Utils.GenerateGuid(), parent: undefined }; - const opts = { title: "Untitled folder", _stayInCollection: true, isFolder: true }; - return Doc.AddDocToList(CurrentUserUtils.MyFilesystem, "data", Docs.Create.TreeDocument([], opts, TreeView._editTitleOnLoad.id)); -});
\ No newline at end of file diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts index 0435cd535..d3ac2f03f 100644 --- a/src/client/util/DocumentManager.ts +++ b/src/client/util/DocumentManager.ts @@ -4,44 +4,43 @@ import { Id } from '../../fields/FieldSymbols'; import { Cast } from '../../fields/Types'; import { returnFalse } from '../../Utils'; import { DocumentType } from '../documents/DocumentTypes'; -import { CollectionDockingView } from '../views/collections/CollectionDockingView'; -import { CollectionFreeFormView } from '../views/collections/collectionFreeForm'; -import { CollectionView } from '../views/collections/CollectionView'; import { LightboxView } from '../views/LightboxView'; import { DocumentView, ViewAdjustment } from '../views/nodes/DocumentView'; import { LinkAnchorBox } from '../views/nodes/LinkAnchorBox'; -import { CurrentUserUtils } from './CurrentUserUtils'; +import { CollectionDockingView } from '../views/collections/CollectionDockingView'; +import { CollectionFreeFormView } from '../views/collections/collectionFreeForm'; +import { CollectionView } from '../views/collections/CollectionView'; import { ScriptingGlobals } from './ScriptingGlobals'; import { SelectionManager } from './SelectionManager'; export class DocumentManager { - //global holds all of the nodes (regardless of which collection they're in) @observable public DocumentViews = new Set<DocumentView>(); @observable public LinkAnchorBoxViews: DocumentView[] = []; @observable public RecordingEvent = 0; - @observable public LinkedDocumentViews: { a: DocumentView, b: DocumentView, l: Doc }[] = []; + @observable public LinkedDocumentViews: { a: DocumentView; b: DocumentView; l: Doc }[] = []; private static _instance: DocumentManager; - public static get Instance(): DocumentManager { return this._instance || (this._instance = new this()); } + public static get Instance(): DocumentManager { + return this._instance || (this._instance = new this()); + } //private constructor so no other class can create a nodemanager - private constructor() { } + private constructor() {} @action public AddView = (view: DocumentView) => { //console.log("MOUNT " + view.props.Document.title + "/" + view.props.LayoutTemplateString); if (view.props.LayoutTemplateString?.includes(LinkAnchorBox.name)) { - const viewAnchorIndex = view.props.LayoutTemplateString.includes("anchor2") ? "anchor2" : "anchor1"; + const viewAnchorIndex = view.props.LayoutTemplateString.includes('anchor2') ? 'anchor2' : 'anchor1'; DocListCast(view.rootDoc.links).forEach(link => { - this.LinkAnchorBoxViews?.filter(dv => Doc.AreProtosEqual(dv.rootDoc, link) && !dv.props.LayoutTemplateString?.includes(viewAnchorIndex)). - forEach(otherView => this.LinkedDocumentViews.push( - { - a: viewAnchorIndex === "anchor2" ? otherView : view, - b: viewAnchorIndex === "anchor2" ? view : otherView, - l: link - }) - ); + this.LinkAnchorBoxViews?.filter(dv => Doc.AreProtosEqual(dv.rootDoc, link) && !dv.props.LayoutTemplateString?.includes(viewAnchorIndex)).forEach(otherView => + this.LinkedDocumentViews.push({ + a: viewAnchorIndex === 'anchor2' ? otherView : view, + b: viewAnchorIndex === 'anchor2' ? view : otherView, + l: link, + }) + ); }); this.LinkAnchorBoxViews.push(view); // this.LinkedDocumentViews.forEach(view => console.log(" LV = " + view.a.props.Document.title + "/" + view.a.props.LayoutTemplateString + " --> " + @@ -49,14 +48,16 @@ export class DocumentManager { } else { this.DocumentViews.add(view); } - } + }; public RemoveView = action((view: DocumentView) => { - this.LinkedDocumentViews.slice().forEach(action(pair => { - if (pair.a === view || pair.b === view) { - const li = this.LinkedDocumentViews.indexOf(pair); - li !== -1 && this.LinkedDocumentViews.splice(li, 1); - } - })); + this.LinkedDocumentViews.slice().forEach( + action(pair => { + if (pair.a === view || pair.b === view) { + const li = this.LinkedDocumentViews.indexOf(pair); + li !== -1 && this.LinkedDocumentViews.splice(li, 1); + } + }) + ); if (view.props.LayoutTemplateString?.includes(LinkAnchorBox.name)) { const index = this.LinkAnchorBoxViews.indexOf(view); @@ -125,11 +126,11 @@ export class DocumentManager { const views: DocumentView[] = []; Array.from(DocumentManager.Instance.DocumentViews).map(view => LightboxView.IsLightboxDocView(view.docViewPath) && Doc.AreProtosEqual(view.rootDoc, toFind) && views.push(view)); return views?.find(view => view.ContentDiv?.getBoundingClientRect().width && view.props.focus !== returnFalse) || views?.find(view => view.props.focus !== returnFalse) || (views.length ? views[0] : undefined); - } + }; public getFirstDocumentView = (toFind: Doc, originatingDoc: Opt<Doc> = undefined): DocumentView | undefined => { const views = this.getDocumentViews(toFind).filter(view => view.rootDoc !== originatingDoc); return views?.find(view => view.ContentDiv?.getBoundingClientRect().width && view.props.focus !== returnFalse) || views?.find(view => view.props.focus !== returnFalse) || (views.length ? views[0] : undefined); - } + }; public getDocumentViews(toFind: Doc): DocumentView[] { const toReturn: DocumentView[] = []; const docViews = Array.from(DocumentManager.Instance.DocumentViews).filter(view => !LightboxView.IsLightboxDocView(view.docViewPath)); @@ -146,17 +147,16 @@ export class DocumentManager { return toReturn; } - static addView = (doc: Doc, finished?: () => void) => { - CollectionDockingView.AddSplit(doc, "right"); + CollectionDockingView.AddSplit(doc, 'right'); finished?.(); - } + }; public jumpToDocument = async ( - targetDoc: Doc, // document to display - willZoom: boolean, // whether to zoom doc to take up most of screen + targetDoc: Doc, // document to display + willZoom: boolean, // whether to zoom doc to take up most of screen createViewFunc = DocumentManager.addView, // how to create a view of the doc if it doesn't exist - docContext: Doc[], // context to load that should contain the target - linkDoc?: Doc, // link that's being followed + docContext: Doc[], // context to load that should contain the target + linkDoc?: Doc, // link that's being followed closeContextIfNotFound: boolean = false, // after opening a context where the document should be, this determines whether the context should be closed if the Doc isn't actually there originatingDoc: Opt<Doc> = undefined, // doc that initiated the display of the target odoc finished?: () => void, @@ -172,14 +172,15 @@ export class DocumentManager { var wasHidden = resolvedTarget.hidden; if (wasHidden) { runInAction(() => { - resolvedTarget.hidden = false; // if the target is hidden, un-hide it here. + resolvedTarget.hidden = false; // if the target is hidden, un-hide it here. docView?.props.bringToFront(resolvedTarget); }); } const focusAndFinish = (didFocus: boolean) => { const finalTargetDoc = resolvedTarget; if (originatingDoc?.isPushpin) { - if (!didFocus && !wasHidden) { // don't toggle the hidden state if the doc was already un-hidden as part of this document traversal + if (!didFocus && !wasHidden) { + // don't toggle the hidden state if the doc was already un-hidden as part of this document traversal finalTargetDoc.hidden = !finalTargetDoc.hidden; } } else { @@ -192,77 +193,92 @@ export class DocumentManager { const contextDocs = docContext.length ? await DocListCastAsync(docContext[0].data) : undefined; const contextDoc = contextDocs?.find(doc => Doc.AreProtosEqual(doc, targetDoc) || Doc.AreProtosEqual(doc, annotatedDoc)) ? docContext.lastElement() : undefined; const targetDocContext = contextDoc || annotatedDoc; - const targetDocContextView = (targetDocContext && getFirstDocView(targetDocContext)) || - (wasHidden && annoContainerView);// if we have an annotation container and the target was hidden, then try again because we just un-hid the document above + const targetDocContextView = (targetDocContext && getFirstDocView(targetDocContext)) || (wasHidden && annoContainerView); // if we have an annotation container and the target was hidden, then try again because we just un-hid the document above const focusView = !docView && targetDoc.type === DocumentType.MARKER && annoContainerView ? annoContainerView : docView; if (annoContainerView) { - if (annoContainerView.props.Document.layoutKey === "layout_icon") { - annoContainerView.iconify(() => annoContainerView.focus(targetDoc, { - originalTarget, willZoom, scale: presZoom, afterFocus: (didFocus: boolean) => - new Promise<ViewAdjustment>(res => { - focusAndFinish(true); - res(ViewAdjustment.doNothing); - }) - })); + if (annoContainerView.props.Document.layoutKey === 'layout_icon') { + annoContainerView.iconify(() => + annoContainerView.focus(targetDoc, { + originalTarget, + willZoom, + scale: presZoom, + afterFocus: (didFocus: boolean) => + new Promise<ViewAdjustment>(res => { + focusAndFinish(true); + res(ViewAdjustment.doNothing); + }), + }) + ); return; } else if (!docView && targetDoc.type !== DocumentType.MARKER) { - annoContainerView.focus(targetDoc); // this allows something like a PDF view to remove its doc filters to expose the target so that it can be found in the retry code below + annoContainerView.focus(targetDoc); // this allows something like a PDF view to remove its doc filters to expose the target so that it can be found in the retry code below } } if (focusView) { !noSelect && Doc.linkFollowHighlight(focusView.rootDoc); //TODO:glr make this a setting in PresBox - const doFocus = (forceDidFocus: boolean) => focusView.focus(originalTarget ?? targetDoc, { - originalTarget, willZoom, scale: presZoom, afterFocus: (didFocus: boolean) => - new Promise<ViewAdjustment>(res => { - focusAndFinish(forceDidFocus || didFocus); - res(ViewAdjustment.doNothing); - }) - }); - if (focusView.props.Document.layoutKey === "layout_icon") { + const doFocus = (forceDidFocus: boolean) => + focusView.focus(originalTarget ?? targetDoc, { + originalTarget, + willZoom, + scale: presZoom, + afterFocus: (didFocus: boolean) => + new Promise<ViewAdjustment>(res => { + focusAndFinish(forceDidFocus || didFocus); + res(ViewAdjustment.doNothing); + }), + }); + if (focusView.props.Document.layoutKey === 'layout_icon') { focusView.iconify(() => doFocus(true)); } else { doFocus(false); } } else { - if (!targetDocContext) { // we don't have a view and there's no context specified ... create a new view of the target using the dockFunc or default + if (!targetDocContext) { + // we don't have a view and there's no context specified ... create a new view of the target using the dockFunc or default createViewFunc(Doc.BrushDoc(targetDoc), finished); // bcz: should we use this?: Doc.MakeAlias(targetDoc))); - } else { // otherwise try to get a view of the context of the target - if (targetDocContextView) { // we found a context view and aren't forced to create a new one ... focus on the context first.. + } else { + // otherwise try to get a view of the context of the target + if (targetDocContextView) { + // we found a context view and aren't forced to create a new one ... focus on the context first.. wasHidden = wasHidden || targetDocContextView.rootDoc.hidden; targetDocContextView.rootDoc.hidden = false; // make sure context isn't hidden - targetDocContext._viewTransition = "transform 500ms"; + targetDocContext._viewTransition = 'transform 500ms'; targetDocContextView.props.focus(targetDocContextView.rootDoc, { - willZoom, afterFocus: async () => { + willZoom, + afterFocus: async () => { targetDocContext._viewTransition = undefined; - if (targetDocContext.layoutKey === "layout_icon") { - targetDocContextView.iconify(() => this.jumpToDocument( - resolvedTarget ?? targetDoc, willZoom, createViewFunc, docContext, linkDoc, closeContextIfNotFound, originatingDoc, - finished, originalTarget, noSelect, presZoom)); + if (targetDocContext.layoutKey === 'layout_icon') { + targetDocContextView.iconify(() => this.jumpToDocument(resolvedTarget ?? targetDoc, willZoom, createViewFunc, docContext, linkDoc, closeContextIfNotFound, originatingDoc, finished, originalTarget, noSelect, presZoom)); } return ViewAdjustment.doNothing; - } + }, }); // now find the target document within the context - if (targetDoc._timecodeToShow) { // if the target has a timecode, it should show up once the (presumed) video context scrubs to the display timecode; + if (targetDoc._timecodeToShow) { + // if the target has a timecode, it should show up once the (presumed) video context scrubs to the display timecode; targetDocContext._currentTimecode = targetDoc.anchorTimecodeToShow; finished?.(); - } else { // no timecode means we need to find the context view and focus on our target + } else { + // no timecode means we need to find the context view and focus on our target const findView = (delay: number) => { - const retryDocView = getFirstDocView(resolvedTarget); // test again for the target view snce we presumably created the context above by focusing on it - if (retryDocView) { // we found the target in the context. + const retryDocView = getFirstDocView(resolvedTarget); // test again for the target view snce we presumably created the context above by focusing on it + if (retryDocView) { + // we found the target in the context. Doc.linkFollowHighlight(retryDocView.rootDoc); retryDocView.focus(targetDoc, { - willZoom, afterFocus: (didFocus: boolean) => + willZoom, + afterFocus: (didFocus: boolean) => new Promise<ViewAdjustment>(res => { - !noSelect && focusAndFinish(didFocus); + !noSelect && focusAndFinish(true); res(ViewAdjustment.doNothing); - }) + }), }); // focus on the target in the context } else if (delay > 1000) { // we didn't find the target, so it must have moved out of the context. Go back to just creating it. if (closeContextIfNotFound) targetDocContextView.props.removeDocument?.(targetDocContextView.rootDoc); - if (targetDoc.layout) { // there will no layout for a TEXTANCHOR type document + if (targetDoc.layout) { + // there will no layout for a TEXTANCHOR type document createViewFunc(Doc.BrushDoc(targetDoc), finished); // create a new view of the target } } else { @@ -272,22 +288,24 @@ export class DocumentManager { setTimeout(() => findView(0), 0); } } else { - if (docContext.length && docContext[0]?.layoutKey === "layout_icon") { + if (docContext.length && docContext[0]?.layoutKey === 'layout_icon') { const docContextView = this.getFirstDocumentView(docContext[0]); if (docContextView) { - return docContextView.iconify(() => this.jumpToDocument( - targetDoc, willZoom, createViewFunc, docContext.slice(1, docContext.length), linkDoc, closeContextIfNotFound, originatingDoc, - finished, originalTarget, noSelect, presZoom)); + return docContextView.iconify(() => + this.jumpToDocument(targetDoc, willZoom, createViewFunc, docContext.slice(1, docContext.length), linkDoc, closeContextIfNotFound, originatingDoc, finished, originalTarget, noSelect, presZoom) + ); } } // there's no context view so we need to create one first and try again when that finishes const finishFunc = () => this.jumpToDocument(targetDoc, true, createViewFunc, docContext, linkDoc, true /* if we don't find the target, we want to get rid of the context just created */, undefined, finished, originalTarget); - createViewFunc(targetDocContext, // after creating the context, this calls the finish function that will retry looking for the target - finishFunc); + createViewFunc( + targetDocContext, // after creating the context, this calls the finish function that will retry looking for the target + finishFunc + ); } } } - } + }; } export function DocFocusOrOpen(doc: Doc, collectionDoc?: Doc) { const cv = collectionDoc && DocumentManager.Instance.getDocumentView(collectionDoc); @@ -295,14 +313,12 @@ export function DocFocusOrOpen(doc: Doc, collectionDoc?: Doc) { if (dv && Doc.AreProtosEqual(dv.props.Document, doc)) { dv.props.focus(dv.props.Document, { willZoom: true }); Doc.linkFollowHighlight(dv?.props.Document, false); - } - else { - const context = doc.context !== CurrentUserUtils.MyFilesystem && Cast(doc.context, Doc, null); + } else { + const context = doc.context !== Doc.MyFilesystem && Cast(doc.context, Doc, null); const showDoc = context || doc; const bestAlias = showDoc === Doc.GetProto(showDoc) ? DocListCast(showDoc.aliases).find(doc => !doc.context && doc.author === Doc.CurrentUserEmail) : showDoc; - CollectionDockingView.AddSplit(bestAlias ? bestAlias : Doc.MakeAlias(showDoc), "right") && context && - setTimeout(() => DocumentManager.Instance.getDocumentView(Doc.GetProto(doc))?.focus(doc)); + CollectionDockingView.AddSplit(bestAlias ? bestAlias : Doc.MakeAlias(showDoc), 'right') && context && setTimeout(() => DocumentManager.Instance.getDocumentView(Doc.GetProto(doc))?.focus(doc)); } } -ScriptingGlobals.add(DocFocusOrOpen);
\ No newline at end of file +ScriptingGlobals.add(DocFocusOrOpen); diff --git a/src/client/util/GroupManager.tsx b/src/client/util/GroupManager.tsx index 62d656c5d..59334f6a2 100644 --- a/src/client/util/GroupManager.tsx +++ b/src/client/util/GroupManager.tsx @@ -1,19 +1,19 @@ -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { action, computed, observable } from "mobx"; -import { observer } from "mobx-react"; -import * as React from "react"; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { action, computed, observable } from 'mobx'; +import { observer } from 'mobx-react'; +import * as React from 'react'; import Select from 'react-select'; -import * as RequestPromise from "request-promise"; -import { Doc, DocListCast, DocListCastAsync, Opt } from "../../fields/Doc"; -import { StrCast, Cast } from "../../fields/Types"; -import { Utils } from "../../Utils"; -import { MainViewModal } from "../views/MainViewModal"; -import { TaskCompletionBox } from "../views/nodes/TaskCompletedBox"; -import "./GroupManager.scss"; -import { GroupMemberView } from "./GroupMemberView"; -import { SharingManager, User } from "./SharingManager"; -import { listSpec } from "../../fields/Schema"; -import { DateField } from "../../fields/DateField"; +import * as RequestPromise from 'request-promise'; +import { Doc, DocListCast, DocListCastAsync, Opt } from '../../fields/Doc'; +import { StrCast, Cast } from '../../fields/Types'; +import { Utils } from '../../Utils'; +import { MainViewModal } from '../views/MainViewModal'; +import { TaskCompletionBox } from '../views/nodes/TaskCompletedBox'; +import './GroupManager.scss'; +import { GroupMemberView } from './GroupMemberView'; +import { SharingManager, User } from './SharingManager'; +import { listSpec } from '../../fields/Schema'; +import { DateField } from '../../fields/DateField'; /** * Interface for options for the react-select component @@ -25,7 +25,6 @@ export interface UserOptions { @observer export class GroupManager extends React.Component<{}> { - static Instance: GroupManager; @observable isOpen: boolean = false; // whether the GroupManager is to be displayed or not. @observable private users: string[] = []; // list of users populated from the database. @@ -34,24 +33,26 @@ export class GroupManager extends React.Component<{}> { @observable private createGroupModalOpen: boolean = false; private inputRef: React.RefObject<HTMLInputElement> = React.createRef(); // the ref for the input box. private createGroupButtonRef: React.RefObject<HTMLButtonElement> = React.createRef(); // the ref for the group creation button - @observable private buttonColour: "#979797" | "black" = "#979797"; - @observable private groupSort: "ascending" | "descending" | "none" = "none"; + @observable private buttonColour: '#979797' | 'black' = '#979797'; + @observable private groupSort: 'ascending' | 'descending' | 'none' = 'none'; constructor(props: Readonly<{}>) { super(props); GroupManager.Instance = this; } - componentDidMount() { this.populateUsers(); } + componentDidMount() { + this.populateUsers(); + } /** * Fetches the list of users stored on the database. */ populateUsers = async () => { - const userList = await RequestPromise.get(Utils.prepend("/getUsers")); + const userList = await RequestPromise.get(Utils.prepend('/getUsers')); const raw = JSON.parse(userList) as User[]; raw.map(action(user => !this.users.some(umail => umail === user.email) && this.users.push(user.email))); - } + }; /** * @returns the options to be rendered in the dropdown menu to add users and create a group. @@ -68,11 +69,11 @@ export class GroupManager extends React.Component<{}> { // SelectionManager.DeselectAll(); this.isOpen = true; this.populateUsers(); - } + }; /** * Hides the GroupManager. - */ + */ @action close = () => { this.isOpen = false; @@ -81,26 +82,32 @@ export class GroupManager extends React.Component<{}> { // this.users = []; this.createGroupModalOpen = false; TaskCompletionBox.taskCompleted = false; - } + }; /** * @returns the database of groups. */ - @computed get GroupManagerDoc(): Doc | undefined { return Doc.UserDoc().globalGroupDatabase as Doc; } + @computed get GroupManagerDoc(): Doc | undefined { + return Doc.UserDoc().globalGroupDatabase as Doc; + } /** * @returns a list of all group documents. */ - @computed get allGroups(): Doc[] { return DocListCast(this.GroupManagerDoc?.data); } + @computed get allGroups(): Doc[] { + return DocListCast(this.GroupManagerDoc?.data); + } /** * @returns the members of the admin group. */ - @computed get adminGroupMembers(): string[] { return this.getGroup("Admin") ? JSON.parse(StrCast(this.getGroup("Admin")!.members)) : ""; } + @computed get adminGroupMembers(): string[] { + return this.getGroup('Admin') ? JSON.parse(StrCast(this.getGroup('Admin')!.members)) : ''; + } /** * @returns a group document based on the group name. - * @param groupName + * @param groupName */ getGroup(groupName: string): Doc | undefined { return this.allGroups.find(group => group.title === groupName); @@ -114,10 +121,9 @@ export class GroupManager extends React.Component<{}> { return JSON.parse(StrCast(this.getGroup(group)!.members)) as string[]; } - /** * @returns a boolean indicating whether the current user has access to edit group documents. - * @param groupDoc + * @param groupDoc */ hasEditAccess(groupDoc: Doc): boolean { if (!groupDoc) return false; @@ -127,12 +133,12 @@ export class GroupManager extends React.Component<{}> { /** * Helper method that sets up the group document. - * @param groupName - * @param memberEmails + * @param groupName + * @param memberEmails */ createGroupDoc(groupName: string, memberEmails: string[] = []) { - const name = groupName.toLowerCase() === "admin" ? "Admin" : groupName; - const groupDoc = new Doc("GROUP:" + name, true); + const name = groupName.toLowerCase() === 'admin' ? 'Admin' : groupName; + const groupDoc = new Doc('GROUP:' + name, true); groupDoc.title = name; groupDoc.owners = JSON.stringify([Doc.CurrentUserEmail]); groupDoc.members = JSON.stringify(memberEmails); @@ -141,12 +147,12 @@ export class GroupManager extends React.Component<{}> { /** * Helper method that adds a group document to the database of group documents and @returns whether it was successfully added or not. - * @param groupDoc + * @param groupDoc */ addGroup(groupDoc: Doc): boolean { if (this.GroupManagerDoc) { - Doc.AddDocToList(this.GroupManagerDoc, "data", groupDoc); - this.GroupManagerDoc["data-lastModified"] = new DateField; + Doc.AddDocToList(this.GroupManagerDoc, 'data', groupDoc); + this.GroupManagerDoc['data-lastModified'] = new DateField(); return true; } return false; @@ -154,20 +160,20 @@ export class GroupManager extends React.Component<{}> { /** * Deletes a group from the database of group documents and @returns whether the group was deleted or not. - * @param group + * @param group */ @action deleteGroup(group: Doc): boolean { if (group) { if (this.GroupManagerDoc && this.hasEditAccess(group)) { - Doc.RemoveDocFromList(this.GroupManagerDoc, "data", group); + Doc.RemoveDocFromList(this.GroupManagerDoc, 'data', group); SharingManager.Instance.removeGroup(group); const members = JSON.parse(StrCast(group.members)); if (members.includes(Doc.CurrentUserEmail)) { const index = DocListCast(this.GroupManagerDoc.data).findIndex(grp => grp === group); index !== -1 && Cast(this.GroupManagerDoc.data, listSpec(Doc), [])?.splice(index, 1); } - this.GroupManagerDoc["data-lastModified"] = new DateField; + this.GroupManagerDoc['data-lastModified'] = new DateField(); if (group === this.currentGroup) { this.currentGroup = undefined; } @@ -179,8 +185,8 @@ export class GroupManager extends React.Component<{}> { /** * Adds a member to a group. - * @param groupDoc - * @param email + * @param groupDoc + * @param email */ addMemberToGroup(groupDoc: Doc, email: string) { if (this.hasEditAccess(groupDoc)) { @@ -188,14 +194,14 @@ export class GroupManager extends React.Component<{}> { !memberList.includes(email) && memberList.push(email); groupDoc.members = JSON.stringify(memberList); SharingManager.Instance.shareWithAddedMember(groupDoc, email); - this.GroupManagerDoc && (this.GroupManagerDoc["data-lastModified"] = new DateField); + this.GroupManagerDoc && (this.GroupManagerDoc['data-lastModified'] = new DateField()); } } /** * Removes a member from the group. - * @param groupDoc - * @param email + * @param groupDoc + * @param email */ removeMemberFromGroup(groupDoc: Doc, email: string) { if (this.hasEditAccess(groupDoc)) { @@ -205,27 +211,27 @@ export class GroupManager extends React.Component<{}> { const user = memberList.splice(index, 1)[0]; groupDoc.members = JSON.stringify(memberList); SharingManager.Instance.removeMember(groupDoc, email); - this.GroupManagerDoc && (this.GroupManagerDoc["data-lastModified"] = new DateField); + this.GroupManagerDoc && (this.GroupManagerDoc['data-lastModified'] = new DateField()); } } } /** * Handles changes in the users selected in the "Select users" dropdown. - * @param selectedOptions + * @param selectedOptions */ @action handleChange = (selectedOptions: any) => { this.selectedUsers = selectedOptions as UserOptions[]; - } + }; /** * Creates the group when the enter key has been pressed (when in the input). - * @param e + * @param e */ handleKeyDown = (e: React.KeyboardEvent) => { - e.key === "Enter" && this.createGroup(); - } + e.key === 'Enter' && this.createGroup(); + }; /** * Handles the input of required fields in the setup of a group and resets the relevant variables. @@ -234,32 +240,37 @@ export class GroupManager extends React.Component<{}> { createGroup = () => { const { value } = this.inputRef.current!; if (!value) { - alert("Please enter a group name"); + alert('Please enter a group name'); return; } - if (["admin", "public", "override"].includes(value.toLowerCase())) { - if (value.toLowerCase() !== "admin" || (value.toLowerCase() === "admin" && this.getGroup("Admin"))) { + if (['admin', 'public', 'override'].includes(value.toLowerCase())) { + if (value.toLowerCase() !== 'admin' || (value.toLowerCase() === 'admin' && this.getGroup('Admin'))) { alert(`You cannot override the ${value.charAt(0).toUpperCase() + value.slice(1)} group`); return; } } if (this.getGroup(value)) { - alert("Please select a unique group name"); + alert('Please select a unique group name'); return; } - this.createGroupDoc(value, this.selectedUsers?.map(user => user.value)); + this.createGroupDoc( + value, + this.selectedUsers?.map(user => user.value) + ); this.selectedUsers = null; - this.inputRef.current!.value = ""; - this.buttonColour = "#979797"; + this.inputRef.current!.value = ''; + this.buttonColour = '#979797'; const { left, width, top } = this.createGroupButtonRef.current!.getBoundingClientRect(); TaskCompletionBox.popupX = left - 2 * width; TaskCompletionBox.popupY = top; - TaskCompletionBox.textDisplayed = "Group created!"; + TaskCompletionBox.textDisplayed = 'Group created!'; TaskCompletionBox.taskCompleted = true; - setTimeout(action(() => TaskCompletionBox.taskCompleted = false), 2000); - - } + setTimeout( + action(() => (TaskCompletionBox.taskCompleted = false)), + 2000 + ); + }; /** * @returns the MainViewModal which allows the user to create groups. @@ -268,50 +279,43 @@ export class GroupManager extends React.Component<{}> { const contents = ( <div className="group-create"> <div className="group-heading" style={{ marginBottom: 0 }}> - <p><b>New Group</b></p> - <div className={"close-button"} onClick={action(() => { - this.createGroupModalOpen = false; TaskCompletionBox.taskCompleted = false; - })}> - <FontAwesomeIcon icon={"times"} color={"black"} size={"lg"} /> + <p> + <b>New Group</b> + </p> + <div + className={'close-button'} + onClick={action(() => { + this.createGroupModalOpen = false; + TaskCompletionBox.taskCompleted = false; + })}> + <FontAwesomeIcon icon={'times'} color={'black'} size={'lg'} /> </div> </div> - <input - className="group-input" - ref={this.inputRef} - onKeyDown={this.handleKeyDown} - autoFocus - type="text" - placeholder="Group name" - onChange={action(() => this.buttonColour = this.inputRef.current?.value ? "black" : "#979797")} /> + <input className="group-input" ref={this.inputRef} onKeyDown={this.handleKeyDown} autoFocus type="text" placeholder="Group name" onChange={action(() => (this.buttonColour = this.inputRef.current?.value ? 'black' : '#979797'))} /> <Select isMulti options={this.options} onChange={this.handleChange} - placeholder={"Select users"} + placeholder={'Select users'} value={this.selectedUsers} closeMenuOnSelect={false} styles={{ dropdownIndicator: (base, state) => ({ ...base, transition: '0.5s all ease', - transform: state.selectProps.menuIsOpen ? 'rotate(180deg)' : undefined + transform: state.selectProps.menuIsOpen ? 'rotate(180deg)' : undefined, }), - multiValue: (base) => ({ + multiValue: base => ({ ...base, - maxWidth: "50%", + maxWidth: '50%', '&:hover': { - maxWidth: "unset" - } - }) + maxWidth: 'unset', + }, + }), }} /> - <button - ref={this.createGroupButtonRef} - onClick={this.createGroup} - style={{ background: this.buttonColour }} - disabled={this.buttonColour === "#979797"} - > + <button ref={this.createGroupButtonRef} onClick={this.createGroup} style={{ background: this.buttonColour }} disabled={this.buttonColour === '#979797'}> Create </button> </div> @@ -322,8 +326,12 @@ export class GroupManager extends React.Component<{}> { isDisplayed={this.createGroupModalOpen} interactive={true} contents={contents} - dialogueBoxStyle={{ width: "90%", height: "70%" }} - closeOnExternalClick={action(() => { this.createGroupModalOpen = false; this.selectedUsers = null; TaskCompletionBox.taskCompleted = false; })} + dialogueBoxStyle={{ width: '90%', height: '70%' }} + closeOnExternalClick={action(() => { + this.createGroupModalOpen = false; + this.selectedUsers = null; + TaskCompletionBox.taskCompleted = false; + })} /> ); } @@ -332,7 +340,6 @@ export class GroupManager extends React.Component<{}> { * A getter that @returns the main interface for the GroupManager. */ private get groupInterface() { - const sortGroups = (d1: Doc, d2: Doc) => { const g1 = StrCast(d1.title); const g2 = StrCast(d2.title); @@ -340,62 +347,50 @@ export class GroupManager extends React.Component<{}> { return g1 < g2 ? -1 : g1 === g2 ? 0 : 1; }; - const groups = this.groupSort === "ascending" ? this.allGroups.sort(sortGroups) : this.groupSort === "descending" ? this.allGroups.sort(sortGroups).reverse() : this.allGroups; + const groups = this.groupSort === 'ascending' ? this.allGroups.sort(sortGroups) : this.groupSort === 'descending' ? this.allGroups.sort(sortGroups).reverse() : this.allGroups; return ( <div className="group-interface"> {this.groupCreationModal} - {this.currentGroup ? - <GroupMemberView - group={this.currentGroup} - onCloseButtonClick={action(() => this.currentGroup = undefined)} - /> - : null} + {this.currentGroup ? <GroupMemberView group={this.currentGroup} onCloseButtonClick={action(() => (this.currentGroup = undefined))} /> : null} <div className="group-heading"> - <p><b>Manage Groups</b></p> - <button onClick={action(() => this.createGroupModalOpen = true)}> - <FontAwesomeIcon icon={"plus"} size={"sm"} /> Create Group + <p> + <b>Manage Groups</b> + </p> + <button onClick={action(() => (this.createGroupModalOpen = true))}> + <FontAwesomeIcon icon={'plus'} size={'sm'} /> Create Group </button> - <div className={"close-button"} onClick={this.close}> - <FontAwesomeIcon icon={"times"} color={"black"} size={"lg"} /> + <div className={'close-button'} onClick={this.close}> + <FontAwesomeIcon icon={'times'} color={'black'} size={'lg'} /> </div> </div> <div className="main-container"> - <div - className="sort-groups" - onClick={action(() => this.groupSort = this.groupSort === "ascending" ? "descending" : this.groupSort === "descending" ? "none" : "ascending")}> - Name {this.groupSort === "ascending" ? <FontAwesomeIcon icon={"caret-up"} size={"xs"} /> - : this.groupSort === "descending" ? <FontAwesomeIcon icon={"caret-down"} size={"xs"} /> - : <FontAwesomeIcon icon={"caret-right"} size={"xs"} /> - } + <div className="sort-groups" onClick={action(() => (this.groupSort = this.groupSort === 'ascending' ? 'descending' : this.groupSort === 'descending' ? 'none' : 'ascending'))}> + Name{' '} + {this.groupSort === 'ascending' ? ( + <FontAwesomeIcon icon={'caret-up'} size={'xs'} /> + ) : this.groupSort === 'descending' ? ( + <FontAwesomeIcon icon={'caret-down'} size={'xs'} /> + ) : ( + <FontAwesomeIcon icon={'caret-right'} size={'xs'} /> + )} </div> <div className="group-body"> - {groups.map(group => - <div - className="group-row" - key={StrCast(group.title || group.groupName)} - > - <div className="group-name" >{group.title || group.groupName}</div> - <div className="group-info" onClick={action(() => this.currentGroup = group)}> - <FontAwesomeIcon icon={"info-circle"} color={"#e8e8e8"} size={"sm"} style={{ backgroundColor: "#1e89d7", borderRadius: "100%", border: "1px solid #1e89d7" }} /> + {groups.map(group => ( + <div className="group-row" key={StrCast(group.title || group.groupName)}> + <div className="group-name">{StrCast(group.title || group.groupName)}</div> + <div className="group-info" onClick={action(() => (this.currentGroup = group))}> + <FontAwesomeIcon icon={'info-circle'} color={'#e8e8e8'} size={'sm'} style={{ backgroundColor: '#1e89d7', borderRadius: '100%', border: '1px solid #1e89d7' }} /> </div> </div> - )} + ))} </div> </div> - </div> ); } render() { - return <MainViewModal - contents={this.groupInterface} - isDisplayed={this.isOpen} - interactive={true} - dialogueBoxStyle={{ zIndex: 1002 }} - overlayStyle={{ zIndex: 1001 }} - closeOnExternalClick={this.close} - />; + return <MainViewModal contents={this.groupInterface} isDisplayed={this.isOpen} interactive={true} dialogueBoxStyle={{ zIndex: 1002 }} overlayStyle={{ zIndex: 1001 }} closeOnExternalClick={this.close} />; } -}
\ No newline at end of file +} diff --git a/src/client/util/History.ts b/src/client/util/History.ts index 7dcff9c56..18aee6444 100644 --- a/src/client/util/History.ts +++ b/src/client/util/History.ts @@ -1,8 +1,8 @@ -import { Doc } from "../../fields/Doc"; -import { DocServer } from "../DocServer"; import * as qs from 'query-string'; -import { Utils, OmitKeys } from "../../Utils"; -import { CurrentUserUtils } from "./CurrentUserUtils"; +import { Doc } from '../../fields/Doc'; +import { OmitKeys, Utils } from '../../Utils'; +import { DocServer } from '../DocServer'; +import { DashboardView } from '../views/DashboardView'; export namespace HistoryUtil { export interface DocInitializerList { @@ -10,7 +10,7 @@ export namespace HistoryUtil { } export interface DocUrl { - type: "doc"; + type: 'doc'; docId: string; initializers?: { [docId: string]: DocInitializerList; @@ -25,11 +25,11 @@ export namespace HistoryUtil { // const handlers: ((state: ParsedUrl | null) => void)[] = []; function onHistory(e: PopStateEvent) { - if (window.location.pathname !== "/home") { - const url = e.state as ParsedUrl || parseUrl(window.location); + if (window.location.pathname !== '/home') { + const url = (e.state as ParsedUrl) || parseUrl(window.location); if (url) { switch (url.type) { - case "doc": + case 'doc': onDocUrl(url); break; } @@ -43,13 +43,13 @@ export namespace HistoryUtil { let _lastStatePush = 0; export function pushState(state: ParsedUrl) { if (Date.now() - _lastStatePush > 1000) { - history.pushState(state, "", createUrl(state)); + history.pushState(state, '', createUrl(state)); } _lastStatePush = Date.now(); } export function replaceState(state: ParsedUrl) { - history.replaceState(state, "", createUrl(state)); + history.replaceState(state, '', createUrl(state)); } function copyState(state: ParsedUrl): ParsedUrl { @@ -61,7 +61,7 @@ export namespace HistoryUtil { if (state) { state.initializers = state.initializers || {}; } - return state ?? {initializers:{}}; + return state ?? { initializers: {} }; } // export function addHandler(handler: (state: ParsedUrl | null) => void) { @@ -78,10 +78,10 @@ export namespace HistoryUtil { const parsers: { [type: string]: (pathname: string[], opts: qs.ParsedQuery) => ParsedUrl | undefined } = {}; const stringifiers: { [type: string]: (state: ParsedUrl) => string } = {}; - type ParserValue = true | "none" | "json" | ((value: string) => any); + type ParserValue = true | 'none' | 'json' | ((value: string) => any); type Parser = { - [key: string]: ParserValue + [key: string]: ParserValue; }; function addParser(type: string, requiredFields: Parser, optionalFields: Parser, customParser?: (pathname: string[], opts: qs.ParsedQuery, current: ParsedUrl) => ParsedUrl | null | undefined) { @@ -90,9 +90,9 @@ export namespace HistoryUtil { return value; } if (Array.isArray(value)) { - } else if (parser === true || parser === "json") { + } else if (parser === true || parser === 'json') { value = JSON.parse(value); - } else if (parser === "none") { + } else if (parser === 'none') { } else { value = parser(value); } @@ -142,29 +142,28 @@ export namespace HistoryUtil { } const queryObj = OmitKeys(state, keys).extract; const query: any = {}; - Object.keys(queryObj).forEach(key => query[key] = queryObj[key] === null ? null : JSON.stringify(queryObj[key])); + Object.keys(queryObj).forEach(key => (query[key] = queryObj[key] === null ? null : JSON.stringify(queryObj[key]))); const queryString = qs.stringify(query); - return path + (queryString ? `?${queryString}` : ""); + return path + (queryString ? `?${queryString}` : ''); }; } - addParser("doc", {}, { readonly: true, initializers: true, nro: true, sharing: true }, (pathname, opts, current) => { + addParser('doc', {}, { readonly: true, initializers: true, nro: true, sharing: true }, (pathname, opts, current) => { if (pathname.length !== 2) return undefined; current.initializers = current.initializers || {}; const docId = pathname[1]; current.docId = docId; }); - addStringifier("doc", ["initializers", "readonly", "nro"], (state, current) => { + addStringifier('doc', ['initializers', 'readonly', 'nro'], (state, current) => { return `${current}/${state.docId}`; }); - export function parseUrl(location: Location | URL): ParsedUrl | undefined { const pathname = location.pathname.substring(1); const search = location.search; const opts = search.length ? qs.parse(search, { sort: false }) : {}; - const pathnameSplit = pathname.split("/"); + const pathnameSplit = pathname.split('/'); const type = pathnameSplit[0]; @@ -179,7 +178,7 @@ export namespace HistoryUtil { if (params.type in stringifiers) { return stringifiers[params.type](params); } - return ""; + return ''; } export async function initDoc(id: string, initializer: DocInitializerList) { @@ -197,7 +196,7 @@ export namespace HistoryUtil { await Promise.all(Object.keys(init).map(id => initDoc(id, init[id]))); } if (field instanceof Doc) { - CurrentUserUtils.openDashboard(field, true); + DashboardView.openDashboard(field, true); } } diff --git a/src/client/util/LinkFollower.ts b/src/client/util/LinkFollower.ts new file mode 100644 index 000000000..f75ac24f5 --- /dev/null +++ b/src/client/util/LinkFollower.ts @@ -0,0 +1,112 @@ +import { action, observable, observe } from 'mobx'; +import { computedFn } from 'mobx-utils'; +import { DirectLinksSym, Doc, DocListCast, DocListCastAsync, Field, Opt } from '../../fields/Doc'; +import { List } from '../../fields/List'; +import { ProxyField } from '../../fields/Proxy'; +import { BoolCast, Cast, StrCast } from '../../fields/Types'; +import { LightboxView } from '../views/LightboxView'; +import { DocumentViewSharedProps, ViewAdjustment } from '../views/nodes/DocumentView'; +import { DocumentManager } from './DocumentManager'; +import { UndoManager } from './UndoManager'; + +type CreateViewFunc = (doc: Doc, followLinkLocation: string, finished?: () => void) => void; +/* + * link doc: + * - anchor1: doc + * - anchor2: doc + * + * group doc: + * - type: string representing the group type/name/category + * - metadata: doc representing the metadata kvps + * + * metadata doc: + * - user defined kvps + */ +export class LinkFollower { + // follows a link - if the target is on screen, it highlights/pans to it. + // if the target isn't onscreen, then it will open up the target in the lightbox, or in place + // depending on the followLinkLocation property of the source (or the link itself as a fallback); + public static FollowLink = (linkDoc: Opt<Doc>, sourceDoc: Doc, docViewProps: DocumentViewSharedProps, altKey: boolean, zoom: boolean = false) => { + const batch = UndoManager.StartBatch('follow link click'); + // open up target if it's not already in view ... + const createViewFunc = (doc: Doc, followLoc: string, finished?: Opt<() => void>) => { + const createTabForTarget = (didFocus: boolean) => + new Promise<ViewAdjustment>(res => { + const where = LightboxView.LightboxDoc ? 'lightbox' : StrCast(sourceDoc.followLinkLocation, followLoc); + docViewProps.addDocTab(doc, where); + setTimeout(() => { + const getFirstDocView = LightboxView.LightboxDoc ? DocumentManager.Instance.getLightboxDocumentView : DocumentManager.Instance.getFirstDocumentView; + const targDocView = getFirstDocView(doc); // get first document view available within the lightbox if that's open, or anywhere otherwise. + if (targDocView) { + targDocView.props.focus(doc, { + willZoom: BoolCast(sourceDoc.followLinkZoom, false), + afterFocus: (didFocus: boolean) => { + finished?.(); + res(ViewAdjustment.resetView); + return new Promise<ViewAdjustment>(res2 => res2(ViewAdjustment.doNothing)); + }, + }); + } else { + res(where !== 'inPlace' ? ViewAdjustment.resetView : ViewAdjustment.doNothing); // for 'inPlace' resetting the initial focus&zoom would negate the zoom into the target + } + }); + }); + + if (!sourceDoc.followLinkZoom) { + createTabForTarget(false); + } else { + // first focus & zoom onto this (the clicked document). Then execute the function to focus on the target + docViewProps.focus(sourceDoc, { willZoom: BoolCast(sourceDoc.followLinkZoom, true), scale: 1, afterFocus: createTabForTarget }); + } + }; + LinkFollower.traverseLink(linkDoc, sourceDoc, createViewFunc, BoolCast(sourceDoc.followLinkZoom, zoom), docViewProps.ContainingCollectionDoc, batch.end, altKey ? true : undefined); + }; + + public static traverseLink(link: Opt<Doc>, sourceDoc: Doc, createViewFunc: CreateViewFunc, zoom = false, currentContext?: Doc, finished?: () => void, traverseBacklink?: boolean) { + const linkDocs = link ? [link] : DocListCast(sourceDoc.links); + const firstDocs = linkDocs.filter(linkDoc => Doc.AreProtosEqual(linkDoc.anchor1 as Doc, sourceDoc) || Doc.AreProtosEqual((linkDoc.anchor1 as Doc).annotationOn as Doc, sourceDoc)); // link docs where 'doc' is anchor1 + const secondDocs = linkDocs.filter(linkDoc => Doc.AreProtosEqual(linkDoc.anchor2 as Doc, sourceDoc) || Doc.AreProtosEqual((linkDoc.anchor2 as Doc).annotationOn as Doc, sourceDoc)); // link docs where 'doc' is anchor2 + const fwdLinkWithoutTargetView = firstDocs.find(d => DocumentManager.Instance.getDocumentViews(d.anchor2 as Doc).length === 0); + const backLinkWithoutTargetView = secondDocs.find(d => DocumentManager.Instance.getDocumentViews(d.anchor1 as Doc).length === 0); + const linkWithoutTargetDoc = traverseBacklink === undefined ? fwdLinkWithoutTargetView || backLinkWithoutTargetView : traverseBacklink ? backLinkWithoutTargetView : fwdLinkWithoutTargetView; + const linkDocList = linkWithoutTargetDoc ? [linkWithoutTargetDoc] : traverseBacklink === undefined ? firstDocs.concat(secondDocs) : traverseBacklink ? secondDocs : firstDocs; + const followLinks = sourceDoc.isPushpin ? linkDocList : linkDocList.slice(0, 1); + var count = 0; + const allFinished = () => ++count === followLinks.length && finished?.(); + followLinks.forEach(async linkDoc => { + if (linkDoc) { + const target = ( + sourceDoc === linkDoc.anchor1 + ? linkDoc.anchor2 + : sourceDoc === linkDoc.anchor2 + ? linkDoc.anchor1 + : Doc.AreProtosEqual(sourceDoc, linkDoc.anchor1 as Doc) || Doc.AreProtosEqual((linkDoc.anchor1 as Doc).annotationOn as Doc, sourceDoc) + ? linkDoc.anchor2 + : linkDoc.anchor1 + ) as Doc; + if (target) { + if (target.TourMap) { + const fieldKey = Doc.LayoutFieldKey(target); + const tour = DocListCast(target[fieldKey]).reverse(); + LightboxView.SetLightboxDoc(currentContext, undefined, tour); + setTimeout(LightboxView.Next); + allFinished(); + } else { + const containerAnnoDoc = Cast(target.annotationOn, Doc, null); + const containerDoc = containerAnnoDoc || target; + var containerDocContext = containerDoc?.context ? [Cast(containerDoc?.context, Doc, null)] : ([] as Doc[]); + while (containerDocContext.length && !DocumentManager.Instance.getDocumentView(containerDocContext[0]) && containerDocContext[0].context) { + containerDocContext = [Cast(containerDocContext[0].context, Doc, null), ...containerDocContext]; + } + const targetContexts = LightboxView.LightboxDoc ? [containerAnnoDoc || containerDocContext[0]].filter(a => a) : containerDocContext; + DocumentManager.Instance.jumpToDocument(target, zoom, (doc, finished) => createViewFunc(doc, StrCast(linkDoc.followLinkLocation, 'lightbox'), finished), targetContexts, linkDoc, undefined, sourceDoc, allFinished); + } + } else { + allFinished(); + } + } else { + allFinished(); + } + }); + } +} diff --git a/src/client/util/LinkManager.ts b/src/client/util/LinkManager.ts index d51cd350d..7a12a8580 100644 --- a/src/client/util/LinkManager.ts +++ b/src/client/util/LinkManager.ts @@ -1,35 +1,30 @@ -import { action, observable, observe } from "mobx"; -import { computedFn } from "mobx-utils"; -import { DirectLinksSym, Doc, DocListCast, DocListCastAsync, Field, Opt } from "../../fields/Doc"; -import { List } from "../../fields/List"; -import { ProxyField } from "../../fields/Proxy"; -import { BoolCast, Cast, StrCast } from "../../fields/Types"; -import { LightboxView } from "../views/LightboxView"; -import { DocumentViewSharedProps, ViewAdjustment } from "../views/nodes/DocumentView"; -import { DocumentManager } from "./DocumentManager"; -import { UndoManager } from "./UndoManager"; - -type CreateViewFunc = (doc: Doc, followLinkLocation: string, finished?: () => void) => void; -/* - * link doc: - * - anchor1: doc +import { action, observable, observe } from 'mobx'; +import { computedFn } from 'mobx-utils'; +import { DirectLinksSym, Doc, DocListCast, DocListCastAsync, Field, Opt } from '../../fields/Doc'; +import { List } from '../../fields/List'; +import { ProxyField } from '../../fields/Proxy'; +import { Cast, StrCast } from '../../fields/Types'; +/* + * link doc: + * - anchor1: doc * - anchor2: doc - * + * * group doc: * - type: string representing the group type/name/category * - metadata: doc representing the metadata kvps - * + * * metadata doc: - * - user defined kvps + * - user defined kvps */ export class LinkManager { - @observable static _instance: LinkManager; @observable static userLinkDBs: Doc[] = []; public static currentLink: Opt<Doc>; - public static get Instance() { return LinkManager._instance; } + public static get Instance() { + return LinkManager._instance; + } public static addLinkDB = (linkDb: any) => LinkManager.userLinkDBs.push(linkDb); - public static AutoKeywords = "keywords:Usages"; + public static AutoKeywords = 'keywords:Usages'; static links: Doc[] = []; constructor() { LinkManager._instance = this; @@ -38,50 +33,61 @@ export class LinkManager { const addLinkToDoc = (link: Doc) => { const a1Prom = link?.anchor1; const a2Prom = link?.anchor2; - Promise.all([a1Prom, a2Prom]).then((all) => { + Promise.all([a1Prom, a2Prom]).then(all => { const a1 = all[0]; const a2 = all[1]; const a1ProtoProm = (link?.anchor1 as Doc)?.proto; const a2ProtoProm = (link?.anchor2 as Doc)?.proto; - Promise.all([a1ProtoProm, a2ProtoProm]).then(action((all) => { - if (a1 instanceof Doc && a2 instanceof Doc && ((a1.author !== undefined && a2.author !== undefined) || link.author === Doc.CurrentUserEmail)) { - Doc.GetProto(a1)[DirectLinksSym].add(link); - Doc.GetProto(a2)[DirectLinksSym].add(link); - Doc.GetProto(link)[DirectLinksSym].add(link); - } - })); + Promise.all([a1ProtoProm, a2ProtoProm]).then( + action(all => { + if (a1 instanceof Doc && a2 instanceof Doc && ((a1.author !== undefined && a2.author !== undefined) || link.author === Doc.CurrentUserEmail)) { + Doc.GetProto(a1)[DirectLinksSym].add(link); + Doc.GetProto(a2)[DirectLinksSym].add(link); + Doc.GetProto(link)[DirectLinksSym].add(link); + } + }) + ); }); }; const remLinkFromDoc = (link: Doc) => { const a1 = link?.anchor1; const a2 = link?.anchor2; - Promise.all([a1, a2]).then(action(() => { - if (a1 instanceof Doc && a2 instanceof Doc && ((a1.author !== undefined && a2.author !== undefined) || link.author === Doc.CurrentUserEmail)) { - Doc.GetProto(a1)[DirectLinksSym].delete(link); - Doc.GetProto(a2)[DirectLinksSym].delete(link); - Doc.GetProto(link)[DirectLinksSym].delete(link); - } - })); + Promise.all([a1, a2]).then( + action(() => { + if (a1 instanceof Doc && a2 instanceof Doc && ((a1.author !== undefined && a2.author !== undefined) || link.author === Doc.CurrentUserEmail)) { + Doc.GetProto(a1)[DirectLinksSym].delete(link); + Doc.GetProto(a2)[DirectLinksSym].delete(link); + Doc.GetProto(link)[DirectLinksSym].delete(link); + } + }) + ); }; const watchUserLinkDB = (userLinkDBDoc: Doc) => { LinkManager.links.push(...DocListCast(userLinkDBDoc.data)); - const toRealField = (field: Field) => field instanceof ProxyField ? field.value() : field; // see List.ts. data structure is not a simple list of Docs, but a list of ProxyField/Fields - observe(userLinkDBDoc.data as Doc, change => { // observe pushes/splices on a user link DB 'data' field (should only happen for local changes) - switch (change.type as any) { - case "splice": - (change as any).added.forEach((link: any) => addLinkToDoc(toRealField(link))); - (change as any).removed.forEach((link: any) => remLinkFromDoc(toRealField(link))); - break; - case "update": //let oldValue = change.oldValue; - } - }, true); - observe(userLinkDBDoc, "data", // obsever when a new array of links is assigned as the link DB 'data' field (should happen whenever a remote user adds/removes a link) + const toRealField = (field: Field) => (field instanceof ProxyField ? field.value() : field); // see List.ts. data structure is not a simple list of Docs, but a list of ProxyField/Fields + observe( + userLinkDBDoc.data as Doc, change => { + // observe pushes/splices on a user link DB 'data' field (should only happen for local changes) switch (change.type as any) { - case "update": - Promise.all([...(change.oldValue as any as Doc[] || []), ...(change.newValue as any as Doc[] || [])]).then(doclist => { - const oldDocs = doclist.slice(0, (change.oldValue as any as Doc[] || []).length); - const newDocs = doclist.slice((change.oldValue as any as Doc[] || []).length, doclist.length); + case 'splice': + (change as any).added.forEach((link: any) => addLinkToDoc(toRealField(link))); + (change as any).removed.forEach((link: any) => remLinkFromDoc(toRealField(link))); + break; + case 'update': //let oldValue = change.oldValue; + } + }, + true + ); + observe( + userLinkDBDoc, + 'data', // obsever when a new array of links is assigned as the link DB 'data' field (should happen whenever a remote user adds/removes a link) + change => { + switch (change.type as any) { + case 'update': + Promise.all([...((change.oldValue as any as Doc[]) || []), ...((change.newValue as any as Doc[]) || [])]).then(doclist => { + const oldDocs = doclist.slice(0, ((change.oldValue as any as Doc[]) || []).length); + const newDocs = doclist.slice(((change.oldValue as any as Doc[]) || []).length, doclist.length); const added = newDocs?.filter(link => !(oldDocs || []).includes(link)); const removed = oldDocs?.filter(link => !(newDocs || []).includes(link)); @@ -89,38 +95,54 @@ export class LinkManager { removed?.forEach((link: any) => remLinkFromDoc(toRealField(link))); }); } - }, true); + }, + true + ); }; - observe(LinkManager.userLinkDBs, change => { - switch (change.type as any) { - case "splice": (change as any).added.forEach(watchUserLinkDB); break; - case "update": //let oldValue = change.oldValue; - } - }, true); + observe( + LinkManager.userLinkDBs, + change => { + switch (change.type as any) { + case 'splice': + (change as any).added.forEach(watchUserLinkDB); + break; + case 'update': //let oldValue = change.oldValue; + } + }, + true + ); LinkManager.addLinkDB(Doc.LinkDBDoc()); - DocListCastAsync(Doc.LinkDBDoc()?.data).then(dblist => dblist?.forEach(async link => { // make sure anchors are loaded to avoid incremental updates to computedFn's in LinkManager - const a1 = await Cast(link?.anchor1, Doc, null); - const a2 = await Cast(link?.anchor2, Doc, null); - })); + DocListCastAsync(Doc.LinkDBDoc()?.data).then(dblist => + dblist?.forEach(async link => { + // make sure anchors are loaded to avoid incremental updates to computedFn's in LinkManager + const a1 = await Cast(link?.anchor1, Doc, null); + const a2 = await Cast(link?.anchor2, Doc, null); + }) + ); } - public createLinkrelationshipLists = () => { //create new lists for link relations and their associated colors if the lists don't already exist !Doc.UserDoc().linkRelationshipList && (Doc.UserDoc().linkRelationshipList = new List<string>()); !Doc.UserDoc().linkColorList && (Doc.UserDoc().linkColorList = new List<string>()); !Doc.UserDoc().linkRelationshipSizes && (Doc.UserDoc().linkRelationshipSizes = new List<number>()); - } + }; public addLink(linkDoc: Doc, checkExists = false) { if (!checkExists || !DocListCast(Doc.LinkDBDoc().data).includes(linkDoc)) { - Doc.AddDocToList(Doc.LinkDBDoc(), "data", linkDoc); + Doc.AddDocToList(Doc.LinkDBDoc(), 'data', linkDoc); } } - public deleteLink(linkDoc: Doc) { return Doc.RemoveDocFromList(Doc.LinkDBDoc(), "data", linkDoc); } - public deleteAllLinksOnAnchor(anchor: Doc) { LinkManager.Instance.relatedLinker(anchor).forEach(linkDoc => LinkManager.Instance.deleteLink(linkDoc)); } + public deleteLink(linkDoc: Doc) { + return Doc.RemoveDocFromList(Doc.LinkDBDoc(), 'data', linkDoc); + } + public deleteAllLinksOnAnchor(anchor: Doc) { + LinkManager.Instance.relatedLinker(anchor).forEach(linkDoc => LinkManager.Instance.deleteLink(linkDoc)); + } - public getAllRelatedLinks(anchor: Doc) { return this.relatedLinker(anchor); } // finds all links that contain the given anchor + public getAllRelatedLinks(anchor: Doc) { + return this.relatedLinker(anchor); + } // finds all links that contain the given anchor public getAllDirectLinks(anchor: Doc): Doc[] { // FIXME:glr Why is Doc undefined? if (Doc.GetProto(anchor)[DirectLinksSym]) { @@ -133,18 +155,16 @@ export class LinkManager { relatedLinker = computedFn(function relatedLinker(this: any, anchor: Doc): Doc[] { const lfield = Doc.LayoutFieldKey(anchor); if (!anchor || anchor instanceof Promise || Doc.GetProto(anchor) instanceof Promise) { - console.log("WAITING FOR DOC/PROTO IN LINKMANAGER"); + console.log('WAITING FOR DOC/PROTO IN LINKMANAGER'); return []; } const dirLinks = Doc.GetProto(anchor)[DirectLinksSym]; - const annos = DocListCast(anchor[lfield + "-annotations"]); - const timelineAnnos = DocListCast(anchor[lfield + "-annotations-timeline"]); + const annos = DocListCast(anchor[lfield + '-annotations']); + const timelineAnnos = DocListCast(anchor[lfield + '-annotations-timeline']); if (!annos || !timelineAnnos) { debugger; } - const related = [...annos, ...timelineAnnos].reduce((list, anno) => - [...list, ...LinkManager.Instance.relatedLinker(anno)], - Array.from(dirLinks).slice()); + const related = [...annos, ...timelineAnnos].reduce((list, anno) => [...list, ...LinkManager.Instance.relatedLinker(anno)], Array.from(dirLinks).slice()); return related; }, true); @@ -152,15 +172,15 @@ export class LinkManager { public getRelatedGroupedLinks(anchor: Doc): Map<string, Array<Doc>> { const anchorGroups = new Map<string, Array<Doc>>(); this.relatedLinker(anchor).forEach(link => { - if (link.linkRelationship && link.linkRelationship !== "-ungrouped-") { + if (link.linkRelationship && link.linkRelationship !== '-ungrouped-') { const relation = StrCast(link.linkRelationship); - const anchorRelation = relation.indexOf(":") !== -1 ? relation.split(":")[Doc.AreProtosEqual(Cast(link.anchor1, Doc, null), anchor) ? 0 : 1] : relation; + const anchorRelation = relation.indexOf(':') !== -1 ? relation.split(':')[Doc.AreProtosEqual(Cast(link.anchor1, Doc, null), anchor) ? 0 : 1] : relation; const group = anchorGroups.get(anchorRelation); anchorGroups.set(anchorRelation, group ? [...group, link] : [link]); } else { // if link is in no groups then put it in default group - const group = anchorGroups.get("*"); - anchorGroups.set("*", group ? [...group, link] : [link]); + const group = anchorGroups.get('*'); + anchorGroups.set('*', group ? [...group, link] : [link]); } }); return anchorGroups; @@ -178,85 +198,4 @@ export class LinkManager { if (Doc.AreProtosEqual(anchor, a2.annotationOn as Doc)) return a1; if (Doc.AreProtosEqual(anchor, linkDoc)) return linkDoc; } - - - // follows a link - if the target is on screen, it highlights/pans to it. - // if the target isn't onscreen, then it will open up the target in the lightbox, or in place - // depending on the followLinkLocation property of the source (or the link itself as a fallback); - public static FollowLink = (linkDoc: Opt<Doc>, sourceDoc: Doc, docViewProps: DocumentViewSharedProps, altKey: boolean, zoom: boolean = false) => { - const batch = UndoManager.StartBatch("follow link click"); - // open up target if it's not already in view ... - const createViewFunc = (doc: Doc, followLoc: string, finished?: Opt<() => void>) => { - const createTabForTarget = (didFocus: boolean) => new Promise<ViewAdjustment>(res => { - const where = LightboxView.LightboxDoc ? "lightbox" : StrCast(sourceDoc.followLinkLocation, followLoc); - docViewProps.addDocTab(doc, where); - setTimeout(() => { - const getFirstDocView = LightboxView.LightboxDoc ? DocumentManager.Instance.getLightboxDocumentView : DocumentManager.Instance.getFirstDocumentView; - const targDocView = getFirstDocView(doc); // get first document view available within the lightbox if that's open, or anywhere otherwise. - if (targDocView) { - targDocView.props.focus(doc, { - willZoom: BoolCast(sourceDoc.followLinkZoom, false), - afterFocus: (didFocus: boolean) => { - finished?.(); - res(ViewAdjustment.resetView); - return new Promise<ViewAdjustment>(res2 => res2(ViewAdjustment.doNothing)); - } - }); - } else { - res(where !== "inPlace" ? ViewAdjustment.resetView : ViewAdjustment.doNothing); // for 'inPlace' resetting the initial focus&zoom would negate the zoom into the target - } - }); - }); - - if (!sourceDoc.followLinkZoom) { - createTabForTarget(false); - } else { - // first focus & zoom onto this (the clicked document). Then execute the function to focus on the target - docViewProps.focus(sourceDoc, { willZoom: BoolCast(sourceDoc.followLinkZoom, true), scale: 1, afterFocus: createTabForTarget }); - } - }; - LinkManager.traverseLink(linkDoc, sourceDoc, createViewFunc, BoolCast(sourceDoc.followLinkZoom, zoom), docViewProps.ContainingCollectionDoc, batch.end, altKey ? true : undefined); - } - - public static traverseLink(link: Opt<Doc>, sourceDoc: Doc, createViewFunc: CreateViewFunc, zoom = false, currentContext?: Doc, finished?: () => void, traverseBacklink?: boolean) { - const linkDocs = link ? [link] : DocListCast(sourceDoc.links); - const firstDocs = linkDocs.filter(linkDoc => Doc.AreProtosEqual(linkDoc.anchor1 as Doc, sourceDoc) || Doc.AreProtosEqual((linkDoc.anchor1 as Doc).annotationOn as Doc, sourceDoc)); // link docs where 'doc' is anchor1 - const secondDocs = linkDocs.filter(linkDoc => Doc.AreProtosEqual(linkDoc.anchor2 as Doc, sourceDoc) || Doc.AreProtosEqual((linkDoc.anchor2 as Doc).annotationOn as Doc, sourceDoc)); // link docs where 'doc' is anchor2 - const fwdLinkWithoutTargetView = firstDocs.find(d => DocumentManager.Instance.getDocumentViews(d.anchor2 as Doc).length === 0); - const backLinkWithoutTargetView = secondDocs.find(d => DocumentManager.Instance.getDocumentViews(d.anchor1 as Doc).length === 0); - const linkWithoutTargetDoc = traverseBacklink === undefined ? fwdLinkWithoutTargetView || backLinkWithoutTargetView : traverseBacklink ? backLinkWithoutTargetView : fwdLinkWithoutTargetView; - const linkDocList = linkWithoutTargetDoc ? [linkWithoutTargetDoc] : (traverseBacklink === undefined ? firstDocs.concat(secondDocs) : traverseBacklink ? secondDocs : firstDocs); - const followLinks = sourceDoc.isPushpin ? linkDocList : linkDocList.slice(0, 1); - var count = 0; - const allFinished = () => ++count === followLinks.length && finished?.(); - followLinks.forEach(async linkDoc => { - if (linkDoc) { - const target = (sourceDoc === linkDoc.anchor1 ? linkDoc.anchor2 : sourceDoc === linkDoc.anchor2 ? linkDoc.anchor1 : - (Doc.AreProtosEqual(sourceDoc, linkDoc.anchor1 as Doc) || Doc.AreProtosEqual((linkDoc.anchor1 as Doc).annotationOn as Doc, sourceDoc) ? linkDoc.anchor2 : linkDoc.anchor1)) as Doc; - if (target) { - if (target.TourMap) { - const fieldKey = Doc.LayoutFieldKey(target); - const tour = DocListCast(target[fieldKey]).reverse(); - LightboxView.SetLightboxDoc(currentContext, undefined, tour); - setTimeout(LightboxView.Next); - allFinished(); - } else { - const containerAnnoDoc = Cast(target.annotationOn, Doc, null); - const containerDoc = containerAnnoDoc || target; - var containerDocContext = containerDoc?.context ? [Cast(containerDoc?.context, Doc, null)] : [] as Doc[]; - while (containerDocContext.length && !DocumentManager.Instance.getDocumentView(containerDocContext[0]) && containerDocContext[0].context) { - containerDocContext = [Cast(containerDocContext[0].context, Doc, null), ...containerDocContext]; - } - const targetContexts = LightboxView.LightboxDoc ? [containerAnnoDoc || containerDocContext[0]].filter(a => a) : containerDocContext; - DocumentManager.Instance.jumpToDocument(target, zoom, (doc, finished) => createViewFunc(doc, StrCast(linkDoc.followLinkLocation, "lightbox"), finished), targetContexts, linkDoc, undefined, sourceDoc, allFinished); - } - } else { - allFinished(); - } - } else { - allFinished(); - } - }); - } - -}
\ No newline at end of file +} diff --git a/src/client/util/RecordingApi.ts b/src/client/util/RecordingApi.ts new file mode 100644 index 000000000..7bffb0379 --- /dev/null +++ b/src/client/util/RecordingApi.ts @@ -0,0 +1,269 @@ +import { CollectionFreeFormView } from '../views/collections/collectionFreeForm'; +import { IReactionDisposer, observable, reaction } from 'mobx'; +import { NumCast } from '../../fields/Types'; +import { Doc } from '../../fields/Doc'; +import { VideoBox } from '../views/nodes/VideoBox'; +import { scaleDiverging } from 'd3-scale'; +import { Transform } from './Transform'; + +type Movement = { + time: number; + panX: number; + panY: number; + scale: number; +}; + +type Presentation = { + movements: Array<Movement> | null; + meta: Object; +}; + +export class RecordingApi { + private static NULL_PRESENTATION: Presentation = { + movements: null, + meta: {}, + }; + + // instance variables + private currentPresentation: Presentation; + private isRecording: boolean; + private absoluteStart: number; + + // create static instance and getter for global use + @observable static _instance: RecordingApi; + public static get Instance(): RecordingApi { + return RecordingApi._instance; + } + public constructor() { + // init the global instance + RecordingApi._instance = this; + + // init the instance variables + this.currentPresentation = RecordingApi.NULL_PRESENTATION; + this.isRecording = false; + this.absoluteStart = -1; + + // used for tracking movements in the view frame + this.disposeFunc = null; + this.recordingFFView = null; + + // for now, set playFFView + this.playFFView = null; + this.timers = null; + } + + // little helper :) + private get isInitPresenation(): boolean { + return this.currentPresentation.movements === null; + } + + public start = (meta?: Object): Error | undefined => { + // check if already init a presentation + if (!this.isInitPresenation) { + console.error('[recordingApi.ts] start() failed: current presentation data exists. please call clear() first.'); + return new Error('[recordingApi.ts] start()'); + } + + // update the presentation mode + Doc.UserDoc().presentationMode = 'recording'; + + // (1a) get start date for presenation + const startDate = new Date(); + // (1b) set start timestamp to absolute timestamp + this.absoluteStart = startDate.getTime(); + + // (2) assign meta content if it exists + this.currentPresentation.meta = meta || {}; + // (3) assign start date to currentPresenation + this.currentPresentation.movements = []; + // (4) set isRecording true to allow trackMovements + this.isRecording = true; + }; + + public clear = (): Error | Presentation => { + // TODO: maybe archive the data? + if (this.isRecording) { + console.error('[recordingApi.ts] clear() failed: currently recording presentation. call pause() first'); + return new Error('[recordingApi.ts] clear()'); + } + + // update the presentation mode + Doc.UserDoc().presentationMode = 'none'; + // set the previus recording view to the play view + this.playFFView = this.recordingFFView; + + const presCopy = { ...this.currentPresentation }; + + // clear presenation data + this.currentPresentation = RecordingApi.NULL_PRESENTATION; + // clear isRecording + this.isRecording = false; + // clear absoluteStart + this.absoluteStart = -1; + // clear the disposeFunc + this.removeRecordingFFView(); + + return presCopy; + }; + + public pause = (): Error | undefined => { + if (this.isInitPresenation) { + console.error('[recordingApi.ts] pause() failed: no presentation started. try calling init() first'); + return new Error('[recordingApi.ts] pause(): no presentation'); + } + // don't allow track movments + this.isRecording = false; + + // set adjust absoluteStart to add the time difference + const timestamp = new Date().getTime(); + this.absoluteStart = timestamp - this.absoluteStart; + }; + + public resume = () => { + this.isRecording = true; + // set absoluteStart to the difference in time + this.absoluteStart = new Date().getTime() - this.absoluteStart; + }; + + private trackMovements = (panX: number, panY: number, scale: number = 0): Error | undefined => { + // ensure we are recording + if (!this.isRecording) { + return new Error('[recordingApi.ts] trackMovements()'); + } + // check to see if the presetation is init + if (this.isInitPresenation) { + return new Error('[recordingApi.ts] trackMovements(): no presentation'); + } + + // get the time + const time = new Date().getTime() - this.absoluteStart; + // make new movement object + const movement: Movement = { time, panX, panY, scale }; + + // add that movement to the current presentation data's movement array + this.currentPresentation.movements && this.currentPresentation.movements.push(movement); + }; + + // instance variable for the FFView + private disposeFunc: IReactionDisposer | null; + private recordingFFView: CollectionFreeFormView | null; + + // set the FFView that will be used in a reaction to track the movements + public setRecordingFFView = (view: CollectionFreeFormView): void => { + // set the view to the current view + if (view === this.recordingFFView || view == null) return; + + // this.recordingFFView = view; + // set the reaction to track the movements + this.disposeFunc = reaction( + () => ({ x: NumCast(view.Document.panX, -1), y: NumCast(view.Document.panY, -1), scale: NumCast(view.Document.viewScale, -1) }), + res => res.x !== -1 && res.y !== -1 && this.isRecording && this.trackMovements(res.x, res.y, res.scale) + ); + + // for now, set the most recent recordingFFView to the playFFView + this.recordingFFView = view; + }; + + // call on dispose function to stop tracking movements + public removeRecordingFFView = (): void => { + this.disposeFunc?.(); + this.disposeFunc = null; + }; + + // TODO: extract this into different class with pause and resume recording + // TODO: store the FFview with the movements + private playFFView: CollectionFreeFormView | null; + private timers: NodeJS.Timeout[] | null; + + public setPlayFFView = (view: CollectionFreeFormView): void => { + this.playFFView = view; + }; + + // pausing movements will dispose all timers that are planned to replay the movements + // play movemvents will recreate them when the user resumes the presentation + public pauseMovements = (): undefined | Error => { + if (this.playFFView === null) { + return new Error('[recordingApi.ts] pauseMovements() failed: no view'); + } + + if (!this._isPlaying) { + //return new Error('[recordingApi.ts] pauseMovements() failed: not playing') + return; + } + this._isPlaying = false; + // TODO: set userdoc presentMode to browsing + this.timers?.map(timer => clearTimeout(timer)); + + // this.videoBox = null; + }; + + private videoBox: VideoBox | null = null; + + // by calling pause on the VideoBox, the pauseMovements will be called + public pauseVideoAndMovements = (): boolean => { + this.videoBox?.Pause(); + + this.pauseMovements(); + return this.videoBox == null; + }; + + public _isPlaying = false; + + public playMovements = (presentation: Presentation, timeViewed: number = 0, videoBox?: VideoBox): undefined | Error => { + if (presentation.movements === null || this.playFFView === null) { + return new Error('[recordingApi.ts] followMovements() failed: no presentation data or no view'); + } + if (this._isPlaying) return; + + this._isPlaying = true; + Doc.UserDoc().presentationMode = 'watching'; + + // TODO: consider this bug at the end of the clip on seek + this.videoBox = videoBox || null; + + // only get the movements that are remaining in the video time left + const filteredMovements = presentation.movements.filter(movement => movement.time > timeViewed * 1000); + + // helper to replay a movement + const document = this.playFFView; + let preScale = -1; + const zoomAndPan = (movement: Movement) => { + const { panX, panY, scale } = movement; + scale !== -1 && preScale !== scale && document.zoomSmoothlyAboutPt([panX, panY], scale, 0); + document.Document._panX = panX; + document.Document._panY = panY; + + preScale = scale; + }; + + // set the first frame to be at the start of the pres + zoomAndPan(filteredMovements[0]); + + // make timers that will execute each movement at the correct replay time + this.timers = filteredMovements.map(movement => { + const timeDiff = movement.time - timeViewed * 1000; + return setTimeout(() => { + // replay the movement + zoomAndPan(movement); + // if last movement, presentation is done -> set the instance var + if (movement === filteredMovements[filteredMovements.length - 1]) RecordingApi.Instance._isPlaying = false; + }, timeDiff); + }); + }; + + // Unfinished code for tracing multiple free form views + // export let pres: Map<CollectionFreeFormView, IReactionDisposer> = new Map() + + // export function AddRecordingFFView(ffView: CollectionFreeFormView): void { + // pres.set(ffView, + // reaction(() => ({ x: ffView.panX, y: ffView.panY }), + // (pt) => RecordingApi.trackMovements(ffView, pt.x, pt.y))) + // ) + // } + + // export function RemoveRecordingFFView(ffView: CollectionFreeFormView): void { + // const disposer = pres.get(ffView); + // disposer?.(); + // pres.delete(ffView) + // } +} diff --git a/src/client/util/ScrollBox.tsx b/src/client/util/ScrollBox.tsx index a209874a3..d4620ae3f 100644 --- a/src/client/util/ScrollBox.tsx +++ b/src/client/util/ScrollBox.tsx @@ -1,21 +1,24 @@ -import React = require("react"); +import React = require('react'); -export class ScrollBox extends React.Component { +export class ScrollBox extends React.Component<React.PropsWithChildren<{}>> { onWheel = (e: React.WheelEvent) => { - if (e.currentTarget.scrollHeight > e.currentTarget.clientHeight) { // If the element has a scroll bar, then we don't want the containing collection to zoom + if (e.currentTarget.scrollHeight > e.currentTarget.clientHeight) { + // If the element has a scroll bar, then we don't want the containing collection to zoom e.stopPropagation(); } - } + }; render() { return ( - <div style={{ - overflow: "auto", - width: "100%", - height: "100%", - }} onWheel={this.onWheel}> + <div + style={{ + overflow: 'auto', + width: '100%', + height: '100%', + }} + onWheel={this.onWheel}> {this.props.children} </div> ); } -}
\ No newline at end of file +} diff --git a/src/client/util/SelectionManager.ts b/src/client/util/SelectionManager.ts index b71086561..1c84af94a 100644 --- a/src/client/util/SelectionManager.ts +++ b/src/client/util/SelectionManager.ts @@ -1,15 +1,13 @@ -import { action, observable, ObservableMap } from "mobx"; -import { computedFn } from "mobx-utils"; -import { Doc, Opt } from "../../fields/Doc"; -import { DocumentType } from "../documents/DocumentTypes"; -import { CollectionViewType } from "../views/collections/CollectionView"; -import { DocumentView } from "../views/nodes/DocumentView"; -import { CurrentUserUtils } from "./CurrentUserUtils"; +import { action, observable, ObservableMap } from 'mobx'; +import { computedFn } from 'mobx-utils'; +import { Doc, Opt } from '../../fields/Doc'; +import { DocCast } from '../../fields/Types'; +import { CollectionViewType, DocumentType } from '../documents/DocumentTypes'; +import { DocumentView } from '../views/nodes/DocumentView'; +import { ScriptingGlobals } from './ScriptingGlobals'; export namespace SelectionManager { - class Manager { - @observable IsDragging: boolean = false; SelectedViews: ObservableMap<DocumentView, Doc> = new ObservableMap(); @observable SelectedSchemaDocument: Doc | undefined; @@ -63,16 +61,21 @@ export namespace SelectionManager { manager.SelectSchemaViewDoc(document); } - const IsSelectedCache = computedFn(function isSelected(doc: DocumentView) { // wrapping get() in a computedFn only generates mobx() invalidations when the return value of the function for the specific get parameters has changed + const IsSelectedCache = computedFn(function isSelected(doc: DocumentView) { + // wrapping get() in a computedFn only generates mobx() invalidations when the return value of the function for the specific get parameters has changed return manager.SelectedViews.get(doc) ? true : false; }); // computed functions, such as used in IsSelected generate errors if they're called outside of a // reaction context. Specifying the context with 'outsideReaction' allows an efficiency feature // to avoid unnecessary mobx invalidations when running inside a reaction. export function IsSelected(doc: DocumentView | undefined, outsideReaction?: boolean): boolean { - return !doc ? false : outsideReaction ? - manager.SelectedViews.get(doc) ? true : false : // get() accesses a hashtable -- setting anything in the hashtable generates a mobx invalidation for every get() - IsSelectedCache(doc); + return !doc + ? false + : outsideReaction + ? manager.SelectedViews.get(doc) + ? true + : false // get() accesses a hashtable -- setting anything in the hashtable generates a mobx invalidation for every get() + : IsSelectedCache(doc); } export function DeselectAll(except?: Doc): void { @@ -96,4 +99,8 @@ export namespace SelectionManager { export function Docs(): Doc[] { return Array.from(manager.SelectedViews.values()).filter(doc => doc?._viewType !== CollectionViewType.Docking); } -}
\ No newline at end of file +} +ScriptingGlobals.add(function SelectionManager_selectedDocType(docType?: DocumentType, colType?: CollectionViewType, checkContext?: boolean) { + let selected = (sel => (checkContext ? DocCast(sel?.context) : sel))(SelectionManager.SelectedSchemaDoc() ?? SelectionManager.Docs().lastElement()); + return docType ? selected?.type === docType : colType ? selected?.viewType === colType : true; +}); diff --git a/src/client/util/SettingsManager.tsx b/src/client/util/SettingsManager.tsx index 080237649..12d1793af 100644 --- a/src/client/util/SettingsManager.tsx +++ b/src/client/util/SettingsManager.tsx @@ -1,29 +1,28 @@ -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { action, computed, observable, runInAction } from "mobx"; -import { observer } from "mobx-react"; -import * as React from "react"; -import { ColorState, SketchPicker } from "react-color"; -import { Doc } from "../../fields/Doc"; -import { BoolCast, StrCast, Cast } from "../../fields/Types"; -import { addStyleSheet, addStyleSheetRule, Utils } from "../../Utils"; -import { GoogleAuthenticationManager } from "../apis/GoogleAuthenticationManager"; -import { DocServer } from "../DocServer"; -import { Networking } from "../Network"; -import { MainViewModal } from "../views/MainViewModal"; -import { FontIconBox } from "../views/nodes/button/FontIconBox"; -import { CurrentUserUtils } from "./CurrentUserUtils"; -import { DragManager } from "./DragManager"; -import { GroupManager } from "./GroupManager"; -import "./SettingsManager.scss"; -import { undoBatch } from "./UndoManager"; -const higflyout = require("@hig/flyout"); +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { action, computed, observable, runInAction } from 'mobx'; +import { observer } from 'mobx-react'; +import * as React from 'react'; +import { ColorState, SketchPicker } from 'react-color'; +import { Doc } from '../../fields/Doc'; +import { BoolCast, Cast, StrCast } from '../../fields/Types'; +import { addStyleSheet, addStyleSheetRule, Utils } from '../../Utils'; +import { GoogleAuthenticationManager } from '../apis/GoogleAuthenticationManager'; +import { DocServer } from '../DocServer'; +import { Networking } from '../Network'; +import { MainViewModal } from '../views/MainViewModal'; +import { FontIconBox } from '../views/nodes/button/FontIconBox'; +import { DragManager } from './DragManager'; +import { GroupManager } from './GroupManager'; +import './SettingsManager.scss'; +import { undoBatch } from './UndoManager'; +const higflyout = require('@hig/flyout'); export const { anchorPoints } = higflyout; export const Flyout = higflyout.default; export enum ColorScheme { - Dark = "-Dark", - Light = "-Light", - System = "-MatchSystem" + Dark = '-Dark', + Light = '-Light', + System = '-MatchSystem', } @observer @@ -31,67 +30,77 @@ export class SettingsManager extends React.Component<{}> { public static Instance: SettingsManager; static _settingsStyle = addStyleSheet(); @observable private isOpen = false; - @observable private passwordResultText = ""; + @observable private passwordResultText = ''; @observable private playgroundMode = false; - @observable private curr_password = ""; - @observable private new_password = ""; - @observable private new_confirm = ""; - @observable activeTab = "Accounts"; + @observable private curr_password = ''; + @observable private new_password = ''; + @observable private new_confirm = ''; + @observable activeTab = 'Accounts'; - @computed get backgroundColor() { return Doc.UserDoc().activeCollectionBackground; } - @computed get colorScheme() { return CurrentUserUtils.ActiveDashboard?.colorScheme; } + @observable public static propertiesWidth: number = 0; + @observable public static headerBarHeight: number = 0; + + @computed get backgroundColor() { + return Doc.UserDoc().activeCollectionBackground; + } + @computed get colorScheme() { + return Doc.ActiveDashboard?.colorScheme; + } constructor(props: {}) { super(props); SettingsManager.Instance = this; } - public close = action(() => this.isOpen = false); - public open = action(() => this.isOpen = true); + public close = action(() => (this.isOpen = false)); + public open = action(() => (this.isOpen = true)); private googleAuthorize = action(() => GoogleAuthenticationManager.Instance.fetchOrGenerateAccessToken(true)); private changePassword = async () => { if (!(this.curr_password && this.new_password && this.new_confirm)) { - runInAction(() => this.passwordResultText = "Error: Hey, we're missing some fields!"); + runInAction(() => (this.passwordResultText = "Error: Hey, we're missing some fields!")); } else { const passwordBundle = { curr_pass: this.curr_password, new_pass: this.new_password, new_confirm: this.new_confirm }; const { error } = await Networking.PostToServer('/internalResetPassword', passwordBundle); - runInAction(() => this.passwordResultText = error ? "Error: " + error[0].msg + "..." : "Password successfully updated!"); + runInAction(() => (this.passwordResultText = error ? 'Error: ' + error[0].msg + '...' : 'Password successfully updated!')); } - } - - @undoBatch selectUserMode = action((e: React.ChangeEvent) => Doc.noviceMode = (e.currentTarget as any)?.value === "Novice"); - @undoBatch changeShowTitle = action((e: React.ChangeEvent) => Doc.UserDoc().showTitle = (e.currentTarget as any).value ? "title" : undefined); - @undoBatch changeFontFamily = action((e: React.ChangeEvent) => Doc.UserDoc().fontFamily = (e.currentTarget as any).value); - @undoBatch changeFontSize = action((e: React.ChangeEvent) => Doc.UserDoc().fontSize = (e.currentTarget as any).value); - @undoBatch switchActiveBackgroundColor = action((color: ColorState) => Doc.UserDoc().activeCollectionBackground = String(color.hex)); - @undoBatch switchUserColor = action((color: ColorState) => { Doc.SharingDoc().userColor = undefined; Doc.GetProto(Doc.SharingDoc()).userColor = String(color.hex); }); + }; + + @undoBatch selectUserMode = action((e: React.ChangeEvent) => (Doc.noviceMode = (e.currentTarget as any)?.value === 'Novice')); + @undoBatch changeShowTitle = action((e: React.ChangeEvent) => (Doc.UserDoc().showTitle = (e.currentTarget as any).value ? 'title' : undefined)); + @undoBatch changeFontFamily = action((e: React.ChangeEvent) => (Doc.UserDoc().fontFamily = (e.currentTarget as any).value)); + @undoBatch changeFontSize = action((e: React.ChangeEvent) => (Doc.UserDoc().fontSize = (e.currentTarget as any).value)); + @undoBatch switchActiveBackgroundColor = action((color: ColorState) => (Doc.UserDoc().activeCollectionBackground = String(color.hex))); + @undoBatch switchUserColor = action((color: ColorState) => { + Doc.SharingDoc().userColor = undefined; + Doc.GetProto(Doc.SharingDoc()).userColor = String(color.hex); + }); @undoBatch playgroundModeToggle = action(() => { this.playgroundMode = !this.playgroundMode; if (this.playgroundMode) { DocServer.Control.makeReadOnly(); - addStyleSheetRule(SettingsManager._settingsStyle, "topbar-inner-container", { background: "red !important" }); - } - else DocServer.Control.makeEditable(); + addStyleSheetRule(SettingsManager._settingsStyle, 'topbar-inner-container', { background: 'red !important' }); + } else DocServer.Control.makeEditable(); }); @undoBatch @action changeColorScheme = action((e: React.ChangeEvent) => { - const activeDashboard= CurrentUserUtils.ActiveDashboard; + const activeDashboard = Doc.ActiveDashboard; if (!activeDashboard) return; const scheme: ColorScheme = (e.currentTarget as any).value; switch (scheme) { case ColorScheme.Light: activeDashboard.colorScheme = undefined; // undefined means ColorScheme.Light until all CSS is updated with values for each color scheme (e.g., see MainView.scss, DocumentDecorations.scss) - addStyleSheetRule(SettingsManager._settingsStyle, "lm_header", { background: "#d3d3d3 !important" }); + addStyleSheetRule(SettingsManager._settingsStyle, 'lm_header', { background: '#d3d3d3 !important' }); break; case ColorScheme.Dark: activeDashboard.colorScheme = ColorScheme.Dark; - addStyleSheetRule(SettingsManager._settingsStyle, "lm_header", { background: "black !important" }); + addStyleSheetRule(SettingsManager._settingsStyle, 'lm_header', { background: 'black !important' }); break; - case ColorScheme.System: default: + case ColorScheme.System: + default: window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', e => { activeDashboard.colorScheme = e.matches ? ColorScheme.Dark : undefined; // undefined means ColorScheme.Light until all CSS is updated with values for each color scheme (e.g., see MainView.scss, DocumentDecorations.scss) }); @@ -99,223 +108,285 @@ export class SettingsManager extends React.Component<{}> { } }); - @computed get colorsContent() { - const colorBox = (func: (color: ColorState) => void) => <SketchPicker onChange={func} color={StrCast(this.backgroundColor)} - presetColors={['#D0021B', '#F5A623', '#F8E71C', '#8B572A', '#7ED321', '#417505', - '#9013FE', '#4A90E2', '#50E3C2', '#B8E986', '#000000', '#4A4A4A', '#9B9B9B', - '#FFFFFF', '#f1efeb', 'transparent']} />; - - const colorFlyout = <div className="colorFlyout"> - <Flyout anchorPoint={anchorPoints.LEFT_TOP} content={colorBox(this.switchActiveBackgroundColor)}> - <div className="colorFlyout-button" style={{ backgroundColor: StrCast(this.backgroundColor) }} onPointerDown={e => e.stopPropagation()} > - <FontAwesomeIcon icon="palette" size="sm" color={StrCast(this.backgroundColor)} /> - </div> - </Flyout> - </div>; - const userColorFlyout = <div className="colorFlyout"> - <Flyout anchorPoint={anchorPoints.LEFT_TOP} content={colorBox(this.switchUserColor)}> - <div className="colorFlyout-button" style={{ backgroundColor: StrCast(this.backgroundColor) }} onPointerDown={e => e.stopPropagation()} > - <FontAwesomeIcon icon="palette" size="sm" color={StrCast(this.backgroundColor)} /> - </div> - </Flyout> - </div>; + const colorBox = (func: (color: ColorState) => void) => ( + <SketchPicker + onChange={func} + color={StrCast(this.backgroundColor)} + presetColors={['#D0021B', '#F5A623', '#F8E71C', '#8B572A', '#7ED321', '#417505', '#9013FE', '#4A90E2', '#50E3C2', '#B8E986', '#000000', '#4A4A4A', '#9B9B9B', '#FFFFFF', '#f1efeb', 'transparent']} + /> + ); + + const colorFlyout = ( + <div className="colorFlyout"> + <Flyout anchorPoint={anchorPoints.LEFT_TOP} content={colorBox(this.switchActiveBackgroundColor)}> + <div className="colorFlyout-button" style={{ backgroundColor: StrCast(this.backgroundColor) }} onPointerDown={e => e.stopPropagation()}> + <FontAwesomeIcon icon="palette" size="sm" color={StrCast(this.backgroundColor)} /> + </div> + </Flyout> + </div> + ); + const userColorFlyout = ( + <div className="colorFlyout"> + <Flyout anchorPoint={anchorPoints.LEFT_TOP} content={colorBox(this.switchUserColor)}> + <div className="colorFlyout-button" style={{ backgroundColor: StrCast(this.backgroundColor) }} onPointerDown={e => e.stopPropagation()}> + <FontAwesomeIcon icon="palette" size="sm" color={StrCast(this.backgroundColor)} /> + </div> + </Flyout> + </div> + ); const colorSchemes = [ColorScheme.Light, ColorScheme.Dark, ColorScheme.System]; - const schemeMap = ["Light", "Dark", "Match system"]; + const schemeMap = ['Light', 'Dark', 'Match system']; - return <div className="colors-content"> - <div className="preferences-color"> - <div className="preferences-color-text">Background Color</div> - {colorFlyout} - </div> - <div className="preferences-color"> - <div className="preferences-color-text">Border/Header Color</div> - {userColorFlyout} - </div> - <div className="preferences-colorScheme"> - <div className="preferences-color-text">Color Scheme</div> - <div className="preferences-color-controls"> - <select className="scheme-select" onChange={this.changeColorScheme} defaultValue={StrCast(CurrentUserUtils.ActiveDashboard?.colorScheme)}> - {colorSchemes.map((scheme, i) => <option key={scheme} value={scheme}> {schemeMap[i]} </option>)} - </select> + return ( + <div className="colors-content"> + <div className="preferences-color"> + <div className="preferences-color-text">Background Color</div> + {colorFlyout} + </div> + <div className="preferences-color"> + <div className="preferences-color-text">Border/Header Color</div> + {userColorFlyout} + </div> + <div className="preferences-colorScheme"> + <div className="preferences-color-text">Color Scheme</div> + <div className="preferences-color-controls"> + <select className="scheme-select" onChange={this.changeColorScheme} defaultValue={StrCast(Doc.ActiveDashboard?.colorScheme)}> + {colorSchemes.map((scheme, i) => ( + <option key={scheme} value={scheme}> + {' '} + {schemeMap[i]}{' '} + </option> + ))} + </select> + </div> </div> </div> - </div>; + ); } @computed get formatsContent() { - return <div className="prefs-content"> - <div> - <input type="checkbox" onChange={e => Doc.UserDoc().showTitle = Doc.UserDoc().showTitle ? undefined : "creationDate"} checked={Doc.UserDoc().showTitle !== undefined} /> - <div className="preferences-check">Show doc header</div> - </div> - <div> - <input type="checkbox" onChange={e => Doc.UserDoc()["documentLinksButton-fullMenu"] = !Doc.UserDoc()["documentLinksButton-fullMenu"]} - checked={BoolCast(Doc.UserDoc()["documentLinksButton-fullMenu"])} /> - <div className="preferences-check">Show full toolbar</div> - </div> - <div> - <input type="checkbox" onChange={e => DragManager.SetRaiseWhenDragged(!DragManager.GetRaiseWhenDragged())} - checked={DragManager.GetRaiseWhenDragged()} /> - <div className="preferences-check">Raise on drag</div> - </div> - <div> - <input type="checkbox" onChange={e => FontIconBox.SetShowLabels(!FontIconBox.GetShowLabels())} - checked={FontIconBox.GetShowLabels()} /> - <div className="preferences-check">Show button labels</div> + return ( + <div className="prefs-content"> + <div> + <input type="checkbox" onChange={e => (Doc.UserDoc().showTitle = Doc.UserDoc().showTitle ? undefined : 'creationDate')} checked={Doc.UserDoc().showTitle !== undefined} /> + <div className="preferences-check">Show doc header</div> + </div> + <div> + <input type="checkbox" onChange={e => (Doc.UserDoc()['documentLinksButton-fullMenu'] = !Doc.UserDoc()['documentLinksButton-fullMenu'])} checked={BoolCast(Doc.UserDoc()['documentLinksButton-fullMenu'])} /> + <div className="preferences-check">Show full toolbar</div> + </div> + <div> + <input type="checkbox" onChange={e => DragManager.SetRaiseWhenDragged(!DragManager.GetRaiseWhenDragged())} checked={DragManager.GetRaiseWhenDragged()} /> + <div className="preferences-check">Raise on drag</div> + </div> + <div> + <input type="checkbox" onChange={e => FontIconBox.SetShowLabels(!FontIconBox.GetShowLabels())} checked={FontIconBox.GetShowLabels()} /> + <div className="preferences-check">Show button labels</div> + </div> </div> - </div>; + ); } @computed get appearanceContent() { - - return <div className="tab-content appearances-content"> - <div className="tab-column"> - <div className="tab-column-title">Colors</div> - <div className="tab-column-content">{this.colorsContent}</div> - </div> - <div className="tab-column"> - <div className="tab-column-title">Formats</div> - <div className="tab-column-content">{this.formatsContent}</div> + return ( + <div className="tab-content appearances-content"> + <div className="tab-column"> + <div className="tab-column-title">Colors</div> + <div className="tab-column-content">{this.colorsContent}</div> + </div> + <div className="tab-column"> + <div className="tab-column-title">Formats</div> + <div className="tab-column-content">{this.formatsContent}</div> + </div> </div> - </div>; + ); } @computed get textContent() { - - const fontFamilies = ["Times New Roman", "Arial", "Georgia", "Comic Sans MS", "Tahoma", "Impact", "Crimson Text", "Roboto"]; - const fontSizes = ["7px", "8px", "9px", "10px", "12px", "14px", "16px", "18px", "20px", "24px", "32px", "48px", "72px"]; + const fontFamilies = ['Times New Roman', 'Arial', 'Georgia', 'Comic Sans MS', 'Tahoma', 'Impact', 'Crimson Text', 'Roboto']; + const fontSizes = ['7px', '8px', '9px', '10px', '12px', '14px', '16px', '18px', '20px', '24px', '32px', '48px', '72px']; return ( <div className="tab-content appearances-content"> <div className="preferences-font"> <div className="preferences-font-text">Default Font</div> <div className="preferences-font-controls"> - <select className="size-select" onChange={this.changeFontSize} value={StrCast(Doc.UserDoc().fontSize, "7px")}> - {fontSizes.map(size => <option key={size} value={size} defaultValue={StrCast(Doc.UserDoc().fontSize)}> {size} </option>)} + <select className="size-select" onChange={this.changeFontSize} value={StrCast(Doc.UserDoc().fontSize, '7px')}> + {fontSizes.map(size => ( + <option key={size} value={size} defaultValue={StrCast(Doc.UserDoc().fontSize)}> + {' '} + {size}{' '} + </option> + ))} </select> - <select className="font-select" onChange={this.changeFontFamily} value={StrCast(Doc.UserDoc().fontFamily, "Times New Roman")} > - {fontFamilies.map(font => <option key={font} value={font} defaultValue={StrCast(Doc.UserDoc().fontFamily)}> {font} </option>)} + <select className="font-select" onChange={this.changeFontFamily} value={StrCast(Doc.UserDoc().fontFamily, 'Times New Roman')}> + {fontFamilies.map(font => ( + <option key={font} value={font} defaultValue={StrCast(Doc.UserDoc().fontFamily)}> + {' '} + {font}{' '} + </option> + ))} </select> </div> </div> - </div>); + </div> + ); } @action changeVal = (e: React.ChangeEvent, pass: string) => { const value = (e.target as any).value; switch (pass) { - case "curr": this.curr_password = value; break; - case "new": this.new_password = value; break; - case "conf": this.new_confirm = value; break; + case 'curr': + this.curr_password = value; + break; + case 'new': + this.new_password = value; + break; + case 'conf': + this.new_confirm = value; + break; } - } + }; @computed get passwordContent() { - return <div className="password-content"> - <div className="password-content-inputs"> - <input className="password-inputs" type="password" placeholder="current password" onChange={e => this.changeVal(e, "curr")} /> - <input className="password-inputs" type="password" placeholder="new password" onChange={e => this.changeVal(e, "new")} /> - <input className="password-inputs" type="password" placeholder="confirm new password" onChange={e => this.changeVal(e, "conf")} /> - </div> - <div className="password-content-buttons"> - {!this.passwordResultText ? (null) : <div className={`${this.passwordResultText.startsWith("Error") ? "error" : "success"}-text`}>{this.passwordResultText}</div>} - <a className="password-forgot" href="/forgotPassword">forgot password?</a> - <button className="password-submit" onClick={this.changePassword}>submit</button> + return ( + <div className="password-content"> + <div className="password-content-inputs"> + <input className="password-inputs" type="password" placeholder="current password" onChange={e => this.changeVal(e, 'curr')} /> + <input className="password-inputs" type="password" placeholder="new password" onChange={e => this.changeVal(e, 'new')} /> + <input className="password-inputs" type="password" placeholder="confirm new password" onChange={e => this.changeVal(e, 'conf')} /> + </div> + <div className="password-content-buttons"> + {!this.passwordResultText ? null : <div className={`${this.passwordResultText.startsWith('Error') ? 'error' : 'success'}-text`}>{this.passwordResultText}</div>} + <a className="password-forgot" href="/forgotPassword"> + forgot password? + </a> + <button className="password-submit" onClick={this.changePassword}> + submit + </button> + </div> </div> - </div>; + ); } @computed get accountOthersContent() { - return <div className="account-others-content"> - <button onClick={this.googleAuthorize} value="data">Authorize Google Acc</button> - </div>; + return ( + <div className="account-others-content"> + <button onClick={this.googleAuthorize} value="data"> + Authorize Google Acc + </button> + </div> + ); } @computed get accountsContent() { - return <div className="tab-content accounts-content"> - <div className="tab-column"> - <div className="tab-column-title">Password</div> - <div className="tab-column-content">{this.passwordContent}</div> - </div> - <div className="tab-column"> - <div className="tab-column-title">Others</div> - <div className="tab-column-content">{this.accountOthersContent}</div> + return ( + <div className="tab-content accounts-content"> + <div className="tab-column"> + <div className="tab-column-title">Password</div> + <div className="tab-column-content">{this.passwordContent}</div> + </div> + <div className="tab-column"> + <div className="tab-column-title">Others</div> + <div className="tab-column-content">{this.accountOthersContent}</div> + </div> </div> - </div>; + ); } @computed get modesContent() { - return <div className="tab-content modes-content"> - <div className="tab-column"> - <div className="tab-column-title">Modes</div> - <div className="tab-column-content"> - <select className="modes-select" onChange={this.selectUserMode} defaultValue={Doc.noviceMode ? "Novice" : "Developer"}> - <option key={"Novice"} value={"Novice"}> Novice </option> - <option key={"Developer"} value={"Developer"}> Developer</option> - </select> - <div className="modes-playground"> - <input className="playground-check" type="checkbox" checked={this.playgroundMode} onChange={this.playgroundModeToggle} /> - <div className="playground-text">Playground Mode</div> + return ( + <div className="tab-content modes-content"> + <div className="tab-column"> + <div className="tab-column-title">Modes</div> + <div className="tab-column-content"> + <select className="modes-select" onChange={this.selectUserMode} defaultValue={Doc.noviceMode ? 'Novice' : 'Developer'}> + <option key={'Novice'} value={'Novice'}> + {' '} + Novice{' '} + </option> + <option key={'Developer'} value={'Developer'}> + {' '} + Developer + </option> + </select> + <div className="modes-playground"> + <input className="playground-check" type="checkbox" checked={this.playgroundMode} onChange={this.playgroundModeToggle} /> + <div className="playground-text">Playground Mode</div> + </div> </div> </div> - </div> - <div className="tab-column"> - <div className="tab-column-title">Permissions</div> - <div className="tab-column-content"> - <button onClick={() => GroupManager.Instance?.open()}>Manage groups</button> - <div className="default-acl"> - <input className="acl-check" type="checkbox" checked={BoolCast(Doc.defaultAclPrivate)} - onChange={action(() => Doc.defaultAclPrivate = !Doc.defaultAclPrivate)} /> - <div className="acl-text">Default access private</div> + <div className="tab-column"> + <div className="tab-column-title">Permissions</div> + <div className="tab-column-content"> + <button onClick={() => GroupManager.Instance?.open()}>Manage groups</button> + <div className="default-acl"> + <input className="acl-check" type="checkbox" checked={BoolCast(Doc.defaultAclPrivate)} onChange={action(() => (Doc.defaultAclPrivate = !Doc.defaultAclPrivate))} /> + <div className="acl-text">Default access private</div> + </div> </div> </div> </div> - - </div>; + ); } - private get settingsInterface() { // const pairs = [{ title: "Password", ele: this.passwordContent }, { title: "Modes", ele: this.modesContent }, // { title: "Accounts", ele: this.accountsContent }, { title: "Preferences", ele: this.preferencesContent }]; - const tabs = [{ title: "Accounts", ele: this.accountsContent }, { title: "Modes", ele: this.modesContent }, - { title: "Appearance", ele: this.appearanceContent }, { title: "Text", ele: this.textContent }]; + const tabs = [ + { title: 'Accounts', ele: this.accountsContent }, + { title: 'Modes', ele: this.modesContent }, + { title: 'Appearance', ele: this.appearanceContent }, + { title: 'Text', ele: this.textContent }, + ]; - return <div className="settings-interface"> - <div className="settings-panel"> - <div className="settings-tabs"> - {tabs.map(tab => <div key={tab.title} className={"tab-control " + (this.activeTab === tab.title ? "active" : "inactive")} onClick={action(() => this.activeTab = tab.title)}>{tab.title}</div>)} - </div> + return ( + <div className="settings-interface"> + <div className="settings-panel"> + <div className="settings-tabs"> + {tabs.map(tab => ( + <div key={tab.title} className={'tab-control ' + (this.activeTab === tab.title ? 'active' : 'inactive')} onClick={action(() => (this.activeTab = tab.title))}> + {tab.title} + </div> + ))} + </div> - <div className="settings-user"> - <div className="settings-username">{Doc.CurrentUserEmail}</div> - <button className="logout-button" onClick={() => window.location.assign(Utils.prepend("/logout"))} > - {CurrentUserUtils.GuestDashboard ? "Exit" : "Log Out"} - </button> + <div className="settings-user"> + <div className="settings-username">{Doc.CurrentUserEmail}</div> + <button className="logout-button" onClick={() => window.location.assign(Utils.prepend('/logout'))}> + {Doc.GuestDashboard ? 'Exit' : 'Log Out'} + </button> + </div> </div> - </div> - <div className="close-button" onClick={this.close}> - <FontAwesomeIcon icon={"times"} color="black" size={"lg"} /> - </div> + <div className="close-button" onClick={this.close}> + <FontAwesomeIcon icon={'times'} color="black" size={'lg'} /> + </div> - <div className="settings-content"> - {tabs.map(tab => <div key={tab.title} className={"tab-section " + (this.activeTab === tab.title ? "active" : "inactive")}>{tab.ele}</div>)} + <div className="settings-content"> + {tabs.map(tab => ( + <div key={tab.title} className={'tab-section ' + (this.activeTab === tab.title ? 'active' : 'inactive')}> + {tab.ele} + </div> + ))} + </div> </div> - </div>; - + ); } render() { - return <MainViewModal - contents={this.settingsInterface} - isDisplayed={this.isOpen} - interactive={true} - closeOnExternalClick={this.close} - dialogueBoxStyle={{ width: "500px", height: "300px", background: Cast(Doc.SharingDoc().userColor, "string", null) }} />; + return ( + <MainViewModal + contents={this.settingsInterface} + isDisplayed={this.isOpen} + interactive={true} + closeOnExternalClick={this.close} + dialogueBoxStyle={{ width: '500px', height: '300px', background: Cast(Doc.SharingDoc().userColor, 'string', null) }} + /> + ); } -}
\ No newline at end of file +} diff --git a/src/client/util/SharingManager.tsx b/src/client/util/SharingManager.tsx index d77633b8d..793027ea1 100644 --- a/src/client/util/SharingManager.tsx +++ b/src/client/util/SharingManager.tsx @@ -1,29 +1,28 @@ -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { intersection } from "lodash"; -import { action, computed, observable, runInAction } from "mobx"; -import { observer } from "mobx-react"; -import * as React from "react"; -import Select from "react-select"; -import * as RequestPromise from "request-promise"; -import { AclAugment, AclAdmin, AclEdit, AclPrivate, AclReadonly, AclSym, AclUnset, DataSym, Doc, DocListCast, DocListCastAsync, Opt, AclSelfEdit } from "../../fields/Doc"; -import { List } from "../../fields/List"; -import { Cast, NumCast, StrCast } from "../../fields/Types"; -import { distributeAcls, GetEffectiveAcl, normalizeEmail, SharingPermissions, TraceMobx } from "../../fields/util"; -import { Utils } from "../../Utils"; -import { DocServer } from "../DocServer"; -import { CollectionView } from "../views/collections/CollectionView"; -import { DictationOverlay } from "../views/DictationOverlay"; -import { MainViewModal } from "../views/MainViewModal"; -import { DocumentView } from "../views/nodes/DocumentView"; -import { TaskCompletionBox } from "../views/nodes/TaskCompletedBox"; -import { SearchBox } from "../views/search/SearchBox"; -import { CurrentUserUtils } from "./CurrentUserUtils"; -import { DocumentManager } from "./DocumentManager"; -import { GroupManager, UserOptions } from "./GroupManager"; -import { GroupMemberView } from "./GroupMemberView"; -import { SelectionManager } from "./SelectionManager"; -import "./SharingManager.scss"; -import { LinkManager } from "./LinkManager"; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { intersection } from 'lodash'; +import { action, computed, observable, runInAction } from 'mobx'; +import { observer } from 'mobx-react'; +import * as React from 'react'; +import Select from 'react-select'; +import * as RequestPromise from 'request-promise'; +import { AclAugment, AclAdmin, AclEdit, AclPrivate, AclReadonly, AclSym, AclUnset, DataSym, Doc, DocListCast, DocListCastAsync, Opt, AclSelfEdit } from '../../fields/Doc'; +import { List } from '../../fields/List'; +import { Cast, NumCast, StrCast } from '../../fields/Types'; +import { distributeAcls, GetEffectiveAcl, normalizeEmail, SharingPermissions, TraceMobx } from '../../fields/util'; +import { Utils } from '../../Utils'; +import { DocServer } from '../DocServer'; +import { CollectionView } from '../views/collections/CollectionView'; +import { DictationOverlay } from '../views/DictationOverlay'; +import { MainViewModal } from '../views/MainViewModal'; +import { DocumentView } from '../views/nodes/DocumentView'; +import { TaskCompletionBox } from '../views/nodes/TaskCompletedBox'; +import { SearchBox } from '../views/search/SearchBox'; +import { DocumentManager } from './DocumentManager'; +import { GroupManager, UserOptions } from './GroupManager'; +import { GroupMemberView } from './GroupMemberView'; +import { SelectionManager } from './SelectionManager'; +import './SharingManager.scss'; +import { LinkManager } from './LinkManager'; export interface User { email: string; @@ -44,19 +43,19 @@ interface GroupedOptions { // const DefaultColor = "black"; // used to differentiate between individuals and groups when sharing -const indType = "!indType/"; -const groupType = "!groupType/"; +const indType = '!indType/'; +const groupType = '!groupType/'; -const storage = "data"; +const storage = 'data'; /** * A user who also has a sharing doc. */ interface ValidatedUser { - user: User; // database minimal info to identify / communicate with a user (email, sharing doc id) - sharingDoc: Doc; // document to share/message another user + user: User; // database minimal info to identify / communicate with a user (email, sharing doc id) + sharingDoc: Doc; // document to share/message another user linkDatabase: Doc; - userColor: string; // stored on the sharinDoc, extracted for convenience? + userColor: string; // stored on the sharinDoc, extracted for convenience? } @observer @@ -71,8 +70,8 @@ export class SharingManager extends React.Component<{}> { @observable private overlayOpacity = 0.4; // for the modal @observable private selectedUsers: UserOptions[] | null = null; // users (individuals/groups) selected to share with @observable private permissions: SharingPermissions = SharingPermissions.Edit; // the permission with which to share with other users - @observable private individualSort: "ascending" | "descending" | "none" = "none"; // sorting options for the list of individuals - @observable private groupSort: "ascending" | "descending" | "none" = "none"; // sorting options for the list of groups + @observable private individualSort: 'ascending' | 'descending' | 'none' = 'none'; // sorting options for the list of individuals + @observable private groupSort: 'ascending' | 'descending' | 'none' = 'none'; // sorting options for the list of groups private shareDocumentButtonRef: React.RefObject<HTMLButtonElement> = React.createRef(); // ref for the share button, used for the position of the popup private distributeAclsButtonRef: React.RefObject<HTMLButtonElement> = React.createRef(); // ref for the distribute button, used for the position of the popup // if both showUserOptions and showGroupOptions are false then both are displayed @@ -89,7 +88,7 @@ export class SharingManager extends React.Component<{}> { [AclAugment, SharingPermissions.Augment], [AclSelfEdit, SharingPermissions.SelfEdit], [AclEdit, SharingPermissions.Edit], - [AclAdmin, SharingPermissions.Admin] + [AclAdmin, SharingPermissions.Admin], ]); // private get linkVisible() { @@ -105,17 +104,20 @@ export class SharingManager extends React.Component<{}> { this.isOpen = this.targetDoc !== undefined; this.permissions = SharingPermissions.Augment; }); - } + }; public close = action(() => { this.isOpen = false; this.selectedUsers = null; // resets the list of users and selected users (in the react-select component) TaskCompletionBox.taskCompleted = false; - setTimeout(action(() => { - // this.copied = false; - DictationOverlay.Instance.hasActiveModal = false; - this.targetDoc = undefined; - }), 500); + setTimeout( + action(() => { + // this.copied = false; + DictationOverlay.Instance.hasActiveModal = false; + this.targetDoc = undefined; + }), + 500 + ); }); constructor(props: {}) { @@ -136,7 +138,7 @@ export class SharingManager extends React.Component<{}> { populateUsers = async () => { if (!this.populating) { this.populating = true; - const userList = await RequestPromise.get(Utils.prepend("/getUsers")); + const userList = await RequestPromise.get(Utils.prepend('/getUsers')); const raw = JSON.parse(userList) as User[]; const sharingDocs: ValidatedUser[] = []; const evaluating = raw.map(async user => { @@ -146,7 +148,8 @@ export class SharingManager extends React.Component<{}> { const linkDatabase = await DocServer.GetRefField(user.linkDatabaseId); if (sharingDoc instanceof Doc && linkDatabase instanceof Doc) { await DocListCastAsync(linkDatabase.data); - (await DocListCastAsync(Cast(linkDatabase, Doc, null).data))?.forEach(async link => { // makes sure link anchors are loaded to avoid incremental updates to computedFns in LinkManager + (await DocListCastAsync(Cast(linkDatabase, Doc, null).data))?.forEach(async link => { + // makes sure link anchors are loaded to avoid incremental updates to computedFns in LinkManager const a1 = await Cast(link?.anchor1, Doc, null); const a2 = await Cast(link?.anchor2, Doc, null); }); @@ -166,7 +169,7 @@ export class SharingManager extends React.Component<{}> { this.populating = false; }); } - } + }; /** * Shares the document with a user. @@ -176,74 +179,77 @@ export class SharingManager extends React.Component<{}> { const target = targetDoc || this.targetDoc!; const acl = `acl-${normalizeEmail(user.email)}`; const myAcl = `acl-${Doc.CurrentUserEmailNormalized}`; - const isDashboard = DocListCast(CurrentUserUtils.MyDashboards.data).indexOf(target) !== -1; + const isDashboard = DocListCast(Doc.MyDashboards.data).indexOf(target) !== -1; const docs = SelectionManager.Views().length < 2 ? [target] : SelectionManager.Views().map(docView => docView.props.Document); - return !docs.map(doc => { - doc.author === Doc.CurrentUserEmail && !doc[myAcl] && distributeAcls(myAcl, SharingPermissions.Admin, doc, undefined, undefined, isDashboard); - - if (permission === SharingPermissions.None) { - if (doc[acl] && doc[acl] !== SharingPermissions.None) doc.numUsersShared = NumCast(doc.numUsersShared, 1) - 1; - } - else { - if (!doc[acl] || doc[acl] === SharingPermissions.None) doc.numUsersShared = NumCast(doc.numUsersShared, 0) + 1; - } + return !docs + .map(doc => { + doc.author === Doc.CurrentUserEmail && !doc[myAcl] && distributeAcls(myAcl, SharingPermissions.Admin, doc, undefined, undefined, isDashboard); + + if (permission === SharingPermissions.None) { + if (doc[acl] && doc[acl] !== SharingPermissions.None) doc.numUsersShared = NumCast(doc.numUsersShared, 1) - 1; + } else { + if (!doc[acl] || doc[acl] === SharingPermissions.None) doc.numUsersShared = NumCast(doc.numUsersShared, 0) + 1; + } - distributeAcls(acl, permission as SharingPermissions, doc, undefined, undefined, isDashboard); + distributeAcls(acl, permission as SharingPermissions, doc, undefined, undefined, isDashboard); - this.setDashboardBackground(doc, permission as SharingPermissions); - if (permission !== SharingPermissions.None) return Doc.AddDocToList(sharingDoc, storage, doc); - else return GetEffectiveAcl(doc, user.email) === AclPrivate && Doc.RemoveDocFromList(sharingDoc, storage, (doc.aliasOf as Doc || doc)); - }).some(success => !success); - } + this.setDashboardBackground(doc, permission as SharingPermissions); + if (permission !== SharingPermissions.None) return Doc.AddDocToList(sharingDoc, storage, doc); + else return GetEffectiveAcl(doc, user.email) === AclPrivate && Doc.RemoveDocFromList(sharingDoc, storage, (doc.aliasOf as Doc) || doc); + }) + .some(success => !success); + }; /** * Sets the permission on the target for the group. - * @param group - * @param permission + * @param group + * @param permission */ setInternalGroupSharing = (group: Doc | { title: string }, permission: string, targetDoc?: Doc) => { - const target = targetDoc || this.targetDoc!; const key = normalizeEmail(StrCast(group.title)); const acl = `acl-${key}`; - const isDashboard = DocListCast(CurrentUserUtils.MyDashboards.data).indexOf(target) !== -1; + const isDashboard = DocListCast(Doc.MyDashboards.data).indexOf(target) !== -1; const docs = SelectionManager.Views().length < 2 ? [target] : SelectionManager.Views().map(docView => docView.props.Document); // ! ensures it returns true if document has been shared successfully, false otherwise - return !docs.map(doc => { - doc.author === Doc.CurrentUserEmail && !doc[`acl-${Doc.CurrentUserEmailNormalized}`] && distributeAcls(`acl-${Doc.CurrentUserEmailNormalized}`, SharingPermissions.Admin, doc, undefined, undefined, isDashboard); - - if (permission === SharingPermissions.None) { - if (doc[acl] && doc[acl] !== SharingPermissions.None) doc.numGroupsShared = NumCast(doc.numGroupsShared, 1) - 1; - } - else { - if (!doc[acl] || doc[acl] === SharingPermissions.None) doc.numGroupsShared = NumCast(doc.numGroupsShared, 0) + 1; - } + return !docs + .map(doc => { + doc.author === Doc.CurrentUserEmail && !doc[`acl-${Doc.CurrentUserEmailNormalized}`] && distributeAcls(`acl-${Doc.CurrentUserEmailNormalized}`, SharingPermissions.Admin, doc, undefined, undefined, isDashboard); + + if (permission === SharingPermissions.None) { + if (doc[acl] && doc[acl] !== SharingPermissions.None) doc.numGroupsShared = NumCast(doc.numGroupsShared, 1) - 1; + } else { + if (!doc[acl] || doc[acl] === SharingPermissions.None) doc.numGroupsShared = NumCast(doc.numGroupsShared, 0) + 1; + } - distributeAcls(acl, permission as SharingPermissions, doc, undefined, undefined, isDashboard); - this.setDashboardBackground(doc, permission as SharingPermissions); + distributeAcls(acl, permission as SharingPermissions, doc, undefined, undefined, isDashboard); + this.setDashboardBackground(doc, permission as SharingPermissions); - if (group instanceof Doc) { - const members: string[] = JSON.parse(StrCast(group.members)); - const users: ValidatedUser[] = this.users.filter(({ user: { email } }) => members.includes(email)); + if (group instanceof Doc) { + const members: string[] = JSON.parse(StrCast(group.members)); + const users: ValidatedUser[] = this.users.filter(({ user: { email } }) => members.includes(email)); - // if documents have been shared, add the doc to that list if it doesn't already exist, otherwise create a new list with the doc - group.docsShared ? Doc.IndexOf(doc, DocListCast(group.docsShared)) === -1 && (group.docsShared as List<Doc>).push(doc) : group.docsShared = new List<Doc>([doc]); + // if documents have been shared, add the doc to that list if it doesn't already exist, otherwise create a new list with the doc + group.docsShared ? Doc.IndexOf(doc, DocListCast(group.docsShared)) === -1 && (group.docsShared as List<Doc>).push(doc) : (group.docsShared = new List<Doc>([doc])); - return users.map(({ user, sharingDoc }) => { - if (permission !== SharingPermissions.None) return Doc.AddDocToList(sharingDoc, storage, doc); // add the doc to the sharingDoc if it hasn't already been added - else return GetEffectiveAcl(doc, user.email) === AclPrivate && Doc.RemoveDocFromList(sharingDoc, storage, (doc.aliasOf as Doc || doc)); // remove the doc from the list if it already exists - }).some(success => !success); - } - }).some(success => success); - } + return users + .map(({ user, sharingDoc }) => { + if (permission !== SharingPermissions.None) return Doc.AddDocToList(sharingDoc, storage, doc); // add the doc to the sharingDoc if it hasn't already been added + else return GetEffectiveAcl(doc, user.email) === AclPrivate && Doc.RemoveDocFromList(sharingDoc, storage, (doc.aliasOf as Doc) || doc); // remove the doc from the list if it already exists + }) + .some(success => !success); + } + }) + .some(success => success); + }; /** * Shares the documents shared with a group with a new user who has been added to that group. - * @param group - * @param emailId + * @param group + * @param emailId */ shareWithAddedMember = (group: Doc, emailId: string, retry: boolean = true) => { const user = this.users.find(({ user: { email } }) => email === emailId)!; @@ -255,54 +261,53 @@ export class SharingManager extends React.Component<{}> { DocListCastAsync(group.docsShared).then(dl => { const filtered = dl?.filter(doc => !userdocs?.includes(doc)); filtered && userdocs?.push(...filtered); - })); + }) + ); } } - } + }; /** * Called from the properties sidebar to change permissions of a user. */ shareFromPropertiesSidebar = (shareWith: string, permission: SharingPermissions, docs: Doc[]) => { - if (shareWith !== "Public" && shareWith !== "Override") { - const user = this.users.find(({ user: { email } }) => email === (shareWith === "Me" ? Doc.CurrentUserEmail : shareWith)); + if (shareWith !== 'Public' && shareWith !== 'Override') { + const user = this.users.find(({ user: { email } }) => email === (shareWith === 'Me' ? Doc.CurrentUserEmail : shareWith)); docs.forEach(doc => { if (user) this.setInternalSharing(user, permission, doc); else this.setInternalGroupSharing(GroupManager.Instance.getGroup(shareWith)!, permission, doc); }); - } - else { - const dashboards = DocListCast(CurrentUserUtils.MyDashboards.data); + } else { + const dashboards = DocListCast(Doc.MyDashboards.data); docs.forEach(doc => { const isDashboard = dashboards.indexOf(doc) !== -1; if (GetEffectiveAcl(doc) === AclAdmin) distributeAcls(`acl-${shareWith}`, permission, doc, undefined, undefined, isDashboard); }); } - } + }; /** * Sets the background of the Dashboard if it has been shared as a visual indicator */ setDashboardBackground = async (doc: Doc, permission: SharingPermissions) => { - if (Doc.IndexOf(doc, DocListCast(CurrentUserUtils.MyDashboards.data)) !== -1) { + if (Doc.IndexOf(doc, DocListCast(Doc.MyDashboards.data)) !== -1) { if (permission !== SharingPermissions.None) { doc.isShared = true; - doc.backgroundColor = "green"; - } - else { + doc.backgroundColor = 'green'; + } else { const acls = doc[DataSym][AclSym]; - if (Object.keys(acls).every(key => key === `acl-${Doc.CurrentUserEmailNormalized}` ? true : [AclUnset, AclPrivate].includes(acls[key]))) { + if (Object.keys(acls).every(key => (key === `acl-${Doc.CurrentUserEmailNormalized}` ? true : [AclUnset, AclPrivate].includes(acls[key])))) { doc.isShared = undefined; doc.backgroundColor = undefined; } } } - } + }; /** * Removes the documents shared with a user through a group when the user is removed from the group. - * @param group - * @param emailId + * @param group + * @param emailId */ removeMember = (group: Doc, emailId: string) => { const user: ValidatedUser = this.users.find(({ user: { email } }) => email === emailId)!; @@ -315,15 +320,15 @@ export class SharingManager extends React.Component<{}> { }) ); } - } + }; /** * Removes a group's permissions from documents that have been shared with it. - * @param group + * @param group */ removeGroup = (group: Doc) => { if (group.docsShared) { - const dashboards = DocListCast(CurrentUserUtils.MyDashboards.data); + const dashboards = DocListCast(Doc.MyDashboards.data); DocListCast(group.docsShared).forEach(doc => { const acl = `acl-${StrCast(group.title)}`; const isDashboard = dashboards.indexOf(doc) !== -1; @@ -335,9 +340,7 @@ export class SharingManager extends React.Component<{}> { users.forEach(({ sharingDoc }) => Doc.RemoveDocFromList(sharingDoc, storage, doc)); }); } - } - - + }; // private setExternalSharing = (permission: string) => { // const targetDoc = this.targetDoc; @@ -367,23 +370,23 @@ export class SharingManager extends React.Component<{}> { */ private sharingOptions(uniform: boolean, override?: boolean) { const dropdownValues: string[] = Object.values(SharingPermissions); - if (!uniform) dropdownValues.unshift("-multiple-"); - if (override) dropdownValues.unshift("None"); - return dropdownValues.filter(permission => !Doc.noviceMode || ![SharingPermissions.View, SharingPermissions.SelfEdit].includes(permission as any)).map(permission => - ( - <option key={permission} value={permission}> - {permission} - </option> - ) - ); + if (!uniform) dropdownValues.unshift('-multiple-'); + if (override) dropdownValues.unshift('None'); + return dropdownValues + .filter(permission => !Doc.noviceMode || ![SharingPermissions.View, SharingPermissions.SelfEdit].includes(permission as any)) + .map(permission => ( + <option key={permission} value={permission}> + {permission} + </option> + )); } private focusOn = (contents: string) => { - const title = this.targetDoc ? StrCast(this.targetDoc.title) : ""; + const title = this.targetDoc ? StrCast(this.targetDoc.title) : ''; const docs = SelectionManager.Views().length > 1 ? SelectionManager.Views().map(docView => docView.props.Document) : [this.targetDoc]; return ( <span - className={"focus-span"} + className={'focus-span'} title={title} onClick={() => { let context: Opt<CollectionView>; @@ -404,12 +407,11 @@ export class SharingManager extends React.Component<{}> { this.dialogueBoxOpacity = 1; this.overlayOpacity = 0.4; } - })} - > + })}> {contents} </span> ); - } + }; /** * Handles changes in the users selected in react-select @@ -417,7 +419,7 @@ export class SharingManager extends React.Component<{}> { @action handleUsersChange = (selectedOptions: any) => { this.selectedUsers = selectedOptions as UserOptions[]; - } + }; /** * Handles changes in the permission chosen to share with someone with @@ -425,7 +427,7 @@ export class SharingManager extends React.Component<{}> { @action handlePermissionsChange = (event: React.ChangeEvent<HTMLSelectElement>) => { this.permissions = event.currentTarget.value as SharingPermissions; - } + }; /** * Calls the relevant method for sharing, displays the popup, and resets the relevant variables. @@ -436,8 +438,7 @@ export class SharingManager extends React.Component<{}> { this.selectedUsers.forEach(user => { if (user.value.includes(indType)) { this.setInternalSharing(this.users.find(u => u.user.email === user.label)!, this.permissions); - } - else { + } else { this.setInternalGroupSharing(GroupManager.Instance.getGroup(user.label)!, this.permissions); } }); @@ -445,13 +446,16 @@ export class SharingManager extends React.Component<{}> { const { left, width, top, height } = this.shareDocumentButtonRef.current!.getBoundingClientRect(); TaskCompletionBox.popupX = left - 1.5 * width; TaskCompletionBox.popupY = top - 1.5 * height; - TaskCompletionBox.textDisplayed = "Document shared!"; + TaskCompletionBox.textDisplayed = 'Document shared!'; TaskCompletionBox.taskCompleted = true; - setTimeout(action(() => TaskCompletionBox.taskCompleted = false), 2000); + setTimeout( + action(() => (TaskCompletionBox.taskCompleted = false)), + 2000 + ); this.selectedUsers = null; } - } + }; // distributeOverCollection = (targetDoc?: Doc) => { // const target = targetDoc || this.targetDoc!; @@ -471,7 +475,7 @@ export class SharingManager extends React.Component<{}> { const { email: e1 } = u1.user; const { email: e2 } = u2.user; return e1 < e2 ? -1 : e1 === e2 ? 0 : 1; - } + }; /** * Sorting algorithm to sort groups. @@ -480,7 +484,7 @@ export class SharingManager extends React.Component<{}> { const g1 = StrCast(group1.title); const g2 = StrCast(group2.title); return g1 < g2 ? -1 : g1 === g2 ? 0 : 1; - } + }; /** * @returns the main interface of the SharingManager. @@ -488,28 +492,29 @@ export class SharingManager extends React.Component<{}> { @computed get sharingInterface() { TraceMobx(); const groupList = GroupManager.Instance?.allGroups || []; - const sortedUsers = this.users.slice().sort(this.sortUsers).map(({ user: { email } }) => ({ label: email, value: indType + email })); - const sortedGroups = groupList.slice().sort(this.sortGroups).map(({ title }) => ({ label: StrCast(title), value: groupType + StrCast(title) })); + const sortedUsers = this.users + .slice() + .sort(this.sortUsers) + .map(({ user: { email } }) => ({ label: email, value: indType + email })); + const sortedGroups = groupList + .slice() + .sort(this.sortGroups) + .map(({ title }) => ({ label: StrCast(title), value: groupType + StrCast(title) })); // the next block handles the users shown (individuals/groups/both) const options: GroupedOptions[] = []; if (GroupManager.Instance) { if ((this.showUserOptions && this.showGroupOptions) || (!this.showUserOptions && !this.showGroupOptions)) { - options.push( - { label: 'Individuals', options: sortedUsers }, - { label: 'Groups', options: sortedGroups }); - } - else if (this.showUserOptions) options.push({ label: 'Individuals', options: sortedUsers }); + options.push({ label: 'Individuals', options: sortedUsers }, { label: 'Groups', options: sortedGroups }); + } else if (this.showUserOptions) options.push({ label: 'Individuals', options: sortedUsers }); else options.push({ label: 'Groups', options: sortedGroups }); } - const users = this.individualSort === "ascending" ? this.users.slice().sort(this.sortUsers) : this.individualSort === "descending" ? this.users.slice().sort(this.sortUsers).reverse() : this.users; - const groups = this.groupSort === "ascending" ? groupList.slice().sort(this.sortGroups) : this.groupSort === "descending" ? groupList.slice().sort(this.sortGroups).reverse() : groupList; + const users = this.individualSort === 'ascending' ? this.users.slice().sort(this.sortUsers) : this.individualSort === 'descending' ? this.users.slice().sort(this.sortUsers).reverse() : this.users; + const groups = this.groupSort === 'ascending' ? groupList.slice().sort(this.sortGroups) : this.groupSort === 'descending' ? groupList.slice().sort(this.sortGroups).reverse() : groupList; // handles the case where multiple documents are selected - let docs = SelectionManager.Views().length < 2 ? - [this.layoutDocAcls ? this.targetDoc : this.targetDoc?.[DataSym]] - : SelectionManager.Views().map(docView => this.layoutDocAcls ? docView.props.Document : docView.props.Document?.[DataSym]); + let docs = SelectionManager.Views().length < 2 ? [this.layoutDocAcls ? this.targetDoc : this.targetDoc?.[DataSym]] : SelectionManager.Views().map(docView => (this.layoutDocAcls ? docView.props.Document : docView.props.Document?.[DataSym])); if (this.myDocAcls) { const newDocs: Doc[] = []; @@ -524,107 +529,78 @@ export class SharingManager extends React.Component<{}> { const admin = this.myDocAcls ? Boolean(docs.length) : effectiveAcls.every(acl => acl === AclAdmin); // users in common between all docs - const commonKeys = intersection(...docs.map(doc => this.layoutDocAcls ? doc?.[AclSym] && Object.keys(doc[AclSym]) : doc?.[DataSym]?.[AclSym] && Object.keys(doc[DataSym][AclSym]))); + const commonKeys = intersection(...docs.map(doc => (this.layoutDocAcls ? doc?.[AclSym] && Object.keys(doc[AclSym]) : doc?.[DataSym]?.[AclSym] && Object.keys(doc[DataSym][AclSym])))); // the list of users shared with - const userListContents: (JSX.Element | null)[] = users.filter(({ user }) => docs.length > 1 ? commonKeys.includes(`acl-${normalizeEmail(user.email)}`) : docs[0]?.author !== user.email).map(({ user, linkDatabase, sharingDoc, userColor }) => { - const userKey = `acl-${normalizeEmail(user.email)}`; - const uniform = docs.every(doc => this.layoutDocAcls ? doc?.[AclSym]?.[userKey] === docs[0]?.[AclSym]?.[userKey] : doc?.[DataSym]?.[AclSym]?.[userKey] === docs[0]?.[DataSym]?.[AclSym]?.[userKey]); - const permissions = uniform ? StrCast(targetDoc?.[userKey]) : "-multiple-"; - - return !permissions ? (null) : ( - <div - key={userKey} - className={"container"} - > - <span className={"padding"}>{user.email}</span> - <div className="edit-actions"> - {admin || this.myDocAcls ? ( - <select - className={"permissions-dropdown"} - value={permissions} - onChange={e => this.setInternalSharing({ user, linkDatabase, sharingDoc, userColor }, e.currentTarget.value)} - > - {this.sharingOptions(uniform)} - </select> - ) : ( - <div className={"permissions-dropdown"}> - {permissions} - </div> - )} + const userListContents: (JSX.Element | null)[] = users + .filter(({ user }) => (docs.length > 1 ? commonKeys.includes(`acl-${normalizeEmail(user.email)}`) : docs[0]?.author !== user.email)) + .map(({ user, linkDatabase, sharingDoc, userColor }) => { + const userKey = `acl-${normalizeEmail(user.email)}`; + const uniform = docs.every(doc => (this.layoutDocAcls ? doc?.[AclSym]?.[userKey] === docs[0]?.[AclSym]?.[userKey] : doc?.[DataSym]?.[AclSym]?.[userKey] === docs[0]?.[DataSym]?.[AclSym]?.[userKey])); + const permissions = uniform ? StrCast(targetDoc?.[userKey]) : '-multiple-'; + + return !permissions ? null : ( + <div key={userKey} className={'container'}> + <span className={'padding'}>{user.email}</span> + <div className="edit-actions"> + {admin || this.myDocAcls ? ( + <select className={'permissions-dropdown'} value={permissions} onChange={e => this.setInternalSharing({ user, linkDatabase, sharingDoc, userColor }, e.currentTarget.value)}> + {this.sharingOptions(uniform)} + </select> + ) : ( + <div className={'permissions-dropdown'}>{permissions}</div> + )} + </div> </div> - </div> - ); - }); + ); + }); // checks if every doc has the same author const sameAuthor = docs.every(doc => doc?.author === docs[0]?.author); // the owner of the doc and the current user are placed at the top of the user list. userListContents.unshift( - sameAuthor ? - ( - <div - key={"owner"} - className={"container"} - > - <span className={"padding"}>{targetDoc?.author === Doc.CurrentUserEmail ? "Me" : targetDoc?.author}</span> - <div className="edit-actions"> - <div className={"permissions-dropdown"}> - Owner - </div> - </div> + sameAuthor ? ( + <div key={'owner'} className={'container'}> + <span className={'padding'}>{targetDoc?.author === Doc.CurrentUserEmail ? 'Me' : targetDoc?.author}</span> + <div className="edit-actions"> + <div className={'permissions-dropdown'}>Owner</div> </div> - ) : null, - sameAuthor && targetDoc?.author !== Doc.CurrentUserEmail ? - ( - <div - key={"me"} - className={"container"} - > - <span className={"padding"}>Me</span> - <div className="edit-actions"> - <div className={"permissions-dropdown"}> - {effectiveAcls.every(acl => acl === effectiveAcls[0]) ? this.AclMap.get(effectiveAcls[0])! : "-multiple-"} - </div> - </div> + </div> + ) : null, + sameAuthor && targetDoc?.author !== Doc.CurrentUserEmail ? ( + <div key={'me'} className={'container'}> + <span className={'padding'}>Me</span> + <div className="edit-actions"> + <div className={'permissions-dropdown'}>{effectiveAcls.every(acl => acl === effectiveAcls[0]) ? this.AclMap.get(effectiveAcls[0])! : '-multiple-'}</div> </div> - ) : null + </div> + ) : null ); - // the list of groups shared with - const groupListMap: (Doc | { title: string })[] = groups.filter(({ title }) => docs.length > 1 ? commonKeys.includes(`acl-${normalizeEmail(StrCast(title))}`) : true); - groupListMap.unshift({ title: "Public" });//, { title: "ALL" }); + const groupListMap: (Doc | { title: string })[] = groups.filter(({ title }) => (docs.length > 1 ? commonKeys.includes(`acl-${normalizeEmail(StrCast(title))}`) : true)); + groupListMap.unshift({ title: 'Public' }); //, { title: "ALL" }); const groupListContents = groupListMap.map(group => { const groupKey = `acl-${StrCast(group.title)}`; - const uniform = docs.every(doc => this.layoutDocAcls ? doc?.[AclSym]?.[groupKey] === docs[0]?.[AclSym]?.[groupKey] : doc?.[DataSym]?.[AclSym]?.[groupKey] === docs[0]?.[DataSym]?.[AclSym]?.[groupKey]); - const permissions = uniform ? StrCast(targetDoc?.[`acl-${StrCast(group.title)}`]) : "-multiple-"; - - return !permissions ? (null) : ( - <div - key={groupKey} - className={"container"} - > - <div className={"padding"}>{group.title}</div> - {group instanceof Doc ? - (<div className="group-info" onClick={action(() => GroupManager.Instance.currentGroup = group)}> - <FontAwesomeIcon icon={"info-circle"} color={"#e8e8e8"} size={"sm"} style={{ backgroundColor: "#1e89d7", borderRadius: "100%", border: "1px solid #1e89d7" }} /> - </div>) - : (null)} + const uniform = docs.every(doc => (this.layoutDocAcls ? doc?.[AclSym]?.[groupKey] === docs[0]?.[AclSym]?.[groupKey] : doc?.[DataSym]?.[AclSym]?.[groupKey] === docs[0]?.[DataSym]?.[AclSym]?.[groupKey])); + const permissions = uniform ? StrCast(targetDoc?.[`acl-${StrCast(group.title)}`]) : '-multiple-'; + + return !permissions ? null : ( + <div key={groupKey} className={'container'}> + <div className={'padding'}>{StrCast(group.title)}</div> + {group instanceof Doc ? ( + <div className="group-info" onClick={action(() => (GroupManager.Instance.currentGroup = group))}> + <FontAwesomeIcon icon={'info-circle'} color={'#e8e8e8'} size={'sm'} style={{ backgroundColor: '#1e89d7', borderRadius: '100%', border: '1px solid #1e89d7' }} /> + </div> + ) : null} <div className="edit-actions"> {admin || this.myDocAcls ? ( - <select - className={"permissions-dropdown"} - value={permissions} - onChange={e => this.setInternalGroupSharing(group, e.currentTarget.value)} - > - {this.sharingOptions(uniform, group.title === "Override")} + <select className={'permissions-dropdown'} value={permissions} onChange={e => this.setInternalGroupSharing(group, e.currentTarget.value)}> + {this.sharingOptions(uniform, group.title === 'Override')} </select> ) : ( - <div className={"permissions-dropdown"}> - {permissions} - </div> + <div className={'permissions-dropdown'}>{permissions}</div> )} </div> </div> @@ -632,102 +608,95 @@ export class SharingManager extends React.Component<{}> { }); return ( - <div className={"sharing-interface"}> - {GroupManager.Instance?.currentGroup ? - <GroupMemberView - group={GroupManager.Instance.currentGroup} - onCloseButtonClick={action(() => GroupManager.Instance.currentGroup = undefined)} - /> : - null} + <div className={'sharing-interface'}> + {GroupManager.Instance?.currentGroup ? <GroupMemberView group={GroupManager.Instance.currentGroup} onCloseButtonClick={action(() => (GroupManager.Instance.currentGroup = undefined))} /> : null} <div className="sharing-contents"> - <p className={"share-title"}><b>Share </b>{this.focusOn(docs.length < 2 ? StrCast(targetDoc?.title, "this document") : "-multiple-")}</p> - <div className={"close-button"} onClick={this.close}> - <FontAwesomeIcon icon={"times"} color={"black"} size={"lg"} /> + <p className={'share-title'}> + <b>Share </b> + {this.focusOn(docs.length < 2 ? StrCast(targetDoc?.title, 'this document') : '-multiple-')} + </p> + <div className={'close-button'} onClick={this.close}> + <FontAwesomeIcon icon={'times'} color={'black'} size={'lg'} /> </div> {/* {this.linkVisible ? <div> {this.sharingUrl} </div> : (null)} */} - {<div className="share-container"> - <div className="share-setup"> - <Select - className="user-search" - placeholder="Enter user or group name..." - isMulti - isSearchable - closeMenuOnSelect={false} - options={options} - onKeyDown={e => e.stopPropagation()} - onChange={this.handleUsersChange} - value={this.selectedUsers} - styles={{ - indicatorSeparator: () => ({ - visibility: "hidden" - }) - }} - /> - <select className="permissions-select" onChange={this.handlePermissionsChange} value={this.permissions}> - {this.sharingOptions(true)} - </select> - <button ref={this.shareDocumentButtonRef} className="share-button" onClick={this.share}> - Share - </button> - </div> - <div className="sort-checkboxes"> - <input type="checkbox" onChange={action(() => this.showUserOptions = !this.showUserOptions)} /> <label style={{ marginRight: 10 }}>Individuals</label> - <input type="checkbox" onChange={action(() => this.showGroupOptions = !this.showGroupOptions)} /> <label>Groups</label> - </div> + { + <div className="share-container"> + <div className="share-setup"> + <Select + className="user-search" + placeholder="Enter user or group name..." + isMulti + isSearchable + closeMenuOnSelect={false} + options={options} + onKeyDown={e => e.stopPropagation()} + onChange={this.handleUsersChange} + value={this.selectedUsers} + styles={{ + indicatorSeparator: () => ({ + visibility: 'hidden', + }), + }} + /> + <select className="permissions-select" onChange={this.handlePermissionsChange} value={this.permissions}> + {this.sharingOptions(true)} + </select> + <button ref={this.shareDocumentButtonRef} className="share-button" onClick={this.share}> + Share + </button> + </div> + <div className="sort-checkboxes"> + <input type="checkbox" onChange={action(() => (this.showUserOptions = !this.showUserOptions))} /> <label style={{ marginRight: 10 }}>Individuals</label> + <input type="checkbox" onChange={action(() => (this.showGroupOptions = !this.showGroupOptions))} /> <label>Groups</label> + </div> - <div className="acl-container"> - {Doc.noviceMode ? (null) : - <div className="layoutDoc-acls"> - <input type="checkbox" onChange={action(() => this.layoutDocAcls = !this.layoutDocAcls)} checked={this.layoutDocAcls} /> <label>Layout</label> - </div>} + <div className="acl-container"> + {Doc.noviceMode ? null : ( + <div className="layoutDoc-acls"> + <input type="checkbox" onChange={action(() => (this.layoutDocAcls = !this.layoutDocAcls))} checked={this.layoutDocAcls} /> <label>Layout</label> + </div> + )} + </div> </div> - </div> } <div className="main-container"> - <div className={"individual-container"}> - <div - className="user-sort" - onClick={action(() => this.individualSort = this.individualSort === "ascending" ? "descending" : this.individualSort === "descending" ? "none" : "ascending")}> - Individuals {this.individualSort === "ascending" ? <FontAwesomeIcon icon={"caret-up"} size={"xs"} /> - : this.individualSort === "descending" ? <FontAwesomeIcon icon={"caret-down"} size={"xs"} /> - : <FontAwesomeIcon icon={"caret-right"} size={"xs"} />} - </div> - <div className={"users-list"}> - {userListContents} + <div className={'individual-container'}> + <div className="user-sort" onClick={action(() => (this.individualSort = this.individualSort === 'ascending' ? 'descending' : this.individualSort === 'descending' ? 'none' : 'ascending'))}> + Individuals{' '} + {this.individualSort === 'ascending' ? ( + <FontAwesomeIcon icon={'caret-up'} size={'xs'} /> + ) : this.individualSort === 'descending' ? ( + <FontAwesomeIcon icon={'caret-down'} size={'xs'} /> + ) : ( + <FontAwesomeIcon icon={'caret-right'} size={'xs'} /> + )} </div> + <div className={'users-list'}>{userListContents}</div> </div> - <div className={"group-container"}> - <div - className="user-sort" - onClick={action(() => this.groupSort = this.groupSort === "ascending" ? "descending" : this.groupSort === "descending" ? "none" : "ascending")}> - Groups {this.groupSort === "ascending" ? <FontAwesomeIcon icon={"caret-up"} size={"xs"} /> - : this.groupSort === "descending" ? <FontAwesomeIcon icon={"caret-down"} size={"xs"} /> - : <FontAwesomeIcon icon={"caret-right"} size={"xs"} />} - - </div> - <div className={"groups-list"}> - {groupListContents} + <div className={'group-container'}> + <div className="user-sort" onClick={action(() => (this.groupSort = this.groupSort === 'ascending' ? 'descending' : this.groupSort === 'descending' ? 'none' : 'ascending'))}> + Groups{' '} + {this.groupSort === 'ascending' ? ( + <FontAwesomeIcon icon={'caret-up'} size={'xs'} /> + ) : this.groupSort === 'descending' ? ( + <FontAwesomeIcon icon={'caret-down'} size={'xs'} /> + ) : ( + <FontAwesomeIcon icon={'caret-right'} size={'xs'} /> + )} </div> + <div className={'groups-list'}>{groupListContents}</div> </div> </div> - </div> </div> ); } render() { - return <MainViewModal - contents={this.sharingInterface} - isDisplayed={this.isOpen} - interactive={true} - dialogueBoxDisplayedOpacity={this.dialogueBoxOpacity} - overlayDisplayedOpacity={this.overlayOpacity} - closeOnExternalClick={this.close} - />; + return <MainViewModal contents={this.sharingInterface} isDisplayed={this.isOpen} interactive={true} dialogueBoxDisplayedOpacity={this.dialogueBoxOpacity} overlayDisplayedOpacity={this.overlayOpacity} closeOnExternalClick={this.close} />; } -}
\ No newline at end of file +} diff --git a/src/client/util/SnappingManager.ts b/src/client/util/SnappingManager.ts index 057843c68..2c0a1da8b 100644 --- a/src/client/util/SnappingManager.ts +++ b/src/client/util/SnappingManager.ts @@ -1,9 +1,8 @@ -import { observable, action, runInAction } from "mobx"; -import { computedFn } from "mobx-utils"; -import { Doc } from "../../fields/Doc"; +import { observable, action, runInAction } from 'mobx'; +import { computedFn } from 'mobx-utils'; +import { Doc } from '../../fields/Doc'; export namespace SnappingManager { - class Manager { @observable IsDragging: boolean = false; @observable public horizSnapLines: number[] = []; @@ -16,28 +15,34 @@ export namespace SnappingManager { this.horizSnapLines = horizLines; this.vertSnapLines = vertLines; } - - @observable cachedGroups: string[] = []; - @action setCachedGroups(groups: string[]) { this.cachedGroups = groups; } } const manager = new Manager(); - export function clearSnapLines() { manager.clearSnapLines(); } - export function setSnapLines(horizLines: number[], vertLines: number[]) { manager.setSnapLines(horizLines, vertLines); } - export function horizSnapLines() { return manager.horizSnapLines; } - export function vertSnapLines() { return manager.vertSnapLines; } - - export function SetIsDragging(dragging: boolean) { runInAction(() => manager.IsDragging = dragging); } - export function GetIsDragging() { return manager.IsDragging; } + export function clearSnapLines() { + manager.clearSnapLines(); + } + export function setSnapLines(horizLines: number[], vertLines: number[]) { + manager.setSnapLines(horizLines, vertLines); + } + export function horizSnapLines() { + return manager.horizSnapLines; + } + export function vertSnapLines() { + return manager.vertSnapLines; + } - export function SetShowSnapLines(show: boolean) { runInAction(() => Doc.UserDoc().showSnapLines = show); } - export function GetShowSnapLines() { return Doc.UserDoc().showSnapLines; } + export function SetIsDragging(dragging: boolean) { + runInAction(() => (manager.IsDragging = dragging)); + } + export function GetIsDragging() { + return manager.IsDragging; + } - /// bcz; argh!! TODO; These do not belong here, but there were include order problems with leaving them in util.ts - // need to investigate further what caused the mobx update problems and move to a better location. - const getCachedGroupByNameCache = computedFn(function (name: string) { return manager.cachedGroups.includes(name); }, true); - export function GetCachedGroupByName(name: string) { return getCachedGroupByNameCache(name); } - export function SetCachedGroups(groups: string[]) { manager.setCachedGroups(groups); } + export function SetShowSnapLines(show: boolean) { + runInAction(() => (Doc.UserDoc().showSnapLines = show)); + } + export function GetShowSnapLines() { + return Doc.UserDoc().showSnapLines; + } } - |
