From 72e56efea3f453f592b04895cf2cc9b429b17962 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Sun, 22 Oct 2023 10:08:44 +0200 Subject: [PATCH] Delete and clear Tasks (Frontend) --- frontend/src/api/.openapi-generator/FILES | 1 + frontend/src/api/api/task.service.ts | 70 +++++++++++++ frontend/src/api/api/taskgroup.service.ts | 60 +++++++++++ frontend/src/api/model/models.ts | 1 + .../src/api/model/simpleStatusResponse.ts | 28 ++++++ frontend/src/api/model/taskEntityInfo.ts | 4 + frontend/src/app/app.module.ts | 4 +- .../clear-task-dialog.component.css | 3 + .../clear-task-dialog.component.html | 13 +++ .../clear-task-dialog.component.spec.ts | 21 ++++ .../clear-task-dialog.component.ts | 69 +++++++++++++ .../task-dashboard.component.css | 8 +- .../task-dashboard.component.html | 3 +- .../task-dashboard.component.ts | 42 +++++++- openapi.yaml | 99 +++++++++++++++++++ 15 files changed, 418 insertions(+), 8 deletions(-) create mode 100644 frontend/src/api/model/simpleStatusResponse.ts create mode 100644 frontend/src/app/tasks/clear-task-dialog/clear-task-dialog.component.css create mode 100644 frontend/src/app/tasks/clear-task-dialog/clear-task-dialog.component.html create mode 100644 frontend/src/app/tasks/clear-task-dialog/clear-task-dialog.component.spec.ts create mode 100644 frontend/src/app/tasks/clear-task-dialog/clear-task-dialog.component.ts diff --git a/frontend/src/api/.openapi-generator/FILES b/frontend/src/api/.openapi-generator/FILES index 7a04693..52fe019 100644 --- a/frontend/src/api/.openapi-generator/FILES +++ b/frontend/src/api/.openapi-generator/FILES @@ -26,6 +26,7 @@ model/propertiesInfo.ts model/propertyInfo.ts model/propertyUpdateRequest.ts model/signUpRequest.ts +model/simpleStatusResponse.ts model/taskEntityInfo.ts model/taskFieldInfo.ts model/taskgroupEntityInfo.ts diff --git a/frontend/src/api/api/task.service.ts b/frontend/src/api/api/task.service.ts index 725f0bd..36cdf7e 100644 --- a/frontend/src/api/api/task.service.ts +++ b/frontend/src/api/api/task.service.ts @@ -19,6 +19,7 @@ import { CustomHttpParameterCodec } from '../encoder'; import { Observable } from 'rxjs'; import { InlineResponse403 } from '../model/models'; +import { SimpleStatusResponse } from '../model/models'; import { TaskEntityInfo } from '../model/models'; import { TaskFieldInfo } from '../model/models'; @@ -87,6 +88,75 @@ export class TaskService { return httpParams; } + /** + * edits an existing task + * edits an existing task + * @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 tasksTaskIDDelete(taskID: number, taskFieldInfo?: TaskFieldInfo, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext}): Observable; + public tasksTaskIDDelete(taskID: number, taskFieldInfo?: TaskFieldInfo, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext}): Observable>; + public tasksTaskIDDelete(taskID: number, taskFieldInfo?: TaskFieldInfo, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext}): Observable>; + public tasksTaskIDDelete(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 tasksTaskIDDelete.'); + } + + 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.delete(`${this.configuration.basePath}/tasks/${encodeURIComponent(String(taskID))}`, + { + 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/api/api/taskgroup.service.ts b/frontend/src/api/api/taskgroup.service.ts index 2cd182c..851fd07 100644 --- a/frontend/src/api/api/taskgroup.service.ts +++ b/frontend/src/api/api/taskgroup.service.ts @@ -20,6 +20,7 @@ import { Observable } from 'rxjs'; import { InlineResponse200 } from '../model/models'; import { InlineResponse403 } from '../model/models'; +import { SimpleStatusResponse } from '../model/models'; import { TaskgroupEntityInfo } from '../model/models'; import { TaskgroupFieldInfo } from '../model/models'; @@ -264,6 +265,65 @@ export class TaskgroupService { ); } + /** + * clears tasks + * Deletes all tasks of that taskgroup + * @param taskgroupID internal id of taskgroup + * @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 taskgroupsTaskgroupIDClearDelete(taskgroupID: number, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext}): Observable; + public taskgroupsTaskgroupIDClearDelete(taskgroupID: number, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext}): Observable>; + public taskgroupsTaskgroupIDClearDelete(taskgroupID: number, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext}): Observable>; + public taskgroupsTaskgroupIDClearDelete(taskgroupID: number, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext}): Observable { + if (taskgroupID === null || taskgroupID === undefined) { + throw new Error('Required parameter taskgroupID was null or undefined when calling taskgroupsTaskgroupIDClearDelete.'); + } + + 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(); + } + + + let responseType_: 'text' | 'json' = 'json'; + if(localVarHttpHeaderAcceptSelected && localVarHttpHeaderAcceptSelected.startsWith('text')) { + responseType_ = 'text'; + } + + return this.httpClient.delete(`${this.configuration.basePath}/taskgroups/${encodeURIComponent(String(taskgroupID))}/clear`, + { + context: localVarHttpContext, + responseType: responseType_, + withCredentials: this.configuration.withCredentials, + headers: localVarHeaders, + observe: observe, + reportProgress: reportProgress + } + ); + } + /** * deletes taskgroup * deletes taskgroup diff --git a/frontend/src/api/model/models.ts b/frontend/src/api/model/models.ts index 54ad6fc..2647fda 100644 --- a/frontend/src/api/model/models.ts +++ b/frontend/src/api/model/models.ts @@ -11,6 +11,7 @@ export * from './propertiesInfo'; export * from './propertyInfo'; export * from './propertyUpdateRequest'; export * from './signUpRequest'; +export * from './simpleStatusResponse'; export * from './taskEntityInfo'; export * from './taskFieldInfo'; export * from './taskgroupEntityInfo'; diff --git a/frontend/src/api/model/simpleStatusResponse.ts b/frontend/src/api/model/simpleStatusResponse.ts new file mode 100644 index 0000000..8821ff9 --- /dev/null +++ b/frontend/src/api/model/simpleStatusResponse.ts @@ -0,0 +1,28 @@ +/** + * API Title + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * The version of the OpenAPI document: 1.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +export interface SimpleStatusResponse { + /** + * Response Status der Request + */ + status: SimpleStatusResponse.StatusEnum; +} +export namespace SimpleStatusResponse { + export type StatusEnum = 'success' | 'failed'; + export const StatusEnum = { + Success: 'success' as StatusEnum, + Failed: 'failed' as StatusEnum + }; +} + + diff --git a/frontend/src/api/model/taskEntityInfo.ts b/frontend/src/api/model/taskEntityInfo.ts index 10b82d2..ec68fa9 100644 --- a/frontend/src/api/model/taskEntityInfo.ts +++ b/frontend/src/api/model/taskEntityInfo.ts @@ -36,5 +36,9 @@ export interface TaskEntityInfo { * determines whether the task is overdue */ overdue: boolean; + /** + * determines whether the task is finished + */ + finished: boolean; } diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts index 9172a3c..a9fc4e0 100644 --- a/frontend/src/app/app.module.ts +++ b/frontend/src/app/app.module.ts @@ -51,6 +51,7 @@ import { TaskDashboardComponent } from './tasks/task-dashboard/task-dashboard.co import {MatSlideToggleModule} from "@angular/material/slide-toggle"; import {MatSortModule} from "@angular/material/sort"; import {MatPaginatorModule} from "@angular/material/paginator"; +import { ClearTaskDialogComponent } from './tasks/clear-task-dialog/clear-task-dialog.component'; @NgModule({ declarations: [ @@ -72,7 +73,8 @@ import {MatPaginatorModule} from "@angular/material/paginator"; TaskgroupCreationComponent, TaskgroupDeletionComponent, TaskEditorComponent, - TaskDashboardComponent + TaskDashboardComponent, + ClearTaskDialogComponent ], imports: [ BrowserModule, diff --git a/frontend/src/app/tasks/clear-task-dialog/clear-task-dialog.component.css b/frontend/src/app/tasks/clear-task-dialog/clear-task-dialog.component.css new file mode 100644 index 0000000..14143ca --- /dev/null +++ b/frontend/src/app/tasks/clear-task-dialog/clear-task-dialog.component.css @@ -0,0 +1,3 @@ +.deleteSetttings { + margin-right: 20px; +} diff --git a/frontend/src/app/tasks/clear-task-dialog/clear-task-dialog.component.html b/frontend/src/app/tasks/clear-task-dialog/clear-task-dialog.component.html new file mode 100644 index 0000000..887b230 --- /dev/null +++ b/frontend/src/app/tasks/clear-task-dialog/clear-task-dialog.component.html @@ -0,0 +1,13 @@ +

