aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/client/views/nodes/TaskBox.tsx339
-rw-r--r--src/server/apis/google/google_project_credentials.json7
2 files changed, 197 insertions, 149 deletions
diff --git a/src/client/views/nodes/TaskBox.tsx b/src/client/views/nodes/TaskBox.tsx
index 37f6124a1..6c966a59d 100644
--- a/src/client/views/nodes/TaskBox.tsx
+++ b/src/client/views/nodes/TaskBox.tsx
@@ -15,14 +15,10 @@ import './TaskBox.scss';
*/
@observer
export class TaskBox extends ViewBoxBaseComponent<FieldViewProps>() {
- /**
- * 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) {
- return FieldView.LayoutString(TaskBox, fieldStr);
- }
+ _googleTaskCreateDisposer?: IReactionDisposer;
+ _heightDisposer?: IReactionDisposer;
+ _widthDisposer?: IReactionDisposer;
+ @observable _needsSync = false; // Whether the task needs to be synced with Google Tasks
// contains the last synced task information
private _lastSyncedTask: {
@@ -37,10 +33,9 @@ export class TaskBox extends ViewBoxBaseComponent<FieldViewProps>() {
completed: false,
};
- // Whether the task needs to be synced with Google Tasks
- @observable _needsSync = false;
-
- // Getter for needsSync
+ /**
+ * Getter for needsSync
+ */
get needsSync() {
return this._needsSync;
}
@@ -55,10 +50,18 @@ export class TaskBox extends ViewBoxBaseComponent<FieldViewProps>() {
}
/**
+ * 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) {
+ 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.Document[this.fieldKey] = e.target.value;
@@ -171,38 +174,127 @@ export class TaskBox extends ViewBoxBaseComponent<FieldViewProps>() {
this.Document.$task_completed = e.target.checked;
};
- _googleTaskCreateDisposer?: IReactionDisposer;
- _heightDisposer?: IReactionDisposer;
- _widthDisposer?: IReactionDisposer;
+ /**
+ * 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;
+
+ 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;
+ }
+
+ /**
+ * Builds the body for the Google Tasks API request
+ * @returns - an object containing the task details
+ */
+
+ private buildGoogleTaskBody(): Record<string, string | 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;
+
+ return {
+ title,
+ notes,
+ due,
+ status: completed ? 'completed' : 'needsAction',
+ completed: completed ? new Date().toISOString() : undefined,
+ };
+ }
+
+ /**
+ * Method to automatically sync the task to Google Tasks when the component loses focus
+ * @returns - a Promise that resolves when the sync is complete
+ */
+ autoSyncToGoogle = async () => {
+ if (!this._needsSync) return;
+
+ const token = await GoogleAuthenticationManager.Instance.fetchOrGenerateAccessToken();
+ if (!token) return;
+
+ const doc = this.Document;
+ const taskTitle = StrCast(doc.title);
+ const taskDesc = StrCast(doc[this.fieldKey]);
+ const due = this.computeDueDate();
+ const isCompleted = !!doc.$task_completed;
+
+ const body = this.buildGoogleTaskBody();
+
+ const isUpdate = !!doc.$googleTaskId;
+ const endpoint = isUpdate ? `/googleTasks/${doc.$googleTaskId}` : '/googleTasks/create';
+ const method = isUpdate ? 'PATCH' : 'POST';
+
+ const res = await fetch(endpoint, {
+ method,
+ credentials: 'include',
+ headers: {
+ 'Content-Type': 'application/json',
+ Authorization: `Bearer ${token}`,
+ },
+ body: JSON.stringify(body),
+ });
+
+ const result = await res.json();
+ if (result?.id) {
+ runInAction(() => {
+ this._lastSyncedTask = { title: taskTitle, text: taskDesc, due, completed: isCompleted };
+ this._needsSync = false;
+ });
+ console.log('✅ Auto-synced with Google Tasks on blur');
+ } else {
+ console.warn('❌ Auto-sync failed:', result);
+ }
+ };
+
+ /**
+ * Handles the blur event for the task box
+ * @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.autoSyncToGoogle();
+ }
+ };
+
+ /**
+ * 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: { title: string; notes: string; status: string; completed?: string; due?: string } = {
- title: StrCast(doc.title, 'Untitled Task'),
- notes: StrCast(doc[this.fieldKey]),
- status: doc.$task_completed ? 'completed' : 'needsAction',
- completed: doc.$task_completed ? new Date().toISOString() : undefined,
- };
-
- const datePart = StrCast(doc.$task_dateRange).split('|')[0];
- if (doc.$task_allDay && datePart) {
- if (datePart && !isNaN(new Date(datePart).getTime())) {
- const baseDate = datePart.includes('T') ? datePart : datePart + 'T00:00:00Z';
- body.due = new Date(baseDate).toISOString();
- }
- } else if (doc.$task_endTime instanceof DateField) {
- body.due = doc.$task_endTime.date.toISOString();
- }
+ const body = this.buildGoogleTaskBody();
const res = await fetch('/googleTasks/create', {
method: 'POST',
@@ -249,22 +341,8 @@ export class TaskBox extends ViewBoxBaseComponent<FieldViewProps>() {
runInAction(() => {
const completed = BoolCast(doc.$task_completed);
- const $task_allDay = BoolCast(doc.$task_allDay);
- const endTime = DateCast(doc.$task_endTime);
- const startTime = DateCast(doc.$task_startTime);
- const datePart = StrCast(doc.$task_dateRange)?.split('|')[0];
-
- const due = (() => {
- if ($task_allDay && datePart && !isNaN(new Date(datePart).getTime())) {
- return new Date(datePart).toISOString();
- } else if (endTime && !isNaN(+endTime.date)) {
- return endTime.date.toISOString();
- } else if (startTime && !isNaN(+startTime.date)) {
- return startTime.date.toISOString();
- }
- return undefined;
- })();
-
+ const due = this.computeDueDate();
+
this._lastSyncedTask = {
title: StrCast(doc.title),
text: StrCast(doc[this.fieldKey]),
@@ -277,17 +355,7 @@ export class TaskBox extends ViewBoxBaseComponent<FieldViewProps>() {
this._googleTaskCreateDisposer = reaction(
() => {
const completed = BoolCast(doc.$task_completed);
- const $task_allDay = BoolCast(doc.$task_allDay);
- const endTime = DateCast(doc.$task_endTime);
- const startTime = DateCast(doc.$task_startTime);
- const datePart = StrCast(doc.$task_dateRange)?.split('|')[0];
-
- const due = (() => {
- if ($task_allDay && datePart) {
- if (!isNaN(new Date(datePart).getTime())) return new Date(datePart).toISOString();
- } else if (endTime && !isNaN(+endTime.date)) return endTime.date.toISOString();
- else if (startTime && !isNaN(+startTime.date)) return startTime.date.toISOString();
- })();
+ const due = this.computeDueDate();
return { title: StrCast(doc.title), text: StrCast(doc[this.fieldKey]), completed, due };
},
@@ -302,6 +370,9 @@ export class TaskBox extends ViewBoxBaseComponent<FieldViewProps>() {
);
}
+ /**
+ * Method to clean up the task box on unmount
+ */
componentWillUnmount() {
const doc = this.Document;
this._googleTaskCreateDisposer?.();
@@ -347,6 +418,7 @@ export class TaskBox extends ViewBoxBaseComponent<FieldViewProps>() {
const taskDesc = StrCast(doc[this.fieldKey]);
const taskTitle = StrCast(doc.title);
const allDay = !!doc.$task_allDay;
+ const due = this.computeDueDate();
const isCompleted = !!this.Document.$task_completed;
const startTime = DateCast(doc.$task_startTime) ? toLocalDateTimeString(DateCast(doc.$task_startTime)!.date) : '';
@@ -370,26 +442,7 @@ export class TaskBox extends ViewBoxBaseComponent<FieldViewProps>() {
return;
}
- let due: string | undefined;
-
- 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;
- }
+ const body = this.buildGoogleTaskBody();
const isUpdate = !!doc.$googleTaskId;
const endpoint = isUpdate ? `/googleTasks/${doc.$googleTaskId}` : '/googleTasks/create';
@@ -402,13 +455,7 @@ export class TaskBox extends ViewBoxBaseComponent<FieldViewProps>() {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
},
- body: JSON.stringify({
- title: taskTitle || 'Untitled Task',
- notes: taskDesc,
- due,
- status: doc.$task_completed ? 'completed' : 'needsAction',
- completed: doc.$task_completed ? new Date().toISOString() : undefined,
- }),
+ body: JSON.stringify(body),
});
const result = await response.json();
@@ -435,67 +482,69 @@ export class TaskBox extends ViewBoxBaseComponent<FieldViewProps>() {
};
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 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`;
+ <div className="task-box-blur-wrapper" tabIndex={0} onBlur={this.handleBlur}>
+ <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>
-
- <button
- className="task-manager-google"
- disabled={!this.needsSync}
- onClick={event => {
- event.preventDefault();
- handleGoogleTaskSync();
- }}>
- Sync to Google
- </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} />
+ }}
+ 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>
+
+ <button
+ className="task-manager-google"
+ disabled={!this.needsSync}
+ onClick={event => {
+ event.preventDefault();
+ handleGoogleTaskSync();
+ }}>
+ Sync to Google
+ </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>
);
}
diff --git a/src/server/apis/google/google_project_credentials.json b/src/server/apis/google/google_project_credentials.json
index 8a9a38dac..8abc13b80 100644
--- a/src/server/apis/google/google_project_credentials.json
+++ b/src/server/apis/google/google_project_credentials.json
@@ -1,12 +1,11 @@
{
"installed": {
- "client_id": "838617994486-a28072lirm8uk8cm78t7ic4krp0rgkgv.apps.googleusercontent.com",
- "project_id": "gtasks-test-dash",
+ "client_id": "740987818053-dtflji3hfkn5r9t8ad6jb8740pls8moh.apps.googleusercontent.com",
+ "project_id": "dash-web-461920",
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://oauth2.googleapis.com/token",
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
- "client_secret": "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",
+ "client_secret": "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",
"redirect_uris": ["http://localhost:1050/refreshGoogle"]
}
}
-