diff options
author | bobzel <zzzman@gmail.com> | 2025-06-19 15:34:13 -0400 |
---|---|---|
committer | bobzel <zzzman@gmail.com> | 2025-06-19 15:34:13 -0400 |
commit | 21fb3a555cdd861452e2e54de3d0524e29f2bc08 (patch) | |
tree | 44b93ba0b4f9a85dafdb744ccb11dfe3386f4829 /src/client/views/nodes/TaskBox.tsx | |
parent | abdfe5b109f42cd5da4aceef1bd667c8e1f6cbbd (diff) | |
parent | 765c7a3870a1d446622a28b157ab00a4dced2879 (diff) |
Merge branch 'task_nodes_aarav'
Diffstat (limited to 'src/client/views/nodes/TaskBox.tsx')
-rw-r--r-- | src/client/views/nodes/TaskBox.tsx | 578 |
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> ); } |