diff options
Diffstat (limited to 'src/client/util')
-rw-r--r-- | src/client/util/CaptureManager.scss | 18 | ||||
-rw-r--r-- | src/client/util/CurrentUserUtils.ts | 155 | ||||
-rw-r--r-- | src/client/util/DocumentManager.ts | 2 | ||||
-rw-r--r-- | src/client/util/DropConverter.ts | 2 | ||||
-rw-r--r-- | src/client/util/GroupManager.scss | 12 | ||||
-rw-r--r-- | src/client/util/LinkFollower.ts | 2 | ||||
-rw-r--r-- | src/client/util/LinkManager.ts | 65 | ||||
-rw-r--r-- | src/client/util/RTFMarkup.tsx | 2 | ||||
-rw-r--r-- | src/client/util/ReportManager.tsx | 4 | ||||
-rw-r--r-- | src/client/util/ScriptManager.ts | 38 | ||||
-rw-r--r-- | src/client/util/ServerStats.tsx | 22 | ||||
-rw-r--r-- | src/client/util/SettingsManager.scss | 141 | ||||
-rw-r--r-- | src/client/util/SettingsManager.tsx | 470 | ||||
-rw-r--r-- | src/client/util/SharingManager.scss | 169 | ||||
-rw-r--r-- | src/client/util/SharingManager.tsx | 406 | ||||
-rw-r--r-- | src/client/util/UndoManager.ts | 9 |
16 files changed, 834 insertions, 683 deletions
diff --git a/src/client/util/CaptureManager.scss b/src/client/util/CaptureManager.scss index a5024247e..11e31fe2e 100644 --- a/src/client/util/CaptureManager.scss +++ b/src/client/util/CaptureManager.scss @@ -155,21 +155,3 @@ } } -.close-button { - position: absolute; - top: 10; - right: 10; - background:transparent; - border-radius:100%; - width: 25px; - height: 25px; - display: flex; - align-items: center; - justify-content: center; - transition: 0.2s; -} - -.close-button:hover { - background: rgba(0,0,0,0.1); -} - diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts index 074e89753..6977fb0b6 100644 --- a/src/client/util/CurrentUserUtils.ts +++ b/src/client/util/CurrentUserUtils.ts @@ -12,7 +12,7 @@ import { Cast, DateCast, DocCast, StrCast } from "../../fields/Types"; import { nullAudio } from "../../fields/URLField"; import { SetCachedGroups, SharingPermissions } from "../../fields/util"; import { GestureUtils } from "../../pen-gestures/GestureUtils"; -import { OmitKeys, Utils } from "../../Utils"; +import { OmitKeys, Utils, addStyleSheetRule } from "../../Utils"; import { DocServer } from "../DocServer"; import { Docs, DocumentOptions, DocUtils, FInfo } from "../documents/Documents"; import { CollectionViewType, DocumentType } from "../documents/DocumentTypes"; @@ -20,7 +20,7 @@ import { TreeViewType } from "../views/collections/CollectionTreeView"; import { DashboardView } from "../views/DashboardView"; import { Colors } from "../views/global/globalEnums"; import { MainView } from "../views/MainView"; -import { ButtonType } from "../views/nodes/button/FontIconBox"; +import { ButtonType } from "../views/nodes/FontIconBox/FontIconBox"; import { OpenWhere } from "../views/nodes/DocumentView"; import { OverlayView } from "../views/OverlayView"; import { DragManager, dropActionType } from "./DragManager"; @@ -28,7 +28,7 @@ import { MakeTemplate } from "./DropConverter"; import { FollowLinkScript } from "./LinkFollower"; import { LinkManager } from "./LinkManager"; import { ScriptingGlobals } from "./ScriptingGlobals"; -import { ColorScheme } from "./SettingsManager"; +import { ColorScheme, SettingsManager } from "./SettingsManager"; import { UndoManager } from "./UndoManager"; import { PresElementBox } from "../views/nodes/trails"; import { ImportElementBox } from "../views/nodes/importBox/importElementBox"; @@ -267,7 +267,7 @@ export class CurrentUserUtils { }[] = [ {key: "Note", creator: opts => Docs.Create.TextDocument("", opts), opts: { _width: 200, _layout_autoHeight: true }}, {key: "Flashcard", creator: opts => Docs.Create.TextDocument("", opts), opts: { _width: 200, _layout_autoHeight: true, _layout_enableAltContentUI: true}}, - {key: "Equation", creator: opts => Docs.Create.EquationDocument(opts), opts: { _width: 300, _height: 35, }}, + {key: "Equation", creator: opts => Docs.Create.EquationDocument("",opts), opts: { _width: 300, _height: 35, }}, {key: "Noteboard", creator: opts => Docs.Create.NoteTakingDocument([], opts), opts: { _width: 250, _height: 200, _layout_fitWidth: true}}, {key: "Simulation", creator: opts => Docs.Create.SimulationDocument(opts), opts: { _width: 300, _height: 300, }}, {key: "Collection", creator: opts => Docs.Create.FreeformDocument([], opts), opts: { _width: 150, _height: 100, _layout_fitWidth: true }}, @@ -340,19 +340,19 @@ export class CurrentUserUtils { } /// returns descriptions needed to buttons for the left sidebar to open up panes displaying different collections of documents - static leftSidebarMenuBtnDescriptions(doc: Doc):{title:string, target:Doc, icon:string, scripts:{[key:string]:any}, funcs?:{[key:string]:any}}[] { + static leftSidebarMenuBtnDescriptions(doc: Doc):{title:string, target:Doc, icon:string, toolTip: string, scripts:{[key:string]:any}, funcs?:{[key:string]:any}}[] { const badgeValue = "((len) => len && len !== '0' ? len: undefined)(docList(self.target.data).filter(doc => !docList(self.target.viewed).includes(doc)).length.toString())"; const getActiveDashTrails = "Doc.ActiveDashboard?.myTrails"; return [ - { title: "Dashboards", target: this.setupDashboards(doc, "myDashboards"), icon: "desktop", funcs: {hidden: "IsNoviceMode()"} }, - { title: "Search", target: this.setupSearcher(doc, "mySearcher"), icon: "search", }, - { title: "Files", target: this.setupFilesystem(doc, "myFilesystem"), icon: "folder-open", }, - { 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: Doc.MySharedDocs, icon: "users", funcs: {badgeValue:badgeValue}}, - { title: "Trails", target: Doc.UserDoc(), icon: "pres-trail", funcs: {target: getActiveDashTrails}}, - { title: "User Doc View", target: this.setupUserDocView(doc, "myUserDocView"), icon: "address-card",funcs: {hidden: "IsNoviceMode()"} }, + { title: "Dashboards", toolTip: "Dashboards ⌘D", target: this.setupDashboards(doc, "myDashboards"), icon: "desktop", funcs: {hidden: "IsNoviceMode()"} }, + { title: "Search", toolTip: "Search ⌘F", target: this.setupSearcher(doc, "mySearcher"), icon: "search", }, + { title: "Files", toolTip: "Files ⌘⇧F", target: this.setupFilesystem(doc, "myFilesystem"), icon: "folder-open", }, + { title: "Tools", toolTip: "Tools ⌘T", target: this.setupToolsBtnPanel(doc, "myTools"), icon: "wrench", funcs: {hidden: "IsNoviceMode()"} }, + { title: "Imports", toolTip: "Imports ⌘I", target: this.setupImportSidebar(doc, "myImports"), icon: "upload", }, + { title: "Closed", toolTip: "Recently Closed ⌘R", target: this.setupRecentlyClosed(doc, "myRecentlyClosed"), icon: "archive", }, + { title: "Shared", toolTip: "Shared Docs ⌘⇧S", target: Doc.MySharedDocs, icon: "users", funcs: {badgeValue:badgeValue}}, + { title: "Trails", toolTip: "Trails ⌘⇧", target: Doc.UserDoc(), icon: "pres-trail", funcs: {target: getActiveDashTrails}}, + { title: "User Doc", toolTip: "User Doc ⌘U", target: this.setupUserDocView(doc, "myUserDocView"), icon: "address-card",funcs: {hidden: "IsNoviceMode()"} }, ].map(tuple => ({...tuple, scripts:{onClick: 'selectMainMenu(self)'}})); } @@ -365,17 +365,17 @@ export class CurrentUserUtils { static setupLeftSidebarMenu(doc: Doc, field="myLeftSidebarMenu") { this.setupLeftSidebarPanel(doc); const myLeftSidebarMenu = DocCast(doc[field]); - const menuBtns = CurrentUserUtils.leftSidebarMenuBtnDescriptions(doc).map(({ title, target, icon, scripts, funcs }) => { + const menuBtns = CurrentUserUtils.leftSidebarMenuBtnDescriptions(doc).map(({ title, target, icon, toolTip, scripts, funcs }) => { const btnDoc = myLeftSidebarMenu ? DocListCast(myLeftSidebarMenu.data).find(doc => doc.title === title) : undefined; const reqdBtnOpts:DocumentOptions = { - title, icon, target, btnType: ButtonType.MenuButton, isSystem: true, undoIgnoreFields: new List<string>(['height', 'data_columnHeaders']), dontRegisterView: true, + title, icon, target, toolTip, btnType: ButtonType.MenuButton, isSystem: true, undoIgnoreFields: new List<string>(['height', 'data_columnHeaders']), dontRegisterView: true, _width: 60, _height: 60, _dragOnlyWithinContainer: true, _layout_hideContextMenu: true, }; return DocUtils.AssignScripts(DocUtils.AssignOpts(btnDoc, reqdBtnOpts) ?? Docs.Create.FontIconDocument(reqdBtnOpts), scripts, funcs); }); const reqdStackOpts:DocumentOptions ={ - title: "menuItemPanel", childDragAction: "same", backgroundColor: Colors.DARK_GRAY, layout_boxShadow: "rgba(0,0,0,0)", dontRegisterView: true, ignoreClick: true, + title: "menuItemPanel", childDragAction: "same", layout_boxShadow: "rgba(0,0,0,0)", dontRegisterView: true, ignoreClick: true, _chromeHidden: true, _gridGap: 0, _yMargin: 0, _yPadding: 0, _xMargin: 0, _layout_autoHeight: false, _width: 60, _columnWidth: 60, _lockedPosition: true, isSystem: true, }; return DocUtils.AssignDocField(doc, field, (opts, items) => Docs.Create.StackingDocument(items??[], opts), reqdStackOpts, menuBtns, { dropConverter: "convertToButtons(dragData)" }); @@ -525,7 +525,7 @@ export class CurrentUserUtils { const newFolder = `TreeView_addNewFolder()`; const newFolderOpts: DocumentOptions = { _forceActive: true, _dragOnlyWithinContainer: true, _layout_hideContextMenu: true, _width: 30, _height: 30, undoIgnoreFields:new List<string>(['treeViewSortCriterion']), - title: "New folder", btnType: ButtonType.ClickButton, toolTip: "Create new folder", buttonText: "New folder", icon: "folder-plus", isSystem: true + title: "New folder", color: Colors.BLACK, btnType: ButtonType.ClickButton, toolTip: "Create new folder", buttonText: "New folder", icon: "folder-plus", isSystem: true }; const newFolderScript = { onClick: newFolder}; const newFolderButton = DocUtils.AssignScripts(DocUtils.AssignOpts(DocCast(myFilesystem?.layout_headerButton), newFolderOpts) ?? Docs.Create.FontIconDocument(newFolderOpts), newFolderScript); @@ -559,7 +559,7 @@ export class CurrentUserUtils { const clearAll = (target:string) => `getProto(${target}).data = new List([])`; const clearBtnsOpts:DocumentOptions = { _width: 30, _height: 30, _forceActive: true, _dragOnlyWithinContainer: true, _layout_hideContextMenu: true, - title: "Empty", target: recentlyClosed, btnType: ButtonType.ClickButton, buttonText: "Empty", icon: "trash", isSystem: true, + title: "Empty", target: recentlyClosed, btnType: ButtonType.ClickButton, color: Colors.BLACK, buttonText: "Empty", icon: "trash", isSystem: true, toolTip: "Empty recently closed",}; DocUtils.AssignDocField(recentlyClosed, "layout_headerButton", (opts) => Docs.Create.FontIconDocument(opts), clearBtnsOpts, undefined, {onClick: clearAll("self.target")}); @@ -602,8 +602,8 @@ export class CurrentUserUtils { CurrentUserUtils.createToolButton(opts), scripts, funcs); 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 - { scripts: { onClick: "undo()"}, opts: { title: "undo", icon: "undo-alt", toolTip: "Click to undo" }}, - { scripts: { onClick: "redo()"}, opts: { title: "redo", icon: "redo-alt", toolTip: "Click to redo" }}, + { scripts: { onClick: "undo()"}, opts: { title: "Undo", icon: "undo-alt", toolTip: "Undo ⌘Z" }}, + { scripts: { onClick: "redo()"}, opts: { title: "Redo", icon: "redo-alt", toolTip: "Redo ⌘R" }}, { scripts: { }, opts: { title: "undoStack", layout: "<UndoStack>", toolTip: "Undo/Redo Stack"}}, // note: layout fields are hacks -- they don't actually run through the JSX parser (yet) { scripts: { }, opts: { title: "linker", layout: "<LinkingUI>", toolTip: "link started"}}, { scripts: { }, opts: { title: "currently playing", layout: "<CurrentlyPlayingUI>", toolTip: "currently playing media"}}, @@ -611,10 +611,10 @@ export class CurrentUserUtils { const btns = btnDescs.map(desc => dockBtn({_width: 30, _height: 30, defaultDoubleClick: 'ignore', undoIgnoreFields: new List<string>(['opacity']), _dragOnlyWithinContainer: true, ...desc.opts}, desc.scripts)); const dockBtnsReqdOpts:DocumentOptions = { title: "docked buttons", _height: 40, flexGap: 0, layout_boxShadow: "standard", childDragAction: 'move', - childDontRegisterViews: true, linearView_IsExpanded: true, linearView_Expandable: true, ignoreClick: true + childDontRegisterViews: true, linearView_IsOpen: true, linearView_Expandable: true, ignoreClick: true }; - 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 }); + 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 DocUtils.AssignDocField(doc, field, (opts, items) => this.linearButtonList(opts, items??[]), dockBtnsReqdOpts, btns); } @@ -627,30 +627,34 @@ export class CurrentUserUtils { } static viewTools(): Button[] { return [ - { title: "Snap\xA0Lines", icon: "th", toolTip: "Show Snap Lines", btnType: ButtonType.ToggleButton, expertMode: false, toolType:"snaplines", funcs: {}, scripts: { onClick: 'showFreeform(self.toolType, _readOnly_)'}}, // Only when floating document is selected in freeform - { title: "Grid", icon: "border-all", toolTip: "Show Grid", btnType: ButtonType.ToggleButton, expertMode: false, toolType:"grid", funcs: {}, scripts: { onClick: 'showFreeform(self.toolType, _readOnly_)'}}, // Only when floating document is selected in freeform - { title: "View\xA0All", icon: "object-group", toolTip: "Fit all Docs to View",btnType: ButtonType.ToggleButton, expertMode: false, toolType:"viewAll", funcs: {}, scripts: { onClick: 'showFreeform(self.toolType, _readOnly_)'}}, // Only when floating document is selected in freeform - { title: "Clusters", icon: "braille", toolTip: "Show Doc Clusters", btnType: ButtonType.ToggleButton, expertMode: false, toolType:"clusters", funcs: {}, scripts: { onClick: 'showFreeform(self.toolType, _readOnly_)'}}, // Only when floating document is selected in freeform - { title: "Cards", icon: "brain", toolTip: "Flashcards", btnType: ButtonType.ToggleButton, expertMode: false, toolType:"flashcards", funcs: {}, scripts: { onClick: 'showFreeform(self.toolType, _readOnly_)'}}, // Only when floating document is selected in freeform - { title: "Arrange",icon:"arrow-down-short-wide",toolTip:"Toggle Auto Arrange", btnType: ButtonType.ToggleButton, expertMode: false, toolType:"arrange", funcs: {hidden: 'IsNoviceMode()'}, scripts: { onClick: 'showFreeform(self.toolType, _readOnly_)'}}, // Only when floating document is selected in freeform + { title: "Snap", icon: "th", toolTip: "Show Snap Lines", btnType: ButtonType.ToggleButton, ignoreClick: true, expertMode: false, toolType:"snaplines", funcs: {}, scripts: { onClick: '{ return showFreeform(self.toolType, _readOnly_);}'}}, // Only when floating document is selected in freeform + { title: "Grid", icon: "border-all", toolTip: "Show Grid", btnType: ButtonType.ToggleButton, ignoreClick: true, expertMode: false, toolType:"grid", funcs: {}, scripts: { onClick: '{ return showFreeform(self.toolType, _readOnly_);}'}}, // Only when floating document is selected in freeform + { title: "View All", icon: "object-group", toolTip: "Fit all Docs to View", btnType: ButtonType.ToggleButton, ignoreClick: true, expertMode: false, toolType:"viewAll", funcs: {}, scripts: { onClick: '{ return showFreeform(self.toolType, _readOnly_);}'}}, // Only when floating document is selected in freeform + { title: "Clusters", icon: "braille", toolTip: "Show Doc Clusters", btnType: ButtonType.ToggleButton, ignoreClick: true, expertMode: false, toolType:"clusters", funcs: {}, scripts: { onClick: '{ return showFreeform(self.toolType, _readOnly_);}'}}, // Only when floating document is selected in freeform + { title: "Cards", icon: "brain", toolTip: "Flashcards", btnType: ButtonType.ToggleButton, ignoreClick: true, expertMode: false, toolType:"flashcards", funcs: {}, scripts: { onClick: '{ return showFreeform(self.toolType, _readOnly_);}'}}, // Only when floating document is selected in freeform + { title: "Arrange",icon:"arrow-down-short-wide",toolTip:"Toggle Auto Arrange", btnType: ButtonType.ToggleButton, ignoreClick: true, expertMode: false, toolType:"arrange", funcs: {hidden: 'IsNoviceMode()'}, scripts: { onClick: '{ return showFreeform(self.toolType, _readOnly_);}'}}, // Only when floating document is selected in freeform ] } static textTools():Button[] { return [ { title: "Font", toolTip: "Font", width: 100, btnType: ButtonType.DropdownList, toolType:"font", ignoreClick: true, scripts: {script: '{ return setFontAttr(self.toolType, value, _readOnly_);}'}, btnList: new List<string>(["Roboto", "Roboto Mono", "Nunito", "Times New Roman", "Arial", "Georgia", "Comic Sans MS", "Tahoma", "Impact", "Crimson Text"]) }, - { title: "Font Size",toolTip: "Font size (%size)", btnType: ButtonType.NumberDropdownButton, width: 75, toolType:"fontSize", ignoreClick: true, scripts: {script: '{ return setFontAttr(self.toolType, value, _readOnly_);}'}, numBtnMax: 200, numBtnMin: 0 }, + { title: "Font Size",toolTip: "Font size (%size)", btnType: ButtonType.NumberDropdownButton, toolType:"fontSize", ignoreClick: true, scripts: {script: '{ return setFontAttr(self.toolType, value, _readOnly_);}'}, numBtnMax: 200, numBtnMin: 6 }, { title: "Color", toolTip: "Font color (%color)", btnType: ButtonType.ColorButton, icon: "font", toolType:"fontColor",ignoreClick: true, scripts: {script: '{ return setFontAttr(self.toolType, value, _readOnly_);}'}}, - { title: "Highlight",toolTip:"Font highlight", btnType: ButtonType.ColorButton, icon: "highlighter", toolType:"highlight",ignoreClick: true, scripts: {script: '{ return setFontAttr(self.toolType, value, _readOnly_);}'},funcs: {hidden: "IsNoviceMode()"} }, - { title: "Bold", toolTip: "Bold (Ctrl+B)", btnType: ButtonType.ToggleButton, icon: "bold", toolType:"bold", scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}'} }, - { title: "Italic", toolTip: "Italic (Ctrl+I)", btnType: ButtonType.ToggleButton, icon: "italic", toolType:"italics", scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}'} }, - { title: "Under", toolTip: "Underline (Ctrl+U)", btnType: ButtonType.ToggleButton, icon: "underline", toolType:"underline", scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}'} }, - { title: "Bullets", toolTip: "Bullet List", btnType: ButtonType.ToggleButton, icon: "list", toolType:"bullet", scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}'} }, - { title: "#", toolTip: "Number List", btnType: ButtonType.ToggleButton, icon: "list-ol", toolType:"decimal", scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}'} }, - { title: "Left", toolTip: "Left align (Cmd-[)", btnType: ButtonType.ToggleButton, icon: "align-left", toolType:"left", scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}' }}, - { title: "Center", toolTip: "Center align (Cmd-\\)",btnType: ButtonType.ToggleButton, icon: "align-center",toolType:"center", scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}'} }, - { title: "Right", toolTip: "Right align (Cmd-])", btnType: ButtonType.ToggleButton, icon: "align-right", toolType:"right", scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}'} }, - { title: "Dictate", toolTip: "Dictate", btnType: ButtonType.ToggleButton, icon: "microphone", toolType:"dictation", scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}'}}, + { title: "Highlight",toolTip:"Font highlight", btnType: ButtonType.ColorButton, icon: "highlighter", toolType:"highlight",ignoreClick: true, scripts: {script: '{ return setFontAttr(self.toolType, value, _readOnly_);}'},funcs: {hidden: "IsNoviceMode()"} }, + { title: "Bold", toolTip: "Bold (Ctrl+B)", btnType: ButtonType.ToggleButton, icon: "bold", toolType:"bold", ignoreClick: true, scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}'} }, + { title: "Italic", toolTip: "Italic (Ctrl+I)", btnType: ButtonType.ToggleButton, icon: "italic", toolType:"italics", ignoreClick: true, scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}'} }, + { title: "Under", toolTip: "Underline (Ctrl+U)", btnType: ButtonType.ToggleButton, icon: "underline", toolType:"underline",ignoreClick: true, scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}'} }, + { title: "Bullets", toolTip: "Bullet List", btnType: ButtonType.ToggleButton, icon: "list", toolType:"bullet", ignoreClick: true, scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}'} }, + { title: "#", toolTip: "Number List", btnType: ButtonType.ToggleButton, icon: "list-ol", toolType:"decimal", ignoreClick: true, scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}'} }, + { title: "Alignment",toolTip: "Alignment", btnType: ButtonType.MultiToggleButton, toolType:"alignment", ignoreClick: true, scripts: {script: '{ return setFontAttr(self.toolType, value, _readOnly_);}'}, + subMenu: [ + { title: "Left", toolTip: "Left align (Cmd-[)", btnType: ButtonType.ToggleButton, icon: "align-left", toolType:"left", ignoreClick: true, scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}' }}, + { title: "Center", toolTip: "Center align (Cmd-\\)",btnType: ButtonType.ToggleButton, icon: "align-center",toolType:"center",ignoreClick: true, scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}'} }, + { title: "Right", toolTip: "Right align (Cmd-])", btnType: ButtonType.ToggleButton, icon: "align-right", toolType:"right", ignoreClick: true, scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}'} }, + ] + }, + { title: "Dictate", toolTip: "Dictate", btnType: ButtonType.ToggleButton, icon: "microphone", toolType:"dictation", ignoreClick: true, scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}'}}, { title: "NoLink", toolTip: "Auto Link", btnType: ButtonType.ToggleButton, icon: "link", toolType:"noAutoLink", expertMode:true, scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}'}, funcs: {hidden: 'IsNoviceMode()'}}, // { title: "Strikethrough", tooltip: "Strikethrough", btnType: ButtonType.ToggleButton, icon: "strikethrough", scripts: {onClick:: 'toggleStrikethrough()'}}, // { title: "Superscript", tooltip: "Superscript", btnType: ButtonType.ToggleButton, icon: "superscript", scripts: {onClick:: 'toggleSuperscript()'}}, @@ -695,19 +699,19 @@ export class CurrentUserUtils { CollectionViewType.Carousel3D, CollectionViewType.Linear, CollectionViewType.Map, CollectionViewType.Grid, CollectionViewType.NoteTaking]), title: "Perspective", toolTip: "View", btnType: ButtonType.DropdownList, ignoreClick: true, width: 100, scripts: { script: 'setView(value, _readOnly_)'}}, - { title: "Pin", icon: "map-pin", toolTip: "Pin View to Trail", btnType: ButtonType.ClickButton, expertMode: false, width: 20, scripts: { onClick: 'pinWithView(altKey)'}}, - { title: "Fill", icon: "fill-drip", toolTip: "Background Fill Color",btnType: ButtonType.ColorButton, expertMode: false, ignoreClick: true, width: 20, scripts: { script: 'return setBackgroundColor(value, _readOnly_)'}}, // Only when a document is selected - { title: "Header", icon: "heading", toolTip: "Header Color", btnType: ButtonType.ColorButton, expertMode: false, ignoreClick: true, scripts: { script: 'return setHeaderColor(value, _readOnly_)'}}, - { title: "Overlay", icon: "layer-group", toolTip: "Overlay", btnType: ButtonType.ToggleButton, expertMode: false, toolType:CollectionViewType.Freeform, funcs: {hidden: '!SelectionManager_selectedDocType(self.toolType, self.expertMode, true)'}, scripts: { onClick: 'toggleOverlay(_readOnly_)'}}, // Only when floating document is selected in freeform - { title: "Back", icon: "chevron-left", toolTip: "Prev Animation Frame", btnType: ButtonType.ClickButton, expertMode: true, toolType:CollectionViewType.Freeform, funcs: {hidden: '!SelectionManager_selectedDocType(self.toolType, self.expertMode)'}, width: 20, scripts: { onClick: 'prevKeyFrame(_readOnly_)'}}, - { title: "Num", icon:"",toolTip: "Frame Number (click to toggle edit mode)",btnType: ButtonType.TextButton, expertMode: true, toolType:CollectionViewType.Freeform, funcs: {hidden: '!SelectionManager_selectedDocType(self.toolType, self.expertMode)', buttonText: 'selectedDocs()?.lastElement()?.currentFrame?.toString()'}, width: 20, scripts: { onClick: '{ return curKeyFrame(_readOnly_);}'}}, - { title: "Fwd", icon: "chevron-right", toolTip: "Next Animation Frame", btnType: ButtonType.ClickButton, expertMode: true, toolType:CollectionViewType.Freeform, funcs: {hidden: '!SelectionManager_selectedDocType(self.toolType, self.expertMode)'}, width: 20, scripts: { onClick: 'nextKeyFrame(_readOnly_)'}}, - { title: "Text", icon: "Text", toolTip: "Text functions", subMenu: CurrentUserUtils.textTools(), expertMode: false, toolType:DocumentType.RTF, funcs: { linearView_IsExpanded: `SelectionManager_selectedDocType(self.toolType, self.expertMode)`} }, // Always available - { title: "Ink", icon: "Ink", toolTip: "Ink functions", subMenu: CurrentUserUtils.inkTools(), expertMode: false, toolType:DocumentType.INK, funcs: { linearView_IsExpanded: `SelectionManager_selectedDocType(self.toolType, self.expertMode)`}, scripts: { onClick: 'setInkToolDefaults()'} }, // Always available - { title: "Doc", icon: "Doc", toolTip: "Freeform Doc tools", subMenu: CurrentUserUtils.freeTools(), expertMode: false, toolType:CollectionViewType.Freeform, funcs: {hidden: `!SelectionManager_selectedDocType(self.toolType, self.expertMode, true)`, linearView_IsExpanded: `SelectionManager_selectedDocType(self.toolType, self.expertMode)`} }, // Always available - { title: "View", icon: "View", toolTip: "View tools", subMenu: CurrentUserUtils.viewTools(), expertMode: false, toolType:CollectionViewType.Freeform, funcs: {hidden: `!SelectionManager_selectedDocType(self.toolType, self.expertMode)`, linearView_IsExpanded: `SelectionManager_selectedDocType(self.toolType, self.expertMode)`} }, // Always available - { title: "Web", icon: "Web", toolTip: "Web functions", subMenu: CurrentUserUtils.webTools(), expertMode: false, toolType:DocumentType.WEB, funcs: {hidden: `!SelectionManager_selectedDocType(self.toolType, self.expertMode)`, linearView_IsExpanded: `SelectionManager_selectedDocType(self.toolType, self.expertMode)`} }, // Only when Web is selected - { title: "Schema", icon: "Schema",linearBtnWidth:58,toolTip: "Schema functions",subMenu: CurrentUserUtils.schemaTools(), expertMode: false, toolType:CollectionViewType.Schema, funcs: {hidden: `!SelectionManager_selectedDocType(self.toolType, self.expertMode)`, linearView_IsExpanded: `SelectionManager_selectedDocType(self.toolType, self.expertMode)`} } // Only when Schema is selected + { title: "Pin", icon: "map-pin", toolTip: "Pin View to Trail", btnType: ButtonType.ClickButton, expertMode: false, width: 30, scripts: { onClick: 'pinWithView(altKey)'}}, + { title: "Fill", icon: "fill-drip", toolTip: "Background Fill Color",btnType: ButtonType.ColorButton, expertMode: false, ignoreClick: true, width: 30, scripts: { script: 'return setBackgroundColor(value, _readOnly_)'}}, // Only when a document is selected + { title: "Header", icon: "heading", toolTip: "Header Color", btnType: ButtonType.ColorButton, expertMode: true, ignoreClick: true, scripts: { script: 'return setHeaderColor(value, _readOnly_)'}}, + { title: "Overlay", icon: "layer-group", toolTip: "Overlay", btnType: ButtonType.ToggleButton, expertMode: true, toolType:CollectionViewType.Freeform, funcs: {hidden: '!SelectionManager_selectedDocType(self.toolType, self.expertMode, true)'}, scripts: { onClick: 'return { toggleOverlay(_readOnly_); }'}}, // Only when floating document is selected in freeform + { title: "Back", icon: "chevron-left", toolTip: "Prev Animation Frame", btnType: ButtonType.ClickButton, expertMode: true, toolType:CollectionViewType.Freeform, funcs: {hidden: '!SelectionManager_selectedDocType(self.toolType, self.expertMode)'}, width: 30, scripts: { onClick: 'prevKeyFrame(_readOnly_)'}}, + { title: "Num", icon:"", toolTip: "Frame Number (click to toggle edit mode)", btnType: ButtonType.TextButton, expertMode: true, toolType:CollectionViewType.Freeform, funcs: {hidden: '!SelectionManager_selectedDocType(self.toolType, self.expertMode)', buttonText: 'selectedDocs()?.lastElement()?.currentFrame?.toString()'}, width: 20, scripts: { onClick: '{ return curKeyFrame(_readOnly_);}'}}, + { title: "Fwd", icon: "chevron-right", toolTip: "Next Animation Frame", btnType: ButtonType.ClickButton, expertMode: true, toolType:CollectionViewType.Freeform, funcs: {hidden: '!SelectionManager_selectedDocType(self.toolType, self.expertMode)'}, width: 30, scripts: { onClick: 'nextKeyFrame(_readOnly_)'}}, + { title: "Text", icon: "Text", toolTip: "Text functions", subMenu: CurrentUserUtils.textTools(), expertMode: false, toolType:DocumentType.RTF, funcs: { linearView_IsOpen: `SelectionManager_selectedDocType(self.toolType, self.expertMode)`} }, // Always available + { title: "Ink", icon: "Ink", toolTip: "Ink functions", subMenu: CurrentUserUtils.inkTools(), expertMode: false, toolType:DocumentType.INK, funcs: { linearView_IsOpen: `SelectionManager_selectedDocType(self.toolType, self.expertMode)`}, scripts: { onClick: 'setInkToolDefaults()'} }, // Always available + { title: "Doc", icon: "Doc", toolTip: "Freeform Doc tools", subMenu: CurrentUserUtils.freeTools(), expertMode: false, toolType:CollectionViewType.Freeform, funcs: {hidden: `!SelectionManager_selectedDocType(self.toolType, self.expertMode, true)`, linearView_IsOpen: `SelectionManager_selectedDocType(self.toolType, self.expertMode)`} }, // Always available + { title: "View", icon: "View", toolTip: "View tools", subMenu: CurrentUserUtils.viewTools(), expertMode: false, toolType:CollectionViewType.Freeform, funcs: {hidden: `!SelectionManager_selectedDocType(self.toolType, self.expertMode)`, linearView_IsOpen: `SelectionManager_selectedDocType(self.toolType, self.expertMode)`} }, // Always available + { title: "Web", icon: "Web", toolTip: "Web functions", subMenu: CurrentUserUtils.webTools(), expertMode: false, toolType:DocumentType.WEB, funcs: {hidden: `!SelectionManager_selectedDocType(self.toolType, self.expertMode)`, linearView_IsOpen: `SelectionManager_selectedDocType(self.toolType, self.expertMode)`} }, // Only when Web is selected + { title: "Schema", icon: "Schema",linearBtnWidth:58,toolTip: "Schema functions",subMenu: CurrentUserUtils.schemaTools(), expertMode: false, toolType:CollectionViewType.Schema, funcs: {hidden: `!SelectionManager_selectedDocType(self.toolType, self.expertMode)`, linearView_IsOpen: `SelectionManager_selectedDocType(self.toolType, self.expertMode)`} } // Only when Schema is selected ]; } @@ -715,7 +719,6 @@ export class CurrentUserUtils { static setupContextMenuButton(params:Button, btnDoc?:Doc) { const reqdOpts:DocumentOptions = { ...OmitKeys(params, ["scripts", "funcs", "subMenu"]).omit, - backgroundColor: params.backgroundColor ??"transparent", /// a bit hacky. if an onClick is specified, then assume a toggle uses onClick to get the backgroundColor (see below). Otherwise, assume a transparent background color: Colors.WHITE, isSystem: true, _nativeWidth: params.width ?? 30, _width: params.width ?? 30, _height: 30, _nativeHeight: 30, linearBtnWidth: params.linearBtnWidth, @@ -724,23 +727,29 @@ export class CurrentUserUtils { }; const reqdFuncs:{[key:string]:any} = { ...params.funcs, - backgroundColor: params.btnType === ButtonType.ToggleButton ? params.scripts?.onClick:undefined /// 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 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:DocumentOptions = { title: "context menu buttons", undoIgnoreFields:new List<string>(['width', "linearView_IsExpanded"]), flexGap: 0, childDragAction: 'embed', childDontRegisterViews: true, linearView_IsExpanded: true, ignoreClick: true, linearView_Expandable: false, _height: 35 }; + const reqdCtxtOpts:DocumentOptions = { title: "context menu buttons", undoIgnoreFields:new List<string>(['width', "linearView_IsOpen"]), flexGap: 0, childDragAction: 'embed', childDontRegisterViews: true, linearView_IsOpen: true, ignoreClick: true, linearView_Expandable: false, _height: 35 }; 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) { + if (!params.subMenu) { // button does not have a sub menu return this.setupContextMenuButton(params, menuBtnDoc); - } else { - const reqdSubMenuOpts = { ...OmitKeys(params, ["scripts", "funcs", "subMenu"]).omit, undoIgnoreFields: new List<string>(['width', "linearView_IsExpanded"]), + } else { // linear view + let reqdSubMenuOpts; + if (params.btnType === ButtonType.MultiToggleButton) { + reqdSubMenuOpts = { ...OmitKeys(params, ["scripts", "funcs", "subMenu"]).omit, undoIgnoreFields: new List<string>(['width', "linearView_IsOpen"]), + childDontRegisterViews: true, flexGap: 0, _height: 30, ignoreClick: params.scripts?.onClick ? false : true, + linearView_SubMenu: true, linearView_Dropdown: true, }; + } else { + reqdSubMenuOpts = { ...OmitKeys(params, ["scripts", "funcs", "subMenu"]).omit, undoIgnoreFields: new List<string>(['width', "linearView_IsOpen"]), childDontRegisterViews: true, flexGap: 0, _height: 30, ignoreClick: params.scripts?.onClick ? false : true, linearView_SubMenu: true, linearView_Expandable: true, }; + } const items = params.subMenu?.map(sub => this.setupContextMenuButton(sub, DocListCast(menuBtnDoc?.data).find(doc => doc.title === sub.title)) ); @@ -767,7 +776,7 @@ export class CurrentUserUtils { linkDocs.title = "LINK DATABASE: " + Doc.CurrentUserEmail; linkDocs.author = Doc.CurrentUserEmail; linkDocs.data = new List<Doc>([]); - linkDocs["acl-Public"] = SharingPermissions.Augment; + linkDocs["acl-Guest"] = SharingPermissions.Augment; doc.myLinkDatabase = new PrefetchProxy(linkDocs); } } @@ -777,8 +786,6 @@ export class CurrentUserUtils { // When the user views one of these documents, it will be added to the sharing documents 'viewed' list field // The sharing document also stores the user's color value which helps distinguish shared documents from personal documents static setupSharedDocs(doc: Doc, sharingDocumentId: string) { - const addToDashboards = ScriptField.MakeScript(`addToDashboards(self)`); - const dashboardFilter = ScriptField.MakeFunction(`doc._type_collection === '${CollectionViewType.Docking}'`, { doc: Doc.name }); const dblClkScript = "{scriptContext.openLevel(documentView); addDocToList(scriptContext.props.treeView.props.Document, 'viewed', documentView.rootDoc);}"; const sharedScripts = { treeViewChildDoubleClick: dblClkScript, } @@ -786,11 +793,11 @@ export class CurrentUserUtils { title: "My Shared Docs", userColor: "rgb(202, 202, 202)", isFolder:true, undoIgnoreFields:new List<string>(['treeViewSortCriterion']), - childContextMenuFilters: new List<ScriptField>([dashboardFilter!,]), - childContextMenuScripts: new List<ScriptField>([addToDashboards!,]), - childContextMenuLabels: new List<string>(["Add to Dashboards",]), - childContextMenuIcons: new List<string>(["user-plus",]), - "acl-Public": SharingPermissions.Augment, "_acl-Public": SharingPermissions.Augment, + // childContextMenuFilters: new List<ScriptField>([dashboardFilter!,]), + // childContextMenuScripts: new List<ScriptField>([addToDashboards!,]), + // childContextMenuLabels: new List<string>(["Add to Dashboards",]), + // childContextMenuIcons: new List<string>(["user-plus",]), + "acl-Guest": SharingPermissions.Augment, "_acl-Guest": SharingPermissions.Augment, childDragAction: "embed", isSystem: true, contentPointerEvents: "all", childLimitHeight: 0, _yMargin: 0, _gridGap: 15, childDontRegisterViews:true, // NOTE: treeViewHideTitle & _layout_showTitle is for a TreeView's editable title, _layout_showTitle is for DocumentViews title bar _layout_showTitle: "title", treeViewHideTitle: true, ignoreClick: true, _lockedPosition: true, layout_boxShadow: "0 0", _chromeHidden: true, dontRegisterView: true, @@ -798,6 +805,8 @@ export class CurrentUserUtils { }; DocUtils.AssignDocField(doc, "mySharedDocs", opts => Docs.Create.TreeDocument([], opts, sharingDocumentId + "layout", sharingDocumentId), sharedDocOpts, undefined, sharedScripts); + if (!Doc.GetProto(DocCast(doc.mySharedDocs)).data_dashboards) Doc.GetProto(DocCast(doc.mySharedDocs)).data_dashboards = new List<Doc>(); + console.log(doc.mySharedDocs); } /// Import option on the left side button panel @@ -812,7 +821,7 @@ export class CurrentUserUtils { 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, _dragOnlyWithinContainer: true, _layout_hideContextMenu: true, title: "Import", btnType: ButtonType.ClickButton, + _width: 30, _height: 30, color: Colors.BLACK, _dragOnlyWithinContainer: true, _layout_hideContextMenu: true, title: "Import", btnType: ButtonType.ClickButton, buttonText: "Import", icon: "upload", isSystem: true }; DocUtils.AssignDocField(myImports, "layout_headerButton", (opts) => Docs.Create.FontIconDocument(opts), reqdBtnOpts, undefined, { onClick: "importDocument()" }); return myImports; @@ -826,7 +835,7 @@ export class CurrentUserUtils { async () => { const groups = await DocListCastAsync(DocCast(doc.globalGroupDatabase).data); const mygroups = groups?.filter(group => JSON.parse(StrCast(group.members)).includes(Doc.CurrentUserEmail)) || []; - SetCachedGroups(["Public", ...mygroups?.map(g => StrCast(g.title))]); + SetCachedGroups(["Guest", ...mygroups?.map(g => StrCast(g.title))]); }, { fireImmediately: true }); doc.isSystem ?? (doc.isSystem = true); doc.title ?? (doc.title = Doc.CurrentUserEmail); @@ -850,8 +859,14 @@ export class CurrentUserUtils { doc.fontHighlight ?? (doc.fontHighlight = ""); doc.defaultAclPrivate ?? (doc.defaultAclPrivate = false); doc.savedFilters ?? (doc.savedFilters = new List<Doc>()); + doc.userBackgroundColor ?? (doc.userBackgroundColor = Colors.DARK_GRAY); + addStyleSheetRule(SettingsManager._settingsStyle, 'lm_header', { background: `${doc.userBackgroundColor} !important` }); + doc.userVariantColor ?? (doc.userVariantColor = Colors.MEDIUM_BLUE); + doc.userColor ?? (doc.userColor = Colors.LIGHT_GRAY); + doc.userTheme ?? (doc.userTheme = ColorScheme.Dark); doc.filterDocCount = 0; doc.treeViewFreezeChildren = "remove|add"; + doc.activePage = doc.activeDashboard === undefined ? 'home': doc.activePage; this.setupLinkDocs(doc, linkDatabaseId); this.setupSharedDocs(doc, sharingDocumentId); // sets up the right sidebar collection for mobile upload documents and sharing this.setupDefaultIconTemplates(doc); // creates a set of icon templates triggered by the document deoration icon diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts index 3f0848d00..b921b3116 100644 --- a/src/client/util/DocumentManager.ts +++ b/src/client/util/DocumentManager.ts @@ -194,7 +194,7 @@ export class DocumentManager { var containerDocContext = srcContext ? [srcContext, doc] : [doc]; while ( containerDocContext.length && - containerDocContext[0]?.embedContainer && + DocCast(containerDocContext[0]?.embedContainer) && DocCast(containerDocContext[0].embedContainer)?._type_collection !== CollectionViewType.Docking && (includeExistingViews || !DocumentManager.Instance.getDocumentView(containerDocContext[0])) ) { diff --git a/src/client/util/DropConverter.ts b/src/client/util/DropConverter.ts index 47997cc5c..f235be192 100644 --- a/src/client/util/DropConverter.ts +++ b/src/client/util/DropConverter.ts @@ -9,7 +9,7 @@ import { RichTextField } from '../../fields/RichTextField'; import { ImageField } from '../../fields/URLField'; import { ScriptingGlobals } from './ScriptingGlobals'; import { listSpec } from '../../fields/Schema'; -import { ButtonType } from '../views/nodes/button/FontIconBox'; +import { ButtonType } from '../views/nodes/FontIconBox/FontIconBox'; export function MakeTemplate(doc: Doc, first: boolean = true, rename: Opt<string> = undefined, templateField: string = '') { if (templateField) Doc.GetProto(doc).title = templateField; /// the title determines which field is being templated diff --git a/src/client/util/GroupManager.scss b/src/client/util/GroupManager.scss index 9438bdd72..253ed5d2a 100644 --- a/src/client/util/GroupManager.scss +++ b/src/client/util/GroupManager.scss @@ -1,6 +1,7 @@ .group-interface { width: 380px; height: 300px; + position: relative; .dialogue-box { .group-create { @@ -56,8 +57,9 @@ flex-direction: column; .overlay { - transform: translate(-20px, -20px); - border-radius: 10px; + transform: translate(-10px, -10px); + width: 400px; + height: 320px; } .delete-button { @@ -66,10 +68,8 @@ .close-button { position: absolute; - right: 1em; - top: 1em; - cursor: pointer; - z-index: 999; + right: 2px; + top: 2px; } .group-heading { diff --git a/src/client/util/LinkFollower.ts b/src/client/util/LinkFollower.ts index f74409e42..3e526c4c0 100644 --- a/src/client/util/LinkFollower.ts +++ b/src/client/util/LinkFollower.ts @@ -103,7 +103,7 @@ export class LinkFollower { Doc.AddDocToList(sourceDocParent, Doc.LayoutFieldKey(sourceDocParent), target); movedTarget = true; } - target.embedContainer = sourceDocParent; + Doc.SetContainer(target, sourceDocParent); const moveTo = [NumCast(sourceDoc.x) + NumCast(sourceDoc.followLinkXoffset), NumCast(sourceDoc.y) + NumCast(sourceDoc.followLinkYoffset)]; if (sourceDoc.followLinkXoffset !== undefined && moveTo[0] !== target.x) { target.x = moveTo[0]; diff --git a/src/client/util/LinkManager.ts b/src/client/util/LinkManager.ts index ce422f849..c7f092565 100644 --- a/src/client/util/LinkManager.ts +++ b/src/client/util/LinkManager.ts @@ -46,38 +46,41 @@ export class LinkManager { LinkManager._instance = this; this.createlink_relationshipLists(); LinkManager.userLinkDBs = []; - const addLinkToDoc = (link: Doc) => { - Promise.all([link]).then(linkdoc => { - const link = DocCast(linkdoc[0]); - Promise.all([link.proto]).then(linkproto => { - Promise.all([link.link_anchor_1, link.link_anchor_2]).then(linkdata => { - const a1 = DocCast(linkdata[0]); - const a2 = DocCast(linkdata[1]); - a1 && - a2 && - Promise.all([Doc.GetProto(a1), Doc.GetProto(a2)]).then( - action(protos => { - (protos[0] as Doc)?.[DirectLinks].add(link); - (protos[1] as Doc)?.[DirectLinks].add(link); + // since this is an action, not a reaction, we get only one shot to add this link to the Anchor docs + // Thus make sure all promised values are resolved from link -> link.proto -> link.link_anchor_[1,2] -> link.link_anchor_[1,2].proto + // Then add the link to the anchor protos. + const addLinkToDoc = (lprom: Doc) => + PromiseValue(lprom).then((link: Opt<Doc>) => + PromiseValue(link?.proto as Doc).then((lproto: Opt<Doc>) => + Promise.all([lproto?.link_anchor_1 as Doc, lproto?.link_anchor_2 as Doc].map(PromiseValue)).then((lAnchs: Opt<Doc>[]) => + Promise.all(lAnchs.map(lAnch => PromiseValue(lAnch?.proto as Doc))).then((lAnchProtos: Opt<Doc>[]) => + Promise.all(lAnchProtos.map(lAnchProto => PromiseValue(lAnchProto?.proto as Doc))).then( + action(lAnchProtoProtos => { + link && lAnchs[0] && Doc.GetProto(lAnchs[0])[DirectLinks].add(link); + link && lAnchs[1] && Doc.GetProto(lAnchs[1])[DirectLinks].add(link); }) - ); - }); - }); - }); - }; - const remLinkFromDoc = (link: Doc) => { - const a1 = link?.link_anchor_1; - const a2 = link?.link_anchor_2; - 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)[DirectLinks].delete(link); - Doc.GetProto(a2)[DirectLinks].delete(link); - Doc.GetProto(link)[DirectLinks].delete(link); - } - }) + ) + ) + ) + ) ); - }; + + const remLinkFromDoc = (lprom: Doc) => + PromiseValue(lprom).then((link: Opt<Doc>) => + PromiseValue(link?.proto as Doc).then((lproto: Opt<Doc>) => + Promise.all([lproto?.link_anchor_1 as Doc, lproto?.link_anchor_2 as Doc].map(PromiseValue)).then((lAnchs: Opt<Doc>[]) => + Promise.all(lAnchs.map(lAnch => PromiseValue(lAnch?.proto as Doc))).then((lAnchProtos: Opt<Doc>[]) => + Promise.all(lAnchProtos.map(lAnchProto => PromiseValue(lAnchProto?.proto as Doc))).then( + action(lAnchProtoProtos => { + link && lAnchs[0] && Doc.GetProto(lAnchs[0])[DirectLinks].delete(link); + link && lAnchs[1] && Doc.GetProto(lAnchs[1])[DirectLinks].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 @@ -156,7 +159,7 @@ export class LinkManager { return this.relatedLinker(anchor); } // finds all links that contain the given anchor public getAllDirectLinks(anchor: Doc): Doc[] { - return Array.from(Doc.GetProto(anchor)[DirectLinks] ?? []); + return Array.from(Doc.GetProto(anchor)[DirectLinks]); } // finds all links that contain the given anchor relatedLinker = computedFn(function relatedLinker(this: any, anchor: Doc): Doc[] { diff --git a/src/client/util/RTFMarkup.tsx b/src/client/util/RTFMarkup.tsx index 247267710..b93d4f293 100644 --- a/src/client/util/RTFMarkup.tsx +++ b/src/client/util/RTFMarkup.tsx @@ -52,7 +52,7 @@ export class RTFMarkup extends React.Component<{}> { {` add a sidebar text document inline`} </p> <p> - <b style={{ fontSize: 'larger' }}>{`\`\` `}</b> + <b style={{ fontSize: 'larger' }}>{`\`\`\` `}</b> {` create a code snippet block`} </p> <p> diff --git a/src/client/util/ReportManager.tsx b/src/client/util/ReportManager.tsx index 4c1020455..89c17e42f 100644 --- a/src/client/util/ReportManager.tsx +++ b/src/client/util/ReportManager.tsx @@ -11,7 +11,7 @@ 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 { FontIconBox } from '../views/nodes/FontIconBox/FontIconBox'; import { DragManager } from './DragManager'; import { GroupManager } from './GroupManager'; import './SettingsManager.scss'; @@ -290,7 +290,7 @@ export class ReportManager extends React.Component<{}> { isDisplayed={this.isOpen} interactive={true} closeOnExternalClick={this.close} - dialogueBoxStyle={{ width: 'auto', height: '500px', background: Cast(Doc.SharingDoc().userColor, 'string', null) }} + dialogueBoxStyle={{ width: 'auto', height: '500px', background: Cast(Doc.UserDoc().userColor, 'string', null) }} /> ); } diff --git a/src/client/util/ScriptManager.ts b/src/client/util/ScriptManager.ts index 42a6493ea..87509f2ea 100644 --- a/src/client/util/ScriptManager.ts +++ b/src/client/util/ScriptManager.ts @@ -1,12 +1,11 @@ -import { Doc, DocListCast } from "../../fields/Doc"; -import { List } from "../../fields/List"; -import { listSpec } from "../../fields/Schema"; -import { Cast, StrCast } from "../../fields/Types"; -import { Docs } from "../documents/Documents"; -import { ScriptingGlobals } from "./ScriptingGlobals"; +import { Doc, DocListCast } from '../../fields/Doc'; +import { List } from '../../fields/List'; +import { listSpec } from '../../fields/Schema'; +import { Cast, StrCast } from '../../fields/Types'; +import { Docs } from '../documents/Documents'; +import { ScriptingGlobals } from './ScriptingGlobals'; export class ScriptManager { - static _initialized = false; private static _instance: ScriptManager; public static get Instance(): ScriptManager { @@ -24,11 +23,7 @@ export class ScriptManager { } public getAllScripts(): Doc[] { const sdoc = ScriptManager.Instance.ScriptManagerDoc; - if (sdoc) { - const docs = DocListCast(sdoc.data); - return docs; - } - return []; + return sdoc ? DocListCast(sdoc.data) : []; } public addScript(scriptDoc: Doc): boolean { @@ -59,36 +54,35 @@ export class ScriptManager { } public static addScriptToGlobals(scriptDoc: Doc): void { - ScriptingGlobals.removeGlobal(StrCast(scriptDoc.name)); - const params = Cast(scriptDoc["data-params"], listSpec("string"), []); + const params = Cast(scriptDoc['data-params'], listSpec('string'), []); const paramNames = params.reduce((o: string, p: string) => { if (params.indexOf(p) === params.length - 1) { - o = o + p.split(":")[0].trim(); + o = o + p.split(':')[0].trim(); } else { - o = o + p.split(":")[0].trim() + ","; + o = o + p.split(':')[0].trim() + ','; } return o; - }, "" as string); + }, '' as string); const f = new Function(paramNames, StrCast(scriptDoc.script)); Object.defineProperty(f, 'name', { value: StrCast(scriptDoc.name), writable: false }); - let parameters = "("; + let parameters = '('; params.forEach((element: string, i: number) => { if (i === params.length - 1) { - parameters = parameters + element + ")"; + parameters = parameters + element + ')'; } else { - parameters = parameters + element + ", "; + parameters = parameters + element + ', '; } }); - if (parameters === "(") { + if (parameters === '(') { ScriptingGlobals.add(f, StrCast(scriptDoc.description)); } else { ScriptingGlobals.add(f, StrCast(scriptDoc.description), parameters); } } -}
\ No newline at end of file +} diff --git a/src/client/util/ServerStats.tsx b/src/client/util/ServerStats.tsx index f84ad8598..b6aa4d95a 100644 --- a/src/client/util/ServerStats.tsx +++ b/src/client/util/ServerStats.tsx @@ -3,6 +3,7 @@ import { observer } from 'mobx-react'; import * as React from 'react'; import { MainViewModal } from '../views/MainViewModal'; import './SharingManager.scss'; +import { PingManager } from './PingManager'; @observer export class ServerStats extends React.Component<{}> { @@ -39,11 +40,22 @@ export class ServerStats extends React.Component<{}> { */ @computed get sharingInterface() { return ( - <div> - <span>Active users:{this._stats?.socketMap.length}</span> - {this._stats?.socketMap.map((user: any) => ( - <p>{user.username}</p> - ))} + <div style={{ + display: "flex", + height: 300, + width: 400, + background: "black", + opacity: 0.6}}> + <div style={{width: 300,height: 100,margin: "auto",display: "flex",flexDirection: "column"}}> + {PingManager.Instance.IsBeating ? 'The server connection is active' : + 'The server connection has been interrupted.NOTE: Any changes made will appear to persist but will be lost after a browser refreshes.'} + + <br/> + <span>Active users:{this._stats?.socketMap.length}</span> + {this._stats?.socketMap.map((user: any) => ( + <p>{user.username}</p> + ))} + </div> </div> ); } diff --git a/src/client/util/SettingsManager.scss b/src/client/util/SettingsManager.scss index 1289ca2b4..bca649bc3 100644 --- a/src/client/util/SettingsManager.scss +++ b/src/client/util/SettingsManager.scss @@ -2,41 +2,20 @@ .settings-interface { //background-color: whitesmoke !important; - color: grey; width: 450px; - - button { - background: #315a96; - outline: none; - border-radius: 5px; - border: 0px; - color: #fcfbf7; - text-transform: uppercase; - letter-spacing: 2px; - font-size: 75%; - padding: 10px; - margin: 10px; - transition: transform 0.2s; - margin: 2px; - } } .settings-title { font-size: 25px; font-weight: bold; padding-right: 10px; - color: black; } .settings-username { font-size: 12px; padding-right: 15px; - color: black; margin-top: 10px; margin-bottom: 10px; - /* right: 135; */ - // position: absolute; - // left: 243; } .settings-section { @@ -49,7 +28,6 @@ font-size: 16; font-weight: bold; text-align: left; - color: black; width: 80; margin-right: 50px; } @@ -61,40 +39,10 @@ .password-content { display: flex; + width: 100%; flex-direction: column; - - .password-content-inputs { - width: 100; - // margin-bottom: 10px; - font-size: 10px; - - .password-inputs { - border: 1px solid rgb(160, 160, 160); - margin-bottom: 8px; - width: 130; - color: black; - border-radius: 5px; - padding: 7px; - } - } - - .password-content-buttons { - //margin-left: 84px; - //width: 100; - padding: 7px; - - .password-submit { - //margin-left: 85px; - margin-top: 5px; - } - - .password-forgot { - //margin-left: 65px; - //margin-top: -20px; - font-size: 12px; - white-space: nowrap; - } - } + align-items: flex-start; + gap: 5px; } .accounts-content { @@ -103,7 +51,6 @@ .modes-content { display: flex; - margin-left: 10px; font-size: 12px; .modes-select { @@ -111,7 +58,6 @@ width: 80%; height: 35px; margin-right: 10px; - color: black; border-radius: 5px; &:hover { @@ -135,12 +81,6 @@ } } - .playground-text { - color: black; - margin-right: 10px; - margin-top: 2; - } - .acl-text { color: black; margin-top: 2; @@ -172,12 +112,11 @@ .appearances-content { display: flex; - margin-top: 4px; - color: black; font-size: 10px; .preferences-color { display: flex; + align-items: center; margin-top: 2px; .preferences-color-text { @@ -197,7 +136,6 @@ margin-top: 2px; .preferences-font-text { - color: black; margin-top: 4; margin-right: 4; margin-bottom: 2px; @@ -212,7 +150,6 @@ .font-select { height: 35px; - color: black; font-size: 9; margin-right: 6; border-radius: 5px; @@ -225,7 +162,6 @@ .size-select { height: 35px; - color: black; font-size: 9; border-radius: 5px; width: 30%; @@ -237,7 +173,6 @@ } .preferences-check { - color: black; margin-right: 4; margin-bottom: -3; margin-left: 5; @@ -252,40 +187,17 @@ display: flex; flex-direction: column; - button { - width: auto; - align-self: center; - background: #252b33; - margin-right: 15px; - - //margin-top: 4px; - - &:hover { - background: $medium-gray; - } - } - - // .delete-button { - // background: rgb(227, 86, 86); - // } .close-button { position: absolute; - right: 1em; - top: 1em; - cursor: pointer; + right: 2px; + top: 2px; } - // .logout-button { - // right: 355; - // position: absolute; - // } .settings-content { - background: #e4e4e4; - //border-radius: 6px; padding: 10px; - //width: 560px; + width: 500px; flex: 1 1 auto; } @@ -296,11 +208,8 @@ .error-text { color: #c40233; - width: 300; - margin-left: -20; font-size: 10; margin-bottom: 4; - margin-top: -3; } .success-text { @@ -317,7 +226,6 @@ } h1 { - color: #121721; text-transform: uppercase; letter-spacing: 2px; font-size: 19; @@ -346,7 +254,6 @@ .padding { padding: 0 0 0 20px; - color: black; } } } @@ -357,21 +264,19 @@ min-height: 250px; height: 100%; width: 100%; - - .settings-content { - background-color: $off-white; - } } .settings-panel { position: relative; min-width: 150px; - background-color: $light-blue; .settings-user { position: absolute; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; bottom: 10px; - text-align: center; left: 0; right: 0; @@ -388,16 +293,11 @@ .settings-tabs { // font-size: 16px; font-weight: 600; - color: black; .tab-control { padding: 10px; border-bottom: 1px solid #9f9e9e; cursor: pointer; - - &.active { - background-color: #fdfdfd; - } } } @@ -416,20 +316,31 @@ .tab-content { display: flex; + flex-flow: row wrap; + gap: 10px; + overflow: hidden; .tab-column { - flex: 0 0 50%; + flex: 0 0 calc(50% - 10px); + flex-direction: column; + display: flex; + justify-content: flex-start; + align-items: flex-start; .tab-column-title { - color: black; font-size: 16px; font-weight: bold; - margin-bottom: 16px; + margin-bottom: 10px; } .tab-column-title, .tab-column-content { - padding-left: 16px; + display: flex; + justify-content: center; + align-items: flex-start; + flex-direction: column; + gap: 10px; + width: 100%; } } } diff --git a/src/client/util/SettingsManager.tsx b/src/client/util/SettingsManager.tsx index f886ce2ca..b6df5f26a 100644 --- a/src/client/util/SettingsManager.tsx +++ b/src/client/util/SettingsManager.tsx @@ -11,19 +11,23 @@ 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 { FontIconBox } from '../views/nodes/FontIconBox/FontIconBox'; import { DragManager } from './DragManager'; import { GroupManager } from './GroupManager'; import './SettingsManager.scss'; import { undoBatch } from './UndoManager'; +import { Button, ColorPicker, Dropdown, DropdownType, EditableText, Group, NumberDropdown, Size, Toggle, ToggleType, Type } from 'browndash-components'; +import { BsGoogle } from 'react-icons/bs' +import { FaFillDrip, FaPalette } from 'react-icons/fa' 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 = 'Match System', + Custom = 'Custom' } export enum freeformScrollMode { @@ -50,8 +54,8 @@ export class SettingsManager extends React.Component<{}> { @computed get backgroundColor() { return Doc.UserDoc().activeCollectionBackground; } - @computed get colorScheme() { - return Doc.ActiveDashboard?.colorScheme; + @computed get userTheme() { + return Doc.UserDoc().userTheme; } constructor(props: {}) { @@ -73,14 +77,31 @@ export class SettingsManager extends React.Component<{}> { } }; - @undoBatch selectUserMode = action((e: React.ChangeEvent) => (Doc.noviceMode = (e.currentTarget as any)?.value === 'Novice')); + @computed get userColor() { + return StrCast(Doc.UserDoc().userColor) + } + + @computed get userVariantColor() { + return StrCast(Doc.UserDoc().userVariantColor) + } + + @computed get userBackgroundColor() { + return StrCast(Doc.UserDoc().userBackgroundColor) + } + + @undoBatch selectUserMode = action((mode: string) => (Doc.noviceMode = mode === 'Novice')); @undoBatch changelayout_showTitle = action((e: React.ChangeEvent) => (Doc.UserDoc().layout_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 changeFontFamily = action((font: string) => (Doc.UserDoc().fontFamily = font)); + @undoBatch changeFontSize = action((val: number) => (Doc.UserDoc().fontSize = val)); + @undoBatch switchUserBackgroundColor = action((color: string) => { + Doc.UserDoc().userBackgroundColor = color; + addStyleSheetRule(SettingsManager._settingsStyle, 'lm_header', { background: `${color} !important` }); + }); + @undoBatch switchUserColor = action((color: string) => { + Doc.UserDoc().userColor = color; + }); + @undoBatch switchUserVariantColor = action((color: string) => { + Doc.UserDoc().userVariantColor = color; }); @undoBatch playgroundModeToggle = action(() => { this.playgroundMode = !this.playgroundMode; @@ -92,82 +113,59 @@ export class SettingsManager extends React.Component<{}> { @undoBatch @action - changeColorScheme = action((e: React.ChangeEvent) => { - const activeDashboard = Doc.ActiveDashboard; - if (!activeDashboard) return; - const scheme: ColorScheme = (e.currentTarget as any).value; + changeColorScheme = action((scheme: string) => { + Doc.UserDoc().userTheme = scheme; 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' }); + this.switchUserColor("#323232") + this.switchUserBackgroundColor("#DFDFDF") + this.switchUserVariantColor("#BDDDF5") break; case ColorScheme.Dark: - activeDashboard.colorScheme = ColorScheme.Dark; - addStyleSheetRule(SettingsManager._settingsStyle, 'lm_header', { background: 'black !important' }); + this.switchUserColor("#DFDFDF") + this.switchUserBackgroundColor("#323232") + this.switchUserVariantColor("#4476F7") + break; + case ColorScheme.Custom: break; 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) + e.matches ? ColorScheme.Dark : ColorScheme.Light; // undefined means ColorScheme.Light until all CSS is updated with values for each color scheme (e.g., see MainView.scss, DocumentDecorations.scss) }); break; } }); @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 colorSchemes = [ColorScheme.Light, ColorScheme.Dark, ColorScheme.System]; - const schemeMap = ['Light', 'Dark', 'Match system']; + const colorSchemes = [ColorScheme.Light, ColorScheme.Dark, ColorScheme.Custom, ColorScheme.System]; + const schemeMap = ['Light', 'Dark', 'Custom', 'Match System']; + const userTheme = StrCast(Doc.UserDoc().userTheme); 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 style={{width: '100%'}}> + <Dropdown + formLabel='Theme' + size={Size.SMALL} + type={Type.TERT} + selectedVal={userTheme} + setSelectedVal={(scheme) => this.changeColorScheme(scheme as string)} + items={colorSchemes.map((scheme, i) => ( + { + text: schemeMap[i], + val: scheme + } + ))} + dropdownType={DropdownType.SELECT} + color={this.userColor} + fillWidth + /> + {userTheme === ColorScheme.Custom && <Group formLabel='Custom Theme'> + <ColorPicker tooltip={'User Color'} color={this.userColor} type={Type.SEC} icon={<FaFillDrip/>} selectedColor={this.userColor} setSelectedColor={this.switchUserColor}/> + <ColorPicker tooltip={'User Background Color'} color={this.userColor} type={Type.SEC} icon={<FaPalette/>} selectedColor={this.userBackgroundColor} setSelectedColor={this.switchUserBackgroundColor}/> + <ColorPicker tooltip={'User Variant Color'} color={this.userColor} type={Type.SEC} icon={<FaPalette/>} selectedColor={this.userVariantColor} setSelectedColor={this.switchUserVariantColor}/> + </Group>} </div> ); } @@ -175,30 +173,65 @@ export class SettingsManager extends React.Component<{}> { @computed get formatsContent() { return ( <div className="prefs-content"> - <div> - <input type="checkbox" onChange={e => (Doc.UserDoc().layout_showTitle = Doc.UserDoc().layout_showTitle ? undefined : 'author_date')} checked={Doc.UserDoc().layout_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 => FontIconBox.SetShowLabels(!FontIconBox.GetShowLabels())} checked={FontIconBox.GetShowLabels()} /> - <div className="preferences-check">Show button labels</div> - </div> - <div> - <input type="checkbox" onChange={e => FontIconBox.SetRecognizeGestures(!FontIconBox.GetRecognizeGestures())} checked={FontIconBox.GetRecognizeGestures()} /> - <div className="preferences-check">Recognize ink Gestures</div> - </div> - <div> - <input type="checkbox" onChange={e => (Doc.UserDoc().activeInkHideTextLabels = !Doc.UserDoc().activeInkHideTextLabels)} checked={BoolCast(Doc.UserDoc().activeInkHideTextLabels)} /> - <div className="preferences-check">Hide Labels In Ink Shapes</div> - </div> - <div> - <input type="checkbox" onChange={e => (Doc.UserDoc().openInkInLightbox = !Doc.UserDoc().openInkInLightbox)} checked={BoolCast(Doc.UserDoc().openInkInLightbox)} /> - <div className="preferences-check">Open Ink Docs in Lightbox</div> - </div> + <Toggle + formLabel={'Show document header'} + formLabelPlacement={'right'} + toggleType={ToggleType.SWITCH} + onClick={e => (Doc.UserDoc().layout_showTitle = Doc.UserDoc().layout_showTitle ? undefined : 'author_date')} + toggleStatus={Doc.UserDoc().layout_showTitle !== undefined} size={Size.XSMALL} + color={this.userColor} + + /> + <Toggle + formLabel={'Show Full Toolbar'} + formLabelPlacement={'right'} + toggleType={ToggleType.SWITCH} + onClick={e => (Doc.UserDoc()['documentLinksButton-fullMenu'] = !Doc.UserDoc()['documentLinksButton-fullMenu'])} + toggleStatus={BoolCast(Doc.UserDoc()['documentLinksButton-fullMenu'])} + size={Size.XSMALL} + color={this.userColor} + + /> + <Toggle + formLabel={'Show Button Labels'} + formLabelPlacement={'right'} + toggleType={ToggleType.SWITCH} + onClick={e => FontIconBox.SetShowLabels(!FontIconBox.GetShowLabels())} + toggleStatus={FontIconBox.GetShowLabels()} + size={Size.XSMALL} + color={this.userColor} + + /> + <Toggle + formLabel={'Recognize Ink Gestures'} + formLabelPlacement={'right'} + toggleType={ToggleType.SWITCH} + onClick={e => FontIconBox.SetRecognizeGestures(!FontIconBox.GetRecognizeGestures())} + toggleStatus={FontIconBox.GetRecognizeGestures()} + size={Size.XSMALL} + color={this.userColor} + + /> + <Toggle + formLabel={'Hide Labels In Ink Shapes'} + formLabelPlacement={'right'} + toggleType={ToggleType.SWITCH} + onClick={e => (Doc.UserDoc().activeInkHideTextLabels = !Doc.UserDoc().activeInkHideTextLabels)} + toggleStatus={BoolCast(Doc.UserDoc().activeInkHideTextLabels)} + size={Size.XSMALL} + color={this.userColor} + + /> + <Toggle + formLabel={'Open Ink Docs in Lightbox'} + formLabelPlacement={'right'} + toggleType={ToggleType.SWITCH} + onClick={e => (Doc.UserDoc().openInkInLightbox = !Doc.UserDoc().openInkInLightbox)} + toggleStatus={BoolCast(Doc.UserDoc().openInkInLightbox)} + size={Size.XSMALL} + color={this.userColor} + + /> </div> ); } @@ -224,25 +257,38 @@ export class SettingsManager extends React.Component<{}> { 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> - <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 className="tab-column"> + <div className="tab-column-title">Text</div> + <div className="tab-column-content"> + {/* <NumberInput/> */} + <Group formLabel={'Default Font'}> + <NumberDropdown + color={this.userColor} + numberDropdownType={'input'} + min={0} max={50} step={2} + type={Type.TERT} + number={0} + unit={"px"} + setNumber={() => {}} + /> + <Dropdown + items={fontFamilies.map((val) => { + return { + text: val, + val: val, + style: { + fontFamily: val + } + } + })} + dropdownType={DropdownType.SELECT} + type={Type.TERT} + selectedVal={StrCast(Doc.UserDoc().fontFamily)} + setSelectedVal={(val) => {this.changeFontFamily(val as string)}} + color={this.userColor} + fillWidth + /> + </Group> </div> </div> </div> @@ -250,8 +296,7 @@ export class SettingsManager extends React.Component<{}> { } @action - changeVal = (e: React.ChangeEvent, pass: string) => { - const value = (e.target as any).value; + changeVal = (value: string, pass: string) => { switch (pass) { case 'curr': this.curr_password = value; @@ -268,20 +313,33 @@ export class SettingsManager extends React.Component<{}> { @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> - </div> + <EditableText placeholder="Current password" + type={Type.SEC} + color={this.userColor} + val={""} + setVal={val => this.changeVal(val as string, 'curr')} + fillWidth + password + /> + <EditableText placeholder="New password" + type={Type.SEC} + color={this.userColor} + val={""} + setVal={val => this.changeVal(val as string, 'new')} + fillWidth + password + /> + <EditableText placeholder="Confirm new password" + type={Type.SEC} + color={this.userColor} + val={""} + setVal={val => this.changeVal(val as string, 'conf')} + fillWidth + password + /> + {!this.passwordResultText ? null : <div className={`${this.passwordResultText.startsWith('Error') ? 'error' : 'success'}-text`}>{this.passwordResultText}</div>} + <Button type={Type.SEC} text={'Forgot Password'} color={this.userColor}/> + <Button type={Type.TERT} text={'Submit'} onClick={this.changePassword} color={this.userColor}/> </div> ); } @@ -289,9 +347,7 @@ export class SettingsManager extends React.Component<{}> { @computed get accountOthersContent() { return ( <div className="account-others-content"> - <button onClick={this.googleAuthorize} value="data"> - Authorize Google Acc - </button> + <Button type={Type.TERT} text={'Connect to Google'} iconPlacement='left' icon={<BsGoogle/>} onClick={() => this.googleAuthorize()}/> </div> ); } @@ -311,7 +367,7 @@ export class SettingsManager extends React.Component<{}> { ); } - setFreeformScrollMode = (mode: freeformScrollMode) => { + setFreeformScrollMode = (mode: string) => { Doc.UserDoc().freeformScrollMode = mode; }; @@ -321,45 +377,78 @@ export class SettingsManager extends React.Component<{}> { <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> + <Dropdown + formLabel={"Mode"} + items={[ + { + text: 'Novice', + description: 'Novice mode is a user-friendly setting designed to cater to those who are new to Dash', + val: "Novice" + }, + { + text: 'Developer', + description: 'Developer mode is an advanced setting that grants you greater control and access to the underlying mechanics and tools of a software or system. Developer mode is still under development as there are experimental features.', + val: "Developer" + }, + ]} + selectedVal={Doc.noviceMode ? 'Novice' : 'Developer'} + setSelectedVal={(val) => {this.selectUserMode(val as string)}} + dropdownType={DropdownType.SELECT} + type={Type.TERT} + placement='bottom-start' + color={this.userColor} + fillWidth + /> + <Toggle + formLabel={'Playground Mode'} + toggleType={ToggleType.SWITCH} + toggleStatus={this.playgroundMode} + onClick={this.playgroundModeToggle} + color={this.userColor} + /> </div> - <div className="tab-column-title" style={{ marginTop: 10, marginBottom: 0 }}> - Freeform scrolling + <div className="tab-column-title" style={{ marginTop: 20, marginBottom: 10 }}> + Freeform Navigation </div> <div className="tab-column-content"> - <button style={{ backgroundColor: Doc.UserDoc().freeformScrollMode === freeformScrollMode.Pan ? 'blue' : '' }} onClick={() => this.setFreeformScrollMode(freeformScrollMode.Pan)}> - Scroll to pan - </button> - <div> - <div>Scrolling pans canvas, shift + scrolling zooms</div> - </div> - <button style={{ backgroundColor: Doc.UserDoc().freeformScrollMode === freeformScrollMode.Zoom ? 'blue' : '' }} onClick={() => this.setFreeformScrollMode(freeformScrollMode.Zoom)}> - Scroll to zoom - </button> - <div>Scrolling zooms canvas</div> + <Dropdown + formLabel={"Scroll Mode"} + items={[ + { + text: 'Scroll to Pan', + description: 'Scrolling pans canvas, shift + scrolling zooms', + val: freeformScrollMode.Pan + }, + { + text: 'Scroll to Zoom', + description: 'Scrolling zooms canvas', + val: freeformScrollMode.Zoom + }, + ]} + selectedVal={StrCast(Doc.UserDoc().freeformScrollMode)} + setSelectedVal={(val) => this.setFreeformScrollMode(val as string)} + dropdownType={DropdownType.SELECT} + type={Type.TERT} + placement='bottom-start' + color={this.userColor} + /> </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> + <Button + text={"Manage Groups"} + type={Type.TERT} + onClick={() => GroupManager.Instance?.open()} + color={this.userColor} + /> + <Toggle + toggleType={ToggleType.SWITCH} + formLabel={"Default access private"} + color={this.userColor} + toggleStatus={BoolCast(Doc.defaultAclPrivate)} + onClick={action(() => (Doc.defaultAclPrivate = !Doc.defaultAclPrivate))}/> </div> </div> </div> @@ -376,31 +465,50 @@ export class SettingsManager extends React.Component<{}> { { title: 'Appearance', ele: this.appearanceContent }, { title: 'Text', ele: this.textContent }, ]; - return ( <div className="settings-interface"> - <div className="settings-panel"> + <div className="settings-panel" style={{ background: this.userColor }}> <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> - ))} + {tabs.map(tab => { + const isActive = this.activeTab === tab.title + return ( + <div key={tab.title} + style={{ + background: isActive ? this.userBackgroundColor : this.userColor, + color: isActive ? this.userColor : this.userBackgroundColor, + }} + className={'tab-control ' + (isActive ? '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'))}> - {Doc.GuestDashboard ? 'Exit' : 'Log Out'} - </button> + <div className="settings-username" + style={{color: this.userBackgroundColor}} + >{Doc.CurrentUserEmail}</div> + <Button + text={Doc.GuestDashboard ? 'Exit' : 'Log Out'} + type={Type.TERT} + color={this.userVariantColor} + onClick={() => window.location.assign(Utils.prepend('/logout'))} + /> </div> </div> - <div className="close-button" onClick={this.close}> - <FontAwesomeIcon icon={'times'} color="black" size={'lg'} /> + + <div className="close-button"> + <Button + icon={<FontAwesomeIcon icon={'times'} size={'lg'} />} + onClick={this.close} + color={this.userColor} + /> </div> - <div className="settings-content"> + <div className="settings-content" style={{color: this.userColor, background: this.userBackgroundColor}}> {tabs.map(tab => ( <div key={tab.title} className={'tab-section ' + (this.activeTab === tab.title ? 'active' : 'inactive')}> {tab.ele} @@ -418,7 +526,7 @@ export class SettingsManager extends React.Component<{}> { isDisplayed={this.isOpen} interactive={true} closeOnExternalClick={this.close} - dialogueBoxStyle={{ width: '500px', height: '300px', background: Cast(Doc.SharingDoc().userColor, 'string', null) }} + dialogueBoxStyle={{ width: 'fit-content', height: '300px', background: Cast(Doc.UserDoc().userColor, 'string', null) }} /> ); } diff --git a/src/client/util/SharingManager.scss b/src/client/util/SharingManager.scss index 932e94664..b11e694ff 100644 --- a/src/client/util/SharingManager.scss +++ b/src/client/util/SharingManager.scss @@ -6,24 +6,46 @@ transform: translate(-20px, -20px); } - select { + .select { text-align: justify; text-align-last: end } .sharing-contents { + padding: 10px; display: flex; flex-direction: column; .close-button { position: absolute; - right: 1em; - top: 1em; - cursor: pointer; - z-index: 999; + right: 2px; + top: 2px; + } + + .share-title { + display: inline-flex; + gap: 5px; + + .share-info { + align-self: center; + cursor: pointer; + } + } + + .share-copy-link { + display: inline; + border-radius: 4px; + border: solid gray 1px; + font-size: x-small; + background: #E8E8E8; + color: black; + margin-top: -15px; + margin-bottom: 15px; + width: fit-content; } .share-container { + .share-setup { display: flex; margin-bottom: 20px; @@ -44,11 +66,15 @@ outline: none; text-align: justify; // for Edge text-align-last: end; + font-size: 13px; + min-width: 90px; + height: 36; + margin-left: 2px; } .share-button { - height: 105%; - margin-left: 2%; + height: 36; + margin-left: 3%; background-color: black; } } @@ -76,15 +102,16 @@ float: right; align-items: baseline; margin-top: -12; + margin-bottom: 10; .layoutDoc-acls, .myDocs-acls { flex-direction: column; - margin-right: 12; label { font-weight: normal; font-style: italic; + padding-right: 12; } input { @@ -102,6 +129,7 @@ .group-container { width: 50%; display: flex; + top:0; flex-direction: column; .user-sort { @@ -120,9 +148,10 @@ .users-list { font-style: italic; background: #e8e8e8; + border: 2px solid gray; padding-left: 10px; padding-right: 10px; - width: 100%; + width: 97%; overflow-y: scroll; overflow-x: hidden; text-align: left; @@ -190,53 +219,149 @@ } } + .title-individual{ + height: 25px; + padding-left: 2; + width: 97%; + margin-top: 10px; + margin-left: -8px; + font-size: 14; + margin-bottom: -4; + border: 2px solid gray; + border-bottom: none; + align-items: center; + display: flex; + } + + .title-group{ + height: 25px; + padding-left: 2; + width: 97%; + margin-top: 10px; + margin-left: -.5px; + font-size: 14; + margin-bottom: -4; + border: 2px solid gray; + border-bottom: none; + align-items: center; + display: flex; + } + .container { display: flex; position: relative; margin-top: 5px; - margin-bottom: 10px; + margin-left: -5px; font-size: 22px; -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; - width: 100%; + width: 97%; text-align: left; font-style: normal; - font-size: 14; + font-size: 12.5; font-weight: normal; - padding: 0; - align-items: center; + + padding: 3px; + border-bottom: 0.5px solid grey; .group-info { cursor: pointer; } &:hover .padding { + overflow-x: unset; white-space: unset; + overflow-wrap: break-word; } .padding { - padding: 0 10px 0 0; - color: black; + max-width: 150px; + overflow-x: hidden; + display: inline-block; text-overflow: ellipsis; - overflow: hidden; white-space: nowrap; - max-width: 40%; } .permissions-dropdown { - border: none; - height: 25; - background-color: #e8e8e8; + display: flex; + align-items: flex-end; + text-align: right; + margin-left: auto; + margin-right: -12px; + } .edit-actions { display: flex; position: absolute; - right: -10; + align-items: flex-end; + right: -10; } + } + .permissions-dropdown-None{ + height: 100%; + min-width: 85px; + text-align: right; + margin-right: -12px; + padding: 0px; + padding-left: 3px; + background: grey; + color: rgb(71, 71, 71); + border-radius: 6px; + border: 1px solid rgb(71, 71, 71); + } + .permissions-dropdown-Edit, + .permissions-dropdown-Admin { + height: 100%; + min-width: 85px; + text-align: right; + margin-right: -12px; + padding: 0px; + padding-left: 3px; + background: rgb(254, 254, 199); + color: rgb(75, 75, 5); + border-radius: 6px; + border: 1px solid rgb(75, 75, 5); + } + .permissions-dropdown-Augment{ + height: 100%; + min-width: 85px; + text-align: right; + margin-right: -12px; + padding: 0px; + padding-left: 3px; + background: rgb(208, 255, 208); + color:rgb(19, 80, 19); + border-radius: 6px; + border: 1px solid rgb(19, 80, 19); + + } + .permissions-dropdown-View{ + height: 100%; + min-width: 85px; + text-align: right; + margin-right: -12px; + padding: 0px; + padding-left: 3px; + background: rgb(213, 213, 255); + color: rgb(25, 25, 101); + border-radius: 6px; + border: 1px solid rgb(25, 25, 101); + } + .permissions-dropdown-Not-Shared{ + height: 100%; + min-width: 85px; + text-align: right; + margin-right: -12px; + padding: 0px; + padding-left: 3px; + background: rgb(255, 207, 207); + color: rgb(138, 47, 47); + border-radius: 6px; + border: 1px solid rgb(138, 47, 47); } .no-users { diff --git a/src/client/util/SharingManager.tsx b/src/client/util/SharingManager.tsx index 97e64ab71..33c9992d0 100644 --- a/src/client/util/SharingManager.tsx +++ b/src/client/util/SharingManager.tsx @@ -1,15 +1,15 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { intersection } from 'lodash'; +import { Colors } from 'browndash-components'; +import { concat, 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 { Doc, DocListCast, DocListCastAsync, HierarchyMapping } from '../../fields/Doc'; -import { AclAdmin, AclPrivate, DocAcl, AclUnset, DocData } from '../../fields/DocSymbols'; +import { Doc, DocListCast, DocListCastAsync, HierarchyMapping, ReverseHierarchyMap } from '../../fields/Doc'; +import { AclAdmin, AclPrivate, DocAcl, DocData } from '../../fields/DocSymbols'; import { Id } from '../../fields/FieldSymbols'; -import { List } from '../../fields/List'; -import { NumCast, StrCast } from '../../fields/Types'; +import { StrCast } from '../../fields/Types'; import { distributeAcls, GetEffectiveAcl, normalizeEmail, SharingPermissions, TraceMobx } from '../../fields/util'; import { Utils } from '../../Utils'; import { DocServer } from '../DocServer'; @@ -21,8 +21,10 @@ import { SearchBox } from '../views/search/SearchBox'; import { DocumentManager } from './DocumentManager'; import { GroupManager, UserOptions } from './GroupManager'; import { GroupMemberView } from './GroupMemberView'; +import { LinkManager } from './LinkManager'; import { SelectionManager } from './SelectionManager'; import './SharingManager.scss'; +import { undoable } from './UndoManager'; export interface User { email: string; @@ -47,6 +49,7 @@ const indType = '!indType/'; const groupType = '!groupType/'; const storage = 'data'; +const dashStorage = 'data_dashboards'; /** * A user who also has a sharing doc. @@ -73,13 +76,14 @@ export class SharingManager extends React.Component<{}> { @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 @observable private showUserOptions: boolean = false; // whether to show individuals as options when sharing (in the react-select component) @observable private showGroupOptions: boolean = false; // // whether to show groups as options when sharing (in the react-select component) private populating: boolean = false; // whether the list of users is populating or not + @observable private overrideNested: boolean = false; // whether child docs in a collection/dashboard should be changed to be less private - initially selected so default is override @observable private layoutDocAcls: boolean = false; // whether the layout doc or data doc's acls are to be used @observable private myDocAcls: boolean = false; // whether the My Docs checkbox is selected or not + @observable private _buttonDown = false; // private get linkVisible() { // return this.targetDoc ? this.targetDoc["acl-" + PublicKey] !== SharingPermissions.None : false; @@ -93,6 +97,7 @@ export class SharingManager extends React.Component<{}> { DictationOverlay.Instance.hasActiveModal = true; this.isOpen = this.targetDoc !== undefined; this.permissions = SharingPermissions.Augment; + this.overrideNested = true; }); }; @@ -108,6 +113,7 @@ export class SharingManager extends React.Component<{}> { }), 500 ); + this.layoutDocAcls = false; }); constructor(props: {}) { @@ -138,7 +144,7 @@ export class SharingManager extends React.Component<{}> { if (sharingDoc instanceof Doc && linkDatabase instanceof Doc) { if (!this.users.find(user => user.user.email === newUser.email)) { this.users.push({ user: newUser, sharingDoc, linkDatabase, userColor: StrCast(sharingDoc.userColor) }); - // LinkManager.addLinkDB(sharer.linkDatabase); + LinkManager.addLinkDB(linkDatabase); } } }) @@ -150,79 +156,44 @@ export class SharingManager extends React.Component<{}> { /** * Shares the document with a user. */ - setInternalSharing = (recipient: ValidatedUser, permission: string, targetDoc?: Doc) => { + setInternalSharing = undoable((recipient: ValidatedUser, permission: string, targetDoc: Doc | undefined) => { const { user, sharingDoc } = recipient; const target = targetDoc || this.targetDoc!; const acl = `acl-${normalizeEmail(user.email)}`; - const myAcl = `acl-${Doc.CurrentUserEmailNormalized}`; - 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 => (this.layoutDocAcls ? doc : doc[DocData])) - .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); - - this.setDashboardBackground(doc, permission as SharingPermissions); - if (permission !== SharingPermissions.None) return Doc.AddDocToList(sharingDoc, storage, doc); - return GetEffectiveAcl(doc, user.email) === AclPrivate && Doc.RemoveDocFromList(sharingDoc, storage, (doc.createdFrom as Doc) || doc); - }) - .some(success => !success); - }; + const docs = SelectionManager.Views().length < 2 ? [target] : SelectionManager.Views().map(docView => docView.rootDoc); + docs.map(doc => (this.layoutDocAcls || doc.dockingConfig ? doc : Doc.GetProto(doc))).forEach(doc => { + distributeAcls(acl, permission as SharingPermissions, doc, undefined, this.overrideNested ? true : undefined); + if (permission !== SharingPermissions.None) { + Doc.AddDocToList(sharingDoc, doc.dockingConfig ? dashStorage : storage, doc); + } else GetEffectiveAcl(doc, user.email) === AclPrivate && Doc.RemoveDocFromList(sharingDoc, ((doc.createdFrom as Doc) || doc).dockingConfig ? dashStorage : storage, (doc.createdFrom as Doc) || doc); + }); + }, 'set Doc permissions'); /** * Sets the permission on the target for the group. * @param group * @param permission */ - setInternalGroupSharing = (group: Doc | { title: string }, permission: string, targetDoc?: Doc) => { + setInternalGroupSharing = undoable((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(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 => (this.layoutDocAcls ? doc : doc[DocData])) - .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; - } + const acl = `acl-${normalizeEmail(StrCast(group.title))}`; - distributeAcls(acl, permission as SharingPermissions, doc, undefined, undefined, isDashboard); - this.setDashboardBackground(doc, permission as SharingPermissions); + const docs = SelectionManager.Views().length < 2 ? [target] : SelectionManager.Views().map(docView => docView.rootDoc); + docs.map(doc => (this.layoutDocAcls || doc.dockingConfig ? doc : Doc.GetProto(doc))).forEach(doc => { + distributeAcls(acl, permission as SharingPermissions, doc, undefined, this.overrideNested ? true : undefined); - 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) { + Doc.AddDocToList(group, 'docsShared', 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.createdFrom as Doc) || doc); // remove the doc from the list if it already exists - }) - .some(success => !success); - } - }) - .some(success => success); - }; + this.users + .filter(({ user: { email } }) => JSON.parse(StrCast(group.members)).includes(email)) + .forEach(({ user, sharingDoc }) => { + if (permission !== SharingPermissions.None) Doc.AddDocToList(sharingDoc, doc.dockingConfig ? dashStorage : storage, doc); // add the doc to the sharingDoc if it hasn't already been added + else GetEffectiveAcl(doc, user.email) === AclPrivate && Doc.RemoveDocFromList(sharingDoc, ((doc.createdFrom as Doc) || doc).dockingConfig ? dashStorage : storage, (doc.createdFrom as Doc) || doc); // remove the doc from the list if it already exists + }); + } + }); + }, 'set group permissions'); /** * Shares the documents shared with a group with a new user who has been added to that group. @@ -237,7 +208,13 @@ export class SharingManager extends React.Component<{}> { else { DocListCastAsync(user.sharingDoc[storage]).then(userdocs => DocListCastAsync(group.docsShared).then(dl => { - const filtered = dl?.filter(doc => !userdocs?.includes(doc)); + const filtered = dl?.filter(doc => !doc.dockingConfig && !userdocs?.includes(doc)); + filtered && userdocs?.push(...filtered); + }) + ); + DocListCastAsync(user.sharingDoc[dashStorage]).then(userdocs => + DocListCastAsync(group.docsShared).then(dl => { + const filtered = dl?.filter(doc => doc.dockingConfig && !userdocs?.includes(doc)); filtered && userdocs?.push(...filtered); }) ); @@ -248,44 +225,23 @@ export class SharingManager extends React.Component<{}> { /** * Called from the properties sidebar to change permissions of a user. */ - shareFromPropertiesSidebar = (shareWith: string, permission: SharingPermissions, docs: Doc[]) => { - if (shareWith !== 'Public' && shareWith !== 'Override') { + shareFromPropertiesSidebar = undoable((shareWith: string, permission: SharingPermissions, docs: Doc[], layout: boolean) => { + if (layout) this.layoutDocAcls = true; + if (shareWith !== 'Guest') { 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 this.setInternalGroupSharing(GroupManager.Instance.getGroup(shareWith)!, permission, doc, undefined, true); }); } 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); - this.setDashboardBackground(doc, permission as SharingPermissions); - }); - } - }; - - /** - * Sets the background of the Dashboard if it has been shared as a visual indicator - */ - setDashboardBackground = (doc: Doc, permission: SharingPermissions) => { - if (Doc.IndexOf(doc, DocListCast(Doc.MyDashboards.data)) !== -1) { - if (permission !== SharingPermissions.None) { - doc.isShared = true; - doc.backgroundColor = 'green'; - } else { - const acls = doc[DocData][DocAcl]; - if ( - Object.keys(acls) - .filter(key => key !== `acl-${Doc.CurrentUserEmailNormalized}` && key !== 'acl-Me') - .every(key => [AclUnset, AclPrivate].includes(acls[key])) - ) { - doc.isShared = undefined; - doc.backgroundColor = undefined; + if (GetEffectiveAcl(doc) === AclAdmin) { + distributeAcls(`acl-${shareWith}`, permission, doc, undefined); } - } + }); } - }; + this.layoutDocAcls = false; + }, 'sidebar set permissions'); /** * Removes the documents shared with a user through a group when the user is removed from the group. @@ -295,13 +251,19 @@ export class SharingManager extends React.Component<{}> { removeMember = (group: Doc, emailId: string) => { const user: ValidatedUser = this.users.find(({ user: { email } }) => email === emailId)!; - if (group.docsShared) { + if (group.docsShared && user) { DocListCastAsync(user.sharingDoc[storage]).then(userdocs => DocListCastAsync(group.docsShared).then(dl => { const remaining = userdocs?.filter(doc => !dl?.includes(doc)) || []; userdocs?.splice(0, userdocs.length, ...remaining); }) ); + DocListCastAsync(user.sharingDoc[dashStorage]).then(userdocs => + DocListCastAsync(group.docsShared).then(dl => { + const remaining = userdocs?.filter(doc => !dl?.includes(doc)) || []; + userdocs?.splice(0, userdocs.length, ...remaining); + }) + ); } }; @@ -311,11 +273,9 @@ export class SharingManager extends React.Component<{}> { */ removeGroup = (group: Doc) => { if (group.docsShared) { - const dashboards = DocListCast(Doc.MyDashboards.data); DocListCast(group.docsShared).forEach(doc => { const acl = `acl-${StrCast(group.title)}`; - const isDashboard = dashboards.indexOf(doc) !== -1; - distributeAcls(acl, SharingPermissions.None, doc, undefined, undefined, isDashboard); + distributeAcls(acl, SharingPermissions.None, doc); const members: string[] = JSON.parse(StrCast(group.members)); const users: ValidatedUser[] = this.users.filter(({ user: { email } }) => members.includes(email)); @@ -331,37 +291,26 @@ export class SharingManager extends React.Component<{}> { // return; // } // targetDoc["acl-" + PublicKey] = permission; - // } + // }s - // private get sharingUrl() { - // if (!this.targetDoc) { - // return undefined; - // } - // const baseUrl = Utils.prepend("/doc/" + this.targetDoc[Id]); - // return `${baseUrl}?sharing=true`; - // } - - // copy = action(() => { - // if (this.sharingUrl) { - // Utils.CopyText(this.sharingUrl); - // this.copied = true; - // } - // }); + /** + * Copies the Public sharing url to the user's clipboard. + */ + private copyURL = (e: any) => { + Utils.CopyText(Utils.shareUrl(this.targetDoc![Id])); + }; /** * Returns the SharingPermissions (Admin, Can Edit etc) access that's used to share */ - private sharingOptions(uniform: boolean, override?: boolean) { - const dropdownValues: string[] = Object.values(SharingPermissions); + private sharingOptions(uniform: boolean, showGuestOptions?: boolean) { + const dropdownValues: string[] = showGuestOptions ? [SharingPermissions.None, SharingPermissions.View] : Object.values(SharingPermissions); if (!uniform) dropdownValues.unshift('-multiple-'); - if (!override) dropdownValues.splice(dropdownValues.indexOf(SharingPermissions.Unset), 1); - return dropdownValues - .filter(permission => !Doc.noviceMode || ![SharingPermissions.SelfEdit].includes(permission as any)) - .map(permission => ( - <option key={permission} value={permission}> - {permission} - </option> - )); + return dropdownValues.map(permission => ( + <option key={permission} value={permission}> + {concat(ReverseHierarchyMap.get(permission)?.image, ' ', permission)} + </option> + )); } private focusOn = (contents: string) => { @@ -406,38 +355,43 @@ export class SharingManager extends React.Component<{}> { /** * Handles changes in the permission chosen to share with someone with */ - @action - handlePermissionsChange = (event: React.ChangeEvent<HTMLSelectElement>) => { - this.permissions = event.currentTarget.value as SharingPermissions; - }; + handlePermissionsChange = undoable( + action((event: React.ChangeEvent<HTMLSelectElement>) => { + this.permissions = event.currentTarget.value as SharingPermissions; + }), + 'permission change' + ); /** * Calls the relevant method for sharing, displays the popup, and resets the relevant variables. */ - @action - share = () => { - if (this.selectedUsers) { - this.selectedUsers.forEach(user => { - if (user.value.includes(indType)) { - this.setInternalSharing(this.users.find(u => u.user.email === user.label)!, this.permissions); - } else { - this.setInternalGroupSharing(GroupManager.Instance.getGroup(user.label)!, this.permissions); - } - }); - - 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.taskCompleted = true; - setTimeout( - action(() => (TaskCompletionBox.taskCompleted = false)), - 2000 - ); + share = undoable( + action(() => { + if (this.selectedUsers) { + this.selectedUsers.forEach(user => { + if (user.value.includes(indType)) { + this.setInternalSharing(this.users.find(u => u.user.email === user.label)!, this.permissions, undefined); + } else { + this.setInternalGroupSharing(GroupManager.Instance.getGroup(user.label)!, this.permissions); + } + }); + + 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.taskCompleted = true; + setTimeout( + action(() => (TaskCompletionBox.taskCompleted = false)), + 2000 + ); - this.selectedUsers = null; - } - }; + this.layoutDocAcls = false; + this.selectedUsers = null; + } + }), + 'share Doc' + ); /** * Sorting algorithm to sort users. @@ -464,6 +418,7 @@ export class SharingManager extends React.Component<{}> { if (!this.targetDoc) return null; TraceMobx(); const groupList = GroupManager.Instance?.allGroups || []; + const sortedUsers = this.users .slice() .sort(this.sortUsers) @@ -485,8 +440,7 @@ export class SharingManager extends React.Component<{}> { 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?.[DocData]] : SelectionManager.Views().map(docView => (this.layoutDocAcls ? docView.props.Document : docView.props.Document?.[DocData])); + let docs = SelectionManager.Views().length < 2 ? [this.targetDoc] : SelectionManager.Views().map(docView => docView.rootDoc); if (this.myDocAcls) { const newDocs: Doc[] = []; @@ -501,26 +455,32 @@ 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 : doc[DocData])).map(doc => doc?.[DocAcl] && Object.keys(doc[DocAcl]))); + const commonKeys = intersection(docs).reduce((list, doc) => (doc?.[DocAcl] ? [...list, ...Object.keys(doc[DocAcl])] : list), [] as string[]); // 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)) + const userListContents = users + // .filter(({ user }) => (docs.length > 1 ? commonKeys.includes(`acl-${normalizeEmail(user.email)}`) : docs[0]?.author !== user.email)) + .filter(({ user }) => docs[0]?.author !== user.email) .map(({ user, linkDatabase, sharingDoc, userColor }) => { const userKey = `acl-${normalizeEmail(user.email)}`; - const uniform = docs.map(doc => (this.layoutDocAcls ? doc : doc[DocData])).every(doc => doc?.[DocAcl]?.[userKey] === docs[0]?.[DocAcl]?.[userKey]); - const permissions = uniform ? StrCast(targetDoc?.[userKey]) : '-multiple-'; + const uniform = docs.every(doc => doc?.[DocAcl]?.[userKey] === docs[0]?.[DocAcl]?.[userKey]); + // const permissions = uniform ? StrCast(targetDoc?.[userKey]) : '-multiple-'; + let permissions = targetDoc[DocAcl][userKey] ? HierarchyMapping.get(targetDoc[DocAcl][userKey])?.name : StrCast(targetDoc[userKey]); + 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)}> + <select className={`permissions-dropdown-${permissions}`} value={permissions} onChange={e => this.setInternalSharing({ user, linkDatabase, sharingDoc, userColor }, e.currentTarget.value, undefined)}> {this.sharingOptions(uniform)} </select> ) : ( - <div className={'permissions-dropdown'}>{permissions}</div> + <div className={`permissions-dropdown-${permissions}`}> + {concat(ReverseHierarchyMap.get(permissions)?.image, ' ', permissions)} + + </div> )} </div> </div> @@ -531,6 +491,9 @@ export class SharingManager extends React.Component<{}> { 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. + const userKey = `acl-${normalizeEmail(Doc.CurrentUserEmail)}`; + const curUserPermission = StrCast(targetDoc[userKey]); + // const curUserPermission = HierarchyMapping.get(effectiveAcls[0])!.name userListContents.unshift( sameAuthor ? ( <div key={'owner'} className={'container'}> @@ -544,7 +507,10 @@ export class SharingManager extends React.Component<{}> { <div key={'me'} className={'container'}> <span className={'padding'}>Me</span> <div className="edit-actions"> - <div className={'permissions-dropdown'}>{effectiveAcls.every(acl => acl === effectiveAcls[0]) ? HierarchyMapping.get(effectiveAcls[0])!.name : '-multiple-'}</div> + <div className={`permissions-dropdown-${curUserPermission}`}> + {effectiveAcls.every(acl => acl === effectiveAcls[0]) ? concat(ReverseHierarchyMap.get(curUserPermission!)?.image, ' ', curUserPermission) : '-multiple-'} + + </div> </div> </div> ) : null @@ -552,29 +518,31 @@ export class SharingManager extends React.Component<{}> { // 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" }); + groupListMap.unshift({ title: 'Guest' }); //, { title: "ALL" }); const groupListContents = groupListMap.map(group => { - const groupKey = `acl-${StrCast(group.title)}`; - const uniform = docs - .map(doc => (this.layoutDocAcls ? doc : doc[DocData])) - .every(doc => (this.layoutDocAcls ? doc?.[DocAcl]?.[groupKey] === docs[0]?.[DocAcl]?.[groupKey] : doc?.[DocData]?.[DocAcl]?.[groupKey] === docs[0]?.[DocData]?.[DocAcl]?.[groupKey])); - const permissions = uniform ? StrCast(targetDoc?.[`acl-${StrCast(group.title)}`]) : '-multiple-'; + let groupKey = `acl-${StrCast(group.title)}`; + const uniform = docs.every(doc => doc?.[DocAcl]?.[groupKey] === docs[0]?.[DocAcl]?.[groupKey]); + const permissions = uniform ? StrCast(targetDoc?.[groupKey]) : '-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"> + <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-${permissions}`} value={permissions} onChange={e => this.setInternalGroupSharing(group, e.currentTarget.value)}> + {this.sharingOptions(uniform, group.title === 'Guest')} </select> ) : ( - <div className={'permissions-dropdown'}>{permissions}</div> + <div className={`permissions-dropdown-${permissions}`}> + {concat(ReverseHierarchyMap.get(permissions)?.image, ' ', permissions)} + + </div> )} </div> </div> @@ -583,20 +551,32 @@ 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-contents"> - <p className={'share-title'}> + <div className="sharing-contents" + style={{ + background: StrCast(Doc.UserDoc().userBackgroundColor), + color: StrCast(Doc.UserDoc().userColor) + }} + > + <p className="share-title"> + <div className="share-info" onClick={() => window.open('https://brown-dash.github.io/Dash-Documentation/properties/sharing-and-permissions/', '_blank')}> + <FontAwesomeIcon icon={'question-circle'} size={'sm'} onClick={() => window.open('https://brown-dash.github.io/Dash-Documentation/properties/sharing-and-permissions/', '_blank')} /> + </div> <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'} /> + <button + className="share-copy-link" + style={{ background: this._buttonDown ? Colors.LIGHT_BLUE : undefined }} + onPointerDown={action(e => (this._buttonDown = true))} + onPointerUp={action(e => (this._buttonDown = false))} + onClick={this.copyURL}> + <FontAwesomeIcon title="Copy Public URL" icon="copy" size="sm" /> + Copy Public URL + </button> + <div className="close-button" onClick={this.close}> + <FontAwesomeIcon icon="times" color="black" size="lg" /> </div> - {/* {this.linkVisible ? - <div> - {this.sharingUrl} - </div> : - (null)} */} - { + {admin ? ( <div className="share-container"> <div className="share-setup"> <Select @@ -615,9 +595,11 @@ export class SharingManager extends React.Component<{}> { }), }} /> - <select className="permissions-select" onChange={this.handlePermissionsChange} value={this.permissions}> - {this.sharingOptions(true)} - </select> + <div className="permissions-select"> + <select className={`permissions-dropdown-${this.permissions}`} onChange={this.handlePermissionsChange} value={this.permissions}> + {this.sharingOptions(true)} + </select> + </div> <button ref={this.shareDocumentButtonRef} className="share-button" onClick={this.share}> Share </button> @@ -630,36 +612,41 @@ export class SharingManager extends React.Component<{}> { <div className="acl-container"> {Doc.noviceMode ? null : ( <div className="layoutDoc-acls"> + <input type="checkbox" onChange={action(() => (this.overrideNested = !this.overrideNested))} checked={this.overrideNested} /> <label>Override Nested </label> <input type="checkbox" onChange={action(() => (this.layoutDocAcls = !this.layoutDocAcls))} checked={this.layoutDocAcls} /> <label>Layout</label> </div> )} </div> </div> - } + ) : ( + <div className="share-container"> + <div className="acl-container"> + <div className="layoutDoc-acls"> + <input type="checkbox" onChange={action(() => (this.layoutDocAcls = !this.layoutDocAcls))} checked={this.layoutDocAcls} /> <label>Layout</label> + </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 className="title-individual"> + Individuals + <FontAwesomeIcon icon={this.individualSort === 'ascending' ? 'caret-up' : this.individualSort === 'descending' ? 'caret-down' : 'caret-right'} size="xs" /> + </div> </div> - <div className={'users-list'}>{userListContents}</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 className="title-group"> + Groups + <div className="group-info" onClick={action(() => GroupManager.Instance?.open())}> + <FontAwesomeIcon icon={'info-circle'} color={'#e8e8e8'} size={'sm'} style={{ backgroundColor: '#1e89d7', borderRadius: '100%', border: '1px solid #1e89d7' }} /> + </div> + + <FontAwesomeIcon icon={this.groupSort === 'ascending' ? 'caret-up' : this.groupSort === 'descending' ? 'caret-down' : 'caret-right'} size="xs" /> + </div> </div> <div className={'groups-list'}>{groupListContents}</div> </div> @@ -670,6 +657,13 @@ export class SharingManager extends React.Component<{}> { } 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} + />; } } diff --git a/src/client/util/UndoManager.ts b/src/client/util/UndoManager.ts index b59af6656..9a6719ea5 100644 --- a/src/client/util/UndoManager.ts +++ b/src/client/util/UndoManager.ts @@ -1,5 +1,6 @@ import { observable, action, runInAction } from 'mobx'; import { Field } from '../../fields/Doc'; +import { RichTextField } from '../../fields/RichTextField'; import { Without } from '../../Utils'; function getBatchName(target: any, key: string | symbol): string { @@ -96,7 +97,13 @@ export namespace UndoManager { export function AddEvent(event: UndoEvent, value?: any): void { if (currentBatch && batchCounter.get() && !undoing) { - console.log(' '.slice(0, batchCounter.get()) + 'UndoEvent : ' + event.prop + ' = ' + (value instanceof Array ? value.map(val => Field.toScriptString(val)).join(',') : Field.toScriptString(value))); + console.log( + ' '.slice(0, batchCounter.get()) + + 'UndoEvent : ' + + event.prop + + ' = ' + + (value instanceof RichTextField ? value.Text : value instanceof Array ? value.map(val => Field.toScriptString(val)).join(',') : Field.toScriptString(value)) + ); currentBatch.push(event); tempEvents?.push(event); } |