Skip to content

Development: Use signals in title channel name component #10979

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

Open
wants to merge 68 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 61 commits
Commits
Show all changes
68 commits
Select commit Hold shift + click to select a range
876dc2b
Make sure that inputs use signals
florian-glombik Jun 2, 2025
e0c4ee3
Replace formValid variable with signal
florian-glombik Jun 2, 2025
551996f
Use signal for improved reactivity in modeling component
florian-glombik Jun 2, 2025
d48131d
Remove form valid changes
florian-glombik Jun 2, 2025
4be9aed
Remove subscription
florian-glombik Jun 2, 2025
23caa0f
Fix mistakes
florian-glombik Jun 4, 2025
0accb5a
Make sure to use signals in template
florian-glombik Jun 4, 2025
32939d7
Fix remaining build issues
florian-glombik Jun 4, 2025
eaacbb4
Fix issues resulting from using viewChild
florian-glombik Jun 4, 2025
805d3d1
Fix text exercise updates
florian-glombik Jun 4, 2025
93ba4ae
Fix file upload exercise updates
florian-glombik Jun 4, 2025
c4e2ce4
Remove fallback that is not required anymore
florian-glombik Jun 4, 2025
d82eda3
Fix modeling exercise updates
florian-glombik Jun 4, 2025
5d16cbf
Make injections readonly
florian-glombik Jun 4, 2025
c5edf1d
Use signals in programming exercise component
florian-glombik Jun 4, 2025
854b2d8
Remove redundant notNull operators
florian-glombik Jun 4, 2025
6306388
Minor adjustments from self-review
florian-glombik Jun 4, 2025
c5cffa4
Make sure tests are running again
florian-glombik Jun 4, 2025
f7723c1
Remove not required fakeAsync
florian-glombik Jun 4, 2025
237d880
Merge branch 'develop' into chore/development/use-signals-in-title-ch…
florian-glombik Jun 5, 2025
a41fedc
Fix updateChannel name refactoring
florian-glombik Jun 6, 2025
b8aa322
Fix further tests
florian-glombik Jun 6, 2025
3ef2095
Fix last test in shared component
florian-glombik Jun 6, 2025
196005f
Fix file-upload tests
florian-glombik Jun 6, 2025
939e141
Fix text-exercise tests
florian-glombik Jun 6, 2025
6435564
Remove unused subscription
florian-glombik Jun 6, 2025
8ab8b0a
Fix typos
florian-glombik Jun 6, 2025
e2a882f
make constants readonly
florian-glombik Jun 6, 2025
b6fec64
Fix typo
florian-glombik Jun 6, 2025
7018b40
Make constants protected readonly
florian-glombik Jun 6, 2025
17dfef5
Fix modeling exercise update test
florian-glombik Jun 6, 2025
b0d3b07
Apply self-review
florian-glombik Jun 6, 2025
eb83655
Merge branch 'develop' into chore/development/use-signals-in-title-ch…
florian-glombik Jun 9, 2025
e372ea1
Improve logic for replacement regex
florian-glombik Jun 9, 2025
78c6c2d
Impove regExp readability
florian-glombik Jun 9, 2025
e7cdd2b
Fix tests
florian-glombik Jun 9, 2025
2135762
Add tests for coverage
florian-glombik Jun 9, 2025
9fe0576
Add test for registration translation
florian-glombik Jun 9, 2025
41b3e93
Add test for coverage
florian-glombik Jun 9, 2025
3ec953f
Reduce coverage
florian-glombik Jun 9, 2025
e23fea1
Add test for coverage
florian-glombik Jun 10, 2025
3cc6abf
Fix test
florian-glombik Jun 10, 2025
439156f
Add test setup
florian-glombik Jun 10, 2025
c62e699
Add test
florian-glombik Jun 10, 2025
69687fd
Add more tests
florian-glombik Jun 10, 2025
fd792f5
Improve coverage by using nullish concealing
florian-glombik Jun 10, 2025
8a1dfa2
Fix coverage conditions
florian-glombik Jun 11, 2025
d6f4478
Fix coverage conditions
florian-glombik Jun 11, 2025
9e3e9db
Fix pre build
florian-glombik Jun 11, 2025
629c60a
Merge branch 'develop' into chore/development/use-signals-in-title-ch…
florian-glombik Jun 11, 2025
3076fd6
Trigger pipeline to receive a build that can be deployed to the test …
florian-glombik Jun 12, 2025
ce00a59
Make Coderabbit happy
florian-glombik Jun 12, 2025
9958abd
Revert credential.util.spec.ts changes
florian-glombik Jun 13, 2025
41e0568
Revert doughnut-chart.component.spec.ts changes
florian-glombik Jun 13, 2025
0f31eae
Remove modeling-exercise-resolver.service.spec.ts
florian-glombik Jun 13, 2025
0b95834
Remove exif.utils.spec.ts
florian-glombik Jun 13, 2025
b16d725
Remove text-exercise-resolver.service.spec.ts
florian-glombik Jun 13, 2025
c193f03
Remove resize.utils.spec.ts
florian-glombik Jun 13, 2025
f469523
Decrease coverage
florian-glombik Jun 13, 2025
1185c02
Merge branch 'develop' into chore/development/use-signals-in-title-ch…
florian-glombik Jun 13, 2025
d07d270
Fix coverage conditions
florian-glombik Jun 13, 2025
845c346
Address Ahments review
florian-glombik Jun 13, 2025
e8ee08f
Merge branch 'develop' into chore/development/use-signals-in-title-ch…
florian-glombik Jun 13, 2025
5766c11
Merge branch 'develop' into chore/development/use-signals-in-title-ch…
florian-glombik Jun 14, 2025
f68f20f
Bump coverage conditions
florian-glombik Jun 14, 2025
44ab605
Adjust coverage treshold
florian-glombik Jun 15, 2025
acfd324
Merge branch 'develop' into chore/development/use-signals-in-title-ch…
florian-glombik Jun 15, 2025
b3e577b
Merge branch 'develop' into chore/development/use-signals-in-title-ch…
florian-glombik Jun 16, 2025
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
2 changes: 1 addition & 1 deletion jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ module.exports = {
global: {
// TODO: in the future, the following values should increase to at least 90%
statements: 89.14,
branches: 75.18,
branches: 75.14,
functions: 82.94,
lines: 89.22,
},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Component, Input, OnChanges, SimpleChanges, ViewChild, effect, inject, input, output, signal } from '@angular/core';
import { Component, Input, OnChanges, SimpleChanges, effect, inject, input, output, signal, viewChild } from '@angular/core';
import { Course, isCommunicationEnabled } from 'app/core/course/shared/entities/course.model';
import { Exercise } from 'app/exercise/shared/entities/exercise/exercise.model';
import { TitleChannelNameComponent } from 'app/shared/form/title-channel-name/title-channel-name.component';
Expand All @@ -22,7 +22,7 @@ export class ExerciseTitleChannelNameComponent implements OnChanges {
@Input() isImport: boolean;
@Input() hideTitleLabel: boolean;

@ViewChild(TitleChannelNameComponent) titleChannelNameComponent: TitleChannelNameComponent;
readonly titleChannelNameComponent = viewChild.required(TitleChannelNameComponent);

onTitleChange = output<string>();
onChannelNameChange = output<string>();
Expand Down Expand Up @@ -51,14 +51,14 @@ export class ExerciseTitleChannelNameComponent implements OnChanges {
}
}

