[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
This commit is contained in:
Thomas Hunter
2022-08-23 09:54:08 +01:00
committed by GitHub
parent 19b88458ef
commit 9cd616ff8c
25 changed files with 531 additions and 77 deletions

View File

@@ -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": {

View File

@@ -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 {

View File

@@ -18,18 +18,22 @@
<ng-template #onLoaded>
<ng-container *ngIf="folderInfo$ | async; else genericError">
<adf-toolbar class="adf-toolbar--inline aca-rules-actions-bar">
<adf-toolbar-title>
<adf-toolbar class="adf-toolbar--inline aca-manage-rules__actions-bar">
<adf-toolbar-title class="aca-manage-rules__actions-bar__title">
<mat-icon class="icon-aligner">folder</mat-icon>
<adf-breadcrumb root="{{ (folderInfo$ | async).name }}:{{'ACA_FOLDER_RULES.MANAGE_RULES.TOOLBAR.BREADCRUMB.RULES' | translate}}"
class="aca-rules-breadcrumb"></adf-breadcrumb>
class="aca-manage-rules__actions-bar__title__breadcrumb"></adf-breadcrumb>
</adf-toolbar-title>
<button mat-flat-button color="primary" (click)="openNewRuleDialog()">{{ 'ACA_FOLDER_RULES.MANAGE_RULES.TOOLBAR.ACTIONS.NEW_RULE' | translate }}</button>
</adf-toolbar>
<mat-divider></mat-divider>
<div class="aca-manage-rules-container" *ngIf="(rules$ | async).length > 0 ; else emptyContent">
<aca-rules-list [rules]="rules$ | async" (ruleSelected)="onRuleSelected($event)" [selectedRule]="selectedRule"></aca-rules-list>
<aca-rule-details [readOnly]="true" ></aca-rule-details>
<div class="aca-manage-rules__container" *ngIf="(rules$ | async).length > 0 ; else emptyContent">
<aca-rules-list [rules]="rules$ | async" (ruleSelected)="onRuleSelected($event)" [selectedRule]="selectedRule"></aca-rules-list>
<div class="aca-manage-rules__container__rule-details">
<aca-rule-details [readOnly]="true" [value]="selectedRule"></aca-rule-details>
</div>
</div>
<ng-template #emptyContent>

View File

@@ -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;
}

View File

@@ -23,7 +23,7 @@
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
*/
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<Rule[]>;
@@ -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'
});
}
}

View File

@@ -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',

View File

@@ -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',

View File

@@ -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[];
}

View File

@@ -31,12 +31,14 @@
*ngIf="!isFormControlSimpleCondition(control)"
[secondaryBackground]="!secondaryBackground"
[childCondition]="true"
[formControl]="control">
[formControl]="control"
[readOnly]="readOnly">
</aca-rule-composite-condition>
<aca-rule-simple-condition
*ngIf="isFormControlSimpleCondition(control)"
[formControl]="control">
[formControl]="control"
[readOnly]="readOnly">
</aca-rule-simple-condition>
<button mat-icon-button [matMenuTriggerFor]="menu" *ngIf="!readOnly" data-automation-id="condition-actions-button">

View File