Clear Tasks ({{editorData.taskgroupID}})

+
+

Are you sure you want to clear the Tasks? This cannot be undone!

+ Finished Tasks ( {{finishedTasks}}Tasks) + Overdue Tasks ({{overdueTasks}} Tasks) + Unfinished Tasks ( {{unfinishedTasks}}Tasks) + Upcoming Tasks ({{upcomingTasks}} Tasks) +
+ +
+ + +
diff --git a/frontend/src/app/tasks/clear-task-dialog/clear-task-dialog.component.spec.ts b/frontend/src/app/tasks/clear-task-dialog/clear-task-dialog.component.spec.ts new file mode 100644 index 0000000..e108c79 --- /dev/null +++ b/frontend/src/app/tasks/clear-task-dialog/clear-task-dialog.component.spec.ts @@ -0,0 +1,21 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ClearTaskDialogComponent } from './clear-task-dialog.component'; + +describe('ClearTaskDialogComponent', () => { + let component: ClearTaskDialogComponent; + let fixture: ComponentFixture; + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [ClearTaskDialogComponent] + }); + fixture = TestBed.createComponent(ClearTaskDialogComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/tasks/clear-task-dialog/clear-task-dialog.component.ts b/frontend/src/app/tasks/clear-task-dialog/clear-task-dialog.component.ts new file mode 100644 index 0000000..86c8af5 --- /dev/null +++ b/frontend/src/app/tasks/clear-task-dialog/clear-task-dialog.component.ts @@ -0,0 +1,69 @@ +import {Component, Inject, OnInit} from '@angular/core'; +import {TaskEditorData} from "../task-editor/TaskEditorData"; +import {TaskEntityInfo, TaskgroupService} from "../../../api"; +import {MAT_DIALOG_DATA, MatDialogRef} from "@angular/material/dialog"; +import {MatSnackBar} from "@angular/material/snack-bar"; + +export interface ClearTaskDialogData { + taskgroupID: number, + tasks: TaskEntityInfo[] +} +@Component({ + selector: 'app-clear-task-dialog', + templateUrl: './clear-task-dialog.component.html', + styleUrls: ['./clear-task-dialog.component.css'] +}) +export class ClearTaskDialogComponent implements OnInit{ + + finishedTasks: number = 0; + unfinishedTasks: number = 0; + overdueTasks: number = 0; + upcomingTasks: number = 0; + + constructor(@Inject(MAT_DIALOG_DATA) public editorData: ClearTaskDialogData, + private taskgroupService: TaskgroupService, + private dialogRef: MatDialogRef, + private snackbar: MatSnackBar) { + } + + ngOnInit(): void { + this.editorData.tasks.forEach(task => { + if(task.overdue) { + this.overdueTasks +=1; + } else { + this.upcomingTasks += 1; + } + + if(task.finished) { + this.finishedTasks += 1; + } else { + this.unfinishedTasks += 1; + } + }) + } + + + cancel() { + + } + + clearTasks() { + this.taskgroupService.taskgroupsTaskgroupIDClearDelete(this.editorData.taskgroupID).subscribe({ + next: resp => { + if(resp.status == "success") { + const deletedTasks = this.editorData.tasks; + this.dialogRef.close(deletedTasks); + } + }, + error: err => { + if(err.status == 403) { + this.snackbar.open("No permission", "", {duration: 2000}); + } else if(err.status == 404) { + this.snackbar.open("Not found", "", {duration: 2000}); + } else { + this.snackbar.open("Unexpected error", "", {duration: 2000}); + } + } + }) + } +} diff --git a/frontend/src/app/tasks/task-dashboard/task-dashboard.component.css b/frontend/src/app/tasks/task-dashboard/task-dashboard.component.css index 65aa36b..481ebb4 100644 --- a/frontend/src/app/tasks/task-dashboard/task-dashboard.component.css +++ b/frontend/src/app/tasks/task-dashboard/task-dashboard.component.css @@ -1,7 +1,7 @@ mat-form-field { - width: 70%; + width: 80%; } table { @@ -29,3 +29,9 @@ td, th { width: 32px; text-align: center; } + + +#clear-tasks-btn { + float: right; + margin-top: 10px; +} diff --git a/frontend/src/app/tasks/task-dashboard/task-dashboard.component.html b/frontend/src/app/tasks/task-dashboard/task-dashboard.component.html index 91b6367..e9e1ace 100644 --- a/frontend/src/app/tasks/task-dashboard/task-dashboard.component.html +++ b/frontend/src/app/tasks/task-dashboard/task-dashboard.component.html @@ -3,7 +3,8 @@ -Show finished tasks + +
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 80d1178..077858a 100644 --- a/frontend/src/app/tasks/task-dashboard/task-dashboard.component.ts +++ b/frontend/src/app/tasks/task-dashboard/task-dashboard.component.ts @@ -1,11 +1,13 @@ -import {AfterViewInit, Component, Input, OnChanges, OnInit, SimpleChanges, ViewChild} from '@angular/core'; +import {Component, Input, OnChanges, ViewChild} from '@angular/core'; import {TaskEntityInfo, TaskService} from "../../../api"; import {MatPaginator} from "@angular/material/paginator"; import {MatSort} from "@angular/material/sort"; import {MatTableDataSource} from "@angular/material/table"; import {TaskEditorComponent} from "../task-editor/task-editor.component"; import {TaskEditorData} from "../task-editor/TaskEditorData"; -import {MatDialog, MatDialogRef} from "@angular/material/dialog"; +import {MatDialog} from "@angular/material/dialog"; +import {MatSnackBar} from "@angular/material/snack-bar"; +import {ClearTaskDialogComponent, ClearTaskDialogData} from "../clear-task-dialog/clear-task-dialog.component"; @Component({ selector: 'app-task-dashboard', @@ -33,7 +35,8 @@ export class TaskDashboardComponent implements OnChanges{ datasource: MatTableDataSource = new MatTableDataSource(); constructor(private taskService: TaskService, - private dialog: MatDialog) { + private dialog: MatDialog, + private snackbar: MatSnackBar) { } @@ -50,8 +53,25 @@ export class TaskDashboardComponent implements OnChanges{ return "green"; } - deleteTask(task: TaskEntityInfo) { - //todo: implement task delete api call + deleteTask(deletedTask: TaskEntityInfo) { + this.taskService.tasksTaskIDDelete(deletedTask.taskID).subscribe({ + next: resp => { + if(resp.status == "success") { + this.datasource.data = this.datasource.data.filter(task => task.taskID !== deletedTask.taskID); + } else { + this.snackbar.open("Unexpected behavior", "", {duration: 2000}); + } + }, + error: err => { + if(err.status == 403) { + this.snackbar.open("No permission", "", {duration: 2000}); + } else if(err.status == 404) { + this.snackbar.open("Task not found", "", {duration: 2000}); + } else { + this.snackbar.open("Unexpected error", "", {duration: 2000}); + } + } + }) } editTask(task: TaskEntityInfo) { @@ -60,6 +80,18 @@ export class TaskDashboardComponent implements OnChanges{ taskgroupID: this.taskgroupID! }; const dialogRef = this.dialog.open(TaskEditorComponent, {data: taskEditorInfo, minWidth: "400px"}) + } + clearTasks() { + const clearTaskData: ClearTaskDialogData = { + tasks: this.datasource.data, + taskgroupID: this.taskgroupID! + } + const dialogRef = this.dialog.open(ClearTaskDialogComponent, {data: clearTaskData, width: "600px"}); + dialogRef.afterClosed().subscribe(res => { + if(res != undefined) { + this.datasource.data = this.datasource.data.filter(task => !res.includes(task)); + } + }) } } diff --git a/openapi.yaml b/openapi.yaml index 2547368..a4b4cf8 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -829,6 +829,44 @@ paths: example: "failed" enum: - "failed" + /taskgroups/{taskgroupID}/clear: + delete: + security: + - API_TOKEN: [] + tags: + - taskgroup + summary: clears tasks + description: Deletes all tasks of that taskgroup + parameters: + - name: taskgroupID + in: path + description: internal id of taskgroup + required: true + schema: + type: number + example: 1 + responses: + 200: + description: Anfrage erfolgreich + content: + 'application/json': + schema: + type: object + $ref: "#/components/schemas/SimpleStatusResponse" + 403: + description: No permission + content: + 'application/json': + schema: + type: object + $ref: "#/components/schemas/SimpleStatusResponse" + 404: + description: Taskgroup does not exist + content: + 'application/json': + schema: + type: object + $ref: "#/components/schemas/SimpleStatusResponse" /tasks/{taskgroupID}/{status}: get: security: @@ -1043,6 +1081,50 @@ paths: example: "failed" enum: - "failed" + delete: + security: + - API_TOKEN: [] + tags: + - task + summary: edits an existing task + description: edits an existing task + 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: Anfrage erfolgreich + content: + 'application/json': + schema: + type: object + $ref: "#/components/schemas/SimpleStatusResponse" + 403: + description: No permission + content: + 'application/json': + schema: + type: object + $ref: "#/components/schemas/SimpleStatusResponse" + 404: + description: Taskgroup does not exist + content: + 'application/json': + schema: + type: object + $ref: "#/components/schemas/SimpleStatusResponse" + + components: securitySchemes: @@ -1051,6 +1133,18 @@ components: scheme: bearer bearerFormat: JWT schemas: + SimpleStatusResponse: + required: + - status + additionalProperties: false + properties: + status: + type: string + description: Response Status der Request + example: "failed" + enum: + - success + - failed LoginRequest: required: - username @@ -1274,6 +1368,7 @@ components: - startDate - deadline - overdue + - finished additionalProperties: false properties: taskID: @@ -1301,6 +1396,10 @@ components: type: boolean description: determines whether the task is overdue example: True + finished: + type: boolean + description: determines whether the task is finished + example: True TaskFieldInfo: required: - taskName