From 31dd1ba057175a899cc479f86d2f9d5e366a6079 Mon Sep 17 00:00:00 2001 From: Sophie Methe Date: Wed, 3 Jul 2024 10:07:27 +0200 Subject: [PATCH 01/21] Add group model and services (#1675) --- modules/fbs-core/web/src/app/model/Group.ts | 13 ++++ .../app/service/group-registration.sevice.ts | 78 +++++++++++++++++++ .../web/src/app/service/group.service.ts | 74 ++++++++++++++++++ 3 files changed, 165 insertions(+) create mode 100644 modules/fbs-core/web/src/app/model/Group.ts create mode 100644 modules/fbs-core/web/src/app/service/group-registration.sevice.ts create mode 100644 modules/fbs-core/web/src/app/service/group.service.ts diff --git a/modules/fbs-core/web/src/app/model/Group.ts b/modules/fbs-core/web/src/app/model/Group.ts new file mode 100644 index 000000000..641646dfd --- /dev/null +++ b/modules/fbs-core/web/src/app/model/Group.ts @@ -0,0 +1,13 @@ +export interface Group { + id: number; + courseId: number; + name: string; + membership: number; + visible?: boolean; +} + +export interface GroupInput { + name: string; + membership: number; + visible: boolean; +} diff --git a/modules/fbs-core/web/src/app/service/group-registration.sevice.ts b/modules/fbs-core/web/src/app/service/group-registration.sevice.ts new file mode 100644 index 000000000..6648e3ebe --- /dev/null +++ b/modules/fbs-core/web/src/app/service/group-registration.sevice.ts @@ -0,0 +1,78 @@ +import { Injectable } from "@angular/core"; +import { Observable } from "rxjs"; +import { HttpClient } from "@angular/common/http"; +import { Participant } from "../model/Participant"; +import { Group } from "../model/Group"; + +@Injectable({ + providedIn: "root", +}) +export class GroupRegistrationService { + constructor(private http: HttpClient) {} + + /** + * @param uid User id + * @return All registered groups + */ + getRegisteredGroups(uid: number): Observable { + return this.http.get(`/api/v1/users/${uid}/groups`); + } + + /** + * @param cid Course id + * @param gid Group id + * @return All participants of the group + */ + getGroupParticipants(cid: number, gid: number): Observable { + return this.http.get( + `/api/v1/courses/${cid}/groups/${gid}/participants` + ); + } + + /** + * Register a user into a group + * @param cid Course id + * @param gid Group id + * @param uid User id + * @return Observable that succeeds on successful registration + */ + registerGroup(cid: number, gid: number, uid: number): Observable { + return this.http.put( + `/api/v1/courses/${cid}/groups/${gid}/users/${uid}`, + {} + ); + } + + /** + * De-register a user from a group + * @param cid Course id + * @param gid Group id + * @param uid User id + */ + deregisterGroup(cid: number, gid: number, uid: number): Observable { + return this.http.delete( + `/api/v1/courses/${cid}/groups/${gid}/users/${uid}` + ); + } + + /** + * De-register all users from a course + * @param cid Course id + * @param gid Group id + */ + deregisterAll(cid: number, gid: number): Observable { + return this.http.delete(`/api/v1/courses/${cid}/groups/${gid}/users`); + } + + /** + * Get current number of members of a group + * @param cid Course id + * @param gid Group id + * @return Number of members + */ + getGroupMembership(cid: number, gid: number): Observable { + return this.http.get( + `/api/v1/courses/${cid}/groups/${gid}/membership` + ); + } +} diff --git a/modules/fbs-core/web/src/app/service/group.service.ts b/modules/fbs-core/web/src/app/service/group.service.ts new file mode 100644 index 000000000..24de40143 --- /dev/null +++ b/modules/fbs-core/web/src/app/service/group.service.ts @@ -0,0 +1,74 @@ +import { Injectable } from "@angular/core"; +import { Observable } from "rxjs"; +import { Group, GroupInput } from "../model/Group"; +import { HttpClient } from "@angular/common/http"; + +@Injectable({ + providedIn: "root", +}) +export class GroupService { + private cid: number; + + constructor(private http: HttpClient) {} + /** + * @return Observable with all groups + * @param visible Optional filter to filter only for visible groups + * @param cid The course id + */ + getGroupList(cid: number, visible?: boolean): Observable { + let url = `/api/v1/courses/${cid}/groups`; + if (visible !== undefined) { + url += `?visible = ${visible}`; + } + return this.http.get(url); + } + + /** + * Get a single group by its id, if it exits + * @param cid The course id + * @param gid The group id + */ + getGroup(cid: number, gid: number): Observable { + return this.http.get(`/api/v1/courses/${cid}/groups/${gid}`); + } + + /** + * Create a new group + * @param cid The course id + * @param postData The necessary input to create a group + * @return The created group, adjusted by the system + */ + createGroup(cid: number, postData: GroupInput): Observable { + const groupData = { ...postData, courseId: cid }; + return this.http.post(`/api/v1/courses/${cid}/groups`, groupData); + } + + /** + * Update an existing group + * @param cid The course id + * @param gid The group id + * @param postData The necessary input to create a group + */ + updateGroup( + cid: number, + gid: number, + postData: GroupInput + ): Observable { + const groupData = { ...postData, courseId: cid }; + return this.http.put( + `/api/v1/courses/${cid}/groups/${gid}`, + groupData + ); + } + + /** + * Delete a group by its id + * @param cid The course id + * @param gid The group id + * @return Observable that succeeds if the course does not exist after the operation + */ + deleteGroup(cid: number, gid: number): Observable { + // returns an Observable + return this.http.delete(`/api/v1/courses/${cid}/groups/${gid}`); + } +} From 12b72422143b0544d56b5825cea69bad99805e23 Mon Sep 17 00:00:00 2001 From: Sophie Methe Date: Wed, 3 Jul 2024 10:32:47 +0200 Subject: [PATCH 02/21] Refactor GroupRegistrationController API to retrieve current group membership (#1675) --- .../controller/GroupRegistrationController.scala | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/modules/fbs-core/api/src/main/scala/de/thm/ii/fbs/controller/GroupRegistrationController.scala b/modules/fbs-core/api/src/main/scala/de/thm/ii/fbs/controller/GroupRegistrationController.scala index 7de518adb..b960e4bd7 100644 --- a/modules/fbs-core/api/src/main/scala/de/thm/ii/fbs/controller/GroupRegistrationController.scala +++ b/modules/fbs-core/api/src/main/scala/de/thm/ii/fbs/controller/GroupRegistrationController.scala @@ -7,7 +7,7 @@ import de.thm.ii.fbs.services.security.AuthService import javax.servlet.http.{HttpServletRequest, HttpServletResponse} import org.springframework.beans.factory.annotation.Autowired -import org.springframework.http.MediaType +import org.springframework.http.{MediaType, ResponseEntity} import org.springframework.web.bind.annotation._ /** @@ -142,4 +142,17 @@ class GroupRegistrationController { throw new ForbiddenException() } } + + /** + * Get current number of members of a group + * + * @param cid Course id + * @param gid Group id + * @return Number of members + */ + @GetMapping(Array("/courses/{cid}/groups/{gid}/membership")) + def getGroupMembership(@PathVariable("cid") cid: Integer, @PathVariable("gid") gid: Int): ResponseEntity[Int] = { + val membership = groupRegistrationService.getGroupMembership(cid, gid) + ResponseEntity.ok(membership) + } } From 5f1fdf5a53134d57e8990b89234f77753940d4c6 Mon Sep 17 00:00:00 2001 From: Sophie Methe Date: Wed, 3 Jul 2024 10:42:41 +0200 Subject: [PATCH 03/21] Update course detail component and sidebar component (#1675) --- .../course-detail.component.html | 305 +++++++++--------- .../course-detail/course-detail.component.ts | 4 +- .../sidebar/sidebar.component.html | 5 + 3 files changed, 165 insertions(+), 149 deletions(-) diff --git a/modules/fbs-core/web/src/app/page-components/course-detail/course-detail.component.html b/modules/fbs-core/web/src/app/page-components/course-detail/course-detail.component.html index 942b8ec50..dd81bca53 100644 --- a/modules/fbs-core/web/src/app/page-components/course-detail/course-detail.component.html +++ b/modules/fbs-core/web/src/app/page-components/course-detail/course-detail.component.html @@ -144,160 +144,171 @@
{{ (course | async)?.description }}
-
- -
+ + +
+ +
-
-
- - {{ - "course.edit-multiple-tasks-select-all" | i18nextEager - }} - {{ - "course.edit-multiple-tasks-de-select-all" | i18nextEager - }} - -
+
+
+ + {{ + "course.edit-multiple-tasks-select-all" | i18nextEager + }} + {{ + "course.edit-multiple-tasks-de-select-all" | i18nextEager + }} + +
- -
+ +
+ + + + +
+

{{ "course.progress-title" | i18nextEager }}

+ + + + + question_mark + + + + + + + military_tech + + - - - -
-

{{ "course.progress-title" | i18nextEager }}

- - - - + + stars + + + +
+ + + +
+
+ priority_high + {{ courseProgressBar.mandatory.done }} + / + {{ courseProgressBar.mandatory.sum }} +
+
+ star +   + {{ courseProgressBar.optional.done }} + / + {{ courseProgressBar.optional.sum }} + +
+
+ school +    + {{ courseProgressBar.practice.done }} + / + {{ courseProgressBar.practice.sum }} +
+
+ +
+ - question_mark - - - - - - + - military_tech - - - - - + - stars - - - -
+ + +
+
- - -
-
- priority_high - {{ courseProgressBar.mandatory.done }} - / - {{ courseProgressBar.mandatory.sum }} -
-
- star -   - {{ courseProgressBar.optional.done }} - / - {{ courseProgressBar.optional.sum }} - -
-
- school -    - {{ courseProgressBar.practice.done }} - / - {{ courseProgressBar.practice.sum }} + +
+
+ +
- -
- - - - - - -
- - - - -
-
- - -
-
+ + + + +
diff --git a/modules/fbs-core/web/src/app/page-components/course-detail/course-detail.component.ts b/modules/fbs-core/web/src/app/page-components/course-detail/course-detail.component.ts index 691e3faab..d7a970667 100644 --- a/modules/fbs-core/web/src/app/page-components/course-detail/course-detail.component.ts +++ b/modules/fbs-core/web/src/app/page-components/course-detail/course-detail.component.ts @@ -296,7 +296,7 @@ export class CourseDetailComponent implements OnInit { /* if (element.bonusFormula === undefined) { element.bonusFormula = "0"; - + points = +element.bonusFormula; } else { @@ -306,7 +306,7 @@ export class CourseDetailComponent implements OnInit { points = +element.bonusFormula; points += element.points; element.bonusFormula = points.toString(); - + */ } } diff --git a/modules/fbs-core/web/src/app/page-components/sidebar/sidebar.component.html b/modules/fbs-core/web/src/app/page-components/sidebar/sidebar.component.html index 8899dcf43..a80e70d21 100644 --- a/modules/fbs-core/web/src/app/page-components/sidebar/sidebar.component.html +++ b/modules/fbs-core/web/src/app/page-components/sidebar/sidebar.component.html @@ -35,6 +35,11 @@

{{ "sidebar.label.allCourses" | i18nextEager }}

+ + groups +

{{ "sidebar.label.myGroups" | i18nextEager }}

