Skip to content

Commit

Permalink
Merged in feature/SHIBUI-907 (pull request #277)
Browse files Browse the repository at this point in the history
SHIBUI-907 - Fixes issues with required property validation messages

Approved-by: Jonathan Johnson <jj@scaldingspoon.com>
  • Loading branch information
rmathis committed Jan 25, 2019
2 parents 4d8039b + cb0b202 commit 2e320de
Show file tree
Hide file tree
Showing 18 changed files with 176 additions and 18 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -599,8 +599,7 @@
"certificateFile": {
"title": "label.certificate-file",
"description": "tooltip.certificate-file",
"type": "string",
"default": ""
"type": "string"
}
},
"anyOf": [
Expand Down
1 change: 1 addition & 0 deletions backend/src/main/resources/i18n/messages_en.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
22 changes: 22 additions & 0 deletions ui/src/app/metadata/domain/model/wizards/metadata-source-base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,28 @@ export class MetadataSourceBase implements Wizard<MetadataResolver> {
}

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 || {};
Expand Down
9 changes: 7 additions & 2 deletions ui/src/app/metadata/metadata.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand All @@ -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
Expand Down
3 changes: 3 additions & 0 deletions ui/src/app/schema-form/model/messages.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const HARD_CODED_REQUIRED_MSG = RegExp('Missing required property');

export const REQUIRED_MSG_OVERRIDE = 'message.required';
3 changes: 2 additions & 1 deletion ui/src/app/schema-form/schema-form.module.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -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,
Expand Down
19 changes: 19 additions & 0 deletions ui/src/app/schema-form/service/schema-validator.ts
Original file line number Diff line number Diff line change
@@ -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
});
}
}

3 changes: 2 additions & 1 deletion ui/src/app/schema-form/service/schema.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,10 @@
role="textbox"
[attr.aria-label]="schema.title | translate">
</auto-complete>
<small class="form-text text-danger" *ngIf="errorMessages && errorMessages.length && control.touched">
<ng-container *ngFor="let error of errorMessages; let first=first;">
<ng-container *ngIf="!first">, </ng-container>
<translate-i18n [key]="getError(error)">error</translate-i18n>
</ng-container>
</small>
</div>
5 changes: 5 additions & 0 deletions ui/src/app/schema-form/widget/datalist/datalist.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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;
}
}
2 changes: 1 addition & 1 deletion ui/src/app/schema-form/widget/select/select.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
<small class="form-text text-danger" *ngIf="errorMessages && errorMessages.length && control.touched">
<ng-container *ngFor="let error of errorMessages; let first=first;">
<ng-container *ngIf="!first">, </ng-container>
<translate-i18n [key]="error">error</translate-i18n>
<translate-i18n [key]="getError(error)">error</translate-i18n>
</ng-container>
</small>
<input *ngIf="schema.readOnly" [attr.name]="name" type="hidden" [formControl]="control">
Expand Down
24 changes: 21 additions & 3 deletions ui/src/app/schema-form/widget/select/select.component.ts
Original file line number Diff line number Diff line change
@@ -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
) {
Expand All @@ -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;
}
}
3 changes: 2 additions & 1 deletion ui/src/app/schema-form/widget/string/string.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -25,7 +26,7 @@
<small class="form-text text-danger" *ngIf="errorMessages && errorMessages.length && control.touched">
<ng-container *ngFor="let error of errorMessages; let first=first;">
<ng-container *ngIf="!first">, </ng-container>
<translate-i18n [key]="error">error</translate-i18n>
<translate-i18n [key]="getError(error)">error</translate-i18n>
</ng-container>
</small>
<small class="form-text text-secondary"
Expand Down
38 changes: 35 additions & 3 deletions ui/src/app/schema-form/widget/string/string.component.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,54 @@
import { Component } from '@angular/core';
import { Component, AfterViewInit, OnDestroy } from '@angular/core';
import { StringWidget } from 'ngx-schema-form';
import { Validators } from '@angular/forms';
import { SchemaService } from '../../service/schema.service';
import { startWith } from 'rxjs/operators';
import { Subscription } from 'rxjs';

import { HARD_CODED_REQUIRED_MSG, REQUIRED_MSG_OVERRIDE } from '../../model/messages';

@Component({
selector: 'custom-string',
templateUrl: `./string.component.html`,
styleUrls: ['../widget.component.scss']
})
export class CustomStringComponent extends StringWidget {
export class CustomStringComponent extends StringWidget implements AfterViewInit, OnDestroy {

errorSub: Subscription;

constructor(
private widgetService: SchemaService
) {
super();
}

ngAfterViewInit(): void {
super.ngAfterViewInit();
let listener = this.formProperty.parent ? this.formProperty.parent : this.control;
this.errorSub = listener.valueChanges.pipe(startWith(listener.value)).subscribe(v => {
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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,10 @@
[rows]="schema.widget.rows || 5"
[formControl]="control"
[attr.aria-label]="schema.title"></textarea>
<small class="form-text text-danger" *ngIf="errorMessages && errorMessages.length && control.touched">
<ng-container *ngFor="let error of errorMessages; let first=first;">
<ng-container *ngIf="!first">, </ng-container>
<translate-i18n [key]="getError(error)">error</translate-i18n>
</ng-container>
</small>
</div>
27 changes: 25 additions & 2 deletions ui/src/app/schema-form/widget/textarea/textarea.component.ts
Original file line number Diff line number Diff line change
@@ -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;
}
}
13 changes: 13 additions & 0 deletions ui/src/app/shared/autocomplete/autocomplete.component.scss
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
@import "~bootstrap/scss/functions";
@import "~bootstrap/scss/variables";
@import "../../../theme/palette";

.dropdown.form-group {
margin-bottom: 0px;
Expand All @@ -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;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,15 +50,18 @@
"title": "label.certificate-file",
"description": "tooltip.certificate-file",
"type": "string",
"widget": "textline",
"default": ""
"widget": "textline"
}
},
"anyOf": [
{
"properties": {
"requireSignedRoot": {
"enum": [ true ]
},
"certificateFile": {
"minLength": 1,
"type": "string"
}
},
"required": [
Expand Down

0 comments on commit 2e320de

Please sign in to comment.