Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Development: Refactor competencies management page to signals #9629

Original file line number Diff line number Diff line change
@@ -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';
JohannesWt marked this conversation as resolved.
Show resolved Hide resolved
import { AlertService } from 'app/core/util/alert.service';
import { CompetencyWithTailRelationDTO, CourseCompetency, CourseCompetencyType, getIcon } from 'app/entities/competency.model';
Expand All @@ -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<CourseCompetency[]>();
@Input() competencyType: CourseCompetencyType;
@Input() standardizedCompetenciesEnabled: boolean;

Expand Down Expand Up @@ -96,7 +96,7 @@ export class CompetencyManagementTableComponent implements OnInit, OnDestroy {
updateDataAfterImportAll(res: Array<CompetencyWithTailRelationDTO>) {
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));
}

JohannesWt marked this conversation as resolved.
Show resolved Hide resolved
/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ <h2 class="m-0" jhiTranslate="artemisApp.competency.manage.title"></h2>
</button>
</div>
<div class="ms-auto justify-content-end">
@if (irisCompetencyGenerationEnabled) {
<a class="btn btn-primary" id="generateButton" [routerLink]="['/course-management', courseId, 'competency-management', 'generate']">
@if (irisCompetencyGenerationEnabled()) {
<a class="btn btn-primary" id="generateButton" [routerLink]="['/course-management', courseId(), 'competency-management', 'generate']">
<fa-icon [icon]="faRobot" />
<span jhiTranslate="artemisApp.competency.manage.generateButton"></span>
</a>
Expand All @@ -24,27 +24,27 @@ <h2 class="m-0" jhiTranslate="artemisApp.competency.manage.title"></h2>
</button>
</div>
</div>
@if (isLoading) {
@if (isLoading()) {
<div class="d-flex justify-content-center">
<div class="spinner-border" role="status">
<span class="sr-only" jhiTranslate="loading"></span>
</div>
</div>
}
<jhi-competency-management-table
[courseId]="courseId"
[courseCompetencies]="competencies"
[courseId]="courseId()"
[(allCompetencies)]="courseCompetencies"
[courseCompetencies]="competencies()"
[competencyType]="CourseCompetencyType.COMPETENCY"
[allCompetencies]="courseCompetencies"
[standardizedCompetenciesEnabled]="standardizedCompetenciesEnabled"
[standardizedCompetenciesEnabled]="standardizedCompetenciesEnabled()"
(competencyDeleted)="onRemoveCompetency($event)"
/>
<jhi-competency-management-table
[courseId]="courseId"
[courseCompetencies]="prerequisites"
[courseId]="courseId()"
[(allCompetencies)]="courseCompetencies"
[courseCompetencies]="prerequisites()"
[competencyType]="CourseCompetencyType.PREREQUISITE"
[allCompetencies]="courseCompetencies"
[standardizedCompetenciesEnabled]="standardizedCompetenciesEnabled"
[standardizedCompetenciesEnabled]="standardizedCompetenciesEnabled()"
(competencyDeleted)="onRemoveCompetency($event)"
/>
</div>
Original file line number Diff line number Diff line change
@@ -1,17 +1,15 @@
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';
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,
Expand All @@ -23,27 +21,15 @@ 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',
templateUrl: './competency-management.component.html',
standalone: true,
imports: [CompetencyManagementTableComponent, TranslateDirective, FontAwesomeModule, RouterModule, ArtemisSharedComponentModule],
})
export class CompetencyManagementComponent implements OnInit, OnDestroy {
courseId: number;
isLoading = false;
irisCompetencyGenerationEnabled = false;
private dialogErrorSource = new Subject<string>();
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;
Expand All @@ -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);
Expand All @@ -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<boolean>(false);
JohannesWt marked this conversation as resolved.
Show resolved Hide resolved

readonly courseCompetencies = signal<CourseCompetency[]>([]);
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<boolean>(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();
}
});
});
}

JohannesWt marked this conversation as resolved.
Show resolved Hide resolved
ngOnInit(): void {
JohannesWt marked this conversation as resolved.
Show resolved Hide resolved
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);
}
JohannesWt marked this conversation as resolved.
Show resolved Hide resolved
}

/**
* 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);
}
}

Expand All @@ -127,8 +114,8 @@ export class CompetencyManagementComponent implements OnInit, OnDestroy {
backdrop: 'static',
windowClass: 'course-competencies-relation-graph-modal',
});
modalRef.componentInstance.courseId = signal<number>(this.courseId);
modalRef.componentInstance.courseCompetencies = signal<CourseCompetency[]>(this.courseCompetencies);
modalRef.componentInstance.courseId = signal<number>(this.courseId());
modalRef.componentInstance.courseCompetencies = signal<CourseCompetency[]>(this.courseCompetencies());
}

/**
Expand All @@ -139,14 +126,14 @@ export class CompetencyManagementComponent implements OnInit, OnDestroy {
size: 'lg',
backdrop: 'static',
});
modalRef.componentInstance.courseId = signal<number>(this.courseId);
modalRef.componentInstance.courseId = signal<number>(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,
Expand All @@ -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);
}
}

Expand All @@ -167,18 +154,12 @@ export class CompetencyManagementComponent implements OnInit, OnDestroy {
* @private
*/
updateDataAfterImportAll(res: Array<CompetencyWithTailRelationDTO>) {
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));
JohannesWt marked this conversation as resolved.
Show resolved Hide resolved
}

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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ describe('CompetencyManagementTableComponent', () => {
});

it('should handle import all data', () => {
fixture.componentRef.setInput('allCompetencies', []);
fixture.detectChanges();
component.courseCompetencies = [];

const responseBody: CompetencyWithTailRelationDTO[] = [
Expand Down
Loading
Loading