Manage different level of confidence in the submission

This commit is contained in:
Giuseppe Digilio
2018-12-06 22:39:18 +01:00
parent fcd997dba3
commit 2d20d524de
20 changed files with 475 additions and 190 deletions

View File

@@ -49,33 +49,60 @@ module.exports = {
// NOTE: every how many minutes submission is saved automatically // NOTE: every how many minutes submission is saved automatically
timer: 5 timer: 5
}, },
metadata: { icons: {
// NOTE: allow to set icons used to represent metadata belonging to a relation group metadata: [
icons: [
/** /**
* NOTE: example of configuration * NOTE: example of configuration
* { * {
* // NOTE: metadata name * // NOTE: metadata name
* name: 'dc.author', * name: 'dc.author',
* config: { * // NOTE: fontawesome (v4.x) icon classes and bootstrap utility classes can be used
* // NOTE: used when metadata value has an authority * style: 'fa-user'
* withAuthority: {
* // NOTE: fontawesome (v4.x) icon classes and bootstrap color utility classes can be used
* style: 'fa-user'
* },
* // NOTE: used when metadata value has not an authority
* withoutAuthority: {
* style: 'fa-user text-muted'
* }
* }
* } * }
*/ */
{
// NOTE: metadata name
name: 'dc.author',
// NOTE: fontawesome (v4.x) icon classes and bootstrap utility classes can be used
style: 'fa-user'
},
// default configuration // default configuration
{ {
name: 'default', name: 'default',
config: {} style: ''
} }
] ],
authority: {
confidence: [
/**
* NOTE: example of configuration
* {
* // NOTE: confidence value
* value: 'dc.author',
* // NOTE: fontawesome (v4.x) icon classes and bootstrap utility classes can be used
* style: 'fa-user'
* }
*/
{
value: 600,
style: 'text-success'
},
{
value: 500,
style: 'text-info'
},
{
value: 400,
style: 'text-warning'
},
// default configuration
{
value: 'default',
style: 'text-muted'
},
]
}
} }
}, },
// Angular Universal settings // Angular Universal settings

View File

@@ -0,0 +1,44 @@
export enum ConfidenceType {
/**
* This authority value has been confirmed as accurate by an
* interactive user or authoritative policy
*/
CF_ACCEPTED = 600,
/**
* Value is singular and valid but has not been seen and accepted
* by a human, so its provenance is uncertain.
*/
CF_UNCERTAIN = 500,
/**
* There are multiple matching authority values of equal validity.
*/
CF_AMBIGUOUS = 400,
/**
* There are no matching answers from the authority.
*/
CF_NOTFOUND = 300,
/**
* The authority encountered an internal failure - this preserves a
* record in the metadata of why there is no value.
*/
CF_FAILED = 200,
/**
* The authority recommends this submission be rejected.
*/
CF_REJECTED = 100,
/**
* No reasonable confidence value is available
*/
CF_NOVALUE = 0,
/**
* Value has not been set (DB default).
*/
CF_UNSET = -1
}

View File

@@ -0,0 +1,95 @@
import {
Directive,
ElementRef, EventEmitter,
HostListener,
Inject,
Input,
OnChanges,
Output,
Renderer2,
SimpleChanges
} from '@angular/core';
import { findIndex } from 'lodash';
import { AuthorityValue } from '../../core/integration/models/authority.value';
import { FormFieldMetadataValueObject } from '../form/builder/models/form-field-metadata-value.model';
import { ConfidenceType } from '../../core/integration/models/confidence-type';
import { isNotEmpty, isNull } from '../empty.util';
import { GLOBAL_CONFIG, GlobalConfig } from '../../../config';
import { ConfidenceIconConfig } from '../../../config/submission-config.interface';
@Directive({
selector: '[dsAuthorityConfidenceState]'
})
export class AuthorityConfidenceStateDirective implements OnChanges {
@Input() authorityValue: AuthorityValue | FormFieldMetadataValueObject | string;
private previousClass: string = null;
private newClass: string;
@Output() whenClickOnConfidenceNotAccepted: EventEmitter<ConfidenceType> = new EventEmitter<ConfidenceType>();
@HostListener('click') onClick() {
if (isNotEmpty(this.authorityValue) && this.getConfidenceByValue(this.authorityValue) !== ConfidenceType.CF_ACCEPTED) {
this.whenClickOnConfidenceNotAccepted.emit(this.getConfidenceByValue(this.authorityValue));
}
}
constructor(
@Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig,
private elem: ElementRef,
private renderer: Renderer2
) {
}
ngOnChanges(changes: SimpleChanges): void {
if (!changes.authorityValue.firstChange) {
this.previousClass = this.getClassByConfidence(this.getConfidenceByValue(changes.authorityValue.previousValue))
}
this.newClass = this.getClassByConfidence(this.getConfidenceByValue(changes.authorityValue.currentValue));
if (isNull(this.previousClass)) {
this.renderer.addClass(this.elem.nativeElement, this.newClass);
} else if (this.previousClass !== this.newClass) {
this.renderer.removeClass(this.elem.nativeElement, this.previousClass);
this.renderer.addClass(this.elem.nativeElement, this.newClass);
}
}
ngAfterViewInit() {
if (isNull(this.previousClass)) {
this.renderer.addClass(this.elem.nativeElement, this.newClass);
} else if (this.previousClass !== this.newClass) {
this.renderer.removeClass(this.elem.nativeElement, this.previousClass);
this.renderer.addClass(this.elem.nativeElement, this.newClass);
}
}
private getConfidenceByValue(value: any): ConfidenceType {
let confidence: ConfidenceType = ConfidenceType.CF_UNSET;
if (isNotEmpty(value) && value instanceof AuthorityValue && value.hasAuthority()) {
confidence = ConfidenceType.CF_ACCEPTED;
}
if (isNotEmpty(value) && value instanceof FormFieldMetadataValueObject) {
confidence = value.confidence;
}
return confidence;
}
private getClassByConfidence(confidence: any): string {
const confidenceIcons: ConfidenceIconConfig[] = this.EnvConfig.submission.icons.authority.confidence;
const confidenceIndex: number = findIndex(confidenceIcons, {value: confidence});
const defaultconfidenceIndex: number = findIndex(confidenceIcons, {value: 'default'});
const defaultClass: string = (defaultconfidenceIndex !== -1) ? confidenceIcons[defaultconfidenceIndex].style : '';
return (confidenceIndex !== -1) ? confidenceIcons[confidenceIndex].style : defaultClass;
}
}

