From c96a14c881ea28d820a53a38fa9360814dcb6385 Mon Sep 17 00:00:00 2001
From: Skitty1238 <157652284+Skitty1238@users.noreply.github.com>
Date: Tue, 3 Jun 2025 22:41:11 -0400
Subject: gcal task addition route added -- problems with
 fetchOrGenerateAccessToken

---
 src/client/views/nodes/TaskBox.tsx | 40 ++++++++++++++++++++++++++++++++++++++
 1 file changed, 40 insertions(+)

(limited to 'src/client/views/nodes/TaskBox.tsx')

diff --git a/src/client/views/nodes/TaskBox.tsx b/src/client/views/nodes/TaskBox.tsx
index 9d59746f8..1c7aeeb82 100644
--- a/src/client/views/nodes/TaskBox.tsx
+++ b/src/client/views/nodes/TaskBox.tsx
@@ -8,6 +8,7 @@ import { DateField } from '../../../fields/DateField';
 import { Doc } from '../../../fields/Doc';
 
 import './TaskBox.scss';
+import { GoogleAuthenticationManager } from '../../apis/GoogleAuthenticationManager';
 
 /**
  * Props (reference to document) for Task Box
@@ -267,6 +268,45 @@ export class TaskBox extends React.Component<TaskBoxProps> {
                         </label>
                     </div>
                 )}
+
+                {/** test button */}
+            <button
+            className="task-manager-google"
+            onClick={async () => {
+                console.log('GT button clicked');
+                const token = await GoogleAuthenticationManager.Instance.fetchOrGenerateAccessToken();
+                console.log('Got token', token);
+
+                try {
+                const response = await fetch('/googleTasks/create', {
+                    method: 'POST',
+                    headers: {
+                    'Content-Type': 'application/json',
+                    Authorization: `Bearer ${token}`,
+                    },
+                    body: JSON.stringify({
+                    title: taskTitle || 'Untitled Task',
+                    notes: taskDesc,
+                    due: allDay
+                        ? String(doc.$task_dateRange)?.split('|')[0] + 'T23:59:00Z'
+                        : (doc.$task_endTime instanceof DateField && doc.$task_endTime.date?.toISOString()) || undefined,
+                    }),
+                });
+
+                const result = await response.json();
+                console.log("📬 Google Task result:", result);
+                alert(result?.id ? 'Task sent to Google Tasks!' : `Failed: ${result?.error?.message || 'Unknown error'}`);
+                } catch (err) {
+                console.error("Fetch error:", err);
+                alert("Fetch failed: ");
+                }
+            }}
+            >
+            GT
+            </button>
+
+
+
             </div>
         );
     }
-- 
cgit v1.2.3-70-g09d2


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/client/apis/GoogleAuthenticationManager.tsx    | 35 ++++++-----
 src/client/views/MainViewModal.tsx                 |  2 +-
 src/client/views/nodes/TaskBox.scss                | 11 +++-
 src/client/views/nodes/TaskBox.tsx                 | 69 ++++++++++----------
 src/server/ApiManagers/FireflyManager.ts           | 11 +++-
 src/server/ApiManagers/GeneralGoogleManager.ts     | 73 +++++++++++++---------
 src/server/apis/google/GoogleApiServerUtils.ts     | 39 +++++++-----
 .../apis/google/google_project_credentials.json    |  6 +-
 src/server/authentication/DashUserModel.ts         |  6 ++
 9 files changed, 153 insertions(+), 99 deletions(-)

(limited to 'src/client/views/nodes/TaskBox.tsx')

diff --git a/src/client/apis/GoogleAuthenticationManager.tsx b/src/client/apis/GoogleAuthenticationManager.tsx
index 46581397d..1b1d6f734 100644
--- a/src/client/apis/GoogleAuthenticationManager.tsx
+++ b/src/client/apis/GoogleAuthenticationManager.tsx
@@ -1,4 +1,4 @@
-import { action, IReactionDisposer, observable, reaction, runInAction } from 'mobx';
+import { action, IReactionDisposer, makeObservable, observable, reaction, runInAction } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
 import { Opt } from '../../fields/Doc';
@@ -6,12 +6,13 @@ import { Networking } from '../Network';
 import { ScriptingGlobals } from '../util/ScriptingGlobals';
 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
-export class GoogleAuthenticationManager extends React.Component<object> {
+export class GoogleAuthenticationManager extends ObservableReactComponent<object> {
     // eslint-disable-next-line no-use-before-define
     public static Instance: GoogleAuthenticationManager;
     private authenticationLink: Opt<string> = undefined;
@@ -23,6 +24,12 @@ export class GoogleAuthenticationManager extends React.Component<object> {
     @observable private credentials: { user_info: { name: string; picture: string }; access_token: string } | undefined = undefined;
     private disposer: Opt<IReactionDisposer>;
 
+    constructor(props: object) {
+        super(props);
+        makeObservable(this);
+        GoogleAuthenticationManager.Instance = this;
+    }
+
     private set isOpen(value: boolean) {
         runInAction(() => (this.openState = value));
     }
@@ -49,14 +56,15 @@ export class GoogleAuthenticationManager extends React.Component<object> {
                     () => 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 };
-                            });
+                            // 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();
-                            resolve((response2 as { access_token: string }).access_token);
                         }
                     }
                 );
@@ -109,11 +117,6 @@ export class GoogleAuthenticationManager extends React.Component<object> {
         }
     });
 
-    constructor(props: object) {
-        super(props);
-        GoogleAuthenticationManager.Instance = this;
-    }
-
     private get renderPrompt() {
         return (
             <div className={'authorize-container'}>
@@ -153,7 +156,11 @@ export class GoogleAuthenticationManager extends React.Component<object> {
     }
 
     render() {
-        return <MainViewModal isDisplayed={this.openState} interactive={true} contents={this.renderPrompt} dialogueBoxStyle={this.dialogueBoxStyle} overlayStyle={{ zIndex: 1001 }} closeOnExternalClick={action(() => (this.isOpen = false))} />;
+        return (
+            <div className="hell">
+                <MainViewModal isDisplayed={this.openState} interactive={true} contents={this.renderPrompt} dialogueBoxStyle={this.dialogueBoxStyle} overlayStyle={{ zIndex: 1001 }} closeOnExternalClick={action(() => (this.isOpen = false))} />
+            </div>
+        );
     }
 }
 
diff --git a/src/client/views/MainViewModal.tsx b/src/client/views/MainViewModal.tsx
index b05292c47..d7640dc72 100644
--- a/src/client/views/MainViewModal.tsx
+++ b/src/client/views/MainViewModal.tsx
@@ -41,7 +41,7 @@ export class MainViewModal extends React.Component<MainViewOverlayProps> {
                     className="overlay"
                     onClick={this.props?.closeOnExternalClick}
                     style={{
-                        backgroundColor: isDark(SnappingManager.userColor) ? '#DFDFDF30' : '#32323230',
+                        backgroundColor: isDark(SnappingManager.userColor ?? '') ? '#DFDFDF30' : '#32323230',
                         ...(p.overlayStyle || {}),
                     }}
                 />
diff --git a/src/client/views/nodes/TaskBox.scss b/src/client/views/nodes/TaskBox.scss
index 0fcc2f955..6ef0c6454 100644
--- a/src/client/views/nodes/TaskBox.scss
+++ b/src/client/views/nodes/TaskBox.scss
@@ -6,6 +6,15 @@
     width: 100%;
     height: 100%;
     box-sizing: border-box;
+
+    input,
+    textarea,
+    select,
+    button {
+        background-color: #fff !important;
+        color: #000 !important;
+        border-color: #ccc !important;
+    }
 }
 
 .task-manager-title {
@@ -69,4 +78,4 @@ input[type="datetime-local"] {
     border: 1px solid #ccc;
     border-radius: 6px;
     box-sizing: border-box;
-}
+}
\ No newline at end of file
diff --git a/src/client/views/nodes/TaskBox.tsx b/src/client/views/nodes/TaskBox.tsx
index 1c7aeeb82..3990356b9 100644
--- a/src/client/views/nodes/TaskBox.tsx
+++ b/src/client/views/nodes/TaskBox.tsx
@@ -270,43 +270,38 @@ export class TaskBox extends React.Component<TaskBoxProps> {
                 )}
 
                 {/** test button */}
-            <button
-            className="task-manager-google"
-            onClick={async () => {
-                console.log('GT button clicked');
-                const token = await GoogleAuthenticationManager.Instance.fetchOrGenerateAccessToken();
-                console.log('Got token', token);
-
-                try {
-                const response = await fetch('/googleTasks/create', {
-                    method: 'POST',
-                    headers: {
-                    'Content-Type': 'application/json',
-                    Authorization: `Bearer ${token}`,
-                    },
-                    body: JSON.stringify({
-                    title: taskTitle || 'Untitled Task',
-                    notes: taskDesc,
-                    due: allDay
-                        ? String(doc.$task_dateRange)?.split('|')[0] + 'T23:59:00Z'
-                        : (doc.$task_endTime instanceof DateField && doc.$task_endTime.date?.toISOString()) || undefined,
-                    }),
-                });
-
-                const result = await response.json();
-                console.log("📬 Google Task result:", result);
-                alert(result?.id ? 'Task sent to Google Tasks!' : `Failed: ${result?.error?.message || 'Unknown error'}`);
-                } catch (err) {
-                console.error("Fetch error:", err);
-                alert("Fetch failed: ");
-                }
-            }}
-            >
-            GT
-            </button>
-
-
-
+                <button
+                    className="task-manager-google"
+                    onClick={async () => {
+                        console.log('GT button clicked');
+                        const token = await GoogleAuthenticationManager.Instance.fetchOrGenerateAccessToken();
+                        console.log('Got token', token);
+
+                        try {
+                            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: allDay ? String(doc.$task_dateRange)?.split('|')[0] + 'T23:59:00Z' : (doc.$task_endTime instanceof DateField && doc.$task_endTime.date?.toISOString()) || undefined,
+                                }),
+                            });
+
+                            const result = await response.text();
+                            console.log('Google Task result:', result);
+                            alert(result?.id ? 'Task sent to Google Tasks!' : `Failed: ${result?.error?.message || 'Unknown error'}`);
+                        } catch (err) {
+                            console.error('Fetch error:', err);
+                            alert('Fetch failed: ');
+                        }
+                    }}>
+                    GT
+                </button>
             </div>
         );
     }
