import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Button, ColorPicker, EditableText, Size, Type } from 'browndash-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 { Doc, DocListCast } from '../../fields/Doc';
import { AclPrivate, DocAcl, DocData } 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 { normalizeEmail } from '../../fields/util';
import { DocServer } from '../DocServer';
import { Docs, DocumentOptions, DocUtils } from '../documents/Documents';
import { HistoryUtil } from '../util/History';
import { ScriptingGlobals } from '../util/ScriptingGlobals';
import { SettingsManager } from '../util/SettingsManager';
import { SharingManager } from '../util/SharingManager';
import { undoable, undoBatch, UndoManager } from '../util/UndoManager';
import { CollectionDockingView } from './collections/CollectionDockingView';
import { CollectionView } from './collections/CollectionView';
import { ContextMenu } from './ContextMenu';
import './DashboardView.scss';
import { Colors } from './global/globalEnums';
import { MainViewModal } from './MainViewModal';
import { ButtonType } from './nodes/FontIconBox/FontIconBox';
import { ObservableReactComponent } from './ObservableReactComponent';
import { dropActionType } from '../util/DragManager';
enum DashboardGroup {
MyDashboards,
SharedDashboards,
}
// 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;
constructor(props: any) {
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[DocData].author === Doc.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: () => SharingManager.Instance.open(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 = SettingsManager.userColor;
const variant = SettingsManager.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(Doc.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[DocData].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 snapshotDashboard() {
return CollectionDockingView.TakeSnapshot(Doc.ActiveDashboard);
}
public static openSharedDashboard = (dashboard: Doc) => {
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.AddDocToList(Doc.MyDashboards, 'data', doc);
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.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 {
Doc.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.RemoveDocFromList(Doc.MyDashboards, 'data', dashboard);
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,
},
],
},
],
};
if (dashboard.dockingConfig && dashboard.dockingConfig !== dashboard[DocData].dockingConfig) dashboard.dockingConfig = JSON.stringify(reset);
else Doc.SetInPlace(dashboard, 'dockingConfig', JSON.stringify(reset), true);
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 ? name : `Dashboard ${dashboardCount}`;
const freeformDoc = Doc.GuestTarget || Docs.Create.FreeformDocument([], freeformOptions);
const dashboardDoc = Docs.Create.StandardCollectionDockingDocument([{ doc: freeformDoc, initialWidth: 600 }], { title: title }, id, 'row');
Doc.AddDocToList(Doc.MyHeaderBar, 'data', freeformDoc, undefined, undefined, true);
dashboardDoc.pane_count = 1;
freeformDoc.embedContainer = dashboardDoc;
dashboardDoc.myOverlayDocs = new List();
dashboardDoc.myPublishedDocs = new List();
Doc.AddDocToList(Doc.MyDashboards, 'data', dashboardDoc);
DashboardView.SetupDashboardTrails(dashboardDoc);
DashboardView.SetupDashboardCalendars(dashboardDoc);
// open this new dashboard
Doc.ActiveDashboard = dashboardDoc;
Doc.ActivePage = 'dashboard';
Doc.ActivePresentation = undefined;
};
public static SetupDashboardCalendars(dashboardDoc: Doc) {
// 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.CalendarCollectionDocument([], reqdOpts));
// { treeView_ChildDoubleClick: 'openPresentation(documentView.rootDoc)' }
dashboardDoc.myCalendars = new PrefetchProxy(myCalendars);
}
public static SetupDashboardTrails(dashboardDoc: Doc) {
// 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)' });
dashboardDoc.myTrails = new PrefetchProxy(myTrails);
const contextMenuScripts = [reqdBtnScript.onClick];
if (Cast(myTrails.contextMenuScripts, listSpec(ScriptField), null)?.length !== contextMenuScripts.length) {
myTrails.contextMenuScripts = new List(contextMenuScripts.map(script => ScriptField.MakeFunction(script)!));
}
}
}
ScriptingGlobals.add(function createNewDashboard() {
return DashboardView.createNewDashboard();
}, 'creates a new dashboard when called');
ScriptingGlobals.add(function shareDashboard(dashboard: Doc) {
SharingManager.Instance.open(undefined, dashboard);
}, 'opens sharing dialog for Dashboard');
ScriptingGlobals.add(function removeDashboard(dashboard: Doc) {
DashboardView.removeDashboard(dashboard);
}, 'Remove Dashboard from Dashboards');
ScriptingGlobals.add(function resetDashboard(dashboard: Doc) {
DashboardView.resetDashboard(dashboard);
}, 'move all dashboard tabs to single stack');
ScriptingGlobals.add(function addToDashboards(dashboard: Doc) {
DashboardView.openDashboard(Doc.MakeEmbedding(dashboard));
}, 'adds Dashboard to set of Dashboards');
ScriptingGlobals.add(async function snapshotDashboard() {
const batch = UndoManager.StartBatch('snapshot');
await DashboardView.snapshotDashboard();
batch.end();
}, 'creates a snapshot copy of a dashboard');