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.tsx298
1 files changed, 298 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..dca01817f
--- /dev/null
+++ b/src/client/views/nodes/TaskBox.tsx
@@ -0,0 +1,298 @@
+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';
+
+interface TaskBoxProps {
+ Document: Doc;
+}
+
+@observer
+export class TaskBox extends React.Component<TaskBoxProps> {
+ public static LayoutString(fieldStr: string) {
+ return FieldView.LayoutString(TaskBox, fieldStr);
+ }
+
+ @action
+ updateText = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
+ this.props.Document.text = e.target.value;
+ };
+
+ @action
+ updateTitle = (e: React.ChangeEvent<HTMLInputElement>) => {
+ this.props.Document.title = e.target.value;
+ };
+
+ @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();
+ };
+
+ @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();
+ };
+
+
+
+ @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();
+ };
+
+
+
+
+ @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;
+ }
+ }
+ }
+
+ @action
+ toggleComplete = (e: React.ChangeEvent<HTMLInputElement>) => {
+ this.props.Document.$completed = e.target.checked;
+ };
+
+ 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?.();
+ }
+
+ 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
+ },
+});