From d7b2683ffce23ed161b003e5801f939f7b8f63f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20B=C3=B6ckelmann?= Date: Sat, 16 Mar 2024 10:57:50 +0100 Subject: [PATCH] Ui Support for Creating Subtasks --- .../core/api/controller/TaskController.java | 16 +++++ .../java/core/entities/timemanager/Task.java | 2 +- frontend/src/api/api/task.service.ts | 70 +++++++++++++++++++ .../active-task-overview.component.ts | 3 +- .../task-overview/task-overview.component.ts | 3 +- .../taskgroup-dashboard.component.ts | 3 +- .../task-dashboard.component.ts | 3 +- .../task-detail-overview.component.html | 2 +- .../task-detail-overview.component.ts | 15 +++- .../app/tasks/task-editor/TaskEditorData.ts | 1 + .../task-editor/task-editor.component.html | 3 +- .../task-editor/task-editor.component.ts | 37 +++++++++- .../upcoming-task-overview.component.ts | 3 +- openapi.yaml | 47 +++++++++++++ 14 files changed, 197 insertions(+), 11 deletions(-) diff --git a/backend/src/main/java/core/api/controller/TaskController.java b/backend/src/main/java/core/api/controller/TaskController.java index 823e1aa..408f6f7 100644 --- a/backend/src/main/java/core/api/controller/TaskController.java +++ b/backend/src/main/java/core/api/controller/TaskController.java @@ -164,4 +164,20 @@ public class TaskController { taskService.finishTask(taskPermissionResult.getResult()); return ResponseEntity.ok(new SimpleStatusResponse("success")); } + + @PutMapping("/tasks/{taskID}/createSubtask") + public ResponseEntity onCreateSubTask(@PathVariable long taskID, @Valid @RequestBody TaskFieldInfo taskFieldInfo) { + var taskPermissionResult = taskService.getTaskPermissions(taskID, SecurityContextHolder.getContext().getAuthentication().getName()); + if(taskPermissionResult.hasIssue()) { + return taskPermissionResult.mapToResponseEntity(); + } + + var serviceResult = taskService.createSubTask(taskPermissionResult.getResult(), taskFieldInfo); + if(serviceResult.hasIssue()) { + return serviceResult.mapToResponseEntity(); + } else { + return ResponseEntity.ok(new TaskEntityInfo(serviceResult.getResult())); + } + + } } diff --git a/backend/src/main/java/core/entities/timemanager/Task.java b/backend/src/main/java/core/entities/timemanager/Task.java index b66b9c5..c1a8d0f 100644 --- a/backend/src/main/java/core/entities/timemanager/Task.java +++ b/backend/src/main/java/core/entities/timemanager/Task.java @@ -33,7 +33,7 @@ public class Task { @OneToOne(mappedBy = "task", cascade = CascadeType.ALL, orphanRemoval = true) private TaskSerieItem taskSerieItem; - @OneToMany(mappedBy = "parent", cascade = CascadeType.ALL, orphanRemoval = true) + @OneToMany(mappedBy = "parent", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.EAGER) private Set subtasks; @ManyToOne diff --git a/frontend/src/api/api/task.service.ts b/frontend/src/api/api/task.service.ts index d34276b..2f417ef 100644 --- a/frontend/src/api/api/task.service.ts +++ b/frontend/src/api/api/task.service.ts @@ -153,6 +153,76 @@ export class TaskService { ); } + /** + * Creates Subtask + * Create Subtask + * @param taskID internal id of task + * @param taskFieldInfo + * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. + * @param reportProgress flag to report request and response progress. + */ + public tasksTaskIDCreateSubtaskPut(taskID: number, taskFieldInfo?: TaskFieldInfo, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext}): Observable; + public tasksTaskIDCreateSubtaskPut(taskID: number, taskFieldInfo?: TaskFieldInfo, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext}): Observable>; + public tasksTaskIDCreateSubtaskPut(taskID: number, taskFieldInfo?: TaskFieldInfo, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext}): Observable>; + public tasksTaskIDCreateSubtaskPut(taskID: number, taskFieldInfo?: TaskFieldInfo, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext}): Observable { + if (taskID === null || taskID === undefined) { + throw new Error('Required parameter taskID was null or undefined when calling tasksTaskIDCreateSubtaskPut.'); + } + + let localVarHeaders = this.defaultHeaders; + + let localVarCredential: string | undefined; + // authentication (API_TOKEN) required + localVarCredential = this.configuration.lookupCredential('API_TOKEN'); + if (localVarCredential) { + localVarHeaders = localVarHeaders.set('Authorization', 'Bearer ' + localVarCredential); + } + + let localVarHttpHeaderAcceptSelected: string | undefined = options && options.httpHeaderAccept; + if (localVarHttpHeaderAcceptSelected === undefined) { + // to determine the Accept header + const httpHeaderAccepts: string[] = [ + 'application/json' + ]; + localVarHttpHeaderAcceptSelected = this.configuration.selectHeaderAccept(httpHeaderAccepts); + } + if (localVarHttpHeaderAcceptSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Accept', localVarHttpHeaderAcceptSelected); + } + + let localVarHttpContext: HttpContext | undefined = options && options.context; + if (localVarHttpContext === undefined) { + localVarHttpContext = new HttpContext(); + } + + + // to determine the Content-Type header + const consumes: string[] = [ + 'application/json' + ]; + const httpContentTypeSelected: string | undefined = this.configuration.selectHeaderContentType(consumes); + if (httpContentTypeSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Content-Type', httpContentTypeSelected); + } + + let responseType_: 'text' | 'json' = 'json'; + if(localVarHttpHeaderAcceptSelected && localVarHttpHeaderAcceptSelected.startsWith('text')) { + responseType_ = 'text'; + } + + return this.httpClient.put(`${this.configuration.basePath}/tasks/${encodeURIComponent(String(taskID))}/createSubtask`, + taskFieldInfo, + { + context: localVarHttpContext, + responseType: responseType_, + withCredentials: this.configuration.withCredentials, + headers: localVarHeaders, + observe: observe, + reportProgress: reportProgress + } + ); + } + /** * edits an existing task * edits an existing task diff --git a/frontend/src/app/active-task-overview/active-task-overview.component.ts b/frontend/src/app/active-task-overview/active-task-overview.component.ts index c945579..c82a11e 100644 --- a/frontend/src/app/active-task-overview/active-task-overview.component.ts +++ b/frontend/src/app/active-task-overview/active-task-overview.component.ts @@ -93,7 +93,8 @@ export class ActiveTaskOverviewComponent implements OnInit{ editTask(editedTask: TaskTaskgroupInfo) { const taskEditorInfo: TaskEditorData = { task: editedTask, - taskgroupID: editedTask.taskgroups[editedTask.taskgroups.length-1].taskgroupID + taskgroupID: editedTask.taskgroups[editedTask.taskgroups.length-1].taskgroupID, + parentTask: undefined }; this.dialog.open(TaskEditorComponent, {data: taskEditorInfo, width: "600px"}) } diff --git a/frontend/src/app/dashboard/task-overview/task-overview.component.ts b/frontend/src/app/dashboard/task-overview/task-overview.component.ts index ef54dca..6c19c3f 100644 --- a/frontend/src/app/dashboard/task-overview/task-overview.component.ts +++ b/frontend/src/app/dashboard/task-overview/task-overview.component.ts @@ -75,7 +75,8 @@ export class TaskOverviewComponent { openTaskCreation() { const editorData: TaskEditorData = { task: undefined, - taskgroupID: this.taskgroupID! + taskgroupID: this.taskgroupID!, + parentTask: undefined } const dialogRef = this.dialog.open(TaskEditorComponent, {data: editorData, width: "600px"}) dialogRef.afterClosed().subscribe(res => { diff --git a/frontend/src/app/taskgroups/taskgroup-dashboard/taskgroup-dashboard.component.ts b/frontend/src/app/taskgroups/taskgroup-dashboard/taskgroup-dashboard.component.ts index 79835b5..8fead1a 100644 --- a/frontend/src/app/taskgroups/taskgroup-dashboard/taskgroup-dashboard.component.ts +++ b/frontend/src/app/taskgroups/taskgroup-dashboard/taskgroup-dashboard.component.ts @@ -104,7 +104,8 @@ export class TaskgroupDashboardComponent implements OnInit { openTaskCreation() { const editorData: TaskEditorData = { task: undefined, - taskgroupID: this.taskgroupID + taskgroupID: this.taskgroupID, + parentTask: undefined } const dialogRef = this.dialog.open(TaskEditorComponent, {data: editorData, width: "600px"}) dialogRef.afterClosed().subscribe(res => { diff --git a/frontend/src/app/tasks/task-dashboard/task-dashboard.component.ts b/frontend/src/app/tasks/task-dashboard/task-dashboard.component.ts index 49b5e14..21e8949 100644 --- a/frontend/src/app/tasks/task-dashboard/task-dashboard.component.ts +++ b/frontend/src/app/tasks/task-dashboard/task-dashboard.component.ts @@ -97,7 +97,8 @@ export class TaskDashboardComponent implements OnChanges{ editTask(task: TaskEntityInfo) { const taskEditorInfo: TaskEditorData = { task: task, - taskgroupID: this.taskgroupID! + taskgroupID: this.taskgroupID!, + parentTask: undefined }; const dialogRef = this.dialog.open(TaskEditorComponent, {data: taskEditorInfo, minWidth: "400px"}) } diff --git a/frontend/src/app/tasks/task-detail-overview/task-detail-overview.component.html b/frontend/src/app/tasks/task-detail-overview/task-detail-overview.component.html index 24038f3..90577ea 100644 --- a/frontend/src/app/tasks/task-detail-overview/task-detail-overview.component.html +++ b/frontend/src/app/tasks/task-detail-overview/task-detail-overview.component.html @@ -7,7 +7,7 @@
{{taskStatus + " " + task!.taskName}}
- + diff --git a/frontend/src/app/tasks/task-detail-overview/task-detail-overview.component.ts b/frontend/src/app/tasks/task-detail-overview/task-detail-overview.component.ts index d557009..66259bd 100644 --- a/frontend/src/app/tasks/task-detail-overview/task-detail-overview.component.ts +++ b/frontend/src/app/tasks/task-detail-overview/task-detail-overview.component.ts @@ -135,7 +135,8 @@ export class TaskDetailOverviewComponent implements OnInit { if(this.task != undefined) { const taskEditorInfo: TaskEditorData = { task: this.task!, - taskgroupID: this.taskgroupID! + taskgroupID: this.taskgroupID!, + parentTask: undefined }; this.dialog.open(TaskEditorComponent, {data: taskEditorInfo, width: "600px"}) } @@ -166,4 +167,16 @@ export class TaskDetailOverviewComponent implements OnInit { minWidth: "400px" }) } + + addSubtask() { + const editorData: TaskEditorData = { + task: undefined, + taskgroupID: this.taskgroupID, + parentTask: this.task + } + const dialogRef = this.dialog.open(TaskEditorComponent, { + data: editorData, + minWidth: "400px" + }) + } } diff --git a/frontend/src/app/tasks/task-editor/TaskEditorData.ts b/frontend/src/app/tasks/task-editor/TaskEditorData.ts index f1140e2..30ddcc7 100644 --- a/frontend/src/app/tasks/task-editor/TaskEditorData.ts +++ b/frontend/src/app/tasks/task-editor/TaskEditorData.ts @@ -3,4 +3,5 @@ import {TaskEntityInfo, TaskTaskgroupInfo} from "../../../api"; export interface TaskEditorData { taskgroupID: number; task: TaskTaskgroupInfo | TaskEntityInfo | undefined + parentTask: TaskEntityInfo | undefined } diff --git a/frontend/src/app/tasks/task-editor/task-editor.component.html b/frontend/src/app/tasks/task-editor/task-editor.component.html index c413dff..1731ac0 100644 --- a/frontend/src/app/tasks/task-editor/task-editor.component.html +++ b/frontend/src/app/tasks/task-editor/task-editor.component.html @@ -1,6 +1,7 @@

