Added possibility to show additional information to search results returned by form fields which provide search

This commit is contained in:
Giuseppe Digilio
2018-11-28 11:12:31 +01:00
parent 4a353fce91
commit fcd997dba3
12 changed files with 212 additions and 65 deletions

View File

@@ -242,7 +242,14 @@
"submit": "Submit", "submit": "Submit",
"cancel": "Cancel", "cancel": "Cancel",
"search": "Search", "search": "Search",
"search-help": "Click here to looking for an existing correspondence",
"remove": "Remove", "remove": "Remove",
"clear": "Clear",
"clear-help": "Click here to remove the selected value",
"edit": "Edit",
"edit-help": "Click here to edit the selected value",
"save": "Save",
"save-help": "Save changes",
"first-name": "First name", "first-name": "First name",
"last-name": "Last name", "last-name": "Last name",
"loading": "Loading...", "loading": "Loading...",
@@ -251,7 +258,9 @@
"group-collapse": "Collapse", "group-collapse": "Collapse",
"group-expand": "Expand", "group-expand": "Expand",
"group-collapse-help": "Click here to collapse", "group-collapse-help": "Click here to collapse",
"group-expand-help": "Click here to expand and add more elements" "group-expand-help": "Click here to expand and add more element",
"other-information": {
}
}, },
"login": { "login": {
"title": "Login", "title": "Login",

View File

@@ -23,4 +23,8 @@ export class AuthorityValue extends IntegrationModel {
hasValue(): boolean { hasValue(): boolean {
return isNotEmpty(this.value); return isNotEmpty(this.value);
} }
hasOtherInformation(): boolean {
return isNotEmpty(this.otherInformation);
}
} }

View File

@@ -1,15 +1,13 @@
import { WorkspaceitemSectionFormObject } from './workspaceitem-section-form.model'; import { WorkspaceitemSectionFormObject } from './workspaceitem-section-form.model';
import { WorkspaceitemSectionLicenseObject } from './workspaceitem-section-license.model'; import { WorkspaceitemSectionLicenseObject } from './workspaceitem-section-license.model';
import { WorkspaceitemSectionUploadObject } from './workspaceitem-section-upload.model'; import { WorkspaceitemSectionUploadObject } from './workspaceitem-section-upload.model';
import { isNotEmpty, isNotNull } from '../../../shared/empty.util'; import { isNotNull } from '../../../shared/empty.util';
import { FormFieldLanguageValueObject } from '../../../shared/form/builder/models/form-field-language-value.model';
import { WorkspaceitemSectionRecycleObject } from './workspaceitem-section-recycle.model'; import { WorkspaceitemSectionRecycleObject } from './workspaceitem-section-recycle.model';
import { WorkspaceitemSectionDetectDuplicateObject } from './workspaceitem-section-deduplication.model'; import { WorkspaceitemSectionDetectDuplicateObject } from './workspaceitem-section-deduplication.model';
import { FormFieldMetadataValueObject } from '../../../shared/form/builder/models/form-field-metadata-value.model'; import { FormFieldMetadataValueObject } from '../../../shared/form/builder/models/form-field-metadata-value.model';
export class WorkspaceitemSectionsObject { export class WorkspaceitemSectionsObject {
[name: string]: WorkspaceitemSectionDataType; [name: string]: WorkspaceitemSectionDataType;
} }
export function isServerFormValue(obj: any): boolean { export function isServerFormValue(obj: any): boolean {
@@ -28,18 +26,16 @@ export function normalizeSectionData(obj: any) {
if (typeof obj === 'object' && isServerFormValue(obj)) { if (typeof obj === 'object' && isServerFormValue(obj)) {
// If authority property is set normalize as a FormFieldMetadataValueObject object // If authority property is set normalize as a FormFieldMetadataValueObject object
/* NOTE: Data received from server could have authority property equal to null, but into form /* NOTE: Data received from server could have authority property equal to null, but into form
field's model is required a FormFieldMetadataValueObject object as field value, so double-check in field's model is required a FormFieldMetadataValueObject object as field value, so instantiate it */
field's parser and eventually instantiate it */ result = new FormFieldMetadataValueObject(
// if (isNotEmpty(obj.authority)) { obj.value,
// result = new FormFieldMetadataValueObject(obj.value, obj.language, obj.authority, (obj.display || obj.value), obj.place, obj.confidence); obj.language,
// } else if (isNotEmpty(obj.language)) { obj.authority,
// const languageValue = new FormFieldLanguageValueObject(obj.value, obj.language); (obj.display || obj.value),
// result = languageValue; obj.place,
// } else { obj.confidence,
// // Normalize as a string value obj.otherInformation
// result = obj.value; );
// }
result = new FormFieldMetadataValueObject(obj.value, obj.language, obj.authority, (obj.display || obj.value), obj.place, obj.confidence);
} else if (Array.isArray(obj)) { } else if (Array.isArray(obj)) {
result = []; result = [];
obj.forEach((item, index) => { obj.forEach((item, index) => {

View File

@@ -1,7 +1,12 @@
<div [className]="'float-left w-100 ' + wrapperClass"> <div [className]="'float-left w-100 ' + wrapperClass">
<ul class="nav nav-pills d-flex flex-column flex-sm-row" [sortablejs]="chips.getChips()" [sortablejsOptions]="options"> <ul class="nav nav-pills d-flex flex-column flex-sm-row" [sortablejs]="chips.getChips()" [sortablejsOptions]="options">
<ng-container *ngFor="let c of chips.getChips(); let i = index"> <ng-container *ngFor="let c of chips.getChips(); let i = index">
<ng-template #tipContent>{{tipText}}</ng-template> <ng-template #tipContent>
<p class="text-left p-0 m-0" *ngFor="let tip of tipText">
{{tip}}
</p>
</ng-template>
<li class="nav-item mr-2 mb-1" <li class="nav-item mr-2 mb-1"
#t="ngbTooltip" #t="ngbTooltip"
triggers="manual" triggers="manual"

View File

@@ -7,6 +7,7 @@ import { isObject } from 'lodash';
import { Chips } from './models/chips.model'; import { Chips } from './models/chips.model';
import { ChipsItem } from './models/chips-item.model'; import { ChipsItem } from './models/chips-item.model';
import { UploaderService } from '../uploader/uploader.service'; import { UploaderService } from '../uploader/uploader.service';
import { TranslateService } from '@ngx-translate/core';
@Component({ @Component({
selector: 'ds-chips', selector: 'ds-chips',
@@ -25,9 +26,13 @@ export class ChipsComponent implements OnChanges {
options: SortablejsOptions; options: SortablejsOptions;
dragged = -1; dragged = -1;
tipText: string; tipText: string[];
constructor(
private cdr: ChangeDetectorRef,
private uploaderService: UploaderService,
private translate: TranslateService) {
constructor(private cdr: ChangeDetectorRef, private uploaderService: UploaderService) {
this.options = { this.options = {
animation: 300, animation: 300,
chosenClass: 'm-0', chosenClass: 'm-0',
@@ -79,17 +84,30 @@ export class ChipsComponent implements OnChanges {
showTooltip(tooltip: NgbTooltip, index, field?) { showTooltip(tooltip: NgbTooltip, index, field?) {
tooltip.close(); tooltip.close();
const item = this.chips.getChipByIndex(index); const chipsItem = this.chips.getChipByIndex(index);
let textToDisplay: string; const textToDisplay: string[] = [];
if (!item.editMode && this.dragged === -1) { if (!chipsItem.editMode && this.dragged === -1) {
if (field) { if (field) {
textToDisplay = (isObject(item.item[field])) ? item.item[field].display : item.item[field]; if (isObject(chipsItem.item[field])) {
textToDisplay.push(chipsItem.item[field].display);
if (chipsItem.item[field].hasOtherInformation()) {
Object.keys(chipsItem.item[field].otherInformation)
.forEach((otherField) => {
this.translate.get('form.other-information.' + otherField)
.subscribe((label) => {
textToDisplay.push(label + ': ' + chipsItem.item[field].otherInformation[otherField]);
})
})
}
} else { } else {
textToDisplay = item.display; textToDisplay.push(chipsItem.item[field]);
}
} else {
textToDisplay.push(chipsItem.display);
} }
this.cdr.detectChanges(); this.cdr.detectChanges();
if (!item.hasIcons() || field) { if (!chipsItem.hasIcons() || field) {
this.tipText = textToDisplay; this.tipText = textToDisplay;
tooltip.open(); tooltip.open();
} }

View File

@@ -56,7 +56,8 @@
</div> </div>
<div class="d-flex"> <div class="d-flex">
<div *ngIf="!chips.hasItems()"> <ds-loading *ngIf="!chips" [showMessage]="false"></ds-loading>
<div *ngIf="chips && !chips.hasItems()">
<input type="text" <input type="text"
class="border-0 form-control-plaintext tag-input mt-1 mb-1 pl-2 text-muted" class="border-0 form-control-plaintext tag-input mt-1 mb-1 pl-2 text-muted"
readonly readonly
@@ -64,7 +65,7 @@
value="{{'form.no-value' | translate}}"> value="{{'form.no-value' | translate}}">
</div> </div>
<ds-chips <ds-chips
*ngIf="chips.hasItems()" *ngIf="chips && chips.hasItems()"
[chips]="chips" [chips]="chips"
[editable]="true" [editable]="true"
(selected)="onChipSelected($event)"></ds-chips> (selected)="onChipSelected($event)"></ds-chips>

View File

@@ -9,8 +9,11 @@ import {
Output, Output,
ViewChild ViewChild
} from '@angular/core'; } from '@angular/core';
import { FormGroup } from '@angular/forms';
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 { DynamicFormControlModel, DynamicFormGroupModel, DynamicInputModel } from '@ng-dynamic-forms/core'; import { DynamicFormControlModel, DynamicFormGroupModel, DynamicInputModel } from '@ng-dynamic-forms/core';
import { isEqual } from 'lodash'; import { isEqual } from 'lodash';
@@ -25,11 +28,10 @@ import { shrinkInOut } from '../../../../../animations/shrink';
import { ChipsItem } from '../../../../../chips/models/chips-item.model'; import { ChipsItem } from '../../../../../chips/models/chips-item.model';
import { GlobalConfig } from '../../../../../../../config/global-config.interface'; import { GlobalConfig } from '../../../../../../../config/global-config.interface';
import { GLOBAL_CONFIG } from '../../../../../../../config'; import { GLOBAL_CONFIG } from '../../../../../../../config';
import { FormGroup } from '@angular/forms';
import { Subscription } from 'rxjs/Subscription';
import { hasOnlyEmptyProperties } from '../../../../../object.util'; import { hasOnlyEmptyProperties } from '../../../../../object.util';
import { FormFieldMetadataValueObject } from '../../../models/form-field-metadata-value.model'; import { IntegrationSearchOptions } from '../../../../../../core/integration/models/integration-options.model';
import { AuthorityValue } from '../../../../../../core/integration/models/authority.value'; import { AuthorityService } from '../../../../../../core/integration/authority.service';
import { IntegrationData } from '../../../../../../core/integration/integration-data';
@Component({ @Component({
selector: 'ds-dynamic-group', selector: 'ds-dynamic-group',
@@ -59,6 +61,7 @@ export class DsDynamicGroupComponent implements OnDestroy, OnInit {
@ViewChild('formRef') private formRef: FormComponent; @ViewChild('formRef') private formRef: FormComponent;
constructor(@Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig, constructor(@Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig,
private authorityService: AuthorityService,
private formBuilderService: FormBuilderService, private formBuilderService: FormBuilderService,
private formService: FormService, private formService: FormService,
private cdr: ChangeDetectorRef) { private cdr: ChangeDetectorRef) {
@@ -75,7 +78,6 @@ export class DsDynamicGroupComponent implements OnDestroy, OnInit {
} else { } else {
this.expandForm(); this.expandForm();
} }
// this.formCollapsed = (isNotEmpty(value) && !(value.length === 1 && hasOnlyEmptyProperties(value[0]))) ? Observable.of(true) : Observable.of(false);
}); });
this.formId = this.formService.getUniqueId(this.model.id); this.formId = this.formService.getUniqueId(this.model.id);
@@ -85,30 +87,10 @@ export class DsDynamicGroupComponent implements OnDestroy, OnInit {
{}, {},
this.model.submissionScope, this.model.submissionScope,
this.model.readOnly); this.model.readOnly);
const initChipsValue = this.model.isEmpty() ? [] : this.model.value; this.initChipsFromModelValue();
this.chips = new Chips(
initChipsValue,
'value',
this.model.mandatoryField,
this.EnvConfig.submission.metadata.icons);
this.subs.push(
this.chips.chipsItems
.subscribe((subItems: any[]) => {
const items = this.chips.getChipsItems();
// Does not emit change if model value is equal to the current value
if (!isEqual(items, this.model.value)) {
// if ((isNotEmpty(items) && !this.model.isEmpty()) || (isEmpty(items) && !this.model.isEmpty())) {
if (!(isEmpty(items) && this.model.isEmpty())) {
this.model.valueUpdates.next(items);
this.change.emit();
}
}
}),
)
} }
isMandatoryFieldEmpty() { isMandatoryFieldEmpty() {
// formModel[0].group[0].value == null
let res = true; let res = true;
this.formModel.forEach((row) => { this.formModel.forEach((row) => {
const modelRow = row as DynamicFormGroupModel; const modelRow = row as DynamicFormGroupModel;
@@ -136,11 +118,6 @@ export class DsDynamicGroupComponent implements OnDestroy, OnInit {
|| this.selectedChipItem.item[model.name].value === PLACEHOLDER_PARENT_METADATA) || this.selectedChipItem.item[model.name].value === PLACEHOLDER_PARENT_METADATA)
? null ? null
: this.selectedChipItem.item[model.name]; : this.selectedChipItem.item[model.name];
// if (value instanceof FormFieldMetadataValueObject || value instanceof AuthorityValueModel) {
// model.valueUpdates.next(value.display);
// } else {
// model.valueUpdates.next(value);
// }
model.valueUpdates.next(value); model.valueUpdates.next(value);
}); });
}); });
@@ -226,6 +203,91 @@ export class DsDynamicGroupComponent implements OnDestroy, OnInit {
return item; return item;
} }
private initChipsFromModelValue() {
let initChipsValue$: Observable<any[]>;
if (this.model.isEmpty()) {
this.initChips([]);
} else {
initChipsValue$ = Observable.of(this.model.value);
// If authority
this.subs.push(initChipsValue$.pipe(
flatMap((valueModel) => {
const returnList: Array<Observable<any>> = [];
valueModel.forEach((valueObj) => {
let returnObj = Object.create({});
returnObj = Object.keys(valueObj).map((fieldName) => {
let return$: Observable<any>;
if (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(
(model as any).authorityOptions.scope,
(model as any).authorityOptions.name,
(model as any).authorityOptions.metadata,
valueObj[fieldName].authority,
(model as any).maxOptions,
1);
return$ = this.authorityService.getEntryByValue(searchOptions)
.map((result: IntegrationData) => result.payload[0]);
} else {
return$ = Observable.of(valueObj[fieldName]);
}
return return$.map((entry) => ({[fieldName]: entry}));
});
returnList.push(Observable.combineLatest(returnObj));
});
return returnList;
}),
mergeMap((valueListObj: Observable<any>, index: number) => {
return valueListObj.pipe(
map((valueObj: any) => ({
index: index, value: valueObj.reduce(
(acc: any, value: any) => Object.assign({}, acc, value)
)
})
)
)
}),
scan((acc: any[], valueObj: any) => {
if (acc.length === 0) {
acc.push(valueObj.value);
} else {
acc.splice(valueObj.index, 0, valueObj.value);
}
return acc;
}, []),
filter((modelValues: any[]) => this.model.value.length === modelValues.length)
).subscribe((modelValue) => {
this.initChips(modelValue);
}));
}
}
private initChips(initChipsValue) {
this.chips = new Chips(
initChipsValue,
'value',
this.model.mandatoryField,
this.EnvConfig.submission.metadata.icons);
this.subs.push(
this.chips.chipsItems
.subscribe((subItems: any[]) => {
const items = this.chips.getChipsItems();
// Does not emit change if model value is equal to the current value
if (!isEqual(items, this.model.value)) {
if (!(isEmpty(items) && this.model.isEmpty())) {
this.model.valueUpdates.next(items);
this.change.emit();
}
}
}),
)
}
private resetForm() { private resetForm() {
if (this.formRef) { if (this.formRef) {
this.formService.resetForm(this.formRef.formGroup, this.formModel, this.formId); this.formService.resetForm(this.formRef.formGroup, this.formModel, this.formId);

View File

@@ -75,14 +75,31 @@
*ngIf="optionsList && optionsList.length == 0" *ngIf="optionsList && optionsList.length == 0"
(click)="$event.stopPropagation(); clearFields(); sdRef.close();">{{'form.no-results' | translate}} (click)="$event.stopPropagation(); clearFields(); sdRef.close();">{{'form.no-results' | translate}}
</button> </button>
<button class="dropdown-item collection-item" <button class="dropdown-item lookup-item"
*ngFor="let listEntry of optionsList" *ngFor="let listEntry of optionsList"
(click)="$event.stopPropagation(); onSelect(listEntry); sdRef.close();" (click)="$event.stopPropagation(); onSelect(listEntry); sdRef.close();"
title="{{ listEntry.display }}"> title="{{ listEntry.display }}">
{{listEntry.value}} <ng-container
[ngTemplateOutlet]="(listEntry.hasOtherInformation()) ? hasInfo : noInfo"
[ngTemplateOutletContext]="{entry: listEntry}">
</ng-container>
</button> </button>
<div class="scrollable-dropdown-loading text-center" *ngIf="loading"><p>{{'form.loading' | translate}}</p></div> <div class="scrollable-dropdown-loading text-center" *ngIf="loading"><p>{{'form.loading' | translate}}</p></div>
</div> </div>
</div> </div>
</div> </div>
<ng-template #hasInfo let-entry="entry">
<ul class="list-unstyled mb-0">
<li class="list-item text-truncate text-primary font-weight-bold">{{entry.value}}</li>
<li class="list-item text-truncate text-secondary" *ngFor="let item of entry.otherInformation | dsObjNgFor" >
{{ 'form.other-information.' + item.key | translate }} : {{item.value}}
</li>
</ul>
</ng-template>
<ng-template #noInfo let-entry="entry">
<ul class="list-unstyled mb-0">
<li class="list-item text-truncate text-primary font-weight-bold">{{entry.value}}</li>
</ul>
</ng-template>

View File

@@ -20,11 +20,11 @@
/* align fa-spin */ /* align fa-spin */
.left-addon .fa-spin { .left-addon .fa-spin {
left: 0px; left: 0;
} }
.right-addon .fa-spin { .right-addon .fa-spin {
right: 0px; right: 0;
} }
/* add padding */ /* add padding */
@@ -55,3 +55,7 @@
div { div {
overflow: visible; overflow: visible;
} }
.lookup-item {
border-bottom: $dropdown-border-width solid $dropdown-border-color;
}

View File

@@ -1,6 +1,25 @@
<ng-template #rt let-r="result" let-t="term"> <ng-template #rt let-listEntry="result" let-t="term">
{{ r.display}} <ng-container
[ngTemplateOutlet]="(listEntry.hasOtherInformation()) ? hasInfo : noInfo"
[ngTemplateOutletContext]="{entry: listEntry}">
</ng-container>
</ng-template> </ng-template>
<ng-template #hasInfo let-entry="entry">
<ul class="list-unstyled mb-0">
<li class="list-item text-truncate text-primary font-weight-bold">{{entry.value}}</li>
<li class="list-item text-truncate text-secondary" *ngFor="let item of entry.otherInformation | dsObjNgFor" >
{{ 'form.other-information.' + item.key | translate }} : {{item.value}}
</li>
</ul>
</ng-template>
<ng-template #noInfo let-entry="entry">
<ul class="list-unstyled mb-0">
<li class="list-item text-truncate text-primary font-weight-bold">{{entry.value}}</li>
</ul>
</ng-template>
<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>
<input class="form-control" <input class="form-control"

View File

@@ -16,6 +16,10 @@
overflow-x: hidden; overflow-x: hidden;
} }
:host /deep/ .dropdown-item {
border-bottom: $dropdown-border-width solid $dropdown-border-color;
}
:host /deep/ .dropdown-item.active, :host /deep/ .dropdown-item.active,
:host /deep/ .dropdown-item:active, :host /deep/ .dropdown-item:active,
:host /deep/ .dropdown-item:focus, :host /deep/ .dropdown-item:focus,

View File

@@ -10,6 +10,7 @@ export class FormFieldMetadataValueObject {
place: number; place: number;
closed: boolean; closed: boolean;
label: string; label: string;
otherInformation: any;
constructor(value: any = null, constructor(value: any = null,
language: any = null, language: any = null,
@@ -17,6 +18,7 @@ export class FormFieldMetadataValueObject {
display: string = null, display: string = null,
place: number = 0, place: number = 0,
confidence: number = -1, confidence: number = -1,
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;
this.language = language; this.language = language;
@@ -34,6 +36,8 @@ export class FormFieldMetadataValueObject {
if (isNotEmpty(metadata)) { if (isNotEmpty(metadata)) {
this.metadata = metadata; this.metadata = metadata;
} }
this.otherInformation = otherInformation;
} }
hasAuthority(): boolean { hasAuthority(): boolean {
@@ -43,4 +47,8 @@ export class FormFieldMetadataValueObject {
hasValue(): boolean { hasValue(): boolean {
return isNotEmpty(this.value); return isNotEmpty(this.value);
} }
hasOtherInformation(): boolean {
return isNotEmpty(this.otherInformation);
}
} }