diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/client/util/CurrentUserUtils.ts | 89 | ||||
-rw-r--r-- | src/client/util/History.ts | 4 | ||||
-rw-r--r-- | src/client/views/MainView.tsx | 89 | ||||
-rw-r--r-- | src/client/views/collections/CollectionDockingView.tsx | 8 | ||||
-rw-r--r-- | src/client/views/collections/CollectionTreeView.tsx | 4 | ||||
-rw-r--r-- | src/client/views/search/SearchBox.scss | 50 | ||||
-rw-r--r-- | src/client/views/search/SearchBox.tsx | 28 |
7 files changed, 171 insertions, 101 deletions
diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts index 435944a74..a76aa0dee 100644 --- a/src/client/util/CurrentUserUtils.ts +++ b/src/client/util/CurrentUserUtils.ts @@ -1,4 +1,4 @@ -import { computed, observable, reaction } from "mobx"; +import { computed, observable, reaction, action } from "mobx"; import * as rp from 'request-promise'; import { Utils } from "../../Utils"; import { DocServer } from "../DocServer"; @@ -24,6 +24,8 @@ import { DimUnit } from "../views/collections/collectionMulticolumn/CollectionMu import { LabelBox } from "../views/nodes/LabelBox"; import { LinkManager } from "./LinkManager"; import { Id } from "../../fields/FieldSymbols"; +import { HistoryUtil } from "./History"; +import { CollectionDockingView } from "../views/collections/CollectionDockingView"; export class CurrentUserUtils { private static curr_id: string; @@ -730,7 +732,7 @@ export class CurrentUserUtils { treeViewTruncateTitleWidth: 150, hideFilterView: true, treeViewPreventOpen: false, lockedPosition: true, boxShadow: "0 0", dontRegisterChildViews: true, targetDropAction: "same", system: true })); - const newDashboard = ScriptField.MakeScript(`createNewDashboard()`); + const newDashboard = ScriptField.MakeScript(`createNewDashboard(Doc.UserDoc())`); (doc.myDashboards as any as Doc).contextMenuScripts = new List<ScriptField>([newDashboard!]); (doc.myDashboards as any as Doc).contextMenuLabels = new List<string>(["Create New Dashboard"]); } @@ -976,9 +978,90 @@ export class CurrentUserUtils { } }); } + + public static _urlState: HistoryUtil.DocUrl; + + public static openDashboard = (userDoc: Doc, doc: Doc, fromHistory = false) => { + CurrentUserUtils.MainDocId = doc[Id]; + + if (doc) { // this has the side-effect of setting the main container since we're assigning the active/guest dashboard + !("presentationView" in doc) && (doc.presentationView = new List<Doc>([Docs.Create.TreeDocument([], { title: "Presentation" })])); + userDoc ? (userDoc.activeDashboard = doc) : (CurrentUserUtils.GuestDashboard = doc); + } + const state = CurrentUserUtils._urlState; + if (state.sharing === true && !userDoc) { + DocServer.Control.makeReadOnly(); + } else { + fromHistory || HistoryUtil.pushState({ + type: "doc", + docId: doc[Id], + readonly: state.readonly, + nro: state.nro, + sharing: false, + }); + if (state.readonly === true || state.readonly === null) { + DocServer.Control.makeReadOnly(); + } else if (state.safe) { + if (!state.nro) { + DocServer.Control.makeReadOnly(); + } + CollectionView.SetSafeMode(true); + } else if (state.nro || state.nro === null || state.readonly === false) { + } else if (doc.readOnly) { + DocServer.Control.makeReadOnly(); + } else { + DocServer.Control.makeEditable(); + } + } + + return true; + } + + public static snapshotDashboard = (userDoc: Doc) => { + const activeDashboard = Cast(userDoc.activeDashboard, Doc, null); + CollectionDockingView.Copy(activeDashboard).then(copy => { + Doc.AddDocToList(Cast(userDoc.myDashboards, Doc, null), "data", copy); + // bcz: strangely, we need a timeout to prevent exceptions/issues initializing GoldenLayout (the rendering engine for Main Container) + setTimeout(() => CurrentUserUtils.openDashboard(userDoc, copy), 0); + }); + } + + public static createNewDashboard = async (userDoc: Doc, id?: string) => { + const myPresentations = userDoc.myPresentations as Doc; + const presentation = Doc.MakeCopy(userDoc.emptyPresentation as Doc, true); + const dashboards = Cast(userDoc.myDashboards, Doc) as Doc; + const dashboardCount = DocListCast(dashboards.data).length + 1; + const emptyPane = Cast(userDoc.emptyPane, Doc, null); + emptyPane["dragFactory-count"] = NumCast(emptyPane["dragFactory-count"]) + 1; + const freeformOptions: DocumentOptions = { + x: 0, + y: 400, + _width: 1500, + _height: 1000, + title: `Untitled Tab ${NumCast(emptyPane["dragFactory-count"])}`, + }; + const freeformDoc = CurrentUserUtils.GuestTarget || Docs.Create.FreeformDocument([], freeformOptions); + const dashboardDoc = Docs.Create.StandardCollectionDockingDocument([{ doc: freeformDoc, initialWidth: 600 }], { title: `Dashboard ${dashboardCount}` }, id, "row"); + Doc.AddDocToList(myPresentations, "data", presentation); + userDoc.activePresentation = presentation; + const toggleTheme = ScriptField.MakeScript(`self.darkScheme = !self.darkScheme`); + const toggleComic = ScriptField.MakeScript(`toggleComicMode()`); + const snapshotDashboard = ScriptField.MakeScript(`snapshotDashboard()`); + const createDashboard = ScriptField.MakeScript(`createNewDashboard()`); + dashboardDoc.contextMenuScripts = new List<ScriptField>([toggleTheme!, toggleComic!, snapshotDashboard!, createDashboard!]); + dashboardDoc.contextMenuLabels = new List<string>(["Toggle Theme Colors", "Toggle Comic Mode", "Snapshot Dashboard", "Create Dashboard"]); + + Doc.AddDocToList(dashboards, "data", dashboardDoc); + // bcz: strangely, we need a timeout to prevent exceptions/issues initializing GoldenLayout (the rendering engine for Main Container) + setTimeout(() => { + CurrentUserUtils.openDashboard(userDoc, dashboardDoc); + }, 0); + } } -Scripting.addGlobal(function createNewDashboard() { return MainView.Instance.createNewDashboard(); }, +Scripting.addGlobal(function snapshotDashboard() { CurrentUserUtils.snapshotDashboard(Doc.UserDoc()); }, + "creates a snapshot copy of a dashboard"); +Scripting.addGlobal(function createNewDashboard() { return CurrentUserUtils.createNewDashboard(Doc.UserDoc()); }, "creates a new dashboard when called"); Scripting.addGlobal(function createNewPresentation() { return MainView.Instance.createNewPresentation(); }, "creates a new presentation when called"); diff --git a/src/client/util/History.ts b/src/client/util/History.ts index cab682ac7..cbe36b401 100644 --- a/src/client/util/History.ts +++ b/src/client/util/History.ts @@ -1,8 +1,8 @@ import { Doc } from "../../fields/Doc"; import { DocServer } from "../DocServer"; -import { MainView } from "../views/MainView"; import * as qs from 'query-string'; import { Utils, OmitKeys } from "../../Utils"; +import { CurrentUserUtils } from "./CurrentUserUtils"; export namespace HistoryUtil { export interface DocInitializerList { @@ -197,7 +197,7 @@ export namespace HistoryUtil { await Promise.all(Object.keys(init).map(id => initDoc(id, init[id]))); } if (field instanceof Doc) { - MainView.Instance.openDashboard(field, true); + CurrentUserUtils.openDashboard(Doc.UserDoc(), field, true); } } diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index cfa2534c9..d107b74f0 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -70,7 +70,6 @@ import "./MainView.scss"; export class MainView extends React.Component { public static Instance: MainView; private _buttonBarHeight = 36; - private _urlState: HistoryUtil.DocUrl; private _docBtnRef = React.createRef<HTMLDivElement>(); private _mainViewRef = React.createRef<HTMLDivElement>(); @@ -145,7 +144,7 @@ export class MainView extends React.Component { super(props); MainView.Instance = this; this.sidebarContent.proto = undefined; - this._urlState = HistoryUtil.parseUrl(window.location) || {} as any; + CurrentUserUtils._urlState = HistoryUtil.parseUrl(window.location) || {} as any; // causes errors to be generated when modifying an observable outside of an action CurrentUserUtils.propertiesWidth = 0; @@ -233,11 +232,11 @@ export class MainView extends React.Component { if (received && !this.userDoc) { reaction( () => CurrentUserUtils.GuestTarget, - target => target && this.createNewDashboard(), + target => target && CurrentUserUtils.createNewDashboard(Doc.UserDoc()), { fireImmediately: true } ); } else { - if (received && this._urlState.sharing) { + if (received && CurrentUserUtils._urlState.sharing) { reaction(() => CollectionDockingView.Instance && CollectionDockingView.Instance.initialized, initialized => initialized && received && DocServer.GetRefField(received).then(docField => { if (docField instanceof Doc && docField._viewType !== CollectionViewType.Docking) { @@ -248,9 +247,9 @@ export class MainView extends React.Component { } const doc = this.userDoc && await Cast(this.userDoc.activeDashboard, Doc); if (doc) { - this.openDashboard(doc); + CurrentUserUtils.openDashboard(Doc.UserDoc(), doc); } else { - this.createNewDashboard(); + CurrentUserUtils.createNewDashboard(Doc.UserDoc()); } } } @@ -271,73 +270,6 @@ export class MainView extends React.Component { Doc.AddDocToList(myPresentations, "data", pres); } - @action - createNewDashboard = async (id?: string) => { - const myPresentations = Doc.UserDoc().myPresentations as Doc; - const presentation = Doc.MakeCopy(Doc.UserDoc().emptyPresentation as Doc, true); - const dashboards = Cast(this.userDoc.myDashboards, Doc) as Doc; - const dashboardCount = DocListCast(dashboards.data).length + 1; - const emptyPane = Cast(this.userDoc.emptyPane, Doc, null); - emptyPane["dragFactory-count"] = NumCast(emptyPane["dragFactory-count"]) + 1; - const freeformOptions: DocumentOptions = { - x: 0, - y: 400, - _width: this._panelWidth * .7 - this.propertiesWidth() * 0.7, - _height: this._panelHeight, - title: `Untitled Tab ${NumCast(emptyPane["dragFactory-count"])}`, - }; - const freeformDoc = CurrentUserUtils.GuestTarget || Docs.Create.FreeformDocument([], freeformOptions); - const dashboardDoc = Docs.Create.StandardCollectionDockingDocument([{ doc: freeformDoc, initialWidth: 600 }], { title: `Dashboard ${dashboardCount}` }, id, "row"); - Doc.AddDocToList(myPresentations, "data", presentation); - Doc.UserDoc().activePresentation = presentation; - const toggleTheme = ScriptField.MakeScript(`self.darkScheme = !self.darkScheme`); - const toggleComic = ScriptField.MakeScript(`toggleComicMode()`); - const copyDashboard = ScriptField.MakeScript(`copyDashboard()`); - const createDashboard = ScriptField.MakeScript(`createDashboard()`); - dashboardDoc.contextMenuScripts = new List<ScriptField>([toggleTheme!, toggleComic!, copyDashboard!, createDashboard!]); - dashboardDoc.contextMenuLabels = new List<string>(["Toggle Theme Colors", "Toggle Comic Mode", "Snapshot Dashboard", "Create Dashboard"]); - - Doc.AddDocToList(dashboards, "data", dashboardDoc); - // bcz: strangely, we need a timeout to prevent exceptions/issues initializing GoldenLayout (the rendering engine for Main Container) - setTimeout(() => this.openDashboard(dashboardDoc), 0); - } - - @action - openDashboard = (doc: Doc, fromHistory = false) => { - CurrentUserUtils.MainDocId = doc[Id]; - - if (doc) { // this has the side-effect of setting the main container since we're assigning the active/guest dashboard - !("presentationView" in doc) && (doc.presentationView = new List<Doc>([Docs.Create.TreeDocument([], { title: "Presentation" })])); - this.userDoc ? (this.userDoc.activeDashboard = doc) : (CurrentUserUtils.GuestDashboard = doc); - } - const state = this._urlState; - if (state.sharing === true && !this.userDoc) { - DocServer.Control.makeReadOnly(); - } else { - fromHistory || HistoryUtil.pushState({ - type: "doc", - docId: doc[Id], - readonly: state.readonly, - nro: state.nro, - sharing: false, - }); - if (state.readonly === true || state.readonly === null) { - DocServer.Control.makeReadOnly(); - } else if (state.safe) { - if (!state.nro) { - DocServer.Control.makeReadOnly(); - } - CollectionView.SetSafeMode(true); - } else if (state.nro || state.nro === null || state.readonly === false) { - } else if (doc.readOnly) { - DocServer.Control.makeReadOnly(); - } else { - DocServer.Control.makeEditable(); - } - } - - return true; - } onDrop = (e: React.DragEvent<HTMLDivElement>) => { e.preventDefault(); @@ -454,7 +386,7 @@ export class MainView extends React.Component { flyoutWidthFunc = () => this.flyoutWidth; addDocTabFunc = (doc: Doc, where: string, libraryPath?: Doc[]): boolean => { return where === "close" ? CollectionDockingView.CloseRightSplit(doc) : - doc.dockingConfig ? this.openDashboard(doc) : + doc.dockingConfig ? CurrentUserUtils.openDashboard(Doc.UserDoc(), doc) : CollectionDockingView.AddRightSplit(doc, libraryPath); } sidebarScreenToLocal = () => new Transform(0, (CollectionMenu.Instance.Pinned ? -35 : 0) - Number(SEARCH_PANEL_HEIGHT.replace("px", "")), 1); @@ -998,14 +930,5 @@ export class MainView extends React.Component { } Scripting.addGlobal(function selectMainMenu(doc: Doc, title: string) { MainView.Instance.selectMenu(doc); }); Scripting.addGlobal(function toggleComicMode() { Doc.UserDoc().fontFamily = "Comic Sans MS"; Doc.UserDoc().renderStyle = Doc.UserDoc().renderStyle === "comic" ? undefined : "comic"; }); -Scripting.addGlobal(function copyDashboard() { - const activeDashboard = Cast(Doc.UserDoc().activeDashboard, Doc, null); - CollectionDockingView.Copy(activeDashboard).then(copy => { - Doc.AddDocToList(Cast(Doc.UserDoc().myDashboards, Doc, null), "data", copy); - // bcz: strangely, we need a timeout to prevent exceptions/issues initializing GoldenLayout (the rendering engine for Main Container) - setTimeout(() => MainView.Instance.openDashboard(copy), 0); - }); -}); -Scripting.addGlobal(function createDashboard() { MainView.Instance.createNewDashboard(); }); Scripting.addGlobal(function importDocument() { return MainView.Instance.importDocument(); }, "imports files from device directly into the import sidebar"); diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx index e7f54ff86..dfefd645a 100644 --- a/src/client/views/collections/CollectionDockingView.tsx +++ b/src/client/views/collections/CollectionDockingView.tsx @@ -25,7 +25,6 @@ import { SelectionManager } from '../../util/SelectionManager'; import { SnappingManager } from '../../util/SnappingManager'; import { Transform } from '../../util/Transform'; import { undoBatch } from "../../util/UndoManager"; -import { MainView } from '../MainView'; import { DocumentView } from "../nodes/DocumentView"; import { PresBox } from '../nodes/PresBox'; import "./CollectionDockingView.scss"; @@ -34,6 +33,7 @@ import { SubCollectionViewProps, CollectionSubView } from "./CollectionSubView"; import { CollectionViewType } from './CollectionView'; import { DockingViewButtonSelector } from './ParentDocumentSelector'; import React = require("react"); +import { CurrentUserUtils } from '../../util/CurrentUserUtils'; const _global = (window /* browser */ || global /* node */) as any; @observer @@ -87,7 +87,7 @@ export class CollectionDockingView extends CollectionSubView(doc => doc) { @action public OpenFullScreen(docView: DocumentView, libraryPath?: Doc[]) { if (docView.props.Document._viewType === CollectionViewType.Docking && docView.props.Document.layoutKey === "layout") { - return MainView.Instance.openDashboard(docView.props.Document); + return CurrentUserUtils.openDashboard(Doc.UserDoc(), docView.props.Document); } const document = Doc.MakeAlias(docView.props.Document); const newItemStackConfig = { @@ -188,7 +188,7 @@ export class CollectionDockingView extends CollectionSubView(doc => doc) { @action public static AddRightSplit(document: Doc, libraryPath?: Doc[]) { if (!CollectionDockingView.Instance) return false; - if (document._viewType === CollectionViewType.Docking) return MainView.Instance.openDashboard(document); + if (document._viewType === CollectionViewType.Docking) return CurrentUserUtils.openDashboard(Doc.UserDoc(), document); const instance = CollectionDockingView.Instance; const newItemStackConfig = { type: 'stack', @@ -864,7 +864,7 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> { addDocTab = (doc: Doc, location: string, libraryPath?: Doc[]) => { SelectionManager.DeselectAll(); if (doc._viewType === CollectionViewType.Docking) { - return MainView.Instance.openDashboard(doc); + return CurrentUserUtils.openDashboard(Doc.UserDoc(), doc); } else if (location === "onRight") { return CollectionDockingView.AddRightSplit(doc, libraryPath); } else if (location === "close") { diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx index 5099f0022..d7f0f8e14 100644 --- a/src/client/views/collections/CollectionTreeView.tsx +++ b/src/client/views/collections/CollectionTreeView.tsx @@ -20,7 +20,6 @@ import { undoBatch, UndoManager } from '../../util/UndoManager'; import { ContextMenu } from '../ContextMenu'; import { ContextMenuProps } from '../ContextMenuItem'; import { EditableView } from "../EditableView"; -import { MainView } from '../MainView'; import { ContentFittingDocumentView } from '../nodes/ContentFittingDocumentView'; import { DocumentView } from '../nodes/DocumentView'; import { ImageBox } from '../nodes/ImageBox'; @@ -32,6 +31,7 @@ import { CollectionViewType } from './CollectionView'; import React = require("react"); import { makeTemplate } from '../../util/DropConverter'; import { TraceMobx } from '../../../fields/util'; +import { CurrentUserUtils } from '../../util/CurrentUserUtils'; export interface TreeViewProps { document: Doc; @@ -739,7 +739,7 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll onContextMenu = (e: React.MouseEvent): void => { // need to test if propagation has stopped because GoldenLayout forces a parallel react hierarchy to be created for its top-level layout if (!e.isPropagationStopped() && this.doc === Doc.UserDoc().myDashboards) { - ContextMenu.Instance.addItem({ description: "Create Dashboard", event: () => MainView.Instance.createNewDashboard(), icon: "plus" }); + ContextMenu.Instance.addItem({ description: "Create Dashboard", event: () => CurrentUserUtils.createNewDashboard(Doc.UserDoc()), icon: "plus" }); ContextMenu.Instance.addItem({ description: "Delete Dashboard", event: () => this.remove(this.doc), icon: "minus" }); e.stopPropagation(); e.preventDefault(); diff --git a/src/client/views/search/SearchBox.scss b/src/client/views/search/SearchBox.scss index b5557e0d9..aa5180c96 100644 --- a/src/client/views/search/SearchBox.scss +++ b/src/client/views/search/SearchBox.scss @@ -6,23 +6,69 @@ flex-direction: column; width: 100%; height: 100%; - position: absolute; + position: relative; font-size: 10px; line-height: 1; overflow-y: auto; overflow-x: visible; background: lightgrey; + overflow: visible; + z-index: 10000; + .searchBox-lozenge-user, + .searchBox-lozenge-dashboard, .searchBox-lozenge { background-color: #313131; border-radius: 5px; - height: 15px; + height: 18px; padding: 4px; box-shadow: lightgrey 0.15em 0.15em 0.1em; margin: 2px; margin-bottom: 4px; border-top: dimgrey 1px solid; border-left: dimgrey 1px solid; + display: flex; + .searchBox-logoff, + .searchBox-dashboards { + border-radius: 3px; + background: olivedrab; + color: white; + position: relative; + display: none; + margin-left: 3px; + padding-left: 2px; + padding-right: 2px; + padding-bottom: 11px; + cursor: default; + } + .searchBox-logoff { + background: red; + } + + .searchBox-dashSelect{ + background-color: black; + color: white; + font-size: 9; + margin-right: 6; + border-radius: 5px; + position: relative; + height: 15px; + transform: translate(0,-3px); + + &:hover { + cursor: pointer; + } + } + } + .searchBox-lozenge-user:hover { + .searchBox-logoff { + display:inline-block; + } + } + .searchBox-lozenge-dashboard:hover { + .searchBox-dashboards { + display:inline-block; + } } } diff --git a/src/client/views/search/SearchBox.tsx b/src/client/views/search/SearchBox.tsx index 5739c2a67..7e233ecbb 100644 --- a/src/client/views/search/SearchBox.tsx +++ b/src/client/views/search/SearchBox.tsx @@ -5,14 +5,15 @@ import { observer } from 'mobx-react'; import * as React from 'react'; import { Doc, DocListCast, Field, Opt } from '../../../fields/Doc'; import { documentSchema } from "../../../fields/documentSchemas"; -import { Id, Copy } from '../../../fields/FieldSymbols'; +import { Copy, Id } from '../../../fields/FieldSymbols'; import { List } from '../../../fields/List'; import { createSchema, listSpec, makeInterface } from '../../../fields/Schema'; import { SchemaHeaderField } from '../../../fields/SchemaHeaderField'; import { Cast, NumCast, StrCast } from '../../../fields/Types'; -import { returnFalse, returnZero, setupMoveUpEvents, emptyFunction } from '../../../Utils'; +import { emptyFunction, returnFalse, returnZero, setupMoveUpEvents, Utils } from '../../../Utils'; import { Docs } from '../../documents/Documents'; import { DocumentType } from "../../documents/DocumentTypes"; +import { CurrentUserUtils } from "../../util/CurrentUserUtils"; import { SetupDrag } from '../../util/DragManager'; import { SearchUtil } from '../../util/SearchUtil'; import { Transform } from '../../util/Transform'; @@ -22,6 +23,7 @@ import { CollectionViewType } from '../collections/CollectionView'; import { ViewBoxBaseComponent } from "../DocComponent"; import { FieldView, FieldViewProps } from '../nodes/FieldView'; import "./SearchBox.scss"; +import { undoBatch } from "../../util/UndoManager"; export const searchSchema = createSchema({ Document: Doc }); @@ -486,18 +488,34 @@ export class SearchBox extends ViewBoxBaseComponent<FieldViewProps, SearchBoxDoc collectionView.props.Document._docFilters = docsForFilter?.length && docFilters?.length ? new List<string>(docFilters) : undefined; } } + showLogout = () => { + + } render() { + const myDashboards = DocListCast(Cast(Doc.UserDoc().myDashboards, Doc, null).data); return ( <div style={{ pointerEvents: "all" }} className="searchBox-container"> <div style={{ position: "absolute", left: 15, height: 32, alignItems: "center", display: "flex" }}> - <div className="searchBox-lozenge"> + <div className="searchBox-lozenge-user"> {`${Doc.CurrentUserEmail}`} + <div className="searchBox-logoff" onClick={() => window.location.assign(Utils.prepend("/logout"))}> + Logoff + </div> </div> <div className="searchBox-lozenge"> {`UI project`} </div> - <div className="searchBox-lozenge" > - {`➱ ${Cast(Doc.UserDoc().activeDashboard, Doc, null)?.title}`} + <div className="searchBox-lozenge-dashboard" > + <select className="searchBox-dashSelect" onChange={e => CurrentUserUtils.openDashboard(Doc.UserDoc(), myDashboards[Number(e.target.value)])} + value={myDashboards.indexOf(Cast(Doc.UserDoc().activeDashboard, Doc, null)!)}> + {myDashboards.map((dash, i) => <option key={dash[Id]} value={i}> {StrCast(dash.title)} </option>)} + </select> + <div className="searchBox-dashboards" onClick={undoBatch(() => CurrentUserUtils.createNewDashboard(Doc.UserDoc()))}> + New + </div> + <div className="searchBox-dashboards" onClick={undoBatch(() => CurrentUserUtils.snapshotDashboard(Doc.UserDoc()))}> + Snapshot + </div> </div> </div> <div className="searchBox-bar"> |