2023-12-20 08:20:38 +01:00
import { ApplicationRef , Injectable , NgZone } from '@angular/core' ;
import { AbstractControl , UntypedFormArray , UntypedFormGroup } from '@angular/forms' ;
import { isNumeric } from '@app/utilities/enhancers/utils' ;
import { Observable , Subject } from 'rxjs' ;
import { VisibilityRule } from './models/visibility-rule' ;
import { VisibilityRuleSource } from './models/visibility-rule-source' ;
import { VisibilityRulesContext } from './models/visibility-rules-context' ;
2024-01-31 20:16:39 +01:00
import { DescriptionTemplate , DescriptionTemplatePage , DescriptionTemplateSection } from '@app/core/model/description-template/description-template' ;
2023-12-29 16:04:16 +01:00
import { Rule } from './models/rule' ;
2023-12-20 08:20:38 +01:00
@Injectable ( )
export class VisibilityRulesService {
private readonly VISIBILITY_RULE_LOGIC : 'OR' | 'AND' = 'OR' ;
private readonly DEFAULTVISIBILITY = false ;
private visibilityRuleContext : VisibilityRulesContext ;
private form : AbstractControl ;
2023-12-28 16:18:49 +01:00
2024-01-31 20:16:39 +01:00
public isVisibleMap : { [ key : string ] : boolean } = { } ;
2023-12-28 16:18:49 +01:00
private elementVisibilityMapSubject = new Subject < { [ key : string ] : boolean } > ( ) ;
2023-12-20 08:20:38 +01:00
private elementComputationalMap = new Map < String , Map < String , boolean > > ( ) ; /// keep saved the values of each form control validity value
private _changeMade $ = new Subject < void > ( ) ;
2023-12-28 16:18:49 +01:00
// get isVisibleMap(): MapWithDefault {
// // console.log('isVisibleMap');
// return this.elementVisibilityMap;
2023-12-20 08:20:38 +01:00
2023-12-28 16:18:49 +01:00
// }; /// keep saved the values of each form control validity value
2023-12-20 08:20:38 +01:00
constructor (
public applicationReference : ApplicationRef ,
public ngZone : NgZone
) {
}
2023-12-28 16:18:49 +01:00
getElementVisibilityMapObservable ( ) : Observable < { [ key : string ] : boolean } > {
// this.isVisibleMap
// console.log('getElementVisibilityMapObservable: ');
2023-12-20 08:20:38 +01:00
return this . elementVisibilityMapSubject . asObservable ( ) ;
}
public checkElementVisibility ( id : string ) : boolean {
2024-01-19 13:28:31 +01:00
//console.log('checkElementVisibility: ' + id);
2023-12-20 08:20:38 +01:00
return true ;
// if (this.visibilityRuleContext.rules.filter(item => item.targetControlId === id).length === 0) { return true; }
// console.log(this.elementVisibilityMap.has(id) ? this.elementVisibilityMap.get(id) : false);
// return this.elementVisibilityMap.has(id) ? this.elementVisibilityMap.get(id) : false;
}
public buildVisibilityRules ( item : Array < Rule > , form : AbstractControl ) {
this . visibilityRuleContext = new VisibilityRulesContext ( ) ;
this . visibilityRuleContext . buildVisibilityRuleContext ( item || [ ] ) ;
this . form = form ;
this . resetVisibilityRules ( ) ;
}
public updateValueAndVisibility ( id : string , value : any ) {
2024-01-19 13:28:31 +01:00
//console.log('updateValueAndVisibility: ' + id + ' value: ' + value);
2023-12-20 08:20:38 +01:00
const visibilityRules = this . visibilityRuleContext . rules . filter ( item = > item . sourceVisibilityRules . filter ( source = > source . sourceControlId === id ) . length > 0 ) ;
if ( visibilityRules . length > 0 ) {
visibilityRules . forEach ( item = > this . evaluateVisibility ( item , value , id ) ) ;
2023-12-28 16:18:49 +01:00
this . elementVisibilityMapSubject . next ( this . isVisibleMap ) ;
2023-12-20 08:20:38 +01:00
}
}
private evaluateVisibility ( visibilityRule : VisibilityRule , value : any , sourceId : string ) { // source controlId is the same
2024-01-19 13:28:31 +01:00
//console.log('evaluateVisibility: ' + visibilityRule + ' value: ' + value + ' sourceId: ' + sourceId);
2023-12-20 08:20:38 +01:00
const targetId = visibilityRule . targetControlId ;
const visibilityMap = this . elementComputationalMap . get ( targetId ) ? this . elementComputationalMap . get ( targetId ) : new Map < String , boolean > ( ) ;
if ( value instanceof Array ) {
const parsedSourceControlValues = visibilityRule . sourceVisibilityRules . map ( e = > this . parseValue ( e . sourceControlValue ) ) ;
const parsedValues = value . map ( e = > this . parseValue ( e ) ) ;
const isVisible = parsedValues . map ( v = > parsedSourceControlValues . includes ( v ) ) . reduce ( ( acc , current ) = > acc || current , false ) ;
// if(isVisible){
// this._emitChangesIfNeeded(visibilityRule.targetControlId, true);
// this.elementVisibilityMap.set(visibilityRule.targetControlId, true);
// return;
// }
visibilityMap . set ( sourceId , isVisible ) ;
} else {
const visibilityDependencySource = visibilityRule . sourceVisibilityRules . filter ( x = > x . sourceControlId === sourceId ) ;
const shouldBeVisible = visibilityDependencySource . reduce ( ( isVisible , x ) = > {
const shouldBeHidden = value !== null && ( this . parseValue ( value ) !== this . parseValue ( x . sourceControlValue ) ) ;
return this . VISIBILITY_RULE_LOGIC === 'OR' ? ( isVisible || ! shouldBeHidden ) : ( isVisible && ! shouldBeHidden ) ;
// if(value !== null && )
} , this . VISIBILITY_RULE_LOGIC === 'AND' ) ;
visibilityMap . set ( sourceId , shouldBeVisible ) ;
}
this . elementComputationalMap . set ( targetId , visibilityMap ) ; // unnessecary
const isVisible = this . _computeVisibility ( targetId ) ;
this . _emitChangesIfNeeded ( targetId , isVisible ) ;
2023-12-28 16:18:49 +01:00
const previousVisibility = this . isVisibleMap [ targetId ] ;
this . isVisibleMap [ targetId ] = isVisible ;
2023-12-20 08:20:38 +01:00
if ( ! isVisible && previousVisibility !== isVisible ) {
this . resetControlWithId ( this . form , targetId ) ;
}
// for (let i = 0; i < visibilityRule.sourceVisibilityRules.length; i++) {
// if (value != null && (this.parseValue(value) !== this.parseValue(visibilityRule.sourceVisibilityRules[i].sourceControlValue))) {
// this._emitChangesIfNeeded(visibilityRule.targetControlId, false);
// this.elementVisibilityMap.set(visibilityRule.targetControlId, false);
// this.resetControlWithId(this.form, visibilityRule.targetControlId);
// //this.updateValueAndVisibility(visibilityRule.targetControlId, null);
// // this.clearValues(targetPathKey);
// return;
// }
// }
// this._emitChangesIfNeeded(visibilityRule.targetControlId, true);
// this.elementVisibilityMap.set(visibilityRule.targetControlId, true);
// this.updateValueAndVisibility(visibilityRule.targetControlId, null);
}
private _computeVisibility ( targetId : string ) : boolean {
2024-01-19 13:28:31 +01:00
//console.log('_computeVisibility: ' + targetId);
2023-12-20 08:20:38 +01:00
const visibilityMap = this . elementComputationalMap . get ( targetId ) ;
const values = visibilityMap . values ( ) ;
let currentVal = values . next ( ) ;
let visibilityValues : boolean [ ] = [ ] ;
while ( ! currentVal . done ) {
visibilityValues . push ( currentVal . value ) ;
currentVal = values . next ( ) ;
}
if ( visibilityValues . length ) {
return visibilityValues . reduce ( ( r , c ) = > {
if ( this . VISIBILITY_RULE_LOGIC === 'OR' ) {
return r || c ;
} else {
return r && c ;
}
} , visibilityValues [ 0 ] ) ;
}
return this . DEFAULTVISIBILITY ;
}
private resetVisibilityRules() {
2024-01-19 13:28:31 +01:00
//console.log('resetVisibilityRules: ');
2023-12-20 08:20:38 +01:00
2023-12-28 16:18:49 +01:00
this . isVisibleMap = { } ;
2023-12-20 08:20:38 +01:00
this . elementComputationalMap . clear ( ) ;
this . elementComputationalMap = new Map < String , Map < String , boolean > > ( ) ;
this . _populateComputationMap ( ) ; /// !IMPORTANT FOR THE AND LOGIC
this . _changeMade $ . next ( ) ;
}
private _populateComputationMap ( ) : void {
2024-01-19 13:28:31 +01:00
//console.log('_populateComputationMap: ');
2023-12-28 16:18:49 +01:00
// this.visibilityRuleContext.rules.forEach(rule => {
// const targetId = rule.targetControlId;
// const visibilityMap = this.elementComputationalMap.get(targetId) ? this.elementComputationalMap.get(targetId) : new Map<String, boolean>();
// rule.sourceVisibilityRules.forEach(vr => {
// visibilityMap.set(vr.sourceControlId, this.DEFAULTVISIBILITY);
// });
// this.elementComputationalMap.set(targetId, visibilityMap);
// });
2023-12-20 08:20:38 +01:00
}
parseValue ( value : any ) {
if ( typeof value === 'string' ) {
if ( isNumeric ( value ) ) { return value ; }
else if ( value === 'true' ) {
return true ;
}
else if ( value === 'false' ) {
return false ;
}
else { return this . translate ( value ) ; }
} else { return value ; }
}
search ( path , obj , target ) {
for ( const k in obj ) {
if ( obj . hasOwnProperty ( k ) ) {
if ( obj [ k ] === target ) {
return path + '.' + k ;
} else if ( typeof obj [ k ] === 'object' ) {
const result = this . search ( path + '.' + k , obj [ k ] , target ) ;
if ( result ) {
return result ;
}
}
}
}
return false ;
}
scanIfChildsOfCompositeFieldHasVisibleItems ( compositeFieldParent : UntypedFormGroup ) : boolean {
// console.log('scanIfChildsOfCompositeFieldHasVisibleItems: ' + compositeFieldParent);
//TODO: implement this
return true ;
// let isVisible = false;
// (<UntypedFormArray>(compositeFieldParent.get('fields'))).controls.forEach(element => {
// if (this.checkElementVisibility(element.get('id').value)) {
// isVisible = true;
// }
// });
// return isVisible;
}
private translate ( item : any ) {
try {
return JSON . parse ( item ) . value ;
} catch ( error ) {
return item ;
}
}
private resetControlWithId ( formControl : AbstractControl , id : string ) {
2024-01-19 13:28:31 +01:00
//'resetControlWithId: ' + id);
2023-12-20 08:20:38 +01:00
//TODO: implement this
// if (formControl instanceof UntypedFormGroup) {
// if ((formControl as UntypedFormGroup).contains('id') && (formControl as UntypedFormGroup).contains('value') && (formControl as UntypedFormGroup).get('id').value === id) {
// this.resetFieldFormGroup(formControl);
// } if ((formControl as UntypedFormGroup).contains('id') && (formControl as UntypedFormGroup).contains('fields') && (formControl as UntypedFormGroup).get('id').value === id) {
// this.resetCompositeFieldFormGroup(formControl);
// } else {
// Object.keys(formControl.controls).forEach(item => {
// const control = formControl.get(item);
// this.resetControlWithId(control, id);
// });
// }
// } else if (formControl instanceof UntypedFormArray) {
// formControl.controls.forEach(item => {
// this.resetControlWithId(item, id);
// });
// }
}
private resetFieldFormGroup ( formGroup : UntypedFormGroup ) {
2024-01-19 13:28:31 +01:00
//console.log('resetFieldFormGroup: ' + formGroup);
2023-12-20 08:20:38 +01:00
//TODO: implement this
// const renderStyle = formGroup.getRawValue().viewStyle.renderStyle;
// if (renderStyle === DatasetProfileFieldViewStyle.Validation || renderStyle === DatasetProfileFieldViewStyle.DatasetIdentifier) {
// formGroup.get('value').setValue({ identifier: '', type: '' });
// } else {
// formGroup.get('value').setValue(formGroup.get('defaultValue').value ? this.parseValue(formGroup.get('defaultValue').value.value) : undefined);
// }
}
private resetCompositeFieldFormGroup ( formGroup : UntypedFormGroup ) {
2024-01-19 13:28:31 +01:00
//console.log('resetCompositeFieldFormGroup: ' + formGroup);
2023-12-20 08:20:38 +01:00
//TODO: implement this
// (formGroup.get('fields') as UntypedFormArray).controls.forEach((element: UntypedFormGroup) => {
// this.resetFieldFormGroup(element);
// });
// (formGroup.get('multiplicityItems') as UntypedFormArray).controls.splice(0);
}
private _emitChangesIfNeeded ( id : string , valueToBeSet : boolean ) {
2023-12-28 16:18:49 +01:00
if ( this . isVisibleMap [ id ] ) {
if ( this . isVisibleMap [ id ] != valueToBeSet ) {
2023-12-20 08:20:38 +01:00
this . _changeMade $ . next ( ) ;
}
} else {
this . _changeMade $ . next ( ) ;
}
}
public get visibilityChange() {
return this . _changeMade $ . asObservable ( ) ;
}
public getVisibilityDependency ( targetId : string ) : VisibilityRuleSource [ ] | null {
2024-01-19 13:28:31 +01:00
//console.log('getVisibilityDependency: ' + targetId);
2023-12-20 08:20:38 +01:00
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 [ ] {
console . log ( 'getVisibilityTargets: ' + sourceId ) ;
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 removeAllIdReferences ( id : string ) : void {
2024-01-19 13:28:31 +01:00
//console.log('removeAllIdReferences: ' + id);
2023-12-20 08:20:38 +01:00
// * Remove from visibility rues and visibility rules context
//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 ) ) ;
2023-12-28 16:18:49 +01:00
this . isVisibleMap [ id ] = undefined ;
2023-12-20 08:20:38 +01:00
//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 ) ;
} ) ;
// * Remove from computational map
// as a target
if ( this . elementComputationalMap . get ( id ) ) {
this . elementComputationalMap . delete ( id ) ;
}
// as a source
const keyIterator = this . elementComputationalMap . keys ( ) ;
let currentKey = keyIterator . next ( ) ;
while ( ! currentKey . done ) {
const currentVals = this . elementComputationalMap . get ( currentKey . value ) ;
currentVals . delete ( id ) ;
currentKey = keyIterator . next ( ) ;
}
}
public addNewRule ( rule : Rule , currentVisibility = this . DEFAULTVISIBILITY ) : void {
2024-01-19 13:28:31 +01:00
//console.log('addNewRule: ' + rule + ' currentVisibility: ' + currentVisibility);
2023-12-20 08:20:38 +01:00
const targetId = rule . targetField ;
const sourceId = rule . sourceField ;
this . visibilityRuleContext . addToVisibilityRulesContext ( rule ) ;
let visibilityMap = this . elementComputationalMap . get ( targetId ) ;
if ( ! visibilityMap ) {
visibilityMap = new Map < String , boolean > ( ) ;
this . elementComputationalMap . set ( targetId , visibilityMap ) ;
}
visibilityMap . set ( sourceId , currentVisibility ) ;
const isVisible = this . _computeVisibility ( targetId ) ;
this . _emitChangesIfNeeded ( targetId , isVisible ) ;
2023-12-28 16:18:49 +01:00
this . isVisibleMap [ targetId ] = isVisible ;
this . elementVisibilityMapSubject . next ( this . isVisibleMap ) ;
2023-12-20 08:20:38 +01:00
}
/ * *
* Check what sourceId hides or shows the target field
* return true if no rule found
* /
public checkTargetVisibilityProvidedBySource ( sourceId : string , targetId : string ) : boolean {
2024-01-19 13:28:31 +01:00
//console.log('checkTargetVisibilityProvidedBySource: ' + sourceId + ' targetId: ' + targetId);
2023-12-20 08:20:38 +01:00
const computationalMap = this . elementComputationalMap . get ( targetId ) ;
if ( computationalMap ) {
return ! ! computationalMap . get ( sourceId ) ;
}
return true ;
}
public getVisibilityRulesFromDescriptionTempalte ( descriptionTemplate : DescriptionTemplate ) : Rule [ ] {
2024-01-19 13:28:31 +01:00
//console.log('getVisibilityRulesFromDescriptionTempalte: ' + descriptionTemplate);
2024-01-31 20:16:39 +01:00
const result : Rule [ ] = this . getVisibilityRulesFromDescriptionTempalteSections ( descriptionTemplate ? . definition ? . pages ) ;
2023-12-20 08:20:38 +01:00
return result ;
}
2024-01-31 20:16:39 +01:00
public getVisibilityRulesFromDescriptionTempalteSections ( pages : DescriptionTemplatePage [ ] ) : Rule [ ] {
2024-01-19 13:28:31 +01:00
//console.log('getVisibilityRulesFromDescriptionTempalteSections: ' + sections);
2023-12-20 08:20:38 +01:00
const result : Rule [ ] = [ ] ;
2024-01-31 20:16:39 +01:00
pages . forEach ( page = > {
page ? . sections ? . forEach ( section = > {
if ( section . sections != null ) { result . push ( . . . this . getVisibilityRulesFromDescriptionTempalteSections ( section . sections ) ) ; } ;
section ? . fieldSets ? . forEach ( fieldSet = > {
fieldSet ? . fields ? . forEach ( field = > {
field . visibilityRules ? . forEach ( visibilityRule = > {
result . push ( {
sourceField : field.id.toString ( ) ,
targetField : visibilityRule.target ,
requiredValue : visibilityRule.value
} )
} ) ;
2023-12-20 08:20:38 +01:00
} ) ;
} ) ;
} ) ;
} ) ;
return result ;
}
}
class MapWithDefault extends Map < string , boolean > {
get ( key ) {
2024-01-19 13:28:31 +01:00
//console.log('MapWithDefault');
2023-12-20 08:20:38 +01:00
if ( ! this . has ( key ) ) {
this . set ( key , true ) ;
}
return super . get ( key ) ;
}
}