diff --git a/backend/src/main/resources/dynamic-http-metadata-provider.schema.json b/backend/src/main/resources/dynamic-http-metadata-provider.schema.json index 167e79ab1..da9635f96 100644 --- a/backend/src/main/resources/dynamic-http-metadata-provider.schema.json +++ b/backend/src/main/resources/dynamic-http-metadata-provider.schema.json @@ -599,8 +599,7 @@ "certificateFile": { "title": "label.certificate-file", "description": "tooltip.certificate-file", - "type": "string", - "default": "" + "type": "string" } }, "anyOf": [ diff --git a/backend/src/main/resources/i18n/messages_en.properties b/backend/src/main/resources/i18n/messages_en.properties index f7c238fb5..df0ae980c 100644 --- a/backend/src/main/resources/i18n/messages_en.properties +++ b/backend/src/main/resources/i18n/messages_en.properties @@ -401,6 +401,7 @@ message.org-incomplete=These three fields must all be entered if any single fiel message.type-required=Missing required property: Type message.match-required=Missing required property: Match message.value-required=Missing required property: Value +message.required=Missing required property. message.conflict=Conflict message.data-version-contention=Data Version Contention diff --git a/ui/src/app/metadata/domain/model/wizards/metadata-source-base.ts b/ui/src/app/metadata/domain/model/wizards/metadata-source-base.ts index ebc8921d8..673f176c1 100644 --- a/ui/src/app/metadata/domain/model/wizards/metadata-source-base.ts +++ b/ui/src/app/metadata/domain/model/wizards/metadata-source-base.ts @@ -60,6 +60,28 @@ export class MetadataSourceBase implements Wizard { } getValidators(entityIdList: string[]): { [key: string]: any } { + const checkRequiredChild = (value, property, form) => { + if (!value) { + return { + code: 'REQUIRED', + path: `#${property.path}`, + message: `message.required`, + params: [value] + }; + } + return null; + }; + const checkRequiredChildren = (value, property, form) => { + let errors; + Object.keys(value).forEach((item, index, all) => { + const error = checkRequiredChild(item, { path: `${index}` }, form); + if (error) { + errors = errors || []; + errors.push(error); + } + }); + return errors; + }; const checkOrg = (value, property, form) => { const org = property.parent; const orgValue = org.value || {}; diff --git a/ui/src/app/metadata/metadata.module.ts b/ui/src/app/metadata/metadata.module.ts index 946c89423..fd00a2274 100644 --- a/ui/src/app/metadata/metadata.module.ts +++ b/ui/src/app/metadata/metadata.module.ts @@ -9,7 +9,8 @@ import { MetadataRoutingModule } from './metadata.routing'; import { ProviderModule } from './provider/provider.module'; import { I18nModule } from '../i18n/i18n.module'; import { CustomWidgetRegistry } from '../schema-form/registry'; -import { WidgetRegistry } from 'ngx-schema-form'; +import { WidgetRegistry, SchemaValidatorFactory } from 'ngx-schema-form'; +import { CustomSchemaValidatorFactory } from '../schema-form/service/schema-validator'; @NgModule({ @@ -23,7 +24,11 @@ import { WidgetRegistry } from 'ngx-schema-form'; I18nModule ], providers: [ - { provide: WidgetRegistry, useClass: CustomWidgetRegistry } + { provide: WidgetRegistry, useClass: CustomWidgetRegistry }, + { + provide: SchemaValidatorFactory, + useClass: CustomSchemaValidatorFactory + } ], declarations: [ MetadataPageComponent diff --git a/ui/src/app/schema-form/model/messages.ts b/ui/src/app/schema-form/model/messages.ts new file mode 100644 index 000000000..6d6fa61fd --- /dev/null +++ b/ui/src/app/schema-form/model/messages.ts @@ -0,0 +1,3 @@ +export const HARD_CODED_REQUIRED_MSG = RegExp('Missing required property'); + +export const REQUIRED_MSG_OVERRIDE = 'message.required'; \ No newline at end of file diff --git a/ui/src/app/schema-form/schema-form.module.ts b/ui/src/app/schema-form/schema-form.module.ts index 842e903dd..13aba1369 100644 --- a/ui/src/app/schema-form/schema-form.module.ts +++ b/ui/src/app/schema-form/schema-form.module.ts @@ -1,5 +1,5 @@ import { NgModule } from '@angular/core'; -import { SchemaFormModule } from 'ngx-schema-form'; +import { SchemaFormModule, SchemaValidatorFactory } from 'ngx-schema-form'; import { CommonModule } from '@angular/common'; import { ReactiveFormsModule } from '@angular/forms'; @@ -23,6 +23,7 @@ import { CustomObjectWidget } from './widget/object/object.component'; import { CustomRadioComponent } from './widget/radio/radio.component'; import { InlineObjectListComponent } from './widget/array/inline-obj-list.component'; import { InlineObjectComponent } from './widget/object/inline-obj.component'; +import { CustomSchemaValidatorFactory } from './service/schema-validator'; export const COMPONENTS = [ BooleanRadioComponent, diff --git a/ui/src/app/schema-form/service/schema-validator.ts b/ui/src/app/schema-form/service/schema-validator.ts new file mode 100644 index 000000000..075a78ec9 --- /dev/null +++ b/ui/src/app/schema-form/service/schema-validator.ts @@ -0,0 +1,19 @@ +import * as ZSchema from 'z-schema'; +import { ZSchemaValidatorFactory } from 'ngx-schema-form'; + +export class CustomSchemaValidatorFactory extends ZSchemaValidatorFactory { + + protected zschema; + + constructor() { + super(); + this.createSchemaValidator(); + } + + private createSchemaValidator() { + this.zschema = new ZSchema({ + breakOnFirstError: false + }); + } +} + diff --git a/ui/src/app/schema-form/service/schema.service.ts b/ui/src/app/schema-form/service/schema.service.ts index 9cd7346de..617769de6 100644 --- a/ui/src/app/schema-form/service/schema.service.ts +++ b/ui/src/app/schema-form/service/schema.service.ts @@ -31,7 +31,8 @@ export class SchemaService { Object .keys(condition.properties) .some( - key => values.hasOwnProperty(key) ? condition.properties[key].enum[0] === values[key] : false + key => values.hasOwnProperty(key) && condition.properties[key].enum ? + condition.properties[key].enum[0] === values[key] : false ) ); currentConditions.forEach(el => { diff --git a/ui/src/app/schema-form/widget/datalist/datalist.component.html b/ui/src/app/schema-form/widget/datalist/datalist.component.html index 6ec585342..ece3d7878 100644 --- a/ui/src/app/schema-form/widget/datalist/datalist.component.html +++ b/ui/src/app/schema-form/widget/datalist/datalist.component.html @@ -21,4 +21,10 @@ role="textbox" [attr.aria-label]="schema.title | translate"> + + + , + error + + diff --git a/ui/src/app/schema-form/widget/datalist/datalist.component.ts b/ui/src/app/schema-form/widget/datalist/datalist.component.ts index 3393bd2c5..aecbe2fc3 100644 --- a/ui/src/app/schema-form/widget/datalist/datalist.component.ts +++ b/ui/src/app/schema-form/widget/datalist/datalist.component.ts @@ -2,6 +2,7 @@ import { Component, AfterViewInit } from '@angular/core'; import { ControlWidget } from 'ngx-schema-form'; import { SchemaService } from '../../service/schema.service'; +import { HARD_CODED_REQUIRED_MSG } from '../../model/messages'; @Component({ selector: 'datalist-component', @@ -26,4 +27,8 @@ export class DatalistComponent extends ControlWidget implements AfterViewInit { get required(): boolean { return this.widgetService.isRequired(this.formProperty); } + + getError(error: string): string { + return HARD_CODED_REQUIRED_MSG.test(error) ? 'message.required' : error; + } } diff --git a/ui/src/app/schema-form/widget/select/select.component.html b/ui/src/app/schema-form/widget/select/select.component.html index e66a83350..a7d1dd2ff 100644 --- a/ui/src/app/schema-form/widget/select/select.component.html +++ b/ui/src/app/schema-form/widget/select/select.component.html @@ -42,7 +42,7 @@ , - error + error diff --git a/ui/src/app/schema-form/widget/select/select.component.ts b/ui/src/app/schema-form/widget/select/select.component.ts index 5c6a2b65d..ecd50a75b 100644 --- a/ui/src/app/schema-form/widget/select/select.component.ts +++ b/ui/src/app/schema-form/widget/select/select.component.ts @@ -1,17 +1,21 @@ -import { Component, AfterViewInit } from '@angular/core'; +import { Component, AfterViewInit, OnDestroy } from '@angular/core'; import { SelectWidget } from 'ngx-schema-form'; import { SchemaService } from '../../service/schema.service'; -import { map, shareReplay } from 'rxjs/operators'; +import { map, shareReplay, startWith } from 'rxjs/operators'; +import { Subscription } from 'rxjs'; +import { HARD_CODED_REQUIRED_MSG } from '../../model/messages'; @Component({ selector: 'select-component', templateUrl: `./select.component.html` }) -export class CustomSelectComponent extends SelectWidget implements AfterViewInit { +export class CustomSelectComponent extends SelectWidget implements AfterViewInit, OnDestroy { options$: any; + errorSub: Subscription; + constructor( private widgetService: SchemaService ) { @@ -38,9 +42,23 @@ export class CustomSelectComponent extends SelectWidget implements AfterViewInit ) ); } + + this.errorSub = this.control.valueChanges.pipe(startWith(this.control.value)).subscribe(v => { + if (!v && this.required && !this.errorMessages.some(msg => HARD_CODED_REQUIRED_MSG.test(msg))) { + this.errorMessages.push('message.required'); + } + }); + } + + ngOnDestroy(): void { + this.errorSub.unsubscribe(); } get required(): boolean { return this.widgetService.isRequired(this.formProperty); } + + getError(error: string): string { + return HARD_CODED_REQUIRED_MSG.test(error) ? 'message.required' : error; + } } diff --git a/ui/src/app/schema-form/widget/string/string.component.html b/ui/src/app/schema-form/widget/string/string.component.html index 26964a59f..4986846ff 100644 --- a/ui/src/app/schema-form/widget/string/string.component.html +++ b/ui/src/app/schema-form/widget/string/string.component.html @@ -13,6 +13,7 @@ validate="true" [attr.readonly]="schema.readOnly?true:null" class="text-widget.id textline-widget form-control" + [class.is-invalid]="control.touched && !control.value && errorMessages.length" [attr.type]="this.getInputType()" [attr.id]="id" [formControl]="control" @@ -25,7 +26,7 @@ , - error + error { + if (!this.control.value + && this.required + && !this.errorMessages.some(msg => HARD_CODED_REQUIRED_MSG.test(msg)) + && this.errorMessages.indexOf(REQUIRED_MSG_OVERRIDE) < 0) { + this.errorMessages.push(REQUIRED_MSG_OVERRIDE); + } + if (!this.required) { + this.errorMessages = this.errorMessages.filter(e => e !== REQUIRED_MSG_OVERRIDE); + } + }); + } + + ngOnDestroy(): void { + this.errorSub.unsubscribe(); + } + get required(): boolean { - return this.widgetService.isRequired(this.formProperty); + const req = this.widgetService.isRequired(this.formProperty); + + return req; + } + + getError(error: string): string { + return HARD_CODED_REQUIRED_MSG.test(error) ? REQUIRED_MSG_OVERRIDE : error; } } diff --git a/ui/src/app/schema-form/widget/textarea/textarea.component.html b/ui/src/app/schema-form/widget/textarea/textarea.component.html index fb8164600..d47d6007d 100644 --- a/ui/src/app/schema-form/widget/textarea/textarea.component.html +++ b/ui/src/app/schema-form/widget/textarea/textarea.component.html @@ -18,4 +18,10 @@ [rows]="schema.widget.rows || 5" [formControl]="control" [attr.aria-label]="schema.title"> + + + , + error + + diff --git a/ui/src/app/schema-form/widget/textarea/textarea.component.ts b/ui/src/app/schema-form/widget/textarea/textarea.component.ts index 0f68e524f..fdbce4b35 100644 --- a/ui/src/app/schema-form/widget/textarea/textarea.component.ts +++ b/ui/src/app/schema-form/widget/textarea/textarea.component.ts @@ -1,20 +1,43 @@ -import { Component } from '@angular/core'; +import { Component, AfterViewInit, OnDestroy } from '@angular/core'; import { TextAreaWidget } from 'ngx-schema-form'; import { SchemaService } from '../../service/schema.service'; +import { Subscription } from 'rxjs'; +import { startWith } from 'rxjs/operators'; +import { HARD_CODED_REQUIRED_MSG } from '../../model/messages'; @Component({ selector: 'textarea-component', templateUrl: `./textarea.component.html` }) -export class CustomTextAreaComponent extends TextAreaWidget { +export class CustomTextAreaComponent extends TextAreaWidget implements AfterViewInit, OnDestroy { + + errorSub: Subscription; + constructor( private widgetService: SchemaService ) { super(); } + ngAfterViewInit(): void { + super.ngAfterViewInit(); + this.errorSub = this.control.valueChanges.pipe(startWith(this.control.value)).subscribe(v => { + if (!v && this.required && !this.errorMessages.some(msg => HARD_CODED_REQUIRED_MSG.test(msg))) { + this.errorMessages.push('message.required'); + } + }); + } + + ngOnDestroy(): void { + this.errorSub.unsubscribe(); + } + get required(): boolean { return this.widgetService.isRequired(this.formProperty); } + + getError(error: string): string { + return error.match('required').length ? 'message.required' : error; + } } diff --git a/ui/src/app/shared/autocomplete/autocomplete.component.scss b/ui/src/app/shared/autocomplete/autocomplete.component.scss index b721d9ea2..f794dce0c 100644 --- a/ui/src/app/shared/autocomplete/autocomplete.component.scss +++ b/ui/src/app/shared/autocomplete/autocomplete.component.scss @@ -1,5 +1,6 @@ @import "~bootstrap/scss/functions"; @import "~bootstrap/scss/variables"; +@import "../../../theme/palette"; .dropdown.form-group { margin-bottom: 0px; @@ -14,4 +15,16 @@ .btn-outline-secondary { border-color: $input-border-color; } + + &.is-invalid { + input.form-control { + border-color: $brand-danger; + } + } + + &.is-valid { + input.form-control { + border-color: $brand-success; + } + } } \ No newline at end of file diff --git a/ui/src/assets/schema/provider/filebacked-http-filters.schema.json b/ui/src/assets/schema/provider/filebacked-http-filters.schema.json index 4bb2f399c..820063bfa 100644 --- a/ui/src/assets/schema/provider/filebacked-http-filters.schema.json +++ b/ui/src/assets/schema/provider/filebacked-http-filters.schema.json @@ -50,8 +50,7 @@ "title": "label.certificate-file", "description": "tooltip.certificate-file", "type": "string", - "widget": "textline", - "default": "" + "widget": "textline" } }, "anyOf": [ @@ -59,6 +58,10 @@ "properties": { "requireSignedRoot": { "enum": [ true ] + }, + "certificateFile": { + "minLength": 1, + "type": "string" } }, "required": [