From 54b8c7e9ec258fd43ec523aaf3d967a646022cee Mon Sep 17 00:00:00 2001 From: aaravkumar Date: Wed, 30 Apr 2025 22:54:16 -0400 Subject: made document options as data doc options ($), and fixed namign conventions --- src/client/views/nodes/TaskBox.scss | 72 +++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 src/client/views/nodes/TaskBox.scss (limited to 'src/client/views/nodes/TaskBox.scss') diff --git a/src/client/views/nodes/TaskBox.scss b/src/client/views/nodes/TaskBox.scss new file mode 100644 index 000000000..0fcc2f955 --- /dev/null +++ b/src/client/views/nodes/TaskBox.scss @@ -0,0 +1,72 @@ +.task-manager-container { + display: flex; + flex-direction: column; + padding: 8px; + gap: 10px; + width: 100%; + height: 100%; + box-sizing: border-box; +} + +.task-manager-title { + width: 100%; + font-size: 1.25rem; + font-weight: 600; + padding: 6px 10px; + border: 1px solid #ccc; + border-radius: 6px; + box-sizing: border-box; +} + +.task-manager-description { + width: 100%; + font-size: 1rem; + padding: 8px 10px; + border: 1px solid #ccc; + border-radius: 6px; + min-height: 40px; + box-sizing: border-box; + vertical-align: top; + text-align: start; + resize: none; + line-height: 1.4; + resize: none; + flex-grow: 1 +} + +.task-manager-checkboxes { + display: flex; + align-items: center; + gap: 16px; +} + +.task-manager-allday, .task-manager-complete { + display: flex; + align-items: center; + gap: 6px; + font-size: 0.95rem; +} + +.task-manager-times { + display: flex; + flex-direction: column; + gap: 6px; + width: 100%; +} + +.task-manager-times label { + display: flex; + flex-direction: column; + font-size: 0.9rem; + font-weight: 500; + gap: 4px; +} + +input[type="datetime-local"] { + width: 100%; + font-size: 0.9rem; + padding: 6px 8px; + border: 1px solid #ccc; + border-radius: 6px; + box-sizing: border-box; +} -- 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.scss') 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 { +export class GoogleAuthenticationManager extends ObservableReactComponent { // eslint-disable-next-line no-use-before-define public static Instance: GoogleAuthenticationManager; private authenticationLink: Opt = undefined; @@ -23,6 +24,12 @@ export class GoogleAuthenticationManager extends React.Component { @observable private credentials: { user_info: { name: string; picture: string }; access_token: string } | undefined = undefined; private disposer: Opt; + 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 { () => 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 { } }); - constructor(props: object) { - super(props); - GoogleAuthenticationManager.Instance = this; - } - private get renderPrompt() { return (
@@ -153,7 +156,11 @@ export class GoogleAuthenticationManager extends React.Component { } render() { - return (this.isOpen = false))} />; + return ( +
+ (this.isOpen = false))} /> +
+ ); } } 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 { 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 { )} {/** test button */} - - - - + ); } 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([ ['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 { - const auth = await retrieveOAuthClient(userId); + export async function GetEndpoint(sector: string, user: DashUserModel): Promise { + 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 { - const { credentials, refreshed } = await retrieveCredentials(userId); + export async function retrieveOAuthClient(user: DashUserModel): Promise { + 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; refreshed: boolean }> { - let credentials = await Database.Auxiliary.GoogleAccessToken.Fetch(userId); + export async function retrieveCredentials(user: DashUserModel): Promise<{ credentials: Opt; 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 { + async function refreshAccessToken(credentials: Credentials, user: DashUserModel): Promise { 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 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.scss') 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 { Complete + + {!allDay && ( @@ -269,94 +311,6 @@ export class TaskBox extends React.Component { )} - - {/** test button */} - {/* */} - - {/* */} - - ); } -- cgit v1.2.3-70-g09d2 From d73f7d9960285aceec01ef41a80bbb19e5d86f8c Mon Sep 17 00:00:00 2001 From: Skitty1238 <157652284+Skitty1238@users.noreply.github.com> Date: Thu, 5 Jun 2025 11:35:57 -0400 Subject: style changes --- src/client/views/nodes/TaskBox.scss | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) (limited to 'src/client/views/nodes/TaskBox.scss') diff --git a/src/client/views/nodes/TaskBox.scss b/src/client/views/nodes/TaskBox.scss index a7f75acdb..cc04f7973 100644 --- a/src/client/views/nodes/TaskBox.scss +++ b/src/client/views/nodes/TaskBox.scss @@ -1,4 +1,5 @@ .task-manager-container { + color-scheme: light; display: flex; flex-direction: column; padding: 8px; @@ -6,15 +7,6 @@ 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 { @@ -93,11 +85,17 @@ input[type='datetime-local'] { font-size: 0.85rem; padding: 6px 12px; border-radius: 6px; - background-color: #4285f4; + background-color: #5e88c8; 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); + + &:hover { + background-color: #4773b0; // darker shade of your base blue + color: white; + transform: scale(1.01); // subtle hover feel without real size change + } } -- 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.scss') 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 { + // 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 { } } ); + + 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 { 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 { + + {!allDay && (
- - - +
+ + + +
{!allDay && (
-- cgit v1.2.3-70-g09d2