diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/client/documents/DocumentTypes.ts | 3 | ||||
-rw-r--r-- | src/client/documents/Documents.ts | 70 | ||||
-rw-r--r-- | src/client/util/CurrentUserUtils.ts | 4 | ||||
-rw-r--r-- | src/client/views/Main.tsx | 8 | ||||
-rw-r--r-- | src/client/views/MainView.tsx | 3 | ||||
-rw-r--r-- | src/client/views/nodes/TaskBox.scss | 72 | ||||
-rw-r--r-- | src/client/views/nodes/TaskBox.tsx | 345 | ||||
-rw-r--r-- | src/client/views/nodes/calendarBox/CalendarBox.scss | 19 | ||||
-rw-r--r-- | src/client/views/nodes/calendarBox/CalendarBox.tsx | 77 | ||||
-rw-r--r-- | src/client/views/nodes/formattedText/DailyJournal.tsx | 225 |
10 files changed, 757 insertions, 69 deletions
diff --git a/src/client/documents/DocumentTypes.ts b/src/client/documents/DocumentTypes.ts index cef44e999..a73d8ba59 100644 --- a/src/client/documents/DocumentTypes.ts +++ b/src/client/documents/DocumentTypes.ts @@ -44,8 +44,9 @@ export enum DocumentType { SCRIPTDB = 'scriptdb', // database of scripts GROUPDB = 'groupdb', // database of groups + JOURNAL = 'journal', // AARAV ADD JOURNAL + TASK = 'task', // AARAV ADD TASK SCRAPBOOK = 'scrapbook', - JOURNAL = 'journal', // AARAV ADD } export enum CollectionViewType { // general collections diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index 9f9058cc5..0b6385466 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -11,7 +11,7 @@ import { List } from '../../fields/List'; import { RichTextField } from '../../fields/RichTextField'; import { SchemaHeaderField } from '../../fields/SchemaHeaderField'; import { ComputedField, ScriptField } from '../../fields/ScriptField'; -import { ScriptCast, StrCast } from '../../fields/Types'; +import { DocCast, ScriptCast, StrCast } from '../../fields/Types'; import { AudioField, CsvField, ImageField, PdfField, VideoField, WebField } from '../../fields/URLField'; import { SharingPermissions } from '../../fields/util'; import { PointData } from '../../pen-gestures/GestureTypes'; @@ -524,6 +524,12 @@ export class DocumentOptions { ai_prompt?: STRt = new StrInfo('input prompt to GAI engine'); ai_generatedDocs?: List<Doc>; // list of documents generated by GAI engine + // TASK MANAGER + $startTime?: DateInfo | DateField = new DateInfo('start date and time', /*filterable*/ false); + $endTime?: DateInfo | DateField = new DateInfo('end date and time', /*filterable*/ false); + $allDay?: BoolInfo | boolean = new BoolInfo('whether task is all-day or not', /*filterable*/ false); + $completed?: BoolInfo | boolean = new BoolInfo('whether the task is completed', /*filterable*/ false); + /** * JSON‐stringified slot configuration for ScrapbookBox */ @@ -582,6 +588,30 @@ export namespace Docs { options: { acl: '' }, }, ], + + // AARAV ADD // + [ + DocumentType.JOURNAL, + { + layout: { view: EmptyBox, dataField: 'text' }, + options: { + title: 'Daily Journal', + acl_Guest: SharingPermissions.View, + }, + }, + ], + + [ + DocumentType.TASK, + { + layout: { view: EmptyBox, dataField: 'text' }, + options: { + title: 'Task', + acl_Guest: SharingPermissions.View, + }, + }, + ], + // AARAV ADD // ]); const suffix = 'Proto'; @@ -936,31 +966,6 @@ export namespace Docs { // AARAV ADD // export function DailyJournalDocument(text: string | RichTextField, options: DocumentOptions = {}, fieldKey: string = 'text') { - // const getFormattedDate = () => { - // const date = new Date().toLocaleDateString(undefined, { - // weekday: 'long', - // year: 'numeric', - // month: 'long', - // day: 'numeric', - // }); - // return date; - // }; - - // const getDailyText = () => { - // const placeholderText = 'Start writing here...'; - // const dateText = `${getFormattedDate()}`; - - // return RichTextField.textToRtfFormat( - // [ - // { text: 'Journal Entry:', styles: { bold: true, color: 'black', fontSize: 20 } }, - // { text: dateText, styles: { italic: true, color: 'gray', fontSize: 15 } }, - // { text: placeholderText, styles: { fontSize: 14, color: 'gray' } }, - // ], - // undefined, - // placeholderText.length - // ); - // }; - return InstanceFromProto( Prototypes.get(DocumentType.JOURNAL), '', @@ -973,7 +978,18 @@ export namespace Docs { ); } - // AARAV ADD // + export function TaskDocument(text = '', options: DocumentOptions = {}, fieldKey = 'text') { + return InstanceFromProto( + Prototypes.get(DocumentType.TASK), + '', + { + title: '', + ...options, + }, + undefined, + fieldKey + ); + } export function LinkDocument(source: Doc, target: Doc, options: DocumentOptions = {}, id?: string) { const linkDoc = InstanceFromProto( diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts index 28b19d55e..a25c491d9 100644 --- a/src/client/util/CurrentUserUtils.ts +++ b/src/client/util/CurrentUserUtils.ts @@ -407,7 +407,8 @@ pie title Minerals in my tap water {key: "DataViz", creator: opts => Docs.Create.DataVizDocument("", opts), opts: { _width: 300, _height: 300, }}, // AARAV ADD // {key: "DailyJournal",creator:opts => Docs.Create.DailyJournalDocument("", opts),opts: { _width: 300, _height: 300, }}, - {key: "Scrapbook",creator:opts => Docs.Create.ScrapbookDocument([], opts),opts:{ _width: 300, _height: 300}}, + {key: "Task", creator: opts => Docs.Create.TaskDocument("", opts), opts: { _width: 300, _height: 300, _layout_autoHeight: true, title: "Task", }}, + {key: "Scrapbook", creator: opts => Docs.Create.ScrapbookDocument([], opts), opts: { _width: 300, _height: 300}}, //{key: "Scrapbook",creator:opts => Docs.Create.ScrapbookDocument([], opts),opts:{ _width: 300, _height: 300}}, {key: "Chat", creator: Docs.Create.ChatDocument, opts: { _width: 500, _height: 500, _layout_fitWidth: true, }}, {key: "MetaNote", creator: metaNoteTemplate, opts: { _width: 300, _height: 120, _header_pointerEvents: "all", _header_height: 50, _header_fontSize: 9,_layout_autoHeightMargins: 50, _layout_autoHeight: true, treeView_HideUnrendered: true}}, @@ -453,6 +454,7 @@ pie title Minerals in my tap water { toolTip: "Tap or drag to create a data viz node", title: "DataViz", icon: "chart-bar", dragFactory: doc.emptyDataViz as Doc, clickFactory: DocCast(doc.emptyDataViz)}, { toolTip: "Tap or drag to create a scrapbook template", title: "Scrapbook", icon: "palette", dragFactory: doc.emptyScrapbook as Doc,clickFactory:DocCast(doc.emptyScrapbook), }, { toolTip: "Tap or drag to create a journal entry", title: "Journal", icon: "book", dragFactory:doc.emptyDailyJournal as Doc,clickFactory: DocCast(doc.emptyDataJournal), }, + { toolTip: "Tap or drag to create a task", title: "Task", icon: "check-square", dragFactory: doc.emptyTask as Doc, clickFactory: DocCast(doc.emptyTask), }, { toolTip: "Tap or drag to create a bullet slide", title: "PPT Slide", icon:"person-chalkboard",dragFactory: doc.emptySlide as Doc, clickFactory: DocCast(doc.emptySlide), openFactoryLocation: OpenWhere.overlay, funcs: { hidden: "IsNoviceMode()"}}, { toolTip: "Tap or drag to create a view slide", title: "View Slide", icon: "address-card", dragFactory: doc.emptyViewSlide as Doc, clickFactory: DocCast(doc.emptyViewSlide), openFactoryLocation: OpenWhere.overlay, funcs: { hidden: "IsNoviceMode()"}}, { toolTip: "Tap or drag to create a data note", title: "MetaNote", icon: "window-maximize", dragFactory: doc.emptyMetaNote as Doc, clickFactory: DocCast(doc.emptyMetaNote), openFactoryAsDelegate: true, funcs: { hidden: "IsNoviceMode()"} }, diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx index 67e8078ba..33e930abf 100644 --- a/src/client/views/Main.tsx +++ b/src/client/views/Main.tsx @@ -31,6 +31,8 @@ import './global/globalScripts'; import { AudioBox } from './nodes/AudioBox'; import { ComparisonBox } from './nodes/ComparisonBox'; import { DataVizBox } from './nodes/DataVizBox/DataVizBox'; +import { TemplateField } from './nodes/DataVizBox/DocCreatorMenu/TemplateFieldTypes/TemplateField'; +import { TemplateFieldUtils } from './nodes/DataVizBox/DocCreatorMenu/TemplateFieldTypes/TemplateFieldUtils'; import { DiagramBox } from './nodes/DiagramBox'; import { DocumentContentsView, HTMLtag } from './nodes/DocumentContentsView'; import { EquationBox } from './nodes/EquationBox'; @@ -48,6 +50,7 @@ import { PDFBox } from './nodes/PDFBox'; import { RecordingBox } from './nodes/RecordingBox'; import { ScreenshotBox } from './nodes/ScreenshotBox'; import { ScriptingBox } from './nodes/ScriptingBox'; +import { TaskBox } from './nodes/TaskBox'; import { VideoBox } from './nodes/VideoBox'; import { WebBox } from './nodes/WebBox'; import { CalendarBox } from './nodes/calendarBox/CalendarBox'; @@ -61,13 +64,11 @@ import { FootnoteView } from './nodes/formattedText/FootnoteView'; import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox'; import { SummaryView } from './nodes/formattedText/SummaryView'; import { ImportElementBox } from './nodes/importBox/ImportElementBox'; +import { ScrapbookBox } from './nodes/scrapbook/ScrapbookBox'; import { PresBox, PresSlideBox } from './nodes/trails'; import { FaceRecognitionHandler } from './search/FaceRecognitionHandler'; import { SearchBox } from './search/SearchBox'; import { StickerPalette } from './smartdraw/StickerPalette'; -import { TemplateField } from './nodes/DataVizBox/DocCreatorMenu/TemplateFieldTypes/TemplateField'; -import { TemplateFieldUtils } from './nodes/DataVizBox/DocCreatorMenu/TemplateFieldTypes/TemplateFieldUtils'; -import { ScrapbookBox } from './nodes/scrapbook/ScrapbookBox'; dotenv.config(); @@ -123,6 +124,7 @@ FieldLoader.ServerLoadStatus = { requested: 0, retrieved: 0, message: 'cache' }; StickerPalette: StickerPalette, FormattedTextBox, DailyJournal, // AARAV + TaskBox, // AARAV ImageBox, FontIconBox, LabelBox, diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index c49b7e6de..686f8ed88 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -730,7 +730,8 @@ export class MainView extends ObservableReactComponent<object> { style={{ width: `calc(100% - ${this._leftMenuFlyoutWidth + this.leftMenuWidth() + this.propertiesWidth()}px)`, minWidth: `calc(100% - ${this._leftMenuFlyoutWidth + this.leftMenuWidth() + this.propertiesWidth()}px)`, - transform: DocumentView.LightboxDoc() ? 'scale(0.0001)' : undefined, + opacity: DocumentView.LightboxDoc() ? 0 : undefined, + pointerEvents: DocumentView.LightboxDoc() ? 'none' : undefined, }}> {!this.mainContainer ? null : this.mainDocView} </div> diff --git a/src/client/views/nodes/TaskBox.scss b/src/client/views/nodes/TaskBox.scss new file mode 100644 index 000000000..0fcc2f955 --- /dev/null +++ b/src/client/views/nodes/TaskBox.scss @@ -0,0 +1,72 @@ +.task-manager-container { + display: flex; + flex-direction: column; + padding: 8px; + gap: 10px; + width: 100%; + height: 100%; + box-sizing: border-box; +} + +.task-manager-title { + width: 100%; + font-size: 1.25rem; + font-weight: 600; + padding: 6px 10px; + border: 1px solid #ccc; + border-radius: 6px; + box-sizing: border-box; +} + +.task-manager-description { + width: 100%; + font-size: 1rem; + padding: 8px 10px; + border: 1px solid #ccc; + border-radius: 6px; + min-height: 40px; + box-sizing: border-box; + vertical-align: top; + text-align: start; + resize: none; + line-height: 1.4; + resize: none; + flex-grow: 1 +} + +.task-manager-checkboxes { + display: flex; + align-items: center; + gap: 16px; +} + +.task-manager-allday, .task-manager-complete { + display: flex; + align-items: center; + gap: 6px; + font-size: 0.95rem; +} + +.task-manager-times { + display: flex; + flex-direction: column; + gap: 6px; + width: 100%; +} + +.task-manager-times label { + display: flex; + flex-direction: column; + font-size: 0.9rem; + font-weight: 500; + gap: 4px; +} + +input[type="datetime-local"] { + width: 100%; + font-size: 0.9rem; + padding: 6px 8px; + border: 1px solid #ccc; + border-radius: 6px; + box-sizing: border-box; +} diff --git a/src/client/views/nodes/TaskBox.tsx b/src/client/views/nodes/TaskBox.tsx new file mode 100644 index 000000000..ff1c70b90 --- /dev/null +++ b/src/client/views/nodes/TaskBox.tsx @@ -0,0 +1,345 @@ +import { action, observable, makeObservable, IReactionDisposer, reaction } from 'mobx'; +import { observer } from 'mobx-react'; +import * as React from 'react'; +import { Docs } from '../../documents/Documents'; +import { DocumentType } from '../../documents/DocumentTypes'; +import { FieldView } from './FieldView'; +import { DateField } from '../../../fields/DateField'; +import { Doc } from '../../../fields/Doc'; + +import './TaskBox.scss'; + +/** + * Props (reference to document) for Task Box + */ + +interface TaskBoxProps { + Document: Doc; +} + +/** + * TaskBox class for adding task information + completing tasks + */ +@observer +export class TaskBox extends React.Component<TaskBoxProps> { + + /** + * Method to reuturn the + * @param fieldStr + * @returns + */ + public static LayoutString(fieldStr: string) { + return FieldView.LayoutString(TaskBox, fieldStr); + } + + /** + * Method to update the task description + * @param e - event of changing the description box input + */ + + @action + updateText = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => { + this.props.Document.text = e.target.value; + }; + + /** + * Method to update the task title + * @param e - event of changing the title box input + */ + @action + updateTitle = (e: React.ChangeEvent<HTMLInputElement>) => { + this.props.Document.title = e.target.value; + }; + + /** + * Method to update the all day status + * @param e - event of changing the all day checkbox + */ + @action + updateAllDay = (e: React.ChangeEvent<HTMLInputElement>) => { + this.props.Document.$allDay = e.target.checked; + + if (e.target.checked) { + delete this.props.Document.$startTime; + delete this.props.Document.$endTime; + } + + this.setTaskDateRange(); + }; + + /** + * Method to update the task start time + * @param e - event of changing the start time input + */ + @action + updateStart = (e: React.ChangeEvent<HTMLInputElement>) => { + const newStart = new Date(e.target.value); + + this.props.Document.$startTime = new DateField(newStart); + + const endDate = this.props.Document.$endTime instanceof DateField ? this.props.Document.$endTime.date : undefined; + if (endDate && newStart > endDate) { + // Alert user + alert('Start time cannot be after end time. End time has been adjusted.'); + + // Fix end time + const adjustedEnd = new Date(newStart.getTime() + 60 * 60 * 1000); + this.props.Document.$endTime = new DateField(adjustedEnd); + } + + this.setTaskDateRange(); + }; + + /** + * Method to update the task end time + * @param e - event of changing the end time input + */ + @action + updateEnd = (e: React.ChangeEvent<HTMLInputElement>) => { + const newEnd = new Date(e.target.value); + + this.props.Document.$endTime = new DateField(newEnd); + + const startDate = this.props.Document.$startTime instanceof DateField ? this.props.Document.$startTime.date : undefined; + if (startDate && newEnd < startDate) { + // Alert user + alert('End time cannot be before start time. Start time has been adjusted.'); + + // Fix start time + const adjustedStart = new Date(newEnd.getTime() - 60 * 60 * 1000); + this.props.Document.$startTime = new DateField(adjustedStart); + } + + this.setTaskDateRange(); + }; + + /** + * Method to update the task date range + */ + @action + setTaskDateRange() { + const doc = this.props.Document; + + if (doc.$allDay) { + const range = typeof doc.date_range === 'string' ? doc.date_range.split('|') : []; + const dateStr = range[0] ?? new Date().toISOString().split('T')[0]; // default to today + + doc.date_range = `${dateStr}|${dateStr}`; + doc.$allDay = true; + } else { + const startField = doc.$startTime; + const endField = doc.$endTime; + const startDate = startField instanceof DateField ? startField.date : null; + const endDate = endField instanceof DateField ? endField.date : null; + + if (startDate && endDate && !isNaN(startDate.getTime()) && !isNaN(endDate.getTime())) { + doc.date_range = `${startDate.toISOString()}|${endDate.toISOString()}`; + doc.$allDay = false; + } + } + } + + /** + * Method to set task's completion status + * @param e - event of changing the "completed" input checkbox + */ + + @action + toggleComplete = (e: React.ChangeEvent<HTMLInputElement>) => { + this.props.Document.$completed = e.target.checked; + }; + + /** + * Constructor for the task box + * @param props - props containing the document reference + */ + + constructor(props: TaskBoxProps) { + super(props); + makeObservable(this); + } + + _heightDisposer?: IReactionDisposer; + _widthDisposer?: IReactionDisposer; + + componentDidMount() { + this.setTaskDateRange(); + + const doc = this.props.Document; + this._heightDisposer = reaction( + () => Number(doc._height), + height => { + const minHeight = Number(doc.height_min ?? 0); + if (!isNaN(height) && height < minHeight) { + doc._height = minHeight; + } + } + ); + + this._widthDisposer = reaction( + () => Number(doc._width), + width => { + const minWidth = Number(doc.width_min ?? 0); + if (!isNaN(width) && width < minWidth) { + doc._width = minWidth; + } + } + ); + } + + componentWillUnmount() { + this._heightDisposer?.(); + this._widthDisposer?.(); + } + + /** + * Method to render the task box + * @returns - HTML with taskbox components + */ + + render() { + + function toLocalDateTimeString(date: Date): string { + const pad = (n: number) => n.toString().padStart(2, '0'); + return ( + date.getFullYear() + + '-' + + pad(date.getMonth() + 1) + + '-' + + pad(date.getDate()) + + 'T' + + pad(date.getHours()) + + ':' + + pad(date.getMinutes()) + ); + } + + const doc = this.props.Document; + + const taskDesc = typeof doc.text === 'string' ? doc.text : ''; + const taskTitle = typeof doc.title === 'string' ? doc.title : ''; + const allDay = !!doc.$allDay; + const isCompleted = !!this.props.Document.$completed; + + const startTime = doc.$startTime instanceof DateField && doc.$startTime.date instanceof Date + ? toLocalDateTimeString(doc.$startTime.date) + : ''; + + const endTime = doc.$endTime instanceof DateField && doc.$endTime.date instanceof Date + ? toLocalDateTimeString(doc.$endTime.date) + : ''; + + + + return ( + <div className="task-manager-container"> + <input + className="task-manager-title" + type="text" + placeholder="Task Title" + value={taskTitle} + onChange={this.updateTitle} + disabled={isCompleted} + style={{opacity: isCompleted ? 0.7 : 1,}} + /> + + <textarea + className="task-manager-description" + placeholder="What’s your task?" + value={taskDesc} + onChange={this.updateText} + disabled={isCompleted} + style={{opacity: isCompleted ? 0.7 : 1,}} + /> + + <div className="task-manager-checkboxes"> + + <label className="task-manager-allday" style={{opacity: isCompleted ? 0.7 : 1,}}> + <input + type="checkbox" + checked={allDay} + onChange={this.updateAllDay} + disabled={isCompleted} + /> + All day + + {allDay && ( + <input + type="date" + value={(() => { + const rawRange = doc.date_range; + if (typeof rawRange !== 'string') return ''; + const datePart = rawRange.split('|')[0]; + if (!datePart) return ''; + const d = new Date(datePart); + return !isNaN(d.getTime()) ? d.toISOString().split('T')[0] : ''; + })()} + onChange={e => { + const newDate = new Date(e.target.value); + if (!isNaN(newDate.getTime())) { + const dateStr = e.target.value; + if (dateStr) { + doc.date_range = `${dateStr}T00:00:00|${dateStr}T00:00:00`; + } + } + }} + disabled={isCompleted} + style={{ marginLeft: '8px' }} + /> + )} + </label> + + <label className="task-manager-complete"> + <input type="checkbox" checked={isCompleted} onChange={this.toggleComplete} /> + Complete + </label> + + </div> + + {!allDay && ( + <div + className="task-manager-times" + style={{ opacity: isCompleted ? 0.7 : 1 }} + > + <label> + Start: + <input + type="datetime-local" + value={startTime} + onChange={this.updateStart} + disabled={isCompleted} + /> + </label> + <label> + End: + <input + type="datetime-local" + value={endTime} + onChange={this.updateEnd} + disabled={isCompleted} + /> + </label> + </div> + )} + </div> + ); + } +} + +Docs.Prototypes.TemplateMap.set(DocumentType.TASK, { + layout: { view: TaskBox, dataField: 'text' }, + options: { + acl: '', + _height: 35, + _xMargin: 10, + _yMargin: 10, + _layout_autoHeight: true, + _layout_nativeDimEditable: true, + _layout_reflowVertical: true, + _layout_reflowHorizontal: true, + defaultDoubleClick: 'ignore', + systemIcon: 'BsCheckSquare', + height_min: 300, + width_min: 300 + }, +}); diff --git a/src/client/views/nodes/calendarBox/CalendarBox.scss b/src/client/views/nodes/calendarBox/CalendarBox.scss index a607846df..891db9d90 100644 --- a/src/client/views/nodes/calendarBox/CalendarBox.scss +++ b/src/client/views/nodes/calendarBox/CalendarBox.scss @@ -26,6 +26,25 @@ } } } + +// AARAV ADD + +/* Existing styles */ +.fc-event.mother { + font-weight: 500; + border-radius: 4px; + padding: 2px 4px; + border-width: 2px; + } + + /* New styles for completed tasks */ + .fc-event.completed-task { + opacity: 1; + filter: grayscale(70%) brightness(90%); + text-decoration: line-through; + color: #ffffff; + } + .calendarBox-interactive { > div { pointer-events: unset; diff --git a/src/client/views/nodes/calendarBox/CalendarBox.tsx b/src/client/views/nodes/calendarBox/CalendarBox.tsx index 504dc2559..40064ad4d 100644 --- a/src/client/views/nodes/calendarBox/CalendarBox.tsx +++ b/src/client/views/nodes/calendarBox/CalendarBox.tsx @@ -1,4 +1,5 @@ import { Calendar, EventClickArg, EventDropArg, EventSourceInput } from '@fullcalendar/core'; +import { EventResizeDoneArg } from '@fullcalendar/interaction'; import dayGridPlugin from '@fullcalendar/daygrid'; import interactionPlugin from '@fullcalendar/interaction'; import multiMonthPlugin from '@fullcalendar/multimonth'; @@ -18,6 +19,7 @@ import { ContextMenu } from '../../ContextMenu'; import { DocumentView } from '../DocumentView'; import { OpenWhere } from '../OpenWhere'; import './CalendarBox.scss'; +import { DateField } from '../../../../fields/DateField'; import { undoable } from '../../../util/UndoManager'; type CalendarView = 'multiMonth' | 'dayGridMonth' | 'timeGridWeek' | 'timeGridDay'; @@ -67,6 +69,7 @@ export class CalendarBox extends CollectionSubView() { @computed get calendarEvents(): EventSourceInput | undefined { return this.childDocs.map(doc => { const { start, end } = dateRangeStrToDates(StrCast(doc.date_range)); + const isCompleted = BoolCast(doc.$completed); // AARAV ADD return { title: StrCast(doc.title), start, @@ -74,8 +77,8 @@ export class CalendarBox extends CollectionSubView() { groupId: doc[Id], startEditable: true, endEditable: true, - allDay: BoolCast(doc.allDay), - classNames: ['mother'], // will determine the style + allDay: BoolCast(doc.$allDay), + classNames: ['mother', isCompleted ? 'completed-task' : ''], // will determine the style editable: true, // subject to change in the future backgroundColor: this.eventToColor(doc), borderColor: this.eventToColor(doc), @@ -106,7 +109,11 @@ export class CalendarBox extends CollectionSubView() { // TODO: Return a different color based on the event type eventToColor = (event: Doc): string => { - return 'red' + event; + const docType = StrCast(event.type); + if (docType === 'task') { + return '#20B2AA'; // teal for tasks + } + return 'red'; }; // eslint-disable-next-line @typescript-eslint/no-unused-vars @@ -124,11 +131,29 @@ export class CalendarBox extends CollectionSubView() { return false; }; - handleEventDrop = undoable((arg: EventDropArg) => { + handleEventDrop = undoable((arg: EventDropArg | EventResizeDoneArg ) => { const doc = DocServer.GetCachedRefField(arg.event._def.groupId ?? ''); - if (doc && arg.event.start) { - doc.$allDay = false; - doc.$date_range = arg.event.start?.toString() + '|' + (arg.event.end ?? arg.event.start).toString(); + // doc && arg.event.start && (doc.date_range = arg.event.start?.toString() + '|' + (arg.event.end ?? arg.event.start).toString()); + if (!doc || !arg.event.start) return; + + // get the new start and end dates + const startDate = new Date(arg.event.start); + const endDate = new Date(arg.event.end ?? arg.event.start); + + // update date range, time range, and all day status + doc.date_range = `${startDate.toISOString()}|${endDate.toISOString()}`; + + const allDayStatus = arg.event.allDay ?? false; + if (doc.$allDay !== allDayStatus) { + doc.$allDay = allDayStatus; + } + + if (doc.$allDay) { + delete doc.$startTime; + delete doc.$endTime; + } else { + doc.$startTime = new DateField(startDate); + doc.$endTime = new DateField(endDate); } }, 'change event date'); @@ -154,7 +179,7 @@ export class CalendarBox extends CollectionSubView() { const btn = (text: string, view: string | (() => void), hint: string) => ({ text, hint, click: typeof view === 'string' ? () => this._calendarRef?.getApi().changeView(view) : view }); return ( <FullCalendar - ref={r => (this._calendarRef = r)} + ref={(r:any) => (this._calendarRef = r)} customButtons={{ nowBtn: btn('Now', () => this._calendarRef?.getApi().gotoDate(new Date()), 'Go to Today'), multiBtn: btn('M+', 'multiMonth', 'Multiple Month View'), @@ -198,7 +223,7 @@ export class CalendarBox extends CollectionSubView() { eventDrop={this.handleEventDrop} unselectAuto={false} // unselect={() => {}} - select={info => { + select={(info:any) => { const start = dateRangeStrToDates(info.startStr).start.toISOString(); const end = info.allDay ? start : dateRangeStrToDates(info.endStr).start.toISOString(); this.dataDoc.date = start + '|' + end; @@ -206,15 +231,41 @@ export class CalendarBox extends CollectionSubView() { // eventContent={() => { // return null; // }} - eventDidMount={arg => { - arg.el.addEventListener('pointerdown', ev => ev.button && ev.stopPropagation()); + eventDidMount={(arg:any) => { const doc = DocServer.GetCachedRefField(arg.event._def.groupId ?? ''); + if (!doc) return; + + if (doc.type === 'task') { + const checkButton = document.createElement('button'); + checkButton.innerText = doc.$completed ? '✅' : '⬜'; + checkButton.style.position = 'absolute'; + checkButton.style.right = '5px'; + checkButton.style.top = '50%'; + checkButton.style.transform = 'translateY(-50%)'; + checkButton.style.background = 'transparent'; + checkButton.style.border = 'none'; + checkButton.style.cursor = 'pointer'; + checkButton.style.fontSize = '18px'; + checkButton.style.zIndex = '1000'; + checkButton.style.padding = '0'; + checkButton.style.margin = '0'; + + checkButton.onclick = ev => { + ev.stopPropagation(); + doc.$completed = !doc.$completed; + this._calendar?.refetchEvents(); + }; + + arg.el.style.position = 'relative'; + arg.el.appendChild(checkButton); + } + arg.el.addEventListener('pointerdown', (ev:any) => ev.button && ev.stopPropagation()); if (navigator.userAgent.includes('Macintosh')) { - arg.el.addEventListener('pointerup', ev => { + arg.el.addEventListener('pointerup', (ev:any) => { ev.button && ev.stopPropagation(); ev.button && this.handleEventContextMenu(ev.pageX, ev.pageY, arg.event._def.groupId); }); } - arg.el.addEventListener('contextmenu', ev => { + arg.el.addEventListener('contextmenu', (ev:any) => { if (!navigator.userAgent.includes('Macintosh')) { this.handleEventContextMenu(ev.pageX, ev.pageY, arg.event._def.groupId); } diff --git a/src/client/views/nodes/formattedText/DailyJournal.tsx b/src/client/views/nodes/formattedText/DailyJournal.tsx index d6d30dc13..ae5582ef7 100644 --- a/src/client/views/nodes/formattedText/DailyJournal.tsx +++ b/src/client/views/nodes/formattedText/DailyJournal.tsx @@ -10,11 +10,20 @@ import { RichTextField } from '../../../../fields/RichTextField'; import { Plugin } from 'prosemirror-state'; import { RTFCast } from '../../../../fields/Types'; import { Mark } from 'prosemirror-model'; +import { observer } from 'mobx-react'; + export class DailyJournal extends ViewBoxAnnotatableComponent<FieldViewProps>() { @observable journalDate: string; - @observable typingTimeout: NodeJS.Timeout | null = null; // Track typing delay - @observable lastUserText: string = ''; // Store last user-entered text + @observable typingTimeout: NodeJS.Timeout | null = null; // track typing delay + @observable lastUserText: string = ''; // store last user-entered text + @observable isLoadingPrompts: boolean = false; // track if prompts are loading + @observable showPromptMenu = false; + @observable inlinePromptsEnabled = true; + @observable askPromptsEnabled = true; + + + _ref = React.createRef<FormattedTextBox>(); // reference to the formatted textbox predictiveTextRange: { from: number; to: number } | null = null; // where predictive text starts and ends private predictiveText: string | null = ' ... why?'; @@ -42,7 +51,7 @@ export class DailyJournal extends ViewBoxAnnotatableComponent<FieldViewProps>() month: 'long', day: 'numeric', }); - console.log('getFormattedDate():', date); + // console.log('getFormattedDate():', date); return date; } @@ -51,15 +60,15 @@ export class DailyJournal extends ViewBoxAnnotatableComponent<FieldViewProps>() */ @action setDailyTitle() { - console.log('setDailyTitle() called...'); - console.log('Current title before update:', this.dataDoc.title); + // console.log('setDailyTitle() called...'); + // console.log('Current title before update:', this.dataDoc.title); if (!this.dataDoc.title || this.dataDoc.title !== this.journalDate) { - console.log('Updating title to:', this.journalDate); + // console.log('Updating title to:', this.journalDate); this.dataDoc.title = this.journalDate; } - console.log('New title after update:', this.dataDoc.title); + // console.log('New title after update:', this.dataDoc.title); } /** @@ -70,7 +79,7 @@ export class DailyJournal extends ViewBoxAnnotatableComponent<FieldViewProps>() const placeholderText = 'Start writing here...'; const dateText = `${this.journalDate}\n`; - console.log('Checking if dataDoc has text field...'); + // console.log('Checking if dataDoc has text field...'); this.dataDoc[this.fieldKey] = RichTextField.textToRtfFormat( [ @@ -82,9 +91,50 @@ export class DailyJournal extends ViewBoxAnnotatableComponent<FieldViewProps>() placeholderText.length ); - console.log('Current text field:', this.dataDoc[this.fieldKey]); + // console.log('Current text field:', this.dataDoc[this.fieldKey]); + } + + /** + * Method to show/hide the prompts menu + */ + @action.bound togglePromptMenu() { + this.showPromptMenu = !this.showPromptMenu; + } + + /** + * Method to toggle on/off inline predictive prompts + */ + @action.bound toggleInlinePrompts() { + this.inlinePromptsEnabled = !this.inlinePromptsEnabled; + } + + /** + * Method to toggle on/off inline /ask prompts + */ + @action.bound toggleAskPrompts() { + this.askPromptsEnabled = !this.askPromptsEnabled; + } + + /** + * Method to handle click on document (to close prompt menu) + * @param e - a click on the document + */ + @action.bound + handleDocumentClick(e: MouseEvent) { + const menu = document.getElementById('prompts-menu'); + const button = document.getElementById('prompts-button'); + if ( + this.showPromptMenu && + menu && + !menu.contains(e.target as Node) && + button && + !button.contains(e.target as Node) + ) { + this.showPromptMenu = false; + } } + /** * Method to set initial date of document in the calendar view */ @@ -99,9 +149,9 @@ export class DailyJournal extends ViewBoxAnnotatableComponent<FieldViewProps>() this.dataDoc.date_range = `${localStart.toISOString()}|${localEnd.toISOString()}`; this.dataDoc.allDay = true; - console.log('Set date_range and allDay on journal (from local date):', this.dataDoc.date_range); + // console.log('Set date_range and allDay on journal (from local date):', this.dataDoc.date_range); } else { - console.log('Could not parse journalDate:', this.journalDate); + // console.log('Could not parse journalDate:', this.journalDate); } } } @@ -123,7 +173,7 @@ export class DailyJournal extends ViewBoxAnnotatableComponent<FieldViewProps>() // characters before cursor const triggerText = state.doc.textBetween(Math.max(0, cursorPos - 4), cursorPos); - if (triggerText === '/ask') { + if (triggerText === '/ask' && this.askPromptsEnabled) { // remove /ask text const tr = state.tr.delete(cursorPos - 4, cursorPos); editorView.dispatch(tr); @@ -134,7 +184,10 @@ export class DailyJournal extends ViewBoxAnnotatableComponent<FieldViewProps>() } this.typingTimeout = setTimeout(() => { - this.insertPredictiveQuestion(); + if (this.inlinePromptsEnabled) { + this.insertPredictiveQuestion(); + } + }, 3500); }; @@ -143,6 +196,7 @@ export class DailyJournal extends ViewBoxAnnotatableComponent<FieldViewProps>() */ @action insertPredictiveQuestion = async () => { + const editorView = this._ref.current?.EditorView; if (!editorView) return; @@ -181,7 +235,7 @@ export class DailyJournal extends ViewBoxAnnotatableComponent<FieldViewProps>() this.predictiveText = ' ...'; // placeholder const fullTextUpToCursor = state.doc.textBetween(0, state.selection.to, '\n', '\n'); - const gptPrompt = `Given the following incomplete journal entry, generate a single 2-5 word question that continues the user's thought:\n\n"${fullTextUpToCursor}"`; + const gptPrompt = `Given the following incomplete journal entry, generate a single 2-5 word reflective question that continues the user's thought:\n\n"${fullTextUpToCursor}"`; const res = await gptAPICall(gptPrompt, GPTCallType.COMPLETION); if (!res) return; @@ -196,6 +250,10 @@ export class DailyJournal extends ViewBoxAnnotatableComponent<FieldViewProps>() this.predictiveText = text; }; + /** + * Method to remove the predictive question upon type/click + * @returns - once predictive text is found, or all text has been checked + */ createPredictiveCleanupPlugin = () => { return new Plugin({ view: () => { @@ -213,7 +271,7 @@ export class DailyJournal extends ViewBoxAnnotatableComponent<FieldViewProps>() if (node.isText && node.text === textToRemove) { const tr = state.tr.delete(pos, pos + node.nodeSize); - // Set the desired default marks for future input + // default marks for input const fontSizeMark = state.schema.marks.pFontSize.create({ fontSize: '14px' }); const fontColorMark = state.schema.marks.pFontColor.create({ fontColor: 'gray' }); tr.setStoredMarks([]); @@ -244,8 +302,9 @@ export class DailyJournal extends ViewBoxAnnotatableComponent<FieldViewProps>() }; componentDidMount(): void { - console.log('componentDidMount() triggered...'); - console.log('Text: ' + RTFCast(this.Document.text)?.Text); + // console.log('componentDidMount() triggered...'); + document.addEventListener('mousedown', this.handleDocumentClick); + // console.log('Text: ' + RTFCast(this.Document.text)?.Text); const editorView = this._ref.current?.EditorView; if (editorView) { @@ -264,16 +323,17 @@ export class DailyJournal extends ViewBoxAnnotatableComponent<FieldViewProps>() const isDefaultTitle = isTitleString && currentTitle.includes('Untitled DailyJournal'); if (isTextEmpty && isDefaultTitle) { - console.log('Journal title and text are default. Initializing...'); + // console.log('Journal title and text are default. Initializing...'); this.setDailyTitle(); this.setDailyText(); this.setInitialDateRange(); } else { - console.log('Journal already has content. Skipping initialization.'); + // console.log('Journal already has content. Skipping initialization.'); } } componentWillUnmount(): void { + document.removeEventListener('mousedown', this.handleDocumentClick); const editorView = this._ref.current?.EditorView; if (editorView) { editorView.dom.removeEventListener('input', this.onTextInput); @@ -281,10 +341,20 @@ export class DailyJournal extends ViewBoxAnnotatableComponent<FieldViewProps>() if (this.typingTimeout) clearTimeout(this.typingTimeout); } + /** + * Method to generate pormpts via GPT + * @returns - if failed + */ @action handleGeneratePrompts = async () => { + if (this.isLoadingPrompts) { + return + } + + this.isLoadingPrompts = true; + const rawText = RTFCast(this.Document.text)?.Text ?? ''; - console.log('Extracted Journal Text:', rawText); - console.log('Before Update:', this.Document.text, 'Type:', typeof this.Document.text); + // console.log('Extracted Journal Text:', rawText); + // console.log('Before Update:', this.Document.text, 'Type:', typeof this.Document.text); if (!rawText.trim()) { alert('Journal is empty! Write something first.'); @@ -321,12 +391,20 @@ export class DailyJournal extends ViewBoxAnnotatableComponent<FieldViewProps>() // Insert formatted text const transaction = state.tr.insert(state.selection.from, headerText).insert(state.selection.from + headerText.nodeSize, responseText); dispatch(transaction); + (this._props as any)?.updateLayout?.(); + } } catch (err) { console.error('Error calling GPT:', err); + } finally { + this.isLoadingPrompts = false; } }; + /** + * Method to render the styled DailyJournal + * @returns - the HTML component for the journal + */ render() { return ( <div @@ -347,6 +425,7 @@ export class DailyJournal extends ViewBoxAnnotatableComponent<FieldViewProps>() }}> {/* GPT Button */} <button + id="prompts-button" style={{ position: 'absolute', bottom: '5px', @@ -359,9 +438,107 @@ export class DailyJournal extends ViewBoxAnnotatableComponent<FieldViewProps>() cursor: 'pointer', zIndex: 10, }} - onClick={this.handleGeneratePrompts}> + onClick={this.togglePromptMenu} + > Prompts </button> + {this.showPromptMenu && ( + <div + id="prompts-menu" + style={{ + position: 'absolute', + bottom: '45px', + right: '5px', + backgroundColor: 'white', + border: '1px solid #ccc', + borderRadius: '4px', + padding: '10px', + boxShadow: '0 2px 6px rgba(0,0,0,0.2)', + zIndex: 20, + minWidth: '170px', + maxWidth: 'fit-content', + overflow: 'auto', + }} + > + <div + style={{ + display: 'flex', + justifyContent: 'flex-end', + alignItems: 'center', + marginBottom: '10px', + }} + > + <label + style={{ + display: 'flex', + alignItems: 'center', + gap: '6px', + fontSize: '14px', + justifyContent: 'flex-end', + width: '100%', + }} + > + /ask + <input + type="checkbox" + checked={this.askPromptsEnabled} + onChange={this.toggleAskPrompts} + style={{ margin: 0 }} + /> + </label> + </div> + + <div + style={{ + display: 'flex', + justifyContent: 'flex-end', + alignItems: 'center', + marginBottom: '10px', + }} + > + <label + style={{ + display: 'flex', + alignItems: 'center', + gap: '6px', + fontSize: '14px', + justifyContent: 'flex-end', + width: '100%', + }} + > + Inline Prompting + <input + type="checkbox" + checked={this.inlinePromptsEnabled} + onChange={this.toggleInlinePrompts} + style={{ margin: 0 }} + /> + </label> + </div> + + <button + onClick={() => { + this.showPromptMenu = false; + this.handleGeneratePrompts(); + }} + disabled={this.isLoadingPrompts} + style={{ + backgroundColor: '#9EAD7C', + color: 'white', + border: 'none', + borderRadius: '4px', + cursor: this.isLoadingPrompts ? 'not-allowed' : 'pointer', + opacity: this.isLoadingPrompts ? 0.6 : 1, + padding: '5px 10px', + float: 'right', + }} + > + Generate Prompts + </button> + </div> + )} + + <FormattedTextBox ref={this._ref} {...this._props} fieldKey={'text'} Document={this.Document} TemplateDataDocument={undefined} /> </div> @@ -369,8 +546,10 @@ export class DailyJournal extends ViewBoxAnnotatableComponent<FieldViewProps>() } } +const ObservedDailyJournal = observer(DailyJournal); + Docs.Prototypes.TemplateMap.set(DocumentType.JOURNAL, { - layout: { view: DailyJournal, dataField: 'text' }, + layout: { view: ObservedDailyJournal, dataField: 'text' }, options: { acl: '', _height: 35, |