Fix issue with visibility rules on multiplicity fields.

* Append new visibility rules on creation of multiplicity item
* Compute visibility rules for multiplicityItems on loading data (Researchers page)
This commit is contained in:
Kristian Ntavidi 2021-09-20 14:34:09 +03:00
parent ba865c9708
commit b40e30ee60
6 changed files with 382 additions and 8 deletions

View File

@ -1,5 +1,5 @@
import { Component, Input, OnInit } from '@angular/core';
import { FormArray, FormGroup, AbstractControl } from '@angular/forms';
import { FormArray, FormGroup, AbstractControl, FormArrayName } from '@angular/forms';
import { DatasetDescriptionCompositeFieldEditorModel, DatasetDescriptionFieldEditorModel } from '../../dataset-description-form.model';
import { ToCEntry } from '../../dataset-description.component';
import { VisibilityRulesService } from '../../visibility-rules/visibility-rules.service';
@ -49,17 +49,33 @@ export class FormCompositeFieldComponent {
}
deleteCompositeFieldFormGroup() {
const compositeFieldId = ((this.form.get('multiplicityItems') as FormArray).get(''+0) as FormGroup).getRawValue().id;
const fieldIds = (this.form.get('fields') as FormArray).controls.map(control => control.get('id').value) as string[];
const numberOfItems = this.form.get('multiplicityItems').get('' + 0).get('fields').value.length;
for (let i = 0; i < numberOfItems; i++) {
const multiplicityItem = this.form.get('multiplicityItems').get('' + 0).get('fields').get('' + i).value;
this.form.get('fields').get('' + i).patchValue(multiplicityItem);
}
(<FormArray>(this.form.get('multiplicityItems'))).removeAt(0);
this.visibilityRulesService.annihilateId(compositeFieldId);
fieldIds.forEach( x => this.visibilityRulesService.annihilateId(x));
}
deleteMultipeFieldFromCompositeFormGroup() {
const parent = this.form.parent;
const index = (parent as FormArray).controls.indexOf(this.form);
const currentId = this.form.get('id').value;
const fieldIds = (this.form.get('fields') as FormArray).controls.map(control => control.get('id').value) as string[];
this.visibilityRulesService.annihilateId(currentId);
fieldIds.forEach(x => this.visibilityRulesService.annihilateId(x));
(parent as FormArray).removeAt(index);
(parent as FormArray).controls.forEach((control, i)=>{
try{

View File

@ -6,6 +6,9 @@ import { DatasetDescriptionSectionEditorModel, DatasetDescriptionCompositeFieldE
import { FormCompositeFieldComponent } from '../form-composite-field/form-composite-field.component';
import { LinkToScroll } from '../../tableOfContentsMaterial/table-of-contents';
import { ToCEntry, ToCEntryType } from '../../dataset-description.component';
import { VisibilityRuleSource } from '../../visibility-rules/models/visibility-rule-source';
import { VisibilityRule } from '../../visibility-rules/models/visibility-rule';
import { Rule } from '@app/core/model/dataset-profile-definition/rule';
@Component({
@ -70,8 +73,118 @@ export class FormSectionComponent implements OnInit, OnChanges {
addMultipleField(fieldsetIndex: number) {
const compositeFieldToBeCloned = (this.form.get('compositeFields').get('' + fieldsetIndex) as FormGroup).getRawValue();
const multiplicityItemsArray = (<FormArray>(this.form.get('compositeFields').get('' + fieldsetIndex).get('multiplicityItems')));;
const compositeField: DatasetDescriptionCompositeFieldEditorModel = new DatasetDescriptionCompositeFieldEditorModel().cloneForMultiplicity(compositeFieldToBeCloned, multiplicityItemsArray.length);
const multiplicityItemsArray = (<FormArray>(this.form.get('compositeFields').get('' + fieldsetIndex).get('multiplicityItems')));
const idMappings = [] as {old: string, new: string}[];
const compositeField: DatasetDescriptionCompositeFieldEditorModel = new DatasetDescriptionCompositeFieldEditorModel().cloneForMultiplicity(compositeFieldToBeCloned, multiplicityItemsArray.length,idMappings);
// ** COMPOSITE FIELD IS SHOWN OR HIDDEN FROM ANOTHER ELEMENT
const compositeFieldVisibilityDependencies = this.visibilityRulesService.getVisibilityDependency(compositeFieldToBeCloned);
if(compositeFieldVisibilityDependencies && compositeFieldVisibilityDependencies.length){
compositeFieldVisibilityDependencies.forEach(x =>{
const visRule: Rule = {
targetField: compositeField.id,
sourceField: x.sourceControlId,
requiredValue: x.sourceControlValue,
type: ''
}
this.visibilityRulesService.addNewRule(visRule);
});
}
// console.log('idMappings ', idMappings);
compositeFieldToBeCloned.fields.forEach(element => {
// console.log(this.visibilityRulesService.getVisibilityDependency(element.id));
const dependency = this.visibilityRulesService.getVisibilityDependency(element.id);
try{
if(dependency && dependency.length){
// * INNER VISIBILITY DEPENDENCIES
// * IF INNER INPUT HIDES ANOTHER INNER INPUT
const innerDependency = compositeFieldToBeCloned.fields.reduce((innerD, currentElement )=>{
const innerDependecies = dependency.filter(d => d.sourceControlId === currentElement.id);
return[...innerD, ...innerDependecies];
},[]) as VisibilityRuleSource[];
if(innerDependency.length){
//Build visibility source
const updatedRules = innerDependency.map(x => {
const newId = idMappings.find(y=> y.old === x.sourceControlId);
return {...x, sourceControlId: newId.new};
});
const visRule: VisibilityRule = {
targetControlId: idMappings.find(x => x.old === element.id).new,
sourceVisibilityRules: updatedRules
}
this.visibilityRulesService.appendVisibilityRule(visRule);
}
}
// * OUTER DEPENDENCIES
// * IF INNER INPUT HIDES OUTER INPUTS
const innerIds = idMappings.map(x => x.old) as string[];
const outerTargets = this.visibilityRulesService.getVisibilityTargets(element.id).filter( x=> !innerIds.includes(x));
outerTargets.forEach(target =>{
const outerRules = (this.visibilityRulesService.getVisibilityDependency(target) as VisibilityRuleSource[]).filter(x => x.sourceControlId === element.id);
const updatedRules = outerRules.map(x => {
return {...x ,sourceControlId: idMappings.find(y => y.old === element.id).new};
});
const visRule: VisibilityRule = {
targetControlId: target,
sourceVisibilityRules: updatedRules
}
this.visibilityRulesService.appendVisibilityRule(visRule);
});
// * IF INNER INPUT IS HIDDEN BY OUTER INPUT
if(dependency && dependency.length){
const fieldsThatHideInnerElement = dependency.filter(x => !innerIds.includes(x.sourceControlId));
if( fieldsThatHideInnerElement.length){
fieldsThatHideInnerElement.forEach(x =>{
const visRule: Rule = {
targetField: idMappings.find(l => l.old === element.id).new,
sourceField: x.sourceControlId,
requiredValue: x.sourceControlValue,
type: ''
}
this.visibilityRulesService.addNewRule(visRule);
});
}
}
// console.log(`for ${element.id} targets are`, outerTargets);
} catch {
console.log('error');
}
});
// console.log(this.visibilityRulesService);
// console.log(this.visibilityRulesService.getVisibilityDependency());
multiplicityItemsArray.push(compositeField.buildForm());
}

View File

@ -194,11 +194,17 @@ export class DatasetDescriptionCompositeFieldEditorModel extends BaseFormModel {
// return newItem;
// }
cloneForMultiplicity(item: CompositeField, ordinal: number): DatasetDescriptionCompositeFieldEditorModel {
cloneForMultiplicity(item: CompositeField, ordinal: number, idMappings:{old:string, new:string}[] = []): DatasetDescriptionCompositeFieldEditorModel {
const newItem: DatasetDescriptionCompositeFieldEditorModel = new DatasetDescriptionCompositeFieldEditorModel();
newItem.id = 'multiple_' + item.id + '_' + Guid.create();
idMappings.push({old: item.id, new: newItem.id });
item.fields.forEach((field, index) => {
newItem.fields.push(new DatasetDescriptionFieldEditorModel().cloneForMultiplicity(field, newItem.id));
const clonedItem = new DatasetDescriptionFieldEditorModel().cloneForMultiplicity(field, newItem.id)
newItem.fields.push(clonedItem);
idMappings.push({old: field.id, new: clonedItem.id});
});
newItem.ordinal = ordinal;
return newItem;
@ -238,7 +244,7 @@ export class DatasetDescriptionFieldEditorModel extends BaseFormModel {
if (item.multiplicity) this.multiplicity = new DatasetDescriptionMultiplicityEditorModel().fromModel(item.multiplicity);
if (item.defaultValue) this.defaultValue = new DatasetDescriptionDefaultValueEditorModel().fromModel(item.defaultValue);
this.value = item.value ? item.value : (this.defaultValue.value ? this.defaultValue.value : undefined);
if (this.viewStyle.renderStyle === 'checkBox') { this.value = this.value === 'true'; }
if (this.viewStyle.renderStyle === 'checkBox' && (item.value !== true)) { this.value = this.value === 'true'; } //Cover both posibilites of boolean true or false and string 'true' or 'false'
if (item.multiplicityItems) { item.multiplicityItems.map(x => this.multiplicityItems.push(new DatasetDescriptionFieldEditorModel().fromModel(x))); }
this.data = item.data;
return this;

View File

@ -2,6 +2,8 @@ import { AfterViewInit, Component, Input, OnChanges, OnInit, SimpleChanges, View
import { AbstractControl, AbstractControlOptions, FormArray, FormGroup } from '@angular/forms';
import { MatExpansionPanel } from '@angular/material';
import { MatHorizontalStepper } from '@angular/material/stepper';
import { CompositeField } from '@app/core/model/dataset-profile-definition/composite-field';
import { Field } from '@app/core/model/dataset-profile-definition/field';
import { Rule } from '@app/core/model/dataset-profile-definition/rule';
import { DatasetProfileTableOfContentsInternalSection } from '@app/ui/admin/dataset-profile/table-of-contents/table-of-contents-internal-section/table-of-contents-internal-section';
import { LinkToScroll } from '@app/ui/misc/dataset-description-form/tableOfContentsMaterial/table-of-contents';
@ -53,6 +55,10 @@ export class DatasetDescriptionComponent extends BaseComponent implements OnInit
this.visibilityRulesInstance.emit(this.visibilityRulesService);
this.tocentries = this.getTocEntries();
this._enrichWithMultiplicityRules(this.tocentries);
this.hiddenEntriesIds = this._findHiddenEntries(this.tocentries);
this.visibilityRulesService.visibilityChange
@ -90,6 +96,156 @@ export class DatasetDescriptionComponent extends BaseComponent implements OnInit
private _enrichWithMultiplicityRules(tocentries: ToCEntry[]) : void {
if (tocentries){
tocentries.forEach(entry => {
if(entry.type === ToCEntryType.Field) return; // * TODO Me to tora implementation den tha ftasei pote edo
if(entry.type === ToCEntryType.FieldSet){
// if(multiplicity: )
try {
// TODO OTAN KANW HIDE MULTIPLE PEDIO TOTE STO ON SHOW HANO TA VALUES (AUTO MPOREI NA EINAI KAI LEGIT) ('NA DOUME AN ONTOS DIAGRAFONTAI I APLA DEN TA DEIXNOUME')
// * UPDATE KANEI DESTROY TO COMPONENT H NGIF . PITHANOTATA NA XREIASTEI NA TO KANOUME HIDDEN AN THELOUME KATI ALLO
const multiplicity = entry.form.get('multiplicity').value;
if( (multiplicity.max > 1 ) && (multiplicity.min> 0) && (multiplicity.max >= multiplicity.min)){ // has valid multiplicity
this._createAndAppendVisibilityRule(entry);
}
} catch {
}
return
}
if(entry.subEntries){
this._enrichWithMultiplicityRules(entry.subEntries);
}
})
}
}
private _createAndAppendVisibilityRule(entry: ToCEntry): void{
if(entry && (entry.type === ToCEntryType.FieldSet)){
//childs that are either target or source
const childIdsWithVisRules = (entry.form.get('fields') as FormArray).controls.reduce((all, s) =>
{
const sval = s.value as Field;
return this.visibilityRules.find(x => (x.targetField === sval.id) || (x.sourceField === sval.id)) ? [...all, sval] : all;
},[]) as Field[];
const innerCompositeFieldOriginalIds = (entry.form.get('fields') as FormArray).controls.map( x=> x.get('id').value) as string[];
//multiplicity items
const multiplicityItemsValue = entry.form.get('multiplicityItems').value as CompositeField[];
// ********* FIELDS OF FIELDSET ARE EITHER TARGETS OR SOURCES *****
if( childIdsWithVisRules.length && multiplicityItemsValue && multiplicityItemsValue.length ){
//check each multiplicity item composite field
multiplicityItemsValue.forEach( mi =>{
const multiplicityCompositeFieldIds = mi.fields.map(x => x.id);
const idMappings = multiplicityCompositeFieldIds.map(x => {
return {
original: innerCompositeFieldOriginalIds.find( l => x.includes(l)),
multiplicityIdValue: x
}
}) as {original: string, multiplicityIdValue: string}[];
//each field of mutliplicity item
mi.fields.forEach(field =>{
//get original visibility rules (original field)
//original id
const original_id = childIdsWithVisRules.find(x=> field.id.includes(x.id)).id;
//get vis rules
//as source
const original_as_source = this.visibilityRules.filter( x => x.sourceField === original_id);
const original_as_target = this.visibilityRules.filter( x => x.targetField === original_id);
if(original_as_source.length){
//inner dependencies
const innerDep = original_as_source.filter(x => innerCompositeFieldOriginalIds.includes(x.targetField));
innerDep.forEach(x =>{
const newRule = {...x, sourceField: field.id, targetField: idMappings.find(l => l.original === x.targetField).multiplicityIdValue} as Rule;
this.visibilityRulesService.addNewRule(newRule);
})
//outer dependencies
const outerDep = original_as_source.filter(x => !innerCompositeFieldOriginalIds.includes(x.targetField));
outerDep.forEach(x =>{
const newRule = {...x, sourceField: field.id};
this.visibilityRulesService.addNewRule(newRule);
})
}
if( original_as_target.length){
//inner dependencies
const innerDep = original_as_target.filter( x=> innerCompositeFieldOriginalIds.includes(x.sourceField));
innerDep.forEach(x =>{
const newRule = {...x, targetField: field.id, sourceField: idMappings.find(l => l.original === x.sourceField).multiplicityIdValue} as Rule;
this.visibilityRulesService.addNewRule(newRule);
})
//outer dependencies
const outerDep = original_as_target.filter( x=> !innerCompositeFieldOriginalIds.includes(x.sourceField));
outerDep.forEach(x=>{
const newRule = {...x, targetField: field.id} as Rule;
this.visibilityRulesService.addNewRule(newRule);
})
}
})
});
}
// ** FIELDSET ITSELF IS TARGET
// ** source it can never be
const compositeFieldAsTargetRules = this.visibilityRules.filter(x => x.targetField === entry.id);
const idCompositeFieldMappings = multiplicityItemsValue.map(x =>{
return {
originalValue: entry.id,
newValue:x.id
}
}) as {originalValue: string, newValue: string}[];
if(compositeFieldAsTargetRules.length){
compositeFieldAsTargetRules.forEach(x =>{
idCompositeFieldMappings.forEach(l=>{
const newRule = {...x, targetField: l.newValue};
this.visibilityRulesService.addNewRule(newRule);
});
});
}
}
}
private _buildRecursively(form: FormGroup,whatAmI:ToCEntryType):ToCEntry{
if(!form) return null;

View File

@ -21,10 +21,22 @@ export class VisibilityRulesContext {
});
}
private addToVisibilityRulesContext(item: Rule): void {
public addToVisibilityRulesContext(item: Rule): void {
for (let i = 0; i < this.rules.length; i++) {
if (this.rules[i].targetControlId === item.targetField) {
this.rules[i].sourceVisibilityRules.push({ sourceControlId: item.sourceField, sourceControlValue: item.requiredValue });
const newRule = { sourceControlId: item.sourceField, sourceControlValue: item.requiredValue };
const ruleExists = this.rules[i].sourceVisibilityRules.find(x =>{
return Object.keys(x).reduce((exact, key)=>{
if(!exact) return false;
return x[key] === newRule[key];
},true);
})
if(!ruleExists){
this.rules[i].sourceVisibilityRules.push(newRule);
}
return;
}
}

View File

@ -5,17 +5,20 @@ import { Subject } from 'rxjs';
import { isNumeric } from 'rxjs/internal/util/isNumeric';
import { Rule } from '../../../../core/model/dataset-profile-definition/rule';
import { VisibilityRule } from './models/visibility-rule';
import { VisibilityRuleSource } from './models/visibility-rule-source';
import { VisibilityRulesContext } from './models/visibility-rules-context';
@Injectable()
export class VisibilityRulesService {
private readonly VISIBILITY_RULE_LOGIC: 'OR'| 'AND' = 'OR';
private visibilityRuleContext: VisibilityRulesContext;
private form: AbstractControl;
private elementVisibilityMap = new Map<String, boolean>();
private _changeMade$ = new Subject<void>();
constructor(
public applicationReference: ApplicationRef,
public ngZone: NgZone
@ -173,4 +176,72 @@ export class VisibilityRulesService {
public get visibilityChange(){
return this._changeMade$.asObservable();
}
public getVisibilityDependency(targetId: string): VisibilityRuleSource[] | null {
return this.visibilityRuleContext.rules.reduce((hasDependency, rule)=>{
if(hasDependency) return hasDependency;
if(rule.targetControlId === targetId){
return rule.sourceVisibilityRules;
}
return null;
}, null) as VisibilityRuleSource[];
}
public getVisibilityTargets(sourceId: string) : string[]{
return this.visibilityRuleContext.rules.filter(x => {
const result = x.sourceVisibilityRules.filter(y => y.sourceControlId === sourceId);
return result.length;
}).map(x => x.targetControlId);
}
public appendVisibilityRule(rule: VisibilityRule): void{
const existingTargetRule = this.visibilityRuleContext.rules.find( r => r.targetControlId === rule.targetControlId);
if(existingTargetRule){
rule.sourceVisibilityRules.forEach(svr =>{
existingTargetRule.sourceVisibilityRules.push(svr);
});
}else{
this.visibilityRuleContext.rules.push(rule);
}
}
//removes rule that has the specific id either as a source either as a target
public annihilateId(id: string) : void{
//remove as a target
const temp = this.visibilityRuleContext.rules.map((x,i) => (x.targetControlId === id )? i : null );
const indexes = temp.filter( x => x !== null);
indexes.reverse().forEach(index => this.visibilityRuleContext.rules.splice(index, 1));
this.elementVisibilityMap.delete(id);
//remove as a source
const tbd = this.visibilityRuleContext.rules.reduce((to_be_deleted ,rule, ruleIdx) =>{
const idxs = rule.sourceVisibilityRules.map((x,i) => (x.sourceControlId === id) ? i : null).filter( x=> x !== null );
idxs.reverse().forEach( index => rule.sourceVisibilityRules.splice(index, 1));
if(!rule.sourceVisibilityRules.length){
to_be_deleted.push(ruleIdx);
}
return to_be_deleted
},[]);
//clean up empty
tbd.reverse().forEach(index =>{
this.visibilityRuleContext.rules.splice(index,1);
});
}
public addNewRule(rule: Rule): void{
this.visibilityRuleContext.addToVisibilityRulesContext(rule);
}
}