+
+ Date: Wed, 3 Jul 2024 10:47:22 +0200 Subject: [PATCH 04/21] Update package dependencies (#1675) --- modules/fbs-core/web/package-lock.json | 653 +++++++++++++------------ modules/fbs-core/web/package.json | 2 +- 2 files changed, 351 insertions(+), 304 deletions(-) diff --git a/modules/fbs-core/web/package-lock.json b/modules/fbs-core/web/package-lock.json index 77c727b01..6544a48ca 100644 --- a/modules/fbs-core/web/package-lock.json +++ b/modules/fbs-core/web/package-lock.json @@ -45,7 +45,7 @@ "@angular-eslint/eslint-plugin-template": "14.0.2", "@angular-eslint/schematics": "14.0.2", "@angular-eslint/template-parser": "14.0.2", - "@angular/cli": "^14.1.0", + "@angular/cli": "^14.2.13", "@angular/compiler-cli": "^14.1.0", "@angular/language-service": "^14.1.0", "@types/file-saver": "^2.0.1", @@ -97,18 +97,6 @@ "yarn": ">= 1.13.0" } }, - "node_modules/@angular-devkit/architect/node_modules/rxjs": { - "version": "6.6.7", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", - "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", - "dev": true, - "dependencies": { - "tslib": "^1.9.0" - }, - "engines": { - "npm": ">=2.0.0" - } - }, "node_modules/@angular-devkit/build-angular": { "version": "14.1.0", "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-14.1.0.tgz", @@ -321,24 +309,6 @@ "node": ">=10" } }, - "node_modules/@angular-devkit/build-angular/node_modules/rxjs": { - "version": "6.6.7", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", - "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", - "dev": true, - "dependencies": { - "tslib": "^1.9.0" - }, - "engines": { - "npm": ">=2.0.0" - } - }, - "node_modules/@angular-devkit/build-angular/node_modules/rxjs/node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "dev": true - }, "node_modules/@angular-devkit/build-angular/node_modules/semver": { "version": "7.3.7", "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", @@ -379,18 +349,6 @@ "webpack-dev-server": "^4.0.0" } }, - "node_modules/@angular-devkit/build-webpack/node_modules/rxjs": { - "version": "6.6.7", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", - "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", - "dev": true, - "dependencies": { - "tslib": "^1.9.0" - }, - "engines": { - "npm": ">=2.0.0" - } - }, "node_modules/@angular-devkit/core": { "version": "14.1.0", "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-14.1.0.tgz", @@ -439,48 +397,72 @@ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "dev": true }, - "node_modules/@angular-devkit/core/node_modules/rxjs": { - "version": "6.6.7", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", - "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", + "node_modules/@angular-devkit/schematics": { + "version": "14.2.13", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-14.2.13.tgz", + "integrity": "sha512-2zczyeNzeBcrT2HOysv52X9SH3tZoHfWJvVf6H0SIa74rfDKEl7hFpKNXnh3x8sIMLj5mZn05n5RCqGxCczcIg==", "dev": true, "dependencies": { - "tslib": "^1.9.0" + "@angular-devkit/core": "14.2.13", + "jsonc-parser": "3.1.0", + "magic-string": "0.26.2", + "ora": "5.4.1", + "rxjs": "6.6.7" }, "engines": { - "npm": ">=2.0.0" + "node": "^14.15.0 || >=16.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" } }, - "node_modules/@angular-devkit/schematics": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-14.1.0.tgz", - "integrity": "sha512-5QC01k9eznuQSiqxijKhVkAEmA8sioYuLhBzyffaPszSySH8kPMNxhAc8zJhBTNLumbS6iDaGkSqTQl5Kv9fOw==", + "node_modules/@angular-devkit/schematics/node_modules/@angular-devkit/core": { + "version": "14.2.13", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-14.2.13.tgz", + "integrity": "sha512-aIefeZcbjghQg/V6U9CTLtyB5fXDJ63KwYqVYkWP+i0XriS5A9puFgq2u/OVsWxAfYvqpDqp5AdQ0g0bi3CAsA==", "dev": true, "dependencies": { - "@angular-devkit/core": "14.1.0", + "ajv": "8.11.0", + "ajv-formats": "2.1.1", "jsonc-parser": "3.1.0", - "magic-string": "0.26.2", - "ora": "5.4.1", - "rxjs": "6.6.7" + "rxjs": "6.6.7", + "source-map": "0.7.4" }, "engines": { "node": "^14.15.0 || >=16.10.0", "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^3.5.2" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } } }, - "node_modules/@angular-devkit/schematics/node_modules/rxjs": { - "version": "6.6.7", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", - "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", + "node_modules/@angular-devkit/schematics/node_modules/ajv": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz", + "integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==", "dev": true, "dependencies": { - "tslib": "^1.9.0" + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" }, - "engines": { - "npm": ">=2.0.0" + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/@angular-devkit/schematics/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, "node_modules/@angular-eslint/builder": { "version": "14.0.2", "resolved": "https://registry.npmjs.org/@angular-eslint/builder/-/builder-14.0.2.tgz", @@ -667,15 +649,15 @@ "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" }, "node_modules/@angular/cli": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-14.1.0.tgz", - "integrity": "sha512-W/t2PkGHu9r87po1ZXQRYU81VtjzNMuGsP5tmoW1pGuibK7Kj+25G+jrXK/WADTi+pjTMXHNXYn8PlMNAIrZ/w==", + "version": "14.2.13", + "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-14.2.13.tgz", + "integrity": "sha512-I5EepRem2CCyS3GDzQxZ2ZrqQwVqoGoLY+ZQhsK1QGWUnUyFOjbv3OlUGxRUYwcedu19V1EBAKjmQ96HzMIcVQ==", "dev": true, "dependencies": { - "@angular-devkit/architect": "0.1401.0", - "@angular-devkit/core": "14.1.0", - "@angular-devkit/schematics": "14.1.0", - "@schematics/angular": "14.1.0", + "@angular-devkit/architect": "0.1402.13", + "@angular-devkit/core": "14.2.13", + "@angular-devkit/schematics": "14.2.13", + "@schematics/angular": "14.2.13", "@yarnpkg/lockfile": "1.1.0", "ansi-colors": "4.1.3", "debug": "4.3.4", @@ -686,9 +668,9 @@ "npm-pick-manifest": "7.0.1", "open": "8.4.0", "ora": "5.4.1", - "pacote": "13.6.1", + "pacote": "13.6.2", "resolve": "1.22.1", - "semver": "7.3.7", + "semver": "7.5.3", "symbol-observable": "4.0.0", "uuid": "8.3.2", "yargs": "17.5.1" @@ -702,6 +684,63 @@ "yarn": ">= 1.13.0" } }, + "node_modules/@angular/cli/node_modules/@angular-devkit/architect": { + "version": "0.1402.13", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1402.13.tgz", + "integrity": "sha512-n0ISBuvkZHoOpAzuAZql1TU9VLHUE9e/a9g4VNOPHewjMzpN02VqeGKvJfOCKtzkCs6gVssIlILm2/SXxkIFxQ==", + "dev": true, + "dependencies": { + "@angular-devkit/core": "14.2.13", + "rxjs": "6.6.7" + }, + "engines": { + "node": "^14.15.0 || >=16.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular/cli/node_modules/@angular-devkit/core": { + "version": "14.2.13", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-14.2.13.tgz", + "integrity": "sha512-aIefeZcbjghQg/V6U9CTLtyB5fXDJ63KwYqVYkWP+i0XriS5A9puFgq2u/OVsWxAfYvqpDqp5AdQ0g0bi3CAsA==", + "dev": true, + "dependencies": { + "ajv": "8.11.0", + "ajv-formats": "2.1.1", + "jsonc-parser": "3.1.0", + "rxjs": "6.6.7", + "source-map": "0.7.4" + }, + "engines": { + "node": "^14.15.0 || >=16.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^3.5.2" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/@angular/cli/node_modules/ajv": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz", + "integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, "node_modules/@angular/cli/node_modules/ini": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/ini/-/ini-3.0.0.tgz", @@ -711,6 +750,12 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, + "node_modules/@angular/cli/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, "node_modules/@angular/cli/node_modules/lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -724,9 +769,9 @@ } }, "node_modules/@angular/cli/node_modules/semver": { - "version": "7.3.7", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", - "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", + "version": "7.5.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", + "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==", "dev": true, "dependencies": { "lru-cache": "^6.0.0" @@ -3529,9 +3574,9 @@ } }, "node_modules/@npmcli/git": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@npmcli/git/-/git-3.0.1.tgz", - "integrity": "sha512-UU85F/T+F1oVn3IsB/L6k9zXIMpXBuUBE25QDH0SsURwT6IOBqkC7M16uqo2vVZIyji3X1K4XH9luip7YekH1A==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@npmcli/git/-/git-3.0.2.tgz", + "integrity": "sha512-CAcd08y3DWBJqJDpfuVL0uijlq5oaXaOJEKHKc4wqrjd00gkvTZB+nFuLn+doOOKddaQS9JfqtNoFCO2LCvA3w==", "dev": true, "dependencies": { "@npmcli/promise-spawn": "^3.0.0", @@ -3561,13 +3606,10 @@ } }, "node_modules/@npmcli/git/node_modules/semver": { - "version": "7.3.7", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", - "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, "bin": { "semver": "bin/semver.js" }, @@ -3575,18 +3617,6 @@ "node": ">=10" } }, - "node_modules/@npmcli/git/node_modules/semver/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/@npmcli/git/node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -3665,9 +3695,9 @@ } }, "node_modules/@npmcli/run-script": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@npmcli/run-script/-/run-script-4.1.7.tgz", - "integrity": "sha512-WXr/MyM4tpKA4BotB81NccGAv8B48lNH0gRoILucbcAhTQXLCoi6HflMV3KdXubIqvP9SuLsFn68Z7r4jl+ppw==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@npmcli/run-script/-/run-script-4.2.1.tgz", + "integrity": "sha512-7dqywvVudPSrRCW5nTHpHgeWnbBtz8cFkOuKrecm6ih+oO9ciydhWt6OF7HlqupRRmB8Q/gECVdB9LMfToJbRg==", "dev": true, "dependencies": { "@npmcli/node-gyp": "^2.0.0", @@ -3783,13 +3813,13 @@ } }, "node_modules/@schematics/angular": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-14.1.0.tgz", - "integrity": "sha512-lhqNZzA+iT3XwlwRU757mhYmd5WE9XB2OKFhosvvszou2zuNUJMDPR9P01ZVNCOa2fScOeCMg2q3ZDgGTBl96Q==", + "version": "14.2.13", + "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-14.2.13.tgz", + "integrity": "sha512-MLxTpTU3E8QACQ/5c0sENMR2gRiMXpGaKeD5IHY+3wyU2fUSJVB0QPU/l1WhoyZbX8N9ospBgf5UEG7taVF9rg==", "dev": true, "dependencies": { - "@angular-devkit/core": "14.1.0", - "@angular-devkit/schematics": "14.1.0", + "@angular-devkit/core": "14.2.13", + "@angular-devkit/schematics": "14.2.13", "jsonc-parser": "3.1.0" }, "engines": { @@ -3798,6 +3828,54 @@ "yarn": ">= 1.13.0" } }, + "node_modules/@schematics/angular/node_modules/@angular-devkit/core": { + "version": "14.2.13", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-14.2.13.tgz", + "integrity": "sha512-aIefeZcbjghQg/V6U9CTLtyB5fXDJ63KwYqVYkWP+i0XriS5A9puFgq2u/OVsWxAfYvqpDqp5AdQ0g0bi3CAsA==", + "dev": true, + "dependencies": { + "ajv": "8.11.0", + "ajv-formats": "2.1.1", + "jsonc-parser": "3.1.0", + "rxjs": "6.6.7", + "source-map": "0.7.4" + }, + "engines": { + "node": "^14.15.0 || >=16.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^3.5.2" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/@schematics/angular/node_modules/ajv": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz", + "integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@schematics/angular/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, "node_modules/@tootallnate/once": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", @@ -4728,13 +4806,11 @@ } }, "node_modules/agentkeepalive": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.2.1.tgz", - "integrity": "sha512-Zn4cw2NEqd+9fiSVWMscnjyQ1a8Yfoc5oBajLeo5w+YBHgDUcEBY2hS4YpTz6iN5f/2zQiktcuM6tS8x1p9dpA==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.5.0.tgz", + "integrity": "sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew==", "dev": true, "dependencies": { - "debug": "^4.1.0", - "depd": "^1.1.2", "humanize-ms": "^1.2.1" }, "engines": { @@ -4915,6 +4991,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz", "integrity": "sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==", + "deprecated": "This package is no longer supported.", "dev": true, "dependencies": { "delegates": "^1.0.0", @@ -4925,9 +5002,9 @@ } }, "node_modules/are-we-there-yet/node_modules/readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", "dev": true, "dependencies": { "inherits": "^2.0.3", @@ -5509,34 +5586,19 @@ "dev": true }, "node_modules/builtins": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/builtins/-/builtins-5.0.1.tgz", - "integrity": "sha512-qwVpFEHNfhYJIzNRBvd2C1kyo6jz3ZSMPyyuR47OPdiKWlbYnZNyDWuyR175qDnAJLiCo5fBBqPb3RiXgWlkOQ==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/builtins/-/builtins-5.1.0.tgz", + "integrity": "sha512-SW9lzGTLvWTP1AY8xeAMZimqDrIaSdLQUcVr9DMef51niJ022Ri87SwRRKYm4A6iHfkPaiVUu/Duw2Wc4J7kKg==", "dev": true, "dependencies": { "semver": "^7.0.0" } }, - "node_modules/builtins/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/builtins/node_modules/semver": { - "version": "7.3.7", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", - "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, "bin": { "semver": "bin/semver.js" }, @@ -8146,6 +8208,12 @@ "node": ">= 0.8.0" } }, + "node_modules/exponential-backoff": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.1.tgz", + "integrity": "sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw==", + "dev": true + }, "node_modules/express": { "version": "4.18.1", "resolved": "https://registry.npmjs.org/express/-/express-4.18.1.tgz", @@ -8722,6 +8790,7 @@ "version": "4.0.4", "resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz", "integrity": "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==", + "deprecated": "This package is no longer supported.", "dev": true, "dependencies": { "aproba": "^1.0.3 || ^2.0.0", @@ -9010,15 +9079,15 @@ "dev": true }, "node_modules/hosted-git-info": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-5.0.0.tgz", - "integrity": "sha512-rRnjWu0Bxj+nIfUOkz0695C0H6tRrN5iYIzYejb0tDEefe2AekHu/U5Kn9pEie5vsJqpNQU02az7TGSH3qpz4Q==", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-5.2.1.tgz", + "integrity": "sha512-xIcQYMnhcx2Nr4JTjsFmwwnr9vldugPy9uVm0o87bjqqWMv9GaqsTeT+i99wTl0mk1uLxJtHxLb8kymqTENQsw==", "dev": true, "dependencies": { "lru-cache": "^7.5.1" }, "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16" + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, "node_modules/hpack.js": { @@ -9046,9 +9115,9 @@ "dev": true }, "node_modules/http-cache-semantics": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz", - "integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", + "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==", "dev": true }, "node_modules/http-deceiver": { @@ -9339,9 +9408,9 @@ } }, "node_modules/ignore-walk/node_modules/minimatch": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.0.tgz", - "integrity": "sha512-9TPBGGak4nHfGZsPBohm9AWg6NoT7QTCehS3BIJABslyZbzxfV78QM2Y6+i741OPZIafFAaiiEMh5OyIrJPgtg==", + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", "dev": true, "dependencies": { "brace-expansion": "^2.0.1" @@ -9582,10 +9651,29 @@ "node": ">=12" } }, - "node_modules/ip": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ip/-/ip-2.0.0.tgz", - "integrity": "sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ==", + "node_modules/ip-address": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", + "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", + "dev": true, + "dependencies": { + "jsbn": "1.1.0", + "sprintf-js": "^1.1.3" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/ip-address/node_modules/jsbn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", + "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==", + "dev": true + }, + "node_modules/ip-address/node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", "dev": true }, "node_modules/ipaddr.js": { @@ -10714,9 +10802,9 @@ "dev": true }, "node_modules/make-fetch-happen": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-10.2.0.tgz", - "integrity": "sha512-OnEfCLofQVJ5zgKwGk55GaqosqKjaR6khQlJY3dBAA+hM25Bc5CmX5rKUfVut+rYA3uidA7zb7AvcglU87rPRg==", + "version": "10.2.1", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-10.2.1.tgz", + "integrity": "sha512-NgOPbRiaQM10DYXvN3/hhGVI2M5MtITFryzBGxHM5p4wnFxsVCbxkrBrDsk+EZ5OB4jEOT7AjDxtdF+KVEFT7w==", "dev": true, "dependencies": { "agentkeepalive": "^4.2.1", @@ -11045,9 +11133,9 @@ } }, "node_modules/minipass-fetch": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-2.1.0.tgz", - "integrity": "sha512-H9U4UVBGXEyyWJnqYDCLp1PwD8XIkJ4akNHp1aGVI+2Ym7wQMlxDKi4IB4JbmyU+pl9pEs/cVrK6cOuvmbK4Sg==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-2.1.2.tgz", + "integrity": "sha512-LT49Zi2/WMROHYoqGgdlQIZh8mLPZmOrN2NdJjMXxYe4nkN6FUyuPuOAOedNJDrx0IRGg9+4guZewtp8hE6TxA==", "dev": true, "dependencies": { "minipass": "^3.1.6", @@ -11353,16 +11441,17 @@ } }, "node_modules/node-gyp": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-9.1.0.tgz", - "integrity": "sha512-HkmN0ZpQJU7FLbJauJTHkHlSVAXlNGDAzH/VYFZGDOnFyn/Na3GlNJfkudmufOdS6/jNFhy88ObzL7ERz9es1g==", + "version": "9.4.1", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-9.4.1.tgz", + "integrity": "sha512-OQkWKbjQKbGkMf/xqI1jjy3oCTgMKJac58G2+bjZb3fza6gW2YrCSdMQYaoTb70crvE//Gngr4f0AgVHmqHvBQ==", "dev": true, "dependencies": { "env-paths": "^2.2.0", + "exponential-backoff": "^3.1.1", "glob": "^7.1.4", "graceful-fs": "^4.2.6", "make-fetch-happen": "^10.0.3", - "nopt": "^5.0.0", + "nopt": "^6.0.0", "npmlog": "^6.0.0", "rimraf": "^3.0.2", "semver": "^7.3.5", @@ -11373,7 +11462,7 @@ "node-gyp": "bin/node-gyp.js" }, "engines": { - "node": "^12.22 || ^14.13 || >=16" + "node": "^12.13 || ^14.13 || >=16" } }, "node_modules/node-gyp-build": { @@ -11387,26 +11476,11 @@ "node-gyp-build-test": "build-test.js" } }, - "node_modules/node-gyp/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/node-gyp/node_modules/semver": { - "version": "7.3.7", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", - "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, "bin": { "semver": "bin/semver.js" }, @@ -11441,24 +11515,24 @@ "integrity": "sha512-gkXMxRzUH+PB0ax9dUN0yYF0S25BqeAYqhgMaLUFmpXLEk7Fcu8f4emJuOAY0V8kjDICxROIKsTAKsV/v355xw==" }, "node_modules/nopt": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", - "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-6.0.0.tgz", + "integrity": "sha512-ZwLpbTgdhuZUnZzjd7nb1ZV+4DoiC6/sfiVKok72ym/4Tlf+DFdlHYmT2JPmcNNWV6Pi3SDf1kT+A4r9RTuT9g==", "dev": true, "dependencies": { - "abbrev": "1" + "abbrev": "^1.0.0" }, "bin": { "nopt": "bin/nopt.js" }, "engines": { - "node": ">=6" + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, "node_modules/normalize-package-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-4.0.0.tgz", - "integrity": "sha512-m+GL22VXJKkKbw62ZaBBjv8u6IE3UI4Mh5QakIqs3fWiKe0Xyi6L97hakwZK41/LD4R/2ly71Bayx0NLMwLA/g==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-4.0.1.tgz", + "integrity": "sha512-EBk5QKKuocMJhB3BILuKhmaPjI8vNRSpIfO9woLC6NyHVkKKdVEdAO1mrT0ZfxNR1lKwCcTkuZfmGIFdizZ8Pg==", "dev": true, "dependencies": { "hosted-git-info": "^5.0.0", @@ -11467,29 +11541,14 @@ "validate-npm-package-license": "^3.0.4" }, "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16" - } - }, - "node_modules/normalize-package-data/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, "node_modules/normalize-package-data/node_modules/semver": { - "version": "7.3.7", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", - "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, "bin": { "semver": "bin/semver.js" }, @@ -11536,26 +11595,11 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, - "node_modules/npm-install-checks/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/npm-install-checks/node_modules/semver": { - "version": "7.3.7", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", - "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, "bin": { "semver": "bin/semver.js" }, @@ -11584,26 +11628,11 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, - "node_modules/npm-package-arg/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/npm-package-arg/node_modules/semver": { - "version": "7.3.7", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", - "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, "bin": { "semver": "bin/semver.js" }, @@ -11612,15 +11641,15 @@ } }, "node_modules/npm-packlist": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-5.1.1.tgz", - "integrity": "sha512-UfpSvQ5YKwctmodvPPkK6Fwk603aoVsf8AEbmVKAEECrfvL8SSe1A2YIwrJ6xmTHAITKPwwZsWo7WwEbNk0kxw==", + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-5.1.3.tgz", + "integrity": "sha512-263/0NGrn32YFYi4J533qzrQ/krmmrWwhKkzwTuM4f/07ug51odoaNjUexxO4vxlzURHcmYMH1QjvHjsNDKLVg==", "dev": true, "dependencies": { "glob": "^8.0.1", "ignore-walk": "^5.0.1", - "npm-bundled": "^1.1.2", - "npm-normalize-package-bin": "^1.0.1" + "npm-bundled": "^2.0.0", + "npm-normalize-package-bin": "^2.0.0" }, "bin": { "npm-packlist": "bin/index.js" @@ -11639,9 +11668,10 @@ } }, "node_modules/npm-packlist/node_modules/glob": { - "version": "8.0.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-8.0.3.tgz", - "integrity": "sha512-ull455NHSHI/Y1FqGaaYFaLGkNMMJbavMrEGFXG/PGrg6y7sutWHUHrz6gy6WEBH6akM1M414dWKCNs+IhKdiQ==", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "deprecated": "Glob versions prior to v9 are no longer supported", "dev": true, "dependencies": { "fs.realpath": "^1.0.0", @@ -11658,9 +11688,9 @@ } }, "node_modules/npm-packlist/node_modules/minimatch": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.0.tgz", - "integrity": "sha512-9TPBGGak4nHfGZsPBohm9AWg6NoT7QTCehS3BIJABslyZbzxfV78QM2Y6+i741OPZIafFAaiiEMh5OyIrJPgtg==", + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", "dev": true, "dependencies": { "brace-expansion": "^2.0.1" @@ -11669,6 +11699,27 @@ "node": ">=10" } }, + "node_modules/npm-packlist/node_modules/npm-bundled": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-2.0.1.tgz", + "integrity": "sha512-gZLxXdjEzE/+mOstGDqR6b0EkhJ+kM6fxM6vUuckuctuVPh80Q6pw/rSZj9s4Gex9GxWtIicO1pc8DB9KZWudw==", + "dev": true, + "dependencies": { + "npm-normalize-package-bin": "^2.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/npm-packlist/node_modules/npm-normalize-package-bin": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-2.0.0.tgz", + "integrity": "sha512-awzfKUO7v0FscrSpRoogyNm0sajikhBWpU0QMrW09AMi9n1PoKU6WaIqUzuJSQnpciZZmJ/jMZ2Egfmb/9LiWQ==", + "dev": true, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, "node_modules/npm-pick-manifest": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-7.0.1.tgz", @@ -11684,26 +11735,11 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, - "node_modules/npm-pick-manifest/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/npm-pick-manifest/node_modules/semver": { - "version": "7.3.7", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", - "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, "bin": { "semver": "bin/semver.js" }, @@ -11712,9 +11748,9 @@ } }, "node_modules/npm-registry-fetch": { - "version": "13.3.0", - "resolved": "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-13.3.0.tgz", - "integrity": "sha512-10LJQ/1+VhKrZjIuY9I/+gQTvumqqlgnsCufoXETHAPFTS3+M+Z5CFhZRDHGavmJ6rOye3UvNga88vl8n1r6gg==", + "version": "13.3.1", + "resolved": "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-13.3.1.tgz", + "integrity": "sha512-eukJPi++DKRTjSBRcDZSDDsGqRK3ehbxfFUcgaRd0Yp6kRwOwh2WVn0r+8rMB4nnuzvAk6rQVzl6K5CkYOmnvw==", "dev": true, "dependencies": { "make-fetch-happen": "^10.0.6", @@ -11745,6 +11781,7 @@ "version": "6.0.2", "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz", "integrity": "sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==", + "deprecated": "This package is no longer supported.", "dev": true, "dependencies": { "are-we-there-yet": "^3.0.0", @@ -12336,9 +12373,9 @@ } }, "node_modules/pacote": { - "version": "13.6.1", - "resolved": "https://registry.npmjs.org/pacote/-/pacote-13.6.1.tgz", - "integrity": "sha512-L+2BI1ougAPsFjXRyBhcKmfT016NscRFLv6Pz5EiNf1CCFJFU0pSKKQwsZTyAQB+sTuUL4TyFyp6J1Ork3dOqw==", + "version": "13.6.2", + "resolved": "https://registry.npmjs.org/pacote/-/pacote-13.6.2.tgz", + "integrity": "sha512-Gu8fU3GsvOPkak2CkbojR7vjs3k3P9cA6uazKTHdsdV0gpCEQq2opelnEv30KRQWgVzP5Vd/5umjcedma3MKtg==", "dev": true, "dependencies": { "@npmcli/git": "^3.0.0", @@ -13762,15 +13799,16 @@ } }, "node_modules/read-package-json": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/read-package-json/-/read-package-json-5.0.1.tgz", - "integrity": "sha512-MALHuNgYWdGW3gKzuNMuYtcSSZbGQm94fAp16xt8VsYTLBjUSc55bLMKe6gzpWue0Tfi6CBgwCSdDAqutGDhMg==", + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/read-package-json/-/read-package-json-5.0.2.tgz", + "integrity": "sha512-BSzugrt4kQ/Z0krro8zhTwV1Kd79ue25IhNN/VtHFy1mG/6Tluyi+msc0UpwaoQzxSHa28mntAjIZY6kEgfR9Q==", + "deprecated": "This package is no longer supported. Please use @npmcli/package-json instead.", "dev": true, "dependencies": { "glob": "^8.0.1", "json-parse-even-better-errors": "^2.3.1", "normalize-package-data": "^4.0.0", - "npm-normalize-package-bin": "^1.0.1" + "npm-normalize-package-bin": "^2.0.0" }, "engines": { "node": "^12.13.0 || ^14.15.0 || >=16.0.0" @@ -13799,9 +13837,10 @@ } }, "node_modules/read-package-json/node_modules/glob": { - "version": "8.0.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-8.0.3.tgz", - "integrity": "sha512-ull455NHSHI/Y1FqGaaYFaLGkNMMJbavMrEGFXG/PGrg6y7sutWHUHrz6gy6WEBH6akM1M414dWKCNs+IhKdiQ==", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "deprecated": "Glob versions prior to v9 are no longer supported", "dev": true, "dependencies": { "fs.realpath": "^1.0.0", @@ -13818,9 +13857,9 @@ } }, "node_modules/read-package-json/node_modules/minimatch": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.0.tgz", - "integrity": "sha512-9TPBGGak4nHfGZsPBohm9AWg6NoT7QTCehS3BIJABslyZbzxfV78QM2Y6+i741OPZIafFAaiiEMh5OyIrJPgtg==", + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", "dev": true, "dependencies": { "brace-expansion": "^2.0.1" @@ -13829,6 +13868,15 @@ "node": ">=10" } }, + "node_modules/read-package-json/node_modules/npm-normalize-package-bin": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-2.0.0.tgz", + "integrity": "sha512-awzfKUO7v0FscrSpRoogyNm0sajikhBWpU0QMrW09AMi9n1PoKU6WaIqUzuJSQnpciZZmJ/jMZ2Egfmb/9LiWQ==", + "dev": true, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, "node_modules/readable-stream": { "version": "2.3.7", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", @@ -14179,10 +14227,9 @@ "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==" }, "node_modules/rxjs": { - "version": "6.6.3", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.3.tgz", - "integrity": "sha512-trsQc+xYYXZ3urjOiJOuCOa5N3jAZ3eiSpQB5hIT8zGlL2QfnHLJ2r7GMkBGuIausdJN1OneaI6gQlsqNHHmZQ==", - "peer": true, + "version": "6.6.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", + "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", "dependencies": { "tslib": "^1.9.0" }, @@ -14685,16 +14732,16 @@ } }, "node_modules/socks": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/socks/-/socks-2.7.0.tgz", - "integrity": "sha512-scnOe9y4VuiNUULJN72GrM26BNOjVsfPXI+j+98PkyEfsIXroa5ofyjT+FzGvn/xHs73U2JtoBYAVx9Hl4quSA==", + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.3.tgz", + "integrity": "sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw==", "dev": true, "dependencies": { - "ip": "^2.0.0", + "ip-address": "^9.0.5", "smart-buffer": "^4.2.0" }, "engines": { - "node": ">= 10.13.0", + "node": ">= 10.0.0", "npm": ">= 3.0.0" } }, @@ -14800,9 +14847,9 @@ "dev": true }, "node_modules/spdx-correct": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz", - "integrity": "sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", "dev": true, "dependencies": { "spdx-expression-parse": "^3.0.0", @@ -14810,9 +14857,9 @@ } }, "node_modules/spdx-exceptions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", - "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", "dev": true }, "node_modules/spdx-expression-parse": { @@ -14826,9 +14873,9 @@ } }, "node_modules/spdx-license-ids": { - "version": "3.0.11", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.11.tgz", - "integrity": "sha512-Ctl2BrFiM0X3MANYgj3CkygxhRmr9mi6xhejbdO960nF6EDJApTYpn0BQnDKlnNBULKiCN1n3w9EBkHK8ZWg+g==", + "version": "3.0.18", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.18.tgz", + "integrity": "sha512-xxRs31BqRYHwiMzudOrpSiHtZ8i/GeionCBDSilhYRj+9gIcI8wCZTlXZKu9vZIVqViP3dcp9qE5G6AlIaD+TQ==", "dev": true }, "node_modules/spdy": { diff --git a/modules/fbs-core/web/package.json b/modules/fbs-core/web/package.json index beec79c8d..fc483f575 100644 --- a/modules/fbs-core/web/package.json +++ b/modules/fbs-core/web/package.json @@ -50,7 +50,7 @@ "@angular-eslint/eslint-plugin-template": "14.0.2", "@angular-eslint/schematics": "14.0.2", "@angular-eslint/template-parser": "14.0.2", - "@angular/cli": "^14.1.0", + "@angular/cli": "^14.2.13", "@angular/compiler-cli": "^14.1.0", "@angular/language-service": "^14.1.0", "@types/file-saver": "^2.0.1", From 199f09d01842aad55aec339f94187fcd38c56d08 Mon Sep 17 00:00:00 2001 From: Sophie Methe Date: Wed, 3 Jul 2024 10:50:11 +0200 Subject: [PATCH 05/21] Update app-routing, app.module, and i18n files (#1675) --- .../fbs-core/web/src/app/app-routing.module.ts | 15 +++++++++++++++ modules/fbs-core/web/src/app/app.module.ts | 11 +++++++++++ modules/fbs-core/web/src/i18n/de.ts | 18 ++++++++++++++++++ modules/fbs-core/web/src/i18n/en.ts | 18 ++++++++++++++++++ 4 files changed, 62 insertions(+) diff --git a/modules/fbs-core/web/src/app/app-routing.module.ts b/modules/fbs-core/web/src/app/app-routing.module.ts index 4501363ca..dbd574cbe 100644 --- a/modules/fbs-core/web/src/app/app-routing.module.ts +++ b/modules/fbs-core/web/src/app/app-routing.module.ts @@ -6,6 +6,7 @@ import { ChangePasswordComponent } from "./page-components/change-password/chang import { MyCoursesComponent } from "./page-components/my-courses/my-courses.component"; import { SearchCoursesComponent } from "./page-components/search-courses/search-courses.component"; import { CourseDetailComponent } from "./page-components/course-detail/course-detail.component"; +import { MyGroupsComponent } from "./page-components/my-groups/my-groups.component"; import { UserManagementComponent } from "./page-components/user-management/user-management.component"; import { NotFoundComponent } from "./page-components/not-found/not-found.component"; import { LoginComponent } from "./page-components/login/login.component"; @@ -20,6 +21,7 @@ import { SqlCheckerResultsComponent } from "./page-components/sql-checker/sql-ch import { SqlPlaygroundComponent } from "./page-components/sql-playground/sql-playground.component"; import { AnalyticsToolComponent } from "./page-components/analytics-tool/analytics-tool.component"; import { FbsModellingComponent } from "./page-components/fbs-modelling/fbs-modelling.component"; +import { GroupDetailComponent } from "./page-components/group-detail/group-detail.component"; const routes: Routes = [ { path: "login", component: LoginComponent }, @@ -74,6 +76,19 @@ const routes: Routes = [ canActivate: [AuthGuard], }, + //groups + { + path: "groups", + component: MyGroupsComponent, + canActivate: [AuthGuard], + }, + + { + path: "groups/:courseId/:id", + component: GroupDetailComponent, + canActivate: [AuthGuard], + }, + //sql playground { path: "sqlplayground", diff --git a/modules/fbs-core/web/src/app/app.module.ts b/modules/fbs-core/web/src/app/app.module.ts index 782935490..11efa26f5 100644 --- a/modules/fbs-core/web/src/app/app.module.ts +++ b/modules/fbs-core/web/src/app/app.module.ts @@ -117,6 +117,11 @@ import { LanguageMenuComponent } from "./page-components/sidebar/language-menu/l import { registerLocaleData } from "@angular/common"; import localeDe from "@angular/common/locales/de"; import localeDeExtra from "@angular/common/locales/extra/de"; +import { MyGroupsComponent } from "./page-components/my-groups/my-groups.component"; +import { GroupSelectionComponent } from "./page-components/course-detail/group-selection/group-selection.component"; +import { NewGroupDialogComponent } from "./dialogs/new-group-dialog/new-group-dialog.component"; +import { GroupPreviewComponent } from "./page-components/group-preview/group-preview.component"; +import { GroupDetailComponent } from "./page-components/group-detail/group-detail.component"; @Injectable() export class ApiURIHttpInterceptor implements HttpInterceptor { @@ -217,6 +222,12 @@ export const httpInterceptorProviders = [ SharePlaygroundLinkDialogComponent, FbsModellingComponent, LanguageMenuComponent, + MyGroupsComponent, + GroupSelectionComponent, + NewGroupDialogComponent, + GroupPreviewComponent, + GroupPreviewComponent, + GroupDetailComponent, ], imports: [ BrowserModule, diff --git a/modules/fbs-core/web/src/i18n/de.ts b/modules/fbs-core/web/src/i18n/de.ts index 771a7e658..7408feee6 100644 --- a/modules/fbs-core/web/src/i18n/de.ts +++ b/modules/fbs-core/web/src/i18n/de.ts @@ -28,6 +28,11 @@ export const germanTranslation = { "Liebe/r Nutzer/in, Sie betrachten einen Kurs in dem Sie nicht als Teilnehmer/in geführt werden. Sie können alle Inhalte einsehen und an Übungen teilnehmen, jedoch wird keine Ihrer Abgaben gewertet und der Dozent wird nicht über Ihre Teilnahme informiert. Falls Sie an diesem Kurs teilnehmen wollen, dann melden Sie sich bitte an.", "course.join": "Am Kurs teilnehmen", "course.create-task": "Aufgabe erstellen", + "course.create-group": "Gruppe erstellen", + "course.select-group": "Gruppenwahl", + "course.group": "Gruppe", + "course.group-membership": "Mitgliederanzahl", + "course.save-selected-group": "Wahl speichern", "course.progress-title": "Mein Kurs Fortschritt", "course.progress-no-conditions": "Dieser Kurs hat keine Voraussetzungen", "course.progress-complete": "Du hast den Kurs erfolgreich abgeschlossen", @@ -91,6 +96,8 @@ export const germanTranslation = { "course.not-found.message": "Entschuldigung, leider existiert die von Ihnen aufgerufenen Seite nicht.", "course.not-found.backHome": "Zurück zur Startseite", + "course.tabs.tasks": "Aufgaben", + "course.tabs.groups": "Gruppen", "result.comparison.resultsOfSubmissions": "Ergebnisse der Abgaben", "result.comparison.submission": "Abgabe", "result.comparison.checkerType": "Checker Typ", @@ -109,6 +116,7 @@ export const germanTranslation = { "sidebar.label.feedbacksystem": "Feedbacksystem", "sidebar.label.myCourses": "Meine Kurse", "sidebar.label.searchCourses": "Kurs Suche", + "sidebar.label.myGroups": "Meine Gruppen", "sidebar.label.userManagement": "Benutzerverwaltung", "sidebar.label.allCourses": "Alle Kurse", "sidebar.label.users": "Benutzer", @@ -279,6 +287,16 @@ export const germanTranslation = { "dialog.task.points.tooltip.addCategory": "Kategorie hinzufügen", "dialog.task.points.label.cancelButton": "Abbrechen", "dialog.task.points.label.saveButton": "Erstellen", + "dialog.group.new.create-group": "Gruppe erstellen", + "dialog.group.new.group-name": "Gruppenname", + "dialog.group.new.group-name-error": "Der Gruppenname ist erforderlich!", + "dialog.group.new.group-membership": "Maximale Mitgliederanzahl", + "dialog.group.new.group-membership-error": + "Die maximale Mitgliederanzahl ist erforderlich!", + "dialog.group.new.showGroup": "Gruppe anzeigen", + "dialog.group.new.hideGroup": "Gruppe verbergen", + "dialog.group.new.cancel": "Abbrechen", + "dialog.group.new.create": "Erstellen", "task.detail.tooltip.backToCourse": "Zurück zum Kurs", "task.detail.tooltip.exportTask": "Aufgabe Exportieren", "task.detail.tooltip.configureCheck": "Überprüfung konfigurieren", diff --git a/modules/fbs-core/web/src/i18n/en.ts b/modules/fbs-core/web/src/i18n/en.ts index d5b552b8e..bbc573c15 100644 --- a/modules/fbs-core/web/src/i18n/en.ts +++ b/modules/fbs-core/web/src/i18n/en.ts @@ -28,6 +28,11 @@ export const englishTranslation = { "Dear user, you are viewing a course in which you are not registered as a participant. You can view all content and participate in exercises, but none of your submissions will be evaluated and the lecturer will not be notified of your participation. If you want to participate in this course, please sign up.", "course.join": "Join Course", "course.create-task": "Create Task", + "course.create-group": "Create Group", + "course.select-group": "Select Group", + "course.group": "Group", + "course.group-membership": "Number of members", + "course.save-selected-group": "Save selection", "course.progress-title": "My Course Progress", "course.progress-no-conditions": "There are no conditions for this course", "course.progress-complete": "You have successfully completed the course", @@ -90,6 +95,8 @@ export const englishTranslation = { "course.update.button.save": "Save", "course.not-found.message": "Sorry, the page you requested does not exist.", "course.not-found.backHome": "Back to Homepage", + "course.tabs.tasks": "Tasks", + "course.tabs.groups": "Groups", "result.comparison.resultsOfSubmissions": "Results of Submissions", "result.comparison.submission": "Submission", "result.comparison.checkerType": "Checker Type", @@ -108,6 +115,7 @@ export const englishTranslation = { "sidebar.label.feedbacksystem": "Feedbacksystem", "sidebar.label.myCourses": "My Courses", "sidebar.label.searchCourses": "Kurs Search", + "sidebar.label.myGroups": "My Groups", "sidebar.label.userManagement": "User Management", "sidebar.label.allCourses": "All Courses", "sidebar.label.users": "Users", @@ -273,6 +281,16 @@ export const englishTranslation = { "dialog.task.points.tooltip.addCategory": "Add category", "dialog.task.points.label.cancelButton": "Cancel", "dialog.task.points.label.saveButton": "Create", + "dialog.group.new.create-group": "Create group", + "dialog.group.new.group-name": "Group name", + "dialog.group.new.group-name-error": "The group name is required!", + "dialog.group.new.group-membership": "Maximum number of members", + "dialog.group.new.group-membership-error": + "The maximum number of members is required!", + "dialog.group.new.showGroup": "Display group", + "dialog.group.new.hideGroup": "Hide group", + "dialog.group.new.cancel": "Cancel", + "dialog.group.new.create": "Create", "task.detail.tooltip.backToCourse": "Back to Course", "task.detail.tooltip.exportTask": "Export Task", "task.detail.tooltip.configureCheck": "Configure Checker", From 4acd8c03d9f0dc6fcf08f520695517c014259ccb Mon Sep 17 00:00:00 2001 From: Sophie Methe Date: Wed, 3 Jul 2024 10:55:17 +0200 Subject: [PATCH 06/21] Add new page-components (#1675) --- .../group-selection.component.html | 44 +++++++++ .../group-selection.component.scss | 22 +++++ .../group-selection.component.ts | 89 +++++++++++++++++++ .../group-detail/group-detail.component.html | 1 + .../group-detail/group-detail.component.scss | 0 .../group-detail/group-detail.component.ts | 12 +++ .../group-preview.component.html | 4 + .../group-preview.component.scss | 30 +++++++ .../group-preview/group-preview.component.ts | 37 ++++++++ .../my-groups/my-groups.component.html | 6 ++ .../my-groups/my-groups.component.scss | 18 ++++ .../my-groups/my-groups.component.ts | 42 +++++++++ 12 files changed, 305 insertions(+) create mode 100644 modules/fbs-core/web/src/app/page-components/course-detail/group-selection/group-selection.component.html create mode 100644 modules/fbs-core/web/src/app/page-components/course-detail/group-selection/group-selection.component.scss create mode 100644 modules/fbs-core/web/src/app/page-components/course-detail/group-selection/group-selection.component.ts create mode 100644 modules/fbs-core/web/src/app/page-components/group-detail/group-detail.component.html create mode 100644 modules/fbs-core/web/src/app/page-components/group-detail/group-detail.component.scss create mode 100644 modules/fbs-core/web/src/app/page-components/group-detail/group-detail.component.ts create mode 100644 modules/fbs-core/web/src/app/page-components/group-preview/group-preview.component.html create mode 100644 modules/fbs-core/web/src/app/page-components/group-preview/group-preview.component.scss create mode 100644 modules/fbs-core/web/src/app/page-components/group-preview/group-preview.component.ts create mode 100644 modules/fbs-core/web/src/app/page-components/my-groups/my-groups.component.html create mode 100644 modules/fbs-core/web/src/app/page-components/my-groups/my-groups.component.scss create mode 100644 modules/fbs-core/web/src/app/page-components/my-groups/my-groups.component.ts diff --git a/modules/fbs-core/web/src/app/page-components/course-detail/group-selection/group-selection.component.html b/modules/fbs-core/web/src/app/page-components/course-detail/group-selection/group-selection.component.html new file mode 100644 index 000000000..a306c0905 --- /dev/null +++ b/modules/fbs-core/web/src/app/page-components/course-detail/group-selection/group-selection.component.html @@ -0,0 +1,44 @@ +
+
+ +
+ + + + + + + + + + + + + + +
{{ "course.select-group" | i18nextEager }}{{ "course.group" | i18nextEager }}{{ "course.group-membership" | i18nextEager }}
+ + {{ group.name }}{{ group.membership }}
+ +
diff --git a/modules/fbs-core/web/src/app/page-components/course-detail/group-selection/group-selection.component.scss b/modules/fbs-core/web/src/app/page-components/course-detail/group-selection/group-selection.component.scss new file mode 100644 index 000000000..0bafaf234 --- /dev/null +++ b/modules/fbs-core/web/src/app/page-components/course-detail/group-selection/group-selection.component.scss @@ -0,0 +1,22 @@ +.create-group { + display: flex; + margin-top: 30px; + margin-bottom: 30px; + justify-content: center; +} + +table { + width: 100%; + border-collapse: collapse; +} + +th, +td { + border: 1px solid black; + padding: 10px; + text-align: left; +} + +button { + margin-top: 10px; +} diff --git a/modules/fbs-core/web/src/app/page-components/course-detail/group-selection/group-selection.component.ts b/modules/fbs-core/web/src/app/page-components/course-detail/group-selection/group-selection.component.ts new file mode 100644 index 000000000..e478d1c60 --- /dev/null +++ b/modules/fbs-core/web/src/app/page-components/course-detail/group-selection/group-selection.component.ts @@ -0,0 +1,89 @@ +import { Component, Input, OnInit } from "@angular/core"; +import { ActivatedRoute } from "@angular/router"; +import { AuthService } from "../../../service/auth.service"; +import { Observable } from "rxjs"; +import { Requirement } from "../../../model/Requirement"; +import { MatDialog } from "@angular/material/dialog"; +import { Roles } from "../../../model/Roles"; +import { NewGroupDialogComponent } from "../../../dialogs/new-group-dialog/new-group-dialog.component"; +import { Group } from "../../../model/Group"; +import { GroupService } from "../../../service/group.service"; +import { GroupRegistrationService } from "../../../service/group-registration.sevice"; + +@Component({ + selector: "app-group-selection", + templateUrl: "./group-selection.component.html", + styleUrls: ["./group-selection.component.scss"], +}) +export class GroupSelectionComponent implements OnInit { + @Input() requirements: Observable; + + groups: Observable; + selectedGroup: Group; + courseId: number; + editGroups: boolean = false; + + constructor( + private route: ActivatedRoute, + private auth: AuthService, + private dialog: MatDialog, + private groupService: GroupService, + private groupRegistrationService: GroupRegistrationService + ) {} + + ngOnInit(): void { + this.route.params.subscribe((param) => { + this.courseId = param.id; + }); + this.loadGroups(); + } + + public isAuthorized(ignoreTutor: boolean = false) { + const token = this.auth.getToken(); + const courseRole = token.courseRoles[this.courseId]; + const globalRole = token.globalRole; + return ( + Roles.GlobalRole.isAdmin(globalRole) || + Roles.GlobalRole.isModerator(globalRole) || + Roles.CourseRole.isDocent(courseRole) || + (Roles.CourseRole.isTutor(courseRole) && !ignoreTutor) + ); + } + + loadGroups(): void { + this.groups = this.groupService.getGroupList(this.courseId); + } + + createGroup() { + this.dialog + .open(NewGroupDialogComponent, { + data: { cid: this.courseId }, + height: "auto", + width: "50%", + }) + .afterClosed() + .subscribe( + () => { + this.loadGroups(); + }, + (error) => console.error(error) + ); + } + + joinGroup(): void { + if (this.selectedGroup) { + this.groupRegistrationService + .registerGroup( + this.courseId, + this.selectedGroup.id, + this.auth.getToken().id + ) + .subscribe( + () => { + this.loadGroups(); + }, + (error) => console.error(error) + ); + } else console.error("No group selected"); + } +} diff --git a/modules/fbs-core/web/src/app/page-components/group-detail/group-detail.component.html b/modules/fbs-core/web/src/app/page-components/group-detail/group-detail.component.html new file mode 100644 index 000000000..1051ef28b --- /dev/null +++ b/modules/fbs-core/web/src/app/page-components/group-detail/group-detail.component.html @@ -0,0 +1 @@ +

group-detail works!

diff --git a/modules/fbs-core/web/src/app/page-components/group-detail/group-detail.component.scss b/modules/fbs-core/web/src/app/page-components/group-detail/group-detail.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/modules/fbs-core/web/src/app/page-components/group-detail/group-detail.component.ts b/modules/fbs-core/web/src/app/page-components/group-detail/group-detail.component.ts new file mode 100644 index 000000000..86679f71b --- /dev/null +++ b/modules/fbs-core/web/src/app/page-components/group-detail/group-detail.component.ts @@ -0,0 +1,12 @@ +import { Component, OnInit } from "@angular/core"; + +@Component({ + selector: "app-group-detail", + templateUrl: "./group-detail.component.html", + styleUrls: ["./group-detail.component.scss"], +}) +export class GroupDetailComponent implements OnInit { + constructor() {} + + ngOnInit(): void {} +} diff --git a/modules/fbs-core/web/src/app/page-components/group-preview/group-preview.component.html b/modules/fbs-core/web/src/app/page-components/group-preview/group-preview.component.html new file mode 100644 index 000000000..19dc355b8 --- /dev/null +++ b/modules/fbs-core/web/src/app/page-components/group-preview/group-preview.component.html @@ -0,0 +1,4 @@ + + {{ data.name }} - {{ courseName }} + + diff --git a/modules/fbs-core/web/src/app/page-components/group-preview/group-preview.component.scss b/modules/fbs-core/web/src/app/page-components/group-preview/group-preview.component.scss new file mode 100644 index 000000000..6d56fd5e2 --- /dev/null +++ b/modules/fbs-core/web/src/app/page-components/group-preview/group-preview.component.scss @@ -0,0 +1,30 @@ +.card { + margin: 8px 0; + height: 30px; + display: flex; + align-items: center; + justify-content: space-between; + cursor: pointer; +} +.card:hover { + background: #e9e9ee; +} + +.card .invisible { + font-weight: bold; + color: #cccccc; +} + +.passed { + font-weight: bold; + color: rgba(100,223,89,255); +} + +.failed { + font-weight: bold; + color: red; +} + +.all { + font-weight: bold; +} diff --git a/modules/fbs-core/web/src/app/page-components/group-preview/group-preview.component.ts b/modules/fbs-core/web/src/app/page-components/group-preview/group-preview.component.ts new file mode 100644 index 000000000..4a8da3bfb --- /dev/null +++ b/modules/fbs-core/web/src/app/page-components/group-preview/group-preview.component.ts @@ -0,0 +1,37 @@ +import { Component, Input, OnInit } from "@angular/core"; +import { Group } from "../../model/Group"; +import { Course } from "../../model/Course"; +import { CourseService } from "../../service/course.service"; +import { Router } from "@angular/router"; + +@Component({ + selector: "app-group-preview", + templateUrl: "./group-preview.component.html", + styleUrls: ["./group-preview.component.scss"], +}) +export class GroupPreviewComponent implements OnInit { + @Input() data: Group; + course: Course; + courseName: string; + + constructor(private router: Router, private courseService: CourseService) {} + + ngOnInit(): void { + this.loadCourseName(); + } + + loadCourseName(): void { + this.courseService.getCourse(this.data.courseId).subscribe((course) => { + this.course = course; + this.courseName = course.name; + }); + } + + /** + * Show group in detail + */ + goToGroup(): void { + console.log("Navigationsdaten:", this.data.courseId, this.data.id); + this.router.navigate(["groups", this.data.courseId, this.data.id]); + } +} diff --git a/modules/fbs-core/web/src/app/page-components/my-groups/my-groups.component.html b/modules/fbs-core/web/src/app/page-components/my-groups/my-groups.component.html new file mode 100644 index 000000000..508f031bc --- /dev/null +++ b/modules/fbs-core/web/src/app/page-components/my-groups/my-groups.component.html @@ -0,0 +1,6 @@ +
+ +
diff --git a/modules/fbs-core/web/src/app/page-components/my-groups/my-groups.component.scss b/modules/fbs-core/web/src/app/page-components/my-groups/my-groups.component.scss new file mode 100644 index 000000000..dbab665c3 --- /dev/null +++ b/modules/fbs-core/web/src/app/page-components/my-groups/my-groups.component.scss @@ -0,0 +1,18 @@ +@import "src/colors"; + +.container { + display: block; + padding: 16px; + + .card { + margin: 8px 0; + height: 30px; + display: flex; + align-items: center; + justify-content: space-between; + } + + .card:hover { + background: #819ca9; + } +} diff --git a/modules/fbs-core/web/src/app/page-components/my-groups/my-groups.component.ts b/modules/fbs-core/web/src/app/page-components/my-groups/my-groups.component.ts new file mode 100644 index 000000000..477cb9e5c --- /dev/null +++ b/modules/fbs-core/web/src/app/page-components/my-groups/my-groups.component.ts @@ -0,0 +1,42 @@ +import { Component, Inject, Input, OnInit } from "@angular/core"; +import { Observable, of } from "rxjs"; +import { TitlebarService } from "../../service/titlebar.service"; +import { Group } from "../../model/Group"; +import { AuthService } from "../../service/auth.service"; +import { GroupRegistrationService } from "../../service/group-registration.sevice"; +import { + I18NEXT_SERVICE, + I18NextPipe, + ITranslationService, +} from "angular-i18next"; + +@Component({ + selector: "app-my-groups", + templateUrl: "./my-groups.component.html", + styleUrls: ["./my-groups.component.scss"], +}) +export class MyGroupsComponent implements OnInit { + @Input() data: Group; + userID: number; + groups: Observable = of([]); + + constructor( + private titlebar: TitlebarService, + private groupRegistrationService: GroupRegistrationService, + private authService: AuthService, + private i18NextPipe: I18NextPipe, + @Inject(I18NEXT_SERVICE) private i18NextService: ITranslationService + ) {} + + ngOnInit(): void { + this.i18NextService.events.languageChanged.subscribe(() => { + this.titlebar.emitTitle( + this.i18NextPipe.transform("sidebar.label.myGroups") + ); + }); + this.userID = this.authService.getToken().id; + this.groups = this.groupRegistrationService.getRegisteredGroups( + this.userID + ); + } +} From 5fa84ad7f3827210422e5110a4a25efe691c7262 Mon Sep 17 00:00:00 2001 From: Sophie Methe Date: Wed, 3 Jul 2024 10:56:51 +0200 Subject: [PATCH 07/21] Add new dialog for group creation (#1675) --- .../new-group-dialog.component.html | 70 +++++++++++++++++++ .../new-group-dialog.component.scss | 30 ++++++++ .../new-group-dialog.component.ts | 61 ++++++++++++++++ 3 files changed, 161 insertions(+) create mode 100644 modules/fbs-core/web/src/app/dialogs/new-group-dialog/new-group-dialog.component.html create mode 100644 modules/fbs-core/web/src/app/dialogs/new-group-dialog/new-group-dialog.component.scss create mode 100644 modules/fbs-core/web/src/app/dialogs/new-group-dialog/new-group-dialog.component.ts diff --git a/modules/fbs-core/web/src/app/dialogs/new-group-dialog/new-group-dialog.component.html b/modules/fbs-core/web/src/app/dialogs/new-group-dialog/new-group-dialog.component.html new file mode 100644 index 000000000..09a2a4319 --- /dev/null +++ b/modules/fbs-core/web/src/app/dialogs/new-group-dialog/new-group-dialog.component.html @@ -0,0 +1,70 @@ +
+
+ {{ "dialog.group.new.create-group" | i18nextEager }} +
+ + + + {{ + "dialog.group.new.group-name" | i18nextEager + }} + + + {{ "dialog.group.new.group-name-error" | i18nextEager }} + + + + + {{ + "dialog.group.new.group-membership" | i18nextEager + }} + + + {{ "dialog.group.new.group-membership-error" | i18nextEager }} + + + +

+ {{ "dialog.group.new.showGroup" | i18nextEager }} +

+

+ {{ "dialog.group.new.hideGroup" | i18nextEager }} +

+
+
+ + + + +
diff --git a/modules/fbs-core/web/src/app/dialogs/new-group-dialog/new-group-dialog.component.scss b/modules/fbs-core/web/src/app/dialogs/new-group-dialog/new-group-dialog.component.scss new file mode 100644 index 000000000..8bd3df716 --- /dev/null +++ b/modules/fbs-core/web/src/app/dialogs/new-group-dialog/new-group-dialog.component.scss @@ -0,0 +1,30 @@ +.container { + height: 100%; +} + +mat-form-field { + width: 100%; +} + +.actions { + display: flex; + justify-content: flex-end; + button { + margin-left: 16px; + } +} + +.choose_docent_tutor_area { + height: 150px; +} + +.tag_selection { + margin-bottom: 15px; + display: block; +} + +.spinner { + display: flex; + justify-content: center; + align-items: center; +} diff --git a/modules/fbs-core/web/src/app/dialogs/new-group-dialog/new-group-dialog.component.ts b/modules/fbs-core/web/src/app/dialogs/new-group-dialog/new-group-dialog.component.ts new file mode 100644 index 000000000..aadd20733 --- /dev/null +++ b/modules/fbs-core/web/src/app/dialogs/new-group-dialog/new-group-dialog.component.ts @@ -0,0 +1,61 @@ +import { Component, Inject } from "@angular/core"; +import { UntypedFormControl, Validators } from "@angular/forms"; +import { GroupService } from "../../service/group.service"; +import { MAT_DIALOG_DATA, MatDialogRef } from "@angular/material/dialog"; +import { GroupInput } from "../../model/Group"; + +@Component({ + selector: "app-new-group-dialog", + templateUrl: "./new-group-dialog.component.html", + styleUrls: ["./new-group-dialog.component.scss"], +}) +export class NewGroupDialogComponent { + name = new UntypedFormControl("", [Validators.required]); + membership = new UntypedFormControl("", [Validators.required]); + isVisible = true; + isUpdateDialog = false; + pending: boolean = false; + + constructor( + private groupService: GroupService, + public dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public data: { cid: number } + ) {} + + createGroup() { + if (!this.isInputValid()) { + this.pending = false; + return; + } + + const group: GroupInput = { + name: this.name.value, + membership: this.membership.value, + visible: this.isVisible, + }; + + this.pending = true; + + if (this.isUpdateDialog) { + // update DB + } else { + this.groupService.createGroup(this.data.cid, group).subscribe( + () => { + this.dialogRef.close({ success: true }); + }, + (error) => { + console.log(error); + this.dialogRef.close({ success: false }); + } + ); + } + } + + isInputValid(): boolean { + return this.name.valid && this.membership.valid; + } + + closeDialog() { + this.dialogRef.close({ success: false }); + } +} From e5dbbda0cadce44ecf63ca30801f7f4d9e083027 Mon Sep 17 00:00:00 2001 From: Sophie Methe Date: Tue, 9 Jul 2024 10:50:54 +0200 Subject: [PATCH 08/21] Add feature to display current membership count (#1675) --- .../group-selection.component.html | 6 ++---- .../group-selection.component.ts | 21 ++++++++++++++++--- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/modules/fbs-core/web/src/app/page-components/course-detail/group-selection/group-selection.component.html b/modules/fbs-core/web/src/app/page-components/course-detail/group-selection/group-selection.component.html index a306c0905..1266005b2 100644 --- a/modules/fbs-core/web/src/app/page-components/course-detail/group-selection/group-selection.component.html +++ b/modules/fbs-core/web/src/app/page-components/course-detail/group-selection/group-selection.component.html @@ -16,9 +16,8 @@ {{ "course.select-group" | i18nextEager }} {{ "course.group" | i18nextEager }} {{ "course.group-membership" | i18nextEager }} - - + {{ group.name }} - {{ group.membership }} - + {{ group.currentMembership }} / {{ group.membership }} +

diff --git a/modules/fbs-core/web/src/app/page-components/group-detail/group-detail.component.scss b/modules/fbs-core/web/src/app/page-components/group-detail/group-detail.component.scss index e69de29bb..cd64f722a 100644 --- a/modules/fbs-core/web/src/app/page-components/group-detail/group-detail.component.scss +++ b/modules/fbs-core/web/src/app/page-components/group-detail/group-detail.component.scss @@ -0,0 +1,9 @@ +@import "../../../colors"; + +.group-details { + .mat-icon { + color: $cPrimary; + margin-top: 100px; + transform: scale(10); + } +} diff --git a/modules/fbs-core/web/src/app/page-components/group-detail/group-detail.component.ts b/modules/fbs-core/web/src/app/page-components/group-detail/group-detail.component.ts index 86679f71b..4dcc186dd 100644 --- a/modules/fbs-core/web/src/app/page-components/group-detail/group-detail.component.ts +++ b/modules/fbs-core/web/src/app/page-components/group-detail/group-detail.component.ts @@ -1,4 +1,9 @@ import { Component, OnInit } from "@angular/core"; +import { Observable, of } from "rxjs"; +import { Group } from "../../model/group"; +import { GroupService } from "../../service/group.service"; +import { ActivatedRoute } from "@angular/router"; +import { TitlebarService } from "../../service/titlebar.service"; @Component({ selector: "app-group-detail", @@ -6,7 +11,31 @@ import { Component, OnInit } from "@angular/core"; styleUrls: ["./group-detail.component.scss"], }) export class GroupDetailComponent implements OnInit { - constructor() {} + constructor( + private groupService: GroupService, + private route: ActivatedRoute, + private titlebar: TitlebarService + ) {} + courseID: number; + groupID: number; + group$: Observable = of(); - ngOnInit(): void {} + ngOnInit(): void { + this.route.params.subscribe((param) => { + this.courseID = param.courseId; + this.groupID = param.id; + this.loadGroup(); + }); + } + + loadGroup(): void { + this.group$ = this.groupService.getGroup(this.courseID, this.groupID); + this.group$.subscribe((group) => { + this.titlebar.emitTitle(group.name); + }); + } + + navigateToKanban() { + window.location.href = "http://localhost:3000/"; + } } From 9f4263475efba97c8cad1fffac151fa6a47252bf Mon Sep 17 00:00:00 2001 From: Sophie Methe Date: Wed, 17 Jul 2024 15:13:47 +0200 Subject: [PATCH 10/21] Add menu-bar and functionalities to deregister and delete (#1675) --- .../group-detail/group-detail.component.html | 42 +++++- .../group-detail/group-detail.component.scss | 26 +++- .../group-detail/group-detail.component.ts | 137 +++++++++++++++++- modules/fbs-core/web/src/i18n/de.ts | 8 + modules/fbs-core/web/src/i18n/en.ts | 8 + 5 files changed, 208 insertions(+), 13 deletions(-) diff --git a/modules/fbs-core/web/src/app/page-components/group-detail/group-detail.component.html b/modules/fbs-core/web/src/app/page-components/group-detail/group-detail.component.html index 7ef80f4a1..167d06dee 100644 --- a/modules/fbs-core/web/src/app/page-components/group-detail/group-detail.component.html +++ b/modules/fbs-core/web/src/app/page-components/group-detail/group-detail.component.html @@ -1,5 +1,43 @@ -
- + + + + + + + +
diff --git a/modules/fbs-core/web/src/app/page-components/group-detail/group-detail.component.scss b/modules/fbs-core/web/src/app/page-components/group-detail/group-detail.component.scss index cd64f722a..f7ac8f97c 100644 --- a/modules/fbs-core/web/src/app/page-components/group-detail/group-detail.component.scss +++ b/modules/fbs-core/web/src/app/page-components/group-detail/group-detail.component.scss @@ -1,9 +1,23 @@ @import "../../../colors"; -.group-details { - .mat-icon { - color: $cPrimary; - margin-top: 100px; - transform: scale(10); - } +.container { + display: block; + margin-top: 16px; + margin-bottom: 30px; + padding: 16px; +} + +.spacer { + flex: 1 1 auto; +} + +app-menu-bar mat-icon { + cursor: pointer; + color: #333333; +} + +.kanban-button mat-icon { + color: $cPrimary; + transform: scale(10); + margin-top: 100px; } diff --git a/modules/fbs-core/web/src/app/page-components/group-detail/group-detail.component.ts b/modules/fbs-core/web/src/app/page-components/group-detail/group-detail.component.ts index 4dcc186dd..563f4b79b 100644 --- a/modules/fbs-core/web/src/app/page-components/group-detail/group-detail.component.ts +++ b/modules/fbs-core/web/src/app/page-components/group-detail/group-detail.component.ts @@ -1,9 +1,23 @@ -import { Component, OnInit } from "@angular/core"; +import { Component, Inject, OnInit } from "@angular/core"; import { Observable, of } from "rxjs"; -import { Group } from "../../model/group"; + import { GroupService } from "../../service/group.service"; -import { ActivatedRoute } from "@angular/router"; +import { ActivatedRoute, Router } from "@angular/router"; import { TitlebarService } from "../../service/titlebar.service"; +import { Roles } from "../../model/Roles"; +import { AuthService } from "../../service/auth.service"; +import { ConfirmDialogComponent } from "../../dialogs/confirm-dialog/confirm-dialog.component"; +import { MatDialog } from "@angular/material/dialog"; +import { GroupRegistrationService } from "../../service/group-registration.sevice"; +import { Group } from "../../model/Group"; +import { + I18NEXT_SERVICE, + I18NextPipe, + ITranslationService, +} from "angular-i18next"; +import { mergeMap } from "rxjs/operators"; +import { Course } from "../../model/Course"; +import { CourseService } from "../../service/course.service"; @Component({ selector: "app-group-detail", @@ -12,13 +26,22 @@ import { TitlebarService } from "../../service/titlebar.service"; }) export class GroupDetailComponent implements OnInit { constructor( + private auth: AuthService, + private dialog: MatDialog, private groupService: GroupService, + private groupRegistrationService: GroupRegistrationService, + private courseService: CourseService, private route: ActivatedRoute, - private titlebar: TitlebarService + private router: Router, + private titlebar: TitlebarService, + private i18NextPipe: I18NextPipe, + @Inject(I18NEXT_SERVICE) private i18NextService: ITranslationService ) {} courseID: number; groupID: number; group$: Observable = of(); + role: string = null; + course$: Observable = of(); ngOnInit(): void { this.route.params.subscribe((param) => { @@ -26,16 +49,120 @@ export class GroupDetailComponent implements OnInit { this.groupID = param.id; this.loadGroup(); }); + this.role = this.auth.getToken().courseRoles[this.courseID]; + } + + derigisterAllMembers(): void { + const title = this.i18NextPipe.transform("group.deregister.all") + "?"; + const message = this.i18NextPipe.transform("group.deregister.all.message"); + + this.dialog + .open(ConfirmDialogComponent, { + data: { + title: title, + message: message, + }, + }) + .afterClosed() + .subscribe((confirmed) => { + if (confirmed) { + this.groupRegistrationService + .deregisterAll(this.courseID, this.groupID) + .subscribe( + () => { + this.router.navigate(["/groups"]).then(); + }, + (error) => console.error(error) + ); + } + }); + } + + exitGroup(): void { + const title = this.i18NextPipe.transform("group.deregister") + "?"; + const message = this.i18NextPipe.transform("group.deregister.message"); + + this.dialog + .open(ConfirmDialogComponent, { + data: { + title: title, + message: message, + }, + }) + .afterClosed() + .subscribe((confirmed) => { + if (confirmed) { + this.groupRegistrationService + .deregisterGroup( + this.courseID, + this.groupID, + this.auth.getToken().id + ) + .subscribe( + () => { + this.router.navigate(["/groups"]).then(); + }, + (error) => console.error(error) + ); + } + }); + } + + deleteGroup() { + this.group$.subscribe((group) => { + const groupName = group.name; + const title = this.i18NextPipe.transform("group.delete") + "?"; + const message = + groupName + this.i18NextPipe.transform("group.delete.message"); + this.dialog + .open(ConfirmDialogComponent, { + data: { + title: title, + message: message, + }, + }) + .afterClosed() + .pipe( + mergeMap((confirmed) => { + if (confirmed) { + return this.groupService.deleteGroup(this.courseID, this.groupID); + } else { + return of(); + } + }) + ) + .subscribe( + () => { + this.router.navigate(["/groups"]).then(); + }, + (error) => console.error(error) + ); + }); } loadGroup(): void { this.group$ = this.groupService.getGroup(this.courseID, this.groupID); this.group$.subscribe((group) => { - this.titlebar.emitTitle(group.name); + this.course$ = this.courseService.getCourse(group.courseId); + this.course$.subscribe((course) => { + this.titlebar.emitTitle(`${group.name} - ${course.name}`); + }); }); } navigateToKanban() { window.location.href = "http://localhost:3000/"; } + + public isAuthorized(ignoreTutor: boolean = false) { + const token = this.auth.getToken(); + const courseRole = token.courseRoles[this.courseID]; + const globalRole = token.globalRole; + return ( + Roles.GlobalRole.isAdmin(globalRole) || + Roles.GlobalRole.isModerator(globalRole) || + Roles.CourseRole.isDocent(courseRole) || + (Roles.CourseRole.isTutor(courseRole) && !ignoreTutor) + ); + } } diff --git a/modules/fbs-core/web/src/i18n/de.ts b/modules/fbs-core/web/src/i18n/de.ts index 7408feee6..67596d4b4 100644 --- a/modules/fbs-core/web/src/i18n/de.ts +++ b/modules/fbs-core/web/src/i18n/de.ts @@ -377,4 +377,12 @@ export const germanTranslation = { "sql-playground.db-schema.tables.label.constraints": "Constraints:", "sql-playground.db-schema.tables.label.constraint-name": "Name", "sql-playground.db-schema.tables.label.constraint-clause": "Clause", + "group.deregister": "Gruppe verlassen", + "group.deregister.message": + "Wollen Sie diese Gruppe verlassen? Alle Ihre Abgaben könnten verloren gehen!", + "group.deregister.all": "Mitglieder entfernen", + "group.deregister.all.message": + "Wirklich alle Mitglieder entfernen? Ihre Abgaben könnten verloren gehen!", + "group.delete": "Gruppe löschen", + "group.delete.message": " wirklich löschen?", }; diff --git a/modules/fbs-core/web/src/i18n/en.ts b/modules/fbs-core/web/src/i18n/en.ts index bbc573c15..25e839412 100644 --- a/modules/fbs-core/web/src/i18n/en.ts +++ b/modules/fbs-core/web/src/i18n/en.ts @@ -370,4 +370,12 @@ export const englishTranslation = { "sql-playground.db-schema.tables.label.constraints": "Constraints:", "sql-playground.db-schema.tables.label.constraint-name": "Name", "sql-playground.db-schema.tables.label.constraint-clause": "Clause", + "group.deregister": "Leave Group", + "group.deregister.message": + "Do you want to leave this group? All your submissions might be lost!", + "group.deregister.all": "Deregister all members", + "group.deregister.all.message": + "Really deregister all members? Their submissions might be lost!", + "group.delete": "Delete Group", + "group.delete.message": ", really delete this group?", }; From d4877216f3934d93132bd44ad3e03784447b5680 Mon Sep 17 00:00:00 2001 From: Sophie Methe Date: Tue, 30 Jul 2024 11:01:16 +0200 Subject: [PATCH 11/21] Fixed settings and added visibility restrictions (#1675) --- .../new-group-dialog.component.html | 7 +- .../new-group-dialog.component.ts | 43 ++++- .../group-selection.component.html | 83 ++++++-- .../group-selection.component.scss | 9 +- .../group-selection.component.ts | 177 +++++++++++++++--- .../group-detail/group-detail.component.html | 14 +- .../group-detail/group-detail.component.scss | 3 +- .../group-detail/group-detail.component.ts | 92 +++------ modules/fbs-core/web/src/i18n/de.ts | 5 +- modules/fbs-core/web/src/i18n/en.ts | 6 +- 10 files changed, 317 insertions(+), 122 deletions(-) diff --git a/modules/fbs-core/web/src/app/dialogs/new-group-dialog/new-group-dialog.component.html b/modules/fbs-core/web/src/app/dialogs/new-group-dialog/new-group-dialog.component.html index 09a2a4319..226299c79 100644 --- a/modules/fbs-core/web/src/app/dialogs/new-group-dialog/new-group-dialog.component.html +++ b/modules/fbs-core/web/src/app/dialogs/new-group-dialog/new-group-dialog.component.html @@ -1,4 +1,7 @@
+
+ {{ "dialog.group.new.edit-group" | i18nextEager }} +
{{ "dialog.group.new.create-group" | i18nextEager }}
@@ -22,7 +25,7 @@ - + {{ "dialog.group.new.group-membership" | i18nextEager }} @@ -39,7 +42,7 @@ -

{{ "dialog.group.new.showGroup" | i18nextEager }}

diff --git a/modules/fbs-core/web/src/app/dialogs/new-group-dialog/new-group-dialog.component.ts b/modules/fbs-core/web/src/app/dialogs/new-group-dialog/new-group-dialog.component.ts index aadd20733..7c1a95122 100644 --- a/modules/fbs-core/web/src/app/dialogs/new-group-dialog/new-group-dialog.component.ts +++ b/modules/fbs-core/web/src/app/dialogs/new-group-dialog/new-group-dialog.component.ts @@ -13,14 +13,39 @@ export class NewGroupDialogComponent { name = new UntypedFormControl("", [Validators.required]); membership = new UntypedFormControl("", [Validators.required]); isVisible = true; - isUpdateDialog = false; + isUpdateDialog: boolean; + student: boolean; pending: boolean = false; constructor( private groupService: GroupService, public dialogRef: MatDialogRef, - @Inject(MAT_DIALOG_DATA) public data: { cid: number } - ) {} + @Inject(MAT_DIALOG_DATA) + public data: { + cid: number; + gid?: number; + student?: boolean; + isUpdateDialog: boolean; + } + ) { + this.isUpdateDialog = data.isUpdateDialog; + this.student = data.student; + /**if (data.student) {*/ + this.loadGroupData(); + /** }*/ + } + + loadGroupData() { + if (this.data.gid) { + this.groupService + .getGroup(this.data.cid, this.data.gid) + .subscribe((group) => { + this.name.setValue(group.name); + this.membership.setValue(group.membership); + this.isVisible = group.visible; + }); + } + } createGroup() { if (!this.isInputValid()) { @@ -37,7 +62,17 @@ export class NewGroupDialogComponent { this.pending = true; if (this.isUpdateDialog) { - // update DB + this.groupService + .updateGroup(this.data.cid, this.data.gid, group) + .subscribe( + () => { + this.dialogRef.close({ success: true }); + }, + (error) => { + console.log(error); + this.dialogRef.close({ success: false }); + } + ); } else { this.groupService.createGroup(this.data.cid, group).subscribe( () => { diff --git a/modules/fbs-core/web/src/app/page-components/course-detail/group-selection/group-selection.component.html b/modules/fbs-core/web/src/app/page-components/course-detail/group-selection/group-selection.component.html index 1266005b2..237adebd4 100644 --- a/modules/fbs-core/web/src/app/page-components/course-detail/group-selection/group-selection.component.html +++ b/modules/fbs-core/web/src/app/page-components/course-detail/group-selection/group-selection.component.html @@ -16,27 +16,78 @@ {{ "course.select-group" | i18nextEager }} {{ "course.group" | i18nextEager }} {{ "course.group-membership" | i18nextEager }} + {{ "course.settings" | i18nextEager }} - - - - - {{ group.name }} - {{ group.currentMembership }} / {{ group.membership }} - + + + + + + + {{ group.name }} + {{ group.currentMembership }} / {{ group.membership }} + + + + + + + + + +
diff --git a/modules/fbs-core/web/src/app/page-components/course-detail/group-selection/group-selection.component.scss b/modules/fbs-core/web/src/app/page-components/course-detail/group-selection/group-selection.component.scss index 0bafaf234..974674b14 100644 --- a/modules/fbs-core/web/src/app/page-components/course-detail/group-selection/group-selection.component.scss +++ b/modules/fbs-core/web/src/app/page-components/course-detail/group-selection/group-selection.component.scss @@ -17,6 +17,13 @@ td { text-align: left; } -button { +.invisible { + font-weight: bold; + color: #cccccc; + padding: 5px; +} + +.selection-button { + float: right; margin-top: 10px; } diff --git a/modules/fbs-core/web/src/app/page-components/course-detail/group-selection/group-selection.component.ts b/modules/fbs-core/web/src/app/page-components/course-detail/group-selection/group-selection.component.ts index 5ea07fa56..45fa27e68 100644 --- a/modules/fbs-core/web/src/app/page-components/course-detail/group-selection/group-selection.component.ts +++ b/modules/fbs-core/web/src/app/page-components/course-detail/group-selection/group-selection.component.ts @@ -1,7 +1,7 @@ -import { Component, Input, OnInit } from "@angular/core"; +import { Component, EventEmitter, Input, OnInit, Output } from "@angular/core"; import { ActivatedRoute } from "@angular/router"; import { AuthService } from "../../../service/auth.service"; -import { forkJoin, Observable } from "rxjs"; +import { forkJoin, Observable, of } from "rxjs"; import { Requirement } from "../../../model/Requirement"; import { MatDialog } from "@angular/material/dialog"; import { Roles } from "../../../model/Roles"; @@ -9,7 +9,9 @@ import { NewGroupDialogComponent } from "../../../dialogs/new-group-dialog/new-g import { Group } from "../../../model/Group"; import { GroupService } from "../../../service/group.service"; import { GroupRegistrationService } from "../../../service/group-registration.sevice"; -import { map, mergeMap } from "rxjs/operators"; +import { map, mergeMap, tap } from "rxjs/operators"; +import { ConfirmDialogComponent } from "../../../dialogs/confirm-dialog/confirm-dialog.component"; +import { I18NextPipe } from "angular-i18next"; @Component({ selector: "app-group-selection", @@ -18,18 +20,23 @@ import { map, mergeMap } from "rxjs/operators"; }) export class GroupSelectionComponent implements OnInit { @Input() requirements: Observable; + @Output() valueChosen: EventEmitter = new EventEmitter(); groups$: Observable<(Group & { currentMembership: number })[]>; - selectedGroup: Group; + preselectedGroup: Group; courseId: number; editGroups: boolean = false; + role: string = null; + student: boolean = false; + preselectionExists: boolean = false; constructor( private route: ActivatedRoute, private auth: AuthService, private dialog: MatDialog, private groupService: GroupService, - private groupRegistrationService: GroupRegistrationService + private groupRegistrationService: GroupRegistrationService, + private i18NextPipe: I18NextPipe ) {} ngOnInit(): void { @@ -37,6 +44,12 @@ export class GroupSelectionComponent implements OnInit { this.courseId = param.id; }); this.loadGroups(); + this.role = this.auth.getToken().courseRoles[this.courseId]; + this.choose(this.preselectedGroup); + } + + choose(value: Group): void { + this.valueChosen.emit(value); } public isAuthorized(ignoreTutor: boolean = false) { @@ -52,45 +65,65 @@ export class GroupSelectionComponent implements OnInit { } loadGroups(): void { - this.groups$ = this.groupService - .getGroupList(this.courseId) - .pipe( - mergeMap((groups: Group[]) => - forkJoin( - groups.map((group) => - this.groupRegistrationService - .getGroupMembership(this.courseId, group.id) - .pipe( - map((currentMembership) => ({ ...group, currentMembership })) - ) - ) + this.groups$ = this.groupService.getGroupList(this.courseId).pipe( + mergeMap((groups: Group[]) => + forkJoin( + groups.map((group) => + this.groupRegistrationService + .getGroupMembership(this.courseId, group.id) + .pipe( + map((currentMembership) => ({ ...group, currentMembership })) + ) ) + ).pipe( + tap((groups) => { + const savedGroupId = this.getSavedGroupId( + `selectedGroupId_${this.courseId}` + ); + if (savedGroupId) { + this.preselectedGroup = groups.find( + (group) => group.id === savedGroupId + ); + this.preselectionExists = !!this.preselectedGroup; + } else { + this.preselectedGroup = null; + this.preselectionExists = false; + } + }) ) - ); + ) + ); } createGroup() { this.dialog .open(NewGroupDialogComponent, { - data: { cid: this.courseId }, + data: { cid: this.courseId, isUpdateDialog: false }, height: "auto", width: "50%", }) .afterClosed() .subscribe( - () => { - this.loadGroups(); + (confirm) => { + if (confirm.success) { + this.loadGroups(); + } }, (error) => console.error(error) ); } joinGroup(): void { - if (this.selectedGroup) { + if (this.preselectedGroup) { + localStorage.setItem( + `selectedGroupId_${this.courseId}`, + this.preselectedGroup.id.toString() + ); + this.preselectionExists = true; this.groupRegistrationService .registerGroup( this.courseId, - this.selectedGroup.id, + this.preselectedGroup.id, this.auth.getToken().id ) .subscribe( @@ -101,4 +134,102 @@ export class GroupSelectionComponent implements OnInit { ); } else console.error("No group selected"); } + + removeGroup(): void { + if (this.preselectedGroup) { + localStorage.removeItem(`selectedGroupId_${this.courseId}`); + this.groupRegistrationService + .deregisterGroup( + this.courseId, + this.preselectedGroup.id, + this.auth.getToken().id + ) + .subscribe( + () => { + this.preselectedGroup = null; + this.preselectionExists = false; + this.loadGroups(); + }, + (error) => console.error(error) + ); + } else console.error("No group pre-selected"); + } + + getSavedGroupId(key: string): number { + let data = localStorage.getItem(key) || ""; + return data ? parseInt(data, 10) : null; + } + + updateGroup(group: Group) { + this.dialog + .open(NewGroupDialogComponent, { + width: "50%", + height: "auto", + data: { + cid: group.courseId, + gid: group.id, + student: this.student, + isUpdateDialog: true, + }, + }) + .afterClosed() + .subscribe( + (confirm) => { + if (confirm.success) { + this.loadGroups(); + } + }, + (error) => console.error(error) + ); + } + + derigisterAllMembers(group: Group): void { + const title = this.i18NextPipe.transform("group.deregister.all") + "?"; + const message = this.i18NextPipe.transform("group.deregister.all.message"); + + this.dialog + .open(ConfirmDialogComponent, { + data: { + title: title, + message: message, + }, + }) + .afterClosed() + .subscribe((confirmed) => { + if (confirmed) { + this.groupRegistrationService + .deregisterAll(group.courseId, group.id) + .subscribe( + () => { + this.loadGroups(); + }, + (error) => console.error(error) + ); + } + }); + } + + deleteGroup(group: Group): void { + const title = this.i18NextPipe.transform("group.delete") + "?"; + const message = + group.name + this.i18NextPipe.transform("group.delete.message"); + this.dialog + .open(ConfirmDialogComponent, { + data: { + title: title, + message: message, + }, + }) + .afterClosed() + .subscribe((confirmed) => { + if (confirmed) { + this.groupService.deleteGroup(group.courseId, group.id).subscribe( + () => { + this.loadGroups(); + }, + (error) => console.error(error) + ); + } + }); + } } diff --git a/modules/fbs-core/web/src/app/page-components/group-detail/group-detail.component.html b/modules/fbs-core/web/src/app/page-components/group-detail/group-detail.component.html index 167d06dee..4d9ebee04 100644 --- a/modules/fbs-core/web/src/app/page-components/group-detail/group-detail.component.html +++ b/modules/fbs-core/web/src/app/page-components/group-detail/group-detail.component.html @@ -20,23 +20,15 @@ settings - - - diff --git a/modules/fbs-core/web/src/app/page-components/group-detail/group-detail.component.scss b/modules/fbs-core/web/src/app/page-components/group-detail/group-detail.component.scss index f7ac8f97c..97fe9f0e8 100644 --- a/modules/fbs-core/web/src/app/page-components/group-detail/group-detail.component.scss +++ b/modules/fbs-core/web/src/app/page-components/group-detail/group-detail.component.scss @@ -19,5 +19,6 @@ app-menu-bar mat-icon { .kanban-button mat-icon { color: $cPrimary; transform: scale(10); - margin-top: 100px; + margin-top: 150px; + margin-left: 80px; } diff --git a/modules/fbs-core/web/src/app/page-components/group-detail/group-detail.component.ts b/modules/fbs-core/web/src/app/page-components/group-detail/group-detail.component.ts index 563f4b79b..70015c49f 100644 --- a/modules/fbs-core/web/src/app/page-components/group-detail/group-detail.component.ts +++ b/modules/fbs-core/web/src/app/page-components/group-detail/group-detail.component.ts @@ -10,14 +10,11 @@ import { ConfirmDialogComponent } from "../../dialogs/confirm-dialog/confirm-dia import { MatDialog } from "@angular/material/dialog"; import { GroupRegistrationService } from "../../service/group-registration.sevice"; import { Group } from "../../model/Group"; -import { - I18NEXT_SERVICE, - I18NextPipe, - ITranslationService, -} from "angular-i18next"; +import { I18NextPipe } from "angular-i18next"; import { mergeMap } from "rxjs/operators"; import { Course } from "../../model/Course"; import { CourseService } from "../../service/course.service"; +import { NewGroupDialogComponent } from "../../dialogs/new-group-dialog/new-group-dialog.component"; @Component({ selector: "app-group-detail", @@ -34,13 +31,13 @@ export class GroupDetailComponent implements OnInit { private route: ActivatedRoute, private router: Router, private titlebar: TitlebarService, - private i18NextPipe: I18NextPipe, - @Inject(I18NEXT_SERVICE) private i18NextService: ITranslationService + private i18NextPipe: I18NextPipe ) {} courseID: number; groupID: number; group$: Observable = of(); role: string = null; + student: boolean = true; course$: Observable = of(); ngOnInit(): void { @@ -52,30 +49,33 @@ export class GroupDetailComponent implements OnInit { this.role = this.auth.getToken().courseRoles[this.courseID]; } - derigisterAllMembers(): void { - const title = this.i18NextPipe.transform("group.deregister.all") + "?"; - const message = this.i18NextPipe.transform("group.deregister.all.message"); - - this.dialog - .open(ConfirmDialogComponent, { - data: { - title: title, - message: message, - }, - }) - .afterClosed() - .subscribe((confirmed) => { - if (confirmed) { - this.groupRegistrationService - .deregisterAll(this.courseID, this.groupID) - .subscribe( - () => { - this.router.navigate(["/groups"]).then(); + updateGroup() { + this.groupService + .getGroup(this.courseID, this.groupID) + .pipe( + mergeMap((group) => + this.dialog + .open(NewGroupDialogComponent, { + width: "50%", + height: "auto", + data: { + cid: this.courseID, + gid: this.groupID, + student: this.student, + isUpdateDialog: true, }, - (error) => console.error(error) - ); - } - }); + }) + .afterClosed() + ) + ) + .subscribe( + (confirm) => { + if (confirm.success) { + this.loadGroup(); + } + }, + (error) => console.error(error) + ); } exitGroup(): void { @@ -108,38 +108,6 @@ export class GroupDetailComponent implements OnInit { }); } - deleteGroup() { - this.group$.subscribe((group) => { - const groupName = group.name; - const title = this.i18NextPipe.transform("group.delete") + "?"; - const message = - groupName + this.i18NextPipe.transform("group.delete.message"); - this.dialog - .open(ConfirmDialogComponent, { - data: { - title: title, - message: message, - }, - }) - .afterClosed() - .pipe( - mergeMap((confirmed) => { - if (confirmed) { - return this.groupService.deleteGroup(this.courseID, this.groupID); - } else { - return of(); - } - }) - ) - .subscribe( - () => { - this.router.navigate(["/groups"]).then(); - }, - (error) => console.error(error) - ); - }); - } - loadGroup(): void { this.group$ = this.groupService.getGroup(this.courseID, this.groupID); this.group$.subscribe((group) => { diff --git a/modules/fbs-core/web/src/i18n/de.ts b/modules/fbs-core/web/src/i18n/de.ts index 67596d4b4..b225458ec 100644 --- a/modules/fbs-core/web/src/i18n/de.ts +++ b/modules/fbs-core/web/src/i18n/de.ts @@ -33,6 +33,7 @@ export const germanTranslation = { "course.group": "Gruppe", "course.group-membership": "Mitgliederanzahl", "course.save-selected-group": "Wahl speichern", + "course.remove-selected-group": "Meine Wahl entfernen", "course.progress-title": "Mein Kurs Fortschritt", "course.progress-no-conditions": "Dieser Kurs hat keine Voraussetzungen", "course.progress-complete": "Du hast den Kurs erfolgreich abgeschlossen", @@ -288,6 +289,8 @@ export const germanTranslation = { "dialog.task.points.label.cancelButton": "Abbrechen", "dialog.task.points.label.saveButton": "Erstellen", "dialog.group.new.create-group": "Gruppe erstellen", + "dialog.group.new.edit-group": "Gruppe bearbeiten", + "dialog.group.new.edit-group-name": "Gruppenname bearbeiten", "dialog.group.new.group-name": "Gruppenname", "dialog.group.new.group-name-error": "Der Gruppenname ist erforderlich!", "dialog.group.new.group-membership": "Maximale Mitgliederanzahl", @@ -296,7 +299,7 @@ export const germanTranslation = { "dialog.group.new.showGroup": "Gruppe anzeigen", "dialog.group.new.hideGroup": "Gruppe verbergen", "dialog.group.new.cancel": "Abbrechen", - "dialog.group.new.create": "Erstellen", + "dialog.group.new.create": "Speichern", "task.detail.tooltip.backToCourse": "Zurück zum Kurs", "task.detail.tooltip.exportTask": "Aufgabe Exportieren", "task.detail.tooltip.configureCheck": "Überprüfung konfigurieren", diff --git a/modules/fbs-core/web/src/i18n/en.ts b/modules/fbs-core/web/src/i18n/en.ts index 25e839412..b42291a36 100644 --- a/modules/fbs-core/web/src/i18n/en.ts +++ b/modules/fbs-core/web/src/i18n/en.ts @@ -33,6 +33,7 @@ export const englishTranslation = { "course.group": "Group", "course.group-membership": "Number of members", "course.save-selected-group": "Save selection", + "course.remove-selected-group": "Remove my selection", "course.progress-title": "My Course Progress", "course.progress-no-conditions": "There are no conditions for this course", "course.progress-complete": "You have successfully completed the course", @@ -282,6 +283,8 @@ export const englishTranslation = { "dialog.task.points.label.cancelButton": "Cancel", "dialog.task.points.label.saveButton": "Create", "dialog.group.new.create-group": "Create group", + "dialog.group.new.edit-group": "Edit group", + "dialog.group.new.edit-group-name": "Edit group name", "dialog.group.new.group-name": "Group name", "dialog.group.new.group-name-error": "The group name is required!", "dialog.group.new.group-membership": "Maximum number of members", @@ -290,7 +293,7 @@ export const englishTranslation = { "dialog.group.new.showGroup": "Display group", "dialog.group.new.hideGroup": "Hide group", "dialog.group.new.cancel": "Cancel", - "dialog.group.new.create": "Create", + "dialog.group.new.create": "Save", "task.detail.tooltip.backToCourse": "Back to Course", "task.detail.tooltip.exportTask": "Export Task", "task.detail.tooltip.configureCheck": "Configure Checker", @@ -378,4 +381,5 @@ export const englishTranslation = { "Really deregister all members? Their submissions might be lost!", "group.delete": "Delete Group", "group.delete.message": ", really delete this group?", + "group.edit": "Edit Group", }; From 2ff3638a52767e2771a7f18fbe02e1c124b5f150 Mon Sep 17 00:00:00 2001 From: Jonas-Ian Kuche Date: Wed, 31 Jul 2024 09:27:17 +0200 Subject: [PATCH 12/21] chore(web): fix linting errors --- .../course-detail/group-selection/group-selection.component.ts | 2 +- .../app/page-components/group-detail/group-detail.component.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/fbs-core/web/src/app/page-components/course-detail/group-selection/group-selection.component.ts b/modules/fbs-core/web/src/app/page-components/course-detail/group-selection/group-selection.component.ts index 45fa27e68..27f4b730b 100644 --- a/modules/fbs-core/web/src/app/page-components/course-detail/group-selection/group-selection.component.ts +++ b/modules/fbs-core/web/src/app/page-components/course-detail/group-selection/group-selection.component.ts @@ -1,7 +1,7 @@ import { Component, EventEmitter, Input, OnInit, Output } from "@angular/core"; import { ActivatedRoute } from "@angular/router"; import { AuthService } from "../../../service/auth.service"; -import { forkJoin, Observable, of } from "rxjs"; +import { forkJoin, Observable } from "rxjs"; import { Requirement } from "../../../model/Requirement"; import { MatDialog } from "@angular/material/dialog"; import { Roles } from "../../../model/Roles"; diff --git a/modules/fbs-core/web/src/app/page-components/group-detail/group-detail.component.ts b/modules/fbs-core/web/src/app/page-components/group-detail/group-detail.component.ts index 70015c49f..997116cce 100644 --- a/modules/fbs-core/web/src/app/page-components/group-detail/group-detail.component.ts +++ b/modules/fbs-core/web/src/app/page-components/group-detail/group-detail.component.ts @@ -1,4 +1,4 @@ -import { Component, Inject, OnInit } from "@angular/core"; +import { Component, OnInit } from "@angular/core"; import { Observable, of } from "rxjs"; import { GroupService } from "../../service/group.service"; From 20edd4e222a38d56667d7bf66178eca251bb46a8 Mon Sep 17 00:00:00 2001 From: Sophie Methe Date: Sat, 17 Aug 2024 12:00:41 +0200 Subject: [PATCH 13/21] Fix linting errors and the update functionality (#1675) --- modules/fbs-core/web/src/app/app.module.ts | 2 ++ .../group-detail/group-detail.component.ts | 33 ++++++++----------- 2 files changed, 15 insertions(+), 20 deletions(-) diff --git a/modules/fbs-core/web/src/app/app.module.ts b/modules/fbs-core/web/src/app/app.module.ts index 11efa26f5..562592957 100644 --- a/modules/fbs-core/web/src/app/app.module.ts +++ b/modules/fbs-core/web/src/app/app.module.ts @@ -122,6 +122,7 @@ import { GroupSelectionComponent } from "./page-components/course-detail/group-s import { NewGroupDialogComponent } from "./dialogs/new-group-dialog/new-group-dialog.component"; import { GroupPreviewComponent } from "./page-components/group-preview/group-preview.component"; import { GroupDetailComponent } from "./page-components/group-detail/group-detail.component"; +import { GroupDeregisterDialogComponent } from "./dialogs/group-deregister-dialog/group-deregister-dialog.component"; @Injectable() export class ApiURIHttpInterceptor implements HttpInterceptor { @@ -228,6 +229,7 @@ export const httpInterceptorProviders = [ GroupPreviewComponent, GroupPreviewComponent, GroupDetailComponent, + GroupDeregisterDialogComponent, ], imports: [ BrowserModule, diff --git a/modules/fbs-core/web/src/app/page-components/group-detail/group-detail.component.ts b/modules/fbs-core/web/src/app/page-components/group-detail/group-detail.component.ts index 70015c49f..8b926c740 100644 --- a/modules/fbs-core/web/src/app/page-components/group-detail/group-detail.component.ts +++ b/modules/fbs-core/web/src/app/page-components/group-detail/group-detail.component.ts @@ -1,4 +1,4 @@ -import { Component, Inject, OnInit } from "@angular/core"; +import { Component, OnInit } from "@angular/core"; import { Observable, of } from "rxjs"; import { GroupService } from "../../service/group.service"; @@ -11,7 +11,6 @@ import { MatDialog } from "@angular/material/dialog"; import { GroupRegistrationService } from "../../service/group-registration.sevice"; import { Group } from "../../model/Group"; import { I18NextPipe } from "angular-i18next"; -import { mergeMap } from "rxjs/operators"; import { Course } from "../../model/Course"; import { CourseService } from "../../service/course.service"; import { NewGroupDialogComponent } from "../../dialogs/new-group-dialog/new-group-dialog.component"; @@ -50,24 +49,18 @@ export class GroupDetailComponent implements OnInit { } updateGroup() { - this.groupService - .getGroup(this.courseID, this.groupID) - .pipe( - mergeMap((group) => - this.dialog - .open(NewGroupDialogComponent, { - width: "50%", - height: "auto", - data: { - cid: this.courseID, - gid: this.groupID, - student: this.student, - isUpdateDialog: true, - }, - }) - .afterClosed() - ) - ) + this.dialog + .open(NewGroupDialogComponent, { + width: "50%", + height: "auto", + data: { + cid: this.courseID, + gid: this.groupID, + student: this.student, + isUpdateDialog: true, + }, + }) + .afterClosed() .subscribe( (confirm) => { if (confirm.success) { From 3057599485c99c6f87d405161088661836284a18 Mon Sep 17 00:00:00 2001 From: Sophie Methe Date: Sat, 17 Aug 2024 12:26:15 +0200 Subject: [PATCH 14/21] Refactor Course API to save group registration settings (#1675) --- .../22_group_selection_field_added.sql | 8 ++++++ .../ii/fbs/controller/CourseController.scala | 28 +++++++++++++++++++ .../scala/de/thm/ii/fbs/model/Course.scala | 4 ++- .../services/persistence/CourseService.scala | 12 ++++++++ 4 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 modules/fbs-core/api/src/main/resources/migrations/22_group_selection_field_added.sql diff --git a/modules/fbs-core/api/src/main/resources/migrations/22_group_selection_field_added.sql b/modules/fbs-core/api/src/main/resources/migrations/22_group_selection_field_added.sql new file mode 100644 index 000000000..ab3523c6e --- /dev/null +++ b/modules/fbs-core/api/src/main/resources/migrations/22_group_selection_field_added.sql @@ -0,0 +1,8 @@ +BEGIN; + +alter table `fbs`.`course` + ADD `group_selection` BOOLEAN NULL DEFAULT NULL; + +INSERT INTO migration (number) VALUES (22); + +COMMIT; \ No newline at end of file diff --git a/modules/fbs-core/api/src/main/scala/de/thm/ii/fbs/controller/CourseController.scala b/modules/fbs-core/api/src/main/scala/de/thm/ii/fbs/controller/CourseController.scala index a54dcfc31..9316381c3 100644 --- a/modules/fbs-core/api/src/main/scala/de/thm/ii/fbs/controller/CourseController.scala +++ b/modules/fbs-core/api/src/main/scala/de/thm/ii/fbs/controller/CourseController.scala @@ -164,4 +164,32 @@ class CourseController { case _ => throw new ForbiddenException() } } + + + /** + * Update only the group selection of a course + * + * @param cid Course id + * @param req http request + * @param res http response + * @param body Request Body + */ + @PutMapping(value = Array("/{cid}/groupSelection")) + def updateGroupSelection(@PathVariable("cid") cid: Integer, req: HttpServletRequest, res: HttpServletResponse, + @RequestBody body: JsonNode): Unit = { + val user = authService.authorize(req, res) + val someCourseRole = courseRegistrationService.getParticipants(cid).find(_.user.id == user.id).map(_.role) + + (user.globalRole, someCourseRole) match { + case (GlobalRole.ADMIN | GlobalRole.MODERATOR, _) | (_, Some(CourseRole.DOCENT)) => + ( + body.retrive("groupSelection").asBool(), + ) match { + case (Some(groupSelection)) => + courseService.updateGroupSelection(cid, groupSelection) + case _ => throw new BadRequestException("Malformed Request Body") + } + case _ => throw new ForbiddenException() + } + } } diff --git a/modules/fbs-core/api/src/main/scala/de/thm/ii/fbs/model/Course.scala b/modules/fbs-core/api/src/main/scala/de/thm/ii/fbs/model/Course.scala index ad6c8f37e..b2a7cf3f6 100644 --- a/modules/fbs-core/api/src/main/scala/de/thm/ii/fbs/model/Course.scala +++ b/modules/fbs-core/api/src/main/scala/de/thm/ii/fbs/model/Course.scala @@ -7,5 +7,7 @@ package de.thm.ii.fbs.model * @param description The description of this course * @param visible The visibility of the course, false = invisible * @param id The id of the course, if 0, then none was assigned. + * @param groupSelection Whether registration for groups is possible */ -case class Course(name: String, description: String = "", visible: Boolean = true, id: Int = 0, semesterId: Option[Int] = None) +case class Course(name: String, description: String = "", visible: Boolean = true, id: Int = 0, semesterId: Option[Int] = None, + groupSelection: Option[Boolean] = None) diff --git a/modules/fbs-core/api/src/main/scala/de/thm/ii/fbs/services/persistence/CourseService.scala b/modules/fbs-core/api/src/main/scala/de/thm/ii/fbs/services/persistence/CourseService.scala index c37955b29..1cbc9dca9 100644 --- a/modules/fbs-core/api/src/main/scala/de/thm/ii/fbs/services/persistence/CourseService.scala +++ b/modules/fbs-core/api/src/main/scala/de/thm/ii/fbs/services/persistence/CourseService.scala @@ -100,4 +100,16 @@ class CourseService { Some(tmp) } } + + /** + * Update only the group selection of a course + * + * @param cid The course id + * @param groupSelection The new group selection status + * @return True if successful + */ + def updateGroupSelection(cid: Int, groupSelection: Boolean): Boolean = { + 1 == DB.update("UPDATE course SET group_selection = ? WHERE course_id = ?", + groupSelection, cid) + } } From a69ab949a587de806c5cd64ef9bcfdde4eb454a9 Mon Sep 17 00:00:00 2001 From: Sophie Methe Date: Sat, 17 Aug 2024 12:29:52 +0200 Subject: [PATCH 15/21] Update course model and service (#1675) --- modules/fbs-core/web/src/app/model/Course.ts | 1 + .../fbs-core/web/src/app/service/course.service.ts | 13 +++++++++++++ 2 files changed, 14 insertions(+) diff --git a/modules/fbs-core/web/src/app/model/Course.ts b/modules/fbs-core/web/src/app/model/Course.ts index d36ec4309..2f90ed4ba 100644 --- a/modules/fbs-core/web/src/app/model/Course.ts +++ b/modules/fbs-core/web/src/app/model/Course.ts @@ -4,4 +4,5 @@ export interface Course { description?: string; visible?: boolean; semesterId?: number; + groupSelection?: boolean; } diff --git a/modules/fbs-core/web/src/app/service/course.service.ts b/modules/fbs-core/web/src/app/service/course.service.ts index 2faf82664..9dbf4c152 100644 --- a/modules/fbs-core/web/src/app/service/course.service.ts +++ b/modules/fbs-core/web/src/app/service/course.service.ts @@ -51,5 +51,18 @@ export class CourseService { return this.http.delete(`/api/v1/courses/${cid}`); } + /** + * Update only the group selection of a course + * @param cid Course id + * @param groupSelection The group selection status to update + */ + updateGroupSelection(cid: number, groupSelection: boolean): Observable { + const requestBody = { groupSelection: groupSelection }; + return this.http.put( + `/api/v1/courses/${cid}/groupSelection`, + requestBody + ); + } + // TODO: export a course as zip format } From 4fd66ca5676d42bd832946afc67433619dd9303d Mon Sep 17 00:00:00 2001 From: Sophie Methe Date: Sat, 17 Aug 2024 12:32:22 +0200 Subject: [PATCH 16/21] Add dialog to register and deregister groupmebers (#1675) --- .../group-deregister-dialog.component.html | 40 +++++++++ .../group-deregister-dialog.component.scss | 7 ++ .../group-deregister-dialog.component.ts | 83 +++++++++++++++++++ 3 files changed, 130 insertions(+) create mode 100644 modules/fbs-core/web/src/app/dialogs/group-deregister-dialog/group-deregister-dialog.component.html create mode 100644 modules/fbs-core/web/src/app/dialogs/group-deregister-dialog/group-deregister-dialog.component.scss create mode 100644 modules/fbs-core/web/src/app/dialogs/group-deregister-dialog/group-deregister-dialog.component.ts diff --git a/modules/fbs-core/web/src/app/dialogs/group-deregister-dialog/group-deregister-dialog.component.html b/modules/fbs-core/web/src/app/dialogs/group-deregister-dialog/group-deregister-dialog.component.html new file mode 100644 index 000000000..2c03c5fb8 --- /dev/null +++ b/modules/fbs-core/web/src/app/dialogs/group-deregister-dialog/group-deregister-dialog.component.html @@ -0,0 +1,40 @@ +
+
+ {{ groupName }} - {{ "group.add.member" | i18nextEager }} +
+
+ {{ groupName }} - {{ "group.deregister.member" | i18nextEager }} +
+ + + + {{ member.user.prename }} {{ member.user.surname }} + + + + + + + + + + +
diff --git a/modules/fbs-core/web/src/app/dialogs/group-deregister-dialog/group-deregister-dialog.component.scss b/modules/fbs-core/web/src/app/dialogs/group-deregister-dialog/group-deregister-dialog.component.scss new file mode 100644 index 000000000..3d0c1f346 --- /dev/null +++ b/modules/fbs-core/web/src/app/dialogs/group-deregister-dialog/group-deregister-dialog.component.scss @@ -0,0 +1,7 @@ +.spacer { + flex: 1 1 auto; +} +.actions { + display: flex; + justify-content: flex-end; +} diff --git a/modules/fbs-core/web/src/app/dialogs/group-deregister-dialog/group-deregister-dialog.component.ts b/modules/fbs-core/web/src/app/dialogs/group-deregister-dialog/group-deregister-dialog.component.ts new file mode 100644 index 000000000..ff6756b5d --- /dev/null +++ b/modules/fbs-core/web/src/app/dialogs/group-deregister-dialog/group-deregister-dialog.component.ts @@ -0,0 +1,83 @@ +import { Component, Inject, OnInit } from "@angular/core"; +import { Observable } from "rxjs"; +import { GroupRegistrationService } from "../../service/group-registration.sevice"; +import { MAT_DIALOG_DATA, MatDialogRef } from "@angular/material/dialog"; +import { Participant } from "../../model/Participant"; +import { Group } from "../../model/Group"; +import { GroupService } from "../../service/group.service"; +import { CourseRegistrationService } from "../../service/course-registration.service"; + +@Component({ + selector: "app-group-deregister-dialog", + templateUrl: "./group-deregister-dialog.component.html", + styleUrls: ["./group-deregister-dialog.component.scss"], +}) +export class GroupDeregisterDialogComponent implements OnInit { + members$: Observable; + group$: Observable; + groupName: string; + + constructor( + private groupRegistrationService: GroupRegistrationService, + private courseRegistrationService: CourseRegistrationService, + private groupService: GroupService, + public dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) + public data: { + cid: number; + gid: number; + adding: boolean; + } + ) {} + + ngOnInit(): void { + this.loadMembers(); + this.group$ = this.groupService.getGroup(this.data.cid, this.data.gid); + this.group$.subscribe((group: Group) => { + this.groupName = group.name; + }); + } + + loadMembers(): void { + if (this.data.adding) { + this.members$ = this.courseRegistrationService.getCourseParticipants( + this.data.cid + ); + } else { + this.members$ = this.groupRegistrationService.getGroupParticipants( + this.data.cid, + this.data.gid + ); + } + } + + removeMember(member: Participant): void { + this.groupRegistrationService + .deregisterGroup(this.data.cid, this.data.gid, member.user.id) + .subscribe( + () => { + this.loadMembers(); + }, + (error) => { + console.error(error); + } + ); + } + + addMember(member: Participant): void { + this.groupRegistrationService + .registerGroup(this.data.cid, this.data.gid, member.user.id) + .subscribe( + () => { + this.loadMembers(); + }, + (error) => { + console.error(error); + } + ); + } + + closeDialog() { + this.dialogRef.close(); + } +} From bd03d7fe802d72ac8179a6471ad1c7570988f964 Mon Sep 17 00:00:00 2001 From: Sophie Methe Date: Sat, 17 Aug 2024 12:41:29 +0200 Subject: [PATCH 17/21] Update group-selection component and i18n files (#1675) --- .../group-selection.component.html | 71 ++++++--- .../group-selection.component.scss | 16 +- .../group-selection.component.ts | 138 +++++++++++++----- modules/fbs-core/web/src/i18n/de.ts | 6 + modules/fbs-core/web/src/i18n/en.ts | 6 + 5 files changed, 184 insertions(+), 53 deletions(-) diff --git a/modules/fbs-core/web/src/app/page-components/course-detail/group-selection/group-selection.component.html b/modules/fbs-core/web/src/app/page-components/course-detail/group-selection/group-selection.component.html index 237adebd4..4b8c8676a 100644 --- a/modules/fbs-core/web/src/app/page-components/course-detail/group-selection/group-selection.component.html +++ b/modules/fbs-core/web/src/app/page-components/course-detail/group-selection/group-selection.component.html @@ -31,6 +31,7 @@ [value]="group" [(ngModel)]="preselectedGroup" (ngModelChange)="choose($event)" + [class.disabled-style]="!selectionIsOpen" /> {{ group.name }} @@ -53,6 +54,14 @@ edit {{ "dialog.group.new.edit-group" | i18nextEager }} + + +
+ + +
diff --git a/modules/fbs-core/web/src/app/page-components/course-detail/group-selection/group-selection.component.scss b/modules/fbs-core/web/src/app/page-components/course-detail/group-selection/group-selection.component.scss index 974674b14..819caeb6a 100644 --- a/modules/fbs-core/web/src/app/page-components/course-detail/group-selection/group-selection.component.scss +++ b/modules/fbs-core/web/src/app/page-components/course-detail/group-selection/group-selection.component.scss @@ -23,7 +23,19 @@ td { padding: 5px; } -.selection-button { - float: right; +.button-container { + display: flex; + flex-direction: column; + align-items: flex-end; margin-top: 10px; } + +.selection-button { + margin-bottom: 10px; +} + + +.disabled-style { + pointer-events: none; + opacity: 0.5; +} diff --git a/modules/fbs-core/web/src/app/page-components/course-detail/group-selection/group-selection.component.ts b/modules/fbs-core/web/src/app/page-components/course-detail/group-selection/group-selection.component.ts index 27f4b730b..e0cdac88e 100644 --- a/modules/fbs-core/web/src/app/page-components/course-detail/group-selection/group-selection.component.ts +++ b/modules/fbs-core/web/src/app/page-components/course-detail/group-selection/group-selection.component.ts @@ -12,6 +12,9 @@ import { GroupRegistrationService } from "../../../service/group-registration.se import { map, mergeMap, tap } from "rxjs/operators"; import { ConfirmDialogComponent } from "../../../dialogs/confirm-dialog/confirm-dialog.component"; import { I18NextPipe } from "angular-i18next"; +import { GroupDeregisterDialogComponent } from "../../../dialogs/group-deregister-dialog/group-deregister-dialog.component"; +import { Participant } from "../../../model/Participant"; +import { CourseService } from "../../../service/course.service"; @Component({ selector: "app-group-selection", @@ -22,18 +25,25 @@ export class GroupSelectionComponent implements OnInit { @Input() requirements: Observable; @Output() valueChosen: EventEmitter = new EventEmitter(); - groups$: Observable<(Group & { currentMembership: number })[]>; - preselectedGroup: Group; + groups$: Observable< + (Group & { currentMembership: number } & { participants: Participant[] })[] + >; + preselectedGroup: Group & { currentMembership: number } & { + participants: Participant[]; + }; courseId: number; editGroups: boolean = false; role: string = null; student: boolean = false; preselectionExists: boolean = false; + selectionIsOpen: boolean; + userId: number; constructor( private route: ActivatedRoute, private auth: AuthService, private dialog: MatDialog, + private courseService: CourseService, private groupService: GroupService, private groupRegistrationService: GroupRegistrationService, private i18NextPipe: I18NextPipe @@ -43,15 +53,33 @@ export class GroupSelectionComponent implements OnInit { this.route.params.subscribe((param) => { this.courseId = param.id; }); + this.userId = this.auth.getToken().id; this.loadGroups(); this.role = this.auth.getToken().courseRoles[this.courseId]; this.choose(this.preselectedGroup); + this.getSelectionPossibility(); } choose(value: Group): void { this.valueChosen.emit(value); } + getSelectionPossibility(): void { + this.courseService + .getCourse(this.courseId) + .pipe( + map((course) => { + const selection = course.groupSelection; + if (selection) { + this.selectionIsOpen = selection; + } else { + this.selectionIsOpen = false; + } + }) + ) + .subscribe(); + } + public isAuthorized(ignoreTutor: boolean = false) { const token = this.auth.getToken(); const courseRole = token.courseRoles[this.courseId]; @@ -69,29 +97,35 @@ export class GroupSelectionComponent implements OnInit { mergeMap((groups: Group[]) => forkJoin( groups.map((group) => - this.groupRegistrationService - .getGroupMembership(this.courseId, group.id) - .pipe( - map((currentMembership) => ({ ...group, currentMembership })) - ) + forkJoin({ + currentMembership: + this.groupRegistrationService.getGroupMembership( + this.courseId, + group.id + ), + participants: this.groupRegistrationService.getGroupParticipants( + this.courseId, + group.id + ), + }).pipe( + map(({ currentMembership, participants }) => ({ + ...group, + currentMembership, + participants, + })) + ) ) - ).pipe( - tap((groups) => { - const savedGroupId = this.getSavedGroupId( - `selectedGroupId_${this.courseId}` - ); - if (savedGroupId) { - this.preselectedGroup = groups.find( - (group) => group.id === savedGroupId - ); - this.preselectionExists = !!this.preselectedGroup; - } else { - this.preselectedGroup = null; - this.preselectionExists = false; - } - }) ) - ) + ), + tap((groups) => { + const groupWithPreselection = groups.find((group) => + group.participants.some( + (participant) => participant.user.id === this.userId + ) + ); + this.preselectionExists = !!groupWithPreselection; + this.preselectedGroup = groupWithPreselection || null; + }) ); } @@ -113,13 +147,15 @@ export class GroupSelectionComponent implements OnInit { ); } + manageSelection() { + this.courseService + .updateGroupSelection(this.courseId, !this.selectionIsOpen) + .subscribe(); + this.getSelectionPossibility(); + } + joinGroup(): void { if (this.preselectedGroup) { - localStorage.setItem( - `selectedGroupId_${this.courseId}`, - this.preselectedGroup.id.toString() - ); - this.preselectionExists = true; this.groupRegistrationService .registerGroup( this.courseId, @@ -137,7 +173,6 @@ export class GroupSelectionComponent implements OnInit { removeGroup(): void { if (this.preselectedGroup) { - localStorage.removeItem(`selectedGroupId_${this.courseId}`); this.groupRegistrationService .deregisterGroup( this.courseId, @@ -155,11 +190,6 @@ export class GroupSelectionComponent implements OnInit { } else console.error("No group pre-selected"); } - getSavedGroupId(key: string): number { - let data = localStorage.getItem(key) || ""; - return data ? parseInt(data, 10) : null; - } - updateGroup(group: Group) { this.dialog .open(NewGroupDialogComponent, { @@ -183,6 +213,26 @@ export class GroupSelectionComponent implements OnInit { ); } + deregisterMember(group: Group): void { + this.dialog + .open(GroupDeregisterDialogComponent, { + width: "25%", + height: "auto", + data: { + cid: group.courseId, + gid: group.id, + adding: false, + }, + }) + .afterClosed() + .subscribe( + () => { + this.loadGroups(); + }, + (error) => console.error(error) + ); + } + derigisterAllMembers(group: Group): void { const title = this.i18NextPipe.transform("group.deregister.all") + "?"; const message = this.i18NextPipe.transform("group.deregister.all.message"); @@ -209,6 +259,26 @@ export class GroupSelectionComponent implements OnInit { }); } + addMember(group: Group): void { + this.dialog + .open(GroupDeregisterDialogComponent, { + width: "25%", + height: "auto", + data: { + cid: group.courseId, + gid: group.id, + adding: true, + }, + }) + .afterClosed() + .subscribe( + () => { + this.loadGroups(); + }, + (error) => console.error(error) + ); + } + deleteGroup(group: Group): void { const title = this.i18NextPipe.transform("group.delete") + "?"; const message = diff --git a/modules/fbs-core/web/src/i18n/de.ts b/modules/fbs-core/web/src/i18n/de.ts index b225458ec..ed9965f95 100644 --- a/modules/fbs-core/web/src/i18n/de.ts +++ b/modules/fbs-core/web/src/i18n/de.ts @@ -34,6 +34,8 @@ export const germanTranslation = { "course.group-membership": "Mitgliederanzahl", "course.save-selected-group": "Wahl speichern", "course.remove-selected-group": "Meine Wahl entfernen", + "course.open.selection": "Einwahl öffnen", + "course.hide.selection": "Einwahl schließen", "course.progress-title": "Mein Kurs Fortschritt", "course.progress-no-conditions": "Dieser Kurs hat keine Voraussetzungen", "course.progress-complete": "Du hast den Kurs erfolgreich abgeschlossen", @@ -300,6 +302,8 @@ export const germanTranslation = { "dialog.group.new.hideGroup": "Gruppe verbergen", "dialog.group.new.cancel": "Abbrechen", "dialog.group.new.create": "Speichern", + "dialog.group.deregister.remove": "Mitglied entfernen", + "dialog.group.deregister.add": "Mitglied hinzufügen", "task.detail.tooltip.backToCourse": "Zurück zum Kurs", "task.detail.tooltip.exportTask": "Aufgabe Exportieren", "task.detail.tooltip.configureCheck": "Überprüfung konfigurieren", @@ -383,9 +387,11 @@ export const germanTranslation = { "group.deregister": "Gruppe verlassen", "group.deregister.message": "Wollen Sie diese Gruppe verlassen? Alle Ihre Abgaben könnten verloren gehen!", + "group.deregister.member": "Mitglied entfernen", "group.deregister.all": "Mitglieder entfernen", "group.deregister.all.message": "Wirklich alle Mitglieder entfernen? Ihre Abgaben könnten verloren gehen!", + "group.add.member": "Mitglied hinzufügen", "group.delete": "Gruppe löschen", "group.delete.message": " wirklich löschen?", }; diff --git a/modules/fbs-core/web/src/i18n/en.ts b/modules/fbs-core/web/src/i18n/en.ts index b42291a36..0372ee0b6 100644 --- a/modules/fbs-core/web/src/i18n/en.ts +++ b/modules/fbs-core/web/src/i18n/en.ts @@ -34,6 +34,8 @@ export const englishTranslation = { "course.group-membership": "Number of members", "course.save-selected-group": "Save selection", "course.remove-selected-group": "Remove my selection", + "course.open.selection": "Open selection", + "course.hide.selection": "Close selection", "course.progress-title": "My Course Progress", "course.progress-no-conditions": "There are no conditions for this course", "course.progress-complete": "You have successfully completed the course", @@ -294,6 +296,8 @@ export const englishTranslation = { "dialog.group.new.hideGroup": "Hide group", "dialog.group.new.cancel": "Cancel", "dialog.group.new.create": "Save", + "dialog.group.deregister.remove": "Remove member", + "dialog.group.deregister.add": "Add member", "task.detail.tooltip.backToCourse": "Back to Course", "task.detail.tooltip.exportTask": "Export Task", "task.detail.tooltip.configureCheck": "Configure Checker", @@ -376,9 +380,11 @@ export const englishTranslation = { "group.deregister": "Leave Group", "group.deregister.message": "Do you want to leave this group? All your submissions might be lost!", + "group.deregister.member": "Deregister member", "group.deregister.all": "Deregister all members", "group.deregister.all.message": "Really deregister all members? Their submissions might be lost!", + "group.add.member": "Add member", "group.delete": "Delete Group", "group.delete.message": ", really delete this group?", "group.edit": "Edit Group", From 2faeed40c76696f00c30190e81cec4eddfc9a5f2 Mon Sep 17 00:00:00 2001 From: Sophie Methe Date: Sat, 24 Aug 2024 12:01:08 +0200 Subject: [PATCH 18/21] Fix linting error (#1675) --- .../main/scala/de/thm/ii/fbs/controller/CourseController.scala | 1 - 1 file changed, 1 deletion(-) diff --git a/modules/fbs-core/api/src/main/scala/de/thm/ii/fbs/controller/CourseController.scala b/modules/fbs-core/api/src/main/scala/de/thm/ii/fbs/controller/CourseController.scala index 9316381c3..61d402b42 100644 --- a/modules/fbs-core/api/src/main/scala/de/thm/ii/fbs/controller/CourseController.scala +++ b/modules/fbs-core/api/src/main/scala/de/thm/ii/fbs/controller/CourseController.scala @@ -165,7 +165,6 @@ class CourseController { } } - /** * Update only the group selection of a course * From 15a4bbd9abc324b9c5ce300a06d654c5fdc5ecb7 Mon Sep 17 00:00:00 2001 From: Sophie Methe Date: Sat, 24 Aug 2024 12:03:08 +0200 Subject: [PATCH 19/21] Update functionality to add groupmembers (#1675) --- .../group-deregister-dialog.component.ts | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/modules/fbs-core/web/src/app/dialogs/group-deregister-dialog/group-deregister-dialog.component.ts b/modules/fbs-core/web/src/app/dialogs/group-deregister-dialog/group-deregister-dialog.component.ts index ff6756b5d..7367c494d 100644 --- a/modules/fbs-core/web/src/app/dialogs/group-deregister-dialog/group-deregister-dialog.component.ts +++ b/modules/fbs-core/web/src/app/dialogs/group-deregister-dialog/group-deregister-dialog.component.ts @@ -1,11 +1,12 @@ import { Component, Inject, OnInit } from "@angular/core"; -import { Observable } from "rxjs"; +import { Observable, combineLatest } from "rxjs"; import { GroupRegistrationService } from "../../service/group-registration.sevice"; import { MAT_DIALOG_DATA, MatDialogRef } from "@angular/material/dialog"; import { Participant } from "../../model/Participant"; import { Group } from "../../model/Group"; import { GroupService } from "../../service/group.service"; import { CourseRegistrationService } from "../../service/course-registration.service"; +import { map } from "rxjs/operators"; @Component({ selector: "app-group-deregister-dialog", @@ -40,8 +41,22 @@ export class GroupDeregisterDialogComponent implements OnInit { loadMembers(): void { if (this.data.adding) { - this.members$ = this.courseRegistrationService.getCourseParticipants( - this.data.cid + const courseMembers$ = + this.courseRegistrationService.getCourseParticipants(this.data.cid); + const groupMembers$ = this.groupRegistrationService.getGroupParticipants( + this.data.cid, + this.data.gid + ); + + this.members$ = combineLatest([courseMembers$, groupMembers$]).pipe( + map(([courseMembers, groupMembers]) => + courseMembers.filter( + (courseMember) => + !groupMembers.some( + (groupMember) => groupMember.user.id === courseMember.user.id + ) + ) + ) ); } else { this.members$ = this.groupRegistrationService.getGroupParticipants( From 861d72a37b670b04957017169c25b9c419ba8c1d Mon Sep 17 00:00:00 2001 From: Sophie Methe Date: Thu, 29 Aug 2024 19:31:04 +0200 Subject: [PATCH 20/21] Fixed course service to save group registration settings (#1675) --- .../ii/fbs/services/persistence/CourseService.scala | 12 +++++++++++- .../group-selection/group-selection.component.ts | 8 +++++--- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/modules/fbs-core/api/src/main/scala/de/thm/ii/fbs/services/persistence/CourseService.scala b/modules/fbs-core/api/src/main/scala/de/thm/ii/fbs/services/persistence/CourseService.scala index 1cbc9dca9..ed2246a97 100644 --- a/modules/fbs-core/api/src/main/scala/de/thm/ii/fbs/services/persistence/CourseService.scala +++ b/modules/fbs-core/api/src/main/scala/de/thm/ii/fbs/services/persistence/CourseService.scala @@ -45,7 +45,7 @@ class CourseService { * @return The found course */ def find(id: Int): Option[Course] = DB.query( - "SELECT course_id, semester_id, name, description, visible FROM course WHERE course_id = ?", + "SELECT course_id, semester_id, name, description, visible, group_selection FROM course WHERE course_id = ?", (res, _) => parseResult(res), id).headOption /** @@ -86,6 +86,7 @@ class CourseService { private def parseResult(res: ResultSet): Course = Course( semesterId = maybeInt(res, "semester_id"), + groupSelection = maybeBoolean(res, "group_selection"), name = res.getString("name"), description = res.getString("description"), visible = res.getBoolean("visible"), @@ -101,6 +102,15 @@ class CourseService { } } + private def maybeBoolean(res: ResultSet, columnName: String): Option[Boolean] = { + val tmp = res.getBoolean(columnName) + if (res.wasNull()) { + null + } else { + Some(tmp) + } + } + /** * Update only the group selection of a course * diff --git a/modules/fbs-core/web/src/app/page-components/course-detail/group-selection/group-selection.component.ts b/modules/fbs-core/web/src/app/page-components/course-detail/group-selection/group-selection.component.ts index e0cdac88e..0a9b23b69 100644 --- a/modules/fbs-core/web/src/app/page-components/course-detail/group-selection/group-selection.component.ts +++ b/modules/fbs-core/web/src/app/page-components/course-detail/group-selection/group-selection.component.ts @@ -9,7 +9,7 @@ import { NewGroupDialogComponent } from "../../../dialogs/new-group-dialog/new-g import { Group } from "../../../model/Group"; import { GroupService } from "../../../service/group.service"; import { GroupRegistrationService } from "../../../service/group-registration.sevice"; -import { map, mergeMap, tap } from "rxjs/operators"; +import { delay, map, mergeMap, tap } from "rxjs/operators"; import { ConfirmDialogComponent } from "../../../dialogs/confirm-dialog/confirm-dialog.component"; import { I18NextPipe } from "angular-i18next"; import { GroupDeregisterDialogComponent } from "../../../dialogs/group-deregister-dialog/group-deregister-dialog.component"; @@ -150,8 +150,10 @@ export class GroupSelectionComponent implements OnInit { manageSelection() { this.courseService .updateGroupSelection(this.courseId, !this.selectionIsOpen) - .subscribe(); - this.getSelectionPossibility(); + .pipe( + delay(250) // To make sure database has been updated + ) + .subscribe(() => this.getSelectionPossibility()); } joinGroup(): void { From 6e030caecc3c5f71127eac7f858f7ee789ce994a Mon Sep 17 00:00:00 2001 From: Jonas-Ian Kuche Date: Tue, 17 Sep 2024 09:37:04 +0200 Subject: [PATCH 21/21] fix: add missing course select column --- .../de/thm/ii/fbs/services/persistence/CourseService.scala | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/fbs-core/api/src/main/scala/de/thm/ii/fbs/services/persistence/CourseService.scala b/modules/fbs-core/api/src/main/scala/de/thm/ii/fbs/services/persistence/CourseService.scala index ed2246a97..e722e7704 100644 --- a/modules/fbs-core/api/src/main/scala/de/thm/ii/fbs/services/persistence/CourseService.scala +++ b/modules/fbs-core/api/src/main/scala/de/thm/ii/fbs/services/persistence/CourseService.scala @@ -24,7 +24,7 @@ class CourseService { * @return List of courses */ def getAll(ignoreHidden: Boolean = true): List[Course] = DB.query( - "SELECT course_id, semester_id, name, description, visible FROM course" + (if (ignoreHidden) " WHERE visible = 1" else ""), + "SELECT course_id, group_selection, semester_id, name, description, visible FROM course" + (if (ignoreHidden) " WHERE visible = 1" else ""), (res, _) => parseResult(res)) /** @@ -35,7 +35,7 @@ class CourseService { * @return List of courses */ def findByPattern(pattern: String, ignoreHidden: Boolean = true): List[Course] = DB.query( - "SELECT course_id, semester_id, name, description, visible FROM course WHERE name like ?" + (if (ignoreHidden) " AND visible = 1" else ""), + "SELECT course_id, group_selection, semester_id, name, description, visible FROM course WHERE name like ?" + (if (ignoreHidden) " AND visible = 1" else ""), (res, _) => parseResult(res), "%" + pattern + "%") /** @@ -45,7 +45,7 @@ class CourseService { * @return The found course */ def find(id: Int): Option[Course] = DB.query( - "SELECT course_id, semester_id, name, description, visible, group_selection FROM course WHERE course_id = ?", + "SELECT course_id, group_selection, semester_id, name, description, visible, group_selection FROM course WHERE course_id = ?", (res, _) => parseResult(res), id).headOption /**