import { Component, ContentChild, EventEmitter, Input, OnDestroy, OnInit, Output, TemplateRef } from '@angular/core'; import { FormArray, FormGroup, Validators } from '@angular/forms'; import { AutomationAPI } from '@core/typings/api/automation.typing'; import { Automation } from '@core/typings/ui/automation.typing'; import { TypeaheadSelectOption, TypeSafeFormArray, TypeSafeFormBuilder, TypeSafeFormGroup } from '@yourcause/common'; import { I18nService } from '@yourcause/common/i18n'; import { Subscription } from 'rxjs'; import { RuleAutomationService } from '../rule-automation.service'; interface RuleIsAnyGroup { isAny: boolean; } export interface RuleGroup { object: Automation.ObjectConfig; column: Automation.ObjectColumnConfig; values: Automation.CriteriaValueFormState[]; isRemoved: boolean; ruleLabel: string; } @Component({ selector: 'gc-automation-rule', templateUrl: './automation-rule.component.html', styleUrls: ['./automation-rule.component.scss'] }) export class AutomationRuleComponent implements OnInit, OnDestroy { @Input() isAnyLabel = this.i18n.translate( 'CONFIG:lblSubmissionsWillBeRouted', {}, 'Submissions will be routed when' ); @Input() noRulesHeader: string; @Input() noRulesDesc: string; @Input() objects: Automation.ObjectConfig[]; @Input() rules: AutomationAPI.AutomationRuleSetExpressionBaseModel[] = []; @Input() requireOneRule: boolean; @Input() isModalView: boolean; @Input() applyRulesWithOr: boolean; @Input() canDeleteRuleset: boolean; @Output() onValidationChanged = new EventEmitter(); @Output() onEditRuleset = new EventEmitter(); @Output() onDeleteRuleset = new EventEmitter(); @Output() applyRulesWithOrChange = new EventEmitter(); @Output() uiRulesChanged = new EventEmitter(); @ContentChild('noRulesTemplate') noRulesTemplate: TemplateRef; uiRules: Automation.CriteriaFormState[] = []; sub: Subscription; rulesFormArray: TypeSafeFormArray; usedColumns: string[] = []; totalNonDeleted = 0; formGroup: TypeSafeFormGroup; isAnyOptions: TypeaheadSelectOption[] = [{ label: this.i18n.translate( 'GLOBAL:lblAny', {}, 'any' ), value: true // applyRulesWithOr for API }, { label: this.i18n.translate( 'GLOBAL:lblAll', {}, 'all' ), value: false }]; formArrayValid: boolean; showRuleSummary = true; hasRulesForDisplay: boolean; hasValidSummaryRule: boolean; constructor ( private formBuilder: TypeSafeFormBuilder, private automationService: RuleAutomationService, private i18n: I18nService ) { } ngOnInit () { const rules = this.automationService.mapAPIRulesToCriteria( this.rules || [], this.objects ); this.rulesFormArray = this.formBuilder.array( [], this.automationService.ruleArrayValidator() ); this.formGroup = this.formBuilder.group({ isAny: [this.applyRulesWithOr, Validators.required] }); this.sub = this.rulesFormArray.statusChanges.subscribe(() => { this.checkValidationState(); }); this.sub.add(this.rulesFormArray.valueChanges.subscribe((values) => { this.setHelpers(); this.uiRulesChanged.emit(values); })); rules.forEach((rule, index) => { if (rule.column) { this.usedColumns = [ ...this.usedColumns ]; this.usedColumns[index] = rule.object.key + '.' + rule.column.key; } return this.addRule(rule, true); }); this.checkValidationState(); this.setUiRules(rules); this.updateRuleLabels(); } updateRuleLabels () { let currentRuleNumber: number; this.uiRules.forEach((rule) => { if (!rule.isRemoved) { if (!currentRuleNumber) { currentRuleNumber = 1; } else { currentRuleNumber = currentRuleNumber + 1; } rule.ruleLabel = this.i18n.translate( 'common:textRuleNumber', { number: currentRuleNumber }, 'Rule __number__' ); } }); } private checkValidationState () { let invalid = false; this.totalNonDeleted = 0; for (let index = 0; index < this.rulesFormArray.length; index++) { const element = this.rulesFormArray.at(index); if (element.invalid) { invalid = true; } if (!element.value.isRemoved) { ++this.totalNonDeleted; } } this.formArrayValid = !invalid; if (this.requireOneRule) { invalid = invalid || this.totalNonDeleted === 0; } this.onValidationChanged.emit(!invalid); } columnChanged ( column: string, index: number ) { this.usedColumns = [ ...this.usedColumns ]; this.usedColumns[index] = column; } setUiRules (rules: Automation.CriteriaFormState[]) { this.uiRules = rules; this.setHelpers(); } setHelpers () { this.hasRulesForDisplay = this.uiRules.some((rule) => !rule.isRemoved); this.hasValidSummaryRule = this.rulesFormArray?.controls.some((control) => { return !!control.value?.column && !control.value?.isRemoved; }); } flushRemovedRules () { const rules = this.uiRules.filter((rule, index) => { if (!rule.isRemoved) { return true; } const shouldPurge = rule.isRemoved && ( !rule.values.length || rule.values.every((value) => { return !(value.greaterThanId || value.lessThanId || value.valueId); }) ); if (shouldPurge) { this.rulesFormArray.removeAt(index); } return !shouldPurge; }); this.setUiRules(rules); } addRule ( rule: Automation.CriteriaFormState = { object: null, column: null, isRemoved: false, values: [{ comparisonType: Automation.CriteriaComparisonTypes.Default, comparisonNegated: false, comparisonGreaterThan: null, comparisonLessThan: null, comparisonValue: null, isRemoved: false, greaterThanId: null, lessThanId: null, valueId: null }] }, isInit = false ) { if (this.objects.length === 1) { rule.object = this.objects[0]; } this.rulesFormArray.push( this.formBuilder.group({ object: [rule.object], column: [rule.column], values: this.formBuilder.array( rule.values.map(val => this.formBuilder.group(val)), this.automationService.ruleValueArrayValidator() ), isRemoved: false, ruleLabel: '' }, { validator: [ this.requiredIfNotRemoved() ] }) ); const rules = [ ...this.uiRules, rule ]; this.setUiRules(rules); this.flushRemovedRules(); this.updateRuleLabels(); if (!isInit) { setTimeout(() => { document.getElementById('scroll-to-target').scrollIntoView(); }); } } private requiredIfNotRemoved () { return (group: FormGroup) => { if (group.value.isRemoved) { return null; } for (const name of ['object', 'column']) { const control = group.get(name); if (!control.value) { return { [name]: { required: true } }; } } return null; }; } removeRule (index: number) { this.uiRules[index] = { ...this.uiRules[index], isRemoved: true, values: this.uiRules[index].values.map(val => ({ ...val, isRemoved: true })) }; this.usedColumns = [ ...this.usedColumns ]; this.usedColumns[index] = null; const ruleGroup = this.rulesFormArray.at(index); ruleGroup.get('isRemoved').setValue(true); const valueArray = ruleGroup.get('values') as FormArray; for (let i = 0; i < valueArray.length; i++) { valueArray.at(i).get('isRemoved').setValue(true); } this.flushRemovedRules(); this.updateRuleLabels(); } ngOnDestroy () { if (this.sub) { this.sub.unsubscribe(); } } }