From 7790d19818a9f1f11bf6e8e58f437ee46933122c Mon Sep 17 00:00:00 2001 From: sgiannopoulos Date: Thu, 16 May 2024 16:40:51 +0300 Subject: [PATCH] visibility rule fixes --- .../visibility/PropertyDefinition.java | 2 +- .../PropertyDefinitionFieldSet.java | 2 +- .../PropertyDefinitionFieldSetItem.java | 4 +- .../visibility/VisibilityServiceImpl.java | 75 ++++++++++++++- .../visibility-rules.service.ts | 91 +++++++++++++++++-- 5 files changed, 157 insertions(+), 17 deletions(-) diff --git a/backend/core/src/main/java/org/opencdmp/service/visibility/PropertyDefinition.java b/backend/core/src/main/java/org/opencdmp/service/visibility/PropertyDefinition.java index b9f9e99c1..cd96c0691 100644 --- a/backend/core/src/main/java/org/opencdmp/service/visibility/PropertyDefinition.java +++ b/backend/core/src/main/java/org/opencdmp/service/visibility/PropertyDefinition.java @@ -13,7 +13,7 @@ public class PropertyDefinition { private final Map fieldSets; public Map getFieldSets() { - return fieldSets; + return this.fieldSets; } public PropertyDefinition(PropertyDefinitionPersist persist){ diff --git a/backend/core/src/main/java/org/opencdmp/service/visibility/PropertyDefinitionFieldSet.java b/backend/core/src/main/java/org/opencdmp/service/visibility/PropertyDefinitionFieldSet.java index 1b700a9cf..e768cfe21 100644 --- a/backend/core/src/main/java/org/opencdmp/service/visibility/PropertyDefinitionFieldSet.java +++ b/backend/core/src/main/java/org/opencdmp/service/visibility/PropertyDefinitionFieldSet.java @@ -13,7 +13,7 @@ public class PropertyDefinitionFieldSet { private final List items; public List getItems() { - return items; + return this.items; } public PropertyDefinitionFieldSet(PropertyDefinitionFieldSetPersist persist){ diff --git a/backend/core/src/main/java/org/opencdmp/service/visibility/PropertyDefinitionFieldSetItem.java b/backend/core/src/main/java/org/opencdmp/service/visibility/PropertyDefinitionFieldSetItem.java index cc90ad546..fa08b3c99 100644 --- a/backend/core/src/main/java/org/opencdmp/service/visibility/PropertyDefinitionFieldSetItem.java +++ b/backend/core/src/main/java/org/opencdmp/service/visibility/PropertyDefinitionFieldSetItem.java @@ -15,12 +15,12 @@ public class PropertyDefinitionFieldSetItem { private final Integer ordinal; public Map getFields() { - return fields; + return this.fields; } public Integer getOrdinal() { - return ordinal; + return this.ordinal; } diff --git a/backend/core/src/main/java/org/opencdmp/service/visibility/VisibilityServiceImpl.java b/backend/core/src/main/java/org/opencdmp/service/visibility/VisibilityServiceImpl.java index e778c4e30..abbadf92b 100644 --- a/backend/core/src/main/java/org/opencdmp/service/visibility/VisibilityServiceImpl.java +++ b/backend/core/src/main/java/org/opencdmp/service/visibility/VisibilityServiceImpl.java @@ -14,6 +14,7 @@ public class VisibilityServiceImpl implements VisibilityService { private final PropertyDefinition propertyDefinition; private Map> rulesBySources; + private Map> rulesByTarget; private Map visibility; public VisibilityServiceImpl(DefinitionEntity definition, PropertyDefinitionPersist propertyDefinition) { @@ -27,14 +28,18 @@ public class VisibilityServiceImpl implements VisibilityService { } private void initRules(){ - if (this.rulesBySources != null) return; + if (this.rulesBySources != null && this.rulesByTarget != null) return; this.rulesBySources = new HashMap<>(); + this.rulesByTarget = new HashMap<>(); for (FieldEntity fieldEntity : this.definition.getAllField()){ if (fieldEntity.getVisibilityRules() != null && !fieldEntity.getVisibilityRules().isEmpty()) { for (RuleEntity rule : fieldEntity.getVisibilityRules()){ if (!this.rulesBySources.containsKey(fieldEntity.getId())) this.rulesBySources.put(fieldEntity.getId(), new ArrayList<>()); RuleWithTarget ruleWithTarget = new RuleWithTarget(fieldEntity.getId(), rule, fieldEntity); this.rulesBySources.get(fieldEntity.getId()).add(ruleWithTarget); + + if (!this.rulesByTarget.containsKey(rule.getTarget())) this.rulesByTarget.put(rule.getTarget(), new ArrayList<>()); + this.rulesByTarget.get(rule.getTarget()).add(ruleWithTarget); } } } @@ -76,21 +81,24 @@ public class VisibilityServiceImpl implements VisibilityService { if (rule.getSource().equals(key)){ Field field = definitionFieldSetItem.getFields().get(key); + List rulesForParentKey = this.getChainParentRules(rule); + + boolean parentIsVisible = rulesForParentKey != null && !rulesForParentKey.isEmpty() ? this.isChainParentVisible(rulesForParentKey, definitionFieldSetItem.getFields(), definitionFieldSetItem.getOrdinal()) : true; if (definitionFieldSetItem.getFields().containsKey(rule.getTarget())){ //Rule applies only for current multiple item FieldKey fieldKey = new FieldKey(rule.getTarget(), definitionFieldSetItem.getOrdinal()); boolean currentState = this.visibility.getOrDefault(fieldKey, false); - this.visibility.put(fieldKey, currentState || this.ruleIsTrue(rule, field)); + this.visibility.put(fieldKey, parentIsVisible && (currentState || this.ruleIsTrue(rule, field))); } else if (!this.definition.getFieldById(rule.getTarget()).isEmpty() || !this.definition.getFieldSetById(rule.getTarget()).isEmpty()) { //Rule applies to different fieldset, so we apply for all multiple items List ordinals = this.getKeyOrdinals(rule.getTarget()); for (Integer ordinal : ordinals){ FieldKey fieldKey = new FieldKey(rule.getTarget(), ordinal); boolean currentState = this.visibility.getOrDefault(fieldKey, false); - this.visibility.put(fieldKey, currentState || this.ruleIsTrue(rule, field)); + this.visibility.put(fieldKey, parentIsVisible && (currentState || this.ruleIsTrue(rule, field))); } } else { FieldKey fieldKey = new FieldKey(rule.getTarget(), null); //Ordinal is null if target not on field boolean currentState = this.visibility.getOrDefault(fieldKey, false); - this.visibility.put(fieldKey, currentState || this.ruleIsTrue(rule, field)); + this.visibility.put(fieldKey, parentIsVisible && (currentState || this.ruleIsTrue(rule, field))); } } } @@ -102,6 +110,65 @@ public class VisibilityServiceImpl implements VisibilityService { } } } + + private boolean isChainParentVisible(List rulesForParentKey, Map fieldsMap, int ordinal) { + boolean isVisible = false; + if (rulesForParentKey == null || rulesForParentKey.isEmpty()) return false; + + for (RuleWithTarget ruleForParentKey : rulesForParentKey) { + Field field = fieldsMap.get(ruleForParentKey.getSource()); + + List rulesForGrandParentKey = this.getChainParentRules(ruleForParentKey); + + if (fieldsMap.containsKey(ruleForParentKey.getTarget())){ //Rule applies only for current multiple item + FieldKey fieldKey = new FieldKey(ruleForParentKey.getTarget(), ordinal); + boolean currentState = this.visibility.getOrDefault(fieldKey, false); + isVisible = isVisible || currentState || this.ruleIsTrue(ruleForParentKey, field); + if (rulesForGrandParentKey != null && !rulesForGrandParentKey.isEmpty()) isVisible = isVisible || this.isChainParentVisible(rulesForGrandParentKey, fieldsMap, ordinal); + + } else if (!this.definition.getFieldById(ruleForParentKey.getTarget()).isEmpty() || !this.definition.getFieldSetById(ruleForParentKey.getTarget()).isEmpty()) { //Rule applies to different fieldset, so we apply for all multiple items + List ordinals = this.getKeyOrdinals(ruleForParentKey.getTarget()); + for (Integer curentOrdinal : ordinals){ + FieldKey fieldKey = new FieldKey(ruleForParentKey.getTarget(), curentOrdinal); + boolean currentState = this.visibility.getOrDefault(fieldKey, false); + isVisible = isVisible || currentState || this.ruleIsTrue(ruleForParentKey, field); + if (rulesForGrandParentKey != null && !rulesForGrandParentKey.isEmpty()) isVisible = isVisible || this.isChainParentVisible(rulesForGrandParentKey, this.getKeyFields(ruleForParentKey.getTarget(), curentOrdinal), ordinal); + } + } else { + FieldKey fieldKey = new FieldKey(ruleForParentKey.getTarget(), null); //Ordinal is null if target not on field + boolean currentState = this.visibility.getOrDefault(fieldKey, false); + isVisible = isVisible || currentState || this.ruleIsTrue(ruleForParentKey, field); + //Nothing to check for grandfather this type of field can not have rules + } + + if (isVisible) break; + } + return isVisible; + } + + private List getChainParentRules(RuleWithTarget rule) { + if (rule == null || rule.getSource() == null || rule.getSource().isBlank() || this.rulesByTarget == null) return null; + return this.rulesByTarget.containsKey(rule.getSource()) ? this.rulesByTarget.get(rule.getSource()).stream().filter(Objects::nonNull).toList() : new ArrayList<>(); + } + + private Map getKeyFields(String key, int ordinal){ + if (this.propertyDefinition.getFieldSets() != null && !this.propertyDefinition.getFieldSets().isEmpty()){ + for (Map.Entry propertyDefinitionFieldSet: this.propertyDefinition.getFieldSets().entrySet()) { + if (propertyDefinitionFieldSet.getKey().equals(key)) return propertyDefinitionFieldSet.getValue().getItems().stream().filter(x-> x.getOrdinal() == ordinal).map(PropertyDefinitionFieldSetItem::getFields).findFirst().orElse(new HashMap<>()); + + if (propertyDefinitionFieldSet.getValue() != null && propertyDefinitionFieldSet.getValue().getItems() != null && !propertyDefinitionFieldSet.getValue().getItems().isEmpty()) { + for (PropertyDefinitionFieldSetItem definitionFieldSetItem : propertyDefinitionFieldSet.getValue().getItems()) { + if (definitionFieldSetItem.getFields() != null && !definitionFieldSetItem.getFields().isEmpty()) { + for (String fieldKey : definitionFieldSetItem.getFields().keySet()) { + if (fieldKey.equals(key)) return propertyDefinitionFieldSet.getValue().getItems().stream().filter(x-> x.getOrdinal() == ordinal).map(PropertyDefinitionFieldSetItem::getFields).findFirst().orElse(new HashMap<>()); + } + } + } + } + } + } + return new HashMap<>(); + } private List getKeyOrdinals(String key){ if (this.propertyDefinition.getFieldSets() != null && !this.propertyDefinition.getFieldSets().isEmpty()){ diff --git a/dmp-frontend/src/app/ui/description/editor/description-form/visibility-rules/visibility-rules.service.ts b/dmp-frontend/src/app/ui/description/editor/description-form/visibility-rules/visibility-rules.service.ts index c427aaf2f..7ff90880c 100644 --- a/dmp-frontend/src/app/ui/description/editor/description-form/visibility-rules/visibility-rules.service.ts +++ b/dmp-frontend/src/app/ui/description/editor/description-form/visibility-rules/visibility-rules.service.ts @@ -1,4 +1,4 @@ -import { Injectable } from '@angular/core'; +import { Injectable, booleanAttribute } from '@angular/core'; import { AbstractControl } from '@angular/forms'; import { Observable, Subject } from 'rxjs'; import { DescriptionTemplateDefinition, DescriptionTemplateField, DescriptionTemplateFieldSet, DescriptionTemplatePage, DescriptionTemplateSection } from '@app/core/model/description-template/description-template'; @@ -11,7 +11,8 @@ import { DescriptionTemplateFieldType } from '@app/core/common/enum/description- export class VisibilityRulesService { private form: AbstractControl; private definition: DescriptionTemplateDefinition; - private rulesBySources: Map ; + private rulesBySources: Map ; + private rulesByTarget: Map ; public isVisibleMap: { [key: string]: boolean } = {}; private _isVisibleMap: { [key: string]: boolean } = null; @@ -36,6 +37,7 @@ export class VisibilityRulesService { this.allDescriptionTemplateFields = null; this.allDescriptionTemplateFieldSets = null; this.rulesBySources = null; + this.rulesByTarget = null; this._isVisibleMap = null; this.calculateVisibility(); } @@ -65,6 +67,7 @@ export class VisibilityRulesService { public reloadVisibility() { this.rulesBySources = null; + this.rulesByTarget = null; this._isVisibleMap = null; this.calculateVisibility(); } @@ -88,8 +91,9 @@ export class VisibilityRulesService { private initRules(){ if (this.definition == null || this.form == null) return; - if (this.rulesBySources != null) return; + if (this.rulesBySources != null && this.rulesByTarget != null) return; this.rulesBySources = new Map(); + this.rulesByTarget = new Map(); const fields: DescriptionTemplateField[] = this.getAllDescriptionTemplateDefinitionFields(this.definition); for (let i = 0; i < fields.length; i++) { @@ -100,6 +104,9 @@ export class VisibilityRulesService { if (!this.rulesBySources.has(fieldEntity.id)) this.rulesBySources.set(fieldEntity.id, []); const ruleWithTarget: RuleWithTarget = new RuleWithTarget(fieldEntity.id, rule, fieldEntity); this.rulesBySources.get(fieldEntity.id).push(ruleWithTarget); + + if (!this.rulesByTarget.has(rule.target)) this.rulesByTarget.set(rule.target, []); + this.rulesByTarget.get(rule.target).push(ruleWithTarget); } } } @@ -212,25 +219,25 @@ export class VisibilityRulesService { const fieldsMap = new Map(Object.entries(definitionFieldSetItem.fields)); fieldsMap.forEach((field: DescriptionFieldPersist, key: string) => { if (rule.source == key){ + const rulesForParentKey: RuleWithTarget[] = this.getChainParentRules(rule); + const parentIsVisible = rulesForParentKey != null && rulesForParentKey.length > 0 ? this.isChainParentVisible(rulesForParentKey, propertyDefinition, fieldsMap, definitionFieldSetItem.ordinal) : true; if (fieldsMap.has(rule.target)){ //Rule applies only for current multiple item const fieldKey = this.buildVisibilityKey(rule.target, definitionFieldSetItem.ordinal); const currentState = this._isVisibleMap[fieldKey] ?? false; - this._isVisibleMap[fieldKey] = currentState || this.ruleIsTrue(rule, field); - //console.log(fieldKey + " " + this._isVisibleMap[fieldKey] + " " + field.textListValue); + this._isVisibleMap[fieldKey] = parentIsVisible && (currentState || this.ruleIsTrue(rule, field)); } else if (this.getDescriptionTemplateDefinitionFieldById(this.definition, rule.target).length > 0 || this.getDescriptionTemplateDefinitionFieldSetById(this.definition, rule.target).length > 0) { //Rule applies to different fieldset, so we apply for all multiple items const ordinals: number[] = this.getKeyOrdinals(rule.target, propertyDefinition); for (let k = 0; k < ordinals.length; k++) { - const ordinal = ordinals[j]; + const ordinal = ordinals[k]; const fieldKey = this.buildVisibilityKey(rule.target, ordinal); const currentState = this._isVisibleMap[fieldKey] ?? false; - this._isVisibleMap[fieldKey] = currentState || this.ruleIsTrue(rule, field); - + this._isVisibleMap[fieldKey] = parentIsVisible && (currentState || this.ruleIsTrue(rule, field)); } } else { const fieldKey = this.buildVisibilityKey(rule.target, null); //Ordinal is null if target not on field const currentState = this._isVisibleMap[fieldKey] ?? false; - this._isVisibleMap[fieldKey] = currentState || this.ruleIsTrue(rule, field); + this._isVisibleMap[fieldKey] = parentIsVisible && (currentState || this.ruleIsTrue(rule, field)); } } }); @@ -243,6 +250,48 @@ export class VisibilityRulesService { }); } + private isChainParentVisible(rulesForParentKey: RuleWithTarget[], propertyDefinition: DescriptionPropertyDefinitionPersist, fieldsMap: Map, ordinal: number): boolean { + let isVisible = false; + if (rulesForParentKey == null || rulesForParentKey.length == 0) return false; + + for (let i = 0; i < rulesForParentKey.length; i++) { + const ruleForParentKey = rulesForParentKey[i]; + const field: DescriptionFieldPersist = fieldsMap.get(ruleForParentKey.source); + + const rulesForGrandParentKey: RuleWithTarget[] = this.getChainParentRules(ruleForParentKey); + + if (fieldsMap.has(ruleForParentKey.target)){ //Rule applies only for current multiple item + const fieldKey = this.buildVisibilityKey(ruleForParentKey.target, ordinal); + const currentState = this._isVisibleMap[fieldKey] ?? false; + isVisible = isVisible || currentState || this.ruleIsTrue(ruleForParentKey, field); + if (rulesForGrandParentKey != null && rulesForGrandParentKey.length > 0) isVisible = isVisible || this.isChainParentVisible(rulesForGrandParentKey, propertyDefinition, fieldsMap, ordinal); + + } else if (this.getDescriptionTemplateDefinitionFieldById(this.definition, ruleForParentKey.target).length > 0 || this.getDescriptionTemplateDefinitionFieldSetById(this.definition, ruleForParentKey.target).length > 0) { //Rule applies to different fieldset, so we apply for all multiple items + const ordinals: number[] = this.getKeyOrdinals(ruleForParentKey.target, propertyDefinition); + for (let k = 0; k < ordinals.length; k++) { + const curentOrdinal = ordinals[k]; + const fieldKey = this.buildVisibilityKey(ruleForParentKey.target, curentOrdinal); + const currentState = this._isVisibleMap[fieldKey] ?? false; + isVisible = isVisible || currentState || this.ruleIsTrue(ruleForParentKey, field); + if (rulesForGrandParentKey != null && rulesForGrandParentKey.length > 0) isVisible = isVisible || this.isChainParentVisible(rulesForGrandParentKey, propertyDefinition, this.getKeyFields(ruleForParentKey.target, curentOrdinal, propertyDefinition), ordinal); + } + } else { + const fieldKey = this.buildVisibilityKey(ruleForParentKey.target, null); //Ordinal is null if target not on field + const currentState = this._isVisibleMap[fieldKey] ?? false; + isVisible = isVisible || currentState || this.ruleIsTrue(ruleForParentKey, field); + //Nothing to check for grandfather this type of field can not have rules + } + + if (isVisible) break; + } + return isVisible; + } + + private getChainParentRules(rule: RuleWithTarget) : RuleWithTarget[] { + if (!rule?.source || this.rulesByTarget == null) return null; + return this.rulesByTarget?.get(rule.source)?.filter(x=> x != null); + } + private getKeyOrdinals(key: string, propertyDefinition: DescriptionPropertyDefinitionPersist): number[]{ let ordinals = []; if (propertyDefinition.fieldSets != null) { @@ -267,6 +316,30 @@ export class VisibilityRulesService { return ordinals; } + private getKeyFields(key: string, ordinal: number, propertyDefinition: DescriptionPropertyDefinitionPersist): Map{ + let fields: Map; + if (propertyDefinition.fieldSets != null) { + new Map(Object.entries(propertyDefinition.fieldSets)).forEach((propertyDefinitionFieldSet: DescriptionPropertyDefinitionFieldSetPersist, propertyDefinitionFieldSetKey: string) => { + if (propertyDefinitionFieldSetKey == key) { + fields = propertyDefinitionFieldSet.items?.find(x => x.ordinal == ordinal)?.fields; + return fields; + } + if (propertyDefinitionFieldSet.items != null && propertyDefinitionFieldSet.items.length > 0) { + for (let i = 0; i < propertyDefinitionFieldSet.items.length; i++) { + const definitionFieldSetItem = propertyDefinitionFieldSet.items[i]; + if (definitionFieldSetItem?.fields != null) { + new Map(Object.entries(definitionFieldSetItem.fields)).forEach((field: DescriptionFieldPersist, fieldKey: string) => { + if (fieldKey == key) fields = propertyDefinitionFieldSet.items?.find(x => x.ordinal == ordinal)?.fields; + }); + if (fields != null && fields.size > 0) return fields; + } + } + } + }); + } + return fields ? new Map(Object.entries(fields)) : new Map(); + } + private ruleIsTrue(rule: RuleWithTarget, field: DescriptionFieldPersist) :boolean{ if (field != null){ const fieldType: DescriptionTemplateFieldType = rule.field != null && rule.field.data != null ? rule.field.data.fieldType : DescriptionTemplateFieldType.FREE_TEXT;