From e2bd263d32fabd15c186a4cbe3a486ec1eb6e50e Mon Sep 17 00:00:00 2001 From: EgeDoguKaya Date: Tue, 23 Jul 2024 02:21:26 +0300 Subject: [PATCH 01/74] Implement new course card design --- .../app/overview/course-card.component.html | 99 +++++++++++++++++-- src/main/webapp/app/overview/course-card.scss | 9 +- .../app/overview/courses.component.html | 8 +- .../shared/layouts/main/main.component.html | 2 +- .../shared/layouts/main/main.component.scss | 3 + .../app/shared/layouts/main/main.component.ts | 1 + 6 files changed, 108 insertions(+), 14 deletions(-) create mode 100644 src/main/webapp/app/shared/layouts/main/main.component.scss diff --git a/src/main/webapp/app/overview/course-card.component.html b/src/main/webapp/app/overview/course-card.component.html index 0ab968d60075..31501ed88b4f 100644 --- a/src/main/webapp/app/overview/course-card.component.html +++ b/src/main/webapp/app/overview/course-card.component.html @@ -1,6 +1,6 @@
- +
@@ -38,9 +38,92 @@
-
- +
+

{{ course.title }}

+ @if (nextRelevantExercise) { +
+
+
Next Unit
+ +
+ {{ nextRelevantExercise.title }} +
+
+ +
+ } @else { +
Next Unit
+
+ +
+ } +
@if (exerciseCount > 0 && (totalReachableScore > 0 || totalAbsoluteScore > 0)) { +
+
+
Score
+
+
{{ totalAbsoluteScore }} / {{ totalReachableScore }} Points ({{ totalRelativeScore }}%)
+
+
+
+ + + + : {{ model.value }} + + +
+
+ } @else { +
Score
+
+ +
+ } +
+ + + + + + +
- @if (nextRelevantExercise) { + +
diff --git a/src/main/webapp/app/overview/course-card.scss b/src/main/webapp/app/overview/course-card.scss index f7a475404894..f6e6dc3f461d 100644 --- a/src/main/webapp/app/overview/course-card.scss +++ b/src/main/webapp/app/overview/course-card.scss @@ -1,6 +1,7 @@ .card { min-height: 320px; transition: transform 0.2s linear; + border-radius: 20px; &:hover { transform: scale(1.012); @@ -10,7 +11,9 @@ .card-header { position: relative; padding: 0; - height: 80px; + height: 120px; + border-top-right-radius: 20px; + border-top-left-radius: 20px; display: flex; justify-content: center; align-items: center; @@ -70,6 +73,10 @@ } } + .card-body2 { + padding: 20px; + } + .card-body { position: relative; display: flex; diff --git a/src/main/webapp/app/overview/courses.component.html b/src/main/webapp/app/overview/courses.component.html index ebfd4deb06f6..6abbd9c2b91f 100644 --- a/src/main/webapp/app/overview/courses.component.html +++ b/src/main/webapp/app/overview/courses.component.html @@ -21,7 +21,7 @@

{{ nextRelevantExam.title }}

} -
+

@@ -38,7 +38,7 @@

@for (course of recentlyAccessedCourses; track course) { - + }

@if (regularCourses.length > 0) { @@ -55,9 +55,9 @@

+
@for (course of regularCourses; track course) { - + }
} diff --git a/src/main/webapp/app/shared/layouts/main/main.component.html b/src/main/webapp/app/shared/layouts/main/main.component.html index b0ecb8241133..7b647de03ca1 100644 --- a/src/main/webapp/app/shared/layouts/main/main.component.html +++ b/src/main/webapp/app/shared/layouts/main/main.component.html @@ -13,7 +13,7 @@

} @if (showSkeleton) { -
+
diff --git a/src/main/webapp/app/shared/layouts/main/main.component.scss b/src/main/webapp/app/shared/layouts/main/main.component.scss new file mode 100644 index 000000000000..889f23182cc2 --- /dev/null +++ b/src/main/webapp/app/shared/layouts/main/main.component.scss @@ -0,0 +1,3 @@ +.course-overview { + background-color: var(--bs-body-bg); +} \ No newline at end of file diff --git a/src/main/webapp/app/shared/layouts/main/main.component.ts b/src/main/webapp/app/shared/layouts/main/main.component.ts index 5d8368c7e90a..2158622226ea 100644 --- a/src/main/webapp/app/shared/layouts/main/main.component.ts +++ b/src/main/webapp/app/shared/layouts/main/main.component.ts @@ -12,6 +12,7 @@ import { ExamParticipationService } from 'app/exam/participate/exam-participatio @Component({ selector: 'jhi-main', templateUrl: './main.component.html', + styleUrls: ['./main.component.scss'], }) export class JhiMainComponent implements OnInit, OnDestroy { /** From 75efc714cbd3ba2b8bb128947a4c517ebc1baa9b Mon Sep 17 00:00:00 2001 From: EgeDoguKaya Date: Wed, 24 Jul 2024 17:19:21 +0300 Subject: [PATCH 02/74] Add management button to course cards --- .../app/overview/course-card.component.html | 68 ++++++++++--------- src/main/webapp/app/overview/course-card.scss | 42 ++++++++++-- .../app/overview/courses.component.html | 7 +- .../shared/layouts/main/main.component.html | 2 +- .../shared/layouts/main/main.component.scss | 3 - .../app/shared/layouts/main/main.component.ts | 1 - 6 files changed, 78 insertions(+), 45 deletions(-) delete mode 100644 src/main/webapp/app/shared/layouts/main/main.component.scss diff --git a/src/main/webapp/app/overview/course-card.component.html b/src/main/webapp/app/overview/course-card.component.html index 31501ed88b4f..c612ae7e9698 100644 --- a/src/main/webapp/app/overview/course-card.component.html +++ b/src/main/webapp/app/overview/course-card.component.html @@ -1,50 +1,51 @@
- +
-
-
+
+
@if (course.courseIcon) { }
-
-
+
+
{{ course.title }}
-
+
+
+
-

{{ course.title }}

+ +
@if (nextRelevantExercise) {
-
Next Unit
+
Next Unit
{{ nextRelevantExercise.title }}
@@ -52,9 +53,12 @@

{{ course.title }}

} @else { -
Next Unit
-
- +
+ +
Next Unit
+
+ +
}
@@ -86,16 +90,18 @@

{{ course.title }}

} @else { -
Score
-
- +
+ +
Score
+
+ +
}
- -
+

@if (nextRelevantExercise) {
-
Next Unit
-
- {{ nextRelevantExercise.title }} -
+ + {{ nextRelevantExercise.title }} +
- +
} @else {
-
Next Unit
@@ -91,7 +67,6 @@
} @else {
-
Score
@@ -102,90 +77,5 @@
- - - - -
- -
diff --git a/src/main/webapp/app/overview/course-card.scss b/src/main/webapp/app/overview/course-card.scss index 9a3877e3ca9f..5ec2ea5b7c06 100644 --- a/src/main/webapp/app/overview/course-card.scss +++ b/src/main/webapp/app/overview/course-card.scss @@ -26,43 +26,11 @@ background-color: color-mix(in srgb, var(--background-color-for-hover), transparent 15%) !important; } - .header-col { - height: 80px; - display: flex; - align-items: center; - } - - .image-col { - text-align: left; - } - - .title-col { - justify-content: center; - } - - .course-info-col { - justify-content: flex-end; - - .course-info-amounts { - display: flex; - flex-direction: column; - line-height: 20px; - white-space: nowrap; - z-index: 1; - - a { - color: white; - font-weight: unset; - } - } - } - .container { height: 80px; .row { height: 80px; - // justify-content: space-between; } } @@ -73,9 +41,9 @@ } } - .card-body2 { + .card-body { padding: 20px; - + .next-exercise-title { overflow: hidden; text-overflow: ellipsis; @@ -107,99 +75,14 @@ z-index: 2; position: relative; } - } - - .card-body { - position: relative; - display: flex; - justify-content: center; - align-items: center; - flex-direction: column; - - .chart-container { - display: flex; - align-items: center; - justify-content: center; - - .chart-text { - position: absolute; - } - - .chart-level { - z-index: 2; - cursor: pointer; - } - } - - .points { - display: block; - max-width: 11ch; - } - - canvas { - cursor: pointer; - z-index: 1; - width: 150px; - height: 150px; - } - - .no-statistics { - display: flex; - height: 200px; - justify-content: center; - flex-direction: column; - margin-bottom: 0; - } - } - - .card-footer { - position: relative; - padding: 0; - height: 50px; - .container { - height: 50px; - - .row { - height: 50px; - } - } - - .next-exercise-col { - display: flex; - justify-content: center; - align-items: center; - - .next-exercise-icon { - z-index: 1; - font-size: large; - } - - .next-exercise-title { - font-weight: bolder; - font-size: large; - margin-bottom: 0; - display: inline-block; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - max-width: 40ch; - padding-left: 15px; - } - - h6 { - margin-bottom: 0; - } - } - - .no-exercise { - display: flex; - height: 50px; - align-items: center; - padding-left: 30px; + .btn-exercise { + color: var(--bs-body-color); + z-index: 2; + position: relative; - h6 { - margin-bottom: 0; + &:hover { + text-decoration: none !important; } } } From 486d02771f4e4176db0a96669e23900588c6a99f Mon Sep 17 00:00:00 2001 From: EgeDoguKaya Date: Mon, 29 Jul 2024 13:21:15 +0300 Subject: [PATCH 05/74] Change background --- .../app/course/manage/course-management.service.ts | 11 +++++++++++ src/main/webapp/app/overview/courses.component.html | 6 +++--- src/main/webapp/app/overview/courses.component.ts | 2 ++ .../app/shared/layouts/main/main.component.html | 2 +- .../app/shared/layouts/main/main.component.scss | 4 ++++ .../webapp/app/shared/layouts/main/main.component.ts | 10 ++++++++++ 6 files changed, 31 insertions(+), 4 deletions(-) create mode 100644 src/main/webapp/app/shared/layouts/main/main.component.scss diff --git a/src/main/webapp/app/course/manage/course-management.service.ts b/src/main/webapp/app/course/manage/course-management.service.ts index 6b1a0a567412..ea11ccbece56 100644 --- a/src/main/webapp/app/course/manage/course-management.service.ts +++ b/src/main/webapp/app/course/manage/course-management.service.ts @@ -41,6 +41,9 @@ export class CourseManagementService { private fetchingCoursesForNotifications = false; + private courseOverviewSubject = new BehaviorSubject(false); + isCourseOverview$ = this.courseOverviewSubject.asObservable(); + constructor( private http: HttpClient, private courseStorageService: CourseStorageService, @@ -692,4 +695,12 @@ export class CourseManagementService { // Note: 0 is the default value in case the server returns something that does not make sense return this.http.get(`${this.resourceUrl}/${courseId}/allowed-complaints?teamMode=${teamMode}`) ?? 0; } + + setCourseOverviewBackground() { + this.courseOverviewSubject.next(true); + } + + resetCourseOverviewBackground() { + this.courseOverviewSubject.next(false); + } } diff --git a/src/main/webapp/app/overview/courses.component.html b/src/main/webapp/app/overview/courses.component.html index f70a1ac01d0f..b9bc9cbae5a7 100644 --- a/src/main/webapp/app/overview/courses.component.html +++ b/src/main/webapp/app/overview/courses.component.html @@ -3,7 +3,7 @@

{{ 'artemisApp.studentDashboard.examTitle' | artemisTranslate: { course: nextRelevantCourseForExam.title } }}

-
+
@@ -21,7 +21,7 @@

{{ nextRelevantExam.title }}

} -
+ @if (recentlyAccessedCourses.length > 0) {
diff --git a/src/main/webapp/app/overview/courses.component.ts b/src/main/webapp/app/overview/courses.component.ts index 73ce68512b8c..ce565224949a 100644 --- a/src/main/webapp/app/overview/courses.component.ts +++ b/src/main/webapp/app/overview/courses.component.ts @@ -49,6 +49,7 @@ export class CoursesComponent implements OnInit, OnDestroy { async ngOnInit() { this.loadAndFilterCourses(); (await this.teamService.teamAssignmentUpdates).subscribe(); + this.courseService.setCourseOverviewBackground(); } /** @@ -58,6 +59,7 @@ export class CoursesComponent implements OnInit, OnDestroy { if (this.quizExercisesChannels) { this.quizExercisesChannels.forEach((channel) => this.jhiWebsocketService.unsubscribe(channel)); } + this.courseService.resetCourseOverviewBackground(); } loadAndFilterCourses() { diff --git a/src/main/webapp/app/shared/layouts/main/main.component.html b/src/main/webapp/app/shared/layouts/main/main.component.html index a141532bd9ed..ecdc0fdca900 100644 --- a/src/main/webapp/app/shared/layouts/main/main.component.html +++ b/src/main/webapp/app/shared/layouts/main/main.component.html @@ -14,7 +14,7 @@ } @if (showSkeleton) {
-
+
diff --git a/src/main/webapp/app/shared/layouts/main/main.component.scss b/src/main/webapp/app/shared/layouts/main/main.component.scss new file mode 100644 index 000000000000..6064d158320f --- /dev/null +++ b/src/main/webapp/app/shared/layouts/main/main.component.scss @@ -0,0 +1,4 @@ +.course-overview { + background-color: var(--bs-body-bg); + padding-top: 0px; +} diff --git a/src/main/webapp/app/shared/layouts/main/main.component.ts b/src/main/webapp/app/shared/layouts/main/main.component.ts index 5d8368c7e90a..a8d41420dc32 100644 --- a/src/main/webapp/app/shared/layouts/main/main.component.ts +++ b/src/main/webapp/app/shared/layouts/main/main.component.ts @@ -8,10 +8,12 @@ import { DOCUMENT } from '@angular/common'; import { AnalyticsService } from 'app/core/posthog/analytics.service'; import { Subscription } from 'rxjs'; import { ExamParticipationService } from 'app/exam/participate/exam-participation.service'; +import { CourseManagementService } from '../../../course/manage/course-management.service'; @Component({ selector: 'jhi-main', templateUrl: './main.component.html', + styleUrls: ['./main.component.scss'], }) export class JhiMainComponent implements OnInit, OnDestroy { /** @@ -22,11 +24,13 @@ export class JhiMainComponent implements OnInit, OnDestroy { public showSkeleton = true; profileSubscription: Subscription; examStartedSubscription: Subscription; + courseOverviewSubscription: Subscription; testRunSubscription: Subscription; isProduction: boolean = true; isTestServer: boolean = false; isExamStarted: boolean = false; isTestRunExam: boolean = false; + isCourseOverview: boolean = false; constructor( private jhiLanguageHelper: JhiLanguageHelper, @@ -39,6 +43,7 @@ export class JhiMainComponent implements OnInit, OnDestroy { @Inject(DOCUMENT) private document: Document, private renderer: Renderer2, + private courseService: CourseManagementService, ) { this.setupErrorHandling().then(undefined); this.setupAnalytics().then(undefined); @@ -111,6 +116,10 @@ export class JhiMainComponent implements OnInit, OnDestroy { this.isTestRunExam = isStarted; }); + this.courseOverviewSubscription = this.courseService.isCourseOverview$.subscribe((isPresent) => { + this.isCourseOverview = isPresent; + }); + this.themeService.initialize(); } @@ -128,5 +137,6 @@ export class JhiMainComponent implements OnInit, OnDestroy { this.profileSubscription?.unsubscribe(); this.examStartedSubscription?.unsubscribe(); this.testRunSubscription?.unsubscribe(); + this.courseOverviewSubscription?.unsubscribe(); } } From 4be06417ee81cc5cb4600a332ff4131774e85ba9 Mon Sep 17 00:00:00 2001 From: EgeDoguKaya Date: Sat, 3 Aug 2024 02:08:28 +0300 Subject: [PATCH 06/74] Fix pie-chart and next exercise icon alignments --- .../webapp/app/overview/course-card.component.html | 11 +++++++---- src/main/webapp/app/overview/course-card.scss | 9 ++++++--- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/src/main/webapp/app/overview/course-card.component.html b/src/main/webapp/app/overview/course-card.component.html index 2d04fafbda2f..f20feda95b2f 100644 --- a/src/main/webapp/app/overview/course-card.component.html +++ b/src/main/webapp/app/overview/course-card.component.html @@ -20,14 +20,16 @@

@if (nextRelevantExercise) { -
+ } @else {
@@ -50,11 +52,12 @@
diff --git a/src/main/webapp/app/overview/course-card.scss b/src/main/webapp/app/overview/course-card.scss index 5ec2ea5b7c06..cb1c4e7916ac 100644 --- a/src/main/webapp/app/overview/course-card.scss +++ b/src/main/webapp/app/overview/course-card.scss @@ -49,10 +49,13 @@ text-overflow: ellipsis; white-space: nowrap; } + .icon-wrapper { + width: 65px; - .next-exercise-icon { - z-index: 1; - font-size: large; + .next-exercise-icon { + z-index: 1; + font-size: large; + } } .chart-container { From daf974f85b4b40c1781fd5c762ac4139f80bc76c Mon Sep 17 00:00:00 2001 From: EgeDoguKaya Date: Thu, 8 Aug 2024 16:38:22 +0200 Subject: [PATCH 07/74] improve course card paddings --- .../app/overview/course-card.component.html | 28 ++++++++++--------- .../app/overview/course-card.component.ts | 3 +- src/main/webapp/app/overview/course-card.scss | 7 +---- .../webapp/i18n/de/student-dashboard.json | 3 ++ .../webapp/i18n/en/student-dashboard.json | 3 ++ 5 files changed, 24 insertions(+), 20 deletions(-) diff --git a/src/main/webapp/app/overview/course-card.component.html b/src/main/webapp/app/overview/course-card.component.html index f20feda95b2f..d7b02453d617 100644 --- a/src/main/webapp/app/overview/course-card.component.html +++ b/src/main/webapp/app/overview/course-card.component.html @@ -22,28 +22,28 @@
@if (nextRelevantExercise) { } @else {
-
Next Unit
+
} -
+
@if (exerciseCount > 0 && (totalReachableScore > 0 || totalAbsoluteScore > 0)) {
-
Score
+
{{ totalAbsoluteScore }} / {{ totalReachableScore }} Points ({{ totalRelativeScore }}%)
@@ -52,9 +52,9 @@
} @else { -
-
Score
+
+
} -
- + @if (course.isAtLeastTutor || course.isAtLeastEditor || course.isAtLeastInstructor) { +
+
+ +
+ }
diff --git a/src/main/webapp/app/overview/course-card.component.ts b/src/main/webapp/app/overview/course-card.component.ts index 6fc36a26c528..0c7f49e0a6ae 100644 --- a/src/main/webapp/app/overview/course-card.component.ts +++ b/src/main/webapp/app/overview/course-card.component.ts @@ -1,6 +1,6 @@ import { Component, Input, OnChanges } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; -import { IconProp } from '@fortawesome/fontawesome-svg-core'; +import { IconProp, SizeProp } from '@fortawesome/fontawesome-svg-core'; import { Color, ScaleType } from '@swimlane/ngx-charts'; import { ARTEMIS_DEFAULT_COLOR } from 'app/app.constants'; import { Course } from 'app/entities/course.model'; @@ -40,6 +40,7 @@ export class CourseCardComponent implements OnChanges { totalAbsoluteScore: number; courseColor: string; + readonly iconSize: SizeProp = 'lg'; // ngx ngxDoughnutData: any[] = [ diff --git a/src/main/webapp/app/overview/course-card.scss b/src/main/webapp/app/overview/course-card.scss index cb1c4e7916ac..f8e36b373bde 100644 --- a/src/main/webapp/app/overview/course-card.scss +++ b/src/main/webapp/app/overview/course-card.scss @@ -50,7 +50,7 @@ white-space: nowrap; } .icon-wrapper { - width: 65px; + width: 90px; .next-exercise-icon { z-index: 1; @@ -69,11 +69,6 @@ } } - .no-score { - padding-top: 10px; - padding-bottom: 16px; - } - .btn-management { z-index: 2; position: relative; diff --git a/src/main/webapp/i18n/de/student-dashboard.json b/src/main/webapp/i18n/de/student-dashboard.json index 38820f89e17b..a4e6e0169905 100644 --- a/src/main/webapp/i18n/de/student-dashboard.json +++ b/src/main/webapp/i18n/de/student-dashboard.json @@ -12,6 +12,9 @@ "noStatistics": "Keine Statistik verfügbar", "cardNoExerciseLabel": "Keine Übung geplant", "cardExerciseLabel": "Nächste Übung:", + "cardNextUnit": "Nächste Einheit", + "cardScore": "Punktzahl", + "cardManageCourse": "Kurs Verwalten", "enroll": { "title": "Kurseinschreibung", "enrollSuccessful": "Einschreibung erfolgreich", diff --git a/src/main/webapp/i18n/en/student-dashboard.json b/src/main/webapp/i18n/en/student-dashboard.json index 586cdf840ce1..72d7005fe5f4 100644 --- a/src/main/webapp/i18n/en/student-dashboard.json +++ b/src/main/webapp/i18n/en/student-dashboard.json @@ -12,6 +12,9 @@ "noStatistics": "No statistics available", "cardNoExerciseLabel": "No exercise planned", "cardExerciseLabel": "Next Exercise:", + "cardNextUnit": "Next Unit", + "cardScore": "Score", + "cardManageCourse": "Manage Course", "enroll": { "title": "Course Enrollment", "enrollSuccessful": "Enrollment successful", From cda7f603bb6b285240ae1a92b4e271d1293fabd4 Mon Sep 17 00:00:00 2001 From: EgeDoguKaya Date: Thu, 15 Aug 2024 18:21:12 +0200 Subject: [PATCH 08/74] Add new design of course cards for bigger score icon --- .../app/overview/course-card.component.html | 122 +++++++++--------- .../app/overview/course-card.component.ts | 22 ++++ src/main/webapp/app/overview/course-card.scss | 27 ++-- .../app/overview/courses.component.html | 8 +- .../app/overview/courses.component.scss | 22 +++- 5 files changed, 115 insertions(+), 86 deletions(-) diff --git a/src/main/webapp/app/overview/course-card.component.html b/src/main/webapp/app/overview/course-card.component.html index d7b02453d617..005510ae98bd 100644 --- a/src/main/webapp/app/overview/course-card.component.html +++ b/src/main/webapp/app/overview/course-card.component.html @@ -16,71 +16,69 @@
-
- -
- @if (nextRelevantExercise) { - - } @else { -
-
-
- -
-
- } -
- @if (exerciseCount > 0 && (totalReachableScore > 0 || totalAbsoluteScore > 0)) { -
-
-
-
-
{{ totalAbsoluteScore }} / {{ totalReachableScore }} Points ({{ totalRelativeScore }}%)
+
+
+ +
+
+
+ @if (exerciseCount > 0 && (totalReachableScore > 0 || totalAbsoluteScore > 0)) { +
+
+
+
{{ totalAbsoluteScore }} / {{ totalReachableScore }} Points ({{ totalRelativeScore }}%)
+
+
+ } @else { +
+
+ +
+ } +
+
+ @if (nextRelevantExercise) { + + {{ nextRelevantExercise.title }} + + } @else { +
+ +
+ }
-
-
- - - - : {{ model.value }} - - + @if (exerciseCount > 0 && (totalReachableScore > 0 || totalAbsoluteScore > 0)) { +
+ + + + : {{ model.value }} + + +
+ }
- } @else { -
-
-
- + @if (course.isAtLeastTutor || course.isAtLeastEditor || course.isAtLeastInstructor) { + -
- } - @if (course.isAtLeastTutor || course.isAtLeastEditor || course.isAtLeastInstructor) { -
-
- -
- } + } +
diff --git a/src/main/webapp/app/overview/course-card.component.ts b/src/main/webapp/app/overview/course-card.component.ts index 0c7f49e0a6ae..3d81ad3dd345 100644 --- a/src/main/webapp/app/overview/course-card.component.ts +++ b/src/main/webapp/app/overview/course-card.component.ts @@ -14,6 +14,7 @@ import { GraphColors } from 'app/entities/statistics.model'; import { ScoresStorageService } from 'app/course/course-scores/scores-storage.service'; import { ScoreType } from 'app/shared/constants/score-type.constants'; import { CourseScores } from 'app/course/course-scores/course-scores'; +import { faArrowRight } from '@fortawesome/free-solid-svg-icons'; @Component({ selector: 'jhi-overview-course-card', @@ -42,6 +43,9 @@ export class CourseCardComponent implements OnChanges { courseColor: string; readonly iconSize: SizeProp = 'lg'; + // Icons + readonly faArrowRight = faArrowRight; + // ngx ngxDoughnutData: any[] = [ { name: 'achievedPointsLabel', value: 0 }, @@ -53,6 +57,7 @@ export class CourseCardComponent implements OnChanges { group: ScaleType.Ordinal, domain: [GraphColors.GREEN, GraphColors.RED], } as Color; + ngxSize = 140; constructor( private router: Router, @@ -88,6 +93,23 @@ export class CourseCardComponent implements OnChanges { this.ngxDoughnutData[0].value = this.totalAbsoluteScore; this.ngxDoughnutData[1].value = scoreNotReached; this.ngxDoughnutData = [...this.ngxDoughnutData]; + + const cardBody = document.querySelector('.card-body'); + + if (cardBody) { + const resizeObserver = new ResizeObserver((entries) => { + for (const entry of entries) { + const width = entry.contentRect.width; + + if (width < 260) { + this.ngxSize = 120; + } + } + }); + + // Start observing the cardBody element + resizeObserver.observe(cardBody); + } } this.lectureCount = this.course.numberOfLectures ?? this.course.lectures?.length ?? 0; diff --git a/src/main/webapp/app/overview/course-card.scss b/src/main/webapp/app/overview/course-card.scss index f8e36b373bde..1cf68be3efb0 100644 --- a/src/main/webapp/app/overview/course-card.scss +++ b/src/main/webapp/app/overview/course-card.scss @@ -1,5 +1,6 @@ .card { - min-height: 380px; + height: 320px; + min-width: 380px; transition: transform 0.2s linear; border-radius: 20px; @@ -42,20 +43,8 @@ } .card-body { - padding: 20px; - - .next-exercise-title { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - } - .icon-wrapper { - width: 90px; - - .next-exercise-icon { - z-index: 1; - font-size: large; - } + .information-box-wrapper { + height: 145px; } .chart-container { @@ -74,6 +63,14 @@ position: relative; } + .exercise-title { + display: inline-block; + max-width: 100%; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + .btn-exercise { color: var(--bs-body-color); z-index: 2; diff --git a/src/main/webapp/app/overview/courses.component.html b/src/main/webapp/app/overview/courses.component.html index b9bc9cbae5a7..57da0aa92476 100644 --- a/src/main/webapp/app/overview/courses.component.html +++ b/src/main/webapp/app/overview/courses.component.html @@ -35,9 +35,9 @@

-
+
@for (course of recentlyAccessedCourses; track course) { - + }
@if (regularCourses.length > 0) { @@ -54,9 +54,9 @@

+
@for (course of regularCourses; track course) { - + }
} diff --git a/src/main/webapp/app/overview/courses.component.scss b/src/main/webapp/app/overview/courses.component.scss index 93bd4a898edf..95a0db1309f9 100644 --- a/src/main/webapp/app/overview/courses.component.scss +++ b/src/main/webapp/app/overview/courses.component.scss @@ -18,20 +18,32 @@ width: 100% !important; } +@media screen and (min-width: 576px) { + .col-sm-12 { + width: 100% !important; + } +} + +@media screen and (min-width: 910px) { + .col-md-6 { + width: 50% !important; + } +} + @media screen and (min-width: 992px) { - .col-lg-6 { + .col-lg-4 { width: 50% !important; } } -@media screen and (min-width: 1200px) { - .col-xl-4 { +@media screen and (min-width: 1300px) { + .col-xl-3 { width: 33.33% !important; } } -@media screen and (min-width: 1900px) { - .col-xl-4 { +@media screen and (min-width: 1685px) { + .col-xl-3 { flex: 0 0 25; width: 25% !important; } From ac61dafde586279ebed1814d22a2b6689a3f9f9d Mon Sep 17 00:00:00 2001 From: EgeDoguKaya Date: Thu, 15 Aug 2024 20:27:28 +0200 Subject: [PATCH 09/74] Remove unused code --- .../app/overview/course-card.component.ts | 46 +------------------ 1 file changed, 1 insertion(+), 45 deletions(-) diff --git a/src/main/webapp/app/overview/course-card.component.ts b/src/main/webapp/app/overview/course-card.component.ts index 3d81ad3dd345..247dfd70090d 100644 --- a/src/main/webapp/app/overview/course-card.component.ts +++ b/src/main/webapp/app/overview/course-card.component.ts @@ -1,15 +1,12 @@ import { Component, Input, OnChanges } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; -import { IconProp, SizeProp } from '@fortawesome/fontawesome-svg-core'; import { Color, ScaleType } from '@swimlane/ngx-charts'; import { ARTEMIS_DEFAULT_COLOR } from 'app/app.constants'; import { Course } from 'app/entities/course.model'; -import { Exercise, getIcon, getIconTooltip } from 'app/entities/exercise.model'; +import { Exercise } from 'app/entities/exercise.model'; import { ExerciseService } from 'app/exercises/shared/exercise/exercise.service'; import { CachingStrategy } from 'app/shared/image/secured-image.component'; import { roundValueSpecifiedByCourseSettings } from 'app/shared/util/utils'; -import dayjs from 'dayjs/esm'; -import { getExerciseDueDate } from 'app/exercises/shared/exercise/exercise.utils'; import { GraphColors } from 'app/entities/statistics.model'; import { ScoresStorageService } from 'app/course/course-scores/scores-storage.service'; import { ScoreType } from 'app/shared/constants/score-type.constants'; @@ -29,19 +26,13 @@ export class CourseCardComponent implements OnChanges { CachingStrategy = CachingStrategy; nextRelevantExercise?: Exercise; - nextExerciseDueDate?: dayjs.Dayjs; - nextExerciseIcon: IconProp; - nextExerciseTooltip: string; exerciseCount = 0; - lectureCount = 0; - examCount = 0; totalRelativeScore: number; totalReachableScore: number; totalAbsoluteScore: number; courseColor: string; - readonly iconSize: SizeProp = 'lg'; // Icons readonly faArrowRight = faArrowRight; @@ -57,7 +48,6 @@ export class CourseCardComponent implements OnChanges { group: ScaleType.Ordinal, domain: [GraphColors.GREEN, GraphColors.RED], } as Color; - ngxSize = 140; constructor( private router: Router, @@ -76,9 +66,6 @@ export class CourseCardComponent implements OnChanges { if (nextExercises.length > 0 && nextExercises[0]) { this.nextRelevantExercise = nextExercises[0]; - this.updateNextDueDate(); - this.nextExerciseIcon = getIcon(this.nextRelevantExercise!.type); - this.nextExerciseTooltip = getIconTooltip(this.nextRelevantExercise!.type); } const totalScoresForCourse: CourseScores | undefined = this.scoresStorageService.getStoredTotalScores(this.course.id!); @@ -93,27 +80,8 @@ export class CourseCardComponent implements OnChanges { this.ngxDoughnutData[0].value = this.totalAbsoluteScore; this.ngxDoughnutData[1].value = scoreNotReached; this.ngxDoughnutData = [...this.ngxDoughnutData]; - - const cardBody = document.querySelector('.card-body'); - - if (cardBody) { - const resizeObserver = new ResizeObserver((entries) => { - for (const entry of entries) { - const width = entry.contentRect.width; - - if (width < 260) { - this.ngxSize = 120; - } - } - }); - - // Start observing the cardBody element - resizeObserver.observe(cardBody); - } } - this.lectureCount = this.course.numberOfLectures ?? this.course.lectures?.length ?? 0; - this.examCount = this.course.numberOfExams ?? this.course.exams?.length ?? 0; this.courseColor = this.course.color || this.ARTEMIS_DEFAULT_COLOR; } @@ -133,16 +101,4 @@ export class CourseCardComponent implements OnChanges { event.stopPropagation(); this.router.navigate(['courses', this.course.id, 'exams']); } - - private updateNextDueDate() { - let nextExerciseDueDate = undefined; - if (this.nextRelevantExercise) { - if (this.nextRelevantExercise.studentParticipations && this.nextRelevantExercise.studentParticipations.length > 0) { - nextExerciseDueDate = getExerciseDueDate(this.nextRelevantExercise, this.nextRelevantExercise.studentParticipations[0]); - } else { - nextExerciseDueDate = this.nextRelevantExercise.dueDate; - } - } - this.nextExerciseDueDate = nextExerciseDueDate; - } } From 42359ab5a1599c2a0cf16c0f575ca6e1cb19e996 Mon Sep 17 00:00:00 2001 From: EgeDoguKaya Date: Mon, 19 Aug 2024 11:36:33 +0200 Subject: [PATCH 10/74] Fix paddings in responsive layout --- src/main/webapp/app/overview/course-card.scss | 2 +- src/main/webapp/app/overview/courses.component.html | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/webapp/app/overview/course-card.scss b/src/main/webapp/app/overview/course-card.scss index 1cf68be3efb0..44b6d770c34f 100644 --- a/src/main/webapp/app/overview/course-card.scss +++ b/src/main/webapp/app/overview/course-card.scss @@ -1,6 +1,6 @@ .card { height: 320px; - min-width: 380px; + min-width: 360px; transition: transform 0.2s linear; border-radius: 20px; diff --git a/src/main/webapp/app/overview/courses.component.html b/src/main/webapp/app/overview/courses.component.html index 57da0aa92476..9530fd79af25 100644 --- a/src/main/webapp/app/overview/courses.component.html +++ b/src/main/webapp/app/overview/courses.component.html @@ -35,9 +35,9 @@

-
+
@for (course of recentlyAccessedCourses; track course) { - + }
@if (regularCourses.length > 0) { @@ -54,9 +54,9 @@

+
@for (course of regularCourses; track course) { - + }
} From 9f05883b8d2ef41be976c00ec4964b9b964fe67e Mon Sep 17 00:00:00 2001 From: EgeDoguKaya Date: Mon, 19 Aug 2024 11:59:06 +0200 Subject: [PATCH 11/74] improve responsiveness --- src/main/webapp/app/overview/courses.component.html | 8 ++++---- src/main/webapp/app/overview/courses.component.scss | 7 ++++++- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/main/webapp/app/overview/courses.component.html b/src/main/webapp/app/overview/courses.component.html index 9530fd79af25..2ac8e9c1d00c 100644 --- a/src/main/webapp/app/overview/courses.component.html +++ b/src/main/webapp/app/overview/courses.component.html @@ -35,9 +35,9 @@

-
+
@for (course of recentlyAccessedCourses; track course) { - + }
@if (regularCourses.length > 0) { @@ -54,9 +54,9 @@

+
@for (course of regularCourses; track course) { - + }
} diff --git a/src/main/webapp/app/overview/courses.component.scss b/src/main/webapp/app/overview/courses.component.scss index 95a0db1309f9..2d76b95a5cf6 100644 --- a/src/main/webapp/app/overview/courses.component.scss +++ b/src/main/webapp/app/overview/courses.component.scss @@ -24,10 +24,15 @@ } } -@media screen and (min-width: 910px) { +@media screen and (min-width: 830px) { .col-md-6 { width: 50% !important; } + + .px-sm-4 { + padding-right: 1rem !important; + padding-left: 1rem !important; + } } @media screen and (min-width: 992px) { From f0187f91d9df90d35bcbe3a16a7f6d08711e1ab7 Mon Sep 17 00:00:00 2001 From: EgeDoguKaya Date: Tue, 20 Aug 2024 19:44:44 +0200 Subject: [PATCH 12/74] Fix guided tour test --- src/main/webapp/app/guided-tour/tours/course-overview-tour.ts | 2 +- src/main/webapp/app/overview/course-card.component.html | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/webapp/app/guided-tour/tours/course-overview-tour.ts b/src/main/webapp/app/guided-tour/tours/course-overview-tour.ts index a76c25357da2..5e5a82fe2fd5 100644 --- a/src/main/webapp/app/guided-tour/tours/course-overview-tour.ts +++ b/src/main/webapp/app/guided-tour/tours/course-overview-tour.ts @@ -60,7 +60,7 @@ export const courseOverviewTour: GuidedTour = { orientation: Orientation.RIGHT, }), new TextTourStep({ - highlightSelector: '.guided-tour .card-footer', + highlightSelector: '.guided-tour .exercise-guided-tour', headlineTranslateKey: 'tour.courseOverview.courseFooter.headline', contentTranslateKey: 'tour.courseOverview.courseFooter.content', orientation: Orientation.TOPLEFT, diff --git a/src/main/webapp/app/overview/course-card.component.html b/src/main/webapp/app/overview/course-card.component.html index 005510ae98bd..486a6d3458e2 100644 --- a/src/main/webapp/app/overview/course-card.component.html +++ b/src/main/webapp/app/overview/course-card.component.html @@ -38,11 +38,11 @@

@if (nextRelevantExercise) { - + {{ nextRelevantExercise.title }} } @else { -
+
} From e4ff8affebb0dedd0a8201f51f98f140336bb0e3 Mon Sep 17 00:00:00 2001 From: EgeDoguKaya Date: Tue, 20 Aug 2024 20:52:00 +0200 Subject: [PATCH 13/74] Remove animation --- src/main/webapp/app/overview/course-card.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/webapp/app/overview/course-card.component.html b/src/main/webapp/app/overview/course-card.component.html index 486a6d3458e2..6f56cb1387db 100644 --- a/src/main/webapp/app/overview/course-card.component.html +++ b/src/main/webapp/app/overview/course-card.component.html @@ -58,7 +58,7 @@
[arcWidth]="0.3" [scheme]="ngxColor" [doughnut]="true" - [animations]="true" + [animations]="false" (select)="onSelect()" (click)="onSelect()" > From 9cca49e4cb90f2e2c056e4fc1634cdddaf2d0bd6 Mon Sep 17 00:00:00 2001 From: EgeDoguKaya Date: Wed, 21 Aug 2024 03:13:40 +0200 Subject: [PATCH 14/74] Implement archive feature --- .../www1/artemis/service/CourseService.java | 6 ++ .../www1/artemis/web/rest/CourseResource.java | 6 ++ .../manage/course-management.component.ts | 38 +--------- .../manage/course-management.service.ts | 50 +++++++++++++ .../course-archive.component.html | 28 +++++++ .../course-archive.component.scss | 40 ++++++++++ .../course-archive.component.ts | 73 +++++++++++++++++++ .../app/overview/courses-routing.module.ts | 10 +++ .../app/overview/courses.component.html | 1 + 9 files changed, 215 insertions(+), 37 deletions(-) create mode 100644 src/main/webapp/app/overview/course-archive/course-archive.component.html create mode 100644 src/main/webapp/app/overview/course-archive/course-archive.component.scss create mode 100644 src/main/webapp/app/overview/course-archive/course-archive.component.ts diff --git a/src/main/java/de/tum/in/www1/artemis/service/CourseService.java b/src/main/java/de/tum/in/www1/artemis/service/CourseService.java index 8015980acfe5..61de92f9328a 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/CourseService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/CourseService.java @@ -646,6 +646,12 @@ public List getAllCoursesForManagementOverview(boolean onlyActive) { return courseRepository.findAllCoursesByManagementGroupNames(userGroups); } + public List getAllCoursesForCourseArchive() { + var user = userRepository.getUserWithGroupsAndAuthorities(); + List courses = courseRepository.findAll(); + return courses.stream().filter(course -> authCheckService.isAtLeastStudentInCourse(course, user) && course.getSemester() != null).collect(Collectors.toList()); + } + /** * Get the active students for these particular exercise ids * diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/CourseResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/CourseResource.java index 0c2749ccb50d..6cf0a6ff00a8 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/CourseResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/CourseResource.java @@ -554,6 +554,12 @@ public ResponseEntity> getCoursesForManagementOverview(@RequestPara return ResponseEntity.ok(courseService.getAllCoursesForManagementOverview(onlyActive)); } + @GetMapping("courses/archive") + @EnforceAtLeastStudent + public ResponseEntity> getCoursesForArchive() { + return ResponseEntity.ok(courseService.getAllCoursesForCourseArchive()); + } + /** * GET /courses/{courseId}/for-enrollment : get a course by id if the course allows enrollment and is currently active. * diff --git a/src/main/webapp/app/course/manage/course-management.component.ts b/src/main/webapp/app/course/manage/course-management.component.ts index e6706bd2b2ee..8d9701e0bb99 100644 --- a/src/main/webapp/app/course/manage/course-management.component.ts +++ b/src/main/webapp/app/course/manage/course-management.component.ts @@ -83,7 +83,7 @@ export class CourseManagementComponent implements OnInit, OnDestroy, AfterViewIn this.courses = res.body!.sort((a, b) => (a.title ?? '').localeCompare(b.title ?? '')); this.courseForGuidedTour = this.guidedTourService.enableTourForCourseOverview(this.courses, tutorAssessmentTour, true); - this.courseSemesters = this.getUniqueSemesterNamesSorted(this.courses); + this.courseSemesters = this.courseManagementService.getUniqueSemesterNamesSorted(this.courses); this.sortCoursesIntoSemesters(); // First fetch important data like title for each exercise @@ -99,42 +99,6 @@ export class CourseManagementComponent implements OnInit, OnDestroy, AfterViewIn }); } - /** - * Sorts and returns the semesters by year descending - * WS is sorted above SS - * - * @param coursesWithSemesters the courses to sort the semesters of - * @return An array of sorted semester names - */ - private getUniqueSemesterNamesSorted(coursesWithSemesters: Course[]): string[] { - return ( - coursesWithSemesters - // Test courses get their own section later - .filter((course) => !course.testCourse) - .map((course) => course.semester ?? '') - // Filter down to unique values - .filter((course, index, courses) => courses.indexOf(course) === index) - .sort((semesterA, semesterB) => { - // Sort last if the semester is unset - if (semesterA === '') { - return 1; - } - if (semesterB === '') { - return -1; - } - - // Parse years in base 10 by extracting the two digits after the WS or SS prefix - const yearsCompared = parseInt(semesterB.slice(2, 4), 10) - parseInt(semesterA.slice(2, 4), 10); - if (yearsCompared !== 0) { - return yearsCompared; - } - - // If years are the same, sort WS over SS - return semesterA.slice(0, 2) === 'WS' ? -1 : 1; - }) - ); - } - /** * Sorts the courses into the coursesBySemester map. * Fills the semesterCollapsed map depending on if the semester should be expanded by default. diff --git a/src/main/webapp/app/course/manage/course-management.service.ts b/src/main/webapp/app/course/manage/course-management.service.ts index 6b1a0a567412..aa0fcc9aff36 100644 --- a/src/main/webapp/app/course/manage/course-management.service.ts +++ b/src/main/webapp/app/course/manage/course-management.service.ts @@ -340,6 +340,20 @@ export class CourseManagementService { ); } + getCoursesForArchive(req?: any): Observable> { + const options = createRequestOption(req); // This will handle query params if needed + return this.http.get(`${this.resourceUrl}/archive`, { params: options, observe: 'response' }).pipe( + tap((res: HttpResponse) => { + if (res.body) { + res.body.forEach((course) => { + // If you need to perform any action on each course + console.log(course); + }); + } + }), + ); + } + /** * returns the exercise details of the courses for the courses' management dashboard * @param onlyActive - if true, only active courses will be considered in the result @@ -692,4 +706,40 @@ export class CourseManagementService { // Note: 0 is the default value in case the server returns something that does not make sense return this.http.get(`${this.resourceUrl}/${courseId}/allowed-complaints?teamMode=${teamMode}`) ?? 0; } + + /** + * Sorts and returns the semesters by year descending + * WS is sorted above SS + * + * @param coursesWithSemesters the courses to sort the semesters of + * @return An array of sorted semester names + */ + getUniqueSemesterNamesSorted(coursesWithSemesters: Course[]): string[] { + return ( + coursesWithSemesters + // Test courses get their own section later + .filter((course) => !course.testCourse) + .map((course) => course.semester ?? '') + // Filter down to unique values + .filter((course, index, courses) => courses.indexOf(course) === index) + .sort((semesterA, semesterB) => { + // Sort last if the semester is unset + if (semesterA === '') { + return 1; + } + if (semesterB === '') { + return -1; + } + + // Parse years in base 10 by extracting the two digits after the WS or SS prefix + const yearsCompared = parseInt(semesterB.slice(2, 4), 10) - parseInt(semesterA.slice(2, 4), 10); + if (yearsCompared !== 0) { + return yearsCompared; + } + + // If years are the same, sort WS over SS + return semesterA.slice(0, 2) === 'WS' ? -1 : 1; + }) + ); + } } diff --git a/src/main/webapp/app/overview/course-archive/course-archive.component.html b/src/main/webapp/app/overview/course-archive/course-archive.component.html new file mode 100644 index 000000000000..45a954a721ca --- /dev/null +++ b/src/main/webapp/app/overview/course-archive/course-archive.component.html @@ -0,0 +1,28 @@ +

Course Archive

+@if (courses) { +
+ @for (semester of semesters; track semester) { +
+
+ + @if (semester !== '') { + {{ 'artemisApp.course.semester' | artemisTranslate }}: {{ semester }} + } +
+ @if (!semesterCollapsed[semester]) { +
+ @for (course of coursesBySemester[semester]; track course) { +
+
+ +

{{ course.title }}

+
+
+ } +
+ } + +
+ } +
+} diff --git a/src/main/webapp/app/overview/course-archive/course-archive.component.scss b/src/main/webapp/app/overview/course-archive/course-archive.component.scss new file mode 100644 index 000000000000..25a0f61af157 --- /dev/null +++ b/src/main/webapp/app/overview/course-archive/course-archive.component.scss @@ -0,0 +1,40 @@ +.course-table { + padding: 5px 0; + background-color: var(--overview-light-background-color); + border-radius: 3px; + transition: box-shadow 0.1s linear; +} + +.course-table-container { + .control-label { + cursor: pointer; + margin-bottom: 16px; + } + + .container { + display: inline-flex; + } + + .collapsed { + margin-top: 16px; + margin-left: -12px; + margin-right: -12px; + border-bottom: 1px solid var(--overview-light-border-color); + } +} + +.table-responsive { + overflow-x: hidden; +} + +.course-card { + background-color: var(--background-color-for-hover) !important; + transition: 0.15s; + filter: alpha(opacity = 100); + opacity: 1; + + &:hover { + transform: scale(1.012); + background-color: color-mix(in srgb, var(--background-color-for-hover), transparent 15%) !important; + } +} diff --git a/src/main/webapp/app/overview/course-archive/course-archive.component.ts b/src/main/webapp/app/overview/course-archive/course-archive.component.ts new file mode 100644 index 000000000000..0c5d7c2826d5 --- /dev/null +++ b/src/main/webapp/app/overview/course-archive/course-archive.component.ts @@ -0,0 +1,73 @@ +import { Component, OnDestroy, OnInit } from '@angular/core'; +import { ArtemisSharedModule } from 'app/shared/shared.module'; +import { ArtemisSharedComponentModule } from 'app/shared/components/shared-component.module'; +import { Course } from 'app/entities/course.model'; +import { CourseManagementService } from '../../course/manage/course-management.service'; +import { HttpErrorResponse, HttpResponse } from '@angular/common/http'; +import { AlertService } from 'app/core/util/alert.service'; +import { onError } from 'app/shared/util/global.utils'; +import { Subscription } from 'rxjs'; +import { faAngleDown, faAngleUp } from '@fortawesome/free-solid-svg-icons'; +import { sortCourses } from 'app/shared/util/course.util'; +import { ARTEMIS_DEFAULT_COLOR } from 'app/app.constants'; + +@Component({ + standalone: true, + selector: 'course-archive', + imports: [ArtemisSharedModule, ArtemisSharedComponentModule], + templateUrl: './course-archive.component.html', + styleUrls: ['./course-archive.component.scss'], +}) +export class CourseArchiveComponent implements OnInit, OnDestroy { + private archiveCourseSubscription: Subscription; + + courses: Course[]; + semesters: string[]; + semesterCollapsed: { [key: string]: boolean }; + coursesBySemester: { [key: string]: Course[] }; + courseColor: string; + + readonly ARTEMIS_DEFAULT_COLOR = ARTEMIS_DEFAULT_COLOR; + + //Icons + readonly faAngleDown = faAngleDown; + readonly faAngleUp = faAngleUp; + + constructor( + private courseService: CourseManagementService, + private alertService: AlertService, + ) {} + + ngOnInit(): void { + this.loadArchivedCourses(); + } + + /** + * Loads all courses that the student has been enrolled in + */ + loadArchivedCourses(): void { + this.archiveCourseSubscription = this.courseService.getCoursesForArchive().subscribe({ + next: (response: HttpResponse) => { + this.courses = response.body || []; + this.courses = sortCourses(this.courses); + this.semesters = this.courseService.getUniqueSemesterNamesSorted(this.courses); + this.mapCoursesIntoSemesters(); + }, + error: (error: HttpErrorResponse) => onError(this.alertService, error), + }); + } + + private mapCoursesIntoSemesters(): void { + this.semesterCollapsed = {}; + this.coursesBySemester = {}; + + for (const semester of this.semesters) { + this.semesterCollapsed[semester] = true; + this.coursesBySemester[semester] = this.courses.filter((course) => course.semester === semester); + } + } + + ngOnDestroy(): void { + this.archiveCourseSubscription.unsubscribe(); + } +} diff --git a/src/main/webapp/app/overview/courses-routing.module.ts b/src/main/webapp/app/overview/courses-routing.module.ts index 8b011de2206c..58006de4273b 100644 --- a/src/main/webapp/app/overview/courses-routing.module.ts +++ b/src/main/webapp/app/overview/courses-routing.module.ts @@ -12,6 +12,7 @@ import { CourseTutorialGroupDetailComponent } from './tutorial-group-details/cou import { ExamParticipationComponent } from 'app/exam/participate/exam-participation.component'; import { PendingChangesGuard } from 'app/shared/guard/pending-changes.guard'; import { CourseDashboardGuard } from 'app/overview/course-dashboard/course-dashboard-guard.service'; +import { CourseArchiveComponent } from './course-archive/course-archive.component'; const routes: Routes = [ { @@ -27,6 +28,15 @@ const routes: Routes = [ path: 'enroll', loadChildren: () => import('./course-registration/course-registration.module').then((m) => m.CourseRegistrationModule), }, + { + path: 'archive', + component: CourseArchiveComponent, + data: { + authorities: [Authority.USER], + pageTitle: 'Course Archive', + }, + canActivate: [UserRouteAccessService], + }, // /courses/:courseId/register is special, // because we won't have access to the course object before the user is registered, // so we need to load it outside the normal course routing diff --git a/src/main/webapp/app/overview/courses.component.html b/src/main/webapp/app/overview/courses.component.html index ebfd4deb06f6..d2c01a320d69 100644 --- a/src/main/webapp/app/overview/courses.component.html +++ b/src/main/webapp/app/overview/courses.component.html @@ -61,3 +61,4 @@

Course Archive From a824b2807a278d09306220f3cfeab73a81774e8e Mon Sep 17 00:00:00 2001 From: EgeDoguKaya Date: Wed, 21 Aug 2024 03:25:45 +0200 Subject: [PATCH 15/74] Integrate feedback --- .../webapp/app/overview/course-card.component.html | 12 ++++++++++-- src/main/webapp/i18n/de/student-dashboard.json | 4 ++-- src/main/webapp/i18n/en/student-dashboard.json | 4 ++-- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/main/webapp/app/overview/course-card.component.html b/src/main/webapp/app/overview/course-card.component.html index 6f56cb1387db..93b6ab55e3d6 100644 --- a/src/main/webapp/app/overview/course-card.component.html +++ b/src/main/webapp/app/overview/course-card.component.html @@ -26,7 +26,15 @@
-
{{ totalAbsoluteScore }} / {{ totalReachableScore }} Points ({{ totalRelativeScore }}%)
+ +
} @else { @@ -36,7 +44,7 @@

}
-
+
@if (nextRelevantExercise) { {{ nextRelevantExercise.title }} diff --git a/src/main/webapp/i18n/de/student-dashboard.json b/src/main/webapp/i18n/de/student-dashboard.json index a4e6e0169905..f77903bfa14b 100644 --- a/src/main/webapp/i18n/de/student-dashboard.json +++ b/src/main/webapp/i18n/de/student-dashboard.json @@ -11,8 +11,8 @@ "cardTitle": "Deine insgesamte Punktzahl:", "noStatistics": "Keine Statistik verfügbar", "cardNoExerciseLabel": "Keine Übung geplant", - "cardExerciseLabel": "Nächste Übung:", - "cardNextUnit": "Nächste Einheit", + "cardExerciseLabel": "Nächste Übung", + "points": "{{ totalAbsoluteScore }} / {{ totalReachableScore }} Punkte ({{ totalRelativeScore }}%)", "cardScore": "Punktzahl", "cardManageCourse": "Kurs Verwalten", "enroll": { diff --git a/src/main/webapp/i18n/en/student-dashboard.json b/src/main/webapp/i18n/en/student-dashboard.json index 72d7005fe5f4..f8802efd6e77 100644 --- a/src/main/webapp/i18n/en/student-dashboard.json +++ b/src/main/webapp/i18n/en/student-dashboard.json @@ -11,8 +11,8 @@ "cardTitle": "Your overall points:", "noStatistics": "No statistics available", "cardNoExerciseLabel": "No exercise planned", - "cardExerciseLabel": "Next Exercise:", - "cardNextUnit": "Next Unit", + "cardExerciseLabel": "Next Exercise", + "points": "{{ totalAbsoluteScore }} / {{ totalReachableScore }} Points ({{ totalRelativeScore }}%)", "cardScore": "Score", "cardManageCourse": "Manage Course", "enroll": { From 8d93712329ac5dfa87b5203268e7072e410c4b73 Mon Sep 17 00:00:00 2001 From: EgeDoguKaya Date: Wed, 21 Aug 2024 03:26:36 +0200 Subject: [PATCH 16/74] Remove comment --- src/main/webapp/app/overview/course-card.component.html | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/webapp/app/overview/course-card.component.html b/src/main/webapp/app/overview/course-card.component.html index 93b6ab55e3d6..be64f967da4b 100644 --- a/src/main/webapp/app/overview/course-card.component.html +++ b/src/main/webapp/app/overview/course-card.component.html @@ -26,7 +26,6 @@
-
Course Archive
-@if (courses) { -
- @for (semester of semesters; track semester) { -
-
- - @if (semester !== '') { - {{ 'artemisApp.course.semester' | artemisTranslate }}: {{ semester }} + +@if (courses) { +
+
+ @for (semester of semesters; track semester; let last = $last) { +
+
+ + @if (semester !== '') { + {{ 'artemisApp.course.semester' | artemisTranslate }}: {{ semester }} }
+ @if (!semesterCollapsed[semester]) { +
+ @for (course of coursesBySemester[semester] | searchFilter: ['title'] : searchCourseText; track course) { +
+
+ +

{{ course.title }}

+
+
+ } +
+ } +
+ @if (!last) { +
} - -
- } + } +
} diff --git a/src/main/webapp/app/overview/course-archive/course-archive.component.scss b/src/main/webapp/app/overview/course-archive/course-archive.component.scss index 25a0f61af157..0e059b7b5528 100644 --- a/src/main/webapp/app/overview/course-archive/course-archive.component.scss +++ b/src/main/webapp/app/overview/course-archive/course-archive.component.scss @@ -14,13 +14,6 @@ .container { display: inline-flex; } - - .collapsed { - margin-top: 16px; - margin-left: -12px; - margin-right: -12px; - border-bottom: 1px solid var(--overview-light-border-color); - } } .table-responsive { diff --git a/src/main/webapp/app/overview/course-archive/course-archive.component.ts b/src/main/webapp/app/overview/course-archive/course-archive.component.ts index 0c5d7c2826d5..1c9111410ace 100644 --- a/src/main/webapp/app/overview/course-archive/course-archive.component.ts +++ b/src/main/webapp/app/overview/course-archive/course-archive.component.ts @@ -7,9 +7,10 @@ import { HttpErrorResponse, HttpResponse } from '@angular/common/http'; import { AlertService } from 'app/core/util/alert.service'; import { onError } from 'app/shared/util/global.utils'; import { Subscription } from 'rxjs'; -import { faAngleDown, faAngleUp } from '@fortawesome/free-solid-svg-icons'; +import { faAngleDown, faAngleUp, faArrowDownAZ, faArrowUpAZ, faQuestionCircle } from '@fortawesome/free-solid-svg-icons'; import { sortCourses } from 'app/shared/util/course.util'; import { ARTEMIS_DEFAULT_COLOR } from 'app/app.constants'; +import { SizeProp } from '@fortawesome/fontawesome-svg-core'; @Component({ standalone: true, @@ -26,12 +27,18 @@ export class CourseArchiveComponent implements OnInit, OnDestroy { semesterCollapsed: { [key: string]: boolean }; coursesBySemester: { [key: string]: Course[] }; courseColor: string; + searchCourseText = ''; + isSortAscending = true; + iconSize: SizeProp = 'lg'; readonly ARTEMIS_DEFAULT_COLOR = ARTEMIS_DEFAULT_COLOR; //Icons readonly faAngleDown = faAngleDown; readonly faAngleUp = faAngleUp; + readonly faArrowDownAZ = faArrowDownAZ; + readonly faArrowUpAZ = faArrowUpAZ; + readonly faQuestionCircle = faQuestionCircle; constructor( private courseService: CourseManagementService, @@ -40,6 +47,7 @@ export class CourseArchiveComponent implements OnInit, OnDestroy { ngOnInit(): void { this.loadArchivedCourses(); + this.courseService.setCourseOverviewBackground(); } /** @@ -69,5 +77,27 @@ export class CourseArchiveComponent implements OnInit, OnDestroy { ngOnDestroy(): void { this.archiveCourseSubscription.unsubscribe(); + this.courseService.resetCourseOverviewBackground(); + } + + setSearchValue(searchValue: string): void { + this.searchCourseText = searchValue; + if (searchValue !== '') { + this.expandOrCollapseBasedOnSearchValue(); + } + } + + onSort(): void { + if (this.semesters) { + this.semesters.reverse(); + this.isSortAscending = !this.isSortAscending; + } + } + + expandOrCollapseBasedOnSearchValue(): void { + for (const semester of this.semesters) { + const hasMatchingCourse = this.coursesBySemester[semester].some((course) => course.title?.toLowerCase().includes(this.searchCourseText.toLowerCase())); + this.semesterCollapsed[semester] = !hasMatchingCourse; + } } } diff --git a/src/main/webapp/app/overview/courses-routing.module.ts b/src/main/webapp/app/overview/courses-routing.module.ts index 58006de4273b..7e4d9ef4bf56 100644 --- a/src/main/webapp/app/overview/courses-routing.module.ts +++ b/src/main/webapp/app/overview/courses-routing.module.ts @@ -33,7 +33,7 @@ const routes: Routes = [ component: CourseArchiveComponent, data: { authorities: [Authority.USER], - pageTitle: 'Course Archive', + pageTitle: 'overview.archive', }, canActivate: [UserRouteAccessService], }, diff --git a/src/main/webapp/app/overview/courses.component.html b/src/main/webapp/app/overview/courses.component.html index d2c01a320d69..bfc63ada73e6 100644 --- a/src/main/webapp/app/overview/courses.component.html +++ b/src/main/webapp/app/overview/courses.component.html @@ -61,4 +61,13 @@

Course Archive + +
+
+ {{ 'artemisApp.studentDashboard.oldCourses' | artemisTranslate }} + {{ 'artemisApp.studentDashboard.here' | artemisTranslate }} +
+
diff --git a/src/main/webapp/app/shared/layouts/main/main.component.html b/src/main/webapp/app/shared/layouts/main/main.component.html index 683aff8d541e..8853c5535f0d 100644 --- a/src/main/webapp/app/shared/layouts/main/main.component.html +++ b/src/main/webapp/app/shared/layouts/main/main.component.html @@ -14,7 +14,7 @@ } @if (showSkeleton) {
-
+
diff --git a/src/main/webapp/app/shared/layouts/main/main.component.scss b/src/main/webapp/app/shared/layouts/main/main.component.scss new file mode 100644 index 000000000000..669f15a198f7 --- /dev/null +++ b/src/main/webapp/app/shared/layouts/main/main.component.scss @@ -0,0 +1,6 @@ +.course-overview { + background-color: var(--bs-body-bg); + padding-top: 0; + padding-right: 0; + padding-left: 0; +} diff --git a/src/main/webapp/app/shared/layouts/main/main.component.ts b/src/main/webapp/app/shared/layouts/main/main.component.ts index 5d8368c7e90a..a8d41420dc32 100644 --- a/src/main/webapp/app/shared/layouts/main/main.component.ts +++ b/src/main/webapp/app/shared/layouts/main/main.component.ts @@ -8,10 +8,12 @@ import { DOCUMENT } from '@angular/common'; import { AnalyticsService } from 'app/core/posthog/analytics.service'; import { Subscription } from 'rxjs'; import { ExamParticipationService } from 'app/exam/participate/exam-participation.service'; +import { CourseManagementService } from '../../../course/manage/course-management.service'; @Component({ selector: 'jhi-main', templateUrl: './main.component.html', + styleUrls: ['./main.component.scss'], }) export class JhiMainComponent implements OnInit, OnDestroy { /** @@ -22,11 +24,13 @@ export class JhiMainComponent implements OnInit, OnDestroy { public showSkeleton = true; profileSubscription: Subscription; examStartedSubscription: Subscription; + courseOverviewSubscription: Subscription; testRunSubscription: Subscription; isProduction: boolean = true; isTestServer: boolean = false; isExamStarted: boolean = false; isTestRunExam: boolean = false; + isCourseOverview: boolean = false; constructor( private jhiLanguageHelper: JhiLanguageHelper, @@ -39,6 +43,7 @@ export class JhiMainComponent implements OnInit, OnDestroy { @Inject(DOCUMENT) private document: Document, private renderer: Renderer2, + private courseService: CourseManagementService, ) { this.setupErrorHandling().then(undefined); this.setupAnalytics().then(undefined); @@ -111,6 +116,10 @@ export class JhiMainComponent implements OnInit, OnDestroy { this.isTestRunExam = isStarted; }); + this.courseOverviewSubscription = this.courseService.isCourseOverview$.subscribe((isPresent) => { + this.isCourseOverview = isPresent; + }); + this.themeService.initialize(); } @@ -128,5 +137,6 @@ export class JhiMainComponent implements OnInit, OnDestroy { this.profileSubscription?.unsubscribe(); this.examStartedSubscription?.unsubscribe(); this.testRunSubscription?.unsubscribe(); + this.courseOverviewSubscription?.unsubscribe(); } } diff --git a/src/main/webapp/app/shared/layouts/navbar/navbar.component.ts b/src/main/webapp/app/shared/layouts/navbar/navbar.component.ts index b24b25a4b64d..fb206f726875 100644 --- a/src/main/webapp/app/shared/layouts/navbar/navbar.component.ts +++ b/src/main/webapp/app/shared/layouts/navbar/navbar.component.ts @@ -384,6 +384,7 @@ export class NavbarComponent implements OnInit, OnDestroy { live: 'artemisApp.submission.detail.title', courses: 'artemisApp.course.home.title', enroll: 'artemisApp.studentDashboard.enroll.title', + archive: 'artemisApp.course.archive.title', }; /** diff --git a/src/main/webapp/i18n/de/course.json b/src/main/webapp/i18n/de/course.json index 7bc9274e7461..0433ae60df88 100644 --- a/src/main/webapp/i18n/de/course.json +++ b/src/main/webapp/i18n/de/course.json @@ -15,6 +15,11 @@ "typeNameToConfirm": "Bitte gib den Namen des Kurses zur Bestätigung ein.", "icon": "Kursicon löschen" }, + "archive": { + "title": "Archiv", + "sort": "Sortieren", + "tip": "Das Archiv ermöglicht dir, all deine vergangenen Kurse, organisiert nach Semestern, anzusehen. Klicke auf ein Semester, um es zu erweitern und die Kurse zu sehen, in die du in diesem Zeitraum eingeschrieben warst." + }, "showActive": "Nur aktive Kurse anzeigen", "totalScore": "Gesamtergebnis:", "title": "Titel", diff --git a/src/main/webapp/i18n/de/global.json b/src/main/webapp/i18n/de/global.json index e687aaaf72fb..bf166eed1261 100644 --- a/src/main/webapp/i18n/de/global.json +++ b/src/main/webapp/i18n/de/global.json @@ -344,7 +344,8 @@ "tutorialGroups": "Übungsgruppen", "statistics": "Kursstatistiken", "exams": "Klausuren", - "communication": "Kommunikation" + "communication": "Kommunikation", + "archive": "Kursarchiv" }, "connectionStatus": { "connected": "Verbunden", diff --git a/src/main/webapp/i18n/de/student-dashboard.json b/src/main/webapp/i18n/de/student-dashboard.json index 38820f89e17b..476ff10fc111 100644 --- a/src/main/webapp/i18n/de/student-dashboard.json +++ b/src/main/webapp/i18n/de/student-dashboard.json @@ -12,6 +12,8 @@ "noStatistics": "Keine Statistik verfügbar", "cardNoExerciseLabel": "Keine Übung geplant", "cardExerciseLabel": "Nächste Übung:", + "oldCourses": "Suchst du nach alten Kursen? Klicke ", + "here": "hier", "enroll": { "title": "Kurseinschreibung", "enrollSuccessful": "Einschreibung erfolgreich", diff --git a/src/main/webapp/i18n/en/course.json b/src/main/webapp/i18n/en/course.json index e977ca0d3f3f..abbfd65252a0 100644 --- a/src/main/webapp/i18n/en/course.json +++ b/src/main/webapp/i18n/en/course.json @@ -15,6 +15,11 @@ "typeNameToConfirm": "Please type in the name of the course to confirm.", "icon": "Remove Icon" }, + "archive": { + "title": "Archive", + "sort": "Sort", + "tip": "The archive enables you to view all your past courses, organized by semester. Click on a semester to expand and see the courses you were enrolled in during that period." + }, "showActive": "Show only active courses", "totalScore": "Total Score:", "title": "Title", diff --git a/src/main/webapp/i18n/en/global.json b/src/main/webapp/i18n/en/global.json index 7e59d4c46f22..ec0aa4b718e4 100644 --- a/src/main/webapp/i18n/en/global.json +++ b/src/main/webapp/i18n/en/global.json @@ -346,7 +346,8 @@ "exercises": "Exercises", "statistics": "Course statistics", "exams": "Exams", - "communication": "Communication" + "communication": "Communication", + "archive": "Course Archive" }, "connectionStatus": { "connected": "Connected", diff --git a/src/main/webapp/i18n/en/student-dashboard.json b/src/main/webapp/i18n/en/student-dashboard.json index 586cdf840ce1..6a2bd64c0295 100644 --- a/src/main/webapp/i18n/en/student-dashboard.json +++ b/src/main/webapp/i18n/en/student-dashboard.json @@ -12,6 +12,8 @@ "noStatistics": "No statistics available", "cardNoExerciseLabel": "No exercise planned", "cardExerciseLabel": "Next Exercise:", + "oldCourses": "Looking for old courses? Click ", + "here": "here", "enroll": { "title": "Course Enrollment", "enrollSuccessful": "Enrollment successful", From a3cdde167ed2d8598e8b8092a0a28e2bbd28e392 Mon Sep 17 00:00:00 2001 From: Ramona Beinstingel Date: Thu, 22 Aug 2024 17:20:40 +0200 Subject: [PATCH 18/74] adjust styling and alignment of course cards --- .../app/overview/course-card.component.html | 35 ++++---- src/main/webapp/app/overview/course-card.scss | 20 ++--- .../app/overview/courses.component.html | 80 ++++++++++--------- .../app/overview/courses.component.scss | 4 - .../content/scss/_artemis-variables.scss | 1 + 5 files changed, 75 insertions(+), 65 deletions(-) diff --git a/src/main/webapp/app/overview/course-card.component.html b/src/main/webapp/app/overview/course-card.component.html index be64f967da4b..c5f94b661332 100644 --- a/src/main/webapp/app/overview/course-card.component.html +++ b/src/main/webapp/app/overview/course-card.component.html @@ -1,26 +1,29 @@ -
-
+
+
-
-
+
+ @if (course.courseIcon) {
- @if (course.courseIcon) { - - } -
-
-
- {{ course.title }} -
+
+ } +
+
+ {{ course.title }} +
-
+
-
-
+
+
@if (exerciseCount > 0 && (totalReachableScore > 0 || totalAbsoluteScore > 0)) {
@@ -79,7 +82,7 @@
@if (course.isAtLeastTutor || course.isAtLeastEditor || course.isAtLeastInstructor) { -
+
diff --git a/src/main/webapp/app/overview/course-card.scss b/src/main/webapp/app/overview/course-card.scss index 44b6d770c34f..26c1d0f46650 100644 --- a/src/main/webapp/app/overview/course-card.scss +++ b/src/main/webapp/app/overview/course-card.scss @@ -1,8 +1,8 @@ .card { - height: 320px; - min-width: 360px; + height: 283px; + min-width: 320px; + max-width: 400px; transition: transform 0.2s linear; - border-radius: 20px; &:hover { transform: scale(1.012); @@ -11,10 +11,7 @@ .card-header { position: relative; - padding: 15px; - height: 100px; - border-top-right-radius: 20px; - border-top-left-radius: 20px; + height: 85px; display: flex; justify-content: center; align-items: center; @@ -38,13 +35,14 @@ .card-title { overflow: hidden; padding-bottom: 1px; - max-height: 80px; + // matches 4 lines + max-height: 76px; } } .card-body { .information-box-wrapper { - height: 145px; + height: 135px; } .chart-container { @@ -94,3 +92,7 @@ jhi-secured-image { width: auto; } } + +.card-header-title { + max-width: 280px; +} diff --git a/src/main/webapp/app/overview/courses.component.html b/src/main/webapp/app/overview/courses.component.html index 2ac8e9c1d00c..58eaf2173e4f 100644 --- a/src/main/webapp/app/overview/courses.component.html +++ b/src/main/webapp/app/overview/courses.component.html @@ -1,10 +1,10 @@ @if (nextRelevantExam && nextRelevantCourseForExam) { -
+

{{ 'artemisApp.studentDashboard.examTitle' | artemisTranslate: { course: nextRelevantCourseForExam.title } }}

-
-
+
+

{{ nextRelevantExam.title }}

@@ -21,42 +21,50 @@

{{ nextRelevantExam.title }}

} -
-@if (recentlyAccessedCourses.length > 0) { -
-

-
-
- @for (course of recentlyAccessedCourses; track course) { - +
+ - @if (regularCourses.length > 0) { + @if (recentlyAccessedCourses.length) {
-

+

- } -} -@if (coursesLoaded && !regularCourses.length && !recentlyAccessedCourses.length) { -
-

-
- {{ 'artemisApp.studentDashboard.enroll.title' | artemisTranslate }} +
+ @for (course of recentlyAccessedCourses; track course) { + + }
-
-} @else { -
- @for (course of regularCourses; track course) { - + @if (regularCourses.length) { +
+

+
} -
-} + } + @if (coursesLoaded && !regularCourses.length && !recentlyAccessedCourses.length) { + + } @else { +
+ @for (course of regularCourses; track course) { + + } +
+ } +
diff --git a/src/main/webapp/app/overview/courses.component.scss b/src/main/webapp/app/overview/courses.component.scss index 2d76b95a5cf6..0ac34f5a5891 100644 --- a/src/main/webapp/app/overview/courses.component.scss +++ b/src/main/webapp/app/overview/courses.component.scss @@ -14,10 +14,6 @@ opacity: 1; } -.col-12 { - width: 100% !important; -} - @media screen and (min-width: 576px) { .col-sm-12 { width: 100% !important; diff --git a/src/main/webapp/content/scss/_artemis-variables.scss b/src/main/webapp/content/scss/_artemis-variables.scss index e06a4772e431..7771e583d3bd 100644 --- a/src/main/webapp/content/scss/_artemis-variables.scss +++ b/src/main/webapp/content/scss/_artemis-variables.scss @@ -17,6 +17,7 @@ $grid-gutter-width: 1rem; // Define common padding and border radius sizes and more. $border-radius: 0.15rem; $border-radius-lg: 0.25rem; +$border-radius-xl: 0.5rem; $border-radius-sm: 0.1rem; // Typography: From f7fd16c5695c49a5ace112d9895e0afda67889c7 Mon Sep 17 00:00:00 2001 From: EgeDoguKaya Date: Fri, 23 Aug 2024 17:07:04 +0200 Subject: [PATCH 19/74] Do further enchancements and polishing --- .../app/overview/course-card.component.html | 8 +++-- src/main/webapp/app/overview/course-card.scss | 9 ++++++ .../app/overview/courses.component.html | 8 ++--- .../app/overview/courses.component.scss | 30 ++++++++++++++----- 4 files changed, 41 insertions(+), 14 deletions(-) diff --git a/src/main/webapp/app/overview/course-card.component.html b/src/main/webapp/app/overview/course-card.component.html index c5f94b661332..7db3e2134051 100644 --- a/src/main/webapp/app/overview/course-card.component.html +++ b/src/main/webapp/app/overview/course-card.component.html @@ -1,16 +1,20 @@
-
+
@if (course.courseIcon) {
+ } @else { +
+ {{ course.title | slice: 0 : 1 }} +
}
diff --git a/src/main/webapp/app/overview/course-card.scss b/src/main/webapp/app/overview/course-card.scss index 26c1d0f46650..9a46bb619b53 100644 --- a/src/main/webapp/app/overview/course-card.scss +++ b/src/main/webapp/app/overview/course-card.scss @@ -38,6 +38,15 @@ // matches 4 lines max-height: 76px; } + + .course-circle { + height: 65px; + min-width: 65px; + background-color: var(--course-image-bg); + border-radius: 50%; + display: inline-block; + color: var(--bs-body-color); + } } .card-body { diff --git a/src/main/webapp/app/overview/courses.component.html b/src/main/webapp/app/overview/courses.component.html index 58eaf2173e4f..2350dd191e39 100644 --- a/src/main/webapp/app/overview/courses.component.html +++ b/src/main/webapp/app/overview/courses.component.html @@ -34,10 +34,10 @@

-
+
@for (course of recentlyAccessedCourses; track course) { @@ -57,10 +57,10 @@

+
@for (course of regularCourses; track course) { diff --git a/src/main/webapp/app/overview/courses.component.scss b/src/main/webapp/app/overview/courses.component.scss index 0ac34f5a5891..52cc8bf6cc69 100644 --- a/src/main/webapp/app/overview/courses.component.scss +++ b/src/main/webapp/app/overview/courses.component.scss @@ -31,21 +31,35 @@ } } -@media screen and (min-width: 992px) { - .col-lg-4 { - width: 50% !important; - } -} - -@media screen and (min-width: 1300px) { +@media screen and (min-width: 1150px) { .col-xl-3 { width: 33.33% !important; } } -@media screen and (min-width: 1685px) { +@media screen and (min-width: 1500px) { .col-xl-3 { flex: 0 0 25; width: 25% !important; } } + +// Bottom padding adjustments based on the screen sizes + +@media screen and (min-width: 1710px) { + .col-xl-3 { + padding-bottom: 1.5rem !important; + } +} + +@media screen and (max-width: 1500px) and (min-width: 1300px) { + .col-lg-4 { + padding-bottom: 1.5rem !important; + } +} + +@media screen and (max-width: 1149px) and (min-width: 890px) { + .col-md-6 { + padding-bottom: 1.5rem !important; + } +} From b8714dd329eb0ab97dd360e7470d44bef74b820e Mon Sep 17 00:00:00 2001 From: EgeDoguKaya Date: Fri, 23 Aug 2024 17:30:17 +0200 Subject: [PATCH 20/74] Integrate feedback --- .../webapp/app/overview/course-card.component.html | 10 +++++----- src/main/webapp/app/overview/courses.component.scss | 4 +--- .../webapp/app/shared/layouts/main/main.component.html | 2 +- .../webapp/app/shared/layouts/main/main.component.scss | 1 - 4 files changed, 7 insertions(+), 10 deletions(-) diff --git a/src/main/webapp/app/overview/course-card.component.html b/src/main/webapp/app/overview/course-card.component.html index 7db3e2134051..a0af4d5a8c2c 100644 --- a/src/main/webapp/app/overview/course-card.component.html +++ b/src/main/webapp/app/overview/course-card.component.html @@ -28,8 +28,8 @@
-
- @if (exerciseCount > 0 && (totalReachableScore > 0 || totalAbsoluteScore > 0)) { +
+ @if (exerciseCount && (totalReachableScore || totalAbsoluteScore)) {
@@ -61,7 +61,7 @@
}
- @if (exerciseCount > 0 && (totalReachableScore > 0 || totalAbsoluteScore > 0)) { + @if (exerciseCount && (totalReachableScore || totalAbsoluteScore)) {
}
- @if (course.isAtLeastTutor || course.isAtLeastEditor || course.isAtLeastInstructor) { -
+ @if (course.isAtLeastTutor) { +
diff --git a/src/main/webapp/app/overview/courses.component.scss b/src/main/webapp/app/overview/courses.component.scss index 52cc8bf6cc69..8bf6b305c5a4 100644 --- a/src/main/webapp/app/overview/courses.component.scss +++ b/src/main/webapp/app/overview/courses.component.scss @@ -44,8 +44,6 @@ } } -// Bottom padding adjustments based on the screen sizes - @media screen and (min-width: 1710px) { .col-xl-3 { padding-bottom: 1.5rem !important; @@ -58,7 +56,7 @@ } } -@media screen and (max-width: 1149px) and (min-width: 890px) { +@media screen and (max-width: 1149px) and (min-width: 900px) { .col-md-6 { padding-bottom: 1.5rem !important; } diff --git a/src/main/webapp/app/shared/layouts/main/main.component.html b/src/main/webapp/app/shared/layouts/main/main.component.html index 8853c5535f0d..2265fffa291b 100644 --- a/src/main/webapp/app/shared/layouts/main/main.component.html +++ b/src/main/webapp/app/shared/layouts/main/main.component.html @@ -14,7 +14,7 @@ } @if (showSkeleton) {
-
+
diff --git a/src/main/webapp/app/shared/layouts/main/main.component.scss b/src/main/webapp/app/shared/layouts/main/main.component.scss index 6064d158320f..b4ac86dfa9d5 100644 --- a/src/main/webapp/app/shared/layouts/main/main.component.scss +++ b/src/main/webapp/app/shared/layouts/main/main.component.scss @@ -1,4 +1,3 @@ .course-overview { background-color: var(--bs-body-bg); - padding-top: 0px; } From 5ea6e84c47a9d17c6c47c54c0ff3f582c0e31de7 Mon Sep 17 00:00:00 2001 From: EgeDoguKaya Date: Fri, 23 Aug 2024 17:49:04 +0200 Subject: [PATCH 21/74] Improve responsiveness --- src/main/webapp/app/overview/courses.component.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/webapp/app/overview/courses.component.scss b/src/main/webapp/app/overview/courses.component.scss index 8bf6b305c5a4..d2435bf98323 100644 --- a/src/main/webapp/app/overview/courses.component.scss +++ b/src/main/webapp/app/overview/courses.component.scss @@ -44,13 +44,13 @@ } } -@media screen and (min-width: 1710px) { +@media screen and (min-width: 1715px) { .col-xl-3 { padding-bottom: 1.5rem !important; } } -@media screen and (max-width: 1500px) and (min-width: 1300px) { +@media screen and (max-width: 1500px) and (min-width: 1308px) { .col-lg-4 { padding-bottom: 1.5rem !important; } From 6ac0eaa30f5541d20a8c44bdf7a4eb772c7eb8d1 Mon Sep 17 00:00:00 2001 From: EgeDoguKaya Date: Mon, 26 Aug 2024 14:44:15 +0200 Subject: [PATCH 22/74] fix border issue, make exercise title look clickable --- src/main/webapp/app/overview/course-card.component.html | 6 +++--- src/main/webapp/app/overview/course-card.scss | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/main/webapp/app/overview/course-card.component.html b/src/main/webapp/app/overview/course-card.component.html index a0af4d5a8c2c..c4a98f485a7e 100644 --- a/src/main/webapp/app/overview/course-card.component.html +++ b/src/main/webapp/app/overview/course-card.component.html @@ -1,4 +1,4 @@ -
+
-
+
+
-
diff --git a/src/main/webapp/app/overview/course-card.scss b/src/main/webapp/app/overview/course-card.scss index 9a46bb619b53..aade95461616 100644 --- a/src/main/webapp/app/overview/course-card.scss +++ b/src/main/webapp/app/overview/course-card.scss @@ -10,6 +10,7 @@ } .card-header { + z-index: 2; position: relative; height: 85px; display: flex; @@ -79,13 +80,12 @@ } .btn-exercise { - color: var(--bs-body-color); z-index: 2; position: relative; - &:hover { - text-decoration: none !important; - } + // &:hover { + // text-decoration: none !important; + // } } } } From 5f29b8d64307f5dedad82f554ad64487f2ea7fa3 Mon Sep 17 00:00:00 2001 From: EgeDoguKaya Date: Mon, 26 Aug 2024 15:42:21 +0200 Subject: [PATCH 23/74] improve header --- .../webapp/app/overview/course-card.component.html | 14 ++++++++------ src/main/webapp/app/overview/course-card.scss | 14 +------------- 2 files changed, 9 insertions(+), 19 deletions(-) diff --git a/src/main/webapp/app/overview/course-card.component.html b/src/main/webapp/app/overview/course-card.component.html index c4a98f485a7e..705644fd8267 100644 --- a/src/main/webapp/app/overview/course-card.component.html +++ b/src/main/webapp/app/overview/course-card.component.html @@ -5,8 +5,7 @@ [routerLink]="['/courses', course.id!]" [ngStyle]="{ '--background-color-for-hover': courseColor }" > - -
+
@if (course.courseIcon) {
@@ -16,16 +15,19 @@ {{ course.title | slice: 0 : 1 }}
} -
+
{{ course.title }}
+
+ +
-
+
@@ -52,7 +54,7 @@

@if (nextRelevantExercise) { - + {{ nextRelevantExercise.title }} } @else { @@ -87,7 +89,7 @@
@if (course.isAtLeastTutor) {
- + diff --git a/src/main/webapp/app/overview/course-card.scss b/src/main/webapp/app/overview/course-card.scss index aade95461616..8ee640ecbaa0 100644 --- a/src/main/webapp/app/overview/course-card.scss +++ b/src/main/webapp/app/overview/course-card.scss @@ -13,9 +13,6 @@ z-index: 2; position: relative; height: 85px; - display: flex; - justify-content: center; - align-items: center; opacity: 1; filter: alpha(opacity = 100); transition: 0.15s; @@ -66,7 +63,7 @@ } } - .btn-management { + .btn-wrapper { z-index: 2; position: relative; } @@ -78,15 +75,6 @@ overflow: hidden; text-overflow: ellipsis; } - - .btn-exercise { - z-index: 2; - position: relative; - - // &:hover { - // text-decoration: none !important; - // } - } } } From 092407438533fcd4ec286435788e16bd5891a412 Mon Sep 17 00:00:00 2001 From: EgeDoguKaya Date: Wed, 28 Aug 2024 19:32:18 +0200 Subject: [PATCH 24/74] Add breakpoints for card sizes, incorporate feedback --- .../manage/course-management.service.ts | 4 +-- .../app/overview/course-card.component.html | 12 +++---- .../app/overview/course-card.component.ts | 5 ++- src/main/webapp/app/overview/course-card.scss | 32 +++++++++++++++++++ .../app/overview/courses.component.html | 21 ++++++------ .../app/overview/courses.component.scss | 1 + .../webapp/app/overview/courses.component.ts | 4 +-- 7 files changed, 56 insertions(+), 23 deletions(-) diff --git a/src/main/webapp/app/course/manage/course-management.service.ts b/src/main/webapp/app/course/manage/course-management.service.ts index ea11ccbece56..d017e15b03cf 100644 --- a/src/main/webapp/app/course/manage/course-management.service.ts +++ b/src/main/webapp/app/course/manage/course-management.service.ts @@ -696,11 +696,11 @@ export class CourseManagementService { return this.http.get(`${this.resourceUrl}/${courseId}/allowed-complaints?teamMode=${teamMode}`) ?? 0; } - setCourseOverviewBackground() { + enableCourseOverviewBackground() { this.courseOverviewSubject.next(true); } - resetCourseOverviewBackground() { + disableCourseOverviewBackground() { this.courseOverviewSubject.next(false); } } diff --git a/src/main/webapp/app/overview/course-card.component.html b/src/main/webapp/app/overview/course-card.component.html index 705644fd8267..3a565d5565de 100644 --- a/src/main/webapp/app/overview/course-card.component.html +++ b/src/main/webapp/app/overview/course-card.component.html @@ -28,7 +28,7 @@
-
+
@if (exerciseCount && (totalReachableScore || totalAbsoluteScore)) { @@ -53,8 +53,8 @@
}
- @if (nextRelevantExercise) { - + @if (nextRelevantExercise && nextRelevantExercise.id && course.id) { + {{ nextRelevantExercise.title }} } @else { @@ -87,9 +87,9 @@
}
- @if (course.isAtLeastTutor) { -
- + @if (course.isAtLeastTutor && course.id) { +
+ diff --git a/src/main/webapp/app/overview/course-card.component.ts b/src/main/webapp/app/overview/course-card.component.ts index 247dfd70090d..043c4a892b48 100644 --- a/src/main/webapp/app/overview/course-card.component.ts +++ b/src/main/webapp/app/overview/course-card.component.ts @@ -19,6 +19,8 @@ import { faArrowRight } from '@fortawesome/free-solid-svg-icons'; styleUrls: ['course-card.scss'], }) export class CourseCardComponent implements OnChanges { + protected readonly faArrowRight = faArrowRight; + readonly ARTEMIS_DEFAULT_COLOR = ARTEMIS_DEFAULT_COLOR; @Input() course: Course; @Input() hasGuidedTour: boolean; @@ -34,9 +36,6 @@ export class CourseCardComponent implements OnChanges { courseColor: string; - // Icons - readonly faArrowRight = faArrowRight; - // ngx ngxDoughnutData: any[] = [ { name: 'achievedPointsLabel', value: 0 }, diff --git a/src/main/webapp/app/overview/course-card.scss b/src/main/webapp/app/overview/course-card.scss index 8ee640ecbaa0..4b581cca1788 100644 --- a/src/main/webapp/app/overview/course-card.scss +++ b/src/main/webapp/app/overview/course-card.scss @@ -4,12 +4,25 @@ max-width: 400px; transition: transform 0.2s linear; + @media screen and (max-width: 768px) and (min-width: 478px) { + max-width: 550px !important; + } + + @media screen and (min-width: 2000px) { + max-width: 500px !important; + } + + @media screen and (min-width: 2700px) { + max-width: 600px !important; + } + &:hover { transform: scale(1.012); background-color: var(--hover-slightly-darker-body-bg); } .card-header { + // needed, otherwise hover effect won't work due to stretched-link class z-index: 2; position: relative; height: 85px; @@ -50,6 +63,10 @@ .card-body { .information-box-wrapper { height: 135px; + + @media screen and (min-width: 2000px) { + margin-left: 1rem !important; + } } .chart-container { @@ -57,6 +74,14 @@ align-items: center; justify-content: center; + @media screen and (max-width: 768px) and (min-width: 478px) { + justify-content: flex-end !important; + } + + @media screen and (min-width: 2000px) { + justify-content: flex-end !important; + } + .chart-level { z-index: 2; cursor: pointer; @@ -64,10 +89,17 @@ } .btn-wrapper { + // needed, otherwise button won't work due to stretched-link class z-index: 2; position: relative; } + .btn-management { + @media screen and (min-width: 2000px) { + margin-left: 1rem !important; + } + } + .exercise-title { display: inline-block; max-width: 100%; diff --git a/src/main/webapp/app/overview/courses.component.html b/src/main/webapp/app/overview/courses.component.html index eb5ab0ea6073..e3c721c44009 100644 --- a/src/main/webapp/app/overview/courses.component.html +++ b/src/main/webapp/app/overview/courses.component.html @@ -34,11 +34,7 @@

@for (course of recentlyAccessedCourses; track course) { - + }

@if (regularCourses.length) { @@ -57,12 +53,17 @@

@for (course of regularCourses; track course) { - + }

}
+ + + + + diff --git a/src/main/webapp/app/overview/courses.component.scss b/src/main/webapp/app/overview/courses.component.scss index d2435bf98323..d7a7345dab21 100644 --- a/src/main/webapp/app/overview/courses.component.scss +++ b/src/main/webapp/app/overview/courses.component.scss @@ -44,6 +44,7 @@ } } +/** We adjust bottom paddings dynamically to be consistent with the spacing between cards */ @media screen and (min-width: 1715px) { .col-xl-3 { padding-bottom: 1.5rem !important; diff --git a/src/main/webapp/app/overview/courses.component.ts b/src/main/webapp/app/overview/courses.component.ts index ce565224949a..da9fd16c49ea 100644 --- a/src/main/webapp/app/overview/courses.component.ts +++ b/src/main/webapp/app/overview/courses.component.ts @@ -49,7 +49,7 @@ export class CoursesComponent implements OnInit, OnDestroy { async ngOnInit() { this.loadAndFilterCourses(); (await this.teamService.teamAssignmentUpdates).subscribe(); - this.courseService.setCourseOverviewBackground(); + this.courseService.enableCourseOverviewBackground(); } /** @@ -59,7 +59,7 @@ export class CoursesComponent implements OnInit, OnDestroy { if (this.quizExercisesChannels) { this.quizExercisesChannels.forEach((channel) => this.jhiWebsocketService.unsubscribe(channel)); } - this.courseService.resetCourseOverviewBackground(); + this.courseService.disableCourseOverviewBackground(); } loadAndFilterCourses() { From 2e1e626b3e27fd507caee7b77d0d6311ec627219 Mon Sep 17 00:00:00 2001 From: EgeDoguKaya Date: Sat, 31 Aug 2024 19:09:41 +0200 Subject: [PATCH 25/74] Incorporate feedback, display more cards instead of shrinking the cards --- .../app/overview/course-card.component.html | 4 +- src/main/webapp/app/overview/course-card.scss | 33 ++---------- .../app/overview/courses.component.html | 7 +-- .../app/overview/courses.component.scss | 51 +++++++------------ 4 files changed, 24 insertions(+), 71 deletions(-) diff --git a/src/main/webapp/app/overview/course-card.component.html b/src/main/webapp/app/overview/course-card.component.html index 3a565d5565de..530353b169d2 100644 --- a/src/main/webapp/app/overview/course-card.component.html +++ b/src/main/webapp/app/overview/course-card.component.html @@ -28,7 +28,7 @@
-
+
@if (exerciseCount && (totalReachableScore || totalAbsoluteScore)) { @@ -88,7 +88,7 @@
@if (course.isAtLeastTutor && course.id) { -
+
diff --git a/src/main/webapp/app/overview/course-card.scss b/src/main/webapp/app/overview/course-card.scss index 4b581cca1788..78d9fef47c5b 100644 --- a/src/main/webapp/app/overview/course-card.scss +++ b/src/main/webapp/app/overview/course-card.scss @@ -1,21 +1,11 @@ .card { + // best possible height to fit header, information, and manage course button: 80px header + 200px card content height: 283px; + // needed, otherwise score chart will appear outside of the card min-width: 320px; max-width: 400px; transition: transform 0.2s linear; - @media screen and (max-width: 768px) and (min-width: 478px) { - max-width: 550px !important; - } - - @media screen and (min-width: 2000px) { - max-width: 500px !important; - } - - @media screen and (min-width: 2700px) { - max-width: 600px !important; - } - &:hover { transform: scale(1.012); background-color: var(--hover-slightly-darker-body-bg); @@ -51,6 +41,7 @@ } .course-circle { + // same size as the course icons height: 65px; min-width: 65px; background-color: var(--course-image-bg); @@ -63,10 +54,6 @@ .card-body { .information-box-wrapper { height: 135px; - - @media screen and (min-width: 2000px) { - margin-left: 1rem !important; - } } .chart-container { @@ -74,14 +61,6 @@ align-items: center; justify-content: center; - @media screen and (max-width: 768px) and (min-width: 478px) { - justify-content: flex-end !important; - } - - @media screen and (min-width: 2000px) { - justify-content: flex-end !important; - } - .chart-level { z-index: 2; cursor: pointer; @@ -94,12 +73,6 @@ position: relative; } - .btn-management { - @media screen and (min-width: 2000px) { - margin-left: 1rem !important; - } - } - .exercise-title { display: inline-block; max-width: 100%; diff --git a/src/main/webapp/app/overview/courses.component.html b/src/main/webapp/app/overview/courses.component.html index e3c721c44009..883437dfbd89 100644 --- a/src/main/webapp/app/overview/courses.component.html +++ b/src/main/webapp/app/overview/courses.component.html @@ -59,11 +59,6 @@

+ diff --git a/src/main/webapp/app/overview/courses.component.scss b/src/main/webapp/app/overview/courses.component.scss index d7a7345dab21..bc3aa2b06f8f 100644 --- a/src/main/webapp/app/overview/courses.component.scss +++ b/src/main/webapp/app/overview/courses.component.scss @@ -14,51 +14,36 @@ opacity: 1; } -@media screen and (min-width: 576px) { - .col-sm-12 { - width: 100% !important; - } -} - -@media screen and (min-width: 830px) { - .col-md-6 { - width: 50% !important; - } - - .px-sm-4 { - padding-right: 1rem !important; - padding-left: 1rem !important; - } -} - -@media screen and (min-width: 1150px) { - .col-xl-3 { - width: 33.33% !important; +/** +* Column width adjustments derived from course card width sizes +* Ensures that no cards will appear on top of each other +*/ +@media screen and (max-width: 2550px) { + .col-2 { + width: 20% !important; } } -@media screen and (min-width: 1500px) { - .col-xl-3 { - flex: 0 0 25; +@media screen and (max-width: 2000px) { + .col-2 { width: 25% !important; } } -/** We adjust bottom paddings dynamically to be consistent with the spacing between cards */ -@media screen and (min-width: 1715px) { - .col-xl-3 { - padding-bottom: 1.5rem !important; +@media screen and (max-width: 1650px) { + .col-2 { + width: 33% !important; } } -@media screen and (max-width: 1500px) and (min-width: 1308px) { - .col-lg-4 { - padding-bottom: 1.5rem !important; +@media screen and (max-width: 1172px) { + .col-2 { + width: 50% !important; } } -@media screen and (max-width: 1149px) and (min-width: 900px) { - .col-md-6 { - padding-bottom: 1.5rem !important; +@media screen and (max-width: 819px) { + .col-2 { + width: 100% !important; } } From 4a376772e7b79e70ed6b5124dbee38cfdc668506 Mon Sep 17 00:00:00 2001 From: EgeDoguKaya Date: Sat, 31 Aug 2024 20:47:49 +0200 Subject: [PATCH 26/74] Adjust 3 card row layout better --- src/main/webapp/app/overview/courses.component.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/webapp/app/overview/courses.component.scss b/src/main/webapp/app/overview/courses.component.scss index bc3aa2b06f8f..9c43fae1fbd0 100644 --- a/src/main/webapp/app/overview/courses.component.scss +++ b/src/main/webapp/app/overview/courses.component.scss @@ -32,7 +32,7 @@ @media screen and (max-width: 1650px) { .col-2 { - width: 33% !important; + width: 33.33% !important; } } From 06074e5d3a1577d2082a49d5297f959e60b55ac4 Mon Sep 17 00:00:00 2001 From: EgeDoguKaya Date: Mon, 2 Sep 2024 11:08:27 +0200 Subject: [PATCH 27/74] Enhance client ui, create course card header component --- .../course-archive.component.html | 60 +++++++++++-------- .../course-archive.component.scss | 42 ++++++------- .../course-archive.component.ts | 21 ++++--- .../course-card-header.component.html | 27 +++++++++ .../course-card-header.component.scss | 57 ++++++++++++++++++ .../course-card-header.component.ts | 22 +++++++ .../app/overview/course-card.component.html | 27 +-------- .../app/overview/course-card.component.ts | 4 -- .../webapp/app/overview/courses.module.ts | 4 ++ 9 files changed, 178 insertions(+), 86 deletions(-) create mode 100644 src/main/webapp/app/overview/course-card-header/course-card-header.component.html create mode 100644 src/main/webapp/app/overview/course-card-header/course-card-header.component.scss create mode 100644 src/main/webapp/app/overview/course-card-header/course-card-header.component.ts diff --git a/src/main/webapp/app/overview/course-archive/course-archive.component.html b/src/main/webapp/app/overview/course-archive/course-archive.component.html index 5c161f284daa..b681e24204cd 100644 --- a/src/main/webapp/app/overview/course-archive/course-archive.component.html +++ b/src/main/webapp/app/overview/course-archive/course-archive.component.html @@ -1,17 +1,34 @@ -
+
-
+ +
+

+ +
+
@if (courses) { -
-
+
+
@for (semester of semesters; track semester; let last = $last) { -
- + } @if (!last) {
} diff --git a/src/main/webapp/app/overview/course-archive/course-archive.component.scss b/src/main/webapp/app/overview/course-archive/course-archive.component.scss index 0e059b7b5528..5a1a82472cac 100644 --- a/src/main/webapp/app/overview/course-archive/course-archive.component.scss +++ b/src/main/webapp/app/overview/course-archive/course-archive.component.scss @@ -1,33 +1,33 @@ -.course-table { - padding: 5px 0; - background-color: var(--overview-light-background-color); - border-radius: 3px; - transition: box-shadow 0.1s linear; +.table { + cursor: pointer; } -.course-table-container { - .control-label { - cursor: pointer; - margin-bottom: 16px; +@media screen and (max-width: 2550px) { + .col-2 { + width: 20% !important; } +} - .container { - display: inline-flex; +@media screen and (max-width: 2000px) { + .col-2 { + width: 25% !important; } } -.table-responsive { - overflow-x: hidden; +@media screen and (max-width: 1650px) { + .col-2 { + width: 33.33% !important; + } } -.course-card { - background-color: var(--background-color-for-hover) !important; - transition: 0.15s; - filter: alpha(opacity = 100); - opacity: 1; +@media screen and (max-width: 1172px) { + .col-2 { + width: 50% !important; + } +} - &:hover { - transform: scale(1.012); - background-color: color-mix(in srgb, var(--background-color-for-hover), transparent 15%) !important; +@media screen and (max-width: 819px) { + .col-2 { + width: 100% !important; } } diff --git a/src/main/webapp/app/overview/course-archive/course-archive.component.ts b/src/main/webapp/app/overview/course-archive/course-archive.component.ts index 1c9111410ace..67035a453069 100644 --- a/src/main/webapp/app/overview/course-archive/course-archive.component.ts +++ b/src/main/webapp/app/overview/course-archive/course-archive.component.ts @@ -1,6 +1,4 @@ import { Component, OnDestroy, OnInit } from '@angular/core'; -import { ArtemisSharedModule } from 'app/shared/shared.module'; -import { ArtemisSharedComponentModule } from 'app/shared/components/shared-component.module'; import { Course } from 'app/entities/course.model'; import { CourseManagementService } from '../../course/manage/course-management.service'; import { HttpErrorResponse, HttpResponse } from '@angular/common/http'; @@ -9,13 +7,10 @@ import { onError } from 'app/shared/util/global.utils'; import { Subscription } from 'rxjs'; import { faAngleDown, faAngleUp, faArrowDownAZ, faArrowUpAZ, faQuestionCircle } from '@fortawesome/free-solid-svg-icons'; import { sortCourses } from 'app/shared/util/course.util'; -import { ARTEMIS_DEFAULT_COLOR } from 'app/app.constants'; import { SizeProp } from '@fortawesome/fontawesome-svg-core'; @Component({ - standalone: true, selector: 'course-archive', - imports: [ArtemisSharedModule, ArtemisSharedComponentModule], templateUrl: './course-archive.component.html', styleUrls: ['./course-archive.component.scss'], }) @@ -24,6 +19,7 @@ export class CourseArchiveComponent implements OnInit, OnDestroy { courses: Course[]; semesters: string[]; + expandedSemesterStrings: { [key: string]: string }; semesterCollapsed: { [key: string]: boolean }; coursesBySemester: { [key: string]: Course[] }; courseColor: string; @@ -31,8 +27,6 @@ export class CourseArchiveComponent implements OnInit, OnDestroy { isSortAscending = true; iconSize: SizeProp = 'lg'; - readonly ARTEMIS_DEFAULT_COLOR = ARTEMIS_DEFAULT_COLOR; - //Icons readonly faAngleDown = faAngleDown; readonly faAngleUp = faAngleUp; @@ -47,7 +41,7 @@ export class CourseArchiveComponent implements OnInit, OnDestroy { ngOnInit(): void { this.loadArchivedCourses(); - this.courseService.setCourseOverviewBackground(); + this.courseService.enableCourseOverviewBackground(); } /** @@ -77,7 +71,7 @@ export class CourseArchiveComponent implements OnInit, OnDestroy { ngOnDestroy(): void { this.archiveCourseSubscription.unsubscribe(); - this.courseService.resetCourseOverviewBackground(); + this.courseService.disableCourseOverviewBackground(); } setSearchValue(searchValue: string): void { @@ -100,4 +94,13 @@ export class CourseArchiveComponent implements OnInit, OnDestroy { this.semesterCollapsed[semester] = !hasMatchingCourse; } } + + mapSemesterString(semester: string): string { + const year = semester.slice(2); + if (semester.startsWith('WS')) { + return `Wintersemester 20${year}`; + } else { + return `Sommersemester 20${year}`; + } + } } diff --git a/src/main/webapp/app/overview/course-card-header/course-card-header.component.html b/src/main/webapp/app/overview/course-card-header/course-card-header.component.html new file mode 100644 index 000000000000..5ef22cbbc48f --- /dev/null +++ b/src/main/webapp/app/overview/course-card-header/course-card-header.component.html @@ -0,0 +1,27 @@ +
diff --git a/src/main/webapp/app/overview/course-card-header/course-card-header.component.scss b/src/main/webapp/app/overview/course-card-header/course-card-header.component.scss new file mode 100644 index 000000000000..f9d6ce7d9259 --- /dev/null +++ b/src/main/webapp/app/overview/course-card-header/course-card-header.component.scss @@ -0,0 +1,57 @@ +.card-header { + min-width: 320px; + max-width: 400px; + // needed, otherwise hover effect won't work due to stretched-link class + z-index: 2; + position: relative; + height: 85px; + opacity: 1; + filter: alpha(opacity = 100); + transition: 0.15s; + background-color: var(--background-color-for-hover) !important; + + &:hover { + background-color: color-mix(in srgb, var(--background-color-for-hover), transparent 15%) !important; + } + + .container { + height: 80px; + + .row { + height: 80px; + } + } + + .card-title { + overflow: hidden; + padding-bottom: 1px; + // matches 4 lines + max-height: 76px; + } + + .course-circle { + // same size as the course icons + height: 65px; + min-width: 65px; + background-color: var(--course-image-bg); + border-radius: 50%; + display: inline-block; + color: var(--bs-body-color); + } +} + +.container { + max-width: unset; +} + +jhi-secured-image { + ::ng-deep img { + border-radius: 50%; + height: 65px; + width: auto; + } +} + +.card-header-title { + max-width: 280px; +} diff --git a/src/main/webapp/app/overview/course-card-header/course-card-header.component.ts b/src/main/webapp/app/overview/course-card-header/course-card-header.component.ts new file mode 100644 index 000000000000..55525e93b417 --- /dev/null +++ b/src/main/webapp/app/overview/course-card-header/course-card-header.component.ts @@ -0,0 +1,22 @@ +import { Component, Input, OnInit } from '@angular/core'; +import { Course } from 'app/entities/course.model'; +import { CachingStrategy } from 'app/shared/image/secured-image.component'; +import { ARTEMIS_DEFAULT_COLOR } from 'app/app.constants'; + +@Component({ + selector: 'jhi-course-card-header', + templateUrl: './course-card-header.component.html', + styleUrls: ['./course-card-header.component.scss'], +}) +export class CourseCardHeaderComponent implements OnInit { + protected readonly ARTEMIS_DEFAULT_COLOR = ARTEMIS_DEFAULT_COLOR; + @Input() course: Course; + @Input() courseColor: string; + @Input() archiveMode = false; + + CachingStrategy = CachingStrategy; + + ngOnInit() { + this.courseColor = this.course.color || this.ARTEMIS_DEFAULT_COLOR; + } +} diff --git a/src/main/webapp/app/overview/course-card.component.html b/src/main/webapp/app/overview/course-card.component.html index 530353b169d2..c36ba1fae6da 100644 --- a/src/main/webapp/app/overview/course-card.component.html +++ b/src/main/webapp/app/overview/course-card.component.html @@ -1,30 +1,5 @@
-
-
- @if (course.courseIcon) { -
- -
- } @else { -
- {{ course.title | slice: 0 : 1 }} -
- } -
-
- {{ course.title }} -
-
-
- -
-
-
+
diff --git a/src/main/webapp/app/overview/course-card.component.ts b/src/main/webapp/app/overview/course-card.component.ts index 043c4a892b48..380f7eb7bb8b 100644 --- a/src/main/webapp/app/overview/course-card.component.ts +++ b/src/main/webapp/app/overview/course-card.component.ts @@ -1,7 +1,6 @@ import { Component, Input, OnChanges } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; import { Color, ScaleType } from '@swimlane/ngx-charts'; -import { ARTEMIS_DEFAULT_COLOR } from 'app/app.constants'; import { Course } from 'app/entities/course.model'; import { Exercise } from 'app/entities/exercise.model'; import { ExerciseService } from 'app/exercises/shared/exercise/exercise.service'; @@ -21,7 +20,6 @@ import { faArrowRight } from '@fortawesome/free-solid-svg-icons'; export class CourseCardComponent implements OnChanges { protected readonly faArrowRight = faArrowRight; - readonly ARTEMIS_DEFAULT_COLOR = ARTEMIS_DEFAULT_COLOR; @Input() course: Course; @Input() hasGuidedTour: boolean; @@ -80,8 +78,6 @@ export class CourseCardComponent implements OnChanges { this.ngxDoughnutData[1].value = scoreNotReached; this.ngxDoughnutData = [...this.ngxDoughnutData]; } - - this.courseColor = this.course.color || this.ARTEMIS_DEFAULT_COLOR; } /** diff --git a/src/main/webapp/app/overview/courses.module.ts b/src/main/webapp/app/overview/courses.module.ts index 01e27a171163..b3fa8df28fcf 100644 --- a/src/main/webapp/app/overview/courses.module.ts +++ b/src/main/webapp/app/overview/courses.module.ts @@ -19,6 +19,8 @@ import { ArtemisCourseExerciseRowModule } from 'app/overview/course-exercises/co import { NgxChartsModule, PieChartModule } from '@swimlane/ngx-charts'; import { CourseUnenrollmentModalComponent } from 'app/overview/course-unenrollment-modal.component'; import { ArtemisSidebarModule } from 'app/shared/sidebar/sidebar.module'; +import { CourseCardHeaderComponent } from 'app/overview/course-card-header/course-card-header.component'; +import { CourseArchiveComponent } from 'app/overview/course-archive/course-archive.component'; @NgModule({ imports: [ @@ -45,6 +47,8 @@ import { ArtemisSidebarModule } from 'app/shared/sidebar/sidebar.module'; CourseLecturesComponent, CourseLectureRowComponent, CourseUnenrollmentModalComponent, + CourseCardHeaderComponent, + CourseArchiveComponent, ], }) export class ArtemisCoursesModule {} From 8c9c2ac7366bafc729976eeb5e4496b9f405fb56 Mon Sep 17 00:00:00 2001 From: EgeDoguKaya Date: Mon, 2 Sep 2024 11:38:46 +0200 Subject: [PATCH 28/74] Fix inner border radius --- src/main/webapp/app/overview/course-card.component.html | 7 +------ src/main/webapp/app/overview/course-card.scss | 3 +++ 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/main/webapp/app/overview/course-card.component.html b/src/main/webapp/app/overview/course-card.component.html index 530353b169d2..5e70d4797792 100644 --- a/src/main/webapp/app/overview/course-card.component.html +++ b/src/main/webapp/app/overview/course-card.component.html @@ -1,10 +1,5 @@
-
+
@if (course.courseIcon) {
diff --git a/src/main/webapp/app/overview/course-card.scss b/src/main/webapp/app/overview/course-card.scss index 78d9fef47c5b..630d49440234 100644 --- a/src/main/webapp/app/overview/course-card.scss +++ b/src/main/webapp/app/overview/course-card.scss @@ -20,6 +20,9 @@ filter: alpha(opacity = 100); transition: 0.15s; background-color: var(--background-color-for-hover) !important; + // inner border radius : outer border radius - outer border thickness (8px - 1px) + border-top-left-radius: 7px; + border-top-right-radius: 7px; &:hover { background-color: color-mix(in srgb, var(--background-color-for-hover), transparent 15%) !important; From 203b96e33abcdd8c0e47bb098cac9f7dd4bd7ac0 Mon Sep 17 00:00:00 2001 From: EgeDoguKaya Date: Mon, 2 Sep 2024 17:36:09 +0200 Subject: [PATCH 29/74] Display more cards for very large screens --- .../app/overview/courses.component.scss | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/main/webapp/app/overview/courses.component.scss b/src/main/webapp/app/overview/courses.component.scss index 9c43fae1fbd0..d508f6b73e46 100644 --- a/src/main/webapp/app/overview/courses.component.scss +++ b/src/main/webapp/app/overview/courses.component.scss @@ -18,6 +18,25 @@ * Column width adjustments derived from course card width sizes * Ensures that no cards will appear on top of each other */ + +@media screen and (min-width: 3350px) { + .col-2 { + width: 14.28% !important; + } +} + +@media screen and (min-width: 3800px) { + .col-2 { + width: 12.5% !important; + } +} + +@media screen and (min-width: 4700px) { + .col-2 { + width: 11.11% !important; + } +} + @media screen and (max-width: 2550px) { .col-2 { width: 20% !important; From 42cd8daed624ccefb68b7127fbbc54c0ee4e5f40 Mon Sep 17 00:00:00 2001 From: EgeDoguKaya Date: Mon, 2 Sep 2024 17:41:08 +0200 Subject: [PATCH 30/74] Remove empty line --- src/main/webapp/app/overview/courses.component.scss | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/webapp/app/overview/courses.component.scss b/src/main/webapp/app/overview/courses.component.scss index d508f6b73e46..acd1e4bd9995 100644 --- a/src/main/webapp/app/overview/courses.component.scss +++ b/src/main/webapp/app/overview/courses.component.scss @@ -18,7 +18,6 @@ * Column width adjustments derived from course card width sizes * Ensures that no cards will appear on top of each other */ - @media screen and (min-width: 3350px) { .col-2 { width: 14.28% !important; From 9be99332c024fe47cc1cd8bd3ef9c11b00beefb7 Mon Sep 17 00:00:00 2001 From: EgeDoguKaya Date: Wed, 4 Sep 2024 01:14:46 +0200 Subject: [PATCH 31/74] Use grid layout to omit hardcoded breakpoints --- src/main/webapp/app/overview/course-card.scss | 5 +- .../app/overview/courses.component.html | 26 ++++++--- .../app/overview/courses.component.scss | 58 ++++--------------- 3 files changed, 31 insertions(+), 58 deletions(-) diff --git a/src/main/webapp/app/overview/course-card.scss b/src/main/webapp/app/overview/course-card.scss index 630d49440234..774ed641c58b 100644 --- a/src/main/webapp/app/overview/course-card.scss +++ b/src/main/webapp/app/overview/course-card.scss @@ -1,9 +1,8 @@ .card { // best possible height to fit header, information, and manage course button: 80px header + 200px card content height: 283px; - // needed, otherwise score chart will appear outside of the card - min-width: 320px; - max-width: 400px; + // ensure the card takes the full width of its column in the container + width: 100%; transition: transform 0.2s linear; &:hover { diff --git a/src/main/webapp/app/overview/courses.component.html b/src/main/webapp/app/overview/courses.component.html index 883437dfbd89..a9d1655aba7c 100644 --- a/src/main/webapp/app/overview/courses.component.html +++ b/src/main/webapp/app/overview/courses.component.html @@ -32,10 +32,14 @@

-
- @for (course of recentlyAccessedCourses; track course) { - - } +
+
+ @for (course of recentlyAccessedCourses; track course) { +
+ +
+ } +
@if (regularCourses.length) {
@@ -51,14 +55,18 @@

- @for (course of regularCourses; track course) { - - } +
+
+ @for (course of regularCourses; track course) { +
+ +
+ } +
}

- + diff --git a/src/main/webapp/app/overview/courses.component.scss b/src/main/webapp/app/overview/courses.component.scss index acd1e4bd9995..6681e8340eab 100644 --- a/src/main/webapp/app/overview/courses.component.scss +++ b/src/main/webapp/app/overview/courses.component.scss @@ -14,54 +14,20 @@ opacity: 1; } -/** -* Column width adjustments derived from course card width sizes -* Ensures that no cards will appear on top of each other -*/ -@media screen and (min-width: 3350px) { - .col-2 { - width: 14.28% !important; - } +.course-grid { + display: grid; + // cards can shrink to 325px + grid-template-columns: repeat(auto-fill, minmax(325px, 1fr)); + grid-gap: 2rem; + justify-items: center; } -@media screen and (min-width: 3800px) { - .col-2 { - width: 12.5% !important; - } +.course-card-wrapper { + width: 100%; + max-width: 400px; } -@media screen and (min-width: 4700px) { - .col-2 { - width: 11.11% !important; - } -} - -@media screen and (max-width: 2550px) { - .col-2 { - width: 20% !important; - } -} - -@media screen and (max-width: 2000px) { - .col-2 { - width: 25% !important; - } -} - -@media screen and (max-width: 1650px) { - .col-2 { - width: 33.33% !important; - } -} - -@media screen and (max-width: 1172px) { - .col-2 { - width: 50% !important; - } -} - -@media screen and (max-width: 819px) { - .col-2 { - width: 100% !important; - } +.container-fluid { + // ensure that horizontal spacing in container is consistent + --bs-gutter-x: 2rem; } From 59df1ac0581c8c3a80216caf0f935c221fd6ed13 Mon Sep 17 00:00:00 2001 From: EgeDoguKaya Date: Wed, 4 Sep 2024 02:23:10 +0200 Subject: [PATCH 32/74] Make grid gaps more consistent --- src/main/webapp/app/overview/courses.component.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/webapp/app/overview/courses.component.scss b/src/main/webapp/app/overview/courses.component.scss index 6681e8340eab..8e820688fa3c 100644 --- a/src/main/webapp/app/overview/courses.component.scss +++ b/src/main/webapp/app/overview/courses.component.scss @@ -17,8 +17,8 @@ .course-grid { display: grid; // cards can shrink to 325px - grid-template-columns: repeat(auto-fill, minmax(325px, 1fr)); - grid-gap: 2rem; + grid-template-columns: repeat(auto-fill, minmax(350px, 1fr)); + grid-gap: 1rem; justify-items: center; } From 2cf69307f85d0f0f6afffb24139682c68c195fea Mon Sep 17 00:00:00 2001 From: EgeDoguKaya Date: Wed, 4 Sep 2024 22:27:22 +0200 Subject: [PATCH 33/74] Reduce min width --- src/main/webapp/app/overview/courses.component.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/webapp/app/overview/courses.component.scss b/src/main/webapp/app/overview/courses.component.scss index 8e820688fa3c..be68863c0df2 100644 --- a/src/main/webapp/app/overview/courses.component.scss +++ b/src/main/webapp/app/overview/courses.component.scss @@ -17,7 +17,7 @@ .course-grid { display: grid; // cards can shrink to 325px - grid-template-columns: repeat(auto-fill, minmax(350px, 1fr)); + grid-template-columns: repeat(auto-fill, minmax(325px, 1fr)); grid-gap: 1rem; justify-items: center; } From 0bfdf5aed1be69c1fb1b366c2b4965412b347c34 Mon Sep 17 00:00:00 2001 From: EgeDoguKaya Date: Wed, 4 Sep 2024 22:49:06 +0200 Subject: [PATCH 34/74] improve layout for smaller screens --- src/main/webapp/app/overview/courses.component.scss | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/main/webapp/app/overview/courses.component.scss b/src/main/webapp/app/overview/courses.component.scss index be68863c0df2..90047d3652c4 100644 --- a/src/main/webapp/app/overview/courses.component.scss +++ b/src/main/webapp/app/overview/courses.component.scss @@ -16,10 +16,14 @@ .course-grid { display: grid; - // cards can shrink to 325px - grid-template-columns: repeat(auto-fill, minmax(325px, 1fr)); + // cards can shrink to 350px + grid-template-columns: repeat(auto-fill, minmax(350px, 1fr)); grid-gap: 1rem; justify-items: center; + // for 4 or less cards, we let each card shrink to 325px to display more cards in smaller screens, 1751px is the breakpoint for 4 cards + @media screen and (max-width: 1751px) { + grid-template-columns: repeat(auto-fill, minmax(325px, 1fr)); + } } .course-card-wrapper { From 33fbbf51f50dc2afaafd0ebcc192674dd589afc2 Mon Sep 17 00:00:00 2001 From: EgeDoguKaya Date: Sun, 8 Sep 2024 16:29:11 +0200 Subject: [PATCH 35/74] Implement feedback --- src/main/webapp/app/overview/course-card.component.html | 6 ++++-- src/main/webapp/app/overview/course-card.scss | 4 ++++ src/main/webapp/i18n/de/student-dashboard.json | 2 +- src/main/webapp/i18n/en/student-dashboard.json | 2 +- 4 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/main/webapp/app/overview/course-card.component.html b/src/main/webapp/app/overview/course-card.component.html index 5e70d4797792..4b69401c3d85 100644 --- a/src/main/webapp/app/overview/course-card.component.html +++ b/src/main/webapp/app/overview/course-card.component.html @@ -35,7 +35,6 @@
[translateValues]="{ totalAbsoluteScore: totalAbsoluteScore, totalReachableScore: totalReachableScore, - totalRelativeScore: totalRelativeScore, }" >
@@ -59,7 +58,10 @@
}
@if (exerciseCount && (totalReachableScore || totalAbsoluteScore)) { -
+
+
+

{{ totalRelativeScore }}%

+
Date: Mon, 16 Sep 2024 02:51:49 +0200 Subject: [PATCH 36/74] Add empty course message to improve ux --- .../course-archive.component.html | 47 +++++++++++-------- .../webapp/i18n/de/student-dashboard.json | 1 + .../webapp/i18n/en/student-dashboard.json | 1 + 3 files changed, 30 insertions(+), 19 deletions(-) diff --git a/src/main/webapp/app/overview/course-archive/course-archive.component.html b/src/main/webapp/app/overview/course-archive/course-archive.component.html index 0a3569866c3e..d6950bef593a 100644 --- a/src/main/webapp/app/overview/course-archive/course-archive.component.html +++ b/src/main/webapp/app/overview/course-archive/course-archive.component.html @@ -1,25 +1,30 @@ -
-
-
-

- -
-
- -@if (courses) {
@for (semester of semesters; track semester; let last = $last) { @@ -44,4 +49,8 @@

}
+} @else { +
+

+
} diff --git a/src/main/webapp/i18n/de/student-dashboard.json b/src/main/webapp/i18n/de/student-dashboard.json index 710950fbfe5b..113086d3be5c 100644 --- a/src/main/webapp/i18n/de/student-dashboard.json +++ b/src/main/webapp/i18n/de/student-dashboard.json @@ -14,6 +14,7 @@ "cardExerciseLabel": "Nächste Übung", "oldCourses": "Suchst du nach alten Kursen? Klicke ", "here": "hier", + "noCoursesPreviousSemester": "Keine Kurse aus früheren Semestern gefunden", "points": "{{ totalAbsoluteScore }} / {{ totalReachableScore }} Punkte", "cardScore": "Punktzahl", "cardManageCourse": "Kurs Verwalten", diff --git a/src/main/webapp/i18n/en/student-dashboard.json b/src/main/webapp/i18n/en/student-dashboard.json index ee06c302c0a4..32740eb87f38 100644 --- a/src/main/webapp/i18n/en/student-dashboard.json +++ b/src/main/webapp/i18n/en/student-dashboard.json @@ -14,6 +14,7 @@ "cardExerciseLabel": "Next Exercise", "oldCourses": "Looking for old courses? Click ", "here": "here", + "noCoursesPreviousSemester": "No courses found from previous semesters", "points": "{{ totalAbsoluteScore }} / {{ totalReachableScore }} Points", "cardScore": "Score", "cardManageCourse": "Manage Course", From 9ed164de43e9fc79707519e10e0695db6405bc5c Mon Sep 17 00:00:00 2001 From: EgeDoguKaya Date: Thu, 19 Sep 2024 21:44:13 +0200 Subject: [PATCH 37/74] Add Old Course Cards --- .../course-archive.component.html | 2 +- .../course-card-header.component.ts | 5 + .../app/overview/course-card.component.html | 161 ++++++++++------- .../app/overview/course-card.component.ts | 34 +++- src/main/webapp/app/overview/course-card.scss | 166 ++++++++++++++++-- .../app/overview/courses.component.html | 86 ++++----- .../app/overview/courses.component.scss | 31 ++-- .../webapp/app/overview/courses.component.ts | 2 - 8 files changed, 331 insertions(+), 156 deletions(-) diff --git a/src/main/webapp/app/overview/course-archive/course-archive.component.html b/src/main/webapp/app/overview/course-archive/course-archive.component.html index d6950bef593a..c8605a51882f 100644 --- a/src/main/webapp/app/overview/course-archive/course-archive.component.html +++ b/src/main/webapp/app/overview/course-archive/course-archive.component.html @@ -44,7 +44,7 @@

} @if (!last) { -
+
} }
diff --git a/src/main/webapp/app/overview/course-card-header/course-card-header.component.ts b/src/main/webapp/app/overview/course-card-header/course-card-header.component.ts index bd4126dd2b57..56f975c948d7 100644 --- a/src/main/webapp/app/overview/course-card-header/course-card-header.component.ts +++ b/src/main/webapp/app/overview/course-card-header/course-card-header.component.ts @@ -3,6 +3,11 @@ import { Course } from 'app/entities/course.model'; import { CachingStrategy } from 'app/shared/image/secured-image.component'; import { ARTEMIS_DEFAULT_COLOR } from 'app/app.constants'; +/** TODO '@edkaya': New course card design also uses this header design. + * Therefore, this component will be reused in course-card.component.html + * after new course cards are merged into develop. I will refactor its html + * and scss file to avoid duplicates to maintain reusability in the follow-up. + * */ @Component({ selector: 'jhi-course-card-header', templateUrl: './course-card-header.component.html', diff --git a/src/main/webapp/app/overview/course-card.component.html b/src/main/webapp/app/overview/course-card.component.html index 638221baa0ee..0ab968d60075 100644 --- a/src/main/webapp/app/overview/course-card.component.html +++ b/src/main/webapp/app/overview/course-card.component.html @@ -1,77 +1,102 @@ -
- -
+
+
-
-
-
-
- @if (exerciseCount && (totalReachableScore || totalAbsoluteScore)) { -
-
-
-
-
-
- } @else { -
-
- -
+
+
+
+ @if (course.courseIcon) { + + } +
+
+
+ {{ course.title }} +
+
+
+
+ @if (exerciseCount === 0) { + Exercises: {{ exerciseCount }} } -
-
- @if (nextRelevantExercise && nextRelevantExercise.id && course.id) { - - {{ nextRelevantExercise.title }} - - } @else { -
- -
+ @if (exerciseCount > 0) { + Exercises: {{ exerciseCount }} + } + @if (lectureCount === 0) { + Lectures: {{ lectureCount }} + } + @if (lectureCount > 0) { + Lectures: {{ lectureCount }} + } + @if (examCount === 0) { + Exams: {{ examCount }} + } + @if (examCount > 0) { + Exams: {{ examCount }} }
- @if (exerciseCount && (totalReachableScore || totalAbsoluteScore)) { -
-
-

{{ totalRelativeScore }}%

-
- - - - : {{ model.value }} - - -
- }
- @if (course.isAtLeastTutor && course.id) { - - }
+
+ + @if (exerciseCount > 0 && (totalReachableScore > 0 || totalAbsoluteScore > 0)) { +
+
+

{{ totalRelativeScore }}%

+
{{ totalAbsoluteScore }} / {{ totalReachableScore }} Pts
+
+ + + + : {{ model.value }} + + +
+ } @else { +
+ } +
+ @if (nextRelevantExercise) { + + } + @if (!nextRelevantExercise) { + + }
diff --git a/src/main/webapp/app/overview/course-card.component.ts b/src/main/webapp/app/overview/course-card.component.ts index 380f7eb7bb8b..6fc36a26c528 100644 --- a/src/main/webapp/app/overview/course-card.component.ts +++ b/src/main/webapp/app/overview/course-card.component.ts @@ -1,16 +1,19 @@ import { Component, Input, OnChanges } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; +import { IconProp } from '@fortawesome/fontawesome-svg-core'; import { Color, ScaleType } from '@swimlane/ngx-charts'; +import { ARTEMIS_DEFAULT_COLOR } from 'app/app.constants'; import { Course } from 'app/entities/course.model'; -import { Exercise } from 'app/entities/exercise.model'; +import { Exercise, getIcon, getIconTooltip } from 'app/entities/exercise.model'; import { ExerciseService } from 'app/exercises/shared/exercise/exercise.service'; import { CachingStrategy } from 'app/shared/image/secured-image.component'; import { roundValueSpecifiedByCourseSettings } from 'app/shared/util/utils'; +import dayjs from 'dayjs/esm'; +import { getExerciseDueDate } from 'app/exercises/shared/exercise/exercise.utils'; import { GraphColors } from 'app/entities/statistics.model'; import { ScoresStorageService } from 'app/course/course-scores/scores-storage.service'; import { ScoreType } from 'app/shared/constants/score-type.constants'; import { CourseScores } from 'app/course/course-scores/course-scores'; -import { faArrowRight } from '@fortawesome/free-solid-svg-icons'; @Component({ selector: 'jhi-overview-course-card', @@ -18,15 +21,19 @@ import { faArrowRight } from '@fortawesome/free-solid-svg-icons'; styleUrls: ['course-card.scss'], }) export class CourseCardComponent implements OnChanges { - protected readonly faArrowRight = faArrowRight; - + readonly ARTEMIS_DEFAULT_COLOR = ARTEMIS_DEFAULT_COLOR; @Input() course: Course; @Input() hasGuidedTour: boolean; CachingStrategy = CachingStrategy; nextRelevantExercise?: Exercise; + nextExerciseDueDate?: dayjs.Dayjs; + nextExerciseIcon: IconProp; + nextExerciseTooltip: string; exerciseCount = 0; + lectureCount = 0; + examCount = 0; totalRelativeScore: number; totalReachableScore: number; @@ -63,6 +70,9 @@ export class CourseCardComponent implements OnChanges { if (nextExercises.length > 0 && nextExercises[0]) { this.nextRelevantExercise = nextExercises[0]; + this.updateNextDueDate(); + this.nextExerciseIcon = getIcon(this.nextRelevantExercise!.type); + this.nextExerciseTooltip = getIconTooltip(this.nextRelevantExercise!.type); } const totalScoresForCourse: CourseScores | undefined = this.scoresStorageService.getStoredTotalScores(this.course.id!); @@ -78,6 +88,10 @@ export class CourseCardComponent implements OnChanges { this.ngxDoughnutData[1].value = scoreNotReached; this.ngxDoughnutData = [...this.ngxDoughnutData]; } + + this.lectureCount = this.course.numberOfLectures ?? this.course.lectures?.length ?? 0; + this.examCount = this.course.numberOfExams ?? this.course.exams?.length ?? 0; + this.courseColor = this.course.color || this.ARTEMIS_DEFAULT_COLOR; } /** @@ -96,4 +110,16 @@ export class CourseCardComponent implements OnChanges { event.stopPropagation(); this.router.navigate(['courses', this.course.id, 'exams']); } + + private updateNextDueDate() { + let nextExerciseDueDate = undefined; + if (this.nextRelevantExercise) { + if (this.nextRelevantExercise.studentParticipations && this.nextRelevantExercise.studentParticipations.length > 0) { + nextExerciseDueDate = getExerciseDueDate(this.nextRelevantExercise, this.nextRelevantExercise.studentParticipations[0]); + } else { + nextExerciseDueDate = this.nextRelevantExercise.dueDate; + } + } + this.nextExerciseDueDate = nextExerciseDueDate; + } } diff --git a/src/main/webapp/app/overview/course-card.scss b/src/main/webapp/app/overview/course-card.scss index 1a388204ca93..f7a475404894 100644 --- a/src/main/webapp/app/overview/course-card.scss +++ b/src/main/webapp/app/overview/course-card.scss @@ -1,8 +1,5 @@ .card { - // best possible height to fit header, information, and manage course button: 80px header + 200px card content - height: 283px; - // ensure the card takes the full width of its column in the container - width: 100%; + min-height: 320px; transition: transform 0.2s linear; &:hover { @@ -10,11 +7,76 @@ background-color: var(--hover-slightly-darker-body-bg); } - .card-body { - .information-box-wrapper { - height: 135px; + .card-header { + position: relative; + padding: 0; + height: 80px; + display: flex; + justify-content: center; + align-items: center; + opacity: 1; + filter: alpha(opacity = 100); + transition: 0.15s; + background-color: var(--background-color-for-hover) !important; + + &:hover { + background-color: color-mix(in srgb, var(--background-color-for-hover), transparent 15%) !important; + } + + .header-col { + height: 80px; + display: flex; + align-items: center; + } + + .image-col { + justify-content: flex-start; + } + + .title-col { + justify-content: center; + } + + .course-info-col { + justify-content: flex-end; + + .course-info-amounts { + display: flex; + flex-direction: column; + line-height: 20px; + white-space: nowrap; + z-index: 1; + + a { + color: white; + font-weight: unset; + } + } + } + + .container { + height: 80px; + + .row { + height: 80px; + justify-content: space-between; + } } + .card-title { + overflow: hidden; + padding-bottom: 1px; + max-height: 80px; + } + } + + .card-body { + position: relative; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + .chart-container { display: flex; align-items: center; @@ -30,18 +92,88 @@ } } - .btn-wrapper { - // needed, otherwise button won't work due to stretched-link class - z-index: 2; - position: relative; + .points { + display: block; + max-width: 11ch; } - .exercise-title { - display: inline-block; - max-width: 100%; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; + canvas { + cursor: pointer; + z-index: 1; + width: 150px; + height: 150px; } + + .no-statistics { + display: flex; + height: 200px; + justify-content: center; + flex-direction: column; + margin-bottom: 0; + } + } + + .card-footer { + position: relative; + padding: 0; + height: 50px; + + .container { + height: 50px; + + .row { + height: 50px; + } + } + + .next-exercise-col { + display: flex; + justify-content: center; + align-items: center; + + .next-exercise-icon { + z-index: 1; + font-size: large; + } + + .next-exercise-title { + font-weight: bolder; + font-size: large; + margin-bottom: 0; + display: inline-block; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + max-width: 40ch; + padding-left: 15px; + } + + h6 { + margin-bottom: 0; + } + } + + .no-exercise { + display: flex; + height: 50px; + align-items: center; + padding-left: 30px; + + h6 { + margin-bottom: 0; + } + } + } +} + +.container { + max-width: unset; +} + +jhi-secured-image { + ::ng-deep img { + border-radius: 50%; + height: 65px; + width: auto; } } diff --git a/src/main/webapp/app/overview/courses.component.html b/src/main/webapp/app/overview/courses.component.html index 07d0895614ca..00859527fe8a 100644 --- a/src/main/webapp/app/overview/courses.component.html +++ b/src/main/webapp/app/overview/courses.component.html @@ -1,8 +1,8 @@ @if (nextRelevantExam && nextRelevantCourseForExam) { -
+

-
-
+
+

{{ nextRelevantExam.title }}

@@ -19,54 +19,46 @@

{{ nextRelevantExam.title }}

} -
-
-

- @if (regularCourses.length) { -
- -
+
+
+

+
+ + @if (regularCourses.length) { +
+ +
+ } +
+@if (recentlyAccessedCourses.length > 0) { +
+

+
+
+ @for (course of recentlyAccessedCourses; track course) { + }
- @if (recentlyAccessedCourses.length) { + @if (regularCourses.length > 0) {
-

-
-
-
- @for (course of recentlyAccessedCourses; track course) { -
- -
- } -
+

- @if (regularCourses.length) { -
-

-
- } } - @if (coursesLoaded && !regularCourses.length && !recentlyAccessedCourses.length) { -
-

-
- -
-
- } @else { -
-
- @for (course of regularCourses; track course) { -
- -
- } -
+} +@if (coursesLoaded && !regularCourses.length && !recentlyAccessedCourses.length) { +
+

+
+
- } -
- +
+} @else { +
+ @for (course of regularCourses; track course) { + + } +
+} @if (coursesLoaded) {
@@ -75,7 +67,3 @@

- diff --git a/src/main/webapp/app/overview/courses.component.scss b/src/main/webapp/app/overview/courses.component.scss index 90047d3652c4..93bd4a898edf 100644 --- a/src/main/webapp/app/overview/courses.component.scss +++ b/src/main/webapp/app/overview/courses.component.scss @@ -14,24 +14,25 @@ opacity: 1; } -.course-grid { - display: grid; - // cards can shrink to 350px - grid-template-columns: repeat(auto-fill, minmax(350px, 1fr)); - grid-gap: 1rem; - justify-items: center; - // for 4 or less cards, we let each card shrink to 325px to display more cards in smaller screens, 1751px is the breakpoint for 4 cards - @media screen and (max-width: 1751px) { - grid-template-columns: repeat(auto-fill, minmax(325px, 1fr)); +.col-12 { + width: 100% !important; +} + +@media screen and (min-width: 992px) { + .col-lg-6 { + width: 50% !important; } } -.course-card-wrapper { - width: 100%; - max-width: 400px; +@media screen and (min-width: 1200px) { + .col-xl-4 { + width: 33.33% !important; + } } -.container-fluid { - // ensure that horizontal spacing in container is consistent - --bs-gutter-x: 2rem; +@media screen and (min-width: 1900px) { + .col-xl-4 { + flex: 0 0 25; + width: 25% !important; + } } diff --git a/src/main/webapp/app/overview/courses.component.ts b/src/main/webapp/app/overview/courses.component.ts index fad63ba437cb..0d5218bb21b0 100644 --- a/src/main/webapp/app/overview/courses.component.ts +++ b/src/main/webapp/app/overview/courses.component.ts @@ -49,7 +49,6 @@ export class CoursesComponent implements OnInit, OnDestroy { async ngOnInit() { this.loadAndFilterCourses(); (await this.teamService.teamAssignmentUpdates).subscribe(); - this.courseService.enableCourseOverviewBackground(); } /** @@ -59,7 +58,6 @@ export class CoursesComponent implements OnInit, OnDestroy { if (this.quizExercisesChannels) { this.quizExercisesChannels.forEach((channel) => this.jhiWebsocketService.unsubscribe(channel)); } - this.courseService.disableCourseOverviewBackground(); } loadAndFilterCourses() { From 915119e569c9837f8b9f12302da979bb844be598 Mon Sep 17 00:00:00 2001 From: EgeDoguKaya Date: Fri, 20 Sep 2024 00:25:44 +0200 Subject: [PATCH 38/74] Clean up --- .../course/manage/course-management.service.ts | 17 +++++------------ .../course-archive.component.html | 6 +++--- src/main/webapp/i18n/de/course.json | 3 ++- src/main/webapp/i18n/de/student-dashboard.json | 6 +----- src/main/webapp/i18n/en/course.json | 3 ++- src/main/webapp/i18n/en/student-dashboard.json | 6 +----- 6 files changed, 14 insertions(+), 27 deletions(-) diff --git a/src/main/webapp/app/course/manage/course-management.service.ts b/src/main/webapp/app/course/manage/course-management.service.ts index 9fa71a8eccee..63d042148cd6 100644 --- a/src/main/webapp/app/course/manage/course-management.service.ts +++ b/src/main/webapp/app/course/manage/course-management.service.ts @@ -343,18 +343,11 @@ export class CourseManagementService { ); } - getCoursesForArchive(req?: any): Observable> { - const options = createRequestOption(req); // This will handle query params if needed - return this.http.get(`${this.resourceUrl}/archive`, { params: options, observe: 'response' }).pipe( - tap((res: HttpResponse) => { - if (res.body) { - res.body.forEach((course) => { - // If you need to perform any action on each course - console.log(course); - }); - } - }), - ); + /** + * find all courses for the archive using a GET request + */ + getCoursesForArchive(): Observable> { + return this.http.get(`${this.resourceUrl}/archive`, { observe: 'response' }).pipe(); } /** diff --git a/src/main/webapp/app/overview/course-archive/course-archive.component.html b/src/main/webapp/app/overview/course-archive/course-archive.component.html index c8605a51882f..956e994e5ec2 100644 --- a/src/main/webapp/app/overview/course-archive/course-archive.component.html +++ b/src/main/webapp/app/overview/course-archive/course-archive.component.html @@ -26,9 +26,9 @@

-
+
@for (semester of semesters; track semester; let last = $last) { -
+
{{ mapSemesterString(semester) }}
@@ -51,6 +51,6 @@

} @else {
-

+

} diff --git a/src/main/webapp/i18n/de/course.json b/src/main/webapp/i18n/de/course.json index 0433ae60df88..20cd62939cdd 100644 --- a/src/main/webapp/i18n/de/course.json +++ b/src/main/webapp/i18n/de/course.json @@ -18,7 +18,8 @@ "archive": { "title": "Archiv", "sort": "Sortieren", - "tip": "Das Archiv ermöglicht dir, all deine vergangenen Kurse, organisiert nach Semestern, anzusehen. Klicke auf ein Semester, um es zu erweitern und die Kurse zu sehen, in die du in diesem Zeitraum eingeschrieben warst." + "tip": "Das Archiv ermöglicht dir, all deine vergangenen Kurse, organisiert nach Semestern, anzusehen. Klicke auf ein Semester, um es zu erweitern und die Kurse zu sehen, in die du in diesem Zeitraum eingeschrieben warst.", + "noCoursesPreviousSemester": "Keine Kurse aus früheren Semestern gefunden" }, "showActive": "Nur aktive Kurse anzeigen", "totalScore": "Gesamtergebnis:", diff --git a/src/main/webapp/i18n/de/student-dashboard.json b/src/main/webapp/i18n/de/student-dashboard.json index 113086d3be5c..2133f3081e90 100644 --- a/src/main/webapp/i18n/de/student-dashboard.json +++ b/src/main/webapp/i18n/de/student-dashboard.json @@ -11,13 +11,9 @@ "cardTitle": "Deine insgesamte Punktzahl:", "noStatistics": "Keine Statistik verfügbar", "cardNoExerciseLabel": "Keine Übung geplant", - "cardExerciseLabel": "Nächste Übung", + "cardExerciseLabel": "Nächste Übung:", "oldCourses": "Suchst du nach alten Kursen? Klicke ", "here": "hier", - "noCoursesPreviousSemester": "Keine Kurse aus früheren Semestern gefunden", - "points": "{{ totalAbsoluteScore }} / {{ totalReachableScore }} Punkte", - "cardScore": "Punktzahl", - "cardManageCourse": "Kurs Verwalten", "enroll": { "title": "Kurseinschreibung", "enrollSuccessful": "Einschreibung erfolgreich", diff --git a/src/main/webapp/i18n/en/course.json b/src/main/webapp/i18n/en/course.json index abbfd65252a0..0ad45d5afed7 100644 --- a/src/main/webapp/i18n/en/course.json +++ b/src/main/webapp/i18n/en/course.json @@ -18,7 +18,8 @@ "archive": { "title": "Archive", "sort": "Sort", - "tip": "The archive enables you to view all your past courses, organized by semester. Click on a semester to expand and see the courses you were enrolled in during that period." + "tip": "The archive enables you to view all your past courses, organized by semester. Click on a semester to expand and see the courses you were enrolled in during that period.", + "noCoursesPreviousSemester": "No courses found from previous semesters" }, "showActive": "Show only active courses", "totalScore": "Total Score:", diff --git a/src/main/webapp/i18n/en/student-dashboard.json b/src/main/webapp/i18n/en/student-dashboard.json index 32740eb87f38..11f851639cb9 100644 --- a/src/main/webapp/i18n/en/student-dashboard.json +++ b/src/main/webapp/i18n/en/student-dashboard.json @@ -11,13 +11,9 @@ "cardTitle": "Your overall points:", "noStatistics": "No statistics available", "cardNoExerciseLabel": "No exercise planned", - "cardExerciseLabel": "Next Exercise", + "cardExerciseLabel": "Next Exercise:", "oldCourses": "Looking for old courses? Click ", "here": "here", - "noCoursesPreviousSemester": "No courses found from previous semesters", - "points": "{{ totalAbsoluteScore }} / {{ totalReachableScore }} Points", - "cardScore": "Score", - "cardManageCourse": "Manage Course", "enroll": { "title": "Course Enrollment", "enrollSuccessful": "Enrollment successful", From 3aecc59f0e9e7df0b8afe265c82ba8b1f6ea7263 Mon Sep 17 00:00:00 2001 From: EgeDoguKaya Date: Fri, 20 Sep 2024 01:28:17 +0200 Subject: [PATCH 39/74] Add translations for semesters --- .../course-archive.component.html | 9 ++++++-- .../course-archive.component.ts | 21 +++++++++---------- src/main/webapp/i18n/de/course.json | 4 +++- src/main/webapp/i18n/en/course.json | 4 +++- 4 files changed, 23 insertions(+), 15 deletions(-) diff --git a/src/main/webapp/app/overview/course-archive/course-archive.component.html b/src/main/webapp/app/overview/course-archive/course-archive.component.html index 956e994e5ec2..9eebf1c00614 100644 --- a/src/main/webapp/app/overview/course-archive/course-archive.component.html +++ b/src/main/webapp/app/overview/course-archive/course-archive.component.html @@ -28,8 +28,13 @@

@for (semester of semesters; track semester; let last = $last) { -
- {{ mapSemesterString(semester) }} +
+
@if (!semesterCollapsed[semester]) { diff --git a/src/main/webapp/app/overview/course-archive/course-archive.component.ts b/src/main/webapp/app/overview/course-archive/course-archive.component.ts index 83a76d6448fa..9b2eabeea9c9 100644 --- a/src/main/webapp/app/overview/course-archive/course-archive.component.ts +++ b/src/main/webapp/app/overview/course-archive/course-archive.component.ts @@ -19,7 +19,7 @@ export class CourseArchiveComponent implements OnInit, OnDestroy { courses: Course[]; semesters: string[]; - expandedSemesterStrings: { [key: string]: string }; + fullFormOfSemesterStrings: { [key: string]: string }; semesterCollapsed: { [key: string]: boolean }; coursesBySemester: { [key: string]: Course[] }; searchCourseText = ''; @@ -52,20 +52,26 @@ export class CourseArchiveComponent implements OnInit, OnDestroy { this.courses = response.body || []; this.courses = sortCourses(this.courses); this.semesters = this.courseService.getUniqueSemesterNamesSorted(this.courses); + console.log('this.semesters: ' + this.semesters); this.mapCoursesIntoSemesters(); }, error: (error: HttpErrorResponse) => onError(this.alertService, error), }); } + /** + * maps existing courses to each semester + */ private mapCoursesIntoSemesters(): void { this.semesterCollapsed = {}; this.coursesBySemester = {}; + this.fullFormOfSemesterStrings = {}; let isCollapsed = false; for (const semester of this.semesters) { this.semesterCollapsed[semester] = isCollapsed; this.coursesBySemester[semester] = this.courses.filter((course) => course.semester === semester); + this.fullFormOfSemesterStrings[semester] = semester.startsWith('WS') ? 'artemisApp.course.archive.winterSemester' : 'artemisApp.course.archive.summerSemester'; isCollapsed = true; } } @@ -88,20 +94,13 @@ export class CourseArchiveComponent implements OnInit, OnDestroy { this.isSortAscending = !this.isSortAscending; } } - + /** + * if the searched text is matched with a course title, expand the accordion, otherwise collapse + */ expandOrCollapseBasedOnSearchValue(): void { for (const semester of this.semesters) { const hasMatchingCourse = this.coursesBySemester[semester].some((course) => course.title?.toLowerCase().includes(this.searchCourseText.toLowerCase())); this.semesterCollapsed[semester] = !hasMatchingCourse; } } - - mapSemesterString(semester: string): string { - const year = semester.slice(2); - if (semester.startsWith('WS')) { - return `Wintersemester 20${year}`; - } else { - return `Sommersemester 20${year}`; - } - } } diff --git a/src/main/webapp/i18n/de/course.json b/src/main/webapp/i18n/de/course.json index 20cd62939cdd..784b703f2424 100644 --- a/src/main/webapp/i18n/de/course.json +++ b/src/main/webapp/i18n/de/course.json @@ -19,7 +19,9 @@ "title": "Archiv", "sort": "Sortieren", "tip": "Das Archiv ermöglicht dir, all deine vergangenen Kurse, organisiert nach Semestern, anzusehen. Klicke auf ein Semester, um es zu erweitern und die Kurse zu sehen, in die du in diesem Zeitraum eingeschrieben warst.", - "noCoursesPreviousSemester": "Keine Kurse aus früheren Semestern gefunden" + "noCoursesPreviousSemester": "Keine Kurse aus früheren Semestern gefunden", + "winterSemester": "Wintersemester 20{{ param }}", + "summerSemester": "Sommersemester 20{{ param }}" }, "showActive": "Nur aktive Kurse anzeigen", "totalScore": "Gesamtergebnis:", diff --git a/src/main/webapp/i18n/en/course.json b/src/main/webapp/i18n/en/course.json index 0ad45d5afed7..06335cedcf33 100644 --- a/src/main/webapp/i18n/en/course.json +++ b/src/main/webapp/i18n/en/course.json @@ -19,7 +19,9 @@ "title": "Archive", "sort": "Sort", "tip": "The archive enables you to view all your past courses, organized by semester. Click on a semester to expand and see the courses you were enrolled in during that period.", - "noCoursesPreviousSemester": "No courses found from previous semesters" + "noCoursesPreviousSemester": "No courses found from previous semesters", + "winterSemester": "Winter semester 20{{ param }}", + "summerSemester": "Summer semester 20{{ param }}" }, "showActive": "Show only active courses", "totalScore": "Total Score:", From a0824afc3845fb34d4e26c14cda6a3c75591098e Mon Sep 17 00:00:00 2001 From: EgeDoguKaya Date: Fri, 20 Sep 2024 01:51:11 +0200 Subject: [PATCH 40/74] Improve spacing --- .../app/overview/course-archive/course-archive.component.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/webapp/app/overview/course-archive/course-archive.component.html b/src/main/webapp/app/overview/course-archive/course-archive.component.html index 9eebf1c00614..2d8396317c89 100644 --- a/src/main/webapp/app/overview/course-archive/course-archive.component.html +++ b/src/main/webapp/app/overview/course-archive/course-archive.component.html @@ -29,8 +29,8 @@

@for (semester of semesters; track semester; let last = $last) {
From f3b352fa4bf8ca05404c4c5cdc40d1b11417f8a5 Mon Sep 17 00:00:00 2001 From: EgeDoguKaya Date: Fri, 20 Sep 2024 02:15:40 +0200 Subject: [PATCH 41/74] use lower case for page title --- src/main/webapp/i18n/en/global.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/webapp/i18n/en/global.json b/src/main/webapp/i18n/en/global.json index 3db0bee44d32..e9a59ba3738f 100644 --- a/src/main/webapp/i18n/en/global.json +++ b/src/main/webapp/i18n/en/global.json @@ -348,7 +348,7 @@ "statistics": "Course statistics", "exams": "Exams", "communication": "Communication", - "archive": "Course Archive" + "archive": "Course archive" }, "connectionStatus": { "connected": "Connected", From a64a145230ca35992831ac0d9085f17264b261a8 Mon Sep 17 00:00:00 2001 From: EgeDoguKaya Date: Fri, 20 Sep 2024 02:23:47 +0200 Subject: [PATCH 42/74] Add javadoc comment --- .../de/tum/cit/aet/artemis/core/service/CourseService.java | 7 +++++++ .../de/tum/cit/aet/artemis/core/web/CourseResource.java | 5 +++++ 2 files changed, 12 insertions(+) diff --git a/src/main/java/de/tum/cit/aet/artemis/core/service/CourseService.java b/src/main/java/de/tum/cit/aet/artemis/core/service/CourseService.java index b2f1f3b1e766..dd2c4873a69c 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/service/CourseService.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/service/CourseService.java @@ -652,6 +652,13 @@ public List getAllCoursesForManagementOverview(boolean onlyActive) { return courseRepository.findAllCoursesByManagementGroupNames(userGroups); } + /** + * Fetches all courses, filters out courses that the user does not have + * access to based on their role, and excludes any courses that do not + * belong to a specific semester. + * + * @return A list of courses for the course archive + */ public List getAllCoursesForCourseArchive() { var user = userRepository.getUserWithGroupsAndAuthorities(); List courses = courseRepository.findAll(); diff --git a/src/main/java/de/tum/cit/aet/artemis/core/web/CourseResource.java b/src/main/java/de/tum/cit/aet/artemis/core/web/CourseResource.java index 2673934a4b13..21db04df8c17 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/web/CourseResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/web/CourseResource.java @@ -555,6 +555,11 @@ public ResponseEntity> getCoursesForManagementOverview(@RequestPara return ResponseEntity.ok(courseService.getAllCoursesForManagementOverview(onlyActive)); } + /** + * GET /courses/archive : get all courses for course archive + * + * @return the ResponseEntity with status 200 (OK) and with body a list of courses (the user has access to) + */ @GetMapping("courses/archive") @EnforceAtLeastStudent public ResponseEntity> getCoursesForArchive() { From ad738bbed8d1e6a9e1e4b32ca07636461fbca2b4 Mon Sep 17 00:00:00 2001 From: EgeDoguKaya Date: Fri, 20 Sep 2024 02:28:01 +0200 Subject: [PATCH 43/74] remove unused code --- src/main/webapp/app/course/manage/course-management.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/webapp/app/course/manage/course-management.service.ts b/src/main/webapp/app/course/manage/course-management.service.ts index 63d042148cd6..097775459db0 100644 --- a/src/main/webapp/app/course/manage/course-management.service.ts +++ b/src/main/webapp/app/course/manage/course-management.service.ts @@ -347,7 +347,7 @@ export class CourseManagementService { * find all courses for the archive using a GET request */ getCoursesForArchive(): Observable> { - return this.http.get(`${this.resourceUrl}/archive`, { observe: 'response' }).pipe(); + return this.http.get(`${this.resourceUrl}/archive`, { observe: 'response' }); } /** From b8bc564a3a7f30f1f63c16a5591eb863ba19f83a Mon Sep 17 00:00:00 2001 From: EgeDoguKaya Date: Fri, 20 Sep 2024 02:30:21 +0200 Subject: [PATCH 44/74] remove console.logs --- .../app/overview/course-archive/course-archive.component.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/webapp/app/overview/course-archive/course-archive.component.ts b/src/main/webapp/app/overview/course-archive/course-archive.component.ts index 9b2eabeea9c9..7ae7532e5b56 100644 --- a/src/main/webapp/app/overview/course-archive/course-archive.component.ts +++ b/src/main/webapp/app/overview/course-archive/course-archive.component.ts @@ -52,7 +52,6 @@ export class CourseArchiveComponent implements OnInit, OnDestroy { this.courses = response.body || []; this.courses = sortCourses(this.courses); this.semesters = this.courseService.getUniqueSemesterNamesSorted(this.courses); - console.log('this.semesters: ' + this.semesters); this.mapCoursesIntoSemesters(); }, error: (error: HttpErrorResponse) => onError(this.alertService, error), From 20b50c88c8b96805236223eef69f1feb48a8b9bc Mon Sep 17 00:00:00 2001 From: EgeDoguKaya Date: Fri, 20 Sep 2024 04:54:07 +0200 Subject: [PATCH 45/74] Fix client test --- .../spec/component/course/course-management.component.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/javascript/spec/component/course/course-management.component.spec.ts b/src/test/javascript/spec/component/course/course-management.component.spec.ts index b1b33b125911..4812d0cb54d5 100644 --- a/src/test/javascript/spec/component/course/course-management.component.spec.ts +++ b/src/test/javascript/spec/component/course/course-management.component.spec.ts @@ -148,7 +148,7 @@ describe('CourseManagementComponent', () => { const course5 = { id: 5, semester: '' } as Course; // course with no semester component.courses = [course1, course2, course3, course4, course5]; - const sortedSemesters = component['getUniqueSemesterNamesSorted'](component.courses); + const sortedSemesters = service.getUniqueSemesterNamesSorted(component.courses); expect(sortedSemesters).toEqual(['WS20', 'SS20', 'WS19', 'SS19', '']); }); From 8615d035dde31625a61208c6f26ee442e421a505 Mon Sep 17 00:00:00 2001 From: EgeDoguKaya Date: Fri, 20 Sep 2024 15:23:53 +0200 Subject: [PATCH 46/74] Bring old state --- src/main/webapp/app/guided-tour/tours/course-overview-tour.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/webapp/app/guided-tour/tours/course-overview-tour.ts b/src/main/webapp/app/guided-tour/tours/course-overview-tour.ts index 5e5a82fe2fd5..a76c25357da2 100644 --- a/src/main/webapp/app/guided-tour/tours/course-overview-tour.ts +++ b/src/main/webapp/app/guided-tour/tours/course-overview-tour.ts @@ -60,7 +60,7 @@ export const courseOverviewTour: GuidedTour = { orientation: Orientation.RIGHT, }), new TextTourStep({ - highlightSelector: '.guided-tour .exercise-guided-tour', + highlightSelector: '.guided-tour .card-footer', headlineTranslateKey: 'tour.courseOverview.courseFooter.headline', contentTranslateKey: 'tour.courseOverview.courseFooter.content', orientation: Orientation.TOPLEFT, From 5ed403f6df9af57f0b9c36786ad266c1be28626c Mon Sep 17 00:00:00 2001 From: EgeDoguKaya Date: Fri, 20 Sep 2024 15:36:28 +0200 Subject: [PATCH 47/74] Make new components standalone --- .../app/overview/course-archive/course-archive.component.ts | 5 +++++ .../course-card-header/course-card-header.component.ts | 4 ++++ src/main/webapp/app/overview/courses.module.ts | 4 ---- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/main/webapp/app/overview/course-archive/course-archive.component.ts b/src/main/webapp/app/overview/course-archive/course-archive.component.ts index 7ae7532e5b56..77d75cb9d433 100644 --- a/src/main/webapp/app/overview/course-archive/course-archive.component.ts +++ b/src/main/webapp/app/overview/course-archive/course-archive.component.ts @@ -8,11 +8,16 @@ import { Subscription } from 'rxjs'; import { faAngleDown, faAngleUp, faArrowDownAZ, faArrowUpAZ, faQuestionCircle } from '@fortawesome/free-solid-svg-icons'; import { sortCourses } from 'app/shared/util/course.util'; import { SizeProp } from '@fortawesome/fontawesome-svg-core'; +import { ArtemisSharedModule } from 'app/shared/shared.module'; +import { ArtemisSharedComponentModule } from 'app/shared/components/shared-component.module'; +import { CourseCardHeaderComponent } from '../course-card-header/course-card-header.component'; @Component({ selector: 'course-archive', templateUrl: './course-archive.component.html', styleUrls: ['./course-archive.component.scss'], + standalone: true, + imports: [ArtemisSharedModule, ArtemisSharedComponentModule, CourseCardHeaderComponent], }) export class CourseArchiveComponent implements OnInit, OnDestroy { private archiveCourseSubscription: Subscription; diff --git a/src/main/webapp/app/overview/course-card-header/course-card-header.component.ts b/src/main/webapp/app/overview/course-card-header/course-card-header.component.ts index 56f975c948d7..1391893f88b3 100644 --- a/src/main/webapp/app/overview/course-card-header/course-card-header.component.ts +++ b/src/main/webapp/app/overview/course-card-header/course-card-header.component.ts @@ -2,6 +2,8 @@ import { Component, OnInit, input } from '@angular/core'; import { Course } from 'app/entities/course.model'; import { CachingStrategy } from 'app/shared/image/secured-image.component'; import { ARTEMIS_DEFAULT_COLOR } from 'app/app.constants'; +import { ArtemisSharedModule } from 'app/shared/shared.module'; +import { ArtemisSharedComponentModule } from 'app/shared/components/shared-component.module'; /** TODO '@edkaya': New course card design also uses this header design. * Therefore, this component will be reused in course-card.component.html @@ -12,6 +14,8 @@ import { ARTEMIS_DEFAULT_COLOR } from 'app/app.constants'; selector: 'jhi-course-card-header', templateUrl: './course-card-header.component.html', styleUrls: ['./course-card-header.component.scss'], + standalone: true, + imports: [ArtemisSharedModule, ArtemisSharedComponentModule], }) export class CourseCardHeaderComponent implements OnInit { protected readonly ARTEMIS_DEFAULT_COLOR = ARTEMIS_DEFAULT_COLOR; diff --git a/src/main/webapp/app/overview/courses.module.ts b/src/main/webapp/app/overview/courses.module.ts index b3fa8df28fcf..01e27a171163 100644 --- a/src/main/webapp/app/overview/courses.module.ts +++ b/src/main/webapp/app/overview/courses.module.ts @@ -19,8 +19,6 @@ import { ArtemisCourseExerciseRowModule } from 'app/overview/course-exercises/co import { NgxChartsModule, PieChartModule } from '@swimlane/ngx-charts'; import { CourseUnenrollmentModalComponent } from 'app/overview/course-unenrollment-modal.component'; import { ArtemisSidebarModule } from 'app/shared/sidebar/sidebar.module'; -import { CourseCardHeaderComponent } from 'app/overview/course-card-header/course-card-header.component'; -import { CourseArchiveComponent } from 'app/overview/course-archive/course-archive.component'; @NgModule({ imports: [ @@ -47,8 +45,6 @@ import { CourseArchiveComponent } from 'app/overview/course-archive/course-archi CourseLecturesComponent, CourseLectureRowComponent, CourseUnenrollmentModalComponent, - CourseCardHeaderComponent, - CourseArchiveComponent, ], }) export class ArtemisCoursesModule {} From 10ea7df199ce18c7a6db7df4dd0dac6cc515832a Mon Sep 17 00:00:00 2001 From: EgeDoguKaya Date: Fri, 20 Sep 2024 18:35:19 +0200 Subject: [PATCH 48/74] Add client tests for course archive --- .../course-archive.component.html | 6 +- .../course/course-archive.component.spec.ts | 206 ++++++++++++++++++ 2 files changed, 209 insertions(+), 3 deletions(-) create mode 100644 src/test/javascript/spec/component/course/course-archive.component.spec.ts diff --git a/src/main/webapp/app/overview/course-archive/course-archive.component.html b/src/main/webapp/app/overview/course-archive/course-archive.component.html index 2d8396317c89..e2ae0f4943b7 100644 --- a/src/main/webapp/app/overview/course-archive/course-archive.component.html +++ b/src/main/webapp/app/overview/course-archive/course-archive.component.html @@ -12,11 +12,11 @@

- + @if (isSortAscending) { - + } @else { - + } diff --git a/src/test/javascript/spec/component/course/course-archive.component.spec.ts b/src/test/javascript/spec/component/course/course-archive.component.spec.ts new file mode 100644 index 000000000000..a576756b08be --- /dev/null +++ b/src/test/javascript/spec/component/course/course-archive.component.spec.ts @@ -0,0 +1,206 @@ +import { HttpHeaders, HttpResponse } from '@angular/common/http'; +import { HttpTestingController } from '@angular/common/http/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { TranslateService } from '@ngx-translate/core'; +import { CourseManagementService } from 'app/course/manage/course-management.service'; +import { Course } from 'app/entities/course.model'; +import { ArtemisDatePipe } from 'app/shared/pipes/artemis-date.pipe'; +import { MockComponent, MockDirective, MockPipe } from 'ng-mocks'; +import { LocalStorageService, SessionStorageService } from 'ngx-webstorage'; +import { MockHasAnyAuthorityDirective } from '../../helpers/mocks/directive/mock-has-any-authority.directive'; +import { MockSyncStorage } from '../../helpers/mocks/service/mock-sync-storage.service'; +import { MockTranslateService } from '../../helpers/mocks/service/mock-translate.service'; +import { ArtemisTestModule } from '../../test.module'; +import { ArtemisTranslatePipe } from 'app/shared/pipes/artemis-translate.pipe'; +import { SortByDirective } from 'app/shared/sort/sort-by.directive'; +import { SortDirective } from 'app/shared/sort/sort.directive'; +import { of } from 'rxjs'; +import { By } from '@angular/platform-browser'; +import { CourseArchiveComponent } from 'app/overview/course-archive/course-archive.component'; +import { CourseCardHeaderComponent } from 'app/overview/course-card-header/course-card-header.component'; + +const course1: Course = { id: 1, semester: 'WS21/22' }; +const course2: Course = { id: 2, semester: 'WS21/22' }; +const course3: Course = { id: 3, semester: 'SS22' }; +const course4: Course = { id: 4, semester: 'SS22' }; +const course5: Course = { id: 5, semester: 'WS23/24' }; +const course6: Course = { id: 6, semester: 'SS19' }; +const course7: Course = { id: 7, semester: 'WS22/23' }; +const courses: Course[] = [course1, course2, course3, course4, course5, course6, course7]; + +describe('CourseArchiveComponent', () => { + let component: CourseArchiveComponent; + let fixture: ComponentFixture; + let courseService: CourseManagementService; + let httpMock: HttpTestingController; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ArtemisTestModule], + declarations: [ + CourseArchiveComponent, + MockDirective(MockHasAnyAuthorityDirective), + MockPipe(ArtemisTranslatePipe), + MockDirective(SortDirective), + MockDirective(SortByDirective), + MockPipe(ArtemisDatePipe), + MockComponent(CourseCardHeaderComponent), + ], + providers: [ + { provide: LocalStorageService, useClass: MockSyncStorage }, + { provide: SessionStorageService, useClass: MockSyncStorage }, + { provide: TranslateService, useClass: MockTranslateService }, + ], + }) + .compileComponents() + .then(() => { + fixture = TestBed.createComponent(CourseArchiveComponent); + component = fixture.componentInstance; + courseService = TestBed.inject(CourseManagementService); + httpMock = TestBed.inject(HttpTestingController); + fixture.detectChanges(); + }); + }); + + afterEach(() => { + component.ngOnDestroy(); + jest.restoreAllMocks(); + }); + + describe('onInit', () => { + it('should call loadArchivedCourses on init', () => { + const loadArchivedCoursesSpy = jest.spyOn(component, 'loadArchivedCourses'); + + component.ngOnInit(); + + expect(loadArchivedCoursesSpy).toHaveBeenCalledOnce(); + }); + + it('should load archived courses on init', () => { + const getCoursesForArchiveSpy = jest.spyOn(courseService, 'getCoursesForArchive'); + getCoursesForArchiveSpy.mockReturnValue(of(new HttpResponse({ body: courses, headers: new HttpHeaders() }))); + + component.ngOnInit(); + + expect(component.courses).toEqual(courses); + expect(component.courses).toHaveLength(7); + }); + + it('should handle an empty response body correctly when fetching all courses for archive', () => { + const emptyCourses: Course[] = []; + const getCoursesForArchiveSpy = jest.spyOn(courseService, 'getCoursesForArchive'); + + const req = httpMock.expectOne({ method: 'GET', url: `api/courses/archive` }); + component.ngOnInit(); + + expect(getCoursesForArchiveSpy).toHaveBeenCalledOnce(); + req.flush(null); + expect(component.courses).toStrictEqual(emptyCourses); + }); + + it('should sort the name of the semesters uniquely', () => { + const getCoursesForArchiveSpy = jest.spyOn(courseService, 'getCoursesForArchive'); + getCoursesForArchiveSpy.mockReturnValue(of(new HttpResponse({ body: courses, headers: new HttpHeaders() }))); + component.ngOnInit(); + + expect(getCoursesForArchiveSpy).toHaveBeenCalledOnce(); + + expect(component.semesters).toHaveLength(5); + expect(component.semesters[0]).toBe('WS23/24'); + expect(component.semesters[1]).toBe('WS22/23'); + expect(component.semesters[2]).toBe('SS22'); + expect(component.semesters[3]).toBe('WS21/22'); + expect(component.semesters[4]).toBe('SS19'); + }); + + it('should map courses into semesters', () => { + const getCoursesForArchiveSpy = jest.spyOn(courseService, 'getCoursesForArchive'); + getCoursesForArchiveSpy.mockReturnValue(of(new HttpResponse({ body: courses, headers: new HttpHeaders() }))); + const mapCoursesIntoSemestersSpy = jest.spyOn(component, 'mapCoursesIntoSemesters'); + component.ngOnInit(); + + expect(getCoursesForArchiveSpy).toHaveBeenCalledOnce(); + expect(mapCoursesIntoSemestersSpy).toHaveBeenCalledOnce(); + + expect(component.coursesBySemester).toStrictEqual({ + 'WS23/24': [course5], + 'WS22/23': [course7], + SS22: [course3, course4], + 'WS21/22': [course1, course2], + SS19: [course6], + }); + }); + + it('should initialize collapse state of semesters correctly', () => { + const getCoursesForArchiveSpy = jest.spyOn(courseService, 'getCoursesForArchive'); + getCoursesForArchiveSpy.mockReturnValue(of(new HttpResponse({ body: courses, headers: new HttpHeaders() }))); + const mapCoursesIntoSemestersSpy = jest.spyOn(component, 'mapCoursesIntoSemesters'); + component.ngOnInit(); + + expect(getCoursesForArchiveSpy).toHaveBeenCalledOnce(); + expect(mapCoursesIntoSemestersSpy).toHaveBeenCalledOnce(); + + // we expand the newest semester at first, others are collapsed + expect(component.semesterCollapsed).toStrictEqual({ + 'WS23/24': false, + 'WS22/23': true, + SS22: true, + 'WS21/22': true, + SS19: true, + }); + }); + + it('should initialize translate of semesters correctly', () => { + const getCoursesForArchiveSpy = jest.spyOn(courseService, 'getCoursesForArchive'); + getCoursesForArchiveSpy.mockReturnValue(of(new HttpResponse({ body: courses, headers: new HttpHeaders() }))); + const mapCoursesIntoSemestersSpy = jest.spyOn(component, 'mapCoursesIntoSemesters'); + component.ngOnInit(); + + expect(getCoursesForArchiveSpy).toHaveBeenCalledOnce(); + expect(mapCoursesIntoSemestersSpy).toHaveBeenCalledOnce(); + + // we expand the newest semester at first, others are collapsed + expect(component.fullFormOfSemesterStrings).toStrictEqual({ + 'WS23/24': 'artemisApp.course.archive.winterSemester', + 'WS22/23': 'artemisApp.course.archive.winterSemester', + SS22: 'artemisApp.course.archive.summerSemester', + 'WS21/22': 'artemisApp.course.archive.winterSemester', + SS19: 'artemisApp.course.archive.summerSemester', + }); + }); + + it('should toggle sort order and update the icon accordingly', async () => { + const getCoursesForArchiveSpy = jest.spyOn(courseService, 'getCoursesForArchive'); + getCoursesForArchiveSpy.mockReturnValue(of(new HttpResponse({ body: courses, headers: new HttpHeaders() }))); + const mapCoursesIntoSemestersSpy = jest.spyOn(component, 'mapCoursesIntoSemesters'); + component.ngOnInit(); + + fixture.detectChanges(); + await fixture.whenStable(); + + expect(getCoursesForArchiveSpy).toHaveBeenCalledOnce(); + expect(mapCoursesIntoSemestersSpy).toHaveBeenCalledOnce(); + expect(component.courses).toBeDefined(); + expect(component.courses).toHaveLength(7); + + const onSortSpy = jest.spyOn(component, 'onSort'); + const button = fixture.debugElement.nativeElement.querySelector('#sort-test'); + + expect(button).not.toBeNull(); + button.click(); + fixture.detectChanges(); + + expect(onSortSpy).toHaveBeenCalled(); + expect(component.isSortAscending).toBeFalse(); + expect(component.semesters[4]).toBe('WS23/24'); + expect(component.semesters[3]).toBe('WS22/23'); + expect(component.semesters[2]).toBe('SS22'); + expect(component.semesters[1]).toBe('WS21/22'); + expect(component.semesters[0]).toBe('SS19'); + + const iconComponent = fixture.debugElement.query(By.css('#icon-test')).componentInstance; + + expect(iconComponent.icon).toBe(component.faArrowUpAZ); + }); + }); +}); From e45e7b10b99de34a63a227d2bc1f0a896c3bafb4 Mon Sep 17 00:00:00 2001 From: EgeDoguKaya Date: Fri, 20 Sep 2024 18:37:51 +0200 Subject: [PATCH 49/74] Remove comment --- .../spec/component/course/course-archive.component.spec.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/test/javascript/spec/component/course/course-archive.component.spec.ts b/src/test/javascript/spec/component/course/course-archive.component.spec.ts index a576756b08be..5167afbdb271 100644 --- a/src/test/javascript/spec/component/course/course-archive.component.spec.ts +++ b/src/test/javascript/spec/component/course/course-archive.component.spec.ts @@ -159,7 +159,6 @@ describe('CourseArchiveComponent', () => { expect(getCoursesForArchiveSpy).toHaveBeenCalledOnce(); expect(mapCoursesIntoSemestersSpy).toHaveBeenCalledOnce(); - // we expand the newest semester at first, others are collapsed expect(component.fullFormOfSemesterStrings).toStrictEqual({ 'WS23/24': 'artemisApp.course.archive.winterSemester', 'WS22/23': 'artemisApp.course.archive.winterSemester', From b255621218a673279c04c4e857d7abc2e3273f8b Mon Sep 17 00:00:00 2001 From: EgeDoguKaya Date: Fri, 20 Sep 2024 19:28:25 +0200 Subject: [PATCH 50/74] Change signature --- .../app/overview/course-archive/course-archive.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/webapp/app/overview/course-archive/course-archive.component.ts b/src/main/webapp/app/overview/course-archive/course-archive.component.ts index 77d75cb9d433..c19c7029f2b1 100644 --- a/src/main/webapp/app/overview/course-archive/course-archive.component.ts +++ b/src/main/webapp/app/overview/course-archive/course-archive.component.ts @@ -66,7 +66,7 @@ export class CourseArchiveComponent implements OnInit, OnDestroy { /** * maps existing courses to each semester */ - private mapCoursesIntoSemesters(): void { + mapCoursesIntoSemesters(): void { this.semesterCollapsed = {}; this.coursesBySemester = {}; this.fullFormOfSemesterStrings = {}; From 06f25611b50c0c025729afd1640112602099cc9d Mon Sep 17 00:00:00 2001 From: EgeDoguKaya Date: Fri, 20 Sep 2024 23:53:42 +0200 Subject: [PATCH 51/74] Use toList --- .../java/de/tum/cit/aet/artemis/core/service/CourseService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/de/tum/cit/aet/artemis/core/service/CourseService.java b/src/main/java/de/tum/cit/aet/artemis/core/service/CourseService.java index dd2c4873a69c..2e212bf48a7a 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/service/CourseService.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/service/CourseService.java @@ -662,7 +662,7 @@ public List getAllCoursesForManagementOverview(boolean onlyActive) { public List getAllCoursesForCourseArchive() { var user = userRepository.getUserWithGroupsAndAuthorities(); List courses = courseRepository.findAll(); - return courses.stream().filter(course -> authCheckService.isAtLeastStudentInCourse(course, user) && course.getSemester() != null).collect(Collectors.toList()); + return courses.stream().filter(course -> authCheckService.isAtLeastStudentInCourse(course, user) && course.getSemester() != null).toList(); } /** From 16bf7524a3caadba73bcbf150218cd5514f49ee8 Mon Sep 17 00:00:00 2001 From: EgeDoguKaya Date: Sat, 21 Sep 2024 16:59:51 +0200 Subject: [PATCH 52/74] integrate rabbit feedback --- .../course-archive.component.html | 9 +++--- .../webapp/i18n/de/student-dashboard.json | 2 +- .../webapp/i18n/en/student-dashboard.json | 2 +- .../course/course-archive.component.spec.ts | 32 ++++++++++++++++--- 4 files changed, 35 insertions(+), 10 deletions(-) diff --git a/src/main/webapp/app/overview/course-archive/course-archive.component.html b/src/main/webapp/app/overview/course-archive/course-archive.component.html index e2ae0f4943b7..a9b20192be4f 100644 --- a/src/main/webapp/app/overview/course-archive/course-archive.component.html +++ b/src/main/webapp/app/overview/course-archive/course-archive.component.html @@ -10,13 +10,13 @@

ngbTooltip="{{ 'artemisApp.course.archive.tip' | artemisTranslate }}" />
-
+
- + @if (isSortAscending) { - + } @else { - + } @@ -32,6 +32,7 @@

class="d-flex justify-content-between align-items-center px-3 mb-2" [ngClass]="{ 'mb-2': !(last && semesterCollapsed[semester]) }" (click)="semesterCollapsed[semester] = !semesterCollapsed[semester]" + tabindex="0" role="button" > diff --git a/src/main/webapp/i18n/de/student-dashboard.json b/src/main/webapp/i18n/de/student-dashboard.json index 2133f3081e90..29ea115cda60 100644 --- a/src/main/webapp/i18n/de/student-dashboard.json +++ b/src/main/webapp/i18n/de/student-dashboard.json @@ -12,7 +12,7 @@ "noStatistics": "Keine Statistik verfügbar", "cardNoExerciseLabel": "Keine Übung geplant", "cardExerciseLabel": "Nächste Übung:", - "oldCourses": "Suchst du nach alten Kursen? Klicke ", + "oldCourses": "Suchst du nach alten Kursen? Klicke", "here": "hier", "enroll": { "title": "Kurseinschreibung", diff --git a/src/main/webapp/i18n/en/student-dashboard.json b/src/main/webapp/i18n/en/student-dashboard.json index 11f851639cb9..05053936a491 100644 --- a/src/main/webapp/i18n/en/student-dashboard.json +++ b/src/main/webapp/i18n/en/student-dashboard.json @@ -12,7 +12,7 @@ "noStatistics": "No statistics available", "cardNoExerciseLabel": "No exercise planned", "cardExerciseLabel": "Next Exercise:", - "oldCourses": "Looking for old courses? Click ", + "oldCourses": "Looking for old courses? Click", "here": "here", "enroll": { "title": "Course Enrollment", diff --git a/src/test/javascript/spec/component/course/course-archive.component.spec.ts b/src/test/javascript/spec/component/course/course-archive.component.spec.ts index 5167afbdb271..0f30266dbabf 100644 --- a/src/test/javascript/spec/component/course/course-archive.component.spec.ts +++ b/src/test/javascript/spec/component/course/course-archive.component.spec.ts @@ -19,7 +19,7 @@ import { By } from '@angular/platform-browser'; import { CourseArchiveComponent } from 'app/overview/course-archive/course-archive.component'; import { CourseCardHeaderComponent } from 'app/overview/course-card-header/course-card-header.component'; -const course1: Course = { id: 1, semester: 'WS21/22' }; +const course1: Course = { id: 1, semester: 'WS21/22', title: 'iPraktikum' }; const course2: Course = { id: 2, semester: 'WS21/22' }; const course3: Course = { id: 3, semester: 'SS22' }; const course4: Course = { id: 4, semester: 'SS22' }; @@ -90,8 +90,8 @@ describe('CourseArchiveComponent', () => { const emptyCourses: Course[] = []; const getCoursesForArchiveSpy = jest.spyOn(courseService, 'getCoursesForArchive'); - const req = httpMock.expectOne({ method: 'GET', url: `api/courses/archive` }); component.ngOnInit(); + const req = httpMock.expectOne({ method: 'GET', url: `api/courses/archive` }); expect(getCoursesForArchiveSpy).toHaveBeenCalledOnce(); req.flush(null); @@ -126,7 +126,7 @@ describe('CourseArchiveComponent', () => { 'WS23/24': [course5], 'WS22/23': [course7], SS22: [course3, course4], - 'WS21/22': [course1, course2], + 'WS21/22': [course2, course1], SS19: [course6], }); }); @@ -168,6 +168,29 @@ describe('CourseArchiveComponent', () => { }); }); + it('should collapse semester groups based on the search value correctly', () => { + const getCoursesForArchiveSpy = jest.spyOn(courseService, 'getCoursesForArchive'); + getCoursesForArchiveSpy.mockReturnValue(of(new HttpResponse({ body: courses, headers: new HttpHeaders() }))); + const mapCoursesIntoSemestersSpy = jest.spyOn(component, 'mapCoursesIntoSemesters'); + component.ngOnInit(); + + expect(getCoursesForArchiveSpy).toHaveBeenCalledOnce(); + expect(mapCoursesIntoSemestersSpy).toHaveBeenCalledOnce(); + + const expandOrCollapseBasedOnSearchValueSpy = jest.spyOn(component, 'expandOrCollapseBasedOnSearchValue'); + component.setSearchValue('iPraktikum'); + + expect(expandOrCollapseBasedOnSearchValueSpy).toHaveBeenCalledOnce(); + // Every semester accordion should be collapsed except WS21/22, because iPraktikum is in semester WS21/22 + expect(component.semesterCollapsed).toStrictEqual({ + 'WS23/24': true, + 'WS22/23': true, + SS22: true, + 'WS21/22': false, + SS19: true, + }); + }); + it('should toggle sort order and update the icon accordingly', async () => { const getCoursesForArchiveSpy = jest.spyOn(courseService, 'getCoursesForArchive'); getCoursesForArchiveSpy.mockReturnValue(of(new HttpResponse({ body: courses, headers: new HttpHeaders() }))); @@ -197,8 +220,9 @@ describe('CourseArchiveComponent', () => { expect(component.semesters[1]).toBe('WS21/22'); expect(component.semesters[0]).toBe('SS19'); - const iconComponent = fixture.debugElement.query(By.css('#icon-test')).componentInstance; + const iconComponent = fixture.debugElement.query(By.css('#icon-test-up')).componentInstance; + expect(iconComponent).not.toBeNull(); expect(iconComponent.icon).toBe(component.faArrowUpAZ); }); }); From 2d54c39c4e01e3de4c1dadb30719deaaeaf314e5 Mon Sep 17 00:00:00 2001 From: EgeDoguKaya Date: Sat, 21 Sep 2024 17:13:36 +0200 Subject: [PATCH 53/74] Fix one archive test --- .../spec/component/course/course-archive.component.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/javascript/spec/component/course/course-archive.component.spec.ts b/src/test/javascript/spec/component/course/course-archive.component.spec.ts index 0f30266dbabf..2ea0b7c27789 100644 --- a/src/test/javascript/spec/component/course/course-archive.component.spec.ts +++ b/src/test/javascript/spec/component/course/course-archive.component.spec.ts @@ -90,8 +90,8 @@ describe('CourseArchiveComponent', () => { const emptyCourses: Course[] = []; const getCoursesForArchiveSpy = jest.spyOn(courseService, 'getCoursesForArchive'); - component.ngOnInit(); const req = httpMock.expectOne({ method: 'GET', url: `api/courses/archive` }); + component.ngOnInit(); expect(getCoursesForArchiveSpy).toHaveBeenCalledOnce(); req.flush(null); From 18847758bc0e6c9590d57da38a8db00fce1748bc Mon Sep 17 00:00:00 2001 From: EgeDoguKaya Date: Mon, 23 Sep 2024 13:31:41 +0200 Subject: [PATCH 54/74] Integrate feedback --- .../artemis/core/repository/CourseRepository.java | 12 ++++++++++++ .../cit/aet/artemis/core/service/CourseService.java | 4 ++-- .../course-archive/course-archive.component.ts | 2 +- src/main/webapp/app/overview/courses.component.html | 7 +++---- src/main/webapp/i18n/de/student-dashboard.json | 6 ++++-- src/main/webapp/i18n/en/student-dashboard.json | 6 ++++-- 6 files changed, 26 insertions(+), 11 deletions(-) diff --git a/src/main/java/de/tum/cit/aet/artemis/core/repository/CourseRepository.java b/src/main/java/de/tum/cit/aet/artemis/core/repository/CourseRepository.java index fb6db0e164b3..f13373f754c8 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/repository/CourseRepository.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/repository/CourseRepository.java @@ -542,4 +542,16 @@ SELECT COUNT(c) > 0 """) boolean hasLearningPathsEnabled(@Param("courseId") long courseId); + @Query(""" + SELECT DISTINCT c + FROM Course c + JOIN UserGroup ug ON c.studentGroupName = ug.group + OR c.teachingAssistantGroupName = ug.group + OR c.editorGroupName = ug.group + OR c.instructorGroupName = ug.group + WHERE (ug.userId = :userId OR :isAdmin = TRUE) + AND c.semester IS NOT NULL + """) + List findCoursesForUserRolesWithNonNullSemester(@Param("userId") Long userId, @Param("isAdmin") boolean isAdmin); + } diff --git a/src/main/java/de/tum/cit/aet/artemis/core/service/CourseService.java b/src/main/java/de/tum/cit/aet/artemis/core/service/CourseService.java index 2e212bf48a7a..b5d9fb66162d 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/service/CourseService.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/service/CourseService.java @@ -661,8 +661,8 @@ public List getAllCoursesForManagementOverview(boolean onlyActive) { */ public List getAllCoursesForCourseArchive() { var user = userRepository.getUserWithGroupsAndAuthorities(); - List courses = courseRepository.findAll(); - return courses.stream().filter(course -> authCheckService.isAtLeastStudentInCourse(course, user) && course.getSemester() != null).toList(); + boolean isAdmin = authCheckService.isAdmin(user); + return courseRepository.findCoursesForUserRolesWithNonNullSemester(user.getId(), isAdmin); } /** diff --git a/src/main/webapp/app/overview/course-archive/course-archive.component.ts b/src/main/webapp/app/overview/course-archive/course-archive.component.ts index c19c7029f2b1..6c32ee408389 100644 --- a/src/main/webapp/app/overview/course-archive/course-archive.component.ts +++ b/src/main/webapp/app/overview/course-archive/course-archive.component.ts @@ -13,7 +13,7 @@ import { ArtemisSharedComponentModule } from 'app/shared/components/shared-compo import { CourseCardHeaderComponent } from '../course-card-header/course-card-header.component'; @Component({ - selector: 'course-archive', + selector: 'jhi-course-archive', templateUrl: './course-archive.component.html', styleUrls: ['./course-archive.component.scss'], standalone: true, diff --git a/src/main/webapp/app/overview/courses.component.html b/src/main/webapp/app/overview/courses.component.html index 00859527fe8a..9c5eec4d0929 100644 --- a/src/main/webapp/app/overview/courses.component.html +++ b/src/main/webapp/app/overview/courses.component.html @@ -61,9 +61,8 @@

-
- {{ 'artemisApp.studentDashboard.oldCourses' | artemisTranslate }} - {{ 'artemisApp.studentDashboard.here' | artemisTranslate }} -
+
+
 
+

} diff --git a/src/main/webapp/i18n/de/student-dashboard.json b/src/main/webapp/i18n/de/student-dashboard.json index 29ea115cda60..81580af1d05d 100644 --- a/src/main/webapp/i18n/de/student-dashboard.json +++ b/src/main/webapp/i18n/de/student-dashboard.json @@ -12,8 +12,10 @@ "noStatistics": "Keine Statistik verfügbar", "cardNoExerciseLabel": "Keine Übung geplant", "cardExerciseLabel": "Nächste Übung:", - "oldCourses": "Suchst du nach alten Kursen? Klicke", - "here": "hier", + "archive": { + "oldCourses": "Suchst du nach alten Kursen? Klicke", + "here": "hier" + }, "enroll": { "title": "Kurseinschreibung", "enrollSuccessful": "Einschreibung erfolgreich", diff --git a/src/main/webapp/i18n/en/student-dashboard.json b/src/main/webapp/i18n/en/student-dashboard.json index 05053936a491..01569acac205 100644 --- a/src/main/webapp/i18n/en/student-dashboard.json +++ b/src/main/webapp/i18n/en/student-dashboard.json @@ -12,8 +12,10 @@ "noStatistics": "No statistics available", "cardNoExerciseLabel": "No exercise planned", "cardExerciseLabel": "Next Exercise:", - "oldCourses": "Looking for old courses? Click", - "here": "here", + "archive": { + "oldCourses": "Looking for old courses? Click", + "here": "here" + }, "enroll": { "title": "Course Enrollment", "enrollSuccessful": "Enrollment successful", From d8de68af3afd4215457f7aefd0fb724d8ef6b3a1 Mon Sep 17 00:00:00 2001 From: EgeDoguKaya Date: Tue, 24 Sep 2024 18:02:48 +0200 Subject: [PATCH 55/74] Integrate feedback, add server tests --- .../core/repository/CourseRepository.java | 9 ++ .../artemis/core/service/CourseService.java | 7 +- .../manage/course-management.service.ts | 9 +- .../course-archive.component.html | 90 ++++++++++--------- .../aet/artemis/course/CourseTestService.java | 47 ++++++++++ .../CourseGitlabJenkinsIntegrationTest.java | 12 +++ 6 files changed, 126 insertions(+), 48 deletions(-) diff --git a/src/main/java/de/tum/cit/aet/artemis/core/repository/CourseRepository.java b/src/main/java/de/tum/cit/aet/artemis/core/repository/CourseRepository.java index f13373f754c8..1b2a1660b4d2 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/repository/CourseRepository.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/repository/CourseRepository.java @@ -542,6 +542,15 @@ SELECT COUNT(c) > 0 """) boolean hasLearningPathsEnabled(@Param("courseId") long courseId); + /** + * Retrieves all courses that the user has access to based on their role + * or if they are an admin. Filters out any courses that do not belong to + * a specific semester (i.e., have a null semester). + * + * @param userId The id of the user whose courses are being retrieved + * @param isAdmin A boolean flag indicating whether the user is an admin + * @return A list of courses that the user has access to and belong to a specific semester + */ @Query(""" SELECT DISTINCT c FROM Course c diff --git a/src/main/java/de/tum/cit/aet/artemis/core/service/CourseService.java b/src/main/java/de/tum/cit/aet/artemis/core/service/CourseService.java index b5d9fb66162d..e11fb82ae194 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/service/CourseService.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/service/CourseService.java @@ -653,11 +653,10 @@ public List getAllCoursesForManagementOverview(boolean onlyActive) { } /** - * Fetches all courses, filters out courses that the user does not have - * access to based on their role, and excludes any courses that do not - * belong to a specific semester. + * Retrieves all courses from non-null semesters that the current user is enrolled in + * for the course archive. * - * @return A list of courses for the course archive + * @return A list of courses for the course archive. */ public List getAllCoursesForCourseArchive() { var user = userRepository.getUserWithGroupsAndAuthorities(); diff --git a/src/main/webapp/app/course/manage/course-management.service.ts b/src/main/webapp/app/course/manage/course-management.service.ts index 097775459db0..f8cfc6201b71 100644 --- a/src/main/webapp/app/course/manage/course-management.service.ts +++ b/src/main/webapp/app/course/manage/course-management.service.ts @@ -742,7 +742,14 @@ export class CourseManagementService { } // If years are the same, sort WS over SS - return semesterA.slice(0, 2) === 'WS' ? -1 : 1; + const prefixA = semesterA.slice(0, 2); + const prefixB = semesterB.slice(0, 2); + + if (prefixA === prefixB) { + return 0; // Both semesters are the same (either both WS or both SS) + } + + return prefixA === 'WS' ? -1 : 1; // WS should be placed above SS }) ); } diff --git a/src/main/webapp/app/overview/course-archive/course-archive.component.html b/src/main/webapp/app/overview/course-archive/course-archive.component.html index a9b20192be4f..b4c7d5f6bacb 100644 --- a/src/main/webapp/app/overview/course-archive/course-archive.component.html +++ b/src/main/webapp/app/overview/course-archive/course-archive.component.html @@ -1,4 +1,4 @@ -@if (courses && courses.length) { +@if (courses) {
@@ -10,53 +10,57 @@

ngbTooltip="{{ 'artemisApp.course.archive.tip' | artemisTranslate }}" />
-
-
- @for (semester of semesters; track semester; let last = $last) { -
- - -
- @if (!semesterCollapsed[semester]) { -
-
- @for (course of coursesBySemester[semester] | searchFilter: ['title'] : searchCourseText; track course) { -
- -
- } -
+ @if (courses.length) { +
+ @for (semester of semesters; track semester; let last = $last) { +
+ +
+ @if (!semesterCollapsed[semester]) { +
+
+ @for (course of coursesBySemester[semester] | searchFilter: ['title'] : searchCourseText; track course) { +
+ +
+ } +
+
+ } + @if (!last) { +
+ } } - @if (!last) { -
- } - } -
-
-} @else { -
-

+
+ } @else { +
+

+
+ }
} diff --git a/src/test/java/de/tum/cit/aet/artemis/course/CourseTestService.java b/src/test/java/de/tum/cit/aet/artemis/course/CourseTestService.java index 9bc46ee31d1e..b1475a494cf2 100644 --- a/src/test/java/de/tum/cit/aet/artemis/course/CourseTestService.java +++ b/src/test/java/de/tum/cit/aet/artemis/course/CourseTestService.java @@ -3387,4 +3387,51 @@ public void testGetCoursesForImport() throws Exception { assertThat(found).as("Course is available").isPresent(); } } + + // Test + public void testGetAllCoursesForCourseArchiveWithNonNullSemesters() throws Exception { + List expectedOldCourses = new ArrayList<>(); + for (int i = 1; i <= 4; i++) { + expectedOldCourses.add(courseUtilService.createCourse((long) i)); + } + + expectedOldCourses.get(0).setSemester("SS20"); + expectedOldCourses.get(1).setSemester("SS21"); + expectedOldCourses.get(2).setSemester("WS21/22"); + expectedOldCourses.get(3).setSemester(null); // will be filtered out + + for (Course oldCourse : expectedOldCourses) { + courseRepo.save(oldCourse); + } + + final var actualOldCourses = request.getList("/api/courses/archive", HttpStatus.OK, Course.class); + assertThat(actualOldCourses).as("Course archive has 3 courses").hasSize(3); + assertThat(actualOldCourses).as("Course archive has the correct semesters").extracting("semester").containsExactlyInAnyOrder(expectedOldCourses.get(0).getSemester(), + expectedOldCourses.get(1).getSemester(), expectedOldCourses.get(2).getSemester()); + assertThat(actualOldCourses).as("Course archive got the correct courses").extracting("id").containsExactlyInAnyOrder(expectedOldCourses.get(0).getId(), + expectedOldCourses.get(1).getId(), expectedOldCourses.get(2).getId()); + assertThat(actualOldCourses).as("Course archive does not contain a null semester").doesNotContain(expectedOldCourses.get(3)); + } + + // Test + public void testGetAllCoursesForCourseArchiveForUnenrolledStudent() throws Exception { + Course course1 = courseUtilService.createCourse((long) 1); + course1.setSemester("SS20"); + courseRepo.save(course1); + + Course course2 = courseUtilService.createCourse((long) 2); + course2.setSemester("SS21"); + courseRepo.save(course2); + + Course course3 = courseUtilService.createCourse((long) 3); + course3.setSemester("WS21/22"); + courseRepo.save(course3); + + // remove student from all courses + removeAllGroupsFromStudent1(); + + var actualCoursesForStudent = request.getList("/api/courses/archive", HttpStatus.OK, Course.class); + assertThat(actualCoursesForStudent).as("Course archive does not show any courses to the user removed from these courses").hasSize(0); + } + } diff --git a/src/test/java/de/tum/cit/aet/artemis/exercise/programming/CourseGitlabJenkinsIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/exercise/programming/CourseGitlabJenkinsIntegrationTest.java index bbd5a09f9d22..2630312d03a7 100644 --- a/src/test/java/de/tum/cit/aet/artemis/exercise/programming/CourseGitlabJenkinsIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/exercise/programming/CourseGitlabJenkinsIntegrationTest.java @@ -1060,4 +1060,16 @@ void testGetCoursesForImport_asAdmin() throws Exception { void testFindAllOnlineCoursesForLtiDashboard() throws Exception { courseTestService.testFindAllOnlineCoursesForLtiDashboard(); } + + @Test + @WithMockUser(username = TEST_PREFIX + "student1", roles = "USER") + void testGetAllCoursesForCourseArchiveWithNonNullSemesters() throws Exception { + courseTestService.testGetAllCoursesForCourseArchiveWithNonNullSemesters(); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "student1", roles = "USER") + void testGetAllCoursesForCourseArchiveForUnenrolledStudent() throws Exception { + courseTestService.testGetAllCoursesForCourseArchiveForUnenrolledStudent(); + } } From 33ff96b480f883211059abeaae8a538919cff866 Mon Sep 17 00:00:00 2001 From: EgeDoguKaya Date: Wed, 25 Sep 2024 17:20:01 +0200 Subject: [PATCH 56/74] Integrate collapse state, make the semesters unclickable when there is no match --- .../course/manage/course-management.service.ts | 9 +++++++++ .../course-archive.component.html | 8 ++++---- .../course-archive/course-archive.component.ts | 18 ++++++++++++++++++ 3 files changed, 31 insertions(+), 4 deletions(-) diff --git a/src/main/webapp/app/course/manage/course-management.service.ts b/src/main/webapp/app/course/manage/course-management.service.ts index f8cfc6201b71..f3c87185d039 100644 --- a/src/main/webapp/app/course/manage/course-management.service.ts +++ b/src/main/webapp/app/course/manage/course-management.service.ts @@ -753,4 +753,13 @@ export class CourseManagementService { }) ); } + + getSemesterCollapseStateFromStorage(storageId: string): boolean { + const storedCollapseState: string | null = localStorage.getItem('semester.collapseState.' + storageId); + return storedCollapseState ? JSON.parse(storedCollapseState) : false; + } + + setSemesterCollapseState(storageId: string, isCollapsed: boolean) { + localStorage.setItem('semester.collapseState.' + storageId, JSON.stringify(isCollapsed)); + } } diff --git a/src/main/webapp/app/overview/course-archive/course-archive.component.html b/src/main/webapp/app/overview/course-archive/course-archive.component.html index b4c7d5f6bacb..5e61ad0ca473 100644 --- a/src/main/webapp/app/overview/course-archive/course-archive.component.html +++ b/src/main/webapp/app/overview/course-archive/course-archive.component.html @@ -32,17 +32,17 @@

@for (semester of semesters; track semester; let last = $last) {
@if (!semesterCollapsed[semester]) { -
+
@for (course of coursesBySemester[semester] | searchFilter: ['title'] : searchCourseText; track course) {
diff --git a/src/main/webapp/app/overview/course-archive/course-archive.component.ts b/src/main/webapp/app/overview/course-archive/course-archive.component.ts index 6c32ee408389..4e974cf3314a 100644 --- a/src/main/webapp/app/overview/course-archive/course-archive.component.ts +++ b/src/main/webapp/app/overview/course-archive/course-archive.component.ts @@ -74,6 +74,7 @@ export class CourseArchiveComponent implements OnInit, OnDestroy { let isCollapsed = false; for (const semester of this.semesters) { this.semesterCollapsed[semester] = isCollapsed; + this.courseService.setSemesterCollapseState(semester, isCollapsed); this.coursesBySemester[semester] = this.courses.filter((course) => course.semester === semester); this.fullFormOfSemesterStrings[semester] = semester.startsWith('WS') ? 'artemisApp.course.archive.winterSemester' : 'artemisApp.course.archive.summerSemester'; isCollapsed = true; @@ -89,6 +90,8 @@ export class CourseArchiveComponent implements OnInit, OnDestroy { this.searchCourseText = searchValue; if (searchValue !== '') { this.expandOrCollapseBasedOnSearchValue(); + } else { + this.getCollapseStateForSemesters(); } } @@ -107,4 +110,19 @@ export class CourseArchiveComponent implements OnInit, OnDestroy { this.semesterCollapsed[semester] = !hasMatchingCourse; } } + + getCollapseStateForSemesters(): void { + for (const semester of this.semesters) { + this.semesterCollapsed[semester] = this.courseService.getSemesterCollapseStateFromStorage(semester); + } + } + + toggleCollapseState(semester: string): void { + this.semesterCollapsed[semester] = !this.semesterCollapsed[semester]; + this.courseService.setSemesterCollapseState(semester, this.semesterCollapsed[semester]); + } + + isCourseFoundInSemester(semester: string): boolean { + return this.coursesBySemester[semester].some((course) => course.title?.toLowerCase().includes(this.searchCourseText.toLowerCase())); + } } From 7c8f749d2425ba495d037e19045d5dd42ddff6b9 Mon Sep 17 00:00:00 2001 From: EgeDoguKaya Date: Wed, 25 Sep 2024 17:21:17 +0200 Subject: [PATCH 57/74] clean up --- .../app/overview/course-archive/course-archive.component.scss | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/main/webapp/app/overview/course-archive/course-archive.component.scss b/src/main/webapp/app/overview/course-archive/course-archive.component.scss index 5cd15d3c5222..8f5c6148ae13 100644 --- a/src/main/webapp/app/overview/course-archive/course-archive.component.scss +++ b/src/main/webapp/app/overview/course-archive/course-archive.component.scss @@ -1,7 +1,3 @@ -.table { - cursor: pointer; -} - .course-grid { display: grid; // cards can shrink to 325px From 0267c51f128619bb5d1ad82e032baaaa655cf921 Mon Sep 17 00:00:00 2001 From: EgeDoguKaya Date: Thu, 26 Sep 2024 15:42:03 +0200 Subject: [PATCH 58/74] Fix recently accessed courses --- .../core/repository/CourseRepository.java | 4 ++- .../artemis/core/service/CourseService.java | 2 +- .../app/overview/course-overview.component.ts | 35 +++++++++++++------ .../aet/artemis/course/CourseTestService.java | 8 ++++- .../CourseGitlabJenkinsIntegrationTest.java | 2 +- 5 files changed, 37 insertions(+), 14 deletions(-) diff --git a/src/main/java/de/tum/cit/aet/artemis/core/repository/CourseRepository.java b/src/main/java/de/tum/cit/aet/artemis/core/repository/CourseRepository.java index 1b2a1660b4d2..ed5bdecb36c5 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/repository/CourseRepository.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/repository/CourseRepository.java @@ -549,6 +549,7 @@ SELECT COUNT(c) > 0 * * @param userId The id of the user whose courses are being retrieved * @param isAdmin A boolean flag indicating whether the user is an admin + * @param now The current time to check if the course is still active * @return A list of courses that the user has access to and belong to a specific semester */ @Query(""" @@ -560,7 +561,8 @@ SELECT COUNT(c) > 0 OR c.instructorGroupName = ug.group WHERE (ug.userId = :userId OR :isAdmin = TRUE) AND c.semester IS NOT NULL + AND (c.endDate IS NOT NULL AND c.endDate < :now) """) - List findCoursesForUserRolesWithNonNullSemester(@Param("userId") Long userId, @Param("isAdmin") boolean isAdmin); + List findCoursesForUserRolesWithNonNullSemester(@Param("userId") Long userId, @Param("isAdmin") boolean isAdmin, @Param("now") ZonedDateTime now); } diff --git a/src/main/java/de/tum/cit/aet/artemis/core/service/CourseService.java b/src/main/java/de/tum/cit/aet/artemis/core/service/CourseService.java index e11fb82ae194..0669dcf6ab0b 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/service/CourseService.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/service/CourseService.java @@ -661,7 +661,7 @@ public List getAllCoursesForManagementOverview(boolean onlyActive) { public List getAllCoursesForCourseArchive() { var user = userRepository.getUserWithGroupsAndAuthorities(); boolean isAdmin = authCheckService.isAdmin(user); - return courseRepository.findCoursesForUserRolesWithNonNullSemester(user.getId(), isAdmin); + return courseRepository.findCoursesForUserRolesWithNonNullSemester(user.getId(), isAdmin, ZonedDateTime.now()); } /** diff --git a/src/main/webapp/app/overview/course-overview.component.ts b/src/main/webapp/app/overview/course-overview.component.ts index 7cf4c805712e..b511ab867a7c 100644 --- a/src/main/webapp/app/overview/course-overview.component.ts +++ b/src/main/webapp/app/overview/course-overview.component.ts @@ -211,16 +211,20 @@ export class CourseOverviewComponent implements OnInit, OnDestroy, AfterViewInit this.course = this.courseStorageService.getCourse(this.courseId); this.isNotManagementView = !this.router.url.startsWith('/course-management'); // Notify the course access storage service that the course has been accessed - this.courseAccessStorageService.onCourseAccessed( - this.courseId, - CourseAccessStorageService.STORAGE_KEY, - CourseAccessStorageService.MAX_DISPLAYED_RECENTLY_ACCESSED_COURSES_OVERVIEW, - ); - this.courseAccessStorageService.onCourseAccessed( - this.courseId, - CourseAccessStorageService.STORAGE_KEY_DROPDOWN, - CourseAccessStorageService.MAX_DISPLAYED_RECENTLY_ACCESSED_COURSES_DROPDOWN, - ); + // If course is not active, it means that it is accessed from course archive, which should not + // be stored in local storage and therefore displayed in recently accessed + if (this.course && this.isCourseActive(this.course)) { + this.courseAccessStorageService.onCourseAccessed( + this.courseId, + CourseAccessStorageService.STORAGE_KEY, + CourseAccessStorageService.MAX_DISPLAYED_RECENTLY_ACCESSED_COURSES_OVERVIEW, + ); + this.courseAccessStorageService.onCourseAccessed( + this.courseId, + CourseAccessStorageService.STORAGE_KEY_DROPDOWN, + CourseAccessStorageService.MAX_DISPLAYED_RECENTLY_ACCESSED_COURSES_DROPDOWN, + ); + } await firstValueFrom(this.loadCourse()); await this.initAfterCourseLoad(); @@ -789,4 +793,15 @@ export class CourseOverviewComponent implements OnInit, OnDestroy, AfterViewInit this.isNavbarCollapsed = !this.isNavbarCollapsed; localStorage.setItem('navbar.collapseState', JSON.stringify(this.isNavbarCollapsed)); } + + /** + * A course is active if the end date is after the current date or + * end date is not set at all + * + * @param course The given course to be checked if it is active + * @returns true if the course is active, otherwise false + */ + isCourseActive(course: Course): boolean { + return course.endDate ? dayjs(course.endDate).isAfter(dayjs()) : true; + } } diff --git a/src/test/java/de/tum/cit/aet/artemis/course/CourseTestService.java b/src/test/java/de/tum/cit/aet/artemis/course/CourseTestService.java index b1475a494cf2..a3a5a9b1b80d 100644 --- a/src/test/java/de/tum/cit/aet/artemis/course/CourseTestService.java +++ b/src/test/java/de/tum/cit/aet/artemis/course/CourseTestService.java @@ -3389,15 +3389,18 @@ public void testGetCoursesForImport() throws Exception { } // Test - public void testGetAllCoursesForCourseArchiveWithNonNullSemesters() throws Exception { + public void testGetAllCoursesForCourseArchiveWithNonNullSemestersAndEndDate() throws Exception { List expectedOldCourses = new ArrayList<>(); for (int i = 1; i <= 4; i++) { expectedOldCourses.add(courseUtilService.createCourse((long) i)); } expectedOldCourses.get(0).setSemester("SS20"); + expectedOldCourses.get(0).setEndDate(ZonedDateTime.now().minusDays(10)); expectedOldCourses.get(1).setSemester("SS21"); + expectedOldCourses.get(1).setEndDate(ZonedDateTime.now().minusDays(10)); expectedOldCourses.get(2).setSemester("WS21/22"); + expectedOldCourses.get(2).setEndDate(ZonedDateTime.now().minusDays(10)); expectedOldCourses.get(3).setSemester(null); // will be filtered out for (Course oldCourse : expectedOldCourses) { @@ -3417,14 +3420,17 @@ public void testGetAllCoursesForCourseArchiveWithNonNullSemesters() throws Excep public void testGetAllCoursesForCourseArchiveForUnenrolledStudent() throws Exception { Course course1 = courseUtilService.createCourse((long) 1); course1.setSemester("SS20"); + course1.setEndDate(ZonedDateTime.now().minusDays(10)); courseRepo.save(course1); Course course2 = courseUtilService.createCourse((long) 2); course2.setSemester("SS21"); + course2.setEndDate(ZonedDateTime.now().minusDays(10)); courseRepo.save(course2); Course course3 = courseUtilService.createCourse((long) 3); course3.setSemester("WS21/22"); + course3.setEndDate(ZonedDateTime.now().minusDays(10)); courseRepo.save(course3); // remove student from all courses diff --git a/src/test/java/de/tum/cit/aet/artemis/exercise/programming/CourseGitlabJenkinsIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/exercise/programming/CourseGitlabJenkinsIntegrationTest.java index 2630312d03a7..88c1b3206873 100644 --- a/src/test/java/de/tum/cit/aet/artemis/exercise/programming/CourseGitlabJenkinsIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/exercise/programming/CourseGitlabJenkinsIntegrationTest.java @@ -1064,7 +1064,7 @@ void testFindAllOnlineCoursesForLtiDashboard() throws Exception { @Test @WithMockUser(username = TEST_PREFIX + "student1", roles = "USER") void testGetAllCoursesForCourseArchiveWithNonNullSemesters() throws Exception { - courseTestService.testGetAllCoursesForCourseArchiveWithNonNullSemesters(); + courseTestService.testGetAllCoursesForCourseArchiveWithNonNullSemestersAndEndDate(); } @Test From de095bff7d601c31ef5fa65b4a0c0b30b9d94e8f Mon Sep 17 00:00:00 2001 From: EgeDoguKaya Date: Thu, 26 Sep 2024 16:15:11 +0200 Subject: [PATCH 59/74] integrate rabbit feedback + update comment --- .../core/repository/CourseRepository.java | 16 +++++++++------- .../aet/artemis/core/service/CourseService.java | 2 +- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/main/java/de/tum/cit/aet/artemis/core/repository/CourseRepository.java b/src/main/java/de/tum/cit/aet/artemis/core/repository/CourseRepository.java index ed5bdecb36c5..263fa4d59fbd 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/repository/CourseRepository.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/repository/CourseRepository.java @@ -555,13 +555,15 @@ SELECT COUNT(c) > 0 @Query(""" SELECT DISTINCT c FROM Course c - JOIN UserGroup ug ON c.studentGroupName = ug.group - OR c.teachingAssistantGroupName = ug.group - OR c.editorGroupName = ug.group - OR c.instructorGroupName = ug.group - WHERE (ug.userId = :userId OR :isAdmin = TRUE) - AND c.semester IS NOT NULL - AND (c.endDate IS NOT NULL AND c.endDate < :now) + LEFT JOIN UserGroup ug ON ug.group IN ( + c.studentGroupName, + c.teachingAssistantGroupName, + c.editorGroupName, + c.instructorGroupName + ) + WHERE (:isAdmin = TRUE OR ug.userId = :userId) + AND c.semester IS NOT NULL + AND (c.endDate IS NOT NULL AND c.endDate < :now) """) List findCoursesForUserRolesWithNonNullSemester(@Param("userId") Long userId, @Param("isAdmin") boolean isAdmin, @Param("now") ZonedDateTime now); diff --git a/src/main/java/de/tum/cit/aet/artemis/core/service/CourseService.java b/src/main/java/de/tum/cit/aet/artemis/core/service/CourseService.java index 0669dcf6ab0b..be762895c3a1 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/service/CourseService.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/service/CourseService.java @@ -653,7 +653,7 @@ public List getAllCoursesForManagementOverview(boolean onlyActive) { } /** - * Retrieves all courses from non-null semesters that the current user is enrolled in + * Retrieves all inactive courses from non-null semesters that the current user is enrolled in * for the course archive. * * @return A list of courses for the course archive. From bc2161ee1609731b4b967f119481658291940b4b Mon Sep 17 00:00:00 2001 From: EgeDoguKaya Date: Thu, 26 Sep 2024 16:20:31 +0200 Subject: [PATCH 60/74] Adjust method name --- .../cit/aet/artemis/core/repository/CourseRepository.java | 5 +++-- .../de/tum/cit/aet/artemis/core/service/CourseService.java | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/main/java/de/tum/cit/aet/artemis/core/repository/CourseRepository.java b/src/main/java/de/tum/cit/aet/artemis/core/repository/CourseRepository.java index 263fa4d59fbd..018cc31ff46c 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/repository/CourseRepository.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/repository/CourseRepository.java @@ -563,8 +563,9 @@ LEFT JOIN UserGroup ug ON ug.group IN ( ) WHERE (:isAdmin = TRUE OR ug.userId = :userId) AND c.semester IS NOT NULL - AND (c.endDate IS NOT NULL AND c.endDate < :now) + AND c.endDate IS NOT NULL + AND c.endDate < :now """) - List findCoursesForUserRolesWithNonNullSemester(@Param("userId") Long userId, @Param("isAdmin") boolean isAdmin, @Param("now") ZonedDateTime now); + List findInactiveCoursesForUserRolesWithNonNullSemester(@Param("userId") long userId, @Param("isAdmin") boolean isAdmin, @Param("now") ZonedDateTime now); } diff --git a/src/main/java/de/tum/cit/aet/artemis/core/service/CourseService.java b/src/main/java/de/tum/cit/aet/artemis/core/service/CourseService.java index be762895c3a1..cdbd7b326546 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/service/CourseService.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/service/CourseService.java @@ -661,7 +661,7 @@ public List getAllCoursesForManagementOverview(boolean onlyActive) { public List getAllCoursesForCourseArchive() { var user = userRepository.getUserWithGroupsAndAuthorities(); boolean isAdmin = authCheckService.isAdmin(user); - return courseRepository.findCoursesForUserRolesWithNonNullSemester(user.getId(), isAdmin, ZonedDateTime.now()); + return courseRepository.findInactiveCoursesForUserRolesWithNonNullSemester(user.getId(), isAdmin, ZonedDateTime.now()); } /** From 794f9d4aab5d9d2c1afc3c7030c30351f1e64849 Mon Sep 17 00:00:00 2001 From: EgeDoguKaya Date: Thu, 26 Sep 2024 18:42:17 +0200 Subject: [PATCH 61/74] fix client test --- .../course-archive/course-archive.component.ts | 10 +++------- .../course/course-archive.component.spec.ts | 13 ++++++++----- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/src/main/webapp/app/overview/course-archive/course-archive.component.ts b/src/main/webapp/app/overview/course-archive/course-archive.component.ts index 4e974cf3314a..5488f2114304 100644 --- a/src/main/webapp/app/overview/course-archive/course-archive.component.ts +++ b/src/main/webapp/app/overview/course-archive/course-archive.component.ts @@ -24,9 +24,9 @@ export class CourseArchiveComponent implements OnInit, OnDestroy { courses: Course[]; semesters: string[]; - fullFormOfSemesterStrings: { [key: string]: string }; - semesterCollapsed: { [key: string]: boolean }; - coursesBySemester: { [key: string]: Course[] }; + fullFormOfSemesterStrings: { [key: string]: string } = {}; + semesterCollapsed: { [key: string]: boolean } = {}; + coursesBySemester: { [key: string]: Course[] } = {}; searchCourseText = ''; isSortAscending = true; iconSize: SizeProp = 'lg'; @@ -67,10 +67,6 @@ export class CourseArchiveComponent implements OnInit, OnDestroy { * maps existing courses to each semester */ mapCoursesIntoSemesters(): void { - this.semesterCollapsed = {}; - this.coursesBySemester = {}; - this.fullFormOfSemesterStrings = {}; - let isCollapsed = false; for (const semester of this.semesters) { this.semesterCollapsed[semester] = isCollapsed; diff --git a/src/test/javascript/spec/component/course/course-archive.component.spec.ts b/src/test/javascript/spec/component/course/course-archive.component.spec.ts index 2ea0b7c27789..1f14a0ded89e 100644 --- a/src/test/javascript/spec/component/course/course-archive.component.spec.ts +++ b/src/test/javascript/spec/component/course/course-archive.component.spec.ts @@ -1,6 +1,6 @@ import { HttpHeaders, HttpResponse } from '@angular/common/http'; import { HttpTestingController } from '@angular/common/http/testing'; -import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing'; import { TranslateService } from '@ngx-translate/core'; import { CourseManagementService } from 'app/course/manage/course-management.service'; import { Course } from 'app/entities/course.model'; @@ -18,6 +18,8 @@ import { of } from 'rxjs'; import { By } from '@angular/platform-browser'; import { CourseArchiveComponent } from 'app/overview/course-archive/course-archive.component'; import { CourseCardHeaderComponent } from 'app/overview/course-card-header/course-card-header.component'; +import { SearchFilterPipe } from 'app/shared/pipes/search-filter.pipe'; +import { SearchFilterComponent } from 'app/shared/search-filter/search-filter.component'; const course1: Course = { id: 1, semester: 'WS21/22', title: 'iPraktikum' }; const course2: Course = { id: 2, semester: 'WS21/22' }; @@ -39,6 +41,8 @@ describe('CourseArchiveComponent', () => { imports: [ArtemisTestModule], declarations: [ CourseArchiveComponent, + SearchFilterPipe, + SearchFilterComponent, MockDirective(MockHasAnyAuthorityDirective), MockPipe(ArtemisTranslatePipe), MockDirective(SortDirective), @@ -191,14 +195,13 @@ describe('CourseArchiveComponent', () => { }); }); - it('should toggle sort order and update the icon accordingly', async () => { + it('should toggle sort order and update the icon accordingly', fakeAsync(() => { const getCoursesForArchiveSpy = jest.spyOn(courseService, 'getCoursesForArchive'); getCoursesForArchiveSpy.mockReturnValue(of(new HttpResponse({ body: courses, headers: new HttpHeaders() }))); const mapCoursesIntoSemestersSpy = jest.spyOn(component, 'mapCoursesIntoSemesters'); component.ngOnInit(); - fixture.detectChanges(); - await fixture.whenStable(); + tick(); expect(getCoursesForArchiveSpy).toHaveBeenCalledOnce(); expect(mapCoursesIntoSemestersSpy).toHaveBeenCalledOnce(); @@ -224,6 +227,6 @@ describe('CourseArchiveComponent', () => { expect(iconComponent).not.toBeNull(); expect(iconComponent.icon).toBe(component.faArrowUpAZ); - }); + })); }); }); From 486d8ade3a8eb512058ae0118cec1fe0d6a1762d Mon Sep 17 00:00:00 2001 From: EgeDoguKaya Date: Sun, 29 Sep 2024 17:15:37 +0200 Subject: [PATCH 62/74] use inject, remove unused imports --- .../course-archive/course-archive.component.ts | 12 ++++-------- .../course-card-header.component.ts | 3 +-- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/src/main/webapp/app/overview/course-archive/course-archive.component.ts b/src/main/webapp/app/overview/course-archive/course-archive.component.ts index 5488f2114304..dba736150954 100644 --- a/src/main/webapp/app/overview/course-archive/course-archive.component.ts +++ b/src/main/webapp/app/overview/course-archive/course-archive.component.ts @@ -1,4 +1,4 @@ -import { Component, OnDestroy, OnInit } from '@angular/core'; +import { Component, OnDestroy, OnInit, inject } from '@angular/core'; import { Course } from 'app/entities/course.model'; import { CourseManagementService } from '../../course/manage/course-management.service'; import { HttpErrorResponse, HttpResponse } from '@angular/common/http'; @@ -9,7 +9,6 @@ import { faAngleDown, faAngleUp, faArrowDownAZ, faArrowUpAZ, faQuestionCircle } import { sortCourses } from 'app/shared/util/course.util'; import { SizeProp } from '@fortawesome/fontawesome-svg-core'; import { ArtemisSharedModule } from 'app/shared/shared.module'; -import { ArtemisSharedComponentModule } from 'app/shared/components/shared-component.module'; import { CourseCardHeaderComponent } from '../course-card-header/course-card-header.component'; @Component({ @@ -17,10 +16,12 @@ import { CourseCardHeaderComponent } from '../course-card-header/course-card-hea templateUrl: './course-archive.component.html', styleUrls: ['./course-archive.component.scss'], standalone: true, - imports: [ArtemisSharedModule, ArtemisSharedComponentModule, CourseCardHeaderComponent], + imports: [ArtemisSharedModule, CourseCardHeaderComponent], }) export class CourseArchiveComponent implements OnInit, OnDestroy { private archiveCourseSubscription: Subscription; + private courseService = inject(CourseManagementService); + private alertService = inject(AlertService); courses: Course[]; semesters: string[]; @@ -38,11 +39,6 @@ export class CourseArchiveComponent implements OnInit, OnDestroy { readonly faArrowUpAZ = faArrowUpAZ; readonly faQuestionCircle = faQuestionCircle; - constructor( - private courseService: CourseManagementService, - private alertService: AlertService, - ) {} - ngOnInit(): void { this.loadArchivedCourses(); this.courseService.enableCourseOverviewBackground(); diff --git a/src/main/webapp/app/overview/course-card-header/course-card-header.component.ts b/src/main/webapp/app/overview/course-card-header/course-card-header.component.ts index 1391893f88b3..598eb33d8b48 100644 --- a/src/main/webapp/app/overview/course-card-header/course-card-header.component.ts +++ b/src/main/webapp/app/overview/course-card-header/course-card-header.component.ts @@ -3,7 +3,6 @@ import { Course } from 'app/entities/course.model'; import { CachingStrategy } from 'app/shared/image/secured-image.component'; import { ARTEMIS_DEFAULT_COLOR } from 'app/app.constants'; import { ArtemisSharedModule } from 'app/shared/shared.module'; -import { ArtemisSharedComponentModule } from 'app/shared/components/shared-component.module'; /** TODO '@edkaya': New course card design also uses this header design. * Therefore, this component will be reused in course-card.component.html @@ -15,7 +14,7 @@ import { ArtemisSharedComponentModule } from 'app/shared/components/shared-compo templateUrl: './course-card-header.component.html', styleUrls: ['./course-card-header.component.scss'], standalone: true, - imports: [ArtemisSharedModule, ArtemisSharedComponentModule], + imports: [ArtemisSharedModule], }) export class CourseCardHeaderComponent implements OnInit { protected readonly ARTEMIS_DEFAULT_COLOR = ARTEMIS_DEFAULT_COLOR; From 933df06da6932b8382c876b5b9501f9bb307e447 Mon Sep 17 00:00:00 2001 From: EgeDoguKaya Date: Fri, 4 Oct 2024 22:16:57 +0200 Subject: [PATCH 63/74] Add course archive dto --- .../artemis/core/dto/CourseForArchiveDTO.java | 7 ++ .../core/repository/CourseRepository.java | 2 +- .../artemis/core/service/CourseService.java | 2 +- .../aet/artemis/core/web/CourseResource.java | 23 ++++-- .../course/manage/course-for-archive-dto.ts | 9 +++ .../manage/course-management.component.ts | 38 +++++++++- .../manage/course-management.service.ts | 7 +- .../course-archive.component.html | 19 +++-- .../course-archive.component.ts | 70 ++++++++++++++----- .../course-card-header.component.html | 48 ++++++------- .../course-card-header.component.ts | 15 ++-- .../app/overview/course-card.component.html | 22 +----- .../app/overview/course-card.component.ts | 9 ++- src/main/webapp/app/overview/course-card.scss | 43 ------------ .../webapp/app/overview/courses.module.ts | 9 +-- 15 files changed, 182 insertions(+), 141 deletions(-) create mode 100644 src/main/java/de/tum/cit/aet/artemis/core/dto/CourseForArchiveDTO.java create mode 100644 src/main/webapp/app/course/manage/course-for-archive-dto.ts diff --git a/src/main/java/de/tum/cit/aet/artemis/core/dto/CourseForArchiveDTO.java b/src/main/java/de/tum/cit/aet/artemis/core/dto/CourseForArchiveDTO.java new file mode 100644 index 000000000000..3d2a5771682d --- /dev/null +++ b/src/main/java/de/tum/cit/aet/artemis/core/dto/CourseForArchiveDTO.java @@ -0,0 +1,7 @@ +package de.tum.cit.aet.artemis.core.dto; + +import com.fasterxml.jackson.annotation.JsonInclude; + +@JsonInclude(JsonInclude.Include.NON_EMPTY) +public record CourseForArchiveDTO(Long id, String title, String semester, String color, String icon) { +} diff --git a/src/main/java/de/tum/cit/aet/artemis/core/repository/CourseRepository.java b/src/main/java/de/tum/cit/aet/artemis/core/repository/CourseRepository.java index 55ef35144467..3da1748ab7aa 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/repository/CourseRepository.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/repository/CourseRepository.java @@ -558,6 +558,6 @@ LEFT JOIN UserGroup ug ON ug.group IN ( AND c.endDate IS NOT NULL AND c.endDate < :now """) - List findInactiveCoursesForUserRolesWithNonNullSemester(@Param("userId") long userId, @Param("isAdmin") boolean isAdmin, @Param("now") ZonedDateTime now); + Set findInactiveCoursesForUserRolesWithNonNullSemester(@Param("userId") long userId, @Param("isAdmin") boolean isAdmin, @Param("now") ZonedDateTime now); } diff --git a/src/main/java/de/tum/cit/aet/artemis/core/service/CourseService.java b/src/main/java/de/tum/cit/aet/artemis/core/service/CourseService.java index 13fde6bd53ef..c3d0b51d6fa2 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/service/CourseService.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/service/CourseService.java @@ -667,7 +667,7 @@ public List getAllCoursesForManagementOverview(boolean onlyActive) { * * @return A list of courses for the course archive. */ - public List getAllCoursesForCourseArchive() { + public Set getAllCoursesForCourseArchive() { var user = userRepository.getUserWithGroupsAndAuthorities(); boolean isAdmin = authCheckService.isAdmin(user); return courseRepository.findInactiveCoursesForUserRolesWithNonNullSemester(user.getId(), isAdmin, ZonedDateTime.now()); diff --git a/src/main/java/de/tum/cit/aet/artemis/core/web/CourseResource.java b/src/main/java/de/tum/cit/aet/artemis/core/web/CourseResource.java index daf4e6d3f2fb..101bbc08d448 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/web/CourseResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/web/CourseResource.java @@ -72,6 +72,7 @@ import de.tum.cit.aet.artemis.core.config.Constants; import de.tum.cit.aet.artemis.core.domain.Course; import de.tum.cit.aet.artemis.core.domain.User; +import de.tum.cit.aet.artemis.core.dto.CourseForArchiveDTO; import de.tum.cit.aet.artemis.core.dto.CourseForDashboardDTO; import de.tum.cit.aet.artemis.core.dto.CourseForImportDTO; import de.tum.cit.aet.artemis.core.dto.CourseManagementDetailViewDTO; @@ -556,14 +557,26 @@ public ResponseEntity> getCoursesForManagementOverview(@RequestPara } /** - * GET /courses/archive : get all courses for course archive + * GET /courses/for-archive : get all courses for course archive * - * @return the ResponseEntity with status 200 (OK) and with body a list of courses (the user has access to) + * @return the ResponseEntity with status 200 (OK) and with body containing + * a set of DTOs, which contain the courses with id, title, semester, color, icon */ - @GetMapping("courses/archive") + @GetMapping("courses/for-archive") @EnforceAtLeastStudent - public ResponseEntity> getCoursesForArchive() { - return ResponseEntity.ok(courseService.getAllCoursesForCourseArchive()); + public ResponseEntity> getCoursesForArchive() { + long start = System.nanoTime(); + User user = userRepository.getUserWithGroupsAndAuthorities(); + log.debug("REST request to get all inactive courses from previous semesters user {} has access to", user.getLogin()); + Set courses = courseService.getAllCoursesForCourseArchive(); + log.debug("courseService.getAllCoursesForCourseArchive done"); + + final Set dto = courses.stream() + .map(course -> new CourseForArchiveDTO(course.getId(), course.getTitle(), course.getSemester(), course.getColor(), course.getCourseIcon())) + .collect(Collectors.toSet()); + + log.info("GET /courses/for-archive took {} for {} courses for user {}", TimeLogUtil.formatDurationFrom(start), courses.size(), user.getLogin()); + return ResponseEntity.ok(dto); } /** diff --git a/src/main/webapp/app/course/manage/course-for-archive-dto.ts b/src/main/webapp/app/course/manage/course-for-archive-dto.ts new file mode 100644 index 000000000000..62a74a8370fd --- /dev/null +++ b/src/main/webapp/app/course/manage/course-for-archive-dto.ts @@ -0,0 +1,9 @@ +export class CourseForArchiveDTO { + id: number; + title: string; + semester: string; + color: string; + icon: string; + + constructor() {} +} diff --git a/src/main/webapp/app/course/manage/course-management.component.ts b/src/main/webapp/app/course/manage/course-management.component.ts index 8d9701e0bb99..e6706bd2b2ee 100644 --- a/src/main/webapp/app/course/manage/course-management.component.ts +++ b/src/main/webapp/app/course/manage/course-management.component.ts @@ -83,7 +83,7 @@ export class CourseManagementComponent implements OnInit, OnDestroy, AfterViewIn this.courses = res.body!.sort((a, b) => (a.title ?? '').localeCompare(b.title ?? '')); this.courseForGuidedTour = this.guidedTourService.enableTourForCourseOverview(this.courses, tutorAssessmentTour, true); - this.courseSemesters = this.courseManagementService.getUniqueSemesterNamesSorted(this.courses); + this.courseSemesters = this.getUniqueSemesterNamesSorted(this.courses); this.sortCoursesIntoSemesters(); // First fetch important data like title for each exercise @@ -99,6 +99,42 @@ export class CourseManagementComponent implements OnInit, OnDestroy, AfterViewIn }); } + /** + * Sorts and returns the semesters by year descending + * WS is sorted above SS + * + * @param coursesWithSemesters the courses to sort the semesters of + * @return An array of sorted semester names + */ + private getUniqueSemesterNamesSorted(coursesWithSemesters: Course[]): string[] { + return ( + coursesWithSemesters + // Test courses get their own section later + .filter((course) => !course.testCourse) + .map((course) => course.semester ?? '') + // Filter down to unique values + .filter((course, index, courses) => courses.indexOf(course) === index) + .sort((semesterA, semesterB) => { + // Sort last if the semester is unset + if (semesterA === '') { + return 1; + } + if (semesterB === '') { + return -1; + } + + // Parse years in base 10 by extracting the two digits after the WS or SS prefix + const yearsCompared = parseInt(semesterB.slice(2, 4), 10) - parseInt(semesterA.slice(2, 4), 10); + if (yearsCompared !== 0) { + return yearsCompared; + } + + // If years are the same, sort WS over SS + return semesterA.slice(0, 2) === 'WS' ? -1 : 1; + }) + ); + } + /** * Sorts the courses into the coursesBySemester map. * Fills the semesterCollapsed map depending on if the semester should be expanded by default. diff --git a/src/main/webapp/app/course/manage/course-management.service.ts b/src/main/webapp/app/course/manage/course-management.service.ts index 87950c0ee284..99103cc9d294 100644 --- a/src/main/webapp/app/course/manage/course-management.service.ts +++ b/src/main/webapp/app/course/manage/course-management.service.ts @@ -27,6 +27,7 @@ import { ScoresStorageService } from 'app/course/course-scores/scores-storage.se import { CourseStorageService } from 'app/course/manage/course-storage.service'; import { ExerciseType, ScoresPerExerciseType } from 'app/entities/exercise.model'; import { OnlineCourseDtoModel } from 'app/lti/online-course-dto.model'; +import { CourseForArchiveDTO } from './course-for-archive-dto'; export type EntityResponseType = HttpResponse; export type EntityArrayResponseType = HttpResponse; @@ -344,10 +345,10 @@ export class CourseManagementService { } /** - * find all courses for the archive using a GET request + * Find all courses for the archive using a GET request */ - getCoursesForArchive(): Observable> { - return this.http.get(`${this.resourceUrl}/archive`, { observe: 'response' }); + getCoursesForArchive(): Observable> { + return this.http.get(`${this.resourceUrl}/for-archive`, { observe: 'response' }); } /** diff --git a/src/main/webapp/app/overview/course-archive/course-archive.component.html b/src/main/webapp/app/overview/course-archive/course-archive.component.html index 5e61ad0ca473..549dae00f917 100644 --- a/src/main/webapp/app/overview/course-archive/course-archive.component.html +++ b/src/main/webapp/app/overview/course-archive/course-archive.component.html @@ -1,4 +1,4 @@ -@if (courses) { +@if (coursesDTO) {
@@ -10,14 +10,14 @@

ngbTooltip="{{ 'artemisApp.course.archive.tip' | artemisTranslate }}" />
- @if (courses.length) { + @if (coursesDTO.length) {
- @if (courses.length) { + @if (coursesDTO.length) {
@for (semester of semesters; track semester; let last = $last) {

@for (course of coursesBySemester[semester] | searchFilter: ['title'] : searchCourseText; track course) {
- +
}
diff --git a/src/main/webapp/app/overview/course-archive/course-archive.component.ts b/src/main/webapp/app/overview/course-archive/course-archive.component.ts index dba736150954..fafa85738163 100644 --- a/src/main/webapp/app/overview/course-archive/course-archive.component.ts +++ b/src/main/webapp/app/overview/course-archive/course-archive.component.ts @@ -5,11 +5,11 @@ import { HttpErrorResponse, HttpResponse } from '@angular/common/http'; import { AlertService } from 'app/core/util/alert.service'; import { onError } from 'app/shared/util/global.utils'; import { Subscription } from 'rxjs'; -import { faAngleDown, faAngleUp, faArrowDownAZ, faArrowUpAZ, faQuestionCircle } from '@fortawesome/free-solid-svg-icons'; -import { sortCourses } from 'app/shared/util/course.util'; +import { faAngleDown, faAngleUp, faArrowDown91, faArrowUp91, faQuestionCircle } from '@fortawesome/free-solid-svg-icons'; import { SizeProp } from '@fortawesome/fontawesome-svg-core'; import { ArtemisSharedModule } from 'app/shared/shared.module'; import { CourseCardHeaderComponent } from '../course-card-header/course-card-header.component'; +import { CourseForArchiveDTO } from 'app/course/manage/course-for-archive-dto'; @Component({ selector: 'jhi-course-archive', @@ -23,7 +23,7 @@ export class CourseArchiveComponent implements OnInit, OnDestroy { private courseService = inject(CourseManagementService); private alertService = inject(AlertService); - courses: Course[]; + coursesDTO: CourseForArchiveDTO[] = []; semesters: string[]; fullFormOfSemesterStrings: { [key: string]: string } = {}; semesterCollapsed: { [key: string]: boolean } = {}; @@ -35,8 +35,8 @@ export class CourseArchiveComponent implements OnInit, OnDestroy { //Icons readonly faAngleDown = faAngleDown; readonly faAngleUp = faAngleUp; - readonly faArrowDownAZ = faArrowDownAZ; - readonly faArrowUpAZ = faArrowUpAZ; + readonly faArrowDown91 = faArrowDown91; + readonly faArrowUp91 = faArrowUp91; readonly faQuestionCircle = faQuestionCircle; ngOnInit(): void { @@ -45,15 +45,17 @@ export class CourseArchiveComponent implements OnInit, OnDestroy { } /** - * Loads all courses that the student has been enrolled in + * Loads all courses that the student has been enrolled in from previous semesters */ loadArchivedCourses(): void { this.archiveCourseSubscription = this.courseService.getCoursesForArchive().subscribe({ - next: (response: HttpResponse) => { - this.courses = response.body || []; - this.courses = sortCourses(this.courses); - this.semesters = this.courseService.getUniqueSemesterNamesSorted(this.courses); - this.mapCoursesIntoSemesters(); + next: (res: HttpResponse) => { + if (res.body) { + this.coursesDTO = res.body || []; + this.coursesDTO = this.sortCoursesByTitle(this.coursesDTO); + this.semesters = this.getUniqueSemesterNamesSorted(this.coursesDTO); + this.mapCoursesIntoSemesters(); + } }, error: (error: HttpErrorResponse) => onError(this.alertService, error), }); @@ -63,13 +65,11 @@ export class CourseArchiveComponent implements OnInit, OnDestroy { * maps existing courses to each semester */ mapCoursesIntoSemesters(): void { - let isCollapsed = false; for (const semester of this.semesters) { - this.semesterCollapsed[semester] = isCollapsed; - this.courseService.setSemesterCollapseState(semester, isCollapsed); - this.coursesBySemester[semester] = this.courses.filter((course) => course.semester === semester); + this.semesterCollapsed[semester] = false; + this.courseService.setSemesterCollapseState(semester, false); + this.coursesBySemester[semester] = this.coursesDTO.filter((course) => course.semester === semester); this.fullFormOfSemesterStrings[semester] = semester.startsWith('WS') ? 'artemisApp.course.archive.winterSemester' : 'artemisApp.course.archive.summerSemester'; - isCollapsed = true; } } @@ -117,4 +117,42 @@ export class CourseArchiveComponent implements OnInit, OnDestroy { isCourseFoundInSemester(semester: string): boolean { return this.coursesBySemester[semester].some((course) => course.title?.toLowerCase().includes(this.searchCourseText.toLowerCase())); } + + sortCoursesByTitle(courses: CourseForArchiveDTO[]): CourseForArchiveDTO[] { + return courses.sort((courseA, courseB) => (courseA.title ?? '').localeCompare(courseB.title ?? '')); + } + + getUniqueSemesterNamesSorted(courses: CourseForArchiveDTO[]): string[] { + return ( + courses + .map((course) => course.semester ?? '') + // filter down to unique values + .filter((course, index, courses) => courses.indexOf(course) === index) + .sort((semesterA, semesterB) => { + // Sort last if the semester is unset + if (semesterA === '') { + return 1; + } + if (semesterB === '') { + return -1; + } + + // Parse years in base 10 by extracting the two digits after the WS or SS prefix + const yearsCompared = parseInt(semesterB.slice(2, 4), 10) - parseInt(semesterA.slice(2, 4), 10); + if (yearsCompared !== 0) { + return yearsCompared; + } + + // If years are the same, sort WS over SS + const prefixA = semesterA.slice(0, 2); + const prefixB = semesterB.slice(0, 2); + + if (prefixA === prefixB) { + return 0; // Both semesters are the same (either both WS or both SS) + } + + return prefixA === 'WS' ? -1 : 1; // WS should be placed above SS + }) + ); + } } diff --git a/src/main/webapp/app/overview/course-card-header/course-card-header.component.html b/src/main/webapp/app/overview/course-card-header/course-card-header.component.html index 2241bbd26a33..69b6b496fa1e 100644 --- a/src/main/webapp/app/overview/course-card-header/course-card-header.component.html +++ b/src/main/webapp/app/overview/course-card-header/course-card-header.component.html @@ -1,29 +1,27 @@ -@if (course()) { -
-
- @if (course().courseIcon) { -
- -
- } @else { -
- {{ course().title | slice: 0 : 1 }} -
- } -
-
- {{ course().title }} -
+
+
+ @if (courseIcon()) { +
+
-
- + } @else { +
+ {{ courseTitle() | slice: 0 : 1 }}
+ } +
+
+ {{ courseTitle() }} +
+
+
+
-} +
diff --git a/src/main/webapp/app/overview/course-card-header/course-card-header.component.ts b/src/main/webapp/app/overview/course-card-header/course-card-header.component.ts index 598eb33d8b48..3b1bb9b1c433 100644 --- a/src/main/webapp/app/overview/course-card-header/course-card-header.component.ts +++ b/src/main/webapp/app/overview/course-card-header/course-card-header.component.ts @@ -1,14 +1,8 @@ import { Component, OnInit, input } from '@angular/core'; -import { Course } from 'app/entities/course.model'; import { CachingStrategy } from 'app/shared/image/secured-image.component'; import { ARTEMIS_DEFAULT_COLOR } from 'app/app.constants'; import { ArtemisSharedModule } from 'app/shared/shared.module'; -/** TODO '@edkaya': New course card design also uses this header design. - * Therefore, this component will be reused in course-card.component.html - * after new course cards are merged into develop. I will refactor its html - * and scss file to avoid duplicates to maintain reusability in the follow-up. - * */ @Component({ selector: 'jhi-course-card-header', templateUrl: './course-card-header.component.html', @@ -18,13 +12,16 @@ import { ArtemisSharedModule } from 'app/shared/shared.module'; }) export class CourseCardHeaderComponent implements OnInit { protected readonly ARTEMIS_DEFAULT_COLOR = ARTEMIS_DEFAULT_COLOR; - course = input.required(); + courseIcon = input.required(); + courseTitle = input.required(); + courseColor = input.required(); + courseId = input.required(); archiveMode = input(false); CachingStrategy = CachingStrategy; - courseColor: string; + color: string; ngOnInit() { - this.courseColor = this.course().color || this.ARTEMIS_DEFAULT_COLOR; + this.color = this.courseColor() || this.ARTEMIS_DEFAULT_COLOR; } } diff --git a/src/main/webapp/app/overview/course-card.component.html b/src/main/webapp/app/overview/course-card.component.html index 4b69401c3d85..51e1df0c089c 100644 --- a/src/main/webapp/app/overview/course-card.component.html +++ b/src/main/webapp/app/overview/course-card.component.html @@ -1,25 +1,5 @@
-
-
- @if (course.courseIcon) { -
- -
- } @else { -
- {{ course.title | slice: 0 : 1 }} -
- } -
-
- {{ course.title }} -
-
-
- -
-
-
+
diff --git a/src/main/webapp/app/overview/course-card.component.ts b/src/main/webapp/app/overview/course-card.component.ts index 043c4a892b48..ce8bbb0bcc7e 100644 --- a/src/main/webapp/app/overview/course-card.component.ts +++ b/src/main/webapp/app/overview/course-card.component.ts @@ -12,11 +12,18 @@ import { ScoresStorageService } from 'app/course/course-scores/scores-storage.se import { ScoreType } from 'app/shared/constants/score-type.constants'; import { CourseScores } from 'app/course/course-scores/course-scores'; import { faArrowRight } from '@fortawesome/free-solid-svg-icons'; +import { CourseCardHeaderComponent } from './course-card-header/course-card-header.component'; +import { ArtemisSharedCommonModule } from 'app/shared/shared-common.module'; +import { NgxChartsModule, PieChartModule } from '@swimlane/ngx-charts'; +import { TranslateDirective } from 'app/shared/language/translate.directive'; +import { RouterLink } from '@angular/router'; @Component({ selector: 'jhi-overview-course-card', templateUrl: './course-card.component.html', styleUrls: ['course-card.scss'], + standalone: true, + imports: [CourseCardHeaderComponent, ArtemisSharedCommonModule, NgxChartsModule, PieChartModule, TranslateDirective, RouterLink], }) export class CourseCardComponent implements OnChanges { protected readonly faArrowRight = faArrowRight; @@ -80,8 +87,6 @@ export class CourseCardComponent implements OnChanges { this.ngxDoughnutData[1].value = scoreNotReached; this.ngxDoughnutData = [...this.ngxDoughnutData]; } - - this.courseColor = this.course.color || this.ARTEMIS_DEFAULT_COLOR; } /** diff --git a/src/main/webapp/app/overview/course-card.scss b/src/main/webapp/app/overview/course-card.scss index 35d910731e5f..6525f3881181 100644 --- a/src/main/webapp/app/overview/course-card.scss +++ b/src/main/webapp/app/overview/course-card.scss @@ -10,49 +10,6 @@ background-color: var(--hover-slightly-darker-body-bg); } - .card-header { - // needed, otherwise hover effect won't work due to stretched-link class - z-index: 2; - position: relative; - height: 85px; - opacity: 1; - filter: alpha(opacity = 100); - transition: 0.15s; - background-color: var(--background-color-for-hover) !important; - // inner border radius : outer border radius - outer border thickness (8px - 1px) - border-top-left-radius: 7px; - border-top-right-radius: 7px; - - &:hover { - background-color: color-mix(in srgb, var(--background-color-for-hover), transparent 15%) !important; - } - - .container { - height: 80px; - - .row { - height: 80px; - } - } - - .card-title { - overflow: hidden; - padding-bottom: 1px; - // matches 4 lines - max-height: 76px; - } - - .course-circle { - // same size as the course icons - height: 65px; - min-width: 65px; - background-color: var(--course-image-bg); - border-radius: 50%; - display: inline-block; - color: var(--bs-body-color); - } - } - .card-body { .information-box-wrapper { height: 135px; diff --git a/src/main/webapp/app/overview/courses.module.ts b/src/main/webapp/app/overview/courses.module.ts index 01e27a171163..8c3c4f3f3616 100644 --- a/src/main/webapp/app/overview/courses.module.ts +++ b/src/main/webapp/app/overview/courses.module.ts @@ -36,15 +36,8 @@ import { ArtemisSidebarModule } from 'app/shared/sidebar/sidebar.module'; NgxChartsModule, PieChartModule, ArtemisSidebarModule, - ], - declarations: [ - CoursesComponent, - CourseOverviewComponent, CourseCardComponent, - CourseExercisesComponent, - CourseLecturesComponent, - CourseLectureRowComponent, - CourseUnenrollmentModalComponent, ], + declarations: [CoursesComponent, CourseOverviewComponent, CourseExercisesComponent, CourseLecturesComponent, CourseLectureRowComponent, CourseUnenrollmentModalComponent], }) export class ArtemisCoursesModule {} From 3a7ec300efb87e2caa20b5caa7a756f03696256b Mon Sep 17 00:00:00 2001 From: EgeDoguKaya Date: Fri, 4 Oct 2024 23:31:49 +0200 Subject: [PATCH 64/74] Remove unused function --- .../manage/course-management.service.ts | 43 ------------------- 1 file changed, 43 deletions(-) diff --git a/src/main/webapp/app/course/manage/course-management.service.ts b/src/main/webapp/app/course/manage/course-management.service.ts index 99103cc9d294..7aeb99ef0c0b 100644 --- a/src/main/webapp/app/course/manage/course-management.service.ts +++ b/src/main/webapp/app/course/manage/course-management.service.ts @@ -712,49 +712,6 @@ export class CourseManagementService { this.courseOverviewSubject.next(false); } - /** - * Sorts and returns the semesters by year descending - * WS is sorted above SS - * - * @param coursesWithSemesters the courses to sort the semesters of - * @return An array of sorted semester names - */ - getUniqueSemesterNamesSorted(coursesWithSemesters: Course[]): string[] { - return ( - coursesWithSemesters - // Test courses get their own section later - .filter((course) => !course.testCourse) - .map((course) => course.semester ?? '') - // Filter down to unique values - .filter((course, index, courses) => courses.indexOf(course) === index) - .sort((semesterA, semesterB) => { - // Sort last if the semester is unset - if (semesterA === '') { - return 1; - } - if (semesterB === '') { - return -1; - } - - // Parse years in base 10 by extracting the two digits after the WS or SS prefix - const yearsCompared = parseInt(semesterB.slice(2, 4), 10) - parseInt(semesterA.slice(2, 4), 10); - if (yearsCompared !== 0) { - return yearsCompared; - } - - // If years are the same, sort WS over SS - const prefixA = semesterA.slice(0, 2); - const prefixB = semesterB.slice(0, 2); - - if (prefixA === prefixB) { - return 0; // Both semesters are the same (either both WS or both SS) - } - - return prefixA === 'WS' ? -1 : 1; // WS should be placed above SS - }) - ); - } - getSemesterCollapseStateFromStorage(storageId: string): boolean { const storedCollapseState: string | null = localStorage.getItem('semester.collapseState.' + storageId); return storedCollapseState ? JSON.parse(storedCollapseState) : false; From 8c9fadc0ad901ef5ed1b636a7c751b121e4c1eb0 Mon Sep 17 00:00:00 2001 From: EgeDoguKaya Date: Fri, 4 Oct 2024 23:45:52 +0200 Subject: [PATCH 65/74] adapt client tests to dto --- .../course/manage/course-for-archive-dto.ts | 2 - .../course-archive.component.html | 4 +- .../course-archive.component.ts | 6 +-- .../course/course-archive.component.spec.ts | 44 +++++++++---------- 4 files changed, 27 insertions(+), 29 deletions(-) diff --git a/src/main/webapp/app/course/manage/course-for-archive-dto.ts b/src/main/webapp/app/course/manage/course-for-archive-dto.ts index 62a74a8370fd..9bce2af6232e 100644 --- a/src/main/webapp/app/course/manage/course-for-archive-dto.ts +++ b/src/main/webapp/app/course/manage/course-for-archive-dto.ts @@ -4,6 +4,4 @@ export class CourseForArchiveDTO { semester: string; color: string; icon: string; - - constructor() {} } diff --git a/src/main/webapp/app/overview/course-archive/course-archive.component.html b/src/main/webapp/app/overview/course-archive/course-archive.component.html index 549dae00f917..9c8febc5900d 100644 --- a/src/main/webapp/app/overview/course-archive/course-archive.component.html +++ b/src/main/webapp/app/overview/course-archive/course-archive.component.html @@ -15,9 +15,9 @@

@if (isSortAscending) { - + } @else { - + } diff --git a/src/main/webapp/app/overview/course-archive/course-archive.component.ts b/src/main/webapp/app/overview/course-archive/course-archive.component.ts index fafa85738163..9e738d5b7715 100644 --- a/src/main/webapp/app/overview/course-archive/course-archive.component.ts +++ b/src/main/webapp/app/overview/course-archive/course-archive.component.ts @@ -5,7 +5,7 @@ import { HttpErrorResponse, HttpResponse } from '@angular/common/http'; import { AlertService } from 'app/core/util/alert.service'; import { onError } from 'app/shared/util/global.utils'; import { Subscription } from 'rxjs'; -import { faAngleDown, faAngleUp, faArrowDown91, faArrowUp91, faQuestionCircle } from '@fortawesome/free-solid-svg-icons'; +import { faAngleDown, faAngleUp, faArrowDown19, faArrowUp19, faQuestionCircle } from '@fortawesome/free-solid-svg-icons'; import { SizeProp } from '@fortawesome/fontawesome-svg-core'; import { ArtemisSharedModule } from 'app/shared/shared.module'; import { CourseCardHeaderComponent } from '../course-card-header/course-card-header.component'; @@ -35,8 +35,8 @@ export class CourseArchiveComponent implements OnInit, OnDestroy { //Icons readonly faAngleDown = faAngleDown; readonly faAngleUp = faAngleUp; - readonly faArrowDown91 = faArrowDown91; - readonly faArrowUp91 = faArrowUp91; + readonly faArrowDown19 = faArrowDown19; + readonly faArrowUp19 = faArrowUp19; readonly faQuestionCircle = faQuestionCircle; ngOnInit(): void { diff --git a/src/test/javascript/spec/component/course/course-archive.component.spec.ts b/src/test/javascript/spec/component/course/course-archive.component.spec.ts index 1f14a0ded89e..498af30842ee 100644 --- a/src/test/javascript/spec/component/course/course-archive.component.spec.ts +++ b/src/test/javascript/spec/component/course/course-archive.component.spec.ts @@ -3,7 +3,6 @@ import { HttpTestingController } from '@angular/common/http/testing'; import { ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing'; import { TranslateService } from '@ngx-translate/core'; import { CourseManagementService } from 'app/course/manage/course-management.service'; -import { Course } from 'app/entities/course.model'; import { ArtemisDatePipe } from 'app/shared/pipes/artemis-date.pipe'; import { MockComponent, MockDirective, MockPipe } from 'ng-mocks'; import { LocalStorageService, SessionStorageService } from 'ngx-webstorage'; @@ -20,15 +19,16 @@ import { CourseArchiveComponent } from 'app/overview/course-archive/course-archi import { CourseCardHeaderComponent } from 'app/overview/course-card-header/course-card-header.component'; import { SearchFilterPipe } from 'app/shared/pipes/search-filter.pipe'; import { SearchFilterComponent } from 'app/shared/search-filter/search-filter.component'; +import { CourseForArchiveDTO } from 'app/course/manage/course-for-archive-dto'; -const course1: Course = { id: 1, semester: 'WS21/22', title: 'iPraktikum' }; -const course2: Course = { id: 2, semester: 'WS21/22' }; -const course3: Course = { id: 3, semester: 'SS22' }; -const course4: Course = { id: 4, semester: 'SS22' }; -const course5: Course = { id: 5, semester: 'WS23/24' }; -const course6: Course = { id: 6, semester: 'SS19' }; -const course7: Course = { id: 7, semester: 'WS22/23' }; -const courses: Course[] = [course1, course2, course3, course4, course5, course6, course7]; +const course1 = { id: 1, semester: 'WS21/22', title: 'iPraktikum' } as CourseForArchiveDTO; +const course2 = { id: 2, semester: 'WS21/22' } as CourseForArchiveDTO; +const course3 = { id: 3, semester: 'SS22' } as CourseForArchiveDTO; +const course4 = { id: 4, semester: 'SS22' } as CourseForArchiveDTO; +const course5 = { id: 5, semester: 'WS23/24' } as CourseForArchiveDTO; +const course6 = { id: 6, semester: 'SS19' } as CourseForArchiveDTO; +const course7 = { id: 7, semester: 'WS22/23' } as CourseForArchiveDTO; +const courses: CourseForArchiveDTO[] = [course1, course2, course3, course4, course5, course6, course7]; describe('CourseArchiveComponent', () => { let component: CourseArchiveComponent; @@ -86,20 +86,20 @@ describe('CourseArchiveComponent', () => { component.ngOnInit(); - expect(component.courses).toEqual(courses); - expect(component.courses).toHaveLength(7); + expect(component.coursesDTO).toEqual(courses); + expect(component.coursesDTO).toHaveLength(7); }); it('should handle an empty response body correctly when fetching all courses for archive', () => { - const emptyCourses: Course[] = []; + const emptyCourses: CourseForArchiveDTO[] = []; const getCoursesForArchiveSpy = jest.spyOn(courseService, 'getCoursesForArchive'); - const req = httpMock.expectOne({ method: 'GET', url: `api/courses/archive` }); + const req = httpMock.expectOne({ method: 'GET', url: `api/courses/for-archive` }); component.ngOnInit(); expect(getCoursesForArchiveSpy).toHaveBeenCalledOnce(); req.flush(null); - expect(component.courses).toStrictEqual(emptyCourses); + expect(component.coursesDTO).toStrictEqual(emptyCourses); }); it('should sort the name of the semesters uniquely', () => { @@ -144,13 +144,13 @@ describe('CourseArchiveComponent', () => { expect(getCoursesForArchiveSpy).toHaveBeenCalledOnce(); expect(mapCoursesIntoSemestersSpy).toHaveBeenCalledOnce(); - // we expand the newest semester at first, others are collapsed + // we expand all semesters at first expect(component.semesterCollapsed).toStrictEqual({ 'WS23/24': false, - 'WS22/23': true, - SS22: true, - 'WS21/22': true, - SS19: true, + 'WS22/23': false, + SS22: false, + 'WS21/22': false, + SS19: false, }); }); @@ -205,8 +205,8 @@ describe('CourseArchiveComponent', () => { expect(getCoursesForArchiveSpy).toHaveBeenCalledOnce(); expect(mapCoursesIntoSemestersSpy).toHaveBeenCalledOnce(); - expect(component.courses).toBeDefined(); - expect(component.courses).toHaveLength(7); + expect(component.coursesDTO).toBeDefined(); + expect(component.coursesDTO).toHaveLength(7); const onSortSpy = jest.spyOn(component, 'onSort'); const button = fixture.debugElement.nativeElement.querySelector('#sort-test'); @@ -226,7 +226,7 @@ describe('CourseArchiveComponent', () => { const iconComponent = fixture.debugElement.query(By.css('#icon-test-up')).componentInstance; expect(iconComponent).not.toBeNull(); - expect(iconComponent.icon).toBe(component.faArrowUpAZ); + expect(iconComponent.icon).toBe(component.faArrowUp19); })); }); }); From a4fe0930e1c25f37e565a8313931f9fa90aa68f9 Mon Sep 17 00:00:00 2001 From: EgeDoguKaya Date: Sat, 5 Oct 2024 00:08:38 +0200 Subject: [PATCH 66/74] Adapt server tests to dto --- .../cit/aet/artemis/core/repository/CourseRepository.java | 2 +- .../tum/cit/aet/artemis/core/util/CourseTestService.java | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/main/java/de/tum/cit/aet/artemis/core/repository/CourseRepository.java b/src/main/java/de/tum/cit/aet/artemis/core/repository/CourseRepository.java index 3da1748ab7aa..decece538a7d 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/repository/CourseRepository.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/repository/CourseRepository.java @@ -542,7 +542,7 @@ SELECT COUNT(c) > 0 * @param userId The id of the user whose courses are being retrieved * @param isAdmin A boolean flag indicating whether the user is an admin * @param now The current time to check if the course is still active - * @return A list of courses that the user has access to and belong to a specific semester + * @return A set of courses that the user has access to and belong to a specific semester */ @Query(""" SELECT DISTINCT c diff --git a/src/test/java/de/tum/cit/aet/artemis/core/util/CourseTestService.java b/src/test/java/de/tum/cit/aet/artemis/core/util/CourseTestService.java index 3f9407ff06d5..6d88196631cc 100644 --- a/src/test/java/de/tum/cit/aet/artemis/core/util/CourseTestService.java +++ b/src/test/java/de/tum/cit/aet/artemis/core/util/CourseTestService.java @@ -90,6 +90,7 @@ import de.tum.cit.aet.artemis.core.domain.CourseInformationSharingConfiguration; import de.tum.cit.aet.artemis.core.domain.Organization; import de.tum.cit.aet.artemis.core.domain.User; +import de.tum.cit.aet.artemis.core.dto.CourseForArchiveDTO; import de.tum.cit.aet.artemis.core.dto.CourseForDashboardDTO; import de.tum.cit.aet.artemis.core.dto.CourseForImportDTO; import de.tum.cit.aet.artemis.core.dto.CourseManagementDetailViewDTO; @@ -3404,13 +3405,14 @@ public void testGetAllCoursesForCourseArchiveWithNonNullSemestersAndEndDate() th courseRepo.save(oldCourse); } - final var actualOldCourses = request.getList("/api/courses/archive", HttpStatus.OK, Course.class); + final Set actualOldCourses = request.getSet("/api/courses/for-archive", HttpStatus.OK, CourseForArchiveDTO.class); assertThat(actualOldCourses).as("Course archive has 3 courses").hasSize(3); assertThat(actualOldCourses).as("Course archive has the correct semesters").extracting("semester").containsExactlyInAnyOrder(expectedOldCourses.get(0).getSemester(), expectedOldCourses.get(1).getSemester(), expectedOldCourses.get(2).getSemester()); assertThat(actualOldCourses).as("Course archive got the correct courses").extracting("id").containsExactlyInAnyOrder(expectedOldCourses.get(0).getId(), expectedOldCourses.get(1).getId(), expectedOldCourses.get(2).getId()); - assertThat(actualOldCourses).as("Course archive does not contain a null semester").doesNotContain(expectedOldCourses.get(3)); + Optional notFound = actualOldCourses.stream().filter(c -> Objects.equals(c.id(), expectedOldCourses.get(3).getId())).findFirst(); + assertThat(notFound).as("Course archive did not fetch the last course").isNotPresent(); } // Test @@ -3433,7 +3435,7 @@ public void testGetAllCoursesForCourseArchiveForUnenrolledStudent() throws Excep // remove student from all courses removeAllGroupsFromStudent1(); - var actualCoursesForStudent = request.getList("/api/courses/archive", HttpStatus.OK, Course.class); + final Set actualCoursesForStudent = request.getSet("/api/courses/for-archive", HttpStatus.OK, CourseForArchiveDTO.class); assertThat(actualCoursesForStudent).as("Course archive does not show any courses to the user removed from these courses").hasSize(0); } From 817c873704e0c36f6257c986cb7a3cacb4f6a577 Mon Sep 17 00:00:00 2001 From: EgeDoguKaya Date: Sat, 5 Oct 2024 00:15:52 +0200 Subject: [PATCH 67/74] add javadoc for dto --- .../cit/aet/artemis/core/dto/CourseForArchiveDTO.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/main/java/de/tum/cit/aet/artemis/core/dto/CourseForArchiveDTO.java b/src/main/java/de/tum/cit/aet/artemis/core/dto/CourseForArchiveDTO.java index 3d2a5771682d..0e055d558a62 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/dto/CourseForArchiveDTO.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/dto/CourseForArchiveDTO.java @@ -2,6 +2,15 @@ import com.fasterxml.jackson.annotation.JsonInclude; +/** + * DTO for representing archived courses from previous semesters. + * + * @param id The id of the course + * @param title The title of the course + * @param semester The semester in which the course was offered + * @param color The background color of the course + * @param icon The icon of the course + */ @JsonInclude(JsonInclude.Include.NON_EMPTY) public record CourseForArchiveDTO(Long id, String title, String semester, String color, String icon) { } From 61da73d3b3b4c7805919a2f24e5a4c0cea253995 Mon Sep 17 00:00:00 2001 From: EgeDoguKaya Date: Sat, 5 Oct 2024 00:32:23 +0200 Subject: [PATCH 68/74] fix one client test --- .../spec/component/course/course-management.component.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/javascript/spec/component/course/course-management.component.spec.ts b/src/test/javascript/spec/component/course/course-management.component.spec.ts index 4812d0cb54d5..2fbff71142e1 100644 --- a/src/test/javascript/spec/component/course/course-management.component.spec.ts +++ b/src/test/javascript/spec/component/course/course-management.component.spec.ts @@ -148,7 +148,7 @@ describe('CourseManagementComponent', () => { const course5 = { id: 5, semester: '' } as Course; // course with no semester component.courses = [course1, course2, course3, course4, course5]; - const sortedSemesters = service.getUniqueSemesterNamesSorted(component.courses); + const sortedSemesters = component.getUniqueSemesterNamesSorted(component.courses); expect(sortedSemesters).toEqual(['WS20', 'SS20', 'WS19', 'SS19', '']); }); From 56bec8215fa4dcd73a38f1bfd69475376bd2ba2a Mon Sep 17 00:00:00 2001 From: EgeDoguKaya Date: Sat, 5 Oct 2024 01:17:14 +0200 Subject: [PATCH 69/74] fix --- .../spec/component/course/course-management.component.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/javascript/spec/component/course/course-management.component.spec.ts b/src/test/javascript/spec/component/course/course-management.component.spec.ts index 2fbff71142e1..b1b33b125911 100644 --- a/src/test/javascript/spec/component/course/course-management.component.spec.ts +++ b/src/test/javascript/spec/component/course/course-management.component.spec.ts @@ -148,7 +148,7 @@ describe('CourseManagementComponent', () => { const course5 = { id: 5, semester: '' } as Course; // course with no semester component.courses = [course1, course2, course3, course4, course5]; - const sortedSemesters = component.getUniqueSemesterNamesSorted(component.courses); + const sortedSemesters = component['getUniqueSemesterNamesSorted'](component.courses); expect(sortedSemesters).toEqual(['WS20', 'SS20', 'WS19', 'SS19', '']); }); From 59ba4c66b1d09489c0b67e55b82d4a7c7ac0ea6c Mon Sep 17 00:00:00 2001 From: EgeDoguKaya Date: Sat, 5 Oct 2024 19:05:14 +0200 Subject: [PATCH 70/74] Fix client tests --- .../spec/component/course/course-overview.component.spec.ts | 2 -- .../spec/component/overview/course-card.component.spec.ts | 2 ++ .../integration/guided-tour/guided-tour.integration.spec.ts | 2 ++ 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/test/javascript/spec/component/course/course-overview.component.spec.ts b/src/test/javascript/spec/component/course/course-overview.component.spec.ts index b411f3760596..831c52ca20a2 100644 --- a/src/test/javascript/spec/component/course/course-overview.component.spec.ts +++ b/src/test/javascript/spec/component/course/course-overview.component.spec.ts @@ -14,7 +14,6 @@ import { ArtemisDatePipe } from 'app/shared/pipes/artemis-date.pipe'; import { CourseExerciseRowComponent } from 'app/overview/course-exercises/course-exercise-row.component'; import { CourseExercisesComponent } from 'app/overview/course-exercises/course-exercises.component'; import { CourseRegistrationComponent } from 'app/overview/course-registration/course-registration.component'; -import { CourseCardComponent } from 'app/overview/course-card.component'; import dayjs from 'dayjs/esm'; import { Exercise } from 'app/entities/exercise.model'; import { DueDateStat } from 'app/course/dashboards/due-date-stat.model'; @@ -185,7 +184,6 @@ describe('CourseOverviewComponent', () => { MockComponent(CourseExerciseRowComponent), MockComponent(CourseExercisesComponent), MockComponent(CourseRegistrationComponent), - MockComponent(CourseCardComponent), MockComponent(SecuredImageComponent), ], providers: [ diff --git a/src/test/javascript/spec/component/overview/course-card.component.spec.ts b/src/test/javascript/spec/component/overview/course-card.component.spec.ts index fbe16f160c51..bf2403aecf23 100644 --- a/src/test/javascript/spec/component/overview/course-card.component.spec.ts +++ b/src/test/javascript/spec/component/overview/course-card.component.spec.ts @@ -15,6 +15,7 @@ import { PieChartModule } from '@swimlane/ngx-charts'; import { TranslateDirective } from 'app/shared/language/translate.directive'; import { ScoresStorageService } from 'app/course/course-scores/scores-storage.service'; import { CourseScores } from 'app/course/course-scores/course-scores'; +import { CourseCardHeaderComponent } from 'app/overview/course-card-header/course-card-header.component'; describe('CourseCardComponent', () => { let fixture: ComponentFixture; @@ -40,6 +41,7 @@ describe('CourseCardComponent', () => { MockPipe(ArtemisTimeAgoPipe), MockRouterLinkDirective, MockComponent(SecuredImageComponent), + MockComponent(CourseCardHeaderComponent), MockDirective(TranslateDirective), ], }) diff --git a/src/test/javascript/spec/integration/guided-tour/guided-tour.integration.spec.ts b/src/test/javascript/spec/integration/guided-tour/guided-tour.integration.spec.ts index 57b934b9497c..4884b7fea188 100644 --- a/src/test/javascript/spec/integration/guided-tour/guided-tour.integration.spec.ts +++ b/src/test/javascript/spec/integration/guided-tour/guided-tour.integration.spec.ts @@ -21,6 +21,7 @@ import { ThemeSwitchComponent } from 'app/core/theme/theme-switch.component'; import { User } from 'app/core/user/user.model'; import { MockHasAnyAuthorityDirective } from '../../helpers/mocks/directive/mock-has-any-authority.directive'; import { CourseCardComponent } from 'app/overview/course-card.component'; +import { CourseCardHeaderComponent } from 'app/overview/course-card-header/course-card-header.component'; import { Course } from 'app/entities/course.model'; import { ARTEMIS_DEFAULT_COLOR } from 'app/app.constants'; import { ExerciseService } from 'app/exercises/shared/exercise/exercise.service'; @@ -79,6 +80,7 @@ describe('Guided tour integration', () => { FooterComponent, NotificationSidebarComponent, MockHasAnyAuthorityDirective, + MockComponent(CourseCardHeaderComponent), MockComponent(CourseRegistrationComponent), MockComponent(CourseExerciseRowComponent), MockComponent(LoadingNotificationComponent), From a4b92495a52add8f1706bc102dd6d13529b2c845 Mon Sep 17 00:00:00 2001 From: EgeDoguKaya Date: Wed, 9 Oct 2024 18:15:22 +0200 Subject: [PATCH 71/74] integrate feedback --- .../de/tum/cit/aet/artemis/core/dto/CourseForArchiveDTO.java | 2 +- .../java/de/tum/cit/aet/artemis/core/web/CourseResource.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/de/tum/cit/aet/artemis/core/dto/CourseForArchiveDTO.java b/src/main/java/de/tum/cit/aet/artemis/core/dto/CourseForArchiveDTO.java index 0e055d558a62..c0b003e668bc 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/dto/CourseForArchiveDTO.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/dto/CourseForArchiveDTO.java @@ -12,5 +12,5 @@ * @param icon The icon of the course */ @JsonInclude(JsonInclude.Include.NON_EMPTY) -public record CourseForArchiveDTO(Long id, String title, String semester, String color, String icon) { +public record CourseForArchiveDTO(long id, String title, String semester, String color, String icon) { } diff --git a/src/main/java/de/tum/cit/aet/artemis/core/web/CourseResource.java b/src/main/java/de/tum/cit/aet/artemis/core/web/CourseResource.java index 101bbc08d448..da9757b1837f 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/web/CourseResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/web/CourseResource.java @@ -575,7 +575,7 @@ public ResponseEntity> getCoursesForArchive() { .map(course -> new CourseForArchiveDTO(course.getId(), course.getTitle(), course.getSemester(), course.getColor(), course.getCourseIcon())) .collect(Collectors.toSet()); - log.info("GET /courses/for-archive took {} for {} courses for user {}", TimeLogUtil.formatDurationFrom(start), courses.size(), user.getLogin()); + log.debug("GET /courses/for-archive took {} for {} courses for user {}", TimeLogUtil.formatDurationFrom(start), courses.size(), user.getLogin()); return ResponseEntity.ok(dto); } From 4017d0eb35ead59bd50c711fc6b5cc2ff6ac8700 Mon Sep 17 00:00:00 2001 From: EgeDoguKaya Date: Fri, 11 Oct 2024 16:04:55 +0200 Subject: [PATCH 72/74] change variable name in client tests --- .../course/course-archive.component.spec.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/test/javascript/spec/component/course/course-archive.component.spec.ts b/src/test/javascript/spec/component/course/course-archive.component.spec.ts index 498af30842ee..efbbffd92e27 100644 --- a/src/test/javascript/spec/component/course/course-archive.component.spec.ts +++ b/src/test/javascript/spec/component/course/course-archive.component.spec.ts @@ -86,8 +86,8 @@ describe('CourseArchiveComponent', () => { component.ngOnInit(); - expect(component.coursesDTO).toEqual(courses); - expect(component.coursesDTO).toHaveLength(7); + expect(component.courses).toEqual(courses); + expect(component.courses).toHaveLength(7); }); it('should handle an empty response body correctly when fetching all courses for archive', () => { @@ -99,7 +99,7 @@ describe('CourseArchiveComponent', () => { expect(getCoursesForArchiveSpy).toHaveBeenCalledOnce(); req.flush(null); - expect(component.coursesDTO).toStrictEqual(emptyCourses); + expect(component.courses).toStrictEqual(emptyCourses); }); it('should sort the name of the semesters uniquely', () => { @@ -205,8 +205,8 @@ describe('CourseArchiveComponent', () => { expect(getCoursesForArchiveSpy).toHaveBeenCalledOnce(); expect(mapCoursesIntoSemestersSpy).toHaveBeenCalledOnce(); - expect(component.coursesDTO).toBeDefined(); - expect(component.coursesDTO).toHaveLength(7); + expect(component.courses).toBeDefined(); + expect(component.courses).toHaveLength(7); const onSortSpy = jest.spyOn(component, 'onSort'); const button = fixture.debugElement.nativeElement.querySelector('#sort-test'); @@ -223,7 +223,7 @@ describe('CourseArchiveComponent', () => { expect(component.semesters[1]).toBe('WS21/22'); expect(component.semesters[0]).toBe('SS19'); - const iconComponent = fixture.debugElement.query(By.css('#icon-test-up')).componentInstance; + const iconComponent = fixture.debugElement.query(By.css('#icon-test-down')).componentInstance; expect(iconComponent).not.toBeNull(); expect(iconComponent.icon).toBe(component.faArrowUp19); From bc137e719a705d90b83e6e9d94c955b6be448614 Mon Sep 17 00:00:00 2001 From: EgeDoguKaya Date: Mon, 14 Oct 2024 15:47:02 +0200 Subject: [PATCH 73/74] Increase test coverage --- .../course-archive.component.html | 3 +- .../course/course-archive.component.spec.ts | 45 +++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/src/main/webapp/app/overview/course-archive/course-archive.component.html b/src/main/webapp/app/overview/course-archive/course-archive.component.html index 8eb71bc85f84..d88bcdab0bba 100644 --- a/src/main/webapp/app/overview/course-archive/course-archive.component.html +++ b/src/main/webapp/app/overview/course-archive/course-archive.component.html @@ -26,12 +26,13 @@

@if (courses.length) {
- @for (semester of semesters; track semester; let last = $last) { + @for (semester of semesters; track semester; let last = $last; let i = $index) {
diff --git a/src/test/javascript/spec/component/course/course-archive.component.spec.ts b/src/test/javascript/spec/component/course/course-archive.component.spec.ts index efbbffd92e27..8bc41d3e81b6 100644 --- a/src/test/javascript/spec/component/course/course-archive.component.spec.ts +++ b/src/test/javascript/spec/component/course/course-archive.component.spec.ts @@ -228,5 +228,50 @@ describe('CourseArchiveComponent', () => { expect(iconComponent).not.toBeNull(); expect(iconComponent.icon).toBe(component.faArrowUp19); })); + + it('should find the correct course and call toggle', fakeAsync(() => { + const getCoursesForArchiveSpy = jest.spyOn(courseService, 'getCoursesForArchive'); + getCoursesForArchiveSpy.mockReturnValue(of(new HttpResponse({ body: courses, headers: new HttpHeaders() }))); + const mapCoursesIntoSemestersSpy = jest.spyOn(component, 'mapCoursesIntoSemesters'); + + component.ngOnInit(); + tick(); + fixture.detectChanges(); + expect(component.courses).toHaveLength(7); + expect(getCoursesForArchiveSpy).toHaveBeenCalledOnce(); + expect(mapCoursesIntoSemestersSpy).toHaveBeenCalledOnce(); + + // iPraktikum is in semester-group-3 : WS21/22 + const button = fixture.debugElement.nativeElement.querySelector('#semester-group-3'); + const toggleCollapseStateSpy = jest.spyOn(component, 'toggleCollapseState'); + component.setSearchValue('iPraktikum'); + const courseFound = component.isCourseFoundInSemester('WS21/22'); + expect(courseFound).toBeTrue(); + expect(button).not.toBeNull(); + button.click(); + expect(toggleCollapseStateSpy).toHaveBeenCalledOnce(); + })); + + it('should initialize collapse state correctly', () => { + const getCoursesForArchiveSpy = jest.spyOn(courseService, 'getCoursesForArchive'); + getCoursesForArchiveSpy.mockReturnValue(of(new HttpResponse({ body: courses, headers: new HttpHeaders() }))); + const mapCoursesIntoSemestersSpy = jest.spyOn(component, 'mapCoursesIntoSemesters'); + + component.ngOnInit(); + expect(component.courses).toHaveLength(7); + expect(getCoursesForArchiveSpy).toHaveBeenCalledOnce(); + expect(mapCoursesIntoSemestersSpy).toHaveBeenCalledOnce(); + const getCollapseStateForSemestersSpy = jest.spyOn(component, 'getCollapseStateForSemesters'); + component.setSearchValue(''); + expect(getCollapseStateForSemestersSpy).toHaveBeenCalledOnce(); + + expect(component.semesterCollapsed).toStrictEqual({ + 'WS23/24': false, + 'WS22/23': false, + SS22: false, + 'WS21/22': false, + SS19: false, + }); + }); }); }); From 6d9f82c1483b59fae86e6b69c4a0beb758bbbfde Mon Sep 17 00:00:00 2001 From: EgeDoguKaya Date: Mon, 14 Oct 2024 16:57:13 +0200 Subject: [PATCH 74/74] increase test coverage --- .../course-archive.component.ts | 8 ------- .../app/overview/courses.component.html | 2 +- .../component/course/course.component.spec.ts | 22 +++++++++++++++++++ 3 files changed, 23 insertions(+), 9 deletions(-) diff --git a/src/main/webapp/app/overview/course-archive/course-archive.component.ts b/src/main/webapp/app/overview/course-archive/course-archive.component.ts index 923463236dd1..eb88ee898038 100644 --- a/src/main/webapp/app/overview/course-archive/course-archive.component.ts +++ b/src/main/webapp/app/overview/course-archive/course-archive.component.ts @@ -130,14 +130,6 @@ export class CourseArchiveComponent implements OnInit, OnDestroy { // filter down to unique values .filter((course, index, courses) => courses.indexOf(course) === index) .sort((semesterA, semesterB) => { - // Sort last if the semester is unset - if (semesterA === '') { - return 1; - } - if (semesterB === '') { - return -1; - } - // Parse years in base 10 by extracting the two digits after the WS or SS prefix const yearsCompared = parseInt(semesterB.slice(2, 4), 10) - parseInt(semesterA.slice(2, 4), 10); if (yearsCompared !== 0) { diff --git a/src/main/webapp/app/overview/courses.component.html b/src/main/webapp/app/overview/courses.component.html index f128c6c9a274..b5dabf3a549c 100644 --- a/src/main/webapp/app/overview/courses.component.html +++ b/src/main/webapp/app/overview/courses.component.html @@ -24,7 +24,7 @@

{{ nextRelevantExam.title }}

{{ 'artemisApp.studentDashboard.title' | artemisTranslate }} ({{ regularCourses.length + recentlyAccessedCourses.length }})

- + diff --git a/src/test/javascript/spec/component/course/course.component.spec.ts b/src/test/javascript/spec/component/course/course.component.spec.ts index 925a54864297..2c01e4fc0273 100644 --- a/src/test/javascript/spec/component/course/course.component.spec.ts +++ b/src/test/javascript/spec/component/course/course.component.spec.ts @@ -274,4 +274,26 @@ describe('CoursesComponent', () => { expect(component.courses).toEqual([course1, course2, course6]); expect(component.nextRelevantExams).toEqual([]); })); + + it('should initialize search course text correctly', () => { + const searchedCourse = 'Test Course'; + component.setSearchValue('Test Course'); + expect(searchedCourse).toBe(component.searchCourseText); + }); + + it('should adjust sort direction by clicking on sort icon', () => { + const findAllForDashboardSpy = jest.spyOn(courseService, 'findAllForDashboard'); + findAllForDashboardSpy.mockReturnValue(of(new HttpResponse({ body: coursesDashboard, headers: new HttpHeaders() }))); + component.ngOnInit(); + + expect(findAllForDashboardSpy).toHaveBeenCalledOnce(); + expect(component.courses).toEqual(courses); + expect(component.isSortAscending).toBeTrue(); + + const onSortSpy = jest.spyOn(component, 'onSort'); + const button = fixture.debugElement.nativeElement.querySelector('#test-sort'); + button.click(); + expect(onSortSpy).toHaveBeenCalledOnce(); + expect(component.isSortAscending).toBeFalse(); + }); });