[ACA-3258] Edit rule dialog actions section (#2692)

* Add actions service

* Create components

* Rebasing

* Add card view component

* Moved actions definition call outside of actions list component

* Localisation of parameter and action labels + read only mode for components

* Remove action option

* Default to one item in array

* Handle change of cardview

* Linting

* Add unit tests

* Fix broken unit tests

* Fix unknown word

* Add private to property
This commit is contained in:
Thomas Hunter 2022-10-06 14:07:47 +01:00 committed by GitHub
parent 08d4f46573
commit 59c7d68299
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 914 additions and 43 deletions

View File

@ -15,6 +15,7 @@
"NAME": "Name", "NAME": "Name",
"DESCRIPTION": "Description", "DESCRIPTION": "Description",
"WHEN": "When", "WHEN": "When",
"PERFORM_ACTIONS": "Perform actions",
"OPTIONS": "Other options" "OPTIONS": "Other options"
}, },
"PLACEHOLDER": { "PLACEHOLDER": {
@ -70,21 +71,26 @@
"AND": "And", "AND": "And",
"OR": "Or" "OR": "Or"
}, },
"ACTIONS": { "CONDITION_BUTTONS": {
"ADD_CONDITION": "Add condition", "ADD_CONDITION": "Add condition",
"ADD_GROUP": "Add group", "ADD_GROUP": "Add condition group",
"REMOVE": "Remove" "REMOVE": "Remove"
}, },
"NO_CONDITIONS": "No conditions", "NO_CONDITIONS": "No conditions",
"NO_CONDITIONS_IN_GROUP": "No conditions in the group" "NO_CONDITIONS_IN_GROUP": "No conditions in the group",
"ACTION_BUTTONS": {
"ADD_ACTION": "Add action",
"REMOVE": "Remove"
}, },
"ACTION_SELECT_PLACEHOLDER": "Select an action"
},
"MANAGE_RULES": { "MANAGE_RULES": {
"TOOLBAR": { "TOOLBAR": {
"BREADCRUMB": { "BREADCRUMB": {
"RULES": "rules" "RULES": "rules"
}, },
"ACTIONS": { "ACTIONS": {
"NEW_RULE": "New rule" "CREATE_RULE": "Create rule"
} }
}, },
"EMPTY_RULES_LIST": { "EMPTY_RULES_LIST": {

View File

@ -40,6 +40,8 @@ import { RuleListItemUiComponent } from './rules-list/rule/rule-list-item.ui-com
import { RulesListUiComponent } from './rules-list/rules-list.ui-component'; import { RulesListUiComponent } from './rules-list/rules-list.ui-component';
import { RuleTriggersUiComponent } from './rule-details/triggers/rule-triggers.ui-component'; import { RuleTriggersUiComponent } from './rule-details/triggers/rule-triggers.ui-component';
import { RuleOptionsUiComponent } from './rule-details/options/rule-options.ui-component'; import { RuleOptionsUiComponent } from './rule-details/options/rule-options.ui-component';
import { RuleActionListUiComponent } from './rule-details/actions/rule-action-list.ui-component';
import { RuleActionUiComponent } from './rule-details/actions/rule-action.ui-component';
const routes: Routes = [ const routes: Routes = [
{ {
@ -63,6 +65,8 @@ const routes: Routes = [
declarations: [ declarations: [
EditRuleDialogSmartComponent, EditRuleDialogSmartComponent,
ManageRulesSmartComponent, ManageRulesSmartComponent,
RuleActionListUiComponent,
RuleActionUiComponent,
RuleCompositeConditionUiComponent, RuleCompositeConditionUiComponent,
RuleDetailsUiComponent, RuleDetailsUiComponent,
RuleSimpleConditionUiComponent, RuleSimpleConditionUiComponent,

View File

@ -12,7 +12,7 @@
<aca-page-layout-content> <aca-page-layout-content>
<div class="main-content"> <div class="main-content">
<ng-container *ngIf="isLoading$ | async; else onLoaded"> <ng-container *ngIf="(rulesLoading$ | async) || (actionsLoading$ | async); else onLoaded">
<mat-progress-bar color="primary" mode="indeterminate"></mat-progress-bar> <mat-progress-bar color="primary" mode="indeterminate"></mat-progress-bar>
</ng-container> </ng-container>
@ -25,7 +25,7 @@
class="aca-manage-rules__actions-bar__title__breadcrumb"></adf-breadcrumb> class="aca-manage-rules__actions-bar__title__breadcrumb"></adf-breadcrumb>
</adf-toolbar-title> </adf-toolbar-title>
<button mat-flat-button color="primary" (click)="openNewRuleDialog()">{{ 'ACA_FOLDER_RULES.MANAGE_RULES.TOOLBAR.ACTIONS.NEW_RULE' | translate }}</button> <button mat-flat-button color="primary" (click)="openNewRuleDialog()">{{ 'ACA_FOLDER_RULES.MANAGE_RULES.TOOLBAR.ACTIONS.CREATE_RULE' | translate }}</button>
</adf-toolbar> </adf-toolbar>
<mat-divider></mat-divider> <mat-divider></mat-divider>
@ -44,7 +44,12 @@
</div> </div>
<p>{{ selectedRule.description }}</p> <p>{{ selectedRule.description }}</p>
</div> </div>
<aca-rule-details [readOnly]="true" [preview]="true" [value]="selectedRule"></aca-rule-details> <aca-rule-details
[actionDefinitions]="actionDefinitions$ | async"
[readOnly]="true"
[preview]="true"
[value]="selectedRule">
</aca-rule-details>
</div> </div>
</div> </div>

View File

@ -17,7 +17,7 @@
&__container { &__container {
display: grid; display: grid;
grid-template-columns: 33% 66%; grid-template-columns: minmax(200px,1fr) 3fr;
padding: 32px; padding: 32px;
overflow: scroll; overflow: scroll;

View File

@ -34,12 +34,14 @@ import { dummyRules } from '../mock/rules.mock';
import { By } from '@angular/platform-browser'; import { By } from '@angular/platform-browser';
import { dummyNodeInfo } from '../mock/node.mock'; import { dummyNodeInfo } from '../mock/node.mock';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
import { ActionsService } from '../services/actions.service';
describe('ManageRulesSmartComponent', () => { describe('ManageRulesSmartComponent', () => {
let fixture: ComponentFixture<ManageRulesSmartComponent>; let fixture: ComponentFixture<ManageRulesSmartComponent>;
let component: ManageRulesSmartComponent; let component: ManageRulesSmartComponent;
let debugElement: DebugElement; let debugElement: DebugElement;
let folderRulesService: FolderRulesService; let folderRulesService: FolderRulesService;
let actionsService: ActionsService;
beforeEach( beforeEach(
waitForAsync(() => { waitForAsync(() => {
@ -57,6 +59,7 @@ describe('ManageRulesSmartComponent', () => {
component = fixture.componentInstance; component = fixture.componentInstance;
debugElement = fixture.debugElement; debugElement = fixture.debugElement;
folderRulesService = TestBed.inject<FolderRulesService>(FolderRulesService); folderRulesService = TestBed.inject<FolderRulesService>(FolderRulesService);
actionsService = TestBed.inject<ActionsService>(ActionsService);
}); });
}) })
); );
@ -66,6 +69,7 @@ describe('ManageRulesSmartComponent', () => {
folderRulesService.folderInfo$ = of(dummyNodeInfo); folderRulesService.folderInfo$ = of(dummyNodeInfo);
folderRulesService.rulesListing$ = of(dummyRules); folderRulesService.rulesListing$ = of(dummyRules);
folderRulesService.loading$ = of(false); folderRulesService.loading$ = of(false);
actionsService.loading$ = of(false);
fixture.detectChanges(); fixture.detectChanges();
@ -87,6 +91,7 @@ describe('ManageRulesSmartComponent', () => {
folderRulesService.rulesListing$ = of([]); folderRulesService.rulesListing$ = of([]);
folderRulesService.loading$ = of(false); folderRulesService.loading$ = of(false);
folderRulesService.deletedRuleId$ = of(null); folderRulesService.deletedRuleId$ = of(null);
actionsService.loading$ = of(false);
fixture.detectChanges(); fixture.detectChanges();
@ -106,6 +111,7 @@ describe('ManageRulesSmartComponent', () => {
folderRulesService.deletedRuleId$ = of(null); folderRulesService.deletedRuleId$ = of(null);
folderRulesService.rulesListing$ = of([]); folderRulesService.rulesListing$ = of([]);
folderRulesService.loading$ = of(false); folderRulesService.loading$ = of(false);
actionsService.loading$ = of(false);
fixture.detectChanges(); fixture.detectChanges();
@ -120,11 +126,12 @@ describe('ManageRulesSmartComponent', () => {
expect(ruleDetails).toBeFalsy(); expect(ruleDetails).toBeFalsy();
}); });
it('should only show progress bar while loading', () => { it('should only show progress bar while loading', async () => {
folderRulesService.folderInfo$ = of(null); folderRulesService.folderInfo$ = of(null);
folderRulesService.deletedRuleId$ = of(null); folderRulesService.deletedRuleId$ = of(null);
folderRulesService.rulesListing$ = of([]); folderRulesService.rulesListing$ = of([]);
folderRulesService.loading$ = of(true); folderRulesService.loading$ = of(true);
actionsService.loading$ = of(true);
fixture.detectChanges(); fixture.detectChanges();
@ -145,6 +152,7 @@ describe('ManageRulesSmartComponent', () => {
folderRulesService.folderInfo$ = of(dummyNodeInfo); folderRulesService.folderInfo$ = of(dummyNodeInfo);
folderRulesService.rulesListing$ = of(dummyRules); folderRulesService.rulesListing$ = of(dummyRules);
folderRulesService.loading$ = of(false); folderRulesService.loading$ = of(false);
actionsService.loading$ = of(false);
spyOn(component, 'onRuleDelete').and.callThrough(); spyOn(component, 'onRuleDelete').and.callThrough();

View File

@ -30,11 +30,13 @@ import { Observable, Subscription } from 'rxjs';
import { Rule } from '../model/rule.model'; import { Rule } from '../model/rule.model';
import { ActivatedRoute } from '@angular/router'; import { ActivatedRoute } from '@angular/router';
import { NodeInfo } from '@alfresco/aca-shared/store'; import { NodeInfo } from '@alfresco/aca-shared/store';
import { tap } from 'rxjs/operators'; import { delay, tap } from 'rxjs/operators';
import { EditRuleDialogSmartComponent } from '../rule-details/edit-rule-dialog.smart-component'; import { EditRuleDialogSmartComponent } from '../rule-details/edit-rule-dialog.smart-component';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
import { ConfirmDialogComponent } from '@alfresco/adf-content-services'; import { ConfirmDialogComponent } from '@alfresco/adf-content-services';
import { NotificationService } from '@alfresco/adf-core'; import { NotificationService } from '@alfresco/adf-core';
import { ActionDefinitionTransformed } from '../model/rule-action.model';
import { ActionsService } from '../services/actions.service';
@Component({ @Component({
selector: 'aca-manage-rules', selector: 'aca-manage-rules',
@ -45,8 +47,10 @@ import { NotificationService } from '@alfresco/adf-core';
}) })
export class ManageRulesSmartComponent implements OnInit, OnDestroy { export class ManageRulesSmartComponent implements OnInit, OnDestroy {
rules$: Observable<Rule[]>; rules$: Observable<Rule[]>;
isLoading$: Observable<boolean>; rulesLoading$: Observable<boolean>;
actionsLoading$: Observable<boolean>;
folderInfo$: Observable<NodeInfo>; folderInfo$: Observable<NodeInfo>;
actionDefinitions$: Observable<ActionDefinitionTransformed[]>;
selectedRule: Rule = null; selectedRule: Rule = null;
nodeId: string = null; nodeId: string = null;
deletedRuleSubscription$: Subscription; deletedRuleSubscription$: Subscription;
@ -57,10 +61,12 @@ export class ManageRulesSmartComponent implements OnInit, OnDestroy {
private folderRulesService: FolderRulesService, private folderRulesService: FolderRulesService,
private route: ActivatedRoute, private route: ActivatedRoute,
private matDialogService: MatDialog, private matDialogService: MatDialog,
private notificationService: NotificationService private notificationService: NotificationService,
private actionsService: ActionsService
) {} ) {}
ngOnInit(): void { ngOnInit(): void {
this.actionDefinitions$ = this.actionsService.actionDefinitionsListing$;
this.rules$ = this.folderRulesService.rulesListing$.pipe( this.rules$ = this.folderRulesService.rulesListing$.pipe(
tap((rules) => { tap((rules) => {
if (!rules.includes(this.selectedRule)) { if (!rules.includes(this.selectedRule)) {
@ -73,8 +79,10 @@ export class ManageRulesSmartComponent implements OnInit, OnDestroy {
this.folderRulesService.loadRules(this.nodeId); this.folderRulesService.loadRules(this.nodeId);
} }
}); });
this.isLoading$ = this.folderRulesService.loading$; this.rulesLoading$ = this.folderRulesService.loading$;
this.actionsLoading$ = this.actionsService.loading$.pipe(delay(0));
this.folderInfo$ = this.folderRulesService.folderInfo$; this.folderInfo$ = this.folderRulesService.folderInfo$;
this.actionsService.loadActionDefinitions();
this.route.params.subscribe((params) => { this.route.params.subscribe((params) => {
this.nodeId = params.nodeId; this.nodeId = params.nodeId;
if (this.nodeId) { if (this.nodeId) {

View File

@ -0,0 +1,108 @@
/*!
* @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 { ActionDefinitionList } from '@alfresco/js-api';
import { ActionDefinitionTransformed, ActionParameterDefinitionTransformed } from '../model/rule-action.model';
export const actionDefListMock: ActionDefinitionList = {
list: {
pagination: {
count: 2,
hasMoreItems: false,
totalItems: 2,
skipCount: 0,
maxItems: 100
},
entries: [
{
applicableTypes: [],
parameterDefinitions: [
{
name: 'mock-action-parameter-text',
type: 'd:text',
multiValued: false,
mandatory: false,
displayLabel: 'Mock action parameter text'
},
{
name: 'mock-action-parameter-boolean',
type: 'd:boolean',
multiValued: false,
mandatory: false
}
],
name: 'mock-action-1-definition',
trackStatus: false,
description: 'This is a mock action',
id: 'mock-action-1-definition',
title: 'Action 1 title'
},
{
applicableTypes: [],
name: 'mock-action-2-definition',
trackStatus: false,
id: 'mock-action-2-definition'
}
]
}
};
const actionParam1TransformedMock: ActionParameterDefinitionTransformed = {
name: 'mock-action-parameter-text',
type: 'd:text',
multiValued: false,
mandatory: false,
displayLabel: 'Mock action parameter text'
};
const actionParam2TransformedMock: ActionParameterDefinitionTransformed = {
name: 'mock-action-parameter-boolean',
type: 'd:boolean',
multiValued: false,
mandatory: false,
displayLabel: 'mock-action-parameter-boolean'
};
const action1TransformedMock: ActionDefinitionTransformed = {
id: 'mock-action-1-definition',
name: 'mock-action-1-definition',
description: 'This is a mock action',
title: 'Action 1 title',
applicableTypes: [],
trackStatus: false,
parameterDefinitions: [actionParam1TransformedMock, actionParam2TransformedMock]
};
const action2TransformedMock: ActionDefinitionTransformed = {
id: 'mock-action-2-definition',
name: 'mock-action-2-definition',
description: '',
title: 'mock-action-2-definition',
applicableTypes: [],
trackStatus: false,
parameterDefinitions: []
};
export const actionsTransformedListMock: ActionDefinitionTransformed[] = [action1TransformedMock, action2TransformedMock];

View File

@ -25,11 +25,23 @@
export interface RuleAction { export interface RuleAction {
actionDefinitionId: string; actionDefinitionId: string;
params: RuleActionParams; params: { [key: string]: unknown };
} }
export interface RuleActionParams { export interface ActionDefinitionTransformed {
'deep-copy'?: boolean; id: string;
'destination-folder'?: string; name: string;
actionContext?: string; description: string;
title: string;
applicableTypes: string[];
trackStatus: boolean;
parameterDefinitions: ActionParameterDefinitionTransformed[];
}
export interface ActionParameterDefinitionTransformed {
name: string;
type: string;
multiValued: boolean;
mandatory: boolean;
displayLabel: string;
} }

View File

@ -0,0 +1,31 @@
<div class="aca-rule-action-list__item " *ngFor="let control of formControls">
<aca-rule-action
[actionDefinitions]="actionDefinitions"
[readOnly]="readOnly"
[formControl]="control">
</aca-rule-action>
<button
mat-icon-button
data-automation-id="rule-action-list-action-menu"
*ngIf="!readOnly" [matMenuTriggerFor]="menu">
<mat-icon>more_vert</mat-icon>
</button>
<mat-menu #menu>
<button
mat-menu-item
data-automation-id="rule-action-list-remove-action-button"
[title]="'ACA_FOLDER_RULES.RULE_DETAILS.ACTION_BUTTONS.REMOVE' | translate"
[disabled]="formArray.controls.length <= 1"
(click)="removeAction(control)">
<mat-icon>delete</mat-icon>
<span>{{ 'ACA_FOLDER_RULES.RULE_DETAILS.ACTION_BUTTONS.REMOVE' | translate }}</span>
</button>
</mat-menu>
</div>
<button mat-button data-automation-id="rule-action-list-add-action-button" (click)="addAction()" *ngIf="!readOnly">
<mat-icon>add</mat-icon>
<span>{{ 'ACA_FOLDER_RULES.RULE_DETAILS.ACTION_BUTTONS.ADD_ACTION' | translate }}</span>
</button>

View File

@ -0,0 +1,15 @@
.aca-rule-action-list {
&__item {
padding: 4px 8px;
border-radius: 8px;
display: flex;
& > .aca-rule-action {
flex: 1;
}
&:nth-child(2n) {
background-color: hsl(0,0%,95%);
}
}
}

View File

@ -0,0 +1,104 @@
/*!
* @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 { CoreTestingModule } from '@alfresco/adf-core';
import { RuleActionListUiComponent } from './rule-action-list.ui-component';
import { By } from '@angular/platform-browser';
import { DebugElement } from '@angular/core';
import { RuleActionUiComponent } from './rule-action.ui-component';
describe('RuleActionListUiComponent', () => {
let fixture: ComponentFixture<RuleActionListUiComponent>;
let component: RuleActionListUiComponent;
const getByDataAutomationId = (dataAutomationId: string, index = 0): DebugElement =>
fixture.debugElement.queryAll(By.css(`[data-automation-id="${dataAutomationId}"]`))[index];
beforeEach(() => {
TestBed.configureTestingModule({
imports: [CoreTestingModule]
});
fixture = TestBed.createComponent(RuleActionListUiComponent);
component = fixture.componentInstance;
component.writeValue([]);
fixture.detectChanges();
});
it('should default to 1 empty action when an empty array of actions is written', () => {
const acaRuleActions = fixture.debugElement.queryAll(By.directive(RuleActionUiComponent));
expect(acaRuleActions.length).toBe(1);
});
it('should add a new action when the "add action" button is clicked', () => {
const addActionButton = getByDataAutomationId('rule-action-list-add-action-button').nativeElement as HTMLButtonElement;
addActionButton.click();
fixture.detectChanges();
const acaRuleActions = fixture.debugElement.queryAll(By.directive(RuleActionUiComponent));
expect(acaRuleActions.length).toBe(2);
});
it('should disable the remove button if there is only one action', () => {
const menuButton = getByDataAutomationId('rule-action-list-action-menu', 0).nativeElement as HTMLButtonElement;
menuButton.click();
fixture.detectChanges();
const removeActionButton = getByDataAutomationId('rule-action-list-remove-action-button').nativeElement as HTMLButtonElement;
expect(removeActionButton.disabled).toBeTrue();
});
it('should enable the remove button if there is more than one action', () => {
const addActionButton = getByDataAutomationId('rule-action-list-add-action-button').nativeElement as HTMLButtonElement;
addActionButton.click();
fixture.detectChanges();
const menuButton = getByDataAutomationId('rule-action-list-action-menu', 0).nativeElement as HTMLButtonElement;
menuButton.click();
fixture.detectChanges();
const removeActionButton = getByDataAutomationId('rule-action-list-remove-action-button').nativeElement as HTMLButtonElement;
expect(removeActionButton.disabled).toBeFalse();
});
it('should remove an action when the remove action button is clicked', () => {
const addActionButton = getByDataAutomationId('rule-action-list-add-action-button').nativeElement as HTMLButtonElement;
addActionButton.click();
fixture.detectChanges();
const menuButton = getByDataAutomationId('rule-action-list-action-menu', 0).nativeElement as HTMLButtonElement;
menuButton.click();
fixture.detectChanges();
const removeActionButton = getByDataAutomationId('rule-action-list-remove-action-button').nativeElement as HTMLButtonElement;
removeActionButton.click();
fixture.detectChanges();
const acaRuleActions = fixture.debugElement.queryAll(By.directive(RuleActionUiComponent));
expect(acaRuleActions.length).toBe(1);
});
});

View File

@ -0,0 +1,77 @@
import { Component, forwardRef, Input, OnDestroy, ViewEncapsulation } from '@angular/core';
import { ControlValueAccessor, FormArray, FormControl, NG_VALUE_ACCESSOR } from '@angular/forms';
import { ActionDefinitionTransformed, RuleAction } from '../../model/rule-action.model';
import { Subscription } from 'rxjs';
@Component({
selector: 'aca-rule-action-list',
templateUrl: './rule-action-list.ui-component.html',
styleUrls: ['./rule-action-list.ui-component.scss'],
encapsulation: ViewEncapsulation.None,
host: { class: 'aca-rule-action-list' },
providers: [
{
provide: NG_VALUE_ACCESSOR,
multi: true,
useExisting: forwardRef(() => RuleActionListUiComponent)
}
]
})
export class RuleActionListUiComponent implements ControlValueAccessor, OnDestroy {
@Input()
actionDefinitions: ActionDefinitionTransformed[] = [];
@Input()
readOnly = false;
formArray = new FormArray([]);
private formArraySubscription: Subscription;
get formControls(): FormControl[] {
return this.formArray.controls as FormControl[];
}
onChange: (actions: RuleAction[]) => void = () => undefined;
onTouch: () => void = () => undefined;
writeValue(actions: RuleAction[]) {
if (actions.length === 0) {
actions = [
{
actionDefinitionId: null,
params: {}
}
];
}
this.formArray = new FormArray(actions.map((action: RuleAction) => new FormControl(action)));
this.formArraySubscription?.unsubscribe();
this.formArraySubscription = this.formArray.valueChanges.subscribe((value: any) => {
this.onChange(value);
this.onTouch();
});
}
registerOnChange(fn: (actions: RuleAction[]) => void) {
this.onChange = fn;
}
registerOnTouched(fn: () => void) {
this.onTouch = fn;
}
addAction() {
const newAction: RuleAction = {
actionDefinitionId: null,
params: {}
};
this.formArray.push(new FormControl(newAction));
}
ngOnDestroy() {
this.formArraySubscription?.unsubscribe();
}
removeAction(control: FormControl) {
const index = this.formArray.value.indexOf(control.value);
this.formArray.removeAt(index);
}
}

View File

@ -0,0 +1,22 @@
<form class="aca-rule-action__form" [formGroup]="form">
<mat-form-field>
<mat-select
formControlName="actionDefinitionId"
data-automation-id="rule-action-select"
[placeholder]="'ACA_FOLDER_RULES.RULE_DETAILS.ACTION_SELECT_PLACEHOLDER' | translate">
<mat-option
*ngFor="let actionDefinition of actionDefinitions"
[value]="actionDefinition.id">
{{ actionDefinition.title }}
</mat-option>
</mat-select>
</mat-form-field>
<adf-card-view
data-automation-id="rule-action-card-view"
[properties]="cardViewItems"
[editable]="!readOnly">
</adf-card-view>
</form>

View File

@ -0,0 +1,7 @@
.aca-rule-action {
&__form {
display: flex;
flex-direction: row;
gap: 20px;
}
}

View File

@ -0,0 +1,87 @@
/*!
* @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 { CardViewBoolItemModel, CardViewComponent, CardViewTextItemModel, CoreTestingModule } from '@alfresco/adf-core';
import { RuleActionUiComponent } from './rule-action.ui-component';
import { actionsTransformedListMock } from '../../mock/actions.mock';
import { DebugElement } from '@angular/core';
import { By } from '@angular/platform-browser';
describe('RuleActionUiComponent', () => {
let fixture: ComponentFixture<RuleActionUiComponent>;
let component: RuleActionUiComponent;
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]
});
fixture = TestBed.createComponent(RuleActionUiComponent);
component = fixture.componentInstance;
});
it('should populate the dropdown selector with the action definitions', () => {
component.actionDefinitions = actionsTransformedListMock;
fixture.detectChanges();
const matSelect = getByDataAutomationId('rule-action-select').nativeElement;
matSelect.click();
fixture.detectChanges();
const matOptions = fixture.debugElement.queryAll(By.css(`mat-option`));
expect(matOptions.length).toBe(2);
expect(matOptions[0].nativeElement.innerText).toBe('Action 1 title');
expect(matOptions[1].nativeElement.innerText).toBe('mock-action-2-definition');
});
it('should populate the card view with parameters when an action is selected', () => {
component.actionDefinitions = actionsTransformedListMock;
fixture.detectChanges();
const cardView = getByDataAutomationId('rule-action-card-view').componentInstance as CardViewComponent;
expect(cardView.properties.length).toBe(0);
changeMatSelectValue('rule-action-select', 'mock-action-1-definition');
expect(cardView.properties.length).toBe(2);
expect(cardView.properties[0]).toBeInstanceOf(CardViewTextItemModel);
expect(cardView.properties[1]).toBeInstanceOf(CardViewBoolItemModel);
changeMatSelectValue('rule-action-select', 'mock-action-2-definition');
expect(cardView.properties.length).toBe(0);
});
});

View File

@ -0,0 +1,153 @@
import { Component, forwardRef, Input, OnDestroy, ViewEncapsulation } from '@angular/core';
import { ControlValueAccessor, FormControl, FormGroup, NG_VALUE_ACCESSOR } from '@angular/forms';
import { ActionDefinitionTransformed, RuleAction } from '../../model/rule-action.model';
import { CardViewItem } from '@alfresco/adf-core/lib/card-view/interfaces/card-view-item.interface';
import { CardViewBoolItemModel, CardViewTextItemModel, CardViewUpdateService, UpdateNotification } from '@alfresco/adf-core';
import { ActionParameterDefinition } from '@alfresco/js-api';
@Component({
selector: 'aca-rule-action',
templateUrl: './rule-action.ui-component.html',
styleUrls: ['./rule-action.ui-component.scss'],
encapsulation: ViewEncapsulation.None,
host: { class: 'aca-rule-action' },
providers: [
{
provide: NG_VALUE_ACCESSOR,
multi: true,
useExisting: forwardRef(() => RuleActionUiComponent)
},
CardViewUpdateService
]
})
export class RuleActionUiComponent implements ControlValueAccessor, OnDestroy {
private _actionDefinitions: ActionDefinitionTransformed[];
@Input()
get actionDefinitions(): ActionDefinitionTransformed[] {
return this._actionDefinitions;
}
set actionDefinitions(value: ActionDefinitionTransformed[]) {
this._actionDefinitions = value.sort((a, b) => a.title.localeCompare(b.title));
}
private _readOnly = false;
@Input()
get readOnly(): boolean {
return this._readOnly;
}
set readOnly(isReadOnly: boolean) {
this.setDisabledState(isReadOnly);
}
form = new FormGroup({
actionDefinitionId: new FormControl('copy')
});
cardViewItems: CardViewItem[] = [];
parameters: { [key: string]: unknown } = {};
get selectedActionDefinitionId(): string {
return this.form.get('actionDefinitionId').value;
}
get selectedActionDefinition(): ActionDefinitionTransformed {
return this.actionDefinitions.find((actionDefinition: ActionDefinitionTransformed) => actionDefinition.id === this.selectedActionDefinitionId);
}
private formSubscription = this.form.valueChanges.subscribe(() => {
this.setDefaultParameters();
this.setCardViewProperties();
this.onChange({
actionDefinitionId: this.selectedActionDefinitionId,
params: this.parameters
});
this.onTouch();
});
private updateServiceSubscription = this.cardViewUpdateService.itemUpdated$.subscribe((updateNotification: UpdateNotification) => {
this.parameters = {
...this.parameters,
...updateNotification.changed
};
this.onChange({
actionDefinitionId: this.selectedActionDefinitionId,
params: this.parameters
});
this.onTouch();
});
onChange: (action: RuleAction) => void = () => undefined;
onTouch: () => void = () => undefined;
constructor(private cardViewUpdateService: CardViewUpdateService) {}
writeValue(action: RuleAction) {
this.form.setValue({
actionDefinitionId: action.actionDefinitionId
});
this.parameters = {
...this.parameters,
...action.params
};
this.setCardViewProperties();
}
registerOnChange(fn: (action: RuleAction) => void) {
this.onChange = fn;
}
registerOnTouched(fn: any) {
this.onTouch = fn;
}
ngOnDestroy() {
this.formSubscription.unsubscribe();
this.updateServiceSubscription.unsubscribe();
}
setCardViewProperties() {
this.cardViewItems = (this.selectedActionDefinition?.parameterDefinitions ?? []).map((paramDef) => {
const cardViewPropertiesModel = {
label: paramDef.displayLabel,
key: paramDef.name,
editable: true
};
switch (paramDef.type) {
case 'd:boolean':
return new CardViewBoolItemModel({
...cardViewPropertiesModel,
value: this.parameters[paramDef.name] ?? false
});
default:
return new CardViewTextItemModel({
...cardViewPropertiesModel,
value: this.parameters[paramDef.name] ?? ''
});
}
});
}
setDefaultParameters() {
this.parameters = {};
(this.selectedActionDefinition?.parameterDefinitions ?? []).forEach((paramDef: ActionParameterDefinition) => {
switch (paramDef.type) {
case 'd:boolean':
this.parameters[paramDef.name] = false;
break;
default:
this.parameters[paramDef.name] = '';
}
});
}
setDisabledState(isDisabled: boolean) {
if (isDisabled) {
this._readOnly = true;
this.form.disable();
} else {
this._readOnly = false;
this.form.enable();
}
}
}

View File

@ -48,10 +48,10 @@
<mat-menu #menu="matMenu"> <mat-menu #menu="matMenu">
<button <button
mat-menu-item mat-menu-item
[title]="'ACA_FOLDER_RULES.RULE_DETAILS.ACTIONS.REMOVE' | translate" [title]="'ACA_FOLDER_RULES.RULE_DETAILS.CONDITION_BUTTONS.REMOVE' | translate"
(click)="removeCondition(control)"> (click)="removeCondition(control)">
<mat-icon>delete</mat-icon> <mat-icon>delete</mat-icon>
<span>{{ 'ACA_FOLDER_RULES.RULE_DETAILS.ACTIONS.REMOVE' | translate }}</span> <span>{{ 'ACA_FOLDER_RULES.RULE_DETAILS.CONDITION_BUTTONS.REMOVE' | translate }}</span>
</button> </button>
</mat-menu> </mat-menu>
@ -60,11 +60,11 @@
<div class="aca-rule-composite-condition__form__actions" *ngIf="!readOnly" data-automation-id="add-actions"> <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"> <button mat-button (click)="addSimpleCondition()" data-automation-id="add-condition-button">
<mat-icon>add</mat-icon> <mat-icon>add</mat-icon>
<span>{{ 'ACA_FOLDER_RULES.RULE_DETAILS.ACTIONS.ADD_CONDITION' | translate }}</span> <span>{{ 'ACA_FOLDER_RULES.RULE_DETAILS.CONDITION_BUTTONS.ADD_CONDITION' | translate }}</span>
</button> </button>
<button mat-button (click)="addCompositeCondition()" data-automation-id="add-group-button"> <button mat-button (click)="addCompositeCondition()" data-automation-id="add-group-button">
<mat-icon>add</mat-icon> <mat-icon>add</mat-icon>
<span>{{ 'ACA_FOLDER_RULES.RULE_DETAILS.ACTIONS.ADD_GROUP' | translate }}</span> <span>{{ 'ACA_FOLDER_RULES.RULE_DETAILS.CONDITION_BUTTONS.ADD_GROUP' | translate }}</span>
</button> </button>
</div> </div>
</form> </form>

View File

@ -8,7 +8,21 @@
</div> </div>
<mat-dialog-content class="aca-edit-rule-dialog__content"> <mat-dialog-content class="aca-edit-rule-dialog__content">
<aca-rule-details (formValidationChanged)="formValid = $event" (formValueChanged)="formValue = $event" [value]="model"></aca-rule-details> <div class="aca-edit-rule-dialog__content__spinner" *ngIf="loading$ | async; else ruleDetails">
<mat-progress-spinner
color="primary"
mode="indeterminate">
</mat-progress-spinner>
</div>
<ng-template #ruleDetails>
<aca-rule-details
[actionDefinitions]="actionDefinitions$ | async"
[value]="model"
(formValueChanged)="formValue = $event"
(formValidationChanged)="formValid = $event">
</aca-rule-details>
</ng-template>
</mat-dialog-content> </mat-dialog-content>
<mat-dialog-actions align="end" class="aca-edit-rule-dialog__footer"> <mat-dialog-actions align="end" class="aca-edit-rule-dialog__footer">

View File

@ -31,6 +31,13 @@
&__content { &__content {
margin: 0; margin: 0;
padding: 0; padding: 0;
&__spinner {
display: flex;
align-items: center;
justify-content: center;
margin: 20px 0
}
} }
&__footer { &__footer {

View File

@ -31,9 +31,14 @@ import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { CoreTestingModule } from '@alfresco/adf-core'; import { CoreTestingModule } from '@alfresco/adf-core';
import { RuleCompositeConditionUiComponent } from './conditions/rule-composite-condition.ui-component'; import { RuleCompositeConditionUiComponent } from './conditions/rule-composite-condition.ui-component';
import { RuleTriggersUiComponent } from './triggers/rule-triggers.ui-component'; import { RuleTriggersUiComponent } from './triggers/rule-triggers.ui-component';
import { RuleActionListUiComponent } from './actions/rule-action-list.ui-component';
import { RuleActionUiComponent } from './actions/rule-action.ui-component';
import { ActionsService } from '../services/actions.service';
import { RuleOptionsUiComponent } from './options/rule-options.ui-component';
describe('EditRuleDialogComponent', () => { describe('EditRuleDialogComponent', () => {
let fixture: ComponentFixture<EditRuleDialogSmartComponent>; let fixture: ComponentFixture<EditRuleDialogSmartComponent>;
let actionsService: ActionsService;
const dialogRef = { const dialogRef = {
close: jasmine.createSpy('close'), close: jasmine.createSpy('close'),
@ -43,14 +48,26 @@ describe('EditRuleDialogComponent', () => {
const setupBeforeEach = (dialogOptions: EditRuleDialogOptions = {}) => { const setupBeforeEach = (dialogOptions: EditRuleDialogOptions = {}) => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [CoreTestingModule], imports: [CoreTestingModule],
declarations: [EditRuleDialogSmartComponent, RuleCompositeConditionUiComponent, RuleDetailsUiComponent, RuleTriggersUiComponent], declarations: [
EditRuleDialogSmartComponent,
RuleCompositeConditionUiComponent,
RuleDetailsUiComponent,
RuleTriggersUiComponent,
RuleActionListUiComponent,
RuleActionUiComponent,
RuleOptionsUiComponent
],
providers: [ providers: [
{ provide: MatDialogRef, useValue: dialogRef }, { provide: MatDialogRef, useValue: dialogRef },
{ provide: MAT_DIALOG_DATA, useValue: dialogOptions } { provide: MAT_DIALOG_DATA, useValue: dialogOptions }
] ]
}); });
actionsService = TestBed.inject(ActionsService);
spyOn(actionsService, 'loadActionDefinitions').and.stub();
fixture = TestBed.createComponent(EditRuleDialogSmartComponent); fixture = TestBed.createComponent(EditRuleDialogSmartComponent);
fixture.detectChanges();
}; };
describe('No dialog options passed / indifferent', () => { describe('No dialog options passed / indifferent', () => {
@ -59,7 +76,6 @@ describe('EditRuleDialogComponent', () => {
}); });
it('should activate the submit button only when a valid state is received', () => { it('should activate the submit button only when a valid state is received', () => {
fixture.detectChanges();
const submitButton = fixture.debugElement.query(By.css('[data-automation-id="edit-rule-dialog-submit"]')).nativeElement as HTMLButtonElement; const submitButton = fixture.debugElement.query(By.css('[data-automation-id="edit-rule-dialog-submit"]')).nativeElement as HTMLButtonElement;
const ruleDetails = fixture.debugElement.query(By.directive(RuleDetailsUiComponent)).componentInstance as RuleDetailsUiComponent; const ruleDetails = fixture.debugElement.query(By.directive(RuleDetailsUiComponent)).componentInstance as RuleDetailsUiComponent;
ruleDetails.formValidationChanged.emit(true); ruleDetails.formValidationChanged.emit(true);
@ -73,14 +89,12 @@ describe('EditRuleDialogComponent', () => {
}); });
it('should show a "create" label in the title', () => { it('should show a "create" label in the title', () => {
fixture.detectChanges();
const titleElement = fixture.debugElement.query(By.css('[data-automation-id="edit-rule-dialog-title"]')).nativeElement as HTMLDivElement; const titleElement = fixture.debugElement.query(By.css('[data-automation-id="edit-rule-dialog-title"]')).nativeElement as HTMLDivElement;
expect(titleElement.innerText.trim()).toBe('ACA_FOLDER_RULES.EDIT_RULE_DIALOG.CREATE_TITLE'); expect(titleElement.innerText.trim()).toBe('ACA_FOLDER_RULES.EDIT_RULE_DIALOG.CREATE_TITLE');
}); });
it('should show a "create" label in the submit button', () => { it('should show a "create" label in the submit button', () => {
fixture.detectChanges();
const titleElement = fixture.debugElement.query(By.css('[data-automation-id="edit-rule-dialog-submit"]')).nativeElement as HTMLButtonElement; const titleElement = fixture.debugElement.query(By.css('[data-automation-id="edit-rule-dialog-submit"]')).nativeElement as HTMLButtonElement;
expect(titleElement.innerText.trim()).toBe('ACA_FOLDER_RULES.EDIT_RULE_DIALOG.CREATE'); expect(titleElement.innerText.trim()).toBe('ACA_FOLDER_RULES.EDIT_RULE_DIALOG.CREATE');
@ -90,9 +104,7 @@ describe('EditRuleDialogComponent', () => {
describe('With dialog options passed', () => { describe('With dialog options passed', () => {
const dialogOptions: EditRuleDialogOptions = { const dialogOptions: EditRuleDialogOptions = {
model: { model: {
id: 'rule-id', id: 'rule-id'
name: 'Rule name',
description: 'This is the description of the rule'
} }
}; };
@ -101,14 +113,12 @@ describe('EditRuleDialogComponent', () => {
}); });
it('should show an "update" label in the title', () => { it('should show an "update" label in the title', () => {
fixture.detectChanges();
const titleElement = fixture.debugElement.query(By.css('[data-automation-id="edit-rule-dialog-title"]')).nativeElement as HTMLDivElement; const titleElement = fixture.debugElement.query(By.css('[data-automation-id="edit-rule-dialog-title"]')).nativeElement as HTMLDivElement;
expect(titleElement.innerText.trim()).toBe('ACA_FOLDER_RULES.EDIT_RULE_DIALOG.UPDATE_TITLE'); expect(titleElement.innerText.trim()).toBe('ACA_FOLDER_RULES.EDIT_RULE_DIALOG.UPDATE_TITLE');
}); });
it('should show an "update" 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; const titleElement = fixture.debugElement.query(By.css('[data-automation-id="edit-rule-dialog-submit"]')).nativeElement as HTMLButtonElement;
expect(titleElement.innerText.trim()).toBe('ACA_FOLDER_RULES.EDIT_RULE_DIALOG.UPDATE'); expect(titleElement.innerText.trim()).toBe('ACA_FOLDER_RULES.EDIT_RULE_DIALOG.UPDATE');

View File

@ -23,9 +23,10 @@
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>. * along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
*/ */
import { Component, EventEmitter, Inject, Output, ViewEncapsulation } from '@angular/core'; import { Component, EventEmitter, Inject, OnInit, Output, ViewEncapsulation } from '@angular/core';
import { MAT_DIALOG_DATA } from '@angular/material/dialog'; import { MAT_DIALOG_DATA } from '@angular/material/dialog';
import { Rule } from '../model/rule.model'; import { Rule } from '../model/rule.model';
import { ActionsService } from '../services/actions.service';
export interface EditRuleDialogOptions { export interface EditRuleDialogOptions {
model?: Partial<Rule>; model?: Partial<Rule>;
@ -38,13 +39,15 @@ export interface EditRuleDialogOptions {
encapsulation: ViewEncapsulation.None, encapsulation: ViewEncapsulation.None,
host: { class: 'aca-edit-rule-dialog' } host: { class: 'aca-edit-rule-dialog' }
}) })
export class EditRuleDialogSmartComponent { export class EditRuleDialogSmartComponent implements OnInit {
formValid = false; formValid = false;
model: Partial<Rule>; model: Partial<Rule>;
formValue: Partial<Rule>; formValue: Partial<Rule>;
@Output() submitted = new EventEmitter<Partial<Rule>>(); @Output() submitted = new EventEmitter<Partial<Rule>>();
actionDefinitions$ = this.actionsService.actionDefinitionsListing$;
loading$ = this.actionsService.loading$;
constructor(@Inject(MAT_DIALOG_DATA) public options: EditRuleDialogOptions) { constructor(@Inject(MAT_DIALOG_DATA) public options: EditRuleDialogOptions, private actionsService: ActionsService) {
this.model = this.options?.model || {}; this.model = this.options?.model || {};
} }
@ -63,4 +66,8 @@ export class EditRuleDialogSmartComponent {
onSubmit() { onSubmit() {
this.submitted.emit(this.formValue); this.submitted.emit(this.formValue);
} }
ngOnInit() {
this.actionsService.loadActionDefinitions();
}
} }

View File

@ -45,6 +45,17 @@
<hr> <hr>
<div class="aca-rule-details__form__row aca-rule-details__form__actions">
<div class="label">{{ 'ACA_FOLDER_RULES.RULE_DETAILS.LABEL.PERFORM_ACTIONS' | translate }}</div>
<aca-rule-action-list
formControlName="actions"
[actionDefinitions]="actionDefinitions"
[readOnly]="readOnly">
</aca-rule-action-list>
</div>
<hr>
<div class="aca-rule-details__form__row"> <div class="aca-rule-details__form__row">
<div class="label">{{ 'ACA_FOLDER_RULES.RULE_DETAILS.LABEL.OPTIONS' | translate }}</div> <div class="label">{{ 'ACA_FOLDER_RULES.RULE_DETAILS.LABEL.OPTIONS' | translate }}</div>
<aca-rule-options [form]="form" [preview]="preview" data-automation-id="rule-details-options-component"></aca-rule-options> <aca-rule-options [form]="form" [preview]="preview" data-automation-id="rule-details-options-component"></aca-rule-options>

View File

@ -58,5 +58,11 @@
margin-left: 16px; margin-left: 16px;
color: inherit; color: inherit;
} }
&__actions {
.aca-rule-action-list {
flex: 1;
}
}
} }
} }

View File

@ -32,6 +32,8 @@ import { RuleCompositeConditionUiComponent } from './conditions/rule-composite-c
import { RuleTriggersUiComponent } from './triggers/rule-triggers.ui-component'; import { RuleTriggersUiComponent } from './triggers/rule-triggers.ui-component';
import { MatCheckbox } from '@angular/material/checkbox'; import { MatCheckbox } from '@angular/material/checkbox';
import { RuleOptionsUiComponent } from './options/rule-options.ui-component'; import { RuleOptionsUiComponent } from './options/rule-options.ui-component';
import { RuleActionListUiComponent } from './actions/rule-action-list.ui-component';
import { RuleActionUiComponent } from './actions/rule-action.ui-component';
describe('RuleDetailsUiComponent', () => { describe('RuleDetailsUiComponent', () => {
let fixture: ComponentFixture<RuleDetailsUiComponent>; let fixture: ComponentFixture<RuleDetailsUiComponent>;
@ -56,7 +58,14 @@ describe('RuleDetailsUiComponent', () => {
beforeEach(() => { beforeEach(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [CoreTestingModule], imports: [CoreTestingModule],
declarations: [RuleCompositeConditionUiComponent, RuleDetailsUiComponent, RuleTriggersUiComponent, RuleOptionsUiComponent] declarations: [
RuleCompositeConditionUiComponent,
RuleDetailsUiComponent,
RuleTriggersUiComponent,
RuleOptionsUiComponent,
RuleActionListUiComponent,
RuleActionUiComponent
]
}); });
fixture = TestBed.createComponent(RuleDetailsUiComponent); fixture = TestBed.createComponent(RuleDetailsUiComponent);

View File

@ -30,6 +30,7 @@ import { distinctUntilChanged, map, takeUntil } from 'rxjs/operators';
import { Rule } from '../model/rule.model'; import { Rule } from '../model/rule.model';
import { ruleCompositeConditionValidator } from './validators/rule-composite-condition.validator'; import { ruleCompositeConditionValidator } from './validators/rule-composite-condition.validator';
import { FolderRulesService } from '../services/folder-rules.service'; import { FolderRulesService } from '../services/folder-rules.service';
import { ActionDefinitionTransformed } from '../model/rule-action.model';
@Component({ @Component({
selector: 'aca-rule-details', selector: 'aca-rule-details',
@ -68,7 +69,8 @@ export class RuleDetailsUiComponent implements OnInit, OnDestroy {
isAsynchronous: newValue.isAsynchronous || FolderRulesService.emptyRule.isAsynchronous, isAsynchronous: newValue.isAsynchronous || FolderRulesService.emptyRule.isAsynchronous,
errorScript: newValue.errorScript || FolderRulesService.emptyRule.errorScript, errorScript: newValue.errorScript || FolderRulesService.emptyRule.errorScript,
isInheritable: newValue.isInheritable || FolderRulesService.emptyRule.isInheritable, isInheritable: newValue.isInheritable || FolderRulesService.emptyRule.isInheritable,
isEnabled: newValue.isEnabled || FolderRulesService.emptyRule.isEnabled isEnabled: newValue.isEnabled || FolderRulesService.emptyRule.isEnabled,
actions: newValue.actions || FolderRulesService.emptyRule.actions
}; };
if (this.form) { if (this.form) {
this.form.setValue(newValue); this.form.setValue(newValue);
@ -78,6 +80,8 @@ export class RuleDetailsUiComponent implements OnInit, OnDestroy {
} }
@Input() @Input()
preview: boolean; preview: boolean;
@Input()
actionDefinitions: ActionDefinitionTransformed[] = [];
@Output() @Output()
formValidationChanged = new EventEmitter<boolean>(); formValidationChanged = new EventEmitter<boolean>();
@ -129,7 +133,8 @@ export class RuleDetailsUiComponent implements OnInit, OnDestroy {
isAsynchronous: new UntypedFormControl(this.value.isAsynchronous), isAsynchronous: new UntypedFormControl(this.value.isAsynchronous),
errorScript: new UntypedFormControl(this.value.errorScript), errorScript: new UntypedFormControl(this.value.errorScript),
isInheritable: new UntypedFormControl(this.value.isInheritable), isInheritable: new UntypedFormControl(this.value.isInheritable),
isEnabled: new UntypedFormControl(this.value.isEnabled) isEnabled: new UntypedFormControl(this.value.isEnabled),
actions: new UntypedFormControl(this.value.actions)
}); });
this.readOnly = this._readOnly; this.readOnly = this._readOnly;

View File

@ -0,0 +1,65 @@
/*!
* @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 { ActionsService } from './actions.service';
import { TestBed } from '@angular/core/testing';
import { CoreTestingModule } from '@alfresco/adf-core';
import { ActionsApi } from '@alfresco/js-api';
import { actionDefListMock, actionsTransformedListMock } from '../mock/actions.mock';
import { take } from 'rxjs/operators';
describe('ActionsService', () => {
let actionsService: ActionsService;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [CoreTestingModule],
providers: [ActionsService]
});
actionsService = TestBed.inject(ActionsService);
});
it('should load the data into the observable', async () => {
spyOn(ActionsApi.prototype, 'listActions').and.returnValue(Promise.resolve(actionDefListMock));
const actionsPromise = actionsService.actionDefinitionsListing$.pipe(take(2)).toPromise();
actionsService.loadActionDefinitions();
const actionsList = await actionsPromise;
expect(actionsList).toEqual(actionsTransformedListMock);
});
it('should set loading to true while the request is being sent', async () => {
spyOn(ActionsApi.prototype, 'listActions').and.returnValue(Promise.resolve(actionDefListMock));
const loadingTruePromise = actionsService.loading$.pipe(take(2)).toPromise();
const loadingFalsePromise = actionsService.loading$.pipe(take(3)).toPromise();
actionsService.loadActionDefinitions();
expect(await loadingTruePromise).toBeTrue();
expect(await loadingFalsePromise).toBeFalse();
});
});

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 { Injectable } from '@angular/core';
import { ActionDefinition, ActionDefinitionEntry, ActionDefinitionList, ActionParameterDefinition, ActionsApi } from '@alfresco/js-api';
import { AlfrescoApiService } from '@alfresco/adf-core';
import { BehaviorSubject, from } from 'rxjs';
import { ActionDefinitionTransformed, ActionParameterDefinitionTransformed } from '../model/rule-action.model';
import { finalize, map } from 'rxjs/operators';
@Injectable({ providedIn: 'root' })
export class ActionsService {
private actionDefinitionsListingSource = new BehaviorSubject<ActionDefinitionTransformed[]>([]);
actionDefinitionsListing$ = this.actionDefinitionsListingSource.asObservable();
private loadingSource = new BehaviorSubject<boolean>(false);
loading$ = this.loadingSource.asObservable();
private _actionsApi: ActionsApi;
get actionsApi(): ActionsApi {
if (!this._actionsApi) {
this._actionsApi = new ActionsApi(this.apiService.getInstance());
}
return this._actionsApi;
}
constructor(private apiService: AlfrescoApiService) {}
loadActionDefinitions() {
this.loadingSource.next(true);
from(this.actionsApi.listActions())
.pipe(
map((list: ActionDefinitionList) => list.list.entries.map((entry) => this.transformActionDefinition(entry))),
finalize(() => this.loadingSource.next(false))
)
.subscribe((obj: ActionDefinitionTransformed[]) => {
this.actionDefinitionsListingSource.next(obj);
});
}
private transformActionDefinition(obj: ActionDefinition | ActionDefinitionEntry): ActionDefinitionTransformed {
if (this.isActionDefinitionEntry(obj)) {
obj = obj.entry;
}
return {
id: obj.id,
name: obj.name ?? '',
description: obj.description ?? '',
title: obj.title ?? obj.name ?? '',
applicableTypes: obj.applicableTypes ?? [],
trackStatus: obj.trackStatus ?? false,
parameterDefinitions: (obj.parameterDefinitions ?? []).map((paramDef: ActionParameterDefinition) =>
this.transformActionParameterDefinition(paramDef)
)
};
}
private transformActionParameterDefinition(obj: ActionParameterDefinition): ActionParameterDefinitionTransformed {
return {
name: obj.name ?? '',
type: obj.type ?? '',
multiValued: obj.multiValued ?? false,
mandatory: obj.mandatory ?? false,
displayLabel: obj.displayLabel ?? obj.name ?? ''
};
}
private isActionDefinitionEntry(obj): obj is ActionDefinitionEntry {
return typeof obj.entry !== 'undefined';
}
}

View File

@ -146,11 +146,9 @@ describe('FolderRulesService', () => {
.and.returnValue(Promise.resolve(dummyRules[0])); .and.returnValue(Promise.resolve(dummyRules[0]));
}); });
it('should send correct POST request and return created rule', function () { it('should send correct POST request and return created rule', async () => {
folderRulesService.createRule(nodeId, dummyRules[0]).then((result) => { const result = await folderRulesService.createRule(nodeId, dummyRules[0]);
expect(folderRulesService.createRule).toHaveBeenCalledWith(nodeId, dummyRules[0]); expect(result).toEqual(dummyRules[0]);
expect(result).toEqual(dummyRules[0]);
});
}); });
}); });
}); });