From 0696c37442a47465ad62b3ffe21efa99190cde09 Mon Sep 17 00:00:00 2001 From: Lisa Date: Sun, 25 Jun 2023 10:52:35 +0200 Subject: [PATCH 1/3] feat: added base for subtask being automatically added --- .../store/task-repeat-cfg.effects.ts | 87 ++++++++++++++++++- .../work-context/work-context.model.ts | 1 + 2 files changed, 86 insertions(+), 2 deletions(-) diff --git a/src/app/features/task-repeat-cfg/store/task-repeat-cfg.effects.ts b/src/app/features/task-repeat-cfg/store/task-repeat-cfg.effects.ts index 8172af846a9..52fce531743 100644 --- a/src/app/features/task-repeat-cfg/store/task-repeat-cfg.effects.ts +++ b/src/app/features/task-repeat-cfg/store/task-repeat-cfg.effects.ts @@ -22,10 +22,14 @@ import { import { selectTaskRepeatCfgFeatureState } from './task-repeat-cfg.reducer'; import { PersistenceService } from '../../../core/persistence/persistence.service'; import { Task, TaskArchive, TaskCopy } from '../../tasks/task.model'; -import { updateTask } from '../../tasks/store/task.actions'; +import { addSubTask, updateTask } from '../../tasks/store/task.actions'; import { TaskService } from '../../tasks/task.service'; import { TaskRepeatCfgService } from '../task-repeat-cfg.service'; -import { TaskRepeatCfg, TaskRepeatCfgState } from '../task-repeat-cfg.model'; +import { + DEFAULT_TASK_REPEAT_CFG, + TaskRepeatCfg, + TaskRepeatCfgState, +} from '../task-repeat-cfg.model'; import { forkJoin, from, merge, of } from 'rxjs'; import { setActiveWorkContext } from '../../work-context/store/work-context.actions'; import { SyncTriggerService } from '../../../imex/sync/sync-trigger.service'; @@ -38,6 +42,8 @@ import { Update } from '@ngrx/entity'; import { getDateTimeFromClockString } from '../../../util/get-date-time-from-clock-string'; import { isToday } from '../../../util/is-today.util'; import { DateService } from 'src/app/core/date/date.service'; +import { getWorklogStr } from 'src/app/util/get-work-log-str'; +import { getTaskById } from '../../tasks/store/task.reducer.util'; @Injectable() export class TaskRepeatCfgEffects { @@ -58,6 +64,83 @@ export class TaskRepeatCfgEffects { { dispatch: false }, ); + /* addTaskRepeatCfgForSubTasksOf: any = createEffect( + () => + this._actions$.pipe( + ofType( + addTaskRepeatCfgToTask + ), + tap(async (aAction) => { + // TODO: is there an easier way to get to the parent task? + const allTasks = await this._taskService.allTasks$.pipe(first()).toPromise(); + const parentTask = allTasks.find((aTask) => aTask.id === aAction.taskId); + + if (parentTask !== undefined && parentTask.subTaskIds.length > 0) { + parentTask.subTaskIds.forEach(aSubTaskId => { + const task = allTasks.find((aTask) => aTask.id === aSubTaskId)!; + + const repeatCfg = { + ...DEFAULT_TASK_REPEAT_CFG, + startDate: getWorklogStr(), // TODO: What is happening here? Is this correct? + title: task.title, + notes: task.notes, + tagIds: task.tagIds, + defaultEstimate: task.timeEstimate + }; + + this._taskRepeatCfgService.addTaskRepeatCfgToTask( + task.id, + task.projectId, + repeatCfg + ); + }); + } + }) + ), + { dispatch: false } // Question: What exactly does this do? + ); */ + + /** + * When adding a sub task, this function checks if the parent is a repeatable task and therefore the sub-task also has to be. + */ + /* addTaskRepeatCfgForSubTask: any = createEffect( + () => + this._actions$.pipe( + ofType( + addSubTask + ), + tap(async (aAction) => { + const task = aAction.task; + + console.log("A TASKKK"); + + // TODO: is there an easier way to get to the parent task? + const allTasks = await this._taskService.allTasks$.pipe(first()).toPromise(); + const parentTask = allTasks.find((aTask) => aTask.id === aAction.parentId); + + console.log("PARENT TASK", parentTask); + + if (parentTask !== undefined && parentTask.repeatCfgId !== null) { + const repeatCfg = { + ...DEFAULT_TASK_REPEAT_CFG, + startDate: getWorklogStr(), // TODO: What is happening here? Is this correct? + title: task.title, + notes: task.notes || undefined, + tagIds: task.tagIds, + defaultEstimate: task.timeEstimate + }; + + this._taskRepeatCfgService.addTaskRepeatCfgToTask( + task.id, + task.projectId, + repeatCfg + ); + } + }) + ), + { dispatch: false } // Question: What exactly does this do? + ); */ + private triggerRepeatableTaskCreation$ = merge( this._syncTriggerService.afterInitialSyncDoneAndDataLoadedInitially$, this._actions$.pipe( diff --git a/src/app/features/work-context/work-context.model.ts b/src/app/features/work-context/work-context.model.ts index c915ef6047b..04fe7cc3acc 100644 --- a/src/app/features/work-context/work-context.model.ts +++ b/src/app/features/work-context/work-context.model.ts @@ -53,6 +53,7 @@ export type WorkContextThemeCfg = Readonly<{ backgroundImageLight: string | null; }>; +// TODO: do you mean tag = day? If yes, shouldn't we change this to English? export enum WorkContextType { PROJECT = 'PROJECT', TAG = 'TAG', From 88c07878094951385d9174fbc180e524f6febe44 Mon Sep 17 00:00:00 2001 From: Lisa Date: Fri, 30 Jun 2023 16:13:05 +0200 Subject: [PATCH 2/3] feat:add repeat subtasks for repeating tasks #427 --- .../store/task-repeat-cfg.effects.ts | 179 +++++++++++++----- .../store/task-repeat-cfg.reducer.ts | 18 +- .../task-repeat-cfg/task-repeat-cfg.model.ts | 2 + .../task-repeat-cfg.service.ts | 75 +++++++- 4 files changed, 220 insertions(+), 54 deletions(-) diff --git a/src/app/features/task-repeat-cfg/store/task-repeat-cfg.effects.ts b/src/app/features/task-repeat-cfg/store/task-repeat-cfg.effects.ts index 52fce531743..93e0788c11b 100644 --- a/src/app/features/task-repeat-cfg/store/task-repeat-cfg.effects.ts +++ b/src/app/features/task-repeat-cfg/store/task-repeat-cfg.effects.ts @@ -5,6 +5,7 @@ import { delay, filter, first, + map, mergeMap, take, tap, @@ -28,9 +29,10 @@ import { TaskRepeatCfgService } from '../task-repeat-cfg.service'; import { DEFAULT_TASK_REPEAT_CFG, TaskRepeatCfg, + TaskRepeatCfgCopy, TaskRepeatCfgState, } from '../task-repeat-cfg.model'; -import { forkJoin, from, merge, of } from 'rxjs'; +import { Observable, forkJoin, from, merge, of } from 'rxjs'; import { setActiveWorkContext } from '../../work-context/store/work-context.actions'; import { SyncTriggerService } from '../../../imex/sync/sync-trigger.service'; import { SyncProviderService } from '../../../imex/sync/sync-provider.service'; @@ -64,82 +66,130 @@ export class TaskRepeatCfgEffects { { dispatch: false }, ); - /* addTaskRepeatCfgForSubTasksOf: any = createEffect( + /** + * Updates the repeatCfg of a task, if the task was updated. + */ + updateRepeatCfgWhenTaskUpdates: any = createEffect( () => this._actions$.pipe( - ofType( - addTaskRepeatCfgToTask - ), + ofType(updateTask), + tap(async (aAction) => { + const allTasks = await this._taskService.allTasks$.pipe(first()).toPromise(); + const task = allTasks.find((aTask) => aTask.id === aAction.task.id)!; + + if (task.repeatCfgId !== null) { + const repeatCfgForTask = await this._taskRepeatCfgService + .getTaskRepeatCfgById$(task.repeatCfgId) + .pipe(first()) + .toPromise(); + + const taskChanges = aAction.task.changes; + + // TODO: is there a better way to do this? Is there anything missing? + const repeatCfgChanges: Partial = { + projectId: taskChanges.projectId ?? repeatCfgForTask.projectId, + title: taskChanges.title ?? repeatCfgForTask.title, + tagIds: taskChanges.tagIds ?? repeatCfgForTask.tagIds, + notes: taskChanges.notes ?? repeatCfgForTask.notes, + }; + + // TODO: Do we need to do this for all instances?? + this._taskRepeatCfgService.updateTaskRepeatCfg( + task.repeatCfgId, + repeatCfgChanges, + ); + } + }), + ), + { dispatch: false }, // Question: What exactly does this do? + ); + + /** + * When a main task is made repeatable, this function checks if there are subtasks. + * If that is the case, a repeat-cfg gets added for each subtask, too. + */ + addTaskRepeatCfgForSubTasksOf: any = createEffect( + () => + this._actions$.pipe( + ofType(addTaskRepeatCfgToTask), tap(async (aAction) => { // TODO: is there an easier way to get to the parent task? const allTasks = await this._taskService.allTasks$.pipe(first()).toPromise(); const parentTask = allTasks.find((aTask) => aTask.id === aAction.taskId); + const parentTaskRepeatCfg = aAction.taskRepeatCfg; if (parentTask !== undefined && parentTask.subTaskIds.length > 0) { - parentTask.subTaskIds.forEach(aSubTaskId => { + for (const aSubTaskId of parentTask.subTaskIds) { const task = allTasks.find((aTask) => aTask.id === aSubTaskId)!; const repeatCfg = { - ...DEFAULT_TASK_REPEAT_CFG, - startDate: getWorklogStr(), // TODO: What is happening here? Is this correct? + ...parentTaskRepeatCfg, + // TODO: anything missing in this list that should not be overwritten by the parent? title: task.title, notes: task.notes, - tagIds: task.tagIds, - defaultEstimate: task.timeEstimate + defaultEstimate: task.timeEstimate, // is this correct? + parentId: parentTask.repeatCfgId, }; this._taskRepeatCfgService.addTaskRepeatCfgToTask( task.id, task.projectId, - repeatCfg + repeatCfg, ); - }); + } } - }) + }), ), - { dispatch: false } // Question: What exactly does this do? - ); */ + { dispatch: false }, // Question: What exactly does this do? + ); /** * When adding a sub task, this function checks if the parent is a repeatable task and therefore the sub-task also has to be. + * If that is the case, a repeat-cfg gets added for each subtask, too. */ - /* addTaskRepeatCfgForSubTask: any = createEffect( + addTaskRepeatCfgForSubTask: any = createEffect( () => this._actions$.pipe( - ofType( - addSubTask - ), + ofType(addSubTask), tap(async (aAction) => { const task = aAction.task; - console.log("A TASKKK"); + // we only want to continue if the task doesn't already have a repeatCfgId + if (task.repeatCfgId === null) { + console.log('A TASKKK', task); - // TODO: is there an easier way to get to the parent task? - const allTasks = await this._taskService.allTasks$.pipe(first()).toPromise(); - const parentTask = allTasks.find((aTask) => aTask.id === aAction.parentId); - - console.log("PARENT TASK", parentTask); - - if (parentTask !== undefined && parentTask.repeatCfgId !== null) { - const repeatCfg = { - ...DEFAULT_TASK_REPEAT_CFG, - startDate: getWorklogStr(), // TODO: What is happening here? Is this correct? - title: task.title, - notes: task.notes || undefined, - tagIds: task.tagIds, - defaultEstimate: task.timeEstimate - }; - - this._taskRepeatCfgService.addTaskRepeatCfgToTask( - task.id, - task.projectId, - repeatCfg - ); + // TODO: is there an easier way to get to the parent task? + const allTasks = await this._taskService.allTasks$.pipe(first()).toPromise(); + const parentTask = allTasks.find((aTask) => aTask.id === aAction.parentId); + + console.log('PARENT TASK', parentTask); + + if (parentTask !== undefined && parentTask.repeatCfgId !== null) { + const parentRepeatCfg = await this._taskRepeatCfgService + .getTaskRepeatCfgById$(parentTask.repeatCfgId) + .pipe(first()) + .toPromise(); + + const repeatCfg = { + ...parentRepeatCfg, + // TODO: anything missing in this list that should not be overwritten by the parent? + title: task.title, + notes: task.notes || undefined, + defaultEstimate: task.timeEstimate, // is this correct? + parentId: parentRepeatCfg.id, + }; + + this._taskRepeatCfgService.addTaskRepeatCfgToTask( + task.id, + task.projectId, + repeatCfg, + ); + } } - }) + }), ), - { dispatch: false } // Question: What exactly does this do? - ); */ + { dispatch: false }, // Question: What exactly does this do? + ); private triggerRepeatableTaskCreation$ = merge( this._syncTriggerService.afterInitialSyncDoneAndDataLoadedInitially$, @@ -168,8 +218,14 @@ export class TaskRepeatCfgEffects { // existing tasks with sub tasks are loaded, because need to move them to the archive mergeMap(([taskRepeatCfgs, currentTaskId]) => { + // we only want to work with parent tasks, so filter out sub tasks + const parentTasksRepeatCfgs = taskRepeatCfgs.filter( + (aTask) => aTask.parentId === null, + ); + // NOTE sorting here is important - const sorted = taskRepeatCfgs.sort(sortRepeatableTaskCfgs); + const sorted = parentTasksRepeatCfgs.sort(sortRepeatableTaskCfgs); + return from(sorted).pipe( mergeMap((taskRepeatCfg: TaskRepeatCfg) => this._taskRepeatCfgService.getActionsForTaskRepeatCfg( @@ -189,16 +245,24 @@ export class TaskRepeatCfgEffects { ofType(deleteTaskRepeatCfg), concatMap(({ id }) => this._taskService.getTasksByRepeatCfgId$(id).pipe(take(1))), filter((tasks) => tasks && !!tasks.length), - mergeMap((tasks: Task[]) => - tasks.map((task) => + concatMap((value: TaskCopy[], index) => { + const tasks: Readonly[] = value; + const allSubIds = tasks.flatMap((aTask) => aTask.subTaskIds); + + return this._taskService + .getByIdsLive$(allSubIds) + .pipe(map((aAllSubTasks: TaskCopy[]) => ({ aAllSubTasks, tasks }))); + }), + mergeMap(({ aAllSubTasks, tasks }) => { + return [...aAllSubTasks, ...tasks].map((task) => updateTask({ task: { id: task.id, changes: { repeatCfgId: null }, }, }), - ), - ), + ); + }), ), ); @@ -206,7 +270,22 @@ export class TaskRepeatCfgEffects { () => this._actions$.pipe( ofType(deleteTaskRepeatCfg), - tap(({ id }) => { + tap(async ({ id }) => { + const subTasks = await this._taskRepeatCfgService + .getTaskRepeatCfgsByParentId$(id) + .pipe(first()) + .toPromise(); + + if (subTasks.length > 0) { + const subTaskIds = subTasks.map((aTask) => aTask.id); + + // remove repeat cfgs from sub tasks + for (const aId of subTaskIds) { + this._removeRepeatCfgFromArchiveTasks(aId); + } + } + + // remove repeat cfg from main task this._removeRepeatCfgFromArchiveTasks(id); }), ), diff --git a/src/app/features/task-repeat-cfg/store/task-repeat-cfg.reducer.ts b/src/app/features/task-repeat-cfg/store/task-repeat-cfg.reducer.ts index 48f2d540abe..24411183827 100644 --- a/src/app/features/task-repeat-cfg/store/task-repeat-cfg.reducer.ts +++ b/src/app/features/task-repeat-cfg/store/task-repeat-cfg.reducer.ts @@ -45,6 +45,17 @@ export const selectTaskRepeatCfgById = createSelector( }, ); +export const selectTaskRepeatCfgByParentId = createSelector( + selectAllTaskRepeatCfgs, + (taskRepeatCfgs: TaskRepeatCfg[], props: { id: string }): TaskRepeatCfg[] => { + const cfgs = taskRepeatCfgs.filter( + (aTaskRepeatCfg) => aTaskRepeatCfg.parentId === props.id, + ); + + return cfgs; + }, +); + export const selectTaskRepeatCfgsWithStartTime = createSelector( selectAllTaskRepeatCfgs, (taskRepeatCfgs: TaskRepeatCfg[]): TaskRepeatCfg[] => { @@ -55,7 +66,12 @@ export const selectTaskRepeatCfgsWithStartTime = createSelector( export const selectTaskRepeatCfgsSortedByTitleAndProject = createSelector( selectAllTaskRepeatCfgs, (taskRepeatCfgs: TaskRepeatCfg[]): TaskRepeatCfg[] => { - return taskRepeatCfgs.sort((a, b) => { + // we only want main tasks, no sub tasks + const mainTaskRepeatCfgs = taskRepeatCfgs.filter( + (aTaskRepeatCfg) => aTaskRepeatCfg.parentId === null, + ); + + return mainTaskRepeatCfgs.sort((a, b) => { if (a.projectId !== b.projectId) { if (a.projectId === null) { return -1; diff --git a/src/app/features/task-repeat-cfg/task-repeat-cfg.model.ts b/src/app/features/task-repeat-cfg/task-repeat-cfg.model.ts index 844e6b48944..9b788ace95e 100644 --- a/src/app/features/task-repeat-cfg/task-repeat-cfg.model.ts +++ b/src/app/features/task-repeat-cfg/task-repeat-cfg.model.ts @@ -23,6 +23,7 @@ export type RepeatQuickSetting = export interface TaskRepeatCfgCopy { id: string; + parentId: string | null; projectId: string | null; lastTaskCreation: number; title: string | null; @@ -66,6 +67,7 @@ export const DEFAULT_TASK_REPEAT_CFG: Omit = { defaultEstimate: undefined, // id: undefined, + parentId: null, projectId: null, // lastTaskCreation: Date.now() - 24 * 60 * 60 * 1000, diff --git a/src/app/features/task-repeat-cfg/task-repeat-cfg.service.ts b/src/app/features/task-repeat-cfg/task-repeat-cfg.service.ts index a87fb5b1ee3..c6513e2efad 100644 --- a/src/app/features/task-repeat-cfg/task-repeat-cfg.service.ts +++ b/src/app/features/task-repeat-cfg/task-repeat-cfg.service.ts @@ -4,6 +4,7 @@ import { selectAllTaskRepeatCfgs, selectTaskRepeatCfgById, selectTaskRepeatCfgByIdAllowUndefined, + selectTaskRepeatCfgByParentId, selectTaskRepeatCfgsDueOnDay, selectTaskRepeatCfgsWithStartTime, } from './store/task-repeat-cfg.reducer'; @@ -29,7 +30,7 @@ import { take } from 'rxjs/operators'; import { TaskService } from '../tasks/task.service'; import { TODAY_TAG } from '../tag/tag.const'; import { Task, TaskPlanned } from '../tasks/task.model'; -import { addTask, scheduleTask } from '../tasks/store/task.actions'; +import { addSubTask, addTask, scheduleTask } from '../tasks/store/task.actions'; import { WorkContextService } from '../work-context/work-context.service'; import { WorkContextType } from '../work-context/work-context.model'; import { isValidSplitTime } from '../../util/is-valid-split-time'; @@ -66,6 +67,10 @@ export class TaskRepeatCfgService { return this._store$.pipe(select(selectTaskRepeatCfgById, { id })); } + getTaskRepeatCfgsByParentId$(id: string): Observable { + return this._store$.pipe(select(selectTaskRepeatCfgByParentId, { id })); + } + getTaskRepeatCfgByIdAllowUndefined$(id: string): Observable { return this._store$.pipe(select(selectTaskRepeatCfgByIdAllowUndefined, { id })); } @@ -178,6 +183,7 @@ export class TaskRepeatCfgService { Promise< ( | ReturnType + | ReturnType | ReturnType | ReturnType )[] @@ -205,9 +211,17 @@ export class TaskRepeatCfgService { const createNewActions: ( | ReturnType + | ReturnType | ReturnType | ReturnType - )[] = [ + )[] = []; + + // get subtasks if they exist + const taskRepeatSubTasks = ( + await this.taskRepeatCfgs$.pipe(take(1)).toPromise() + ).filter((aSubTask) => aSubTask.parentId === taskRepeatCfg.id); + + createNewActions.push( addTask({ task: { ...task, @@ -221,6 +235,27 @@ export class TaskRepeatCfgService { isAddToBacklog: false, isAddToBottom, }), + ); + + // actions to generate all sub tasks for parent task + taskRepeatSubTasks.forEach((aRepeatSubTask) => { + const subTask = this._getSubTaskRepeatTemplate(aRepeatSubTask, task.id); + + console.log('GETTING SB TASK', subTask); + + createNewActions.push( + addSubTask({ + task: { + ...subTask, + created: targetDayDate, + }, + parentId: task.id, + }), + ); + }); + + // update parent repeatCfg + createNewActions.push( updateTaskRepeatCfg({ taskRepeatCfg: { id: taskRepeatCfg.id, @@ -230,7 +265,22 @@ export class TaskRepeatCfgService { }, // TODO fix type }), - ]; + ); + + // update sub-items repeat cfg + taskRepeatSubTasks.forEach((aRepeatSubTask) => { + createNewActions.push( + updateTaskRepeatCfg({ + taskRepeatCfg: { + id: aRepeatSubTask.id, + changes: { + lastTaskCreation: targetDayDate, + }, + }, + // TODO fix type + }), + ); + }); // Schedule if given if (isValidSplitTime(taskRepeatCfg.startTime) && taskRepeatCfg.remindAt) { @@ -271,4 +321,23 @@ export class TaskRepeatCfgService { isAddToBottom: taskRepeatCfg.order > 0, }; } + + private _getSubTaskRepeatTemplate( + taskRepeatCfg: TaskRepeatCfg, + aParentId: string, + ): Task { + const isAddToTodayAsFallback = + !taskRepeatCfg.projectId && !taskRepeatCfg.tagIds.length; + return this._taskService.createNewTaskWithDefaults({ + title: taskRepeatCfg.title, + additional: { + repeatCfgId: taskRepeatCfg.id, + timeEstimate: taskRepeatCfg.defaultEstimate, + projectId: taskRepeatCfg.projectId, + tagIds: isAddToTodayAsFallback ? [TODAY_TAG.id] : taskRepeatCfg.tagIds || [], + notes: taskRepeatCfg.notes || '', + parentId: aParentId, + }, + }); + } } From 9713138baf23a973c2af19e1185fb3dc84cb6e21 Mon Sep 17 00:00:00 2001 From: Lisa Date: Fri, 30 Jun 2023 16:17:10 +0200 Subject: [PATCH 3/3] removed comment and console log --- src/app/features/task-repeat-cfg/task-repeat-cfg.service.ts | 2 -- src/app/features/work-context/work-context.model.ts | 1 - 2 files changed, 3 deletions(-) diff --git a/src/app/features/task-repeat-cfg/task-repeat-cfg.service.ts b/src/app/features/task-repeat-cfg/task-repeat-cfg.service.ts index c6513e2efad..8422129e9dd 100644 --- a/src/app/features/task-repeat-cfg/task-repeat-cfg.service.ts +++ b/src/app/features/task-repeat-cfg/task-repeat-cfg.service.ts @@ -241,8 +241,6 @@ export class TaskRepeatCfgService { taskRepeatSubTasks.forEach((aRepeatSubTask) => { const subTask = this._getSubTaskRepeatTemplate(aRepeatSubTask, task.id); - console.log('GETTING SB TASK', subTask); - createNewActions.push( addSubTask({ task: { diff --git a/src/app/features/work-context/work-context.model.ts b/src/app/features/work-context/work-context.model.ts index 04fe7cc3acc..c915ef6047b 100644 --- a/src/app/features/work-context/work-context.model.ts +++ b/src/app/features/work-context/work-context.model.ts @@ -53,7 +53,6 @@ export type WorkContextThemeCfg = Readonly<{ backgroundImageLight: string | null; }>; -// TODO: do you mean tag = day? If yes, shouldn't we change this to English? export enum WorkContextType { PROJECT = 'PROJECT', TAG = 'TAG',