From 231fb2e1db9b1af9387a89bbf025af3a590f5c13 Mon Sep 17 00:00:00 2001 From: Roberto Fontanarosa Date: Sat, 21 Mar 2026 15:02:11 +0100 Subject: [PATCH 1/4] Add trackBy to *ngFor on formArray.controls --- .../components/shared/task-editor/task-editor.component.html | 2 +- .../components/shared/task-editor/task-editor.component.ts | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/web/src/app/components/shared/task-editor/task-editor.component.html b/web/src/app/components/shared/task-editor/task-editor.component.html index 34b928314..015049fd3 100644 --- a/web/src/app/components/shared/task-editor/task-editor.component.html +++ b/web/src/app/components/shared/task-editor/task-editor.component.html @@ -19,7 +19,7 @@
diff --git a/web/src/app/components/shared/task-editor/task-editor.component.ts b/web/src/app/components/shared/task-editor/task-editor.component.ts index efc40a2c5..61d571473 100644 --- a/web/src/app/components/shared/task-editor/task-editor.component.ts +++ b/web/src/app/components/shared/task-editor/task-editor.component.ts @@ -306,4 +306,8 @@ export class TaskEditorComponent { toTasks(): List { return List(this.formArray.controls.map((_, i: number) => this.toTask(i))); } + + trackByTaskId(_index: number, control: AbstractControl): string { + return control.get('id')?.value; + } } From 02020c530501c05b81c13b912c6316fedbffeaec Mon Sep 17 00:00:00 2001 From: Roberto Fontanarosa Date: Sat, 21 Mar 2026 15:05:51 +0100 Subject: [PATCH 2/4] Lift expanded state to TaskEditorComponent --- .../task-editor/task-editor.component.html | 3 +++ .../task-editor/task-editor.component.ts | 22 ++++++++++++++- .../task-form/task-form.component.html | 2 +- .../task-form/task-form.component.ts | 27 +++++-------------- 4 files changed, 32 insertions(+), 22 deletions(-) diff --git a/web/src/app/components/shared/task-editor/task-editor.component.html b/web/src/app/components/shared/task-editor/task-editor.component.html index 015049fd3..056f2dbf4 100644 --- a/web/src/app/components/shared/task-editor/task-editor.component.html +++ b/web/src/app/components/shared/task-editor/task-editor.component.html @@ -21,6 +21,7 @@ class="task-container" *ngFor="let formGroup of formArray.controls; let i = index; trackBy: trackByTaskId" [ngClass]="{'loi-task-container': formGroup.get('addLoiTask').value || formGroup.get('condition')}" + (click)="onTaskContainerClick(i, $event)" >
(); + expandedIndex = signal(null); + + @HostListener('document:click') + onDocumentClick(): void { + this.expandedIndex.set(null); + } + + onTaskContainerClick(index: number, event: MouseEvent): void { + event.stopPropagation(); + this.expandedIndex.set(index); + } + ngOnChanges(): void { this.formGroup = this.formBuilder.group({ tasks: this.formBuilder.array( diff --git a/web/src/app/components/shared/task-editor/task-form/task-form.component.html b/web/src/app/components/shared/task-editor/task-form/task-form.component.html index 2cac23f8d..6704c3242 100644 --- a/web/src/app/components/shared/task-editor/task-form/task-form.component.html +++ b/web/src/app/components/shared/task-editor/task-form/task-form.component.html @@ -14,7 +14,7 @@ limitations under the License. --> -
+ drag_handle
diff --git a/web/src/app/components/shared/task-editor/task-form/task-form.component.ts b/web/src/app/components/shared/task-editor/task-form/task-form.component.ts index da4b4d387..291d6c94a 100644 --- a/web/src/app/components/shared/task-editor/task-form/task-form.component.ts +++ b/web/src/app/components/shared/task-editor/task-form/task-form.component.ts @@ -18,9 +18,9 @@ import '@angular/localize/init'; import { CdkDragDrop } from '@angular/cdk/drag-drop'; import { + ChangeDetectionStrategy, Component, EventEmitter, - HostListener, Input, Output, input, @@ -156,21 +156,19 @@ const AddLoiTaskGroups = List([TaskGroup.DROP_PIN, TaskGroup.DRAW_AREA]); templateUrl: './task-form.component.html', styleUrls: ['./task-form.component.scss'], standalone: false, + changeDetection: ChangeDetectionStrategy.OnPush, }) export class TaskFormComponent { @Input() formGroup!: FormGroup; @Input() formGroupIndex!: number; isCreationMode = input(false); + expanded = input(false); + @Output() delete = new EventEmitter(); @Output() duplicate = new EventEmitter(); @Output() toggleCondition = new EventEmitter(); - - /** When expanded, options and actions below the fold are visible to the user. */ - expanded: boolean; - - /** Set to true when question gets focus, false when it loses focus. */ - selected: boolean; + @Output() expand = new EventEmitter(); addLoiTask?: boolean; @@ -200,21 +198,10 @@ export class TaskFormComponent { public dialog: MatDialog, private dataStoreService: DataStoreService, private formBuilder: FormBuilder - ) { - this.expanded = false; - this.selected = false; - } + ) {} - @HostListener('click') onTaskFocus() { - this.expanded = true; - this.selected = true; - } - - @HostListener('document:click') - onTaskBlur() { - if (!this.selected) this.expanded = false; - this.selected = false; + this.expand.emit(); } ngOnInit(): void { From 6a9406aff74afa86427c414e78f419e22e9a4342 Mon Sep 17 00:00:00 2001 From: Roberto Fontanarosa Date: Sat, 21 Mar 2026 15:07:53 +0100 Subject: [PATCH 3/4] input() + effect() replacing @Input + ngOnChanges --- .../task-editor/task-editor.component.ts | 55 +++++++++---------- 1 file changed, 27 insertions(+), 28 deletions(-) diff --git a/web/src/app/components/shared/task-editor/task-editor.component.ts b/web/src/app/components/shared/task-editor/task-editor.component.ts index c8196735e..9c1001a77 100644 --- a/web/src/app/components/shared/task-editor/task-editor.component.ts +++ b/web/src/app/components/shared/task-editor/task-editor.component.ts @@ -19,8 +19,8 @@ import { Component, EventEmitter, HostListener, - Input, Output, + effect, input, signal, } from '@angular/core'; @@ -100,7 +100,7 @@ export const taskTypeToGroup = new Map([ export class TaskEditorComponent { formGroup!: FormGroup; - @Input() tasks?: List; + tasks = input>(); isCreationMode = input(false); @Output() onValidationChanges: EventEmitter = @@ -114,16 +114,38 @@ export class TaskEditorComponent { TaskGroup.CAPTURE_LOCATION, ]; + multipleChoiceTasks = List(); + + expandedIndex = signal(null); + constructor( private dataStoreService: DataStoreService, private dialogService: DialogService, private taskService: TaskService, private formBuilder: FormBuilder - ) {} + ) { + effect(() => { + this.formGroup = this.formBuilder.group({ + tasks: this.formBuilder.array( + this.tasks()?.toArray().map((task: Task) => this.toControl(task)) || + [], + Validators.required + ), + }) as FormGroup; + + this.formGroup.statusChanges.subscribe(_ => { + this.onValidationChanges.emit(this.formGroup?.valid); + }); - multipleChoiceTasks = List(); + this.formGroup.valueChanges.subscribe(_ => { + this.multipleChoiceTasks = this.toTasks(); + this.onValueChanges.emit(this.formGroup?.valid); + }); - expandedIndex = signal(null); + this.onValidationChanges.emit(this.formGroup?.valid); + this.multipleChoiceTasks = this.toTasks(); + }); + } @HostListener('document:click') onDocumentClick(): void { @@ -135,29 +157,6 @@ export class TaskEditorComponent { this.expandedIndex.set(index); } - ngOnChanges(): void { - this.formGroup = this.formBuilder.group({ - tasks: this.formBuilder.array( - this.tasks?.toArray().map((task: Task) => this.toControl(task)) || [], - Validators.required - ), - }) as FormGroup; - - this.formGroup.statusChanges.subscribe(_ => { - this.onValidationChanges.emit(this.formGroup?.valid); - }); - - this.formGroup.valueChanges.subscribe(_ => { - this.multipleChoiceTasks = this.toTasks(); - - this.onValueChanges.emit(this.formGroup?.valid); - }); - - this.onValidationChanges.emit(this.formGroup?.valid); - - this.multipleChoiceTasks = this.toTasks(); - } - get formArray() { return this.formGroup.get('tasks') as FormArray; } From 8d07054f062f249fc3a6f429861f17ab219c16da Mon Sep 17 00:00:00 2001 From: Roberto Fontanarosa Date: Sat, 21 Mar 2026 20:38:28 +0100 Subject: [PATCH 4/4] Make TaskOptionsPipe pure to avoid re-evaluation on every CD cycle --- .../task-condition-form.component.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/web/src/app/components/shared/task-editor/task-condition-form/task-condition-form.component.ts b/web/src/app/components/shared/task-editor/task-condition-form/task-condition-form.component.ts index d963feaf4..a198d02ad 100644 --- a/web/src/app/components/shared/task-editor/task-condition-form/task-condition-form.component.ts +++ b/web/src/app/components/shared/task-editor/task-condition-form/task-condition-form.component.ts @@ -14,7 +14,13 @@ * limitations under the License. */ -import { Component, Input, Pipe, PipeTransform } from '@angular/core'; +import { + ChangeDetectionStrategy, + Component, + Input, + Pipe, + PipeTransform, +} from '@angular/core'; import { AbstractControl, FormArray, FormGroup } from '@angular/forms'; import { List } from 'immutable'; @@ -23,7 +29,6 @@ import { Task } from 'app/models/task/task.model'; @Pipe({ name: 'getTaskOptions', - pure: false, standalone: false, }) export class TaskOptionsPipe implements PipeTransform { @@ -40,6 +45,7 @@ export class TaskOptionsPipe implements PipeTransform { templateUrl: './task-condition-form.component.html', styleUrls: ['./task-condition-form.component.scss'], standalone: false, + changeDetection: ChangeDetectionStrategy.OnPush, }) export class TaskConditionFormComponent { @Input() formGroup!: FormGroup;