diff --git a/src/main/webapp/app/exam/overview/events/button/exam-live-events-button.component.spec.ts b/src/main/webapp/app/exam/overview/events/button/exam-live-events-button.component.spec.ts index bc4d009ca85b..23afe9681a3b 100644 --- a/src/main/webapp/app/exam/overview/events/button/exam-live-events-button.component.spec.ts +++ b/src/main/webapp/app/exam/overview/events/button/exam-live-events-button.component.spec.ts @@ -6,6 +6,8 @@ import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { ExamLiveEvent, ExamLiveEventType, ExamParticipationLiveEventsService } from 'app/exam/overview/services/exam-participation-live-events.service'; import { of } from 'rxjs'; import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; +import { input } from '@angular/core'; +import dayjs from 'dayjs/esm'; import { MockExamParticipationLiveEventsService } from 'test/helpers/mocks/service/mock-exam-participation-live-events.service'; import { ExamLiveEventsOverlayComponent } from 'app/exam/overview/events/overlay/exam-live-events-overlay.component'; @@ -21,13 +23,13 @@ describe('ExamLiveEventsButtonComponent', () => { imports: [MockModule(FontAwesomeModule)], providers: [MockProvider(AlertService), MockProvider(NgbModal), { provide: ExamParticipationLiveEventsService, useClass: MockExamParticipationLiveEventsService }], }).compileComponents(); - }); - - beforeEach(() => { fixture = TestBed.createComponent(ExamLiveEventsButtonComponent); component = fixture.componentInstance; mockModalService = TestBed.inject(NgbModal); mockLiveEventsService = TestBed.inject(ExamParticipationLiveEventsService); + TestBed.runInInjectionContext(() => { + component.examStartDate = input(dayjs()); + }); fixture.detectChanges(); }); diff --git a/src/main/webapp/app/exam/overview/events/button/exam-live-events-button.component.ts b/src/main/webapp/app/exam/overview/events/button/exam-live-events-button.component.ts index f29b8c600f06..30dfc3f7dff0 100644 --- a/src/main/webapp/app/exam/overview/events/button/exam-live-events-button.component.ts +++ b/src/main/webapp/app/exam/overview/events/button/exam-live-events-button.component.ts @@ -1,4 +1,4 @@ -import { Component, Input, OnDestroy, OnInit, ViewEncapsulation, inject } from '@angular/core'; +import { Component, OnDestroy, OnInit, ViewEncapsulation, inject, input } from '@angular/core'; import { faBullhorn } from '@fortawesome/free-solid-svg-icons'; import { AlertService } from 'app/shared/service/alert.service'; import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; @@ -32,7 +32,7 @@ export class ExamLiveEventsButtonComponent implements OnInit, OnDestroy { private liveEventsSubscription?: Subscription; private allEventsSubscription?: Subscription; eventCount = 0; - @Input() examStartDate: dayjs.Dayjs; + examStartDate = input.required(); // Icons faBullhorn = faBullhorn; @@ -40,11 +40,11 @@ export class ExamLiveEventsButtonComponent implements OnInit, OnDestroy { ngOnInit(): void { this.allEventsSubscription = this.liveEventsService.observeAllEvents(USER_DISPLAY_RELEVANT_EVENTS_REOPEN).subscribe((events: ExamLiveEvent[]) => { // do not count the problem statements events that are made before the start of the exam - const filteredEvents = events.filter((event) => !(event.eventType === ExamLiveEventType.PROBLEM_STATEMENT_UPDATE && event.createdDate.isBefore(this.examStartDate))); + const filteredEvents = events.filter((event) => !(event.eventType === ExamLiveEventType.PROBLEM_STATEMENT_UPDATE && event.createdDate.isBefore(this.examStartDate()))); this.eventCount = filteredEvents.length; }); - this.liveEventsSubscription = this.liveEventsService.observeNewEventsAsUser(USER_DISPLAY_RELEVANT_EVENTS, this.examStartDate).subscribe(() => { + this.liveEventsSubscription = this.liveEventsService.observeNewEventsAsUser(USER_DISPLAY_RELEVANT_EVENTS, this.examStartDate()).subscribe(() => { // If any unacknowledged event comes in, open the dialog to display it if (!this.modalRef) { this.openDialog(); @@ -69,7 +69,7 @@ export class ExamLiveEventsButtonComponent implements OnInit, OnDestroy { windowClass: 'live-events-modal-window', }); - this.modalRef.componentInstance.examStartDate = this.examStartDate; + this.modalRef.componentInstance.examStartDate.set(this.examStartDate()); from(this.modalRef.result).subscribe(() => (this.modalRef = undefined)); } diff --git a/src/main/webapp/app/exam/overview/events/overlay/exam-live-events-overlay.component.spec.ts b/src/main/webapp/app/exam/overview/events/overlay/exam-live-events-overlay.component.spec.ts index 1e4fba3434cc..6ba0fabfcbf9 100644 --- a/src/main/webapp/app/exam/overview/events/overlay/exam-live-events-overlay.component.spec.ts +++ b/src/main/webapp/app/exam/overview/events/overlay/exam-live-events-overlay.component.spec.ts @@ -10,6 +10,8 @@ import { MockSyncStorage } from 'test/helpers/mocks/service/mock-sync-storage.se import { provideHttpClientTesting } from '@angular/common/http/testing'; import { provideHttpClient } from '@angular/common/http'; import { MockProvider } from 'ng-mocks'; +import dayjs from 'dayjs/esm'; +import { model } from '@angular/core'; describe('ExamLiveEventsOverlayComponent', () => { let component: ExamLiveEventsOverlayComponent; @@ -28,14 +30,15 @@ describe('ExamLiveEventsOverlayComponent', () => { MockProvider(NgbActiveModal), ], }).compileComponents(); - }); - beforeEach(() => { fixture = TestBed.createComponent(ExamLiveEventsOverlayComponent); component = fixture.componentInstance; mockLiveEventsService = TestBed.inject(ExamParticipationLiveEventsService); mockExamExerciseUpdateService = TestBed.inject(ExamExerciseUpdateService); mockActiveModal = TestBed.inject(NgbActiveModal); + TestBed.runInInjectionContext(() => { + component.examStartDate = model(dayjs()); + }); fixture.detectChanges(); }); diff --git a/src/main/webapp/app/exam/overview/events/overlay/exam-live-events-overlay.component.ts b/src/main/webapp/app/exam/overview/events/overlay/exam-live-events-overlay.component.ts index f47d959b5685..8940ce0cc7a8 100644 --- a/src/main/webapp/app/exam/overview/events/overlay/exam-live-events-overlay.component.ts +++ b/src/main/webapp/app/exam/overview/events/overlay/exam-live-events-overlay.component.ts @@ -1,4 +1,4 @@ -import { Component, Input, OnDestroy, OnInit, inject } from '@angular/core'; +import { Component, OnDestroy, OnInit, inject, model } from '@angular/core'; import { faCheck } from '@fortawesome/free-solid-svg-icons'; import { ExamLiveEventComponent } from 'app/exam/shared/events/exam-live-event.component'; import { Subscription } from 'rxjs'; @@ -33,7 +33,7 @@ export class ExamLiveEventsOverlayComponent implements OnInit, OnDestroy { eventsToDisplay?: ExamLiveEvent[]; events: ExamLiveEvent[] = []; - @Input() examStartDate: dayjs.Dayjs; + examStartDate = model.required(); // Icons faCheck = faCheck; @@ -47,13 +47,13 @@ export class ExamLiveEventsOverlayComponent implements OnInit, OnDestroy { ngOnInit(): void { this.allLiveEventsSubscription = this.liveEventsService.observeAllEvents(USER_DISPLAY_RELEVANT_EVENTS_REOPEN).subscribe((events: ExamLiveEvent[]) => { // display the problem statements events only after the start of the exam - this.events = events.filter((event) => !(event.eventType === ExamLiveEventType.PROBLEM_STATEMENT_UPDATE && event.createdDate.isBefore(this.examStartDate))); + this.events = events.filter((event) => !(event.eventType === ExamLiveEventType.PROBLEM_STATEMENT_UPDATE && event.createdDate.isBefore(this.examStartDate()))); if (!this.eventsToDisplay) { this.updateEventsToDisplay(); } }); - this.newLiveEventsSubscription = this.liveEventsService.observeNewEventsAsUser(USER_DISPLAY_RELEVANT_EVENTS, this.examStartDate).subscribe((event: ExamLiveEvent) => { + this.newLiveEventsSubscription = this.liveEventsService.observeNewEventsAsUser(USER_DISPLAY_RELEVANT_EVENTS, this.examStartDate()).subscribe((event: ExamLiveEvent) => { this.unacknowledgedEvents.unshift(event); this.updateEventsToDisplay(); }); diff --git a/src/main/webapp/app/exam/overview/exam-bar/exam-bar.component.html b/src/main/webapp/app/exam/overview/exam-bar/exam-bar.component.html index 31bb0e9a0358..be508a4e1cc4 100644 --- a/src/main/webapp/app/exam/overview/exam-bar/exam-bar.component.html +++ b/src/main/webapp/app/exam/overview/exam-bar/exam-bar.component.html @@ -1,19 +1,19 @@ -
+

{{ examTitle }}

- @if (!examTimeLineView) { + @if (!examTimeLineView()) {
- - @if (!isEndView) { + + @if (!isEndView()) { }
- @for (exercise of exercises; track exercise; let i = $index) { + @for (exercise of exercises(); track exercise; let i = $index) { @if ( i >= - exerciseIndex - + exerciseIndex() - itemsVisiblePerSide - - (exerciseIndex + 1 + itemsVisiblePerSide > exercises.length ? exerciseIndex + 1 + itemsVisiblePerSide - exercises.length : 0) && - i <= exerciseIndex + itemsVisiblePerSide - (exerciseIndex - itemsVisiblePerSide <= 0 ? exerciseIndex - itemsVisiblePerSide : 0) + (exerciseIndex() + 1 + itemsVisiblePerSide > exercises().length ? exerciseIndex() + 1 + itemsVisiblePerSide - exercises().length : 0) && + i <= exerciseIndex() + itemsVisiblePerSide - (exerciseIndex() - itemsVisiblePerSide <= 0 ? exerciseIndex() - itemsVisiblePerSide : 0) ) {
- @if (!examTimeLineView) { + @if (!examTimeLineView()) {
- @if (!overviewPageOpen) { + @if (!overviewPageOpen()) { }
- +
} diff --git a/src/main/webapp/app/exam/overview/exam-navigation-bar/exam-navigation-bar.component.spec.ts b/src/main/webapp/app/exam/overview/exam-navigation-bar/exam-navigation-bar.component.spec.ts index ea0b409528a9..a0eabf6ea5b1 100644 --- a/src/main/webapp/app/exam/overview/exam-navigation-bar/exam-navigation-bar.component.spec.ts +++ b/src/main/webapp/app/exam/overview/exam-navigation-bar/exam-navigation-bar.component.spec.ts @@ -17,19 +17,20 @@ import { MockTranslateService } from 'test/helpers/mocks/service/mock-translate. import { TranslateService } from '@ngx-translate/core'; import { provideHttpClient } from '@angular/common/http'; import { provideHttpClientTesting } from '@angular/common/http/testing'; +import { input, model } from '@angular/core'; describe('Exam Navigation Bar Component', () => { let fixture: ComponentFixture; let comp: ExamNavigationBarComponent; let repositoryService: CodeEditorRepositoryService; - - const examExerciseIdForNavigationSourceMock = new BehaviorSubject(-1); - const mockExamExerciseUpdateService = { - currentExerciseIdForNavigation: examExerciseIdForNavigationSourceMock.asObservable(), - }; - - beforeEach(() => { - TestBed.configureTestingModule({ + let examExerciseIdForNavigationSourceMock: BehaviorSubject; + + beforeEach(async () => { + examExerciseIdForNavigationSourceMock = new BehaviorSubject(-1); + const mockExamExerciseUpdateService = { + currentExerciseIdForNavigation: examExerciseIdForNavigationSourceMock.asObservable(), + }; + await TestBed.configureTestingModule({ providers: [ { provide: ExamExerciseUpdateService, useValue: mockExamExerciseUpdateService }, { provide: LocalStorageService, useClass: MockSyncStorage }, @@ -45,8 +46,7 @@ describe('Exam Navigation Bar Component', () => { repositoryService = TestBed.inject(CodeEditorRepositoryService); TestBed.inject(ExamParticipationService); - comp.endDate = dayjs(); - comp.exercises = [ + const exercises = [ { id: 0, type: ExerciseType.PROGRAMMING, @@ -59,6 +59,10 @@ describe('Exam Navigation Bar Component', () => { { id: 1, type: ExerciseType.TEXT } as Exercise, { id: 2, type: ExerciseType.MODELING } as Exercise, ]; + TestBed.runInInjectionContext(() => { + comp.endDate = input(dayjs()); + comp.exercises = input(exercises); + }); }); beforeEach(fakeAsync(() => { @@ -69,8 +73,11 @@ describe('Exam Navigation Bar Component', () => { it('should update the submissions onInit if their CommitState is UNCOMMITTED_CHANGES to isSynced false, if not in initial session', () => { // Given // Create an exam session, which is not an initial session. - comp.examSessions = [{ initialSession: false } as ExamSession]; - const exerciseToBeSynced = comp.exercises[0]; + const examSessions = [{ initialSession: false } as ExamSession]; + const exerciseToBeSynced = comp.exercises()[0]; + TestBed.runInInjectionContext(() => { + comp.examSessions = input(examSessions); + }); jest.spyOn(repositoryService, 'getStatus').mockReturnValue(of({ repositoryStatus: CommitState.UNCOMMITTED_CHANGES })); // When @@ -95,14 +102,14 @@ describe('Exam Navigation Bar Component', () => { jest.spyOn(comp.onPageChanged, 'emit'); jest.spyOn(comp, 'setExerciseButtonStatus'); - expect(comp.exerciseIndex).toBe(0); + expect(comp.exerciseIndex()).toBe(0); const exerciseIndex = 1; const force = false; comp.changePage(false, exerciseIndex, force); - expect(comp.exerciseIndex).toEqual(exerciseIndex); + expect(comp.exerciseIndex()).toEqual(exerciseIndex); expect(comp.onPageChanged.emit).toHaveBeenCalledOnce(); expect(comp.setExerciseButtonStatus).toHaveBeenCalledWith(exerciseIndex); }); @@ -111,32 +118,38 @@ describe('Exam Navigation Bar Component', () => { jest.spyOn(comp.onPageChanged, 'emit'); jest.spyOn(comp, 'setExerciseButtonStatus'); - expect(comp.exerciseIndex).toBe(0); + expect(comp.exerciseIndex()).toBe(0); const exerciseIndex = 5; const force = false; comp.changePage(false, exerciseIndex, force); - expect(comp.exerciseIndex).toBe(0); + expect(comp.exerciseIndex()).toBe(0); expect(comp.onPageChanged.emit).not.toHaveBeenCalled(); expect(comp.setExerciseButtonStatus).not.toHaveBeenCalledWith(exerciseIndex); }); it('should tell the type of the selected programming exercise', () => { - comp.exerciseIndex = 0; + TestBed.runInInjectionContext(() => { + comp.exerciseIndex = model(0); + }); expect(comp.isProgrammingExercise()).toBeTrue(); }); it('should tell the type of the selected text exercise', () => { - comp.exerciseIndex = 1; + TestBed.runInInjectionContext(() => { + comp.exerciseIndex = model(1); + }); expect(comp.isProgrammingExercise()).toBeFalse(); }); it('should tell the type of the selected modeling exercise', () => { - comp.exerciseIndex = 2; + TestBed.runInInjectionContext(() => { + comp.exerciseIndex = model(2); + }); expect(comp.isProgrammingExercise()).toBeFalse(); }); @@ -174,7 +187,7 @@ describe('Exam Navigation Bar Component', () => { }); it('should set the exercise button status for submitted submission', () => { - comp.exercises[0].studentParticipations![0].submissions![0] = { submitted: true }; + comp.exercises()[0].studentParticipations![0].submissions![0] = { submitted: true }; const result = comp.setExerciseButtonStatus(0); @@ -183,7 +196,7 @@ describe('Exam Navigation Bar Component', () => { }); it('should set the exercise button status for submitted and synced submission active', () => { - comp.exercises[0].studentParticipations![0].submissions![0] = { submitted: true, isSynced: true }; + comp.exercises()[0].studentParticipations![0].submissions![0] = { submitted: true, isSynced: true }; const result = comp.setExerciseButtonStatus(0); @@ -191,7 +204,7 @@ describe('Exam Navigation Bar Component', () => { }); it('should set the exercise button status for submitted and synced submission not active', () => { - comp.exercises[0].studentParticipations![0].submissions![0] = { submitted: true, isSynced: true }; + comp.exercises()[0].studentParticipations![0].submissions![0] = { submitted: true, isSynced: true }; const result = comp.setExerciseButtonStatus(1); @@ -199,39 +212,39 @@ describe('Exam Navigation Bar Component', () => { }); it('should get the exercise button tooltip without submission', () => { - const result = comp.getExerciseButtonTooltip(comp.exercises[1]); + const result = comp.getExerciseButtonTooltip(comp.exercises()[1]); expect(result).toBe('synced'); }); it('should get the exercise button tooltip with submitted and synced submission', () => { - comp.exercises[0].studentParticipations![0].submissions![0] = { submitted: true, isSynced: true }; + comp.exercises()[0].studentParticipations![0].submissions![0] = { submitted: true, isSynced: true }; - const result = comp.getExerciseButtonTooltip(comp.exercises[0]); + const result = comp.getExerciseButtonTooltip(comp.exercises()[0]); expect(result).toBe('submitted'); }); it('should get the exercise button tooltip with submitted submission', () => { - comp.exercises[0].studentParticipations![0].submissions![0] = { submitted: true }; + comp.exercises()[0].studentParticipations![0].submissions![0] = { submitted: true }; - const result = comp.getExerciseButtonTooltip(comp.exercises[0]); + const result = comp.getExerciseButtonTooltip(comp.exercises()[0]); expect(result).toBe('notSavedOrSubmitted'); }); it('should get the exercise button tooltip with submission', () => { - comp.exercises[0].studentParticipations![0].submissions![0] = {}; + comp.exercises()[0].studentParticipations![0].submissions![0] = {}; - const result = comp.getExerciseButtonTooltip(comp.exercises[0]); + const result = comp.getExerciseButtonTooltip(comp.exercises()[0]); expect(result).toBe('notSavedOrSubmitted'); }); it('should get the exercise button tooltip with synced submission', () => { - comp.exercises[0].studentParticipations![0].submissions![0] = { isSynced: true }; + comp.exercises()[0].studentParticipations![0].submissions![0] = { isSynced: true }; - const result = comp.getExerciseButtonTooltip(comp.exercises[0]); + const result = comp.getExerciseButtonTooltip(comp.exercises()[0]); expect(result).toBe('notSubmitted'); }); @@ -244,15 +257,19 @@ describe('Exam Navigation Bar Component', () => { }); it('should set exercise button status to synced active if it is the active exercise in the exam timeline view', () => { - comp.examTimeLineView = true; - comp.exerciseIndex = 0; + TestBed.runInInjectionContext(() => { + comp.examTimeLineView = input(true); + comp.exerciseIndex = model(0); + }); expect(comp.setExerciseButtonStatus(0)).toBe('synced active'); expect(comp.icon).toEqual(faCheck); }); it('should set exercise button status to synced if it is not the active exercise in the exam timeline view', () => { - comp.examTimeLineView = true; - comp.exerciseIndex = 0; + TestBed.runInInjectionContext(() => { + comp.examTimeLineView = input(true); + comp.exerciseIndex = model(0); + }); expect(comp.setExerciseButtonStatus(1)).toBe('synced'); expect(comp.icon).toEqual(faCheck); }); diff --git a/src/main/webapp/app/exam/overview/exam-navigation-bar/exam-navigation-bar.component.ts b/src/main/webapp/app/exam/overview/exam-navigation-bar/exam-navigation-bar.component.ts index 5ad4dfa7c650..e206f2af3258 100644 --- a/src/main/webapp/app/exam/overview/exam-navigation-bar/exam-navigation-bar.component.ts +++ b/src/main/webapp/app/exam/overview/exam-navigation-bar/exam-navigation-bar.component.ts @@ -1,4 +1,4 @@ -import { AfterViewInit, Component, EventEmitter, Input, OnInit, Output, inject } from '@angular/core'; +import { AfterViewInit, Component, OnInit, inject, input, model, output } from '@angular/core'; import { Exercise, ExerciseType } from 'app/exercise/shared/entities/exercise/exercise.model'; import { ProgrammingExercise } from 'app/programming/shared/entities/programming-exercise.model'; import { LayoutService } from 'app/shared/breakpoints/layout.service'; @@ -17,7 +17,6 @@ import { faBars, faCheck, faEdit } from '@fortawesome/free-solid-svg-icons'; import { ProgrammingSubmission } from 'app/programming/shared/entities/programming-submission.model'; import { FileUploadSubmission } from 'app/fileupload/shared/entities/file-upload-submission.model'; import { TranslateDirective } from 'app/shared/language/translate.directive'; -import { ExamLiveEventsButtonComponent } from '../events/button/exam-live-events-button.component'; import { NgClass } from '@angular/common'; import { NgbTooltip } from '@ng-bootstrap/ng-bootstrap'; import { FaIconComponent } from '@fortawesome/angular-fontawesome'; @@ -29,7 +28,7 @@ import { SubmissionVersion } from 'app/exam/shared/entities/submission-version.m selector: 'jhi-exam-navigation-bar', templateUrl: './exam-navigation-bar.component.html', styleUrls: ['./exam-navigation-bar.component.scss'], - imports: [TranslateDirective, ExamLiveEventsButtonComponent, NgClass, NgbTooltip, FaIconComponent, ExamTimerComponent, ArtemisTranslatePipe], + imports: [TranslateDirective, NgClass, NgbTooltip, FaIconComponent, ExamTimerComponent, ArtemisTranslatePipe], }) export class ExamNavigationBarComponent implements OnInit, AfterViewInit { private layoutService = inject(LayoutService); @@ -38,20 +37,20 @@ export class ExamNavigationBarComponent implements OnInit, AfterViewInit { private repositoryService = inject(CodeEditorRepositoryService); private conflictService = inject(CodeEditorConflictStateService); - @Input() exercises: Exercise[] = []; - @Input() exerciseIndex = 0; - @Input() endDate: dayjs.Dayjs; - @Input() overviewPageOpen: boolean; - @Input() examSessions?: ExamSession[] = []; - @Input() examTimeLineView = false; - @Output() onPageChanged = new EventEmitter<{ + exercises = input([]); + exerciseIndex = model(0); + endDate = input(dayjs()); + overviewPageOpen = input(); + examSessions = input([]); + examTimeLineView = input(false); + onPageChanged = output<{ overViewChange: boolean; exercise?: Exercise; forceSave: boolean; submission?: ProgrammingSubmission | SubmissionVersion | FileUploadSubmission; }>(); - @Output() examAboutToEnd = new EventEmitter(); - @Output() onExamHandInEarly = new EventEmitter(); + examAboutToEnd = output(); + onExamHandInEarly = output(); static itemsVisiblePerSideDefault = 4; itemsVisiblePerSide = ExamNavigationBarComponent.itemsVisiblePerSideDefault; @@ -66,7 +65,7 @@ export class ExamNavigationBarComponent implements OnInit, AfterViewInit { faBars = faBars; ngOnInit(): void { - if (!this.examTimeLineView) { + if (!this.examTimeLineView()) { this.subscriptionToLiveExamExerciseUpdates = this.examExerciseUpdateService.currentExerciseIdForNavigation.subscribe((exerciseIdToNavigateTo) => { // another exercise will only be displayed if the student clicks on the corresponding pop-up notification this.changeExerciseById(exerciseIdToNavigateTo); @@ -86,13 +85,13 @@ export class ExamNavigationBarComponent implements OnInit, AfterViewInit { } }); - const isInitialSession = this.examSessions && this.examSessions.length > 0 && this.examSessions[0].initialSession; + const isInitialSession = this.examSessions() && this.examSessions().length > 0 && this.examSessions()[0].initialSession; if (isInitialSession || isInitialSession == undefined) { return; } // If it is not an initial session, update the isSynced variable for out of sync submissions. - this.exercises + this.exercises() .filter((exercise) => exercise.type === ExerciseType.PROGRAMMING && exercise.studentParticipations) .forEach((exercise) => { const domain: DomainChange = [DomainType.PARTICIPATION, exercise.studentParticipations![0]]; @@ -130,27 +129,27 @@ export class ExamNavigationBarComponent implements OnInit, AfterViewInit { } /** - * @param overviewPage: user wants to switch to the overview page - * @param exerciseIndex: index of the exercise to switch to, if it should not be used, you can pass -1 - * @param forceSave: true if forceSave shall be used. + * @param overviewPage user wants to switch to the overview page + * @param exerciseIndex index of the exercise to switch to, if it should not be used, you can pass -1 + * @param forceSave true if forceSave shall be used. * @param submission the submission to be viewed, used in the exam timeline */ changePage(overviewPage: boolean, exerciseIndex: number, forceSave?: boolean, submission?: SubmissionVersion | ProgrammingSubmission | FileUploadSubmission): void { if (!overviewPage) { // out of index -> do nothing - if (exerciseIndex > this.exercises.length - 1 || exerciseIndex < 0) { + if (exerciseIndex > this.exercises().length - 1 || exerciseIndex < 0) { return; } // set index and emit event - this.exerciseIndex = exerciseIndex; - this.onPageChanged.emit({ overViewChange: false, exercise: this.exercises[this.exerciseIndex], forceSave: !!forceSave, submission: submission }); + this.exerciseIndex.set(exerciseIndex); + this.onPageChanged.emit({ overViewChange: false, exercise: this.exercises()[this.exerciseIndex()], forceSave: !!forceSave, submission: submission }); } else if (overviewPage) { // set index and emit event - this.exerciseIndex = -1; + this.exerciseIndex.set(-1); // save current exercise this.onPageChanged.emit({ overViewChange: true, exercise: undefined, forceSave: false }); } - this.setExerciseButtonStatus(this.exerciseIndex); + this.setExerciseButtonStatus(this.exerciseIndex()); } /** @@ -158,7 +157,7 @@ export class ExamNavigationBarComponent implements OnInit, AfterViewInit { * @param exerciseId the unique identifier of an exercise that stays the same regardless of student exam ordering */ changeExerciseById(exerciseId: number) { - const foundIndex = this.exercises.findIndex((exercise) => exercise.id === exerciseId); + const foundIndex = this.exercises().findIndex((exercise) => exercise.id === exerciseId); this.changePage(false, foundIndex, true); } @@ -167,16 +166,16 @@ export class ExamNavigationBarComponent implements OnInit, AfterViewInit { * @param changeExercise whether to go to the next exercise {boolean} */ saveExercise(changeExercise = true) { - const newIndex = this.exerciseIndex + 1; - const submission = ExamParticipationService.getSubmissionForExercise(this.exercises[this.exerciseIndex]); + const newIndex = this.exerciseIndex() + 1; + const submission = ExamParticipationService.getSubmissionForExercise(this.exercises()[this.exerciseIndex()]); // we do not submit programming exercises on a save - if (submission && this.exercises[this.exerciseIndex].type !== ExerciseType.PROGRAMMING) { + if (submission && this.exercises()[this.exerciseIndex()].type !== ExerciseType.PROGRAMMING) { submission.submitted = true; } if (changeExercise) { - if (newIndex > this.exercises.length - 1) { + if (newIndex > this.exercises().length - 1) { // we are in the last exercise, if out of range "change" active exercise to current in order to trigger a save - this.changePage(false, this.exerciseIndex, true); + this.changePage(false, this.exerciseIndex(), true); } else { this.changePage(false, newIndex, true); } @@ -184,15 +183,15 @@ export class ExamNavigationBarComponent implements OnInit, AfterViewInit { } isProgrammingExercise() { - return this.exercises[this.exerciseIndex].type === ExerciseType.PROGRAMMING; + return this.exercises()[this.exerciseIndex()].type === ExerciseType.PROGRAMMING; } isFileUploadExercise() { - return this.exercises[this.exerciseIndex].type === ExerciseType.FILE_UPLOAD; + return this.exercises()[this.exerciseIndex()].type === ExerciseType.FILE_UPLOAD; } getOverviewStatus(): 'active' | '' { - return this.overviewPageOpen ? 'active' : ''; + return this.overviewPageOpen() ? 'active' : ''; } /** @@ -208,15 +207,15 @@ export class ExamNavigationBarComponent implements OnInit, AfterViewInit { this.icon = faCheck; // If we are in the exam timeline we do not use not synced as not synced shows // that the current submission is not saved which doesn't make sense in the timeline. - if (this.examTimeLineView) { - return this.exerciseIndex === exerciseIndex ? 'synced active' : 'synced'; + if (this.examTimeLineView()) { + return this.exerciseIndex() === exerciseIndex ? 'synced active' : 'synced'; } // start with a yellow status (edit icon) // TODO: it's a bit weird, that it works that multiple icons (one per exercise) are hold in the same instance variable of the component // we should definitely refactor this and e.g. use the same ExamExerciseOverviewItem as in exam-exercise-overview-page.component.ts ! this.icon = faEdit; - const exercise = this.exercises[exerciseIndex]; + const exercise = this.exercises()[exerciseIndex]; const submission = ExamParticipationService.getSubmissionForExercise(exercise); if (!submission) { // in case no participation/submission yet exists -> display synced @@ -228,7 +227,7 @@ export class ExamNavigationBarComponent implements OnInit, AfterViewInit { } if (submission.isSynced || this.isOnlyOfflineIDE(exercise)) { // make button blue (except for the current page) - if (exerciseIndex === this.exerciseIndex && !this.overviewPageOpen) { + if (exerciseIndex === this.exerciseIndex() && !this.overviewPageOpen()!) { return 'synced active'; } else { return 'synced'; diff --git a/src/main/webapp/app/exam/overview/exam-navigation-sidebar/exam-navigation-sidebar.component.html b/src/main/webapp/app/exam/overview/exam-navigation-sidebar/exam-navigation-sidebar.component.html index 5c8fd99e338d..a2d2dda11921 100644 --- a/src/main/webapp/app/exam/overview/exam-navigation-sidebar/exam-navigation-sidebar.component.html +++ b/src/main/webapp/app/exam/overview/exam-navigation-sidebar/exam-navigation-sidebar.component.html @@ -1,6 +1,6 @@