diff --git a/e2e/calculate-button-validation.e2e-spec.ts b/e2e/calculate-button-validation.e2e-spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..137071d830e0addd09d5f2f0df79d50930c58b91 --- /dev/null +++ b/e2e/calculate-button-validation.e2e-spec.ts @@ -0,0 +1,145 @@ +import { ListPage } from "./list.po"; +import { CalculatorPage } from "./calculator.po"; +import { Navbar } from "./navbar.po"; +import { browser, by, element } 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"); + }); + + 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 4be6378862b7589d86a2314e73a9f1a00ffa8b34..757a2fa707aec856605435c2f14dd7cd7dda971a 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) + ")"; diff --git a/jalhyd_branch b/jalhyd_branch index 554b541df35b8453451cb15ae370893ed9d6d49c..d64531f1305e091791eac674c3a36d86b9e17ddd 100644 --- a/jalhyd_branch +++ b/jalhyd_branch @@ -1 +1 @@ -308-log-ameliorer-la-synthese-de-journal \ No newline at end of file +devel diff --git a/src/app/components/field-set/field-set.component.ts b/src/app/components/field-set/field-set.component.ts index d4a614ab8aa982499eed43ba410e4f0b49900be4..1bde22b4165daa0a0064181ee0804a36359a9e5d 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 @@ -145,11 +146,18 @@ 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, private appSetupService: ApplicationSetupService - ) { } + ) { + this._isValid = new DefinedBoolean(); + } public hasRadioFix(): boolean { if (this._fieldSet.hasInputs) { @@ -223,8 +231,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 +268,15 @@ export class FieldSetComponent implements DoCheck { } // global validity - this._isValid = (paramsAreValid && selectAreValid); + return (paramsAreValid && selectAreValid); + } - this.validChange.emit(); + private updateValidity(forceEmit: boolean = false) { + // global validity + this._isValid.value = this.computeValidity(); + if (forceEmit || this._isValid.changed) { + this.validChange.emit(); + } } /** @@ -274,7 +287,12 @@ export class FieldSetComponent implements DoCheck { } public ngDoCheck() { - this.updateValidity(); + 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 630da2d19d881be096cadcd8f9e349be0281b85c..c98450cdcb53236feb992f69304ac6e7f21b405b 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 @@ -65,10 +66,17 @@ 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 - ) {} + ) { + this._isValid = new DefinedBoolean(); + } /** * Ajoute un nouveau sous-nub (Structure, PabCloisons, YAXN… selon le cas) @@ -105,17 +113,22 @@ export class FieldsetContainerComponent implements DoCheck, AfterViewInit { } public ngDoCheck() { - this.updateValidity(); + 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); + } } /** * 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 +146,18 @@ 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(forceEmit: boolean = false) { + // global validity + this._isValid.value = this.computeValidity(); + if (forceEmit || 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 9650543c6a0add5f27c5b2c5bb7aa61ea0e44f64..8b7f21b2a9b07e7fb2abf7554afe3f4929ef100b 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,16 +518,21 @@ 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 + 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 c30e69123c3fdd550ca1706144355a280100c1a9..3154a308fbdf1b9a83750ca2668e046f5bf5f578 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; } - private setUIValid(b: boolean) { - const old = this.isValid; + protected setUIValid(b: boolean) { this._isValidUI = b; - if (this.isValid !== old) { - this.emitValidChanged(); - } + this.setAndEmitValid(); } protected validateUI() { @@ -147,12 +155,10 @@ export abstract class GenericInputComponentDirective implements OnChanges { return isValid; } - private setModelValid(b: boolean) { - const old = this.isValid; + protected setModelValid(b: boolean) { 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 // setTimeout(() => { // en cas de pb, décommenter le timeout if (b) { diff --git a/src/app/components/ngparam-input/ngparam-input.component.ts b/src/app/components/ngparam-input/ngparam-input.component.ts index 781e29b15614cb04192292ed2c465c22c6939f95..c5d6d9809a4437af1efac6caff16b238337c984b 100644 --- a/src/app/components/ngparam-input/ngparam-input.component.ts +++ b/src/app/components/ngparam-input/ngparam-input.component.ts @@ -116,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": @@ -151,9 +171,7 @@ export class NgParamInputComponent extends GenericInputComponentDirective implem } public ngOnDestroy() { - if (!this.isValid && this.getModelValue() !== undefined) { - this.setModelValue(this, undefined); - } + // résoudre le conflit en supprimant le code ajouté cad ne conserver que removeObserver() this._paramDef.removeObserver(this); } } diff --git a/src/app/components/pab-table/pab-table.component.ts b/src/app/components/pab-table/pab-table.component.ts index 486789521a32d7efb0cf3e782f7f1be51bbf30ac..7d95042fbe040b74b0ce78be4079036ccdc2478e 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) */ @@ -1440,14 +1442,22 @@ 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() { + this._isValid.value = this.computeValidity(); + if (this._isValid.changed) { + this.validChange.emit(); + } } public get uitextEditPabTable() { 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 daad0ba4b73516eb51066cd77ed629066571ffe4..a9114f5f2974450548c16151f381f711af3adb29 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 071aa1fd9f9f6a03e8b668b4708efe7529b800ed..0c68a61120b4a2a02c1c523896277eeba9fe8083 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 @@ -316,14 +316,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 89255ef13580199816bf9f40f4a85a5ce31ea0e9..2571d9ec7cd0fab288fc147345c7e5ced4352a59 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); } diff --git a/src/app/components/pb-schema/pb-schema.component.ts b/src/app/components/pb-schema/pb-schema.component.ts index 7ea4ba81d47b6bbaede7cb478cbf84bef38b4b36..b2f00304cfc517b08766edf36ff7afa9808d46ec 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 */ @@ -303,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 @@ -334,7 +339,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) */ @@ -643,11 +648,18 @@ export class PbSchemaComponent implements AfterViewInit, AfterContentInit, OnIni private updateValidity() { // 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(); + this._isValid.value = this.model.hasUpDownConnection() && !this.model.hasBasinNotConnected(); + + if (this._isValid.changed) { + this.validChange.emit(); + } + } + + /** + * update all items validity rendering + */ + public updateItemsValidity() { + this.highlightErrorItems(this._selectedItem?.uid); } private clearHighlightedItems() { @@ -662,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() { diff --git a/src/app/definedvalue/definedboolean.ts b/src/app/definedvalue/definedboolean.ts new file mode 100644 index 0000000000000000000000000000000000000000..2d8d9a3b836c908b44a62f5e485af428b8f81a60 --- /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 0000000000000000000000000000000000000000..1e72754b0a76aa759ac722746da4e9c0e2678aba --- /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; + } +}