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.tsx578
1 files changed, 478 insertions, 100 deletions
diff --git a/src/client/views/nodes/TaskBox.tsx b/src/client/views/nodes/TaskBox.tsx
index 9d59746f8..ed5982c55 100644
--- a/src/client/views/nodes/TaskBox.tsx
+++ b/src/client/views/nodes/TaskBox.tsx
@@ -1,30 +1,64 @@
-import { action, makeObservable, IReactionDisposer, reaction } from 'mobx';
+import { action, IReactionDisposer, makeObservable, observable, reaction, runInAction } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
+import { DateField } from '../../../fields/DateField';
+import { BoolCast, DateCast, DocCast, NumCast, StrCast } from '../../../fields/Types';
+import { GoogleAuthenticationManager } from '../../apis/GoogleAuthenticationManager';
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 { ViewBoxBaseComponent } from '../DocComponent';
+import { FieldView, FieldViewProps } from './FieldView';
import './TaskBox.scss';
-
-/**
- * Props (reference to document) for Task Box
- */
-
-interface TaskBoxProps {
- Document: Doc;
-}
+import { DocumentDecorations } from '../DocumentDecorations';
+import { Doc } from '../../../fields/Doc';
+import { DocumentView } from './DocumentView';
/**
* TaskBox class for adding task information + completing tasks
*/
@observer
-export class TaskBox extends React.Component<TaskBoxProps> {
+export class TaskBox extends ViewBoxBaseComponent<FieldViewProps>() {
+ _googleTaskCreateDisposer?: IReactionDisposer;
+ _heightDisposer?: IReactionDisposer;
+ _widthDisposer?: IReactionDisposer;
+ @observable _needsSync = false; // Whether the task needs to be synced with Google Tasks
+ @observable _syncing = false; // Whether the task is currently syncing with Google Tasks
+ private _isFocused = false; // Whether the task box is currently focused
+
+ // contains the last synced task information
+ private _lastSyncedTask: {
+ title: string;
+ text: string;
+ due?: string;
+ completed: boolean;
+ deleted?: boolean;
+ } = {
+ title: '',
+ text: '',
+ due: '',
+ completed: false,
+ deleted: false,
+ };
+
+ /**
+ * Getter for needsSync
+ */
+ get needsSync() {
+ return this._needsSync;
+ }
+
+ /**
+ * Constructor for the task box
+ * @param props - props containing the document reference
+ */
+ constructor(props: FieldViewProps) {
+ super(props);
+ makeObservable(this);
+ }
+
/**
- * Method to reuturn the
- * @param fieldStr
+ * Return the JSX string that will create this component
+ * @param fieldStr the Doc field that contains the primary data for this component
* @returns
*/
public static LayoutString(fieldStr: string) {
@@ -35,10 +69,9 @@ export class TaskBox extends React.Component<TaskBoxProps> {
* 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;
+ this.Document[this.fieldKey] = e.target.value;
};
/**
@@ -47,7 +80,7 @@ export class TaskBox extends React.Component<TaskBoxProps> {
*/
@action
updateTitle = (e: React.ChangeEvent<HTMLInputElement>) => {
- this.props.Document.title = e.target.value;
+ this.Document.title = e.target.value;
};
/**
@@ -56,11 +89,11 @@ export class TaskBox extends React.Component<TaskBoxProps> {
*/
@action
updateAllDay = (e: React.ChangeEvent<HTMLInputElement>) => {
- this.props.Document.$task_allDay = e.target.checked;
+ this.Document.$task_allDay = e.target.checked;
if (e.target.checked) {
- delete this.props.Document.$task_startTime;
- delete this.props.Document.$task_endTime;
+ delete this.Document.$task_startTime;
+ delete this.Document.$task_endTime;
}
this.setTaskDateRange();
@@ -74,16 +107,16 @@ export class TaskBox extends React.Component<TaskBoxProps> {
updateStart = (e: React.ChangeEvent<HTMLInputElement>) => {
const newStart = new Date(e.target.value);
- this.props.Document.$task_startTime = new DateField(newStart);
+ this.Document.$task_startTime = new DateField(newStart);
- const endDate = this.props.Document.$task_endTime instanceof DateField ? this.props.Document.$task_endTime.date : undefined;
+ const endDate = this.Document.$task_endTime instanceof DateField ? this.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.Document.$task_endTime = new DateField(adjustedEnd);
}
this.setTaskDateRange();
@@ -97,16 +130,16 @@ export class TaskBox extends React.Component<TaskBoxProps> {
updateEnd = (e: React.ChangeEvent<HTMLInputElement>) => {
const newEnd = new Date(e.target.value);
- this.props.Document.$task_endTime = new DateField(newEnd);
+ this.Document.$task_endTime = new DateField(newEnd);
- const startDate = this.props.Document.$task_startTime instanceof DateField ? this.props.Document.$task_startTime.date : undefined;
+ const startDate = this.Document.$task_startTime instanceof DateField ? this.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.Document.$task_startTime = new DateField(adjustedStart);
}
this.setTaskDateRange();
@@ -117,10 +150,10 @@ export class TaskBox extends React.Component<TaskBoxProps> {
*/
@action
setTaskDateRange() {
- const doc = this.props.Document;
+ const doc = this.Document;
if (doc.$task_allDay) {
- const range = typeof doc.$task_dateRange === 'string' ? doc.$task_dateRange.split('|') : [];
+ const range = StrCast(doc.$task_dateRange).split('|');
const dateStr = range[0] ?? new Date().toISOString().split('T')[0]; // default to today
doc.$task_dateRange = `${dateStr}|${dateStr}`;
@@ -145,53 +178,365 @@ export class TaskBox extends React.Component<TaskBoxProps> {
@action
toggleComplete = (e: React.ChangeEvent<HTMLInputElement>) => {
- this.props.Document.$task_completed = e.target.checked;
+ this.Document.$task_completed = e.target.checked;
};
/**
- * Constructor for the task box
- * @param props - props containing the document reference
+ * Computes due date for the task (for Google Tasks API)
+ * @returns - a string representing the due date in ISO format, or undefined if no valid date is found
*/
+ private computeDueDate(): string | undefined {
+ const doc = this.Document;
+ let due: string | undefined;
+ const allDay = !!doc.$task_allDay;
- constructor(props: TaskBoxProps) {
- super(props);
- makeObservable(this);
+ if (allDay) {
+ const rawRange = StrCast(doc.$task_dateRange);
+ const datePart = rawRange.split('|')[0];
+
+ if (datePart && !isNaN(new Date(datePart).getTime())) {
+ // Set time to midnight UTC to represent the start of the all-day event
+ const baseDate = datePart.includes('T') ? datePart : datePart + 'T00:00:00Z';
+ due = new Date(baseDate).toISOString();
+ } else {
+ due = undefined;
+ }
+ } else if (doc.$task_endTime instanceof DateField && doc.$task_endTime.date) {
+ due = doc.$task_endTime.date.toISOString();
+ } else if (doc.$task_startTime instanceof DateField && doc.$task_startTime.date) {
+ due = doc.$task_startTime.date.toISOString();
+ } else {
+ due = undefined;
+ }
+
+ return due;
}
- _heightDisposer?: IReactionDisposer;
- _widthDisposer?: IReactionDisposer;
+ /**
+ * Builds the body for the Google Tasks API request
+ * @returns - an object containing the task details
+ */
+
+ private buildGoogleTaskBody(): Record<string, string | boolean | undefined> {
+ const doc = this.Document;
+ const title = StrCast(doc.title, 'Untitled Task');
+ const notes = StrCast(doc[this.fieldKey]);
+ const due = this.computeDueDate();
+ const completed = !!doc.$task_completed;
+
+ const body: Record<string, string | boolean | undefined> = {
+ title,
+ notes,
+ due,
+ status: completed ? 'completed' : 'needsAction',
+ completed: completed ? new Date().toISOString() : undefined,
+ };
+
+ if (doc.$dashDeleted === true) {
+ body.deleted = true;
+ } else if (doc.$dashDeleted === false) {
+ body.deleted = false;
+ }
+
+ return body;
+ }
+
+ /**
+ * Handles the focus event for the task box (for auto-syncing)
+ */
+ handleFocus = () => {
+ if (!this._isFocused) {
+ this._isFocused = true;
+ this.syncWithGoogleTaskBidirectional(true); // silent sync
+ }
+ };
+
+ /**
+ * Handles the blur event for the task box (for auto-syncing)
+ * @param e - the focus event
+ */
+ handleBlur = (e: React.FocusEvent<HTMLDivElement>) => {
+ // Check if focus is moving outside this component
+ if (!e.currentTarget.contains(e.relatedTarget)) {
+ this._isFocused = false;
+ this.syncWithGoogleTaskBidirectional(true);
+ }
+ };
+
+ /**
+ * Method to sync the task with Google Tasks bidirectionally
+ * (update Dash from Google and vice versa, based on which is newer)
+ * @param silent - whether to suppress UI prompts to connect to Google (default: false)
+ * @returns - a promise that resolves to true if sync was successful, false otherwise
+ */
+
+ syncWithGoogleTaskBidirectional = async (silent = false): Promise<boolean> => {
+ const doc = this.Document;
+ let token: string | undefined;
+ try {
+ token = silent ? await GoogleAuthenticationManager.Instance.fetchAccessTokenSilently() : await GoogleAuthenticationManager.Instance.fetchOrGenerateAccessToken();
+ } catch (err) {
+ console.warn('Google auth failed:', err);
+ return false;
+ }
+
+ if (!token) {
+ if (!silent) {
+ const listener = () => {
+ window.removeEventListener('focusin', listener);
+ if (confirm('✅ Authorization complete. Try syncing the task again?')) {
+ // try syncing again
+ this.syncWithGoogleTaskBidirectional();
+ }
+ window.removeEventListener('focusin', listener);
+ };
+ setTimeout(() => window.addEventListener('focusin', listener), 100);
+ }
+ return false;
+ }
+
+ if (!doc.$googleTaskId) return false;
+
+ runInAction(() => {
+ this._syncing = true;
+ });
+
+ try {
+ // Fetch current version of Google Task
+ const response = await fetch(`/googleTasks/${doc.$googleTaskId}`, {
+ method: 'GET',
+ headers: {
+ Authorization: `Bearer ${token}`,
+ },
+ credentials: 'include',
+ });
+
+ const googleTask = await response.json();
+ const googleUpdated = new Date(googleTask.updated);
+ const dashUpdated = new Date(StrCast(doc.$task_lastSyncedAt));
+
+ const dashChanged =
+ StrCast(doc.title) !== this._lastSyncedTask.title ||
+ StrCast(doc[this.fieldKey]) !== this._lastSyncedTask.text ||
+ this.computeDueDate() !== this._lastSyncedTask.due ||
+ !!doc.$task_completed !== this._lastSyncedTask.completed ||
+ !!doc.$dashDeleted !== this._lastSyncedTask.deleted;
+
+ if (googleUpdated > dashUpdated && !dashChanged) {
+ // Google version is newer — update Dash
+ runInAction(() => {
+ doc.title = googleTask.title ?? doc.title;
+ doc[this.fieldKey] = googleTask.notes ?? doc[this.fieldKey];
+ doc.$task_completed = googleTask.status === 'completed';
+
+ if (googleTask.due && googleTask.due.split('T')[0] !== this.computeDueDate()?.split('T')[0]) {
+ const dueDate = new Date(googleTask.due);
+ doc.$task_allDay = true;
+ doc.$task_dateRange = `${dueDate.toISOString().split('T')[0]}|${dueDate.toISOString().split('T')[0]}`;
+ }
+
+ doc.$task_lastSyncedAt = googleTask.updated;
+ this._lastSyncedTask = {
+ title: StrCast(doc.title),
+ text: StrCast(doc[this.fieldKey]),
+ due: this.computeDueDate(),
+ completed: !!doc.$task_completed,
+ deleted: !!doc.$dashDeleted,
+ };
+ this._needsSync = false;
+ });
+
+ console.log('Pulled newer version from Google');
+ return true;
+ } else if (googleUpdated <= dashUpdated && !dashChanged) {
+ console.log('No changes to sync');
+ return true;
+ } else {
+ // Dash version is newer — push update to Google
+ const body = this.buildGoogleTaskBody();
+ const res = await fetch(`/googleTasks/${doc.$googleTaskId}`, {
+ method: 'PATCH',
+ headers: {
+ 'Content-Type': 'application/json',
+ Authorization: `Bearer ${token}`,
+ },
+ credentials: 'include',
+ body: JSON.stringify(body),
+ });
+
+ const result = await res.json();
+ if (result?.id) {
+ doc.$task_lastSyncedAt = new Date().toISOString();
+ this._lastSyncedTask = {
+ title: StrCast(doc.title),
+ text: StrCast(doc[this.fieldKey]),
+ due: this.computeDueDate(),
+ completed: !!doc.$task_completed,
+ deleted: !!doc.$dashDeleted,
+ };
+ this._needsSync = false;
+ console.log('Pushed newer version to Google');
+ return true;
+ } else {
+ console.warn('❌ Push failed:', result);
+ return false;
+ }
+ }
+ } catch (err) {
+ console.error('❌ Sync error:', err);
+ return false;
+ } finally {
+ runInAction(() => {
+ this._syncing = false;
+ });
+ }
+ };
+
+ /**
+ * Method to set up the task box on mount
+ */
componentDidMount() {
this.setTaskDateRange();
+ const doc = this.Document;
+
+ // adding task on creation to google
+ (async () => {
+ if (!doc.$googleTaskId && doc.title) {
+ try {
+ const token = await GoogleAuthenticationManager.Instance.fetchOrGenerateAccessToken();
+ if (!token) return;
+ const body = this.buildGoogleTaskBody();
+
+ const res = await fetch('/googleTasks/create', {
+ method: 'POST',
+ credentials: 'include',
+ headers: {
+ 'Content-Type': 'application/json',
+ Authorization: `Bearer ${token}`,
+ },
+ body: JSON.stringify(body),
+ });
+
+ const result = await res.json();
+ if (result?.id) {
+ doc.$googleTaskId = result.id;
+ console.log('✅ Google Task created on mount:', result);
+ } else {
+ console.warn('❌ Google Task creation failed:', result);
+ }
+ } catch (err) {
+ console.warn('❌ Error creating Google Task:', err);
+ }
+ } else if (doc.$googleTaskId) {
+ await this.syncWithGoogleTaskBidirectional();
+ }
+ })();
- const doc = this.props.Document;
this._heightDisposer = reaction(
- () => Number(doc._height),
+ () => NumCast(doc._height),
height => {
- const minHeight = Number(doc.height_min ?? 0);
- if (!isNaN(height) && height < minHeight) {
+ const minHeight = NumCast(doc.height_min);
+ if (height < minHeight) {
doc._height = minHeight;
}
}
);
this._widthDisposer = reaction(
- () => Number(doc._width),
+ () => NumCast(doc._width),
width => {
- const minWidth = Number(doc.width_min ?? 0);
- if (!isNaN(width) && width < minWidth) {
+ const minWidth = NumCast(doc.width_min);
+ if (width < minWidth) {
doc._width = minWidth;
}
}
);
+
+ runInAction(() => {
+ const completed = BoolCast(doc.$task_completed);
+ const due = this.computeDueDate();
+
+ this._lastSyncedTask = {
+ title: StrCast(doc.title),
+ text: StrCast(doc[this.fieldKey]),
+ due,
+ completed,
+ deleted: !!doc.$dashDeleted,
+ };
+ this._needsSync = false;
+ });
+
+ if (this.Document.$dashDeleted) {
+ runInAction(() => {
+ this.Document.$dashDeleted = false;
+ });
+ }
+
+ this._googleTaskCreateDisposer = reaction(
+ () => {
+ const completed = BoolCast(doc.$task_completed);
+ const due = this.computeDueDate();
+ const dashDeleted = !!doc.$dashDeleted;
+
+ return { title: StrCast(doc.title), text: StrCast(doc[this.fieldKey]), completed, due, dashDeleted };
+ },
+ ({ title, text, completed, due, dashDeleted }) => {
+ this._needsSync = title !== this._lastSyncedTask.title || text !== this._lastSyncedTask.text || due !== this._lastSyncedTask.due || completed !== this._lastSyncedTask.completed || dashDeleted !== this._lastSyncedTask.deleted;
+ },
+ { fireImmediately: true }
+ );
}
+ /**
+ * Method to clean up the task box on unmount
+ */
componentWillUnmount() {
+ const doc = this.Document;
+ this._googleTaskCreateDisposer?.();
this._heightDisposer?.();
this._widthDisposer?.();
}
/**
+ * Method to handle task deletion
+ * @returns - a promise that resolves when the task is deleted
+ */
+ handleDeleteTask = async () => {
+ const doc = this.Document;
+ if (!doc.$googleTaskId) return;
+ if (!window.confirm('Are you sure you want to delete this task?')) return;
+
+ doc.$dashDeleted = true;
+ this._needsSync = true;
+
+ try {
+ const token = await GoogleAuthenticationManager.Instance.fetchOrGenerateAccessToken();
+ if (!token) return;
+
+ await fetch(`/googleTasks/${doc.$googleTaskId}`, {
+ method: 'DELETE',
+ credentials: 'include',
+ headers: {
+ Authorization: `Bearer ${token}`,
+ },
+ });
+
+ const view = DocumentView.getDocumentView(this.Document);
+ if (view) {
+ DocumentView.SelectView(view, false); // select document
+ DocumentDecorations.Instance?.onCloseClick?.(true); // simulate clicking the close button
+ }
+
+ // Remove the task from the recently closed list
+ Doc.MyRecentlyClosed && Doc.RemoveDocFromList(Doc.MyRecentlyClosed, undefined, this.Document);
+ console.log(`✅ Deleted Google Task ${doc.$googleTaskId}`);
+ } catch (err) {
+ console.warn('❌ Failed to delete Google Task:', err);
+ }
+ };
+
+ /**
* Method to render the task box
* @returns - HTML with taskbox components
*/
@@ -202,71 +547,104 @@ export class TaskBox extends React.Component<TaskBoxProps> {
return date.getFullYear() + '-' + pad(date.getMonth() + 1) + '-' + pad(date.getDate()) + 'T' + pad(date.getHours()) + ':' + pad(date.getMinutes());
}
- const doc = this.props.Document;
+ const doc = this.Document;
- const taskDesc = typeof doc.text === 'string' ? doc.text : '';
- const taskTitle = typeof doc.title === 'string' ? doc.title : '';
+ const taskDesc = StrCast(doc[this.fieldKey]);
+ const taskTitle = StrCast(doc.title);
const allDay = !!doc.$task_allDay;
- const isCompleted = !!this.props.Document.$task_completed;
+ const due = this.computeDueDate();
+ const isCompleted = !!this.Document.$task_completed;
+
+ const startTime = DateCast(doc.$task_startTime) ? toLocalDateTimeString(DateCast(doc.$task_startTime)!.date) : '';
+ const endTime = DateCast(doc.$task_endTime) ? toLocalDateTimeString(DateCast(doc.$task_endTime)!.date) : '';
- const startTime = doc.$task_startTime instanceof DateField && doc.$task_startTime.date instanceof Date ? toLocalDateTimeString(doc.$task_startTime.date) : '';
+ const handleGoogleTaskSync = async () => {
+ const success = await this.syncWithGoogleTaskBidirectional();
- const endTime = doc.$task_endTime instanceof DateField && doc.$task_endTime.date instanceof Date ? toLocalDateTimeString(doc.$task_endTime.date) : '';
+ if (success) {
+ alert('✅ Task successfully synced!');
+ } else {
+ alert('❌ Task sync failed. Try reloading.');
+ }
+ };
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`;
+ <div className="task-box-blur-wrapper" tabIndex={0} onBlur={this.handleBlur} onFocus={this.handleFocus}>
+ <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 datePart = StrCast(doc.$task_dateRange).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} />
+ }}
+ disabled={isCompleted}
+ style={{ marginLeft: '8px' }}
+ />
+ )}
</label>
- <label>
- End:
- <input type="datetime-local" value={endTime} onChange={this.updateEnd} disabled={isCompleted} />
+
+ <label className="task-manager-complete">
+ <input type="checkbox" checked={isCompleted} onChange={this.toggleComplete} />
+ Complete
</label>
</div>
- )}
+
+ <div className="task-manager-button-row">
+ <button
+ className="task-manager-google"
+ onClick={event => {
+ event.preventDefault();
+ handleGoogleTaskSync();
+ }}>
+ {this._syncing ? 'Syncing...' : this.needsSync ? 'Push Updates' : 'Sync Task'}
+ </button>
+
+ <button
+ className="task-manager-delete"
+ onClick={event => {
+ event.preventDefault();
+ this.handleDeleteTask();
+ }}>
+ <svg xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" width="12" height="12" viewBox="0 0 24 24" style={{ fill: 'white', marginRight: '6px', verticalAlign: 'middle', marginTop: '-2px' }}>
+ <path d="M 10 2 L 9 3 L 5 3 C 4.4 3 4 3.4 4 4 C 4 4.6 4.4 5 5 5 L 7 5 L 17 5 L 19 5 C 19.6 5 20 4.6 20 4 C 20 3.4 19.6 3 19 3 L 15 3 L 14 2 L 10 2 z M 5 7 L 5 20 C 5 21.1 5.9 22 7 22 L 17 22 C 18.1 22 19 21.1 19 20 L 19 7 L 5 7 z M 9 9 C 9.6 9 10 9.4 10 10 L 10 19 C 10 19.6 9.6 20 9 20 C 8.4 20 8 19.6 8 19 L 8 10 C 8 9.4 8.4 9 9 9 z M 15 9 C 15.6 9 16 9.4 16 10 L 16 19 C 16 19.6 15.6 20 15 20 C 14.4 20 14 19.6 14 19 L 14 10 C 14 9.4 14.4 9 15 9 z"></path>
+ </svg>
+ Delete
+ </button>
+ </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>
</div>
);
}