aboutsummaryrefslogtreecommitdiff
path: root/src/client/views/nodes/TaskBox.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'src/client/views/nodes/TaskBox.tsx')
-rw-r--r--src/client/views/nodes/TaskBox.tsx292
1 files changed, 292 insertions, 0 deletions
diff --git a/src/client/views/nodes/TaskBox.tsx b/src/client/views/nodes/TaskBox.tsx
new file mode 100644
index 000000000..9d59746f8
--- /dev/null
+++ b/src/client/views/nodes/TaskBox.tsx
@@ -0,0 +1,292 @@
+import { action, 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.$task_allDay = e.target.checked;
+
+ if (e.target.checked) {
+ delete this.props.Document.$task_startTime;
+ delete this.props.Document.$task_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.$task_startTime = new DateField(newStart);
+
+ const endDate = this.props.Document.$task_endTime instanceof DateField ? this.props.Document.$task_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.$task_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.$task_endTime = new DateField(newEnd);
+
+ const startDate = this.props.Document.$task_startTime instanceof DateField ? this.props.Document.$task_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.$task_startTime = new DateField(adjustedStart);
+ }
+
+ this.setTaskDateRange();
+ };
+
+ /**
+ * Method to update the task date range
+ */
+ @action
+ setTaskDateRange() {
+ const doc = this.props.Document;
+
+ if (doc.$task_allDay) {
+ const range = typeof doc.$task_dateRange === 'string' ? doc.$task_dateRange.split('|') : [];
+ const dateStr = range[0] ?? new Date().toISOString().split('T')[0]; // default to today
+
+ doc.$task_dateRange = `${dateStr}|${dateStr}`;
+ doc.$task_allDay = true;
+ } else {
+ const startField = doc.$task_startTime;
+ const endField = doc.$task_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.$task_dateRange = `${startDate.toISOString()}|${endDate.toISOString()}`;
+ doc.$task_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.$task_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.$task_allDay;
+ const isCompleted = !!this.props.Document.$task_completed;
+
+ const startTime = doc.$task_startTime instanceof DateField && doc.$task_startTime.date instanceof Date ? toLocalDateTimeString(doc.$task_startTime.date) : '';
+
+ const endTime = doc.$task_endTime instanceof DateField && doc.$task_endTime.date instanceof Date ? toLocalDateTimeString(doc.$task_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.$task_dateRange;
+ 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.$task_dateRange = `${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,
+ task: '',
+ defaultDoubleClick: 'ignore',
+ systemIcon: 'BsCheckSquare',
+ height_min: 300,
+ width_min: 300,
+ },
+});