import { Component, OnDestroy, OnInit } from '@angular/core'; import { AbstractControl, Validators } from '@angular/forms'; import { ActivatedRoute, Router } from '@angular/router'; import { SpinnerService } from '@core/services/spinner.service'; import { FooterState } from '@core/states/footer.state'; import { AutomationAPI } from '@core/typings/api/automation.typing'; import { WorkflowLevelAdvancementAPI } from '@core/typings/api/workflow-level-advancement.typing'; import { WorkflowLevelAutomationAPI } from '@core/typings/api/workflow-level-automation.typing'; import { Automation } from '@core/typings/ui/automation.typing'; import { WorkflowLevelAutomationUI } from '@core/typings/ui/workflow-level-automation.typing'; import { AutomationSubLevel, Sublevel, Workflow, WorkflowLevel } from '@core/typings/workflow.typing'; import { FormLogicService } from '@features/formio/services/form-logic/form-logic.service'; import { ReferenceFieldsService } from '@features/reference-fields/services/reference-fields.service'; import { RuleAutomationService } from '@features/rule-automation/rule-automation.service'; import { WorkflowService } from '@features/workflow/workflow.service'; import { ArrayHelpersService, FooterStates, SimpleStringMap, TypeaheadSelectOption, TypeSafeFormBuilder, TypeSafeFormGroup } from '@yourcause/common'; import { I18nService } from '@yourcause/common/i18n'; import { Subscription } from 'rxjs'; import { WFLSaveSwitch, WorkflowLevelAutomationService } from '../workflow-level-automation.service'; @Component({ selector: 'gc-create-edit-rules-page', templateUrl: './create-edit-rule-page.component.html', styleUrls: ['./create-edit-rule-page.component.scss'] }) export class CreateEditRulePageComponent implements OnInit, OnDestroy { id = this.activatedRoute.snapshot.params.id; objects: Automation.ObjectConfig[]; isNew = !this.id; isAdvancement: T = !!this.activatedRoute.snapshot.data.isAdvancement as T; detail = this.workflowLevelAutomationService[this.isAdvancement ? 'workflowAdvancementDetail' : 'workflowAutomationDetail' ][this.id]; title = ''; uiRules: Automation.CriteriaFormState[]; workflowOptions = this.arrayHelper.sort(this.workflowService.get('workflows') .map(workflow => >({ label: workflow.name, value: workflow })), 'label'); levelOptionMap = this.workflowOptions.reduce((acc, { value }) => { return { ...acc, [value.id]: value.levels.reduce((_acc, level) => [ ..._acc, { label: level.name, value: level }, ...level.subLevels.map((subLevel: AutomationSubLevel) => { subLevel.parent = level; return { label: `${level.name} / ${subLevel.name}`, value: subLevel }; }) ], []) }; }, {} as SimpleStringMap[]>); levelOptions: TypeaheadSelectOption[] = []; destinationLevelOptions: TypeaheadSelectOption[] = []; formOptions: TypeaheadSelectOption[]; formGroup: TypeSafeFormGroup; sub: Subscription; rulesValid = true; initialSelectedWflId: number; ready = false; applyRulesWithOr = false; constructor ( private spinnerService: SpinnerService, private automationService: RuleAutomationService, private router: Router, private workflowLevelAutomationService: WorkflowLevelAutomationService, private activatedRoute: ActivatedRoute, private workflowService: WorkflowService, private formBuilder: TypeSafeFormBuilder, private footerState: FooterState, private formLogicService: FormLogicService, private i18n: I18nService, private arrayHelper: ArrayHelpersService, private referenceFieldService: ReferenceFieldsService ) { this.footerState.set('footerState', FooterStates.ACTION); this.footerState.set('footerActionLabel', i18n.translate('common:btnSave')); this.footerState.set('footerAction', this.saveRuleSet); this.footerState.set('footerCancelLabel', i18n.translate('common:btnCancel')); this.footerState.set('footerCancelAction', () => { this.navigateBack(); }); } async ngOnInit () { this.spinnerService.startSpinner(); if (!this.isNew) { if (this.detail) { this.applyRulesWithOr = this.detail.applyRulesWithOr; this.initialSelectedWflId = this.isAdvancement ? (this.detail as WorkflowLevelAdvancementAPI.WorkflowLevelRoutingAutomationRuleSetDetailModel).routeToWorkflowLevelId : this.detail.workflowLevelId; } await this.mapExistingRules(); } else { await this.setForms(); } if (this.isNew) { this.title = this.i18n.translate( this.isAdvancement ? 'CONFIG:hdrCreateRoutingRules' : 'CONFIG:hdrCreateAssignmentRules' ); } else { this.title = this.i18n.translate( this.isAdvancement ? 'CONFIG:hdrEditRoutingRules' : 'CONFIG:hdrEditAssignmentRules' ); } this.ready = true; this.spinnerService.stopSpinner(); } ruleValidationChanged (valid: boolean) { this.rulesValid = valid; const invalid = !valid || this.formGroup.invalid; this.footerState.set( 'footerActionDisabled', invalid ); } async mapExistingRules () { await this.setFormOptions({ id: this.detail.workflowId }); const formOption = this.formOptions.find(option => { return option.value === this.detail.formId; }); await this.formChanged( formOption ? formOption.value : null ); this.setForms(); } getWorkflowOption () { if (this.detail) { const foundOption = this.workflowOptions.find(option => { return option.value.id === this.detail.workflowId; }); return foundOption ? foundOption.value : null; } return null; } getLevelOption () { if (this.detail) { const foundOption = this.levelOptions.find(option => { if (this.isAdvancement) { const selectedWfl = (this.detail as WorkflowLevelAdvancementAPI.WorkflowLevelRoutingAutomationRuleSetDetailModel).routeToWorkflowLevelId; return option.value.id === selectedWfl; } else { return option.value.id === this.detail.workflowLevelId; } }); return foundOption ? foundOption.value : null; } return null; } async setForms () { const formOption = this.detail ? this.formOptions.find(option => option.value === this.detail.formId) : null; const workflowOption = this.getWorkflowOption(); this.levelOptions = workflowOption ? this.arrayHelper.sort( this.levelOptionMap[workflowOption.id].filter((level) => { return !level.value.disabled || level.value.id === this.initialSelectedWflId; }), 'label' ) : []; let levelOption = this.getLevelOption(); let currentWorkflowLevelOption: WorkflowLevel = this.detail ? this.levelOptions[0].value : null; if (this.isAdvancement && this.detail) { const detail = ( this.detail as WorkflowLevelAdvancementAPI.WorkflowLevelRoutingAutomationRuleSetDetailModel ); const foundLevelOption = this.levelOptions.find((opt) => { return opt.value.id === detail.routeToWorkflowLevelId; }); levelOption = foundLevelOption ? foundLevelOption.value : null; await this.setDestinationLevelOptions(levelOption); const foundCurrentLevelOption = this.levelOptions.find(option => { return option.value.id === detail.workflowLevelId; }); currentWorkflowLevelOption = foundCurrentLevelOption ? foundCurrentLevelOption.value : null; } const currentLevelValidation = this.isAdvancement ? [Validators.required] : []; this.formGroup = this.formBuilder.group({ name: [this.detail ? this.detail.name : '', Validators.required], description: this.detail ? this.detail.description : '', form: [formOption ? formOption.value : null, (control: AbstractControl) => { // check if form is still in use on a program with this workflow if ( this.formOptions && !this.formOptions.some(option => option.value === control.value) ) { return { required: true }; } return null; }], workflow: [ workflowOption, [Validators.required], async (control: AbstractControl) => { await this.setFormOptions(control.value); // check if workflow is in use on a program if (!this.formOptions || !this.formOptions.length) { return { noProgramsWithWorkflow: { i18nKey: 'CONFIG:textNoProgramsAreUsingThisWorkflow', defaultValue: 'This workflow is not being used in any program' } }; } return null; } ], level: [levelOption, Validators.required], currentLevel: [ currentWorkflowLevelOption, currentLevelValidation, async (control: AbstractControl) => { if (this.isAdvancement) { await this.setDestinationLevelOptions(control.value); // check if level has routes if ( !this.destinationLevelOptions || !this.destinationLevelOptions.length ) { return { noRoutesOnLevel: { i18nKey: 'CONFIG:textNoRoutesForLevel', defaultValue: 'There are no routes for this workflow level' } }; } } return null; } ], isAny: false }); this.sub = this.formGroup.statusChanges.subscribe(() => { this.ruleValidationChanged(this.rulesValid); }); this.formChanged(this.detail ? this.detail.formId : null); } async setLevelOptions (workflow: Workflow) { const touched = this.formGroup.get('workflow').touched; if (this.isAdvancement) { if (touched) { this.formGroup.get('currentLevel').setValue(null); this.setDestinationLevelOptions(null); } await this.workflowService.getAndSetWorkflowMap(workflow.id); } if (touched) { this.formGroup.get('level').setValue(null); this.formGroup.get('form').setValue(null); this.setFormOptions(workflow); } this.levelOptions = this.arrayHelper.sort( this.levelOptionMap[workflow.id].filter((level) => { return !level.value.disabled || level.value.id === this.initialSelectedWflId; }), 'label' ); } async setDestinationLevelOptions (level: WorkflowLevel) { const touched = this.formGroup ? this.formGroup.get('currentLevel').touched : false; if (level && this.isAdvancement && this.formGroup) { const workflowId = this.formGroup.value.workflow.id; await this.workflowService.getAndSetWorkflowMap(workflowId); let detailedLevel: WorkflowLevel; this.workflowService.workflowMap[workflowId].levels.forEach(desLevel => { if (level.id === desLevel.id) { detailedLevel = desLevel; } desLevel.subLevels.forEach((sub) => { if (level.id === sub.id) { detailedLevel = sub; } }); }); if (detailedLevel) { this.destinationLevelOptions = this.arrayHelper.sort( detailedLevel.routes.filter((route) => { if (this.initialSelectedWflId === route.canRouteToWorkflowLevelId) { return true; } return !route.disabled; }).map(route => { return this.levelOptionMap[workflowId].find(option => { return option.value.id === route.canRouteToWorkflowLevelId; }); }), 'label' ); } else { this.destinationLevelOptions = []; } if (touched) { this.formGroup.get('level').setValue(null); } } else { this.destinationLevelOptions = []; } } async setFormOptions (workflow: { id: number }) { if (workflow) { await this.workflowLevelAutomationService.fetchWorkflowLevelForms(workflow.id); if (this.formGroup) { const foundFormOption = this.workflowLevelAutomationService .workflowForms[workflow.id] .find(form => form.id === this.formGroup.value.form); // wait until template receives new options to adapt the option setTimeout(() => { this.formGroup.get('form').setValue( foundFormOption ? foundFormOption.id : null ); }); } this.formOptions = this.arrayHelper.sort( this.workflowLevelAutomationService.workflowForms[workflow.id].map(form => { return { label: form.name, value: form.id }; }), 'label' ); } else { this.formOptions = []; } } async formChanged (displayFormId: number) { this.spinnerService.startSpinner(); const workflowObjects = this.workflowLevelAutomationService.getWorkflowObjects( this.isAdvancement ); if (displayFormId && (this.detail || this.formGroup)) { const currentWorkflowId = !this.formGroup && this.detail ? this.detail.workflowId : this.formGroup.value.workflow.id; const displayForm = this.workflowLevelAutomationService .workflowForms[currentWorkflowId] .find(workflowForm => workflowForm.id === displayFormId); const [ form, singleResponseRefIds ] = await Promise.all([ this.formLogicService.fetchFormDetail( displayForm.id, displayForm.currentRevisionId ), this.workflowLevelAutomationService.getSingleResponseGmFields( currentWorkflowId, displayForm.id ) ]); const gmFieldsObject = this.isAdvancement ? await this.referenceFieldService.adaptFieldIdsForAutomation( singleResponseRefIds ) : undefined; this.objects = [ ...workflowObjects, await this.automationService.formToRuleObject(form), gmFieldsObject ].filter((item) => !!item); } else { this.objects = [ ...workflowObjects ]; } this.spinnerService.stopSpinner(); } saveRuleSet = async () => { this.spinnerService.startSpinner(); this.footerState.set('footerActionDisabled', true); this.footerState.set('footerSecondaryDisabled', true); const formValue: WorkflowLevelAutomationUI.NewEditFormState = this.formGroup.value; const uiRules: Automation.CriteriaFormState[] = this.uiRules; let rules: AutomationAPI.SaveAutomationRuleSetExpressionModel[] = []; if (uiRules) { rules = uiRules .reduce((acc, rule) => [ ...acc, ...this.automationService.mapCriteriaToAPIRule( rule ) ], [] as AutomationAPI.SaveAutomationRuleSetExpressionModel[]); } const saveInfo = { description: formValue.description, workflowId: formValue.workflow.id, workflowLevelId: formValue.level.id, name: formValue.name, applyRulesWithOr: this.applyRulesWithOr, rules, formId: formValue.form } as WFLSaveSwitch; if (this.isAdvancement) { const info = saveInfo as WorkflowLevelAdvancementAPI.SaveWorkflowLevelRoutingAutomationRuleSet; info.workflowLevelRoutingAutomationRuleSetId = this.id; info.routeToWorkflowLevelId = formValue.level.id; info.workflowLevelId = formValue.currentLevel.id; } else { (saveInfo).workflowLevelAutomationRuleSetId = this.id; } const passed = await this.workflowLevelAutomationService.saveDetail( saveInfo, this.isAdvancement ); if (passed) { this.navigateBack(); } this.spinnerService.stopSpinner(); this.footerState.set('footerActionDisabled', false); this.footerState.set('footerSecondaryDisabled', false); }; rulesChanged (rules: Automation.CriteriaFormState[]) { this.uiRules = rules; } private navigateBack () { this.router.navigate( [`../../view-${this.isAdvancement ? 'advancement' : 'initial'}-rules`], { relativeTo: this.activatedRoute } ); } ngOnDestroy () { this.sub.unsubscribe(); this.workflowLevelAutomationService.clearWorkflowLevelForms(); this.automationService.clearFormRuleObjectMap(); this.footerState.clearAll(); } }