mirror of
https://github.com/Alfresco/alfresco-content-app.git
synced 2025-05-12 17:04:46 +00:00
[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:
parent
08d4f46573
commit
59c7d68299
@ -15,6 +15,7 @@
|
||||
"NAME": "Name",
|
||||
"DESCRIPTION": "Description",
|
||||
"WHEN": "When",
|
||||
"PERFORM_ACTIONS": "Perform actions",
|
||||
"OPTIONS": "Other options"
|
||||
},
|
||||
"PLACEHOLDER": {
|
||||
@ -70,13 +71,18 @@
|
||||
"AND": "And",
|
||||
"OR": "Or"
|
||||
},
|
||||
"ACTIONS": {
|
||||
"CONDITION_BUTTONS": {
|
||||
"ADD_CONDITION": "Add condition",
|
||||
"ADD_GROUP": "Add group",
|
||||
"ADD_GROUP": "Add condition group",
|
||||
"REMOVE": "Remove"
|
||||
},
|
||||
"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": {
|
||||
"TOOLBAR": {
|
||||
@ -84,7 +90,7 @@
|
||||
"RULES": "rules"
|
||||
},
|
||||
"ACTIONS": {
|
||||
"NEW_RULE": "New rule"
|
||||
"CREATE_RULE": "Create rule"
|
||||
}
|
||||
},
|
||||
"EMPTY_RULES_LIST": {
|
||||
|
@ -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 { RuleTriggersUiComponent } from './rule-details/triggers/rule-triggers.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 = [
|
||||
{
|
||||
@ -63,6 +65,8 @@ const routes: Routes = [
|
||||
declarations: [
|
||||
EditRuleDialogSmartComponent,
|
||||
ManageRulesSmartComponent,
|
||||
RuleActionListUiComponent,
|
||||
RuleActionUiComponent,
|
||||
RuleCompositeConditionUiComponent,
|
||||
RuleDetailsUiComponent,
|
||||
RuleSimpleConditionUiComponent,
|
||||
|
@ -12,7 +12,7 @@
|
||||
<aca-page-layout-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>
|
||||
</ng-container>
|
||||
|
||||
@ -25,7 +25,7 @@
|
||||
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>
|
||||
<button mat-flat-button color="primary" (click)="openNewRuleDialog()">{{ 'ACA_FOLDER_RULES.MANAGE_RULES.TOOLBAR.ACTIONS.CREATE_RULE' | translate }}</button>
|
||||
</adf-toolbar>
|
||||
<mat-divider></mat-divider>
|
||||
|
||||
@ -44,7 +44,12 @@
|
||||
</div>
|
||||
<p>{{ selectedRule.description }}</p>
|
||||
</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>
|
||||
|
||||
|
@ -17,7 +17,7 @@
|
||||
|
||||
&__container {
|
||||
display: grid;
|
||||
grid-template-columns: 33% 66%;
|
||||
grid-template-columns: minmax(200px,1fr) 3fr;
|
||||
padding: 32px;
|
||||
overflow: scroll;
|
||||
|
||||
|
@ -34,12 +34,14 @@ import { dummyRules } from '../mock/rules.mock';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { dummyNodeInfo } from '../mock/node.mock';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { ActionsService } from '../services/actions.service';
|
||||
|
||||
describe('ManageRulesSmartComponent', () => {
|
||||
let fixture: ComponentFixture<ManageRulesSmartComponent>;
|
||||
let component: ManageRulesSmartComponent;
|
||||
let debugElement: DebugElement;
|
||||
let folderRulesService: FolderRulesService;
|
||||
let actionsService: ActionsService;
|
||||
|
||||
beforeEach(
|
||||
waitForAsync(() => {
|
||||
@ -57,6 +59,7 @@ describe('ManageRulesSmartComponent', () => {
|
||||
component = fixture.componentInstance;
|
||||
debugElement = fixture.debugElement;
|
||||
folderRulesService = TestBed.inject<FolderRulesService>(FolderRulesService);
|
||||
actionsService = TestBed.inject<ActionsService>(ActionsService);
|
||||
});
|
||||
})
|
||||
);
|
||||
@ -66,6 +69,7 @@ describe('ManageRulesSmartComponent', () => {
|
||||
folderRulesService.folderInfo$ = of(dummyNodeInfo);
|
||||
folderRulesService.rulesListing$ = of(dummyRules);
|
||||
folderRulesService.loading$ = of(false);
|
||||
actionsService.loading$ = of(false);
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
@ -87,6 +91,7 @@ describe('ManageRulesSmartComponent', () => {
|
||||
folderRulesService.rulesListing$ = of([]);
|
||||
folderRulesService.loading$ = of(false);
|
||||
folderRulesService.deletedRuleId$ = of(null);
|
||||
actionsService.loading$ = of(false);
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
@ -106,6 +111,7 @@ describe('ManageRulesSmartComponent', () => {
|
||||
folderRulesService.deletedRuleId$ = of(null);
|
||||
folderRulesService.rulesListing$ = of([]);
|
||||
folderRulesService.loading$ = of(false);
|
||||
actionsService.loading$ = of(false);
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
@ -120,11 +126,12 @@ describe('ManageRulesSmartComponent', () => {
|
||||
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.deletedRuleId$ = of(null);
|
||||
folderRulesService.rulesListing$ = of([]);
|
||||
folderRulesService.loading$ = of(true);
|
||||
actionsService.loading$ = of(true);
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
@ -145,6 +152,7 @@ describe('ManageRulesSmartComponent', () => {
|
||||
folderRulesService.folderInfo$ = of(dummyNodeInfo);
|
||||
folderRulesService.rulesListing$ = of(dummyRules);
|
||||
folderRulesService.loading$ = of(false);
|
||||
actionsService.loading$ = of(false);
|
||||
|
||||
spyOn(component, 'onRuleDelete').and.callThrough();
|
||||
|
||||
|
@ -30,11 +30,13 @@ import { Observable, Subscription } from 'rxjs';
|
||||
import { Rule } from '../model/rule.model';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
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 { MatDialog } from '@angular/material/dialog';
|
||||
import { ConfirmDialogComponent } from '@alfresco/adf-content-services';
|
||||
import { NotificationService } from '@alfresco/adf-core';
|
||||
import { ActionDefinitionTransformed } from '../model/rule-action.model';
|
||||
import { ActionsService } from '../services/actions.service';
|
||||
|
||||
@Component({
|
||||
selector: 'aca-manage-rules',
|
||||
@ -45,8 +47,10 @@ import { NotificationService } from '@alfresco/adf-core';
|
||||
})
|
||||
export class ManageRulesSmartComponent implements OnInit, OnDestroy {
|
||||
rules$: Observable<Rule[]>;
|
||||
isLoading$: Observable<boolean>;
|
||||
rulesLoading$: Observable<boolean>;
|
||||
actionsLoading$: Observable<boolean>;
|
||||
folderInfo$: Observable<NodeInfo>;
|
||||
actionDefinitions$: Observable<ActionDefinitionTransformed[]>;
|
||||
selectedRule: Rule = null;
|
||||
nodeId: string = null;
|
||||
deletedRuleSubscription$: Subscription;
|
||||
@ -57,10 +61,12 @@ export class ManageRulesSmartComponent implements OnInit, OnDestroy {
|
||||
private folderRulesService: FolderRulesService,
|
||||
private route: ActivatedRoute,
|
||||
private matDialogService: MatDialog,
|
||||
private notificationService: NotificationService
|
||||
private notificationService: NotificationService,
|
||||
private actionsService: ActionsService
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.actionDefinitions$ = this.actionsService.actionDefinitionsListing$;
|
||||
this.rules$ = this.folderRulesService.rulesListing$.pipe(
|
||||
tap((rules) => {
|
||||
if (!rules.includes(this.selectedRule)) {
|
||||
@ -73,8 +79,10 @@ export class ManageRulesSmartComponent implements OnInit, OnDestroy {
|
||||
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.actionsService.loadActionDefinitions();
|
||||
this.route.params.subscribe((params) => {
|
||||
this.nodeId = params.nodeId;
|
||||
if (this.nodeId) {
|
||||
|
108
projects/aca-folder-rules/src/lib/mock/actions.mock.ts
Normal file
108
projects/aca-folder-rules/src/lib/mock/actions.mock.ts
Normal 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];
|
@ -25,11 +25,23 @@
|
||||
|
||||
export interface RuleAction {
|
||||
actionDefinitionId: string;
|
||||
params: RuleActionParams;
|
||||
params: { [key: string]: unknown };
|
||||
}
|
||||
|
||||
export interface RuleActionParams {
|
||||
'deep-copy'?: boolean;
|
||||
'destination-folder'?: string;
|
||||
actionContext?: string;
|
||||
export interface ActionDefinitionTransformed {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
title: string;
|
||||
applicableTypes: string[];
|
||||
trackStatus: boolean;
|
||||
parameterDefinitions: ActionParameterDefinitionTransformed[];
|
||||
}
|
||||
|
||||
export interface ActionParameterDefinitionTransformed {
|
||||
name: string;
|
||||
type: string;
|
||||
multiValued: boolean;
|
||||
mandatory: boolean;
|
||||
displayLabel: string;
|
||||
}
|
||||
|
@ -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>
|
@ -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%);
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
});
|
||||
});
|
@ -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);
|
||||
}
|
||||
}
|
@ -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>
|
@ -0,0 +1,7 @@
|
||||
.aca-rule-action {
|
||||
&__form {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 20px;
|
||||
}
|
||||
}
|
@ -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);
|
||||
});
|
||||
});
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@ -48,10 +48,10 @@
|
||||
<mat-menu #menu="matMenu">
|
||||
<button
|
||||
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)">
|
||||
<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>
|
||||
</mat-menu>
|
||||
|
||||
@ -60,11 +60,11 @@
|
||||
<div class="aca-rule-composite-condition__form__actions" *ngIf="!readOnly" data-automation-id="add-actions">
|
||||
<button mat-button (click)="addSimpleCondition()" data-automation-id="add-condition-button">
|
||||
<mat-icon>add</mat-icon>
|
||||
<span>{{ 'ACA_FOLDER_RULES.RULE_DETAILS.ACTIONS.ADD_CONDITION' | translate }}</span>
|
||||
<span>{{ 'ACA_FOLDER_RULES.RULE_DETAILS.CONDITION_BUTTONS.ADD_CONDITION' | translate }}</span>
|
||||
</button>
|
||||
<button mat-button (click)="addCompositeCondition()" data-automation-id="add-group-button">
|
||||
<mat-icon>add</mat-icon>
|
||||
<span>{{ 'ACA_FOLDER_RULES.RULE_DETAILS.ACTIONS.ADD_GROUP' | translate }}</span>
|
||||
<span>{{ 'ACA_FOLDER_RULES.RULE_DETAILS.CONDITION_BUTTONS.ADD_GROUP' | translate }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
@ -8,7 +8,21 @@
|
||||
</div>
|
||||
|
||||
<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-actions align="end" class="aca-edit-rule-dialog__footer">
|
||||
|
@ -31,6 +31,13 @@
|
||||
&__content {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
&__spinner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 20px 0
|
||||
}
|
||||
}
|
||||
|
||||
&__footer {
|
||||
|
@ -31,9 +31,14 @@ 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';
|
||||
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', () => {
|
||||
let fixture: ComponentFixture<EditRuleDialogSmartComponent>;
|
||||
let actionsService: ActionsService;
|
||||
|
||||
const dialogRef = {
|
||||
close: jasmine.createSpy('close'),
|
||||
@ -43,14 +48,26 @@ describe('EditRuleDialogComponent', () => {
|
||||
const setupBeforeEach = (dialogOptions: EditRuleDialogOptions = {}) => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [CoreTestingModule],
|
||||
declarations: [EditRuleDialogSmartComponent, RuleCompositeConditionUiComponent, RuleDetailsUiComponent, RuleTriggersUiComponent],
|
||||
declarations: [
|
||||
EditRuleDialogSmartComponent,
|
||||
RuleCompositeConditionUiComponent,
|
||||
RuleDetailsUiComponent,
|
||||
RuleTriggersUiComponent,
|
||||
RuleActionListUiComponent,
|
||||
RuleActionUiComponent,
|
||||
RuleOptionsUiComponent
|
||||
],
|
||||
providers: [
|
||||
{ provide: MatDialogRef, useValue: dialogRef },
|
||||
{ provide: MAT_DIALOG_DATA, useValue: dialogOptions }
|
||||
]
|
||||
});
|
||||
|
||||
actionsService = TestBed.inject(ActionsService);
|
||||
spyOn(actionsService, 'loadActionDefinitions').and.stub();
|
||||
|
||||
fixture = TestBed.createComponent(EditRuleDialogSmartComponent);
|
||||
fixture.detectChanges();
|
||||
};
|
||||
|
||||
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', () => {
|
||||
fixture.detectChanges();
|
||||
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;
|
||||
ruleDetails.formValidationChanged.emit(true);
|
||||
@ -73,14 +89,12 @@ describe('EditRuleDialogComponent', () => {
|
||||
});
|
||||
|
||||
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;
|
||||
|
||||
expect(titleElement.innerText.trim()).toBe('ACA_FOLDER_RULES.EDIT_RULE_DIALOG.CREATE_TITLE');
|
||||
});
|
||||
|
||||
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;
|
||||
|
||||
expect(titleElement.innerText.trim()).toBe('ACA_FOLDER_RULES.EDIT_RULE_DIALOG.CREATE');
|
||||
@ -90,9 +104,7 @@ describe('EditRuleDialogComponent', () => {
|
||||
describe('With dialog options passed', () => {
|
||||
const dialogOptions: EditRuleDialogOptions = {
|
||||
model: {
|
||||
id: 'rule-id',
|
||||
name: 'Rule name',
|
||||
description: 'This is the description of the rule'
|
||||
id: 'rule-id'
|
||||
}
|
||||
};
|
||||
|
||||
@ -101,14 +113,12 @@ describe('EditRuleDialogComponent', () => {
|
||||
});
|
||||
|
||||
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;
|
||||
|
||||
expect(titleElement.innerText.trim()).toBe('ACA_FOLDER_RULES.EDIT_RULE_DIALOG.UPDATE_TITLE');
|
||||
});
|
||||
|
||||
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;
|
||||
|
||||
expect(titleElement.innerText.trim()).toBe('ACA_FOLDER_RULES.EDIT_RULE_DIALOG.UPDATE');
|
||||
|
@ -23,9 +23,10 @@
|
||||
* 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 { Rule } from '../model/rule.model';
|
||||
import { ActionsService } from '../services/actions.service';
|
||||
|
||||
export interface EditRuleDialogOptions {
|
||||
model?: Partial<Rule>;
|
||||
@ -38,13 +39,15 @@ export interface EditRuleDialogOptions {
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
host: { class: 'aca-edit-rule-dialog' }
|
||||
})
|
||||
export class EditRuleDialogSmartComponent {
|
||||
export class EditRuleDialogSmartComponent implements OnInit {
|
||||
formValid = false;
|
||||
model: Partial<Rule>;
|
||||
formValue: 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 || {};
|
||||
}
|
||||
|
||||
@ -63,4 +66,8 @@ export class EditRuleDialogSmartComponent {
|
||||
onSubmit() {
|
||||
this.submitted.emit(this.formValue);
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.actionsService.loadActionDefinitions();
|
||||
}
|
||||
}
|
||||
|
@ -45,6 +45,17 @@
|
||||
|
||||
<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="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>
|
||||
|
@ -58,5 +58,11 @@
|
||||
margin-left: 16px;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
&__actions {
|
||||
.aca-rule-action-list {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -32,6 +32,8 @@ import { RuleCompositeConditionUiComponent } from './conditions/rule-composite-c
|
||||
import { RuleTriggersUiComponent } from './triggers/rule-triggers.ui-component';
|
||||
import { MatCheckbox } from '@angular/material/checkbox';
|
||||
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', () => {
|
||||
let fixture: ComponentFixture<RuleDetailsUiComponent>;
|
||||
@ -56,7 +58,14 @@ describe('RuleDetailsUiComponent', () => {
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [CoreTestingModule],
|
||||
declarations: [RuleCompositeConditionUiComponent, RuleDetailsUiComponent, RuleTriggersUiComponent, RuleOptionsUiComponent]
|
||||
declarations: [
|
||||
RuleCompositeConditionUiComponent,
|
||||
RuleDetailsUiComponent,
|
||||
RuleTriggersUiComponent,
|
||||
RuleOptionsUiComponent,
|
||||
RuleActionListUiComponent,
|
||||
RuleActionUiComponent
|
||||
]
|
||||
});
|
||||
|
||||
fixture = TestBed.createComponent(RuleDetailsUiComponent);
|
||||
|
@ -30,6 +30,7 @@ import { distinctUntilChanged, map, takeUntil } from 'rxjs/operators';
|
||||
import { Rule } from '../model/rule.model';
|
||||
import { ruleCompositeConditionValidator } from './validators/rule-composite-condition.validator';
|
||||
import { FolderRulesService } from '../services/folder-rules.service';
|
||||
import { ActionDefinitionTransformed } from '../model/rule-action.model';
|
||||
|
||||
@Component({
|
||||
selector: 'aca-rule-details',
|
||||
@ -68,7 +69,8 @@ export class RuleDetailsUiComponent implements OnInit, OnDestroy {
|
||||
isAsynchronous: newValue.isAsynchronous || FolderRulesService.emptyRule.isAsynchronous,
|
||||
errorScript: newValue.errorScript || FolderRulesService.emptyRule.errorScript,
|
||||
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) {
|
||||
this.form.setValue(newValue);
|
||||
@ -78,6 +80,8 @@ export class RuleDetailsUiComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
@Input()
|
||||
preview: boolean;
|
||||
@Input()
|
||||
actionDefinitions: ActionDefinitionTransformed[] = [];
|
||||
|
||||
@Output()
|
||||
formValidationChanged = new EventEmitter<boolean>();
|
||||
@ -129,7 +133,8 @@ export class RuleDetailsUiComponent implements OnInit, OnDestroy {
|
||||
isAsynchronous: new UntypedFormControl(this.value.isAsynchronous),
|
||||
errorScript: new UntypedFormControl(this.value.errorScript),
|
||||
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;
|
||||
|
||||
|
@ -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();
|
||||
});
|
||||
});
|
@ -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';
|
||||
}
|
||||
}
|
@ -146,11 +146,9 @@ describe('FolderRulesService', () => {
|
||||
.and.returnValue(Promise.resolve(dummyRules[0]));
|
||||
});
|
||||
|
||||
it('should send correct POST request and return created rule', function () {
|
||||
folderRulesService.createRule(nodeId, dummyRules[0]).then((result) => {
|
||||
expect(folderRulesService.createRule).toHaveBeenCalledWith(nodeId, dummyRules[0]);
|
||||
it('should send correct POST request and return created rule', async () => {
|
||||
const result = await folderRulesService.createRule(nodeId, dummyRules[0]);
|
||||
expect(result).toEqual(dummyRules[0]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
Loading…
x
Reference in New Issue
Block a user