Skip to content

Commit adc9699

Browse files
rm3lfeloy
andauthored
[UI] Make sure form validation displays non-valid fields as red in all forms (#7064)
* Add validation to multi-container component This covers the following forms: - Add commands when adding a Composite Command Co-authored-by: Philippe Martin <[email protected]> * Add validation to multi-key-value component This covers the following forms: - Add Environment variables in Create Container - Add Deployment annotations in Create Container - Add Service annotations in Create Container Co-authored-by: Philippe Martin <[email protected]> * Add validation to multi-text component This covers the following forms: - Add Command in Create Container - Add Args in Create Container - Add Args in Create Image Co-authored-by: Philippe Martin <[email protected]> * Add validation to select-container component This covers the following forms: - Select or Create container in Add Exec Command - Select or create image component in Add Image Command - Select or create Resource in Add Apply command Co-authored-by: Philippe Martin <[email protected]> * Add validation to volume-mounts component This covers the following forms: - Select or Create volume mount in Create container Co-authored-by: Philippe Martin <[email protected]> * Add error helper message for invalid volume size quantities * Fix Cypress tests * Generate static UI * fixup! Add error helper message for invalid volume size quantities Co-authored-by: Philippe Martin <[email protected]> * Generate static UI --------- Co-authored-by: Philippe Martin <[email protected]>
1 parent 3f93ac0 commit adc9699

14 files changed

+230
-133
lines changed

pkg/apiserver-impl/ui/index.html

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/apiserver-impl/ui/main.4d8dc3ef32c88ca3.js pkg/apiserver-impl/ui/main.9400449aa2437590.js

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

ui/cypress/e2e/spec.cy.ts

+6-6
Original file line numberDiff line numberDiff line change
@@ -62,15 +62,15 @@ describe('devfile editor spec', () => {
6262
cy.getByDataCy('container-env-value-2').type("val3");
6363

6464
cy.getByDataCy('volume-mount-add').click();
65-
cy.getByDataCy('volume-mount-path-0').type("/mnt/vol1");
65+
cy.getByDataCy('volume-mount-path-0').type("/mnt/vol1", {force: true});
6666
cy.getByDataCy('volume-mount-name-0').click().get('mat-option').contains('volume1').click();
6767

6868
cy.getByDataCy('endpoints-add').click();
6969
cy.getByDataCy('endpoint-name-0').type("ep1");
7070
cy.getByDataCy('endpoint-targetPort-0').type("4001");
7171

7272
cy.getByDataCy('volume-mount-add').click();
73-
cy.getByDataCy('volume-mount-path-1').type("/mnt/vol2");
73+
cy.getByDataCy('volume-mount-path-1').type("/mnt/vol2", {force: true});
7474
cy.getByDataCy('volume-mount-name-1').click().get('mat-option').contains('(New Volume)').click();
7575
cy.getByDataCy('volume-name').type('volume2');
7676
cy.getByDataCy('volume-create').click();
@@ -134,11 +134,11 @@ describe('devfile editor spec', () => {
134134
cy.getByDataCy('container-source-mapping').type('/mnt/sources');
135135

136136
cy.getByDataCy('volume-mount-add').click();
137-
cy.getByDataCy('volume-mount-path-0').type("/mnt/vol1");
137+
cy.getByDataCy('volume-mount-path-0').type("/mnt/vol1", {force: true});
138138
cy.getByDataCy('volume-mount-name-0').click().get('mat-option').contains('volume1').click();
139139

140140
cy.getByDataCy('volume-mount-add').click();
141-
cy.getByDataCy('volume-mount-path-1').type("/mnt/vol2");
141+
cy.getByDataCy('volume-mount-path-1').type("/mnt/vol2", {force: true});
142142
cy.getByDataCy('volume-mount-name-1').click().get('mat-option').contains('(New Volume)').click();
143143
cy.getByDataCy('volume-name').type('volume2');
144144
cy.getByDataCy('volume-create').click();
@@ -397,11 +397,11 @@ describe('devfile editor spec', () => {
397397
cy.getByDataCy('container-image').type('an-image');
398398

399399
cy.getByDataCy('volume-mount-add').click();
400-
cy.getByDataCy('volume-mount-path-0').type("/mnt/vol1");
400+
cy.getByDataCy('volume-mount-path-0').type("/mnt/vol1", {force: true});
401401
cy.getByDataCy('volume-mount-name-0').click().get('mat-option').contains('volume1').click();
402402

403403
cy.getByDataCy('volume-mount-add').click();
404-
cy.getByDataCy('volume-mount-path-1').type("/mnt/vol2");
404+
cy.getByDataCy('volume-mount-path-1').type("/mnt/vol2", {force: true});
405405
cy.getByDataCy('volume-mount-name-1').click().get('mat-option').contains('(New Volume)').click();
406406
cy.getByDataCy('volume-name').type('volume2');
407407
cy.getByDataCy('volume-create').click();
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
<h3>{{title}}</h3>
22
<div class="group">
3-
<span *ngFor="let command of commands; let i=index">
3+
<span *ngFor="let control of form.controls; index as i">
44
<mat-form-field appearance="fill">
5-
<mat-select [value]="command" (selectionChange)="onCommandChange(i, $event.value)">
5+
<mat-label><span>Command</span></mat-label>
6+
<mat-select [formControl]="control">
67
<mat-option *ngFor="let commandElement of commandList" [value]="commandElement">{{commandElement}}</mat-option>
78
</mat-select>
89
</mat-form-field>
910
</span>
10-
<button *ngIf="commands.length > 0" mat-icon-button (click)="addCommand()">
11+
<button *ngIf="form.controls.length > 0" mat-icon-button (click)="addCommand('')">
1112
<mat-icon class="tab-icon material-icons-outlined">add</mat-icon>
1213
</button>
13-
<button *ngIf="commands.length == 0" mat-flat-button (click)="addCommand()">{{addLabel}}</button>
14+
<button *ngIf="form.controls.length == 0" mat-flat-button (click)="addCommand('')">{{addLabel}}</button>
1415
</div>
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
1-
import { Component, Input } from '@angular/core';
2-
import { NG_VALUE_ACCESSOR } from '@angular/forms';
1+
import {Component, forwardRef, Input} from '@angular/core';
2+
import {
3+
AbstractControl,
4+
ControlValueAccessor,
5+
FormArray,
6+
FormControl,
7+
FormGroup,
8+
NG_VALIDATORS,
9+
NG_VALUE_ACCESSOR, ValidationErrors, Validator,
10+
Validators
11+
} from '@angular/forms';
312

413
@Component({
514
selector: 'app-multi-command',
@@ -10,21 +19,32 @@ import { NG_VALUE_ACCESSOR } from '@angular/forms';
1019
provide: NG_VALUE_ACCESSOR,
1120
multi: true,
1221
useExisting: MultiCommandComponent
13-
}
22+
},
23+
{
24+
provide: NG_VALIDATORS,
25+
useExisting: forwardRef(() => MultiCommandComponent),
26+
multi: true,
27+
},
1428
]
1529
})
16-
export class MultiCommandComponent {
30+
export class MultiCommandComponent implements ControlValueAccessor, Validator {
1731

1832
@Input() addLabel: string = "";
1933
@Input() commandList: string[] = [];
2034
@Input() title: string = "";
2135

2236
onChange = (_: string[]) => {};
2337

24-
commands: string[] = [];
38+
form = new FormArray<FormControl>([]);
39+
40+
constructor() {
41+
this.form.valueChanges.subscribe(value => {
42+
this.onChange(value);
43+
});
44+
}
2545

26-
writeValue(value: any) {
27-
this.commands = value;
46+
writeValue(value: string[]) {
47+
value.forEach(v => this.addCommand(v));
2848
}
2949

3050
registerOnChange(onChange: any) {
@@ -33,13 +53,19 @@ export class MultiCommandComponent {
3353

3454
registerOnTouched(_: any) {}
3555

36-
addCommand() {
37-
this.commands.push("");
38-
this.onChange(this.commands);
56+
newCommand(cmdName : string) {
57+
return new FormControl(cmdName, [Validators.required]);
58+
}
59+
60+
addCommand(cmdName: string) {
61+
this.form.push(this.newCommand(cmdName));
3962
}
4063

41-
onCommandChange(i: number, cmd: string) {
42-
this.commands[i] = cmd;
43-
this.onChange(this.commands);
64+
/* Validator implementation */
65+
validate(control: AbstractControl): ValidationErrors | null {
66+
if (!this.form.valid) {
67+
return {'internal': true};
68+
}
69+
return null;
4470
}
4571
}
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
11
<div class="group">
2-
<span *ngFor="let entry of entries; let i=index">
3-
<mat-form-field class="mid-width" appearance="outline">
4-
<mat-label><span>Name</span></mat-label>
5-
<input [attr.data-cy]="dataCyPrefix+'-name-'+i" matInput [value]="entry.name" (change)="onKeyChange(i, $event)" (input)="onKeyChange(i, $event)">
6-
</mat-form-field>
7-
<mat-form-field class="mid-width" appearance="outline">
8-
<mat-label><span>Value</span></mat-label>
9-
<input [attr.data-cy]="dataCyPrefix+'-value-'+i" matInput [value]="entry.value" (change)="onValueChange(i, $event)" (input)="onValueChange(i, $event)">
10-
</mat-form-field>
11-
</span>
12-
<button [attr.data-cy]="dataCyPrefix+'-plus'" *ngIf="entries.length > 0" mat-icon-button (click)="addEntry()">
2+
<div *ngFor="let control of form.controls; index as i">
3+
<ng-container [formGroup]="control">
4+
<mat-form-field class="mid-width" appearance="outline">
5+
<mat-label><span>Name</span></mat-label>
6+
<input [attr.data-cy]="dataCyPrefix+'-name-'+i" matInput formControlName="name">
7+
</mat-form-field>
8+
<mat-form-field class="mid-width" appearance="outline">
9+
<mat-label><span>Value</span></mat-label>
10+
<input [attr.data-cy]="dataCyPrefix+'-value-'+i" matInput formControlName="value">
11+
</mat-form-field>
12+
</ng-container>
13+
</div>
14+
<button [attr.data-cy]="dataCyPrefix+'-plus'" *ngIf="form.controls.length > 0" mat-icon-button (click)="addEntry('', '')">
1315
<mat-icon class="tab-icon material-icons-outlined">add</mat-icon>
1416
</button>
15-
<button [attr.data-cy]="dataCyPrefix+'-add'" *ngIf="entries.length == 0" mat-flat-button (click)="addEntry()">{{addLabel}}</button>
17+
<button [attr.data-cy]="dataCyPrefix+'-add'" *ngIf="form.controls.length == 0" mat-flat-button (click)="addEntry('', '')">{{addLabel}}</button>
1618
</div>

ui/src/app/controls/multi-key-value/multi-key-value.component.ts

+30-22
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,16 @@
1-
import { Component, Input, forwardRef } from '@angular/core';
2-
import { AbstractControl, ControlValueAccessor, NG_VALIDATORS, NG_VALUE_ACCESSOR, ValidationErrors, Validator, Validators } from '@angular/forms';
1+
import {Component, forwardRef, Input} from '@angular/core';
2+
import {
3+
AbstractControl,
4+
ControlValueAccessor,
5+
FormArray,
6+
FormControl,
7+
FormGroup,
8+
NG_VALIDATORS,
9+
NG_VALUE_ACCESSOR,
10+
ValidationErrors,
11+
Validator,
12+
Validators
13+
} from '@angular/forms';
314

415
interface KeyValue {
516
name: string;
@@ -28,13 +39,19 @@ export class MultiKeyValueComponent implements ControlValueAccessor, Validator {
2839
@Input() dataCyPrefix: string = "";
2940
@Input() addLabel: string = "";
3041

42+
form = new FormArray<FormGroup>([]);
43+
3144
onChange = (_: KeyValue[]) => {};
3245
onValidatorChange = () => {};
3346

34-
entries: KeyValue[] = [];
47+
constructor() {
48+
this.form.valueChanges.subscribe(value => {
49+
this.onChange(value);
50+
});
51+
}
3552

3653
writeValue(value: KeyValue[]) {
37-
this.entries = value;
54+
value.forEach(v => this.addEntry(v.name, v.value));
3855
}
3956

4057
registerOnChange(onChange: any) {
@@ -43,30 +60,21 @@ export class MultiKeyValueComponent implements ControlValueAccessor, Validator {
4360

4461
registerOnTouched(_: any) {}
4562

46-
addEntry() {
47-
this.entries.push({name: "", value: ""});
48-
this.onChange(this.entries);
49-
}
50-
51-
onKeyChange(i: number, e: Event) {
52-
const target = e.target as HTMLInputElement;
53-
this.entries[i].name = target.value;
54-
this.onChange(this.entries);
63+
newKeyValueForm(kv: KeyValue): FormGroup {
64+
return new FormGroup({
65+
name: new FormControl(kv.name, [Validators.required]),
66+
value: new FormControl(kv.value, [Validators.required]),
67+
});
5568
}
5669

57-
onValueChange(i: number, e: Event) {
58-
const target = e.target as HTMLInputElement;
59-
this.entries[i].value = target.value;
60-
this.onChange(this.entries);
70+
addEntry(name: string, value: string) {
71+
this.form.push(this.newKeyValueForm({name, value}));
6172
}
6273

6374
/* Validator implementation */
6475
validate(control: AbstractControl): ValidationErrors | null {
65-
for (let i=0; i<this.entries.length; i++) {
66-
const entry = this.entries[i];
67-
if (entry.name == "" || entry.value == "") {
68-
return {'internal': true};
69-
}
76+
if (!this.form.valid) {
77+
return {'internal': true};
7078
}
7179
return null;
7280
}
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
<h3 *ngIf="title">{{title}}</h3>
22
<div class="group">
3-
<span *ngFor="let text of texts; let i=index">
3+
<span *ngFor="let control of form.controls; index as i">
44
<mat-form-field class="inline" appearance="outline">
55
<mat-label><span>{{label}}</span></mat-label>
6-
<input matInput [value]="text" (change)="onTextChange(i, $event)">
7-
</mat-form-field>
6+
<input matInput [formControl]="control">
7+
</mat-form-field>
88
</span>
9-
<button *ngIf="texts.length > 0" mat-icon-button (click)="addText()">
9+
<button *ngIf="form.controls.length > 0" mat-icon-button (click)="addText('')">
1010
<mat-icon class="tab-icon material-icons-outlined">add</mat-icon>
1111
</button>
12-
<button *ngIf="texts.length == 0" mat-flat-button (click)="addText()">{{addLabel}}</button>
12+
<button *ngIf="form.controls.length == 0" mat-flat-button (click)="addText('')">{{addLabel}}</button>
1313
</div>
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
1-
import { Component, Input } from '@angular/core';
2-
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
1+
import {Component, forwardRef, Input} from '@angular/core';
2+
import {
3+
AbstractControl,
4+
ControlValueAccessor,
5+
FormArray,
6+
FormControl,
7+
NG_VALIDATORS,
8+
NG_VALUE_ACCESSOR,
9+
ValidationErrors,
10+
Validator,
11+
Validators
12+
} from '@angular/forms';
313

414
@Component({
515
selector: 'app-multi-text',
@@ -10,24 +20,36 @@ import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
1020
provide: NG_VALUE_ACCESSOR,
1121
multi: true,
1222
useExisting: MultiTextComponent
13-
}
23+
},
24+
{
25+
provide: NG_VALIDATORS,
26+
useExisting: forwardRef(() => MultiTextComponent),
27+
multi: true,
28+
},
1429
]
1530
})
16-
export class MultiTextComponent implements ControlValueAccessor {
31+
export class MultiTextComponent implements ControlValueAccessor, Validator {
1732

1833
@Input() label: string = "";
1934
@Input() addLabel: string = "";
2035
@Input() title: string = "";
2136

2237
onChange = (_: string[]) => {};
2338

24-
texts: string[] = [];
39+
form = new FormArray<FormControl>([]);
2540

26-
writeValue(value: any) {
27-
if (value == null) {
28-
value = [];
29-
}
30-
this.texts = value;
41+
constructor() {
42+
this.form.valueChanges.subscribe(value => {
43+
this.onChange(value);
44+
});
45+
}
46+
47+
newText(text: string): FormControl {
48+
return new FormControl(text, [Validators.required]);
49+
}
50+
51+
writeValue(value: string[]) {
52+
value?.forEach(v => this.addText(v));
3153
}
3254

3355
registerOnChange(onChange: any) {
@@ -36,14 +58,15 @@ export class MultiTextComponent implements ControlValueAccessor {
3658

3759
registerOnTouched(_: any) {}
3860

39-
addText() {
40-
this.texts.push("");
41-
this.onChange(this.texts);
61+
addText(text: string) {
62+
this.form.push(this.newText(text));
4263
}
4364

44-
onTextChange(i: number, e: Event) {
45-
const target = e.target as HTMLInputElement;
46-
this.texts[i] = target.value;
47-
this.onChange(this.texts);
65+
/* Validator implementation */
66+
validate(control: AbstractControl): ValidationErrors | null {
67+
if (!this.form.valid) {
68+
return {'internal': true};
69+
}
70+
return null;
4871
}
4972
}

ui/src/app/controls/select-container/select-container.component.html

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<mat-form-field appearance="fill">
22
<mat-label>{{label}}</mat-label>
3-
<mat-select data-cy="select-container" [value]="container" (selectionChange)="onSelectChange($event.value)">
3+
<mat-select [formControl]="formCtrl" data-cy="select-container" (selectionChange)="onSelectChange($event.value)">
44
<mat-option *ngFor="let container of containers" [value]="container">{{container}}</mat-option>
55
<mat-option value="!">(New {{label}})</mat-option>
66
</mat-select>

0 commit comments

Comments
 (0)