aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/client/documents/DocumentTypes.ts3
-rw-r--r--src/client/documents/Documents.ts70
-rw-r--r--src/client/util/CurrentUserUtils.ts4
-rw-r--r--src/client/views/Main.tsx8
-rw-r--r--src/client/views/MainView.tsx3
-rw-r--r--src/client/views/nodes/TaskBox.scss72
-rw-r--r--src/client/views/nodes/TaskBox.tsx345
-rw-r--r--src/client/views/nodes/calendarBox/CalendarBox.scss19
-rw-r--r--src/client/views/nodes/calendarBox/CalendarBox.tsx77
-rw-r--r--src/client/views/nodes/formattedText/DailyJournal.tsx225
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,