@@ -1,14 +1,14 @@
.aca-rule-composite-condition {
display: block;
border-radius: 8px;
background-color: hsl(0,0%,100%);
&.childCompositeCondition {
padding: 8px 16px;
}
background-color: hsl(0,0%,100%);
&.secondaryBackground {
background-color: hsl(0,0%,95%);
&.secondaryBackground {
background-color: hsl(0,0%,95%);
}
}
&__form {

View File

@@ -57,6 +57,15 @@ export class RuleCompositeConditionUiComponent implements ControlValueAccessor,
simpleConditions: new FormArray([])
});
private _readOnly = false;
@Input()
get readOnly(): boolean {
return this._readOnly;
}
set readOnly(isReadOnly: boolean) {
this.setDisabledState(isReadOnly);
}
private formSubscription = this.form.valueChanges.subscribe((value) => {
this.onChange(value);
this.onTouch();
@@ -81,11 +90,6 @@ export class RuleCompositeConditionUiComponent implements ControlValueAccessor,
return this.conditionFormControls.length === 0;
}
private _readOnly = false;
get readOnly(): boolean {
return this._readOnly;
}
onChange: (condition: RuleCompositeCondition) => void = () => undefined;
onTouch: () => void = () => undefined;

View File

@@ -2,6 +2,9 @@
<mat-form-field class="aca-rule-simple-condition__form__field-input">
<mat-select formControlName="field" data-automation-id="field-select"
(selectionChange)="onChangeField()">
<mat-option *ngIf="!isSelectedFieldKnown" [value]="selectedField.name" data-automation-id="unknown-field-option">
{{ selectedField.label }}
</mat-option>
<mat-option *ngFor="let field of fields" [value]="field.name">
{{ field.label | translate }}
</mat-option>

View File

@@ -28,6 +28,7 @@ import { RuleSimpleConditionUiComponent } from './rule-simple-condition.ui-compo
import { CoreTestingModule } from '@alfresco/adf-core';
import { By } from '@angular/platform-browser';
import { DebugElement } from '@angular/core';
import { simpleConditionUnknownFieldMock } from '../../mock/conditions.mock';
describe('RuleSimpleConditionUiComponent', () => {
let fixture: ComponentFixture<RuleSimpleConditionUiComponent>;
@@ -89,4 +90,30 @@ describe('RuleSimpleConditionUiComponent', () => {
expect(onChangeFieldSpy).toHaveBeenCalledTimes(2);
expect(getByDataAutomationId('comparator-select').componentInstance.value).toBe('equals');
});
it('should display an additional option for a currently selected unknown field', () => {
fixture.componentInstance.writeValue(simpleConditionUnknownFieldMock);
fixture.detectChanges();
expect(getByDataAutomationId('field-select').componentInstance.value).toBe(simpleConditionUnknownFieldMock.field);
const matSelect = getByDataAutomationId('field-select').nativeElement;
matSelect.click();
fixture.detectChanges();
const unknownOptionMatOption = getByDataAutomationId('unknown-field-option');
expect(unknownOptionMatOption).not.toBeNull();
expect((unknownOptionMatOption.nativeElement as HTMLElement).innerText.trim()).toBe(simpleConditionUnknownFieldMock.field);
});
it('should remove the option for the unknown field as soon as another option is selected', () => {
fixture.componentInstance.writeValue(simpleConditionUnknownFieldMock);
fixture.detectChanges();
changeMatSelectValue('field-select', 'cm:name');
const matSelect = getByDataAutomationId('field-select').nativeElement;
matSelect.click();
fixture.detectChanges();
const unknownOptionMatOption = getByDataAutomationId('unknown-field-option');
expect(unknownOptionMatOption).toBeNull();
});
});

View File

@@ -23,7 +23,7 @@
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
*/
import { Component, forwardRef, OnDestroy, ViewEncapsulation } from '@angular/core';
import { Component, forwardRef, Input, OnDestroy, ViewEncapsulation } from '@angular/core';
import { AbstractControl, ControlValueAccessor, FormControl, FormGroup, NG_VALUE_ACCESSOR } from '@angular/forms';
import { RuleSimpleCondition } from '../../model/rule-simple-condition.model';
import { RuleConditionField, ruleConditionFields } from './rule-condition-fields';
@@ -52,14 +52,36 @@ export class RuleSimpleConditionUiComponent implements ControlValueAccessor, OnD
parameter: new FormControl()
});
private _readOnly = false;
@Input()
get readOnly(): boolean {
return this._readOnly;
}
set readOnly(isReadOnly: boolean) {
this.setDisabledState(isReadOnly);
}
private formSubscription = this.form.valueChanges.subscribe((value) => {
this.onChange(value);
this.onTouch();
});
get selectedField(): RuleConditionField {
return this.fields.find((field) => field.name === this.form.get('field').value);
get isSelectedFieldKnown(): boolean {
const selectedFieldName = this.form.get('field').value;
return this.fields.findIndex((field: RuleConditionField) => selectedFieldName === field.name) > -1;
}
get selectedField(): RuleConditionField {
const selectedFieldName = this.form.get('field').value;
if (!this.isSelectedFieldKnown) {
return {
name: selectedFieldName,
label: selectedFieldName,
type: 'special'
};
}
return this.fields.find((field) => field.name === selectedFieldName);
}
get selectedFieldComparators(): RuleConditionComparator[] {
return ruleConditionComparators.filter((comparator) => Object.keys(comparator.labels).includes(this.selectedField.type));
}
@@ -87,8 +109,10 @@ export class RuleSimpleConditionUiComponent implements ControlValueAccessor, OnD
setDisabledState(isDisabled: boolean) {
if (isDisabled) {
this._readOnly = true;
this.form.disable();
} else {
this._readOnly = false;
this.form.enable();
}
}

View File

@@ -8,7 +8,7 @@
</div>
<mat-dialog-content class="aca-edit-rule-dialog__content">
<aca-rule-details (formValidationChanged)="formValid = $event" [initialValue]="model"></aca-rule-details>
<aca-rule-details (formValidationChanged)="formValid = $event" [value]="model"></aca-rule-details>
</mat-dialog-content>
<mat-dialog-actions align="end" class="aca-edit-rule-dialog__footer">

View File

@@ -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<EditRuleDialogSmartComponent>;
@@ -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 }