diff --git a/src/server/ApiManagers/FireflyManager.ts b/src/server/ApiManagers/FireflyManager.ts
index 07ef271a1..6393a1f74 100644
--- a/src/server/ApiManagers/FireflyManager.ts
+++ b/src/server/ApiManagers/FireflyManager.ts
@@ -23,7 +23,16 @@ export default class FireflyManager extends ApiManager {
             return undefined;
         });
 
-    generateImageFromStructure = (prompt: string = 'a realistic illustration of a cat coding', width: number = 2048, height: number = 2048, structureUrl: string, strength: number = 50, styles: string[], styleUrl: string | undefined, variations: number = 4) =>
+    generateImageFromStructure = (
+        prompt: string = 'a realistic illustration of a cat coding',
+        width: number = 2048,
+        height: number = 2048,
+        structureUrl: string,
+        strength: number = 50,
+        styles: string[],
+        styleUrl: string | undefined,
+        variations: number = 4
+    ) =>
         this.getBearerToken().then(response =>
             response?.json().then((data: { access_token: string }) =>
                 //prettier-ignore
diff --git a/src/server/ApiManagers/GeneralGoogleManager.ts b/src/server/ApiManagers/GeneralGoogleManager.ts
index aa06ca1b3..e8debfc12 100644
--- a/src/server/ApiManagers/GeneralGoogleManager.ts
+++ b/src/server/ApiManagers/GeneralGoogleManager.ts
@@ -1,11 +1,10 @@
 import ApiManager, { Registration } from './ApiManager';
-import { Method } from '../RouteManager';
+import { Method, _success } from '../RouteManager';
 import { GoogleApiServerUtils } from '../apis/google/GoogleApiServerUtils';
 import RouteSubscriber from '../RouteSubscriber';
 import { Database } from '../database';
 import { google } from 'googleapis';
 
-
 const EndpointHandlerMap = new Map<GoogleApiServerUtils.Action, GoogleApiServerUtils.ApiRouter>([
     ['create', (api, params) => api.create(params)],
     ['retrieve', (api, params) => api.get(params)],
@@ -18,9 +17,10 @@ export default class GeneralGoogleManager extends ApiManager {
             method: Method.GET,
             subscription: '/readGoogleAccessToken',
             secureHandler: async ({ user, res }) => {
-                const { credentials } = await GoogleApiServerUtils.retrieveCredentials(user.id);
+                const { credentials } = await GoogleApiServerUtils.retrieveCredentials(user);
                 if (!credentials?.access_token) {
-                    return res.send(GoogleApiServerUtils.generateAuthenticationUrl());
+                    const url = GoogleApiServerUtils.generateAuthenticationUrl();
+                    return res.send(url);
                 }
                 return res.send(credentials);
             },
@@ -49,7 +49,7 @@ export default class GeneralGoogleManager extends ApiManager {
             secureHandler: async ({ req, res, user }) => {
                 const sector: GoogleApiServerUtils.Service = req.params.sector as GoogleApiServerUtils.Service;
                 const action: GoogleApiServerUtils.Action = req.params.action as GoogleApiServerUtils.Action;
-                const endpoint = await GoogleApiServerUtils.GetEndpoint(GoogleApiServerUtils.Service[sector], user.id);
+                const endpoint = await GoogleApiServerUtils.GetEndpoint(GoogleApiServerUtils.Service[sector], user);
                 const handler = EndpointHandlerMap.get(action);
                 if (endpoint && handler) {
                     try {
@@ -70,31 +70,48 @@ export default class GeneralGoogleManager extends ApiManager {
             method: Method.POST,
             subscription: new RouteSubscriber('googleTasks').add('create'),
             secureHandler: async ({ req, res, user }) => {
-              try {
-                const { credentials } = await GoogleApiServerUtils.retrieveCredentials(user.id);
-                if (!credentials?.access_token) {
-                  return res.status(401).send('Google access token not found.');
+                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 = new google.auth.OAuth2();
+                    auth.setCredentials({ access_token: access_token });
+
+                    const tasks = google.tasks({ version: 'v1', auth });
+
+                    const { title, notes, due } = req.body;
+                    const result = await tasks.tasks.insert({
+                        tasklist: '@default',
+                        requestBody: { title, notes, due },
+                    });
+
+                    res.status(200).send(result.data);
+                } catch (err) {
+                    console.error('Google Tasks error:', err);
+                    res.status(500).send('Failed to create task.');
                 }
-          
-                const auth = new google.auth.OAuth2();
-                auth.setCredentials({ access_token: credentials.access_token });
-          
-                const tasks = google.tasks({ version: 'v1', auth });
-          
-                const { title, notes, due } = req.body;
-                const result = await tasks.tasks.insert({
-                  tasklist: '@default',
-                  requestBody: { title, notes, due },
-                });
-          
-                res.status(200).send(result.data);
-              } catch (err) {
-                console.error('Google Tasks error:', err);
-                res.status(500).send('Failed to create task.');
-              }
             },
-          });
+        });
 
-        
+        register({
+            method: Method.GET,
+            subscription: '/refreshGoogle',
+            secureHandler: async ({ user, req, res }) => {
+                const code = req.query.code as string;
+                _success(res, code);
+                user.googleToken = code;
+                user.save();
+
+                // 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 7373df473..75f904331 100644
--- a/src/server/apis/google/GoogleApiServerUtils.ts
+++ b/src/server/apis/google/GoogleApiServerUtils.ts
@@ -6,6 +6,7 @@ import * as request from 'request-promise';
 import { Opt } from '../../../fields/Doc';
 import { Database } from '../../database';
 import { GoogleCredentialsLoader } from './CredentialsLoader';
+import { DashUserModel } from '../../authentication/DashUserModel';
 
 /**
  * Scopes give Google users fine granularity of control
@@ -13,7 +14,7 @@ import { GoogleCredentialsLoader } from './CredentialsLoader';
  * This is the somewhat overkill list of what Dash requests
  * from the user.
  */
-const scope = ['documents.readonly', 'documents', 'presentations', 'presentations.readonly', 'drive', 'drive.file', 'photoslibrary', 'photoslibrary.appendonly', 'photoslibrary.sharing', 'userinfo.profile'].map(
+const scope = ['tasks', 'documents.readonly', 'documents', 'presentations', 'presentations.readonly', 'drive', 'drive.file', 'photoslibrary', 'photoslibrary.appendonly', 'photoslibrary.sharing', 'userinfo.profile'].map(
     relative => `https://www.googleapis.com/auth/${relative}`
 );
 
@@ -118,8 +119,9 @@ export namespace GoogleApiServerUtils {
      * @param userId the id of the Dash user making the request to the API
      * @returns the relevant 'googleapis' wrapper, if any
      */
-    export async function GetEndpoint(sector: string, userId: string): Promise<Endpoint | void> {
-        const auth = await retrieveOAuthClient(userId);
+    export async function GetEndpoint(sector: string, user: DashUserModel): Promise<Endpoint | void> {
+        if (!user.googleToken) await retrieveOAuthClient(user);
+        const auth = user.googleToken; // await retrieveOAuthClient(user);
         if (!auth) {
             return;
         }
@@ -145,14 +147,14 @@ export namespace GoogleApiServerUtils {
      * npm-installed API wrappers that use authenticated client instances rather than access codes for
      * security.
      */
-    export async function retrieveOAuthClient(userId: string): Promise<OAuth2Client | void> {
-        const { credentials, refreshed } = await retrieveCredentials(userId);
+    export async function retrieveOAuthClient(user: DashUserModel): Promise<OAuth2Client | void> {
+        const { credentials, refreshed } = await retrieveCredentials(user);
         if (!credentials) {
             return;
         }
-        let client = authenticationClients.get(userId);
+        let client = authenticationClients.get(user.id);
         if (!client) {
-            authenticationClients.set(userId, (client = generateClient(credentials)));
+            authenticationClients.set(user.id, (client = generateClient(credentials)));
         } else if (refreshed) {
             client.setCredentials(credentials);
         }
@@ -181,7 +183,16 @@ export namespace GoogleApiServerUtils {
      * @returns the newly generated url to the authentication landing page
      */
     export function generateAuthenticationUrl(): string {
-        return worker.generateAuthUrl({ scope, access_type: 'offline' });
+        const oauth2Client = new google.auth.OAuth2(
+            '838617994486-a28072lirm8uk8cm78t7ic4krp0rgkgv.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'],
+        });
     }
 
     /**
@@ -267,15 +278,15 @@ export namespace GoogleApiServerUtils {
      * @returns the credentials, or undefined if the user has no stored associated credentials,
      * and a flag indicating whether or not they were refreshed during retrieval
      */
-    export async function retrieveCredentials(userId: string): Promise<{ credentials: Opt<EnrichedCredentials>; refreshed: boolean }> {
-        let credentials = await Database.Auxiliary.GoogleAccessToken.Fetch(userId);
+    export async function retrieveCredentials(user: DashUserModel): Promise<{ credentials: Opt<EnrichedCredentials>; refreshed: boolean }> {
+        let credentials = await Database.Auxiliary.GoogleAccessToken.Fetch(user.id);
         let refreshed = false;
         if (!credentials) {
             return { credentials: undefined, refreshed };
         }
         // check for token expiry
         if (credentials.expiry_date! <= new Date().getTime()) {
-            credentials = { ...credentials, ...(await refreshAccessToken(credentials, userId)) };
+            credentials = { ...credentials, ...(await refreshAccessToken(credentials, user)) };
             refreshed = true;
         }
         return { credentials, refreshed };
@@ -291,11 +302,11 @@ export namespace GoogleApiServerUtils {
      * his/her credentials be refreshed
      * @returns the updated credentials
      */
-    async function refreshAccessToken(credentials: Credentials, userId: string): Promise<Credentials> {
+    async function refreshAccessToken(credentials: Credentials, user: DashUserModel): Promise<Credentials> {
         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!,
+            refresh_token: credentials.refresh_token!, // AARAV use user.googleToken 
             client_id,
             client_secret,
             grant_type: 'refresh_token',
@@ -306,7 +317,7 @@ export namespace GoogleApiServerUtils {
         });
         // expires_in is in seconds, but we're building the new expiry date in milliseconds
         const expiry_date = new Date().getTime() + expires_in * 1000;
-        await Database.Auxiliary.GoogleAccessToken.Update(userId, access_token, expiry_date);
+        await Database.Auxiliary.GoogleAccessToken.Update(user.id, access_token, expiry_date);
         // update the relevant properties
         credentials.access_token = access_token;
         credentials.expiry_date = expiry_date;
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
diff --git a/src/server/authentication/DashUserModel.ts b/src/server/authentication/DashUserModel.ts
index debeef60c..4397e2bd4 100644
--- a/src/server/authentication/DashUserModel.ts
+++ b/src/server/authentication/DashUserModel.ts
@@ -9,9 +9,14 @@ export type DashUserModel = mongoose.Document & {
     passwordResetToken?: string;
     passwordResetExpires?: Date;
 
+
+    // AARAV ADD
+    googleToken?: string;
+    
     dropboxRefresh?: string;
     dropboxToken?: string;
 
+
     userDocumentId: string;
     sharingDocumentId: string;
     linkDatabaseId: string;
@@ -40,6 +45,7 @@ const userSchema = new mongoose.Schema(
         passwordResetToken: String,
         passwordResetExpires: Date,
 
+        googleToken: String,
         dropboxRefresh: String,
         dropboxToken: String,
         userDocumentId: String, // id that identifies a document which hosts all of a user's account data
-- 
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/client/views/nodes/TaskBox.tsx')

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<object
         this.openState && this.resetState(0, 0);
     }
 
-    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;
-
-            // GETS STUCK AT THIS PROMISE!!
-            return new Promise<string>(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<string>(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<string> => {
+        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<object
                     </button>
                 ) : null}
                 {this.showPasteTargetState ? <input className={'paste-target'} onChange={action(e => (this.authenticationCode = e.currentTarget.value))} placeholder={prompt} /> : null}
-                {this.credentials ? (
+                {this.credentials?.user_info?.picture ? (
                     <>
                         <img className={'avatar'} src={this.credentials.user_info.picture} />
                         <span className={'welcome'}>Welcome to Dash, {this.credentials.user_info.name}</span>
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<TaskBoxProps> {
                 )}
 
                 {/** test button */}
-                <button
+                {/* <button
                     className="task-manager-google"
                     onClick={async () => {
                         console.log('GT button clicked');
@@ -301,6 +302,60 @@ export class TaskBox extends React.Component<TaskBoxProps> {
                         }
                     }}>
                     GT
+                </button> */}
+
+                {/* <button
+                className="task-manager-google"
+                onClick={async () => {
+                    console.log('GT button clicked');
+                    const url = await Networking.FetchFromServer('/readGoogleAccessToken');
+                    console.log('🔗 Redirecting to Google auth:', url);
+                    window.location.href = url; // Redirect user to authenticate
+                }}
+                >
+                GT
+                </button> */}
+
+                <button
+                    className="task-manager-google"
+                    onClick={async event => {
+                        event.preventDefault();
+
+                        console.log('GT button clicked');
+                        try {
+                            const token = await GoogleAuthenticationManager.Instance.fetchOrGenerateAccessToken();
+                            console.log('Got token', token);
+
+                            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: allDay ? String(doc.$task_dateRange)?.split('|')[0] + 'T23:59:00Z' : (doc.$task_endTime instanceof DateField && doc.$task_endTime.date?.toISOString()) || undefined,
+                                    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.');
+                        }
+                    }}>
+                    GT
                 </button>
             </div>
         );
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 <zzzman@gmail.com>
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/client/views/nodes/TaskBox.tsx')

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<object
     //     return (response2 as { access_token: string }).access_token;
     // };
 
-    public fetchOrGenerateAccessToken = async (): Promise<string> => {
+    public fetchOrGenerateAccessToken = async (): Promise<string | undefined> => {
         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<object> {
     @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<TaskBoxProps> {
                         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<EnrichedCredentials> {
         const credentials = await new Promise<Credentials>((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 eb9c93a635191ef9ec842592c4a85262811cf108 Mon Sep 17 00:00:00 2001
From: Skitty1238 <157652284+Skitty1238@users.noreply.github.com>
Date: Wed, 4 Jun 2025 21:05:47 -0400
Subject: allowed for incomplete tasks + fix for all day tasks not being added
 correctly + button styling

---
 src/client/views/nodes/TaskBox.scss |  30 +++++++--
 src/client/views/nodes/TaskBox.tsx  | 130 ++++++++++++------------------------
 2 files changed, 68 insertions(+), 92 deletions(-)

(limited to 'src/client/views/nodes/TaskBox.tsx')

diff --git a/src/client/views/nodes/TaskBox.scss b/src/client/views/nodes/TaskBox.scss
index 6ef0c6454..a7f75acdb 100644
--- a/src/client/views/nodes/TaskBox.scss
+++ b/src/client/views/nodes/TaskBox.scss
@@ -40,7 +40,7 @@
     resize: none;
     line-height: 1.4;
     resize: none;
-    flex-grow: 1
+    flex-grow: 1;
 }
 
 .task-manager-checkboxes {
@@ -49,7 +49,8 @@
     gap: 16px;
 }
 
-.task-manager-allday, .task-manager-complete {
+.task-manager-allday,
+.task-manager-complete {
     display: flex;
     align-items: center;
     gap: 6px;
@@ -71,11 +72,32 @@
     gap: 4px;
 }
 
-input[type="datetime-local"] {
+input[type='datetime-local'] {
     width: 100%;
     font-size: 0.9rem;
     padding: 6px 8px;
     border: 1px solid #ccc;
     border-radius: 6px;
     box-sizing: border-box;
-}
\ No newline at end of file
+}
+
+.task-manager-checkboxes {
+    display: flex;
+    flex-wrap: wrap; /* allows wrapping on small screens */
+    align-items: center;
+    gap: 16px;
+    row-gap: 8px; /* optional: tighter vertical spacing if it wraps */
+}
+
+.task-manager-google {
+    font-size: 0.85rem;
+    padding: 6px 12px;
+    border-radius: 6px;
+    background-color: #4285f4;
+    color: white;
+    border: none;
+    cursor: pointer;
+    white-space: nowrap;
+    transition: background-color 0.2s ease;
+    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
+}
diff --git a/src/client/views/nodes/TaskBox.tsx b/src/client/views/nodes/TaskBox.tsx
index 8855e43c8..0d4fb622b 100644
--- a/src/client/views/nodes/TaskBox.tsx
+++ b/src/client/views/nodes/TaskBox.tsx
@@ -255,6 +255,48 @@ export class TaskBox extends React.Component<TaskBoxProps> {
                         <input type="checkbox" checked={isCompleted} onChange={this.toggleComplete} />
                         Complete
                     </label>
+
+                    <button
+                        className="task-manager-google"
+                        onClick={async event => {
+                            event.preventDefault();
+
+                            console.log('GT button clicked');
+                            try {
+                                const token = await GoogleAuthenticationManager.Instance.fetchOrGenerateAccessToken();
+                                console.log('Got token', token);
+
+                                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: allDay ? new Date(String(doc.$task_dateRange)?.split('|')[0]).toISOString() : (doc.$task_endTime instanceof DateField && doc.$task_endTime.date?.toISOString()) || undefined,
+                                        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.');
+                            }
+                        }}>
+                        Add to Google
+                    </button>
                 </div>
 
                 {!allDay && (
@@ -269,94 +311,6 @@ export class TaskBox extends React.Component<TaskBoxProps> {
                         </label>
                     </div>
                 )}
-
-                {/** test button */}
-                {/* <button
-                    className="task-manager-google"
-                    onClick={async () => {
-                        console.log('GT button clicked');
-                        const token = await GoogleAuthenticationManager.Instance.fetchOrGenerateAccessToken();
-                        console.log('Got token', token);
-
-                        try {
-                            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: allDay ? String(doc.$task_dateRange)?.split('|')[0] + 'T23:59:00Z' : (doc.$task_endTime instanceof DateField && doc.$task_endTime.date?.toISOString()) || undefined,
-                                }),
-                            });
-
-                            const result = await response.text();
-                            console.log('Google Task result:', result);
-                            alert(result?.id ? 'Task sent to Google Tasks!' : `Failed: ${result?.error?.message || 'Unknown error'}`);
-                        } catch (err) {
-                            console.error('Fetch error:', err);
-                            alert('Fetch failed: ');
-                        }
-                    }}>
-                    GT
-                </button> */}
-
-                {/* <button
-                className="task-manager-google"
-                onClick={async () => {
-                    console.log('GT button clicked');
-                    const url = await Networking.FetchFromServer('/readGoogleAccessToken');
-                    console.log('🔗 Redirecting to Google auth:', url);
-                    window.location.href = url; // Redirect user to authenticate
-                }}
-                >
-                GT
-                </button> */}
-
-                <button
-                    className="task-manager-google"
-                    onClick={async event => {
-                        event.preventDefault();
-
-                        console.log('GT button clicked');
-                        try {
-                            const token = await GoogleAuthenticationManager.Instance.fetchOrGenerateAccessToken();
-                            console.log('Got token', token);
-
-                            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: allDay ? String(doc.$task_dateRange)?.split('|')[0] + 'T23:59:00Z' : (doc.$task_endTime instanceof DateField && doc.$task_endTime.date?.toISOString()) || undefined,
-                                    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.');
-                        }
-                    }}>
-                    GT
-                </button>
             </div>
         );
     }
-- 
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/client/views/nodes/TaskBox.tsx')

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<object
             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')) {
-                if (window.confirm('Authorize Dash to access your Google tasks?')) {
+                if (window.confirm('Authorize Dash to access your Google account?')) {
                     window.open(response)?.focus();
                     return undefined;
                 }
diff --git a/src/client/views/nodes/TaskBox.tsx b/src/client/views/nodes/TaskBox.tsx
index e755c6fe3..b59599052 100644
--- a/src/client/views/nodes/TaskBox.tsx
+++ b/src/client/views/nodes/TaskBox.tsx
@@ -211,9 +211,77 @@ export class TaskBox extends React.Component<TaskBoxProps> {
         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 (
             <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 }} />
@@ -257,44 +325,11 @@ export class TaskBox extends React.Component<TaskBoxProps> {
 
                     <button
                         className="task-manager-google"
-                        onClick={async event => {
+                        onClick={event => {
                             event.preventDefault();
-
-                            console.log('GT button clicked');
-                            try {
-                                const token = await GoogleAuthenticationManager.Instance.fetchOrGenerateAccessToken();
-                                console.log('Got token', token);
-
-                                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: allDay ? new Date(String(doc.$task_dateRange)?.split('|')[0]).toISOString() : (doc.$task_endTime instanceof DateField && doc.$task_endTime.date?.toISOString()) || undefined,
-                                        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.');
-                            }
+                            handleGoogleTaskClick();
                         }}>
-                        Add to Google
+                        Add to Google Tasks
                     </button>
                 </div>
 
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 1ec0291a689ed0aea0bb6fdd91e3c08113bfac46 Mon Sep 17 00:00:00 2001
From: Skitty1238 <157652284+Skitty1238@users.noreply.github.com>
Date: Thu, 5 Jun 2025 13:34:53 -0400
Subject: changed add to google tasks button to sync to google button.
 Implemented auto creation and auto deletion in Google when node is
 created/deleted in dash

---
 src/client/documents/Documents.ts              |  1 +
 src/client/views/nodes/TaskBox.tsx             | 97 +++++++++++++++++++++++---
 src/server/ApiManagers/GeneralGoogleManager.ts | 65 ++++++++++++++++-
 src/server/RouteManager.ts                     |  8 +++
 4 files changed, 160 insertions(+), 11 deletions(-)

(limited to 'src/client/views/nodes/TaskBox.tsx')

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 4ad9c9bd8..120ad0688 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -528,6 +528,7 @@ export class DocumentOptions {
     $task_endTime?: DateInfo | DateField = new DateInfo('end date and time', /*filterable*/ false);
     $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');
 
     _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 b59599052..7df3876c3 100644
--- a/src/client/views/nodes/TaskBox.tsx
+++ b/src/client/views/nodes/TaskBox.tsx
@@ -159,13 +159,62 @@ export class TaskBox extends React.Component<TaskBoxProps> {
         makeObservable(this);
     }
 
+    _googleTaskCreateDisposer?: IReactionDisposer;
     _heightDisposer?: IReactionDisposer;
     _widthDisposer?: IReactionDisposer;
 
     componentDidMount() {
         this.setTaskDateRange();
-
         const doc = this.props.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: any = {
+                        title: doc.title || 'Untitled Task',
+                        notes: doc.text || '',
+                        status: doc.$task_completed ? 'completed' : 'needsAction',
+                        completed: doc.$task_completed ? new Date().toISOString() : undefined,
+                    };
+
+                    if (doc.$task_allDay && typeof doc.$task_dateRange === 'string') {
+                        const datePart = doc.$task_dateRange.split('|')[0];
+                        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 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.error('❌ Error creating Google Task:', err);
+                }
+            }
+        })();
+
         this._heightDisposer = reaction(
             () => Number(doc._height),
             height => {
@@ -188,8 +237,32 @@ export class TaskBox extends React.Component<TaskBoxProps> {
     }
 
     componentWillUnmount() {
+        const doc = this.props.Document;
+        this._googleTaskCreateDisposer?.();
         this._heightDisposer?.();
         this._widthDisposer?.();
+
+        // task deletion
+        if (doc.$googleTaskId) {
+            (async () => {
+                try {
+                    const token = await GoogleAuthenticationManager.Instance.fetchOrGenerateAccessToken();
+                    if (!token) return;
+
+                    await fetch(`/googleTasks/${doc.$googleTaskId}`, {
+                        method: 'DELETE',
+                        credentials: 'include',
+                        headers: {
+                            Authorization: `Bearer ${token}`,
+                        },
+                    });
+
+                    console.log(`✅ Deleted Google Task ${doc.$googleTaskId}`);
+                } catch (err) {
+                    console.warn('❌ Failed to delete Google Task:', err);
+                }
+            })();
+        }
     }
 
     /**
@@ -213,7 +286,7 @@ export class TaskBox extends React.Component<TaskBoxProps> {
         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 () => {
+        const handleGoogleTaskSync = async () => {
             console.log('GT button clicked');
 
             try {
@@ -221,9 +294,9 @@ export class TaskBox extends React.Component<TaskBoxProps> {
                 if (!token) {
                     const listener = () => {
                         window.removeEventListener('focusin', listener);
-                        if (confirm('✅ Authorization complete. Try adding the task again?')) {
+                        if (confirm('✅ Authorization complete. Try syncing the task again?')) {
                             // you could refactor the click handler here
-                            handleGoogleTaskClick();
+                            handleGoogleTaskSync();
                         }
                         window.removeEventListener('focusin', listener);
                     };
@@ -252,8 +325,12 @@ export class TaskBox extends React.Component<TaskBoxProps> {
                     due = undefined;
                 }
 
-                const response = await fetch('/googleTasks/create', {
-                    method: 'POST',
+                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',
@@ -272,13 +349,13 @@ export class TaskBox extends React.Component<TaskBoxProps> {
                 console.log('Google Task result:', result);
 
                 if (result?.id) {
-                    alert('✅ Task sent to Google Tasks!');
+                    alert('✅ Task synced with Google Tasks!');
                 } else {
                     alert(`❌ Failed: ${result?.error?.message || 'Unknown error'}`);
                 }
             } catch (err) {
                 console.error('Fetch error:', err);
-                alert('❌ Task creation failed.');
+                alert('❌ Task syncing failed.');
             }
         };
 
@@ -327,9 +404,9 @@ export class TaskBox extends React.Component<TaskBoxProps> {
                         className="task-manager-google"
                         onClick={event => {
                             event.preventDefault();
-                            handleGoogleTaskClick();
+                            handleGoogleTaskSync();
                         }}>
-                        Add to Google Tasks
+                        Sync to Google
                     </button>
                 </div>
 
diff --git a/src/server/ApiManagers/GeneralGoogleManager.ts b/src/server/ApiManagers/GeneralGoogleManager.ts
index 4f0b8c02b..79ca55444 100644
--- a/src/server/ApiManagers/GeneralGoogleManager.ts
+++ b/src/server/ApiManagers/GeneralGoogleManager.ts
@@ -63,8 +63,9 @@ export default class GeneralGoogleManager extends ApiManager {
             },
         });
 
-        // AARAV ADD (creating a task)
+        // AARAV ADD
 
+        // Task Creation
         register({
             method: Method.POST,
             subscription: new RouteSubscriber('googleTasks').add('create'),
@@ -92,6 +93,68 @@ export default class GeneralGoogleManager extends ApiManager {
             },
         });
 
+        // Task Update
+        register({
+            method: Method.PATCH,
+            subscription: new RouteSubscriber('googleTasks').add('taskId'), 
+            // any way to add static params? like /update (this is not very descriptive)
+            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 { title, notes, due, status, completed } = req.body;
+
+                    const result = await tasks.tasks.patch({
+                        tasklist: '@default',
+                        task: taskId,
+                        requestBody: { title, notes, due, status, completed },
+                    });
+
+                    res.status(200).send(result.data);
+                } catch (err) {
+                    console.error('Google Tasks update error:', err);
+                    res.status(500).send('Failed to update task.');
+                }
+            },
+        });
+
+        // Task Deletion
+        register({
+            method: Method.DELETE,
+            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;
+          
+                await tasks.tasks.delete({
+                  tasklist: '@default',
+                  task: taskId,
+                });
+          
+                res.status(200).send({ success: true });
+              } catch (err) {
+                console.error('Google Tasks delete error:', err);
+                res.status(500).send('Failed to delete task.');
+              }
+            },
+          });
+          
+        
+
         register({
             method: Method.GET,
             subscription: '/refreshGoogle',
diff --git a/src/server/RouteManager.ts b/src/server/RouteManager.ts
index 2f6cf80b5..c5d70da3d 100644
--- a/src/server/RouteManager.ts
+++ b/src/server/RouteManager.ts
@@ -8,6 +8,8 @@ import { DashUserModel } from './authentication/DashUserModel';
 export enum Method {
     GET,
     POST,
+    PATCH,
+    DELETE,
 }
 
 export interface CoreArguments {
@@ -207,6 +209,12 @@ export default class RouteManager {
                     case Method.POST:
                         this.server.post(route, supervised);
                         break;
+                    case Method.PATCH:
+                        this.server.patch(route, supervised);
+                        break;
+                    case Method.DELETE:
+                        this.server.delete(route, supervised);
+                        break;
                     default:
                 }
             }
-- 
cgit v1.2.3-70-g09d2


From 353fcddb918ee9ccaedd69032f9212fc66c04a7f Mon Sep 17 00:00:00 2001
From: Skitty1238 <157652284+Skitty1238@users.noreply.github.com>
Date: Thu, 5 Jun 2025 13:54:09 -0400
Subject: sync button only accessible when a change is made to the task (to
 counter unnecessary syncing)

---
 src/client/views/nodes/TaskBox.scss |  8 +++++-
 src/client/views/nodes/TaskBox.tsx  | 55 +++++++++++++++++++++++++++++++++++++
 2 files changed, 62 insertions(+), 1 deletion(-)

(limited to 'src/client/views/nodes/TaskBox.tsx')

diff --git a/src/client/views/nodes/TaskBox.scss b/src/client/views/nodes/TaskBox.scss
index cc04f7973..071fe1f7c 100644
--- a/src/client/views/nodes/TaskBox.scss
+++ b/src/client/views/nodes/TaskBox.scss
@@ -92,10 +92,16 @@ input[type='datetime-local'] {
     white-space: nowrap;
     transition: background-color 0.2s ease;
     box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
-    
+
     &:hover {
         background-color: #4773b0; // darker shade of your base blue
         color: white;
         transform: scale(1.01); // subtle hover feel without real size change
     }
+
+    &:disabled {
+        opacity: 0.5;
+        cursor: not-allowed;
+        pointer-events: none;
+      }
 }
diff --git a/src/client/views/nodes/TaskBox.tsx b/src/client/views/nodes/TaskBox.tsx
index 7df3876c3..82c22057e 100644
--- a/src/client/views/nodes/TaskBox.tsx
+++ b/src/client/views/nodes/TaskBox.tsx
@@ -23,6 +23,23 @@ interface TaskBoxProps {
  */
 @observer
 export class TaskBox extends React.Component<TaskBoxProps> {
+    // contains the last synced task information
+    lastSyncedTask: {
+        title: string;
+        text: string;
+        due?: string;
+        completed: boolean;
+    } = {
+        title: '',
+        text: '',
+        due: '',
+        completed: false,
+    };
+
+    state = {
+        needsSync: false,
+    };
+
     /**
      * Method to reuturn the
      * @param fieldStr
@@ -234,6 +251,34 @@ export class TaskBox extends React.Component<TaskBoxProps> {
                 }
             }
         );
+
+        this._googleTaskCreateDisposer = reaction(
+            () => {
+                const { title, text, $task_completed, $task_dateRange, $task_startTime, $task_endTime, $task_allDay } = doc;
+
+                const completed = !!$task_completed;
+                let due: string | undefined;
+
+                if ($task_allDay && typeof $task_dateRange === 'string') {
+                    const datePart = $task_dateRange.split('|')[0];
+                    if (datePart && !isNaN(new Date(datePart).getTime())) {
+                        due = new Date(datePart + 'T00:00:00Z').toISOString();
+                    }
+                } else if ($task_endTime && $task_endTime instanceof DateField && $task_endTime.date) {
+                    due = $task_endTime.date.toISOString();
+                } else if ($task_startTime && $task_startTime instanceof DateField && $task_startTime.date) {
+                    due = $task_startTime.date.toISOString();
+                }
+
+                return { title, text, completed, due };
+            },
+            current => {
+                const hasChanged = current.title !== this.lastSyncedTask.title || current.text !== this.lastSyncedTask.text || current.due !== this.lastSyncedTask.due || current.completed !== this.lastSyncedTask.completed;
+
+                this.setState({ needsSync: hasChanged });
+            },
+            { fireImmediately: true }
+        );
     }
 
     componentWillUnmount() {
@@ -350,6 +395,15 @@ export class TaskBox extends React.Component<TaskBoxProps> {
 
                 if (result?.id) {
                     alert('✅ Task synced with Google Tasks!');
+                    if (result?.id) {
+                        this.lastSyncedTask = {
+                            title: taskTitle,
+                            text: taskDesc,
+                            due,
+                            completed: isCompleted,
+                        };
+                        this.setState({ needsSync: false });
+                    }
                 } else {
                     alert(`❌ Failed: ${result?.error?.message || 'Unknown error'}`);
                 }
@@ -402,6 +456,7 @@ export class TaskBox extends React.Component<TaskBoxProps> {
 
                     <button
                         className="task-manager-google"
+                        disabled={!this.state.needsSync}
                         onClick={event => {
                             event.preventDefault();
                             handleGoogleTaskSync();
-- 
cgit v1.2.3-70-g09d2


From 11cdfbcf26714351b5fd43ac2f95d30a7ae6de4b Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Thu, 5 Jun 2025 14:58:31 -0400
Subject: updated taskBox to use Cast methods and mobx instead of react state.
 Made TaskBox extend ViewBoxBaseComponent  to access
 this.Document/this.fieldKey, etc

---
 src/client/documents/Documents.ts  |  24 ------
 src/client/views/nodes/TaskBox.tsx | 164 +++++++++++++++++--------------------
 2 files changed, 75 insertions(+), 113 deletions(-)

(limited to 'src/client/views/nodes/TaskBox.tsx')

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 120ad0688..27cb95144 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -591,30 +591,6 @@ export namespace Docs {
                     options: { acl: '' },
                 },
             ],
-
-            // AARAV ADD //
-            [
-                DocumentType.JOURNAL,
-                {
-                    layout: { view: EmptyBox, dataField: 'text' },
-                    options: {
-                        title: 'Daily Journal',
-                        acl_Guest: SharingPermissions.View,
-                    },
-                },
-            ],
-
-            [
-                DocumentType.TASK,
-                {
-                    layout: { view: EmptyBox, dataField: 'text' },
-                    options: {
-                        title: 'Task',
-                        acl_Guest: SharingPermissions.View,
-                    },
-                },
-            ],
-            // AARAV ADD //
         ]);
 
         const suffix = 'Proto';
diff --git a/src/client/views/nodes/TaskBox.tsx b/src/client/views/nodes/TaskBox.tsx
index 82c22057e..168e1455e 100644
--- a/src/client/views/nodes/TaskBox.tsx
+++ b/src/client/views/nodes/TaskBox.tsx
@@ -1,30 +1,30 @@
-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, 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';
-import { GoogleAuthenticationManager } from '../../apis/GoogleAuthenticationManager';
-
-/**
- * Props (reference to document) for Task Box
- */
-
-interface TaskBoxProps {
-    Document: Doc;
-}
 
 /**
  * TaskBox class for adding task information + completing tasks
  */
 @observer
-export class TaskBox extends React.Component<TaskBoxProps> {
+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);
+    }
     // contains the last synced task information
-    lastSyncedTask: {
+    private _lastSyncedTask: {
         title: string;
         text: string;
         due?: string;
@@ -36,18 +36,7 @@ export class TaskBox extends React.Component<TaskBoxProps> {
         completed: false,
     };
 
-    state = {
-        needsSync: false,
-    };
-
-    /**
-     * Method to reuturn the
-     * @param fieldStr
-     * @returns
-     */
-    public static LayoutString(fieldStr: string) {
-        return FieldView.LayoutString(TaskBox, fieldStr);
-    }
+    @observable _needsSync = false;
 
     /**
      * Method to update the task description
@@ -56,7 +45,7 @@ export class TaskBox extends React.Component<TaskBoxProps> {
 
     @action
     updateText = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
-        this.props.Document.text = e.target.value;
+        this.Document[this.fieldKey] = e.target.value;
     };
 
     /**
@@ -65,7 +54,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;
     };
 
     /**
@@ -74,11 +63,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();
@@ -92,16 +81,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();
@@ -115,16 +104,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();
@@ -135,10 +124,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}`;
@@ -163,7 +152,7 @@ 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;
     };
 
     /**
@@ -182,7 +171,7 @@ export class TaskBox extends React.Component<TaskBoxProps> {
 
     componentDidMount() {
         this.setTaskDateRange();
-        const doc = this.props.Document;
+        const doc = this.Document;
 
         // adding task on creation to google
 
@@ -192,15 +181,15 @@ export class TaskBox extends React.Component<TaskBoxProps> {
                     const token = await GoogleAuthenticationManager.Instance.fetchOrGenerateAccessToken();
                     if (!token) return;
 
-                    const body: any = {
-                        title: doc.title || 'Untitled Task',
-                        notes: doc.text || '',
+                    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,
                     };
 
-                    if (doc.$task_allDay && typeof doc.$task_dateRange === 'string') {
-                        const datePart = doc.$task_dateRange.split('|')[0];
+                    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();
@@ -233,20 +222,20 @@ export class TaskBox extends React.Component<TaskBoxProps> {
         })();
 
         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;
                 }
             }
@@ -254,35 +243,34 @@ export class TaskBox extends React.Component<TaskBoxProps> {
 
         this._googleTaskCreateDisposer = reaction(
             () => {
-                const { title, text, $task_completed, $task_dateRange, $task_startTime, $task_endTime, $task_allDay } = doc;
-
-                const completed = !!$task_completed;
-                let due: string | undefined;
-
-                if ($task_allDay && typeof $task_dateRange === 'string') {
-                    const datePart = $task_dateRange.split('|')[0];
-                    if (datePart && !isNaN(new Date(datePart).getTime())) {
-                        due = new Date(datePart + 'T00:00:00Z').toISOString();
-                    }
-                } else if ($task_endTime && $task_endTime instanceof DateField && $task_endTime.date) {
-                    due = $task_endTime.date.toISOString();
-                } else if ($task_startTime && $task_startTime instanceof DateField && $task_startTime.date) {
-                    due = $task_startTime.date.toISOString();
-                }
-
-                return { title, text, completed, due };
+                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();
+                })();
+
+                return { title: StrCast(doc.title), text: StrCast(doc[this.fieldKey]), completed, due };
             },
-            current => {
-                const hasChanged = current.title !== this.lastSyncedTask.title || current.text !== this.lastSyncedTask.text || current.due !== this.lastSyncedTask.due || current.completed !== this.lastSyncedTask.completed;
-
-                this.setState({ needsSync: hasChanged });
+            ({ title, text, completed, due }) => {
+                this._needsSync =
+                    title !== this._lastSyncedTask.title || //
+                    text !== this._lastSyncedTask.text ||
+                    due !== this._lastSyncedTask.due ||
+                    completed !== this._lastSyncedTask.completed;
             },
             { fireImmediately: true }
         );
     }
 
     componentWillUnmount() {
-        const doc = this.props.Document;
+        const doc = this.Document;
         this._googleTaskCreateDisposer?.();
         this._heightDisposer?.();
         this._widthDisposer?.();
@@ -321,15 +309,15 @@ 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 isCompleted = !!this.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 startTime = DateCast(doc.$task_startTime) ? toLocalDateTimeString(DateCast(doc.$task_startTime)!.date) : '';
+        const endTime = DateCast(doc.$task_endTime) ? toLocalDateTimeString(DateCast(doc.$task_endTime)!.date) : '';
 
         const handleGoogleTaskSync = async () => {
             console.log('GT button clicked');
@@ -352,7 +340,7 @@ export class TaskBox extends React.Component<TaskBoxProps> {
                 let due: string | undefined;
 
                 if (allDay) {
-                    const rawRange = typeof doc.$task_dateRange === 'string' ? doc.$task_dateRange : '';
+                    const rawRange = StrCast(doc.$task_dateRange);
                     const datePart = rawRange.split('|')[0];
 
                     if (datePart && !isNaN(new Date(datePart).getTime())) {
@@ -396,13 +384,13 @@ export class TaskBox extends React.Component<TaskBoxProps> {
                 if (result?.id) {
                     alert('✅ Task synced with Google Tasks!');
                     if (result?.id) {
-                        this.lastSyncedTask = {
+                        this._lastSyncedTask = {
                             title: taskTitle,
                             text: taskDesc,
                             due,
                             completed: isCompleted,
                         };
-                        this.setState({ needsSync: false });
+                        runInAction(() => (this._needsSync = false));
                     }
                 } else {
                     alert(`❌ Failed: ${result?.error?.message || 'Unknown error'}`);
@@ -427,9 +415,7 @@ export class TaskBox extends React.Component<TaskBoxProps> {
                             <input
                                 type="date"
                                 value={(() => {
-                                    const rawRange = doc.$task_dateRange;
-                                    if (typeof rawRange !== 'string') return '';
-                                    const datePart = rawRange.split('|')[0];
+                                    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] : '';
@@ -456,7 +442,7 @@ export class TaskBox extends React.Component<TaskBoxProps> {
 
                     <button
                         className="task-manager-google"
-                        disabled={!this.state.needsSync}
+                        disabled={!this._needsSync}
                         onClick={event => {
                             event.preventDefault();
                             handleGoogleTaskSync();
-- 
cgit v1.2.3-70-g09d2


From 0f997ee859c7540d7df0fe7fa7491f8933ab697f Mon Sep 17 00:00:00 2001
From: Skitty1238 <157652284+Skitty1238@users.noreply.github.com>
Date: Thu, 5 Jun 2025 16:41:13 -0400
Subject: fixed automatically enabling/disabling sync button + persistence on
 reload

---
 src/client/views/nodes/TaskBox.tsx             | 55 ++++++++++++++++++++------
 src/server/ApiManagers/GeneralGoogleManager.ts | 44 ++++++++++-----------
 2 files changed, 65 insertions(+), 34 deletions(-)

(limited to 'src/client/views/nodes/TaskBox.tsx')

diff --git a/src/client/views/nodes/TaskBox.tsx b/src/client/views/nodes/TaskBox.tsx
index 168e1455e..37f6124a1 100644
--- a/src/client/views/nodes/TaskBox.tsx
+++ b/src/client/views/nodes/TaskBox.tsx
@@ -23,6 +23,7 @@ export class TaskBox extends ViewBoxBaseComponent<FieldViewProps>() {
     public static LayoutString(fieldStr: string) {
         return FieldView.LayoutString(TaskBox, fieldStr);
     }
+
     // contains the last synced task information
     private _lastSyncedTask: {
         title: string;
@@ -36,8 +37,23 @@ export class TaskBox extends ViewBoxBaseComponent<FieldViewProps>() {
         completed: false,
     };
 
+    // Whether the task needs to be synced with Google Tasks
     @observable _needsSync = 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 update the task description
      * @param e - event of changing the description box input
@@ -155,16 +171,6 @@ export class TaskBox extends ViewBoxBaseComponent<FieldViewProps>() {
         this.Document.$task_completed = e.target.checked;
     };
 
-    /**
-     * Constructor for the task box
-     * @param props - props containing the document reference
-     */
-
-    constructor(props: TaskBoxProps) {
-        super(props);
-        makeObservable(this);
-    }
-
     _googleTaskCreateDisposer?: IReactionDisposer;
     _heightDisposer?: IReactionDisposer;
     _widthDisposer?: IReactionDisposer;
@@ -241,6 +247,33 @@ 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;
+            })();
+        
+            this._lastSyncedTask = {
+                title: StrCast(doc.title),
+                text: StrCast(doc[this.fieldKey]),
+                due,
+                completed,
+            };
+            this._needsSync = false;
+        });
+
         this._googleTaskCreateDisposer = reaction(
             () => {
                 const completed = BoolCast(doc.$task_completed);
@@ -442,7 +475,7 @@ export class TaskBox extends ViewBoxBaseComponent<FieldViewProps>() {
 
                     <button
                         className="task-manager-google"
-                        disabled={!this._needsSync}
+                        disabled={!this.needsSync}
                         onClick={event => {
                             event.preventDefault();
                             handleGoogleTaskSync();
diff --git a/src/server/ApiManagers/GeneralGoogleManager.ts b/src/server/ApiManagers/GeneralGoogleManager.ts
index 79ca55444..7a1969bf4 100644
--- a/src/server/ApiManagers/GeneralGoogleManager.ts
+++ b/src/server/ApiManagers/GeneralGoogleManager.ts
@@ -96,7 +96,7 @@ export default class GeneralGoogleManager extends ApiManager {
         // Task Update
         register({
             method: Method.PATCH,
-            subscription: new RouteSubscriber('googleTasks').add('taskId'), 
+            subscription: new RouteSubscriber('googleTasks').add('taskId'),
             // any way to add static params? like /update (this is not very descriptive)
             secureHandler: async ({ req, res, user }) => {
                 try {
@@ -130,30 +130,28 @@ export default class GeneralGoogleManager extends ApiManager {
             method: Method.DELETE,
             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.');
+                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;
+
+                    await tasks.tasks.delete({
+                        tasklist: '@default',
+                        task: taskId,
+                    });
+
+                    res.status(200).send({ success: true });
+                } catch (err) {
+                    console.error('Google Tasks delete error:', err);
+                    res.status(500).send('Failed to delete task.');
                 }
-          
-                const tasks = google.tasks({ version: 'v1', auth });
-                const { taskId } = req.params;
-          
-                await tasks.tasks.delete({
-                  tasklist: '@default',
-                  task: taskId,
-                });
-          
-                res.status(200).send({ success: true });
-              } catch (err) {
-                console.error('Google Tasks delete error:', err);
-                res.status(500).send('Failed to delete task.');
-              }
             },
-          });
-          
-        
+        });
 
         register({
             method: Method.GET,
-- 
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/client/views/nodes/TaskBox.tsx')

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;
     }
@@ -54,11 +49,19 @@ export class TaskBox extends ViewBoxBaseComponent<FieldViewProps>() {
         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<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"]
     }
 }
-
-- 
cgit v1.2.3-70-g09d2


From 8c4734a1220fa58a2d8ffdce802568759002604a Mon Sep 17 00:00:00 2001
From: Skitty1238 <157652284+Skitty1238@users.noreply.github.com>
Date: Fri, 6 Jun 2025 12:04:59 -0400
Subject: fixed task node styling, and added intermediary syncing... state to
 sync button

---
 src/client/views/nodes/TaskBox.scss | 10 ++++++++++
 src/client/views/nodes/TaskBox.tsx  | 27 ++++++++++++++++-----------
 2 files changed, 26 insertions(+), 11 deletions(-)

(limited to 'src/client/views/nodes/TaskBox.tsx')

diff --git a/src/client/views/nodes/TaskBox.scss b/src/client/views/nodes/TaskBox.scss
index 071fe1f7c..e129e75d1 100644
--- a/src/client/views/nodes/TaskBox.scss
+++ b/src/client/views/nodes/TaskBox.scss
@@ -82,6 +82,8 @@ input[type='datetime-local'] {
 }
 
 .task-manager-google {
+    align-self: flex-start;
+    width: auto;
     font-size: 0.85rem;
     padding: 6px 12px;
     border-radius: 6px;
@@ -105,3 +107,11 @@ input[type='datetime-local'] {
         pointer-events: none;
       }
 }
+
+.task-box-blur-wrapper {
+    width: 100%;
+    height: 100%;
+    display: flex;
+    flex-direction: column;
+    min-width: 0;
+}
diff --git a/src/client/views/nodes/TaskBox.tsx b/src/client/views/nodes/TaskBox.tsx
index 6c966a59d..8e269a21a 100644
--- a/src/client/views/nodes/TaskBox.tsx
+++ b/src/client/views/nodes/TaskBox.tsx
@@ -19,6 +19,7 @@ export class TaskBox extends ViewBoxBaseComponent<FieldViewProps>() {
     _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
 
     // contains the last synced task information
     private _lastSyncedTask: {
@@ -236,6 +237,7 @@ export class TaskBox extends ViewBoxBaseComponent<FieldViewProps>() {
         const token = await GoogleAuthenticationManager.Instance.fetchOrGenerateAccessToken();
         if (!token) return;
 
+        this._syncing = true;
         const doc = this.Document;
         const taskTitle = StrCast(doc.title);
         const taskDesc = StrCast(doc[this.fieldKey]);
@@ -268,6 +270,8 @@ export class TaskBox extends ViewBoxBaseComponent<FieldViewProps>() {
         } else {
             console.warn('❌ Auto-sync failed:', result);
         }
+
+        this._syncing = false;
     };
 
     /**
@@ -442,8 +446,8 @@ export class TaskBox extends ViewBoxBaseComponent<FieldViewProps>() {
                     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';
@@ -479,6 +483,7 @@ export class TaskBox extends ViewBoxBaseComponent<FieldViewProps>() {
                 console.error('Fetch error:', err);
                 alert('❌ Task syncing failed.');
             }
+            this._syncing = false;
         };
 
         return (
@@ -520,18 +525,18 @@ export class TaskBox extends ViewBoxBaseComponent<FieldViewProps>() {
                             <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>
 
+                    <button
+                        className="task-manager-google"
+                        disabled={!this.needsSync}
+                        onClick={event => {
+                            event.preventDefault();
+                            handleGoogleTaskSync();
+                        }}>
+                        {this._syncing ? 'Syncing...' : (this.needsSync ? 'Sync to Google' : 'Synced to Google')}
+                    </button>
+
                     {!allDay && (
                         <div className="task-manager-times" style={{ opacity: isCompleted ? 0.7 : 1 }}>
                             <label>
-- 
cgit v1.2.3-70-g09d2


From d62464eef384e317c0a08334930ca491085ff253 Mon Sep 17 00:00:00 2001
From: Skitty1238 <157652284+Skitty1238@users.noreply.github.com>
Date: Fri, 6 Jun 2025 13:47:30 -0400
Subject: added bi directional syncing for task editing (based on most recent
 edit). Modified button to reflect bi-directionality of syncing

---
 src/client/documents/Documents.ts                  |   1 +
 src/client/views/nodes/TaskBox.tsx                 | 304 ++++++++++++++-------
 src/server/ApiManagers/GeneralGoogleManager.ts     |  29 ++
 .../apis/google/google_project_credentials.json    |   2 +-
 4 files changed, 242 insertions(+), 94 deletions(-)

(limited to 'src/client/views/nodes/TaskBox.tsx')

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
-- 
cgit v1.2.3-70-g09d2


From 0c084cdc58c45c5315ff60f6a34f73fc722e25c9 Mon Sep 17 00:00:00 2001
From: Skitty1238 <157652284+Skitty1238@users.noreply.github.com>
Date: Fri, 6 Jun 2025 18:37:07 -0400
Subject: cleanup of redundant code + comments

---
 src/client/views/nodes/TaskBox.tsx | 112 +------------------------------------
 1 file changed, 2 insertions(+), 110 deletions(-)

(limited to 'src/client/views/nodes/TaskBox.tsx')

diff --git a/src/client/views/nodes/TaskBox.tsx b/src/client/views/nodes/TaskBox.tsx
index 94252fafc..c99a91080 100644
--- a/src/client/views/nodes/TaskBox.tsx
+++ b/src/client/views/nodes/TaskBox.tsx
@@ -228,67 +228,13 @@ export class TaskBox extends ViewBoxBaseComponent<FieldViewProps>() {
     }
 
     /**
-     * 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 && !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;
-        // });
-    };
-
-    /**
-     * Handles the blur event for the task box
+     * 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.autoSyncToGoogle();
+            this.syncWithGoogleTaskBidirectional();
         }
     };
 
@@ -548,60 +494,6 @@ export class TaskBox extends ViewBoxBaseComponent<FieldViewProps>() {
             } else {
                 alert('❌ Task sync failed.');
             }
-
-            // 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 (
-- 
cgit v1.2.3-70-g09d2


From ee7da91564ae293f394b487f96c53f0118af9d7b Mon Sep 17 00:00:00 2001
From: Skitty1238 <157652284+Skitty1238@users.noreply.github.com>
Date: Fri, 6 Jun 2025 21:29:00 -0400
Subject: cleaned up popups so that not linking to Google doesnt bombard user
 with popups. Added debuggign instructions to alerts

---
 src/client/apis/GoogleAuthenticationManager.tsx | 62 ++++++++-----------------
 src/client/views/nodes/TaskBox.tsx              | 43 +++++++++++------
 2 files changed, 48 insertions(+), 57 deletions(-)

(limited to 'src/client/views/nodes/TaskBox.tsx')

diff --git a/src/client/apis/GoogleAuthenticationManager.tsx b/src/client/apis/GoogleAuthenticationManager.tsx
index 23e658329..67a6e01e9 100644
--- a/src/client/apis/GoogleAuthenticationManager.tsx
+++ b/src/client/apis/GoogleAuthenticationManager.tsx
@@ -41,49 +41,6 @@ export class GoogleAuthenticationManager extends ObservableReactComponent<object
         this.openState && this.resetState(0, 0);
     }
 
-    // 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;
-
-    //         // GETS STUCK AT THIS PROMISE!!
-    //         return new Promise<string>(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<string | undefined> => {
         const response = await Networking.FetchFromServer('/readGoogleAccessToken');
 
@@ -111,6 +68,25 @@ export class GoogleAuthenticationManager extends ObservableReactComponent<object
         }
     };
 
+    public fetchAccessTokenSilently = async (): Promise<string | undefined> => {
+        const response = await Networking.FetchFromServer('/readGoogleAccessToken');
+    
+        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 {
+            // Do nothing — just return undefined silently
+            return undefined;
+        }
+    };
+    
+
     resetState = action((visibleForMS: number = 3000, fadesOutInMS: number = 500) => {
         if (!visibleForMS && !fadesOutInMS) {
             runInAction(() => {
diff --git a/src/client/views/nodes/TaskBox.tsx b/src/client/views/nodes/TaskBox.tsx
index c99a91080..5f46c9b2c 100644
--- a/src/client/views/nodes/TaskBox.tsx
+++ b/src/client/views/nodes/TaskBox.tsx
@@ -234,24 +234,39 @@ export class TaskBox extends ViewBoxBaseComponent<FieldViewProps>() {
     handleBlur = (e: React.FocusEvent<HTMLDivElement>) => {
         // Check if focus is moving outside this component
         if (!e.currentTarget.contains(e.relatedTarget)) {
-            this.syncWithGoogleTaskBidirectional();
+            this.syncWithGoogleTaskBidirectional(true);
         }
     };
 
-    syncWithGoogleTaskBidirectional = async (): Promise<boolean> => {
+    /**
+     * 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;
-        const token = await GoogleAuthenticationManager.Instance.fetchOrGenerateAccessToken();
+        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) {
-            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);
+            if (!silent) {
+                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;
         }
 
@@ -373,7 +388,7 @@ export class TaskBox extends ViewBoxBaseComponent<FieldViewProps>() {
                         console.warn('❌ Google Task creation failed:', result);
                     }
                 } catch (err) {
-                    console.error('❌ Error creating Google Task:', err);
+                    console.warn('❌ Error creating Google Task:', err);
                 }
             } else if (doc.$googleTaskId) {
                 await this.syncWithGoogleTaskBidirectional();
@@ -492,7 +507,7 @@ export class TaskBox extends ViewBoxBaseComponent<FieldViewProps>() {
             if (success) {
                 alert('✅ Task successfully synced!');
             } else {
-                alert('❌ Task sync failed.');
+                alert('❌ Task sync failed. Try reloading.');
             }
         };
 
-- 
cgit v1.2.3-70-g09d2


From a91884435ea0fd9f1215c938c636551e42690e9d Mon Sep 17 00:00:00 2001
From: Skitty1238 <157652284+Skitty1238@users.noreply.github.com>
Date: Mon, 9 Jun 2025 10:38:07 -0400
Subject: Fix sync logic to avoid overwriting newer Dash edits with outdated
 Google versions

---
 src/client/views/nodes/TaskBox.tsx | 10 ++++++++--
 1 file changed, 8 insertions(+), 2 deletions(-)

(limited to 'src/client/views/nodes/TaskBox.tsx')

diff --git a/src/client/views/nodes/TaskBox.tsx b/src/client/views/nodes/TaskBox.tsx
index 5f46c9b2c..d29160e02 100644
--- a/src/client/views/nodes/TaskBox.tsx
+++ b/src/client/views/nodes/TaskBox.tsx
@@ -239,7 +239,7 @@ export class TaskBox extends ViewBoxBaseComponent<FieldViewProps>() {
     };
 
     /**
-     * Method to sync the task with Google Tasks bidirectionally 
+     * 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
@@ -290,7 +290,10 @@ export class TaskBox extends ViewBoxBaseComponent<FieldViewProps>() {
             const googleUpdated = new Date(googleTask.updated);
             const dashUpdated = new Date(StrCast(doc.$task_lastSyncedAt));
 
-            if (googleUpdated > dashUpdated) {
+            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;
+
+            if (googleUpdated > dashUpdated && !dashChanged) {
                 // Google version is newer — update Dash
                 runInAction(() => {
                     doc.title = googleTask.title ?? doc.title;
@@ -315,6 +318,9 @@ export class TaskBox extends ViewBoxBaseComponent<FieldViewProps>() {
 
                 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();
-- 
cgit v1.2.3-70-g09d2


From 546d21bb06a13ef69077f93e5ae7fd251ae3e92b Mon Sep 17 00:00:00 2001
From: Skitty1238 <157652284+Skitty1238@users.noreply.github.com>
Date: Mon, 9 Jun 2025 11:51:40 -0400
Subject: added delete button to both close the node and remove it from my
 recently closed (and delete from google tasks)

---
 src/client/views/nodes/TaskBox.tsx | 76 +++++++++++++++++++++++++++-----------
 1 file changed, 55 insertions(+), 21 deletions(-)

(limited to 'src/client/views/nodes/TaskBox.tsx')

diff --git a/src/client/views/nodes/TaskBox.tsx b/src/client/views/nodes/TaskBox.tsx
index d29160e02..3c7a2f345 100644
--- a/src/client/views/nodes/TaskBox.tsx
+++ b/src/client/views/nodes/TaskBox.tsx
@@ -2,13 +2,16 @@ import { action, IReactionDisposer, makeObservable, observable, reaction, runInA
 import { observer } from 'mobx-react';
 import * as React from 'react';
 import { DateField } from '../../../fields/DateField';
-import { BoolCast, DateCast, NumCast, StrCast } from '../../../fields/Types';
+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 { ViewBoxBaseComponent } from '../DocComponent';
 import { FieldView, FieldViewProps } from './FieldView';
 import './TaskBox.scss';
+import { DocumentDecorations } from '../DocumentDecorations';
+import { Doc } from '../../../fields/Doc';
+import { DocumentView } from './DocumentView';
 
 /**
  * TaskBox class for adding task information + completing tasks
@@ -20,6 +23,7 @@ export class TaskBox extends ViewBoxBaseComponent<FieldViewProps>() {
     _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: {
@@ -227,6 +231,17 @@ export class TaskBox extends ViewBoxBaseComponent<FieldViewProps>() {
         };
     }
 
+    /**
+     * 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
@@ -234,6 +249,7 @@ export class TaskBox extends ViewBoxBaseComponent<FieldViewProps>() {
     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);
         }
     };
@@ -460,29 +476,38 @@ export class TaskBox extends ViewBoxBaseComponent<FieldViewProps>() {
         this._googleTaskCreateDisposer?.();
         this._heightDisposer?.();
         this._widthDisposer?.();
+    }
 
-        // task deletion
-        if (doc.$googleTaskId) {
-            (async () => {
-                try {
-                    const token = await GoogleAuthenticationManager.Instance.fetchOrGenerateAccessToken();
-                    if (!token) return;
+    handleDeleteTask = async () => {
+        const doc = this.Document;
+        if (!doc.$googleTaskId) return;
+        if (!window.confirm('Are you sure you want to permanently delete this task from Google Tasks?')) return;
 
-                    await fetch(`/googleTasks/${doc.$googleTaskId}`, {
-                        method: 'DELETE',
-                        credentials: 'include',
-                        headers: {
-                            Authorization: `Bearer ${token}`,
-                        },
-                    });
+        try {
+            const token = await GoogleAuthenticationManager.Instance.fetchOrGenerateAccessToken();
+            if (!token) return;
 
-                    console.log(`✅ Deleted Google Task ${doc.$googleTaskId}`);
-                } catch (err) {
-                    console.warn('❌ Failed to delete Google Task:', err);
-                }
-            })();
+            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
@@ -518,7 +543,7 @@ export class TaskBox extends ViewBoxBaseComponent<FieldViewProps>() {
         };
 
         return (
-            <div className="task-box-blur-wrapper" tabIndex={0} onBlur={this.handleBlur}>
+            <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 }} />
 
@@ -568,6 +593,15 @@ export class TaskBox extends ViewBoxBaseComponent<FieldViewProps>() {
                         {this._syncing ? 'Syncing...' : this.needsSync ? 'Push Updates' : 'Sync Task'}
                     </button>
 
+                    <button
+                        className="task-delete"
+                        onClick={event => {
+                            event.preventDefault();
+                            this.handleDeleteTask();
+                        }}>
+                        Delete Task
+                    </button>
+
                     {!allDay && (
                         <div className="task-manager-times" style={{ opacity: isCompleted ? 0.7 : 1 }}>
                             <label>
-- 
cgit v1.2.3-70-g09d2


From 8e0473f2c7846f68a49d656745542e833cc5eb3e Mon Sep 17 00:00:00 2001
From: Skitty1238 <157652284+Skitty1238@users.noreply.github.com>
Date: Mon, 9 Jun 2025 12:12:01 -0400
Subject: delete button style changes

---
 src/client/views/nodes/TaskBox.scss | 15 +++++++++++++
 src/client/views/nodes/TaskBox.tsx  | 44 +++++++++++++++++++++----------------
 2 files changed, 40 insertions(+), 19 deletions(-)

(limited to 'src/client/views/nodes/TaskBox.tsx')

diff --git a/src/client/views/nodes/TaskBox.scss b/src/client/views/nodes/TaskBox.scss
index e129e75d1..beee58697 100644
--- a/src/client/views/nodes/TaskBox.scss
+++ b/src/client/views/nodes/TaskBox.scss
@@ -115,3 +115,18 @@ input[type='datetime-local'] {
     flex-direction: column;
     min-width: 0;
 }
+
+.task-manager-button-row {
+    display: flex;
+    flex-direction: row;
+    gap: 8px;
+}
+
+.task-manager-delete {
+    @extend .task-manager-google;
+    background-color: #182430;
+
+    &:hover {
+        background-color: #000000;
+    }
+}
diff --git a/src/client/views/nodes/TaskBox.tsx b/src/client/views/nodes/TaskBox.tsx
index 3c7a2f345..5a54e8049 100644
--- a/src/client/views/nodes/TaskBox.tsx
+++ b/src/client/views/nodes/TaskBox.tsx
@@ -481,7 +481,7 @@ export class TaskBox extends ViewBoxBaseComponent<FieldViewProps>() {
     handleDeleteTask = async () => {
         const doc = this.Document;
         if (!doc.$googleTaskId) return;
-        if (!window.confirm('Are you sure you want to permanently delete this task from Google Tasks?')) return;
+        if (!window.confirm('Are you sure you want to permanently delete this task? This action is undoable.')) return;
 
         try {
             const token = await GoogleAuthenticationManager.Instance.fetchOrGenerateAccessToken();
@@ -583,24 +583,30 @@ export class TaskBox extends ViewBoxBaseComponent<FieldViewProps>() {
                         </label>
                     </div>
 
-                    <button
-                        className="task-manager-google"
-                        // disabled={!this.needsSync}
-                        onClick={event => {
-                            event.preventDefault();
-                            handleGoogleTaskSync();
-                        }}>
-                        {this._syncing ? 'Syncing...' : this.needsSync ? 'Push Updates' : 'Sync Task'}
-                    </button>
-
-                    <button
-                        className="task-delete"
-                        onClick={event => {
-                            event.preventDefault();
-                            this.handleDeleteTask();
-                        }}>
-                        Delete Task
-                    </button>
+                    <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 }}>
-- 
cgit v1.2.3-70-g09d2


From b1560c38d5eba32b0b313786d3e049797c770abe Mon Sep 17 00:00:00 2001
From: Skitty1238 <157652284+Skitty1238@users.noreply.github.com>
Date: Mon, 9 Jun 2025 13:43:25 -0400
Subject: implemented undoable deletion so that tasks reappear in Google Tasks
 upon undo in Dash

---
 src/client/documents/Documents.ts              |  1 +
 src/client/views/nodes/TaskBox.tsx             | 52 +++++++++++++++++++-------
 src/server/ApiManagers/GeneralGoogleManager.ts |  8 ++--
 3 files changed, 44 insertions(+), 17 deletions(-)

(limited to 'src/client/views/nodes/TaskBox.tsx')

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index a29e7a9f5..683a18aee 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -530,6 +530,7 @@ export class DocumentOptions {
     $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');
+    $task_deleted?: BoolInfo | boolean = new BoolInfo('whether task is deleted or not', /*filterable*/ false);
 
     _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 5a54e8049..f415f8b52 100644
--- a/src/client/views/nodes/TaskBox.tsx
+++ b/src/client/views/nodes/TaskBox.tsx
@@ -31,11 +31,13 @@ export class TaskBox extends ViewBoxBaseComponent<FieldViewProps>() {
         text: string;
         due?: string;
         completed: boolean;
+        deleted?: boolean;
     } = {
         title: '',
         text: '',
         due: '',
         completed: false,
+        deleted: false,
     };
 
     /**
@@ -215,20 +217,28 @@ export class TaskBox extends ViewBoxBaseComponent<FieldViewProps>() {
      * @returns - an object containing the task details
      */
 
-    private buildGoogleTaskBody(): Record<string, string | undefined> {
+    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;
 
-        return {
+        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;
     }
 
     /**
@@ -307,7 +317,11 @@ export class TaskBox extends ViewBoxBaseComponent<FieldViewProps>() {
             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;
+                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
@@ -328,6 +342,7 @@ export class TaskBox extends ViewBoxBaseComponent<FieldViewProps>() {
                         text: StrCast(doc[this.fieldKey]),
                         due: this.computeDueDate(),
                         completed: !!doc.$task_completed,
+                        deleted: !!doc.$dashDeleted,
                     };
                     this._needsSync = false;
                 });
@@ -358,6 +373,7 @@ export class TaskBox extends ViewBoxBaseComponent<FieldViewProps>() {
                         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');
@@ -446,23 +462,27 @@ export class TaskBox extends ViewBoxBaseComponent<FieldViewProps>() {
                 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 };
+                return { title: StrCast(doc.title), text: StrCast(doc[this.fieldKey]), completed, due, dashDeleted };
             },
-            ({ title, text, completed, due }) => {
-                this._needsSync =
-                    title !== this._lastSyncedTask.title || //
-                    text !== this._lastSyncedTask.text ||
-                    due !== this._lastSyncedTask.due ||
-                    completed !== this._lastSyncedTask.completed;
+            ({ 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 }
         );
@@ -478,10 +498,18 @@ export class TaskBox extends ViewBoxBaseComponent<FieldViewProps>() {
         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 permanently delete this task? This action is undoable.')) 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();
@@ -602,8 +630,6 @@ export class TaskBox extends ViewBoxBaseComponent<FieldViewProps>() {
                             <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>
diff --git a/src/server/ApiManagers/GeneralGoogleManager.ts b/src/server/ApiManagers/GeneralGoogleManager.ts
index b237178f4..aceead3c6 100644
--- a/src/server/ApiManagers/GeneralGoogleManager.ts
+++ b/src/server/ApiManagers/GeneralGoogleManager.ts
@@ -79,10 +79,10 @@ export default class GeneralGoogleManager extends ApiManager {
 
                     const tasks = google.tasks({ version: 'v1', auth });
 
-                    const { title, notes, due, status, completed } = req.body;
+                    const { title, notes, due, status, completed, deleted } = req.body;
                     const result = await tasks.tasks.insert({
                         tasklist: '@default',
-                        requestBody: { title, notes, due, status, completed },
+                        requestBody: { title, notes, due, status, completed, deleted },
                     });
 
                     res.status(200).send(result.data);
@@ -109,12 +109,12 @@ export default class GeneralGoogleManager extends ApiManager {
                     const tasks = google.tasks({ version: 'v1', auth });
 
                     const { taskId } = req.params;
-                    const { title, notes, due, status, completed } = req.body;
+                    const { title, notes, due, status, completed, deleted } = req.body;
 
                     const result = await tasks.tasks.patch({
                         tasklist: '@default',
                         task: taskId,
-                        requestBody: { title, notes, due, status, completed },
+                        requestBody: { title, notes, due, status, completed, deleted},
                     });
 
                     res.status(200).send(result.data);
-- 
cgit v1.2.3-70-g09d2


From 765c7a3870a1d446622a28b157ab00a4dced2879 Mon Sep 17 00:00:00 2001
From: Skitty1238 <157652284+Skitty1238@users.noreply.github.com>
Date: Mon, 9 Jun 2025 17:02:18 -0400
Subject: documentation

---
 src/client/views/nodes/TaskBox.tsx             | 4 +---
 src/server/ApiManagers/GeneralGoogleManager.ts | 4 ++--
 2 files changed, 3 insertions(+), 5 deletions(-)

(limited to 'src/client/views/nodes/TaskBox.tsx')

diff --git a/src/client/views/nodes/TaskBox.tsx b/src/client/views/nodes/TaskBox.tsx
index f415f8b52..ed5982c55 100644
--- a/src/client/views/nodes/TaskBox.tsx
+++ b/src/client/views/nodes/TaskBox.tsx
@@ -286,7 +286,7 @@ export class TaskBox extends ViewBoxBaseComponent<FieldViewProps>() {
                 const listener = () => {
                     window.removeEventListener('focusin', listener);
                     if (confirm('✅ Authorization complete. Try syncing the task again?')) {
-                        // you could refactor the click handler here
+                        // try syncing again
                         this.syncWithGoogleTaskBidirectional();
                     }
                     window.removeEventListener('focusin', listener);
@@ -498,7 +498,6 @@ export class TaskBox extends ViewBoxBaseComponent<FieldViewProps>() {
         this._widthDisposer?.();
     }
 
-    
     /**
      * Method to handle task deletion
      * @returns - a promise that resolves when the task is deleted
@@ -560,7 +559,6 @@ export class TaskBox extends ViewBoxBaseComponent<FieldViewProps>() {
         const endTime = DateCast(doc.$task_endTime) ? toLocalDateTimeString(DateCast(doc.$task_endTime)!.date) : '';
 
         const handleGoogleTaskSync = async () => {
-            console.log('GT button clicked');
             const success = await this.syncWithGoogleTaskBidirectional();
 
             if (success) {
diff --git a/src/server/ApiManagers/GeneralGoogleManager.ts b/src/server/ApiManagers/GeneralGoogleManager.ts
index aceead3c6..110701418 100644
--- a/src/server/ApiManagers/GeneralGoogleManager.ts
+++ b/src/server/ApiManagers/GeneralGoogleManager.ts
@@ -63,8 +63,6 @@ export default class GeneralGoogleManager extends ApiManager {
             },
         });
 
-        // AARAV ADD
-
         // Task Creation
         register({
             method: Method.POST,
@@ -153,6 +151,8 @@ export default class GeneralGoogleManager extends ApiManager {
             },
         });
 
+        // Google Account Linking
+
         register({
             method: Method.GET,
             subscription: '/refreshGoogle',
-- 
cgit v1.2.3-70-g09d2

