[ACS-3257] Create / Update rules dialog condition section (#2585)

* First commit: simple and composite condition UI

* Styling, readonly mode, etc...

* Unit tests for RuleSimpleConditionUiComponent

* Validation for composite condition

* Add unit tests for composite conditions

* Revert manage rules screen

* Reset karma conf singleRun to true

* Couple of small changes

* Typo
This commit is contained in:
Thomas Hunter 2022-08-18 17:39:36 +01:00 committed by GitHub
parent b082aaa011
commit 9a650f5265
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 1006 additions and 15 deletions

View File

@ -21,8 +21,47 @@
"NO_DESCRIPTION": "No description"
},
"ERROR": {
"REQUIRED": "This field is required"
}
"REQUIRED": "This field is required",
"RULE_COMPOSITE_CONDITION_INVALID": "One or more condition groups is empty"
},
"COMPARATORS": {
"EQUALS": "(=) Equals",
"CONTAINS": "Contains",
"STARTS_WITH": "Starts with",
"ENDS_WITH": "Ends with",
"GREATER_THAN": "(>) Greater than",
"LESS_THAN": "(<) Less than",
"GREATER_THAN_OR_EQUAL": "(>=) Greater than or equal",
"LESS_THAN_OR_EQUAL": "(<=) Less than or equal",
"ON": "(=) On",
"AFTER": "(>) After",
"BEFORE": "(<) Before",
"ON_OR_AFTER": "(>=) On or after",
"ON_OR_BEFORE": "(<=) On or before",
"INSTANCE_OF": "(=) Is"
},
"FIELDS": {
"NAME": "Name",
"SIZE": "Size",
"MIMETYPE": "Mimetype",
"ENCODING": "Encoding",
"HAS_CATEGORY": "Has category",
"HAS_TAG": "Has tag",
"HAS_ASPECT": "Has aspect"
},
"LOGIC_OPERATORS": {
"IF": "If",
"NOT_IF": "NOT If",
"AND": "And",
"OR": "Or"
},
"ACTIONS": {
"ADD_CONDITION": "Add condition",
"ADD_GROUP": "Add group",
"REMOVE": "Remove"
},
"NO_CONDITIONS": "No conditions",
"NO_CONDITIONS_IN_GROUP": "No conditions in the group"
}
}
}

View File

