diff --git a/docs/dev/setup/docker-compose.rst b/docs/dev/setup/docker-compose.rst index a655f44e6ab7..0ed185bab82f 100644 --- a/docs/dev/setup/docker-compose.rst +++ b/docs/dev/setup/docker-compose.rst @@ -21,7 +21,7 @@ Getting Started with Docker Compose Make sure that Docker Desktop has enough memory (~ 6GB). To adapt it, go to ``Settings -> Resources``. 2. Check that all local network ports used by Docker Compose are free (e.g. you haven't started a local MySQL server - when you would like to start a Docker Compose instance of mysql) + when you would like to start a Docker Compose instance of MySQL). 3. Run ``docker compose pull && docker compose up`` in the directory ``docker/`` 4. Open the Artemis instance in your browser at https://localhost 5. Run ``docker compose down`` in the directory ``docker/`` to stop and remove the docker containers @@ -62,13 +62,12 @@ Three example commands to run such setups: .. code:: bash - docker compose -f docker/atlassian.yml up docker compose -f docker/mysql.yml -f docker/gitlab-jenkins.yml up docker compose -f docker/artemis-dev-postgres.yml up .. tip:: There is also a single ``docker-compose.yml`` in the directory ``docker/`` which mirrors the setup of ``artemis-prod-mysql.yml``. - This should provide a quick way, without manual changes necessary, for new contributors to startup an Artemis instance. + This should provide a quick way, without manual changes necessary, for new contributors to start up an Artemis instance. If the documentation just mentions to run ``docker compose`` without a ``-f `` argument, it's assumed you are running the command from the ``docker/`` directory. @@ -82,7 +81,7 @@ is defined in the following files: * ``gitlab.yml``: **GitLab Service** * ``jenkins.yml``: **Jenkins Service** -For testing mails or SAML logins, you can append the following services to any setup with an artemis container: +For testing mails or SAML logins, you can append the following services to any setup with an Artemis container: * ``mailhog.yml``: **Mailhog Service** (email testing tool) * ``saml-test.yml``: **Saml-Test Service** (SAML Test Identity Provider for testing SAML features) @@ -145,7 +144,7 @@ Get a shell into the containers ``docker compose exec artemis-app bash`` or if the container is not yet running: ``docker compose run --rm artemis-app bash`` - mysql container: - ``docker compose exec mysql bash`` or directly into mysql ``docker compose exec mysql mysql`` + ``docker compose exec mysql bash`` or directly into MySQL ``docker compose exec mysql mysql`` Analog for other services. @@ -157,7 +156,7 @@ Other useful commands - Stop, remove containers and volumes: ``docker compose down -v`` - Remove Artemis-related volumes/state: ``docker volume rm artemis-data artemis-mysql-data`` - This is helpful in setups where you just want to delete the state of artemis + This is helpful in setups where you just want to delete the state of Artemis but not of Jenkins and GitLab for instance. - Stop a service: ``docker compose stop `` (restart via ``docker compose start ``) diff --git a/docs/dev/setup/jenkins-gitlab.rst b/docs/dev/setup/jenkins-gitlab.rst index 3adbbabac478..8f117cff3cca 100644 --- a/docs/dev/setup/jenkins-gitlab.rst +++ b/docs/dev/setup/jenkins-gitlab.rst @@ -890,7 +890,8 @@ and the corresponding Docker image can be found on .. code:: bash - docker compose -f docker/.yml up --build -d + docker compose -f docker/.yml build --no-cache + docker compose -f docker/.yml up -d 3. Build the new Docker image: diff --git a/gradle.properties b/gradle.properties index 49a2586270bf..3ade8a049c6c 100644 --- a/gradle.properties +++ b/gradle.properties @@ -8,7 +8,7 @@ npm_version=10.8.0 # Dependency versions jhipster_dependencies_version=8.7.2 spring_boot_version=3.3.5 -spring_security_version=6.3.4 +spring_security_version=6.3.5 # TODO: upgrading to 6.6.0 currently leads to issues due to internal changes in Hibernate and potentially wrong use in Artemis server code hibernate_version=6.4.10.Final # TODO: can we update to 5.x? diff --git a/src/main/java/de/tum/cit/aet/artemis/communication/repository/CustomPostRepositoryImpl.java b/src/main/java/de/tum/cit/aet/artemis/communication/repository/CustomPostRepositoryImpl.java index cfdd5f71443e..aae587022c93 100644 --- a/src/main/java/de/tum/cit/aet/artemis/communication/repository/CustomPostRepositoryImpl.java +++ b/src/main/java/de/tum/cit/aet/artemis/communication/repository/CustomPostRepositoryImpl.java @@ -1,6 +1,8 @@ package de.tum.cit.aet.artemis.communication.repository; +import java.util.ArrayList; import java.util.Collections; +import java.util.LinkedHashSet; import java.util.List; import jakarta.persistence.EntityManager; @@ -50,6 +52,8 @@ public Page findPostIdsWithSpecification(Specification specification query.setMaxResults(pageable.getPageSize()); List postIds = query.getResultList(); + // removes all duplicates from the answer posts + List uniquePostIds = new ArrayList<>(new LinkedHashSet<>(postIds)); // Count query CriteriaQuery countQuery = builder.createQuery(Long.class); @@ -66,6 +70,6 @@ public Page findPostIdsWithSpecification(Specification specification Long countResult = entityManager.createQuery(countQuery).getSingleResult(); long count = countResult != null ? countResult : 0L; - return new PageImpl<>(postIds, pageable, count); + return new PageImpl<>(uniquePostIds, pageable, count); } } diff --git a/src/main/java/de/tum/cit/aet/artemis/communication/service/notifications/ConversationNotificationService.java b/src/main/java/de/tum/cit/aet/artemis/communication/service/notifications/ConversationNotificationService.java index d009074927b2..cf5cc2c67cc8 100644 --- a/src/main/java/de/tum/cit/aet/artemis/communication/service/notifications/ConversationNotificationService.java +++ b/src/main/java/de/tum/cit/aet/artemis/communication/service/notifications/ConversationNotificationService.java @@ -41,11 +41,15 @@ public class ConversationNotificationService { private final SingleUserNotificationRepository singleUserNotificationRepository; + private final SingleUserNotificationService singleUserNotificationService; + public ConversationNotificationService(ConversationNotificationRepository conversationNotificationRepository, - GeneralInstantNotificationService generalInstantNotificationService, SingleUserNotificationRepository singleUserNotificationRepository) { + GeneralInstantNotificationService generalInstantNotificationService, SingleUserNotificationRepository singleUserNotificationRepository, + SingleUserNotificationService singleUserNotificationService) { this.conversationNotificationRepository = conversationNotificationRepository; this.generalInstantNotificationService = generalInstantNotificationService; this.singleUserNotificationRepository = singleUserNotificationRepository; + this.singleUserNotificationService = singleUserNotificationService; } /** @@ -83,7 +87,7 @@ public ConversationNotification createNotification(Post createdMessage, Conversa String[] placeholders = createPlaceholdersNewMessageChannelText(course.getTitle(), createdMessage.getContent(), createdMessage.getCreationDate().toString(), conversationName, createdMessage.getAuthor().getName(), conversationType); ConversationNotification notification = createConversationMessageNotification(course.getId(), createdMessage, notificationType, notificationText, true, placeholders); - save(notification, mentionedUsers, placeholders); + save(notification, mentionedUsers, placeholders, createdMessage); return notification; } @@ -93,11 +97,12 @@ public static String[] createPlaceholdersNewMessageChannelText(String courseTitl return new String[] { courseTitle, messageContent, messageCreationDate, conversationName, authorName, conversationType }; } - private void save(ConversationNotification notification, Set mentionedUsers, String[] placeHolders) { + private void save(ConversationNotification notification, Set mentionedUsers, String[] placeHolders, Post createdMessage) { conversationNotificationRepository.save(notification); - Set mentionedUserNotifications = mentionedUsers.stream().map(mentionedUser -> SingleUserNotificationFactory - .createNotification(notification.getMessage(), NotificationType.CONVERSATION_USER_MENTIONED, notification.getText(), placeHolders, mentionedUser)) + Set mentionedUserNotifications = singleUserNotificationService + .filterAllowedRecipientsInMentionedUsers(mentionedUsers, createdMessage.getConversation()).map(mentionedUser -> SingleUserNotificationFactory + .createNotification(notification.getMessage(), NotificationType.CONVERSATION_USER_MENTIONED, notification.getText(), placeHolders, mentionedUser)) .collect(Collectors.toSet()); singleUserNotificationRepository.saveAll(mentionedUserNotifications); } diff --git a/src/main/java/de/tum/cit/aet/artemis/communication/service/notifications/SingleUserNotificationService.java b/src/main/java/de/tum/cit/aet/artemis/communication/service/notifications/SingleUserNotificationService.java index 4e242f93e0fb..33def5698b9f 100644 --- a/src/main/java/de/tum/cit/aet/artemis/communication/service/notifications/SingleUserNotificationService.java +++ b/src/main/java/de/tum/cit/aet/artemis/communication/service/notifications/SingleUserNotificationService.java @@ -443,23 +443,34 @@ public void notifyInvolvedUsersAboutNewMessageReply(Post post, SingleUserNotific usersInvolved.add(post.getAuthor()); } - mentionedUsers.stream().filter(user -> { - boolean isChannelAndCourseWide = post.getConversation() instanceof Channel channel && channel.getIsCourseWide(); - boolean isChannelVisibleToStudents = !(post.getConversation() instanceof Channel channel) || conversationService.isChannelVisibleToStudents(channel); - boolean isChannelVisibleToMentionedUser = isChannelVisibleToStudents - || authorizationCheckService.isAtLeastTeachingAssistantInCourse(post.getConversation().getCourse(), user); - - // Only send a notification to the mentioned user if... - // (for course-wide channels) ...the course-wide channel is visible - // (for all other cases) ...the user is a member of the conversation - return (isChannelAndCourseWide && isChannelVisibleToMentionedUser) || conversationService.isMember(post.getConversation().getId(), user.getId()); - }).forEach(mentionedUser -> notifyUserAboutNewMessageReply(savedAnswerMessage, notification, mentionedUser, author, CONVERSATION_USER_MENTIONED)); + filterAllowedRecipientsInMentionedUsers(mentionedUsers, post.getConversation()) + .forEach(mentionedUser -> notifyUserAboutNewMessageReply(savedAnswerMessage, notification, mentionedUser, author, CONVERSATION_USER_MENTIONED)); Conversation conv = conversationService.getConversationById(post.getConversation().getId()); usersInvolved.stream().filter(userInvolved -> !mentionedUsers.contains(userInvolved)) .forEach(userInvolved -> notifyUserAboutNewMessageReply(savedAnswerMessage, notification, userInvolved, author, getAnswerMessageNotificationType(conv))); } + /** + * Filters which of the mentioned users are permitted to receive a notification + * + * @param mentionedUsers users mentioned in the answer message + * @param conversation the conversation of the created post/notification, used for filtering + * @return the stream of mentioned users which are permitted to receive the notification for the given conversation + */ + public Stream filterAllowedRecipientsInMentionedUsers(Set mentionedUsers, Conversation conversation) { + return mentionedUsers.stream().filter(user -> { + boolean isChannelAndCourseWide = conversation instanceof Channel channel && channel.getIsCourseWide(); + boolean isChannelVisibleToStudents = !(conversation instanceof Channel channel) || conversationService.isChannelVisibleToStudents(channel); + boolean isChannelVisibleToMentionedUser = isChannelVisibleToStudents || authorizationCheckService.isAtLeastTeachingAssistantInCourse(conversation.getCourse(), user); + + // Only send a notification to the mentioned user if... + // (for course-wide channels) ...the course-wide channel is visible + // (for all other cases) ...the user is a member of the conversation + return (isChannelAndCourseWide && isChannelVisibleToMentionedUser) || conversationService.isMember(conversation.getId(), user.getId()); + }); + } + /** * Saves the given notification in database and sends it to the client via websocket. * Also creates and sends an instant notification. diff --git a/src/main/webapp/app/exam/manage/student-exams/student-exam-timeline/programming-exam-diff/programming-exercise-exam-diff.component.html b/src/main/webapp/app/exam/manage/student-exams/student-exam-timeline/programming-exam-diff/programming-exercise-exam-diff.component.html index 7f8623e91b52..cdde46506c49 100644 --- a/src/main/webapp/app/exam/manage/student-exams/student-exam-timeline/programming-exam-diff/programming-exercise-exam-diff.component.html +++ b/src/main/webapp/app/exam/manage/student-exams/student-exam-timeline/programming-exam-diff/programming-exercise-exam-diff.component.html @@ -1,7 +1,7 @@

{{ exercise.title }} [{{ exercise.maxPoints }} {{ 'artemisApp.examParticipation.points' | artemisTranslate }}] + >[{{ exercise.maxPoints }} {{ 'artemisApp.examParticipation.exercisePoints' | artemisTranslate }}] @if (exercise.includedInOverallScore !== IncludedInOverallScore.INCLUDED_COMPLETELY) { } diff --git a/src/main/webapp/app/exercises/file-upload/manage/file-upload-exercise.component.ts b/src/main/webapp/app/exercises/file-upload/manage/file-upload-exercise.component.ts index 01fe874b2afd..0874dea0faf6 100644 --- a/src/main/webapp/app/exercises/file-upload/manage/file-upload-exercise.component.ts +++ b/src/main/webapp/app/exercises/file-upload/manage/file-upload-exercise.component.ts @@ -1,28 +1,30 @@ -import { Component, Input } from '@angular/core'; +import { Component, Input, inject } from '@angular/core'; import { HttpErrorResponse, HttpResponse } from '@angular/common/http'; -import { ActivatedRoute, Router } from '@angular/router'; -import { TranslateService } from '@ngx-translate/core'; import { filter } from 'rxjs/operators'; import { FileUploadExercise } from 'app/entities/file-upload-exercise.model'; import { FileUploadExerciseService } from './file-upload-exercise.service'; import { ExerciseComponent } from 'app/exercises/shared/exercise/exercise.component'; import { onError } from 'app/shared/util/global.utils'; import { AccountService } from 'app/core/auth/account.service'; -import { CourseManagementService } from 'app/course/manage/course-management.service'; import { SortService } from 'app/shared/service/sort.service'; import { ExerciseService } from 'app/exercises/shared/exercise/exercise.service'; import { AlertService } from 'app/core/util/alert.service'; -import { EventManager } from 'app/core/util/event-manager.service'; import { faBook, faPlus, faSort, faTable, faTrash, faUsers, faWrench } from '@fortawesome/free-solid-svg-icons'; import { faListAlt } from '@fortawesome/free-regular-svg-icons'; import { CourseExerciseService } from 'app/exercises/shared/course-exercises/course-exercise.service'; -import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; @Component({ selector: 'jhi-file-upload-exercise', templateUrl: './file-upload-exercise.component.html', }) export class FileUploadExerciseComponent extends ExerciseComponent { + protected exerciseService = inject(ExerciseService); + protected fileUploadExerciseService = inject(FileUploadExerciseService); + private courseExerciseService = inject(CourseExerciseService); + private alertService = inject(AlertService); + private accountService = inject(AccountService); + private sortService = inject(SortService); + @Input() fileUploadExercises: FileUploadExercise[] = []; filteredFileUploadExercises: FileUploadExercise[] = []; @@ -40,23 +42,6 @@ export class FileUploadExerciseComponent extends ExerciseComponent { return this.fileUploadExercises; } - constructor( - public exerciseService: ExerciseService, - public fileUploadExerciseService: FileUploadExerciseService, - private courseExerciseService: CourseExerciseService, - private alertService: AlertService, - private accountService: AccountService, - private modalService: NgbModal, - private router: Router, - private sortService: SortService, - courseService: CourseManagementService, - translateService: TranslateService, - eventManager: EventManager, - route: ActivatedRoute, - ) { - super(courseService, translateService, route, eventManager); - } - protected loadExercises(): void { this.courseExerciseService .findAllFileUploadExercisesForCourse(this.courseId) diff --git a/src/main/webapp/app/exercises/modeling/manage/modeling-exercise.component.ts b/src/main/webapp/app/exercises/modeling/manage/modeling-exercise.component.ts index 74dff3c4b9fc..e1ea83ce136e 100644 --- a/src/main/webapp/app/exercises/modeling/manage/modeling-exercise.component.ts +++ b/src/main/webapp/app/exercises/modeling/manage/modeling-exercise.component.ts @@ -1,18 +1,13 @@ -import { Component, Input } from '@angular/core'; +import { Component, Input, inject } from '@angular/core'; import { HttpErrorResponse, HttpResponse } from '@angular/common/http'; import { ModelingExercise } from 'app/entities/modeling-exercise.model'; import { ModelingExerciseService } from './modeling-exercise.service'; import { AccountService } from 'app/core/auth/account.service'; -import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; -import { ActivatedRoute, Router } from '@angular/router'; -import { CourseManagementService } from 'app/course/manage/course-management.service'; import { ExerciseComponent } from 'app/exercises/shared/exercise/exercise.component'; -import { TranslateService } from '@ngx-translate/core'; import { onError } from 'app/shared/util/global.utils'; import { SortService } from 'app/shared/service/sort.service'; import { ExerciseService } from 'app/exercises/shared/exercise/exercise.service'; import { AlertService } from 'app/core/util/alert.service'; -import { EventManager } from 'app/core/util/event-manager.service'; import { faBook, faPlus, faSort, faTable, faTimes, faTrash, faUsers, faWrench } from '@fortawesome/free-solid-svg-icons'; import { faListAlt } from '@fortawesome/free-regular-svg-icons'; import { CourseExerciseService } from 'app/exercises/shared/course-exercises/course-exercise.service'; @@ -22,7 +17,14 @@ import { CourseExerciseService } from 'app/exercises/shared/course-exercises/cou templateUrl: './modeling-exercise.component.html', }) export class ModelingExerciseComponent extends ExerciseComponent { - @Input() modelingExercises: ModelingExercise[]; + protected exerciseService = inject(ExerciseService); + protected modelingExerciseService = inject(ModelingExerciseService); + private courseExerciseService = inject(CourseExerciseService); + private alertService = inject(AlertService); + private accountService = inject(AccountService); + private sortService = inject(SortService); + + @Input() modelingExercises: ModelingExercise[] = []; filteredModelingExercises: ModelingExercise[]; // Icons faPlus = faPlus; @@ -39,24 +41,6 @@ export class ModelingExerciseComponent extends ExerciseComponent { return this.modelingExercises; } - constructor( - public exerciseService: ExerciseService, - public modelingExerciseService: ModelingExerciseService, - private courseExerciseService: CourseExerciseService, - private alertService: AlertService, - private accountService: AccountService, - private sortService: SortService, - private modalService: NgbModal, - private router: Router, - courseService: CourseManagementService, - translateService: TranslateService, - eventManager: EventManager, - route: ActivatedRoute, - ) { - super(courseService, translateService, route, eventManager); - this.modelingExercises = []; - } - protected loadExercises(): void { this.courseExerciseService.findAllModelingExercisesForCourse(this.courseId).subscribe({ next: (res: HttpResponse) => { diff --git a/src/main/webapp/app/exercises/programming/manage/programming-exercise.component.ts b/src/main/webapp/app/exercises/programming/manage/programming-exercise.component.ts index e1f659c87483..3a33c7b88f83 100644 --- a/src/main/webapp/app/exercises/programming/manage/programming-exercise.component.ts +++ b/src/main/webapp/app/exercises/programming/manage/programming-exercise.component.ts @@ -1,23 +1,19 @@ -import { Component, ContentChild, Input, OnDestroy, OnInit, TemplateRef } from '@angular/core'; +import { Component, ContentChild, Input, OnDestroy, OnInit, TemplateRef, inject } from '@angular/core'; import { HttpErrorResponse, HttpResponse } from '@angular/common/http'; import { merge } from 'rxjs'; import { ProgrammingExercise } from 'app/entities/programming/programming-exercise.model'; import { ProgrammingExerciseInstructorRepositoryType, ProgrammingExerciseService } from './services/programming-exercise.service'; -import { ActivatedRoute } from '@angular/router'; import { ExerciseComponent } from 'app/exercises/shared/exercise/exercise.component'; -import { TranslateService } from '@ngx-translate/core'; import { ActionType } from 'app/shared/delete-dialog/delete-dialog.model'; import { onError } from 'app/shared/util/global.utils'; import { AccountService } from 'app/core/auth/account.service'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { FeatureToggle } from 'app/shared/feature-toggle/feature-toggle.service'; import { ExerciseService } from 'app/exercises/shared/exercise/exercise.service'; -import { CourseManagementService } from 'app/course/manage/course-management.service'; import { SortService } from 'app/shared/service/sort.service'; import { ProgrammingExerciseEditSelectedComponent } from 'app/exercises/programming/manage/programming-exercise-edit-selected.component'; import { ProgrammingExerciseParticipationType } from 'app/entities/programming/programming-exercise-participation.model'; import { AlertService } from 'app/core/util/alert.service'; -import { EventManager } from 'app/core/util/event-manager.service'; import { createBuildPlanUrl } from 'app/exercises/programming/shared/utils/programming-exercise.utils'; import { ProfileService } from 'app/shared/layouts/profiles/profile.service'; import { ConsistencyCheckComponent } from 'app/shared/consistency-check/consistency-check.component'; @@ -46,7 +42,16 @@ import { PROFILE_LOCALCI, PROFILE_LOCALVC, PROFILE_THEIA } from 'app/app.constan templateUrl: './programming-exercise.component.html', }) export class ProgrammingExerciseComponent extends ExerciseComponent implements OnInit, OnDestroy { - @Input() programmingExercises: ProgrammingExercise[]; + protected exerciseService = inject(ExerciseService); + private programmingExerciseService = inject(ProgrammingExerciseService); + private courseExerciseService = inject(CourseExerciseService); + private accountService = inject(AccountService); + private alertService = inject(AlertService); + private modalService = inject(NgbModal); + private sortService = inject(SortService); + private profileService = inject(ProfileService); + + @Input() programmingExercises: ProgrammingExercise[] = []; filteredProgrammingExercises: ProgrammingExercise[]; readonly ActionType = ActionType; FeatureToggle = FeatureToggle; @@ -82,24 +87,6 @@ export class ProgrammingExerciseComponent extends ExerciseComponent implements O return this.programmingExercises; } - constructor( - private programmingExerciseService: ProgrammingExerciseService, - private courseExerciseService: CourseExerciseService, - public exerciseService: ExerciseService, - private accountService: AccountService, - private alertService: AlertService, - private modalService: NgbModal, - private sortService: SortService, - private profileService: ProfileService, - courseService: CourseManagementService, - translateService: TranslateService, - eventManager: EventManager, - route: ActivatedRoute, - ) { - super(courseService, translateService, route, eventManager); - this.programmingExercises = []; - } - ngOnInit(): void { super.ngOnInit(); } diff --git a/src/main/webapp/app/exercises/quiz/manage/quiz-exercise.component.ts b/src/main/webapp/app/exercises/quiz/manage/quiz-exercise.component.ts index 7e3f76e29b65..22ac2e1a7685 100644 --- a/src/main/webapp/app/exercises/quiz/manage/quiz-exercise.component.ts +++ b/src/main/webapp/app/exercises/quiz/manage/quiz-exercise.component.ts @@ -1,18 +1,13 @@ -import { Component, Input } from '@angular/core'; +import { Component, Input, inject } from '@angular/core'; import { HttpErrorResponse, HttpResponse } from '@angular/common/http'; import { QuizExercise, QuizMode, QuizStatus } from 'app/entities/quiz/quiz-exercise.model'; import { QuizExerciseService } from './quiz-exercise.service'; import { AccountService } from 'app/core/auth/account.service'; -import { ActivatedRoute, Router } from '@angular/router'; -import { CourseManagementService } from 'app/course/manage/course-management.service'; import { ExerciseComponent } from 'app/exercises/shared/exercise/exercise.component'; -import { TranslateService } from '@ngx-translate/core'; -import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { ActionType } from 'app/shared/delete-dialog/delete-dialog.model'; import { SortService } from 'app/shared/service/sort.service'; import { ExerciseService } from 'app/exercises/shared/exercise/exercise.service'; import { AlertService } from 'app/core/util/alert.service'; -import { EventManager } from 'app/core/util/event-manager.service'; import { faSort, faTrash } from '@fortawesome/free-solid-svg-icons'; import { isQuizEditable } from 'app/exercises/quiz/shared/quiz-manage-util.service'; @@ -21,6 +16,12 @@ import { isQuizEditable } from 'app/exercises/quiz/shared/quiz-manage-util.servi templateUrl: './quiz-exercise.component.html', }) export class QuizExerciseComponent extends ExerciseComponent { + protected exerciseService = inject(ExerciseService); + quizExerciseService = inject(QuizExerciseService); + private accountService = inject(AccountService); + private alertService = inject(AlertService); + private sortService = inject(SortService); + readonly ActionType = ActionType; readonly QuizStatus = QuizStatus; readonly QuizMode = QuizMode; @@ -36,22 +37,6 @@ export class QuizExerciseComponent extends ExerciseComponent { return this.quizExercises; } - constructor( - public quizExerciseService: QuizExerciseService, - private accountService: AccountService, - private alertService: AlertService, - private modalService: NgbModal, - private router: Router, - private sortService: SortService, - public exerciseService: ExerciseService, - courseService: CourseManagementService, - translateService: TranslateService, - eventManager: EventManager, - route: ActivatedRoute, - ) { - super(courseService, translateService, route, eventManager); - } - protected loadExercises(): void { this.quizExerciseService.findForCourse(this.courseId).subscribe({ next: (res: HttpResponse) => { diff --git a/src/main/webapp/app/exercises/shared/exercise/exercise.component.ts b/src/main/webapp/app/exercises/shared/exercise/exercise.component.ts index fccf7cdb3269..32d96f26b6f7 100644 --- a/src/main/webapp/app/exercises/shared/exercise/exercise.component.ts +++ b/src/main/webapp/app/exercises/shared/exercise/exercise.component.ts @@ -1,4 +1,4 @@ -import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core'; +import { Component, EventEmitter, Input, OnDestroy, OnInit, Output, inject } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { Observable, Subject, Subscription, merge } from 'rxjs'; import { HttpErrorResponse, HttpResponse } from '@angular/common/http'; @@ -15,6 +15,11 @@ interface DeletionServiceInterface { @Component({ template: '' }) export abstract class ExerciseComponent implements OnInit, OnDestroy { + protected translateService = inject(TranslateService); + protected eventManager = inject(EventManager); + private courseService = inject(CourseManagementService); + private route = inject(ActivatedRoute); + private eventSubscriber: Subscription; @Input() embedded = false; @Input() course: Course; @@ -23,8 +28,8 @@ export abstract class ExerciseComponent implements OnInit, OnDestroy { @Output() filteredExerciseCount = new EventEmitter(); showHeading: boolean; courseId: number; - predicate: string; - reverse: boolean; + predicate: string = 'id'; + reverse: boolean = true; selectedExercises: Exercise[] = []; allChecked = false; @@ -35,16 +40,6 @@ export abstract class ExerciseComponent implements OnInit, OnDestroy { protected abstract get exercises(): Exercise[]; - protected constructor( - private courseService: CourseManagementService, - protected translateService: TranslateService, - private route: ActivatedRoute, - protected eventManager: EventManager, - ) { - this.predicate = 'id'; - this.reverse = true; - } - /** * Fetches an exercise from the server (and if needed the course as well) */ diff --git a/src/main/webapp/app/exercises/text/assess/analytics/text-assesment-analytics.service.ts b/src/main/webapp/app/exercises/text/assess/analytics/text-assesment-analytics.service.ts index 22178ca78f3c..b48b4dbca3ff 100644 --- a/src/main/webapp/app/exercises/text/assess/analytics/text-assesment-analytics.service.ts +++ b/src/main/webapp/app/exercises/text/assess/analytics/text-assesment-analytics.service.ts @@ -1,4 +1,4 @@ -import { Injectable } from '@angular/core'; +import { Injectable, inject } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { TextAssessmentService } from 'app/exercises/text/assess/text-assessment.service'; import { TextAssessmentEvent, TextAssessmentEventType } from 'app/entities/text/text-assesment-event.model'; @@ -13,6 +13,11 @@ import { Location } from '@angular/common'; */ @Injectable({ providedIn: 'root' }) export class TextAssessmentAnalytics { + protected assessmentsService = inject(TextAssessmentService); + protected accountService = inject(AccountService); + private location = inject(Location); + private profileService = inject(ProfileService); + private userId: number; private courseId: number; private textExerciseId: number; @@ -23,12 +28,7 @@ export class TextAssessmentAnalytics { private route: ActivatedRoute; public analyticsEnabled = false; - constructor( - protected assessmentsService: TextAssessmentService, - protected accountService: AccountService, - private profileService: ProfileService, - public location: Location, - ) { + constructor() { // retrieve the analytics enabled status from the profile info and set to current property this.profileService.getProfileInfo().subscribe((profileInfo) => { this.analyticsEnabled = profileInfo.textAssessmentAnalyticsEnabled || false; diff --git a/src/main/webapp/app/exercises/text/assess/text-assessment-area/text-assessment-area.component.ts b/src/main/webapp/app/exercises/text/assess/text-assessment-area/text-assessment-area.component.ts index 200f7b69d95a..2646fccb48ad 100644 --- a/src/main/webapp/app/exercises/text/assess/text-assessment-area/text-assessment-area.component.ts +++ b/src/main/webapp/app/exercises/text/assess/text-assessment-area/text-assessment-area.component.ts @@ -1,4 +1,4 @@ -import { Component, EventEmitter, HostListener, Input, OnChanges, Output, SimpleChanges } from '@angular/core'; +import { Component, EventEmitter, HostListener, Input, OnChanges, Output, SimpleChanges, inject } from '@angular/core'; import { TextSubmission } from 'app/entities/text/text-submission.model'; import { TextBlockRef } from 'app/entities/text/text-block-ref.model'; import { StringCountService } from 'app/exercises/text/participate/string-count.service'; @@ -16,6 +16,8 @@ import { GradingCriterion } from 'app/exercises/shared/structured-grading-criter ], }) export class TextAssessmentAreaComponent implements OnChanges { + private stringCountService = inject(StringCountService); + // inputs @Input() submission: TextSubmission; @Input() textBlockRefs: TextBlockRef[]; @@ -32,8 +34,6 @@ export class TextAssessmentAreaComponent implements OnChanges { wordCount = 0; characterCount = 0; - constructor(private stringCountService: StringCountService) {} - /** * Life cycle hook to indicate component change */ diff --git a/src/main/webapp/app/exercises/text/assess/text-assessment-base.component.ts b/src/main/webapp/app/exercises/text/assess/text-assessment-base.component.ts index 8c1f564182d5..a3c60d5352ee 100644 --- a/src/main/webapp/app/exercises/text/assess/text-assessment-base.component.ts +++ b/src/main/webapp/app/exercises/text/assess/text-assessment-base.component.ts @@ -1,4 +1,4 @@ -import { Component, OnInit } from '@angular/core'; +import { Component, OnInit, inject } from '@angular/core'; import { HttpErrorResponse, HttpResponse } from '@angular/common/http'; import { TextBlockRef } from 'app/entities/text/text-block-ref.model'; import { TextSubmission } from 'app/entities/text/text-submission.model'; @@ -17,6 +17,11 @@ import { getCourseFromExercise } from 'app/entities/exercise.model'; template: '', }) export abstract class TextAssessmentBaseComponent implements OnInit { + protected alertService = inject(AlertService); + protected accountService = inject(AccountService); + protected assessmentsService = inject(TextAssessmentService); + protected structuredGradingCriterionService = inject(StructuredGradingCriterionService); + /* * Base Component for TextSubmissionAssessmentComponent and ExampleTextSubmissionComponent since they share a lot of same functions. */ @@ -29,13 +34,6 @@ export abstract class TextAssessmentBaseComponent implements OnInit { readonly getCourseFromExercise = getCourseFromExercise; - protected constructor( - protected alertService: AlertService, - protected accountService: AccountService, - protected assessmentsService: TextAssessmentService, - protected structuredGradingCriterionService: StructuredGradingCriterionService, - ) {} - async ngOnInit() { // Used to check if the assessor is the current user const identity = await this.accountService.identity(); diff --git a/src/main/webapp/app/exercises/text/assess/text-assessment.service.ts b/src/main/webapp/app/exercises/text/assess/text-assessment.service.ts index 492d620afe9c..6304b95f1625 100644 --- a/src/main/webapp/app/exercises/text/assess/text-assessment.service.ts +++ b/src/main/webapp/app/exercises/text/assess/text-assessment.service.ts @@ -1,4 +1,4 @@ -import { Injectable } from '@angular/core'; +import { Injectable, inject } from '@angular/core'; import { HttpClient, HttpParams, HttpResponse } from '@angular/common/http'; import { Observable } from 'rxjs'; import { map, tap } from 'rxjs/operators'; @@ -22,12 +22,10 @@ type TextAssessmentDTO = { feedbacks: Feedback[]; textBlocks: TextBlock[]; asses providedIn: 'root', }) export class TextAssessmentService { - private readonly RESOURCE_URL = 'api'; + private http = inject(HttpClient); + private accountService = inject(AccountService); - constructor( - private http: HttpClient, - private accountService: AccountService, - ) {} + private readonly RESOURCE_URL = 'api'; /** * Saves the passed feedback items of the assessment. diff --git a/src/main/webapp/app/exercises/text/assess/text-submission-assessment.component.ts b/src/main/webapp/app/exercises/text/assess/text-submission-assessment.component.ts index ed71e52c21d4..1d710d15ac1f 100644 --- a/src/main/webapp/app/exercises/text/assess/text-submission-assessment.component.ts +++ b/src/main/webapp/app/exercises/text/assess/text-submission-assessment.component.ts @@ -1,10 +1,8 @@ -import { Component, OnDestroy, OnInit } from '@angular/core'; +import { Component, OnDestroy, OnInit, inject } from '@angular/core'; import { Location } from '@angular/common'; import { HttpErrorResponse, HttpResponse } from '@angular/common/http'; import { ActivatedRoute, Router } from '@angular/router'; -import { AlertService } from 'app/core/util/alert.service'; import dayjs from 'dayjs/esm'; -import { AccountService } from 'app/core/auth/account.service'; import { StudentParticipation } from 'app/entities/participation/student-participation.model'; import { TextSubmission } from 'app/entities/text/text-submission.model'; import { TextExercise } from 'app/entities/text/text-exercise.model'; @@ -16,7 +14,6 @@ import { Feedback, FeedbackType } from 'app/entities/feedback.model'; import { notUndefined, onError } from 'app/shared/util/global.utils'; import { TranslateService } from '@ngx-translate/core'; import { NEW_ASSESSMENT_PATH } from 'app/exercises/text/assess/text-submission-assessment.route'; -import { StructuredGradingCriterionService } from 'app/exercises/shared/structured-grading-criterion/structured-grading-criterion.service'; import { assessmentNavigateBack } from 'app/exercises/shared/navigate-back.util'; import { getLatestSubmissionResult, @@ -45,6 +42,16 @@ import { Subscription } from 'rxjs'; styleUrls: ['./text-submission-assessment.component.scss'], }) export class TextSubmissionAssessmentComponent extends TextAssessmentBaseComponent implements OnInit, OnDestroy { + private activatedRoute = inject(ActivatedRoute); + private router = inject(Router); + private location = inject(Location); + private route = inject(ActivatedRoute); + private complaintService = inject(ComplaintService); + private submissionService = inject(SubmissionService); + private exampleSubmissionService = inject(ExampleSubmissionService); + private athenaService = inject(AthenaService); + private translateService = inject(TranslateService); + /* * The instance of this component is REUSED for multiple assessments if using the "Assess Next" button! * All properties must be initialized with a default value (or null) in the resetComponent() method. @@ -66,7 +73,7 @@ export class TextSubmissionAssessmentComponent extends TextAssessmentBaseCompone assessmentsAreValid: boolean; noNewSubmissions: boolean; hasAssessmentDueDatePassed: boolean; - correctionRound: number; + correctionRound: number = 0; resultId: number; loadingInitialSubmission = true; highlightDifferences = false; @@ -99,24 +106,9 @@ export class TextSubmissionAssessmentComponent extends TextAssessmentBaseCompone // Icons farListAlt = faListAlt; - constructor( - private activatedRoute: ActivatedRoute, - private router: Router, - private location: Location, - private route: ActivatedRoute, - private complaintService: ComplaintService, - private submissionService: SubmissionService, - private exampleSubmissionService: ExampleSubmissionService, - private athenaService: AthenaService, - alertService: AlertService, - accountService: AccountService, - assessmentsService: TextAssessmentService, - structuredGradingCriterionService: StructuredGradingCriterionService, - translateService: TranslateService, - ) { - super(alertService, accountService, assessmentsService, structuredGradingCriterionService); - translateService.get('artemisApp.textAssessment.confirmCancel').subscribe((text) => (this.cancelConfirmationText = text)); - this.correctionRound = 0; + constructor() { + super(); + this.translateService.get('artemisApp.textAssessment.confirmCancel').subscribe((text) => (this.cancelConfirmationText = text)); this.resetComponent(); } diff --git a/src/main/webapp/app/exercises/text/assess/text-submission-assessment.route.ts b/src/main/webapp/app/exercises/text/assess/text-submission-assessment.route.ts index 83e63bcd21b1..aa8fb3f15542 100644 --- a/src/main/webapp/app/exercises/text/assess/text-submission-assessment.route.ts +++ b/src/main/webapp/app/exercises/text/assess/text-submission-assessment.route.ts @@ -1,4 +1,4 @@ -import { Injectable } from '@angular/core'; +import { Injectable, inject } from '@angular/core'; import { ActivatedRouteSnapshot, Resolve, Routes } from '@angular/router'; import { TextSubmission } from 'app/entities/text/text-submission.model'; import { of } from 'rxjs'; @@ -12,7 +12,7 @@ import { catchError, map } from 'rxjs/operators'; @Injectable({ providedIn: 'root' }) export class NewStudentParticipationResolver implements Resolve { - constructor(private textSubmissionService: TextSubmissionService) {} + private textSubmissionService = inject(TextSubmissionService); /** * Resolves the needed StudentParticipations for the TextSubmissionAssessmentComponent using the TextAssessmentService. @@ -33,7 +33,7 @@ export class NewStudentParticipationResolver implements Resolve { - constructor(private textAssessmentService: TextAssessmentService) {} + private textAssessmentService = inject(TextAssessmentService); /** * Resolves the needed StudentParticipations for the TextSubmissionAssessmentComponent using the TextAssessmentService. diff --git a/src/main/webapp/app/exercises/text/assess/textblock-assessment-card/textblock-assessment-card.component.ts b/src/main/webapp/app/exercises/text/assess/textblock-assessment-card/textblock-assessment-card.component.ts index 2a34e2b47136..402d82924caa 100644 --- a/src/main/webapp/app/exercises/text/assess/textblock-assessment-card/textblock-assessment-card.component.ts +++ b/src/main/webapp/app/exercises/text/assess/textblock-assessment-card/textblock-assessment-card.component.ts @@ -1,4 +1,4 @@ -import { Component, EventEmitter, Input, Output, ViewChild } from '@angular/core'; +import { Component, EventEmitter, Input, Output, ViewChild, inject } from '@angular/core'; import { TextBlockRef } from 'app/entities/text/text-block-ref.model'; import { TextblockFeedbackEditorComponent } from 'app/exercises/text/assess/textblock-feedback-editor/textblock-feedback-editor.component'; import { StructuredGradingCriterionService } from 'app/exercises/shared/structured-grading-criterion/structured-grading-criterion.service'; @@ -17,6 +17,10 @@ type OptionalTextBlockRef = TextBlockRef | undefined; styleUrls: ['./textblock-assessment-card.component.scss'], }) export class TextblockAssessmentCardComponent { + protected route = inject(ActivatedRoute); + private structuredGradingCriterionService = inject(StructuredGradingCriterionService); + textAssessmentAnalytics = inject(TextAssessmentAnalytics); + @Input() textBlockRef: TextBlockRef; @Input() selected = false; @Input() readOnly: boolean; @@ -29,12 +33,8 @@ export class TextblockAssessmentCardComponent { @Output() didDelete = new EventEmitter(); @ViewChild(TextblockFeedbackEditorComponent) feedbackEditor: TextblockFeedbackEditorComponent; - constructor( - public structuredGradingCriterionService: StructuredGradingCriterionService, - public textAssessmentAnalytics: TextAssessmentAnalytics, - protected route: ActivatedRoute, - ) { - textAssessmentAnalytics.setComponentRoute(route); + constructor() { + this.textAssessmentAnalytics.setComponentRoute(this.route); } /** diff --git a/src/main/webapp/app/exercises/text/assess/textblock-feedback-editor/textblock-feedback-editor.component.ts b/src/main/webapp/app/exercises/text/assess/textblock-feedback-editor/textblock-feedback-editor.component.ts index 0cae1c87a35b..d52448b4164c 100644 --- a/src/main/webapp/app/exercises/text/assess/textblock-feedback-editor/textblock-feedback-editor.component.ts +++ b/src/main/webapp/app/exercises/text/assess/textblock-feedback-editor/textblock-feedback-editor.component.ts @@ -1,4 +1,4 @@ -import { AfterViewInit, Component, ElementRef, EventEmitter, HostBinding, Input, Output, ViewChild } from '@angular/core'; +import { AfterViewInit, Component, ElementRef, EventEmitter, HostBinding, Input, Output, ViewChild, inject } from '@angular/core'; import { TextBlock } from 'app/entities/text/text-block.model'; import { Feedback, FeedbackType } from 'app/entities/feedback.model'; import { ConfirmIconComponent } from 'app/shared/confirm-icon/confirm-icon.component'; @@ -16,6 +16,11 @@ import { GradingCriterion } from 'app/exercises/shared/structured-grading-criter styleUrls: ['./textblock-feedback-editor.component.scss'], }) export class TextblockFeedbackEditorComponent implements AfterViewInit { + protected route = inject(ActivatedRoute); + protected modalService = inject(NgbModal); + structuredGradingCriterionService = inject(StructuredGradingCriterionService); + textAssessmentAnalytics = inject(TextAssessmentAnalytics); + readonly FeedbackType = FeedbackType; @Input() textBlock: TextBlock = new TextBlock(); @@ -55,13 +60,8 @@ export class TextblockFeedbackEditorComponent implements AfterViewInit { faTrash = faTrash; faAngleRight = faAngleRight; - constructor( - public structuredGradingCriterionService: StructuredGradingCriterionService, - protected modalService: NgbModal, - protected route: ActivatedRoute, - public textAssessmentAnalytics: TextAssessmentAnalytics, - ) { - textAssessmentAnalytics.setComponentRoute(route); + constructor() { + this.textAssessmentAnalytics.setComponentRoute(this.route); } /** diff --git a/src/main/webapp/app/exercises/text/manage/example-text-submission/example-text-submission.component.ts b/src/main/webapp/app/exercises/text/manage/example-text-submission/example-text-submission.component.ts index 1640da7b1978..9592fcd4c481 100644 --- a/src/main/webapp/app/exercises/text/manage/example-text-submission/example-text-submission.component.ts +++ b/src/main/webapp/app/exercises/text/manage/example-text-submission/example-text-submission.component.ts @@ -1,11 +1,9 @@ -import { Component, OnInit } from '@angular/core'; +import { Component, OnInit, inject } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; -import { AlertService } from 'app/core/util/alert.service'; import { HttpResponse } from '@angular/common/http'; import { EntityResponseType, ExampleSubmissionService } from 'app/exercises/shared/example-submission/example-submission.service'; import { TextAssessmentService } from 'app/exercises/text/assess/text-assessment.service'; import { TutorParticipationService } from 'app/exercises/shared/dashboards/tutor/tutor-participation.service'; -import { AccountService } from 'app/core/auth/account.service'; import { GuidedTourService } from 'app/guided-tour/guided-tour.service'; import { tutorAssessmentTour } from 'app/guided-tour/tours/tutor-assessment-tour'; import { ExampleSubmission, ExampleSubmissionMode } from 'app/entities/example-submission.model'; @@ -16,7 +14,6 @@ import { TextSubmission } from 'app/entities/text/text-submission.model'; import { Result } from 'app/entities/result.model'; import { setLatestSubmissionResult } from 'app/entities/submission.model'; import { TextAssessmentBaseComponent } from 'app/exercises/text/assess/text-assessment-base.component'; -import { StructuredGradingCriterionService } from 'app/exercises/shared/structured-grading-criterion/structured-grading-criterion.service'; import { notUndefined } from 'app/shared/util/global.utils'; import { AssessButtonStates, Context, State, SubmissionButtonStates, UIStates } from 'app/exercises/text/manage/example-text-submission/example-text-submission-state.model'; import { filter, mergeMap, switchMap, tap } from 'rxjs/operators'; @@ -35,6 +32,14 @@ type ExampleSubmissionResponseType = EntityResponseType; styleUrls: ['./example-text-submission.component.scss'], }) export class ExampleTextSubmissionComponent extends TextAssessmentBaseComponent implements OnInit, Context, FeedbackMarker { + private route = inject(ActivatedRoute); + private router = inject(Router); + private exampleSubmissionService = inject(ExampleSubmissionService); + private tutorParticipationService = inject(TutorParticipationService); + private guidedTourService = inject(GuidedTourService); + private navigationUtilService = inject(ArtemisNavigationUtilService); + private exerciseService = inject(ExerciseService); + isNewSubmission: boolean; areNewAssessments = true; @@ -62,20 +67,8 @@ export class ExampleTextSubmissionComponent extends TextAssessmentBaseComponent faEdit = faEdit; farListAlt = faListAlt; - constructor( - private exampleSubmissionService: ExampleSubmissionService, - private tutorParticipationService: TutorParticipationService, - private route: ActivatedRoute, - private router: Router, - private guidedTourService: GuidedTourService, - private navigationUtilService: ArtemisNavigationUtilService, - private exerciseService: ExerciseService, - alertService: AlertService, - accountService: AccountService, - assessmentsService: TextAssessmentService, - structuredGradingCriterionService: StructuredGradingCriterionService, - ) { - super(alertService, accountService, assessmentsService, structuredGradingCriterionService); + constructor() { + super(); this.textBlockRefs = []; this.unusedTextBlockRefs = []; this.submission = new TextSubmission(); diff --git a/src/main/webapp/app/exercises/text/manage/text-exercise/text-exercise-detail.component.ts b/src/main/webapp/app/exercises/text/manage/text-exercise/text-exercise-detail.component.ts index e758013e44ee..92c75b1db510 100644 --- a/src/main/webapp/app/exercises/text/manage/text-exercise/text-exercise-detail.component.ts +++ b/src/main/webapp/app/exercises/text/manage/text-exercise/text-exercise-detail.component.ts @@ -1,4 +1,4 @@ -import { Component, OnDestroy, OnInit } from '@angular/core'; +import { Component, OnDestroy, OnInit, inject } from '@angular/core'; import { SafeHtml } from '@angular/platform-browser'; import { ActivatedRoute } from '@angular/router'; import { HttpResponse } from '@angular/common/http'; @@ -29,6 +29,12 @@ import { templateUrl: './text-exercise-detail.component.html', }) export class TextExerciseDetailComponent implements OnInit, OnDestroy { + private route = inject(ActivatedRoute); + private eventManager = inject(EventManager); + private artemisMarkdownService = inject(ArtemisMarkdownService); + private textExerciseService = inject(TextExerciseService); + private statisticsService = inject(StatisticsService); + readonly documentationType: DocumentationType = 'Text'; readonly AssessmentType = AssessmentType; @@ -48,14 +54,6 @@ export class TextExerciseDetailComponent implements OnInit, OnDestroy { private subscription: Subscription; private eventSubscriber: Subscription; - constructor( - private eventManager: EventManager, - private textExerciseService: TextExerciseService, - private route: ActivatedRoute, - private artemisMarkdown: ArtemisMarkdownService, - private statisticsService: StatisticsService, - ) {} - /** * Loads the text exercise and subscribes to changes of it on component initialization. */ @@ -78,9 +76,9 @@ export class TextExerciseDetailComponent implements OnInit, OnDestroy { this.isExamExercise = !!this.textExercise.exerciseGroup; this.course = this.isExamExercise ? this.textExercise.exerciseGroup?.exam?.course : this.textExercise.course; - this.formattedGradingInstructions = this.artemisMarkdown.safeHtmlForMarkdown(this.textExercise.gradingInstructions); - this.formattedProblemStatement = this.artemisMarkdown.safeHtmlForMarkdown(this.textExercise.problemStatement); - this.formattedExampleSolution = this.artemisMarkdown.safeHtmlForMarkdown(this.textExercise.exampleSolution); + this.formattedGradingInstructions = this.artemisMarkdownService.safeHtmlForMarkdown(this.textExercise.gradingInstructions); + this.formattedProblemStatement = this.artemisMarkdownService.safeHtmlForMarkdown(this.textExercise.problemStatement); + this.formattedExampleSolution = this.artemisMarkdownService.safeHtmlForMarkdown(this.textExercise.exampleSolution); this.detailOverviewSections = this.getExerciseDetailSections(); }); this.statisticsService.getExerciseStatistics(exerciseId).subscribe((statistics: ExerciseManagementStatisticsDto) => { diff --git a/src/main/webapp/app/exercises/text/manage/text-exercise/text-exercise-paging.service.ts b/src/main/webapp/app/exercises/text/manage/text-exercise/text-exercise-paging.service.ts index ed167d578dd0..6f5538b09ed1 100644 --- a/src/main/webapp/app/exercises/text/manage/text-exercise/text-exercise-paging.service.ts +++ b/src/main/webapp/app/exercises/text/manage/text-exercise/text-exercise-paging.service.ts @@ -1,5 +1,5 @@ import { HttpClient } from '@angular/common/http'; -import { Injectable } from '@angular/core'; +import { Injectable, inject } from '@angular/core'; import { TextExercise } from 'app/entities/text/text-exercise.model'; import { ExercisePagingService } from 'app/exercises/shared/manage/exercise-paging.service'; @@ -7,7 +7,8 @@ import { ExercisePagingService } from 'app/exercises/shared/manage/exercise-pagi export class TextExercisePagingService extends ExercisePagingService { private static readonly RESOURCE_URL = 'api/text-exercises'; - constructor(http: HttpClient) { + constructor() { + const http = inject(HttpClient); super(http, TextExercisePagingService.RESOURCE_URL); } } diff --git a/src/main/webapp/app/exercises/text/manage/text-exercise/text-exercise-row-buttons.component.ts b/src/main/webapp/app/exercises/text/manage/text-exercise/text-exercise-row-buttons.component.ts index 00e2973a5cc6..a1aab34bf04f 100644 --- a/src/main/webapp/app/exercises/text/manage/text-exercise/text-exercise-row-buttons.component.ts +++ b/src/main/webapp/app/exercises/text/manage/text-exercise/text-exercise-row-buttons.component.ts @@ -1,4 +1,4 @@ -import { Component, Input } from '@angular/core'; +import { Component, Input, inject } from '@angular/core'; import { HttpErrorResponse } from '@angular/common/http'; import { Subject } from 'rxjs'; import { TextExerciseService } from 'app/exercises/text/manage/text-exercise/text-exercise.service'; @@ -12,6 +12,9 @@ import { faListAlt } from '@fortawesome/free-regular-svg-icons'; templateUrl: './text-exercise-row-buttons.component.html', }) export class TextExerciseRowButtonsComponent { + private eventManager = inject(EventManager); + private textExerciseService = inject(TextExerciseService); + @Input() courseId: number; @Input() exercise: TextExercise; private dialogErrorSource = new Subject(); @@ -25,11 +28,6 @@ export class TextExerciseRowButtonsComponent { faTable = faTable; farListAlt = faListAlt; - constructor( - private textExerciseService: TextExerciseService, - private eventManager: EventManager, - ) {} - deleteExercise() { this.textExerciseService.delete(this.exercise.id!).subscribe({ next: () => { diff --git a/src/main/webapp/app/exercises/text/manage/text-exercise/text-exercise-update.component.ts b/src/main/webapp/app/exercises/text/manage/text-exercise/text-exercise-update.component.ts index e6abb283f200..18a5e8131205 100644 --- a/src/main/webapp/app/exercises/text/manage/text-exercise/text-exercise-update.component.ts +++ b/src/main/webapp/app/exercises/text/manage/text-exercise/text-exercise-update.component.ts @@ -1,4 +1,4 @@ -import { AfterViewInit, ChangeDetectionStrategy, Component, OnDestroy, OnInit, ViewChild } from '@angular/core'; +import { AfterViewInit, ChangeDetectionStrategy, Component, OnDestroy, OnInit, ViewChild, inject } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { HttpErrorResponse } from '@angular/common/http'; import { TextExercise } from 'app/entities/text/text-exercise.model'; @@ -37,6 +37,18 @@ import { FormulaAction } from 'app/shared/monaco-editor/model/actions/formula.ac changeDetection: ChangeDetectionStrategy.OnPush, }) export class TextExerciseUpdateComponent implements OnInit, OnDestroy, AfterViewInit { + private activatedRoute = inject(ActivatedRoute); + private alertService = inject(AlertService); + private textExerciseService = inject(TextExerciseService); + private modalService = inject(NgbModal); + private popupService = inject(ExerciseUpdateWarningService); + private exerciseService = inject(ExerciseService); + private exerciseGroupService = inject(ExerciseGroupService); + private courseService = inject(CourseManagementService); + private eventManager = inject(EventManager); + private navigationUtilService = inject(ArtemisNavigationUtilService); + private athenaService = inject(AthenaService); + readonly IncludedInOverallScore = IncludedInOverallScore; readonly documentationType: DocumentationType = 'Text'; @@ -78,20 +90,6 @@ export class TextExerciseUpdateComponent implements OnInit, OnDestroy, AfterView plagiarismSubscription?: Subscription; teamSubscription?: Subscription; - constructor( - private alertService: AlertService, - private textExerciseService: TextExerciseService, - private modalService: NgbModal, - private popupService: ExerciseUpdateWarningService, - private exerciseService: ExerciseService, - private exerciseGroupService: ExerciseGroupService, - private courseService: CourseManagementService, - private eventManager: EventManager, - private activatedRoute: ActivatedRoute, - private navigationUtilService: ArtemisNavigationUtilService, - private athenaService: AthenaService, - ) {} - get editType(): EditType { if (this.isImport) { return EditType.IMPORT; diff --git a/src/main/webapp/app/exercises/text/manage/text-exercise/text-exercise.component.ts b/src/main/webapp/app/exercises/text/manage/text-exercise/text-exercise.component.ts index ac46f89650d7..1ab47a466b49 100644 --- a/src/main/webapp/app/exercises/text/manage/text-exercise/text-exercise.component.ts +++ b/src/main/webapp/app/exercises/text/manage/text-exercise/text-exercise.component.ts @@ -1,19 +1,16 @@ -import { Component, Input } from '@angular/core'; +import { Component, Input, inject } from '@angular/core'; import { HttpErrorResponse, HttpResponse } from '@angular/common/http'; import { ExerciseType } from 'app/entities/exercise.model'; import { TextExercise } from 'app/entities/text/text-exercise.model'; import { TextExerciseService } from './text-exercise.service'; -import { CourseManagementService } from 'app/course/manage/course-management.service'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; -import { ActivatedRoute, Router } from '@angular/router'; +import { Router } from '@angular/router'; import { ExerciseComponent } from 'app/exercises/shared/exercise/exercise.component'; -import { TranslateService } from '@ngx-translate/core'; import { onError } from 'app/shared/util/global.utils'; import { AccountService } from 'app/core/auth/account.service'; import { SortService } from 'app/shared/service/sort.service'; import { ExerciseService } from 'app/exercises/shared/exercise/exercise.service'; import { AlertService } from 'app/core/util/alert.service'; -import { EventManager } from 'app/core/util/event-manager.service'; import { faPlus, faSort, faTrash } from '@fortawesome/free-solid-svg-icons'; import { CourseExerciseService } from 'app/exercises/shared/course-exercises/course-exercise.service'; import { ExerciseImportWrapperComponent } from 'app/exercises/shared/import/exercise-import-wrapper/exercise-import-wrapper.component'; @@ -23,8 +20,17 @@ import { ExerciseImportWrapperComponent } from 'app/exercises/shared/import/exer templateUrl: './text-exercise.component.html', }) export class TextExerciseComponent extends ExerciseComponent { - @Input() textExercises: TextExercise[]; - filteredTextExercises: TextExercise[]; + protected exerciseService = inject(ExerciseService); + protected textExerciseService = inject(TextExerciseService); + private router = inject(Router); + private courseExerciseService = inject(CourseExerciseService); + private modalService = inject(NgbModal); + private alertService = inject(AlertService); + private sortService = inject(SortService); + private accountService = inject(AccountService); + + @Input() textExercises: TextExercise[] = []; + filteredTextExercises: TextExercise[] = []; // Icons faSort = faSort; @@ -35,25 +41,6 @@ export class TextExerciseComponent extends ExerciseComponent { return this.textExercises; } - constructor( - public exerciseService: ExerciseService, - public textExerciseService: TextExerciseService, - private courseExerciseService: CourseExerciseService, - private modalService: NgbModal, - private router: Router, - courseService: CourseManagementService, - translateService: TranslateService, - private alertService: AlertService, - private sortService: SortService, - eventManager: EventManager, - route: ActivatedRoute, - private accountService: AccountService, - ) { - super(courseService, translateService, route, eventManager); - this.textExercises = []; - this.filteredTextExercises = []; - } - protected loadExercises(): void { this.courseExerciseService.findAllTextExercisesForCourse(this.courseId).subscribe({ next: (res: HttpResponse) => { diff --git a/src/main/webapp/app/exercises/text/manage/text-exercise/text-exercise.route.ts b/src/main/webapp/app/exercises/text/manage/text-exercise/text-exercise.route.ts index 30d4763745fc..d5431d66cc5f 100644 --- a/src/main/webapp/app/exercises/text/manage/text-exercise/text-exercise.route.ts +++ b/src/main/webapp/app/exercises/text/manage/text-exercise/text-exercise.route.ts @@ -3,7 +3,7 @@ import { UserRouteAccessService } from 'app/core/auth/user-route-access-service' import { TextExerciseDetailComponent } from './text-exercise-detail.component'; import { TextExerciseUpdateComponent } from './text-exercise-update.component'; import { TextExercise } from 'app/entities/text/text-exercise.model'; -import { Injectable } from '@angular/core'; +import { Injectable, inject } from '@angular/core'; import { TextExerciseService } from 'app/exercises/text/manage/text-exercise/text-exercise.service'; import { CourseManagementService } from 'app/course/manage/course-management.service'; import { of } from 'rxjs'; @@ -19,11 +19,9 @@ import { ExampleSubmissionsComponent } from 'app/exercises/shared/example-submis @Injectable({ providedIn: 'root' }) export class TextExerciseResolver implements Resolve { - constructor( - private textExerciseService: TextExerciseService, - private courseService: CourseManagementService, - private exerciseGroupService: ExerciseGroupService, - ) {} + private textExerciseService = inject(TextExerciseService); + private courseService = inject(CourseManagementService); + private exerciseGroupService = inject(ExerciseGroupService); /** * Resolves the route and initializes text exercise diff --git a/src/main/webapp/app/exercises/text/manage/text-exercise/text-exercise.service.ts b/src/main/webapp/app/exercises/text/manage/text-exercise/text-exercise.service.ts index 994f1ac75eeb..5c6b9275b3a1 100644 --- a/src/main/webapp/app/exercises/text/manage/text-exercise/text-exercise.service.ts +++ b/src/main/webapp/app/exercises/text/manage/text-exercise/text-exercise.service.ts @@ -1,4 +1,4 @@ -import { Injectable } from '@angular/core'; +import { Injectable, inject } from '@angular/core'; import { HttpClient, HttpResponse } from '@angular/common/http'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; @@ -16,12 +16,10 @@ export type EntityArrayResponseType = HttpResponse; @Injectable({ providedIn: 'root' }) export class TextExerciseService implements ExerciseServicable { - private resourceUrl = 'api/text-exercises'; + private http = inject(HttpClient); + private exerciseService = inject(ExerciseService); - constructor( - private http: HttpClient, - private exerciseService: ExerciseService, - ) {} + private resourceUrl = 'api/text-exercises'; /** * Store a new text exercise on the server. diff --git a/src/main/webapp/app/exercises/text/manage/tutor-effort/tutor-effort-statistics.component.ts b/src/main/webapp/app/exercises/text/manage/tutor-effort/tutor-effort-statistics.component.ts index e6c54d0ce175..45d7cb002e0b 100644 --- a/src/main/webapp/app/exercises/text/manage/tutor-effort/tutor-effort-statistics.component.ts +++ b/src/main/webapp/app/exercises/text/manage/tutor-effort/tutor-effort-statistics.component.ts @@ -1,4 +1,4 @@ -import { Component, OnInit } from '@angular/core'; +import { Component, OnInit, inject } from '@angular/core'; import { TutorEffort } from 'app/entities/tutor-effort.model'; import { TextExerciseService } from 'app/exercises/text/manage/text-exercise/text-exercise.service'; import { ActivatedRoute, Router } from '@angular/router'; @@ -21,6 +21,12 @@ interface TutorEffortRange { styleUrls: ['./tutor-effort-statistics.component.scss'], }) export class TutorEffortStatisticsComponent extends PlagiarismAndTutorEffortDirective implements OnInit { + private route = inject(ActivatedRoute); + private router = inject(Router); + private textExerciseService = inject(TextExerciseService); + private textAssessmentService = inject(TextAssessmentService); + private translateService = inject(TranslateService); + tutorEfforts: TutorEffort[] = []; numberOfSubmissions: number; totalTimeSpent: number; @@ -43,13 +49,7 @@ export class TutorEffortStatisticsComponent extends PlagiarismAndTutorEffortDire // Icons faSync = faSync; - constructor( - private textExerciseService: TextExerciseService, - private textAssessmentService: TextAssessmentService, - private route: ActivatedRoute, - private translateService: TranslateService, - private router: Router, - ) { + constructor() { super(); this.translateService.onLangChange.subscribe(() => { this.translateLabels(); diff --git a/src/main/webapp/app/exercises/text/participate/text-editor.component.ts b/src/main/webapp/app/exercises/text/participate/text-editor.component.ts index 94659dfc211d..81deae339df6 100644 --- a/src/main/webapp/app/exercises/text/participate/text-editor.component.ts +++ b/src/main/webapp/app/exercises/text/participate/text-editor.component.ts @@ -1,5 +1,4 @@ -import { Component, Input, OnDestroy, OnInit } from '@angular/core'; -import { TranslateService } from '@ngx-translate/core'; +import { Component, Input, OnDestroy, OnInit, inject } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { HttpErrorResponse } from '@angular/common/http'; import { AlertService } from 'app/core/util/alert.service'; @@ -34,7 +33,7 @@ import { ProfileService } from 'app/shared/layouts/profiles/profile.service'; import { PROFILE_IRIS } from 'app/app.constants'; import { IrisSettingsService } from 'app/iris/settings/shared/iris-settings.service'; import { AssessmentType } from 'app/entities/assessment-type.model'; -import { CourseExerciseService } from 'app/exercises/shared/course-exercises/course-exercise.service'; + @Component({ selector: 'jhi-text-editor', templateUrl: './text-editor.component.html', @@ -42,6 +41,16 @@ import { CourseExerciseService } from 'app/exercises/shared/course-exercises/cou styleUrls: ['./text-editor.component.scss'], }) export class TextEditorComponent implements OnInit, OnDestroy, ComponentCanDeactivate { + private route = inject(ActivatedRoute); + private textSubmissionService = inject(TextSubmissionService); + private textService = inject(TextEditorService); + private alertService = inject(AlertService); + private participationWebsocketService = inject(ParticipationWebsocketService); + private stringCountService = inject(StringCountService); + private accountService = inject(AccountService); + private profileService = inject(ProfileService); + private irisSettingsService = inject(IrisSettingsService); + readonly ButtonType = ButtonType; readonly MAX_CHARACTER_COUNT = MAX_SUBMISSION_TEXT_LENGTH; protected readonly Result = Result; @@ -64,7 +73,7 @@ export class TextEditorComponent implements OnInit, OnDestroy, ComponentCanDeact resultWithComplaint?: Result; submission: TextSubmission; course?: Course; - isSaving: boolean; + isSaving: boolean = false; private textEditorInput = new Subject(); textEditorInputObservable = this.textEditorInput.asObservable(); private submissionChange = new Subject(); @@ -96,22 +105,6 @@ export class TextEditorComponent implements OnInit, OnDestroy, ComponentCanDeact showHistory: boolean = false; submissionId: number | undefined; - constructor( - private route: ActivatedRoute, - private textSubmissionService: TextSubmissionService, - private textService: TextEditorService, - private alertService: AlertService, - private translateService: TranslateService, - private participationWebsocketService: ParticipationWebsocketService, - private stringCountService: StringCountService, - private accountService: AccountService, - private courseExerciseService: CourseExerciseService, - private profileService: ProfileService, - private irisSettingsService: IrisSettingsService, - ) { - this.isSaving = false; - } - ngOnInit() { if (this.inputValuesArePresent()) { this.setupComponentWithInputValues(); diff --git a/src/main/webapp/app/exercises/text/participate/text-editor.service.ts b/src/main/webapp/app/exercises/text/participate/text-editor.service.ts index cf7d67767a0a..5c87204ace41 100644 --- a/src/main/webapp/app/exercises/text/participate/text-editor.service.ts +++ b/src/main/webapp/app/exercises/text/participate/text-editor.service.ts @@ -1,4 +1,4 @@ -import { Injectable } from '@angular/core'; +import { Injectable, inject } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { Observable, tap } from 'rxjs'; import { francAll } from 'franc-min'; @@ -8,7 +8,7 @@ import { ExerciseService } from 'app/exercises/shared/exercise/exercise.service' @Injectable({ providedIn: 'root' }) export class TextEditorService { - constructor(private http: HttpClient) {} + private http = inject(HttpClient); get(participationId: number): Observable { return this.http diff --git a/src/main/webapp/app/exercises/text/participate/text-result/text-result.component.ts b/src/main/webapp/app/exercises/text/participate/text-result/text-result.component.ts index c63aaece1335..00fdd151b73b 100644 --- a/src/main/webapp/app/exercises/text/participate/text-result/text-result.component.ts +++ b/src/main/webapp/app/exercises/text/participate/text-result/text-result.component.ts @@ -1,4 +1,4 @@ -import { Component, Input } from '@angular/core'; +import { Component, Input, inject } from '@angular/core'; import { Feedback, buildFeedbackTextForReview, checkSubsequentFeedbackInAssessment } from 'app/entities/feedback.model'; import { TextSubmission } from 'app/entities/text/text-submission.model'; import { Result } from 'app/entities/result.model'; @@ -15,6 +15,9 @@ import { Course } from 'app/entities/course.model'; styleUrls: ['./text-result.component.scss'], }) export class TextResultComponent { + private translateService = inject(TranslateService); + private localeConversionService = inject(LocaleConversionService); + public submissionText: string; public textResults: TextResultBlock[]; @@ -40,11 +43,6 @@ export class TextResultComponent { @Input() course?: Course; - constructor( - private translateService: TranslateService, - private localeConversionService: LocaleConversionService, - ) {} - private convertTextToResultBlocks(feedbacks: Feedback[] = []): void { checkSubsequentFeedbackInAssessment(feedbacks); diff --git a/src/main/webapp/app/exercises/text/participate/text-submission.service.ts b/src/main/webapp/app/exercises/text/participate/text-submission.service.ts index 2c90d0c7188c..7d406b3fe8d3 100644 --- a/src/main/webapp/app/exercises/text/participate/text-submission.service.ts +++ b/src/main/webapp/app/exercises/text/participate/text-submission.service.ts @@ -1,4 +1,4 @@ -import { Injectable } from '@angular/core'; +import { Injectable, inject } from '@angular/core'; import { HttpClient, HttpParams, HttpResponse } from '@angular/common/http'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; @@ -12,10 +12,8 @@ export type EntityResponseType = HttpResponse; @Injectable({ providedIn: 'root' }) export class TextSubmissionService { - constructor( - private http: HttpClient, - private submissionService: SubmissionService, - ) {} + private http = inject(HttpClient); + private submissionService = inject(SubmissionService); create(textSubmission: TextSubmission, exerciseId: number): Observable { const copy = this.submissionService.convert(textSubmission); diff --git a/src/main/webapp/app/exercises/text/shared/manual-text-selection/manual-text-selection.component.ts b/src/main/webapp/app/exercises/text/shared/manual-text-selection/manual-text-selection.component.ts index 1a846ba58988..b7c83334e01e 100644 --- a/src/main/webapp/app/exercises/text/shared/manual-text-selection/manual-text-selection.component.ts +++ b/src/main/webapp/app/exercises/text/shared/manual-text-selection/manual-text-selection.component.ts @@ -1,4 +1,4 @@ -import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { Component, EventEmitter, Input, Output, inject } from '@angular/core'; import { TextAssessmentEventType } from 'app/entities/text/text-assesment-event.model'; import { FeedbackType } from 'app/entities/feedback.model'; import { TextBlockType } from 'app/entities/text/text-block.model'; @@ -21,6 +21,9 @@ const SPACE = ' '; styleUrls: ['./manual-text-selection.component.scss'], }) export class ManualTextSelectionComponent { + protected route = inject(ActivatedRoute); + textAssessmentAnalytics = inject(TextAssessmentAnalytics); + @Input() public textBlockRefGroup: TextBlockRefGroup; @Input() submission: TextSubmission; @Output() public didSelectWord = new EventEmitter(); @@ -33,11 +36,8 @@ export class ManualTextSelectionComponent { public selectedWords = new Array(); public ready = false; - constructor( - public textAssessmentAnalytics: TextAssessmentAnalytics, - protected route: ActivatedRoute, - ) { - textAssessmentAnalytics.setComponentRoute(route); + constructor() { + this.textAssessmentAnalytics.setComponentRoute(this.route); } calculateIndex(index: number): void { diff --git a/src/main/webapp/app/exercises/text/shared/text-select.directive.ts b/src/main/webapp/app/exercises/text/shared/text-select.directive.ts index 3ff564df7462..8ead1a2d74ee 100644 --- a/src/main/webapp/app/exercises/text/shared/text-select.directive.ts +++ b/src/main/webapp/app/exercises/text/shared/text-select.directive.ts @@ -1,4 +1,4 @@ -import { Directive, ElementRef, EventEmitter, NgZone, OnDestroy, OnInit, Output } from '@angular/core'; +import { Directive, ElementRef, EventEmitter, NgZone, OnDestroy, OnInit, Output, inject } from '@angular/core'; /** * Disclaimer: @@ -32,15 +32,13 @@ enum EventType { selector: '[jhiTextSelect]', }) export class TextSelectDirective implements OnInit, OnDestroy { + private elementRef = inject(ElementRef); + private zone = inject(NgZone); + @Output() public jhiTextSelect = new EventEmitter(); private hasSelection = false; - constructor( - private elementRef: ElementRef, - private zone: NgZone, - ) {} - /** * Init text select directive by adding event listenes mouseDown and selectionChange event listeners to element. */ diff --git a/src/main/webapp/app/shared/metis/answer-post/answer-post.component.html b/src/main/webapp/app/shared/metis/answer-post/answer-post.component.html index 073e4e907593..6682f5c146c0 100644 --- a/src/main/webapp/app/shared/metis/answer-post/answer-post.component.html +++ b/src/main/webapp/app/shared/metis/answer-post/answer-post.component.html @@ -5,7 +5,6 @@ [isReadOnlyMode]="isReadOnlyMode" [isCommunicationPage]="isCommunicationPage" [lastReadDate]="lastReadDate" - [hasChannelModerationRights]="hasChannelModerationRights" [isDeleted]="isDeleted" /> } diff --git a/src/main/webapp/app/shared/metis/post/post.component.html b/src/main/webapp/app/shared/metis/post/post.component.html index 1fcad5a5987f..42352bdfa0d6 100644 --- a/src/main/webapp/app/shared/metis/post/post.component.html +++ b/src/main/webapp/app/shared/metis/post/post.component.html @@ -6,7 +6,6 @@ [posting]="posting" [isDeleted]="isDeleted" [isCommunicationPage]="isCommunicationPage" - [hasChannelModerationRights]="hasChannelModerationRights" (isModalOpen)="displayInlineInput = true" [lastReadDate]="lastReadDate" /> @@ -68,6 +67,7 @@ >
@if (!previewMode) { + - + @if (!previewMode) { } - diff --git a/src/main/webapp/app/shared/metis/post/post.component.scss b/src/main/webapp/app/shared/metis/post/post.component.scss index 1a6cf4b3ace2..10cb7ef83132 100644 --- a/src/main/webapp/app/shared/metis/post/post.component.scss +++ b/src/main/webapp/app/shared/metis/post/post.component.scss @@ -20,6 +20,7 @@ top: -1.8rem; right: 1%; display: flex; + max-height: 2.2rem; gap: 10px; visibility: hidden; transition: diff --git a/src/main/webapp/app/shared/metis/post/post.component.ts b/src/main/webapp/app/shared/metis/post/post.component.ts index d70c13abae68..bd502fbea1ac 100644 --- a/src/main/webapp/app/shared/metis/post/post.component.ts +++ b/src/main/webapp/app/shared/metis/post/post.component.ts @@ -90,7 +90,7 @@ export class PostComponent extends PostingDirective implements OnInit, OnC isConsecutive = input(false); dropdownPosition = { x: 0, y: 0 }; - @ViewChild(PostReactionsBarComponent) private reactionsBarComponent!: PostReactionsBarComponent; + @ViewChild(PostReactionsBarComponent) protected reactionsBarComponent!: PostReactionsBarComponent; constructor( public metisService: MetisService, @@ -218,6 +218,13 @@ export class PostComponent extends PostingDirective implements OnInit, OnC this.postFooterComponent.openCreateAnswerPostModal(); } + /** + * Close create answer modal + */ + closeCreateAnswerPostModal() { + this.postFooterComponent.closeCreateAnswerPostModal(); + } + /** * sorts answerPosts by two criteria * 1. criterion: resolvesPost -> true comes first diff --git a/src/main/webapp/app/shared/metis/posting-footer/post-footer/post-footer.component.ts b/src/main/webapp/app/shared/metis/posting-footer/post-footer/post-footer.component.ts index 7f6d3b0531a5..5808a9b342ba 100644 --- a/src/main/webapp/app/shared/metis/posting-footer/post-footer/post-footer.component.ts +++ b/src/main/webapp/app/shared/metis/posting-footer/post-footer/post-footer.component.ts @@ -173,6 +173,13 @@ export class PostFooterComponent extends PostingFooterDirective implements this.createAnswerPostModalComponent.open(); } + /** + * Close create answer modal + */ + closeCreateAnswerPostModal() { + this.createAnswerPostModalComponent.close(); + } + protected postsTrackByFn(index: number, post: Post): number { return post.id!; } diff --git a/src/main/webapp/app/shared/metis/posting-header/answer-post-header/answer-post-header.component.ts b/src/main/webapp/app/shared/metis/posting-header/answer-post-header/answer-post-header.component.ts index 9871b3bbee8f..bb1ff3cbbbd2 100644 --- a/src/main/webapp/app/shared/metis/posting-header/answer-post-header/answer-post-header.component.ts +++ b/src/main/webapp/app/shared/metis/posting-header/answer-post-header/answer-post-header.component.ts @@ -2,7 +2,7 @@ import { Component, EventEmitter, Input, OnChanges, OnInit, Output } from '@angu import { AnswerPost } from 'app/entities/metis/answer-post.model'; import { PostingHeaderDirective } from 'app/shared/metis/posting-header/posting-header.directive'; import { MetisService } from 'app/shared/metis/metis.service'; -import { faCheck, faCog, faPencilAlt } from '@fortawesome/free-solid-svg-icons'; +import { faCheck, faPencilAlt } from '@fortawesome/free-solid-svg-icons'; import dayjs from 'dayjs/esm'; import { AccountService } from 'app/core/auth/account.service'; @@ -18,13 +18,9 @@ export class AnswerPostHeaderComponent extends PostingHeaderDirective(); - isAuthorOfOriginalPost: boolean; - isAnswerOfAnnouncement: boolean; - // Icons - faCheck = faCheck; - faPencilAlt = faPencilAlt; - faCog = faCog; + readonly faCheck = faCheck; + readonly faPencilAlt = faPencilAlt; constructor( protected metisService: MetisService, diff --git a/src/main/webapp/app/shared/metis/posting-header/post-header/post-header.component.ts b/src/main/webapp/app/shared/metis/posting-header/post-header/post-header.component.ts index 3463b66456fc..fe4bc129addd 100644 --- a/src/main/webapp/app/shared/metis/posting-header/post-header/post-header.component.ts +++ b/src/main/webapp/app/shared/metis/posting-header/post-header/post-header.component.ts @@ -3,7 +3,7 @@ import { Post } from 'app/entities/metis/post.model'; import { PostingHeaderDirective } from 'app/shared/metis/posting-header/posting-header.directive'; import { MetisService } from 'app/shared/metis/metis.service'; import { PostCreateEditModalComponent } from 'app/shared/metis/posting-create-edit-modal/post-create-edit-modal/post-create-edit-modal.component'; -import { faCheckSquare, faCog, faPencilAlt } from '@fortawesome/free-solid-svg-icons'; +import { faCheckSquare, faPencilAlt } from '@fortawesome/free-solid-svg-icons'; import dayjs from 'dayjs/esm'; import { CachingStrategy } from 'app/shared/image/secured-image.component'; import { AccountService } from 'app/core/auth/account.service'; @@ -22,9 +22,8 @@ export class PostHeaderComponent extends PostingHeaderDirective implements isAtLeastInstructorInCourse: boolean; // Icons - faPencilAlt = faPencilAlt; - faCheckSquare = faCheckSquare; - faCog = faCog; + readonly faPencilAlt = faPencilAlt; + readonly faCheckSquare = faCheckSquare; constructor( protected metisService: MetisService, diff --git a/src/main/webapp/app/shared/metis/posting-reactions-bar/answer-post-reactions-bar/answer-post-reactions-bar.component.html b/src/main/webapp/app/shared/metis/posting-reactions-bar/answer-post-reactions-bar/answer-post-reactions-bar.component.html index 86a4a499a599..e0ed87168dc9 100644 --- a/src/main/webapp/app/shared/metis/posting-reactions-bar/answer-post-reactions-bar/answer-post-reactions-bar.component.html +++ b/src/main/webapp/app/shared/metis/posting-reactions-bar/answer-post-reactions-bar/answer-post-reactions-bar.component.html @@ -89,13 +89,4 @@ } }
- - @if (isLastAnswer && !isThreadSidebar) { -
- -
- } diff --git a/src/main/webapp/app/shared/metis/posting-reactions-bar/post-reactions-bar/post-reactions-bar.component.html b/src/main/webapp/app/shared/metis/posting-reactions-bar/post-reactions-bar/post-reactions-bar.component.html index 5ba32fbe573e..500954b38df8 100644 --- a/src/main/webapp/app/shared/metis/posting-reactions-bar/post-reactions-bar/post-reactions-bar.component.html +++ b/src/main/webapp/app/shared/metis/posting-reactions-bar/post-reactions-bar/post-reactions-bar.component.html @@ -1,10 +1,18 @@ -
+
+ @if (hoverBar && sortedAnswerPosts.length === 0) { +
+ +
+ } @if (!isCommunicationPage) { @if (sortedAnswerPosts.length) { @if (showAnswers) {
- @@ -12,8 +20,8 @@ } @else {
-
} - } @else { - -
- -
} } @else { @if (!isThreadSidebar) { - - @if (hoverBar && sortedAnswerPosts.length === 0) { -
- -
- } @if (!showAnswers && sortedAnswerPosts.length) {
@@ -142,7 +133,7 @@ }
- @if (getShowNewMessageIcon()) { + @if (isEmojiCount && getShowNewMessageIcon()) {
}
diff --git a/src/main/webapp/app/shared/metis/posting-reactions-bar/post-reactions-bar/post-reactions-bar.component.ts b/src/main/webapp/app/shared/metis/posting-reactions-bar/post-reactions-bar/post-reactions-bar.component.ts index 1d039326b20b..2ff96f282a6b 100644 --- a/src/main/webapp/app/shared/metis/posting-reactions-bar/post-reactions-bar/post-reactions-bar.component.ts +++ b/src/main/webapp/app/shared/metis/posting-reactions-bar/post-reactions-bar/post-reactions-bar.component.ts @@ -41,6 +41,7 @@ export class PostReactionsBarComponent extends PostingsReactionsBarDirective(); @Output() openPostingCreateEditModal = new EventEmitter(); + @Output() closePostingCreateEditModal = new EventEmitter(); @Output() openThread = new EventEmitter(); @Input() previewMode: boolean; isAtLeastInstructorInCourse: boolean; @@ -63,6 +64,16 @@ export class PostReactionsBarComponent extends PostingsReactionsBarDirective reaction.count >= 1); } + openAnswerView() { + this.showAnswersChange.emit(true); + this.openPostingCreateEditModal.emit(); + } + + closeAnswerView() { + this.showAnswersChange.emit(false); + this.closePostingCreateEditModal.emit(); + } + /** * on initialization: call resetTooltipsAndPriority */ diff --git a/src/main/webapp/app/shared/metis/posting-reactions-bar/posting-reactions-bar.component.scss b/src/main/webapp/app/shared/metis/posting-reactions-bar/posting-reactions-bar.component.scss index 0a289a54ceaa..bc7fbfc61165 100644 --- a/src/main/webapp/app/shared/metis/posting-reactions-bar/posting-reactions-bar.component.scss +++ b/src/main/webapp/app/shared/metis/posting-reactions-bar/posting-reactions-bar.component.scss @@ -13,6 +13,7 @@ border-radius: 1rem; align-items: center; height: 1.2rem; + display: inline-flex; &.open-selector { box-shadow: inset 0 0 0 1px var(--metis-gray); diff --git a/src/main/webapp/i18n/en/buildAgents.json b/src/main/webapp/i18n/en/buildAgents.json index 1610e1bd0847..974086655128 100644 --- a/src/main/webapp/i18n/en/buildAgents.json +++ b/src/main/webapp/i18n/en/buildAgents.json @@ -8,9 +8,9 @@ "memberAddress": "Member Address", "maxNumberOfConcurrentBuildJobs": "Max # of concurrent build jobs", "numberOfCurrentBuildJobs": "# of current build jobs", - "runningBuildJobs": "Running build jobs", + "runningBuildJobs": "Running Build Jobs", "status": "Status", - "recentBuildJobs": "Recent build jobs", + "recentBuildJobs": "Recent Build Jobs", "running": "Running", "idle": "Idle", "paused": "Paused", diff --git a/src/test/javascript/spec/component/shared/metis/post/post.component.spec.ts b/src/test/javascript/spec/component/shared/metis/post/post.component.spec.ts index cdcc598b7988..c7c9cf4f24db 100644 --- a/src/test/javascript/spec/component/shared/metis/post/post.component.spec.ts +++ b/src/test/javascript/spec/component/shared/metis/post/post.component.spec.ts @@ -170,6 +170,15 @@ describe('PostComponent', () => { expect(postFooterOpenCreateAnswerPostModal).toHaveBeenCalledOnce(); }); + it('should close create answer post modal', () => { + component.posting = metisPostExerciseUser1; + component.ngOnInit(); + fixture.detectChanges(); + const postFooterOpenCreateAnswerPostModal = jest.spyOn(component.postFooterComponent, 'closeCreateAnswerPostModal'); + component.closeCreateAnswerPostModal(); + expect(postFooterOpenCreateAnswerPostModal).toHaveBeenCalledOnce(); + }); + it('should create or navigate to oneToOneChat when not on messaging page', () => { const navigateSpy = jest.spyOn(router, 'navigate'); const oneToOneChatService = TestBed.inject(OneToOneChatService); diff --git a/src/test/javascript/spec/component/shared/metis/postings-footer/post-footer/post-footer.component.spec.ts b/src/test/javascript/spec/component/shared/metis/postings-footer/post-footer/post-footer.component.spec.ts index 45c38925db52..8071b5a84a95 100644 --- a/src/test/javascript/spec/component/shared/metis/postings-footer/post-footer/post-footer.component.spec.ts +++ b/src/test/javascript/spec/component/shared/metis/postings-footer/post-footer/post-footer.component.spec.ts @@ -151,4 +151,13 @@ describe('PostFooterComponent', () => { component.openCreateAnswerPostModal(); expect(createAnswerPostModalOpen).toHaveBeenCalledOnce(); }); + + it('should close create answer post modal', () => { + component.posting = metisPostExerciseUser1; + component.ngOnInit(); + fixture.detectChanges(); + const createAnswerPostModalClose = jest.spyOn(component.createAnswerPostModalComponent, 'close'); + component.closeCreateAnswerPostModal(); + expect(createAnswerPostModalClose).toHaveBeenCalledOnce(); + }); }); diff --git a/src/test/javascript/spec/component/shared/metis/postings-reactions-bar/answer-post-reactions-bar/answer-post-reactions-bar.component.spec.ts b/src/test/javascript/spec/component/shared/metis/postings-reactions-bar/answer-post-reactions-bar/answer-post-reactions-bar.component.spec.ts index 8e5b51590166..a3b889e8d146 100644 --- a/src/test/javascript/spec/component/shared/metis/postings-reactions-bar/answer-post-reactions-bar/answer-post-reactions-bar.component.spec.ts +++ b/src/test/javascript/spec/component/shared/metis/postings-reactions-bar/answer-post-reactions-bar/answer-post-reactions-bar.component.spec.ts @@ -193,15 +193,6 @@ describe('AnswerPostReactionsBarComponent', () => { expect(answerNowButton).toBeNull(); }); - it('answer now button should be visible if answer is the last one', () => { - component.posting = post; - component.isLastAnswer = true; - component.ngOnInit(); - fixture.detectChanges(); - const answerNowButton = fixture.debugElement.query(By.css('.reply-btn')).nativeElement; - expect(answerNowButton.innerHTML).toContain('reply'); - }); - it('should invoke metis service when toggle resolve is clicked', () => { metisServiceUserPostingAuthorMock.mockReturnValue(true); fixture.detectChanges(); diff --git a/src/test/javascript/spec/component/shared/metis/postings-reactions-bar/post-reactions-bar/post-reactions-bar.component.spec.ts b/src/test/javascript/spec/component/shared/metis/postings-reactions-bar/post-reactions-bar/post-reactions-bar.component.spec.ts index f5ccda3ada15..d1b1607ffb1d 100644 --- a/src/test/javascript/spec/component/shared/metis/postings-reactions-bar/post-reactions-bar/post-reactions-bar.component.spec.ts +++ b/src/test/javascript/spec/component/shared/metis/postings-reactions-bar/post-reactions-bar/post-reactions-bar.component.spec.ts @@ -3,7 +3,7 @@ import { MetisService } from 'app/shared/metis/metis.service'; import { DebugElement } from '@angular/core'; import { Post } from 'app/entities/metis/post.model'; import { MockComponent, MockDirective, MockModule, MockPipe, MockProvider } from 'ng-mocks'; -import { getElement, getElements } from '../../../../../helpers/utils/general.utils'; +import { getElement } from '../../../../../helpers/utils/general.utils'; import { PostReactionsBarComponent } from 'app/shared/metis/posting-reactions-bar/post-reactions-bar/post-reactions-bar.component'; import { OverlayModule } from '@angular/cdk/overlay'; import { Reaction } from 'app/entities/metis/reaction.model'; @@ -239,9 +239,8 @@ describe('PostReactionsBarComponent', () => { component.posting = post; component.ngOnInit(); + component.isEmojiCount = true; fixture.detectChanges(); - const reactions = getElements(debugElement, 'jhi-emoji'); - expect(reactions).toHaveLength(2); expect(component.reactionMetaDataMap).toEqual({ smile: { count: 1, @@ -311,14 +310,6 @@ describe('PostReactionsBarComponent', () => { expect(component.pinTooltip).toBe('artemisApp.metis.pinnedPostTooltip'); }); - it('start discussion button should be visible if post does not yet have any answers', () => { - component.posting = post; - component.sortedAnswerPosts = []; - fixture.detectChanges(); - const startDiscussion = fixture.debugElement.query(By.css('.reply-btn')).nativeElement; - expect(startDiscussion.innerHTML).toContain('reply'); - }); - it('should display button to show single answer', () => { component.posting = post; component.sortedAnswerPosts = [metisPostExerciseUser1]; @@ -343,4 +334,24 @@ describe('PostReactionsBarComponent', () => { const answerNowButton = fixture.debugElement.query(By.css('.collapse-answers-btn')).nativeElement; expect(answerNowButton.innerHTML).toContain('collapseAnswers'); }); + + it('should emit showAnswersChange and openPostingCreateEditModal when openAnswerView is called', () => { + const showAnswersChangeSpy = jest.spyOn(component.showAnswersChange, 'emit'); + const openPostingCreateEditModalSpy = jest.spyOn(component.openPostingCreateEditModal, 'emit'); + + component.openAnswerView(); + + expect(showAnswersChangeSpy).toHaveBeenCalledWith(true); + expect(openPostingCreateEditModalSpy).toHaveBeenCalled(); + }); + + it('should emit showAnswersChange and closePostingCreateEditModal when closeAnswerView is called', () => { + const showAnswersChangeSpy = jest.spyOn(component.showAnswersChange, 'emit'); + const closePostingCreateEditModalSpy = jest.spyOn(component.closePostingCreateEditModal, 'emit'); + + component.closeAnswerView(); + + expect(showAnswersChangeSpy).toHaveBeenCalledWith(false); + expect(closePostingCreateEditModalSpy).toHaveBeenCalled(); + }); }); diff --git a/src/test/javascript/spec/service/text-editor.service.spec.ts b/src/test/javascript/spec/service/text-editor.service.spec.ts index 4171441cdcc0..c701dad2f618 100644 --- a/src/test/javascript/spec/service/text-editor.service.spec.ts +++ b/src/test/javascript/spec/service/text-editor.service.spec.ts @@ -1,21 +1,19 @@ import { TextEditorService } from 'app/exercises/text/participate/text-editor.service'; -import { HttpClient, HttpEvent, HttpHandler } from '@angular/common/http'; -import { Observable } from 'rxjs'; +import { provideHttpClient } from '@angular/common/http'; import { Language } from 'app/entities/course.model'; - -class MockHttpHandler implements HttpHandler { - handle(): Observable> { - return new Observable>(); - } -} +import { provideHttpClientTesting } from '@angular/common/http/testing'; +import { TestBed } from '@angular/core/testing'; describe('TextEditorService', () => { let textEditorService: TextEditorService; - let httpClient: HttpClient; + beforeEach(() => { - httpClient = new HttpClient(new MockHttpHandler()); - textEditorService = new TextEditorService(httpClient); + TestBed.configureTestingModule({ + imports: [], + providers: [provideHttpClient(), provideHttpClientTesting()], + }); + textEditorService = TestBed.inject(TextEditorService); }); it('Can detect a short German string', () => { const testString = 'Das ist ein kurzer, deutscher Satz';