diff options
-rw-r--r-- | src/client/documents/Documents.ts | 1 | ||||
-rw-r--r-- | src/client/views/nodes/TaskBox.tsx | 304 | ||||
-rw-r--r-- | src/server/ApiManagers/GeneralGoogleManager.ts | 29 | ||||
-rw-r--r-- | src/server/apis/google/google_project_credentials.json | 2 |
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 |