@ -32,7 +32,9 @@ import { RouterModule, Routes } from '@angular/router';
import { EditRuleDialogSmartComponent } from './rule-details/edit-rule-dialog.smart-component';
import { ManageRulesSmartComponent } from './manage-rules/manage-rules.smart-component';
import { RuleCompositeConditionUiComponent } from './rule-details/conditions/rule-composite-condition.ui-component';
import { RuleDetailsUiComponent } from './rule-details/rule-details.ui-component';
import { RuleSimpleConditionUiComponent } from './rule-details/conditions/rule-simple-condition.ui-component';
const routes: Routes = [
{
@ -44,7 +46,13 @@ const routes: Routes = [
@NgModule({
providers: [provideExtensionConfig(['folder-rules.plugin.json'])],
imports: [CommonModule, RouterModule.forChild(routes), CoreModule.forChild()],
declarations: [EditRuleDialogSmartComponent, ManageRulesSmartComponent, RuleDetailsUiComponent]
declarations: [
EditRuleDialogSmartComponent,
ManageRulesSmartComponent,
RuleCompositeConditionUiComponent,
RuleDetailsUiComponent,
RuleSimpleConditionUiComponent
]
})
export class AcaFolderRulesModule {
constructor(translation: TranslationService, extensions: ExtensionService) {

View File

@ -0,0 +1,68 @@
/*!
* @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 { RuleCompositeCondition } from '../model/rule-composite-condition.model';
import { RuleSimpleCondition } from '../model/rule-simple-condition.model';
const simpleConditionMock: RuleSimpleCondition = {
field: 'cm:name',
comparator: 'equals',
parameter: ''
};
const emptyCompositeConditionMock: RuleCompositeCondition = {
inverted: false,
booleanMode: 'and',
compositeConditions: [],
simpleConditions: []
};
const compositeConditionWithOneConditionMock: RuleCompositeCondition = {
...emptyCompositeConditionMock,
simpleConditions: [{ ...simpleConditionMock }]
};
export const compositeConditionWithOneGroupMock: RuleCompositeCondition = {
...emptyCompositeConditionMock,
compositeConditions: [{ ...compositeConditionWithOneConditionMock }]
};
export const compositeConditionWithNestedGroupsMock: RuleCompositeCondition = {
...emptyCompositeConditionMock,
compositeConditions: [
{
...emptyCompositeConditionMock,
compositeConditions: [{ ...compositeConditionWithOneConditionMock }]
},
{ ...compositeConditionWithOneConditionMock }
]
};
export const compositeConditionWithThreeConditionMock: RuleCompositeCondition = {
inverted: false,
booleanMode: 'and',
compositeConditions: [],
simpleConditions: [{ ...simpleConditionMock }, { ...simpleConditionMock }, { ...simpleConditionMock }]
};

View File

@ -0,0 +1,68 @@
<form class="aca-rule-composite-condition__form" [formGroup]="form">
<div *ngIf="hasNoConditions" class="aca-rule-composite-condition__form__no-conditions" data-automation-id="no-conditions">
{{ 'ACA_FOLDER_RULES.RULE_DETAILS.' + (childCondition ? 'NO_CONDITIONS_IN_GROUP' : 'NO_CONDITIONS') | translate }}
</div>
<div
class="aca-rule-composite-condition__form__row"
*ngFor="let control of conditionFormControls; let i = index">
<mat-form-field *ngIf="i === 0">
<mat-select
[value]="invertedControl.value"
[disabled]="readOnly"
(selectionChange)="setInverted($event.value)">
<mat-option [value]="false">{{ 'ACA_FOLDER_RULES.RULE_DETAILS.LOGIC_OPERATORS.IF' | translate }}</mat-option>
<mat-option [value]="true">{{ 'ACA_FOLDER_RULES.RULE_DETAILS.LOGIC_OPERATORS.NOT_IF' | translate }}</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field *ngIf="i > 0">
<mat-select
[value]="booleanModeControl.value"
[disabled]="i > 1 || readOnly"
(selectionChange)="setBooleanMode($event.value)">
<mat-option value="and">{{ 'ACA_FOLDER_RULES.RULE_DETAILS.LOGIC_OPERATORS.AND' | translate }}</mat-option>
<mat-option value="or">{{ 'ACA_FOLDER_RULES.RULE_DETAILS.LOGIC_OPERATORS.OR' | translate }}</mat-option>
</mat-select>
</mat-form-field>
<aca-rule-composite-condition
*ngIf="!isFormControlSimpleCondition(control)"
[secondaryBackground]="!secondaryBackground"
[childCondition]="true"
[formControl]="control">
</aca-rule-composite-condition>
<aca-rule-simple-condition
*ngIf="isFormControlSimpleCondition(control)"
[formControl]="control">
</aca-rule-simple-condition>
<button mat-icon-button [matMenuTriggerFor]="menu" *ngIf="!readOnly" data-automation-id="condition-actions-button">
<mat-icon>more_vert</mat-icon>
</button>
<mat-menu #menu="matMenu">
<button
mat-menu-item
[title]="'ACA_FOLDER_RULES.RULE_DETAILS.ACTIONS.REMOVE' | translate"
(click)="removeCondition(control)">
<mat-icon>delete</mat-icon>
<span>{{ 'ACA_FOLDER_RULES.RULE_DETAILS.ACTIONS.REMOVE' | translate }}</span>
</button>
</mat-menu>
</div>
<div class="aca-rule-composite-condition__form__actions" *ngIf="!readOnly" data-automation-id="add-actions">
<button mat-button (click)="addSimpleCondition()" data-automation-id="add-condition-button">
<mat-icon>add</mat-icon>
<span>{{ 'ACA_FOLDER_RULES.RULE_DETAILS.ACTIONS.ADD_CONDITION' | translate }}</span>
</button>
<button mat-button (click)="addCompositeCondition()" data-automation-id="add-group-button">
<mat-icon>add</mat-icon>
<span>{{ 'ACA_FOLDER_RULES.RULE_DETAILS.ACTIONS.ADD_GROUP' | translate }}</span>
</button>
</div>
</form>

View File

@ -0,0 +1,38 @@
.aca-rule-composite-condition {
display: block;
border-radius: 8px;
background-color: hsl(0,0%,100%);
&.childCompositeCondition {
padding: 8px 16px;
}
&.secondaryBackground {
background-color: hsl(0,0%,95%);
}
&__form {
display: flex;
flex-direction: column;
gap: 8px;
&__no-conditions {
color: var(--theme-disabled-text-color);
margin: 0.5em 0;
}
&__row {
display: flex;
gap: 8px;
& > :nth-child(1) {
width: 5em;
font-size: inherit;
}
& > :nth-child(2) {
flex: 1;
}
}
}
}

View File

@ -0,0 +1,143 @@
/*!
* @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 { ComponentFixture, TestBed } from '@angular/core/testing';
import { RuleCompositeConditionUiComponent } from './rule-composite-condition.ui-component';
import { CoreTestingModule } from '@alfresco/adf-core';
import { By } from '@angular/platform-browser';
import { DebugElement } from '@angular/core';
import {
compositeConditionWithNestedGroupsMock,
compositeConditionWithOneGroupMock,
compositeConditionWithThreeConditionMock
} from '../../mock/conditions.mock';
import { RuleSimpleConditionUiComponent } from './rule-simple-condition.ui-component';
describe('RuleCompositeConditionUiComponent', () => {
let fixture: ComponentFixture<RuleCompositeConditionUiComponent>;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [CoreTestingModule],
declarations: [RuleCompositeConditionUiComponent, RuleSimpleConditionUiComponent]
});
fixture = TestBed.createComponent(RuleCompositeConditionUiComponent);
});
describe('No conditions', () => {
let noConditionsElement: DebugElement;
beforeEach(() => {
fixture.detectChanges();
noConditionsElement = fixture.debugElement.query(By.css(`[data-automation-id="no-conditions"]`));
});
it('should default to no conditions', () => {
const rowElements = fixture.debugElement.queryAll(By.css(`.aca-rule-composite-condition__form__row`));
expect(rowElements.length).toBe(0);
});
it('should show a message if there are no conditions', () => {
fixture.componentInstance.childCondition = false;
fixture.detectChanges();
expect((noConditionsElement.nativeElement as HTMLElement).innerText.trim()).toBe('ACA_FOLDER_RULES.RULE_DETAILS.NO_CONDITIONS');
});
it('should show a different message if the component is not a root condition group', () => {
fixture.componentInstance.childCondition = true;
fixture.detectChanges();
expect((noConditionsElement.nativeElement as HTMLElement).innerText.trim()).toBe('ACA_FOLDER_RULES.RULE_DETAILS.NO_CONDITIONS_IN_GROUP');
});
});
describe('Read only mode', () => {
it('should hide the add buttons in read only mode', () => {
fixture.componentInstance.setDisabledState(true);
fixture.detectChanges();
const actionsElement = fixture.debugElement.query(By.css(`[data-automation-id="add-actions"]`));
expect(actionsElement).toBeNull();
});
it('should hide the more actions button on the right side of the condition', () => {
fixture.componentInstance.writeValue(compositeConditionWithOneGroupMock);
fixture.componentInstance.setDisabledState(true);
fixture.detectChanges();
const actionsButtonElements = fixture.debugElement.queryAll(By.css(`[data-automation-id="condition-actions-button"]`));
expect(actionsButtonElements.length).toBe(0);
});
});
it('should have as many simple condition components as defined in the simpleConditions array', () => {
fixture.componentInstance.writeValue(compositeConditionWithThreeConditionMock);
fixture.detectChanges();
const simpleConditionComponents = fixture.debugElement.queryAll(By.css(`.aca-rule-simple-condition`));
expect(simpleConditionComponents.length).toBe(3);
});
it('should have as many composite condition components as defined in the compositeConditions array, including nested', () => {
fixture.componentInstance.writeValue(compositeConditionWithNestedGroupsMock);
fixture.detectChanges();
const compositeConditionComponents = fixture.debugElement.queryAll(By.css(`.aca-rule-composite-condition`));
expect(compositeConditionComponents.length).toBe(3);
});
it('should add a simple condition component on click of add condition button', () => {
fixture.detectChanges();
const predicate = By.css(`.aca-rule-simple-condition`);
const simpleConditionComponentsBeforeClick = fixture.debugElement.queryAll(predicate);
expect(simpleConditionComponentsBeforeClick.length).toBe(0);
const addConditionButton = fixture.debugElement.query(By.css(`[data-automation-id="add-condition-button"]`));
(addConditionButton.nativeElement as HTMLButtonElement).click();
fixture.detectChanges();
const simpleConditionComponentsAfterClick = fixture.debugElement.queryAll(predicate);
expect(simpleConditionComponentsAfterClick.length).toBe(1);
});
it('should add a composite condition component on click of add group button', () => {
fixture.detectChanges();
const predicate = By.css(`.aca-rule-composite-condition`);
const compositeConditionComponentsBeforeClick = fixture.debugElement.queryAll(predicate);
expect(compositeConditionComponentsBeforeClick.length).toBe(0);
const addGroupButton = fixture.debugElement.query(By.css(`[data-automation-id="add-group-button"]`));
(addGroupButton.nativeElement as HTMLButtonElement).click();
fixture.detectChanges();
const compositeConditionComponentsAfterClick = fixture.debugElement.queryAll(predicate);
expect(compositeConditionComponentsAfterClick.length).toBe(1);
});
});

View File

@ -0,0 +1,157 @@
/*!
* @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, HostBinding, Input, OnDestroy, ViewEncapsulation } from '@angular/core';
import { RuleCompositeCondition } from '../../model/rule-composite-condition.model';
import { ControlValueAccessor, FormArray, FormControl, FormGroup, NG_VALUE_ACCESSOR } from '@angular/forms';
import { RuleSimpleCondition } from '../../model/rule-simple-condition.model';
@Component({
selector: 'aca-rule-composite-condition',
templateUrl: './rule-composite-condition.ui-component.html',
styleUrls: ['./rule-composite-condition.ui-component.scss'],
encapsulation: ViewEncapsulation.None,
host: { class: 'aca-rule-composite-condition' },
providers: [
{
provide: NG_VALUE_ACCESSOR,
multi: true,
useExisting: forwardRef(() => RuleCompositeConditionUiComponent)
}
]
})
export class RuleCompositeConditionUiComponent implements ControlValueAccessor, OnDestroy {
@HostBinding('class.secondaryBackground')
@Input()
secondaryBackground = false;
@HostBinding('class.childCompositeCondition')
@Input()
childCondition = false;
form = new FormGroup({
inverted: new FormControl(),
booleanMode: new FormControl(),
compositeConditions: new FormArray([]),
simpleConditions: new FormArray([])
});
private formSubscription = this.form.valueChanges.subscribe((value) => {
this.onChange(value);
this.onTouch();
});
get invertedControl(): FormControl {
return this.form.get('inverted') as FormControl;
}
get booleanModeControl(): FormControl {
return this.form.get('booleanMode') as FormControl;
}
get compositeConditionsFormArray(): FormArray {
return this.form.get('compositeConditions') as FormArray;
}
get simpleConditionsFormArray(): FormArray {
return this.form.get('simpleConditions') as FormArray;
}
get conditionFormControls(): FormControl[] {
return [...(this.compositeConditionsFormArray.controls as FormControl[]), ...(this.simpleConditionsFormArray.controls as FormControl[])];
}
get hasNoConditions(): boolean {
return this.conditionFormControls.length === 0;
}
private _readOnly = false;
get readOnly(): boolean {
return this._readOnly;
}
onChange: (condition: RuleCompositeCondition) => void = () => undefined;
onTouch: () => void = () => undefined;
writeValue(value: RuleCompositeCondition) {
this.form.get('inverted').setValue(value.inverted);
this.form.get('booleanMode').setValue(value.booleanMode);
this.form.setControl('compositeConditions', new FormArray(value.compositeConditions.map((condition) => new FormControl(condition))));
this.form.setControl('simpleConditions', new FormArray(value.simpleConditions.map((condition) => new FormControl(condition))));
}
registerOnChange(fn: () => void) {
this.onChange = fn;
}
registerOnTouched(fn: () => void) {
this.onTouch = fn;
}
setDisabledState(isDisabled: boolean) {
if (isDisabled) {
this._readOnly = true;
this.form.disable();
} else {
this._readOnly = false;
this.form.enable();
}
}
setInverted(value: boolean) {
this.invertedControl.setValue(value);
}
setBooleanMode(value: 'and' | 'or') {
this.booleanModeControl.setValue(value);
}
isFormControlSimpleCondition(control: FormControl): boolean {
return control.value.hasOwnProperty('field');
}
removeCondition(control: FormControl) {
const formArray = this.isFormControlSimpleCondition(control) ? this.simpleConditionsFormArray : this.compositeConditionsFormArray;
const index = (formArray.value as FormControl[]).indexOf(control.value);
formArray.removeAt(index);
}
addSimpleCondition() {
const newCondition: RuleSimpleCondition = {
field: 'cm:name',
comparator: 'equals',
parameter: ''
};
this.simpleConditionsFormArray.push(new FormControl(newCondition));
}
addCompositeCondition() {
const newCondition: RuleCompositeCondition = {
inverted: false,
booleanMode: 'and',
compositeConditions: [],
simpleConditions: []
};
this.compositeConditionsFormArray.push(new FormControl(newCondition));
}
ngOnDestroy() {
this.formSubscription.unsubscribe();
}
}

View File

@ -0,0 +1,42 @@
/*!
* @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 { RuleCompositeCondition } from '../../model/rule-composite-condition.model';
import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms';
const isCompositeConditionValid = (value: RuleCompositeCondition, isRootCondition = true): boolean => {
if (value.compositeConditions.length > 0) {
return value.compositeConditions.reduce(
(arrayValid: boolean, nestedCondition: RuleCompositeCondition) => arrayValid && isCompositeConditionValid(nestedCondition, false),
true
);
}
return !!value.simpleConditions.length || isRootCondition;
};
export const ruleCompositeConditionValidator =
(): ValidatorFn =>
(control: AbstractControl): ValidationErrors | null =>
isCompositeConditionValid(control.value) ? null : { ruleCompositeConditionInvalid: true };

View File

@ -0,0 +1,95 @@
/*!
* @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/>.
*/
export interface RuleConditionComparator {
name: string;
labels: {
[key: string]: string;
};
}
export const ruleConditionComparators: RuleConditionComparator[] = [
{
name: 'equals',
labels: {
string: 'ACA_FOLDER_RULES.RULE_DETAILS.COMPARATORS.EQUALS',
number: 'ACA_FOLDER_RULES.RULE_DETAILS.COMPARATORS.EQUALS',
date: 'ACA_FOLDER_RULES.RULE_DETAILS.COMPARATORS.ON',
special: ''
}
},
{
name: 'contains',
labels: {
string: 'ACA_FOLDER_RULES.RULE_DETAILS.COMPARATORS.CONTAINS'
}
},
{
name: 'startsWith',
labels: {
string: 'ACA_FOLDER_RULES.RULE_DETAILS.COMPARATORS.STARTS_WITH'
}
},
{
name: 'endsWith',
labels: {
string: 'ACA_FOLDER_RULES.RULE_DETAILS.COMPARATORS.ENDS_WITH'
}
},
{
name: 'greaterThan',
labels: {
number: 'ACA_FOLDER_RULES.RULE_DETAILS.COMPARATORS.GREATER_THAN',
date: 'ACA_FOLDER_RULES.RULE_DETAILS.COMPARATORS.AFTER'
}
},
{
name: 'lessThan',
labels: {
number: 'ACA_FOLDER_RULES.RULE_DETAILS.COMPARATORS.LESS_THAN',
date: 'ACA_FOLDER_RULES.RULE_DETAILS.COMPARATORS.BEFORE'
}
},
{
name: 'greaterThanOrEqual',
labels: {
number: 'ACA_FOLDER_RULES.RULE_DETAILS.COMPARATORS.GREATER_THAN_OR_EQUAL',
date: 'ACA_FOLDER_RULES.RULE_DETAILS.COMPARATORS.ON_OR_AFTER'
}
},
{
name: 'lessThanOrEqual',
labels: {
number: 'ACA_FOLDER_RULES.RULE_DETAILS.COMPARATORS.LESS_THAN_OR_EQUAL',
date: 'ACA_FOLDER_RULES.RULE_DETAILS.COMPARATORS.ON_OR_BEFORE'
}
},
{
name: 'instanceOf',
labels: {
type: 'ACA_FOLDER_RULES.RULE_DETAILS.COMPARATORS.INSTANCE_OF'
}
}
];

View File

@ -0,0 +1,70 @@
/*!
* @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/>.
*/
export type RuleConditionFieldType = 'string' | 'number' | 'date' | 'type' | 'special';
export interface RuleConditionField {
name: string;
label: string;
type: RuleConditionFieldType;
}
export const ruleConditionFields: RuleConditionField[] = [
{
name: 'cm:name',
label: 'ACA_FOLDER_RULES.RULE_DETAILS.FIELDS.NAME',
type: 'string'
},
{
name: 'size',
label: 'ACA_FOLDER_RULES.RULE_DETAILS.FIELDS.SIZE',
type: 'number'
},
{
name: 'mimetype',
label: 'ACA_FOLDER_RULES.RULE_DETAILS.FIELDS.MIMETYPE',
type: 'string'
},
{
name: 'encoding',
label: 'ACA_FOLDER_RULES.RULE_DETAILS.FIELDS.ENCODING',
type: 'string'
},
{
name: 'category',
label: 'ACA_FOLDER_RULES.RULE_DETAILS.FIELDS.HAS_CATEGORY',
type: 'special'
},
{
name: 'tag',
label: 'ACA_FOLDER_RULES.RULE_DETAILS.FIELDS.HAS_TAG',
type: 'special'
},
{
name: 'aspect',
label: 'ACA_FOLDER_RULES.RULE_DETAILS.FIELDS.HAS_ASPECT',
type: 'special'
}
];

View File

@ -0,0 +1,24 @@
<form class="aca-rule-simple-condition__form" [formGroup]="form">
<mat-form-field class="aca-rule-simple-condition__form__field-input">
<mat-select formControlName="field" data-automation-id="field-select"
(selectionChange)="onChangeField()">
<mat-option *ngFor="let field of fields" [value]="field.name">
{{ field.label | translate }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field class="aca-rule-simple-condition__form__comparator-input" [class]="{ hidden: isComparatorHidden }" data-automation-id="comparator-form-field">
<mat-select formControlName="comparator" data-automation-id="comparator-select">
<mat-option
*ngFor="let comparator of selectedFieldComparators"
[value]="comparator.name">
{{ comparator.labels[this.selectedField?.type || 'equals'] | translate }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field class="aca-rule-simple-condition__form__parameter-input">
<input matInput type="text" formControlName="parameter" data-automation-id="value-input">
</mat-form-field>
</form>

View File

@ -0,0 +1,18 @@
.aca-rule-simple-condition {
&__form {
display: flex;
flex-direction: row;
gap: 8px;
mat-form-field {
flex: 2;
font-size: inherit;
}
&__comparator-input {
&.hidden {
display: none;
}
}
}
}

View File

@ -0,0 +1,92 @@
/*!
* @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 { ComponentFixture, TestBed } from '@angular/core/testing';
import { RuleSimpleConditionUiComponent } from './rule-simple-condition.ui-component';
import { CoreTestingModule } from '@alfresco/adf-core';
import { By } from '@angular/platform-browser';
import { DebugElement } from '@angular/core';
describe('RuleSimpleConditionUiComponent', () => {
let fixture: ComponentFixture<RuleSimpleConditionUiComponent>;
const getByDataAutomationId = (dataAutomationId: string): DebugElement =>
fixture.debugElement.query(By.css(`[data-automation-id="${dataAutomationId}"]`));
const changeMatSelectValue = (dataAutomationId: string, value: string) => {
const matSelect = getByDataAutomationId(dataAutomationId).nativeElement;
matSelect.click();
fixture.detectChanges();
const matOption = fixture.debugElement.query(By.css(`.mat-option[ng-reflect-value="${value}"]`)).nativeElement;
matOption.click();
fixture.detectChanges();
};
beforeEach(() => {
TestBed.configureTestingModule({
imports: [CoreTestingModule],
declarations: [RuleSimpleConditionUiComponent]
});
fixture = TestBed.createComponent(RuleSimpleConditionUiComponent);
});
it('should default the field to the name, the comparator to equals and the value empty', () => {
fixture.detectChanges();
expect(getByDataAutomationId('field-select').componentInstance.value).toBe('cm:name');
expect(getByDataAutomationId('comparator-select').componentInstance.value).toBe('equals');
expect(getByDataAutomationId('value-input').nativeElement.value).toBe('');
});
it('should hide the comparator select box if the type of the field is special', () => {
fixture.detectChanges();
const comparatorFormField = getByDataAutomationId('comparator-form-field').nativeElement;
expect(fixture.componentInstance.isComparatorHidden).toBeFalsy();
expect(getComputedStyle(comparatorFormField).display).not.toBe('none');
changeMatSelectValue('field-select', 'category');
expect(fixture.componentInstance.isComparatorHidden).toBeTruthy();
expect(getComputedStyle(comparatorFormField).display).toBe('none');
});
it('should set the comparator to equals if the field is set to a type with different comparators', () => {
const onChangeFieldSpy = spyOn(fixture.componentInstance, 'onChangeField').and.callThrough();
fixture.detectChanges();
changeMatSelectValue('comparator-select', 'contains');
expect(getByDataAutomationId('comparator-select').componentInstance.value).toBe('contains');
changeMatSelectValue('field-select', 'mimetype');
expect(onChangeFieldSpy).toHaveBeenCalledTimes(1);
expect(getByDataAutomationId('comparator-select').componentInstance.value).toBe('contains');
changeMatSelectValue('field-select', 'size');
expect(onChangeFieldSpy).toHaveBeenCalledTimes(2);
expect(getByDataAutomationId('comparator-select').componentInstance.value).toBe('equals');
});
});

View File

@ -0,0 +1,105 @@
/*!
* @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, 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';
import { RuleConditionComparator, ruleConditionComparators } from './rule-condition-comparators';
@Component({
selector: 'aca-rule-simple-condition',
templateUrl: './rule-simple-condition.ui-component.html',
styleUrls: ['./rule-simple-condition.ui-component.scss'],
encapsulation: ViewEncapsulation.None,
host: { class: 'aca-rule-simple-condition' },
providers: [
{
provide: NG_VALUE_ACCESSOR,
multi: true,
useExisting: forwardRef(() => RuleSimpleConditionUiComponent)
}
]
})
export class RuleSimpleConditionUiComponent implements ControlValueAccessor, OnDestroy {
readonly fields = ruleConditionFields;
form = new FormGroup({
field: new FormControl('cm:name'),
comparator: new FormControl('equals'),
parameter: new FormControl()
});
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 selectedFieldComparators(): RuleConditionComparator[] {
return ruleConditionComparators.filter((comparator) => Object.keys(comparator.labels).includes(this.selectedField.type));
}
get isComparatorHidden(): boolean {
return this.selectedField?.type === 'special';
}
get comparatorControl(): AbstractControl {
return this.form.get('comparator');
}
onChange: (condition: RuleSimpleCondition) => void = () => undefined;
onTouch: () => void = () => undefined;
writeValue(value: RuleSimpleCondition) {
this.form.setValue(value);
}
registerOnChange(fn: () => void) {
this.onChange = fn;
}
registerOnTouched(fn: () => void) {
this.onTouch = fn;
}
setDisabledState(isDisabled: boolean) {
if (isDisabled) {
this.form.disable();
} else {
this.form.enable();
}
}
onChangeField() {
if (!this.selectedFieldComparators.find((comparator) => comparator.name === this.comparatorControl.value)) {
this.comparatorControl.setValue('equals');
}
}
ngOnDestroy() {
this.formSubscription.unsubscribe();
}
}

View File

@ -29,6 +29,7 @@ import { By } from '@angular/platform-browser';
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';
describe('EditRuleDialogComponent', () => {
let fixture: ComponentFixture<EditRuleDialogSmartComponent>;
@ -41,7 +42,7 @@ describe('EditRuleDialogComponent', () => {
const setupBeforeEach = (dialogOptions: EditRuleDialogOptions = {}) => {
TestBed.configureTestingModule({
imports: [CoreTestingModule],
declarations: [EditRuleDialogSmartComponent, RuleDetailsUiComponent],
declarations: [EditRuleDialogSmartComponent, RuleCompositeConditionUiComponent, RuleDetailsUiComponent],
providers: [
{ provide: MatDialogRef, useValue: dialogRef },
{ provide: MAT_DIALOG_DATA, useValue: dialogOptions }
@ -105,7 +106,7 @@ describe('EditRuleDialogComponent', () => {
expect(titleElement.innerText.trim()).toBe('ACA_FOLDER_RULES.EDIT_RULE_DIALOG.UPDATE_TITLE');
});
it('should show a "create" label in the submit button', () => {
it('should show an "update" label in the submit button', () => {
fixture.detectChanges();
const titleElement = fixture.debugElement.query(By.css('[data-automation-id="edit-rule-dialog-submit"]')).nativeElement as HTMLButtonElement;

View File

@ -24,4 +24,9 @@
</mat-form-field>
</div>
</div>
<hr>
<aca-rule-composite-condition formControlName="conditions"></aca-rule-composite-condition>
<mat-error>{{ getErrorMessage(conditions) | translate }}</mat-error>
</form>

View File

@ -33,12 +33,17 @@
border-bottom: 1px solid var(--theme-border-color);
}
*:disabled {
color: #000000;
*:disabled, .mat-select-disabled .mat-select-value {
color: inherit;
}
textarea {
min-height: 4em;
}
& > .aca-rule-composite-condition + .mat-error {
font-size: 75%;
margin-left: 16px;
}
}
}

View File

@ -28,6 +28,7 @@ import { CoreTestingModule } from '@alfresco/adf-core';
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';
describe('RuleDetailsUiComponent', () => {
let fixture: ComponentFixture<RuleDetailsUiComponent>;
@ -45,7 +46,7 @@ describe('RuleDetailsUiComponent', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [CoreTestingModule],
declarations: [RuleDetailsUiComponent]
declarations: [RuleCompositeConditionUiComponent, RuleDetailsUiComponent]
});
fixture = TestBed.createComponent(RuleDetailsUiComponent);

View File

@ -24,10 +24,11 @@
*/
import { Component, EventEmitter, Input, OnDestroy, OnInit, Output, ViewEncapsulation } from '@angular/core';
import { AbstractControl, FormBuilder, FormGroup, Validators } from '@angular/forms';
import { AbstractControl, FormControl, FormGroup, Validators } from '@angular/forms';
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';
@Component({
selector: 'aca-rule-details',
@ -66,17 +67,26 @@ export class RuleDetailsUiComponent implements OnInit, OnDestroy {
get name(): AbstractControl {
return this.form.get('name');
}
get description(): AbstractControl {
return this.form.get('description');
}
constructor(private formBuilder: FormBuilder) {}
get conditions(): AbstractControl {
return this.form.get('conditions');
}
ngOnInit() {
this.form = this.formBuilder.group({
name: [this.initialValue.name || '', Validators.required],
description: [this.initialValue.description || '']
this.form = new FormGroup({
name: new FormControl(this.initialValue.name || '', Validators.required),
description: new FormControl(this.initialValue.description || ''),
conditions: new FormControl(
this.initialValue.conditions || {
inverted: false,
booleanMode: 'and',
compositeConditions: [],
simpleConditions: []
},
ruleCompositeConditionValidator()
)
});
this.readOnly = this._readOnly;
@ -104,6 +114,8 @@ export class RuleDetailsUiComponent implements OnInit, OnDestroy {
getErrorMessage(control: AbstractControl): string {
if (control.hasError('required')) {
return 'ACA_FOLDER_RULES.RULE_DETAILS.ERROR.REQUIRED';
} else if (control.hasError('ruleCompositeConditionInvalid')) {
return 'ACA_FOLDER_RULES.RULE_DETAILS.ERROR.RULE_COMPOSITE_CONDITION_INVALID';
}
return '';
}