View File

@@ -27,6 +27,16 @@
<hr>
<div class="aca-rule-details__form__row aca-rule-details__form__triggers">
<div class="label">{{ 'ACA_FOLDER_RULES.RULE_DETAILS.LABEL.WHEN' | translate }}</div>
<div>
<aca-rule-triggers formControlName="triggers" data-automation-id="rule-details-triggers-component"></aca-rule-triggers>
<mat-error class="rule-details-error">{{ getErrorMessage(triggers) | translate }}</mat-error>
</div>
</div>
<hr>
<aca-rule-composite-condition formControlName="conditions"></aca-rule-composite-condition>
<mat-error>{{ getErrorMessage(conditions) | translate }}</mat-error>
<mat-error class="rule-details-error">{{ getErrorMessage(conditions) | translate }}</mat-error>
</form>

View File

@@ -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;
}
}
}

View File

@@ -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<RuleDetailsUiComponent>;
let component: RuleDetailsUiComponent;
const initialValue: Partial<Rule> = {
const testValue: Partial<Rule> = {
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 = <T>(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 = <T>(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<HTMLInputElement>('rule-details-name-input');
const descriptionTextarea = getHtmlElement<HTMLTextAreaElement>('rule-details-description-textarea');
const ruleTriggersComponent = getComponentInstance<RuleTriggersUiComponent>('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<HTMLInputElement>('rule-details-name-input');
const descriptionTextarea = getHtmlElement<HTMLTextAreaElement>('rule-details-description-textarea');
const ruleTriggersComponent = getComponentInstance<RuleTriggersUiComponent>('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<HTMLInputElement>('rule-details-name-input');
const descriptionTextarea = getHtmlElement<HTMLTextAreaElement>('rule-details-description-textarea');
const ruleTriggersComponent = getComponentInstance<RuleTriggersUiComponent>('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<HTMLInputElement>('rule-details-name-input');
const descriptionTextarea = getHtmlElement<HTMLTextAreaElement>('rule-details-description-textarea');
const ruleTriggersComponent = getComponentInstance<RuleTriggersUiComponent>('rule-details-triggers-component');
expect(nameInput.disabled).toBeTruthy();
expect(descriptionTextarea.disabled).toBeTruthy();
expect(ruleTriggersComponent.readOnly).toBeTruthy();
});
});

View File

@@ -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<Rule> = FolderRulesService.emptyRule;
@Input()
initialValue: Partial<Rule> = {};
get value(): Partial<Rule> {
return this.form ? this.form.value : this._initialValue;
}
set value(newValue: Partial<Rule>) {
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<boolean>();
@@ -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';
}

View File

@@ -0,0 +1,19 @@
<div *ngFor="let trigger of triggerOptions">
<ng-container *ngIf="readOnly; else checkbox">
<div
*ngIf="isTriggerSelected(trigger)"
[attr.data-automation-id]="'rule-trigger-value-' + trigger | lowercase">
{{ 'ACA_FOLDER_RULES.RULE_DETAILS.TRIGGERS.' + trigger | uppercase | translate }}
</div>
</ng-container>
<ng-template #checkbox>
<mat-checkbox
[attr.data-automation-id]="'rule-trigger-checkbox-' + trigger | lowercase"
[checked]="isTriggerChecked(trigger)"
[disabled]="readOnly"
(change)="onTriggerChange(trigger, $event.checked)">
{{ 'ACA_FOLDER_RULES.RULE_DETAILS.TRIGGERS.' + trigger | uppercase | translate }}
</mat-checkbox>
</ng-template>
</div>

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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<RuleTriggersUiComponent>;
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();
});
});

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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);
}
}

View File

@@ -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<Rule[]>([]);
rulesListing$: Observable<Rule[]> = this.rulesListingSource.asObservable();
private folderInfoSource = new BehaviorSubject<NodeInfo>(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 || ''
};
}
}