diff --git a/projects/aca-folder-rules/assets/i18n/en.json b/projects/aca-folder-rules/assets/i18n/en.json index 80d421deb..2484d6988 100644 --- a/projects/aca-folder-rules/assets/i18n/en.json +++ b/projects/aca-folder-rules/assets/i18n/en.json @@ -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" } } } 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 4938df587..71de638bb 100644 --- a/projects/aca-folder-rules/src/lib/folder-rules.module.ts +++ b/projects/aca-folder-rules/src/lib/folder-rules.module.ts @@ -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) { diff --git a/projects/aca-folder-rules/src/lib/mock/conditions.mock.ts b/projects/aca-folder-rules/src/lib/mock/conditions.mock.ts new file mode 100644 index 000000000..7b5bb5d10 --- /dev/null +++ b/projects/aca-folder-rules/src/lib/mock/conditions.mock.ts @@ -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 . + */ + +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 }] +}; 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 new file mode 100644 index 000000000..165f74057 --- /dev/null +++ b/projects/aca-folder-rules/src/lib/rule-details/conditions/rule-composite-condition.ui-component.html @@ -0,0 +1,68 @@ +
+
+ {{ 'ACA_FOLDER_RULES.RULE_DETAILS.' + (childCondition ? 'NO_CONDITIONS_IN_GROUP' : 'NO_CONDITIONS') | translate }} +
+ +
+ + + + {{ 'ACA_FOLDER_RULES.RULE_DETAILS.LOGIC_OPERATORS.IF' | translate }} + {{ 'ACA_FOLDER_RULES.RULE_DETAILS.LOGIC_OPERATORS.NOT_IF' | translate }} + + + + + + {{ 'ACA_FOLDER_RULES.RULE_DETAILS.LOGIC_OPERATORS.AND' | translate }} + {{ 'ACA_FOLDER_RULES.RULE_DETAILS.LOGIC_OPERATORS.OR' | translate }} + + + + + + + + + + + + + + + +
+ +
+ + +
+
diff --git a/projects/aca-folder-rules/src/lib/rule-details/conditions/rule-composite-condition.ui-component.scss b/projects/aca-folder-rules/src/lib/rule-details/conditions/rule-composite-condition.ui-component.scss new file mode 100644 index 000000000..e9d1709cd --- /dev/null +++ b/projects/aca-folder-rules/src/lib/rule-details/conditions/rule-composite-condition.ui-component.scss @@ -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; + } + } + } +} diff --git a/projects/aca-folder-rules/src/lib/rule-details/conditions/rule-composite-condition.ui-component.spec.ts b/projects/aca-folder-rules/src/lib/rule-details/conditions/rule-composite-condition.ui-component.spec.ts new file mode 100644 index 000000000..ed9cc089c --- /dev/null +++ b/projects/aca-folder-rules/src/lib/rule-details/conditions/rule-composite-condition.ui-component.spec.ts @@ -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 . + */ + +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; + + 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); + }); +}); diff --git a/projects/aca-folder-rules/src/lib/rule-details/conditions/rule-composite-condition.ui-component.ts b/projects/aca-folder-rules/src/lib/rule-details/conditions/rule-composite-condition.ui-component.ts new file mode 100644 index 000000000..f1ae96d81 --- /dev/null +++ b/projects/aca-folder-rules/src/lib/rule-details/conditions/rule-composite-condition.ui-component.ts @@ -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 . + */ + +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(); + } +} 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/conditions/rule-composite-condition.validators.ts new file mode 100644 index 000000000..cbd36f5fe --- /dev/null +++ b/projects/aca-folder-rules/src/lib/rule-details/conditions/rule-composite-condition.validators.ts @@ -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 . + */ + +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 }; diff --git a/projects/aca-folder-rules/src/lib/rule-details/conditions/rule-condition-comparators.ts b/projects/aca-folder-rules/src/lib/rule-details/conditions/rule-condition-comparators.ts new file mode 100644 index 000000000..f7b31bb6d --- /dev/null +++ b/projects/aca-folder-rules/src/lib/rule-details/conditions/rule-condition-comparators.ts @@ -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 . + */ + +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' + } + } +]; diff --git a/projects/aca-folder-rules/src/lib/rule-details/conditions/rule-condition-fields.ts b/projects/aca-folder-rules/src/lib/rule-details/conditions/rule-condition-fields.ts new file mode 100644 index 000000000..d7af1b30e --- /dev/null +++ b/projects/aca-folder-rules/src/lib/rule-details/conditions/rule-condition-fields.ts @@ -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 . + */ + +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' + } +]; diff --git a/projects/aca-folder-rules/src/lib/rule-details/conditions/rule-simple-condition.ui-component.html b/projects/aca-folder-rules/src/lib/rule-details/conditions/rule-simple-condition.ui-component.html new file mode 100644 index 000000000..381d127f3 --- /dev/null +++ b/projects/aca-folder-rules/src/lib/rule-details/conditions/rule-simple-condition.ui-component.html @@ -0,0 +1,24 @@ +
+ + + + {{ field.label | translate }} + + + + + + + + {{ comparator.labels[this.selectedField?.type || 'equals'] | translate }} + + + + + + + +
diff --git a/projects/aca-folder-rules/src/lib/rule-details/conditions/rule-simple-condition.ui-component.scss b/projects/aca-folder-rules/src/lib/rule-details/conditions/rule-simple-condition.ui-component.scss new file mode 100644 index 000000000..1d9e989ed --- /dev/null +++ b/projects/aca-folder-rules/src/lib/rule-details/conditions/rule-simple-condition.ui-component.scss @@ -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; + } + } + } +} diff --git a/projects/aca-folder-rules/src/lib/rule-details/conditions/rule-simple-condition.ui-component.spec.ts b/projects/aca-folder-rules/src/lib/rule-details/conditions/rule-simple-condition.ui-component.spec.ts new file mode 100644 index 000000000..69a943b58 --- /dev/null +++ b/projects/aca-folder-rules/src/lib/rule-details/conditions/rule-simple-condition.ui-component.spec.ts @@ -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 . + */ + +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; + + 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'); + }); +}); diff --git a/projects/aca-folder-rules/src/lib/rule-details/conditions/rule-simple-condition.ui-component.ts b/projects/aca-folder-rules/src/lib/rule-details/conditions/rule-simple-condition.ui-component.ts new file mode 100644 index 000000000..660b70cf0 --- /dev/null +++ b/projects/aca-folder-rules/src/lib/rule-details/conditions/rule-simple-condition.ui-component.ts @@ -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 . + */ + +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(); + } +} 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 84fc7f947..0c339758e 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 @@ -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; @@ -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; 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 492cacfd8..509562fb0 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 @@ -24,4 +24,9 @@ + +
+ + + {{ 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 ac2d8cccf..84186ab77 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 @@ -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; + } } } 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 2368a33a9..b223ec252 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 @@ -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; @@ -45,7 +46,7 @@ describe('RuleDetailsUiComponent', () => { beforeEach(() => { TestBed.configureTestingModule({ imports: [CoreTestingModule], - declarations: [RuleDetailsUiComponent] + declarations: [RuleCompositeConditionUiComponent, RuleDetailsUiComponent] }); fixture = TestBed.createComponent(RuleDetailsUiComponent); 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 1ba61a9d8..50939dbd3 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 @@ -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 ''; }