aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/client/documents/Documents.ts1
-rw-r--r--src/client/views/nodes/TaskBox.tsx304
-rw-r--r--src/server/ApiManagers/GeneralGoogleManager.ts29
-rw-r--r--src/server/apis/google/google_project_credentials.json2
4 files changed, 242 insertions, 94 deletions
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 27cb95144..a29e7a9f5 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -529,6 +529,7 @@ export class DocumentOptions {
$task_allDay?: BoolInfo | boolean = new BoolInfo('whether task is all-day or not', /*filterable*/ false);
$task_completed?: BoolInfo | boolean = new BoolInfo('whether the task is completed', /*filterable*/ false);
$googleTaskId?: STRt = new StrInfo('Google Task ID for syncing');
+ $task_lastSyncedAt?: STRt = new StrInfo('last update time for task node');
_calendar_date?: DateInfo | DateField = new DateInfo('current selected date of a calendar', /*filterable*/ false);
_calendar_dateRange?: STRt = new StrInfo('date range shown on a calendar', false);
diff --git a/src/client/views/nodes/TaskBox.tsx b/src/client/views/nodes/TaskBox.tsx
index 8e269a21a..94252fafc 100644
--- a/src/client/views/nodes/TaskBox.tsx
+++ b/src/client/views/nodes/TaskBox.tsx
@@ -232,56 +232,165 @@ export class TaskBox extends ViewBoxBaseComponent<FieldViewProps>() {
* @returns - a Promise that resolves when the sync is complete
*/
autoSyncToGoogle = async () => {
- if (!this._needsSync) return;
+ if (!this._needsSync && !this.Document.$googleTaskId) return;
+ await this.syncWithGoogleTaskBidirectional();
+
+ // if (!this._needsSync) return;
+
+ // const token = await GoogleAuthenticationManager.Instance.fetchOrGenerateAccessToken();
+ // if (!token) return;
+
+ // runInAction(() => {
+ // this._syncing = true;
+ // });
+ // 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);
+ // }
+
+ // runInAction(() => {
+ // this._syncing = false;
+ // });
+ };
- const token = await GoogleAuthenticationManager.Instance.fetchOrGenerateAccessToken();
- if (!token) return;
+ /**
+ * 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();
+ }
+ };
- this._syncing = true;
+ syncWithGoogleTaskBidirectional = async (): Promise<boolean> => {
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 token = await GoogleAuthenticationManager.Instance.fetchOrGenerateAccessToken();
- const body = this.buildGoogleTaskBody();
+ if (!token) {
+ const listener = () => {
+ window.removeEventListener('focusin', listener);
+ if (confirm('✅ Authorization complete. Try syncing the task again?')) {
+ // you could refactor the click handler here
+ this.syncWithGoogleTaskBidirectional();
+ }
+ window.removeEventListener('focusin', listener);
+ };
+ setTimeout(() => window.addEventListener('focusin', listener), 100);
+ return false;
+ }
- const isUpdate = !!doc.$googleTaskId;
- const endpoint = isUpdate ? `/googleTasks/${doc.$googleTaskId}` : '/googleTasks/create';
- const method = isUpdate ? 'PATCH' : 'POST';
+ if (!doc.$googleTaskId) return false;
- const res = await fetch(endpoint, {
- method,
- credentials: 'include',
- headers: {
- 'Content-Type': 'application/json',
- Authorization: `Bearer ${token}`,
- },
- body: JSON.stringify(body),
+ runInAction(() => {
+ this._syncing = true;
});
- const result = await res.json();
- if (result?.id) {
- runInAction(() => {
- this._lastSyncedTask = { title: taskTitle, text: taskDesc, due, completed: isCompleted };
- this._needsSync = false;
+ try {
+ // Fetch current version of Google Task
+ const response = await fetch(`/googleTasks/${doc.$googleTaskId}`, {
+ method: 'GET',
+ headers: {
+ Authorization: `Bearer ${token}`,
+ },
+ credentials: 'include',
});
- console.log('✅ Auto-synced with Google Tasks on blur');
- } else {
- console.warn('❌ Auto-sync failed:', result);
- }
- this._syncing = false;
- };
+ const googleTask = await response.json();
+ const googleUpdated = new Date(googleTask.updated);
+ const dashUpdated = new Date(StrCast(doc.$task_lastSyncedAt));
+
+ if (googleUpdated > dashUpdated) {
+ // 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]}`;
+ }
- /**
- * 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();
+ doc.$task_lastSyncedAt = googleTask.updated;
+ this._lastSyncedTask = {
+ title: StrCast(doc.title),
+ text: StrCast(doc[this.fieldKey]),
+ due: this.computeDueDate(),
+ completed: !!doc.$task_completed,
+ };
+ this._needsSync = false;
+ });
+
+ console.log('Pulled newer version from Google');
+ 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,
+ };
+ 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;
+ });
}
};
@@ -320,6 +429,8 @@ export class TaskBox extends ViewBoxBaseComponent<FieldViewProps>() {
} catch (err) {
console.error('❌ Error creating Google Task:', err);
}
+ } else if (doc.$googleTaskId) {
+ await this.syncWithGoogleTaskBidirectional();
}
})();
@@ -430,60 +541,67 @@ export class TaskBox extends ViewBoxBaseComponent<FieldViewProps>() {
const handleGoogleTaskSync = async () => {
console.log('GT button clicked');
+ const success = await this.syncWithGoogleTaskBidirectional();
- try {
- const token = await GoogleAuthenticationManager.Instance.fetchOrGenerateAccessToken();
- if (!token) {
- const listener = () => {
- window.removeEventListener('focusin', listener);
- if (confirm('✅ Authorization complete. Try syncing the task again?')) {
- // you could refactor the click handler here
- handleGoogleTaskSync();
- }
- window.removeEventListener('focusin', listener);
- };
- setTimeout(() => window.addEventListener('focusin', listener), 100);
- return;
- }
-
- this._syncing = true;
- const body = this.buildGoogleTaskBody();
- const isUpdate = !!doc.$googleTaskId;
- const endpoint = isUpdate ? `/googleTasks/${doc.$googleTaskId}` : '/googleTasks/create';
- const method = isUpdate ? 'PATCH' : 'POST';
-
- const response = await fetch(endpoint, {
- method,
- credentials: 'include',
- headers: {
- 'Content-Type': 'application/json',
- Authorization: `Bearer ${token}`,
- },
- body: JSON.stringify(body),
- });
-
- const result = await response.json();
- console.log('Google Task result:', result);
-
- if (result?.id) {
- alert('✅ Task synced with Google Tasks!');
- if (result?.id) {
- this._lastSyncedTask = {
- title: taskTitle,
- text: taskDesc,
- due,
- completed: isCompleted,
- };
- runInAction(() => (this._needsSync = false));
- }
- } else {
- alert(`❌ Failed: ${result?.error?.message || 'Unknown error'}`);
- }
- } catch (err) {
- console.error('Fetch error:', err);
- alert('❌ Task syncing failed.');
+ if (success) {
+ alert('✅ Task successfully synced!');
+ } else {
+ alert('❌ Task sync failed.');
}
- this._syncing = false;
+
+ // try {
+ // const token = await GoogleAuthenticationManager.Instance.fetchOrGenerateAccessToken();
+ // if (!token) {
+ // const listener = () => {
+ // window.removeEventListener('focusin', listener);
+ // if (confirm('✅ Authorization complete. Try syncing the task again?')) {
+ // // you could refactor the click handler here
+ // handleGoogleTaskSync();
+ // }
+ // window.removeEventListener('focusin', listener);
+ // };
+ // setTimeout(() => window.addEventListener('focusin', listener), 100);
+ // return;
+ // }
+
+ // this._syncing = true;
+ // const body = this.buildGoogleTaskBody();
+ // const isUpdate = !!doc.$googleTaskId;
+ // const endpoint = isUpdate ? `/googleTasks/${doc.$googleTaskId}` : '/googleTasks/create';
+ // const method = isUpdate ? 'PATCH' : 'POST';
+
+ // const response = await fetch(endpoint, {
+ // method,
+ // credentials: 'include',
+ // headers: {
+ // 'Content-Type': 'application/json',
+ // Authorization: `Bearer ${token}`,
+ // },
+ // body: JSON.stringify(body),
+ // });
+
+ // const result = await response.json();
+ // console.log('Google Task result:', result);
+
+ // if (result?.id) {
+ // alert('✅ Task synced with Google Tasks!');
+ // if (result?.id) {
+ // this._lastSyncedTask = {
+ // title: taskTitle,
+ // text: taskDesc,
+ // due,
+ // completed: isCompleted,
+ // };
+ // runInAction(() => (this._needsSync = false));
+ // }
+ // } else {
+ // alert(`❌ Failed: ${result?.error?.message || 'Unknown error'}`);
+ // }
+ // } catch (err) {
+ // console.error('Fetch error:', err);
+ // alert('❌ Task syncing failed.');
+ // }
+ // this._syncing = false;
};
return (
@@ -529,12 +647,12 @@ export class TaskBox extends ViewBoxBaseComponent<FieldViewProps>() {
<button
className="task-manager-google"
- disabled={!this.needsSync}
+ // disabled={!this.needsSync}
onClick={event => {
event.preventDefault();
handleGoogleTaskSync();
}}>
- {this._syncing ? 'Syncing...' : (this.needsSync ? 'Sync to Google' : 'Synced to Google')}
+ {this._syncing ? 'Syncing...' : this.needsSync ? 'Push Updates' : 'Sync Task'}
</button>
{!allDay && (
diff --git a/src/server/ApiManagers/GeneralGoogleManager.ts b/src/server/ApiManagers/GeneralGoogleManager.ts
index 7a1969bf4..b237178f4 100644
--- a/src/server/ApiManagers/GeneralGoogleManager.ts
+++ b/src/server/ApiManagers/GeneralGoogleManager.ts
@@ -167,5 +167,34 @@ export default class GeneralGoogleManager extends ApiManager {
.finally(resolve)
),
});
+
+ // Task Retrieval
+ register({
+ method: Method.GET,
+ subscription: new RouteSubscriber('googleTasks').add('taskId'),
+ secureHandler: async ({ req, res, user }) => {
+ try {
+ const auth = await GoogleApiServerUtils.retrieveOAuthClient(user.id);
+
+ if (!auth) {
+ return res.status(401).send('Google credentials missing or invalid.');
+ }
+
+ const tasks = google.tasks({ version: 'v1', auth });
+ const { taskId } = req.params;
+
+ const result = await tasks.tasks.get({
+ tasklist: '@default',
+ task: taskId,
+ });
+
+ res.status(200).send(result.data);
+ } catch (err) {
+ console.error('Google Tasks retrieval error:', err);
+ res.status(500).send('Failed to retrieve task.');
+ }
+ },
+ });
}
+
}
diff --git a/src/server/apis/google/google_project_credentials.json b/src/server/apis/google/google_project_credentials.json
index 8abc13b80..438e5157e 100644
--- a/src/server/apis/google/google_project_credentials.json
+++ b/src/server/apis/google/google_project_credentials.json
@@ -8,4 +8,4 @@
"client_secret": "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",
"redirect_uris": ["http://localhost:1050/refreshGoogle"]
}
-}
+} \ No newline at end of file