From 383945adccccd2b2479a7f27220b07be29ba63bd Mon Sep 17 00:00:00 2001 From: Johannes Wiest Date: Wed, 13 Nov 2024 15:53:12 +0100 Subject: [PATCH 01/21] Development: Refactor competencies management page to signals (#9629) --- .../competency-management-table.component.ts | 6 +- .../competency-management.component.html | 22 +-- .../competency-management.component.ts | 127 ++++++--------- ...petency-management-table.component.spec.ts | 2 + .../competency-management.component.spec.ts | 154 ++++++++++++------ 5 files changed, 173 insertions(+), 138 deletions(-) diff --git a/src/main/webapp/app/course/competencies/competency-management/competency-management-table.component.ts b/src/main/webapp/app/course/competencies/competency-management/competency-management-table.component.ts index fa1a6c30b04c..28d4e4399ed9 100644 --- a/src/main/webapp/app/course/competencies/competency-management/competency-management-table.component.ts +++ b/src/main/webapp/app/course/competencies/competency-management/competency-management-table.component.ts @@ -1,4 +1,4 @@ -import { Component, EventEmitter, Input, OnDestroy, OnInit, Output, inject } from '@angular/core'; +import { Component, EventEmitter, Input, OnDestroy, OnInit, Output, inject, model } from '@angular/core'; import { CompetencyService } from 'app/course/competencies/competency.service'; import { AlertService } from 'app/core/util/alert.service'; import { CompetencyWithTailRelationDTO, CourseCompetency, CourseCompetencyType, getIcon } from 'app/entities/competency.model'; @@ -22,7 +22,7 @@ import { ArtemisMarkdownModule } from 'app/shared/markdown.module'; export class CompetencyManagementTableComponent implements OnInit, OnDestroy { @Input() courseId: number; @Input() courseCompetencies: CourseCompetency[] = []; - @Input() allCompetencies: CourseCompetency[] = []; + allCompetencies = model.required(); @Input() competencyType: CourseCompetencyType; @Input() standardizedCompetenciesEnabled: boolean; @@ -96,7 +96,7 @@ export class CompetencyManagementTableComponent implements OnInit, OnDestroy { updateDataAfterImportAll(res: Array) { const importedCompetencies = res.map((dto) => dto.competency).filter((element): element is CourseCompetency => !!element); this.courseCompetencies.push(...importedCompetencies); - this.allCompetencies.push(...importedCompetencies); + this.allCompetencies.update((allCourseCompetencies) => allCourseCompetencies.concat(importedCompetencies)); } /** diff --git a/src/main/webapp/app/course/competencies/competency-management/competency-management.component.html b/src/main/webapp/app/course/competencies/competency-management/competency-management.component.html index 2c84f6829fdc..ae2211f1c881 100644 --- a/src/main/webapp/app/course/competencies/competency-management/competency-management.component.html +++ b/src/main/webapp/app/course/competencies/competency-management/competency-management.component.html @@ -8,8 +8,8 @@

- @if (irisCompetencyGenerationEnabled) { - + @if (irisCompetencyGenerationEnabled()) { + @@ -24,7 +24,7 @@

- @if (isLoading) { + @if (isLoading()) {
@@ -32,19 +32,19 @@

}
diff --git a/src/main/webapp/app/course/competencies/competency-management/competency-management.component.ts b/src/main/webapp/app/course/competencies/competency-management/competency-management.component.ts index e54e4f3975a1..ddad887fc1a2 100644 --- a/src/main/webapp/app/course/competencies/competency-management/competency-management.component.ts +++ b/src/main/webapp/app/course/competencies/competency-management/competency-management.component.ts @@ -1,9 +1,8 @@ -import { Component, OnDestroy, OnInit, inject, signal } from '@angular/core'; +import { Component, OnInit, computed, effect, inject, signal, untracked } from '@angular/core'; import { ActivatedRoute, RouterModule } from '@angular/router'; import { AlertService } from 'app/core/util/alert.service'; -import { Competency, CompetencyWithTailRelationDTO, CourseCompetency, CourseCompetencyType, getIcon } from 'app/entities/competency.model'; -import { onError } from 'app/shared/util/global.utils'; -import { Subject, Subscription } from 'rxjs'; +import { CompetencyWithTailRelationDTO, CourseCompetency, CourseCompetencyType, getIcon } from 'app/entities/competency.model'; +import { firstValueFrom, map } from 'rxjs'; import { faCircleQuestion, faEdit, faFileImport, faPencilAlt, faPlus, faRobot, faTrash } from '@fortawesome/free-solid-svg-icons'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { DocumentationType } from 'app/shared/components/documentation-button/documentation-button.component'; @@ -11,7 +10,6 @@ import { ProfileService } from 'app/shared/layouts/profiles/profile.service'; import { IrisSettingsService } from 'app/iris/settings/shared/iris-settings.service'; import { PROFILE_IRIS } from 'app/app.constants'; import { FeatureToggle, FeatureToggleService } from 'app/shared/feature-toggle/feature-toggle.service'; -import { Prerequisite } from 'app/entities/prerequisite.model'; import { ImportAllCourseCompetenciesModalComponent, ImportAllCourseCompetenciesResult, @@ -23,6 +21,7 @@ import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; import { ArtemisSharedComponentModule } from 'app/shared/components/shared-component.module'; import { CourseCompetenciesRelationModalComponent } from 'app/course/competencies/components/course-competencies-relation-modal/course-competencies-relation-modal.component'; import { CourseCompetencyExplanationModalComponent } from 'app/course/competencies/components/course-competency-explanation-modal/course-competency-explanation-modal.component'; +import { toSignal } from '@angular/core/rxjs-interop'; @Component({ selector: 'jhi-competency-management', @@ -30,20 +29,7 @@ import { CourseCompetencyExplanationModalComponent } from 'app/course/competenci standalone: true, imports: [CompetencyManagementTableComponent, TranslateDirective, FontAwesomeModule, RouterModule, ArtemisSharedComponentModule], }) -export class CompetencyManagementComponent implements OnInit, OnDestroy { - courseId: number; - isLoading = false; - irisCompetencyGenerationEnabled = false; - private dialogErrorSource = new Subject(); - dialogError = this.dialogErrorSource.asObservable(); - standardizedCompetenciesEnabled = false; - private standardizedCompetencySubscription: Subscription; - - competencies: Competency[] = []; - prerequisites: Prerequisite[] = []; - courseCompetencies: CourseCompetency[] = []; - - // Icons +export class CompetencyManagementComponent implements OnInit { protected readonly faEdit = faEdit; protected readonly faPlus = faPlus; protected readonly faFileImport = faFileImport; @@ -52,12 +38,10 @@ export class CompetencyManagementComponent implements OnInit, OnDestroy { protected readonly faRobot = faRobot; protected readonly faCircleQuestion = faCircleQuestion; - // other constants readonly getIcon = getIcon; readonly documentationType: DocumentationType = 'Competencies'; readonly CourseCompetencyType = CourseCompetencyType; - // Injected services private readonly activatedRoute = inject(ActivatedRoute); private readonly courseCompetencyApiService = inject(CourseCompetencyApiService); private readonly alertService = inject(AlertService); @@ -66,58 +50,61 @@ export class CompetencyManagementComponent implements OnInit, OnDestroy { private readonly irisSettingsService = inject(IrisSettingsService); private readonly featureToggleService = inject(FeatureToggleService); - ngOnInit(): void { - this.activatedRoute.parent!.params.subscribe(async (params) => { - this.courseId = Number(params['courseId']); - await this.loadData(); - this.loadIrisEnabled(); + readonly courseId = toSignal(this.activatedRoute.parent!.params.pipe(map((params) => Number(params.courseId))), { requireSync: true }); + readonly isLoading = signal(false); + + readonly courseCompetencies = signal([]); + competencies = computed(() => this.courseCompetencies().filter((cc) => cc.type === CourseCompetencyType.COMPETENCY)); + prerequisites = computed(() => this.courseCompetencies().filter((cc) => cc.type === CourseCompetencyType.PREREQUISITE)); + + private readonly irisEnabled = toSignal(this.profileService.getProfileInfo().pipe(map((profileInfo) => profileInfo?.activeProfiles?.includes(PROFILE_IRIS))), { + initialValue: false, + }); + + irisCompetencyGenerationEnabled = signal(false); + standardizedCompetenciesEnabled = toSignal(this.featureToggleService.getFeatureToggleActive(FeatureToggle.StandardizedCompetencies), { requireSync: true }); + + constructor() { + effect(() => { + const courseId = this.courseId(); + untracked(async () => await this.loadCourseCompetencies(courseId)); + }); + effect(() => { + const irisEnabled = this.irisEnabled(); + untracked(async () => { + if (irisEnabled) { + await this.loadIrisEnabled(); + } + }); }); + } + + ngOnInit(): void { const lastVisit = sessionStorage.getItem('lastTimeVisitedCourseCompetencyExplanation'); if (!lastVisit) { this.openCourseCompetencyExplanation(); } sessionStorage.setItem('lastTimeVisitedCourseCompetencyExplanation', Date.now().toString()); - this.standardizedCompetencySubscription = this.featureToggleService.getFeatureToggleActive(FeatureToggle.StandardizedCompetencies).subscribe((isActive) => { - this.standardizedCompetenciesEnabled = isActive; - }); } - ngOnDestroy() { - this.dialogErrorSource.unsubscribe(); - if (this.standardizedCompetencySubscription) { - this.standardizedCompetencySubscription.unsubscribe(); + private async loadIrisEnabled() { + try { + const combinedCourseSettings = await firstValueFrom(this.irisSettingsService.getCombinedCourseSettings(this.courseId())); + this.irisCompetencyGenerationEnabled.set(combinedCourseSettings?.irisCompetencyGenerationSettings?.enabled ?? false); + } catch (error) { + this.alertService.error(error); } } - /** - * Sends a request to determine if Iris and Competency Generation is enabled - * - * @private - */ - private loadIrisEnabled() { - this.profileService.getProfileInfo().subscribe((profileInfo) => { - const irisEnabled = profileInfo.activeProfiles.includes(PROFILE_IRIS); - if (irisEnabled) { - this.irisSettingsService.getCombinedCourseSettings(this.courseId).subscribe((settings) => { - this.irisCompetencyGenerationEnabled = settings?.irisCompetencyGenerationSettings?.enabled ?? false; - }); - } - }); - } - - /** - * Loads all data for the competency management: Prerequisites and competencies (with average course progress) - */ - async loadData() { + private async loadCourseCompetencies(courseId: number) { try { - this.isLoading = true; - this.courseCompetencies = await this.courseCompetencyApiService.getCourseCompetenciesByCourseId(this.courseId); - this.competencies = this.courseCompetencies.filter((competency) => competency.type === CourseCompetencyType.COMPETENCY); - this.prerequisites = this.courseCompetencies.filter((competency) => competency.type === CourseCompetencyType.PREREQUISITE); + this.isLoading.set(true); + const courseCompetencies = await this.courseCompetencyApiService.getCourseCompetenciesByCourseId(courseId); + this.courseCompetencies.set(courseCompetencies); } catch (error) { - onError(this.alertService, error); + this.alertService.error(error); } finally { - this.isLoading = false; + this.isLoading.set(false); } } @@ -127,8 +114,8 @@ export class CompetencyManagementComponent implements OnInit, OnDestroy { backdrop: 'static', windowClass: 'course-competencies-relation-graph-modal', }); - modalRef.componentInstance.courseId = signal(this.courseId); - modalRef.componentInstance.courseCompetencies = signal(this.courseCompetencies); + modalRef.componentInstance.courseId = signal(this.courseId()); + modalRef.componentInstance.courseCompetencies = signal(this.courseCompetencies()); } /** @@ -139,14 +126,14 @@ export class CompetencyManagementComponent implements OnInit, OnDestroy { size: 'lg', backdrop: 'static', }); - modalRef.componentInstance.courseId = signal(this.courseId); + modalRef.componentInstance.courseId = signal(this.courseId()); const importResults: ImportAllCourseCompetenciesResult | undefined = await modalRef.result; if (!importResults) { return; } const courseTitle = importResults.course.title ?? ''; try { - const importedCompetencies = await this.courseCompetencyApiService.importAllByCourseId(this.courseId, importResults.courseCompetencyImportOptions); + const importedCompetencies = await this.courseCompetencyApiService.importAllByCourseId(this.courseId(), importResults.courseCompetencyImportOptions); if (importedCompetencies.length) { this.alertService.success(`artemisApp.courseCompetency.importAll.success`, { noOfCompetencies: importedCompetencies.length, @@ -157,7 +144,7 @@ export class CompetencyManagementComponent implements OnInit, OnDestroy { this.alertService.warning(`artemisApp.courseCompetency.importAll.warning`, { courseTitle: courseTitle }); } } catch (error) { - onError(this.alertService, error); + this.alertService.error(error); } } @@ -167,18 +154,12 @@ export class CompetencyManagementComponent implements OnInit, OnDestroy { * @private */ updateDataAfterImportAll(res: Array) { - const importedCompetencies = res.map((dto) => dto.competency).filter((element): element is Competency => element?.type === CourseCompetencyType.COMPETENCY); - const importedPrerequisites = res.map((dto) => dto.competency).filter((element): element is Prerequisite => element?.type === CourseCompetencyType.PREREQUISITE); - - this.competencies = this.competencies.concat(importedCompetencies); - this.prerequisites = this.prerequisites.concat(importedPrerequisites); - this.courseCompetencies = this.competencies.concat(this.prerequisites); + const importedCourseCompetencies = res.map((dto) => dto.competency!); + this.courseCompetencies.update((courseCompetencies) => courseCompetencies.concat(importedCourseCompetencies)); } onRemoveCompetency(competencyId: number) { - this.competencies = this.competencies.filter((competency) => competency.id !== competencyId); - this.prerequisites = this.prerequisites.filter((prerequisite) => prerequisite.id !== competencyId); - this.courseCompetencies = this.competencies.concat(this.prerequisites); + this.courseCompetencies.update((courseCompetencies) => courseCompetencies.filter((cc) => cc.id !== competencyId)); } openCourseCompetencyExplanation(): void { diff --git a/src/test/javascript/spec/component/competencies/competency-management/competency-management-table.component.spec.ts b/src/test/javascript/spec/component/competencies/competency-management/competency-management-table.component.spec.ts index e15cefdbc52d..52f2e52d587e 100644 --- a/src/test/javascript/spec/component/competencies/competency-management/competency-management-table.component.spec.ts +++ b/src/test/javascript/spec/component/competencies/competency-management/competency-management-table.component.spec.ts @@ -52,6 +52,8 @@ describe('CompetencyManagementTableComponent', () => { }); it('should handle import all data', () => { + fixture.componentRef.setInput('allCompetencies', []); + fixture.detectChanges(); component.courseCompetencies = []; const responseBody: CompetencyWithTailRelationDTO[] = [ diff --git a/src/test/javascript/spec/component/competencies/competency-management/competency-management.component.spec.ts b/src/test/javascript/spec/component/competencies/competency-management/competency-management.component.spec.ts index b292d1f5145e..86b505b1f78e 100644 --- a/src/test/javascript/spec/component/competencies/competency-management/competency-management.component.spec.ts +++ b/src/test/javascript/spec/component/competencies/competency-management/competency-management.component.spec.ts @@ -2,11 +2,10 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ArtemisTranslatePipe } from 'app/shared/pipes/artemis-translate.pipe'; import { MockComponent, MockDirective, MockPipe, MockProvider } from 'ng-mocks'; import { of } from 'rxjs'; -import { Competency, CompetencyLectureUnitLink, CompetencyWithTailRelationDTO, CourseCompetencyProgress, CourseCompetencyType } from 'app/entities/competency.model'; +import { Competency, CompetencyWithTailRelationDTO, CourseCompetencyProgress, CourseCompetencyType } from 'app/entities/competency.model'; import { CompetencyManagementComponent } from 'app/course/competencies/competency-management/competency-management.component'; import { ActivatedRoute, provideRouter } from '@angular/router'; import { DeleteButtonDirective } from 'app/shared/delete-dialog/delete-button.directive'; -import { TextUnit } from 'app/entities/lecture-unit/textUnit.model'; import { AccountService } from 'app/core/auth/account.service'; import { ArtemisTestModule } from '../../../test.module'; import { NgbModal, NgbModalRef, NgbProgressbar } from '@ng-bootstrap/ng-bootstrap'; @@ -31,6 +30,8 @@ import { ImportAllCourseCompetenciesModalComponent, ImportAllCourseCompetenciesResult, } from 'app/course/competencies/components/import-all-course-competencies-modal/import-all-course-competencies-modal.component'; +import { MockProfileService } from '../../../helpers/mocks/service/mock-profile.service'; +import { MockAlertService } from '../../../helpers/mocks/service/mock-alert.service'; describe('CompetencyManagementComponent', () => { let fixture: ComponentFixture; @@ -39,11 +40,14 @@ describe('CompetencyManagementComponent', () => { let profileService: ProfileService; let irisSettingsService: IrisSettingsService; let modalService: NgbModal; + let alertService: AlertService; - let getAllForCourseSpy: any; + let getProfileInfoSpy: jest.SpyInstance; + let getAllForCourseSpy: jest.SpyInstance; + let getIrisSettingsSpy: jest.SpyInstance; - beforeEach(() => { - TestBed.configureTestingModule({ + beforeEach(async () => { + await TestBed.configureTestingModule({ imports: [ArtemisTestModule, NgbProgressbar], declarations: [ CompetencyManagementComponent, @@ -60,6 +64,14 @@ describe('CompetencyManagementComponent', () => { provideRouter([]), MockProvider(AccountService), MockProvider(AlertService), + { + provide: AlertService, + useClass: MockAlertService, + }, + { + provide: ProfileService, + useClass: MockProfileService, + }, { provide: NgbModal, useClass: MockNgbModalService }, { provide: ActivatedRoute, @@ -73,34 +85,42 @@ describe('CompetencyManagementComponent', () => { }, ], schemas: [], - }) - .compileComponents() - .then(() => { - fixture = TestBed.createComponent(CompetencyManagementComponent); - component = fixture.componentInstance; - courseCompetencyApiService = TestBed.inject(CourseCompetencyApiService); - modalService = fixture.debugElement.injector.get(NgbModal); - - const competency: Competency = new Competency(); - const textUnit = new TextUnit(); - competency.id = 1; - competency.description = 'test'; - competency.lectureUnitLinks = [new CompetencyLectureUnitLink(competency, textUnit, 1)]; - const courseCompetencyProgress = new CourseCompetencyProgress(); - courseCompetencyProgress.competencyId = 1; - courseCompetencyProgress.numberOfStudents = 8; - courseCompetencyProgress.numberOfMasteredStudents = 5; - courseCompetencyProgress.averageStudentScore = 90; - - getAllForCourseSpy = jest.spyOn(courseCompetencyApiService, 'getCourseCompetenciesByCourseId').mockResolvedValue([ - competency, - { id: 5, type: CourseCompetencyType.COMPETENCY } as Competency, - { - id: 3, - type: CourseCompetencyType.PREREQUISITE, - } as Prerequisite, - ]); - }); + }).compileComponents(); + + courseCompetencyApiService = TestBed.inject(CourseCompetencyApiService); + irisSettingsService = TestBed.inject(IrisSettingsService); + profileService = TestBed.inject(ProfileService); + alertService = TestBed.inject(AlertService); + + const competency: Competency = new Competency(); + competency.id = 1; + competency.description = 'test'; + const courseCompetencyProgress = new CourseCompetencyProgress(); + courseCompetencyProgress.competencyId = 1; + courseCompetencyProgress.numberOfStudents = 8; + courseCompetencyProgress.numberOfMasteredStudents = 5; + courseCompetencyProgress.averageStudentScore = 90; + + getAllForCourseSpy = jest.spyOn(courseCompetencyApiService, 'getCourseCompetenciesByCourseId').mockResolvedValue([ + competency, + { id: 5, type: CourseCompetencyType.COMPETENCY } as Competency, + { + id: 3, + type: CourseCompetencyType.PREREQUISITE, + } as Prerequisite, + ]); + + const profileInfoResponse = { + activeProfiles: [PROFILE_IRIS], + } as ProfileInfo; + getProfileInfoSpy = jest.spyOn(profileService, 'getProfileInfo').mockReturnValue(of(profileInfoResponse)); + + getIrisSettingsSpy = jest.spyOn(irisSettingsService, 'getCombinedCourseSettings'); + + fixture = TestBed.createComponent(CompetencyManagementComponent); + component = fixture.componentInstance; + + modalService = fixture.debugElement.injector.get(NgbModal); }); afterEach(() => { @@ -108,20 +128,15 @@ describe('CompetencyManagementComponent', () => { }); it('should show generate button if IRIS is enabled', async () => { - profileService = TestBed.inject(ProfileService); - irisSettingsService = TestBed.inject(IrisSettingsService); - const profileInfoResponse = { - activeProfiles: [PROFILE_IRIS], - } as ProfileInfo; const irisSettingsResponse = { irisCompetencyGenerationSettings: { enabled: true, }, } as IrisCourseSettings; - const getProfileInfoSpy = jest.spyOn(profileService, 'getProfileInfo').mockReturnValue(of(profileInfoResponse)); - const getIrisSettingsSpy = jest.spyOn(irisSettingsService, 'getCombinedCourseSettings').mockReturnValue(of(irisSettingsResponse)); + getIrisSettingsSpy.mockReturnValue(of(irisSettingsResponse)); - component['loadIrisEnabled'](); + fixture.detectChanges(); + await fixture.whenStable(); fixture.detectChanges(); const generateButton = fixture.nativeElement.querySelector('#generateButton'); @@ -132,12 +147,52 @@ describe('CompetencyManagementComponent', () => { }); it('should load competencies and prerequisites', async () => { - await component.loadData(); + fixture.detectChanges(); + await fixture.whenStable(); - expect(getAllForCourseSpy).toHaveBeenCalledOnce(); + expect(getAllForCourseSpy).toHaveBeenCalledExactlyOnceWith(1); - expect(component.competencies).toHaveLength(2); - expect(component.prerequisites).toHaveLength(1); + expect(component.competencies()).toHaveLength(2); + expect(component.prerequisites()).toHaveLength(1); + }); + + it('should set isLoading correctly', async () => { + const isLoadingSpy = jest.spyOn(component.isLoading, 'set'); + + fixture.detectChanges(); + await fixture.whenStable(); + + expect(isLoadingSpy).toHaveBeenNthCalledWith(1, true); + expect(isLoadingSpy).toHaveBeenNthCalledWith(2, false); + }); + + it('should show alert when loading iris settings fails', async () => { + const errorSpy = jest.spyOn(alertService, 'error'); + getIrisSettingsSpy.mockRejectedValueOnce({}); + + fixture.detectChanges(); + await fixture.whenStable(); + + expect(errorSpy).toHaveBeenCalledOnce(); + }); + + it('should show alert when loading course competencies fails', async () => { + const errorSpy = jest.spyOn(alertService, 'error'); + getAllForCourseSpy.mockRejectedValueOnce({}); + + fixture.detectChanges(); + await fixture.whenStable(); + + expect(errorSpy).toHaveBeenCalledOnce(); + }); + + it('should open course competency explanation', () => { + sessionStorage.setItem('lastTimeVisitedCourseCompetencyExplanation', Date.now().toString()); + const openModalSpy = jest.spyOn(modalService, 'open'); + fixture.detectChanges(); + + component.openCourseCompetencyExplanation(); + expect(openModalSpy).toHaveBeenCalledOnce(); }); it('should open import modal and update values', async () => { @@ -163,20 +218,17 @@ describe('CompetencyManagementComponent', () => { jest.spyOn(modalService, 'open').mockReturnValue(modalRef); jest.spyOn(courseCompetencyApiService, 'importAllByCourseId').mockResolvedValue(importedCompetencies); - await component.loadData(); - const existingCompetencies = component.competencies.length; + const existingCompetencies = component.competencies().length; const importButton = fixture.debugElement.query(By.css('#courseCompetencyImportAllButton')); importButton.nativeElement.click(); fixture.detectChanges(); await fixture.whenStable(); - expect(modalService.open).toHaveBeenCalledWith(ImportAllCourseCompetenciesModalComponent, { + expect(modalService.open).toHaveBeenCalledExactlyOnceWith(ImportAllCourseCompetenciesModalComponent, { size: 'lg', backdrop: 'static', }); - fixture.detectChanges(); - await fixture.whenStable(); - expect(component.competencies).toHaveLength(existingCompetencies + 2); + expect(component.competencies()).toHaveLength(existingCompetencies + 2); }); }); From a88c73d1dfccfc167da5c7567094dbb69f19a13f Mon Sep 17 00:00:00 2001 From: Ramona Beinstingel <75392103+rabeatwork@users.noreply.github.com> Date: Wed, 13 Nov 2024 23:59:27 +0100 Subject: [PATCH 02/21] General: Decrease footer height (#9733) --- src/main/webapp/content/scss/themes/_dark-variables.scss | 2 +- src/main/webapp/content/scss/themes/_default-variables.scss | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/webapp/content/scss/themes/_dark-variables.scss b/src/main/webapp/content/scss/themes/_dark-variables.scss index d674a191ee90..1d9385aa67da 100644 --- a/src/main/webapp/content/scss/themes/_dark-variables.scss +++ b/src/main/webapp/content/scss/themes/_dark-variables.scss @@ -133,7 +133,7 @@ $footer-height-prod: 2rem; // Sidebar $sidebar-width: 255px; $sidebar-footer-height-dev: 104px; -$sidebar-footer-height-prod: 96px; +$sidebar-footer-height-prod: 88px; $spacing-modules: 1rem; //Module and Navigationbar diff --git a/src/main/webapp/content/scss/themes/_default-variables.scss b/src/main/webapp/content/scss/themes/_default-variables.scss index e7dd7adbeca6..35047ea29f83 100644 --- a/src/main/webapp/content/scss/themes/_default-variables.scss +++ b/src/main/webapp/content/scss/themes/_default-variables.scss @@ -54,7 +54,7 @@ $footer-height-prod: 2rem; // Sidebar $sidebar-footer-height-dev: 104px; -$sidebar-footer-height-prod: 96px; +$sidebar-footer-height-prod: 88px; $sidebar-width: 255px; $spacing-modules: 1rem; From 3077f198ba3d554dc32d9167f33d90a07724fc56 Mon Sep 17 00:00:00 2001 From: Simon Entholzer <33342534+SimonEntholzer@users.noreply.github.com> Date: Thu, 14 Nov 2024 00:14:48 +0100 Subject: [PATCH 03/21] Programming exercises: Fix access tokens not showing up in repository view and student view (#9736) --- .../core/service/user/UserService.java | 2 +- .../aet/artemis/core/web/AccountResource.java | 2 +- .../ParticipationVcsAccessTokenService.java | 24 ++++++++++++++----- .../repository-view.component.html | 2 ++ .../code-button/code-button.component.html | 4 ++-- .../code-button/code-button.component.ts | 13 +++++++--- .../icl/LocalVCIntegrationTest.java | 2 +- .../icl/LocalVCLocalCITestService.java | 6 ++--- 8 files changed, 38 insertions(+), 17 deletions(-) diff --git a/src/main/java/de/tum/cit/aet/artemis/core/service/user/UserService.java b/src/main/java/de/tum/cit/aet/artemis/core/service/user/UserService.java index c883f774dcad..d06ecfec87af 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/service/user/UserService.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/service/user/UserService.java @@ -832,7 +832,7 @@ public List importUsers(List userDtos) { * @return the users participation vcs access token, or throws an exception if it does not exist */ public ParticipationVCSAccessToken getParticipationVcsAccessTokenForUserAndParticipationIdOrElseThrow(User user, Long participationId) { - return participationVCSAccessTokenService.findByUserIdAndParticipationIdOrElseThrow(user.getId(), participationId); + return participationVCSAccessTokenService.findByUserAndParticipationIdOrElseThrow(user, participationId); } /** diff --git a/src/main/java/de/tum/cit/aet/artemis/core/web/AccountResource.java b/src/main/java/de/tum/cit/aet/artemis/core/web/AccountResource.java index 8a525f751e89..997574c76da7 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/web/AccountResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/web/AccountResource.java @@ -229,7 +229,7 @@ public ResponseEntity getVcsAccessToken(@RequestParam("participationId") } /** - * PUT account/participation-vcs-access-token : get the vcsToken for of a user for a participation + * PUT account/participation-vcs-access-token : add a vcsToken for of a user for a participation * * @param participationId the participation for which the access token should be fetched * diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/ParticipationVcsAccessTokenService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/ParticipationVcsAccessTokenService.java index 7c41d9f8452d..db9cebb2eb6c 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/ParticipationVcsAccessTokenService.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/ParticipationVcsAccessTokenService.java @@ -8,6 +8,7 @@ import org.springframework.stereotype.Service; import de.tum.cit.aet.artemis.core.domain.User; +import de.tum.cit.aet.artemis.core.exception.AccessForbiddenException; import de.tum.cit.aet.artemis.exercise.domain.participation.StudentParticipation; import de.tum.cit.aet.artemis.programming.domain.ParticipationVCSAccessToken; import de.tum.cit.aet.artemis.programming.repository.ParticipationVCSAccessTokenRepository; @@ -46,18 +47,24 @@ public ParticipationVCSAccessToken createParticipationVCSAccessToken(User user, } /** - * Retrieves the participationVCSAccessToken for a User,Participation pair if it exists + * Retrieves the participationVCSAccessToken for a User,Participation pair if it exists and if the user owns the participation * - * @param userId the user's id which is owner of the token + * @param user the user which is owner of the token * @param participationId the participation's id which the token belongs to * @return an Optional participationVCSAccessToken, */ - public ParticipationVCSAccessToken findByUserIdAndParticipationIdOrElseThrow(long userId, long participationId) { - return participationVcsAccessTokenRepository.findByUserIdAndParticipationIdOrElseThrow(userId, participationId); + public ParticipationVCSAccessToken findByUserAndParticipationIdOrElseThrow(User user, long participationId) { + var participation = programmingExerciseStudentParticipationRepository.findByIdElseThrow(participationId); + if (participation.isOwnedBy(user)) { + return participationVcsAccessTokenRepository.findByUserIdAndParticipationIdOrElseThrow(user.getId(), participationId); + } + else { + throw new AccessForbiddenException("Participation not owned by user"); + } } /** - * Checks if the participationVCSAccessToken for a User,Participation pair exists, and creates a new one if not + * Checks if the participationVCSAccessToken for a User,Participation pair exists, and creates a new one if not; if the user owns the participation * * @param user the user's id which is owner of the token * @param participationId the participation's id which the token belongs to @@ -66,7 +73,12 @@ public ParticipationVCSAccessToken findByUserIdAndParticipationIdOrElseThrow(lon public ParticipationVCSAccessToken createVcsAccessTokenForUserAndParticipationIdOrElseThrow(User user, long participationId) { participationVcsAccessTokenRepository.findByUserIdAndParticipationIdAndThrowIfExists(user.getId(), participationId); var participation = programmingExerciseStudentParticipationRepository.findByIdElseThrow(participationId); - return createParticipationVCSAccessToken(user, participation); + if (participation.isOwnedBy(user)) { + return createParticipationVCSAccessToken(user, participation); + } + else { + throw new AccessForbiddenException("Participation not owned by user"); + } } /** diff --git a/src/main/webapp/app/localvc/repository-view/repository-view.component.html b/src/main/webapp/app/localvc/repository-view/repository-view.component.html index 6a41335a4f51..24c332323ed4 100644 --- a/src/main/webapp/app/localvc/repository-view/repository-view.component.html +++ b/src/main/webapp/app/localvc/repository-view/repository-view.component.html @@ -47,6 +47,8 @@

[smallButtons]="false" [routerLinkForRepositoryView]="'.'" [repositoryUri]="repositoryUri" + [participations]="[participation]" + [useParticipationVcsAccessToken]="true" [exercise]="exercise" /> } diff --git a/src/main/webapp/app/shared/components/code-button/code-button.component.html b/src/main/webapp/app/shared/components/code-button/code-button.component.html index 9b8d4f0488fd..80972a802a38 100644 --- a/src/main/webapp/app/shared/components/code-button/code-button.component.html +++ b/src/main/webapp/app/shared/components/code-button/code-button.component.html @@ -17,10 +17,10 @@ @if (useSsh && !copyEnabled) {
} - @if (useToken && !copyEnabled && tokenMissing) { + @if (useToken && tokenMissing) {
} - @if (useToken && !copyEnabled && tokenExpired) { + @if (useToken && tokenExpired) {
} @if (participations && participations.length > 1) { diff --git a/src/main/webapp/app/shared/components/code-button/code-button.component.ts b/src/main/webapp/app/shared/components/code-button/code-button.component.ts index 8f83edec0be8..7e4e0d33e766 100644 --- a/src/main/webapp/app/shared/components/code-button/code-button.component.ts +++ b/src/main/webapp/app/shared/components/code-button/code-button.component.ts @@ -92,7 +92,6 @@ export class CodeButtonComponent implements OnInit, OnChanges { this.copyEnabled = true; this.useSsh = this.localStorage.retrieve('useSsh') || false; this.useToken = this.localStorage.retrieve('useToken') || false; - this.loadParticipationVcsAccessTokens(); // Get ssh information from the user this.profileService.getProfileInfo().subscribe((profileInfo) => { @@ -124,6 +123,7 @@ export class CodeButtonComponent implements OnInit, OnChanges { if (this.useToken) { this.useHttpsUrlWithToken(); } + this.loadParticipationVcsAccessTokens(); }); this.ideSettingsService.loadIdePreferences().then((programmingLanguageToIde) => { @@ -159,7 +159,7 @@ export class CodeButtonComponent implements OnInit, OnChanges { public useHttpsUrlWithToken() { this.useSsh = false; this.useToken = true; - this.copyEnabled = !!(this.accessTokensEnabled && this.useToken && ((!!this.user.vcsAccessToken && !this.isTokenExpired()) || this.useParticipationVcsAccessToken)); + this.copyEnabled = !!(this.accessTokensEnabled && ((!!this.user.vcsAccessToken && !this.isTokenExpired()) || this.useParticipationVcsAccessToken)); this.refreshTokenState(); this.storeToLocalStorage(); } @@ -233,6 +233,9 @@ export class CodeButtonComponent implements OnInit, OnChanges { if (error.status == 404) { this.createNewVcsAccessToken(participation); } + if (error.status == 403) { + this.useParticipationVcsAccessToken = false; + } }, }); } @@ -250,7 +253,11 @@ export class CodeButtonComponent implements OnInit, OnChanges { } } }, - error: () => {}, + error: (error: HttpErrorResponse) => { + if (error.status == 403) { + this.useParticipationVcsAccessToken = false; + } + }, }); } diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/icl/LocalVCIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/programming/icl/LocalVCIntegrationTest.java index af998a54a556..4f6840fd63f4 100644 --- a/src/test/java/de/tum/cit/aet/artemis/programming/icl/LocalVCIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/programming/icl/LocalVCIntegrationTest.java @@ -110,7 +110,7 @@ void testFetchPush_repositoryDoesNotExist() throws IOException, GitAPIException, void testFetchPush_usingVcsAccessToken() { var programmingParticipation = localVCLocalCITestService.createParticipation(programmingExercise, student1Login); var student = userUtilService.getUserByLogin(student1Login); - var participationVcsAccessToken = localVCLocalCITestService.getParticipationVcsAccessToken(student.getId(), programmingParticipation.getId()); + var participationVcsAccessToken = localVCLocalCITestService.getParticipationVcsAccessToken(student, programmingParticipation.getId()); var token = participationVcsAccessToken.getVcsAccessToken(); programmingExerciseRepository.save(programmingExercise); diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/icl/LocalVCLocalCITestService.java b/src/test/java/de/tum/cit/aet/artemis/programming/icl/LocalVCLocalCITestService.java index c6d7556e3766..7ad94838921f 100644 --- a/src/test/java/de/tum/cit/aet/artemis/programming/icl/LocalVCLocalCITestService.java +++ b/src/test/java/de/tum/cit/aet/artemis/programming/icl/LocalVCLocalCITestService.java @@ -676,13 +676,13 @@ public void verifyRepositoryFoldersExist(ProgrammingExercise programmingExercise /** * Gets the participationVcsAccessToken belonging to a user and a participation * - * @param userId The user's id + * @param user The user * @param programmingParticipationId The participation's id * * @return the participationVcsAccessToken of the user for the given participationId */ - public ParticipationVCSAccessToken getParticipationVcsAccessToken(Long userId, Long programmingParticipationId) { - return participationVcsAccessTokenService.findByUserIdAndParticipationIdOrElseThrow(userId, programmingParticipationId); + public ParticipationVCSAccessToken getParticipationVcsAccessToken(User user, Long programmingParticipationId) { + return participationVcsAccessTokenService.findByUserAndParticipationIdOrElseThrow(user, programmingParticipationId); } /** From 050b09688eaff1bcc27656100b309c69558f810d Mon Sep 17 00:00:00 2001 From: Stephan Krusche Date: Thu, 14 Nov 2024 00:20:26 +0100 Subject: [PATCH 04/21] Development: Update server dependencies --- build.gradle | 12 ++++++------ gradle.properties | 2 +- gradle/wrapper/gradle-wrapper.properties | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/build.gradle b/build.gradle index d5650c72036a..90c356b0c7d3 100644 --- a/build.gradle +++ b/build.gradle @@ -351,7 +351,7 @@ dependencies { implementation "tech.jhipster:jhipster-framework:${jhipster_dependencies_version}" implementation "org.springframework.boot:spring-boot-starter-cache:${spring_boot_version}" - implementation "io.micrometer:micrometer-registry-prometheus:1.13.6" + implementation "io.micrometer:micrometer-registry-prometheus:1.14.0" implementation "net.logstash.logback:logstash-logback-encoder:8.0" // Defines low-level streaming API, and includes JSON-specific implementations @@ -414,8 +414,8 @@ dependencies { implementation "org.springframework.cloud:spring-cloud-starter-config:4.1.3" implementation "org.springframework.cloud:spring-cloud-commons:4.1.4" - implementation "io.netty:netty-all:4.1.114.Final" - implementation "io.projectreactor.netty:reactor-netty:1.1.23" + implementation "io.netty:netty-all:4.1.115.Final" + implementation "io.projectreactor.netty:reactor-netty:1.2.0" implementation "org.springframework:spring-messaging:6.1.14" implementation "org.springframework.retry:spring-retry:2.0.10" @@ -536,10 +536,10 @@ dependencies { testImplementation "org.mockito:mockito-core:${mockito_version}" testImplementation "org.mockito:mockito-junit-jupiter:${mockito_version}" - testImplementation "io.github.classgraph:classgraph:4.8.177" + testImplementation "io.github.classgraph:classgraph:4.8.179" testImplementation "org.awaitility:awaitility:4.2.2" testImplementation "org.apache.maven.shared:maven-invoker:3.3.0" - testImplementation "org.gradle:gradle-tooling-api:8.10.2" + testImplementation "org.gradle:gradle-tooling-api:8.11" testImplementation "org.apache.maven.surefire:surefire-report-parser:3.5.2" testImplementation "com.opencsv:opencsv:5.9" testImplementation("io.zonky.test:embedded-database-spring-test:2.5.1") { @@ -621,7 +621,7 @@ tasks.withType(Test).configureEach { } wrapper { - gradleVersion = "8.10.2" + gradleVersion = "8.11" } tasks.register("stage") { diff --git a/gradle.properties b/gradle.properties index 8e95ead2b4ff..2f2ade9049ba 100644 --- a/gradle.properties +++ b/gradle.properties @@ -25,7 +25,7 @@ jplag_version=5.1.0 # NOTE: we do not need to use the latest version 9.x here as long as Stanford CoreNLP does not reference it lucene_version=8.11.4 slf4j_version=2.0.16 -sentry_version=7.16.0 +sentry_version=7.17.0 liquibase_version=4.30.0 docker_java_version=3.4.0 logback_version=1.5.12 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index df97d72b8b91..94113f200e61 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.11-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME From d266ee661825d67f1423dbab7b35460684e1fabe Mon Sep 17 00:00:00 2001 From: Aybike Ece Eren Date: Thu, 14 Nov 2024 13:44:31 +0100 Subject: [PATCH 05/21] General: Fix an issue in Chrome when uploading files (#9766) --- src/main/webapp/app/lecture/attachment.service.ts | 10 ++++++++-- .../attachmentUnit.service.ts | 13 +++++++++++-- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/src/main/webapp/app/lecture/attachment.service.ts b/src/main/webapp/app/lecture/attachment.service.ts index 446d26960f8b..cd8a93d7b94f 100644 --- a/src/main/webapp/app/lecture/attachment.service.ts +++ b/src/main/webapp/app/lecture/attachment.service.ts @@ -35,8 +35,11 @@ export class AttachmentService { copy.lecture.posts = undefined; } + /** Ngsw-worker is bypassed temporarily to fix Chromium file upload issue + * See: https://issues.chromium.org/issues/374550348 + **/ return this.http - .post(this.resourceUrl, this.createFormData(copy, file), { observe: 'response' }) + .post(this.resourceUrl, this.createFormData(copy, file), { headers: { 'ngsw-bypass': 'true' }, observe: 'response' }) .pipe(map((res: EntityResponseType) => this.convertAttachmentResponseDatesFromServer(res))); } @@ -51,8 +54,11 @@ export class AttachmentService { const options = createRequestOption(req); const copy = this.convertAttachmentDatesFromClient(attachment); + /** Ngsw-worker is bypassed temporarily to fix Chromium file upload issue + * See: https://issues.chromium.org/issues/374550348 + **/ return this.http - .put(this.resourceUrl + '/' + attachmentId, this.createFormData(copy, file), { params: options, observe: 'response' }) + .put(this.resourceUrl + '/' + attachmentId, this.createFormData(copy, file), { headers: { 'ngsw-bypass': 'true' }, params: options, observe: 'response' }) .pipe(map((res: EntityResponseType) => this.convertAttachmentResponseDatesFromServer(res))); } diff --git a/src/main/webapp/app/lecture/lecture-unit/lecture-unit-management/attachmentUnit.service.ts b/src/main/webapp/app/lecture/lecture-unit/lecture-unit-management/attachmentUnit.service.ts index 533d454f9a06..4a5e3ed80a71 100644 --- a/src/main/webapp/app/lecture/lecture-unit/lecture-unit-management/attachmentUnit.service.ts +++ b/src/main/webapp/app/lecture/lecture-unit/lecture-unit-management/attachmentUnit.service.ts @@ -26,18 +26,27 @@ export class AttachmentUnitService { } create(formData: FormData, lectureId: number): Observable { + /** Ngsw-worker is bypassed temporarily to fix Chromium file upload issue + * See: https://issues.chromium.org/issues/374550348 + **/ return this.httpClient - .post(`${this.resourceURL}/lectures/${lectureId}/attachment-units?keepFilename=true`, formData, { observe: 'response' }) + .post(`${this.resourceURL}/lectures/${lectureId}/attachment-units?keepFilename=true`, formData, { + headers: { 'ngsw-bypass': 'true' }, + observe: 'response', + }) .pipe(map((res: EntityResponseType) => this.lectureUnitService.convertLectureUnitResponseDatesFromServer(res))); } update(lectureId: number, attachmentUnitId: number, formData: FormData, notificationText?: string): Observable { + /** Ngsw-worker is bypassed temporarily to fix Chromium file upload issue + * See: https://issues.chromium.org/issues/374550348 + **/ return this.httpClient .put( `${this.resourceURL}/lectures/${lectureId}/attachment-units/${attachmentUnitId}?keepFilename=true` + (notificationText ? `¬ificationText=${notificationText}` : ''), formData, - { observe: 'response' }, + { headers: { 'ngsw-bypass': 'true' }, observe: 'response' }, ) .pipe(map((res: EntityResponseType) => this.lectureUnitService.convertLectureUnitResponseDatesFromServer(res))); } From bcce67b3aa7f653a65e55b196e407ccb1126326c Mon Sep 17 00:00:00 2001 From: Maximilian Anzinger <44003963+MaximilianAnzinger@users.noreply.github.com> Date: Thu, 14 Nov 2024 23:58:10 +0100 Subject: [PATCH 06/21] Adaptive learning: Simplify date picker for competency due dates to exclude hours and minutes (#9715) --- ...mmon-course-competency-form.component.html | 1 + ...common-course-competency-form.component.ts | 2 ++ .../date-time-picker.component.html | 26 +++++++++++++++---- .../date-time-picker.component.ts | 10 +++++++ 4 files changed, 34 insertions(+), 5 deletions(-) diff --git a/src/main/webapp/app/course/competencies/forms/common-course-competency-form.component.html b/src/main/webapp/app/course/competencies/forms/common-course-competency-form.component.html index afd6368b570d..56a6347b3aed 100644 --- a/src/main/webapp/app/course/competencies/forms/common-course-competency-form.component.html +++ b/src/main/webapp/app/course/competencies/forms/common-course-competency-form.component.html @@ -55,6 +55,7 @@ labelName="{{ 'artemisApp.' + courseCompetency?.type + '.create.softDueDate' | artemisTranslate }}" labelTooltip="{{ 'artemisApp.' + courseCompetency?.type + '.create.softDueDateHint' | artemisTranslate }}" formControlName="softDueDate" + [pickerType]="DateTimePickerType.CALENDAR" /> @if (!isInConnectMode) { diff --git a/src/main/webapp/app/course/competencies/forms/common-course-competency-form.component.ts b/src/main/webapp/app/course/competencies/forms/common-course-competency-form.component.ts index cd512a1d12f6..e7c2cadf3ad4 100644 --- a/src/main/webapp/app/course/competencies/forms/common-course-competency-form.component.ts +++ b/src/main/webapp/app/course/competencies/forms/common-course-competency-form.component.ts @@ -15,6 +15,7 @@ import { ArtemisCompetenciesModule } from 'app/course/competencies/competency.mo import { ArtemisSharedComponentModule } from 'app/shared/components/shared-component.module'; import { merge } from 'rxjs'; import { ArtemisMarkdownEditorModule } from 'app/shared/markdown-editor/markdown-editor.module'; +import { DateTimePickerType } from 'app/shared/date-time-picker/date-time-picker.component'; @Component({ selector: 'jhi-common-course-competency-form', @@ -54,6 +55,7 @@ export class CommonCourseCompetencyFormComponent implements OnInit, OnChanges { onTitleOrDescriptionChange = new EventEmitter(); protected readonly competencyValidators = CourseCompetencyValidators; + protected readonly DateTimePickerType = DateTimePickerType; suggestedTaxonomies: string[] = []; diff --git a/src/main/webapp/app/shared/date-time-picker/date-time-picker.component.html b/src/main/webapp/app/shared/date-time-picker/date-time-picker.component.html index 99d5dc37099d..f6958870f086 100644 --- a/src/main/webapp/app/shared/date-time-picker/date-time-picker.component.html +++ b/src/main/webapp/app/shared/date-time-picker/date-time-picker.component.html @@ -26,19 +26,35 @@ [min]="minDate()" [max]="maxDate()" (ngModelChange)="updateField($event)" - [owlDateTime]="dt" + [owlDateTime]="pickerType() === DateTimePickerType.CALENDAR ? dtCalendar : pickerType() === DateTimePickerType.TIMER ? dtTimer : dtDefault" name="datePicker" /> - + @switch (pickerType()) { + @case (DateTimePickerType.TIMER) { + + } + @case (DateTimePickerType.CALENDAR) { + + } + @default { + + } + } + + + +
- + + + @if (dateInput.invalid || (requiredField() && !dateInput.value)) {
diff --git a/src/main/webapp/app/shared/date-time-picker/date-time-picker.component.ts b/src/main/webapp/app/shared/date-time-picker/date-time-picker.component.ts index 91333688ca6a..78d1f46b9a11 100644 --- a/src/main/webapp/app/shared/date-time-picker/date-time-picker.component.ts +++ b/src/main/webapp/app/shared/date-time-picker/date-time-picker.component.ts @@ -3,6 +3,12 @@ import { ControlValueAccessor, NG_VALUE_ACCESSOR, NgModel } from '@angular/forms import { faCalendarAlt, faCircleXmark, faClock, faGlobe, faQuestionCircle, faTriangleExclamation } from '@fortawesome/free-solid-svg-icons'; import dayjs from 'dayjs/esm'; +export enum DateTimePickerType { + CALENDAR, + TIMER, + DEFAULT, +} + @Component({ selector: 'jhi-date-time-picker', templateUrl: `./date-time-picker.component.html`, @@ -24,6 +30,7 @@ export class FormDateTimePickerComponent implements ControlValueAccessor { protected readonly faTriangleExclamation = faTriangleExclamation; @ViewChild('dateInput', { static: false }) dateInput: NgModel; + labelName = input(); hideLabelName = input(false); labelTooltip = input(); @@ -36,6 +43,7 @@ export class FormDateTimePickerComponent implements ControlValueAccessor { min = input(); // Dates before this date are not selectable. max = input(); // Dates after this date are not selectable. shouldDisplayTimeZoneWarning = input(true); // Displays a warning that the current time zone might differ from the participants'. + pickerType = input(DateTimePickerType.DEFAULT); // Select type of picker valueChange = output(); protected isInputValid = signal(false); @@ -135,4 +143,6 @@ export class FormDateTimePickerComponent implements ControlValueAccessor { this.dateInput.reset(undefined); this.updateSignals(); } + + protected readonly DateTimePickerType = DateTimePickerType; } From e1520685e4a8af0be3edb12150c73b6be726811b Mon Sep 17 00:00:00 2001 From: Simon Entholzer <33342534+SimonEntholzer@users.noreply.github.com> Date: Fri, 15 Nov 2024 09:19:42 +0100 Subject: [PATCH 07/21] Development: Fix object has been recycled errors on develop (#9780) --- .../service/localvc/LocalVCPushFilter.java | 8 +++++++- .../service/localvc/LocalVCServletService.java | 16 +++++++++------- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/localvc/LocalVCPushFilter.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/localvc/LocalVCPushFilter.java index 914205081e60..df082b7362c2 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/localvc/LocalVCPushFilter.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/localvc/LocalVCPushFilter.java @@ -47,7 +47,13 @@ public void doFilterInternal(HttpServletRequest servletRequest, HttpServletRespo servletResponse.setStatus(localVCServletService.getHttpStatusForException(e, servletRequest.getRequestURI())); return; } - this.localVCServletService.updateVCSAccessLogForPushHTTPS(servletRequest); + + // We need to extract the content of the request here as it is garbage collected before it can be used asynchronously + String authorizationHeader = servletRequest.getHeader(LocalVCServletService.AUTHORIZATION_HEADER); + String method = servletRequest.getMethod(); + LocalVCRepositoryUri localVCRepositoryUri = localVCServletService.parseRepositoryUri(servletRequest); + + this.localVCServletService.updateVCSAccessLogForPushHTTPS(method, authorizationHeader, localVCRepositoryUri); filterChain.doFilter(servletRequest, servletResponse); diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/localvc/LocalVCServletService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/localvc/LocalVCServletService.java index f5fa8b2c9243..986f3837b7af 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/localvc/LocalVCServletService.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/localvc/LocalVCServletService.java @@ -362,7 +362,7 @@ public boolean isUserAllowedToForcePush(User user, Repository repository) { return isAllowedRepository && authorizationCheckService.isAtLeastEditorInCourse(exercise.getCourseViaExerciseGroupOrCourseMember(), user); } - private LocalVCRepositoryUri parseRepositoryUri(HttpServletRequest request) { + public LocalVCRepositoryUri parseRepositoryUri(HttpServletRequest request) { return new LocalVCRepositoryUri(request.getRequestURL().toString().replace("/info/refs", "")); } @@ -797,24 +797,26 @@ public void updateVCSAccessLogForCloneAndPullHTTPS(HttpServletRequest request, i * This method logs the access information if the HTTP request is a POST request and the action * is not performed by a build job user. The repository action type is set as a push action. * - * This method is asynchronous. + * @param method the HTTP method of the request (expected to be "POST" for logging to occur) + * @param authorizationHeader the authorization header containing the username and password in basic authentication format + * @param localVcUri the {@link LocalVCRepositoryUri} identifying the repository in the local version control system + * + * This method is asynchronous. * - * @param request the {@link HttpServletRequest} containing the HTTP request data, including headers. */ @Async - public void updateVCSAccessLogForPushHTTPS(HttpServletRequest request) { - if (!request.getMethod().equals("POST")) { + public void updateVCSAccessLogForPushHTTPS(String method, String authorizationHeader, LocalVCRepositoryUri localVcUri) { + if (!method.equals("POST")) { return; } try { - String authorizationHeader = request.getHeader(LocalVCServletService.AUTHORIZATION_HEADER); UsernameAndPassword usernameAndPassword = extractUsernameAndPassword(authorizationHeader); String userName = usernameAndPassword.username(); if (userName.equals(BUILD_USER_NAME)) { return; } RepositoryActionType repositoryActionType = RepositoryActionType.PUSH; - var participation = getExerciseParticipationFromRequest(request); + var participation = retrieveParticipationFromLocalVCRepositoryUri(localVcUri); vcsAccessLogService.ifPresent(service -> service.updateRepositoryActionType(participation, repositoryActionType)); } From c1bfa2f14fd9e12426995cebef8f00b46a30902c Mon Sep 17 00:00:00 2001 From: Aniruddh Zaveri <92953467+az108@users.noreply.github.com> Date: Fri, 15 Nov 2024 09:22:24 +0100 Subject: [PATCH 08/21] Programming exercises: Fix an issue for grading statistics (#9779) --- .../ProgrammingExerciseGradingService.java | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/ProgrammingExerciseGradingService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/ProgrammingExerciseGradingService.java index 097e0db39d3c..7738b6615b2f 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/ProgrammingExerciseGradingService.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/ProgrammingExerciseGradingService.java @@ -1078,7 +1078,25 @@ private static Map categorizeStaticCodeAnalysisIssues(Result re private static void updateTestCaseMapBasedOnResultFeedback(Result result, HashMap testCaseStatsMap) { result.getFeedbacks().stream() // Filter the feedbacks to include only those that are automatic and have an assigned test case - .filter(feedback -> FeedbackType.AUTOMATIC.equals(feedback.getType()) && feedback.getTestCase() != null) + .filter(feedback -> { + if (!FeedbackType.AUTOMATIC.equals(feedback.getType())) { + return false; + } + if (feedback.getTestCase() == null) { + return false; + } + if (feedback.getTestCase().getTestName() == null) { + // Log the feedback id with null test name to analyse NullPointer issue if it occurs again in the future + log.warn("Feedback with ID {} has a test case with a null test name.", feedback.getId()); + return false; + } + if (feedback.isPositive() == null) { + // Log the feedback with null isPositive value to analyse NullPointer issue if it occurs again in the future + log.warn("Feedback with ID {} has a test case with a null isPositive value.", feedback.getId()); + return false; + } + return true; + }) // Collect the filtered feedbacks into a map grouped by test case name, and partitioned by whether the feedback is positive .collect(Collectors.groupingBy( // Group by the name of the test case associated with the feedback From 23b99021b21a401c60190816ec5ad34144ea3b40 Mon Sep 17 00:00:00 2001 From: Stephan Krusche Date: Fri, 15 Nov 2024 11:13:28 +0100 Subject: [PATCH 09/21] Development: Update client dependencies --- angular.json | 1 + jest.config.js | 4 +- package-lock.json | 804 +++++++++++++++++++++++----------------------- package.json | 50 +-- 4 files changed, 424 insertions(+), 435 deletions(-) diff --git a/angular.json b/angular.json index 815c38a3f705..02cb18df601c 100644 --- a/angular.json +++ b/angular.json @@ -39,6 +39,7 @@ "markdown-it-highlightjs", "mobile-drag-drop", "papaparse", + "pako", "pepjs", "prop-types", "react", diff --git a/jest.config.js b/jest.config.js index 6fc625124844..96eeb24f0890 100644 --- a/jest.config.js +++ b/jest.config.js @@ -105,8 +105,8 @@ module.exports = { coverageThreshold: { global: { // TODO: in the future, the following values should increase to at least 90% - statements: 87.67, - branches: 73.81, + statements: 87.66, + branches: 73.79, functions: 82.17, lines: 87.72, }, diff --git a/package-lock.json b/package-lock.json index 30e371368350..23e7880563ff 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,18 +10,18 @@ "hasInstallScript": true, "license": "MIT", "dependencies": { - "@angular/animations": "18.2.11", - "@angular/cdk": "18.2.12", - "@angular/common": "18.2.11", - "@angular/compiler": "18.2.11", - "@angular/core": "18.2.11", - "@angular/forms": "18.2.11", - "@angular/localize": "18.2.11", - "@angular/material": "18.2.12", - "@angular/platform-browser": "18.2.11", - "@angular/platform-browser-dynamic": "18.2.11", - "@angular/router": "18.2.11", - "@angular/service-worker": "18.2.11", + "@angular/animations": "18.2.12", + "@angular/cdk": "18.2.13", + "@angular/common": "18.2.12", + "@angular/compiler": "18.2.12", + "@angular/core": "18.2.12", + "@angular/forms": "18.2.12", + "@angular/localize": "18.2.12", + "@angular/material": "18.2.13", + "@angular/platform-browser": "18.2.12", + "@angular/platform-browser-dynamic": "18.2.12", + "@angular/router": "18.2.12", + "@angular/service-worker": "18.2.12", "@ctrl/ngx-emoji-mart": "9.2.0", "@danielmoncada/angular-datetime-picker": "18.1.0", "@fingerprintjs/fingerprintjs": "4.5.1", @@ -33,9 +33,9 @@ "@ng-bootstrap/ng-bootstrap": "17.0.1", "@ngx-translate/core": "16.0.3", "@ngx-translate/http-loader": "16.0.0", - "@sentry/angular": "8.37.1", + "@sentry/angular": "8.38.0", "@siemens/ngx-datatable": "22.4.1", - "@swimlane/ngx-charts": "20.5.0", + "@swimlane/ngx-charts": "21.0.0", "@swimlane/ngx-graph": "8.4.0", "@vscode/codicons": "0.0.36", "@vscode/markdown-it-katex": "1.1.0", @@ -45,7 +45,7 @@ "crypto-js": "4.2.0", "dayjs": "1.11.13", "diff-match-patch-typescript": "1.1.0", - "dompurify": "3.1.7", + "dompurify": "3.2.0", "emoji-js": "3.8.0", "export-to-csv": "1.4.0", "fast-json-patch": "3.1.1", @@ -66,7 +66,7 @@ "papaparse": "5.4.1", "pdf-lib": "1.17.1", "pdfjs-dist": "4.8.69", - "posthog-js": "1.181.0", + "posthog-js": "1.186.0", "rxjs": "7.8.1", "simple-statistics": "7.8.7", "smoothscroll-polyfill": "0.4.4", @@ -75,23 +75,23 @@ "ts-cacheable": "1.0.10", "tslib": "2.8.1", "turndown": "7.2.0", - "uuid": "11.0.2", + "uuid": "11.0.3", "webstomp-client": "1.2.6", "xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz", "zone.js": "0.14.10" }, "devDependencies": { "@angular-builders/jest": "18.0.0", - "@angular-devkit/build-angular": "18.2.11", + "@angular-devkit/build-angular": "18.2.12", "@angular-eslint/builder": "18.4.0", "@angular-eslint/eslint-plugin": "18.4.0", "@angular-eslint/eslint-plugin-template": "18.4.0", "@angular-eslint/schematics": "18.4.0", "@angular-eslint/template-parser": "18.4.0", - "@angular/cli": "18.2.11", - "@angular/compiler-cli": "18.2.11", - "@angular/language-service": "18.2.11", - "@sentry/types": "8.37.1", + "@angular/cli": "18.2.12", + "@angular/compiler-cli": "18.2.12", + "@angular/language-service": "18.2.12", + "@sentry/types": "8.38.0", "@types/crypto-js": "4.2.2", "@types/d3-shape": "3.1.6", "@types/dompurify": "3.0.5", @@ -105,8 +105,8 @@ "@types/sockjs-client": "1.5.4", "@types/turndown": "5.0.5", "@types/uuid": "10.0.0", - "@typescript-eslint/eslint-plugin": "8.13.0", - "@typescript-eslint/parser": "8.13.0", + "@typescript-eslint/eslint-plugin": "8.14.0", + "@typescript-eslint/parser": "8.14.0", "eslint": "9.14.0", "eslint-config-prettier": "9.1.0", "eslint-plugin-deprecation": "3.0.0", @@ -127,7 +127,7 @@ "ngxtension": "4.1.0", "prettier": "3.3.3", "rimraf": "6.0.1", - "sass": "1.80.6", + "sass": "1.81.0", "ts-jest": "29.2.5", "typescript": "5.5.4", "weak-napi": "2.0.2" @@ -218,13 +218,13 @@ } }, "node_modules/@angular-devkit/architect": { - "version": "0.1802.11", - "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1802.11.tgz", - "integrity": "sha512-p+XIc/j51aI83ExNdeZwvkm1F4wkuKMGUUoj0MVUUi5E6NoiMlXYm6uU8+HbRvPBzGy5+3KOiGp3Fks0UmDSAA==", + "version": "0.1802.12", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1802.12.tgz", + "integrity": "sha512-bepVb2/GtJppYKaeW8yTGE6egmoWZ7zagFDsmBdbF+BYp+HmeoPsclARcdryBPVq68zedyTRdvhWSUTbw1AYuw==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/core": "18.2.11", + "@angular-devkit/core": "18.2.12", "rxjs": "7.8.1" }, "engines": { @@ -234,17 +234,17 @@ } }, "node_modules/@angular-devkit/build-angular": { - "version": "18.2.11", - "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-18.2.11.tgz", - "integrity": "sha512-09Ln3NAdlMw/wMLgnwYU5VgWV5TPBEHolZUIvE9D8b6SFWBCowk3B3RWeAMgg7Peuf9SKwqQHBz2b1C7RTP/8g==", + "version": "18.2.12", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-18.2.12.tgz", + "integrity": "sha512-quVUi7eqTq9OHumQFNl9Y8t2opm8miu4rlYnuF6rbujmmBDvdUvR6trFChueRczl2p5HWqTOr6NPoDGQm8AyNw==", "dev": true, "license": "MIT", "dependencies": { "@ampproject/remapping": "2.3.0", - "@angular-devkit/architect": "0.1802.11", - "@angular-devkit/build-webpack": "0.1802.11", - "@angular-devkit/core": "18.2.11", - "@angular/build": "18.2.11", + "@angular-devkit/architect": "0.1802.12", + "@angular-devkit/build-webpack": "0.1802.12", + "@angular-devkit/core": "18.2.12", + "@angular/build": "18.2.12", "@babel/core": "7.25.2", "@babel/generator": "7.25.0", "@babel/helper-annotate-as-pure": "7.24.7", @@ -255,7 +255,7 @@ "@babel/preset-env": "7.25.3", "@babel/runtime": "7.25.0", "@discoveryjs/json-ext": "0.6.1", - "@ngtools/webpack": "18.2.11", + "@ngtools/webpack": "18.2.12", "@vitejs/plugin-basic-ssl": "1.1.0", "ansi-colors": "4.1.3", "autoprefixer": "10.4.20", @@ -362,6 +362,13 @@ } } }, + "node_modules/@angular-devkit/build-angular/node_modules/immutable": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.7.tgz", + "integrity": "sha512-1hqclzwYwjRDFLjcFxOM5AYkkG0rpFPpr1RLPMEuGczoS7YA8gLhy8SWXYRAA/XwfEHpfo3cw5JGioS32fnMRw==", + "dev": true, + "license": "MIT" + }, "node_modules/@angular-devkit/build-angular/node_modules/sass": { "version": "1.77.6", "resolved": "https://registry.npmjs.org/sass/-/sass-1.77.6.tgz", @@ -388,13 +395,13 @@ "license": "0BSD" }, "node_modules/@angular-devkit/build-webpack": { - "version": "0.1802.11", - "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1802.11.tgz", - "integrity": "sha512-G76rNsyn1iQk7qjyr+K4rnDzfalmEswmwXQorypSDGaHYzIDY1SZXMoP4225WMq5fJNBOJrk82FA0PSfnPE+zQ==", + "version": "0.1802.12", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1802.12.tgz", + "integrity": "sha512-0Z3fdbZVRnjYWE2/VYyfy+uieY+6YZyEp4ylzklVkc+fmLNsnz4Zw6cK1LzzcBqAwKIyh1IdW20Cg7o8b0sONA==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/architect": "0.1802.11", + "@angular-devkit/architect": "0.1802.12", "rxjs": "7.8.1" }, "engines": { @@ -408,9 +415,9 @@ } }, "node_modules/@angular-devkit/core": { - "version": "18.2.11", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-18.2.11.tgz", - "integrity": "sha512-H9P1shRGigORWJHUY2BRa2YurT+DVminrhuaYHsbhXBRsPmgB2Dx/30YLTnC1s5XmR9QIRUCsg/d3kyT1wd5Zg==", + "version": "18.2.12", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-18.2.12.tgz", + "integrity": "sha512-NtB6ypsaDyPE6/fqWOdfTmACs+yK5RqfH5tStEzWFeeDsIEDYKsJ06ypuRep7qTjYus5Rmttk0Ds+cFgz8JdUQ==", "dev": true, "license": "MIT", "dependencies": { @@ -436,13 +443,13 @@ } }, "node_modules/@angular-devkit/schematics": { - "version": "18.2.11", - "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-18.2.11.tgz", - "integrity": "sha512-efRK3FotTFp4KD5u42jWfXpHUALXB9kJNsWiB4wEImKFH6CN+vjBspJQuLqk2oeBFh/7D2qRMc5P+2tZHM5hdw==", + "version": "18.2.12", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-18.2.12.tgz", + "integrity": "sha512-mMea9txHbnCX5lXLHlo0RAgfhFHDio45/jMsREM2PA8UtVf2S8ltXz7ZwUrUyMQRv8vaSfn4ijDstF4hDMnRgQ==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/core": "18.2.11", + "@angular-devkit/core": "18.2.12", "jsonc-parser": "3.3.1", "magic-string": "0.30.11", "ora": "5.4.1", @@ -556,9 +563,9 @@ } }, "node_modules/@angular/animations": { - "version": "18.2.11", - "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-18.2.11.tgz", - "integrity": "sha512-ghgXa2VhtyJJnTMuH2NYxCMsveQbZno44AZGygPqrcW8UQMQe9GulFaTXCH5s6/so2CLy2ZviIwSZQRgK0ZlDw==", + "version": "18.2.12", + "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-18.2.12.tgz", + "integrity": "sha512-XcWH/VFQ1Rddhdqi/iU8lW3Qg96yVx1NPfrO5lhcSSvVUzYWTZ5r+jh3GqYqUgPWyEp1Kpw3FLsOgVcGcBWQkQ==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -567,18 +574,18 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/core": "18.2.11" + "@angular/core": "18.2.12" } }, "node_modules/@angular/build": { - "version": "18.2.11", - "resolved": "https://registry.npmjs.org/@angular/build/-/build-18.2.11.tgz", - "integrity": "sha512-AgirvSCmqUKiDE3C0rl3JA68OkOqQWDKUvjqRHXCkhxldLVOVoeIl87+jBYK/v9gcmk+K+ju+5wbGEfu1FjhiQ==", + "version": "18.2.12", + "resolved": "https://registry.npmjs.org/@angular/build/-/build-18.2.12.tgz", + "integrity": "sha512-4Ohz+OSILoL+cCAQ4UTiCT5v6pctu3fXNoNpTEUK46OmxELk9jDITO5rNyNS7TxBn9wY69kjX5VcDf7MenquFQ==", "dev": true, "license": "MIT", "dependencies": { "@ampproject/remapping": "2.3.0", - "@angular-devkit/architect": "0.1802.11", + "@angular-devkit/architect": "0.1802.12", "@babel/core": "7.25.2", "@babel/helper-annotate-as-pure": "7.24.7", "@babel/helper-split-export-declaration": "7.24.7", @@ -639,6 +646,13 @@ } } }, + "node_modules/@angular/build/node_modules/immutable": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.7.tgz", + "integrity": "sha512-1hqclzwYwjRDFLjcFxOM5AYkkG0rpFPpr1RLPMEuGczoS7YA8gLhy8SWXYRAA/XwfEHpfo3cw5JGioS32fnMRw==", + "dev": true, + "license": "MIT" + }, "node_modules/@angular/build/node_modules/sass": { "version": "1.77.6", "resolved": "https://registry.npmjs.org/sass/-/sass-1.77.6.tgz", @@ -658,9 +672,9 @@ } }, "node_modules/@angular/cdk": { - "version": "18.2.12", - "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-18.2.12.tgz", - "integrity": "sha512-FOklA6KatPtb0yO0doRhBI/UVY23A8ZhOSws5VuZTQl/6r/jXEXGV9n5JQj4rm8t/6IrReO55hdyw9XfHfZFjQ==", + "version": "18.2.13", + "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-18.2.13.tgz", + "integrity": "sha512-yBKoqcOwmwXnc5phFMEEMO130/Bz9beQLJrKzIS87f6TXaGCeBs4xrPHq2i7Xx/2TqvMiOD9ucjmlVbtGvNG3w==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -675,18 +689,18 @@ } }, "node_modules/@angular/cli": { - "version": "18.2.11", - "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-18.2.11.tgz", - "integrity": "sha512-0JI1xjOLRemBPjdT/yVlabxc3Zkjqa/lhvVxxVC1XhKoW7yGxIGwNrQ4pka4CcQtCuktO6KPMmTGIu8YgC3cpw==", + "version": "18.2.12", + "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-18.2.12.tgz", + "integrity": "sha512-xhuZ/b7IhqNw1MgXf+arWf4x+GfUSt/IwbdWU4+CO8A7h0Y46zQywouP/KUK3cMQZfVdHdciTBvlpF3vFacA6Q==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/architect": "0.1802.11", - "@angular-devkit/core": "18.2.11", - "@angular-devkit/schematics": "18.2.11", + "@angular-devkit/architect": "0.1802.12", + "@angular-devkit/core": "18.2.12", + "@angular-devkit/schematics": "18.2.12", "@inquirer/prompts": "5.3.8", "@listr2/prompt-adapter-inquirer": "2.0.15", - "@schematics/angular": "18.2.11", + "@schematics/angular": "18.2.12", "@yarnpkg/lockfile": "1.1.0", "ini": "4.1.3", "jsonc-parser": "3.3.1", @@ -709,9 +723,9 @@ } }, "node_modules/@angular/common": { - "version": "18.2.11", - "resolved": "https://registry.npmjs.org/@angular/common/-/common-18.2.11.tgz", - "integrity": "sha512-bamJeISl2zUlvjPYebQWazUjhjXU9nrot42cQJng94SkvNENT9LTWfPYgc+Bd972Kg+31jG4H41rgFNs7zySmw==", + "version": "18.2.12", + "resolved": "https://registry.npmjs.org/@angular/common/-/common-18.2.12.tgz", + "integrity": "sha512-gI5o8Bccsi8ow8Wk2vG4Tw/Rw9LoHEA9j8+qHKNR/55SCBsz68Syg310dSyxy+sApJO2WiqIadr5VP36dlSUFw==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -720,14 +734,14 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/core": "18.2.11", + "@angular/core": "18.2.12", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@angular/compiler": { - "version": "18.2.11", - "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-18.2.11.tgz", - "integrity": "sha512-PSVL1YXUhTzkgJNYXiWk9eAZxNV6laQJRGdj9++C1q9m2S9/GlehZGzkt5GtC5rlUweJucCNvBC1+2D5FAt9vA==", + "version": "18.2.12", + "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-18.2.12.tgz", + "integrity": "sha512-D5d5dLrjQal5DbAXJJNSsCC3UxzjOI2wbc+Iv+LOpRM1gpNwuYfZMX5W7cj62Ce4G2++78CJSppdKBp8D4HErQ==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -736,7 +750,7 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/core": "18.2.11" + "@angular/core": "18.2.12" }, "peerDependenciesMeta": { "@angular/core": { @@ -745,9 +759,9 @@ } }, "node_modules/@angular/compiler-cli": { - "version": "18.2.11", - "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-18.2.11.tgz", - "integrity": "sha512-YJlAOiXZUYP6/RK9isu5AOucmNZhFB9lpY/beMzkkWgDku+va8szm4BZbLJFz176IUteyLWF3IP4aE7P9OBlXw==", + "version": "18.2.12", + "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-18.2.12.tgz", + "integrity": "sha512-IWimTNq5Q+i2Wxev6HLqnN4iYbPvLz04W1BBycT1LfGUsHcjFYLuUqbeUzHbk2snmBAzXkixgVpo8SF6P4Y5Pg==", "license": "MIT", "dependencies": { "@babel/core": "7.25.2", @@ -768,7 +782,7 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/compiler": "18.2.11", + "@angular/compiler": "18.2.12", "typescript": ">=5.4 <5.6" } }, @@ -801,9 +815,9 @@ } }, "node_modules/@angular/core": { - "version": "18.2.11", - "resolved": "https://registry.npmjs.org/@angular/core/-/core-18.2.11.tgz", - "integrity": "sha512-/AGAFyZN8KR+kW5FUFCCBCj3qHyDDum7G0lJe5otrT9AqF6+g7PjF8yLha/6wPkJG7ri5xGLhini1sEivVeq/g==", + "version": "18.2.12", + "resolved": "https://registry.npmjs.org/@angular/core/-/core-18.2.12.tgz", + "integrity": "sha512-wCf/OObwS6bpM60rk6bpMpCRGp0DlMLB1WNAMtfcaPNyqimVV5Bm98mWRhkOuRyvU3fU7iHhM/10ePVaoyu9+A==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -817,9 +831,9 @@ } }, "node_modules/@angular/forms": { - "version": "18.2.11", - "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-18.2.11.tgz", - "integrity": "sha512-QjxayOxDTqsTJGBzfWd3nms1LZIXj2f1+wIPxxUNXyNS5ZaM7hBWkz2BTFYeewlD/HdNj0alNVCYK3M8ElLWYw==", + "version": "18.2.12", + "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-18.2.12.tgz", + "integrity": "sha512-FsukBJEU6jfAmht7TrODTkct/o4iwCZvGozuThOp0tYUPD/E1rZZzuKjEyTnT5Azpfkf0Wqx1nmpz80cczELOQ==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -828,16 +842,16 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/common": "18.2.11", - "@angular/core": "18.2.11", - "@angular/platform-browser": "18.2.11", + "@angular/common": "18.2.12", + "@angular/core": "18.2.12", + "@angular/platform-browser": "18.2.12", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@angular/language-service": { - "version": "18.2.11", - "resolved": "https://registry.npmjs.org/@angular/language-service/-/language-service-18.2.11.tgz", - "integrity": "sha512-kI36Wfvw3E01Xox/H535/rrSTiDfzQeXATFR5i5vqc94XWUdQG67e4X6ybnqFUrezXoLPTULHp+5Di896YFPzw==", + "version": "18.2.12", + "resolved": "https://registry.npmjs.org/@angular/language-service/-/language-service-18.2.12.tgz", + "integrity": "sha512-oaiVAnGzmPZvrXdGh8XnosaqfEPbZxO2225MxbbrD49XTqUgpaS2zrz1Uf5j42e8qytA2kj8tckLq7PAMm0D1w==", "dev": true, "license": "MIT", "engines": { @@ -845,9 +859,9 @@ } }, "node_modules/@angular/localize": { - "version": "18.2.11", - "resolved": "https://registry.npmjs.org/@angular/localize/-/localize-18.2.11.tgz", - "integrity": "sha512-ZGemNURZmhZcZhc0i4SzAjyckkvf6Xv24U7DDJ/TpgHQWP9/pu5QExFa2OuGoJJcZRqUrzEmPrbu+4a/xggaQw==", + "version": "18.2.12", + "resolved": "https://registry.npmjs.org/@angular/localize/-/localize-18.2.12.tgz", + "integrity": "sha512-qC3cYFh3miR9revmHGlfbGvugcsK6nQud4QKBNyTUp1XZRrEE0yzPvvsnmbv2lHUOazrvTxQpfVZZKpiifgoLw==", "license": "MIT", "dependencies": { "@babel/core": "7.25.2", @@ -864,21 +878,21 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/compiler": "18.2.11", - "@angular/compiler-cli": "18.2.11" + "@angular/compiler": "18.2.12", + "@angular/compiler-cli": "18.2.12" } }, "node_modules/@angular/material": { - "version": "18.2.12", - "resolved": "https://registry.npmjs.org/@angular/material/-/material-18.2.12.tgz", - "integrity": "sha512-5q8Os6i3D1e3qN+RqP95UgIR+Kx3goncSSYDeT6yPNrdrcqcWdyDPXGK6UsZqTTx/CJee/I7ZxgVVK1YDoVASQ==", + "version": "18.2.13", + "resolved": "https://registry.npmjs.org/@angular/material/-/material-18.2.13.tgz", + "integrity": "sha512-Gxyyo6G+IXJwgf6zDTjPfFJ2PnjC2YXWKGkKKG2oR0jfiYiovDvNR4oXxhsztTwkaxLwck/gscoVTSQXMkU5fg==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" }, "peerDependencies": { "@angular/animations": "^18.0.0 || ^19.0.0", - "@angular/cdk": "18.2.12", + "@angular/cdk": "18.2.13", "@angular/common": "^18.0.0 || ^19.0.0", "@angular/core": "^18.0.0 || ^19.0.0", "@angular/forms": "^18.0.0 || ^19.0.0", @@ -887,9 +901,9 @@ } }, "node_modules/@angular/platform-browser": { - "version": "18.2.11", - "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-18.2.11.tgz", - "integrity": "sha512-bzcP0QdPT/ncTxOx0t7901z5m0wDmkraTo/es4g8reV6VK9Ptv0QDuD8aDvrHh7sLCX5VgwDF9ohc6S2TpYUCA==", + "version": "18.2.12", + "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-18.2.12.tgz", + "integrity": "sha512-DRSMznuxuecrs+v5BRyd60/R4vjkQtuYUEPfzdo+rqxM83Dmr3PGtnqPRgd5oAFUbATxf02hQXijRD27K7rZRg==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -898,9 +912,9 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/animations": "18.2.11", - "@angular/common": "18.2.11", - "@angular/core": "18.2.11" + "@angular/animations": "18.2.12", + "@angular/common": "18.2.12", + "@angular/core": "18.2.12" }, "peerDependenciesMeta": { "@angular/animations": { @@ -909,9 +923,9 @@ } }, "node_modules/@angular/platform-browser-dynamic": { - "version": "18.2.11", - "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-18.2.11.tgz", - "integrity": "sha512-a30U4ZdTZSvL17xWwOq6xh9ToCDP2K7/j1HTJFREObbuAtZTa/6IVgBUM6oOMNQ43kHkT6Mr9Emkgf9iGtWwfw==", + "version": "18.2.12", + "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-18.2.12.tgz", + "integrity": "sha512-dv1QEjYpcFno6+oUeGEDRWpB5g2Ufb0XkUbLJQIgrOk1Qbyzb8tmpDpTjok8jcKdquigMRWolr6Y1EOicfRlLw==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -920,16 +934,16 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/common": "18.2.11", - "@angular/compiler": "18.2.11", - "@angular/core": "18.2.11", - "@angular/platform-browser": "18.2.11" + "@angular/common": "18.2.12", + "@angular/compiler": "18.2.12", + "@angular/core": "18.2.12", + "@angular/platform-browser": "18.2.12" } }, "node_modules/@angular/router": { - "version": "18.2.11", - "resolved": "https://registry.npmjs.org/@angular/router/-/router-18.2.11.tgz", - "integrity": "sha512-xh4+t4pNBWxeH1a6GIoEGVSRZO4NDKK8q6b+AzB5GBgKsYgOz2lc74RXIPA//pK3aHrS9qD4sJLlodwgE/1+bA==", + "version": "18.2.12", + "resolved": "https://registry.npmjs.org/@angular/router/-/router-18.2.12.tgz", + "integrity": "sha512-cz/1YWOZadAT35PPPYmpK3HSzKOE56nlUHue5bFkw73VSZr2iBn03ALLpd9YKzWgRmx3y7DqnlQtCkDu9JPGKQ==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -938,16 +952,16 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/common": "18.2.11", - "@angular/core": "18.2.11", - "@angular/platform-browser": "18.2.11", + "@angular/common": "18.2.12", + "@angular/core": "18.2.12", + "@angular/platform-browser": "18.2.12", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@angular/service-worker": { - "version": "18.2.11", - "resolved": "https://registry.npmjs.org/@angular/service-worker/-/service-worker-18.2.11.tgz", - "integrity": "sha512-FZ1yHCAmmbg+NYNFtvrZE8RzgsSnWgsL2ef+mvlfC/fxyu4pyoZT4+ZshwN7k55L++6M/RgdV7cZevPN4qGNrA==", + "version": "18.2.12", + "resolved": "https://registry.npmjs.org/@angular/service-worker/-/service-worker-18.2.12.tgz", + "integrity": "sha512-rgztA+Eduo69y6cvSDtAXC5lMTWjgowSSreiyM4ssyjwd8vD6h2TZp/3slr8Tt6+Lh9J4bK+UdcqMIjIdDxwSw==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -959,8 +973,8 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/common": "18.2.11", - "@angular/core": "18.2.11" + "@angular/common": "18.2.12", + "@angular/core": "18.2.12" } }, "node_modules/@babel/code-frame": { @@ -1146,9 +1160,9 @@ } }, "node_modules/@babel/helper-define-polyfill-provider": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.2.tgz", - "integrity": "sha512-LV76g+C502biUK6AyZ3LK10vDpDyCzZnhZFXkH1L75zHPj68+qc8Zfpx2th+gzwA2MzyK+1g/3EPl62yFnVttQ==", + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.3.tgz", + "integrity": "sha512-HK7Bi+Hj6H+VTHA3ZvBis7V/6hu9QuTrnMXNybfUf2iiuU/N97I8VjB+KbhFF8Rld/Lx5MzoCwPCpPjfK+n8Cg==", "dev": true, "license": "MIT", "dependencies": { @@ -3567,9 +3581,9 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.2.tgz", - "integrity": "sha512-CXtq5nR4Su+2I47WPOlWud98Y5Lv8Kyxp2ukhgFx/eW6Blm18VXJO5WuQylPugRo8nbluoi6GvvxBLqHcvqUUw==", + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.3.tgz", + "integrity": "sha512-2b/g5hRmpbb1o4GnTZax9N9m0FXzz9OV42ZzI4rDDMDuHUqigAiQCEWChBWCY4ztAGVRjoWT19v0yMmc5/L5kA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -3820,9 +3834,9 @@ } }, "node_modules/@inquirer/figures": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.7.tgz", - "integrity": "sha512-m+Trk77mp54Zma6xLkLuY+mvanPxlE4A7yNKs2HBiyZ4UkVs28Mv5c/pgWrHeInx+USHeX/WEPzjrWrcJiQgjw==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.8.tgz", + "integrity": "sha512-tKd+jsmhq21AP1LhexC0pPwsCxEhGgAkg28byjJAd+xhmIs8LUX8JbUc3vBf3PhLxWiB5EvyBE5X7JSPAqMAqg==", "dev": true, "license": "MIT", "engines": { @@ -5024,9 +5038,9 @@ } }, "node_modules/@ngtools/webpack": { - "version": "18.2.11", - "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-18.2.11.tgz", - "integrity": "sha512-iTdUGJ5O7yMm1DyCzyoMDMxBJ68emUSSXPWbQzEEdcqmtifRebn+VAq4vHN8OmtGM1mtuKeLEsbiZP8ywrw7Ug==", + "version": "18.2.12", + "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-18.2.12.tgz", + "integrity": "sha512-FFJAwtWbtpncMOVNuULPBwFJB7GSjiUwO93eGTzRp8O4EPQ8lCQeFbezQm/NP34+T0+GBLGzPSuQT+muob8YKw==", "dev": true, "license": "MIT", "engines": { @@ -5353,9 +5367,9 @@ } }, "node_modules/@nx/devkit": { - "version": "20.0.12", - "resolved": "https://registry.npmjs.org/@nx/devkit/-/devkit-20.0.12.tgz", - "integrity": "sha512-HsaDoAmzLPE2vHal2eNYvH7x6NCfHjUblm8WDD12Q/uCdTBvDTZqd7P+bukEH+2FhY89Dn/1fy59vKkA+rcB/g==", + "version": "20.1.1", + "resolved": "https://registry.npmjs.org/@nx/devkit/-/devkit-20.1.1.tgz", + "integrity": "sha512-sqihJhJQERCTl0KmKmpRFxWxuTnH8yRqdo8T5uGGaHzTNiMdIp5smTF2dBs7/OMkZDxcJc4dKvcFWfreZr8XNw==", "dev": true, "license": "MIT", "dependencies": { @@ -5399,9 +5413,9 @@ } }, "node_modules/@nx/nx-darwin-arm64": { - "version": "20.0.12", - "resolved": "https://registry.npmjs.org/@nx/nx-darwin-arm64/-/nx-darwin-arm64-20.0.12.tgz", - "integrity": "sha512-iwEDUTKx0n2S6Nz9gc9ShrfBw0MG87U0YIu2x/09tKOSkcsw90QKy54qN/6WNoFIE41Kt3U+dYtWi+NdLRE9kw==", + "version": "20.1.1", + "resolved": "https://registry.npmjs.org/@nx/nx-darwin-arm64/-/nx-darwin-arm64-20.1.1.tgz", + "integrity": "sha512-Ah0ShPQaMfvzVfhsyuI6hNB0bmwLHJqqrWldZeF97SFPhv6vfKdcdlZmSnask+V4N5z9TOCUmCMu2asMQa7+kw==", "cpu": [ "arm64" ], @@ -5416,9 +5430,9 @@ } }, "node_modules/@nx/nx-darwin-x64": { - "version": "20.0.12", - "resolved": "https://registry.npmjs.org/@nx/nx-darwin-x64/-/nx-darwin-x64-20.0.12.tgz", - "integrity": "sha512-JYFNf0yPReejaooQAAIMsjWDGENT777wDXj45e7JQUMM4t6NOMpGBj4qUFyc6a/jXT+/bCGEj4N7VDZDZiogGA==", + "version": "20.1.1", + "resolved": "https://registry.npmjs.org/@nx/nx-darwin-x64/-/nx-darwin-x64-20.1.1.tgz", + "integrity": "sha512-TmdX6pbzclvPGsttTTaZhdF46HV1vfvYSHJaSMsYJX68l3gcQnAJ1ZRDksEgkYeAy+O9KrPimD84NM5W/JvqcQ==", "cpu": [ "x64" ], @@ -5433,9 +5447,9 @@ } }, "node_modules/@nx/nx-freebsd-x64": { - "version": "20.0.12", - "resolved": "https://registry.npmjs.org/@nx/nx-freebsd-x64/-/nx-freebsd-x64-20.0.12.tgz", - "integrity": "sha512-892n8o7vxdmE7pol3ggV78YHlP25p6Y/Z2x69nnC3BBTpWmesyd6lbEmamANofD5KcKCmT1HquC3m6rCT7akHw==", + "version": "20.1.1", + "resolved": "https://registry.npmjs.org/@nx/nx-freebsd-x64/-/nx-freebsd-x64-20.1.1.tgz", + "integrity": "sha512-7/7f3GbUbdvtTFOb/8wcaSQYkhVIxcC4UzFJM5yEyXPJmIrglk+RX3SLuOFRBFJnO+Z7D6jLUnLOBHKCGfqLVw==", "cpu": [ "x64" ], @@ -5450,9 +5464,9 @@ } }, "node_modules/@nx/nx-linux-arm-gnueabihf": { - "version": "20.0.12", - "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm-gnueabihf/-/nx-linux-arm-gnueabihf-20.0.12.tgz", - "integrity": "sha512-ZPcdYIVAc5JMtmvroJOloI9CJgtwBOGr7E7mO1eT44zs5av0j/QMIj6GSDdvJ7fx+I7TmT4mDiu3s6rLO+/JjA==", + "version": "20.1.1", + "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm-gnueabihf/-/nx-linux-arm-gnueabihf-20.1.1.tgz", + "integrity": "sha512-VxpMz5jCZ5gnk1gP2jDBCheYs7qOwQoJmzGbEB8hNy0CwRH/G8pL4RRo4Sz+4aiF6Z+9eax5RM2/Syh+bS0uJw==", "cpu": [ "arm" ], @@ -5467,9 +5481,9 @@ } }, "node_modules/@nx/nx-linux-arm64-gnu": { - "version": "20.0.12", - "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm64-gnu/-/nx-linux-arm64-gnu-20.0.12.tgz", - "integrity": "sha512-TadGwwUKS5WQg2YOMb2WuuVG1k14miSdB9qJOcAX5MGdOiQ1fpV00ph+kMWZSsCCo6N7sKxmvXXXdsUUFSDGjg==", + "version": "20.1.1", + "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm64-gnu/-/nx-linux-arm64-gnu-20.1.1.tgz", + "integrity": "sha512-8T2+j4KvsWb6ljW1Y2s/uCSt4Drtlsr3GSrGdvcETW0IKaTfKZAJlxTLAWQHEF88hP6GAJRGxNrgmUHMr8HwUA==", "cpu": [ "arm64" ], @@ -5484,9 +5498,9 @@ } }, "node_modules/@nx/nx-linux-arm64-musl": { - "version": "20.0.12", - "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm64-musl/-/nx-linux-arm64-musl-20.0.12.tgz", - "integrity": "sha512-EE2HQjgY87/s9+PQ27vbYyDEXFZ4Qot+O8ThVDVuMI/2dosmWs6C4+YEm3VYG+CT31MVwe/vHKXbDlZgkROMuA==", + "version": "20.1.1", + "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm64-musl/-/nx-linux-arm64-musl-20.1.1.tgz", + "integrity": "sha512-TI964w+HFUqG6elriKwQPRX7QRxVRMz5YKdNPgf4+ab4epQ379kwJQEHlyOHR72ir8Tl46z3BoPjvmaLylrT4Q==", "cpu": [ "arm64" ], @@ -5501,9 +5515,9 @@ } }, "node_modules/@nx/nx-linux-x64-gnu": { - "version": "20.0.12", - "resolved": "https://registry.npmjs.org/@nx/nx-linux-x64-gnu/-/nx-linux-x64-gnu-20.0.12.tgz", - "integrity": "sha512-gITJ2g6dH2qvGrI2CHHRyd3soVrJyQQGkqtJnWq04ge+YDy/KniXR2ThQ93LI/QLAxKrKOe3qmIIaNdcdDYnjA==", + "version": "20.1.1", + "resolved": "https://registry.npmjs.org/@nx/nx-linux-x64-gnu/-/nx-linux-x64-gnu-20.1.1.tgz", + "integrity": "sha512-Sg2tQ0v3KP9cAqQST16YR+dT/NbirPts6by+A4vhOtaBrZFVqm9P89K9UdcJf4Aj1CaGbs84lotp2aM4E4bQPA==", "cpu": [ "x64" ], @@ -5518,9 +5532,9 @@ } }, "node_modules/@nx/nx-linux-x64-musl": { - "version": "20.0.12", - "resolved": "https://registry.npmjs.org/@nx/nx-linux-x64-musl/-/nx-linux-x64-musl-20.0.12.tgz", - "integrity": "sha512-vOoCrjL44nFZ5N8a4UAIYELnf/tq1dRaLEhSV+P0hKTEtwONj4k8crfU/2HifG1iU7p3AWJLEyaddMoINhB/2g==", + "version": "20.1.1", + "resolved": "https://registry.npmjs.org/@nx/nx-linux-x64-musl/-/nx-linux-x64-musl-20.1.1.tgz", + "integrity": "sha512-ekKvuIMRJRhZnkWIWEr4TRVEAyKVDgEMwqk83ilB0Mqpj2RoOKbw7jZFvWcxJWI4kSeZjTea3xCWGNPa1GfCww==", "cpu": [ "x64" ], @@ -5535,9 +5549,9 @@ } }, "node_modules/@nx/nx-win32-arm64-msvc": { - "version": "20.0.12", - "resolved": "https://registry.npmjs.org/@nx/nx-win32-arm64-msvc/-/nx-win32-arm64-msvc-20.0.12.tgz", - "integrity": "sha512-gKdaul23bdRnh493iAd6pSLPSW54VBuEv2zPL86cgprLOcEZiGM5BLJWQguKHCib6dYKaIP4CUIs7i7vhEID+A==", + "version": "20.1.1", + "resolved": "https://registry.npmjs.org/@nx/nx-win32-arm64-msvc/-/nx-win32-arm64-msvc-20.1.1.tgz", + "integrity": "sha512-JRycFkk6U8A1sXaDmSFA2HMKT2js3HK/+nI+auyITRqVbV79/r6ir/oFSgIjKth8j/vVbGDL8I4E3nEQ7leZYw==", "cpu": [ "arm64" ], @@ -5552,9 +5566,9 @@ } }, "node_modules/@nx/nx-win32-x64-msvc": { - "version": "20.0.12", - "resolved": "https://registry.npmjs.org/@nx/nx-win32-x64-msvc/-/nx-win32-x64-msvc-20.0.12.tgz", - "integrity": "sha512-R1pz4kAG0Ok0EDxXhHwKM3ZZcK2nLycuR9SDrq2Ldp2knvbFf4quSjWyAQaiofJXo179+noa7o5tZDZbNjBYMw==", + "version": "20.1.1", + "resolved": "https://registry.npmjs.org/@nx/nx-win32-x64-msvc/-/nx-win32-x64-msvc-20.1.1.tgz", + "integrity": "sha512-VwxmJU7o8KqTZ+KYk7atoWOUykKd8D4hdgKqqltdq/UBfsAWD/JCFt5OB/VFvrGDbK6I6iKpMvXWlHy4gkXQiw==", "cpu": [ "x64" ], @@ -5904,6 +5918,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/@pdf-lib/standard-fonts/-/standard-fonts-1.0.0.tgz", "integrity": "sha512-hU30BK9IUN/su0Mn9VdlVKsWBS6GyhVfqjwl1FjZN4TxP6cCw0jP2w7V3Hf5uX7M0AZJ16vey9yE0ny7Sa59ZA==", + "license": "MIT", "dependencies": { "pako": "^1.0.6" } @@ -5912,6 +5927,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/@pdf-lib/upng/-/upng-1.0.1.tgz", "integrity": "sha512-dQK2FUMQtowVP00mtIksrlZhdFXQZPC+taih1q4CvPZ5vqdxR/LKBaFg0oAfzd1GlHZXXSPdQfzQnt+ViGvEIQ==", + "license": "MIT", "dependencies": { "pako": "^1.0.10" } @@ -6232,14 +6248,14 @@ ] }, "node_modules/@schematics/angular": { - "version": "18.2.11", - "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-18.2.11.tgz", - "integrity": "sha512-jT54mc9+hPOwie9bji/g2krVuK1kkNh2PNFGwfgCg3Ofmt3hcyOBai1DKuot5uLTX4VCCbvfwiVR/hJniQl2SA==", + "version": "18.2.12", + "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-18.2.12.tgz", + "integrity": "sha512-sIoeipsisK5eTLW3XuNZYcal83AfslBbgI7LnV+3VrXwpasKPGHwo2ZdwhCd2IXAkuJ02Iyu7MyV0aQRM9i/3g==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/core": "18.2.11", - "@angular-devkit/schematics": "18.2.11", + "@angular-devkit/core": "18.2.12", + "@angular-devkit/schematics": "18.2.12", "jsonc-parser": "3.3.1" }, "engines": { @@ -6249,73 +6265,73 @@ } }, "node_modules/@sentry-internal/browser-utils": { - "version": "8.37.1", - "resolved": "https://registry.npmjs.org/@sentry-internal/browser-utils/-/browser-utils-8.37.1.tgz", - "integrity": "sha512-OSR/V5GCsSCG7iapWtXCT/y22uo3HlawdEgfM1NIKk1mkP15UyGQtGEzZDdih2H+SNuX1mp9jQLTjr5FFp1A5w==", + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/browser-utils/-/browser-utils-8.38.0.tgz", + "integrity": "sha512-5QMVcssrAcmjKT0NdFYcX0b0wwZovGAZ9L2GajErXtHkBenjI2sgR2+5J7n+QZGuk2SC1qhGmT1O9i3p3UEwew==", "license": "MIT", "dependencies": { - "@sentry/core": "8.37.1", - "@sentry/types": "8.37.1", - "@sentry/utils": "8.37.1" + "@sentry/core": "8.38.0", + "@sentry/types": "8.38.0", + "@sentry/utils": "8.38.0" }, "engines": { "node": ">=14.18" } }, "node_modules/@sentry-internal/feedback": { - "version": "8.37.1", - "resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-8.37.1.tgz", - "integrity": "sha512-Se25NXbSapgS2S+JssR5YZ48b3OY4UGmAuBOafgnMW91LXMxRNWRbehZuNUmjjHwuywABMxjgu+Yp5uJDATX+g==", + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-8.38.0.tgz", + "integrity": "sha512-AW5HCCAlc3T1jcSuNhbFVNO1CHyJ5g5tsGKEP4VKgu+D1Gg2kZ5S2eFatLBUP/BD5JYb1A7p6XPuzYp1XfMq0A==", "license": "MIT", "dependencies": { - "@sentry/core": "8.37.1", - "@sentry/types": "8.37.1", - "@sentry/utils": "8.37.1" + "@sentry/core": "8.38.0", + "@sentry/types": "8.38.0", + "@sentry/utils": "8.38.0" }, "engines": { "node": ">=14.18" } }, "node_modules/@sentry-internal/replay": { - "version": "8.37.1", - "resolved": "https://registry.npmjs.org/@sentry-internal/replay/-/replay-8.37.1.tgz", - "integrity": "sha512-E/Plhisk/pXJjOdOU12sg8m/APTXTA21iEniidP6jW3/+O0tD/H/UovEqa4odNTqxPMa798xHQSQNt5loYiaLA==", + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/replay/-/replay-8.38.0.tgz", + "integrity": "sha512-mQPShKnIab7oKwkwrRxP/D8fZYHSkDY+cvqORzgi+wAwgnunytJQjz9g6Ww2lJu98rHEkr5SH4V4rs6PZYZmnQ==", "license": "MIT", "dependencies": { - "@sentry-internal/browser-utils": "8.37.1", - "@sentry/core": "8.37.1", - "@sentry/types": "8.37.1", - "@sentry/utils": "8.37.1" + "@sentry-internal/browser-utils": "8.38.0", + "@sentry/core": "8.38.0", + "@sentry/types": "8.38.0", + "@sentry/utils": "8.38.0" }, "engines": { "node": ">=14.18" } }, "node_modules/@sentry-internal/replay-canvas": { - "version": "8.37.1", - "resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-8.37.1.tgz", - "integrity": "sha512-1JLAaPtn1VL5vblB0BMELFV0D+KUm/iMGsrl4/JpRm0Ws5ESzQl33DhXVv1IX/ZAbx9i14EjR7MG9+Hj70tieQ==", + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-8.38.0.tgz", + "integrity": "sha512-OxmlWzK9J8mRM+KxdSnQ5xuxq+p7TiBzTz70FT3HltxmeugvDkyp6803UcFqHOPHR35OYeVLOalym+FmvNn9kw==", "license": "MIT", "dependencies": { - "@sentry-internal/replay": "8.37.1", - "@sentry/core": "8.37.1", - "@sentry/types": "8.37.1", - "@sentry/utils": "8.37.1" + "@sentry-internal/replay": "8.38.0", + "@sentry/core": "8.38.0", + "@sentry/types": "8.38.0", + "@sentry/utils": "8.38.0" }, "engines": { "node": ">=14.18" } }, "node_modules/@sentry/angular": { - "version": "8.37.1", - "resolved": "https://registry.npmjs.org/@sentry/angular/-/angular-8.37.1.tgz", - "integrity": "sha512-N6IdxEUwVlB5qqd7UR0fiEvWoJrNA4rcdKot0W9uN3G9lqmff5EB3EUIvw9xFZJgZ695WNVZ1f+irvqXt+rYJA==", + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@sentry/angular/-/angular-8.38.0.tgz", + "integrity": "sha512-FBeokQllQwFArdtQ8OMIHatIa1MOj3nJEQjHCuuUgK4ys0vpX/ithPuHU1lEpd1qkUGUnHYHyjjQW6QLY3whwg==", "license": "MIT", "dependencies": { - "@sentry/browser": "8.37.1", - "@sentry/core": "8.37.1", - "@sentry/types": "8.37.1", - "@sentry/utils": "8.37.1", + "@sentry/browser": "8.38.0", + "@sentry/core": "8.38.0", + "@sentry/types": "8.38.0", + "@sentry/utils": "8.38.0", "tslib": "^2.4.1" }, "engines": { @@ -6329,52 +6345,52 @@ } }, "node_modules/@sentry/browser": { - "version": "8.37.1", - "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-8.37.1.tgz", - "integrity": "sha512-5ym+iGiIpjIKKpMWi9S3/tXh9xneS+jqxwRTJqed3cb8i4ydfMAAP8sM3U8xMCWWABpWyIUW+fpewC0tkhE1aQ==", + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-8.38.0.tgz", + "integrity": "sha512-AZR+b0EteNZEGv6JSdBD22S9VhQ7nrljKsSnzxobBULf3BpwmhmCzTbDrqWszKDAIDYmL+yQJIR2glxbknneWQ==", "license": "MIT", "dependencies": { - "@sentry-internal/browser-utils": "8.37.1", - "@sentry-internal/feedback": "8.37.1", - "@sentry-internal/replay": "8.37.1", - "@sentry-internal/replay-canvas": "8.37.1", - "@sentry/core": "8.37.1", - "@sentry/types": "8.37.1", - "@sentry/utils": "8.37.1" + "@sentry-internal/browser-utils": "8.38.0", + "@sentry-internal/feedback": "8.38.0", + "@sentry-internal/replay": "8.38.0", + "@sentry-internal/replay-canvas": "8.38.0", + "@sentry/core": "8.38.0", + "@sentry/types": "8.38.0", + "@sentry/utils": "8.38.0" }, "engines": { "node": ">=14.18" } }, "node_modules/@sentry/core": { - "version": "8.37.1", - "resolved": "https://registry.npmjs.org/@sentry/core/-/core-8.37.1.tgz", - "integrity": "sha512-82csXby589iDupM3VgCHJeWZagUyEEaDnbFcoZ/Z91QX2Sjq8FcF5OsforoXjw09i0XTFqlkFAnQVpDBmMXcpQ==", + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-8.38.0.tgz", + "integrity": "sha512-sGD+5TEHU9G7X7zpyaoJxpOtwjTjvOd1f/MKBrWW2vf9UbYK+GUJrOzLhMoSWp/pHSYgvObkJkDb/HwieQjvhQ==", "license": "MIT", "dependencies": { - "@sentry/types": "8.37.1", - "@sentry/utils": "8.37.1" + "@sentry/types": "8.38.0", + "@sentry/utils": "8.38.0" }, "engines": { "node": ">=14.18" } }, "node_modules/@sentry/types": { - "version": "8.37.1", - "resolved": "https://registry.npmjs.org/@sentry/types/-/types-8.37.1.tgz", - "integrity": "sha512-ryMOTROLSLINKFEbHWvi7GigNrsQhsaScw2NddybJGztJQ5UhxIGESnxGxWCufBmWFDwd7+5u0jDPCVUJybp7w==", + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@sentry/types/-/types-8.38.0.tgz", + "integrity": "sha512-fP5H9ZX01W4Z/EYctk3mkSHi7d06cLcX2/UWqwdWbyPWI+pL2QpUPICeO/C+8SnmYx//wFj3qWDhyPCh1PdFAA==", "license": "MIT", "engines": { "node": ">=14.18" } }, "node_modules/@sentry/utils": { - "version": "8.37.1", - "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-8.37.1.tgz", - "integrity": "sha512-Qtn2IfpII12K17txG/ZtTci35XYjYi4CxbQ3j7nXY7toGv/+MqPXwV5q2i9g94XaSXlE5Wy9/hoCZoZpZs/djA==", + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-8.38.0.tgz", + "integrity": "sha512-3X7MgIKIx+2q5Al7QkhaRB4wV6DvzYsaeIwdqKUzGLuRjXmNgJrLoU87TAwQRmZ6Wr3IoEpThZZMNrzYPXxArw==", "license": "MIT", "dependencies": { - "@sentry/types": "8.37.1" + "@sentry/types": "8.38.0" }, "engines": { "node": ">=14.18" @@ -6516,36 +6532,47 @@ } }, "node_modules/@swimlane/ngx-charts": { - "version": "20.5.0", - "resolved": "https://registry.npmjs.org/@swimlane/ngx-charts/-/ngx-charts-20.5.0.tgz", - "integrity": "sha512-PNBIHdu/R3ceD7jnw1uCBVOj4k3T6IxfdW6xsDsglGkZyoWMEEq4tLoEurjLEKzmDtRv9c35kVNOXy0lkOuXeA==", + "version": "21.0.0", + "resolved": "https://registry.npmjs.org/@swimlane/ngx-charts/-/ngx-charts-21.0.0.tgz", + "integrity": "sha512-4YQNWevbVPekiuLz6w3wLdJY9rD2Pk21xskTUtfpUirUFXdkKZdUByJkSUlup+F8UPvkeZIEC5bhBtOr0yTktA==", "license": "MIT", "dependencies": { - "d3-array": "^3.1.1", + "d3-array": "^3.2.0", "d3-brush": "^3.0.0", "d3-color": "^3.1.0", "d3-ease": "^3.0.1", "d3-format": "^3.1.0", - "d3-hierarchy": "^3.1.0", + "d3-hierarchy": "^3.1.2", "d3-interpolate": "^3.0.1", "d3-sankey": "^0.12.3", "d3-scale": "^4.0.2", "d3-selection": "^3.0.0", "d3-shape": "^3.2.0", - "d3-time-format": "^3.0.0", + "d3-time-format": "^4.1.0", "d3-transition": "^3.0.1", - "rfdc": "^1.3.0", - "tslib": "^2.0.0" + "tslib": "^2.3.1" }, "peerDependencies": { - "@angular/animations": ">=12.0.0", - "@angular/cdk": ">=12.0.0", - "@angular/common": ">=12.0.0", - "@angular/core": ">=12.0.0", - "@angular/forms": ">=12.0.0", - "@angular/platform-browser": ">=12.0.0", - "@angular/platform-browser-dynamic": ">=12.0.0", - "rxjs": "^6.5.3 || ^7.4.0" + "@angular/animations": "17.x || 18.x", + "@angular/cdk": "17.x || 18.x", + "@angular/common": "17.x || 18.x", + "@angular/core": "17.x || 18.x", + "@angular/forms": "17.x || 18.x", + "@angular/platform-browser": "17.x || 18.x", + "@angular/platform-browser-dynamic": "17.x || 18.x", + "rxjs": "7.x" + } + }, + "node_modules/@swimlane/ngx-charts/node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" } }, "node_modules/@swimlane/ngx-graph": { @@ -6584,12 +6611,6 @@ "internmap": "^1.0.0" } }, - "node_modules/@swimlane/ngx-graph/node_modules/d3-dispatch": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-1.0.6.tgz", - "integrity": "sha512-fVjoElzjhCEy+Hbn8KygnmMS7Or0a9sI2UzGwoB7cCtvI1XpVN9GpoYlnb3xt2YV66oXYb1fLJ8GMvP4hdU1RA==", - "license": "BSD-3-Clause" - }, "node_modules/@swimlane/ngx-graph/node_modules/d3-ease": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-1.0.7.tgz", @@ -6639,12 +6660,6 @@ "d3-array": "2" } }, - "node_modules/@swimlane/ngx-graph/node_modules/d3-timer": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-1.0.10.tgz", - "integrity": "sha512-B1JDm0XDaQC+uvo4DT79H0XmBskgS3l6Ve+1SBCfxgmtIb1AVrPIoqd+nPSv+loMX8szQ0sVUhGngL7D5QPiXw==", - "license": "BSD-3-Clause" - }, "node_modules/@swimlane/ngx-graph/node_modules/internmap": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/internmap/-/internmap-1.0.1.tgz", @@ -7272,17 +7287,17 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.13.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.13.0.tgz", - "integrity": "sha512-nQtBLiZYMUPkclSeC3id+x4uVd1SGtHuElTxL++SfP47jR0zfkZBJHc+gL4qPsgTuypz0k8Y2GheaDYn6Gy3rg==", + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.14.0.tgz", + "integrity": "sha512-tqp8H7UWFaZj0yNO6bycd5YjMwxa6wIHOLZvWPkidwbgLCsBMetQoGj7DPuAlWa2yGO3H48xmPwjhsSPPCGU5w==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.13.0", - "@typescript-eslint/type-utils": "8.13.0", - "@typescript-eslint/utils": "8.13.0", - "@typescript-eslint/visitor-keys": "8.13.0", + "@typescript-eslint/scope-manager": "8.14.0", + "@typescript-eslint/type-utils": "8.14.0", + "@typescript-eslint/utils": "8.14.0", + "@typescript-eslint/visitor-keys": "8.14.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", @@ -7306,16 +7321,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.13.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.13.0.tgz", - "integrity": "sha512-w0xp+xGg8u/nONcGw1UXAr6cjCPU1w0XVyBs6Zqaj5eLmxkKQAByTdV/uGgNN5tVvN/kKpoQlP2cL7R+ajZZIQ==", + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.14.0.tgz", + "integrity": "sha512-2p82Yn9juUJq0XynBXtFCyrBDb6/dJombnz6vbo6mgQEtWHfvHbQuEa9kAOVIt1c9YFwi7H6WxtPj1kg+80+RA==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "@typescript-eslint/scope-manager": "8.13.0", - "@typescript-eslint/types": "8.13.0", - "@typescript-eslint/typescript-estree": "8.13.0", - "@typescript-eslint/visitor-keys": "8.13.0", + "@typescript-eslint/scope-manager": "8.14.0", + "@typescript-eslint/types": "8.14.0", + "@typescript-eslint/typescript-estree": "8.14.0", + "@typescript-eslint/visitor-keys": "8.14.0", "debug": "^4.3.4" }, "engines": { @@ -7335,14 +7350,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.13.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.13.0.tgz", - "integrity": "sha512-XsGWww0odcUT0gJoBZ1DeulY1+jkaHUciUq4jKNv4cpInbvvrtDoyBH9rE/n2V29wQJPk8iCH1wipra9BhmiMA==", + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.14.0.tgz", + "integrity": "sha512-aBbBrnW9ARIDn92Zbo7rguLnqQ/pOrUguVpbUwzOhkFg2npFDwTgPGqFqE0H5feXcOoJOfX3SxlJaKEVtq54dw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.13.0", - "@typescript-eslint/visitor-keys": "8.13.0" + "@typescript-eslint/types": "8.14.0", + "@typescript-eslint/visitor-keys": "8.14.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -7353,14 +7368,14 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.13.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.13.0.tgz", - "integrity": "sha512-Rqnn6xXTR316fP4D2pohZenJnp+NwQ1mo7/JM+J1LWZENSLkJI8ID8QNtlvFeb0HnFSK94D6q0cnMX6SbE5/vA==", + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.14.0.tgz", + "integrity": "sha512-Xcz9qOtZuGusVOH5Uk07NGs39wrKkf3AxlkK79RBK6aJC1l03CobXjJbwBPSidetAOV+5rEVuiT1VSBUOAsanQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.13.0", - "@typescript-eslint/utils": "8.13.0", + "@typescript-eslint/typescript-estree": "8.14.0", + "@typescript-eslint/utils": "8.14.0", "debug": "^4.3.4", "ts-api-utils": "^1.3.0" }, @@ -7378,9 +7393,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.13.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.13.0.tgz", - "integrity": "sha512-4cyFErJetFLckcThRUFdReWJjVsPCqyBlJTi6IDEpc1GWCIIZRFxVppjWLIMcQhNGhdWJJRYFHpHoDWvMlDzng==", + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.14.0.tgz", + "integrity": "sha512-yjeB9fnO/opvLJFAsPNYlKPnEM8+z4og09Pk504dkqonT02AyL5Z9SSqlE0XqezS93v6CXn49VHvB2G7XSsl0g==", "dev": true, "license": "MIT", "engines": { @@ -7392,14 +7407,14 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.13.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.13.0.tgz", - "integrity": "sha512-v7SCIGmVsRK2Cy/LTLGN22uea6SaUIlpBcO/gnMGT/7zPtxp90bphcGf4fyrCQl3ZtiBKqVTG32hb668oIYy1g==", + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.14.0.tgz", + "integrity": "sha512-OPXPLYKGZi9XS/49rdaCbR5j/S14HazviBlUQFvSKz3npr3NikF+mrgK7CFVur6XEt95DZp/cmke9d5i3vtVnQ==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "@typescript-eslint/types": "8.13.0", - "@typescript-eslint/visitor-keys": "8.13.0", + "@typescript-eslint/types": "8.14.0", + "@typescript-eslint/visitor-keys": "8.14.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -7421,16 +7436,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.13.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.13.0.tgz", - "integrity": "sha512-A1EeYOND6Uv250nybnLZapeXpYMl8tkzYUxqmoKAWnI4sei3ihf2XdZVd+vVOmHGcp3t+P7yRrNsyyiXTvShFQ==", + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.14.0.tgz", + "integrity": "sha512-OGqj6uB8THhrHj0Fk27DcHPojW7zKwKkPmHXHvQ58pLYp4hy8CSUdTKykKeh+5vFqTTVmjz0zCOOPKRovdsgHA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.13.0", - "@typescript-eslint/types": "8.13.0", - "@typescript-eslint/typescript-estree": "8.13.0" + "@typescript-eslint/scope-manager": "8.14.0", + "@typescript-eslint/types": "8.14.0", + "@typescript-eslint/typescript-estree": "8.14.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -7444,13 +7459,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.13.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.13.0.tgz", - "integrity": "sha512-7N/+lztJqH4Mrf0lb10R/CbI1EaAMMGyF5y0oJvFoAhafwgiRA7TXyd8TFn8FC8k5y2dTsYogg238qavRGNnlw==", + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.14.0.tgz", + "integrity": "sha512-vG0XZo8AdTH9OE6VFRwAZldNc7qtJ/6NLGWak+BtENuEUXGZgFpihILPiBvKXvJ2nFu27XNGC6rKiwuaoMbYzQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.13.0", + "@typescript-eslint/types": "8.14.0", "eslint-visitor-keys": "^3.4.3" }, "engines": { @@ -8284,14 +8299,14 @@ } }, "node_modules/babel-plugin-polyfill-corejs2": { - "version": "0.4.11", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.11.tgz", - "integrity": "sha512-sMEJ27L0gRHShOh5G54uAAPaiCOygY/5ratXuiyb2G46FmlSpc9eFCzYVyDiPxfNbwzA7mYahmjQc5q+CZQ09Q==", + "version": "0.4.12", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.12.tgz", + "integrity": "sha512-CPWT6BwvhrTO2d8QVorhTCQw9Y43zOu7G9HigcfxvepOU6b8o3tcWad6oVgZIsZCTt42FFv97aA7ZJsbM4+8og==", "dev": true, "license": "MIT", "dependencies": { "@babel/compat-data": "^7.22.6", - "@babel/helper-define-polyfill-provider": "^0.6.2", + "@babel/helper-define-polyfill-provider": "^0.6.3", "semver": "^6.3.1" }, "peerDependencies": { @@ -8313,13 +8328,13 @@ } }, "node_modules/babel-plugin-polyfill-regenerator": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.2.tgz", - "integrity": "sha512-2R25rQZWP63nGwaAswvDazbPXfrM3HwVoBXK6HcqeKrSrL/JqcC/rDcf95l4r7LXLyxDXc8uQDa064GubtCABg==", + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.3.tgz", + "integrity": "sha512-LiWSbl4CRSIa5x/JAU6jZiG9eit9w6mz+yVMFwDE83LAWvt0AfGBoZ7HS/mkhrKuh2ZlzfVZYKoLjXdqw6Yt7Q==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.6.2" + "@babel/helper-define-polyfill-provider": "^0.6.3" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" @@ -8815,9 +8830,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001679", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001679.tgz", - "integrity": "sha512-j2YqID/YwpLnKzCmBOS4tlZdWprXm3ZmQLBH9ZBXFOhoxLA46fwyBvx6toCBWBmnuwUY/qB3kEU6gFx8qgCroA==", + "version": "1.0.30001680", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001680.tgz", + "integrity": "sha512-rPQy70G6AGUMnbwS1z6Xg+RkHYPAi18ihs47GH0jcxIG7wArmPgY3XbS2sRdBbxJljp3thdT8BIqv9ccCypiPA==", "funding": [ { "type": "opencollective", @@ -9719,13 +9734,10 @@ } }, "node_modules/d3-dispatch": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", - "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", - "license": "ISC", - "engines": { - "node": ">=12" - } + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-1.0.6.tgz", + "integrity": "sha512-fVjoElzjhCEy+Hbn8KygnmMS7Or0a9sI2UzGwoB7cCtvI1XpVN9GpoYlnb3xt2YV66oXYb1fLJ8GMvP4hdU1RA==", + "license": "BSD-3-Clause" }, "node_modules/d3-drag": { "version": "3.0.0", @@ -9761,18 +9773,6 @@ "d3-timer": "1" } }, - "node_modules/d3-force/node_modules/d3-dispatch": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-1.0.6.tgz", - "integrity": "sha512-fVjoElzjhCEy+Hbn8KygnmMS7Or0a9sI2UzGwoB7cCtvI1XpVN9GpoYlnb3xt2YV66oXYb1fLJ8GMvP4hdU1RA==", - "license": "BSD-3-Clause" - }, - "node_modules/d3-force/node_modules/d3-timer": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-1.0.10.tgz", - "integrity": "sha512-B1JDm0XDaQC+uvo4DT79H0XmBskgS3l6Ve+1SBCfxgmtIb1AVrPIoqd+nPSv+loMX8szQ0sVUhGngL7D5QPiXw==", - "license": "BSD-3-Clause" - }, "node_modules/d3-format": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", @@ -9941,13 +9941,10 @@ "license": "ISC" }, "node_modules/d3-timer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", - "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", - "license": "ISC", - "engines": { - "node": ">=12" - } + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-1.0.10.tgz", + "integrity": "sha512-B1JDm0XDaQC+uvo4DT79H0XmBskgS3l6Ve+1SBCfxgmtIb1AVrPIoqd+nPSv+loMX8szQ0sVUhGngL7D5QPiXw==", + "license": "BSD-3-Clause" }, "node_modules/d3-transition": { "version": "3.0.1", @@ -10325,9 +10322,9 @@ } }, "node_modules/dompurify": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.1.7.tgz", - "integrity": "sha512-VaTstWtsneJY8xzy7DekmYWEOZcmzIe3Qb3zPd4STve1OBTa+e+WmS1ITQec1fZYXI3HCsOZZiSMpG6oxoWMWQ==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.0.tgz", + "integrity": "sha512-AMdOzK44oFWqHEi0wpOqix/fUNY707OmoeFDnbi3Q5I8uOpy21ufUA5cDJPr0bosxrflOVD/H2DMSvuGKJGfmQ==", "license": "(MPL-2.0 OR Apache-2.0)" }, "node_modules/domutils": { @@ -10359,13 +10356,13 @@ } }, "node_modules/dotenv-expand": { - "version": "11.0.6", - "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-11.0.6.tgz", - "integrity": "sha512-8NHi73otpWsZGBSZwwknTXS5pqMOrk9+Ssrna8xCaxkzEpU9OTf9R5ArQGVw03//Zmk9MOwLPng9WwndvpAJ5g==", + "version": "11.0.7", + "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-11.0.7.tgz", + "integrity": "sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "dotenv": "^16.4.4" + "dotenv": "^16.4.5" }, "engines": { "node": ">=12" @@ -10405,9 +10402,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.55", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.55.tgz", - "integrity": "sha512-6maZ2ASDOTBtjt9FhqYPRnbvKU5tjG0IN9SztUOWYw2AzNDNpKJYLJmlK0/En4Hs/aiWnB+JZ+gW19PIGszgKg==", + "version": "1.5.60", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.60.tgz", + "integrity": "sha512-HcraRUkTKJ+8yA3b10i9qvhUlPBRDlKjn1XGek1zDGVfAKcvi8TsUnImGqLiEm9j6ZulxXIWWIo9BmbkbCTGgA==", "license": "ISC" }, "node_modules/emittery": { @@ -12786,9 +12783,9 @@ "license": "MIT" }, "node_modules/immutable": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.7.tgz", - "integrity": "sha512-1hqclzwYwjRDFLjcFxOM5AYkkG0rpFPpr1RLPMEuGczoS7YA8gLhy8SWXYRAA/XwfEHpfo3cw5JGioS32fnMRw==", + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.0.2.tgz", + "integrity": "sha512-1NU7hWZDkV7hJ4PJ9dur9gTNQ4ePNPN4k9/0YhwjzykTi/+3Q5pF93YU5QoVj8BuOnhLgaY8gs0U2pj4kSYVcw==", "dev": true, "license": "MIT" }, @@ -16052,9 +16049,9 @@ } }, "node_modules/node-gyp-build": { - "version": "4.8.2", - "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.2.tgz", - "integrity": "sha512-IRUxE4BVsHWXkV/SFOut4qTlagw2aM8T5/vnTsmrHJvVoKueJHRc/JaFND7QDDc61kLYUJ6qlZM3sqTSyx2dTw==", + "version": "4.8.3", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.3.tgz", + "integrity": "sha512-EMS95CMJzdoSKoIiXo8pxKoL8DYxwIZXYlLmgPb8KUv794abpnLK6ynsCAWNliOjREKruYKdzbh76HHYUHX7nw==", "dev": true, "license": "MIT", "bin": { @@ -16347,9 +16344,9 @@ "license": "MIT" }, "node_modules/nx": { - "version": "20.0.12", - "resolved": "https://registry.npmjs.org/nx/-/nx-20.0.12.tgz", - "integrity": "sha512-pQ7Rwb2Qlhr+fEamd0qc4VsL/aKjVJ0MXPsosuhdZobLJQOKHefe+nXSSZ1Jy19VM3RRpxUKFneD/V2jvs3qDA==", + "version": "20.1.1", + "resolved": "https://registry.npmjs.org/nx/-/nx-20.1.1.tgz", + "integrity": "sha512-bLDEDBUuAvFC5b74QUnmJxUHTRa0mkc2wRPmb2rN3d1VlTFjzKTT9ClJTR1emp/DDO620zyAmVCDVKmnSZNFoQ==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -16392,16 +16389,16 @@ "nx-cloud": "bin/nx-cloud.js" }, "optionalDependencies": { - "@nx/nx-darwin-arm64": "20.0.12", - "@nx/nx-darwin-x64": "20.0.12", - "@nx/nx-freebsd-x64": "20.0.12", - "@nx/nx-linux-arm-gnueabihf": "20.0.12", - "@nx/nx-linux-arm64-gnu": "20.0.12", - "@nx/nx-linux-arm64-musl": "20.0.12", - "@nx/nx-linux-x64-gnu": "20.0.12", - "@nx/nx-linux-x64-musl": "20.0.12", - "@nx/nx-win32-arm64-msvc": "20.0.12", - "@nx/nx-win32-x64-msvc": "20.0.12" + "@nx/nx-darwin-arm64": "20.1.1", + "@nx/nx-darwin-x64": "20.1.1", + "@nx/nx-freebsd-x64": "20.1.1", + "@nx/nx-linux-arm-gnueabihf": "20.1.1", + "@nx/nx-linux-arm64-gnu": "20.1.1", + "@nx/nx-linux-arm64-musl": "20.1.1", + "@nx/nx-linux-x64-gnu": "20.1.1", + "@nx/nx-linux-x64-musl": "20.1.1", + "@nx/nx-win32-arm64-msvc": "20.1.1", + "@nx/nx-win32-x64-msvc": "20.1.1" }, "peerDependencies": { "@swc-node/register": "^1.8.0", @@ -16834,9 +16831,9 @@ } }, "node_modules/p-retry": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-6.2.0.tgz", - "integrity": "sha512-JA6nkq6hKyWLLasXQXUrO4z8BUZGUt/LjlJxx8Gb2+2ntodU/SS63YZ8b0LUTbQ8ZB9iwOfhEPhg4ykKnn2KsA==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-6.2.1.tgz", + "integrity": "sha512-hEt02O4hUct5wtwg4H4KcWgDdm+l1bOaEy/hWzd8xtXB9BqxTWBBhb+2ImAtH4Cv4rPjV76xN3Zumqk3k3AhhQ==", "dev": true, "license": "MIT", "dependencies": { @@ -17134,6 +17131,7 @@ "version": "1.17.1", "resolved": "https://registry.npmjs.org/pdf-lib/-/pdf-lib-1.17.1.tgz", "integrity": "sha512-V/mpyJAoTsN4cnP31vc0wfNA1+p20evqqnap0KLoRUN0Yk/p3wN52DOEsL4oBFcLdb76hlpKPtzJIgo67j/XLw==", + "license": "MIT", "dependencies": { "@pdf-lib/standard-fonts": "^1.0.0", "@pdf-lib/upng": "^1.0.1", @@ -17144,7 +17142,8 @@ "node_modules/pdf-lib/node_modules/tslib": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "license": "0BSD" }, "node_modules/pdfjs-dist": { "version": "4.8.69", @@ -17437,14 +17436,14 @@ } }, "node_modules/postcss-modules-local-by-default": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.5.tgz", - "integrity": "sha512-6MieY7sIfTK0hYfafw1OMEG+2bg8Q1ocHCpoWLqOKj3JXlKu4G7btkmM/B7lFubYkYWmRSPLZi5chid63ZaZYw==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.1.0.tgz", + "integrity": "sha512-rm0bdSv4jC3BDma3s9H19ZddW0aHX6EoqwDYU2IfZhRN+53QrufTRo2IdkAbRqLx4R2IYbZnbjKKxg4VN5oU9Q==", "dev": true, "license": "MIT", "dependencies": { "icss-utils": "^5.0.0", - "postcss-selector-parser": "^6.0.2", + "postcss-selector-parser": "^7.0.0", "postcss-value-parser": "^4.1.0" }, "engines": { @@ -17455,13 +17454,13 @@ } }, "node_modules/postcss-modules-scope": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.2.0.tgz", - "integrity": "sha512-oq+g1ssrsZOsx9M96c5w8laRmvEu9C3adDSjI8oTcbfkrTE8hx/zfyobUoWIxaKPO8bt6S62kxpw5GqypEw1QQ==", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.2.1.tgz", + "integrity": "sha512-m9jZstCVaqGjTAuny8MdgE88scJnCiQSlSrOWcTQgM2t32UBe+MUmFSO5t7VMSfAf/FJKImAxBav8ooCHJXCJA==", "dev": true, "license": "ISC", "dependencies": { - "postcss-selector-parser": "^6.0.4" + "postcss-selector-parser": "^7.0.0" }, "engines": { "node": "^10 || ^12 || >= 14" @@ -17487,9 +17486,9 @@ } }, "node_modules/postcss-selector-parser": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", - "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.0.0.tgz", + "integrity": "sha512-9RbEr1Y7FFfptd/1eEdntyjMwLeghW1bHX9GWjXo19vx4ytPQhANltvVxDggzJl7mnWM+dX28kb6cyS/4iQjlQ==", "dev": true, "license": "MIT", "dependencies": { @@ -17507,9 +17506,9 @@ "license": "MIT" }, "node_modules/posthog-js": { - "version": "1.181.0", - "resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.181.0.tgz", - "integrity": "sha512-bI+J+f4E8x4JwbGtG6LReQv1Xvss01F6cs7UDlvffHySpVhNq4ptkNjV88B92IVEsrCtNYhy/TjFnGxk6RN0Qw==", + "version": "1.186.0", + "resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.186.0.tgz", + "integrity": "sha512-WagGNrDtvyOhmX1Gtf1hJQMBy1mB1vx9gtC6BKEfJi2pvEFtQuAzQ9c/tMUTmY0o2ZF5ZBFiZ2IRs4kbFLMvPQ==", "license": "MIT", "dependencies": { "core-js": "^3.38.1", @@ -18431,6 +18430,7 @@ "version": "1.4.1", "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "dev": true, "license": "MIT" }, "node_modules/rimraf": { @@ -18664,14 +18664,14 @@ "license": "MIT" }, "node_modules/sass": { - "version": "1.80.6", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.80.6.tgz", - "integrity": "sha512-ccZgdHNiBF1NHBsWvacvT5rju3y1d/Eu+8Ex6c21nHp2lZGLBEtuwc415QfiI1PJa1TpCo3iXwwSRjRpn2Ckjg==", + "version": "1.81.0", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.81.0.tgz", + "integrity": "sha512-Q4fOxRfhmv3sqCLoGfvrC9pRV8btc0UtqL9mN6Yrv6Qi9ScL55CVH1vlPP863ISLEEMNLLuu9P+enCeGHlnzhA==", "dev": true, "license": "MIT", "dependencies": { "chokidar": "^4.0.0", - "immutable": "^4.0.0", + "immutable": "^5.0.2", "source-map-js": ">=0.6.2 <2.0.0" }, "bin": { @@ -20183,22 +20183,22 @@ "license": "MIT" }, "node_modules/tldts": { - "version": "6.1.59", - "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.59.tgz", - "integrity": "sha512-472ilPxsRuqBBpn+KuRBHJvZhk6tTo4yTVsmODrLBNLwRYJPkDfMEHivgNwp5iEl+cbrZzzRtLKRxZs7+QKkRg==", + "version": "6.1.61", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.61.tgz", + "integrity": "sha512-rv8LUyez4Ygkopqn+M6OLItAOT9FF3REpPQDkdMx5ix8w4qkuE7Vo2o/vw1nxKQYmJDV8JpAMJQr1b+lTKf0FA==", "dev": true, "license": "MIT", "dependencies": { - "tldts-core": "^6.1.59" + "tldts-core": "^6.1.61" }, "bin": { "tldts": "bin/cli.js" } }, "node_modules/tldts-core": { - "version": "6.1.59", - "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.59.tgz", - "integrity": "sha512-EiYgNf275AQyVORl8HQYYe7rTVnmLb4hkWK7wAk/12Ksy5EiHpmUmTICa4GojookBPC8qkLMBKKwCmzNA47ZPQ==", + "version": "6.1.61", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.61.tgz", + "integrity": "sha512-In7VffkDWUPgwa+c9picLUxvb0RltVwTkSgMNFgvlGSWveCzGBemBqTsgJCL4EDFWZ6WH0fKTsot6yNhzy3ZzQ==", "dev": true, "license": "MIT" }, @@ -20834,9 +20834,9 @@ } }, "node_modules/uuid": { - "version": "11.0.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.0.2.tgz", - "integrity": "sha512-14FfcOJmqdjbBPdDjFQyk/SdT4NySW4eM0zcG+HqbHP5jzuH56xO3J1DGhgs/cEMCfwYi3HQI1gnTO62iaG+tQ==", + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.0.3.tgz", + "integrity": "sha512-d0z310fCWv5dJwnX1Y/MncBAqGMKEzlBb1AOf7z9K8ALnd0utBX/msg/fA0+sbyN1ihbMsLhrBlnl1ak7Wa0rg==", "funding": [ "https://github.com/sponsors/broofa", "https://github.com/sponsors/ctavan" @@ -21498,12 +21498,6 @@ "d3-timer": "^1.0.5" } }, - "node_modules/webcola/node_modules/d3-dispatch": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-1.0.6.tgz", - "integrity": "sha512-fVjoElzjhCEy+Hbn8KygnmMS7Or0a9sI2UzGwoB7cCtvI1XpVN9GpoYlnb3xt2YV66oXYb1fLJ8GMvP4hdU1RA==", - "license": "BSD-3-Clause" - }, "node_modules/webcola/node_modules/d3-drag": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-1.2.5.tgz", @@ -21529,12 +21523,6 @@ "d3-path": "1" } }, - "node_modules/webcola/node_modules/d3-timer": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-1.0.10.tgz", - "integrity": "sha512-B1JDm0XDaQC+uvo4DT79H0XmBskgS3l6Ve+1SBCfxgmtIb1AVrPIoqd+nPSv+loMX8szQ0sVUhGngL7D5QPiXw==", - "license": "BSD-3-Clause" - }, "node_modules/webidl-conversions": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", diff --git a/package.json b/package.json index 6956b8ac3223..2104b83d8908 100644 --- a/package.json +++ b/package.json @@ -13,18 +13,18 @@ "node_modules" ], "dependencies": { - "@angular/animations": "18.2.11", - "@angular/cdk": "18.2.12", - "@angular/common": "18.2.11", - "@angular/compiler": "18.2.11", - "@angular/core": "18.2.11", - "@angular/forms": "18.2.11", - "@angular/localize": "18.2.11", - "@angular/material": "18.2.12", - "@angular/platform-browser": "18.2.11", - "@angular/platform-browser-dynamic": "18.2.11", - "@angular/router": "18.2.11", - "@angular/service-worker": "18.2.11", + "@angular/animations": "18.2.12", + "@angular/cdk": "18.2.13", + "@angular/common": "18.2.12", + "@angular/compiler": "18.2.12", + "@angular/core": "18.2.12", + "@angular/forms": "18.2.12", + "@angular/localize": "18.2.12", + "@angular/material": "18.2.13", + "@angular/platform-browser": "18.2.12", + "@angular/platform-browser-dynamic": "18.2.12", + "@angular/router": "18.2.12", + "@angular/service-worker": "18.2.12", "@ctrl/ngx-emoji-mart": "9.2.0", "@danielmoncada/angular-datetime-picker": "18.1.0", "@fingerprintjs/fingerprintjs": "4.5.1", @@ -36,9 +36,9 @@ "@ng-bootstrap/ng-bootstrap": "17.0.1", "@ngx-translate/core": "16.0.3", "@ngx-translate/http-loader": "16.0.0", - "@sentry/angular": "8.37.1", + "@sentry/angular": "8.38.0", "@siemens/ngx-datatable": "22.4.1", - "@swimlane/ngx-charts": "20.5.0", + "@swimlane/ngx-charts": "21.0.0", "@swimlane/ngx-graph": "8.4.0", "@vscode/codicons": "0.0.36", "@vscode/markdown-it-katex": "1.1.0", @@ -48,7 +48,7 @@ "crypto-js": "4.2.0", "dayjs": "1.11.13", "diff-match-patch-typescript": "1.1.0", - "dompurify": "3.1.7", + "dompurify": "3.2.0", "emoji-js": "3.8.0", "export-to-csv": "1.4.0", "fast-json-patch": "3.1.1", @@ -69,7 +69,7 @@ "papaparse": "5.4.1", "pdf-lib": "1.17.1", "pdfjs-dist": "4.8.69", - "posthog-js": "1.181.0", + "posthog-js": "1.186.0", "rxjs": "7.8.1", "simple-statistics": "7.8.7", "smoothscroll-polyfill": "0.4.4", @@ -78,7 +78,7 @@ "ts-cacheable": "1.0.10", "tslib": "2.8.1", "turndown": "7.2.0", - "uuid": "11.0.2", + "uuid": "11.0.3", "webstomp-client": "1.2.6", "xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz", "zone.js": "0.14.10" @@ -116,16 +116,16 @@ }, "devDependencies": { "@angular-builders/jest": "18.0.0", - "@angular-devkit/build-angular": "18.2.11", + "@angular-devkit/build-angular": "18.2.12", "@angular-eslint/builder": "18.4.0", "@angular-eslint/eslint-plugin": "18.4.0", "@angular-eslint/eslint-plugin-template": "18.4.0", "@angular-eslint/schematics": "18.4.0", "@angular-eslint/template-parser": "18.4.0", - "@angular/cli": "18.2.11", - "@angular/compiler-cli": "18.2.11", - "@angular/language-service": "18.2.11", - "@sentry/types": "8.37.1", + "@angular/cli": "18.2.12", + "@angular/compiler-cli": "18.2.12", + "@angular/language-service": "18.2.12", + "@sentry/types": "8.38.0", "@types/crypto-js": "4.2.2", "@types/d3-shape": "3.1.6", "@types/dompurify": "3.0.5", @@ -139,8 +139,8 @@ "@types/sockjs-client": "1.5.4", "@types/turndown": "5.0.5", "@types/uuid": "10.0.0", - "@typescript-eslint/eslint-plugin": "8.13.0", - "@typescript-eslint/parser": "8.13.0", + "@typescript-eslint/eslint-plugin": "8.14.0", + "@typescript-eslint/parser": "8.14.0", "eslint": "9.14.0", "eslint-config-prettier": "9.1.0", "eslint-plugin-deprecation": "3.0.0", @@ -161,7 +161,7 @@ "ng-mocks": "14.13.1", "prettier": "3.3.3", "rimraf": "6.0.1", - "sass": "1.80.6", + "sass": "1.81.0", "ts-jest": "29.2.5", "typescript": "5.5.4", "weak-napi": "2.0.2" From 9dd174a5c9611ba33bb5f31dc63600df6eb4e874 Mon Sep 17 00:00:00 2001 From: Maximilian Anzinger <44003963+MaximilianAnzinger@users.noreply.github.com> Date: Fri, 15 Nov 2024 15:22:12 +0100 Subject: [PATCH 10/21] Development: Refine issue labeling bot to reduce over-labeling (#9606) --- .github/issue-labeler.yml | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/.github/issue-labeler.yml b/.github/issue-labeler.yml index b2201cc36358..34ca1e28321c 100644 --- a/.github/issue-labeler.yml +++ b/.github/issue-labeler.yml @@ -10,6 +10,8 @@ assessment: athena: - athena + - ai feedback + - request (ai )?feedback - caseSensitive: false atlas: @@ -48,8 +50,6 @@ core: - authority - data export - migration - - user - - group - caseSensitive: false exam: @@ -79,20 +79,19 @@ iris: - iris - llm - chatbot - - ai + - \b(? Date: Fri, 15 Nov 2024 20:34:00 +0100 Subject: [PATCH 11/21] Development: Fix authorization for assetlinks.json (#9701) --- .../tum/cit/aet/artemis/core/config/SecurityConfiguration.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/de/tum/cit/aet/artemis/core/config/SecurityConfiguration.java b/src/main/java/de/tum/cit/aet/artemis/core/config/SecurityConfiguration.java index baacbfd73966..fbd88e0b323b 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/config/SecurityConfiguration.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/config/SecurityConfiguration.java @@ -207,6 +207,7 @@ public SecurityFilterChain filterChain(HttpSecurity http, SecurityProblemSupport // Websocket and other specific endpoints allowed without authentication. .requestMatchers("/websocket/**").permitAll() .requestMatchers("/.well-known/jwks.json").permitAll() + .requestMatchers("/.well-known/assetlinks.json").permitAll() // Prometheus endpoint protected by IP address. .requestMatchers("/management/prometheus/**").access((authentication, context) -> new AuthorizationDecision(monitoringIpAddresses.contains(context.getRequest().getRemoteAddr()))); From e3f1c929d4f2cdf4522110b74e5dfc5c5d76962c Mon Sep 17 00:00:00 2001 From: Asli Aykan <56061820+asliayk@users.noreply.github.com> Date: Fri, 15 Nov 2024 20:37:56 +0100 Subject: [PATCH 12/21] Communication: Add unread message notification icon to sidebar accordion (#9737) --- .../overview/course-overview.component.html | 2 +- .../sidebar-accordion.component.html | 3 +++ .../sidebar-accordion.component.scss | 19 +++++++++++++++++++ .../sidebar-accordion.component.ts | 15 +++++++++++++++ .../sidebar-card-item.component.scss | 1 + .../sidebar-accordion.component.spec.ts | 16 ++++++++++++---- 6 files changed, 51 insertions(+), 5 deletions(-) diff --git a/src/main/webapp/app/overview/course-overview.component.html b/src/main/webapp/app/overview/course-overview.component.html index 5fac3f273643..c19fe24185bd 100644 --- a/src/main/webapp/app/overview/course-overview.component.html +++ b/src/main/webapp/app/overview/course-overview.component.html @@ -259,7 +259,7 @@ [id]="sidebarItem.testId ?? ''" [ngClass]="{ 'guided-tour': sidebarItem.guidedTour, - newMessage: !communicationRouteLoaded && hasUnreadMessages && sidebarItem.title === 'Communication', + newMessage: hasUnreadMessages && sidebarItem.title === 'Communication', collapsed: isNavbarCollapsed, }" jhiOrionFilter diff --git a/src/main/webapp/app/shared/sidebar/sidebar-accordion/sidebar-accordion.component.html b/src/main/webapp/app/shared/sidebar/sidebar-accordion/sidebar-accordion.component.html index a3ec25c5b6d6..92d7b034e785 100644 --- a/src/main/webapp/app/shared/sidebar/sidebar-accordion/sidebar-accordion.component.html +++ b/src/main/webapp/app/shared/sidebar/sidebar-accordion/sidebar-accordion.component.html @@ -14,6 +14,9 @@ ({{ (groupedData[groupKey].entityData | searchFilter: ['title', 'type'] : searchValue)?.length }})
+ @if (totalUnreadMessagesPerGroup[groupKey] > 0 && collapseState[groupKey]) { + {{ totalUnreadMessagesPerGroup[groupKey] }} + }
diff --git a/src/main/webapp/app/shared/sidebar/sidebar-accordion/sidebar-accordion.component.scss b/src/main/webapp/app/shared/sidebar/sidebar-accordion/sidebar-accordion.component.scss index 78788a1fb334..61f8e82f8193 100644 --- a/src/main/webapp/app/shared/sidebar/sidebar-accordion/sidebar-accordion.component.scss +++ b/src/main/webapp/app/shared/sidebar/sidebar-accordion/sidebar-accordion.component.scss @@ -5,3 +5,22 @@ hr { border-top-color: var(--overview-light-border-color); } + +.unread-count { + background-color: var(--bs-primary); + color: var(--bs-white); + border-radius: 50%; + font-size: 0.6rem; + width: 1.1rem; + height: 1.1rem; + display: flex; + justify-content: center; + align-items: center; + overflow: hidden; + white-space: nowrap; +} + +.icon-container { + display: flex; + align-items: center; +} diff --git a/src/main/webapp/app/shared/sidebar/sidebar-accordion/sidebar-accordion.component.ts b/src/main/webapp/app/shared/sidebar/sidebar-accordion/sidebar-accordion.component.ts index 030cdbb879ec..26f65e5258fa 100644 --- a/src/main/webapp/app/shared/sidebar/sidebar-accordion/sidebar-accordion.component.ts +++ b/src/main/webapp/app/shared/sidebar/sidebar-accordion/sidebar-accordion.component.ts @@ -27,6 +27,7 @@ export class SidebarAccordionComponent implements OnChanges, OnInit { readonly faChevronRight = faChevronRight; readonly faFile = faFile; + totalUnreadMessagesPerGroup: { [key: string]: number } = {}; ngOnInit() { this.expandGroupWithSelectedItem(); @@ -39,6 +40,7 @@ export class SidebarAccordionComponent implements OnChanges, OnInit { } else { this.setStoredCollapseState(); } + this.calculateUnreadMessagesOfGroup(); } setStoredCollapseState() { @@ -67,6 +69,19 @@ export class SidebarAccordionComponent implements OnChanges, OnInit { } } + calculateUnreadMessagesOfGroup(): void { + if (!this.groupedData) { + this.totalUnreadMessagesPerGroup = {}; + return; + } + + Object.keys(this.groupedData).forEach((groupKey) => { + this.totalUnreadMessagesPerGroup[groupKey] = this.groupedData[groupKey].entityData + .filter((item: SidebarCardElement) => item.conversation?.unreadMessagesCount) + .reduce((sum, item) => sum + (item.conversation?.unreadMessagesCount || 0), 0); + }); + } + toggleGroupCategoryCollapse(groupCategoryKey: string) { this.collapseState[groupCategoryKey] = !this.collapseState[groupCategoryKey]; localStorage.setItem('sidebar.accordion.collapseState.' + this.storageId + '.byCourse.' + this.courseId, JSON.stringify(this.collapseState)); diff --git a/src/main/webapp/app/shared/sidebar/sidebar-card-item/sidebar-card-item.component.scss b/src/main/webapp/app/shared/sidebar/sidebar-card-item/sidebar-card-item.component.scss index 64f4e5a920c0..8f36ede446dc 100644 --- a/src/main/webapp/app/shared/sidebar/sidebar-card-item/sidebar-card-item.component.scss +++ b/src/main/webapp/app/shared/sidebar/sidebar-card-item/sidebar-card-item.component.scss @@ -12,6 +12,7 @@ border-radius: 50%; font-size: 0.6rem; width: 1.1rem; + min-width: 1.1rem; height: 1.1rem; display: flex; justify-content: center; diff --git a/src/test/javascript/spec/component/shared/sidebar/sidebar-accordion.component.spec.ts b/src/test/javascript/spec/component/shared/sidebar/sidebar-accordion.component.spec.ts index 9e7fe5427253..54015cb0ff41 100644 --- a/src/test/javascript/spec/component/shared/sidebar/sidebar-accordion.component.spec.ts +++ b/src/test/javascript/spec/component/shared/sidebar/sidebar-accordion.component.spec.ts @@ -39,22 +39,23 @@ describe('SidebarAccordionComponent', () => { component.groupedData = { current: { - entityData: [{ title: 'Title 1', type: 'Type A', id: 1, size: 'M' }], + entityData: [{ title: 'Title 1', type: 'Type A', id: 1, size: 'M', conversation: { unreadMessagesCount: 1 } }], }, past: { - entityData: [{ title: 'Title 2', type: 'Type B', id: 2, size: 'M' }], + entityData: [{ title: 'Title 2', type: 'Type B', id: 2, size: 'M', conversation: { unreadMessagesCount: 0 } }], }, future: { - entityData: [{ title: 'Title 3', type: 'Type C', id: 3, size: 'M' }], + entityData: [{ title: 'Title 3', type: 'Type C', id: 3, size: 'M', conversation: { unreadMessagesCount: 1 } }], }, noDate: { - entityData: [{ title: 'Title 4', type: 'Type D', id: 4, size: 'M' }], + entityData: [{ title: 'Title 4', type: 'Type D', id: 4, size: 'M', conversation: { unreadMessagesCount: 0 } }], }, }; component.routeParams = { exerciseId: 3 }; component.collapseState = { current: false, dueSoon: false, past: false, future: true, noDate: true }; fixture.componentRef.setInput('sidebarItemAlwaysShow', { current: false, dueSoon: false, past: false, future: false, noDate: false }); fixture.detectChanges(); + component.calculateUnreadMessagesOfGroup(); }); afterEach(() => { @@ -144,4 +145,11 @@ describe('SidebarAccordionComponent', () => { component.expandGroupWithSelectedItem(); expect(component.collapseState['future']).toBeFalse(); }); + + it('should calculate unread messages of each group correctly', () => { + expect(component.totalUnreadMessagesPerGroup['current']).toBe(1); + expect(component.totalUnreadMessagesPerGroup['past']).toBe(0); + expect(component.totalUnreadMessagesPerGroup['future']).toBe(1); + expect(component.totalUnreadMessagesPerGroup['noDate']).toBe(0); + }); }); From 0db5cc1631d41590657c2c7c33a0b9bd7593c121 Mon Sep 17 00:00:00 2001 From: Johannes Wiest Date: Sat, 16 Nov 2024 10:08:44 +0100 Subject: [PATCH 13/21] Development: Improve client code quality for learning paths (#9654) --- .../competency-graph-modal.component.ts | 23 ++++++++---- .../competency-graph.component.ts | 10 ++--- .../competency-node.component.ts | 3 +- .../learning-path-exercise.component.ts | 9 +++-- .../learning-path-lecture-unit.component.ts | 15 ++++++-- ...nav-overview-learning-objects.component.ts | 36 +++++++++--------- .../learning-path-nav-overview.component.ts | 37 +++++++++++-------- .../learning-path-student-nav.component.ts | 7 ++-- .../learning-paths-analytics.component.ts | 16 ++++++-- .../learning-paths-configuration.component.ts | 15 ++++++-- .../learning-paths-state.component.ts | 17 +++++++-- .../learning-paths-table.component.html | 1 + .../learning-paths-table.component.ts | 11 ++++-- .../exceptions/entity-not-found.error.ts | 6 --- ...learning-path-instructor-page.component.ts | 15 ++++++-- .../learning-path-student-page.component.ts | 11 +++++- ...arning-path-student-page.component.spec.ts | 5 +-- 17 files changed, 148 insertions(+), 89 deletions(-) delete mode 100644 src/main/webapp/app/course/learning-paths/exceptions/entity-not-found.error.ts diff --git a/src/main/webapp/app/course/learning-paths/components/competency-graph-modal/competency-graph-modal.component.ts b/src/main/webapp/app/course/learning-paths/components/competency-graph-modal/competency-graph-modal.component.ts index ac7a27af9f09..28594a2f49d9 100644 --- a/src/main/webapp/app/course/learning-paths/components/competency-graph-modal/competency-graph-modal.component.ts +++ b/src/main/webapp/app/course/learning-paths/components/competency-graph-modal/competency-graph-modal.component.ts @@ -1,25 +1,26 @@ -import { Component, effect, inject, input, signal } from '@angular/core'; -import { FontAwesomeModule, IconDefinition } from '@fortawesome/angular-fontawesome'; +import { ChangeDetectionStrategy, Component, effect, inject, input, signal, untracked } from '@angular/core'; +import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; import { faXmark } from '@fortawesome/free-solid-svg-icons'; import { NgbActiveModal, NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { CompetencyGraphComponent } from 'app/course/learning-paths/components/competency-graph/competency-graph.component'; -import { ArtemisSharedModule } from 'app/shared/shared.module'; import { LearningPathApiService } from 'app/course/learning-paths/services/learning-path-api.service'; import { AlertService } from 'app/core/util/alert.service'; import { CompetencyGraphDTO } from 'app/entities/competency/learning-path.model'; +import { TranslateDirective } from 'app/shared/language/translate.directive'; @Component({ selector: 'jhi-competency-graph-modal', standalone: true, - imports: [FontAwesomeModule, CompetencyGraphComponent, ArtemisSharedModule], + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [FontAwesomeModule, CompetencyGraphComponent, TranslateDirective], templateUrl: './competency-graph-modal.component.html', styleUrl: './competency-graph-modal.component.scss', }) export class CompetencyGraphModalComponent { - protected readonly closeIcon: IconDefinition = faXmark; + protected readonly closeIcon = faXmark; - private readonly learningPathApiService: LearningPathApiService = inject(LearningPathApiService); - private readonly alertService: AlertService = inject(AlertService); + private readonly learningPathApiService = inject(LearningPathApiService); + private readonly alertService = inject(AlertService); readonly learningPathId = input.required(); @@ -28,7 +29,13 @@ export class CompetencyGraphModalComponent { private readonly activeModal: NgbActiveModal = inject(NgbActiveModal); constructor() { - effect(() => this.loadCompetencyGraph(this.learningPathId()), { allowSignalWrites: true }); + effect( + () => { + const learningPathId = this.learningPathId(); + untracked(() => this.loadCompetencyGraph(learningPathId)); + }, + { allowSignalWrites: true }, + ); } private async loadCompetencyGraph(learningPathId: number): Promise { diff --git a/src/main/webapp/app/course/learning-paths/components/competency-graph/competency-graph.component.ts b/src/main/webapp/app/course/learning-paths/components/competency-graph/competency-graph.component.ts index 88cd93ac83e2..0e0bbe4c14a1 100644 --- a/src/main/webapp/app/course/learning-paths/components/competency-graph/competency-graph.component.ts +++ b/src/main/webapp/app/course/learning-paths/components/competency-graph/competency-graph.component.ts @@ -1,20 +1,18 @@ -import { Component, computed, effect, input, signal } from '@angular/core'; +import { ChangeDetectionStrategy, Component, computed, effect, input, signal } from '@angular/core'; import { NgxGraphModule, NgxGraphZoomOptions } from '@swimlane/ngx-graph'; import { Subject } from 'rxjs'; -import { CompetencyGraphDTO, NodeType } from 'app/entities/competency/learning-path.model'; +import { CompetencyGraphDTO } from 'app/entities/competency/learning-path.model'; import { CompetencyNodeComponent, SizeUpdate } from 'app/course/learning-paths/components/competency-node/competency-node.component'; -import { ArtemisSharedModule } from 'app/shared/shared.module'; @Component({ selector: 'jhi-competency-graph', standalone: true, - imports: [CompetencyNodeComponent, NgxGraphModule, ArtemisSharedModule], + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [CompetencyNodeComponent, NgxGraphModule], templateUrl: './competency-graph.component.html', styleUrl: './competency-graph.component.scss', }) export class CompetencyGraphComponent { - protected readonly NodeType = NodeType; - readonly competencyGraph = input.required(); private readonly internalCompetencyGraph = signal({ diff --git a/src/main/webapp/app/course/learning-paths/components/competency-node/competency-node.component.ts b/src/main/webapp/app/course/learning-paths/components/competency-node/competency-node.component.ts index 5365bb22387b..e4d60b32ef47 100644 --- a/src/main/webapp/app/course/learning-paths/components/competency-node/competency-node.component.ts +++ b/src/main/webapp/app/course/learning-paths/components/competency-node/competency-node.component.ts @@ -1,5 +1,5 @@ import { CommonModule } from '@angular/common'; -import { AfterViewInit, Component, ElementRef, computed, inject, input, output } from '@angular/core'; +import { AfterViewInit, ChangeDetectionStrategy, Component, ElementRef, computed, inject, input, output } from '@angular/core'; import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; import { NgbAccordionModule, NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap'; import { NodeDimension } from '@swimlane/ngx-graph'; @@ -13,6 +13,7 @@ export interface SizeUpdate { @Component({ selector: 'jhi-learning-path-competency-node', standalone: true, + changeDetection: ChangeDetectionStrategy.OnPush, imports: [NgbDropdownModule, FontAwesomeModule, NgbAccordionModule, CommonModule], templateUrl: './competency-node.component.html', styleUrl: './competency-node.component.scss', diff --git a/src/main/webapp/app/course/learning-paths/components/learning-path-exercise/learning-path-exercise.component.ts b/src/main/webapp/app/course/learning-paths/components/learning-path-exercise/learning-path-exercise.component.ts index 05f904046533..99f230ce1938 100644 --- a/src/main/webapp/app/course/learning-paths/components/learning-path-exercise/learning-path-exercise.component.ts +++ b/src/main/webapp/app/course/learning-paths/components/learning-path-exercise/learning-path-exercise.component.ts @@ -1,18 +1,19 @@ -import { Component, InputSignal, ViewContainerRef, effect, inject, input } from '@angular/core'; +import { ChangeDetectionStrategy, Component, ViewContainerRef, effect, inject, input } from '@angular/core'; import { CourseExerciseDetailsComponent } from 'app/overview/exercise-details/course-exercise-details.component'; import { CourseExerciseDetailsModule } from 'app/overview/exercise-details/course-exercise-details.module'; @Component({ selector: 'jhi-learning-path-exercise', standalone: true, + changeDetection: ChangeDetectionStrategy.OnPush, imports: [CourseExerciseDetailsModule], templateUrl: './learning-path-exercise.component.html', }) export class LearningPathExerciseComponent { - public readonly courseId: InputSignal = input.required(); - public readonly exerciseId: InputSignal = input.required(); + public readonly courseId = input.required(); + public readonly exerciseId = input.required(); - private readonly viewContainerRef: ViewContainerRef = inject(ViewContainerRef); + private readonly viewContainerRef = inject(ViewContainerRef); constructor() { effect(() => { diff --git a/src/main/webapp/app/course/learning-paths/components/learning-path-lecture-unit/learning-path-lecture-unit.component.ts b/src/main/webapp/app/course/learning-paths/components/learning-path-lecture-unit/learning-path-lecture-unit.component.ts index 77f4cf3dd262..04822ff754d8 100644 --- a/src/main/webapp/app/course/learning-paths/components/learning-path-lecture-unit/learning-path-lecture-unit.component.ts +++ b/src/main/webapp/app/course/learning-paths/components/learning-path-lecture-unit/learning-path-lecture-unit.component.ts @@ -1,4 +1,4 @@ -import { Component, computed, effect, inject, input, signal } from '@angular/core'; +import { ChangeDetectionStrategy, Component, computed, effect, inject, input, signal, untracked } from '@angular/core'; import { LectureUnitService } from 'app/lecture/lecture-unit/lecture-unit-management/lectureUnit.service'; import { AlertService } from 'app/core/util/alert.service'; import { LectureUnit, LectureUnitType } from 'app/entities/lecture-unit/lectureUnit.model'; @@ -6,18 +6,19 @@ import { ArtemisLectureUnitsModule } from 'app/overview/course-lectures/lecture- import { LectureUnitCompletionEvent } from 'app/overview/course-lectures/course-lecture-details.component'; import { LearningPathNavigationService } from 'app/course/learning-paths/services/learning-path-navigation.service'; import { lastValueFrom } from 'rxjs'; -import { ArtemisSharedModule } from 'app/shared/shared.module'; import { VideoUnitComponent } from 'app/overview/course-lectures/video-unit/video-unit.component'; import { TextUnitComponent } from 'app/overview/course-lectures/text-unit/text-unit.component'; import { AttachmentUnitComponent } from 'app/overview/course-lectures/attachment-unit/attachment-unit.component'; import { OnlineUnitComponent } from 'app/overview/course-lectures/online-unit/online-unit.component'; import { isCommunicationEnabled } from 'app/entities/course.model'; import { DiscussionSectionComponent } from 'app/overview/discussion-section/discussion-section.component'; +import { TranslateDirective } from 'app/shared/language/translate.directive'; @Component({ selector: 'jhi-learning-path-lecture-unit', standalone: true, - imports: [ArtemisLectureUnitsModule, ArtemisSharedModule, VideoUnitComponent, TextUnitComponent, AttachmentUnitComponent, OnlineUnitComponent, DiscussionSectionComponent], + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [ArtemisLectureUnitsModule, VideoUnitComponent, TextUnitComponent, AttachmentUnitComponent, OnlineUnitComponent, DiscussionSectionComponent, TranslateDirective], templateUrl: './learning-path-lecture-unit.component.html', }) export class LearningPathLectureUnitComponent { @@ -36,7 +37,13 @@ export class LearningPathLectureUnitComponent { readonly isCommunicationEnabled = computed(() => isCommunicationEnabled(this.lecture()?.course)); constructor() { - effect(() => this.loadLectureUnit(this.lectureUnitId()), { allowSignalWrites: true }); + effect( + () => { + const lectureUnitId = this.lectureUnitId(); + untracked(() => this.loadLectureUnit(lectureUnitId)); + }, + { allowSignalWrites: true }, + ); } async loadLectureUnit(lectureUnitId: number): Promise { diff --git a/src/main/webapp/app/course/learning-paths/components/learning-path-nav-overview-learning-objects/learning-path-nav-overview-learning-objects.component.ts b/src/main/webapp/app/course/learning-paths/components/learning-path-nav-overview-learning-objects/learning-path-nav-overview-learning-objects.component.ts index 97609dd46c27..21c0fa4c1c32 100644 --- a/src/main/webapp/app/course/learning-paths/components/learning-path-nav-overview-learning-objects/learning-path-nav-overview-learning-objects.component.ts +++ b/src/main/webapp/app/course/learning-paths/components/learning-path-nav-overview-learning-objects/learning-path-nav-overview-learning-objects.component.ts @@ -1,47 +1,49 @@ -import { Component, InputSignal, OutputEmitterRef, Signal, WritableSignal, computed, effect, inject, input, output, signal, untracked } from '@angular/core'; -import { ArtemisSharedModule } from 'app/shared/shared.module'; +import { ChangeDetectionStrategy, Component, computed, effect, inject, input, output, signal, untracked } from '@angular/core'; import { AlertService } from 'app/core/util/alert.service'; import { LearningPathApiService } from 'app/course/learning-paths/services/learning-path-api.service'; import { LearningPathNavigationService } from 'app/course/learning-paths/services/learning-path-navigation.service'; import { LearningPathNavigationObjectDTO } from 'app/entities/competency/learning-path.model'; -import { IconDefinition, faCheckCircle, faLock } from '@fortawesome/free-solid-svg-icons'; +import { faCheckCircle, faLock } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; import { NgbAccordionModule } from '@ng-bootstrap/ng-bootstrap'; +import { CommonModule } from '@angular/common'; +import { TranslateDirective } from 'app/shared/language/translate.directive'; @Component({ selector: 'jhi-learning-path-nav-overview-learning-objects', standalone: true, - imports: [NgbAccordionModule, FontAwesomeModule, ArtemisSharedModule], + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [NgbAccordionModule, FontAwesomeModule, CommonModule, TranslateDirective], templateUrl: './learning-path-nav-overview-learning-objects.component.html', styleUrl: './learning-path-nav-overview-learning-objects.component.scss', }) export class LearningPathNavOverviewLearningObjectsComponent { - protected readonly faCheckCircle: IconDefinition = faCheckCircle; - protected readonly faLock: IconDefinition = faLock; + protected readonly faCheckCircle = faCheckCircle; + protected readonly faLock = faLock; - private readonly alertService: AlertService = inject(AlertService); - private readonly learningPathApiService: LearningPathApiService = inject(LearningPathApiService); + private readonly alertService = inject(AlertService); + private readonly learningPathApiService = inject(LearningPathApiService); private readonly learningPathNavigationService = inject(LearningPathNavigationService); - readonly learningPathId: InputSignal = input.required(); - readonly competencyId: InputSignal = input.required(); + readonly learningPathId = input.required(); + readonly competencyId = input.required(); // competency id of current competency of learning path (not the one of the selected learning object) - readonly currentCompetencyIdOnPath: InputSignal = input.required(); - readonly currentLearningObject: Signal = this.learningPathNavigationService.currentLearningObject; + readonly currentCompetencyIdOnPath = input.required(); + readonly currentLearningObject = this.learningPathNavigationService.currentLearningObject; - readonly isLoading: WritableSignal = signal(false); - readonly learningObjects: WritableSignal = signal(undefined); + readonly isLoading = signal(false); + readonly learningObjects = signal(undefined); - readonly nextLearningObjectOnPath: Signal = computed(() => + readonly nextLearningObjectOnPath = computed(() => this.competencyId() === this.currentCompetencyIdOnPath() ? this.learningObjects()?.find((learningObject) => !learningObject.completed) : undefined, ); - readonly onLearningObjectSelected: OutputEmitterRef = output(); + readonly onLearningObjectSelected = output(); constructor() { effect( () => { - untracked(async () => await this.loadLearningObjects()); + untracked(() => this.loadLearningObjects()); }, { allowSignalWrites: true }, ); diff --git a/src/main/webapp/app/course/learning-paths/components/learning-path-nav-overview/learning-path-nav-overview.component.ts b/src/main/webapp/app/course/learning-paths/components/learning-path-nav-overview/learning-path-nav-overview.component.ts index 07722fb3d0e7..6bf5c49cea7a 100644 --- a/src/main/webapp/app/course/learning-paths/components/learning-path-nav-overview/learning-path-nav-overview.component.ts +++ b/src/main/webapp/app/course/learning-paths/components/learning-path-nav-overview/learning-path-nav-overview.component.ts @@ -1,46 +1,53 @@ -import { Component, InputSignal, OutputEmitterRef, Signal, WritableSignal, computed, effect, inject, input, output, signal, viewChild } from '@angular/core'; +import { ChangeDetectionStrategy, Component, computed, effect, inject, input, output, signal, untracked, viewChild } from '@angular/core'; import { NgbAccordionDirective, NgbAccordionModule, NgbDropdownModule, NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { CommonModule } from '@angular/common'; import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; -import { IconDefinition, faCheckCircle } from '@fortawesome/free-solid-svg-icons'; +import { faCheckCircle } from '@fortawesome/free-solid-svg-icons'; import { AlertService } from 'app/core/util/alert.service'; import { LearningPathCompetencyDTO } from 'app/entities/competency/learning-path.model'; -import { ArtemisSharedModule } from 'app/shared/shared.module'; import { LearningPathApiService } from 'app/course/learning-paths/services/learning-path-api.service'; import { CompetencyGraphModalComponent } from 'app/course/learning-paths/components/competency-graph-modal/competency-graph-modal.component'; import { LearningPathNavOverviewLearningObjectsComponent } from 'app/course/learning-paths/components/learning-path-nav-overview-learning-objects/learning-path-nav-overview-learning-objects.component'; import { LearningPathNavigationService } from 'app/course/learning-paths/services/learning-path-navigation.service'; +import { TranslateDirective } from 'app/shared/language/translate.directive'; @Component({ selector: 'jhi-learning-path-nav-overview', standalone: true, - imports: [FontAwesomeModule, CommonModule, NgbDropdownModule, NgbAccordionModule, ArtemisSharedModule, LearningPathNavOverviewLearningObjectsComponent], + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [FontAwesomeModule, CommonModule, NgbDropdownModule, NgbAccordionModule, LearningPathNavOverviewLearningObjectsComponent, TranslateDirective], templateUrl: './learning-path-nav-overview.component.html', styleUrl: './learning-path-nav-overview.component.scss', }) export class LearningPathNavOverviewComponent { - protected readonly faCheckCircle: IconDefinition = faCheckCircle; + protected readonly faCheckCircle = faCheckCircle; - private readonly alertService: AlertService = inject(AlertService); - private readonly modalService: NgbModal = inject(NgbModal); - private readonly learningPathApiService: LearningPathApiService = inject(LearningPathApiService); + private readonly alertService = inject(AlertService); + private readonly modalService = inject(NgbModal); + private readonly learningPathApiService = inject(LearningPathApiService); private readonly learningPathNavigationService = inject(LearningPathNavigationService); - readonly learningPathId: InputSignal = input.required(); + readonly learningPathId = input.required(); - readonly competencyAccordion: Signal = viewChild.required(NgbAccordionDirective); + readonly competencyAccordion = viewChild.required(NgbAccordionDirective); - readonly onLearningObjectSelected: OutputEmitterRef = output(); - readonly isLoading: WritableSignal = signal(false); + readonly onLearningObjectSelected = output(); + readonly isLoading = signal(false); readonly competencies = signal([]); // competency id of currently selected learning object - readonly currentCompetencyId: Signal = computed(() => this.learningPathNavigationService.currentLearningObject()?.competencyId); + readonly currentCompetencyId = computed(() => this.learningPathNavigationService.currentLearningObject()?.competencyId); // current competency of learning path (not the one of the selected learning object) - readonly currentCompetencyOnPath: Signal = computed(() => this.competencies()?.find((competency) => competency.masteryProgress < 1)); + readonly currentCompetencyOnPath = computed(() => this.competencies()?.find((competency) => competency.masteryProgress < 1)); constructor() { - effect(async () => await this.loadCompetencies(this.learningPathId()), { allowSignalWrites: true }); + effect( + () => { + const learningPathId = this.learningPathId(); + untracked(() => this.loadCompetencies(learningPathId)); + }, + { allowSignalWrites: true }, + ); } async loadCompetencies(learningPathId: number): Promise { diff --git a/src/main/webapp/app/course/learning-paths/components/learning-path-student-nav/learning-path-student-nav.component.ts b/src/main/webapp/app/course/learning-paths/components/learning-path-student-nav/learning-path-student-nav.component.ts index d243c506179b..dd938e28bd80 100644 --- a/src/main/webapp/app/course/learning-paths/components/learning-path-student-nav/learning-path-student-nav.component.ts +++ b/src/main/webapp/app/course/learning-paths/components/learning-path-student-nav/learning-path-student-nav.component.ts @@ -1,17 +1,18 @@ -import { Component, computed, effect, inject, input, signal, untracked } from '@angular/core'; +import { ChangeDetectionStrategy, Component, computed, effect, inject, input, signal, untracked } from '@angular/core'; import { LearningPathNavigationObjectDTO } from 'app/entities/competency/learning-path.model'; import { CommonModule } from '@angular/common'; import { NgbAccordionModule, NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap'; import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; import { faCheckCircle, faChevronDown, faChevronLeft, faChevronRight, faFlag, faSpinner } from '@fortawesome/free-solid-svg-icons'; import { LearningPathNavOverviewComponent } from 'app/course/learning-paths/components/learning-path-nav-overview/learning-path-nav-overview.component'; -import { ArtemisSharedModule } from 'app/shared/shared.module'; import { LearningPathNavigationService } from 'app/course/learning-paths/services/learning-path-navigation.service'; +import { TranslateDirective } from 'app/shared/language/translate.directive'; @Component({ selector: 'jhi-learning-path-student-nav', standalone: true, - imports: [CommonModule, NgbDropdownModule, NgbAccordionModule, FontAwesomeModule, LearningPathNavOverviewComponent, ArtemisSharedModule], + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [CommonModule, NgbDropdownModule, NgbAccordionModule, FontAwesomeModule, LearningPathNavOverviewComponent, TranslateDirective], templateUrl: './learning-path-student-nav.component.html', styleUrl: './learning-path-student-nav.component.scss', }) diff --git a/src/main/webapp/app/course/learning-paths/components/learning-paths-analytics/learning-paths-analytics.component.ts b/src/main/webapp/app/course/learning-paths/components/learning-paths-analytics/learning-paths-analytics.component.ts index 41a8261eb206..6c2678d30c5a 100644 --- a/src/main/webapp/app/course/learning-paths/components/learning-paths-analytics/learning-paths-analytics.component.ts +++ b/src/main/webapp/app/course/learning-paths/components/learning-paths-analytics/learning-paths-analytics.component.ts @@ -1,15 +1,17 @@ -import { Component, effect, inject, input, signal } from '@angular/core'; +import { ChangeDetectionStrategy, Component, effect, inject, input, signal, untracked } from '@angular/core'; import { LearningPathApiService } from 'app/course/learning-paths/services/learning-path-api.service'; import { CompetencyGraphDTO, CompetencyGraphNodeValueType } from 'app/entities/competency/learning-path.model'; import { AlertService } from 'app/core/util/alert.service'; -import { ArtemisSharedCommonModule } from 'app/shared/shared-common.module'; import { CompetencyGraphComponent } from 'app/course/learning-paths/components/competency-graph/competency-graph.component'; import { onError } from 'app/shared/util/global.utils'; +import { TranslateDirective } from 'app/shared/language/translate.directive'; +import { CommonModule } from '@angular/common'; @Component({ selector: 'jhi-learning-paths-analytics', standalone: true, - imports: [ArtemisSharedCommonModule, CompetencyGraphComponent], + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [CompetencyGraphComponent, TranslateDirective, CommonModule], templateUrl: './learning-paths-analytics.component.html', styleUrl: './learning-paths-analytics.component.scss', }) @@ -27,7 +29,13 @@ export class LearningPathsAnalyticsComponent { readonly valueSelection = signal(CompetencyGraphNodeValueType.AVERAGE_MASTERY_PROGRESS); constructor() { - effect(() => this.loadInstructionCompetencyGraph(this.courseId()), { allowSignalWrites: true }); + effect( + () => { + const courseId = this.courseId(); + untracked(() => this.loadInstructionCompetencyGraph(courseId)); + }, + { allowSignalWrites: true }, + ); } private async loadInstructionCompetencyGraph(courseId: number): Promise { diff --git a/src/main/webapp/app/course/learning-paths/components/learning-paths-configuration/learning-paths-configuration.component.ts b/src/main/webapp/app/course/learning-paths/components/learning-paths-configuration/learning-paths-configuration.component.ts index 23e5cb8e8612..6f2d305f54cc 100644 --- a/src/main/webapp/app/course/learning-paths/components/learning-paths-configuration/learning-paths-configuration.component.ts +++ b/src/main/webapp/app/course/learning-paths/components/learning-paths-configuration/learning-paths-configuration.component.ts @@ -1,17 +1,18 @@ -import { Component, computed, effect, inject, input, signal } from '@angular/core'; +import { ChangeDetectionStrategy, Component, computed, effect, inject, input, signal, untracked } from '@angular/core'; import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; import { faSpinner } from '@fortawesome/free-solid-svg-icons'; import { LearningPathApiService } from '../../services/learning-path-api.service'; import { LearningPathsConfigurationDTO } from 'app/entities/competency/learning-path.model'; import { AlertService } from 'app/core/util/alert.service'; import { onError } from 'app/shared/util/global.utils'; -import { ArtemisSharedCommonModule } from 'app/shared/shared-common.module'; import { ArtemisSharedComponentModule } from 'app/shared/components/shared-component.module'; +import { TranslateDirective } from 'app/shared/language/translate.directive'; @Component({ selector: 'jhi-learning-paths-configuration', standalone: true, - imports: [FontAwesomeModule, ArtemisSharedCommonModule, ArtemisSharedComponentModule], + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [FontAwesomeModule, ArtemisSharedComponentModule, TranslateDirective], templateUrl: './learning-paths-configuration.component.html', styleUrls: ['../../pages/learning-path-instructor-page/learning-path-instructor-page.component.scss'], }) @@ -32,7 +33,13 @@ export class LearningPathsConfigurationComponent { readonly includeAllGradedExercisesEnabled = computed(() => this.learningPathsConfiguration()?.includeAllGradedExercises ?? false); constructor() { - effect(() => this.loadLearningPathsConfiguration(this.courseId()), { allowSignalWrites: true }); + effect( + () => { + const courseId = this.courseId(); + untracked(() => this.loadLearningPathsConfiguration(courseId)); + }, + { allowSignalWrites: true }, + ); } private async loadLearningPathsConfiguration(courseId: number): Promise { diff --git a/src/main/webapp/app/course/learning-paths/components/learning-paths-state/learning-paths-state.component.ts b/src/main/webapp/app/course/learning-paths/components/learning-paths-state/learning-paths-state.component.ts index e5fc57092472..a8460830ca71 100644 --- a/src/main/webapp/app/course/learning-paths/components/learning-paths-state/learning-paths-state.component.ts +++ b/src/main/webapp/app/course/learning-paths/components/learning-paths-state/learning-paths-state.component.ts @@ -1,16 +1,19 @@ -import { Component, computed, effect, inject, input, signal } from '@angular/core'; +import { ChangeDetectionStrategy, Component, computed, effect, inject, input, signal, untracked } from '@angular/core'; import { LearningPathApiService } from 'app/course/learning-paths/services/learning-path-api.service'; import { HealthStatus, LearningPathHealthDTO } from 'app/entities/competency/learning-path-health.model'; import { AlertService } from 'app/core/util/alert.service'; import { onError } from 'app/shared/util/global.utils'; -import { ArtemisSharedCommonModule } from 'app/shared/shared-common.module'; import { ActivatedRoute, Router } from '@angular/router'; import { faSpinner } from '@fortawesome/free-solid-svg-icons'; +import { TranslateDirective } from 'app/shared/language/translate.directive'; +import { CommonModule } from '@angular/common'; +import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; @Component({ selector: 'jhi-learning-paths-state', standalone: true, - imports: [ArtemisSharedCommonModule], + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [TranslateDirective, CommonModule, FontAwesomeModule], templateUrl: './learning-paths-state.component.html', styleUrls: ['./learning-paths-state.component.scss', '../../pages/learning-path-instructor-page/learning-path-instructor-page.component.scss'], }) @@ -42,7 +45,13 @@ export class LearningPathsStateComponent { readonly learningPathHealthState = computed(() => this.learningPathHealth()?.status ?? []); constructor() { - effect(() => this.loadLearningPathHealthState(this.courseId()), { allowSignalWrites: true }); + effect( + () => { + const courseId = this.courseId(); + untracked(() => this.loadLearningPathHealthState(courseId)); + }, + { allowSignalWrites: true }, + ); } protected async loadLearningPathHealthState(courseId: number): Promise { diff --git a/src/main/webapp/app/course/learning-paths/components/learning-paths-table/learning-paths-table.component.html b/src/main/webapp/app/course/learning-paths/components/learning-paths-table/learning-paths-table.component.html index 4f4b23a690e0..a97a5bbcd377 100644 --- a/src/main/webapp/app/course/learning-paths/components/learning-paths-table/learning-paths-table.component.html +++ b/src/main/webapp/app/course/learning-paths/components/learning-paths-table/learning-paths-table.component.html @@ -55,6 +55,7 @@
(false); constructor() { - effect(() => this.loadCourse(this.courseId()), { allowSignalWrites: true }); + effect( + () => { + const courseId = this.courseId(); + untracked(() => this.loadCourse(courseId)); + }, + { allowSignalWrites: true }, + ); } private async loadCourse(courseId: number): Promise { diff --git a/src/main/webapp/app/course/learning-paths/pages/learning-path-student-page/learning-path-student-page.component.ts b/src/main/webapp/app/course/learning-paths/pages/learning-path-student-page/learning-path-student-page.component.ts index 7d83742015f0..72c77fbf2d40 100644 --- a/src/main/webapp/app/course/learning-paths/pages/learning-path-student-page/learning-path-student-page.component.ts +++ b/src/main/webapp/app/course/learning-paths/pages/learning-path-student-page/learning-path-student-page.component.ts @@ -1,4 +1,4 @@ -import { Component, effect, inject, signal } from '@angular/core'; +import { ChangeDetectionStrategy, Component, effect, inject, signal, untracked } from '@angular/core'; import { LearningObjectType, LearningPathDTO } from 'app/entities/competency/learning-path.model'; import { map } from 'rxjs'; import { toSignal } from '@angular/core/rxjs-interop'; @@ -16,6 +16,7 @@ import { TranslateDirective } from 'app/shared/language/translate.directive'; selector: 'jhi-learning-path-student-page', templateUrl: './learning-path-student-page.component.html', styleUrl: './learning-path-student-page.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, imports: [LearningPathNavComponent, LearningPathLectureUnitComponent, LearningPathExerciseComponent, TranslateDirective], }) @@ -34,7 +35,13 @@ export class LearningPathStudentPageComponent { readonly isLearningPathNavigationLoading = this.learningPathNavigationService.isLoading; constructor() { - effect(() => this.loadLearningPath(this.courseId()), { allowSignalWrites: true }); + effect( + () => { + const courseId = this.courseId(); + untracked(() => this.loadLearningPath(courseId)); + }, + { allowSignalWrites: true }, + ); } private async loadLearningPath(courseId: number): Promise { diff --git a/src/test/javascript/spec/component/learning-paths/pages/learning-path-student-page.component.spec.ts b/src/test/javascript/spec/component/learning-paths/pages/learning-path-student-page.component.spec.ts index 3c4d22ecbf22..19b21643e037 100644 --- a/src/test/javascript/spec/component/learning-paths/pages/learning-path-student-page.component.spec.ts +++ b/src/test/javascript/spec/component/learning-paths/pages/learning-path-student-page.component.spec.ts @@ -1,6 +1,6 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { LearningPathStudentPageComponent } from 'app/course/learning-paths/pages/learning-path-student-page/learning-path-student-page.component'; -import { provideHttpClient } from '@angular/common/http'; +import { HttpErrorResponse, provideHttpClient } from '@angular/common/http'; import { ActivatedRoute } from '@angular/router'; import { TranslateService } from '@ngx-translate/core'; import { of } from 'rxjs'; @@ -12,7 +12,6 @@ import { MockTranslateService } from '../../../helpers/mocks/service/mock-transl import { LearningPathApiService } from 'app/course/learning-paths/services/learning-path-api.service'; import { AlertService } from 'app/core/util/alert.service'; import { MockAlertService } from '../../../helpers/mocks/service/mock-alert.service'; -import { EntityNotFoundError } from 'app/course/learning-paths/exceptions/entity-not-found.error'; import { LearningPathDTO } from 'app/entities/competency/learning-path.model'; import { provideHttpClientTesting } from '@angular/common/http/testing'; @@ -127,7 +126,7 @@ describe('LearningPathStudentPageComponent', () => { }); it('should generate learning path on start when not found', async () => { - jest.spyOn(learningPathApiService, 'getLearningPathForCurrentUser').mockReturnValueOnce(Promise.reject(new EntityNotFoundError())); + jest.spyOn(learningPathApiService, 'getLearningPathForCurrentUser').mockReturnValueOnce(Promise.reject(new HttpErrorResponse({ status: 404 }))); const generateLearningPathSpy = jest.spyOn(learningPathApiService, 'generateLearningPathForCurrentUser').mockResolvedValue(learningPath); const startSpy = jest.spyOn(learningPathApiService, 'startLearningPathForCurrentUser'); From 0ffa44f80c3654f8e0e18ccee007a6acf6bf0e9f Mon Sep 17 00:00:00 2001 From: Simon Entholzer <33342534+SimonEntholzer@users.noreply.github.com> Date: Sat, 16 Nov 2024 10:12:17 +0100 Subject: [PATCH 14/21] Lectures: Use attachment name for file downloads (#9775) --- .../aet/artemis/core/web/FileResource.java | 50 ++++++++++++------- 1 file changed, 31 insertions(+), 19 deletions(-) diff --git a/src/main/java/de/tum/cit/aet/artemis/core/web/FileResource.java b/src/main/java/de/tum/cit/aet/artemis/core/web/FileResource.java index ac1e03f866da..27332e5343a7 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/web/FileResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/web/FileResource.java @@ -219,7 +219,7 @@ public ResponseEntity getMarkdownFileForConversation(@PathVariable Long public ResponseEntity getMarkdownFile(@PathVariable String filename) { log.debug("REST request to get file : {}", filename); sanitizeFilenameElseThrow(filename); - return buildFileResponse(FilePathService.getMarkdownFilePath(), filename); + return buildFileResponse(FilePathService.getMarkdownFilePath(), filename, false); } /** @@ -431,7 +431,7 @@ public ResponseEntity getLectureAttachment(@PathVariable Long lectureId, // check if the user is authorized to access the requested attachment unit checkAttachmentAuthorizationOrThrow(course, attachment); - return buildFileResponse(getActualPathFromPublicPathString(attachment.getLink()), false); + return buildFileResponse(getActualPathFromPublicPathString(attachment.getLink()), Optional.of(attachment.getName())); } /** @@ -470,13 +470,13 @@ public ResponseEntity getLecturePdfAttachmentsMerged(@PathVariable Long /** * GET files/attachments/attachment-unit/:attachmentUnitId/:filename : Get the lecture unit attachment + * Accesses to this endpoint are created by the server itself in the FilePathService * * @param attachmentUnitId ID of the attachment unit, the attachment belongs to * @return The requested file, 403 if the logged-in user is not allowed to access it, or 404 if the file doesn't exist */ @GetMapping("files/attachments/attachment-unit/{attachmentUnitId}/*") @EnforceAtLeastStudent - // TODO: this method is kind of redundant to the method getAttachmentFile below, double check if both are actually needed public ResponseEntity getAttachmentUnitAttachment(@PathVariable Long attachmentUnitId) { log.debug("REST request to get the file for attachment unit {} for students", attachmentUnitId); AttachmentUnit attachmentUnit = attachmentUnitRepository.findByIdElseThrow(attachmentUnitId); @@ -487,8 +487,7 @@ public ResponseEntity getAttachmentUnitAttachment(@PathVariable Long att // check if the user is authorized to access the requested attachment unit checkAttachmentAuthorizationOrThrow(course, attachment); - - return buildFileResponse(getActualPathFromPublicPathString(attachment.getLink()), false); + return buildFileResponse(getActualPathFromPublicPathString(attachment.getLink()), Optional.of(attachment.getName())); } /** @@ -567,36 +566,49 @@ public ResponseEntity getAttachmentUnitAttachmentSlide(@PathVariable Lon } /** - * Builds the response with headers, body and content type for specified path and file name + * Builds the response with headers, body and content type for specified path containing the file name * - * @param path to the file - * @param filename the name of the file + * @param path to the file including the file name + * @param cache true if the response should contain a header that allows caching; false otherwise * @return response entity */ - private ResponseEntity buildFileResponse(Path path, String filename) { - return buildFileResponse(path, filename, false); + private ResponseEntity buildFileResponse(Path path, boolean cache) { + return buildFileResponse(path.getParent(), path.getFileName().toString(), Optional.empty(), cache); } /** * Builds the response with headers, body and content type for specified path containing the file name * - * @param path to the file including the file name - * @param cache true if the response should contain a header that allows caching; false otherwise + * @param path to the file including the file name + * @param filename the name of the file + * @param cache true if the response should contain a header that allows caching; false otherwise * @return response entity */ - private ResponseEntity buildFileResponse(Path path, boolean cache) { - return buildFileResponse(path.getParent(), path.getFileName().toString(), cache); + private ResponseEntity buildFileResponse(Path path, String filename, boolean cache) { + return buildFileResponse(path, filename, Optional.empty(), cache); } /** * Builds the response with headers, body and content type for specified path and file name * - * @param path to the file - * @param filename the name of the file - * @param cache true if the response should contain a header that allows caching; false otherwise + * @param path to the file + * @param replaceFilename replaces the downloaded file's name, if provided * @return response entity */ - private ResponseEntity buildFileResponse(Path path, String filename, boolean cache) { + private ResponseEntity buildFileResponse(Path path, Optional replaceFilename) { + return buildFileResponse(path.getParent(), path.getFileName().toString(), replaceFilename, false); + } + + /** + * Builds the response with headers, body and content type for specified path and file name + * + * @param path to the file + * @param filename the name of the file + * @param replaceFilename replaces the downloaded file's name, if provided + * @param cache true if the response should contain a header that allows caching; false otherwise + * @return response entity + */ + private ResponseEntity buildFileResponse(Path path, String filename, Optional replaceFilename, boolean cache) { try { Path actualPath = path.resolve(filename); byte[] file = fileService.getFileForPath(actualPath); @@ -611,7 +623,7 @@ private ResponseEntity buildFileResponse(Path path, String filename, boo String contentType = lowerCaseFilename.endsWith("htm") || lowerCaseFilename.endsWith("html") || lowerCaseFilename.endsWith("svg") || lowerCaseFilename.endsWith("svgz") ? "attachment" : "inline"; - headers.setContentDisposition(ContentDisposition.builder(contentType).filename(filename).build()); + headers.setContentDisposition(ContentDisposition.builder(contentType).filename(replaceFilename.orElse(filename)).build()); var response = ResponseEntity.ok().headers(headers).contentType(getMediaTypeFromFilename(filename)).header("filename", filename); if (cache) { From 7bf8bcdb5850129c511849cff81960a919623483 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20St=C3=B6hr?= <38322605+JohannesStoehr@users.noreply.github.com> Date: Sat, 16 Nov 2024 10:13:43 +0100 Subject: [PATCH 15/21] Adaptive learning: Improve import of competencies when some already exist (#9774) --- .../service/competency/CompetencyService.java | 14 +----- .../competency/CourseCompetencyService.java | 44 ++++++++++--------- .../competency/PrerequisiteService.java | 14 +----- .../competency-management-table.component.ts | 3 +- .../competency-management.component.ts | 5 ++- ...CompetencyPrerequisiteIntegrationTest.java | 24 ++++++++++ .../competency/CompetencyIntegrationTest.java | 6 +++ .../CourseCompetencyIntegrationTest.java | 6 +++ .../PrerequisiteIntegrationTest.java | 6 +++ 9 files changed, 74 insertions(+), 48 deletions(-) diff --git a/src/main/java/de/tum/cit/aet/artemis/atlas/service/competency/CompetencyService.java b/src/main/java/de/tum/cit/aet/artemis/atlas/service/competency/CompetencyService.java index 9fc79520601b..705c162c6341 100644 --- a/src/main/java/de/tum/cit/aet/artemis/atlas/service/competency/CompetencyService.java +++ b/src/main/java/de/tum/cit/aet/artemis/atlas/service/competency/CompetencyService.java @@ -2,9 +2,7 @@ import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_CORE; -import java.util.ArrayList; import java.util.Collection; -import java.util.HashMap; import java.util.List; import java.util.Set; import java.util.stream.Collectors; @@ -68,17 +66,7 @@ public CompetencyService(CompetencyRepository competencyRepository, Authorizatio * @return The set of imported competencies, each also containing the relations it is the tail competency for. */ public Set importCompetencies(Course course, Collection competencies, CompetencyImportOptionsDTO importOptions) { - var idToImportedCompetency = new HashMap(); - - for (var competency : competencies) { - Competency importedCompetency = new Competency(competency); - importedCompetency.setCourse(course); - - importedCompetency = competencyRepository.save(importedCompetency); - idToImportedCompetency.put(competency.getId(), new CompetencyWithTailRelationDTO(importedCompetency, new ArrayList<>())); - } - - return importCourseCompetencies(course, competencies, idToImportedCompetency, importOptions); + return importCourseCompetencies(course, competencies, importOptions, Competency::new); } /** diff --git a/src/main/java/de/tum/cit/aet/artemis/atlas/service/competency/CourseCompetencyService.java b/src/main/java/de/tum/cit/aet/artemis/atlas/service/competency/CourseCompetencyService.java index 8e47f7443297..cbe33e70b710 100644 --- a/src/main/java/de/tum/cit/aet/artemis/atlas/service/competency/CourseCompetencyService.java +++ b/src/main/java/de/tum/cit/aet/artemis/atlas/service/competency/CourseCompetencyService.java @@ -8,7 +8,6 @@ import java.util.HashMap; import java.util.HashSet; import java.util.List; -import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.function.Function; @@ -213,34 +212,39 @@ public void filterOutLearningObjectsThatUserShouldNotSee(CourseCompetency compet * @return The set of imported course competencies, each also containing the relations it is the tail competency for. */ public Set importCourseCompetencies(Course course, Collection courseCompetencies, CompetencyImportOptionsDTO importOptions) { - var idToImportedCompetency = new HashMap(); - - for (var courseCompetency : courseCompetencies) { - CourseCompetency importedCompetency = switch (courseCompetency) { - case Competency competency -> new Competency(competency); - case Prerequisite prerequisite -> new Prerequisite(prerequisite); - default -> throw new IllegalStateException("Unexpected value: " + courseCompetency); - }; - importedCompetency.setCourse(course); + Function createNewCourseCompetency = courseCompetency -> switch (courseCompetency) { + case Competency competency -> new Competency(competency); + case Prerequisite prerequisite -> new Prerequisite(prerequisite); + default -> throw new IllegalStateException("Unexpected value: " + courseCompetency); + }; - importedCompetency = courseCompetencyRepository.save(importedCompetency); - idToImportedCompetency.put(courseCompetency.getId(), new CompetencyWithTailRelationDTO(importedCompetency, new ArrayList<>())); - } - - return importCourseCompetencies(course, courseCompetencies, idToImportedCompetency, importOptions); + return importCourseCompetencies(course, courseCompetencies, importOptions, createNewCourseCompetency); } /** * Imports the given competencies and relations into a course * - * @param course the course to import into - * @param competenciesToImport the source competencies that were imported - * @param idToImportedCompetency map of original competency id to imported competency - * @param importOptions the import options + * @param course the course to import into + * @param competenciesToImport the source competencies that were imported + * @param importOptions the import options + * @param createNewCourseCompetency the function that creates new course competencies * @return The set of imported competencies, each also containing the relations it is the tail competency for. */ public Set importCourseCompetencies(Course course, Collection competenciesToImport, - Map idToImportedCompetency, CompetencyImportOptionsDTO importOptions) { + CompetencyImportOptionsDTO importOptions, Function createNewCourseCompetency) { + var idToImportedCompetency = new HashMap(); + + Set competenciesInCourse = courseCompetencyRepository.findAllForCourse(course.getId()); + + for (var courseCompetency : competenciesToImport) { + Optional existingCompetency = competenciesInCourse.stream().filter(competency -> competency.getTitle().equals(courseCompetency.getTitle())) + .filter(competency -> competency.getType().equals(courseCompetency.getType())).findFirst(); + CourseCompetency importedCompetency = existingCompetency.orElse(createNewCourseCompetency.apply(courseCompetency)); + importedCompetency.setCourse(course); + idToImportedCompetency.put(courseCompetency.getId(), new CompetencyWithTailRelationDTO(importedCompetency, new ArrayList<>())); + } + courseCompetencyRepository.saveAll(idToImportedCompetency.values().stream().map(CompetencyWithTailRelationDTO::competency).toList()); + if (course.getLearningPathsEnabled()) { var importedCompetencies = idToImportedCompetency.values().stream().map(CompetencyWithTailRelationDTO::competency).toList(); learningPathService.linkCompetenciesToLearningPathsOfCourse(importedCompetencies, course.getId()); diff --git a/src/main/java/de/tum/cit/aet/artemis/atlas/service/competency/PrerequisiteService.java b/src/main/java/de/tum/cit/aet/artemis/atlas/service/competency/PrerequisiteService.java index 996ebd7d4385..eb66a98d641f 100644 --- a/src/main/java/de/tum/cit/aet/artemis/atlas/service/competency/PrerequisiteService.java +++ b/src/main/java/de/tum/cit/aet/artemis/atlas/service/competency/PrerequisiteService.java @@ -2,9 +2,7 @@ import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_CORE; -import java.util.ArrayList; import java.util.Collection; -import java.util.HashMap; import java.util.List; import java.util.Set; @@ -59,17 +57,7 @@ public PrerequisiteService(PrerequisiteRepository prerequisiteRepository, Author * @return The set of imported prerequisites, each also containing the relations for which it is the tail prerequisite for. */ public Set importPrerequisites(Course course, Collection prerequisites, CompetencyImportOptionsDTO importOptions) { - var idToImportedPrerequisite = new HashMap(); - - for (var prerequisite : prerequisites) { - Prerequisite importedPrerequisite = new Prerequisite(prerequisite); - importedPrerequisite.setCourse(course); - - importedPrerequisite = prerequisiteRepository.save(importedPrerequisite); - idToImportedPrerequisite.put(prerequisite.getId(), new CompetencyWithTailRelationDTO(importedPrerequisite, new ArrayList<>())); - } - - return importCourseCompetencies(course, prerequisites, idToImportedPrerequisite, importOptions); + return importCourseCompetencies(course, prerequisites, importOptions, Prerequisite::new); } /** diff --git a/src/main/webapp/app/course/competencies/competency-management/competency-management-table.component.ts b/src/main/webapp/app/course/competencies/competency-management/competency-management-table.component.ts index 28d4e4399ed9..7d939bdf5c67 100644 --- a/src/main/webapp/app/course/competencies/competency-management/competency-management-table.component.ts +++ b/src/main/webapp/app/course/competencies/competency-management/competency-management-table.component.ts @@ -95,7 +95,8 @@ export class CompetencyManagementTableComponent implements OnInit, OnDestroy { */ updateDataAfterImportAll(res: Array) { const importedCompetencies = res.map((dto) => dto.competency).filter((element): element is CourseCompetency => !!element); - this.courseCompetencies.push(...importedCompetencies); + const newCourseCompetencies = importedCompetencies.filter((competency) => !this.courseCompetencies.some((existingCompetency) => existingCompetency.id === competency.id)); + this.courseCompetencies.push(...newCourseCompetencies); this.allCompetencies.update((allCourseCompetencies) => allCourseCompetencies.concat(importedCompetencies)); } diff --git a/src/main/webapp/app/course/competencies/competency-management/competency-management.component.ts b/src/main/webapp/app/course/competencies/competency-management/competency-management.component.ts index ddad887fc1a2..c0c3c3936f71 100644 --- a/src/main/webapp/app/course/competencies/competency-management/competency-management.component.ts +++ b/src/main/webapp/app/course/competencies/competency-management/competency-management.component.ts @@ -155,7 +155,10 @@ export class CompetencyManagementComponent implements OnInit { */ updateDataAfterImportAll(res: Array) { const importedCourseCompetencies = res.map((dto) => dto.competency!); - this.courseCompetencies.update((courseCompetencies) => courseCompetencies.concat(importedCourseCompetencies)); + const newCourseCompetencies = importedCourseCompetencies.filter( + (competency) => !this.courseCompetencies().some((existingCompetency) => existingCompetency.id === competency.id), + ); + this.courseCompetencies.update((courseCompetencies) => courseCompetencies.concat(newCourseCompetencies)); } onRemoveCompetency(competencyId: number) { diff --git a/src/test/java/de/tum/cit/aet/artemis/atlas/competency/AbstractCompetencyPrerequisiteIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/atlas/competency/AbstractCompetencyPrerequisiteIntegrationTest.java index f2f393231477..3a2f639c719f 100644 --- a/src/test/java/de/tum/cit/aet/artemis/atlas/competency/AbstractCompetencyPrerequisiteIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/atlas/competency/AbstractCompetencyPrerequisiteIntegrationTest.java @@ -2,6 +2,7 @@ import static de.tum.cit.aet.artemis.core.util.TestResourceUtils.HalfSecond; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Fail.fail; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; @@ -16,6 +17,7 @@ import org.springframework.http.HttpStatus; import de.tum.cit.aet.artemis.atlas.AbstractAtlasIntegrationTest; +import de.tum.cit.aet.artemis.atlas.domain.competency.Competency; import de.tum.cit.aet.artemis.atlas.domain.competency.CompetencyExerciseLink; import de.tum.cit.aet.artemis.atlas.domain.competency.CompetencyLectureUnitLink; import de.tum.cit.aet.artemis.atlas.domain.competency.CompetencyRelation; @@ -27,6 +29,7 @@ import de.tum.cit.aet.artemis.atlas.dto.CompetencyImportResponseDTO; import de.tum.cit.aet.artemis.atlas.dto.CompetencyWithTailRelationDTO; import de.tum.cit.aet.artemis.core.domain.Course; +import de.tum.cit.aet.artemis.core.domain.DomainObject; import de.tum.cit.aet.artemis.core.domain.User; import de.tum.cit.aet.artemis.exercise.domain.ExerciseMode; import de.tum.cit.aet.artemis.exercise.domain.IncludedInOverallScore; @@ -511,6 +514,27 @@ void shouldImportAllCompetencies(Function createCourse assertThat(competencyDTOList.get(1).tailRelations()).isNull(); } + // Test + void shouldImportAllCompetenciesWithSomeExisting(Function copyCourseCompetency, int expectedCourseCompetencies) throws Exception { + competencyUtilService.createCompetencies(course, 2); + prerequisiteUtilService.createPrerequisites(course, 2); + + CourseCompetency newCompetency = copyCourseCompetency.apply(courseCompetency); + newCompetency.setCourse(course2); + newCompetency = courseCompetencyRepository.save(newCompetency); + + CompetencyImportOptionsDTO importOptions = new CompetencyImportOptionsDTO(Set.of(), Optional.of(course.getId()), false, false, false, Optional.empty(), false); + importAllCall(course2.getId(), importOptions, HttpStatus.CREATED); + + course2 = courseRepository.findByIdWithExercisesAndLecturesAndLectureUnitsAndCompetenciesElseThrow(course2.getId()); + assertThat(course2.getPrerequisites().size() + course2.getCompetencies().size()).isEqualTo(expectedCourseCompetencies); + switch (newCompetency) { + case Competency competency -> assertThat(course2.getCompetencies().stream().map(DomainObject::getId)).contains(competency.getId()); + case Prerequisite prerequisite -> assertThat(course2.getPrerequisites().stream().map(DomainObject::getId)).contains(prerequisite.getId()); + default -> fail("Unexpected CourseCompetency subclass"); + } + } + // Test void shouldImportAllExerciseAndLectureWithCompetency() throws Exception { createProgrammingExercise(ZonedDateTime.now(), ZonedDateTime.now()); diff --git a/src/test/java/de/tum/cit/aet/artemis/atlas/competency/CompetencyIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/atlas/competency/CompetencyIntegrationTest.java index e4e59099d9e5..2f1ccc5c8536 100644 --- a/src/test/java/de/tum/cit/aet/artemis/atlas/competency/CompetencyIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/atlas/competency/CompetencyIntegrationTest.java @@ -284,6 +284,12 @@ void shouldImportAllCompetencies() throws Exception { super.shouldImportAllCompetencies(competencyUtilService::createCompetency); } + @Test + @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") + void shouldImportAllCompetenciesWithSomeExisting() throws Exception { + shouldImportAllCompetenciesWithSomeExisting(Competency::new, 3); + } + @Test @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") void shouldImportAllExerciseAndLectureWithCompetency() throws Exception { diff --git a/src/test/java/de/tum/cit/aet/artemis/atlas/competency/CourseCompetencyIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/atlas/competency/CourseCompetencyIntegrationTest.java index 9b33684117fa..b12b09b6e191 100644 --- a/src/test/java/de/tum/cit/aet/artemis/atlas/competency/CourseCompetencyIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/atlas/competency/CourseCompetencyIntegrationTest.java @@ -194,6 +194,12 @@ void shouldImportAllCompetencies() throws Exception { super.shouldImportAllCompetencies(competencyUtilService::createCompetency); } + @Test + @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") + void shouldImportAllCompetenciesWithSomeExisting() throws Exception { + shouldImportAllCompetenciesWithSomeExisting(Competency::new, 5); + } + @Test @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") void shouldImportAllExerciseAndLectureWithCompetency() throws Exception { diff --git a/src/test/java/de/tum/cit/aet/artemis/atlas/competency/PrerequisiteIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/atlas/competency/PrerequisiteIntegrationTest.java index 42293964287f..9e84044aa6c9 100644 --- a/src/test/java/de/tum/cit/aet/artemis/atlas/competency/PrerequisiteIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/atlas/competency/PrerequisiteIntegrationTest.java @@ -278,6 +278,12 @@ void shouldImportAllCompetencies() throws Exception { super.shouldImportAllCompetencies(prerequisiteUtilService::createPrerequisite); } + @Test + @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") + void shouldImportAllCompetenciesWithSomeExisting() throws Exception { + shouldImportAllCompetenciesWithSomeExisting(Prerequisite::new, 3); + } + @Test @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") void shouldImportAllExerciseAndLectureWithCompetency() throws Exception { From 9d543b972cb1c63a18d99d764c2eff1c385962c3 Mon Sep 17 00:00:00 2001 From: Asli Aykan <56061820+asliayk@users.noreply.github.com> Date: Sat, 16 Nov 2024 10:18:12 +0100 Subject: [PATCH 16/21] Communication: Add profile picture to user selector (#9764) --- .../course-users-selector.component.html | 19 ++++++++++++++++++- .../course-users-selector.module.ts | 3 ++- .../course-users-selector.component.spec.ts | 17 +++++++++++++++-- 3 files changed, 35 insertions(+), 4 deletions(-) diff --git a/src/main/webapp/app/shared/course-users-selector/course-users-selector.component.html b/src/main/webapp/app/shared/course-users-selector/course-users-selector.component.html index e2bc97df120d..d3e4254e97d6 100644 --- a/src/main/webapp/app/shared/course-users-selector/course-users-selector.component.html +++ b/src/main/webapp/app/shared/course-users-selector/course-users-selector.component.html @@ -13,13 +13,30 @@ [placeholder]="'artemisApp.userSelector.placeholderText' | artemisTranslate" [ngbTypeahead]="search" [inputFormatter]="usersFormatter" - [resultFormatter]="usersFormatter" + [resultTemplate]="resultTemplate" [editable]="false" [focusFirst]="false" (input)="onInputChange($event)" placement="bottom-start" #instance="ngbTypeahead" /> + +
+ + {{ getUserLabel(user) }} +
+
@if (isSearching) {
diff --git a/src/main/webapp/app/shared/course-users-selector/course-users-selector.module.ts b/src/main/webapp/app/shared/course-users-selector/course-users-selector.module.ts index ca44726f11bb..8c47180607d0 100644 --- a/src/main/webapp/app/shared/course-users-selector/course-users-selector.module.ts +++ b/src/main/webapp/app/shared/course-users-selector/course-users-selector.module.ts @@ -4,9 +4,10 @@ import { ArtemisSharedModule } from 'app/shared/shared.module'; import { ArtemisSharedComponentModule } from 'app/shared/components/shared-component.module'; import { NgModule } from '@angular/core'; import { CourseUsersSelectorComponent } from 'app/shared/course-users-selector/course-users-selector.component'; +import { ProfilePictureComponent } from 'app/shared/profile-picture/profile-picture.component'; @NgModule({ - imports: [CommonModule, FormsModule, ReactiveFormsModule, ArtemisSharedModule, ArtemisSharedComponentModule], + imports: [CommonModule, FormsModule, ReactiveFormsModule, ArtemisSharedModule, ArtemisSharedComponentModule, ProfilePictureComponent], exports: [CourseUsersSelectorComponent], declarations: [CourseUsersSelectorComponent], }) diff --git a/src/test/javascript/spec/component/shared/course-users-selector/course-users-selector.component.spec.ts b/src/test/javascript/spec/component/shared/course-users-selector/course-users-selector.component.spec.ts index ffb59f1825a3..74693eb15ba5 100644 --- a/src/test/javascript/spec/component/shared/course-users-selector/course-users-selector.component.spec.ts +++ b/src/test/javascript/spec/component/shared/course-users-selector/course-users-selector.component.spec.ts @@ -6,7 +6,7 @@ import { NgbTypeaheadModule } from '@ng-bootstrap/ng-bootstrap'; import { By } from '@angular/platform-browser'; import { of } from 'rxjs'; import { HttpResponse } from '@angular/common/http'; -import { MockPipe, MockProvider } from 'ng-mocks'; +import { MockComponent, MockPipe, MockProvider } from 'ng-mocks'; import { CourseManagementService } from 'app/course/manage/course-management.service'; import { ArtemisTranslatePipe } from 'app/shared/pipes/artemis-translate.pipe'; import { CourseUsersSelectorComponent, SearchRoleGroup } from 'app/shared/course-users-selector/course-users-selector.component'; @@ -14,6 +14,7 @@ import { UserPublicInfoDTO } from 'app/core/user/user.model'; import { ArtemisSharedComponentModule } from 'app/shared/components/shared-component.module'; import { ArtemisSharedModule } from 'app/shared/shared.module'; import { TranslateModule } from '@ngx-translate/core'; +import { ProfilePictureComponent } from 'app/shared/profile-picture/profile-picture.component'; @Component({ template: ` @@ -48,7 +49,7 @@ describe('CourseUsersSelectorComponent', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ imports: [CommonModule, FormsModule, ReactiveFormsModule, NgbTypeaheadModule, ArtemisSharedModule, ArtemisSharedComponentModule, TranslateModule.forRoot()], - declarations: [CourseUsersSelectorComponent, WrapperComponent, MockPipe(ArtemisTranslatePipe)], + declarations: [CourseUsersSelectorComponent, WrapperComponent, MockPipe(ArtemisTranslatePipe), MockComponent(ProfilePictureComponent)], providers: [MockProvider(CourseManagementService)], }).compileComponents(); })); @@ -142,6 +143,18 @@ describe('CourseUsersSelectorComponent', () => { expect(userSelectorComponent.selectedUsers).toEqual([exampleUserPublicInfoDTO]); expect(fixture.debugElement.query(By.css('.delete-user'))).toBeFalsy(); })); + + it('should render profile picture for users in dropdown', fakeAsync(() => { + const user = generateExampleUserPublicInfoDTO({}); + searchUsersSpy.mockReturnValue(of(new HttpResponse({ body: [user], status: 200 }))); + + changeInput(fixture.debugElement.nativeElement, 'test'); + tick(1000); + fixture.detectChanges(); + + const profilePicture = fixture.debugElement.query(By.directive(MockComponent(ProfilePictureComponent))); + expect(profilePicture).not.toBeNull(); + })); }); function getNativeInput(element: HTMLElement): HTMLInputElement { From 7bf4a93b29fff1a9e5a990897325165fb054d0f0 Mon Sep 17 00:00:00 2001 From: Simon Entholzer <33342534+SimonEntholzer@users.noreply.github.com> Date: Sat, 16 Nov 2024 10:19:26 +0100 Subject: [PATCH 17/21] Development: Reactivate auxiliary repository integration tests for LocalVC (#9763) --- ...grammingIntegrationLocalCILocalVCTest.java | 3 + ...gExerciseIntegrationJenkinsGitlabTest.java | 18 ++ ...rammingExerciseIntegrationTestService.java | 37 ++-- ...rammingExerciseLocalVCIntegrationTest.java | 203 ++++++++++++++++++ 4 files changed, 238 insertions(+), 23 deletions(-) create mode 100644 src/test/java/de/tum/cit/aet/artemis/programming/ProgrammingExerciseLocalVCIntegrationTest.java diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/AbstractProgrammingIntegrationLocalCILocalVCTest.java b/src/test/java/de/tum/cit/aet/artemis/programming/AbstractProgrammingIntegrationLocalCILocalVCTest.java index fcbdc0bdef78..56adec3eb361 100644 --- a/src/test/java/de/tum/cit/aet/artemis/programming/AbstractProgrammingIntegrationLocalCILocalVCTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/programming/AbstractProgrammingIntegrationLocalCILocalVCTest.java @@ -52,6 +52,9 @@ public abstract class AbstractProgrammingIntegrationLocalCILocalVCTest extends A // External Repositories // Services + @Autowired + protected ProgrammingExerciseIntegrationTestService programmingExerciseIntegrationTestService; + @Autowired protected AeolusRequestMockProvider aeolusRequestMockProvider; diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/ProgrammingExerciseIntegrationJenkinsGitlabTest.java b/src/test/java/de/tum/cit/aet/artemis/programming/ProgrammingExerciseIntegrationJenkinsGitlabTest.java index 5812c2923e0e..4646464e8458 100644 --- a/src/test/java/de/tum/cit/aet/artemis/programming/ProgrammingExerciseIntegrationJenkinsGitlabTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/programming/ProgrammingExerciseIntegrationJenkinsGitlabTest.java @@ -74,6 +74,18 @@ void testExportSubmissionsByParticipationIds_includePracticeSubmissions() throws programmingExerciseIntegrationTestService.testExportSubmissionsByParticipationIds_includePracticeSubmissions(); } + @Test + @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") + void testExportSubmissionsByParticipationIds_addParticipantIdentifierToProjectNameError() throws Exception { + programmingExerciseIntegrationTestService.testExportSubmissionsByParticipationIds_addParticipantIdentifierToProjectNameError(); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") + void testExportSubmissionsByParticipationIds_addParticipantIdentifierToProjectName() throws Exception { + programmingExerciseIntegrationTestService.testExportSubmissionsByParticipationIds_addParticipantIdentifierToProjectName(); + } + @Test @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") void testExportSubmissionsByParticipationIds() throws Exception { @@ -562,6 +574,12 @@ void createProgrammingExercise_projectTypeNotExpected_badRequest() throws Except programmingExerciseIntegrationTestService.createProgrammingExercise_projectTypeNotExpected_badRequest(); } + @Test + @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") + void createProgrammingExercise_onlineCodeEditorNotExpected_badRequest() throws Exception { + programmingExerciseIntegrationTestService.createProgrammingExercise_onlineCodeEditorNotExpected_badRequest(); + } + private static Set generateSupportedLanguagesWithoutHaskell() { Set supportedLanguages = ArgumentSources.generateJenkinsSupportedLanguages(); supportedLanguages.remove(ProgrammingLanguage.HASKELL); diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/ProgrammingExerciseIntegrationTestService.java b/src/test/java/de/tum/cit/aet/artemis/programming/ProgrammingExerciseIntegrationTestService.java index d35b974e32c7..6b716d45b5a9 100644 --- a/src/test/java/de/tum/cit/aet/artemis/programming/ProgrammingExerciseIntegrationTestService.java +++ b/src/test/java/de/tum/cit/aet/artemis/programming/ProgrammingExerciseIntegrationTestService.java @@ -16,6 +16,7 @@ import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.reset; import static org.mockito.Mockito.timeout; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -292,10 +293,6 @@ void setup(String userPrefix, MockDelegate mockDelegate, VersionControlService v GitService.commit(localGit).setMessage("empty").setAllowEmpty(true).setSign(false).setAuthor("test", "test@test.com").call(); localGit.push().call(); - // we use the temp repository as remote origin for all repositories that are created during the - // TODO: distinguish between template, test and solution - doReturn(new GitUtilService.MockFileRepositoryUri(remoteRepoFile)).when(versionControlService).getCloneRepositoryUri(anyString(), anyString()); - this.plagiarismChecksTestReposDir = Files.createTempDirectory("jplag-repos").toFile(); } @@ -1433,6 +1430,8 @@ void importProgrammingExercise_vcsProjectWithSameTitleAlreadyExists_badRequest() } void importProgrammingExercise_updatesTestCaseIds() throws Exception { + doReturn(new GitUtilService.MockFileRepositoryUri(remoteRepoFile)).when(versionControlService).getCloneRepositoryUri(anyString(), anyString()); + programmingExercise = programmingExerciseRepository.findByIdWithTemplateAndSolutionParticipationAndAuxiliaryRepositoriesElseThrow(programmingExercise.getId()); var tests = programmingExerciseUtilService.addTestCasesToProgrammingExercise(programmingExercise); var test1 = tests.getFirst(); @@ -1464,6 +1463,8 @@ void importProgrammingExercise_updatesTestCaseIds() throws Exception { var savedProgrammingExercise = programmingExerciseRepository.findByIdElseThrow(response.getId()); assertThat(savedProgrammingExercise.getProblemStatement()).isEqualTo(newProblemStatement); + + reset(versionControlService); } void exportSubmissionsByStudentLogins_notInstructorForExercise_forbidden() throws Exception { @@ -1920,11 +1921,11 @@ void testGetPlagiarismResultWithoutExercise() throws Exception { void testValidateValidAuxiliaryRepository() throws Exception { AuxiliaryRepositoryBuilder auxRepoBuilder = AuxiliaryRepositoryBuilder.defaults(); - testAuxRepo(auxRepoBuilder, HttpStatus.CREATED); + testAuxRepo(auxRepoBuilder, HttpStatus.OK); } void testValidateAuxiliaryRepositoryIdSetOnRequest() throws Exception { - testAuxRepo(AuxiliaryRepositoryBuilder.defaults().withId(0L), HttpStatus.BAD_REQUEST); + testAuxRepo(AuxiliaryRepositoryBuilder.defaults().withId(0L), HttpStatus.INTERNAL_SERVER_ERROR); } @@ -1953,12 +1954,12 @@ void testValidateAuxiliaryRepositoryWithInvalidCheckoutDirectory() throws Except void testValidateAuxiliaryRepositoryWithoutCheckoutDirectory() throws Exception { AuxiliaryRepositoryBuilder auxRepoBuilder = AuxiliaryRepositoryBuilder.defaults().withoutCheckoutDirectory(); - testAuxRepo(auxRepoBuilder, HttpStatus.CREATED); + testAuxRepo(auxRepoBuilder, HttpStatus.OK); } void testValidateAuxiliaryRepositoryWithBlankCheckoutDirectory() throws Exception { AuxiliaryRepositoryBuilder auxRepoBuilder = AuxiliaryRepositoryBuilder.defaults().withCheckoutDirectory(" "); - testAuxRepo(auxRepoBuilder, HttpStatus.CREATED); + testAuxRepo(auxRepoBuilder, HttpStatus.OK); } void testValidateAuxiliaryRepositoryWithTooLongCheckoutDirectory() throws Exception { @@ -1981,7 +1982,7 @@ void testValidateAuxiliaryRepositoryWithTooLongDescription() throws Exception { void testValidateAuxiliaryRepositoryWithoutDescription() throws Exception { AuxiliaryRepositoryBuilder auxRepoBuilder = AuxiliaryRepositoryBuilder.defaults().withoutDescription(); - testAuxRepo(auxRepoBuilder, HttpStatus.CREATED); + testAuxRepo(auxRepoBuilder, HttpStatus.OK); } void testGetAuxiliaryRepositoriesMissingExercise() throws Exception { @@ -2158,7 +2159,7 @@ public void addAuxiliaryRepositoryToExercise(ProgrammingExercise exercise) { } private String defaultAuxiliaryRepositoryEndpoint() { - return "/api/programming-exercises/setup"; + return "/api/programming-exercises"; } private String defaultResetEndpoint() { @@ -2191,17 +2192,7 @@ private void testAuxRepo(AuxiliaryRepositoryBuilder body, HttpStatus expectedSta private void testAuxRepo(List body, HttpStatus expectedStatus) throws Exception { programmingExercise.setAuxiliaryRepositories(body); - programmingExercise.setId(null); - programmingExercise.setSolutionParticipation(null); - programmingExercise.setTemplateParticipation(null); - programmingExercise.setChannelName("pe-test"); - programmingExercise.setShortName("ExerciseTitle"); - programmingExercise.setTitle("Title"); - if (expectedStatus == HttpStatus.CREATED) { - mockDelegate.mockConnectorRequestsForSetup(programmingExercise, false, false, false); - mockDelegate.mockGetProjectKeyFromAnyUrl(programmingExercise.getProjectKey()); - } - request.postWithResponseBody(defaultAuxiliaryRepositoryEndpoint(), programmingExercise, ProgrammingExercise.class, expectedStatus); + request.putWithResponseBody(defaultAuxiliaryRepositoryEndpoint(), programmingExercise, ProgrammingExercise.class, expectedStatus); } private static class AuxiliaryRepositoryBuilder { @@ -2275,8 +2266,8 @@ AuxiliaryRepository get() { } } - void testReEvaluateAndUpdateProgrammingExercise_instructorNotInCourse_forbidden() throws Exception { - userUtilService.addInstructor("other-instructors", userPrefix + "instructoralt"); + void testReEvaluateAndUpdateProgrammingExercise_instructorNotInCourse_forbidden(String testPrefix) throws Exception { + userUtilService.addInstructor("other-instructors", testPrefix + "instructoralt"); programmingExerciseUtilService.addCourseWithOneProgrammingExercise(); ProgrammingExercise programmingExercise = programmingExerciseTestRepository.findAllWithEagerTemplateAndSolutionParticipations().getFirst(); request.put("/api/programming-exercises/" + programmingExercise.getId() + "/re-evaluate", programmingExercise, HttpStatus.FORBIDDEN); diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/ProgrammingExerciseLocalVCIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/programming/ProgrammingExerciseLocalVCIntegrationTest.java new file mode 100644 index 000000000000..0194806e6028 --- /dev/null +++ b/src/test/java/de/tum/cit/aet/artemis/programming/ProgrammingExerciseLocalVCIntegrationTest.java @@ -0,0 +1,203 @@ +package de.tum.cit.aet.artemis.programming; + +import java.io.IOException; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.security.test.context.support.WithMockUser; + +class ProgrammingExerciseLocalVCIntegrationTest extends AbstractProgrammingIntegrationLocalCILocalVCTest { + + private static final String TEST_PREFIX = "programmingexerciselocalvc"; + + @BeforeEach + void initTestCase() throws Exception { + programmingExerciseIntegrationTestService.setup(TEST_PREFIX, this, versionControlService, continuousIntegrationService); + } + + @AfterEach + void tearDown() throws IOException { + programmingExerciseIntegrationTestService.tearDown(); + } + + protected String getTestPrefix() { + return TEST_PREFIX; + } + + @Test + @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") + void testValidateValidAuxiliaryRepository() throws Exception { + programmingExerciseIntegrationTestService.testValidateValidAuxiliaryRepository(); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") + void testValidateAuxiliaryRepositoryIdSetOnRequest() throws Exception { + programmingExerciseIntegrationTestService.testValidateAuxiliaryRepositoryIdSetOnRequest(); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") + void testValidateAuxiliaryRepositoryWithoutName() throws Exception { + programmingExerciseIntegrationTestService.testValidateAuxiliaryRepositoryWithoutName(); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") + void testValidateAuxiliaryRepositoryWithTooLongName() throws Exception { + programmingExerciseIntegrationTestService.testValidateAuxiliaryRepositoryWithTooLongName(); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") + void testValidateAuxiliaryRepositoryWithDuplicatedName() throws Exception { + programmingExerciseIntegrationTestService.testValidateAuxiliaryRepositoryWithDuplicatedName(); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") + void testValidateAuxiliaryRepositoryWithRestrictedName() throws Exception { + programmingExerciseIntegrationTestService.testValidateAuxiliaryRepositoryWithRestrictedName(); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") + void testValidateAuxiliaryRepositoryWithInvalidCheckoutDirectory() throws Exception { + programmingExerciseIntegrationTestService.testValidateAuxiliaryRepositoryWithInvalidCheckoutDirectory(); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") + void testValidateAuxiliaryRepositoryWithoutCheckoutDirectory() throws Exception { + programmingExerciseIntegrationTestService.testValidateAuxiliaryRepositoryWithoutCheckoutDirectory(); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") + void testValidateAuxiliaryRepositoryWithBlankCheckoutDirectory() throws Exception { + programmingExerciseIntegrationTestService.testValidateAuxiliaryRepositoryWithBlankCheckoutDirectory(); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") + void testValidateAuxiliaryRepositoryWithTooLongCheckoutDirectory() throws Exception { + programmingExerciseIntegrationTestService.testValidateAuxiliaryRepositoryWithTooLongCheckoutDirectory(); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") + void testValidateAuxiliaryRepositoryWithDuplicatedCheckoutDirectory() throws Exception { + programmingExerciseIntegrationTestService.testValidateAuxiliaryRepositoryWithDuplicatedCheckoutDirectory(); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") + void testValidateAuxiliaryRepositoryWithNullCheckoutDirectory() throws Exception { + programmingExerciseIntegrationTestService.testValidateAuxiliaryRepositoryWithNullCheckoutDirectory(); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") + void testValidateAuxiliaryRepositoryWithTooLongDescription() throws Exception { + programmingExerciseIntegrationTestService.testValidateAuxiliaryRepositoryWithTooLongDescription(); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") + void testValidateAuxiliaryRepositoryWithoutDescription() throws Exception { + programmingExerciseIntegrationTestService.testValidateAuxiliaryRepositoryWithoutDescription(); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") + void testGetAuxiliaryRepositoriesMissingExercise() throws Exception { + programmingExerciseIntegrationTestService.testGetAuxiliaryRepositoriesMissingExercise(); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") + void testGetAuxiliaryRepositoriesOk() throws Exception { + programmingExerciseIntegrationTestService.testGetAuxiliaryRepositoriesOk(); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") + void testGetAuxiliaryRepositoriesEmptyOk() throws Exception { + programmingExerciseIntegrationTestService.testGetAuxiliaryRepositoriesEmptyOk(); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "student1", roles = "USER") + void testGetAuxiliaryRepositoriesForbidden() throws Exception { + programmingExerciseIntegrationTestService.testGetAuxiliaryRepositoriesForbidden(); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "student1", roles = "USER") + void testExportAuxiliaryRepositoryForbidden() throws Exception { + programmingExerciseIntegrationTestService.testExportAuxiliaryRepositoryForbidden(); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") + void testExportAuxiliaryRepositoryBadRequest() throws Exception { + programmingExerciseIntegrationTestService.testExportAuxiliaryRepositoryBadRequest(); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") + void testExportAuxiliaryRepositoryExerciseNotFound() throws Exception { + programmingExerciseIntegrationTestService.testExportAuxiliaryRepositoryExerciseNotFound(); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") + void testExportAuxiliaryRepositoryRepositoryNotFound() throws Exception { + programmingExerciseIntegrationTestService.testExportAuxiliaryRepositoryRepositoryNotFound(); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "instructoralt1", roles = "INSTRUCTOR") + void testReEvaluateAndUpdateProgrammingExercise_instructorNotInCourse_forbidden() throws Exception { + programmingExerciseIntegrationTestService.testReEvaluateAndUpdateProgrammingExercise_instructorNotInCourse_forbidden(TEST_PREFIX); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") + void testReEvaluateAndUpdateProgrammingExercise_notFound() throws Exception { + programmingExerciseIntegrationTestService.testReEvaluateAndUpdateProgrammingExercise_notFound(); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") + void testReEvaluateAndUpdateProgrammingExercise_isNotSameGivenExerciseIdInRequestBody_conflict() throws Exception { + programmingExerciseIntegrationTestService.testReEvaluateAndUpdateProgrammingExercise_isNotSameGivenExerciseIdInRequestBody_conflict(); + } + // + // @Test + // @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") + // void test_redirectGetSolutionRepositoryFilesWithoutContent() throws Exception { + // programmingExerciseIntegrationTestService.test_redirectGetSolutionRepositoryFilesWithoutContent(); + // } + // + // @Test + // @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") + // void test_redirectGetTemplateRepositoryFilesWithContent() throws Exception { + // programmingExerciseIntegrationTestService.test_redirectGetTemplateRepositoryFilesWithContent(); + // } + // + // @Test + // @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") + // void testRedirectGetParticipationRepositoryFilesWithContentAtCommit() throws Exception { + // programmingExerciseIntegrationTestService.testRedirectGetParticipationRepositoryFilesWithContentAtCommit(); + // } + // + // @Test + // @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") + // void testRedirectGetParticipationRepositoryFilesWithContentAtCommitForbidden() throws Exception { + // programmingExerciseIntegrationTestService.testRedirectGetParticipationRepositoryFilesWithContentAtCommitForbidden(); + // } + + // TODO add all other tests +} From f05b0a5214d0f59d6722113a22c437fab927c577 Mon Sep 17 00:00:00 2001 From: Ajayvir Singh <38434017+AjayvirS@users.noreply.github.com> Date: Sat, 16 Nov 2024 10:22:40 +0100 Subject: [PATCH 18/21] Plagiarism checks: Fix scrolling issue on side-by-side viewer for modeling (#9748) --- .../shared/modeling-editor.component.html | 6 +++++- .../shared/modeling-editor.component.scss | 19 ++++++++++++++++++- .../exam/ExamExerciseGroupsPage.ts | 3 ++- 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/src/main/webapp/app/exercises/modeling/shared/modeling-editor.component.html b/src/main/webapp/app/exercises/modeling/shared/modeling-editor.component.html index d29c46e6c02f..1c7637eb8455 100644 --- a/src/main/webapp/app/exercises/modeling/shared/modeling-editor.component.html +++ b/src/main/webapp/app/exercises/modeling/shared/modeling-editor.component.html @@ -108,7 +108,11 @@