import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Button, ColorPicker, EditableText, Size, Type } from '@dash/components'; import { action, computed, makeObservable, observable } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { FaPlus } from 'react-icons/fa'; import { ClientUtils } from '../../ClientUtils'; import { Doc, DocListCast } from '../../fields/Doc'; import { AclPrivate, DocAcl } from '../../fields/DocSymbols'; import { Id } from '../../fields/FieldSymbols'; import { List } from '../../fields/List'; import { PrefetchProxy } from '../../fields/Proxy'; import { listSpec } from '../../fields/Schema'; import { ScriptField } from '../../fields/ScriptField'; import { Cast, ImageCast, StrCast } from '../../fields/Types'; import { SharingPermissions, inheritParentAcls, normalizeEmail } from '../../fields/util'; import { DocServer } from '../DocServer'; import { DocUtils } from '../documents/DocUtils'; import { Docs, DocumentOptions } from '../documents/Documents'; import { dropActionType } from '../util/DropActionTypes'; import { HistoryUtil } from '../util/History'; import { ScriptingGlobals } from '../util/ScriptingGlobals'; import { SnappingManager } from '../util/SnappingManager'; import { undoBatch, undoable } from '../util/UndoManager'; import { ContextMenu } from './ContextMenu'; import './DashboardView.scss'; import { MainViewModal } from './MainViewModal'; import { ObservableReactComponent } from './ObservableReactComponent'; import { Colors } from './global/globalEnums'; import { DocumentView } from './nodes/DocumentView'; import { ButtonType } from './nodes/FontIconBox/FontIconBox'; enum DashboardGroup { MyDashboards, SharedDashboards, } export type DocConfig = { doc: Doc; initialWidth?: number; path?: Doc[]; }; // DashboardView is the view with the dashboard previews, rendered when the app first loads @observer export class DashboardView extends ObservableReactComponent { public static _urlState: HistoryUtil.DocUrl; public static makeDocumentConfig(document: Doc, panelName?: string, width?: number, keyValue?: boolean) { return { type: 'react-component', component: 'DocumentFrameRenderer', title: document.title, width: width, props: { documentId: document[Id], keyValue, panelName, // name of tab that can be used to close or replace its contents }, }; } static StandardCollectionDockingDocument(configs: Array, options: DocumentOptions, id?: string, type: string = 'row') { const layoutConfig = { content: [ { type: type, content: [...configs.map(config => DashboardView.makeDocumentConfig(config.doc, undefined, config.initialWidth))], }, ], }; const doc = Docs.Create.DockDocument( configs.map(c => c.doc), JSON.stringify(layoutConfig), ClientUtils.CurrentUserEmail() === 'guest' ? options : { acl_Guest: SharingPermissions.View, ...options }, id ); configs.forEach(c => { Doc.SetContainer(c.doc, doc); inheritParentAcls(doc, c.doc, false); }); return doc; } constructor(props: object) { super(props); makeObservable(this); } @observable private openModal = false; @observable private selectedDashboardGroup = DashboardGroup.MyDashboards; @observable private newDashboardName = ''; @observable private newDashboardColor = '#AFAFAF'; @action abortCreateNewDashboard = () => { this.openModal = false; }; @action setNewDashboardName = (name: string) => { this.newDashboardName = name; }; @action setNewDashboardColor = (color: string) => { this.newDashboardColor = color; }; @action selectDashboardGroup = (group: DashboardGroup) => { this.selectedDashboardGroup = group; }; clickDashboard = (e: React.MouseEvent, dashboard: Doc) => { if (this.selectedDashboardGroup === DashboardGroup.SharedDashboards) { DashboardView.openSharedDashboard(dashboard); } else { Doc.ActiveDashboard = dashboard; } Doc.ActivePage = 'dashboard'; }; getDashboards = (whichGroup: DashboardGroup) => { if (whichGroup === DashboardGroup.MyDashboards) { return DocListCast(Doc.MyDashboards?.data).filter(dashboard => dashboard.$author === ClientUtils.CurrentUserEmail()); } return DocListCast(Doc.MySharedDocs?.data_dashboards).filter(doc => doc.dockingConfig); }; isUnviewedSharedDashboard = (dashboard: Doc) => !DocListCast(Doc.MySharedDocs?.viewed).includes(dashboard); @undoBatch createNewDashboard = (name: string, background?: string) => { DashboardView.createNewDashboard(undefined, name, background); this.abortCreateNewDashboard(); }; @computed get namingInterface() { return (
Create New Dashboard
this.setNewDashboardName(val as string)} fillWidth />
); } @action openNewDashboardModal = () => { this.openModal = true; this.setNewDashboardName(`Dashboard ${DocListCast(Doc.MyDashboards?.data).length + 1}`); }; _downX: number = 0; _downY: number = 0; onContextMenu = (dashboard: Doc, e: React.MouseEvent) => { // the touch onContextMenu is button 0, the pointer onContextMenu is button 2 if (navigator.userAgent.includes('Mozilla') || (Math.abs(this._downX - e.clientX) < 3 && Math.abs(this._downY - e.clientY) < 3)) { e.preventDefault(); e.stopPropagation(); ContextMenu.Instance.addItem({ description: `Share Dashboard`, event: () => DocumentView.ShareOpen(undefined, dashboard), icon: 'edit', }); ContextMenu.Instance.addItem({ description: `Delete Dashboard ${Doc.noviceMode ? '(disabled)' : ''}`, event: () => !Doc.noviceMode && DashboardView.removeDashboard(dashboard), icon: 'trash', }); ContextMenu.Instance.displayMenu(e.pageX - 15, e.pageY - 15); } }; render() { const color = SnappingManager.userColor; const variant = SnappingManager.userVariantColor; return ( <>
{this.selectedDashboardGroup === DashboardGroup.SharedDashboards && !this.getDashboards(this.selectedDashboardGroup).length ? 'No one has shared a dashboard with you' : this.getDashboards(this.selectedDashboardGroup).map(dashboard => { const href = ImageCast(dashboard.thumb)?.url?.href; const shared = Object.keys(dashboard[DocAcl]) .filter(key => key !== `acl_${normalizeEmail(ClientUtils.CurrentUserEmail())}` && !['acl_Me', 'acl_Guest'].includes(key)) .some(key => dashboard[DocAcl][key] !== AclPrivate); return (
this.onContextMenu(dashboard, e)} onClick={e => this.clickDashboard(e, dashboard)}>
{ dashboard.$title = val; }} /> {this.selectedDashboardGroup === DashboardGroup.SharedDashboards && this.isUnviewedSharedDashboard(dashboard) ?
unviewed
:
}
{ this._downX = e.clientX; this._downY = e.clientY; }} onClick={e => this.onContextMenu(dashboard, e)}>
{shared ? 'shared' : ''}
); })} {this.selectedDashboardGroup === DashboardGroup.SharedDashboards ? null : (
+
)}
); } public static closeActiveDashboard() { Doc.ActiveDashboard = undefined; } public static openSharedDashboard = (dashboard: Doc) => { Doc.MySharedDocs && Doc.AddDocToList(Doc.MySharedDocs, 'viewed', dashboard); DashboardView.openDashboard(Doc.BestEmbedding(dashboard)); }; /// opens a dashboard as the ActiveDashboard (and adds the dashboard to the users list of dashboards if it's not already there). /// this also sets the readonly state of the dashboard based on the current mode of dash (from its url) public static openDashboard = (doc: Doc | undefined, fromHistory = false) => { if (!doc) return false; Doc.MyDashboards && Doc.AddDocToList(Doc.MyDashboards, 'data', doc); Doc.MyRecentlyClosed && Doc.RemoveDocFromList(Doc.MyRecentlyClosed, 'data', doc); // this has the side-effect of setting the main container since we're assigning the active/guest dashboard Doc.UserDoc() ? (Doc.ActiveDashboard = doc) : (Doc.GuestDashboard = doc); const state = DashboardView._urlState; if (state.sharing === true && !Doc.UserDoc()) { DocServer.Control.makeReadOnly(); } else { fromHistory || HistoryUtil.pushState({ type: 'doc', docId: doc[Id], readonly: state.readonly, nro: state.nro, sharing: false, }); if (state.readonly === true || state.readonly === null) { DocServer.Control.makeReadOnly(); } else if (state.nro || state.nro === null || state.readonly === false) { /* empty */ } else if (doc.readOnly) { DocServer.Control.makeReadOnly(); } else { ClientUtils.CurrentUserEmail() !== 'guest' && DocServer.Control.makeEditable(); } } return true; }; public static removeDashboard = (dashboard: Doc) => { const dashboards = DocListCast(Doc.MyDashboards?.data).filter(dash => dash !== dashboard); undoable(() => { if (dashboard === Doc.ActiveDashboard) DashboardView.openDashboard(dashboards.lastElement()); Doc.MyDashboards && Doc.RemoveDocFromList(Doc.MyDashboards, 'data', dashboard); Doc.MyRecentlyClosed && Doc.AddDocToList(Doc.MyRecentlyClosed, 'data', dashboard, undefined, true, true); if (!dashboards.length) Doc.ActivePage = 'home'; }, 'remove dashboard')(); }; public static resetDashboard = (dashboard: Doc) => { const config = StrCast(dashboard.dockingConfig); const matches = config.match(/"documentId":"[a-z0-9-]+"/g); const docids = matches?.map(m => m.replace('"documentId":"', '').replace('"', '')) ?? []; const components = docids.map(docid => ({ type: 'component', component: 'DocumentFrameRenderer', title: 'Untitled Tab 1', width: 600, props: { documentId: docid, }, componentName: 'lm-react-component', isClosable: true, reorderEnabled: true, componentState: null, })) ?? []; const reset = { isClosable: true, reorderEnabled: true, title: '', openPopouts: [], maximisedItemId: null, settings: { hasHeaders: true, constrainDragToContainer: true, reorderEnabled: true, selectionEnabled: false, popoutWholeStack: false, blockedPopoutsThrowError: true, closePopoutsOnUnload: true, showPopoutIcon: true, showMaximiseIcon: true, showCloseIcon: true, responsiveMode: 'onload', tabOverlapAllowance: 0, reorderOnTabMenuClick: false, tabControlOffset: 10, }, dimensions: { borderWidth: 3, borderGrabWidth: 5, minItemHeight: 10, minItemWidth: 20, headerHeight: 27, dragProxyWidth: 300, dragProxyHeight: 200, }, labels: { close: 'close', maximise: 'maximise', minimise: 'minimise', popout: 'new tab', popin: 'pop in', tabDropdown: 'additional tabs', }, content: [ { type: 'row', isClosable: true, reorderEnabled: true, title: '', content: [ { type: 'stack', width: 100, isClosable: true, reorderEnabled: true, title: '', activeItemIndex: 0, content: components, }, ], }, ], }; const dockingOnLayout = dashboard._dockingConfig && dashboard._dockingConfig !== dashboard.$dockingConfig; dashboard[`${dockingOnLayout ? '_' : '$'}dockingConfig`] = JSON.stringify(reset); return reset; }; public static createNewDashboard = (id?: string, name?: string, background?: string) => { const dashboardCount = DocListCast(Doc.MyDashboards?.data).length + 1; const freeformOptions: DocumentOptions = { x: 0, y: 400, _width: 1500, _height: 1000, _layout_fitWidth: true, _freeform_backgroundGrid: true, backgroundColor: background, title: `Untitled Tab 1`, }; const title = name || `Dashboard ${dashboardCount}`; const freeformDoc = Doc.GuestTarget || Docs.Create.FreeformDocument([], freeformOptions); const dashboardDoc = DashboardView.StandardCollectionDockingDocument([{ doc: freeformDoc, initialWidth: 600 }], { title: title }, id, 'row'); Doc.MyHeaderBar && Doc.AddDocToList(Doc.MyHeaderBar, 'data', freeformDoc, undefined, undefined, true); Doc.MyDashboards && Doc.AddDocToList(Doc.MyDashboards, 'data', dashboardDoc); freeformDoc._embedContainer = dashboardDoc; dashboardDoc.$myPaneCount = 1; dashboardDoc.$myOverlayDocs = new List(); dashboardDoc.$myPublishedDocs = new List(); dashboardDoc.$myTagCollections = new List(); dashboardDoc.$myUniqueFaces = new List(); dashboardDoc.$myTrails = DashboardView.SetupDashboardTrails(); dashboardDoc.$myCalendars = DashboardView.SetupDashboardCalendars(); // open this new dashboard Doc.ActiveDashboard = dashboardDoc; Doc.ActivePage = 'dashboard'; Doc.ActivePresentation = undefined; }; public static SetupDashboardCalendars() { // this section is creating the button document itself === myTrails = new Button // create a a list of calendars (as a CalendarCollectionDocument) and store it on the new dashboard const reqdOpts: DocumentOptions = { title: 'My Calendars', _layout_showTitle: 'title', _height: 100, treeView_HideTitle: true, _layout_fitWidth: true, _gridGap: 5, _forceActive: true, childDragAction: dropActionType.embed, treeView_TruncateTitleWidth: 150, ignoreClick: true, contextMenuIcons: new List(['plus']), contextMenuLabels: new List(['Create New Calendar']), _lockedPosition: true, layout_boxShadow: '0 0', childDontRegisterViews: true, dropAction: dropActionType.same, isSystem: true, layout_explainer: 'All of the calendars that you have created will appear here.', }; const myCalendars = DocUtils.AssignScripts(Docs.Create.StackingDocument([], reqdOpts)); // { treeView_ChildDoubleClick: 'openPresentation(documentView.rootDoc)' } return new PrefetchProxy(myCalendars); } public static SetupDashboardTrails() { // this section is creating the button document itself === myTrails = new Button const reqdBtnOpts: DocumentOptions = { _forceActive: true, _width: 30, _height: 30, _dragOnlyWithinContainer: true, title: 'New trail', toolTip: 'Create new trail', color: Colors.BLACK, btnType: ButtonType.ClickButton, buttonText: 'New trail', icon: 'plus', isSystem: true, }; const reqdBtnScript = { onClick: `createNewPresentation()` }; const myTrailsBtn = DocUtils.AssignScripts(Docs.Create.FontIconDocument(reqdBtnOpts), reqdBtnScript); // createa a list of presentations (as a tree view collection) and store it on the new dashboard // instead of assigning Doc.UserDoc().myrails we want to assign Doc.AxtiveDashboard.myTrails // but we don't want to create the list of trails here-- but rather in createDashboard const reqdOpts: DocumentOptions = { title: 'My Trails', _layout_showTitle: 'title', _height: 100, treeView_HideTitle: true, _layout_fitWidth: true, _gridGap: 5, _forceActive: true, childDragAction: dropActionType.embed, treeView_TruncateTitleWidth: 150, ignoreClick: true, layout_headerButton: myTrailsBtn, contextMenuIcons: new List(['plus']), contextMenuLabels: new List(['Create New Trail']), _lockedPosition: true, layout_boxShadow: '0 0', childDontRegisterViews: true, dropAction: dropActionType.same, isSystem: true, layout_explainer: 'All of the trails that you have created will appear here.', }; const myTrails = DocUtils.AssignScripts(Docs.Create.TreeDocument([], reqdOpts), { treeView_ChildDoubleClick: 'openPresentation(documentView.Document)' }); const contextMenuScripts = [reqdBtnScript.onClick]; if (Cast(myTrails.contextMenuScripts, listSpec(ScriptField), null)?.length !== contextMenuScripts.length) { myTrails.contextMenuScripts = new List(contextMenuScripts.map(script => ScriptField.MakeFunction(script)!)); } return new PrefetchProxy(myTrails); } } // eslint-disable-next-line prefer-arrow-callback ScriptingGlobals.add(function createNewDashboard() { return DashboardView.createNewDashboard(); }, 'creates a new dashboard when called'); // eslint-disable-next-line prefer-arrow-callback ScriptingGlobals.add(function shareDashboard(dashboard: Doc) { DocumentView.ShareOpen(undefined, dashboard); }, 'opens sharing dialog for Dashboard'); // eslint-disable-next-line prefer-arrow-callback ScriptingGlobals.add(function removeDashboard(dashboard: Doc) { DashboardView.removeDashboard(dashboard); }, 'Remove Dashboard from Dashboards'); // eslint-disable-next-line prefer-arrow-callback ScriptingGlobals.add(function resetDashboard(dashboard: Doc) { DashboardView.resetDashboard(dashboard); }, 'move all dashboard tabs to single stack'); // eslint-disable-next-line prefer-arrow-callback ScriptingGlobals.add(function addToDashboards(dashboard: Doc) { DashboardView.openDashboard(Doc.MakeEmbedding(dashboard)); }, 'adds Dashboard to set of Dashboards');