Restructure front end filters setup

- Use FieldManager
- Change ValidationManager to support save and filter validation
- Use reactive forms formControl in varchar filter
- Add null checks to datetime formatter
- Add field type to criteria
- Set form control value on field init
- display messages for edit or filter modes
- Validate filter input
- Display warning messages when not valid
- Adjust karma / jasmine tests
This commit is contained in:
Clemente Raposo 2020-12-31 15:08:27 +00:00 committed by Dillon-Brown
parent 0916d14a5c
commit 61de0618ea
15 changed files with 270 additions and 93 deletions

View file

@ -27,11 +27,15 @@ export class BaseFieldComponent implements FieldComponentInterface {
newValue = this.typeFormatter.toInternalFormat(this.field.type, newValue);
}
this.field.value = newValue;
this.setFieldValue(newValue);
}));
}
}
protected setFieldValue(newValue): void {
this.field.value = newValue;
}
protected unsubscribeAll(): void {
this.subs.forEach(sub => sub.unsubscribe());
}

View file

@ -45,7 +45,7 @@ class FieldTestHostComponent {
{field: buildField({type: 'varchar', value: 'My Varchar'}), mode: 'list', expected: 'My Varchar'},
{field: buildField({type: 'varchar', value: 'My Varchar'}), mode: 'edit', expected: 'My Varchar'},
{
field: buildField({type: 'varchar', criteria: {values: ['test'], operator: '='}}),
field: buildField({type: 'varchar', value: 'test', criteria: {values: ['test'], operator: '='}}),
mode: 'filter',
expected: 'test'
},

View file

@ -50,7 +50,7 @@ export class FieldComponent {
}
isEdit(): boolean {
return this.mode === 'edit';
return this.mode === 'edit' || this.mode === 'filter';
}
getLink(): string {

View file

@ -1 +1,4 @@
<input type="text" [ngClass]="klass" [(ngModel)]="value">
<input [class.is-invalid]="field.formControl.invalid && field.formControl.touched"
[formControl]="field.formControl"
[ngClass]="klass"
type="text">

View file

@ -1,7 +1,6 @@
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
import {Component} from '@angular/core';
import {VarcharFilterFieldComponent} from './filter.component';
import {FormsModule} from '@angular/forms';
import {Field} from '@app-common/record/field.model';
import {UserPreferenceStore} from '@store/user-preference/user-preference.store';
import {userPreferenceStoreMock} from '@store/user-preference/user-preference.store.spec.mock';
@ -12,6 +11,9 @@ import {datetimeFormatterMock} from '@services/formatters/datetime/datetime-form
import {DateFormatter} from '@services/formatters/datetime/date-formatter.service';
import {dateFormatterMock} from '@services/formatters/datetime/date-formatter.service.spec.mock';
import {CurrencyFormatter} from '@services/formatters/currency/currency-formatter.service';
import {VarcharFilterFieldModule} from '@fields/varchar/templates/filter/filter.module';
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
import {FormControl} from '@angular/forms';
@Component({
selector: 'varchar-filter-field-test-host-component',
@ -20,10 +22,12 @@ import {CurrencyFormatter} from '@services/formatters/currency/currency-formatte
class VarcharFilterFieldTestHostComponent {
field: Field = {
type: 'varchar',
value: 'test filter value',
criteria: {
values: ['test filter value'],
operator: '='
}
},
formControl: new FormControl('test filter value')
};
}
@ -38,7 +42,8 @@ describe('VarcharFilterFieldComponent', () => {
VarcharFilterFieldComponent,
],
imports: [
FormsModule
VarcharFilterFieldModule,
BrowserAnimationsModule
],
providers: [
{provide: UserPreferenceStore, useValue: userPreferenceStoreMock},
@ -70,7 +75,7 @@ describe('VarcharFilterFieldComponent', () => {
it('should have update input when field changes', async(() => {
expect(testHostComponent).toBeTruthy();
testHostComponent.field.criteria.values = ['New Field value'];
testHostComponent.field.formControl.setValue('New Field value');
testHostFixture.detectChanges();
testHostFixture.whenStable().then(() => {

View file

@ -1,4 +1,4 @@
import {Component} from '@angular/core';
import {Component, OnDestroy, OnInit} from '@angular/core';
import {BaseFieldComponent} from '@fields/base/base-field.component';
import {DataTypeFormatter} from '@services/formatters/data-type.formatter.service';
@ -7,23 +7,31 @@ import {DataTypeFormatter} from '@services/formatters/data-type.formatter.servic
templateUrl: './filter.component.html',
styleUrls: []
})
export class VarcharFilterFieldComponent extends BaseFieldComponent {
export class VarcharFilterFieldComponent extends BaseFieldComponent implements OnInit, OnDestroy {
constructor(protected typeFormatter: DataTypeFormatter) {
super(typeFormatter);
}
get value(): string {
ngOnInit(): void {
let current = '';
if (this.field.criteria.values && this.field.criteria.values.length > 0) {
current = this.field.criteria.values[0];
}
return current;
this.field.value = current;
const formattedValue = this.typeFormatter.toUserFormat(this.field.type, current, {mode: 'edit'});
this.field.formControl.setValue(formattedValue);
this.subscribeValueChanges();
}
set value(newValue: string) {
ngOnDestroy(): void {
this.unsubscribeAll();
}
protected setFieldValue(newValue): void {
this.field.value = newValue;
this.field.criteria.operator = '=';
this.field.criteria.values = [newValue];

View file

@ -3,7 +3,7 @@ import {CommonModule} from '@angular/common';
import {AppManagerModule} from '@base/app-manager/app-manager.module';
import {VarcharFilterFieldComponent} from './filter.component';
import {FormsModule} from '@angular/forms';
import {FormsModule, ReactiveFormsModule} from '@angular/forms';
@NgModule({
declarations: [VarcharFilterFieldComponent],
@ -11,7 +11,8 @@ import {FormsModule} from '@angular/forms';
imports: [
CommonModule,
AppManagerModule.forChild(VarcharFilterFieldComponent),
FormsModule
FormsModule,
ReactiveFormsModule
]
})
export class VarcharFilterFieldModule {

View file

@ -3,6 +3,7 @@ import {BulkActionsMap} from '@app-common/actions/bulk-action.model';
import {LineAction} from '@app-common/actions/line-action.model';
import {ChartTypesMap} from '@app-common/containers/chart/chart.model';
import {WidgetMetadata} from '@app-common/metadata/widget.metadata';
import {FieldDefinition} from '@app-common/record/field.model';
export interface ListViewMeta {
fields: ColumnDefinition[];
@ -33,6 +34,7 @@ export interface SearchMetaField {
label?: string;
default?: boolean;
options?: string;
fieldDefinition?: FieldDefinition;
}
export interface SearchMeta {

View file

@ -7,7 +7,7 @@ export interface AttributeMap {
export interface Record {
id?: string;
type: string;
type?: string;
module: string;
attributes: AttributeMap;
fields?: FieldMap;

View file

@ -1,5 +1,6 @@
export interface SearchCriteriaFieldFilter {
field?: string;
fieldType?: string;
operator: string;
values?: string[];
start?: string;

View file

@ -14,6 +14,7 @@ import {LanguageStore} from '@store/language/language.store';
import {languageStoreMock} from '@store/language/language.store.spec.mock';
import {RouterTestingModule} from '@angular/router/testing';
import {ApolloTestingModule} from 'apollo-angular/testing';
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
describe('ListFilterComponent', () => {
let testHostComponent: ListFilterComponent;
@ -31,7 +32,8 @@ describe('ListFilterComponent', () => {
DropdownButtonModule,
FieldGridModule,
RouterTestingModule,
ApolloTestingModule
ApolloTestingModule,
BrowserAnimationsModule
],
providers: [
{provide: ListViewStore, useValue: listviewStoreMock},

View file

@ -5,10 +5,15 @@ import {DropdownButtonInterface} from '@components/dropdown-button/dropdown-butt
import {ButtonInterface} from '@components/button/button.model';
import {deepClone} from '@base/app-common/utils/object-utils';
import {combineLatest, Observable} from 'rxjs';
import {map} from 'rxjs/operators';
import {Field} from '@app-common/record/field.model';
import {filter, map, startWith, take} from 'rxjs/operators';
import {Field, FieldMap} from '@app-common/record/field.model';
import {SearchCriteria, SearchCriteriaFieldFilter} from '@app-common/views/list/search-criteria.model';
import {Filter, SearchMetaField} from '@app-common/metadata/list.metadata.model';
import {FieldManager} from '@services/record/field/field.manager';
import {ViewFieldDefinition} from '@app-common/metadata/metadata.model';
import {Record} from '@app-common/record/record.model';
import {AbstractControl, FormGroup} from '@angular/forms';
import {MessageService} from '@services/message/message.service';
export interface FilterDataSource {
getFilter(): Observable<Filter>;
@ -34,8 +39,14 @@ export class ListFilterComponent implements OnInit {
searchCriteria: SearchCriteria;
vm$: Observable<any>;
private record: Record;
constructor(protected listStore: ListViewStore, protected language: LanguageStore) {
constructor(
protected listStore: ListViewStore,
protected language: LanguageStore,
protected fieldManager: FieldManager,
protected message: MessageService
) {
this.vm$ = combineLatest([listStore.criteria$, listStore.metadata$]).pipe(
map(([criteria, metadata]) => {
@ -51,6 +62,11 @@ export class ListFilterComponent implements OnInit {
this.reset();
this.record = {
module: this.listStore.getModuleName(),
attributes: {}
} as Record;
this.initFields();
this.initGridButtons();
this.initHeaderButtons();
@ -72,15 +88,23 @@ export class ListFilterComponent implements OnInit {
}
const searchFields = searchMeta.layout[type];
const fields = {} as FieldMap;
const formControls = {} as { [key: string]: AbstractControl };
Object.keys(searchFields).forEach(key => {
const name = searchFields[key].name;
fields[name] = this.buildField(searchFields[key], languages, searchCriteria);
formControls[name] = fields[name].formControl;
if (name.includes('_only')) {
this.special.push(this.buildField(searchFields[key], languages, searchCriteria));
this.special.push(fields[name]);
} else {
this.fields.push(this.buildField(searchFields[key], languages, searchCriteria));
this.fields.push(fields[name]);
}
});
this.record.formGroup = new FormGroup(formControls);
}
protected reset(): void {
@ -128,21 +152,34 @@ export class ListFilterComponent implements OnInit {
}
protected buildField(searchField: SearchMetaField, languages: LanguageStrings, searchCriteria: SearchCriteria): Field {
const module = this.listStore.appState.module;
const fieldName = searchField.name;
this.searchCriteria.filters[fieldName] = this.initFieldFilter(searchCriteria, fieldName);
const type = searchField.type;
this.searchCriteria.filters[fieldName] = this.initFieldFilter(searchCriteria, fieldName, type);
return {
type: 'varchar',
value: '',
const definition = {
name: searchField.name,
label: this.language.getFieldLabel(searchField.label, module, languages),
criteria: this.searchCriteria.filters[fieldName]
} as Field;
label: searchField.label,
type,
fieldDefinition: {}
} as ViewFieldDefinition;
if (searchField.fieldDefinition) {
definition.fieldDefinition = searchField.fieldDefinition;
}
if (type === 'bool' || type === 'boolean') {
definition.fieldDefinition.options = 'dom_int_bool';
}
const field = this.fieldManager.buildFilterField(this.record, definition, this.language);
field.criteria = this.searchCriteria.filters[fieldName];
return field;
}
protected initFieldFilter(searchCriteria: SearchCriteria, fieldName: string): SearchCriteriaFieldFilter {
protected initFieldFilter(searchCriteria: SearchCriteria, fieldName: string, fieldType: string): SearchCriteriaFieldFilter {
let fieldCriteria: SearchCriteriaFieldFilter;
if (searchCriteria.filters[fieldName]) {
@ -150,8 +187,9 @@ export class ListFilterComponent implements OnInit {
} else {
fieldCriteria = {
field: fieldName,
fieldType,
operator: '',
values: [],
values: []
};
}
@ -159,8 +197,28 @@ export class ListFilterComponent implements OnInit {
}
protected applyFilter(): void {
this.listStore.showFilters = false;
this.listStore.updateSearchCriteria(this.searchCriteria);
this.validate().pipe(take(1)).subscribe(valid => {
if (valid) {
this.listStore.showFilters = false;
this.listStore.updateSearchCriteria(this.searchCriteria);
return;
}
this.message.addWarningMessageByKey('LBL_VALIDATION_ERRORS');
});
}
protected validate(): Observable<boolean> {
this.record.formGroup.markAllAsTouched();
return this.record.formGroup.statusChanges.pipe(
startWith(this.record.formGroup.status),
filter(status => status !== 'PENDING'),
take(1),
map(status => status === 'VALID')
);
}
protected clearFilter(): void {

View file

@ -91,7 +91,17 @@ export class DatetimeFormatter implements Formatter {
}
toUserFormat(dateString: string): string {
return formatDate(dateString, this.getUserFormat(), this.locale);
if (!dateString) {
return '';
}
const dateTime = this.toDateTime(dateString);
if (!dateTime.isValid) {
return '';
}
return formatDate(dateTime.toJSDate(), this.getUserFormat(), this.locale);
}
toInternalFormat(dateString: string): string {

View file

@ -1,7 +1,7 @@
import {Record} from '@app-common/record/record.model';
import {ViewFieldDefinition} from '@app-common/metadata/metadata.model';
import {LanguageStore} from '@store/language/language.store';
import {FormControl} from '@angular/forms';
import {AsyncValidatorFn, FormControl, ValidatorFn} from '@angular/forms';
import {Field, FieldDefinition} from '@app-common/record/field.model';
import {Injectable} from '@angular/core';
import {ValidationManager} from '@services/record/validation/validation.manager';
@ -28,14 +28,40 @@ export class FieldManager {
public buildField(record: Record, viewField: ViewFieldDefinition, language: LanguageStore = null): Field {
const definition = (viewField && viewField.fieldDefinition) || {} as FieldDefinition;
const {value, valueList} = this.parseValue(viewField, definition, record);
const {validators, asyncValidators} = this.getValidators(record, viewField);
return this.setupField(viewField, value, valueList, record, definition, validators, asyncValidators, language);
}
public buildFilterField(record: Record, viewField: ViewFieldDefinition, language: LanguageStore = null): Field {
const definition = (viewField && viewField.fieldDefinition) || {} as FieldDefinition;
const {value, valueList} = this.parseValue(viewField, definition, record);
const {validators, asyncValidators} = this.getFilterValidators(record, viewField);
return this.setupField(viewField, value, valueList, record, definition, validators, asyncValidators, language);
}
public getFieldLabel(label: string, module: string, language: LanguageStore): string {
const languages = language.getLanguageStrings();
return language.getFieldLabel(label, module, languages);
}
protected parseValue(
viewField: ViewFieldDefinition,
definition: FieldDefinition,
record: Record
): { value: string; valueList: string[] } {
const type = (viewField && viewField.type) || '';
const source = (definition && definition.source) || '';
const rname = (definition && definition.rname) || 'name';
const viewName = viewField.name || '';
let value;
let valueList = null;
let value: string;
let valueList: string[] = null;
if (!viewName) {
if (!viewName || !record.attributes[viewName]) {
value = '';
} else if (type === 'relate' && source === 'non-db' && rname !== '') {
value = record.attributes[viewName][rname];
@ -48,8 +74,40 @@ export class FieldManager {
value = null;
}
const validators = this.validationManager.getValidations(record.module, viewField, record);
const asyncValidators = this.validationManager.getAsyncValidations(record.module, viewField, record);
return {value, valueList};
}
protected getValidators(
record: Record,
viewField: ViewFieldDefinition
): { validators: ValidatorFn[]; asyncValidators: AsyncValidatorFn[] } {
const validators = this.validationManager.getSaveValidations(record.module, viewField, record);
const asyncValidators = this.validationManager.getAsyncSaveValidations(record.module, viewField, record);
return {validators, asyncValidators};
}
protected getFilterValidators(
record: Record,
viewField: ViewFieldDefinition
): { validators: ValidatorFn[]; asyncValidators: AsyncValidatorFn[] } {
const validators = this.validationManager.getFilterValidations(record.module, viewField, record);
const asyncValidators: AsyncValidatorFn[] = [];
return {validators, asyncValidators};
}
protected setupField(
viewField: ViewFieldDefinition,
value: string,
valueList: string[],
record: Record,
definition: FieldDefinition,
validators: ValidatorFn[],
asyncValidators: AsyncValidatorFn[],
language: LanguageStore
): Field {
const formattedValue = this.typeFormatter.toUserFormat(viewField.type, value, {mode: 'edit'});
@ -73,12 +131,6 @@ export class FieldManager {
if (language) {
field.label = this.getFieldLabel(viewField.label, record.module, language);
}
return field;
}
public getFieldLabel(label: string, module: string, language: LanguageStore): string {
const languages = language.getLanguageStrings();
return language.getFieldLabel(label, module, languages);
}
}

View file

@ -4,7 +4,7 @@ import {Record} from '@app-common/record/record.model';
import {ViewFieldDefinition} from '@app-common/metadata/metadata.model';
import {AsyncValidatorFn, ValidatorFn} from '@angular/forms';
import {AsyncValidatorInterface} from '@services/record/validation/aync-validator.Interface';
import {OverridableMap} from '@app-common/types/OverridableMap';
import {MapEntry, OverridableMap} from '@app-common/types/OverridableMap';
import {RequiredValidator} from '@services/record/validation/validators/required.validator';
import {CurrencyValidator} from '@services/record/validation/validators/currency.validator';
import {DateValidator} from '@services/record/validation/validators/date.validator';
@ -16,25 +16,32 @@ import {PhoneValidator} from '@services/record/validation/validators/phone.valid
import {RangeValidator} from '@services/record/validation/validators/range.validator';
export interface ValidationManagerInterface {
registerValidator(module: string, key: string, validator: ValidatorInterface): void;
registerSaveValidator(module: string, key: string, validator: ValidatorInterface): void;
excludeValidator(module: string, key: string): void;
registerFilterValidator(module: string, key: string, validator: ValidatorInterface): void;
registerAsyncValidator(module: string, key: string, validator: AsyncValidatorInterface): void;
excludeSaveValidator(module: string, key: string): void;
excludeAsyncValidator(module: string, key: string): void;
excludeFilterValidator(module: string, key: string): void;
getValidations(module: string, viewField: ViewFieldDefinition, record: Record): ValidatorFn[];
registerAsyncSaveValidator(module: string, key: string, validator: AsyncValidatorInterface): void;
getAsyncValidations(module: string, viewField: ViewFieldDefinition, record: Record): AsyncValidatorFn[];
excludeAsyncSaveValidator(module: string, key: string): void;
getSaveValidations(module: string, viewField: ViewFieldDefinition, record: Record): ValidatorFn[];
getFilterValidations(module: string, viewField: ViewFieldDefinition, record: Record): ValidatorFn[];
getAsyncSaveValidations(module: string, viewField: ViewFieldDefinition, record: Record): AsyncValidatorFn[];
}
@Injectable({
providedIn: 'root'
})
export class ValidationManager implements ValidationManagerInterface {
protected validators: OverridableMap<ValidatorInterface>;
protected asyncValidators: OverridableMap<AsyncValidatorInterface>;
protected saveValidators: OverridableMap<ValidatorInterface>;
protected asyncSaveValidators: OverridableMap<AsyncValidatorInterface>;
protected filterValidators: OverridableMap<ValidatorInterface>;
constructor(
protected requiredValidator: RequiredValidator,
@ -48,57 +55,66 @@ export class ValidationManager implements ValidationManagerInterface {
protected phoneValidator: PhoneValidator,
) {
this.validators = new OverridableMap<ValidatorInterface>();
this.asyncValidators = new OverridableMap<AsyncValidatorInterface>();
this.saveValidators = new OverridableMap<ValidatorInterface>();
this.asyncSaveValidators = new OverridableMap<AsyncValidatorInterface>();
this.filterValidators = new OverridableMap<ValidatorInterface>();
this.validators.addEntry('default', 'required', requiredValidator);
this.validators.addEntry('default', 'range', rangeValidator);
this.validators.addEntry('default', 'currency', currencyValidator);
this.validators.addEntry('default', 'date', dateValidator);
this.validators.addEntry('default', 'datetime', datetimeValidator);
this.validators.addEntry('default', 'email', emailValidator);
this.validators.addEntry('default', 'float', floatValidator);
this.validators.addEntry('default', 'int', intValidator);
this.validators.addEntry('default', 'phone', phoneValidator);
this.saveValidators.addEntry('default', 'required', requiredValidator);
this.saveValidators.addEntry('default', 'range', rangeValidator);
this.saveValidators.addEntry('default', 'currency', currencyValidator);
this.saveValidators.addEntry('default', 'date', dateValidator);
this.saveValidators.addEntry('default', 'datetime', datetimeValidator);
this.saveValidators.addEntry('default', 'email', emailValidator);
this.saveValidators.addEntry('default', 'float', floatValidator);
this.saveValidators.addEntry('default', 'int', intValidator);
this.saveValidators.addEntry('default', 'phone', phoneValidator);
this.filterValidators.addEntry('default', 'date', dateValidator);
this.filterValidators.addEntry('default', 'datetime', datetimeValidator);
this.filterValidators.addEntry('default', 'float', floatValidator);
this.filterValidators.addEntry('default', 'currency', currencyValidator);
this.filterValidators.addEntry('default', 'int', intValidator);
this.filterValidators.addEntry('default', 'phone', phoneValidator);
}
public registerValidator(module: string, key: string, validator: ValidatorInterface): void {
this.validators.addEntry(module, key, validator);
public registerSaveValidator(module: string, key: string, validator: ValidatorInterface): void {
this.filterValidators.addEntry(module, key, validator);
}
public excludeValidator(module: string, key: string): void {
this.validators.excludeEntry(module, key);
public registerFilterValidator(module: string, key: string, validator: ValidatorInterface): void {
this.saveValidators.addEntry(module, key, validator);
}
public registerAsyncValidator(module: string, key: string, validator: AsyncValidatorInterface): void {
this.asyncValidators.addEntry(module, key, validator);
public excludeSaveValidator(module: string, key: string): void {
this.saveValidators.excludeEntry(module, key);
}
public excludeAsyncValidator(module: string, key: string): void {
this.validators.excludeEntry(module, key);
public excludeFilterValidator(module: string, key: string): void {
this.filterValidators.excludeEntry(module, key);
}
public getValidations(module: string, viewField: ViewFieldDefinition, record: Record): ValidatorFn[] {
let validations = [];
const entries = this.validators.getGroupEntries(module);
Object.keys(entries).forEach(validatorKey => {
const validator = entries[validatorKey];
if (validator.applies(record, viewField)) {
validations = validations.concat(validator.getValidator(viewField, record));
}
});
return validations;
public registerAsyncSaveValidator(module: string, key: string, validator: AsyncValidatorInterface): void {
this.asyncSaveValidators.addEntry(module, key, validator);
}
public getAsyncValidations(module: string, viewField: ViewFieldDefinition, record: Record): AsyncValidatorFn[] {
public excludeAsyncSaveValidator(module: string, key: string): void {
this.saveValidators.excludeEntry(module, key);
}
public getSaveValidations(module: string, viewField: ViewFieldDefinition, record: Record): ValidatorFn[] {
const entries = this.saveValidators.getGroupEntries(module);
return this.filterValidations(entries, record, viewField);
}
public getFilterValidations(module: string, viewField: ViewFieldDefinition, record: Record): ValidatorFn[] {
const entries = this.filterValidators.getGroupEntries(module);
return this.filterValidations(entries, record, viewField);
}
public getAsyncSaveValidations(module: string, viewField: ViewFieldDefinition, record: Record): AsyncValidatorFn[] {
const validations = [];
const entries = this.asyncValidators.getGroupEntries(module);
const entries = this.asyncSaveValidators.getGroupEntries(module);
Object.keys(entries).forEach(validatorKey => {
@ -111,4 +127,19 @@ export class ValidationManager implements ValidationManagerInterface {
return validations;
}
protected filterValidations(entries: MapEntry<ValidatorInterface>, record: Record, viewField: ViewFieldDefinition): ValidatorFn[] {
let validations = [];
Object.keys(entries).forEach(validatorKey => {
const validator = entries[validatorKey];
if (validator.applies(record, viewField)) {
validations = validations.concat(validator.getValidator(viewField, record));
}
});
return validations;
}
}