From 9cd616ff8c4355ddb122867459450715ee730232 Mon Sep 17 00:00:00 2001 From: Thomas Hunter Date: Tue, 23 Aug 2022 09:54:08 +0100 Subject: [PATCH] [ACS-3256] Add trigger / "when" section to rule details & edit rule dialog (#2603) * Add triggers section with checkboxes * Add some unit tests * Changed Input property of rule details so that it takes a changeable value rather than just an initial value * Made separate component with control value accessor for triggers * Linting * Change trigger values to lowercase to be in sync with the API * Minor styling changes and add new rule button to manage rules screen * Add option for unknown field to be selected * Add read only to nested composite and simple conditions * Added unknown field tests for simple condition * Linting * Change how the triggers component displays when disabled --- projects/aca-folder-rules/assets/i18n/en.json | 14 ++- .../src/lib/folder-rules.module.ts | 4 +- .../manage-rules.smart-component.html | 16 ++- .../manage-rules.smart-component.scss | 37 +++--- .../manage-rules.smart-component.ts | 22 +++- .../src/lib/mock/conditions.mock.ts | 6 + .../src/lib/mock/rules.mock.ts | 14 ++- .../src/lib/model/rule.model.ts | 4 +- ...rule-composite-condition.ui-component.html | 6 +- ...rule-composite-condition.ui-component.scss | 8 +- .../rule-composite-condition.ui-component.ts | 14 ++- .../rule-simple-condition.ui-component.html | 3 + ...rule-simple-condition.ui-component.spec.ts | 27 +++++ .../rule-simple-condition.ui-component.ts | 30 ++++- .../edit-rule-dialog.smart-component.html | 2 +- .../edit-rule-dialog.smart-component.spec.ts | 3 +- .../rule-details.ui-component.html | 12 +- .../rule-details.ui-component.scss | 17 ++- .../rule-details.ui-component.spec.ts | 42 +++++-- .../rule-details/rule-details.ui-component.ts | 50 +++++++-- .../triggers/rule-triggers.ui-component.html | 19 ++++ .../rule-triggers.ui-component.spec.ts | 106 ++++++++++++++++++ .../triggers/rule-triggers.ui-component.ts | 88 +++++++++++++++ .../rule-composite-condition.validator.ts} | 0 .../src/lib/services/folder-rules.service.ts | 64 +++++++++-- 25 files changed, 531 insertions(+), 77 deletions(-) create mode 100644 projects/aca-folder-rules/src/lib/rule-details/triggers/rule-triggers.ui-component.html create mode 100644 projects/aca-folder-rules/src/lib/rule-details/triggers/rule-triggers.ui-component.spec.ts create mode 100644 projects/aca-folder-rules/src/lib/rule-details/triggers/rule-triggers.ui-component.ts rename projects/aca-folder-rules/src/lib/rule-details/{conditions/rule-composite-condition.validators.ts => validators/rule-composite-condition.validator.ts} (100%) diff --git a/projects/aca-folder-rules/assets/i18n/en.json b/projects/aca-folder-rules/assets/i18n/en.json index 7eac44f31..0b2dd6bd9 100644 --- a/projects/aca-folder-rules/assets/i18n/en.json +++ b/projects/aca-folder-rules/assets/i18n/en.json @@ -13,7 +13,8 @@ "RULE_DETAILS": { "LABEL": { "NAME": "Name", - "DESCRIPTION": "Description" + "DESCRIPTION": "Description", + "WHEN": "When" }, "PLACEHOLDER": { "NAME": "Enter rule name", @@ -22,7 +23,13 @@ }, "ERROR": { "REQUIRED": "This field is required", - "RULE_COMPOSITE_CONDITION_INVALID": "One or more condition groups is empty" + "RULE_COMPOSITE_CONDITION_INVALID": "One or more condition groups is empty", + "INSUFFICIENT_TRIGGERS_SELECTED": "At least one trigger is required" + }, + "TRIGGERS": { + "INBOUND": "Items are created or enter this folder", + "UPDATE": "Items are updated", + "OUTBOUND": "Items are deleted or leave this folder" }, "COMPARATORS": { "EQUALS": "(=) Equals", @@ -67,6 +74,9 @@ "TOOLBAR": { "BREADCRUMB": { "RULES": "rules" + }, + "ACTIONS": { + "NEW_RULE": "New rule" } }, "EMPTY_RULES_LIST": { diff --git a/projects/aca-folder-rules/src/lib/folder-rules.module.ts b/projects/aca-folder-rules/src/lib/folder-rules.module.ts index 83364a74f..7cc2ebf40 100644 --- a/projects/aca-folder-rules/src/lib/folder-rules.module.ts +++ b/projects/aca-folder-rules/src/lib/folder-rules.module.ts @@ -38,6 +38,7 @@ import { GenericErrorModule, PageLayoutModule } from '@alfresco/aca-shared'; import { BreadcrumbModule, DocumentListModule } from '@alfresco/adf-content-services'; import { RuleListItemUiComponent } from './rules-list/rule/rule-list-item.ui-component'; import { RulesListUiComponent } from './rules-list/rules-list.ui-component'; +import { RuleTriggersUiComponent } from './rule-details/triggers/rule-triggers.ui-component'; const routes: Routes = [ { @@ -65,7 +66,8 @@ const routes: Routes = [ RuleDetailsUiComponent, RuleSimpleConditionUiComponent, RulesListUiComponent, - RuleListItemUiComponent + RuleListItemUiComponent, + RuleTriggersUiComponent ] }) export class AcaFolderRulesModule { diff --git a/projects/aca-folder-rules/src/lib/manage-rules/manage-rules.smart-component.html b/projects/aca-folder-rules/src/lib/manage-rules/manage-rules.smart-component.html index f23b0426b..4e18f7acc 100644 --- a/projects/aca-folder-rules/src/lib/manage-rules/manage-rules.smart-component.html +++ b/projects/aca-folder-rules/src/lib/manage-rules/manage-rules.smart-component.html @@ -18,18 +18,22 @@ - - + + folder + class="aca-manage-rules__actions-bar__title__breadcrumb"> + + -
- - +
+ +
+ +
diff --git a/projects/aca-folder-rules/src/lib/manage-rules/manage-rules.smart-component.scss b/projects/aca-folder-rules/src/lib/manage-rules/manage-rules.smart-component.scss index d23952651..d2650773d 100644 --- a/projects/aca-folder-rules/src/lib/manage-rules/manage-rules.smart-component.scss +++ b/projects/aca-folder-rules/src/lib/manage-rules/manage-rules.smart-component.scss @@ -1,19 +1,28 @@ -.aca-rules-actions-bar { - padding: 0 30px; +.aca-manage-rules { + &__actions-bar { + padding: 0 30px; + display: flex; + align-items: center; - .aca-rules-breadcrumb { - margin-left: 18px; + &__title { + align-items: center; + width: unset !important; + flex: 1; + + &__breadcrumb { + margin-left: 18px; + } + } } - .icon-aligner{ - margin-top: 4px; + &__container { + display: grid; + grid-template-columns: 33% 66%; + padding: 32px; + overflow: scroll; + + &__rule-details { + overflow-x: scroll; + } } } - -.aca-manage-rules-container { - display: grid; - grid-template-columns: 1fr 2fr; - padding: 32px; - overflow: scroll; - -} diff --git a/projects/aca-folder-rules/src/lib/manage-rules/manage-rules.smart-component.ts b/projects/aca-folder-rules/src/lib/manage-rules/manage-rules.smart-component.ts index 676bdd1ad..30e34b520 100644 --- a/projects/aca-folder-rules/src/lib/manage-rules/manage-rules.smart-component.ts +++ b/projects/aca-folder-rules/src/lib/manage-rules/manage-rules.smart-component.ts @@ -23,7 +23,7 @@ * along with Alfresco. If not, see . */ -import { Component, OnInit } from '@angular/core'; +import { Component, OnInit, ViewEncapsulation } from '@angular/core'; import { Location } from '@angular/common'; import { FolderRulesService } from '../services/folder-rules.service'; import { Observable } from 'rxjs'; @@ -31,11 +31,15 @@ import { Rule } from '../model/rule.model'; import { ActivatedRoute } from '@angular/router'; import { NodeInfo } from '@alfresco/aca-shared/store'; import { tap } from 'rxjs/operators'; +import { EditRuleDialogSmartComponent } from '../rule-details/edit-rule-dialog.smart-component'; +import { MatDialog } from '@angular/material/dialog'; @Component({ selector: 'aca-manage-rules', templateUrl: 'manage-rules.smart-component.html', - styleUrls: ['manage-rules.smart-component.scss'] + styleUrls: ['manage-rules.smart-component.scss'], + encapsulation: ViewEncapsulation.None, + host: { class: 'aca-manage-rules' } }) export class ManageRulesSmartComponent implements OnInit { rules$: Observable; @@ -44,7 +48,12 @@ export class ManageRulesSmartComponent implements OnInit { selectedRule: Rule = null; nodeId: string = null; - constructor(private location: Location, private folderRulesService: FolderRulesService, private route: ActivatedRoute) {} + constructor( + private location: Location, + private folderRulesService: FolderRulesService, + private route: ActivatedRoute, + private matDialogService: MatDialog + ) {} ngOnInit(): void { this.rules$ = this.folderRulesService.rulesListing$.pipe( @@ -71,4 +80,11 @@ export class ManageRulesSmartComponent implements OnInit { onRuleSelected(rule: Rule): void { this.selectedRule = rule; } + + openNewRuleDialog() { + this.matDialogService.open(EditRuleDialogSmartComponent, { + width: '90%', + panelClass: 'aca-edit-rule-dialog-container' + }); + } } diff --git a/projects/aca-folder-rules/src/lib/mock/conditions.mock.ts b/projects/aca-folder-rules/src/lib/mock/conditions.mock.ts index 7b5bb5d10..4a7229120 100644 --- a/projects/aca-folder-rules/src/lib/mock/conditions.mock.ts +++ b/projects/aca-folder-rules/src/lib/mock/conditions.mock.ts @@ -32,6 +32,12 @@ const simpleConditionMock: RuleSimpleCondition = { parameter: '' }; +export const simpleConditionUnknownFieldMock: RuleSimpleCondition = { + field: 'unknown-field', + comparator: 'equals', + parameter: '' +}; + const emptyCompositeConditionMock: RuleCompositeCondition = { inverted: false, booleanMode: 'and', diff --git a/projects/aca-folder-rules/src/lib/mock/rules.mock.ts b/projects/aca-folder-rules/src/lib/mock/rules.mock.ts index 85c9ea1bd..6fd4758ea 100644 --- a/projects/aca-folder-rules/src/lib/mock/rules.mock.ts +++ b/projects/aca-folder-rules/src/lib/mock/rules.mock.ts @@ -91,7 +91,12 @@ export const dummyRules: Rule[] = [ errorScript: '', isShared: false, triggers: ['inbound'], - conditions: null, + conditions: { + inverted: false, + booleanMode: 'and', + simpleConditions: [], + compositeConditions: [] + }, actions: [ { actionDefinitionId: 'copy', @@ -113,7 +118,12 @@ export const dummyRules: Rule[] = [ errorScript: '', isShared: false, triggers: ['inbound'], - conditions: null, + conditions: { + inverted: false, + booleanMode: 'and', + simpleConditions: [], + compositeConditions: [] + }, actions: [ { actionDefinitionId: 'move', diff --git a/projects/aca-folder-rules/src/lib/model/rule.model.ts b/projects/aca-folder-rules/src/lib/model/rule.model.ts index 55b53d202..532d43bc1 100644 --- a/projects/aca-folder-rules/src/lib/model/rule.model.ts +++ b/projects/aca-folder-rules/src/lib/model/rule.model.ts @@ -26,6 +26,8 @@ import { RuleCompositeCondition } from './rule-composite-condition.model'; import { RuleAction } from './rule-action.model'; +export type RuleTrigger = 'inbound' | 'update' | 'outbound'; + export interface Rule { id: string; name: string; @@ -35,7 +37,7 @@ export interface Rule { asynchronous: boolean; errorScript: string; isShared: boolean; - triggers: ('inbound' | 'update' | 'outbound')[]; + triggers: RuleTrigger[]; conditions: RuleCompositeCondition; actions: RuleAction[]; } diff --git a/projects/aca-folder-rules/src/lib/rule-details/conditions/rule-composite-condition.ui-component.html b/projects/aca-folder-rules/src/lib/rule-details/conditions/rule-composite-condition.ui-component.html index 165f74057..32a4ec642 100644 --- a/projects/aca-folder-rules/src/lib/rule-details/conditions/rule-composite-condition.ui-component.html +++ b/projects/aca-folder-rules/src/lib/rule-details/conditions/rule-composite-condition.ui-component.html @@ -31,12 +31,14 @@ *ngIf="!isFormControlSimpleCondition(control)" [secondaryBackground]="!secondaryBackground" [childCondition]="true" - [formControl]="control"> + [formControl]="control" + [readOnly]="readOnly"> + [formControl]="control" + [readOnly]="readOnly">
- + diff --git a/projects/aca-folder-rules/src/lib/rule-details/edit-rule-dialog.smart-component.spec.ts b/projects/aca-folder-rules/src/lib/rule-details/edit-rule-dialog.smart-component.spec.ts index 0c339758e..650495ace 100644 --- a/projects/aca-folder-rules/src/lib/rule-details/edit-rule-dialog.smart-component.spec.ts +++ b/projects/aca-folder-rules/src/lib/rule-details/edit-rule-dialog.smart-component.spec.ts @@ -30,6 +30,7 @@ import { RuleDetailsUiComponent } from './rule-details.ui-component'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { CoreTestingModule } from '@alfresco/adf-core'; import { RuleCompositeConditionUiComponent } from './conditions/rule-composite-condition.ui-component'; +import { RuleTriggersUiComponent } from './triggers/rule-triggers.ui-component'; describe('EditRuleDialogComponent', () => { let fixture: ComponentFixture; @@ -42,7 +43,7 @@ describe('EditRuleDialogComponent', () => { const setupBeforeEach = (dialogOptions: EditRuleDialogOptions = {}) => { TestBed.configureTestingModule({ imports: [CoreTestingModule], - declarations: [EditRuleDialogSmartComponent, RuleCompositeConditionUiComponent, RuleDetailsUiComponent], + declarations: [EditRuleDialogSmartComponent, RuleCompositeConditionUiComponent, RuleDetailsUiComponent, RuleTriggersUiComponent], providers: [ { provide: MatDialogRef, useValue: dialogRef }, { provide: MAT_DIALOG_DATA, useValue: dialogOptions } diff --git a/projects/aca-folder-rules/src/lib/rule-details/rule-details.ui-component.html b/projects/aca-folder-rules/src/lib/rule-details/rule-details.ui-component.html index 509562fb0..ddb4ae129 100644 --- a/projects/aca-folder-rules/src/lib/rule-details/rule-details.ui-component.html +++ b/projects/aca-folder-rules/src/lib/rule-details/rule-details.ui-component.html @@ -27,6 +27,16 @@
+
+
{{ 'ACA_FOLDER_RULES.RULE_DETAILS.LABEL.WHEN' | translate }}
+
+ + {{ getErrorMessage(triggers) | translate }} +
+
+ +
+ - {{ getErrorMessage(conditions) | translate }} + {{ getErrorMessage(conditions) | translate }} diff --git a/projects/aca-folder-rules/src/lib/rule-details/rule-details.ui-component.scss b/projects/aca-folder-rules/src/lib/rule-details/rule-details.ui-component.scss index 84186ab77..a2788dc35 100644 --- a/projects/aca-folder-rules/src/lib/rule-details/rule-details.ui-component.scss +++ b/projects/aca-folder-rules/src/lib/rule-details/rule-details.ui-component.scss @@ -6,7 +6,7 @@ display: flex; gap: 8px; - & > label { + & > label, & > .label { font-weight: bold; width: 20%; min-width: 100px; @@ -28,6 +28,14 @@ } } + &__triggers { + padding: 0.75em 0; + + & > .label { + padding: 0; + } + } + hr { border: none; border-bottom: 1px solid var(--theme-border-color); @@ -41,9 +49,14 @@ min-height: 4em; } - & > .aca-rule-composite-condition + .mat-error { + .rule-details-error { font-size: 75%; + height: 1em; + } + + & > .aca-rule-composite-condition + .rule-details-error { margin-left: 16px; + color: inherit; } } } diff --git a/projects/aca-folder-rules/src/lib/rule-details/rule-details.ui-component.spec.ts b/projects/aca-folder-rules/src/lib/rule-details/rule-details.ui-component.spec.ts index b223ec252..e7c6830d0 100644 --- a/projects/aca-folder-rules/src/lib/rule-details/rule-details.ui-component.spec.ts +++ b/projects/aca-folder-rules/src/lib/rule-details/rule-details.ui-component.spec.ts @@ -29,24 +29,29 @@ import { RuleDetailsUiComponent } from './rule-details.ui-component'; import { Rule } from '../model/rule.model'; import { By } from '@angular/platform-browser'; import { RuleCompositeConditionUiComponent } from './conditions/rule-composite-condition.ui-component'; +import { RuleTriggersUiComponent } from './triggers/rule-triggers.ui-component'; describe('RuleDetailsUiComponent', () => { let fixture: ComponentFixture; let component: RuleDetailsUiComponent; - const initialValue: Partial = { + const testValue: Partial = { id: 'rule-id', name: 'Rule name', - description: 'This is the description of the rule' + description: 'This is the description of the rule', + triggers: ['update', 'outbound'] }; const getHtmlElement = (dataAutomationId: string) => - fixture.debugElement.query(By.css(`[data-automation-id="${dataAutomationId}"]`)).nativeElement as T; + fixture.debugElement.query(By.css(`[data-automation-id="${dataAutomationId}"]`))?.nativeElement as T; + + const getComponentInstance = (dataAutomationId: string) => + fixture.debugElement.query(By.css(`[data-automation-id="${dataAutomationId}"]`))?.componentInstance as T; beforeEach(() => { TestBed.configureTestingModule({ imports: [CoreTestingModule], - declarations: [RuleCompositeConditionUiComponent, RuleDetailsUiComponent] + declarations: [RuleCompositeConditionUiComponent, RuleDetailsUiComponent, RuleTriggersUiComponent] }); fixture = TestBed.createComponent(RuleDetailsUiComponent); @@ -54,14 +59,33 @@ describe('RuleDetailsUiComponent', () => { }); it('should fill the form out with initial values', () => { - component.initialValue = initialValue; + component.value = testValue; fixture.detectChanges(); const nameInput = getHtmlElement('rule-details-name-input'); const descriptionTextarea = getHtmlElement('rule-details-description-textarea'); + const ruleTriggersComponent = getComponentInstance('rule-details-triggers-component'); - expect(nameInput.value).toBe(initialValue.name); - expect(descriptionTextarea.value).toBe(initialValue.description); + expect(nameInput.value).toBe(testValue.name); + expect(descriptionTextarea.value).toBe(testValue.description); + expect(ruleTriggersComponent.value).toEqual(testValue.triggers); + }); + + it('should modify the form if the value input property is modified', () => { + fixture.detectChanges(); + const nameInput = getHtmlElement('rule-details-name-input'); + const descriptionTextarea = getHtmlElement('rule-details-description-textarea'); + const ruleTriggersComponent = getComponentInstance('rule-details-triggers-component'); + + expect(nameInput.value).toBe(''); + expect(descriptionTextarea.value).toBe(''); + expect(ruleTriggersComponent.value).toEqual(['inbound']); + component.value = testValue; + fixture.detectChanges(); + + expect(nameInput.value).toBe(testValue.name); + expect(descriptionTextarea.value).toBe(testValue.description); + expect(ruleTriggersComponent.value).toEqual(testValue.triggers); }); it('should be editable if not read-only', () => { @@ -70,9 +94,11 @@ describe('RuleDetailsUiComponent', () => { const nameInput = getHtmlElement('rule-details-name-input'); const descriptionTextarea = getHtmlElement('rule-details-description-textarea'); + const ruleTriggersComponent = getComponentInstance('rule-details-triggers-component'); expect(nameInput.disabled).toBeFalsy(); expect(descriptionTextarea.disabled).toBeFalsy(); + expect(ruleTriggersComponent.readOnly).toBeFalsy(); }); it('should not be editable if read-only', () => { @@ -81,8 +107,10 @@ describe('RuleDetailsUiComponent', () => { const nameInput = getHtmlElement('rule-details-name-input'); const descriptionTextarea = getHtmlElement('rule-details-description-textarea'); + const ruleTriggersComponent = getComponentInstance('rule-details-triggers-component'); expect(nameInput.disabled).toBeTruthy(); expect(descriptionTextarea.disabled).toBeTruthy(); + expect(ruleTriggersComponent.readOnly).toBeTruthy(); }); }); 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 50939dbd3..55888a056 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 @@ -28,7 +28,8 @@ import { AbstractControl, FormControl, FormGroup, Validators } from '@angular/fo import { Subject } from 'rxjs'; import { distinctUntilChanged, map, takeUntil } from 'rxjs/operators'; import { Rule } from '../model/rule.model'; -import { ruleCompositeConditionValidator } from './conditions/rule-composite-condition.validators'; +import { ruleCompositeConditionValidator } from './validators/rule-composite-condition.validator'; +import { FolderRulesService } from '../services/folder-rules.service'; @Component({ selector: 'aca-rule-details', @@ -53,8 +54,24 @@ export class RuleDetailsUiComponent implements OnInit, OnDestroy { } } } + private _initialValue: Partial = FolderRulesService.emptyRule; @Input() - initialValue: Partial = {}; + get value(): Partial { + return this.form ? this.form.value : this._initialValue; + } + set value(newValue: Partial) { + newValue = { + name: newValue.name || FolderRulesService.emptyRule.name, + description: newValue.description || FolderRulesService.emptyRule.description, + triggers: newValue.triggers || FolderRulesService.emptyRule.triggers, + conditions: newValue.conditions || FolderRulesService.emptyRule.conditions + }; + if (this.form) { + this.form.setValue(newValue); + } else { + this._initialValue = newValue; + } + } @Output() formValidationChanged = new EventEmitter(); @@ -64,22 +81,26 @@ export class RuleDetailsUiComponent implements OnInit, OnDestroy { private onDestroy$ = new Subject(); form: FormGroup; - get name(): AbstractControl { - return this.form.get('name'); + get name(): FormControl { + return this.form.get('name') as FormControl; } - get description(): AbstractControl { - return this.form.get('description'); + get description(): FormControl { + return this.form.get('description') as FormControl; } - get conditions(): AbstractControl { - return this.form.get('conditions'); + get triggers(): FormControl { + return this.form.get('triggers') as FormControl; + } + get conditions(): FormControl { + return this.form.get('conditions') as FormControl; } ngOnInit() { this.form = new FormGroup({ - name: new FormControl(this.initialValue.name || '', Validators.required), - description: new FormControl(this.initialValue.description || ''), + name: new FormControl(this.value.name || '', Validators.required), + description: new FormControl(this.value.description || ''), + triggers: new FormControl(this.value.triggers || ['inbound'], Validators.required), conditions: new FormControl( - this.initialValue.conditions || { + this.value.conditions || { inverted: false, booleanMode: 'and', compositeConditions: [], @@ -112,8 +133,13 @@ export class RuleDetailsUiComponent implements OnInit, OnDestroy { } getErrorMessage(control: AbstractControl): string { + if (this.readOnly) { + return ''; + } if (control.hasError('required')) { - return 'ACA_FOLDER_RULES.RULE_DETAILS.ERROR.REQUIRED'; + return control === this.triggers + ? 'ACA_FOLDER_RULES.RULE_DETAILS.ERROR.INSUFFICIENT_TRIGGERS_SELECTED' + : 'ACA_FOLDER_RULES.RULE_DETAILS.ERROR.REQUIRED'; } else if (control.hasError('ruleCompositeConditionInvalid')) { return 'ACA_FOLDER_RULES.RULE_DETAILS.ERROR.RULE_COMPOSITE_CONDITION_INVALID'; } diff --git a/projects/aca-folder-rules/src/lib/rule-details/triggers/rule-triggers.ui-component.html b/projects/aca-folder-rules/src/lib/rule-details/triggers/rule-triggers.ui-component.html new file mode 100644 index 000000000..b8c0ec63d --- /dev/null +++ b/projects/aca-folder-rules/src/lib/rule-details/triggers/rule-triggers.ui-component.html @@ -0,0 +1,19 @@ +
+ +
+ {{ 'ACA_FOLDER_RULES.RULE_DETAILS.TRIGGERS.' + trigger | uppercase | translate }} +
+
+ + + + {{ 'ACA_FOLDER_RULES.RULE_DETAILS.TRIGGERS.' + trigger | uppercase | translate }} + + +
diff --git a/projects/aca-folder-rules/src/lib/rule-details/triggers/rule-triggers.ui-component.spec.ts b/projects/aca-folder-rules/src/lib/rule-details/triggers/rule-triggers.ui-component.spec.ts new file mode 100644 index 000000000..5ec4e40ea --- /dev/null +++ b/projects/aca-folder-rules/src/lib/rule-details/triggers/rule-triggers.ui-component.spec.ts @@ -0,0 +1,106 @@ +/*! + * @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 . + */ + +import { RuleTriggersUiComponent } from './rule-triggers.ui-component'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { CoreTestingModule } from '@alfresco/adf-core'; +import { DebugElement } from '@angular/core'; +import { By } from '@angular/platform-browser'; + +describe('RuleTriggerUiComponent', () => { + let fixture: ComponentFixture; + let component: RuleTriggersUiComponent; + + const getByDataAutomationId = (dataAutomationId: string): DebugElement => + fixture.debugElement.query(By.css(`[data-automation-id="${dataAutomationId}"]`)); + + const toggleMatCheckbox = (dataAutomationId: string) => { + ((getByDataAutomationId(dataAutomationId).nativeElement as HTMLElement).children[0] as HTMLElement).click(); + }; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [CoreTestingModule], + declarations: [RuleTriggersUiComponent] + }); + + fixture = TestBed.createComponent(RuleTriggersUiComponent); + component = fixture.componentInstance; + }); + + it('should default to only the inbound checkbox', () => { + fixture.detectChanges(); + + expect(component.value).toEqual(['inbound']); + expect(getByDataAutomationId('rule-trigger-checkbox-inbound').componentInstance.checked).toBeTruthy(); + expect(getByDataAutomationId('rule-trigger-checkbox-update').componentInstance.checked).toBeFalsy(); + expect(getByDataAutomationId('rule-trigger-checkbox-outbound').componentInstance.checked).toBeFalsy(); + }); + + it('should change the checked boxes when the value is written to', () => { + fixture.detectChanges(); + component.writeValue(['update', 'outbound']); + fixture.detectChanges(); + + expect(component.value).toEqual(['update', 'outbound']); + expect(getByDataAutomationId('rule-trigger-checkbox-inbound').componentInstance.checked).toBeFalsy(); + expect(getByDataAutomationId('rule-trigger-checkbox-update').componentInstance.checked).toBeTruthy(); + expect(getByDataAutomationId('rule-trigger-checkbox-outbound').componentInstance.checked).toBeTruthy(); + }); + + it('should update the value when a checkbox is checked', () => { + const onChangeSpy = spyOn(component, 'onChange'); + fixture.detectChanges(); + toggleMatCheckbox('rule-trigger-checkbox-update'); + fixture.detectChanges(); + + expect(component.value).toEqual(['inbound', 'update']); + expect(onChangeSpy).toHaveBeenCalledWith(['inbound', 'update']); + }); + + it('should update the value when a checkbox is unchecked', () => { + const onChangeSpy = spyOn(component, 'onChange'); + fixture.detectChanges(); + toggleMatCheckbox('rule-trigger-checkbox-inbound'); + fixture.detectChanges(); + + expect(component.value).toEqual([]); + expect(onChangeSpy).toHaveBeenCalledWith([]); + }); + + it('should only show the correct triggers in read only mode', () => { + component.setDisabledState(true); + component.writeValue(['update', 'outbound']); + fixture.detectChanges(); + + expect(getByDataAutomationId('rule-trigger-checkbox-inbound')).toBeNull(); + expect(getByDataAutomationId('rule-trigger-checkbox-update')).toBeNull(); + expect(getByDataAutomationId('rule-trigger-checkbox-outbound')).toBeNull(); + + expect(getByDataAutomationId('rule-trigger-value-inbound')).toBeNull(); + expect(getByDataAutomationId('rule-trigger-value-update')).not.toBeNull(); + expect(getByDataAutomationId('rule-trigger-value-outbound')).not.toBeNull(); + }); +}); diff --git a/projects/aca-folder-rules/src/lib/rule-details/triggers/rule-triggers.ui-component.ts b/projects/aca-folder-rules/src/lib/rule-details/triggers/rule-triggers.ui-component.ts new file mode 100644 index 000000000..50ad3f7dc --- /dev/null +++ b/projects/aca-folder-rules/src/lib/rule-details/triggers/rule-triggers.ui-component.ts @@ -0,0 +1,88 @@ +/*! + * @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 . + */ + +import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { Component, forwardRef, ViewEncapsulation } from '@angular/core'; +import { RuleTrigger } from '../../model/rule.model'; + +@Component({ + selector: 'aca-rule-triggers', + templateUrl: './rule-triggers.ui-component.html', + encapsulation: ViewEncapsulation.None, + host: { class: 'aca-rule-triggers' }, + providers: [ + { + provide: NG_VALUE_ACCESSOR, + multi: true, + useExisting: forwardRef(() => RuleTriggersUiComponent) + } + ] +}) +export class RuleTriggersUiComponent implements ControlValueAccessor { + readonly triggerOptions: RuleTrigger[] = ['inbound', 'update', 'outbound']; + + value: RuleTrigger[] = ['inbound']; + readOnly = false; + + onChange: (triggers: RuleTrigger[]) => void = () => undefined; + onTouch: () => void = () => undefined; + + writeValue(triggers: RuleTrigger[]) { + this.value = triggers; + } + + registerOnChange(fn: () => void) { + this.onChange = fn; + } + + registerOnTouched(fn: any) { + this.onTouch = fn; + } + + setDisabledState(isDisabled: boolean) { + this.readOnly = isDisabled; + } + + isTriggerChecked(trigger: RuleTrigger): boolean { + return this.value.includes(trigger); + } + + onTriggerChange(trigger: RuleTrigger, checked: boolean) { + if (checked) { + this.value.push(trigger); + } else { + this.value.splice( + this.value.findIndex((t) => t === trigger), + 1 + ); + } + this.onTouch(); + this.onChange([...this.value]); + } + + isTriggerSelected(trigger: RuleTrigger): boolean { + return this.value.includes(trigger); + } +} diff --git a/projects/aca-folder-rules/src/lib/rule-details/conditions/rule-composite-condition.validators.ts b/projects/aca-folder-rules/src/lib/rule-details/validators/rule-composite-condition.validator.ts similarity index 100% rename from projects/aca-folder-rules/src/lib/rule-details/conditions/rule-composite-condition.validators.ts rename to projects/aca-folder-rules/src/lib/rule-details/validators/rule-composite-condition.validator.ts diff --git a/projects/aca-folder-rules/src/lib/services/folder-rules.service.ts b/projects/aca-folder-rules/src/lib/services/folder-rules.service.ts index e70060994..2a2d13fee 100644 --- a/projects/aca-folder-rules/src/lib/services/folder-rules.service.ts +++ b/projects/aca-folder-rules/src/lib/services/folder-rules.service.ts @@ -30,11 +30,38 @@ import { catchError, finalize, map } from 'rxjs/operators'; import { Rule } from '../model/rule.model'; import { ContentApiService } from '@alfresco/aca-shared'; import { NodeInfo } from '@alfresco/aca-shared/store'; +import { RuleCompositeCondition } from '../model/rule-composite-condition.model'; +import { RuleSimpleCondition } from '../model/rule-simple-condition.model'; @Injectable({ providedIn: 'root' }) export class FolderRulesService { + public static get emptyCompositeCondition(): RuleCompositeCondition { + return { + inverted: false, + booleanMode: 'and', + compositeConditions: [], + simpleConditions: [] + }; + } + + public static get emptyRule(): Rule { + return { + id: '', + name: '', + description: '', + enabled: true, + cascade: false, + asynchronous: false, + errorScript: '', + isShared: false, + triggers: ['inbound'], + conditions: FolderRulesService.emptyCompositeCondition, + actions: [] + }; + } + private rulesListingSource = new BehaviorSubject([]); rulesListing$: Observable = this.rulesListingSource.asObservable(); private folderInfoSource = new BehaviorSubject(null); @@ -91,16 +118,33 @@ export class FolderRulesService { private formatRule(obj): Rule { return { id: obj.id, - name: obj.name ?? '', - description: obj.description ?? '', - enabled: obj.enabled ?? true, - cascade: obj.cascade ?? false, - asynchronous: obj.asynchronous ?? false, - errorScript: obj.errorScript ?? '', - isShared: obj.isShared ?? false, - triggers: obj.triggers ?? ['inbound'], - conditions: obj.conditions ?? null, - actions: obj.actions ?? [] + name: obj.name ?? FolderRulesService.emptyRule.name, + description: obj.description ?? FolderRulesService.emptyRule.description, + enabled: obj.enabled ?? FolderRulesService.emptyRule.enabled, + cascade: obj.cascade ?? FolderRulesService.emptyRule.cascade, + asynchronous: obj.asynchronous ?? FolderRulesService.emptyRule.asynchronous, + errorScript: obj.errorScript ?? FolderRulesService.emptyRule.errorScript, + isShared: obj.isShared ?? FolderRulesService.emptyRule.isShared, + triggers: obj.triggers ?? FolderRulesService.emptyRule.triggers, + conditions: this.formatCompositeCondition(obj.conditions ?? { ...FolderRulesService.emptyRule.conditions }), + actions: obj.actions ?? FolderRulesService.emptyRule.actions + }; + } + + private formatCompositeCondition(obj): RuleCompositeCondition { + return { + inverted: obj.inverted ?? false, + booleanMode: obj.booleanMode ?? 'and', + compositeConditions: (obj.compositeConditions || []).map((condition) => this.formatCompositeCondition(condition)), + simpleConditions: (obj.simpleConditions || []).map((condition) => this.formatSimpleCondition(condition)) + }; + } + + private formatSimpleCondition(obj): RuleSimpleCondition { + return { + field: obj.field || 'cm:name', + comparator: obj.comparator || 'equals', + parameter: obj.parameter || '' }; } }