View File

@@ -20,7 +20,8 @@
[ngClass]="{'chip-selected disabled': (editable && c.editMode) || dragged == i}" [ngClass]="{'chip-selected disabled': (editable && c.editMode) || dragged == i}"
(click)="chipsSelected($event, i);"> (click)="chipsSelected($event, i);">
<span> <span>
<ng-container *ngIf="c.hasIcons()"> <i *ngIf="showIcons && !c.isNestedItem()" dsAuthorityConfidenceState [authorityValue]="c.item" class="fa fa-circle-o" aria-hidden="true"></i>
<ng-container *ngIf="showIcons && c.hasIcons()">
<i *ngFor="let icon of c.icons; let l = last" <i *ngFor="let icon of c.icons; let l = last"
[ngbTooltip]="tipContent" [ngbTooltip]="tipContent"
triggers="manual" triggers="manual"
@@ -28,6 +29,8 @@
class="fa {{icon.style}}" class="fa {{icon.style}}"
[class.mr-1]="!l" [class.mr-1]="!l"
[class.mr-2]="l" [class.mr-2]="l"
dsAuthorityConfidenceState
[authorityValue]="c.item[icon.metadata] || c.item"
aria-hidden="true" aria-hidden="true"
(dragstart)="tooltip.close();" (dragstart)="tooltip.close();"
(mouseover)="showTooltip(t, i, icon.metadata)" (mouseover)="showTooltip(t, i, icon.metadata)"

View File