updateTitle(newTitle: string) {
updateTitle(newTitle: string | undefined) {
this.exercise.title = newTitle;
this.onTitleChange.emit(newTitle);
this.onTitleChange.emit(newTitle ?? '');
}

updateChannelName(newName: string) {
updateChannelName(newName: string | undefined) {
this.exercise.channelName = newName;
this.onChannelNameChange.emit(newName);
this.onChannelNameChange.emit(newName ?? '');
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ const PIE_CHART_NA_FALLBACK_VALUE = [0, 0, 1];
imports: [RouterLink, NgClass, FaIconComponent, PieChartModule, ArtemisTranslatePipe],
})
export class DoughnutChartComponent implements OnChanges, OnInit {
private router = inject(Router);
protected readonly faSpinner = faSpinner;

private readonly router = inject(Router);

@Input() course: Course;
@Input() contentType: DoughnutChartType;
Expand All @@ -36,10 +38,6 @@ export class DoughnutChartComponent implements OnChanges, OnInit {
stats: number[];
titleLink: string[] | undefined;

// Icons
faSpinner = faSpinner;

// ngx
ngxDoughnutData: NgxChartsSingleSeriesDataEntry[] = [
{ name: 'Done', value: 0 },
{ name: 'Not done', value: 0 },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,24 @@ import { MockActivatedRoute } from 'test/helpers/mocks/activated-route/mock-acti
import { ExerciseGroup } from 'app/exam/shared/entities/exercise-group.model';
import { Course } from 'app/core/course/shared/entities/course.model';
import { TranslateService } from '@ngx-translate/core';
import { MockProvider } from 'ng-mocks';
import { MockNgbModalService } from 'test/helpers/mocks/service/mock-ngb-modal.service';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import dayjs from 'dayjs/esm';
import { TextExercise } from 'app/text/shared/entities/text-exercise.model';
import { Exam } from 'app/exam/shared/entities/exam.model';
import { fileUploadExercise } from 'test/helpers/mocks/service/mock-file-upload-exercise.service';
import { ExerciseTitleChannelNameComponent } from 'app/exercise/exercise-title-channel-name/exercise-title-channel-name.component';
import { TeamConfigFormGroupComponent } from 'app/exercise/team-config-form-group/team-config-form-group.component';
import { NgModel } from '@angular/forms';
import { ExerciseCategory } from 'app/exercise/shared/entities/exercise/exercise-category.model';
import { provideHttpClientTesting } from '@angular/common/http/testing';
import { MockRouter } from 'test/helpers/mocks/mock-router';
import { MockTranslateService } from 'test/helpers/mocks/service/mock-translate.service';
import { FormDateTimePickerComponent } from 'app/shared/date-time-picker/date-time-picker.component';
import { MockComponent } from 'ng-mocks';
import { OwlDateTimeModule, OwlNativeDateTimeModule } from '@danielmoncada/angular-datetime-picker';
import { ProfileService } from 'app/core/layouts/profiles/shared/profile.service';
import { MockProfileService } from 'test/helpers/mocks/service/mock-profile.service';
import { MockResizeObserver } from 'test/helpers/mocks/service/mock-resize-observer';

describe('FileUploadExerciseUpdateComponent', () => {
let comp: FileUploadExerciseUpdateComponent;
Expand All @@ -33,13 +38,16 @@ describe('FileUploadExerciseUpdateComponent', () => {

beforeEach(() => {
TestBed.configureTestingModule({
imports: [OwlDateTimeModule, OwlNativeDateTimeModule],
providers: [
{ provide: LocalStorageService, useClass: MockSyncStorage },
{ provide: SessionStorageService, useClass: MockSyncStorage },
{ provide: ActivatedRoute, useValue: new MockActivatedRoute({}) },
{ provide: NgbModal, useClass: MockNgbModalService },
{ provide: Router, useClass: MockRouter },
MockProvider(TranslateService),
{ provide: TranslateService, useClass: MockTranslateService },
{ provide: ProfileService, useClass: MockProfileService },
MockComponent(FormDateTimePickerComponent),
provideHttpClient(),
provideHttpClientTesting(),
],
Expand Down Expand Up @@ -134,6 +142,10 @@ describe('FileUploadExerciseUpdateComponent', () => {
const route = TestBed.inject(ActivatedRoute);
route.data = of({ fileUploadExercise });
route.url = of([{ path: 'new' } as UrlSegment]);

global.ResizeObserver = jest.fn().mockImplementation((callback: ResizeObserverCallback) => {
return new MockResizeObserver(callback);
});
});

it('should not be in exam mode', fakeAsync(() => {
Expand All @@ -147,17 +159,16 @@ describe('FileUploadExerciseUpdateComponent', () => {

it('should calculate valid sections', () => {
const calculateValidSpy = jest.spyOn(comp, 'calculateFormSectionStatus');
comp.exerciseTitleChannelNameComponent = { titleChannelNameComponent: { formValidChanges: new Subject() } } as ExerciseTitleChannelNameComponent;
comp.exerciseTitleChannelNameComponent().titleChannelNameComponent().isValid.set(false);
comp.teamConfigFormGroupComponent = { formValidChanges: new Subject() } as TeamConfigFormGroupComponent;
comp.bonusPoints = { valueChanges: new Subject(), valid: true } as unknown as NgModel;
comp.points = { valueChanges: new Subject(), valid: true } as unknown as NgModel;

comp.ngOnInit();
comp.ngAfterViewInit();
expect(comp.titleChannelNameComponentSubscription).toBeDefined();

comp.exerciseTitleChannelNameComponent.titleChannelNameComponent.formValid = true;
comp.exerciseTitleChannelNameComponent.titleChannelNameComponent.formValidChanges.next(true);
comp.exerciseTitleChannelNameComponent().titleChannelNameComponent().isValid.set(true);
fixture.detectChanges();
expect(calculateValidSpy).toHaveBeenCalledOnce();
expect(comp.formStatusSections).toBeDefined();
expect(comp.formStatusSections[0].valid).toBeTrue();
Expand All @@ -166,7 +177,6 @@ describe('FileUploadExerciseUpdateComponent', () => {
expect(calculateValidSpy).toHaveBeenCalledTimes(2);

comp.ngOnDestroy();
expect(comp.titleChannelNameComponentSubscription?.closed).toBeTrue();
});
});
describe('imported exercise', () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { AfterViewInit, ChangeDetectionStrategy, Component, OnDestroy, OnInit, ViewChild, inject } from '@angular/core';
import { AfterViewInit, ChangeDetectionStrategy, Component, OnDestroy, OnInit, ViewChild, effect, inject, viewChild } from '@angular/core';
import { ActivatedRoute, Params } from '@angular/router';
import { HttpErrorResponse, HttpResponse } from '@angular/common/http';
import { AlertService, AlertType } from 'app/shared/service/alert.service';
Expand Down Expand Up @@ -70,20 +70,19 @@ import { FormFooterComponent } from 'app/shared/form/form-footer/form-footer.com
],
})
export class FileUploadExerciseUpdateComponent implements AfterViewInit, OnDestroy, OnInit {
private fileUploadExerciseService = inject(FileUploadExerciseService);
private modalService = inject(NgbModal);
private popupService = inject(ExerciseUpdateWarningService);
private activatedRoute = inject(ActivatedRoute);
private courseService = inject(CourseManagementService);
private exerciseService = inject(ExerciseService);
private alertService = inject(AlertService);
private navigationUtilService = inject(ArtemisNavigationUtilService);
private exerciseGroupService = inject(ExerciseGroupService);
private readonly fileUploadExerciseService = inject(FileUploadExerciseService);
private readonly modalService = inject(NgbModal);
private readonly popupService = inject(ExerciseUpdateWarningService);
private readonly activatedRoute = inject(ActivatedRoute);
private readonly courseService = inject(CourseManagementService);
private readonly exerciseService = inject(ExerciseService);
private readonly alertService = inject(AlertService);
private readonly navigationUtilService = inject(ArtemisNavigationUtilService);
private readonly exerciseGroupService = inject(ExerciseGroupService);

protected readonly faQuestionCircle = faQuestionCircle;

readonly IncludedInOverallScore = IncludedInOverallScore;
readonly documentationType: DocumentationType = 'FileUpload';
protected readonly IncludedInOverallScore = IncludedInOverallScore;
protected readonly documentationType: DocumentationType = 'FileUpload';

@ViewChild('bonusPoints') bonusPoints: NgModel;
@ViewChild('points') points: NgModel;
Expand All @@ -92,7 +91,7 @@ export class FileUploadExerciseUpdateComponent implements AfterViewInit, OnDestr
@ViewChild('startDate') startDateField?: FormDateTimePickerComponent;
@ViewChild('dueDate') dueDateField?: FormDateTimePickerComponent;
@ViewChild('assessmentDueDate') assessmentDateField?: FormDateTimePickerComponent;
@ViewChild(ExerciseTitleChannelNameComponent) exerciseTitleChannelNameComponent: ExerciseTitleChannelNameComponent;
exerciseTitleChannelNameComponent = viewChild.required(ExerciseTitleChannelNameComponent);
@ViewChild(TeamConfigFormGroupComponent) teamConfigFormGroupComponent: TeamConfigFormGroupComponent;

isExamMode: boolean;
Expand All @@ -109,8 +108,6 @@ export class FileUploadExerciseUpdateComponent implements AfterViewInit, OnDestr

formStatusSections: FormSectionStatus[];

// Subscriptions
titleChannelNameComponentSubscription?: Subscription;
pointsSubscription?: Subscription;
bonusPointsSubscription?: Subscription;
teamSubscription?: Subscription;
Expand All @@ -122,6 +119,21 @@ export class FileUploadExerciseUpdateComponent implements AfterViewInit, OnDestr
return this.fileUploadExercise.id == undefined ? EditType.CREATE : EditType.UPDATE;
}

constructor() {
effect(() => {
this.updateFormSectionsOnIsValidChange();
});
}

/**
* Triggers {@link calculateFormSectionStatus} whenever a relevant signal changes
*/
private updateFormSectionsOnIsValidChange() {
this.exerciseTitleChannelNameComponent().titleChannelNameComponent().isValid(); // trigger the effect

this.calculateFormSectionStatus();
}

/**
* Initializes information relevant to file upload exercise
*/
Expand Down Expand Up @@ -151,16 +163,12 @@ export class FileUploadExerciseUpdateComponent implements AfterViewInit, OnDestr
}

ngAfterViewInit() {
this.titleChannelNameComponentSubscription = this.exerciseTitleChannelNameComponent.titleChannelNameComponent.formValidChanges.subscribe(() =>
this.calculateFormSectionStatus(),
);
this.pointsSubscription = this.points?.valueChanges?.subscribe(() => this.calculateFormSectionStatus());
this.bonusPointsSubscription = this.bonusPoints?.valueChanges?.subscribe(() => this.calculateFormSectionStatus());
this.teamSubscription = this.teamConfigFormGroupComponent.formValidChanges.subscribe(() => this.calculateFormSectionStatus());
}

ngOnDestroy() {
this.titleChannelNameComponentSubscription?.unsubscribe();
this.pointsSubscription?.unsubscribe();
this.bonusPointsSubscription?.unsubscribe();
this.teamSubscription?.unsubscribe();
Expand All @@ -170,7 +178,7 @@ export class FileUploadExerciseUpdateComponent implements AfterViewInit, OnDestr
this.formStatusSections = [
{
title: 'artemisApp.exercise.sections.general',
valid: this.exerciseTitleChannelNameComponent.titleChannelNameComponent.formValid,
valid: this.exerciseTitleChannelNameComponent().titleChannelNameComponent().isValid(),
},
{ title: 'artemisApp.exercise.sections.mode', valid: this.teamConfigFormGroupComponent.formValid },
{ title: 'artemisApp.exercise.sections.problem', valid: true, empty: !this.fileUploadExercise.problemStatement },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ import { HtmlForMarkdownPipe } from 'app/shared/pipes/html-for-markdown.pipe';
imports: [NgClass, TranslateDirective, FaIconComponent, IrisLogoComponent, HtmlForMarkdownPipe],
})
export class IrisExerciseChatbotButtonComponent implements OnInit, OnDestroy {
protected readonly faCircle = faCircle;
protected readonly faChevronDown = faChevronDown;
protected readonly faAngleDoubleDown = faAngleDoubleDown;

dialog = inject(MatDialog);
protected overlay = inject(Overlay);
protected readonly chatService = inject(IrisChatService);
Expand All @@ -64,11 +68,6 @@ export class IrisExerciseChatbotButtonComponent implements OnInit, OnDestroy {
private latestIrisMessageSubscription: Subscription;
private queryParamsSubscription: Subscription;

// Icons
faCircle = faCircle;
faChevronDown = faChevronDown;
faAngleDoubleDown = faAngleDoubleDown;

@ViewChild('chatBubble') chatBubble: ElementRef;

protected readonly IrisLogoLookDirection = IrisLogoLookDirection;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -363,7 +363,7 @@ describe('LectureUpdateComponent', () => {
lectureUpdateComponent.isEditMode.set(true);
lectureUpdateComponent.titleSection = signal({
titleChannelNameComponent: () => ({
isFormValidSignal: () => true,
isValid: () => true,
}),
} as any);
lectureUpdateComponent.lecturePeriodSection = signal({
Expand All @@ -390,7 +390,7 @@ describe('LectureUpdateComponent', () => {
lectureUpdateComponent.isEditMode.set(false);
lectureUpdateComponent.titleSection = signal({
titleChannelNameComponent: () => ({
isFormValidSignal: () => false,
isValid: () => false,
}),
} as any);
lectureUpdateComponent.lecturePeriodSection = signal({
Expand All @@ -409,7 +409,7 @@ describe('LectureUpdateComponent', () => {
lectureUpdateComponent.isEditMode.set(true);
lectureUpdateComponent.titleSection = signal({
titleChannelNameComponent: () => ({
isFormValidSignal: () => false,
isValid: () => false,
}),
} as any);
lectureUpdateComponent.lecturePeriodSection = signal({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ export class LectureUpdateComponent implements OnInit, OnDestroy {

areSectionsValid = computed(() => {
return (
this.titleSection().titleChannelNameComponent().isFormValidSignal() &&
this.titleSection().titleChannelNameComponent().isValid() &&
this.lecturePeriodSection().isPeriodSectionValid() &&
(this.unitSection()?.isUnitConfigurationValid() ?? true) &&
(this.attachmentsSection()?.isFormValid() ?? true)
Expand All @@ -104,16 +104,16 @@ export class LectureUpdateComponent implements OnInit, OnDestroy {

constructor() {
effect(() => {
if (this.titleSection()?.titleChannelNameComponent() && this.lecturePeriodSection()) {
if (this.titleSection().titleChannelNameComponent() && this.lecturePeriodSection()) {
this.subscriptions.add(
this.titleSection()!
this.titleSection()
.titleChannelNameComponent()
.titleChange.subscribe(() => {
this.updateIsChangesMadeToTitleOrPeriodSection();
}),
);
this.subscriptions.add(
this.titleSection()!
this.titleSection()
.titleChannelNameComponent()
.channelNameChange.subscribe(() => {
this.updateIsChangesMadeToTitleOrPeriodSection();
Expand Down Expand Up @@ -174,7 +174,7 @@ export class LectureUpdateComponent implements OnInit, OnDestroy {
updatedFormStatusSections.push(
{
title: 'artemisApp.lecture.sections.title',
valid: Boolean(this.titleSection().titleChannelNameComponent().isFormValidSignal()),
valid: this.titleSection().titleChannelNameComponent().isValid(),
},
{
title: 'artemisApp.lecture.sections.period',
Expand Down
Loading
Loading