visibility rule fixes

This commit is contained in:
Efstratios Giannopoulos 2024-05-16 16:40:51 +03:00
parent 48eb38a156
commit 7790d19818
5 changed files with 157 additions and 17 deletions

View File

@ -13,7 +13,7 @@ public class PropertyDefinition {
private final Map<String, PropertyDefinitionFieldSet> fieldSets; private final Map<String, PropertyDefinitionFieldSet> fieldSets;
public Map<String, PropertyDefinitionFieldSet> getFieldSets() { public Map<String, PropertyDefinitionFieldSet> getFieldSets() {
return fieldSets; return this.fieldSets;
} }
public PropertyDefinition(PropertyDefinitionPersist persist){ public PropertyDefinition(PropertyDefinitionPersist persist){

View File

@ -13,7 +13,7 @@ public class PropertyDefinitionFieldSet {
private final List<PropertyDefinitionFieldSetItem> items; private final List<PropertyDefinitionFieldSetItem> items;
public List<PropertyDefinitionFieldSetItem> getItems() { public List<PropertyDefinitionFieldSetItem> getItems() {
return items; return this.items;
} }
public PropertyDefinitionFieldSet(PropertyDefinitionFieldSetPersist persist){ public PropertyDefinitionFieldSet(PropertyDefinitionFieldSetPersist persist){

View File

@ -15,12 +15,12 @@ public class PropertyDefinitionFieldSetItem {
private final Integer ordinal; private final Integer ordinal;
public Map<String, Field> getFields() { public Map<String, Field> getFields() {
return fields; return this.fields;
} }
public Integer getOrdinal() { public Integer getOrdinal() {
return ordinal; return this.ordinal;
} }

View File

@ -14,6 +14,7 @@ public class VisibilityServiceImpl implements VisibilityService {
private final PropertyDefinition propertyDefinition; private final PropertyDefinition propertyDefinition;
private Map<String, List<RuleWithTarget>> rulesBySources; private Map<String, List<RuleWithTarget>> rulesBySources;
private Map<String, List<RuleWithTarget>> rulesByTarget;
private Map<FieldKey, Boolean> visibility; private Map<FieldKey, Boolean> visibility;
public VisibilityServiceImpl(DefinitionEntity definition, PropertyDefinitionPersist propertyDefinition) { public VisibilityServiceImpl(DefinitionEntity definition, PropertyDefinitionPersist propertyDefinition) {
@ -27,14 +28,18 @@ public class VisibilityServiceImpl implements VisibilityService {
} }
private void initRules(){ private void initRules(){
if (this.rulesBySources != null) return; if (this.rulesBySources != null && this.rulesByTarget != null) return;
this.rulesBySources = new HashMap<>(); this.rulesBySources = new HashMap<>();
this.rulesByTarget = new HashMap<>();
for (FieldEntity fieldEntity : this.definition.getAllField()){ for (FieldEntity fieldEntity : this.definition.getAllField()){
if (fieldEntity.getVisibilityRules() != null && !fieldEntity.getVisibilityRules().isEmpty()) { if (fieldEntity.getVisibilityRules() != null && !fieldEntity.getVisibilityRules().isEmpty()) {
for (RuleEntity rule : fieldEntity.getVisibilityRules()){ for (RuleEntity rule : fieldEntity.getVisibilityRules()){
if (!this.rulesBySources.containsKey(fieldEntity.getId())) this.rulesBySources.put(fieldEntity.getId(), new ArrayList<>()); if (!this.rulesBySources.containsKey(fieldEntity.getId())) this.rulesBySources.put(fieldEntity.getId(), new ArrayList<>());
RuleWithTarget ruleWithTarget = new RuleWithTarget(fieldEntity.getId(), rule, fieldEntity); RuleWithTarget ruleWithTarget = new RuleWithTarget(fieldEntity.getId(), rule, fieldEntity);
this.rulesBySources.get(fieldEntity.getId()).add(ruleWithTarget); 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)){ if (rule.getSource().equals(key)){
Field field = definitionFieldSetItem.getFields().get(key); Field field = definitionFieldSetItem.getFields().get(key);
List<RuleWithTarget> 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 if (definitionFieldSetItem.getFields().containsKey(rule.getTarget())){ //Rule applies only for current multiple item
FieldKey fieldKey = new FieldKey(rule.getTarget(), definitionFieldSetItem.getOrdinal()); FieldKey fieldKey = new FieldKey(rule.getTarget(), definitionFieldSetItem.getOrdinal());
boolean currentState = this.visibility.getOrDefault(fieldKey, false); 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 } 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<Integer> ordinals = this.getKeyOrdinals(rule.getTarget()); List<Integer> ordinals = this.getKeyOrdinals(rule.getTarget());
for (Integer ordinal : ordinals){ for (Integer ordinal : ordinals){
FieldKey fieldKey = new FieldKey(rule.getTarget(), ordinal); FieldKey fieldKey = new FieldKey(rule.getTarget(), ordinal);
boolean currentState = this.visibility.getOrDefault(fieldKey, false); 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 { } else {
FieldKey fieldKey = new FieldKey(rule.getTarget(), null); //Ordinal is null if target not on field FieldKey fieldKey = new FieldKey(rule.getTarget(), null); //Ordinal is null if target not on field
boolean currentState = this.visibility.getOrDefault(fieldKey, false); 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)));
} }
} }
} }
@ -103,6 +111,65 @@ public class VisibilityServiceImpl implements VisibilityService {
} }
} }
private boolean isChainParentVisible(List<RuleWithTarget> rulesForParentKey, Map<String, Field> fieldsMap, int ordinal) {
boolean isVisible = false;
if (rulesForParentKey == null || rulesForParentKey.isEmpty()) return false;
for (RuleWithTarget ruleForParentKey : rulesForParentKey) {
Field field = fieldsMap.get(ruleForParentKey.getSource());
List<RuleWithTarget> 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<Integer> 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<RuleWithTarget> 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<String, Field> getKeyFields(String key, int ordinal){
if (this.propertyDefinition.getFieldSets() != null && !this.propertyDefinition.getFieldSets().isEmpty()){
for (Map.Entry<String, PropertyDefinitionFieldSet> 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<Integer> getKeyOrdinals(String key){ private List<Integer> getKeyOrdinals(String key){
if (this.propertyDefinition.getFieldSets() != null && !this.propertyDefinition.getFieldSets().isEmpty()){ if (this.propertyDefinition.getFieldSets() != null && !this.propertyDefinition.getFieldSets().isEmpty()){
for (Map.Entry<String, PropertyDefinitionFieldSet> propertyDefinitionFieldSet: this.propertyDefinition.getFieldSets().entrySet()) { for (Map.Entry<String, PropertyDefinitionFieldSet> propertyDefinitionFieldSet: this.propertyDefinition.getFieldSets().entrySet()) {

View File

@ -1,4 +1,4 @@
import { Injectable } from '@angular/core'; import { Injectable, booleanAttribute } from '@angular/core';
import { AbstractControl } from '@angular/forms'; import { AbstractControl } from '@angular/forms';
import { Observable, Subject } from 'rxjs'; import { Observable, Subject } from 'rxjs';
import { DescriptionTemplateDefinition, DescriptionTemplateField, DescriptionTemplateFieldSet, DescriptionTemplatePage, DescriptionTemplateSection } from '@app/core/model/description-template/description-template'; 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 { export class VisibilityRulesService {
private form: AbstractControl; private form: AbstractControl;
private definition: DescriptionTemplateDefinition; private definition: DescriptionTemplateDefinition;
private rulesBySources: Map<String, RuleWithTarget[]> ; private rulesBySources: Map<string, RuleWithTarget[]> ;
private rulesByTarget: Map<string, RuleWithTarget[]> ;
public isVisibleMap: { [key: string]: boolean } = {}; public isVisibleMap: { [key: string]: boolean } = {};
private _isVisibleMap: { [key: string]: boolean } = null; private _isVisibleMap: { [key: string]: boolean } = null;
@ -36,6 +37,7 @@ export class VisibilityRulesService {
this.allDescriptionTemplateFields = null; this.allDescriptionTemplateFields = null;
this.allDescriptionTemplateFieldSets = null; this.allDescriptionTemplateFieldSets = null;
this.rulesBySources = null; this.rulesBySources = null;
this.rulesByTarget = null;
this._isVisibleMap = null; this._isVisibleMap = null;
this.calculateVisibility(); this.calculateVisibility();
} }
@ -65,6 +67,7 @@ export class VisibilityRulesService {
public reloadVisibility() { public reloadVisibility() {
this.rulesBySources = null; this.rulesBySources = null;
this.rulesByTarget = null;
this._isVisibleMap = null; this._isVisibleMap = null;
this.calculateVisibility(); this.calculateVisibility();
} }
@ -88,8 +91,9 @@ export class VisibilityRulesService {
private initRules(){ private initRules(){
if (this.definition == null || this.form == null) return; 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.rulesBySources = new Map();
this.rulesByTarget = new Map();
const fields: DescriptionTemplateField[] = this.getAllDescriptionTemplateDefinitionFields(this.definition); const fields: DescriptionTemplateField[] = this.getAllDescriptionTemplateDefinitionFields(this.definition);
for (let i = 0; i < fields.length; i++) { 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, []); if (!this.rulesBySources.has(fieldEntity.id)) this.rulesBySources.set(fieldEntity.id, []);
const ruleWithTarget: RuleWithTarget = new RuleWithTarget(fieldEntity.id, rule, fieldEntity); const ruleWithTarget: RuleWithTarget = new RuleWithTarget(fieldEntity.id, rule, fieldEntity);
this.rulesBySources.get(fieldEntity.id).push(ruleWithTarget); 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)); const fieldsMap = new Map(Object.entries(definitionFieldSetItem.fields));
fieldsMap.forEach((field: DescriptionFieldPersist, key: string) => { fieldsMap.forEach((field: DescriptionFieldPersist, key: string) => {
if (rule.source == key){ 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 if (fieldsMap.has(rule.target)){ //Rule applies only for current multiple item
const fieldKey = this.buildVisibilityKey(rule.target, definitionFieldSetItem.ordinal); const fieldKey = this.buildVisibilityKey(rule.target, definitionFieldSetItem.ordinal);
const currentState = this._isVisibleMap[fieldKey] ?? false; const currentState = this._isVisibleMap[fieldKey] ?? false;
this._isVisibleMap[fieldKey] = currentState || this.ruleIsTrue(rule, field); this._isVisibleMap[fieldKey] = parentIsVisible && (currentState || this.ruleIsTrue(rule, field));
//console.log(fieldKey + " " + this._isVisibleMap[fieldKey] + " " + field.textListValue);
} 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 } 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); const ordinals: number[] = this.getKeyOrdinals(rule.target, propertyDefinition);
for (let k = 0; k < ordinals.length; k++) { for (let k = 0; k < ordinals.length; k++) {
const ordinal = ordinals[j]; const ordinal = ordinals[k];
const fieldKey = this.buildVisibilityKey(rule.target, ordinal); const fieldKey = this.buildVisibilityKey(rule.target, ordinal);
const currentState = this._isVisibleMap[fieldKey] ?? false; const currentState = this._isVisibleMap[fieldKey] ?? false;
this._isVisibleMap[fieldKey] = currentState || this.ruleIsTrue(rule, field); this._isVisibleMap[fieldKey] = parentIsVisible && (currentState || this.ruleIsTrue(rule, field));
} }
} else { } else {
const fieldKey = this.buildVisibilityKey(rule.target, null); //Ordinal is null if target not on field const fieldKey = this.buildVisibilityKey(rule.target, null); //Ordinal is null if target not on field
const currentState = this._isVisibleMap[fieldKey] ?? false; 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<string, DescriptionFieldPersist>, 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[]{ private getKeyOrdinals(key: string, propertyDefinition: DescriptionPropertyDefinitionPersist): number[]{
let ordinals = []; let ordinals = [];
if (propertyDefinition.fieldSets != null) { if (propertyDefinition.fieldSets != null) {
@ -267,6 +316,30 @@ export class VisibilityRulesService {
return ordinals; return ordinals;
} }
private getKeyFields(key: string, ordinal: number, propertyDefinition: DescriptionPropertyDefinitionPersist): Map<string, DescriptionFieldPersist>{
let fields: Map<string, DescriptionFieldPersist>;
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<string, DescriptionFieldPersist>(Object.entries(fields)) : new Map<string, DescriptionFieldPersist>();
}
private ruleIsTrue(rule: RuleWithTarget, field: DescriptionFieldPersist) :boolean{ private ruleIsTrue(rule: RuleWithTarget, field: DescriptionFieldPersist) :boolean{
if (field != null){ if (field != null){
const fieldType: DescriptionTemplateFieldType = rule.field != null && rule.field.data != null ? rule.field.data.fieldType : DescriptionTemplateFieldType.FREE_TEXT; const fieldType: DescriptionTemplateFieldType = rule.field != null && rule.field.data != null ? rule.field.data.fieldType : DescriptionTemplateFieldType.FREE_TEXT;