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
timer: 5
},
metadata: {
// NOTE: allow to set icons used to represent metadata belonging to a relation group
icons: [
icons: {
metadata: [
/**
* NOTE: example of configuration
* {
* // NOTE: metadata name
* name: 'dc.author',
* config: {
* // NOTE: used when metadata value has an authority
* 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: fontawesome (v4.x) icon classes and bootstrap utility classes can be used
* style: 'fa-user'
* }
*/
{
// NOTE: metadata name
name: 'dc.author',
// NOTE: fontawesome (v4.x) icon classes and bootstrap utility classes can be used
style: 'fa-user'
},
// default configuration
{
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

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}"
(click)="chipsSelected($event, i);">
<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"
[ngbTooltip]="tipContent"
triggers="manual"
@@ -28,6 +29,8 @@
class="fa {{icon.style}}"
[class.mr-1]="!l"
[class.mr-2]="l"
dsAuthorityConfidenceState
[authorityValue]="c.item[icon.metadata] || c.item"
aria-hidden="true"
(dragstart)="tooltip.close();"
(mouseover)="showTooltip(t, i, icon.metadata)"

View File

@@ -19,6 +19,7 @@ export class ChipsComponent implements OnChanges {
@Input() chips: Chips;
@Input() wrapperClass: string;
@Input() editable = true;
@Input() showIcons = false;
@Output() selected: 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 {
metadata: string;
hasAuthority: boolean;
style: string;
tooltip?: any;
}
@@ -11,7 +10,7 @@ export interface ChipsItemIcon {
export class ChipsItem {
public id: string;
public display: string;
public item: any;
private _item: any;
public editMode?: boolean;
public icons?: ChipsItemIcon[];
@@ -25,7 +24,7 @@ export class ChipsItem {
editMode?: boolean) {
this.id = uniqueId();
this.item = item;
this._item = item;
this.fieldToDisplay = fieldToDisplay;
this.objToDisplay = objToDisplay;
this.setDisplayText();
@@ -33,6 +32,21 @@ export class ChipsItem {
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 {
return isNotEmpty(this.icons);
}
@@ -46,7 +60,7 @@ export class ChipsItem {
}
updateItem(item: any): void {
this.item = item;
this._item = item;
this.setDisplayText();
}
@@ -55,10 +69,10 @@ export class ChipsItem {
}
private setDisplayText(): void {
let value = this.item;
if (isObject(this.item)) {
let value = this._item;
if (isObject(this._item)) {
// 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) {
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 { hasValue, isNotEmpty } from '../../empty.util';
import { PLACEHOLDER_PARENT_METADATA } from '../../form/builder/ds-dynamic-form-ui/models/dynamic-group/dynamic-group.model';
export interface IconsConfig {
withAuthority?: {
style: string;
};
withoutAuthority?: {
style: string;
};
}
export interface MetadataIconsConfig {
name: string;
config: IconsConfig;
}
import { MetadataIconConfig } from '../../../../config/submission-config.interface';
export class Chips {
chipsItems: BehaviorSubject<ChipsItem[]>;
displayField: string;
displayObj: string;
iconsConfig: MetadataIconsConfig[];
iconsConfig: MetadataIconConfig[];
private _items: ChipsItem[];
constructor(items: any[] = [],
displayField: string = 'display',
displayObj?: string,
iconsConfig?: MetadataIconsConfig[]) {
iconsConfig?: MetadataIconConfig[]) {
this.displayField = displayField;
this.displayObj = displayObj;
@@ -115,8 +101,8 @@ export class Chips {
private getChipsIcons(item) {
const icons = [];
const defaultConfigIndex: number = findIndex(this.iconsConfig, {name: 'default'});
const defaultConfig: IconsConfig = (defaultConfigIndex !== -1) ? this.iconsConfig[defaultConfigIndex].config : undefined;
let config: IconsConfig;
const defaultConfig: MetadataIconConfig = (defaultConfigIndex !== -1) ? this.iconsConfig[defaultConfigIndex] : undefined;
let config: MetadataIconConfig;
let configIndex: number;
let value: any;
@@ -126,26 +112,31 @@ export class Chips {
value = item[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)) {
let icon: ChipsItemIcon;
const hasAuthority: boolean = !!(isObject(value) && ((value.hasOwnProperty('authority') && value.authority) || (value.hasOwnProperty('id') && value.id)));
// Set icons
if ((this.displayObj && this.displayObj === metadata && hasAuthority)
// Set icon
icon = {
metadata,
style: config.style
};
icons.push(icon);
/* if ((this.displayObj && this.displayObj === metadata && hasAuthority)
|| (this.displayObj && this.displayObj !== metadata)) {
icon = {
metadata,
hasAuthority: hasAuthority,
style: (hasAuthority) ? config.withAuthority.style : config.withoutAuthority.style
style: config.style
};
}
if (icon) {
icons.push(icon);
}
}*/
}
});

View File

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

View File

@@ -15,7 +15,7 @@ import { Subscription } from 'rxjs/Subscription';
import { Observable } from 'rxjs/Observable';
import { filter, flatMap, map, mergeMap, scan } from 'rxjs/operators';
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 { FormBuilderService } from '../../../form-builder.service';
@@ -32,6 +32,8 @@ import { hasOnlyEmptyProperties } from '../../../../../object.util';
import { IntegrationSearchOptions } from '../../../../../../core/integration/models/integration-options.model';
import { AuthorityService } from '../../../../../../core/integration/authority.service';
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({
selector: 'ds-dynamic-group',
@@ -219,7 +221,7 @@ export class DsDynamicGroupComponent implements OnDestroy, OnInit {
let returnObj = Object.create({});
returnObj = Object.keys(valueObj).map((fieldName) => {
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 model = this.formBuilderService.findById(fieldId, this.formModel);
const searchOptions: IntegrationSearchOptions = new IntegrationSearchOptions(
@@ -231,7 +233,13 @@ export class DsDynamicGroupComponent implements OnDestroy, OnInit {
1);
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 {
return$ = Observable.of(valueObj[fieldName]);
}
@@ -262,6 +270,7 @@ export class DsDynamicGroupComponent implements OnDestroy, OnInit {
}, []),
filter((modelValues: any[]) => this.model.value.length === modelValues.length)
).subscribe((modelValue) => {
this.model.valueUpdates.next(modelValue);
this.initChips(modelValue);
}));
}
@@ -272,7 +281,7 @@ export class DsDynamicGroupComponent implements OnDestroy, OnInit {
initChipsValue,
'value',
this.model.mandatoryField,
this.EnvConfig.submission.metadata.icons);
this.EnvConfig.submission.icons.metadata);
this.subs.push(
this.chips.chipsItems
.subscribe((subItems: any[]) => {

View File

@@ -4,7 +4,12 @@
<div class="form-row align-items-center">
<!--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"
[attr.autoComplete]="model.autoComplete"
[class.is-invalid]="showErrorMessages"
@@ -41,19 +46,29 @@
(click)="$event.stopPropagation(); sdRef.close();"
(input)="onInput($event)">
</div>
<div *ngIf="!isInputDisabled()" class="col-auto text-center">
<div class="col-auto text-center">
<button ngbDropdownAnchor
class="btn btn-secondary"
class="btn btn-secondary"
type="button"
[disabled]="model.readOnly || isSearchDisabled()"
[hidden]="isInputDisabled()"
(click)="sdRef.open(); search(); $event.stopPropagation();">{{'form.search' | translate}}
</button>
</div>
<div *ngIf="isInputDisabled()" class="col-auto text-center">
<button class="btn btn-secondary"
type="button"
[disabled]="model.readOnly"
(click)="remove($event)">{{'form.remove' | translate}}
ngbTooltip="{{'form.edit-help' | 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>
</div>
</div>

View File

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

View File

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

View File

@@ -5,9 +5,10 @@
<ds-chips [chips]="chips"
[editable]="false"
[showIcons]="model.hasAuthority"
[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"
type="text"
[class.pl-3]="chips.hasItems()"
@@ -20,7 +21,7 @@
(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"
type="text"
[(ngModel)]="currentValue"
@@ -42,7 +43,8 @@
(selectItem)="onSelectItem($event)"
(keypress)="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>
</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 { 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 { DynamicTagModel } from './dynamic-tag.model';
import { IntegrationSearchOptions } from '../../../../../../core/integration/models/integration-options.model';
import { Chips } from '../../../../../chips/models/chips.model';
import { hasValue, isNotEmpty } from '../../../../../empty.util';
import { isEqual } from 'lodash';
import { GlobalConfig } from '../../../../../../../config/global-config.interface';
import { GLOBAL_CONFIG } from '../../../../../../../config';
@@ -28,6 +28,8 @@ export class DsDynamicTagComponent implements OnInit {
@Output() change: EventEmitter<any> = new EventEmitter<any>();
@Output() focus: EventEmitter<any> = new EventEmitter<any>();
@ViewChild('instance') instance: NgbTypeahead;
chips: Chips;
hasAuthority: boolean;
@@ -82,7 +84,11 @@ export class DsDynamicTagComponent implements OnInit {
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
.subscribe((subItems: any[]) => {
@@ -108,7 +114,7 @@ export class DsDynamicTagComponent implements OnInit {
}
onBlur(event: Event) {
if (isNotEmpty(this.currentValue)) {
if (isNotEmpty(this.currentValue) && !this.instance.isPopupOpen()) {
this.addTagsToChips();
}
this.blur.emit(event);

View File

@@ -22,6 +22,12 @@
<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"
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"
[attr.autoComplete]="model.autoComplete"
[class.is-invalid]="showErrorMessages"

View File

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

View File

@@ -2,6 +2,8 @@ import { ChangeDetectorRef, Component, EventEmitter, Input, OnInit, Output } fro
import { FormGroup } from '@angular/forms';
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 { 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 { isEmpty, isNotEmpty } from '../../../../../empty.util';
import { FormFieldMetadataValueObject } from '../../../models/form-field-metadata-value.model';
import { ConfidenceType } from '../../../../../../core/integration/models/confidence-type';
@Component({
selector: 'ds-dynamic-typeahead',
@@ -28,7 +31,8 @@ export class DsDynamicTypeaheadComponent implements OnInit {
searching = false;
searchOptions: IntegrationSearchOptions;
searchFailed = false;
hideSearchingWhenUnsubscribed = new Observable(() => () => this.changeSearchingStatus(false));
hideSearchingWhenUnsubscribed$ = new Observable(() => () => this.changeSearchingStatus(false));
click$ = new Subject<string>();
currentValue: any;
inputValue: any;
@@ -36,12 +40,13 @@ export class DsDynamicTypeaheadComponent implements OnInit {
return (typeof x === 'object') ? x.display : x
};
search = (text$: Observable<string>) =>
text$
.debounceTime(300)
.distinctUntilChanged()
.do(() => this.changeSearchingStatus(true))
.switchMap((term) => {
search = (text$: Observable<string>) => {
return text$.pipe(
merge(this.click$),
debounceTime(200),
distinctUntilChanged(),
tap(() => this.changeSearchingStatus(true)),
switchMap((term) => {
if (term === '' || term.length < this.model.minChars) {
return Observable.of({list: []});
} else {
@@ -60,10 +65,12 @@ export class DsDynamicTypeaheadComponent implements OnInit {
return Observable.of({list: []});
});
}
})
.map((results) => results.list)
.do(() => this.changeSearchingStatus(false))
.merge(this.hideSearchingWhenUnsubscribed);
}),
map((results) => results.list),
tap(() => this.changeSearchingStatus(false)),
merge(this.hideSearchingWhenUnsubscribed$)
)
};
constructor(private authorityService: AuthorityService, private cdr: ChangeDetectorRef) {
}
@@ -88,8 +95,7 @@ export class DsDynamicTypeaheadComponent implements OnInit {
onInput(event) {
if (!this.model.authorityOptions.closed && isNotEmpty(event.target.value)) {
const valueObj = new FormFieldMetadataValueObject(event.target.value);
this.inputValue = valueObj;
this.inputValue = new FormFieldMetadataValueObject(event.target.value);
this.model.valueUpdates.next(this.inputValue);
}
}
@@ -120,4 +126,10 @@ export class DsDynamicTypeaheadComponent implements OnInit {
this.model.valueUpdates.next(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 {
metadata?: string;
@@ -6,7 +7,7 @@ export class FormFieldMetadataValueObject {
display: string;
language: any;
authority: string;
confidence: number;
confidence: ConfidenceType;
place: number;
closed: boolean;
label: string;
@@ -17,7 +18,7 @@ export class FormFieldMetadataValueObject {
authority: string = null,
display: string = null,
place: number = 0,
confidence: number = -1,
confidence: number = null,
otherInformation: any = null,
metadata: string = null) {
this.value = isNotNull(value) ? ((typeof value === 'string') ? value.trim() : value) : null;
@@ -26,10 +27,12 @@ export class FormFieldMetadataValueObject {
this.display = display || value;
this.confidence = confidence;
if (authority != null) {
this.confidence = 600;
if (authority != null && isEmpty(confidence)) {
this.confidence = ConfidenceType.CF_ACCEPTED;
} else if (isNotEmpty(confidence)) {
this.confidence = confidence;
} else {
this.confidence = ConfidenceType.CF_UNSET;
}
this.place = place;

View File

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

View File

@@ -1,16 +1,28 @@
import { Config } from './config.interface';
import { MetadataIconsConfig } from '../app/shared/chips/models/chips.model';
interface AutosaveConfig extends Config {
metadata: string[],
timer: number
}
interface MetadataConfig extends Config {
icons: MetadataIconsConfig[]
interface IconsConfig extends Config {
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 {
autosave: AutosaveConfig,
metadata: MetadataConfig
icons: IconsConfig
}