From 645df1d00f953524c6da22103d26c38ae4331cd6 Mon Sep 17 00:00:00 2001 From: Skitty1238 <157652284+Skitty1238@users.noreply.github.com> Date: Wed, 4 Jun 2025 13:59:34 -0400 Subject: partial google calendar task integration + fix with dark mode colors for task nodes so that text is still visible --- src/server/apis/google/google_project_credentials.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'src/server/apis/google/google_project_credentials.json') diff --git a/src/server/apis/google/google_project_credentials.json b/src/server/apis/google/google_project_credentials.json index 955c5a3c1..738e13647 100644 --- a/src/server/apis/google/google_project_credentials.json +++ b/src/server/apis/google/google_project_credentials.json @@ -1,11 +1,11 @@ { "installed": { - "client_id": "343179513178-ud6tvmh275r2fq93u9eesrnc66t6akh9.apps.googleusercontent.com", - "project_id": "quickstart-1565056383187", + "client_id": "838617994486-a28072lirm8uk8cm78t7ic4krp0rgkgv.apps.googleusercontent.com", + "project_id": "gtasks-test-dash", "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": "w8KIFSc0MQpmUYHed4qEzn8b", + "client_secret": "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", "redirect_uris": ["urn:ietf:wg:oauth:2.0:oob", "http://localhost"] } } \ No newline at end of file -- cgit v1.2.3-70-g09d2 From 09be9002b5aa8f5ad7c602bcef6b53bbe0398cd3 Mon Sep 17 00:00:00 2001 From: Skitty1238 <157652284+Skitty1238@users.noreply.github.com> Date: Wed, 4 Jun 2025 18:28:35 -0400 Subject: fixed google authentication + linking and task creation via GT button --- src/client/apis/GoogleAuthenticationManager.tsx | 98 ++++++++++++++-------- src/client/views/nodes/TaskBox.tsx | 57 ++++++++++++- src/server/ApiManagers/GeneralGoogleManager.ts | 50 +++++++---- src/server/apis/google/GoogleApiServerUtils.ts | 10 ++- .../apis/google/google_project_credentials.json | 2 +- 5 files changed, 161 insertions(+), 56 deletions(-) (limited to 'src/server/apis/google/google_project_credentials.json') diff --git a/src/client/apis/GoogleAuthenticationManager.tsx b/src/client/apis/GoogleAuthenticationManager.tsx index 1b1d6f734..a93e03e60 100644 --- a/src/client/apis/GoogleAuthenticationManager.tsx +++ b/src/client/apis/GoogleAuthenticationManager.tsx @@ -42,46 +42,72 @@ export class GoogleAuthenticationManager extends ObservableReactComponent { - const response = await Networking.FetchFromServer('/readGoogleAccessToken'); - // if this is an authentication url, activate the UI to register the new access token - if (new RegExp(AuthenticationUrl).test(response)) { - this.isOpen = true; - this.authenticationLink = response; - - // GETS STUCK AT THIS PROMISE!! - return new Promise(resolve => { - this.disposer?.(); - this.disposer = reaction( - () => this.authenticationCode, - async authenticationCode => { - if (authenticationCode && /\d{1}\/[\w-]{55}/.test(authenticationCode)) { - resolve(authenticationCode); - this.disposer?.(); - // const response2 = await Networking.PostToServer('/writeGoogleAccessToken', { authenticationCode }); - // runInAction(() => { - // this.success = true; - // this.credentials = response2 as { user_info: { name: string; picture: string }; access_token: string }; - // }); - // resolve((response2 as { access_token: string }).access_token); - this.resetState(); - } - } - ); - }); - } + // public fetchOrGenerateAccessToken = async (displayIfFound = false) => { + // const response = await Networking.FetchFromServer('/readGoogleAccessToken'); + // // if this is an authentication url, activate the UI to register the new access token + + // if (new RegExp(AuthenticationUrl).test(response)) { + // this.isOpen = true; + // this.authenticationLink = response; - // otherwise, we already have a valid, stored access token and user info - const response2 = JSON.parse(response) as { user_info: { name: string; picture: string }; access_token: string }; - if (displayIfFound) { + // // GETS STUCK AT THIS PROMISE!! + // return new Promise(resolve => { + // this.disposer?.(); + // this.disposer = reaction( + // () => this.authenticationCode, + // async authenticationCode => { + // if (authenticationCode && /\d{1}\/[\w-]{55}/.test(authenticationCode)) { + // resolve(authenticationCode); + // this.disposer?.(); + // // const response2 = await Networking.PostToServer('/writeGoogleAccessToken', { authenticationCode }); + // // runInAction(() => { + // // this.success = true; + // // this.credentials = response2 as { user_info: { name: string; picture: string }; access_token: string }; + // // }); + // // resolve((response2 as { access_token: string }).access_token); + // this.resetState(); + // } + // } + // ); + // }); + // } + + // // otherwise, we already have a valid, stored access token and user info + // const response2 = JSON.parse(response) as { user_info: { name: string; picture: string }; access_token: string }; + // if (displayIfFound) { + // runInAction(() => { + // this.success = true; + // this.credentials = response2; + // }); + // this.resetState(-1, -1); + // this.isOpen = true; + // } + // return (response2 as { access_token: string }).access_token; + // }; + + public fetchOrGenerateAccessToken = async (): Promise => { + const response = await Networking.FetchFromServer('/readGoogleAccessToken'); + + // This will return a JSON object with { access_token, user_info } if already linked + try { + const parsed = JSON.parse(response) as { access_token: string; user_info: { name: string; picture: string } }; + runInAction(() => { this.success = true; - this.credentials = response2; + this.credentials = parsed; }); - this.resetState(-1, -1); - this.isOpen = true; + + return parsed.access_token; + } catch (err) { + console.warn('Not linked yet or invalid JSON. Redirecting to auth...'); + // This is an auth URL β€” redirect the user to /refreshGoogle + if (typeof response === 'string' && response.startsWith('http')) { + window.location.href = response; + return ''; // Won’t be used β€” this page will reload anyway + } + + throw new Error('Unable to fetch Google access token.'); } - return (response2 as { access_token: string }).access_token; }; resetState = action((visibleForMS: number = 3000, fadesOutInMS: number = 500) => { @@ -132,7 +158,7 @@ export class GoogleAuthenticationManager extends ObservableReactComponent ) : null} {this.showPasteTargetState ? (this.authenticationCode = e.currentTarget.value))} placeholder={prompt} /> : null} - {this.credentials ? ( + {this.credentials?.user_info?.picture ? ( <> Welcome to Dash, {this.credentials.user_info.name} diff --git a/src/client/views/nodes/TaskBox.tsx b/src/client/views/nodes/TaskBox.tsx index 3990356b9..8855e43c8 100644 --- a/src/client/views/nodes/TaskBox.tsx +++ b/src/client/views/nodes/TaskBox.tsx @@ -6,6 +6,7 @@ import { DocumentType } from '../../documents/DocumentTypes'; import { FieldView } from './FieldView'; import { DateField } from '../../../fields/DateField'; import { Doc } from '../../../fields/Doc'; +import { Networking } from '../../Network'; import './TaskBox.scss'; import { GoogleAuthenticationManager } from '../../apis/GoogleAuthenticationManager'; @@ -270,7 +271,7 @@ export class TaskBox extends React.Component { )} {/** test button */} - */} + + {/* */} + + ); diff --git a/src/server/ApiManagers/GeneralGoogleManager.ts b/src/server/ApiManagers/GeneralGoogleManager.ts index e8debfc12..25589ccb5 100644 --- a/src/server/ApiManagers/GeneralGoogleManager.ts +++ b/src/server/ApiManagers/GeneralGoogleManager.ts @@ -71,24 +71,20 @@ export default class GeneralGoogleManager extends ApiManager { subscription: new RouteSubscriber('googleTasks').add('create'), secureHandler: async ({ req, res, user }) => { try { - const { credentials } = await GoogleApiServerUtils.retrieveCredentials(user.id); - const access_token = user.googleToken || credentials?.access_token; // if googleToken expires, we need to renew it. - - if (!access_token) { - return res.status(401).send('Google access token not found.'); + const auth = await GoogleApiServerUtils.retrieveOAuthClient(user); + + if (!auth) { + return res.status(401).send('Google credentials missing or invalid.'); } - - const auth = new google.auth.OAuth2(); - auth.setCredentials({ access_token: access_token }); - + const tasks = google.tasks({ version: 'v1', auth }); - - const { title, notes, due } = req.body; + + const { title, notes, due, status, completed } = req.body; const result = await tasks.tasks.insert({ tasklist: '@default', - requestBody: { title, notes, due }, + requestBody: { title, notes, due, status, completed}, }); - + res.status(200).send(result.data); } catch (err) { console.error('Google Tasks error:', err); @@ -102,9 +98,31 @@ export default class GeneralGoogleManager extends ApiManager { subscription: '/refreshGoogle', secureHandler: async ({ user, req, res }) => { const code = req.query.code as string; - _success(res, code); - user.googleToken = code; - user.save(); + console.log('/refreshGoogle hit with code:', code); + + try { + const enriched = await GoogleApiServerUtils.processNewUser(user.id, code); + + if (enriched.refresh_token) { + console.log('Enriched credentials:', enriched); + + if (enriched.refresh_token) { + user.googleToken = enriched.refresh_token; + await user.save(); + console.log('Saved refresh token to user model'); + } else { + console.warn('No refresh token returned'); + } + } + + // await user.save(); + // _success(res, 'Google account successfully linked!'); + res.redirect('/home'); + + } catch (err) { + console.error('Failed to process Google code:', err); + res.status(500).send('Error linking Google account'); + } // const response2 = await Networking.PostToServer('/writeGoogleAccessToken', { authenticationCode }); // runInAction(() => { diff --git a/src/server/apis/google/GoogleApiServerUtils.ts b/src/server/apis/google/GoogleApiServerUtils.ts index 75f904331..56bc79119 100644 --- a/src/server/apis/google/GoogleApiServerUtils.ts +++ b/src/server/apis/google/GoogleApiServerUtils.ts @@ -59,6 +59,7 @@ export namespace GoogleApiServerUtils { */ export function processProjectCredentials(): void { const { client_secret: clientSecret, client_id: clientId, redirect_uris: redirectUris } = GoogleCredentialsLoader.ProjectCredentials; + console.log('Loaded Google redirect URIs:', redirectUris); // initialize the global authorization client oAuthOptions = { clientId, @@ -191,7 +192,12 @@ export namespace GoogleApiServerUtils { return oauth2Client.generateAuthUrl({ access_type: 'offline', - scope: ['https://www.googleapis.com/auth/tasks'], + scope: [ + 'https://www.googleapis.com/auth/tasks', + 'openid', + 'profile' + ], + prompt: 'consent', // This ensures we get a refresh token }); } @@ -306,7 +312,7 @@ export namespace GoogleApiServerUtils { const headerParameters = { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }; const { client_id, client_secret } = GoogleCredentialsLoader.ProjectCredentials; const params = new URLSearchParams({ - refresh_token: credentials.refresh_token!, // AARAV use user.googleToken + refresh_token: credentials.refresh_token!, // AARAV use user.googleToken client_id, client_secret, grant_type: 'refresh_token', diff --git a/src/server/apis/google/google_project_credentials.json b/src/server/apis/google/google_project_credentials.json index 738e13647..010f9a626 100644 --- a/src/server/apis/google/google_project_credentials.json +++ b/src/server/apis/google/google_project_credentials.json @@ -6,6 +6,6 @@ "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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", - "redirect_uris": ["urn:ietf:wg:oauth:2.0:oob", "http://localhost"] + "redirect_uris": ["http://localhost:1050/refreshGoogle"] } } \ No newline at end of file -- cgit v1.2.3-70-g09d2 From 8d424c8cb4d178d5fb92b6543d63fa409eb6430b Mon Sep 17 00:00:00 2001 From: bobzel Date: Wed, 4 Jun 2025 21:05:03 -0400 Subject: changed google authentication to not reload the page. --- src/client/apis/GoogleAuthenticationManager.tsx | 23 +++++++++--------- src/client/util/SettingsManager.tsx | 2 +- src/client/views/nodes/TaskBox.tsx | 11 ++++++++- src/server/ApiManagers/GeneralGoogleManager.ts | 27 +++++----------------- src/server/apis/google/GoogleApiServerUtils.ts | 16 +++++-------- .../apis/google/google_project_credentials.json | 8 +++---- 6 files changed, 39 insertions(+), 48 deletions(-) (limited to 'src/server/apis/google/google_project_credentials.json') diff --git a/src/client/apis/GoogleAuthenticationManager.tsx b/src/client/apis/GoogleAuthenticationManager.tsx index a93e03e60..ffec07512 100644 --- a/src/client/apis/GoogleAuthenticationManager.tsx +++ b/src/client/apis/GoogleAuthenticationManager.tsx @@ -1,4 +1,4 @@ -import { action, IReactionDisposer, makeObservable, observable, reaction, runInAction } from 'mobx'; +import { action, IReactionDisposer, makeObservable, observable, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { Opt } from '../../fields/Doc'; @@ -8,7 +8,6 @@ import { MainViewModal } from '../views/MainViewModal'; import './GoogleAuthenticationManager.scss'; import { ObservableReactComponent } from '../views/ObservableReactComponent'; -const AuthenticationUrl = 'https://accounts.google.com/o/oauth2/v2/auth'; const prompt = 'Paste authorization code here...'; @observer @@ -85,27 +84,29 @@ export class GoogleAuthenticationManager extends ObservableReactComponent => { + public fetchOrGenerateAccessToken = async (): Promise => { const response = await Networking.FetchFromServer('/readGoogleAccessToken'); - + // This will return a JSON object with { access_token, user_info } if already linked try { const parsed = JSON.parse(response) as { access_token: string; user_info: { name: string; picture: string } }; - + runInAction(() => { this.success = true; this.credentials = parsed; }); - + return parsed.access_token; - } catch (err) { - console.warn('Not linked yet or invalid JSON. Redirecting to auth...'); + } catch { + console.warn('Not linked yet or invalid JSON. open auth...'); // This is an auth URL β€” redirect the user to /refreshGoogle if (typeof response === 'string' && response.startsWith('http')) { - window.location.href = response; - return ''; // Won’t be used β€” this page will reload anyway + if (window.confirm('Authorize Dash to access your Google tasks?')) { + window.open(response)?.focus(); + return undefined; + } } - + throw new Error('Unable to fetch Google access token.'); } }; diff --git a/src/client/util/SettingsManager.tsx b/src/client/util/SettingsManager.tsx index 9e79fd870..f54dea90c 100644 --- a/src/client/util/SettingsManager.tsx +++ b/src/client/util/SettingsManager.tsx @@ -40,7 +40,7 @@ export class SettingsManager extends React.Component { @observable private _activeTab = 'Accounts'; @observable private _isOpen = false; - private googleAuthorize = action(() => GoogleAuthenticationManager.Instance.fetchOrGenerateAccessToken(true)); + private googleAuthorize = action(() => GoogleAuthenticationManager.Instance.fetchOrGenerateAccessToken()); public closeMgr = action(() => { this._isOpen = false; diff --git a/src/client/views/nodes/TaskBox.tsx b/src/client/views/nodes/TaskBox.tsx index 8855e43c8..df81d9c69 100644 --- a/src/client/views/nodes/TaskBox.tsx +++ b/src/client/views/nodes/TaskBox.tsx @@ -6,7 +6,6 @@ import { DocumentType } from '../../documents/DocumentTypes'; import { FieldView } from './FieldView'; import { DateField } from '../../../fields/DateField'; import { Doc } from '../../../fields/Doc'; -import { Networking } from '../../Network'; import './TaskBox.scss'; import { GoogleAuthenticationManager } from '../../apis/GoogleAuthenticationManager'; @@ -324,6 +323,16 @@ export class TaskBox extends React.Component { console.log('GT button clicked'); try { const token = await GoogleAuthenticationManager.Instance.fetchOrGenerateAccessToken(); + if (token === undefined) { + const listener = () => { + if (confirm('βœ… Try again?')) { + // refactor this click function and call it again + } + window.removeEventListener('focusin', listener); + }; + setTimeout(() => window.addEventListener('focusin', listener), 100); + return; + } console.log('Got token', token); const response = await fetch('/googleTasks/create', { diff --git a/src/server/ApiManagers/GeneralGoogleManager.ts b/src/server/ApiManagers/GeneralGoogleManager.ts index 25589ccb5..59d066934 100644 --- a/src/server/ApiManagers/GeneralGoogleManager.ts +++ b/src/server/ApiManagers/GeneralGoogleManager.ts @@ -1,5 +1,5 @@ import ApiManager, { Registration } from './ApiManager'; -import { Method, _success } from '../RouteManager'; +import { Method } from '../RouteManager'; import { GoogleApiServerUtils } from '../apis/google/GoogleApiServerUtils'; import RouteSubscriber from '../RouteSubscriber'; import { Database } from '../database'; @@ -72,19 +72,19 @@ export default class GeneralGoogleManager extends ApiManager { secureHandler: async ({ req, res, user }) => { try { const auth = await GoogleApiServerUtils.retrieveOAuthClient(user); - + if (!auth) { return res.status(401).send('Google credentials missing or invalid.'); } - + const tasks = google.tasks({ version: 'v1', auth }); - + const { title, notes, due, status, completed } = req.body; const result = await tasks.tasks.insert({ tasklist: '@default', - requestBody: { title, notes, due, status, completed}, + requestBody: { title, notes, due, status, completed }, }); - + res.status(200).send(result.data); } catch (err) { console.error('Google Tasks error:', err); @@ -98,37 +98,22 @@ export default class GeneralGoogleManager extends ApiManager { subscription: '/refreshGoogle', secureHandler: async ({ user, req, res }) => { const code = req.query.code as string; - console.log('/refreshGoogle hit with code:', code); try { const enriched = await GoogleApiServerUtils.processNewUser(user.id, code); if (enriched.refresh_token) { - console.log('Enriched credentials:', enriched); - if (enriched.refresh_token) { user.googleToken = enriched.refresh_token; await user.save(); - console.log('Saved refresh token to user model'); } else { console.warn('No refresh token returned'); } } - - // await user.save(); - // _success(res, 'Google account successfully linked!'); - res.redirect('/home'); - } catch (err) { console.error('Failed to process Google code:', err); res.status(500).send('Error linking Google account'); } - - // const response2 = await Networking.PostToServer('/writeGoogleAccessToken', { authenticationCode }); - // runInAction(() => { - // this.success = true; - // this.credentials = response2 as { user_info: { name: string; picture: string }; access_token: string }; - // }); }, }); } diff --git a/src/server/apis/google/GoogleApiServerUtils.ts b/src/server/apis/google/GoogleApiServerUtils.ts index 56bc79119..2f7ef473c 100644 --- a/src/server/apis/google/GoogleApiServerUtils.ts +++ b/src/server/apis/google/GoogleApiServerUtils.ts @@ -185,18 +185,14 @@ export namespace GoogleApiServerUtils { */ export function generateAuthenticationUrl(): string { const oauth2Client = new google.auth.OAuth2( - '838617994486-a28072lirm8uk8cm78t7ic4krp0rgkgv.apps.googleusercontent.com', - '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', + '740987818053-dtflji3hfkn5r9t8ad6jb8740pls8moh.apps.googleusercontent.com', + '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', 'http://localhost:1050/refreshGoogle' // Ensure this matches the redirect URI in Google Cloud Console ); return oauth2Client.generateAuthUrl({ access_type: 'offline', - scope: [ - 'https://www.googleapis.com/auth/tasks', - 'openid', - 'profile' - ], + scope: ['https://www.googleapis.com/auth/tasks', 'openid', 'profile'], prompt: 'consent', // This ensures we get a refresh token }); } @@ -220,12 +216,12 @@ export namespace GoogleApiServerUtils { */ export async function processNewUser(userId: string, authenticationCode: string): Promise { const credentials = await new Promise((resolve, reject) => { - worker.getToken(authenticationCode, (err, credentials) => { - if (err || !credentials) { + worker.getToken(authenticationCode, (err, creds) => { + if (err || !creds) { reject(err); return; } - resolve(credentials); + resolve(creds); }); }); const enriched = injectUserInfo(credentials); diff --git a/src/server/apis/google/google_project_credentials.json b/src/server/apis/google/google_project_credentials.json index 010f9a626..8abc13b80 100644 --- a/src/server/apis/google/google_project_credentials.json +++ b/src/server/apis/google/google_project_credentials.json @@ -1,11 +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"] } -} \ No newline at end of file +} -- cgit v1.2.3-70-g09d2 From 87bd169df4a4384e41b32b3252b3b3c6618f4510 Mon Sep 17 00:00:00 2001 From: Skitty1238 <157652284+Skitty1238@users.noreply.github.com> Date: Thu, 5 Jun 2025 11:25:20 -0400 Subject: fixed being able to add tasks to Google Tasks. Cleaned up google authorization button clicks + alerts --- src/client/apis/GoogleAuthenticationManager.tsx | 2 +- src/client/views/nodes/TaskBox.tsx | 109 ++++++++++++++------- .../apis/google/google_project_credentials.json | 7 +- 3 files changed, 77 insertions(+), 41 deletions(-) (limited to 'src/server/apis/google/google_project_credentials.json') diff --git a/src/client/apis/GoogleAuthenticationManager.tsx b/src/client/apis/GoogleAuthenticationManager.tsx index 70e1ccc05..23e658329 100644 --- a/src/client/apis/GoogleAuthenticationManager.tsx +++ b/src/client/apis/GoogleAuthenticationManager.tsx @@ -101,7 +101,7 @@ export class GoogleAuthenticationManager extends ObservableReactComponent { const isCompleted = !!this.props.Document.$task_completed; const startTime = doc.$task_startTime instanceof DateField && doc.$task_startTime.date instanceof Date ? toLocalDateTimeString(doc.$task_startTime.date) : ''; - const endTime = doc.$task_endTime instanceof DateField && doc.$task_endTime.date instanceof Date ? toLocalDateTimeString(doc.$task_endTime.date) : ''; + const handleGoogleTaskClick = async () => { + console.log('GT button clicked'); + + try { + const token = await GoogleAuthenticationManager.Instance.fetchOrGenerateAccessToken(); + if (!token) { + const listener = () => { + window.removeEventListener('focusin', listener); + if (confirm('βœ… Authorization complete. Try adding the task again?')) { + // you could refactor the click handler here + handleGoogleTaskClick(); + } + window.removeEventListener('focusin', listener); + }; + setTimeout(() => window.addEventListener('focusin', listener), 100); + return; + } + + let due: string | undefined; + + if (allDay) { + const rawRange = typeof doc.$task_dateRange === 'string' ? 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 response = await fetch('/googleTasks/create', { + method: 'POST', + credentials: 'include', + headers: { + '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, + }), + }); + + const result = await response.json(); + console.log('Google Task result:', result); + + if (result?.id) { + alert('βœ… Task sent to Google Tasks!'); + } else { + alert(`❌ Failed: ${result?.error?.message || 'Unknown error'}`); + } + } catch (err) { + console.error('Fetch error:', err); + alert('❌ Task creation failed.'); + } + }; + return (
@@ -257,44 +325,11 @@ export class TaskBox extends React.Component {
diff --git a/src/server/apis/google/google_project_credentials.json b/src/server/apis/google/google_project_credentials.json index 8abc13b80..8a9a38dac 100644 --- a/src/server/apis/google/google_project_credentials.json +++ b/src/server/apis/google/google_project_credentials.json @@ -1,11 +1,12 @@ { "installed": { - "client_id": "740987818053-dtflji3hfkn5r9t8ad6jb8740pls8moh.apps.googleusercontent.com", - "project_id": "dash-web-461920", + "client_id": "838617994486-a28072lirm8uk8cm78t7ic4krp0rgkgv.apps.googleusercontent.com", + "project_id": "gtasks-test-dash", "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"] } } + -- cgit v1.2.3-70-g09d2 From adeb3a05a28cc69795f29f92b049b00e11fe1e46 Mon Sep 17 00:00:00 2001 From: Skitty1238 <157652284+Skitty1238@users.noreply.github.com> Date: Fri, 6 Jun 2025 11:36:30 -0400 Subject: implemented auto-sync to Google when task node loses focus. Created helpers for retrieiving due date and body for Google Task, to reduce redundant code --- src/client/views/nodes/TaskBox.tsx | 339 ++++++++++++--------- .../apis/google/google_project_credentials.json | 7 +- 2 files changed, 197 insertions(+), 149 deletions(-) (limited to 'src/server/apis/google/google_project_credentials.json') 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() { - /** - * 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() { 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; } @@ -54,11 +49,19 @@ export class TaskBox extends ViewBoxBaseComponent() { makeObservable(this); } + /** + * 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) => { this.Document[this.fieldKey] = e.target.value; @@ -171,38 +174,127 @@ export class TaskBox extends ViewBoxBaseComponent() { 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 { + 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) => { + // 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() { 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() { 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() { ); } + /** + * 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() { 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() { 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() { '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() { }; return ( -
- - -