diff --git a/src/main/webapp/app/complaints/complaints-for-students/complaints-student-view.component.html b/src/main/webapp/app/complaints/complaints-for-students/complaints-student-view.component.html index ae902f4ec8de..783d822a2e84 100644 --- a/src/main/webapp/app/complaints/complaints-for-students/complaints-student-view.component.html +++ b/src/main/webapp/app/complaints/complaints-for-students/complaints-student-view.component.html @@ -16,6 +16,7 @@ }

} + @if (isCorrectUserToFileAction && !complaint) {
@if (isExamMode || (course?.maxComplaints && course!.maxComplaints! > 0)) { @@ -23,7 +24,7 @@ id="complain" class="btn btn-primary" [class.not-allowed]="(!isExamMode && remainingNumberOfComplaints === 0) || !timeOfComplaintValid" - (click)="formComplaintType = ComplaintType.COMPLAINT" + (click)="openComplaintForm(ComplaintType.COMPLAINT)" [disabled]="(!isExamMode && remainingNumberOfComplaints === 0) || !timeOfComplaintValid" title="{{ (!isExamMode && remainingNumberOfComplaints === 0) || !timeOfComplaintValid @@ -35,9 +36,10 @@ } @if (!isExamMode && course?.requestMoreFeedbackEnabled) {
} +
@if (complaint) {
diff --git a/src/main/webapp/app/complaints/complaints-for-students/complaints-student-view.component.ts b/src/main/webapp/app/complaints/complaints-for-students/complaints-student-view.component.ts index b469dcab432f..26d9308a110e 100644 --- a/src/main/webapp/app/complaints/complaints-for-students/complaints-student-view.component.ts +++ b/src/main/webapp/app/complaints/complaints-for-students/complaints-student-view.component.ts @@ -1,4 +1,4 @@ -import { Component, Input, OnInit } from '@angular/core'; +import { ChangeDetectorRef, Component, ElementRef, Input, OnInit, ViewChild } from '@angular/core'; import { Exercise, getCourseFromExercise } from 'app/entities/exercise.model'; import { Complaint, ComplaintType } from 'app/entities/complaint.model'; import { ComplaintService } from 'app/complaints/complaint.service'; @@ -28,6 +28,8 @@ export class ComplaintsStudentViewComponent implements OnInit { // flag to indicate exam test run. Default set to false. @Input() testRun = false; + @ViewChild('complaintScrollpoint') complaintScrollpoint: ElementRef; + submission: Submission; complaint: Complaint; course?: Course; @@ -51,6 +53,7 @@ export class ComplaintsStudentViewComponent implements OnInit { private serverDateService: ArtemisServerDateService, private accountService: AccountService, private courseService: CourseManagementService, + private cdr: ChangeDetectorRef, ) {} /** @@ -149,4 +152,20 @@ export class ComplaintsStudentViewComponent implements OnInit { } return false; } + + /** + * Function to set complaint type (which opens the complaint form) and scrolls to the complaint form + */ + openComplaintForm(complainType: ComplaintType): void { + this.formComplaintType = complainType; + this.cdr.detectChanges(); // Wait for the view to update + this.scrollToComplaint(); + } + + /** + * Function to scroll to the complaint form + */ + private scrollToComplaint(): void { + this.complaintScrollpoint?.nativeElement.scrollIntoView({ behavior: 'smooth', block: 'end' }); + } } diff --git a/src/test/javascript/spec/component/complaints/complaint-student-view.component.spec.ts b/src/test/javascript/spec/component/complaints/complaint-student-view.component.spec.ts index 528e9d6d5c6a..91172f9c6516 100644 --- a/src/test/javascript/spec/component/complaints/complaint-student-view.component.spec.ts +++ b/src/test/javascript/spec/component/complaints/complaint-student-view.component.spec.ts @@ -25,6 +25,8 @@ import { AssessmentType } from 'app/entities/assessment-type.model'; import { TranslateDirective } from 'app/shared/language/translate.directive'; import { CourseManagementService } from 'app/course/manage/course-management.service'; import { MockCourseManagementService } from '../../helpers/mocks/service/mock-course-management.service'; +import { ElementRef } from '@angular/core'; +import { ComplaintType } from 'app/entities/complaint.model'; describe('ComplaintsStudentViewComponent', () => { const complaintTimeLimitDays = 7; @@ -146,6 +148,39 @@ describe('ComplaintsStudentViewComponent', () => { expect(userMock).toHaveBeenCalledOnce(); })); + it('should set complaint type COMPLAINT and scroll to complaint form when pressing complaint', fakeAsync(() => { + component.exercise = examExercise; + component.result = result; + component.exam = defaultExam; + component.showSection = true; + component.isCorrectUserToFileAction = true; + const complaintBySubmissionMock = jest.spyOn(complaintService, 'findBySubmissionId').mockReturnValue(of()); + + fixture.detectChanges(); + + //Check if button is available + expect(component.complaint).toBeUndefined(); + expect(complaintBySubmissionMock).toHaveBeenCalledOnce(); + + // Mock complaint scrollpoint + const scrollIntoViewMock = jest.fn(); + component.complaintScrollpoint = { + nativeElement: { + scrollIntoView: scrollIntoViewMock, + }, + } as ElementRef; + + const button = fixture.debugElement.nativeElement.querySelector('#complain'); + button.click(); + + fixture.detectChanges(); + + expect(component.formComplaintType).toBe(ComplaintType.COMPLAINT); + // Wait for setTimeout to execute + tick(); + expect(scrollIntoViewMock).toHaveBeenCalledWith({ behavior: 'smooth', block: 'end' }); + })); + it('should be visible on test run', fakeAsync(() => { const now = dayjs(); const examWithFutureReview: Exam = { examStudentReviewStart: dayjs(now).add(1, 'day'), examStudentReviewEnd: dayjs(now).add(2, 'day') } as Exam; @@ -204,6 +239,71 @@ describe('ComplaintsStudentViewComponent', () => { expect(component.complaint).toStrictEqual(complaint); })); + it('should set complaint type COMPLAINT and scroll to complaint form when pressing complaint', fakeAsync(() => { + testInitWithResultStub(of()); + const courseWithMaxComplaints: Course = { + ...course, + maxComplaints: 3, + }; + const exerciseWithMaxComplaints: Exercise = { + ...courseExercise, + course: courseWithMaxComplaints, + }; + component.course = courseWithMaxComplaints; + component.exercise = exerciseWithMaxComplaints; + + component.showSection = true; + component.isCorrectUserToFileAction = true; + component.remainingNumberOfComplaints = 1; + + fixture.detectChanges(); + + // Mock complaint scrollpoint + const scrollIntoViewMock = jest.fn(); + component.complaintScrollpoint = { + nativeElement: { + scrollIntoView: scrollIntoViewMock, + }, + } as ElementRef; + + const button = fixture.debugElement.nativeElement.querySelector('#complain'); + button.click(); + + fixture.detectChanges(); + + expect(component.formComplaintType).toBe(ComplaintType.COMPLAINT); + tick(); // Wait for update to happen + expect(scrollIntoViewMock).toHaveBeenCalledWith({ behavior: 'smooth', block: 'end' }); + })); + + it('should set complaint type MORE_FEEDBACK and scroll to complaint form when pressing complaint', fakeAsync(() => { + testInitWithResultStub(of()); + component.showSection = true; + component.isCorrectUserToFileAction = true; + + fixture.detectChanges(); + + //Check if button is available + expect(component.complaint).toBeUndefined(); + + // Mock complaint scrollpoint + const scrollIntoViewMock = jest.fn(); + component.complaintScrollpoint = { + nativeElement: { + scrollIntoView: scrollIntoViewMock, + }, + } as ElementRef; + + const button = fixture.debugElement.nativeElement.querySelector('#more-feedback'); + button.click(); + + fixture.detectChanges(); + + expect(component.formComplaintType).toBe(ComplaintType.MORE_FEEDBACK); + tick(); // Wait for update to happen + expect(scrollIntoViewMock).toHaveBeenCalledWith({ behavior: 'smooth', block: 'end' }); + })); + it('should not be available if before or at assessment due date', fakeAsync(() => { const exercise: Exercise = { id: 1, teamMode: false, course, assessmentDueDate: dayjs() } as Exercise; const resultMatchingDate: Result = { id: 1, completionDate: dayjs(exercise.assessmentDueDate) } as Result;