diff --git a/backend/.idea/workspace.xml b/backend/.idea/workspace.xml index d582557..4d8cb17 100644 --- a/backend/.idea/workspace.xml +++ b/backend/.idea/workspace.xml @@ -5,12 +5,25 @@ - - - - - + + + + + + + + + + + + + + + + + + + + + + + file://$PROJECT_DIR$/src/main/java/core/services/TaskgroupService.java + 52 + + + + \ No newline at end of file diff --git a/backend/src/main/java/core/api/controller/TaskgroupController.java b/backend/src/main/java/core/api/controller/TaskgroupController.java index d6fedf4..2b7e818 100644 --- a/backend/src/main/java/core/api/controller/TaskgroupController.java +++ b/backend/src/main/java/core/api/controller/TaskgroupController.java @@ -10,11 +10,13 @@ import core.services.ServiceResult; import core.services.TaskgroupService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; +import org.springframework.scheduling.config.Task; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.bind.annotation.*; import javax.validation.Valid; import java.util.List; +import java.util.Set; @CrossOrigin(origins = "*", maxAge = 3600) @RestController @@ -74,8 +76,23 @@ public class TaskgroupController { @GetMapping("/taskgroups") public ResponseEntity> listTaskgroupsOfUser() { - List taskgroups = taskgroupService.getTaskgroupsByUser(SecurityContextHolder.getContext().getAuthentication().getName()); + List taskgroups = taskgroupService.getTopTaskgroupsByUser(SecurityContextHolder.getContext().getAuthentication().getName()); List taskgroupEntityInfos = taskgroups.stream().map(TaskgroupEntityInfo::new).toList(); return ResponseEntity.ok(taskgroupEntityInfos); } + + @GetMapping("/taskgroups/{taskgroupID}") + public ResponseEntity getDetails(@PathVariable long taskgroupID) { + PermissionResult taskgroupPermissionResult = taskgroupService.getTaskgroupByIDAndUsername(taskgroupID, SecurityContextHolder.getContext().getAuthentication().getName()); + if(!taskgroupPermissionResult.isHasPermissions()) { + return ResponseEntity.status(403).body(new SimpleStatusResponse("failed")); + } + + if(taskgroupPermissionResult.getExitCode() == ServiceExitCode.MISSING_ENTITY) { + return ResponseEntity.status(404).body(new SimpleStatusResponse("failed")); + } + + Set children = taskgroupPermissionResult.getResult().getChildren(); + return ResponseEntity.ok(children.stream().map(TaskgroupEntityInfo::new)); + } } diff --git a/backend/src/main/java/core/api/models/timemanager/taskgroup/TaskgroupEntityInfo.java b/backend/src/main/java/core/api/models/timemanager/taskgroup/TaskgroupEntityInfo.java index 99fd360..f8027ef 100644 --- a/backend/src/main/java/core/api/models/timemanager/taskgroup/TaskgroupEntityInfo.java +++ b/backend/src/main/java/core/api/models/timemanager/taskgroup/TaskgroupEntityInfo.java @@ -7,9 +7,17 @@ public class TaskgroupEntityInfo { private long taskgroupID; private String taskgroupName; + private TaskgroupEntityInfo parentTaskgroup; + public TaskgroupEntityInfo(Taskgroup taskgroup) { this.taskgroupID = taskgroup.getTaskgroupID(); this.taskgroupName = taskgroup.getTaskgroupName(); + + if(taskgroup.getParent() == null) { + this.parentTaskgroup = null; + } else { + this.parentTaskgroup = new TaskgroupEntityInfo(taskgroup.getParent()); + } } public long getTaskgroupID() { @@ -27,4 +35,12 @@ public class TaskgroupEntityInfo { public void setTaskgroupName(String taskgroupName) { this.taskgroupName = taskgroupName; } + + public TaskgroupEntityInfo getParentTaskgroup() { + return parentTaskgroup; + } + + public void setParentTaskgroup(TaskgroupEntityInfo parentTaskgroup) { + this.parentTaskgroup = parentTaskgroup; + } } diff --git a/backend/src/main/java/core/api/models/timemanager/taskgroup/TaskgroupFieldInfo.java b/backend/src/main/java/core/api/models/timemanager/taskgroup/TaskgroupFieldInfo.java index 0c3116d..ca6dcdf 100644 --- a/backend/src/main/java/core/api/models/timemanager/taskgroup/TaskgroupFieldInfo.java +++ b/backend/src/main/java/core/api/models/timemanager/taskgroup/TaskgroupFieldInfo.java @@ -13,6 +13,8 @@ public class TaskgroupFieldInfo { @Length(max = 255) private String name; + private long parentID; + public String getName() { return name; } @@ -20,4 +22,12 @@ public class TaskgroupFieldInfo { public void setName(String name) { this.name = name; } + + public long getParentID() { + return parentID; + } + + public void setParentID(long parentID) { + this.parentID = parentID; + } } diff --git a/backend/src/main/java/core/entities/timemanager/Taskgroup.java b/backend/src/main/java/core/entities/timemanager/Taskgroup.java index 4043f4d..aca1666 100644 --- a/backend/src/main/java/core/entities/timemanager/Taskgroup.java +++ b/backend/src/main/java/core/entities/timemanager/Taskgroup.java @@ -4,6 +4,7 @@ import core.entities.User; import javax.persistence.*; import javax.validation.constraints.NotBlank; +import java.util.Set; @Entity @Table(name= "taskgroups") @@ -21,6 +22,13 @@ public class Taskgroup { @JoinColumn(name = "taskgroupuser", nullable = false) private User user; + @OneToMany(mappedBy = "parent", cascade = CascadeType.ALL, fetch = FetchType.EAGER) + private Set children; + + @ManyToOne + @JoinColumn(name = "parent_id") + private Taskgroup parent; + public Taskgroup(String taskgroupName, User user) { this.taskgroupName = taskgroupName; this.user = user; @@ -52,4 +60,20 @@ public class Taskgroup { public void setUser(User user) { this.user = user; } + + public Set getChildren() { + return children; + } + + public void setChildren(Set children) { + this.children = children; + } + + public Taskgroup getParent() { + return parent; + } + + public void setParent(Taskgroup parent) { + this.parent = parent; + } } diff --git a/backend/src/main/java/core/repositories/timemanager/TaskgroupRepository.java b/backend/src/main/java/core/repositories/timemanager/TaskgroupRepository.java index 4c17014..b2699ac 100644 --- a/backend/src/main/java/core/repositories/timemanager/TaskgroupRepository.java +++ b/backend/src/main/java/core/repositories/timemanager/TaskgroupRepository.java @@ -18,6 +18,9 @@ public interface TaskgroupRepository extends CrudRepository { @Query("SELECT tg FROM Taskgroup tg WHERE tg.user.username = ?1") List findAllByUser(String username); + @Query("SELECT tg FROM Taskgroup tg WHERE tg.user.username = ?1 AND tg.parent IS NULL") + List findAllTopTaskgroupsByUser(String username); + @Modifying @Transactional void deleteAllByUser(User user); diff --git a/backend/src/main/java/core/services/TaskgroupService.java b/backend/src/main/java/core/services/TaskgroupService.java index d5031c8..63479db 100644 --- a/backend/src/main/java/core/services/TaskgroupService.java +++ b/backend/src/main/java/core/services/TaskgroupService.java @@ -40,8 +40,20 @@ public class TaskgroupService { } if(!taskgroupRepository.existsByTaskgroupNameAndUser(taskData.getName(), user.get())) { + Taskgroup taskgroup; + if(taskData.getParentID() < 0) { + taskgroup = new Taskgroup(taskData.getName(), user.get()); + } else { + Optional parentTaskgroup = taskgroupRepository.findById(taskData.getParentID()); + if(parentTaskgroup.isEmpty()) { + return new ServiceResult<>(ServiceExitCode.MISSING_ENTITY); + } else { + taskgroup = new Taskgroup(taskData.getName(), user.get()); + taskgroup.setParent(parentTaskgroup.get()); + parentTaskgroup.get().getChildren().add(taskgroup); + } + } - Taskgroup taskgroup = new Taskgroup(taskData.getName(), user.get()); taskgroupRepository.save(taskgroup); return new ServiceResult<>(taskgroup); } else { @@ -72,6 +84,10 @@ public class TaskgroupService { return taskgroupRepository.findAllByUser(username); } + public List getTopTaskgroupsByUser(String username) { + return taskgroupRepository.findAllTopTaskgroupsByUser(username); + } + public void deleteTaskgroupByUser(User user) { taskgroupRepository.deleteAllByUser(user); } diff --git a/frontend/src/api/api/taskgroup.service.ts b/frontend/src/api/api/taskgroup.service.ts index 2d00598..2cd182c 100644 --- a/frontend/src/api/api/taskgroup.service.ts +++ b/frontend/src/api/api/taskgroup.service.ts @@ -94,6 +94,61 @@ export class TaskgroupService { * @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 taskgroupsAllGet(observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext}): Observable>; + public taskgroupsAllGet(observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext}): Observable>>; + public taskgroupsAllGet(observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext}): Observable>>; + public taskgroupsAllGet(observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext}): Observable { + + 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.get>(`${this.configuration.basePath}/taskgroups/all`, + { + context: localVarHttpContext, + responseType: responseType_, + withCredentials: this.configuration.withCredentials, + headers: localVarHeaders, + observe: observe, + reportProgress: reportProgress + } + ); + } + + /** + * list all top level taskgroups of authorized user + * list all taskgroups of authorized user + * @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 taskgroupsGet(observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext}): Observable>; public taskgroupsGet(observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext}): Observable>>; public taskgroupsGet(observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext}): Observable>>; @@ -268,6 +323,65 @@ export class TaskgroupService { ); } + /** + * get details of an existing taskgroup + * get details of an existing 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 taskgroupsTaskgroupIDGet(taskgroupID: number, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext}): Observable>; + public taskgroupsTaskgroupIDGet(taskgroupID: number, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext}): Observable>>; + public taskgroupsTaskgroupIDGet(taskgroupID: number, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext}): Observable>>; + public taskgroupsTaskgroupIDGet(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 taskgroupsTaskgroupIDGet.'); + } + + 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.get>(`${this.configuration.basePath}/taskgroups/${encodeURIComponent(String(taskgroupID))}`, + { + context: localVarHttpContext, + responseType: responseType_, + withCredentials: this.configuration.withCredentials, + headers: localVarHeaders, + observe: observe, + reportProgress: reportProgress + } + ); + } + /** * edits taskgroup * edits taskgroup diff --git a/frontend/src/api/model/taskgroupEntityInfo.ts b/frontend/src/api/model/taskgroupEntityInfo.ts index bbec6ed..56c6186 100644 --- a/frontend/src/api/model/taskgroupEntityInfo.ts +++ b/frontend/src/api/model/taskgroupEntityInfo.ts @@ -20,5 +20,6 @@ export interface TaskgroupEntityInfo { * name of taskgroup */ taskgroupName: string; + parentTaskgroup: TaskgroupEntityInfo; } diff --git a/frontend/src/api/model/taskgroupFieldInfo.ts b/frontend/src/api/model/taskgroupFieldInfo.ts index 66c2b15..11ca1cb 100644 --- a/frontend/src/api/model/taskgroupFieldInfo.ts +++ b/frontend/src/api/model/taskgroupFieldInfo.ts @@ -16,5 +16,9 @@ export interface TaskgroupFieldInfo { * name of taskgroup */ name: string; + /** + * internal id of parent Taskgroup + */ + parentID: number; } diff --git a/frontend/src/app/app-routing.module.ts b/frontend/src/app/app-routing.module.ts index 47103b6..9777643 100644 --- a/frontend/src/app/app-routing.module.ts +++ b/frontend/src/app/app-routing.module.ts @@ -10,7 +10,8 @@ const routes: Routes = [ {path: '', component: MainComponent}, {path: 'admin', component: AdminDashboardComponent}, {path: 'user/settings', component: UserSettingsComponent}, - {path: 'taskgroups', component: TaskgroupDashboardComponent} + {path: 'taskgroups', component: TaskgroupDashboardComponent}, + {path: 'taskgroups/:taskgroupID', component: TaskgroupDashboardComponent} ]; @NgModule({ diff --git a/frontend/src/app/taskgroups/taskgroup-creation/TaskgroupCreationData.ts b/frontend/src/app/taskgroups/taskgroup-creation/TaskgroupCreationData.ts new file mode 100644 index 0000000..19a1942 --- /dev/null +++ b/frontend/src/app/taskgroups/taskgroup-creation/TaskgroupCreationData.ts @@ -0,0 +1,6 @@ +import {TaskgroupEntityInfo} from "../../../api"; + +export interface TaskgroupCreationData { + taskgroup: TaskgroupEntityInfo | undefined, + taskgroupID: number +} diff --git a/frontend/src/app/taskgroups/taskgroup-creation/taskgroup-creation.component.ts b/frontend/src/app/taskgroups/taskgroup-creation/taskgroup-creation.component.ts index 8b299b7..a30c7ed 100644 --- a/frontend/src/app/taskgroups/taskgroup-creation/taskgroup-creation.component.ts +++ b/frontend/src/app/taskgroups/taskgroup-creation/taskgroup-creation.component.ts @@ -4,6 +4,8 @@ import {MAT_DIALOG_DATA, MatDialogRef} from "@angular/material/dialog"; import {TaskgroupEntityInfo, TaskgroupService} from "../../../api"; import {error} from "@angular/compiler/src/util"; import {MatSnackBar} from "@angular/material/snack-bar"; +import {ActivatedRoute} from "@angular/router"; +import {TaskgroupCreationData} from "./TaskgroupCreationData"; @Component({ selector: 'app-taskgroup-creation', @@ -18,11 +20,12 @@ export class TaskgroupCreationComponent implements OnInit { constructor(private dialogRef: MatDialogRef, private taskgroupService: TaskgroupService, private snackbar: MatSnackBar, - @Inject(MAT_DIALOG_DATA) public data: TaskgroupEntityInfo | undefined) { } + @Inject(MAT_DIALOG_DATA) public data: TaskgroupCreationData, + private activatedRoute: ActivatedRoute) { } ngOnInit(): void { - if(this.data != undefined) { - this.nameCtrl.setValue(this.data!.taskgroupName) + if(this.data.taskgroup != undefined) { + this.nameCtrl.setValue(this.data.taskgroup!.taskgroupName) } } @@ -32,9 +35,10 @@ export class TaskgroupCreationComponent implements OnInit { save() { this.pending = true; - if(this.data == undefined) { + if(this.data.taskgroup == undefined) { this.taskgroupService.taskgroupsPut({ - name: this.nameCtrl.value + name: this.nameCtrl.value, + parentID: this.data.taskgroupID, }).subscribe({ next: resp => { this.pending = false; @@ -51,11 +55,12 @@ export class TaskgroupCreationComponent implements OnInit { }) } else { this.taskgroupService.taskgroupsTaskgroupIDPost(this.data.taskgroupID, { - name: this.nameCtrl.value + name: this.nameCtrl.value, + parentID: this.data.taskgroup!.parentTaskgroup.taskgroupID }).subscribe({ next: resp => { this.pending = false; - this.data!.taskgroupName = this.nameCtrl.value + this.data.taskgroup!.taskgroupName = this.nameCtrl.value this.dialogRef.close(true); }, error: err => { diff --git a/frontend/src/app/taskgroups/taskgroup-dashboard/taskgroup-dashboard.component.html b/frontend/src/app/taskgroups/taskgroup-dashboard/taskgroup-dashboard.component.html index 747ee96..7fd2200 100644 --- a/frontend/src/app/taskgroups/taskgroup-dashboard/taskgroup-dashboard.component.html +++ b/frontend/src/app/taskgroups/taskgroup-dashboard/taskgroup-dashboard.component.html @@ -9,7 +9,7 @@

