From bd71a7450f8fe74a9e20cc1806804eae0bec24d0 Mon Sep 17 00:00:00 2001
From: "mathias.chouet" <mathias.chouet@irstea.fr>
Date: Wed, 13 Feb 2019 12:14:55 +0100
Subject: [PATCH] =?UTF-8?q?Fix=20#107=20Param=C3=A8tres=20variables=20:=20?=
 =?UTF-8?q?am=C3=A9lioration=20de=20la=20saisie=20d'une=20liste=20de=20val?=
 =?UTF-8?q?eurs?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../dialog-edit-param-values.component.html   |  29 ++-
 .../dialog-edit-param-values.component.scss   |   2 +-
 .../dialog-edit-param-values.component.ts     | 192 ++++++++++++------
 .../param-values/param-values.component.ts    |   1 +
 .../immediate-error-state-matcher.ts          |   2 +-
 src/locale/messages.en.json                   |   2 +
 src/locale/messages.fr.json                   |   2 +
 7 files changed, 158 insertions(+), 72 deletions(-)

diff --git a/src/app/components/dialog-edit-param-values/dialog-edit-param-values.component.html b/src/app/components/dialog-edit-param-values/dialog-edit-param-values.component.html
index c8fffe408..53718ce0f 100644
--- a/src/app/components/dialog-edit-param-values/dialog-edit-param-values.component.html
+++ b/src/app/components/dialog-edit-param-values/dialog-edit-param-values.component.html
@@ -40,12 +40,15 @@
         <form [formGroup]="valuesListForm">
             <mat-form-field>
                 <textarea matInput matTextareaAutosize [placeholder]="uitextListeValeurs" formControlName="valuesList"
-                    [value]="valuesList" (input)="valuesList = $event.target.value"></textarea>
+                    [value]="valuesList"></textarea>
+                    <!-- (input)="valuesList = $event.target.value" -->
                 <mat-error>
-                    {{ uitextMustBeListOfNumbers }}
-                    <!-- <div *ngIf="! vl.errors.required && vl.errors.jalhydModel">
-                        {{ vl.errors.jalhydModel.message }}
-                    </div> -->
+                    <span *ngIf="valuesListForm.controls.valuesList.hasError('model')">
+                        {{ valuesListForm.controls.valuesList.errors.model }}
+                    </span>
+                    <span *ngIf="! valuesListForm.controls.valuesList.hasError('model')">
+                        {{ uitextMustBeListOfNumbers }}
+                    </span>
                 </mat-error>
             </mat-form-field>
 
@@ -76,7 +79,17 @@
   </div>
 
   <div mat-dialog-actions>
-    <button mat-raised-button [mat-dialog-close]="true" cdkFocusInitial>
-      {{ uitextClose }}
-    </button>
+    <div *ngIf="isMinMax">
+        <button mat-raised-button [mat-dialog-close]="true" cdkFocusInitial>
+            {{ uitextClose }}
+        </button>
+    </div>
+    <div *ngIf="isListe">
+        <button mat-raised-button color="primary" [mat-dialog-close]="true" cdkFocusInitial>
+            {{ uitextCancel }}
+        </button>
+        <button mat-raised-button color="warn" (click)="onValidate()">
+            {{ uitextValidate }}
+        </button>
+    </div>
   </div>
diff --git a/src/app/components/dialog-edit-param-values/dialog-edit-param-values.component.scss b/src/app/components/dialog-edit-param-values/dialog-edit-param-values.component.scss
index b81faedb9..c8602bba0 100644
--- a/src/app/components/dialog-edit-param-values/dialog-edit-param-values.component.scss
+++ b/src/app/components/dialog-edit-param-values/dialog-edit-param-values.component.scss
@@ -17,5 +17,5 @@ mat-form-field {
 }
 
 .decimal-separator-and-file-container {
-    margin-top: 5px;
+    margin-top: 1em;
 }
diff --git a/src/app/components/dialog-edit-param-values/dialog-edit-param-values.component.ts b/src/app/components/dialog-edit-param-values/dialog-edit-param-values.component.ts
index fc65e81a2..436c323e2 100644
--- a/src/app/components/dialog-edit-param-values/dialog-edit-param-values.component.ts
+++ b/src/app/components/dialog-edit-param-values/dialog-edit-param-values.component.ts
@@ -38,12 +38,13 @@ export class DialogEditParamValuesComponent implements OnInit {
         // an explicit ReactiveForm is required for file input component
         this.valuesListForm = this.fb.group({
             file: [""],
-            valuesList: [this.valuesList, [
-                Validators.required,
-                Validators.pattern(new RegExp(this.valuesListPattern))
+            valuesList: ["", [ // not initialized with valuesList because param mode is MIN/MAX at this time
+                Validators.required
+                // Validators.pattern(new RegExp(this.valuesListPattern)) // behaves weirdly
             ]]
         });
 
+        // available options for select controls
         this.valueModes = [
             {
                 value: ParamValueMode.MINMAX,
@@ -73,8 +74,8 @@ export class DialogEditParamValuesComponent implements OnInit {
     public get valuesListPattern() {
         // standard pattern for decimal separator "." : ^-?([0-9]*\.)?([0-9]+[Ee]-?)?[0-9]+$
         const escapedDecimalSeparator = (this.decimalSeparator === "." ? "\\." : this.decimalSeparator);
-        const numberSubPattern = `^-?([0-9]*${escapedDecimalSeparator})?([0-9]+[Ee]-?)?[0-9]+$`;
-        const re = numberSubPattern + "(" + this.separatorPattern + numberSubPattern + ")*";
+        const numberSubPattern = `-?([0-9]*${escapedDecimalSeparator})?([0-9]+[Ee]-?)?[0-9]+`;
+        const re = `^${numberSubPattern}(${this.separatorPattern}${numberSubPattern})*$`;
         return re;
     }
 
@@ -93,50 +94,6 @@ export class DialogEditParamValuesComponent implements OnInit {
         this.param.valueMode = v;
     }
 
-    public get uiTextModeSelection() {
-        return this.intlService.localizeText("INFO_PARAMFIELD_PARAMVARIER_MODE");
-    }
-
-    public get uitextValeurMini() {
-        return this.intlService.localizeText("INFO_PARAMFIELD_VALEURMINI");
-    }
-
-    public get uitextValeurMaxi() {
-        return this.intlService.localizeText("INFO_PARAMFIELD_VALEURMAXI");
-    }
-
-    public get uitextPasVariation() {
-        return this.intlService.localizeText("INFO_PARAMFIELD_PASVARIATION");
-    }
-
-    public get uitextClose() {
-        return this.intlService.localizeText("INFO_OPTION_CLOSE");
-    }
-
-    public get uitextEditParamVariableValues() {
-        return this.intlService.localizeText("INFO_PARAMFIELD_PARAMVARIER_TITLE");
-    }
-
-    public get uitextListeValeurs() {
-        return this.intlService.localizeText("INFO_PARAMFIELD_PARAMVARIER_VALUES_FORMAT");
-    }
-
-    public get uitextMustBeANumber(): string {
-        return this.intlService.localizeText("ERROR_PARAM_MUST_BE_A_NUMBER");
-    }
-
-    public get uitextMustBeListOfNumbers() {
-        return sprintf(this.intlService.localizeText("INFO_PARAMFIELD_PARAMVARIER_VALUES_FORMAT_ERROR"), this.separatorPattern);
-    }
-
-    public get uitextDecimalSeparator() {
-        return this.intlService.localizeText("INFO_PARAMFIELD_PARAMVARIER_SEPARATEUR_DECIMAL");
-    }
-
-    public get uitextImportFile() {
-        return this.intlService.localizeText("INFO_PARAMFIELD_PARAMVARIER_IMPORT_FICHIER");
-    }
-
     public get isMinMax() {
         return this.param.valueMode === ParamValueMode.MINMAX;
     }
@@ -149,13 +106,7 @@ export class DialogEditParamValuesComponent implements OnInit {
      * renders model's numbers list as text values list (semicolon separated)
      */
     public get valuesList() {
-        let baseValue;
-        if (this.param.valueMode === ParamValueMode.LISTE) {
-            // otherwise JalHyd model complains when reading this.param.valueList
-            baseValue = this.param.valueList;
-        }
-        const ret = (baseValue || []).join(";");
-        return ret;
+        return (this.param.valueList || []).join(";");
     }
 
     /**
@@ -169,7 +120,7 @@ export class DialogEditParamValuesComponent implements OnInit {
             if (e.length > 0) {
                 // ensure decimal separator is "." for Number()
                 if (this.decimalSeparator !== ".") {
-                    const re = new RegExp(this.decimalSeparator, "g");
+                    const re = new RegExp(this.decimalSeparator, "g"); // @TODO remove "g" ?
                     e = e.replace(re, ".");
                 }
                 vals.push(Number(e));
@@ -178,11 +129,75 @@ export class DialogEditParamValuesComponent implements OnInit {
         this.param.valueList = vals;
     }
 
+    public onValidate() {
+        const status = this.validateValuesListString(this.valuesListForm.controls.valuesList.value);
+
+        if (status.ok) {
+            this.valuesListForm.controls.valuesList.setErrors(null);
+            this.valuesList = this.valuesListForm.controls.valuesList.value;
+            this.dialogRef.close();
+        } else {
+            this.valuesListForm.controls.valuesList.setErrors({ "model": status.message });
+        }
+    }
+
+    /**
+     * Returns { ok: true } if every element of list is a valid Number, { ok: false, message: "reason" } otherwise
+     * @param list a string containing a list of numbers separated by this.separatorPattern
+     */
+    private validateValuesListString(list: string) {
+        let message: string;
+        // 1. validate against general pattern. Should not be necessary since "validate" button is disabled
+        // if pattern is not matched, BUT, HTML5 "pattern" behaves weirdly and accepts multiple decimal
+        // separators, like "3.3.4" :/
+        let ok = new RegExp(this.valuesListPattern).test(list);
+
+        if (ok) {
+            // 2. validate each value
+            const separatorRE = new RegExp(this.separatorPattern);
+            const parts = list.trim().split(separatorRE);
+            for (let i = 0; i < parts.length && ok; i++) {
+                let e = parts[i];
+                if (e.length > 0) { // should always be true as separator might be several characters long
+                    // ensure decimal separator is "." for Number()
+                    if (this.decimalSeparator !== ".") {
+                        const re = new RegExp(this.decimalSeparator, "g"); // @TODO remove "g" ?
+                        e = e.replace(re, ".");
+                    }
+                    // 2.1 check it is a valid Number
+                    const n = (Number(e));
+                    // 2.2 validate against model
+                    let modelIsHappy = true;
+                    try {
+                        this.param.checkValue(n);
+                    } catch (e) {
+                        modelIsHappy = false;
+                        message = sprintf(this.intlService.localizeText("ERROR_INVALID_AT_POSITION"), i + 1)
+                            + " " + this.intlService.localizeMessage(e);
+                    }
+                    // synthesis
+                    ok = (
+                        ok
+                        && !isNaN(n)
+                        && isFinite(n)
+                        && modelIsHappy
+                    );
+                }
+            }
+        } else {
+            message = this.uitextMustBeListOfNumbers;
+        }
+
+        return { ok, message };
+    }
+
     public onFileSelected(event: any) {
         if (event.target.files && event.target.files.length) {
             const fr = new FileReader();
             fr.onload = () => {
-                this.valuesList = String(fr.result);
+                this.valuesListForm.controls.valuesList.setErrors(null);
+                // this.valuesList = String(fr.result);
+                this.valuesListForm.controls.valuesList.setValue(String(fr.result));
             };
             fr.onerror = () => {
                 fr.abort();
@@ -196,10 +211,6 @@ export class DialogEditParamValuesComponent implements OnInit {
         this.initVariableValues();
     }
 
-    public ngOnInit() {
-        this.initVariableValues();
-    }
-
     private initVariableValues() {
         // init min / max / step
         if (this.isMinMax) {
@@ -223,10 +234,67 @@ export class DialogEditParamValuesComponent implements OnInit {
                 } else {
                     this.param.valueList = [];
                 }
+                // set form control initial value
+                this.valuesListForm.controls.valuesList.setValue(this.valuesList);
             }
         }
     }
 
+    public get uiTextModeSelection() {
+        return this.intlService.localizeText("INFO_PARAMFIELD_PARAMVARIER_MODE");
+    }
+
+    public get uitextValeurMini() {
+        return this.intlService.localizeText("INFO_PARAMFIELD_VALEURMINI");
+    }
+
+    public get uitextValeurMaxi() {
+        return this.intlService.localizeText("INFO_PARAMFIELD_VALEURMAXI");
+    }
+
+    public get uitextPasVariation() {
+        return this.intlService.localizeText("INFO_PARAMFIELD_PASVARIATION");
+    }
+
+    public get uitextClose() {
+        return this.intlService.localizeText("INFO_OPTION_CLOSE");
+    }
+
+    public get uitextCancel() {
+        return this.intlService.localizeText("INFO_OPTION_CANCEL");
+    }
+
+    public get uitextValidate() {
+        return this.intlService.localizeText("INFO_OPTION_VALIDATE");
+    }
+
+    public get uitextEditParamVariableValues() {
+        return this.intlService.localizeText("INFO_PARAMFIELD_PARAMVARIER_TITLE");
+    }
+
+    public get uitextListeValeurs() {
+        return this.intlService.localizeText("INFO_PARAMFIELD_PARAMVARIER_VALUES_FORMAT");
+    }
+
+    public get uitextMustBeANumber(): string {
+        return this.intlService.localizeText("ERROR_PARAM_MUST_BE_A_NUMBER");
+    }
+
+    public get uitextMustBeListOfNumbers() {
+        return sprintf(this.intlService.localizeText("INFO_PARAMFIELD_PARAMVARIER_VALUES_FORMAT_ERROR"), this.separatorPattern);
+    }
+
+    public get uitextDecimalSeparator() {
+        return this.intlService.localizeText("INFO_PARAMFIELD_PARAMVARIER_SEPARATEUR_DECIMAL");
+    }
+
+    public get uitextImportFile() {
+        return this.intlService.localizeText("INFO_PARAMFIELD_PARAMVARIER_IMPORT_FICHIER");
+    }
+
+    public ngOnInit() {
+        this.initVariableValues();
+    }
 
   /*   protected validateModelValue(v: any): { isValid: boolean, message: string } {
       let msg: string;
diff --git a/src/app/components/param-values/param-values.component.ts b/src/app/components/param-values/param-values.component.ts
index acc77bfad..9a8f34c34 100644
--- a/src/app/components/param-values/param-values.component.ts
+++ b/src/app/components/param-values/param-values.component.ts
@@ -68,6 +68,7 @@ export class ParamValuesComponent implements AfterViewInit {
         this.editValuesDialog.open(
             DialogEditParamValuesComponent,
             {
+                disableClose: true,
                 data: {
                     param: this.param
                 }
diff --git a/src/app/formulaire/immediate-error-state-matcher.ts b/src/app/formulaire/immediate-error-state-matcher.ts
index e177f16ef..687edca5b 100644
--- a/src/app/formulaire/immediate-error-state-matcher.ts
+++ b/src/app/formulaire/immediate-error-state-matcher.ts
@@ -11,6 +11,6 @@ export class ImmediateErrorStateMatcher implements ErrorStateMatcher {
 
     isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
         const isSubmitted = form && form.submitted;
-        return !!(control && control.invalid && (control.dirty || control.touched || isSubmitted));
+        return !!(control && control.invalid /* && (control.dirty || control.touched || isSubmitted) */ );
     }
 }
diff --git a/src/locale/messages.en.json b/src/locale/messages.en.json
index 371f5ad99..34099e643 100644
--- a/src/locale/messages.en.json
+++ b/src/locale/messages.en.json
@@ -8,6 +8,7 @@
     "ERROR_DICHO_NULL_STEP": "Dichotomy (initial interval search): invalid null step",
     "ERROR_INTERVAL_OUTSIDE": "Interval: value %value% is outside of %interval",
     "ERROR_INTERVAL_UNDEF": "Interval: invalid 'undefined' value",
+    "ERROR_INVALID_AT_POSITION": "Position %s:",
     "ERROR_LANG_UNSUPPORTED": "internationalisation: unsupported '%locale%' locale",
     "ERROR_NEWTON_DERIVEE_NULLE": "Null function derivative in Newton computation",
     "ERROR_PARAM_NULL": "Parameter value must not be NULL",
@@ -129,6 +130,7 @@
     "INFO_OPTION_CLOSE": "Close",
     "INFO_OPTION_LOAD": "Load",
     "INFO_OPTION_SAVE": "Save",
+    "INFO_OPTION_VALIDATE": "Validate",
     "INFO_OPTION_ALL": "All",
     "INFO_OPTION_ALL_F": "All",
     "INFO_OPTION_NONE": "None",
diff --git a/src/locale/messages.fr.json b/src/locale/messages.fr.json
index 9784cab0a..505f25e4a 100644
--- a/src/locale/messages.fr.json
+++ b/src/locale/messages.fr.json
@@ -8,6 +8,7 @@
     "ERROR_DICHO_NULL_STEP": "Dichotomie&nbsp;: le pas pour la recherche de l'intervalle de départ ne devrait pas être nul",
     "ERROR_INTERVAL_OUTSIDE": "Interval&nbsp;: la valeur %value% est hors de l'intervalle %interval",
     "ERROR_INTERVAL_UNDEF": "Interval&nbsp;: valeur 'undefined' incorrecte",
+    "ERROR_INVALID_AT_POSITION": "Position %s :",
     "ERROR_LANG_UNSUPPORTED": "Internationalisation&nbsp;: locale '%locale%' non prise en charge",
     "ERROR_NEWTON_DERIVEE_NULLE": "Dérivée nulle dans un calcul par la méthode de Newton",
     "ERROR_PARAM_NULL": "La valeur du paramètre ne peut pas être NULL",
@@ -129,6 +130,7 @@
     "INFO_OPTION_CLOSE": "Fermer",
     "INFO_OPTION_LOAD": "Charger",
     "INFO_OPTION_SAVE": "Enregistrer",
+    "INFO_OPTION_VALIDATE": "Valider",
     "INFO_OPTION_ALL": "Tous",
     "INFO_OPTION_ALL_F": "Toutes",
     "INFO_OPTION_NONE": "Aucun",
-- 
GitLab