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 : le pas pour la recherche de l'intervalle de départ ne devrait pas être nul", "ERROR_INTERVAL_OUTSIDE": "Interval : la valeur %value% est hors de l'intervalle %interval", "ERROR_INTERVAL_UNDEF": "Interval : valeur 'undefined' incorrecte", + "ERROR_INVALID_AT_POSITION": "Position %s :", "ERROR_LANG_UNSUPPORTED": "Internationalisation : 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