aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/client/documents/DocumentTypes.ts2
-rw-r--r--src/client/documents/Documents.ts11
-rw-r--r--src/client/util/CalendarManager.scss62
-rw-r--r--src/client/util/CalendarManager.tsx312
-rw-r--r--src/client/views/DashboardView.tsx31
-rw-r--r--src/client/views/DocumentButtonBar.tsx26
-rw-r--r--src/client/views/MainView.tsx2
-rw-r--r--src/client/views/collections/CollectionDockingView.tsx1
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx2
-rw-r--r--src/client/views/nodes/MapBox/MapBox.scss31
-rw-r--r--src/client/views/nodes/MapBox/MapBox.tsx153
-rw-r--r--src/client/views/nodes/MapBox/MapboxApiUtility.ts98
-rw-r--r--src/fields/Doc.ts1
13 files changed, 577 insertions, 155 deletions
diff --git a/src/client/documents/DocumentTypes.ts b/src/client/documents/DocumentTypes.ts
index 8cd36b312..5c2792a53 100644
--- a/src/client/documents/DocumentTypes.ts
+++ b/src/client/documents/DocumentTypes.ts
@@ -37,6 +37,7 @@ export enum DocumentType {
GROUP = 'group',
PUSHPIN = 'pushpin',
MAPROUTE = 'maproute',
+ CALENDAR = 'calendar',
SCRIPTDB = 'scriptdb', // database of scripts
GROUPDB = 'groupdb', // database of groups
@@ -61,4 +62,5 @@ export enum CollectionViewType {
Pile = 'pileup',
StackedTimeline = 'stacked timeline',
NoteTaking = 'notetaking',
+ Calendar = 'calendar_view'
}
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index e78686b3e..d6fd8aea3 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -186,6 +186,8 @@ export class DocumentOptions {
map_bearing?: NUMt = new NumInfo('bearing of a map view', false);
map_style?: STRt = new StrInfo('mapbox style for a map view', false);
+ date_range?: STRt = new StrInfo('date range for calendar', false);
+
wikiData?: STRt = new StrInfo('WikiData ID related to map location');
description?: STRt = new StrInfo('A description of the document');
_timecodeToShow?: NUMt = new NumInfo('the time that a document should be displayed (e.g., when an annotation shows up as a video plays)', false);
@@ -1022,6 +1024,7 @@ export namespace Docs {
export function LoadingDocument(file: File | string, options: DocumentOptions) {
return InstanceFromProto(Prototypes.get(DocumentType.LOADING), undefined, { _height: 150, _width: 200, title: typeof file == 'string' ? file : file.name, ...options }, undefined, '');
}
+
export function RTFDocument(field: RichTextField, options: DocumentOptions = {}, fieldKey: string = 'text') {
return InstanceFromProto(Prototypes.get(DocumentType.RTF), field, options, undefined, fieldKey);
@@ -1143,6 +1146,10 @@ export namespace Docs {
return InstanceFromProto(Prototypes.get(DocumentType.MAPROUTE), new List(documents), { infoWindowOpen, ...options }, id);
}
+ export function CalendarDocument(options: DocumentOptions={}, documents: Array<Doc>){
+ return InstanceFromProto(Prototypes.get(DocumentType.CALENDAR), new List(documents), options)
+ }
+
// shouldn't ever need to create a KVP document-- instead set the LayoutTemplateString to be a KeyValueBox for the DocumentView (see addDocTab in TabDocView)
// export function KVPDocument(document: Doc, options: DocumentOptions = {}) {
// return InstanceFromProto(Prototypes.get(DocumentType.KVP), document, { title: document.title + '.kvp', ...options });
@@ -1197,6 +1204,10 @@ export namespace Docs {
return doc;
}
+ export function CalendarCollectionDocument(documents: Array<Doc>, options: DocumentOptions){
+ return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), {...options, _type_collection: CollectionViewType.Calendar});
+ }
+
export function StackingDocument(documents: Array<Doc>, options: DocumentOptions, id?: string, protoId?: string) {
return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { ...options, _type_collection: CollectionViewType.Stacking }, id, undefined, protoId);
}
diff --git a/src/client/util/CalendarManager.scss b/src/client/util/CalendarManager.scss
new file mode 100644
index 000000000..60610f298
--- /dev/null
+++ b/src/client/util/CalendarManager.scss
@@ -0,0 +1,62 @@
+.calendar-interface{
+ width: 600px;
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ justify-content: center;
+ gap: 7px;
+ padding: 10px;
+
+ .selected-doc-title{
+ font-size: 1.4em;
+ }
+
+ .creation-type-container{
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ align-self: center;
+ gap: 12px;
+
+ .calendar-creation{
+ cursor: pointer;
+ }
+
+ .calendar-creation-selected{
+ border-bottom: 2px solid white;
+ }
+ }
+
+ .choose-calendar-container{
+ margin-top: 10px;
+ align-self: center;
+ width: 60%;
+
+ .MuiFilledInput-input{
+ padding: 10px;
+ }
+ }
+
+ .date-range-picker-container{
+ margin-top: 5px;
+ align-self:center;
+
+ .react-date-range{
+
+ }
+ }
+
+ .create-button-container{
+
+ margin-top: 5px;
+ align-self: center;
+
+ .button-content{
+ font-size: 1.2em;
+ padding: 10px;
+ border-radius: 5px;
+ background-color: #EFF2F7;
+ }
+
+ }
+}
diff --git a/src/client/util/CalendarManager.tsx b/src/client/util/CalendarManager.tsx
new file mode 100644
index 000000000..50a5437a0
--- /dev/null
+++ b/src/client/util/CalendarManager.tsx
@@ -0,0 +1,312 @@
+import * as React from 'react';
+import './CalendarManager.scss';
+import { observer } from 'mobx-react';
+import { action, computed, observable, runInAction, makeObservable } from 'mobx';
+import { Doc, DocListCast } from '../../fields/Doc';
+import { DocumentView } from '../views/nodes/DocumentView';
+import { DictationOverlay } from '../views/DictationOverlay';
+import { TaskCompletionBox } from '../views/nodes/TaskCompletedBox';
+import { MainViewModal } from '../views/MainViewModal';
+import { TextField } from '@mui/material';
+import Select from 'react-select';
+import { SettingsManager } from './SettingsManager';
+import { DocCast, StrCast } from '../../fields/Types';
+import { SelectionManager } from './SelectionManager';
+import { DocumentManager } from './DocumentManager';
+import { DocData } from '../../fields/DocSymbols';
+// import { DateRange, Range, RangeKeyDict } from 'react-date-range';
+import { Button } from 'browndash-components';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { DateRangePicker } from '@adobe/react-spectrum';
+import { IconLookup, faPlus } from '@fortawesome/free-solid-svg-icons';
+import { Docs } from '../documents/Documents';
+import { ObservableReactComponent } from '../views/ObservableReactComponent';
+// import 'react-date-range/dist/styles.css';
+// import 'react-date-range/dist/theme/default.css';
+
+type CreationType = 'new-calendar' | 'existing-calendar' | 'manage-calendars';
+
+interface CalendarSelectOptions {
+ label: string;
+ value: string;
+}
+
+const formatDateToString = (date: Date) => {
+ const year = date.getFullYear();
+ const month = String(date.getMonth() + 1).padStart(2, '0');
+ const day = String(date.getDate()).padStart(2, '0');
+
+ return `${year}-${month}-${day}`;
+};
+
+// TODO: If doc is already part of a calendar, display that
+// TODO: For a doc already in a calendar: give option to edit date range, delete from calendar
+
+@observer
+export class CalendarManager extends ObservableReactComponent<{}> {
+ public static Instance: CalendarManager;
+ @observable private isOpen = false;
+ @observable private targetDoc: Doc | undefined; // the target document
+ @observable private targetDocView: DocumentView | undefined; // the DocumentView of the target doc
+ @observable private dialogueBoxOpacity = 1; // for the modal
+ @observable private overlayOpacity = 0.4; // for the modal
+
+ @observable private layoutDocAcls: boolean = false; // whether the layout doc or data doc's acls are to be used
+
+ @observable private creationType: CreationType = 'new-calendar';
+
+ @observable private existingCalendars: Doc[] = DocListCast(Doc.MyCalendars?.data);
+
+ @computed get selectOptions() {
+ return this.existingCalendars.map(calendar => ({ label: StrCast(calendar.title), value: StrCast(calendar.title) }));
+ }
+
+ @observable
+ selectedExistingCalendarOption: CalendarSelectOptions | null = null;
+
+ @observable
+ calendarName: string = '';
+
+ @observable
+ calendarDescription: string = '';
+
+ @observable
+ errorMessage: string = '';
+
+ @action
+ setInterationType = (type: CreationType) => {
+ this.errorMessage = '';
+ this.calendarName = '';
+ this.creationType = type;
+ };
+
+ public open = (target?: DocumentView, target_doc?: Doc) => {
+ console.log('hi');
+ runInAction(() => {
+ this.targetDoc = target_doc || target?.Document;
+ this.targetDocView = target;
+ DictationOverlay.Instance.hasActiveModal = true;
+ this.isOpen = this.targetDoc !== undefined;
+ });
+ };
+
+ public close = action(() => {
+ this.isOpen = false;
+ TaskCompletionBox.taskCompleted = false;
+ setTimeout(
+ action(() => {
+ DictationOverlay.Instance.hasActiveModal = false;
+ this.targetDoc = undefined;
+ }),
+ 500
+ );
+ this.layoutDocAcls = false;
+ });
+
+ constructor(props: {}) {
+ super(props);
+ CalendarManager.Instance = this;
+ makeObservable(this);
+ }
+
+ componentDidMount(): void {}
+
+ @action
+ handleSelectChange = (option: any) => {
+ let selectOpt = option as CalendarSelectOptions;
+ this.selectedExistingCalendarOption = selectOpt;
+ this.calendarName = selectOpt.value; // or label
+ };
+
+ @action
+ handleTextFieldChange = (event: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>) => {
+ this.calendarName = event.target.value;
+ };
+
+ // TODO: Make undoable
+ private addToCalendar = () => {
+ let docs = SelectionManager.Views.length < 2 ? [this.targetDoc] : SelectionManager.Views.map(docView => docView.Document);
+ const targetDoc = this.layoutDocAcls ? docs[0] : docs[0]?.[DocData]; // doc to add to calendar
+
+ if (targetDoc) {
+ let calendar: Doc;
+ if (this.creationType === 'new-calendar') {
+ if (!this.existingCalendars.find(doc => StrCast(doc.title) === this.calendarName)) {
+ calendar = Docs.Create.CalendarDocument(
+ {
+ title: this.calendarName,
+ description: this.calendarDescription,
+ },
+ []
+ );
+ } else {
+ this.errorMessage = 'Calendar with this name already exists';
+ return;
+ }
+ } else {
+ // find existing calendar based on selected name (should technically always find one)
+ const existingCalendar = this.existingCalendars.find(calendar => StrCast(calendar.title) === this.calendarName);
+ if (existingCalendar) calendar = existingCalendar;
+ else {
+ this.errorMessage = 'Must select an existing calendar';
+ return;
+ }
+ }
+ // Get start and end date strings
+ const startDateStr = formatDateToString(this.selectedDateRange.start);
+ const endDateStr = formatDateToString(this.selectedDateRange.end);
+
+ const subDocEmbedding = Doc.MakeEmbedding(targetDoc); // embedding
+ subDocEmbedding.embedContainer = calendar; // set embed container
+ subDocEmbedding.date_range = `${startDateStr}-${endDateStr}`; // set subDoc date range
+
+ Doc.AddDocToList(calendar, 'data', subDocEmbedding); // add embedded subDoc to calendar
+
+ if (this.creationType === 'new-calendar') {
+ Doc.AddDocToList(Doc.MyCalendars, 'data', calendar); // add to new calendar to dashboard calendars
+ }
+ }
+ };
+
+ private focusOn = (contents: string) => {
+ const title = this.targetDoc ? StrCast(this.targetDoc.title) : '';
+ const docs = SelectionManager.Views.length > 1 ? SelectionManager.Views.map(docView => docView.Document) : [this.targetDoc];
+ return (
+ <span
+ className="focus-span"
+ title={title}
+ onClick={() => {
+ if (this.targetDoc && this.targetDocView && docs.length === 1) {
+ DocumentManager.Instance.showDocument(this.targetDoc, { willZoomCentered: true });
+ }
+ }}
+ onPointerEnter={action(() => {
+ if (docs.length) {
+ docs.forEach(doc => doc && Doc.BrushDoc(doc));
+ this.dialogueBoxOpacity = 0.1;
+ this.overlayOpacity = 0.1;
+ }
+ })}
+ onPointerLeave={action(() => {
+ if (docs.length) {
+ docs.forEach(doc => doc && Doc.UnBrushDoc(doc));
+ this.dialogueBoxOpacity = 1;
+ this.overlayOpacity = 0.4;
+ }
+ })}>
+ {contents}
+ </span>
+ );
+ };
+
+ @observable
+ selectedDateRange: any = [
+ {
+ start: new Date(),
+ end: new Date(),
+ },
+ ];
+
+ @action
+ setSelectedDateRange = (range: any) => {
+ this.selectedDateRange = range;
+ };
+
+ @computed
+ get createButtonActive() {
+ if (this.calendarName.length === 0 || this.errorMessage.length > 0) return false; // disabled if no calendar name
+ let startDate: Date | undefined;
+ let endDate: Date | undefined;
+ try {
+ startDate = this.selectedDateRange[0].startDate;
+ endDate = this.selectedDateRange[0].endDate;
+ } catch (e: any) {
+ return false; // disabled
+ }
+ if (!startDate || !endDate) return false; // disabled if any is undefined
+ return true;
+ }
+
+ @computed
+ get calendarInterface() {
+ let docs = SelectionManager.Views.length < 2 ? [this.targetDoc] : SelectionManager.Views.map(docView => docView.Document);
+ const targetDoc = this.layoutDocAcls ? docs[0] : docs[0]?.[DocData];
+
+ const currentDate = new Date();
+
+ return (
+ <div
+ className="calendar-interface"
+ style={{
+ background: SettingsManager.userBackgroundColor,
+ color: StrCast(Doc.UserDoc().userColor),
+ }}>
+ <p className="selected-doc-title" style={{ color: SettingsManager.userColor }}>
+ <b>{this.focusOn(docs.length < 2 ? StrCast(targetDoc?.title, 'this document') : '-multiple-')}</b>
+ </p>
+ <div className="creation-type-container">
+ <div className={`calendar-creation ${this.creationType === 'new-calendar' ? 'calendar-creation-selected' : ''}`} onClick={e => this.setInterationType('new-calendar')}>
+ Add to New Calendar
+ </div>
+ <div className={`calendar-creation ${this.creationType === 'existing-calendar' ? 'calendar-creation-selected' : ''}`} onClick={e => this.setInterationType('existing-calendar')}>
+ Add to Existing calendar
+ </div>
+ </div>
+ <div className="choose-calendar-container">
+ {this.creationType === 'new-calendar' ? (
+ <TextField
+ fullWidth
+ onChange={this.handleTextFieldChange}
+ placeholder="Enter calendar name..."
+ variant="filled"
+ style={{
+ backgroundColor: 'white',
+ color: 'black',
+ borderRadius: '5px',
+ }}
+ />
+ ) : (
+ <Select
+ className="existing-calendar-search"
+ placeholder="Search for existing calendar..."
+ isClearable
+ isSearchable
+ options={this.selectOptions}
+ value={this.selectedExistingCalendarOption}
+ onChange={this.handleSelectChange}
+ styles={{
+ control: () => ({
+ display: 'inline-flex',
+ width: '100%',
+ }),
+ indicatorSeparator: () => ({
+ display: 'inline-flex',
+ visibility: 'hidden',
+ }),
+ indicatorsContainer: () => ({
+ display: 'inline-flex',
+ textDecorationColor: 'black',
+ }),
+ valueContainer: () => ({
+ display: 'inline-flex',
+ fontStyle: StrCast(Doc.UserDoc().userColor),
+ color: StrCast(Doc.UserDoc().userColor),
+ width: '100%',
+ }),
+ }}></Select>
+ )}
+ </div>
+ <div className="date-range-picker-container">
+ <DateRangePicker value={this.selectedDateRange} onChange={v => this.setSelectedDateRange(v)} label="Date range" />
+ </div>
+ <div className="create-button-container">
+ <Button active={this.createButtonActive} onClick={() => {}} text="Add to Calendar" iconPlacement="right" icon={<FontAwesomeIcon icon={faPlus as IconLookup} />} />
+ </div>
+ </div>
+ );
+ }
+
+ render() {
+ return <MainViewModal contents={this.calendarInterface} isDisplayed={this.isOpen} interactive={true} dialogueBoxDisplayedOpacity={this.dialogueBoxOpacity} overlayDisplayedOpacity={this.overlayOpacity} closeOnExternalClick={this.close} />;
+ }
+}
diff --git a/src/client/views/DashboardView.tsx b/src/client/views/DashboardView.tsx
index 85cee83d4..523721b84 100644
--- a/src/client/views/DashboardView.tsx
+++ b/src/client/views/DashboardView.tsx
@@ -384,13 +384,42 @@ export class DashboardView extends ObservableReactComponent<{}> {
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: 'embed',
+ treeView_TruncateTitleWidth: 150,
+ ignoreClick: true,
+ contextMenuIcons: new List<string>(['plus']),
+ contextMenuLabels: new List<string>(['Create New Calendar']),
+ _lockedPosition: true,
+ layout_boxShadow: '0 0',
+ childDontRegisterViews: true,
+ dropAction: '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 = {
diff --git a/src/client/views/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx
index 66b72226c..03f9ed52f 100644
--- a/src/client/views/DocumentButtonBar.tsx
+++ b/src/client/views/DocumentButtonBar.tsx
@@ -1,4 +1,4 @@
-import { IconProp } from '@fortawesome/fontawesome-svg-core';
+import { IconLookup, IconProp } from '@fortawesome/fontawesome-svg-core';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Tooltip } from '@mui/material';
import { action, computed, makeObservable, observable, runInAction } from 'mobx';
@@ -8,6 +8,7 @@ import { emptyFunction, returnFalse, setupMoveUpEvents, simulateMouseClick } fro
import { Doc } from '../../fields/Doc';
import { Cast, DocCast } from '../../fields/Types';
import { DocUtils } from '../documents/Documents';
+import { CalendarManager } from '../util/CalendarManager';
import { DragManager } from '../util/DragManager';
import { IsFollowLinkScript } from '../util/LinkFollower';
import { SelectionManager } from '../util/SelectionManager';
@@ -22,9 +23,7 @@ import { DocumentLinksButton } from './nodes/DocumentLinksButton';
import { DocumentView, DocumentViewInternal, OpenWhere } from './nodes/DocumentView';
import { DashFieldView } from './nodes/formattedText/DashFieldView';
import { PinProps } from './nodes/trails';
-// import * as higflyout from '@hig/flyout';
-// export const { anchorPoints } = higflyout;
-// export const Flyout = higflyout.default;
+import { faCalendarDays } from '@fortawesome/free-solid-svg-icons';
const cloud: IconProp = 'cloud-upload-alt';
const fetch: IconProp = 'sync-alt';
@@ -315,6 +314,24 @@ export class DocumentButtonBar extends ObservableReactComponent<{ views: () => (
);
}
+ @computed
+ get calendarButton() {
+ const targetDoc = this.view0?.props.Document;
+ return !targetDoc ? null : (
+ <Tooltip title={<div className="dash-calendar-button">Open calendar menu</div>}>
+ <div
+ className="documentButtonBar-icon"
+ style={{ color: 'white' }}
+ onClick={e => {
+ console.log('hi: ', CalendarManager.Instance);
+ CalendarManager.Instance.open(this.view0, targetDoc);
+ }}>
+ <FontAwesomeIcon className="documentdecorations-icon" icon={faCalendarDays as IconLookup} />
+ </div>
+ </Tooltip>
+ );
+ }
+
@observable _isRecording = false;
_stopFunc: () => void = emptyFunction;
@computed
@@ -472,6 +489,7 @@ export class DocumentButtonBar extends ObservableReactComponent<{ views: () => (
{!SelectionManager.Views?.some(v => v.allLinks.length) ? null : <div className="documentButtonBar-button">{this.followLinkButton}</div>}
<div className="documentButtonBar-button">{this.pinButton}</div>
<div className="documentButtonBar-button">{this.recordButton}</div>
+ <div className="documentButtonBar-button">{this.calendarButton}</div>
{!Doc.UserDoc()['documentLinksButton-fullMenu'] ? null : <div className="documentButtonBar-button">{this.shareButton}</div>}
<div className="documentButtonBar-button">{this.menuButton}</div>
</div>
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 655e34592..c71c72257 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -73,6 +73,7 @@ import { GPTPopup } from './pdf/GPTPopup/GPTPopup';
import { TopBar } from './topbar/TopBar';
const { default: { LEFT_MENU_WIDTH, TOPBAR_HEIGHT } } = require('./global/globalCssVariables.module.scss'); // prettier-ignore
import { DirectionsAnchorMenu } from './nodes/MapBox/DirectionsAnchorMenu';
+import { CalendarManager } from '../util/CalendarManager';
const _global = (window /* browser */ || global) /* node */ as any;
@observer
@@ -1023,6 +1024,7 @@ export class MainView extends ObservableReactComponent<{}> {
{this.inkResources}
<DictationOverlay />
<SharingManager />
+ <CalendarManager />
<ServerStats />
<RTFMarkup />
<SettingsManager />
diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx
index 97d5cfc70..a9474fe93 100644
--- a/src/client/views/collections/CollectionDockingView.tsx
+++ b/src/client/views/collections/CollectionDockingView.tsx
@@ -467,6 +467,7 @@ export class CollectionDockingView extends CollectionSubView() {
});
const copy = Docs.Create.DockDocument(newtabs, json, { title: incrementTitleCopy(StrCast(doc.title)) });
DashboardView.SetupDashboardTrails(copy);
+ DashboardView.SetupDashboardCalendars(copy); // Zaul TODO: needed?
return DashboardView.openDashboard(copy);
}
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 1f4688729..311890ee3 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -1434,7 +1434,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
return anchor;
};
- infoUI = () => (this.Document.annotationOn ? null : <CollectionFreeFormInfoUI Document={this.Document} Freeform={this} />);
+ infoUI = () => (this.Document.annotationOn ? null : null); // <CollectionFreeFormInfoUI Document={this.Document} Freeform={this} />);
componentDidMount() {
this._props.setContentView?.(this);
diff --git a/src/client/views/nodes/MapBox/MapBox.scss b/src/client/views/nodes/MapBox/MapBox.scss
index 434e02b27..0987f8795 100644
--- a/src/client/views/nodes/MapBox/MapBox.scss
+++ b/src/client/views/nodes/MapBox/MapBox.scss
@@ -4,6 +4,7 @@
height: 100%;
overflow: hidden;
display: flex;
+ position: absolute;
.mapBox-infoWindow {
background-color: white;
@@ -27,7 +28,7 @@
// }
}
- .mapbox-settings-panel{
+ .mapbox-settings-panel {
z-index: 900;
padding: 10px 20px;
display: flex;
@@ -41,7 +42,7 @@
border-top-left-radius: 5px;
border-bottom-left-radius: 5px;
- .mapbox-style-select{
+ .mapbox-style-select {
display: flex;
flex-direction: column;
align-items: flex-start;
@@ -49,14 +50,13 @@
gap: 4px;
}
- .mapbox-terrain-selection{
+ .mapbox-terrain-selection {
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-start;
gap: 4px;
}
-
}
.mapbox-geocoding-search-results {
@@ -75,11 +75,10 @@
.search-result-container {
width: 100%;
padding: 10px;
- &:hover{
+ &:hover {
background-color: lighten(rgb(187, 187, 187), 10%);
}
}
-
}
.animation-panel {
@@ -88,12 +87,11 @@
flex-direction: column;
justify-content: flex-start;
align-items: flex-start;
- position: absolute;
background-color: rgb(187, 187, 187);
padding: 10px;
border-top-right-radius: 5px;
border-bottom-right-radius: 5px;
- width: 100%;
+ position: absolute;
#route-to-animate-title {
font-size: 1.25em;
@@ -106,7 +104,7 @@
align-items: center;
gap: 7px;
- .animation-suboptions{
+ .animation-suboptions {
display: flex;
justify-content: flex-start;
flex-wrap: wrap;
@@ -114,25 +112,23 @@
gap: 7px;
width: 100%;
- .first-person-label{
+ .first-person-label {
width: '130px' !important;
}
- label{
+ label {
margin-bottom: 0;
}
-
- .speed-label{
+
+ .speed-label {
margin-right: 5px;
}
- #divider{
+ #divider {
margin-left: 10px;
margin-right: 10px;
}
}
-
-
}
}
@@ -149,10 +145,8 @@
bottom: 5px;
left: 5px;
padding: 3px;
-
}
-
.mapBox-topbar {
display: flex;
flex-direction: row;
@@ -236,4 +230,3 @@
display: block;
}
}
-
diff --git a/src/client/views/nodes/MapBox/MapBox.tsx b/src/client/views/nodes/MapBox/MapBox.tsx
index c25d10a0d..efe0a3620 100644
--- a/src/client/views/nodes/MapBox/MapBox.tsx
+++ b/src/client/views/nodes/MapBox/MapBox.tsx
@@ -1,72 +1,45 @@
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import BingMapsReact from 'bingmaps-react';
-// import 'mapbox-gl/dist/mapbox-gl.css';
-
-import { Button, EditableText, IconButton, Size, Type } from 'browndash-components';
-import { action, computed, IReactionDisposer, observable, ObservableMap, reaction, runInAction, flow, toJS, autorun, makeObservable } from 'mobx';
+import { IconButton, Size, Type } from 'browndash-components';
+import { IReactionDisposer, ObservableMap, action, autorun, computed, makeObservable, observable, reaction, runInAction } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
+import { Utils, emptyFunction, setupMoveUpEvents } from '../../../../Utils';
import { Doc, DocListCast, Field, LinkedTo, Opt } from '../../../../fields/Doc';
import { DocCss, Highlight } from '../../../../fields/DocSymbols';
-import { Id } from '../../../../fields/FieldSymbols';
-import { Cast, DocCast, NumCast, StrCast } from '../../../../fields/Types';
-import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnOne, setupMoveUpEvents, Utils } from '../../../../Utils';
-import { Docs, DocUtils } from '../../../documents/Documents';
+import { DocCast, NumCast, StrCast } from '../../../../fields/Types';
import { DocumentType } from '../../../documents/DocumentTypes';
+import { DocUtils, Docs } from '../../../documents/Documents';
import { DocumentManager } from '../../../util/DocumentManager';
import { DragManager } from '../../../util/DragManager';
import { LinkManager } from '../../../util/LinkManager';
import { SnappingManager } from '../../../util/SnappingManager';
-import { Transform } from '../../../util/Transform';
-import { undoable, UndoManager } from '../../../util/UndoManager';
-import { MarqueeOptionsMenu } from '../../collections/collectionFreeForm';
+import { UndoManager, undoable } from '../../../util/UndoManager';
import { ViewBoxAnnotatableComponent, ViewBoxAnnotatableProps } from '../../DocComponent';
-import { Colors } from '../../global/globalEnums';
import { SidebarAnnos } from '../../SidebarAnnos';
+import { MarqueeOptionsMenu } from '../../collections/collectionFreeForm';
+import { Colors } from '../../global/globalEnums';
import { DocumentView } from '../DocumentView';
import { FieldView, FieldViewProps } from '../FieldView';
import { FormattedTextBox } from '../formattedText/FormattedTextBox';
import { PinProps, PresBox } from '../trails';
import { MapAnchorMenu } from './MapAnchorMenu';
-import {
- Map as MapboxMap,
- MapRef,
- Marker,
- ControlPosition,
- FullscreenControl,
- MapProvider,
- MarkerProps,
- NavigationControl,
- ScaleControl,
- ViewState,
- ViewStateChangeEvent,
- useControl,
- GeolocateControl,
- Popup,
- MapEvent,
- Source,
- Layer,
-} from 'react-map-gl';
+import { ControlPosition, Layer, MapProvider, MapRef, Map as MapboxMap, Marker, MarkerProps, Source, ViewState, ViewStateChangeEvent } from 'react-map-gl';
import MapboxGeocoder, { GeocoderOptions } from '@mapbox/mapbox-gl-geocoder!';
import './MapBox.scss';
-import { NumberLiteralType } from 'typescript';
// import { GeocoderControl } from './GeocoderControl';
-import mapboxgl, { LngLat, LngLatBoundsLike, LngLatLike, MapLayerMouseEvent, MercatorCoordinate } from 'mapbox-gl!';
-import { Feature, FeatureCollection, GeoJsonProperties, Geometry, LineString, MultiLineString, Position } from 'geojson';
-import { MarkerEvent } from 'react-map-gl/dist/esm/types';
-import { MapboxApiUtility, TransportationType } from './MapboxApiUtility';
-import { Autocomplete, Checkbox, FormControlLabel, TextField } from '@mui/material';
-import { List } from '../../../../fields/List';
-import { listSpec } from '../../../../fields/Schema';
-import { IconLookup, faCircleXmark, faFileExport, faGear, faMinus, faPause, faPlay, faPlus, faRotate } from '@fortawesome/free-solid-svg-icons';
-import { MarkerIcons } from './MarkerIcons';
-import { SettingsManager } from '../../../util/SettingsManager';
+import { IconLookup, faCircleXmark, faGear, faPause, faPlay, faRotate } from '@fortawesome/free-solid-svg-icons';
+import { Checkbox, FormControlLabel, TextField } from '@mui/material';
import * as turf from '@turf/turf';
import * as d3 from 'd3';
-import { AnimationSpeed, AnimationStatus, AnimationUtility } from './AnimationUtility';
-import { fastSpeedIcon, mediumSpeedIcon, slowSpeedIcon } from './AnimationSpeedIcons';
+import { Feature, FeatureCollection, GeoJsonProperties, Geometry, LineString, Position } from 'geojson';
+import mapboxgl, { LngLat, LngLatBoundsLike, MapLayerMouseEvent } from 'mapbox-gl!';
import { CirclePicker, ColorResult } from 'react-color';
+import { MarkerEvent } from 'react-map-gl/dist/esm/types';
+import { fastSpeedIcon, mediumSpeedIcon, slowSpeedIcon } from './AnimationSpeedIcons';
+import { AnimationSpeed, AnimationStatus, AnimationUtility } from './AnimationUtility';
+import { MapboxApiUtility, TransportationType } from './MapboxApiUtility';
+import { MarkerIcons } from './MarkerIcons';
// amongus
/**
@@ -891,15 +864,16 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
}, 'createpin');
@action
- createMapRoute = undoable((coordinates: Position[], origin: string, destination: any, createPinForDestination: boolean) => {
- const mapRoute = Docs.Create.MapRouteDocument(false, [], { title: `${origin} --> ${destination.place_name}`, routeCoordinates: JSON.stringify(coordinates) });
- this.addDocument(mapRoute, this.annotationKey);
- if (createPinForDestination) {
- this.createPushpin(destination.center[1], destination.center[0], destination.place_name);
+ createMapRoute = undoable((coordinates: Position[], originName: string, destination: any, createPinForDestination: boolean) => {
+ if (originName !== destination.place_name) {
+ const mapRoute = Docs.Create.MapRouteDocument(false, [], { title: `${originName} --> ${destination.place_name}`, routeCoordinates: JSON.stringify(coordinates) });
+ this.addDocument(mapRoute, this.annotationKey);
+ if (createPinForDestination) {
+ this.createPushpin(destination.center[1], destination.center[0], destination.place_name);
+ }
+ return mapRoute;
}
- return mapRoute;
-
- // mapMarker.infoWindowOpen = true;
+ // TODO: Display error that can't create route to same location
}, 'createmaproute');
searchbarKeyDown = (e: any) => e.key === 'Enter' && this.bingSearch();
@@ -958,6 +932,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
@action
handleMapClick = (e: MapLayerMouseEvent) => {
+ this.featuresFromGeocodeResults = [];
if (this._mapRef.current) {
const features = this._mapRef.current.queryRenderedFeatures(e.point, {
layers: ['map-routes-layer'],
@@ -1146,24 +1121,26 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
if (routeDoc) {
MapAnchorMenu.Instance.fadeOut(true);
document.removeEventListener('pointerdown', this.tryHideMapAnchorMenu, true);
+ this.featuresFromGeocodeResults = [];
this.routeToAnimate = routeDoc;
}
};
- @observable
- mapboxMapViewState: ViewState = {
- zoom: this.dataDoc.map_zoom ? NumCast(this.dataDoc.map_zoom) : 8,
- longitude: this.dataDoc.longitude ? NumCast(this.dataDoc.longitude) : -71.4128,
- latitude: this.dataDoc.latitude ? NumCast(this.dataDoc.latitude) : 41.824,
- pitch: this.dataDoc.map_pitch ? NumCast(this.dataDoc.map_pitch) : 0,
- bearing: this.dataDoc.map_bearing ? NumCast(this.dataDoc.map_bearing) : 0,
- padding: {
- top: 0,
- bottom: 0,
- left: 0,
- right: 0,
- },
- };
+ @computed get mapboxMapViewState(): ViewState {
+ return {
+ zoom: NumCast(this.dataDoc.map_zoom, 8),
+ longitude: NumCast(this.dataDoc.longitude, -71.4128),
+ latitude: NumCast(this.dataDoc.latitude, 41.824),
+ pitch: NumCast(this.dataDoc.map_pitch),
+ bearing: NumCast(this.dataDoc.map_bearing),
+ padding: {
+ top: 0,
+ bottom: 0,
+ left: 0,
+ right: 0,
+ },
+ };
+ }
@computed
get preAnimationViewState() {
@@ -1390,9 +1367,6 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
this.routeToAnimate = undefined;
this.animationUtility = null;
}
- if (this.preAnimationViewState) {
- this.mapboxMapViewState = this.preAnimationViewState;
- }
};
@action
@@ -1488,10 +1462,6 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
const fixedBearing = Math.max(0, Math.min(360, bearing));
this._mapRef.current.setBearing(fixedBearing);
this.dataDoc.map_bearing = fixedBearing;
- this.mapboxMapViewState = {
- ...this.mapboxMapViewState,
- bearing: fixedBearing,
- };
}
};
@@ -1502,10 +1472,6 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
const fixedPitch = Math.max(0, Math.min(85, pitch));
this._mapRef.current.setPitch(fixedPitch);
this.dataDoc.map_pitch = fixedPitch;
- this.mapboxMapViewState = {
- ...this.mapboxMapViewState,
- pitch: fixedPitch,
- };
}
};
@@ -1516,10 +1482,6 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
const fixedZoom = Math.max(0, Math.min(16, zoom));
this._mapRef.current.setZoom(fixedZoom);
this.dataDoc.map_zoom = fixedZoom;
- this.mapboxMapViewState = {
- ...this.mapboxMapViewState,
- zoom: fixedZoom,
- };
}
};
@@ -1529,23 +1491,18 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
let newZoom: number;
if (increment) {
console.log('inc');
- newZoom = this.mapboxMapViewState.zoom + 1;
+ newZoom = Math.min(16, this.mapboxMapViewState.zoom + 1);
} else {
console.log('dec');
- newZoom = this.mapboxMapViewState.zoom - 1;
+ newZoom = Math.max(0, this.mapboxMapViewState.zoom - 1);
}
this._mapRef.current.setZoom(newZoom);
this.dataDoc.map_zoom = newZoom;
- this.mapboxMapViewState = {
- ...this.mapboxMapViewState,
- zoom: increment ? Math.min(16, newZoom) : Math.max(0, newZoom),
- };
}
};
@action
onMapMove = (e: ViewStateChangeEvent) => {
- this.mapboxMapViewState = e.viewState;
this.dataDoc.longitude = e.viewState.longitude;
this.dataDoc.latitude = e.viewState.latitude;
};
@@ -1581,8 +1538,8 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
}), MapBox._rerenderDelay);
return null;
}
-
const scale = this._props.NativeDimScaling?.() || 1;
+ const parscale = scale === 1 ? 1 : this._props.ScreenToLocalTransform().Scale ?? 1;
const renderAnnotations = (childFilters?: () => string[]) => null;
return (
@@ -1593,7 +1550,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
onPointerDown={async e => {
e.button === 0 && !e.ctrlKey && e.stopPropagation();
}}
- style={{ width: `calc(100% - ${this.sidebarWidthPercent})`, pointerEvents: this.pointerEvents() }}>
+ style={{ transformOrigin: 'top left', transform: `scale(${scale})`, width: `calc(100% - ${this.sidebarWidthPercent})`, pointerEvents: this.pointerEvents() }}>
<div style={{ mixBlendMode: 'multiply' }}>{renderAnnotations(this.transparentFilter)}</div>
{renderAnnotations(this.opaqueFilter)}
{SnappingManager.IsDragging ? null : renderAnnotations()}
@@ -1639,7 +1596,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
</div>
)}
{this.routeToAnimate && (
- <div className="animation-panel">
+ <div className="animation-panel" style={{ width: this.sidebarWidth() === 0 ? '100%' : `calc(100% - ${this.sidebarWidth()}px)` }}>
<div id="route-to-animate-title">{StrCast(this.routeToAnimate.title)}</div>
<div className="route-animation-options">{this.getRouteAnimationOptions()}</div>
</div>
@@ -1683,15 +1640,15 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
<MapboxMap
ref={this._mapRef}
mapboxAccessToken={MAPBOX_ACCESS_TOKEN}
- id="mapbox-map"
- viewState={{ ...this.mapboxMapViewState, width: NumCast(this.layoutDoc._width) / scale, height: NumCast(this.layoutDoc._height) / scale }}
+ viewState={this.isAnimating ? undefined : { ...this.mapboxMapViewState, width: NumCast(this.layoutDoc._width), height: NumCast(this.layoutDoc._height) }}
mapStyle={this.dataDoc.map_style ? StrCast(this.dataDoc.map_style) : 'mapbox://styles/mapbox/streets-v11'}
style={{
- transformOrigin: 'center',
- transform: `scale(${scale < 1 ? 1 : scale})`,
+ position: 'absolute',
+ top: 0,
+ left: 0,
zIndex: '0',
- width: '100%',
- height: '100%',
+ width: NumCast(this.layoutDoc._width) * parscale,
+ height: NumCast(this.layoutDoc._height) * parscale,
}}
initialViewState={this.isAnimating ? undefined : this.mapboxMapViewState}
onMove={this.onMapMove}
diff --git a/src/client/views/nodes/MapBox/MapboxApiUtility.ts b/src/client/views/nodes/MapBox/MapboxApiUtility.ts
index 011b6f72a..592330ac2 100644
--- a/src/client/views/nodes/MapBox/MapboxApiUtility.ts
+++ b/src/client/views/nodes/MapBox/MapboxApiUtility.ts
@@ -34,48 +34,39 @@ export class MapboxApiUtility {
static getDirections = async (origin: number[], destination: number[]): Promise<Record<TransportationType, any> | undefined> => {
try {
- const drivingQuery = await fetch(
- `${MAPBOX_DIRECTIONS_BASE_URL}/driving/${origin[0]},${origin[1]};${destination[0]},${destination[1]}?steps=true&geometries=geojson&access_token=${MAPBOX_ACCESS_TOKEN}`);
- const cyclingQuery = await fetch(
- `${MAPBOX_DIRECTIONS_BASE_URL}/cycling/${origin[0]},${origin[1]};${destination[0]},${destination[1]}?steps=true&geometries=geojson&access_token=${MAPBOX_ACCESS_TOKEN}`);
+ const directionsPromises: Promise<any>[] = [];
+ const transportationTypes: TransportationType[] = ['driving', 'cycling', 'walking'];
- const walkingQuery = await fetch(
- `${MAPBOX_DIRECTIONS_BASE_URL}/walking/${origin[0]},${origin[1]};${destination[0]},${destination[1]}?steps=true&geometries=geojson&access_token=${MAPBOX_ACCESS_TOKEN}`);
+ transportationTypes.forEach((type) => {
+ directionsPromises.push(
+ fetch(
+ `${MAPBOX_DIRECTIONS_BASE_URL}/${type}/${origin[0]},${origin[1]};${destination[0]},${destination[1]}?steps=true&geometries=geojson&access_token=${MAPBOX_ACCESS_TOKEN}`
+ ).then((response) => response.json())
+ );
+ });
- const drivingJson = await drivingQuery.json();
- const cyclingJson = await cyclingQuery.json();
- const walkingJson = await walkingQuery.json();
-
- console.log("Driving: ", drivingJson);
- console.log("Cycling: ", cyclingJson);
- console.log("Waling: ", walkingJson);
-
- const routeMap = {
- 'driving': drivingJson.routes[0],
- 'cycling': cyclingJson.routes[0],
- 'walking': walkingJson.routes[0]
- }
+ const results = await Promise.all(directionsPromises);
const routeInfoMap: Record<TransportationType, any> = {
'driving': {},
'cycling': {},
'walking': {},
- };
-
- Object.entries(routeMap).forEach(([key, routeData]) => {
- const transportationTypeKey = key as TransportationType;
- const geometry = routeData.geometry;
- const coordinates = geometry.coordinates;
-
- console.log(coordinates);
-
- routeInfoMap[transportationTypeKey] = {
+ };
+
+ transportationTypes.forEach((type, index) => {
+ const routeData = results[index].routes[0];
+ if (routeData) {
+ const geometry = routeData.geometry;
+ const coordinates = geometry.coordinates;
+
+ routeInfoMap[type] = {
duration: this.secondsToMinutesHours(routeData.duration),
distance: this.metersToMiles(routeData.distance),
- coordinates: coordinates
+ coordinates: coordinates,
+ };
}
- })
+ });
return routeInfoMap;
@@ -102,4 +93,47 @@ export class MapboxApiUtility {
return `${parseFloat((meters/1609.34).toFixed(2))} mi`;
}
-} \ No newline at end of file
+}
+
+// const drivingQuery = await fetch(
+// `${MAPBOX_DIRECTIONS_BASE_URL}/driving/${origin[0]},${origin[1]};${destination[0]},${destination[1]}?steps=true&geometries=geojson&access_token=${MAPBOX_ACCESS_TOKEN}`);
+
+// const cyclingQuery = await fetch(
+// `${MAPBOX_DIRECTIONS_BASE_URL}/cycling/${origin[0]},${origin[1]};${destination[0]},${destination[1]}?steps=true&geometries=geojson&access_token=${MAPBOX_ACCESS_TOKEN}`);
+
+// const walkingQuery = await fetch(
+// `${MAPBOX_DIRECTIONS_BASE_URL}/walking/${origin[0]},${origin[1]};${destination[0]},${destination[1]}?steps=true&geometries=geojson&access_token=${MAPBOX_ACCESS_TOKEN}`);
+
+// const drivingJson = await drivingQuery.json();
+// const cyclingJson = await cyclingQuery.json();
+// const walkingJson = await walkingQuery.json();
+
+// console.log("Driving: ", drivingJson);
+// console.log("Cycling: ", cyclingJson);
+// console.log("Waling: ", walkingJson);
+
+// const routeMap = {
+// 'driving': drivingJson.routes[0],
+// 'cycling': cyclingJson.routes[0],
+// 'walking': walkingJson.routes[0]
+// }
+
+// const routeInfoMap: Record<TransportationType, any> = {
+// 'driving': {},
+// 'cycling': {},
+// 'walking': {},
+// };
+
+// Object.entries(routeMap).forEach(([key, routeData]) => {
+// const transportationTypeKey = key as TransportationType;
+// const geometry = routeData.geometry;
+// const coordinates = geometry.coordinates;
+
+// console.log(coordinates);
+
+// routeInfoMap[transportationTypeKey] = {
+// duration: this.secondsToMinutesHours(routeData.duration),
+// distance: this.metersToMiles(routeData.distance),
+// coordinates: coordinates
+// }
+// }) \ No newline at end of file
diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts
index 5449c8dea..8903a9f97 100644
--- a/src/fields/Doc.ts
+++ b/src/fields/Doc.ts
@@ -147,6 +147,7 @@ export class Doc extends RefField {
public static get MyTopBarBtns() { return DocCast(Doc.UserDoc().myTopBarBtns); } // prettier-ignore
public static get MyRecentlyClosed() { return DocCast(Doc.UserDoc().myRecentlyClosed); } // prettier-ignore
public static get MyTrails() { return DocCast(Doc.ActiveDashboard?.myTrails); } // prettier-ignore
+ public static get MyCalendars() { return DocCast(Doc.ActiveDashboard?.myCalendars); } // prettier-ignore
public static get MyOverlayDocs() { return DocListCast(Doc.ActiveDashboard?.myOverlayDocs ?? DocCast(Doc.UserDoc().myOverlayDocs)?.data); } // prettier-ignore
public static get MyPublishedDocs() { return DocListCast(Doc.ActiveDashboard?.myPublishedDocs ?? DocCast(Doc.UserDoc().myPublishedDocs)?.data); } // prettier-ignore
public static get MyDashboards() { return DocCast(Doc.UserDoc().myDashboards); } // prettier-ignore