Edit Task ({{editorData.task!.taskName}})

-

Create New Task

+

Create New {{editorData.parentTask != undefined? 'Sub-':''}}Task

+

Create a new Subtask for the Task {{editorData.parentTask!.taskName}}

Name diff --git a/frontend/src/app/tasks/task-editor/task-editor.component.ts b/frontend/src/app/tasks/task-editor/task-editor.component.ts index 6c4eb37..5eacda2 100644 --- a/frontend/src/app/tasks/task-editor/task-editor.component.ts +++ b/frontend/src/app/tasks/task-editor/task-editor.component.ts @@ -61,7 +61,36 @@ export class TaskEditorComponent implements OnInit { startDate_formatted = moment(this.startDate.value).format('YYYY-MM-DDTHH:mm:ss.SSSZ'); } - this.taskService.tasksTaskgroupIDPut(this.editorData.taskgroupID, { + if(this.editorData.parentTask != undefined) { + this.createSubTask(startDate_formatted, endDate_formatted); + } else { + this.taskService.tasksTaskgroupIDPut(this.editorData.taskgroupID, { + taskName: this.nameCtrl.value, + eta: this.etaCtrl.value, + startDate: startDate_formatted, + deadline: endDate_formatted, + finishable: this.finishable, + }).subscribe({ + next: resp => { + this.dialog.close(resp); + }, + error: err => { + if(err.status == 403) { + this.snackbar.open("No permission", "", {duration: 2000}); + } else if(err.status == 404) { + this.snackbar.open("Taskgroup not found", "", {duration: 2000}); + } else if(err.status == 409) { + this.snackbar.open("Task already exists", "", {duration: 2000}); + } else { + this.snackbar.open("Unexpected error", "", {duration: 3000}); + } + } + }) + } + } + + createSubTask(startDate_formatted: string|undefined, endDate_formatted: string|undefined) { + this.taskService.tasksTaskIDCreateSubtaskPut(this.editorData.parentTask!.taskID, { taskName: this.nameCtrl.value, eta: this.etaCtrl.value, startDate: startDate_formatted, @@ -78,7 +107,9 @@ export class TaskEditorComponent implements OnInit { this.snackbar.open("Taskgroup not found", "", {duration: 2000}); } else if(err.status == 409) { this.snackbar.open("Task already exists", "", {duration: 2000}); - } else { + } else if(err.status == 400) { + this.snackbar.open("Invalid Dates", "", {duration: 2000}) + } else { this.snackbar.open("Unexpected error", "", {duration: 3000}); } } @@ -123,4 +154,6 @@ export class TaskEditorComponent implements OnInit { } }) } + + protected readonly parent = parent; } diff --git a/frontend/src/app/upcoming-task-overview/upcoming-task-overview.component.ts b/frontend/src/app/upcoming-task-overview/upcoming-task-overview.component.ts index b022b24..d1e5891 100644 --- a/frontend/src/app/upcoming-task-overview/upcoming-task-overview.component.ts +++ b/frontend/src/app/upcoming-task-overview/upcoming-task-overview.component.ts @@ -59,7 +59,8 @@ export class UpcomingTaskOverviewComponent implements OnInit{ editTask(editedTask: TaskTaskgroupInfo) { const taskEditorInfo: TaskEditorData = { task: editedTask, - taskgroupID: editedTask.taskgroups[editedTask.taskgroups.length-1].taskgroupID + taskgroupID: editedTask.taskgroups[editedTask.taskgroups.length-1].taskgroupID, + parentTask: undefined }; this.dialog.open(TaskEditorComponent, {data: taskEditorInfo, width: "600px"}) } diff --git a/openapi.yaml b/openapi.yaml index 2ff5ea4..c3eb30e 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -1269,6 +1269,53 @@ paths: schema: type: object $ref: "#/components/schemas/SimpleStatusResponse" + /tasks/{taskID}/createSubtask: + put: + security: + - API_TOKEN: [] + tags: + - task + description: Create Subtask + summary: Creates Subtask + parameters: + - name: taskID + in: path + description: internal id of task + required: true + schema: + type: number + example: 1 + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/TaskFieldInfo' + responses: + 200: + description: Operation successfull + content: + application/json: + schema: + $ref: '#/components/schemas/TaskEntityInfo' + 403: + description: No permission + content: + application/json: + schema: + $ref: '#/components/schemas/SimpleStatusResponse' + 404: + description: Task not found + content: + application/json: + schema: + $ref: '#/components/schemas/SimpleStatusResponse' + 400: + description: Invalid start/end date + content: + application/json: + schema: + $ref: '#/components/schemas/SimpleStatusResponse' + /schedules: get: security: