From 27d0fa055a4ba2d138c344204b561587d07ee77e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Grand?= <francois.grand@inrae.fr> Date: Thu, 19 May 2022 09:25:04 +0200 Subject: [PATCH 01/11] fix: emit validity signal only on validity change refs #544 --- .../field-set/field-set.component.ts | 15 ++++++++++---- .../fieldset-container.component.ts | 20 ++++++++++++++----- .../pab-table/pab-table.component.ts | 17 ++++++++++++---- .../pb-schema/pb-schema.component.ts | 7 ++++++- 4 files changed, 45 insertions(+), 14 deletions(-) diff --git a/src/app/components/field-set/field-set.component.ts b/src/app/components/field-set/field-set.component.ts index d4a614ab8..584f42efa 100644 --- a/src/app/components/field-set/field-set.component.ts +++ b/src/app/components/field-set/field-set.component.ts @@ -223,8 +223,7 @@ export class FieldSetComponent implements DoCheck { * calcul de la validité de tous les ParamFieldLineComponent et tous les * SelectFieldLineComponent de la vue */ - private updateValidity() { - this._isValid = false; + private computeValidity(): boolean { let paramsAreValid = true; let selectAreValid = true; @@ -261,9 +260,17 @@ export class FieldSetComponent implements DoCheck { } // global validity - this._isValid = (paramsAreValid && selectAreValid); + return (paramsAreValid && selectAreValid); + } + + private updateValidity() { + const oldValidity = this._isValid; - this.validChange.emit(); + // global validity + this._isValid = this.computeValidity(); + if (this._isValid !== oldValidity) { + this.validChange.emit(); + } } /** diff --git a/src/app/components/fieldset-container/fieldset-container.component.ts b/src/app/components/fieldset-container/fieldset-container.component.ts index 630da2d19..4d60303d1 100644 --- a/src/app/components/fieldset-container/fieldset-container.component.ts +++ b/src/app/components/fieldset-container/fieldset-container.component.ts @@ -111,11 +111,11 @@ export class FieldsetContainerComponent implements DoCheck, AfterViewInit { /** * calcul de la validité de tous les FieldSet de la vue */ - private updateValidity() { - this._isValid = false; + private computeValidity(): boolean { + let res = false; if (this._fieldsetComponents?.length > 0) { - this._isValid = this._fieldsetComponents.reduce( + res = this._fieldsetComponents.reduce( // callback ( // accumulator (valeur précédente du résultat) @@ -133,10 +133,20 @@ export class FieldsetContainerComponent implements DoCheck, AfterViewInit { , this._fieldsetComponents.length > 0); } else { // empty / hidden container ? everything OK. - this._isValid = true; + res = true; } - this.validChange.emit(); + return res; + } + + private updateValidity() { + const oldValidity = this._isValid; + + // global validity + this._isValid = this.computeValidity(); + if (this._isValid !== oldValidity) { + this.validChange.emit(); + } } /** diff --git a/src/app/components/pab-table/pab-table.component.ts b/src/app/components/pab-table/pab-table.component.ts index 486789521..597d28f3a 100644 --- a/src/app/components/pab-table/pab-table.component.ts +++ b/src/app/components/pab-table/pab-table.component.ts @@ -1440,14 +1440,23 @@ export class PabTableComponent implements AfterViewInit, AfterViewChecked, OnIni /** * Computes the global Pab validity : validity of every cell of every row */ - private updateValidity() { - this._isValid = true; + private computeValidity(): boolean { + let res = true; for (const r of this.rows) { for (const c of r.cells) { - this._isValid = this._isValid && ! this.isInvalid(c); + res = res && !this.isInvalid(c); } } - this.validChange.emit(); + + return res; + } + + private updateValidity() { + const oldValidity = this._isValid; + this._isValid = this.computeValidity(); + if (this._isValid !== oldValidity) { + this.validChange.emit(); + } } public get uitextEditPabTable() { diff --git a/src/app/components/pb-schema/pb-schema.component.ts b/src/app/components/pb-schema/pb-schema.component.ts index 7ea4ba81d..bfc497888 100644 --- a/src/app/components/pb-schema/pb-schema.component.ts +++ b/src/app/components/pb-schema/pb-schema.component.ts @@ -641,13 +641,18 @@ export class PbSchemaComponent implements AfterViewInit, AfterContentInit, OnIni * Computes the global Pab validity : validity of every cell of every row */ private updateValidity() { + const oldValidity = this._isValid; + // check that at least 1 basin is present and a route from river // upstream to river downstream exists (2nd check includes 1st) this._isValid = ( this.model.hasUpDownConnection() && ! this.model.hasBasinNotConnected() ); - this.validChange.emit(); + + if (this._isValid !== oldValidity) { + this.validChange.emit(); + } } private clearHighlightedItems() { -- GitLab From 3761c39a428e1f2a9e3fc87d63dafdcfa5634c41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Grand?= <francois.grand@inrae.fr> Date: Thu, 19 May 2022 10:49:03 +0200 Subject: [PATCH 02/11] refactor: remove unused validity signals refs #544 --- .../param-field-line/param-field-line.component.html | 6 ++---- .../param-field-line/param-field-line.component.ts | 8 -------- src/app/components/param-link/param-link.component.ts | 4 ---- 3 files changed, 2 insertions(+), 16 deletions(-) diff --git a/src/app/components/param-field-line/param-field-line.component.html b/src/app/components/param-field-line/param-field-line.component.html index daad0ba4b..a9114f5f2 100644 --- a/src/app/components/param-field-line/param-field-line.component.html +++ b/src/app/components/param-field-line/param-field-line.component.html @@ -12,13 +12,11 @@ </param-computed> <!-- composant pour gérer le cas "paramètre à varier" (min-max/liste de valeurs) --> - <param-values *ngIf="isRadioVarChecked" [title]="title" [param]="param" (change)="onInputChange($event)" - (valid)=onParamValuesValid($event)> + <param-values *ngIf="isRadioVarChecked" [title]="title" [param]="param" (change)="onInputChange($event)"> </param-values> <!-- composant pour gérer le cas "paramètre lié" --> - <param-link *ngIf="isRadioLinkChecked" [title]="title" [param]="param" (change)="onInputChange($event)" - (valid)=onParamValuesValid($event)> + <param-link *ngIf="isRadioLinkChecked" [title]="title" [param]="param" (change)="onInputChange($event)"> </param-link> </div> diff --git a/src/app/components/param-field-line/param-field-line.component.ts b/src/app/components/param-field-line/param-field-line.component.ts index 1cb5750c7..64caf333a 100644 --- a/src/app/components/param-field-line/param-field-line.component.ts +++ b/src/app/components/param-field-line/param-field-line.component.ts @@ -312,14 +312,6 @@ export class ParamFieldLineComponent implements OnChanges { } } - /** - * réception d'un événement de validité de ParamValuesComponent - */ - public onParamValuesValid(event: boolean) { - this._isRangeValid = event; - this.emitValidity(); - } - public ngOnChanges() { this._ngParamInputComponent.model = this.param; this._ngParamInputComponent.showError = this.isRadioFixChecked; diff --git a/src/app/components/param-link/param-link.component.ts b/src/app/components/param-link/param-link.component.ts index 89255ef13..2571d9ec7 100644 --- a/src/app/components/param-link/param-link.component.ts +++ b/src/app/components/param-link/param-link.component.ts @@ -22,9 +22,6 @@ export class ParamLinkComponent implements OnChanges, Observer, OnDestroy { @Input() public title: string; - @Output() - public valid: EventEmitter<boolean>; - /** * événement signalant un changement de valeur du modèle * @TODO l'utiliser aussi pour le changement de validité à @@ -83,7 +80,6 @@ export class ParamLinkComponent implements OnChanges, Observer, OnDestroy { private intlService: I18nService, private formService: FormulaireService ) { - this.valid = new EventEmitter(); this.formService.addObserver(this); } -- GitLab From 8c4e4132d5ecb7e0e167880ff33713cd618837cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Grand?= <francois.grand@inrae.fr> Date: Thu, 19 May 2022 16:16:32 +0200 Subject: [PATCH 03/11] fix: SVG schema nodes not instantly displayed on error in case of invalid input in a fieldset refs #544 --- .../calculator.component.ts | 3 +++ .../generic-input/generic-input.component.ts | 4 +-- .../ngparam-input/ngparam-input.component.ts | 26 ++++++++++++++++++- .../pb-schema/pb-schema.component.ts | 7 +++++ 4 files changed, 37 insertions(+), 3 deletions(-) diff --git a/src/app/components/generic-calculator/calculator.component.ts b/src/app/components/generic-calculator/calculator.component.ts index 9650543c6..859463c39 100644 --- a/src/app/components/generic-calculator/calculator.component.ts +++ b/src/app/components/generic-calculator/calculator.component.ts @@ -526,6 +526,9 @@ export class GenericCalculatorComponent implements OnInit, DoCheck, AfterViewChe this._isUIValid = this._isUIValid && form.checkParameters().length === 0; } } + + // update prébarrage schema validity + this._pbSchemaComponent.updateItemsValidity(); } public getElementStyleDisplay(id: string) { diff --git a/src/app/components/generic-input/generic-input.component.ts b/src/app/components/generic-input/generic-input.component.ts index 3dc390d50..444127f7f 100644 --- a/src/app/components/generic-input/generic-input.component.ts +++ b/src/app/components/generic-input/generic-input.component.ts @@ -131,7 +131,7 @@ export abstract class GenericInputComponentDirective implements OnChanges { return this._isValidUI && this._isValidModel; } - private setUIValid(b: boolean) { + protected setUIValid(b: boolean) { const old = this.isValid; this._isValidUI = b; if (this.isValid !== old) { @@ -147,7 +147,7 @@ export abstract class GenericInputComponentDirective implements OnChanges { return isValid; } - private setModelValid(b: boolean) { + protected setModelValid(b: boolean) { const old = this.isValid; this._isValidModel = b; if (this.isValid !== old) { diff --git a/src/app/components/ngparam-input/ngparam-input.component.ts b/src/app/components/ngparam-input/ngparam-input.component.ts index 2449f691e..581886d3a 100644 --- a/src/app/components/ngparam-input/ngparam-input.component.ts +++ b/src/app/components/ngparam-input/ngparam-input.component.ts @@ -2,7 +2,7 @@ import { Component, ChangeDetectorRef, OnDestroy, Input, ElementRef } from "@angular/core"; -import { Message, Observer } from "jalhyd"; +import { Message, MessageCode, Observer } from "jalhyd"; import { I18nService } from "../../services/internationalisation.service"; import { NgParameter } from "../../formulaire/elements/ngparam"; @@ -99,6 +99,9 @@ export class NgParamInputComponent extends GenericInputComponentDirective implem msg = "internal error, model undefined"; } else { try { + if (!this._paramDef.allowEmpty && v === undefined) { // from nghyd#501 commit 425ae8fa + throw new Message(MessageCode.ERROR_PARAMDEF_VALUE_UNDEFINED); + } this._paramDef.checkValue(v); valid = true; } catch (e) { @@ -113,6 +116,26 @@ export class NgParamInputComponent extends GenericInputComponentDirective implem return { isValid: valid, message: msg }; } + private undefineModel() { + if (this.getModelValue() !== undefined) { + this.setModelValue(this, undefined); + } + } + + protected setModelValid(b: boolean) { + if (!b) { + this.undefineModel(); + } + super.setModelValid(b); + } + + protected setUIValid(b: boolean) { + if (!b) { + this.undefineModel(); + } + super.setUIValid(b); + } + public update(sender: any, data: any): void { switch (data["action"]) { case "ngparamAfterValue": @@ -148,6 +171,7 @@ export class NgParamInputComponent extends GenericInputComponentDirective implem } public ngOnDestroy() { + // résoudre le conflit en supprimant le code ajouté cad ne conserver que removeObserver() this._paramDef.removeObserver(this); } } diff --git a/src/app/components/pb-schema/pb-schema.component.ts b/src/app/components/pb-schema/pb-schema.component.ts index bfc497888..425b1ba88 100644 --- a/src/app/components/pb-schema/pb-schema.component.ts +++ b/src/app/components/pb-schema/pb-schema.component.ts @@ -655,6 +655,13 @@ export class PbSchemaComponent implements AfterViewInit, AfterContentInit, OnIni } } + /** + * update all items validity rendering + */ + public updateItemsValidity() { + this.highlightErrorItems(this._selectedItem?.uid); + } + private clearHighlightedItems() { this.nativeElement.querySelectorAll("g.node").forEach(item => { item.classList.remove("node-highlighted"); -- GitLab From 04a7f46b7244661d2dd9962b73490baa0b98a9f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Grand?= <francois.grand@inrae.fr> Date: Fri, 20 May 2022 08:14:08 +0200 Subject: [PATCH 04/11] refactor: use DefinedBoolean to manage form validity flag refs #544 --- .../field-set/field-set.component.ts | 15 +++--- .../fieldset-container.component.ts | 15 +++--- .../calculator.component.ts | 24 ++++++---- .../generic-input/generic-input.component.ts | 32 +++++++------ .../pab-table/pab-table.component.ts | 11 +++-- .../pb-schema/pb-schema.component.ts | 15 +++--- src/app/definedvalue/definedboolean.ts | 7 +++ src/app/definedvalue/definedvalue.ts | 46 +++++++++++++++++++ 8 files changed, 115 insertions(+), 50 deletions(-) create mode 100644 src/app/definedvalue/definedboolean.ts create mode 100644 src/app/definedvalue/definedvalue.ts diff --git a/src/app/components/field-set/field-set.component.ts b/src/app/components/field-set/field-set.component.ts index 584f42efa..36d2701bf 100644 --- a/src/app/components/field-set/field-set.component.ts +++ b/src/app/components/field-set/field-set.component.ts @@ -16,6 +16,7 @@ import { I18nService } from "../../services/internationalisation.service"; import { sprintf } from "sprintf-js"; import { capitalize } from "jalhyd"; +import { DefinedBoolean } from "app/definedvalue/definedboolean"; @Component({ selector: "field-set", @@ -51,7 +52,7 @@ export class FieldSetComponent implements DoCheck { } public get isValid() { - return this._isValid; + return this._isValid.value; } /** flag d'affichage des boutons ajouter, supprimer, monter, descendre */ @@ -133,7 +134,7 @@ export class FieldSetComponent implements DoCheck { /** * flag de validité de la saisie */ - private _isValid = false; + private _isValid: DefinedBoolean; /** * événement de changement d'état d'un radio @@ -149,7 +150,9 @@ export class FieldSetComponent implements DoCheck { private notifService: NotificationsService, private i18nService: I18nService, private appSetupService: ApplicationSetupService - ) { } + ) { + this._isValid = new DefinedBoolean(); + } public hasRadioFix(): boolean { if (this._fieldSet.hasInputs) { @@ -264,11 +267,9 @@ export class FieldSetComponent implements DoCheck { } private updateValidity() { - const oldValidity = this._isValid; - // global validity - this._isValid = this.computeValidity(); - if (this._isValid !== oldValidity) { + this._isValid.value = this.computeValidity(); + if (this._isValid.changed) { this.validChange.emit(); } } diff --git a/src/app/components/fieldset-container/fieldset-container.component.ts b/src/app/components/fieldset-container/fieldset-container.component.ts index 4d60303d1..74fac2031 100644 --- a/src/app/components/fieldset-container/fieldset-container.component.ts +++ b/src/app/components/fieldset-container/fieldset-container.component.ts @@ -6,6 +6,7 @@ import { FieldSet } from "../../formulaire/elements/fieldset"; import { FormulaireDefinition } from "../../formulaire/definition/form-definition"; import { I18nService } from "../../services/internationalisation.service"; import { ApplicationSetupService } from "../../services/app-setup.service"; +import { DefinedBoolean } from "app/definedvalue/definedboolean"; @Component({ selector: "fieldset-container", @@ -27,7 +28,7 @@ export class FieldsetContainerComponent implements DoCheck, AfterViewInit { } public get isValid() { - return this._isValid; + return this._isValid.value; } @Input() private _container: FieldsetContainer; @@ -41,7 +42,7 @@ export class FieldsetContainerComponent implements DoCheck, AfterViewInit { /** * flag de validité des FieldSet enfants */ - private _isValid = false; + private _isValid: DefinedBoolean; /** * événément de changement d'état d'un radio @@ -68,7 +69,9 @@ export class FieldsetContainerComponent implements DoCheck, AfterViewInit { public constructor( private i18nService: I18nService, private appSetupService: ApplicationSetupService - ) {} + ) { + this._isValid = new DefinedBoolean(); + } /** * Ajoute un nouveau sous-nub (Structure, PabCloisons, YAXN… selon le cas) @@ -140,11 +143,9 @@ export class FieldsetContainerComponent implements DoCheck, AfterViewInit { } private updateValidity() { - const oldValidity = this._isValid; - // global validity - this._isValid = this.computeValidity(); - if (this._isValid !== oldValidity) { + this._isValid.value = this.computeValidity(); + if (this._isValid.changed) { this.validChange.emit(); } } diff --git a/src/app/components/generic-calculator/calculator.component.ts b/src/app/components/generic-calculator/calculator.component.ts index 859463c39..31aa8ae8e 100644 --- a/src/app/components/generic-calculator/calculator.component.ts +++ b/src/app/components/generic-calculator/calculator.component.ts @@ -61,6 +61,7 @@ import { sprintf } from "sprintf-js"; import * as XLSX from "xlsx"; import { ServiceFactory } from "app/services/service-factory"; +import { DefinedBoolean } from "app/definedvalue/definedboolean"; @Component({ selector: "hydrocalc", @@ -110,7 +111,7 @@ export class GenericCalculatorComponent implements OnInit, DoCheck, AfterViewChe * La validité de l'UI comprend la forme (pas de chaîne alpha dans les champs numériques, etc..). * La validité formulaire comprend le domaine de définition des valeurs saisies. */ - private _isUIValid = false; + private _isUIValid: DefinedBoolean; /** * flag disabled du bouton "calculer" @@ -158,6 +159,7 @@ export class GenericCalculatorComponent implements OnInit, DoCheck, AfterViewChe private formulaireService: FormulaireService, private matomoTracker: MatomoTracker ) { + this._isUIValid = new DefinedBoolean(); // hotkeys listeners this.hotkeysService.add(new Hotkey("alt+w", AppComponent.onHotkey(this.closeCalculator, this))); this.hotkeysService.add(new Hotkey("alt+d", AppComponent.onHotkey(this.cloneCalculator, this))); @@ -331,7 +333,7 @@ export class GenericCalculatorComponent implements OnInit, DoCheck, AfterViewChe * the UI validity state) */ ngDoCheck() { - this.isCalculateDisabled = !this._isUIValid; + this.isCalculateDisabled = !this._isUIValid.value; } ngOnDestroy() { @@ -473,12 +475,12 @@ export class GenericCalculatorComponent implements OnInit, DoCheck, AfterViewChe * calcul de la validité globale de la vue */ private updateUIValidity() { - this._isUIValid = false; + let res = false; if (!this._formulaire.calculateDisabled) { // all fieldsets must be valid - this._isUIValid = true; + res = true; if (this._fieldsetComponents !== undefined) { - this._isUIValid = this._isUIValid && this._fieldsetComponents.reduce( + res = res && this._fieldsetComponents.reduce( // callback ( // accumulator (valeur précédente du résultat) @@ -497,7 +499,7 @@ export class GenericCalculatorComponent implements OnInit, DoCheck, AfterViewChe } // all fieldset containers must be valid if (this._fieldsetContainerComponents !== undefined) { - this._isUIValid = this._isUIValid && this._fieldsetContainerComponents.reduce<boolean>( + res = res && this._fieldsetContainerComponents.reduce<boolean>( // callback ( // accumulator (valeur précédente du résultat) @@ -516,19 +518,23 @@ export class GenericCalculatorComponent implements OnInit, DoCheck, AfterViewChe } // special components must be valid if (this._pabTableComponent !== undefined) { - this._isUIValid = this._isUIValid && this._pabTableComponent.isValid; + res = res && this._pabTableComponent.isValid; } if (this._pbSchemaComponent !== undefined) { - this._isUIValid = this._isUIValid && this._pbSchemaComponent.isValid; + res = res && this._pbSchemaComponent.isValid; } if (this._formulaire.currentNub.calcType === CalculatorType.PreBarrage) { const form: FormulairePrebarrage = this._formulaire as FormulairePrebarrage; - this._isUIValid = this._isUIValid && form.checkParameters().length === 0; + res = res && form.checkParameters().length === 0; } } + this._isUIValid.value = res; + // update prébarrage schema validity + if (this._isUIValid.changed) { this._pbSchemaComponent.updateItemsValidity(); + } } public getElementStyleDisplay(id: string) { diff --git a/src/app/components/generic-input/generic-input.component.ts b/src/app/components/generic-input/generic-input.component.ts index 444127f7f..97d1f801d 100644 --- a/src/app/components/generic-input/generic-input.component.ts +++ b/src/app/components/generic-input/generic-input.component.ts @@ -5,6 +5,7 @@ import { FormulaireDefinition } from "../../formulaire/definition/form-definitio import { NgParameter } from "../../formulaire/elements/ngparam"; import { I18nService } from "../../services/internationalisation.service"; import { ApplicationSetupService } from "../../services/app-setup.service"; +import { DefinedBoolean } from "app/definedvalue/definedboolean"; /** * classe de gestion générique d'un champ de saisie avec titre, validation et message d'erreur @@ -79,6 +80,11 @@ export abstract class GenericInputComponentDirective implements OnChanges { */ private _isValidModel = false; + /** + * flag de validité globale + */ + private _isValid: DefinedBoolean; + /** * message d'erreur UI */ @@ -96,7 +102,9 @@ export abstract class GenericInputComponentDirective implements OnChanges { private cdRef: ChangeDetectorRef, protected intlService: I18nService, protected appSetupService: ApplicationSetupService - ) { } + ) { + this._isValid = new DefinedBoolean(); + } public get isDisabled(): boolean { if (this._model instanceof NgParameter) { @@ -107,10 +115,13 @@ export abstract class GenericInputComponentDirective implements OnChanges { } /** - * événement de changement de la validité de la saisie + * modification et émission d'un événement de changement de la validité */ - private emitValidChanged() { - this.change.emit({ "action": "valid", "value": this.isValid }); + private setAndEmitValid() { + this._isValid.value = this._isValidUI && this._isValidModel; + if (this._isValid.changed) { + this.change.emit({ "action": "valid", "value": this._isValid.value }); + } } /** @@ -128,15 +139,12 @@ export abstract class GenericInputComponentDirective implements OnChanges { * calcul de la validité globale du composant (UI+modèle) */ public get isValid() { - return this._isValidUI && this._isValidModel; + return this._isValid.value; } protected setUIValid(b: boolean) { - const old = this.isValid; this._isValidUI = b; - if (this.isValid !== old) { - this.emitValidChanged(); - } + this.setAndEmitValid(); } protected validateUI() { @@ -148,11 +156,9 @@ export abstract class GenericInputComponentDirective implements OnChanges { } protected setModelValid(b: boolean) { - const old = this.isValid; this._isValidModel = b; - if (this.isValid !== old) { - this.emitValidChanged(); - } + this.setAndEmitValid(); + // répercussion des erreurs sur le Form angular, pour faire apparaître/disparaître les mat-error if (b) { this.inputField.control.setErrors(null); diff --git a/src/app/components/pab-table/pab-table.component.ts b/src/app/components/pab-table/pab-table.component.ts index 597d28f3a..7d95042fb 100644 --- a/src/app/components/pab-table/pab-table.component.ts +++ b/src/app/components/pab-table/pab-table.component.ts @@ -28,6 +28,7 @@ import { PabTable } from "../../formulaire/elements/pab-table"; import { DialogEditPabComponent } from "../dialog-edit-pab/dialog-edit-pab.component"; import { AppComponent } from "../../app.component"; import { NgParameter, ParamRadioConfig } from "../../formulaire/elements/ngparam"; +import { DefinedBoolean } from "app/definedvalue/definedboolean"; /** * The big editable data grid for calculator type "Pab" (component) @@ -45,7 +46,7 @@ export class PabTableComponent implements AfterViewInit, AfterViewChecked, OnIni private pabTable: PabTable; /** flag de validité des FieldSet enfants */ - private _isValid = false; + private _isValid: DefinedBoolean; /** événément de changement de validité */ @Output() @@ -84,6 +85,7 @@ export class PabTableComponent implements AfterViewInit, AfterViewChecked, OnIni private notifService: NotificationsService ) { this.selectedItems = []; + this._isValid = new DefinedBoolean(); } /** update vary value from pab fish ladder and unable compute Button */ @@ -98,7 +100,7 @@ export class PabTableComponent implements AfterViewInit, AfterViewChecked, OnIni /** Global Pab validity */ public get isValid() { - return this._isValid; + return this._isValid.value; } /** returns true if the cell has an underlying model (ie. is editable) */ @@ -1452,9 +1454,8 @@ export class PabTableComponent implements AfterViewInit, AfterViewChecked, OnIni } private updateValidity() { - const oldValidity = this._isValid; - this._isValid = this.computeValidity(); - if (this._isValid !== oldValidity) { + this._isValid.value = this.computeValidity(); + if (this._isValid.changed) { this.validChange.emit(); } } diff --git a/src/app/components/pb-schema/pb-schema.component.ts b/src/app/components/pb-schema/pb-schema.component.ts index 425b1ba88..ae157c56f 100644 --- a/src/app/components/pb-schema/pb-schema.component.ts +++ b/src/app/components/pb-schema/pb-schema.component.ts @@ -22,6 +22,7 @@ import { AppComponent } from "../../app.component"; import { fv } from "app/util"; import { FormulaireNode } from "app/formulaire/elements/formulaire-node"; import { ServiceFactory } from "app/services/service-factory"; +import { DefinedBoolean } from "app/definedvalue/definedboolean"; /** * The interactive schema for calculator type "PreBarrage" (component) @@ -45,7 +46,7 @@ export class PbSchemaComponent implements AfterViewInit, AfterContentInit, OnIni private nativeElement: any; /** flag de validité du composant */ - private _isValid = false; + private _isValid: DefinedBoolean; private upstreamId = "amont"; @@ -75,6 +76,7 @@ export class PbSchemaComponent implements AfterViewInit, AfterContentInit, OnIni private newPbCloisonDialog: MatDialog ) { this.hotkeysService.add(new Hotkey("del", AppComponent.onHotkey(this.removeOnHotkey, this))); + this._isValid = new DefinedBoolean(); } /** tracks the fullscreen state */ @@ -334,7 +336,7 @@ export class PbSchemaComponent implements AfterViewInit, AfterContentInit, OnIni /** Global Pb validity */ public get isValid() { - return this._isValid; + return this._isValid.value; } /** used for a cosmetics CSS trick only (mat-card-header right margin) */ @@ -641,16 +643,11 @@ export class PbSchemaComponent implements AfterViewInit, AfterContentInit, OnIni * Computes the global Pab validity : validity of every cell of every row */ private updateValidity() { - const oldValidity = this._isValid; - // check that at least 1 basin is present and a route from river // upstream to river downstream exists (2nd check includes 1st) - this._isValid = ( - this.model.hasUpDownConnection() - && ! this.model.hasBasinNotConnected() - ); + this._isValid.value = this.model.hasUpDownConnection() && !this.model.hasBasinNotConnected(); - if (this._isValid !== oldValidity) { + if (this._isValid.changed) { this.validChange.emit(); } } diff --git a/src/app/definedvalue/definedboolean.ts b/src/app/definedvalue/definedboolean.ts new file mode 100644 index 000000000..2d8d9a3b8 --- /dev/null +++ b/src/app/definedvalue/definedboolean.ts @@ -0,0 +1,7 @@ +import { DefinedValue } from "./definedvalue"; + +/** + * boolean value with initialised, changed, defined states + */ +export class DefinedBoolean extends DefinedValue<boolean> { +} diff --git a/src/app/definedvalue/definedvalue.ts b/src/app/definedvalue/definedvalue.ts new file mode 100644 index 000000000..1e72754b0 --- /dev/null +++ b/src/app/definedvalue/definedvalue.ts @@ -0,0 +1,46 @@ +/** + * value management with initialised, changed and defined states + */ +export abstract class DefinedValue<T> { + private _initialised: boolean; + + private _value: T; + + private _changed: boolean; + + constructor() { + this._initialised = false; + this._changed = false; + } + + /** + * @returns true if setter has been called at least once + */ + public get initialised(): boolean { + return this._initialised; + } + + /** + * @returns true if value is not undefined + */ + public get defined(): boolean { + return this._value !== undefined; + } + + /** + * @returns true if value has been modified by last call to setter + */ + public get changed(): boolean { + return this._changed; + } + + public get value(): T { + return this._value; + } + + public set value(v: T) { + this._changed = this._value !== v; + this._initialised = true; + this._value = v; + } +} -- GitLab From df4d60fdcac18b93632c2ee8755c3031c6755566 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Grand?= <francois.grand@inrae.fr> Date: Fri, 20 May 2022 08:15:05 +0200 Subject: [PATCH 05/11] refactor: optimise PbSchemaComponent.highlightErrorItems() refs #544 --- .../pb-schema/pb-schema.component.ts | 32 +++++++++++-------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/src/app/components/pb-schema/pb-schema.component.ts b/src/app/components/pb-schema/pb-schema.component.ts index ae157c56f..b2f00304c 100644 --- a/src/app/components/pb-schema/pb-schema.component.ts +++ b/src/app/components/pb-schema/pb-schema.component.ts @@ -305,6 +305,9 @@ export class PbSchemaComponent implements AfterViewInit, AfterContentInit, OnIni return (sommeA <= sommeB ? -1 : 1); } + /** + * @param item DOM element + */ private selectNode(item: any) { // console.debug(`PbSchemaComponent.selectNode(${item?.id})`); // highlight clicked element @@ -671,21 +674,24 @@ export class PbSchemaComponent implements AfterViewInit, AfterContentInit, OnIni item.classList.remove("node-highlighted-error"); }); const invalidUids: string[] = this.pbSchema.form.checkParameters(); - this.nativeElement.querySelectorAll("g.node").forEach(item => { - let itemId: string; - if ([this.upstreamId, this.downstreamId].includes(item.id)) { - itemId = this.model.uid; - } else { - itemId = item.id - } - if (invalidUids.includes(itemId)) { - if (item.id === selectedUid) { - item.classList.add("node-highlighted-error"); + if (invalidUids.length > 0) { + this.nativeElement.querySelectorAll("g.node").forEach(item => { + // in this case, item is a HTML node of the SVG schema which id is a nud uid + let itemId: string; + if ([this.upstreamId, this.downstreamId].includes(item.id)) { + itemId = this.model.uid; } else { - item.classList.add("node-error"); + itemId = item.id } - } - }); + if (invalidUids.includes(itemId)) { + if (item.id === selectedUid) { + item.classList.add("node-highlighted-error"); + } else { + item.classList.add("node-error"); + } + } + }); + } } private unselect() { -- GitLab From 10bb546b6defbc33697aa41a2d60b1ce8a757c98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Grand?= <francois.grand@inrae.fr> Date: Tue, 24 May 2022 17:40:28 +0200 Subject: [PATCH 06/11] test(e2e): check that calculate button enabled status does not depend of other calculators validity status refs #544 --- e2e/calculate-button-validation.e2e-spec.ts | 74 +++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 e2e/calculate-button-validation.e2e-spec.ts diff --git a/e2e/calculate-button-validation.e2e-spec.ts b/e2e/calculate-button-validation.e2e-spec.ts new file mode 100644 index 000000000..c6c29ec75 --- /dev/null +++ b/e2e/calculate-button-validation.e2e-spec.ts @@ -0,0 +1,74 @@ +import { ListPage } from "./list.po"; +import { CalculatorPage } from "./calculator.po"; +import { Navbar } from "./navbar.po"; +import { browser } from "protractor"; +import { PreferencesPage } from "./preferences.po"; + +describe("Calculate button - ", () => { + let listPage: ListPage; + let calcPage: CalculatorPage; + let navBar: Navbar; + let prefPage: PreferencesPage; + + beforeAll(async () => { + prefPage = new PreferencesPage(); + listPage = new ListPage(); + calcPage = new CalculatorPage(); + navBar = new Navbar(); + }); + + beforeEach(async () => { + await prefPage.navigateTo(); + // disable evil option "empty fields on module creation" + await prefPage.disableEvilEmptyFields(); + await browser.sleep(200); + }); + + it("check button status only depends on calculator (no link between calculators)", async () => { + // start page + await navBar.clickNewCalculatorButton(); + await browser.sleep(200); + + // open PAB: chute calculator + await listPage.clickMenuEntryForCalcType(12); + await browser.sleep(200); + + // start page + await navBar.clickNewCalculatorButton(); + await browser.sleep(200); + + // open PAB: dimensions + await listPage.clickMenuEntryForCalcType(5); + await browser.sleep(200); + + // fill width field with invalid data + const inputW = calcPage.getInputById("W"); + await inputW.clear(); + await browser.sleep(20); + await inputW.sendKeys("-1"); + await browser.sleep(200); + debugger + // check that "compute" button is inactive + let calcButtonClone = calcPage.getCalculateButton(); + let disabledStateClone = await calcButtonClone.getAttribute("disabled"); + expect(disabledStateClone).toBe("true"); + + // back to PAB: chute + await navBar.clickCalculatorTab(0); + await browser.sleep(200); + + // check that "compute" button is active + calcButtonClone = calcPage.getCalculateButton(); + disabledStateClone = await calcButtonClone.getAttribute("disabled"); + expect(disabledStateClone).not.toBe("true"); + + // back to PAB: dimensions + await navBar.clickCalculatorTab(1); + await browser.sleep(200); + + // check that "compute" button is inactive + calcButtonClone = calcPage.getCalculateButton(); + disabledStateClone = await calcButtonClone.getAttribute("disabled"); + expect(disabledStateClone).toBe("true"); + }); +}); -- GitLab From 91b62e818ea9a21ce75068c1c45323a2f71f14e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Grand?= <francois.grand@inrae.fr> Date: Tue, 24 May 2022 17:42:04 +0200 Subject: [PATCH 07/11] fix: calculate enabled button of a calculator depends on other calculator validity status refs #544 --- src/app/components/field-set/field-set.component.ts | 6 +++--- .../fieldset-container/fieldset-container.component.ts | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/app/components/field-set/field-set.component.ts b/src/app/components/field-set/field-set.component.ts index 36d2701bf..f3095f8f7 100644 --- a/src/app/components/field-set/field-set.component.ts +++ b/src/app/components/field-set/field-set.component.ts @@ -266,10 +266,10 @@ export class FieldSetComponent implements DoCheck { return (paramsAreValid && selectAreValid); } - private updateValidity() { + private updateValidity(forceEmit: boolean = false) { // global validity this._isValid.value = this.computeValidity(); - if (this._isValid.changed) { + if (forceEmit || this._isValid.changed) { this.validChange.emit(); } } @@ -282,7 +282,7 @@ export class FieldSetComponent implements DoCheck { } public ngDoCheck() { - this.updateValidity(); + this.updateValidity(true); } /** diff --git a/src/app/components/fieldset-container/fieldset-container.component.ts b/src/app/components/fieldset-container/fieldset-container.component.ts index 74fac2031..7a59f0342 100644 --- a/src/app/components/fieldset-container/fieldset-container.component.ts +++ b/src/app/components/fieldset-container/fieldset-container.component.ts @@ -108,7 +108,7 @@ export class FieldsetContainerComponent implements DoCheck, AfterViewInit { } public ngDoCheck() { - this.updateValidity(); + this.updateValidity(true); } /** @@ -142,10 +142,10 @@ export class FieldsetContainerComponent implements DoCheck, AfterViewInit { return res; } - private updateValidity() { + private updateValidity(forceEmit: boolean = false) { // global validity this._isValid.value = this.computeValidity(); - if (this._isValid.changed) { + if (forceEmit || this._isValid.changed) { this.validChange.emit(); } } -- GitLab From cac860899066f5357163eba4b42fc118f7e2e2d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Grand?= <francois.grand@inrae.fr> Date: Wed, 25 May 2022 11:17:35 +0200 Subject: [PATCH 08/11] update jalhyd_branch refs #544 --- jalhyd_branch | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jalhyd_branch b/jalhyd_branch index 554b541df..d64531f13 100644 --- a/jalhyd_branch +++ b/jalhyd_branch @@ -1 +1 @@ -308-log-ameliorer-la-synthese-de-journal \ No newline at end of file +devel -- GitLab From 178fd968594f1e98b3d6df391b3ea6b54b4acb0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Grand?= <francois.grand@inrae.fr> Date: Wed, 25 May 2022 11:18:08 +0200 Subject: [PATCH 09/11] refactor: improve fieldset/fieldset container DoCheck validation refs #544 --- src/app/components/field-set/field-set.component.ts | 12 +++++++++++- .../fieldset-container.component.ts | 12 +++++++++++- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/src/app/components/field-set/field-set.component.ts b/src/app/components/field-set/field-set.component.ts index f3095f8f7..1bde22b41 100644 --- a/src/app/components/field-set/field-set.component.ts +++ b/src/app/components/field-set/field-set.component.ts @@ -146,6 +146,11 @@ export class FieldSetComponent implements DoCheck { @Output() protected tabPressed = new EventEmitter<any>(); + /** + * nombre d'appels à DoCheck + */ + private _DoCheckCount: number = 0; + public constructor( private notifService: NotificationsService, private i18nService: I18nService, @@ -282,7 +287,12 @@ export class FieldSetComponent implements DoCheck { } public ngDoCheck() { - this.updateValidity(true); + this._DoCheckCount++; + // à priori, DoCheck n'est plus utile après quelques cycles de détection de changement + // puisque la validité du fieldset est déterminée par les saisies dans les inputs + if (this._DoCheckCount < 3) { + this.updateValidity(true); + } } /** diff --git a/src/app/components/fieldset-container/fieldset-container.component.ts b/src/app/components/fieldset-container/fieldset-container.component.ts index 7a59f0342..c98450cdc 100644 --- a/src/app/components/fieldset-container/fieldset-container.component.ts +++ b/src/app/components/fieldset-container/fieldset-container.component.ts @@ -66,6 +66,11 @@ export class FieldsetContainerComponent implements DoCheck, AfterViewInit { @Output() protected tabPressed = new EventEmitter<any>(); + /** + * nombre d'appels à DoCheck + */ + private _DoCheckCount: number = 0; + public constructor( private i18nService: I18nService, private appSetupService: ApplicationSetupService @@ -108,7 +113,12 @@ export class FieldsetContainerComponent implements DoCheck, AfterViewInit { } public ngDoCheck() { - this.updateValidity(true); + this._DoCheckCount++; + // à priori, DoCheck n'est plus utile après quelques cycles de détection de changement + // puisque la validité du fieldset container est déterminée par les saisies dans les inputs + if (this._DoCheckCount < 3) { + this.updateValidity(true); + } } /** -- GitLab From 170981eabd414a215e1889040dd5c28423021bff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Grand?= <francois.grand@inrae.fr> Date: Wed, 25 May 2022 15:16:03 +0200 Subject: [PATCH 10/11] test(e2e): check pre-dams calculate button/schema items validity refs #544 --- e2e/calculate-button-validation.e2e-spec.ts | 73 ++++++++++++++++++++- e2e/calculator.po.ts | 7 ++ 2 files changed, 79 insertions(+), 1 deletion(-) diff --git a/e2e/calculate-button-validation.e2e-spec.ts b/e2e/calculate-button-validation.e2e-spec.ts index c6c29ec75..137071d83 100644 --- a/e2e/calculate-button-validation.e2e-spec.ts +++ b/e2e/calculate-button-validation.e2e-spec.ts @@ -1,7 +1,7 @@ import { ListPage } from "./list.po"; import { CalculatorPage } from "./calculator.po"; import { Navbar } from "./navbar.po"; -import { browser } from "protractor"; +import { browser, by, element } from "protractor"; import { PreferencesPage } from "./preferences.po"; describe("Calculate button - ", () => { @@ -71,4 +71,75 @@ describe("Calculate button - ", () => { disabledStateClone = await calcButtonClone.getAttribute("disabled"); expect(disabledStateClone).toBe("true"); }); + + describe("check button status in prébarrages - ", () => { + it("invalid data in Q input", async () => { + // start page + await navBar.clickNewCalculatorButton(); + await browser.sleep(200); + + // open prébarrages calculator + await listPage.clickMenuEntryForCalcType(30); + await browser.sleep(200); + + // Q input + const inputQ = element(by.id("Q")); + await inputQ.clear(); + await browser.sleep(200); + await inputQ.sendKeys("-1"); + await browser.sleep(200); + + calcPage.checkCalcButtonEnabled(false); + + // upstream item + const upstream = element(by.id("amont")); + // should be displayed in error + expect(await upstream.getAttribute('class')).toContain("node-error"); + }); + + it("add basin, invalid data in Q input", async () => { + // start page + await navBar.clickNewCalculatorButton(); + await browser.sleep(200); + + // open prébarrages calculator + await listPage.clickMenuEntryForCalcType(30); + await browser.sleep(200); + + // "add basin" button + const addBasinBtn = element(by.id("add-basin")); + await addBasinBtn.click(); + await browser.sleep(200); + + // upstream item + const upstream = element(by.id("amont")); + await upstream.click(); + await browser.sleep(200); + + // invalid data in Q input + const inputQ = element(by.id("Q")); + await inputQ.clear(); + await browser.sleep(200); + await inputQ.sendKeys("-1"); + await browser.sleep(200); + + // calculate button disabled ? + calcPage.checkCalcButtonEnabled(false); + + // upstream item displayed in error ? + expect(await upstream.getAttribute('class')).toContain("node-error"); + + // valid data in Q input + await inputQ.clear(); + await browser.sleep(200); + await inputQ.sendKeys("1"); + await browser.sleep(200); + + // calculate button still disabled ? (the basin is not connected to anything) + calcPage.checkCalcButtonEnabled(false); + + // upstream item displayed not in error ? + expect(await upstream.getAttribute('class')).not.toContain("node-error"); + }); + }); }); diff --git a/e2e/calculator.po.ts b/e2e/calculator.po.ts index 4be637886..757a2fa70 100644 --- a/e2e/calculator.po.ts +++ b/e2e/calculator.po.ts @@ -221,6 +221,13 @@ export class CalculatorPage { return await cloneButton.click(); } + // check that "compute" button is in given enabled/disabled state + checkCalcButtonEnabled(enabled: boolean) { + const calcButton = this.getCalculateButton(); + expect(calcButton.isEnabled()).toBe(enabled); + return calcButton; + } + async changeSelectValue(elt: ElementFinder, index: number) { await elt.click(); const optionId = ".cdk-overlay-container mat-option:nth-of-type(" + (index + 1) + ")"; -- GitLab From 110dc1528806d950c375b4c6c9d91c141919bec2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Grand?= <francois.grand@inrae.fr> Date: Wed, 25 May 2022 15:16:24 +0200 Subject: [PATCH 11/11] fix: pre-dams item not immediatly displayed in error when entering input invalid data after adding a basin refs #544 --- src/app/components/generic-calculator/calculator.component.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/app/components/generic-calculator/calculator.component.ts b/src/app/components/generic-calculator/calculator.component.ts index 31aa8ae8e..8b7f21b2a 100644 --- a/src/app/components/generic-calculator/calculator.component.ts +++ b/src/app/components/generic-calculator/calculator.component.ts @@ -532,9 +532,7 @@ export class GenericCalculatorComponent implements OnInit, DoCheck, AfterViewChe this._isUIValid.value = res; // update prébarrage schema validity - if (this._isUIValid.changed) { - this._pbSchemaComponent.updateItemsValidity(); - } + this._pbSchemaComponent?.updateItemsValidity(); } public getElementStyleDisplay(id: string) { -- GitLab