diff options
Diffstat (limited to 'src/client/util/CalendarManager.tsx')
-rw-r--r-- | src/client/util/CalendarManager.tsx | 312 |
1 files changed, 312 insertions, 0 deletions
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} />; + } +} |