{{taskgroup.taskgroupName}}

- +
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 fd65051..5175971 100644 --- a/frontend/src/app/taskgroups/taskgroup-dashboard/taskgroup-dashboard.component.ts +++ b/frontend/src/app/taskgroups/taskgroup-dashboard/taskgroup-dashboard.component.ts @@ -3,6 +3,7 @@ import {MatDialog} from "@angular/material/dialog"; import {TaskgroupCreationComponent} from "../taskgroup-creation/taskgroup-creation.component"; import {TaskgroupEntityInfo, TaskgroupService} from "../../../api"; import {TaskgroupDeletionComponent} from "../taskgroup-deletion/taskgroup-deletion.component"; +import {ActivatedRoute} from "@angular/router"; @Component({ selector: 'app-taskgroup-dashboard', @@ -12,20 +13,34 @@ import {TaskgroupDeletionComponent} from "../taskgroup-deletion/taskgroup-deleti export class TaskgroupDashboardComponent implements OnInit { constructor(private dialog: MatDialog, - private taskgroupService: TaskgroupService) { } + private taskgroupService: TaskgroupService, + private activatedRoute: ActivatedRoute) { } taskgroups: TaskgroupEntityInfo[] = [] + taskgroupID: number = -1; ngOnInit(): void { - this.taskgroupService.taskgroupsGet().subscribe({ - next: resp => { - this.taskgroups = resp; + this.activatedRoute.paramMap.subscribe(params => { + if(params.has('taskgroupID')) { + this.taskgroupID = Number(params.get('taskgroupID')); + this.taskgroupService.taskgroupsTaskgroupIDGet(this.taskgroupID).subscribe({ + next: resp => { + this.taskgroups = resp + } + }) + } else { + this.taskgroupService.taskgroupsGet().subscribe({ + next: resp => { + this.taskgroups = resp; + } + }) } }) + } openTaskgroupCreation() { - const dialogRef = this.dialog.open(TaskgroupCreationComponent, {minWidth: "400px"}) + const dialogRef = this.dialog.open(TaskgroupCreationComponent, {data: {taskgroup: undefined, taskgroupID: this.taskgroupID}, minWidth: "400px"}) dialogRef.afterClosed().subscribe(res => { if(res != undefined) { this.taskgroups.push(res); diff --git a/openapi.yaml b/openapi.yaml index 67f719f..af8b5b3 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -564,7 +564,7 @@ paths: example: "failed" enum: - "failed" - /taskgroups: + /taskgroups/all: get: security: - API_TOKEN: [] @@ -581,6 +581,23 @@ paths: type: array items: $ref: '#/components/schemas/TaskgroupEntityInfo' + /taskgroups: + get: + security: + - API_TOKEN: [] + tags: + - taskgroup + summary: list all top level taskgroups of authorized user + description: list all taskgroups of authorized user + responses: + 200: + description: Anfrage erfolgreich + content: + 'application/json': + schema: + type: array + items: + $ref: '#/components/schemas/TaskgroupEntityInfo' put: security: - API_TOKEN: [] @@ -616,6 +633,60 @@ paths: enum: - "failed" /taskgroups/{taskgroupID}: + get: + security: + - API_TOKEN: [] + tags: + - taskgroup + summary: get details of an existing taskgroup + description: get details of an existing 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: array + items: + $ref: '#/components/schemas/TaskgroupEntityInfo' + 403: + description: No permission + content: + 'application/json': + schema: + type: object + required: + - status + properties: + status: + type: string + description: Status + example: "failed" + enum: + - "failed" + 404: + description: Taskgroup does not exist + content: + 'application/json': + schema: + type: object + required: + - status + properties: + status: + type: string + description: Status + example: "failed" + enum: + - "failed" post: security: - API_TOKEN: [] @@ -948,6 +1019,7 @@ components: required: - taskgroupID - taskgroupName + - parentTaskgroup additionalProperties: false properties: taskgroupID: @@ -960,9 +1032,13 @@ components: example: Taskgroup 1 maxLength: 255 minLength: 1 + parentTaskgroup: + type: object + $ref: '#/components/schemas/TaskgroupEntityInfo' TaskgroupFieldInfo: required: - name + - parentID additionalProperties: false properties: name: @@ -970,4 +1046,9 @@ components: description: name of taskgroup example: Taskgroup 1 maxLength: 255 - minLength: 1 \ No newline at end of file + minLength: 1 + parentID: + type: number + description: internal id of parent Taskgroup + example: 1 + \ No newline at end of file