@@ -19,6 +19,7 @@ export class ChipsComponent implements OnChanges {
@Input() chips: Chips; @Input() chips: Chips;
@Input() wrapperClass: string; @Input() wrapperClass: string;
@Input() editable = true; @Input() editable = true;
@Input() showIcons = false;
@Output() selected: EventEmitter<number> = new EventEmitter<number>(); @Output() selected: EventEmitter<number> = new EventEmitter<number>();
@Output() remove: EventEmitter<number> = new EventEmitter<number>(); @Output() remove: EventEmitter<number> = new EventEmitter<number>();

View File

@@ -3,7 +3,6 @@ import { isNotEmpty } from '../../empty.util';
export interface ChipsItemIcon { export interface ChipsItemIcon {
metadata: string; metadata: string;
hasAuthority: boolean;
style: string; style: string;
tooltip?: any; tooltip?: any;
} }
@@ -11,7 +10,7 @@ export interface ChipsItemIcon {
export class ChipsItem { export class ChipsItem {
public id: string; public id: string;
public display: string; public display: string;
public item: any; private _item: any;
public editMode?: boolean; public editMode?: boolean;
public icons?: ChipsItemIcon[]; public icons?: ChipsItemIcon[];
@@ -25,7 +24,7 @@ export class ChipsItem {
editMode?: boolean) { editMode?: boolean) {
this.id = uniqueId(); this.id = uniqueId();
this.item = item; this._item = item;
this.fieldToDisplay = fieldToDisplay; this.fieldToDisplay = fieldToDisplay;
this.objToDisplay = objToDisplay; this.objToDisplay = objToDisplay;
this.setDisplayText(); this.setDisplayText();
@@ -33,6 +32,21 @@ export class ChipsItem {
this.icons = icons || []; this.icons = icons || [];
} }
public set item(item) {
this._item = item;
}
public get item() {
return this._item;
}
isNestedItem(): boolean {
return (isNotEmpty(this.item)
&& isObject(this.item)
&& isNotEmpty(this.objToDisplay)
&& this.item[this.objToDisplay]);
}
hasIcons(): boolean { hasIcons(): boolean {
return isNotEmpty(this.icons); return isNotEmpty(this.icons);
} }
@@ -46,7 +60,7 @@ export class ChipsItem {
} }
updateItem(item: any): void { updateItem(item: any): void {
this.item = item; this._item = item;
this.setDisplayText(); this.setDisplayText();
} }
@@ -55,10 +69,10 @@ export class ChipsItem {
} }
private setDisplayText(): void { private setDisplayText(): void {
let value = this.item; let value = this._item;
if (isObject(this.item)) { if (isObject(this._item)) {
// Check If displayField is in an internal object // Check If displayField is in an internal object
const obj = this.objToDisplay ? this.item[this.objToDisplay] : this.item; const obj = this.objToDisplay ? this._item[this.objToDisplay] : this._item;
if (isObject(obj) && obj) { if (isObject(obj) && obj) {
value = obj[this.fieldToDisplay] || obj.value; value = obj[this.fieldToDisplay] || obj.value;

View File

@@ -3,34 +3,20 @@ import { BehaviorSubject } from 'rxjs/BehaviorSubject';
import { ChipsItem, ChipsItemIcon } from './chips-item.model'; import { ChipsItem, ChipsItemIcon } from './chips-item.model';
import { hasValue, isNotEmpty } from '../../empty.util'; import { hasValue, isNotEmpty } from '../../empty.util';
import { PLACEHOLDER_PARENT_METADATA } from '../../form/builder/ds-dynamic-form-ui/models/dynamic-group/dynamic-group.model'; import { PLACEHOLDER_PARENT_METADATA } from '../../form/builder/ds-dynamic-form-ui/models/dynamic-group/dynamic-group.model';
import { MetadataIconConfig } from '../../../../config/submission-config.interface';
export interface IconsConfig {
withAuthority?: {
style: string;
};
withoutAuthority?: {
style: string;
};
}
export interface MetadataIconsConfig {
name: string;
config: IconsConfig;
}
export class Chips { export class Chips {
chipsItems: BehaviorSubject<ChipsItem[]>; chipsItems: BehaviorSubject<ChipsItem[]>;
displayField: string; displayField: string;
displayObj: string; displayObj: string;
iconsConfig: MetadataIconsConfig[]; iconsConfig: MetadataIconConfig[];
private _items: ChipsItem[]; private _items: ChipsItem[];
constructor(items: any[] = [], constructor(items: any[] = [],
displayField: string = 'display', displayField: string = 'display',
displayObj?: string, displayObj?: string,
iconsConfig?: MetadataIconsConfig[]) { iconsConfig?: MetadataIconConfig[]) {
this.displayField = displayField; this.displayField = displayField;
this.displayObj = displayObj; this.displayObj = displayObj;
@@ -115,8 +101,8 @@ export class Chips {
private getChipsIcons(item) { private getChipsIcons(item) {
const icons = []; const icons = [];
const defaultConfigIndex: number = findIndex(this.iconsConfig, {name: 'default'}); const defaultConfigIndex: number = findIndex(this.iconsConfig, {name: 'default'});
const defaultConfig: IconsConfig = (defaultConfigIndex !== -1) ? this.iconsConfig[defaultConfigIndex].config : undefined; const defaultConfig: MetadataIconConfig = (defaultConfigIndex !== -1) ? this.iconsConfig[defaultConfigIndex] : undefined;
let config: IconsConfig; let config: MetadataIconConfig;
let configIndex: number; let configIndex: number;
let value: any; let value: any;
@@ -126,26 +112,31 @@ export class Chips {
value = item[metadata]; value = item[metadata];
configIndex = findIndex(this.iconsConfig, {name: metadata}); configIndex = findIndex(this.iconsConfig, {name: metadata});
config = (configIndex !== -1) ? this.iconsConfig[configIndex].config : defaultConfig; config = (configIndex !== -1) ? this.iconsConfig[configIndex] : defaultConfig;
if (hasValue(value) && isNotEmpty(config) && !this.hasPlaceholder(value)) { if (hasValue(value) && isNotEmpty(config) && !this.hasPlaceholder(value)) {
let icon: ChipsItemIcon; let icon: ChipsItemIcon;
const hasAuthority: boolean = !!(isObject(value) && ((value.hasOwnProperty('authority') && value.authority) || (value.hasOwnProperty('id') && value.id)));
// Set icons // Set icon
if ((this.displayObj && this.displayObj === metadata && hasAuthority) icon = {
metadata,
style: config.style
};
icons.push(icon);
/* if ((this.displayObj && this.displayObj === metadata && hasAuthority)
|| (this.displayObj && this.displayObj !== metadata)) { || (this.displayObj && this.displayObj !== metadata)) {
icon = { icon = {
metadata, metadata,
hasAuthority: hasAuthority, hasAuthority: hasAuthority,
style: (hasAuthority) ? config.withAuthority.style : config.withoutAuthority.style style: config.style
}; };
} }
if (icon) { if (icon) {
icons.push(icon); icons.push(icon);
} }*/
} }
}); });

View File

@@ -68,6 +68,7 @@
*ngIf="chips && chips.hasItems()" *ngIf="chips && chips.hasItems()"
[chips]="chips" [chips]="chips"
[editable]="true" [editable]="true"
[showIcons]="true"
(selected)="onChipSelected($event)"></ds-chips> (selected)="onChipSelected($event)"></ds-chips>
</div> </div>
</div> </div>

View File

@@ -15,7 +15,7 @@ import { Subscription } from 'rxjs/Subscription';
import { Observable } from 'rxjs/Observable'; import { Observable } from 'rxjs/Observable';
import { filter, flatMap, map, mergeMap, scan } from 'rxjs/operators'; import { filter, flatMap, map, mergeMap, scan } from 'rxjs/operators';
import { DynamicFormControlModel, DynamicFormGroupModel, DynamicInputModel } from '@ng-dynamic-forms/core'; import { DynamicFormControlModel, DynamicFormGroupModel, DynamicInputModel } from '@ng-dynamic-forms/core';
import { isEqual } from 'lodash'; import { isEqual, isObject } from 'lodash';
import { DynamicGroupModel, PLACEHOLDER_PARENT_METADATA } from './dynamic-group.model'; import { DynamicGroupModel, PLACEHOLDER_PARENT_METADATA } from './dynamic-group.model';
import { FormBuilderService } from '../../../form-builder.service'; import { FormBuilderService } from '../../../form-builder.service';
@@ -32,6 +32,8 @@ import { hasOnlyEmptyProperties } from '../../../../../object.util';
import { IntegrationSearchOptions } from '../../../../../../core/integration/models/integration-options.model'; import { IntegrationSearchOptions } from '../../../../../../core/integration/models/integration-options.model';
import { AuthorityService } from '../../../../../../core/integration/authority.service'; import { AuthorityService } from '../../../../../../core/integration/authority.service';
import { IntegrationData } from '../../../../../../core/integration/integration-data'; import { IntegrationData } from '../../../../../../core/integration/integration-data';
import { FormFieldMetadataValueObject } from '../../../models/form-field-metadata-value.model';
import { AuthorityValue } from '../../../../../../core/integration/models/authority.value';
@Component({ @Component({
selector: 'ds-dynamic-group', selector: 'ds-dynamic-group',
@@ -219,7 +221,7 @@ export class DsDynamicGroupComponent implements OnDestroy, OnInit {
let returnObj = Object.create({}); let returnObj = Object.create({});
returnObj = Object.keys(valueObj).map((fieldName) => { returnObj = Object.keys(valueObj).map((fieldName) => {
let return$: Observable<any>; let return$: Observable<any>;
if (valueObj[fieldName].hasAuthority() && isNotEmpty(valueObj[fieldName].authority)) { if (isObject(valueObj[fieldName]) && valueObj[fieldName].hasAuthority() && isNotEmpty(valueObj[fieldName].authority)) {
const fieldId = fieldName.replace(/\./g, '_'); const fieldId = fieldName.replace(/\./g, '_');
const model = this.formBuilderService.findById(fieldId, this.formModel); const model = this.formBuilderService.findById(fieldId, this.formModel);
const searchOptions: IntegrationSearchOptions = new IntegrationSearchOptions( const searchOptions: IntegrationSearchOptions = new IntegrationSearchOptions(
@@ -231,7 +233,13 @@ export class DsDynamicGroupComponent implements OnDestroy, OnInit {
1); 1);
return$ = this.authorityService.getEntryByValue(searchOptions) return$ = this.authorityService.getEntryByValue(searchOptions)
.map((result: IntegrationData) => result.payload[0]); .map((result: IntegrationData) => Object.assign(
new FormFieldMetadataValueObject(),
valueObj[fieldName],
{
otherInformation: (result.payload[0] as AuthorityValue).otherInformation
})
);
} else { } else {
return$ = Observable.of(valueObj[fieldName]); return$ = Observable.of(valueObj[fieldName]);
} }
@@ -262,6 +270,7 @@ export class DsDynamicGroupComponent implements OnDestroy, OnInit {
}, []), }, []),
filter((modelValues: any[]) => this.model.value.length === modelValues.length) filter((modelValues: any[]) => this.model.value.length === modelValues.length)
).subscribe((modelValue) => { ).subscribe((modelValue) => {
this.model.valueUpdates.next(modelValue);
this.initChips(modelValue); this.initChips(modelValue);
})); }));
} }
@@ -272,7 +281,7 @@ export class DsDynamicGroupComponent implements OnDestroy, OnInit {
initChipsValue, initChipsValue,
'value', 'value',
this.model.mandatoryField, this.model.mandatoryField,
this.EnvConfig.submission.metadata.icons); this.EnvConfig.submission.icons.metadata);
this.subs.push( this.subs.push(
this.chips.chipsItems this.chips.chipsItems
.subscribe((subItems: any[]) => { .subscribe((subItems: any[]) => {

View File

@@ -4,7 +4,12 @@
<div class="form-row align-items-center"> <div class="form-row align-items-center">
<!--Simple lookup, first field --> <!--Simple lookup, first field -->
<div class="col"> <div class="col right-addon">
<i dsAuthorityConfidenceState
class="fa fa-circle-o fa-2x fa-fw position-absolute mt-1 p-0"
aria-hidden="true"
[authorityValue]="model.value"
(whenClickOnConfidenceNotAccepted)="whenClickOnConfidenceNotAccepted(sdRef, $event)"></i>
<input class="form-control" <input class="form-control"
[attr.autoComplete]="model.autoComplete" [attr.autoComplete]="model.autoComplete"
[class.is-invalid]="showErrorMessages" [class.is-invalid]="showErrorMessages"
@@ -41,19 +46,29 @@
(click)="$event.stopPropagation(); sdRef.close();" (click)="$event.stopPropagation(); sdRef.close();"
(input)="onInput($event)"> (input)="onInput($event)">
</div> </div>
<div *ngIf="!isInputDisabled()" class="col-auto text-center"> <div class="col-auto text-center">
<button ngbDropdownAnchor <button ngbDropdownAnchor
class="btn btn-secondary" class="btn btn-secondary"
type="button" type="button"
[disabled]="model.readOnly || isSearchDisabled()" [disabled]="model.readOnly || isSearchDisabled()"
[hidden]="isInputDisabled()"
(click)="sdRef.open(); search(); $event.stopPropagation();">{{'form.search' | translate}} (click)="sdRef.open(); search(); $event.stopPropagation();">{{'form.search' | translate}}
</button> </button>
</div> </div>
<div *ngIf="isInputDisabled()" class="col-auto text-center"> <div *ngIf="isInputDisabled()" class="col-auto text-center">
<button class="btn btn-secondary" <button class="btn btn-secondary"
type="button" type="button"
[disabled]="model.readOnly" ngbTooltip="{{'form.edit-help' | translate}}"
(click)="remove($event)">{{'form.remove' | translate}} placement="top"
[disabled]="isEditDisabled()"
(click)="switchEditMode()">{{'form.edit' | translate}}
</button>
<button *ngIf="editMode" class="btn btn-secondary"
type="button"
ngbTooltip="{{'form.save-help' | translate}}"
placement="top"
[disabled]="!hasEmptyValue()"
(click)="saveChanges()">{{'form.save' | translate}}
</button> </button>
</div> </div>
</div> </div>

View File

@@ -19,11 +19,11 @@
} }
/* align fa-spin */ /* align fa-spin */
.left-addon .fa-spin { .left-addon .fa {
left: 0; left: 0;
} }
.right-addon .fa-spin { .right-addon .fa {
right: 0; right: 0;
} }

View File

@@ -11,6 +11,8 @@ import { Subscription } from 'rxjs/Subscription';
import { FormFieldMetadataValueObject } from '../../../models/form-field-metadata-value.model'; import { FormFieldMetadataValueObject } from '../../../models/form-field-metadata-value.model';
import { AuthorityValue } from '../../../../../../core/integration/models/authority.value'; import { AuthorityValue } from '../../../../../../core/integration/models/authority.value';
import { DynamicLookupNameModel } from './dynamic-lookup-name.model'; import { DynamicLookupNameModel } from './dynamic-lookup-name.model';
import { ConfidenceType } from '../../../../../../core/integration/models/confidence-type';
import { NgbDropdown } from '@ng-bootstrap/ng-bootstrap';
@Component({ @Component({
selector: 'ds-dynamic-lookup', selector: 'ds-dynamic-lookup',
@@ -27,6 +29,7 @@ export class DsDynamicLookupComponent implements OnDestroy, OnInit {
@Output() change: EventEmitter<any> = new EventEmitter<any>(); @Output() change: EventEmitter<any> = new EventEmitter<any>();
@Output() focus: EventEmitter<any> = new EventEmitter<any>(); @Output() focus: EventEmitter<any> = new EventEmitter<any>();
public editMode = false;
public firstInputValue = ''; public firstInputValue = '';
public secondInputValue = ''; public secondInputValue = '';
public loading = false; public loading = false;
@@ -40,6 +43,10 @@ export class DsDynamicLookupComponent implements OnDestroy, OnInit {
private cdr: ChangeDetectorRef) { private cdr: ChangeDetectorRef) {
} }
inputFormatter = (x: { display: string }, y: number) => {
return y === 1 ? this.firstInputValue : this.secondInputValue;
};
ngOnInit() { ngOnInit() {
this.searchOptions = new IntegrationSearchOptions( this.searchOptions = new IntegrationSearchOptions(
this.model.authorityOptions.scope, this.model.authorityOptions.scope,
@@ -55,38 +62,33 @@ export class DsDynamicLookupComponent implements OnDestroy, OnInit {
.subscribe((value) => { .subscribe((value) => {
if (isEmpty(value)) { if (isEmpty(value)) {
this.resetFields(); this.resetFields();
} else { } else if (!this.editMode) {
this.setInputsValue(this.model.value); this.setInputsValue(this.model.value);
} }
}); });
} }
public formatItemForInput(item: any, field: number): string { protected getCurrentValue(): string {
if (isUndefined(item) || isNull(item)) { let result = '';
return ''; if (!this.isLookupName()) {
} result = this.firstInputValue;
return (typeof item === 'string') ? item : this.inputFormatter(item, field); } else {
} if (isNotEmpty(this.firstInputValue)) {
result = this.firstInputValue;
inputFormatter = (x: { display: string }, y: number) => { }
return y === 1 ? this.firstInputValue : this.secondInputValue; if (isNotEmpty(this.secondInputValue)) {
}; result = isEmpty(result)
? this.secondInputValue
onInput(event) { : this.firstInputValue + (this.model as DynamicLookupNameModel).separator + ' ' + this.secondInputValue;
if (!this.model.authorityOptions.closed) {
if (isNotEmpty(this.getCurrentValue())) {
const currentValue = new FormFieldMetadataValueObject(this.getCurrentValue());
this.onSelect(currentValue);
} else {
this.remove();
} }
} }
return result;
} }
onScroll() { protected resetFields() {
if (!this.loading && this.pageInfo.currentPage <= this.pageInfo.totalPages) { this.firstInputValue = '';
this.searchOptions.currentPage++; if (this.isLookupName()) {
this.search(); this.secondInputValue = '';
} }
} }
@@ -110,24 +112,110 @@ export class DsDynamicLookupComponent implements OnDestroy, OnInit {
} }
} }
protected getCurrentValue(): string { public formatItemForInput(item: any, field: number): string {
let result = ''; if (isUndefined(item) || isNull(item)) {
if (!this.isLookupName()) { return '';
result = this.firstInputValue;
} else {
if (isNotEmpty(this.firstInputValue)) {
result = this.firstInputValue;
}
if (isNotEmpty(this.secondInputValue)) {
result = isEmpty(result)
? this.secondInputValue
: this.firstInputValue + (this.model as DynamicLookupNameModel).separator + ' ' + this.secondInputValue;
}
} }
return result; return (typeof item === 'string') ? item : this.inputFormatter(item, field);
} }
search() { public hasAuthorityValue() {
return hasValue(this.model.value)
&& this.model.value.hasAuthority();
}
public hasEmptyValue() {
return isNotEmpty(this.getCurrentValue());
}
public clearFields() {
// Clear inputs whether there is no results and authority is closed
if (this.model.authorityOptions.closed) {
this.resetFields();
}
}
public isEditDisabled() {
return !this.hasAuthorityValue();
}
public isInputDisabled() {
return (this.model.authorityOptions.closed && this.hasAuthorityValue() && !this.editMode);
}
public isLookupName() {
return (this.model instanceof DynamicLookupNameModel);
}
public isSearchDisabled() {
return isEmpty(this.firstInputValue);
}
public onBlurEvent(event: Event) {
this.blur.emit(event);
}
public onFocusEvent(event) {
this.focus.emit(event);
}
public onInput(event) {
if (!this.model.authorityOptions.closed) {
if (isNotEmpty(this.getCurrentValue())) {
const currentValue = new FormFieldMetadataValueObject(this.getCurrentValue());
if (!this.editMode) {
this.onSelect(currentValue);
}
} else {
this.remove();
}
}
}
public onScroll() {
if (!this.loading && this.pageInfo.currentPage <= this.pageInfo.totalPages) {
this.searchOptions.currentPage++;
this.search();
}
}
public onSelect(event) {
this.group.markAsDirty();
this.model.valueUpdates.next(event);
this.setInputsValue(event);
this.change.emit(event);
this.optionsList = null;
this.pageInfo = null;
}
public openChange(isOpened: boolean) {
if (!isOpened) {
if (this.model.authorityOptions.closed && !this.hasAuthorityValue()) {
this.setInputsValue('');
}
}
}
public remove() {
this.group.markAsPristine();
this.model.valueUpdates.next(null);
this.change.emit(null);
}
public saveChanges() {
if (isNotEmpty(this.getCurrentValue())) {
const newValue = Object.assign(new AuthorityValue(), this.model.value, {
display: this.getCurrentValue(),
value: this.getCurrentValue()
});
this.onSelect(newValue);
} else {
this.remove();
}
this.switchEditMode();
}
public search() {
this.optionsList = null; this.optionsList = null;
this.pageInfo = null; this.pageInfo = null;
@@ -145,68 +233,17 @@ export class DsDynamicLookupComponent implements OnDestroy, OnInit {
}); });
} }
clearFields() { public switchEditMode() {
// Clear inputs whether there is no results and authority is closed this.editMode = !this.editMode;
if (this.model.authorityOptions.closed) { }
this.resetFields();
public whenClickOnConfidenceNotAccepted(sdRef: NgbDropdown, confidence: ConfidenceType) {
if (!this.model.readOnly) {
sdRef.open();
this.search();
} }
} }
protected resetFields() {
this.firstInputValue = '';
if (this.isLookupName()) {
this.secondInputValue = '';
}
}
onSelect(event) {
this.group.markAsDirty();
this.model.valueUpdates.next(event);
this.setInputsValue(event);
this.change.emit(event);
this.optionsList = null;
this.pageInfo = null;
}
isInputDisabled() {
return this.model.authorityOptions.closed && hasValue(this.model.value);
}
isLookupName() {
return (this.model instanceof DynamicLookupNameModel);
}
isSearchDisabled() {
// if (this.firstInputValue === ''
// && (this.isLookupName ? this.secondInputValue === '' : true)) {
// return true;
// }
// return false;
return isEmpty(this.firstInputValue);
}
remove() {
this.group.markAsPristine();
this.model.valueUpdates.next(null);
this.change.emit(null);
}
openChange(isOpened: boolean) {
if (!isOpened) {
if (this.model.authorityOptions.closed) {
this.setInputsValue('');
}
}
}
onBlurEvent(event: Event) {
this.blur.emit(event);
}
onFocusEvent(event) {
this.focus.emit(event);
}
ngOnDestroy() { ngOnDestroy() {
if (hasValue(this.sub)) { if (hasValue(this.sub)) {
this.sub.unsubscribe(); this.sub.unsubscribe();

View File

@@ -5,9 +5,10 @@
<ds-chips [chips]="chips" <ds-chips [chips]="chips"
[editable]="false" [editable]="false"
[showIcons]="model.hasAuthority"
[wrapperClass]="'border-bottom border-light'"> [wrapperClass]="'border-bottom border-light'">
<input *ngIf="!searchOptions" <input *ngIf="!model.hasAuthority"
class="border-0 form-control-plaintext tag-input flex-grow-1 mt-1 mb-1 chips-sort-ignore" class="border-0 form-control-plaintext tag-input flex-grow-1 mt-1 mb-1 chips-sort-ignore"
type="text" type="text"
[class.pl-3]="chips.hasItems()" [class.pl-3]="chips.hasItems()"
@@ -20,7 +21,7 @@
(keyup)="onKeyUp($event)" /> (keyup)="onKeyUp($event)" />
<input *ngIf="searchOptions" <input *ngIf="model.hasAuthority"
class="border-0 form-control-plaintext tag-input flex-grow-1 mt-1 mb-1 chips-sort-ignore" class="border-0 form-control-plaintext tag-input flex-grow-1 mt-1 mb-1 chips-sort-ignore"
type="text" type="text"
[(ngModel)]="currentValue" [(ngModel)]="currentValue"
@@ -42,7 +43,8 @@
(selectItem)="onSelectItem($event)" (selectItem)="onSelectItem($event)"
(keypress)="preventEventsPropagation($event)" (keypress)="preventEventsPropagation($event)"
(keydown)="preventEventsPropagation($event)" (keydown)="preventEventsPropagation($event)"
(keyup)="onKeyUp($event)"/> (keyup)="onKeyUp($event)"
#instance="ngbTypeahead"/>
<i *ngIf="searching" class="fa fa-circle-o-notch fa-spin fa-2x fa-fw text-primary position-absolute mt-1 p-0" aria-hidden="true"></i> <i *ngIf="searching" class="fa fa-circle-o-notch fa-spin fa-2x fa-fw text-primary position-absolute mt-1 p-0" aria-hidden="true"></i>
</ds-chips> </ds-chips>

View File

@@ -1,15 +1,15 @@
import { ChangeDetectorRef, Component, EventEmitter, Inject, Input, OnInit, Output } from '@angular/core'; import { ChangeDetectorRef, Component, EventEmitter, Inject, Input, OnInit, Output, ViewChild } from '@angular/core';
import { FormGroup } from '@angular/forms'; import { FormGroup } from '@angular/forms';
import { Observable } from 'rxjs/Observable'; import { Observable } from 'rxjs/Observable';
import { NgbTypeaheadSelectItemEvent } from '@ng-bootstrap/ng-bootstrap'; import { NgbTypeahead, NgbTypeaheadSelectItemEvent } from '@ng-bootstrap/ng-bootstrap';
import { isEqual } from 'lodash';
import { AuthorityService } from '../../../../../../core/integration/authority.service'; import { AuthorityService } from '../../../../../../core/integration/authority.service';
import { DynamicTagModel } from './dynamic-tag.model'; import { DynamicTagModel } from './dynamic-tag.model';
import { IntegrationSearchOptions } from '../../../../../../core/integration/models/integration-options.model'; import { IntegrationSearchOptions } from '../../../../../../core/integration/models/integration-options.model';
import { Chips } from '../../../../../chips/models/chips.model'; import { Chips } from '../../../../../chips/models/chips.model';
import { hasValue, isNotEmpty } from '../../../../../empty.util'; import { hasValue, isNotEmpty } from '../../../../../empty.util';
import { isEqual } from 'lodash';
import { GlobalConfig } from '../../../../../../../config/global-config.interface'; import { GlobalConfig } from '../../../../../../../config/global-config.interface';
import { GLOBAL_CONFIG } from '../../../../../../../config'; import { GLOBAL_CONFIG } from '../../../../../../../config';
@@ -28,6 +28,8 @@ export class DsDynamicTagComponent implements OnInit {
@Output() change: EventEmitter<any> = new EventEmitter<any>(); @Output() change: EventEmitter<any> = new EventEmitter<any>();
@Output() focus: EventEmitter<any> = new EventEmitter<any>(); @Output() focus: EventEmitter<any> = new EventEmitter<any>();
@ViewChild('instance') instance: NgbTypeahead;
chips: Chips; chips: Chips;
hasAuthority: boolean; hasAuthority: boolean;
@@ -82,7 +84,11 @@ export class DsDynamicTagComponent implements OnInit {
this.model.authorityOptions.metadata); this.model.authorityOptions.metadata);
} }
this.chips = new Chips(this.model.value, 'display'); this.chips = new Chips(
this.model.value,
'display',
null,
this.EnvConfig.submission.icons.metadata);
this.chips.chipsItems this.chips.chipsItems
.subscribe((subItems: any[]) => { .subscribe((subItems: any[]) => {
@@ -108,7 +114,7 @@ export class DsDynamicTagComponent implements OnInit {
} }
onBlur(event: Event) { onBlur(event: Event) {
if (isNotEmpty(this.currentValue)) { if (isNotEmpty(this.currentValue) && !this.instance.isPopupOpen()) {
this.addTagsToChips(); this.addTagsToChips();
} }
this.blur.emit(event); this.blur.emit(event);

View File

@@ -22,6 +22,12 @@
<div class="position-relative right-addon"> <div class="position-relative right-addon">
<i *ngIf="searching" class="fa fa-circle-o-notch fa-spin fa-2x fa-fw text-primary position-absolute mt-1 p-0" aria-hidden="true"></i> <i *ngIf="searching" class="fa fa-circle-o-notch fa-spin fa-2x fa-fw text-primary position-absolute mt-1 p-0" aria-hidden="true"></i>
<i *ngIf="!searching"
dsAuthorityConfidenceState
class="fa fa-circle-o fa-2x fa-fw position-absolute mt-1 p-0"
aria-hidden="true"
[authorityValue]="currentValue"
(whenClickOnConfidenceNotAccepted)="whenClickOnConfidenceNotAccepted($event)"></i>
<input class="form-control" <input class="form-control"
[attr.autoComplete]="model.autoComplete" [attr.autoComplete]="model.autoComplete"
[class.is-invalid]="showErrorMessages" [class.is-invalid]="showErrorMessages"

View File

@@ -6,8 +6,13 @@
} }
/* align fa-spin */ /* align fa-spin */
.left-addon .fa-spin { left: 0;} .left-addon .fa {
.right-addon .fa-spin { right: 0;} left: 0;
}
.right-addon .fa {
right: 0;
}
:host /deep/ .dropdown-menu { :host /deep/ .dropdown-menu {
width: 100% !important; width: 100% !important;

View File

@@ -2,6 +2,8 @@ import { ChangeDetectorRef, Component, EventEmitter, Input, OnInit, Output } fro
import { FormGroup } from '@angular/forms'; import { FormGroup } from '@angular/forms';
import { Observable } from 'rxjs/Observable'; import { Observable } from 'rxjs/Observable';
import { debounceTime, distinctUntilChanged, map, merge, switchMap, tap } from 'rxjs/operators';
import { Subject } from 'rxjs/Subject';
import { NgbTypeaheadSelectItemEvent } from '@ng-bootstrap/ng-bootstrap'; import { NgbTypeaheadSelectItemEvent } from '@ng-bootstrap/ng-bootstrap';
import { AuthorityService } from '../../../../../../core/integration/authority.service'; import { AuthorityService } from '../../../../../../core/integration/authority.service';
@@ -9,6 +11,7 @@ import { DynamicTypeaheadModel } from './dynamic-typeahead.model';
import { IntegrationSearchOptions } from '../../../../../../core/integration/models/integration-options.model'; import { IntegrationSearchOptions } from '../../../../../../core/integration/models/integration-options.model';
import { isEmpty, isNotEmpty } from '../../../../../empty.util'; import { isEmpty, isNotEmpty } from '../../../../../empty.util';
import { FormFieldMetadataValueObject } from '../../../models/form-field-metadata-value.model'; import { FormFieldMetadataValueObject } from '../../../models/form-field-metadata-value.model';
import { ConfidenceType } from '../../../../../../core/integration/models/confidence-type';
@Component({ @Component({
selector: 'ds-dynamic-typeahead', selector: 'ds-dynamic-typeahead',
@@ -28,7 +31,8 @@ export class DsDynamicTypeaheadComponent implements OnInit {
searching = false; searching = false;
searchOptions: IntegrationSearchOptions; searchOptions: IntegrationSearchOptions;
searchFailed = false; searchFailed = false;
hideSearchingWhenUnsubscribed = new Observable(() => () => this.changeSearchingStatus(false)); hideSearchingWhenUnsubscribed$ = new Observable(() => () => this.changeSearchingStatus(false));
click$ = new Subject<string>();
currentValue: any; currentValue: any;
inputValue: any; inputValue: any;
@@ -36,12 +40,13 @@ export class DsDynamicTypeaheadComponent implements OnInit {
return (typeof x === 'object') ? x.display : x return (typeof x === 'object') ? x.display : x
}; };
search = (text$: Observable<string>) => search = (text$: Observable<string>) => {
text$ return text$.pipe(
.debounceTime(300) merge(this.click$),
.distinctUntilChanged() debounceTime(200),
.do(() => this.changeSearchingStatus(true)) distinctUntilChanged(),
.switchMap((term) => { tap(() => this.changeSearchingStatus(true)),
switchMap((term) => {
if (term === '' || term.length < this.model.minChars) { if (term === '' || term.length < this.model.minChars) {
return Observable.of({list: []}); return Observable.of({list: []});
} else { } else {
@@ -60,10 +65,12 @@ export class DsDynamicTypeaheadComponent implements OnInit {
return Observable.of({list: []}); return Observable.of({list: []});
}); });
} }
}) }),
.map((results) => results.list) map((results) => results.list),
.do(() => this.changeSearchingStatus(false)) tap(() => this.changeSearchingStatus(false)),
.merge(this.hideSearchingWhenUnsubscribed); merge(this.hideSearchingWhenUnsubscribed$)
)
};
constructor(private authorityService: AuthorityService, private cdr: ChangeDetectorRef) { constructor(private authorityService: AuthorityService, private cdr: ChangeDetectorRef) {
} }
@@ -88,8 +95,7 @@ export class DsDynamicTypeaheadComponent implements OnInit {
onInput(event) { onInput(event) {
if (!this.model.authorityOptions.closed && isNotEmpty(event.target.value)) { if (!this.model.authorityOptions.closed && isNotEmpty(event.target.value)) {
const valueObj = new FormFieldMetadataValueObject(event.target.value); this.inputValue = new FormFieldMetadataValueObject(event.target.value);
this.inputValue = valueObj;
this.model.valueUpdates.next(this.inputValue); this.model.valueUpdates.next(this.inputValue);
} }
} }
@@ -120,4 +126,10 @@ export class DsDynamicTypeaheadComponent implements OnInit {
this.model.valueUpdates.next(event.item); this.model.valueUpdates.next(event.item);
this.change.emit(event.item); this.change.emit(event.item);
} }
public whenClickOnConfidenceNotAccepted(confidence: ConfidenceType) {
if (!this.model.readOnly) {
this.click$.next(this.formatter(this.currentValue));
}
}
} }

View File

@@ -1,4 +1,5 @@
import { isNotEmpty, isNotNull } from '../../../empty.util'; import { isEmpty, isNotEmpty, isNotNull } from '../../../empty.util';
import { ConfidenceType } from '../../../../core/integration/models/confidence-type';
export class FormFieldMetadataValueObject { export class FormFieldMetadataValueObject {
metadata?: string; metadata?: string;
@@ -6,7 +7,7 @@ export class FormFieldMetadataValueObject {
display: string; display: string;
language: any; language: any;
authority: string; authority: string;
confidence: number; confidence: ConfidenceType;
place: number; place: number;
closed: boolean; closed: boolean;
label: string; label: string;
@@ -17,7 +18,7 @@ export class FormFieldMetadataValueObject {
authority: string = null, authority: string = null,
display: string = null, display: string = null,
place: number = 0, place: number = 0,
confidence: number = -1, confidence: number = null,
otherInformation: any = null, otherInformation: any = null,
metadata: string = null) { metadata: string = null) {
this.value = isNotNull(value) ? ((typeof value === 'string') ? value.trim() : value) : null; this.value = isNotNull(value) ? ((typeof value === 'string') ? value.trim() : value) : null;
@@ -26,10 +27,12 @@ export class FormFieldMetadataValueObject {
this.display = display || value; this.display = display || value;
this.confidence = confidence; this.confidence = confidence;
if (authority != null) { if (authority != null && isEmpty(confidence)) {
this.confidence = 600; this.confidence = ConfidenceType.CF_ACCEPTED;
} else if (isNotEmpty(confidence)) { } else if (isNotEmpty(confidence)) {
this.confidence = confidence; this.confidence = confidence;
} else {
this.confidence = ConfidenceType.CF_UNSET;
} }
this.place = place; this.place = place;

View File

@@ -83,6 +83,7 @@ import { InputSuggestionsComponent } from './input-suggestions/input-suggestions
import { CapitalizePipe } from './utils/capitalize.pipe'; import { CapitalizePipe } from './utils/capitalize.pipe';
import { MomentModule } from 'angular2-moment'; import { MomentModule } from 'angular2-moment';
import { ObjectKeysPipe } from './utils/object-keys-pipe'; import { ObjectKeysPipe } from './utils/object-keys-pipe';
import { AuthorityConfidenceStateDirective } from './authority-confidence/authority-confidence-state.directive';
const MODULES = [ const MODULES = [
// Do NOT include UniversalModule, HttpModule, or JsonpModule here // Do NOT include UniversalModule, HttpModule, or JsonpModule here
@@ -183,7 +184,8 @@ const DIRECTIVES = [
VarDirective, VarDirective,
DragClickDirective, DragClickDirective,
DebounceDirective, DebounceDirective,
ClickOutsideDirective ClickOutsideDirective,
AuthorityConfidenceStateDirective
]; ];
@NgModule({ @NgModule({

View File

@@ -1,16 +1,28 @@
import { Config } from './config.interface'; import { Config } from './config.interface';
import { MetadataIconsConfig } from '../app/shared/chips/models/chips.model';
interface AutosaveConfig extends Config { interface AutosaveConfig extends Config {
metadata: string[], metadata: string[],
timer: number timer: number
} }
interface MetadataConfig extends Config { interface IconsConfig extends Config {
icons: MetadataIconsConfig[] metadata: MetadataIconConfig[],
authority: {
confidence: ConfidenceIconConfig[];
}
}
export interface MetadataIconConfig extends Config {
name: string,
style: string;
}
export interface ConfidenceIconConfig extends Config {
value: any,
style: string;
} }
export interface SubmissionConfig extends Config { export interface SubmissionConfig extends Config {
autosave: AutosaveConfig, autosave: AutosaveConfig,
metadata: MetadataConfig icons: IconsConfig
} }