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 + 1 file changed, 1 insertion(+) (limited to 'src/client/documents/Documents.ts') 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); -- cgit v1.2.3-70-g09d2 From 11cdfbcf26714351b5fd43ac2f95d30a7ae6de4b Mon Sep 17 00:00:00 2001 From: bobzel 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/documents/Documents.ts') 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 { +export class TaskBox extends ViewBoxBaseComponent() { + /** + * Return the JSX string that will create this component + * @param fieldStr the Doc field that contains the primary data for this component + * @returns + */ + public static LayoutString(fieldStr: string) { + return FieldView.LayoutString(TaskBox, fieldStr); + } // 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 { 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 { @action updateText = (e: React.ChangeEvent) => { - this.props.Document.text = e.target.value; + this.Document[this.fieldKey] = e.target.value; }; /** @@ -65,7 +54,7 @@ export class TaskBox extends React.Component { */ @action updateTitle = (e: React.ChangeEvent) => { - this.props.Document.title = e.target.value; + this.Document.title = e.target.value; }; /** @@ -74,11 +63,11 @@ export class TaskBox extends React.Component { */ @action updateAllDay = (e: React.ChangeEvent) => { - 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 { updateStart = (e: React.ChangeEvent) => { 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 { updateEnd = (e: React.ChangeEvent) => { 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 { */ @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 { @action toggleComplete = (e: React.ChangeEvent) => { - 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 { 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 { 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 { })(); 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 { 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 { 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 { 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 { 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 { { - 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 { {!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 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/documents/Documents.ts') 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() { text: string; due?: string; completed: boolean; + deleted?: boolean; } = { title: '', text: '', due: '', completed: false, + deleted: false, }; /** @@ -215,20 +217,28 @@ export class TaskBox extends ViewBoxBaseComponent() { * @returns - an object containing the task details */ - private buildGoogleTaskBody(): Record { + private buildGoogleTaskBody(): Record { const doc = this.Document; const title = StrCast(doc.title, 'Untitled Task'); const notes = StrCast(doc[this.fieldKey]); const due = this.computeDueDate(); const completed = !!doc.$task_completed; - return { + const body: Record = { 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() { 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() { 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() { 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() { 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() { 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() { - - Delete 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