diff --git a/projects/aca-folder-rules/src/lib/mock/actions.mock.ts b/projects/aca-folder-rules/src/lib/mock/actions.mock.ts index 88a992c9d..cbd3e7fc5 100644 --- a/projects/aca-folder-rules/src/lib/mock/actions.mock.ts +++ b/projects/aca-folder-rules/src/lib/mock/actions.mock.ts @@ -24,7 +24,7 @@ */ import { ActionDefinitionList } from '@alfresco/js-api'; -import { ActionDefinitionTransformed, ActionParameterDefinitionTransformed } from '../model/rule-action.model'; +import { ActionDefinitionTransformed, ActionParameterDefinitionTransformed, RuleAction } from '../model/rule-action.model'; export const actionDefListMock: ActionDefinitionList = { list: { @@ -43,7 +43,7 @@ export const actionDefListMock: ActionDefinitionList = { name: 'mock-action-parameter-text', type: 'd:text', multiValued: false, - mandatory: false, + mandatory: true, displayLabel: 'Mock action parameter text' }, { @@ -73,7 +73,7 @@ const actionParam1TransformedMock: ActionParameterDefinitionTransformed = { name: 'mock-action-parameter-text', type: 'd:text', multiValued: false, - mandatory: false, + mandatory: true, displayLabel: 'Mock action parameter text' }; @@ -106,3 +106,31 @@ const action2TransformedMock: ActionDefinitionTransformed = { }; export const actionsTransformedListMock: ActionDefinitionTransformed[] = [action1TransformedMock, action2TransformedMock]; + +export const validActionMock: RuleAction = { + actionDefinitionId: 'mock-action-1-definition', + params: { + 'mock-action-parameter-text': 'mock' + } +}; +export const nonExistentActionDefinitionIdMock: RuleAction = { + actionDefinitionId: 'non-existent-action-definition-id', + params: {} +}; +export const missingMandatoryParameterMock: RuleAction = { + actionDefinitionId: 'mock-action-1-definition', + params: {} +}; +export const incompleteMandatoryParameterMock: RuleAction = { + actionDefinitionId: 'mock-action-1-definition', + params: { + 'mock-action-parameter-text': '' + } +}; +export const validActionsMock: RuleAction[] = [ + validActionMock, + { + actionDefinitionId: 'mock-action-2-definition', + params: {} + } +]; diff --git a/projects/aca-folder-rules/src/lib/model/rule-action.model.ts b/projects/aca-folder-rules/src/lib/model/rule-action.model.ts index 8fbeacaa9..1d68c4802 100644 --- a/projects/aca-folder-rules/src/lib/model/rule-action.model.ts +++ b/projects/aca-folder-rules/src/lib/model/rule-action.model.ts @@ -28,6 +28,11 @@ export interface RuleAction { params: { [key: string]: unknown }; } +export const isRuleAction = (obj): obj is RuleAction => + typeof obj === 'object' && typeof obj.actionDefinitionId === 'string' && typeof obj.params === 'object'; +export const isRuleActions = (obj): obj is RuleAction[] => + typeof obj === 'object' && obj instanceof Array && obj.reduce((acc, curr) => acc && isRuleAction(curr), true); + export interface ActionDefinitionTransformed { id: string; name: string; diff --git a/projects/aca-folder-rules/src/lib/rule-details/actions/rule-action-list.ui-component.ts b/projects/aca-folder-rules/src/lib/rule-details/actions/rule-action-list.ui-component.ts index 129a03035..964fcc069 100644 --- a/projects/aca-folder-rules/src/lib/rule-details/actions/rule-action-list.ui-component.ts +++ b/projects/aca-folder-rules/src/lib/rule-details/actions/rule-action-list.ui-component.ts @@ -1,7 +1,33 @@ +/*! + * @license + * Alfresco Example Content Application + * + * Copyright (C) 2005 - 2020 Alfresco Software Limited + * + * This file is part of the Alfresco Example Content Application. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * The Alfresco Example Content Application is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * The Alfresco Example Content Application is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see <http://www.gnu.org/licenses/>. + */ + import { Component, forwardRef, Input, OnDestroy, ViewEncapsulation } from '@angular/core'; -import { ControlValueAccessor, FormArray, FormControl, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { ControlValueAccessor, FormArray, FormControl, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; import { ActionDefinitionTransformed, RuleAction } from '../../model/rule-action.model'; import { Subscription } from 'rxjs'; +import { ruleActionValidator } from '../validators/rule-actions.validator'; @Component({ selector: 'aca-rule-action-list', @@ -63,7 +89,7 @@ export class RuleActionListUiComponent implements ControlValueAccessor, OnDestro actionDefinitionId: null, params: {} }; - this.formArray.push(new FormControl(newAction)); + this.formArray.push(new FormControl(newAction, [Validators.required, ruleActionValidator(this.actionDefinitions)])); } ngOnDestroy() { diff --git a/projects/aca-folder-rules/src/lib/rule-details/actions/rule-action.ui-component.ts b/projects/aca-folder-rules/src/lib/rule-details/actions/rule-action.ui-component.ts index cfea506dc..52dd22e97 100644 --- a/projects/aca-folder-rules/src/lib/rule-details/actions/rule-action.ui-component.ts +++ b/projects/aca-folder-rules/src/lib/rule-details/actions/rule-action.ui-component.ts @@ -1,9 +1,36 @@ -import { Component, forwardRef, Input, OnDestroy, ViewEncapsulation } from '@angular/core'; -import { ControlValueAccessor, FormControl, FormGroup, NG_VALUE_ACCESSOR } from '@angular/forms'; +/*! + * @license + * Alfresco Example Content Application + * + * Copyright (C) 2005 - 2020 Alfresco Software Limited + * + * This file is part of the Alfresco Example Content Application. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * The Alfresco Example Content Application is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * The Alfresco Example Content Application is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see <http://www.gnu.org/licenses/>. + */ + +import { Component, forwardRef, Input, OnDestroy, OnInit, ViewEncapsulation } from '@angular/core'; +import { ControlValueAccessor, FormControl, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; import { ActionDefinitionTransformed, RuleAction } from '../../model/rule-action.model'; import { CardViewItem } from '@alfresco/adf-core/lib/card-view/interfaces/card-view-item.interface'; import { CardViewBoolItemModel, CardViewTextItemModel, CardViewUpdateService, UpdateNotification } from '@alfresco/adf-core'; import { ActionParameterDefinition } from '@alfresco/js-api'; +import { Subject } from 'rxjs'; +import { takeUntil } from 'rxjs/operators'; @Component({ selector: 'aca-rule-action', @@ -20,7 +47,7 @@ import { ActionParameterDefinition } from '@alfresco/js-api'; CardViewUpdateService ] }) -export class RuleActionUiComponent implements ControlValueAccessor, OnDestroy { +export class RuleActionUiComponent implements ControlValueAccessor, OnInit, OnDestroy { private _actionDefinitions: ActionDefinitionTransformed[]; @Input() get actionDefinitions(): ActionDefinitionTransformed[] { @@ -40,12 +67,12 @@ export class RuleActionUiComponent implements ControlValueAccessor, OnDestroy { } form = new FormGroup({ - actionDefinitionId: new FormControl('copy') + actionDefinitionId: new FormControl('', Validators.required) }); cardViewItems: CardViewItem[] = []; - parameters: { [key: string]: unknown } = {}; + private onDestroy$ = new Subject<boolean>(); get selectedActionDefinitionId(): string { return this.form.get('actionDefinitionId').value; @@ -55,28 +82,6 @@ export class RuleActionUiComponent implements ControlValueAccessor, OnDestroy { return this.actionDefinitions.find((actionDefinition: ActionDefinitionTransformed) => actionDefinition.id === this.selectedActionDefinitionId); } - private formSubscription = this.form.valueChanges.subscribe(() => { - this.setDefaultParameters(); - this.setCardViewProperties(); - this.onChange({ - actionDefinitionId: this.selectedActionDefinitionId, - params: this.parameters - }); - this.onTouch(); - }); - - private updateServiceSubscription = this.cardViewUpdateService.itemUpdated$.subscribe((updateNotification: UpdateNotification) => { - this.parameters = { - ...this.parameters, - ...updateNotification.changed - }; - this.onChange({ - actionDefinitionId: this.selectedActionDefinitionId, - params: this.parameters - }); - this.onTouch(); - }); - onChange: (action: RuleAction) => void = () => undefined; onTouch: () => void = () => undefined; @@ -101,17 +106,51 @@ export class RuleActionUiComponent implements ControlValueAccessor, OnDestroy { this.onTouch = fn; } + ngOnInit() { + this.form.valueChanges.pipe(takeUntil(this.onDestroy$)).subscribe(() => { + this.setDefaultParameters(); + this.setCardViewProperties(); + this.onChange({ + actionDefinitionId: this.selectedActionDefinitionId, + params: this.parameters + }); + this.onTouch(); + }); + + this.cardViewUpdateService.itemUpdated$.pipe(takeUntil(this.onDestroy$)).subscribe((updateNotification: UpdateNotification) => { + this.parameters = { + ...this.parameters, + ...updateNotification.changed + }; + this.onChange({ + actionDefinitionId: this.selectedActionDefinitionId, + params: this.parameters + }); + this.onTouch(); + }); + } + ngOnDestroy() { - this.formSubscription.unsubscribe(); - this.updateServiceSubscription.unsubscribe(); + this.onDestroy$.next(); + this.onDestroy$.complete(); } setCardViewProperties() { this.cardViewItems = (this.selectedActionDefinition?.parameterDefinitions ?? []).map((paramDef) => { const cardViewPropertiesModel = { - label: paramDef.displayLabel, + label: paramDef.displayLabel + (paramDef.mandatory ? ' *' : ''), key: paramDef.name, - editable: true + editable: true, + ...(paramDef.mandatory + ? { + validators: [ + { + message: 'ACA_FOLDER_RULES.RULE_DETAILS.ERROR.REQUIRED', + isValid: (value: unknown) => !!value + } + ] + } + : {}) }; switch (paramDef.type) { case 'd:boolean': diff --git a/projects/aca-folder-rules/src/lib/rule-details/rule-details.ui-component.ts b/projects/aca-folder-rules/src/lib/rule-details/rule-details.ui-component.ts index f5508905f..b36262ffa 100644 --- a/projects/aca-folder-rules/src/lib/rule-details/rule-details.ui-component.ts +++ b/projects/aca-folder-rules/src/lib/rule-details/rule-details.ui-component.ts @@ -31,6 +31,7 @@ import { Rule } from '../model/rule.model'; import { ruleCompositeConditionValidator } from './validators/rule-composite-condition.validator'; import { FolderRulesService } from '../services/folder-rules.service'; import { ActionDefinitionTransformed } from '../model/rule-action.model'; +import { ruleActionsValidator } from './validators/rule-actions.validator'; @Component({ selector: 'aca-rule-details', @@ -136,7 +137,7 @@ export class RuleDetailsUiComponent implements OnInit, OnDestroy { errorScript: new UntypedFormControl(this.value.errorScript), isInheritable: new UntypedFormControl(this.value.isInheritable), isEnabled: new UntypedFormControl(this.value.isEnabled), - actions: new UntypedFormControl(this.value.actions) + actions: new UntypedFormControl(this.value.actions, [Validators.required, ruleActionsValidator(this.actionDefinitions)]) }); this.readOnly = this._readOnly; diff --git a/projects/aca-folder-rules/src/lib/rule-details/validators/rule-actions.validator.spec.ts b/projects/aca-folder-rules/src/lib/rule-details/validators/rule-actions.validator.spec.ts new file mode 100644 index 000000000..f06895143 --- /dev/null +++ b/projects/aca-folder-rules/src/lib/rule-details/validators/rule-actions.validator.spec.ts @@ -0,0 +1,69 @@ +/*! + * @license + * Alfresco Example Content Application + * + * Copyright (C) 2005 - 2020 Alfresco Software Limited + * + * This file is part of the Alfresco Example Content Application. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * The Alfresco Example Content Application is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * The Alfresco Example Content Application is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see <http://www.gnu.org/licenses/>. + */ + +import { FormControl, ValidatorFn } from '@angular/forms'; +import { ruleActionsValidator, ruleActionValidator } from './rule-actions.validator'; +import { + actionsTransformedListMock, + incompleteMandatoryParameterMock, + missingMandatoryParameterMock, + nonExistentActionDefinitionIdMock, + validActionMock, + validActionsMock +} from '../../mock/actions.mock'; + +describe('ruleActionsValidator', () => { + let validatorFn: ValidatorFn; + + beforeEach(() => { + validatorFn = ruleActionValidator(actionsTransformedListMock); + }); + + it('should return null for a valid action', () => { + const control = new FormControl(validActionMock); + expect(validatorFn(control)).toBeNull(); + }); + + it('should return a validation error for an non-existent action definition ID', () => { + const control = new FormControl(nonExistentActionDefinitionIdMock); + expect(validatorFn(control)).toEqual({ ruleActionInvalid: true }); + }); + + it('should return a validation error for an missing mandatory parameter', () => { + const control = new FormControl(missingMandatoryParameterMock); + expect(validatorFn(control)).toEqual({ ruleActionInvalid: true }); + }); + + it('should return a validation error for an incomplete mandatory parameter', () => { + const control = new FormControl(incompleteMandatoryParameterMock); + expect(validatorFn(control)).toEqual({ ruleActionInvalid: true }); + }); + + it('should return null for valid actions', () => { + const multipleActionsValidatorFn = ruleActionsValidator(actionsTransformedListMock); + const control = new FormControl(validActionsMock); + expect(multipleActionsValidatorFn(control)).toBeNull(); + }); +}); diff --git a/projects/aca-folder-rules/src/lib/rule-details/validators/rule-actions.validator.ts b/projects/aca-folder-rules/src/lib/rule-details/validators/rule-actions.validator.ts new file mode 100644 index 000000000..70dc288b0 --- /dev/null +++ b/projects/aca-folder-rules/src/lib/rule-details/validators/rule-actions.validator.ts @@ -0,0 +1,61 @@ +/*! + * @license + * Alfresco Example Content Application + * + * Copyright (C) 2005 - 2020 Alfresco Software Limited + * + * This file is part of the Alfresco Example Content Application. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * The Alfresco Example Content Application is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * The Alfresco Example Content Application is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see <http://www.gnu.org/licenses/>. + */ + +import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms'; +import { + ActionDefinitionTransformed, + ActionParameterDefinitionTransformed, + isRuleAction, + isRuleActions, + RuleAction +} from '../../model/rule-action.model'; + +const isRuleActionValid = (value: unknown, actionDefinitions: ActionDefinitionTransformed[]): boolean => { + const actionDefinition = isRuleAction(value) + ? actionDefinitions.find((actionDef: ActionDefinitionTransformed) => value.actionDefinitionId === actionDef.id) + : undefined; + return ( + isRuleAction(value) && + actionDefinition && + actionDefinition.parameterDefinitions.reduce( + (isValid: boolean, paramDef: ActionParameterDefinitionTransformed) => isValid && (!paramDef.mandatory || !!value.params[paramDef.name]), + true + ) + ); +}; + +const isRuleActionsValid = (value: unknown, actionDefinitions: ActionDefinitionTransformed[]): boolean => + isRuleActions(value) && + value.reduce((isValid: boolean, currentAction: RuleAction) => isValid && isRuleActionValid(currentAction, actionDefinitions), true); + +export const ruleActionValidator = + (actionDefinitions: ActionDefinitionTransformed[]): ValidatorFn => + (control: AbstractControl): ValidationErrors | null => + isRuleActionValid(control.value, actionDefinitions) ? null : { ruleActionInvalid: true }; + +export const ruleActionsValidator = + (actionDefinitions: ActionDefinitionTransformed[]): ValidatorFn => + (control: AbstractControl): ValidationErrors | null => + isRuleActionsValid(control.value, actionDefinitions) ? null : { ruleActionsInvalid: true };