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

Adaptive learning: Refactor competencies management page to signals #9629

Open
wants to merge 4 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,25 +24,25 @@ <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()"
[courseCompetencies]="competencies()"
[competencyType]="CourseCompetencyType.COMPETENCY"
[standardizedCompetenciesEnabled]="standardizedCompetenciesEnabled"
[standardizedCompetenciesEnabled]="standardizedCompetenciesEnabled()"
(competencyDeleted)="onRemoveCompetency($event)"
/>
<jhi-competency-management-table
[courseId]="courseId"
[courseCompetencies]="prerequisites"
[courseId]="courseId()"
[courseCompetencies]="prerequisites()"
[competencyType]="CourseCompetencyType.PREREQUISITE"
[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

private 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();
}
});
});
}

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);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Provide user-friendly error messages in alerts

Passing the error object directly to this.alertService.error may not display meaningful information to the user. It could show as [object Object]. Extract a user-friendly message from the error object or provide a custom error message to enhance the user's understanding.

Update the error handling as follows:

-this.alertService.error(error);
+const errorMessage = error.message ?? 'An unexpected error occurred';
+this.alertService.error(errorMessage);

Or use the AlertService's capability to handle error objects if available.

Also applies to: 105-105

}

/**
* 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
Loading
Loading