mirror of
https://github.com/Alfresco/alfresco-content-app.git
synced 2025-07-24 17:31:52 +00:00
[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:
@@ -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": {
|
||||
|
@@ -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 {
|
||||
|
@@ -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>
|
||||
|
@@ -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;
|
||||
|
||||
}
|
||||
|
@@ -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'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@@ -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',
|
||||
|
@@ -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',
|
||||
|
@@ -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[];
|
||||
}
|
||||
|
@@ -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">
|
||||
|
@@ -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 {
|
||||
|
@@ -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;
|
||||
|
||||
|
@@ -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>
|
||||
|
@@ -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();
|
||||
});
|
||||
});
|
||||
|
@@ -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();
|
||||
}
|
||||
}
|
||||
|
@@ -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">
|
||||
|
@@ -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 }
|
||||
|
@@ -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>
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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();
|
||||
});
|
||||
});
|
||||
|
@@ -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';
|
||||
}
|
||||
|
@@ -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>
|
@@ -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();
|
||||
});
|
||||
});
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -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 || ''
|
||||
};
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user