[ACS-3859] Action parameter constraints into dropdown box (#2773)

* [ACS-3859] - functionality implementation

* [ACS-3859] - unit tests

* [ACS-3859] - small fix
This commit is contained in:
Nikita Maliarchuk 2022-11-15 11:07:27 +01:00 committed by GitHub
parent 5ce4835e4c
commit 854fb28374
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 156 additions and 72 deletions

View File

@ -35,7 +35,6 @@ import { By } from '@angular/platform-browser';
import { dummyNodeInfo } from '../mock/node.mock';
import { MatDialog } from '@angular/material/dialog';
import { ActionsService } from '../services/actions.service';
import { dummyAspects } from '../mock/aspects.mock';
describe('ManageRulesSmartComponent', () => {
let fixture: ComponentFixture<ManageRulesSmartComponent>;
@ -61,7 +60,6 @@ describe('ManageRulesSmartComponent', () => {
debugElement = fixture.debugElement;
folderRulesService = TestBed.inject<FolderRulesService>(FolderRulesService);
actionsService = TestBed.inject<ActionsService>(ActionsService);
actionsService.aspects$ = of(dummyAspects);
});
})
);

View File

@ -23,19 +23,43 @@
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
*/
import { Aspect } from '../model/aspect.model';
import { ActionParameterConstraint } from '../model/action-parameter-constraint.model';
export const dummyAspects: Aspect[] = [
export const dummyConstraints: ActionParameterConstraint[] = [
{
value: 'cm:aspect1',
label: 'Label 1'
},
{
value: 'cm:aspect2',
label: 'Label 2'
},
{
value: 'cm:aspect3',
label: 'Label 3'
name: 'aspect-name',
constraints: [
{
value: 'cm:aspect1',
label: 'Label 1'
},
{
value: 'cm:aspect2',
label: 'Label 2'
},
{
value: 'cm:aspect3',
label: ''
}
]
}
];
export const rawConstraints = {
entry: {
constraintValues: [
{
value: 'cm:aspect1',
label: 'Label 1'
},
{
value: 'cm:aspect2',
label: 'Label 2'
},
{
value: 'cm:aspect3'
}
],
constraintName: 'ac-aspects'
}
};

View File

@ -23,10 +23,9 @@
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
*/
import { ActionDefinitionList } from '@alfresco/js-api';
import { ActionDefinitionTransformed, ActionParameterDefinitionTransformed, RuleAction } from '../model/rule-action.model';
export const actionDefListMock: ActionDefinitionList = {
export const actionDefListMock = {
list: {
pagination: {
count: 2,
@ -56,7 +55,8 @@ export const actionDefListMock: ActionDefinitionList = {
name: 'aspect-name',
type: 'd:qname',
multiValued: false,
mandatory: false
mandatory: false,
parameterConstraintName: 'ac-aspects'
}
],
name: 'mock-action-1-definition',
@ -80,7 +80,8 @@ const actionParam1TransformedMock: ActionParameterDefinitionTransformed = {
type: 'd:text',
multiValued: false,
mandatory: true,
displayLabel: 'Mock action parameter text'
displayLabel: 'Mock action parameter text',
parameterConstraintName: ''
};
const actionParam2TransformedMock: ActionParameterDefinitionTransformed = {
@ -88,7 +89,8 @@ const actionParam2TransformedMock: ActionParameterDefinitionTransformed = {
type: 'd:boolean',
multiValued: false,
mandatory: false,
displayLabel: 'mock-action-parameter-boolean'
displayLabel: 'mock-action-parameter-boolean',
parameterConstraintName: ''
};
const actionParam3TransformedMock: ActionParameterDefinitionTransformed = {
@ -96,7 +98,8 @@ const actionParam3TransformedMock: ActionParameterDefinitionTransformed = {
type: 'd:qname',
multiValued: false,
mandatory: false,
displayLabel: 'aspect-name'
displayLabel: 'aspect-name',
parameterConstraintName: 'ac-aspects'
};
const action1TransformedMock: ActionDefinitionTransformed = {

View File

@ -23,7 +23,12 @@
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
*/
export interface Aspect {
export interface ActionParameterConstraint {
name: string;
constraints: ConstraintValue[];
}
export interface ConstraintValue {
value: string;
label: string;
}

View File

@ -49,4 +49,5 @@ export interface ActionParameterDefinitionTransformed {
multiValued: boolean;
mandatory: boolean;
displayLabel: string;
parameterConstraintName?: string;
}

View File

@ -1,7 +1,7 @@
<div class="aca-rule-action-list__item " *ngFor="let control of formControls">
<aca-rule-action
[actionDefinitions]="actionDefinitions"
[aspects]="aspects"
[parameterConstraints]="parameterConstraints"
[readOnly]="readOnly"
[formControl]="control">
</aca-rule-action>

View File

@ -28,7 +28,7 @@ import { ControlValueAccessor, FormArray, FormControl, NG_VALUE_ACCESSOR, Valida
import { ActionDefinitionTransformed, RuleAction } from '../../model/rule-action.model';
import { Subscription } from 'rxjs';
import { ruleActionValidator } from '../validators/rule-actions.validator';
import { Aspect } from '../../model/aspect.model';
import { ActionParameterConstraint } from '../../model/action-parameter-constraint.model';
@Component({
selector: 'aca-rule-action-list',
@ -50,7 +50,7 @@ export class RuleActionListUiComponent implements ControlValueAccessor, OnDestro
@Input()
readOnly = false;
@Input()
aspects: Aspect[] = [];
parameterConstraints: ActionParameterConstraint[] = [];
formArray = new FormArray([]);
private formArraySubscription: Subscription;

View File

@ -29,6 +29,7 @@ import { RuleActionUiComponent } from './rule-action.ui-component';
import { actionsTransformedListMock } from '../../mock/actions.mock';
import { DebugElement } from '@angular/core';
import { By } from '@angular/platform-browser';
import { dummyConstraints } from '../../mock/action-parameter-constraints.mock';
describe('RuleActionUiComponent', () => {
let fixture: ComponentFixture<RuleActionUiComponent>;
@ -71,6 +72,7 @@ describe('RuleActionUiComponent', () => {
it('should populate the card view with parameters when an action is selected', () => {
component.actionDefinitions = actionsTransformedListMock;
component.parameterConstraints = dummyConstraints;
fixture.detectChanges();
const cardView = getByDataAutomationId('rule-action-card-view').componentInstance as CardViewComponent;

View File

@ -38,7 +38,7 @@ import {
import { ActionParameterDefinition } from '@alfresco/js-api';
import { of, Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { Aspect } from '../../model/aspect.model';
import { ActionParameterConstraint, ConstraintValue } from '../../model/action-parameter-constraint.model';
@Component({
selector: 'aca-rule-action',
@ -74,13 +74,13 @@ export class RuleActionUiComponent implements ControlValueAccessor, OnInit, OnDe
this.setDisabledState(isReadOnly);
}
private _aspects;
private _parameterConstraints = [];
@Input()
get aspects(): Aspect[] {
return this._aspects;
get parameterConstraints(): ActionParameterConstraint[] {
return this._parameterConstraints;
}
set aspects(value) {
this._aspects = this.parseAspectsToSelectOptions(value);
set parameterConstraints(value) {
this._parameterConstraints = value.map((obj) => ({ ...obj, constraints: this.parseConstraintsToSelectOptions(obj.constraints) }));
}
isFullWidth = false;
@ -182,17 +182,16 @@ export class RuleActionUiComponent implements ControlValueAccessor, OnInit, OnDe
...cardViewPropertiesModel,
value: this.parameters[paramDef.name] ?? false
});
case 'd:qname':
if (paramDef.name === 'aspect-name' && !this.readOnly) {
default:
const constraintsForDropdownBox = this._parameterConstraints.find((obj) => obj.name === paramDef.name);
if (constraintsForDropdownBox && !this.readOnly) {
this.isFullWidth = true;
return new CardViewSelectItemModel({
...cardViewPropertiesModel,
value: (this.parameters[paramDef.name] as string) ?? '',
options$: of(this._aspects)
options$: of(constraintsForDropdownBox.constraints)
});
}
/* falls through */
default:
return new CardViewTextItemModel({
...cardViewPropertiesModel,
value: this.parameters[paramDef.name] ?? ''
@ -224,8 +223,8 @@ export class RuleActionUiComponent implements ControlValueAccessor, OnInit, OnDe
}
}
private parseAspectsToSelectOptions(aspects: Aspect[]): CardViewSelectItemOption<unknown>[] {
return aspects
private parseConstraintsToSelectOptions(constraints: ConstraintValue[]): CardViewSelectItemOption<unknown>[] {
return constraints
.sort((a, b) => {
if (!a.label && b.label) {
return 1;
@ -235,9 +234,9 @@ export class RuleActionUiComponent implements ControlValueAccessor, OnInit, OnDe
}
return a.label?.localeCompare(b.label) ?? -1;
})
.map((aspect) => ({
key: aspect.value,
label: aspect.label ? `${aspect.label} [${aspect.value}]` : aspect.value
.map((constraint) => ({
key: constraint.value,
label: constraint.label ? `${constraint.label} [${constraint.value}]` : constraint.value
}));
}
}

View File

@ -18,7 +18,7 @@
<ng-template #ruleDetails>
<aca-rule-details
[actionDefinitions]="actionDefinitions$ | async"
[aspects]="aspects$ | async"
[parameterConstraints]="parameterConstraints$ | async"
[value]="model"
(formValueChanged)="formValue = $event"
(formValidationChanged)="onFormValidChange($event)">

View File

@ -66,7 +66,7 @@ describe('EditRuleDialogSmartComponent', () => {
actionsService = TestBed.inject(ActionsService);
spyOn(actionsService, 'loadActionDefinitions').and.stub();
spyOn(actionsService, 'loadAspects').and.stub();
spyOn(actionsService, 'getParameterConstraints').and.stub();
fixture = TestBed.createComponent(EditRuleDialogSmartComponent);
fixture.detectChanges();

View File

@ -23,7 +23,7 @@
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
*/
import { Component, EventEmitter, Inject, OnInit, Output, ViewEncapsulation } from '@angular/core';
import { Component, EventEmitter, Inject, OnInit, OnDestroy, 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';
@ -39,14 +39,15 @@ export interface EditRuleDialogOptions {
encapsulation: ViewEncapsulation.None,
host: { class: 'aca-edit-rule-dialog' }
})
export class EditRuleDialogSmartComponent implements OnInit {
export class EditRuleDialogSmartComponent implements OnInit, OnDestroy {
formValid = false;
model: Partial<Rule>;
formValue: Partial<Rule>;
@Output() submitted = new EventEmitter<Partial<Rule>>();
actionDefinitions$ = this.actionsService.actionDefinitionsListing$;
loading$ = this.actionsService.loading$;
aspects$ = this.actionsService.aspects$;
parameterConstraints$ = this.actionsService.parameterConstraints$;
private _actionDefinitionsSub;
constructor(@Inject(MAT_DIALOG_DATA) public data: EditRuleDialogOptions, private actionsService: ActionsService) {
this.model = this.data?.model || {};
@ -69,8 +70,14 @@ export class EditRuleDialogSmartComponent implements OnInit {
}
ngOnInit() {
this.actionsService.loadAspects();
this.actionsService.loadActionDefinitions();
this._actionDefinitionsSub = this.actionDefinitions$.subscribe((actionDefinitions) =>
this.actionsService.loadActionParameterConstraints(actionDefinitions)
);
}
ngOnDestroy() {
this._actionDefinitionsSub.unsubscribe();
}
onFormValidChange(isValid: boolean) {

View File

@ -46,7 +46,7 @@
<aca-rule-action-list
formControlName="actions"
[actionDefinitions]="actionDefinitions"
[aspects]="aspects"
[parameterConstraints]="parameterConstraints"
[readOnly]="readOnly">
</aca-rule-action-list>
</div>

View File

@ -32,7 +32,7 @@ import { ruleCompositeConditionValidator } from './validators/rule-composite-con
import { FolderRulesService } from '../services/folder-rules.service';
import { ActionDefinitionTransformed } from '../model/rule-action.model';
import { ruleActionsValidator } from './validators/rule-actions.validator';
import { Aspect } from '../model/aspect.model';
import { ActionParameterConstraint } from '../model/action-parameter-constraint.model';
@Component({
selector: 'aca-rule-details',
@ -96,7 +96,7 @@ export class RuleDetailsUiComponent implements OnInit, OnDestroy {
@Input()
actionDefinitions: ActionDefinitionTransformed[] = [];
@Input()
aspects: Aspect[] = [];
parameterConstraints: ActionParameterConstraint[] = [];
@Output()
formValidationChanged = new EventEmitter<boolean>();

View File

@ -29,6 +29,8 @@ import { CoreTestingModule } from '@alfresco/adf-core';
import { ActionsApi } from '@alfresco/js-api';
import { actionDefListMock, actionsTransformedListMock } from '../mock/actions.mock';
import { take } from 'rxjs/operators';
import { dummyConstraints, rawConstraints } from '../mock/action-parameter-constraints.mock';
import { of } from 'rxjs';
describe('ActionsService', () => {
let actionsService: ActionsService;
@ -65,12 +67,29 @@ describe('ActionsService', () => {
expect(await loadingFalsePromise).toBeFalse();
});
it('loadAspects should send correct GET request', async () => {
apiCallSpy = spyOn<any>(actionsService, 'publicApiCall').withArgs(`/action-parameter-constraints/ac-aspects`, 'GET', params).and.returnValue([]);
it('loadParameterConstraints should send GET request and return formatted observable', async () => {
const constraintName = dummyConstraints[0].name;
const formattedConstraints = dummyConstraints[0].constraints;
actionsService.loadAspects();
apiCallSpy = spyOn<any>(actionsService, 'publicApiCall')
.withArgs(`/action-parameter-constraints/${constraintName}`, 'GET', params)
.and.returnValue(of(rawConstraints));
actionsService.getParameterConstraints(constraintName).subscribe((result) => expect(result).toEqual(formattedConstraints));
expect(apiCallSpy).toHaveBeenCalled();
expect(apiCallSpy).toHaveBeenCalledWith(`/action-parameter-constraints/ac-aspects`, 'GET', params);
expect(apiCallSpy).toHaveBeenCalledWith(`/action-parameter-constraints/${constraintName}`, 'GET', params);
});
it('loadActionParameterConstraints should load the data into the observable', async () => {
spyOn<any>(actionsService, 'getParameterConstraints').and.returnValue(of(dummyConstraints[0].constraints));
const constraintsPromise = actionsService.parameterConstraints$.pipe(take(2)).toPromise();
actionsService.loadActionParameterConstraints(actionsTransformedListMock);
const parameterConstraints = await constraintsPromise;
expect(parameterConstraints).toEqual(dummyConstraints);
});
});

View File

@ -24,12 +24,12 @@
*/
import { Injectable } from '@angular/core';
import { ActionDefinition, ActionDefinitionEntry, ActionDefinitionList, ActionParameterDefinition, ActionsApi } from '@alfresco/js-api';
import { ActionDefinition, ActionDefinitionEntry, ActionDefinitionList, ActionsApi } from '@alfresco/js-api';
import { AlfrescoApiService } from '@alfresco/adf-core';
import { BehaviorSubject, from, Observable } from 'rxjs';
import { BehaviorSubject, forkJoin, from, Observable, of } from 'rxjs';
import { finalize, map, switchMap } from 'rxjs/operators';
import { ActionDefinitionTransformed, ActionParameterDefinitionTransformed } from '../model/rule-action.model';
import { finalize, map } from 'rxjs/operators';
import { Aspect } from '../model/aspect.model';
import { ActionParameterConstraint, ConstraintValue } from '../model/action-parameter-constraint.model';
@Injectable({ providedIn: 'root' })
export class ActionsService {
@ -37,8 +37,8 @@ export class ActionsService {
actionDefinitionsListing$ = this.actionDefinitionsListingSource.asObservable();
private loadingSource = new BehaviorSubject<boolean>(false);
loading$ = this.loadingSource.asObservable();
private aspectsSource = new BehaviorSubject<Aspect[]>([]);
aspects$: Observable<Aspect[]> = this.aspectsSource.asObservable();
private parameterConstraintsSource = new BehaviorSubject<ActionParameterConstraint[]>([]);
parameterConstraints$: Observable<ActionParameterConstraint[]> = this.parameterConstraintsSource.asObservable();
private _actionsApi: ActionsApi;
get actionsApi(): ActionsApi {
@ -62,12 +62,10 @@ export class ActionsService {
});
}
loadAspects(): void {
from(this.publicApiCall('/action-parameter-constraints/ac-aspects', 'GET', [{}, {}, {}, {}, {}, ['application/json'], ['application/json']]))
.pipe(map((res) => res.entry.constraintValues.map((entry) => this.formatAspect(entry))))
.subscribe((res) => {
this.aspectsSource.next(res);
});
getParameterConstraints(constraintName): Observable<ConstraintValue[]> {
return from(
this.publicApiCall(`/action-parameter-constraints/${constraintName}`, 'GET', [{}, {}, {}, {}, {}, ['application/json'], ['application/json']])
).pipe(map((res) => res.entry.constraintValues.map((entry) => this.formatConstraint(entry))));
}
private transformActionDefinition(obj: ActionDefinition | ActionDefinitionEntry): ActionDefinitionTransformed {
@ -81,19 +79,20 @@ export class ActionsService {
title: obj.title ?? obj.name ?? '',
applicableTypes: obj.applicableTypes ?? [],
trackStatus: obj.trackStatus ?? false,
parameterDefinitions: (obj.parameterDefinitions ?? []).map((paramDef: ActionParameterDefinition) =>
parameterDefinitions: (obj.parameterDefinitions ?? []).map((paramDef: ActionParameterDefinitionTransformed) =>
this.transformActionParameterDefinition(paramDef)
)
};
}
private transformActionParameterDefinition(obj: ActionParameterDefinition): ActionParameterDefinitionTransformed {
private transformActionParameterDefinition(obj: ActionParameterDefinitionTransformed): ActionParameterDefinitionTransformed {
return {
name: obj.name ?? '',
type: obj.type ?? '',
multiValued: obj.multiValued ?? false,
mandatory: obj.mandatory ?? false,
displayLabel: obj.displayLabel ?? obj.name ?? ''
displayLabel: obj.displayLabel ?? obj.name ?? '',
parameterConstraintName: obj.parameterConstraintName ?? ''
};
}
@ -105,10 +104,37 @@ export class ActionsService {
return this.apiService.getInstance().contentClient.callApi(path, httpMethod, ...params);
}
private formatAspect(aspect): Aspect {
private formatConstraint(constraint): ConstraintValue {
return {
value: aspect.value ?? '',
label: aspect.label ?? ''
value: constraint.value ?? '',
label: constraint.label ?? ''
};
}
loadActionParameterConstraints(actionDefinitions: ActionDefinitionTransformed[]): void {
of(actionDefinitions)
.pipe(
map((actionDefinition) =>
actionDefinition
.map((obj) => obj.parameterDefinitions)
.flat()
.filter((parameterDefinition) => parameterDefinition.parameterConstraintName.length > 0)
.map((parameterDefinition) => ({
name: parameterDefinition.name,
parameterConstraintName: parameterDefinition.parameterConstraintName,
constraints: null
}))
),
switchMap((parameterDefinitions) =>
forkJoin(
...parameterDefinitions.map((parameterDefinition) =>
this.getParameterConstraints(parameterDefinition.parameterConstraintName).pipe(
map((constraints) => ({ name: parameterDefinition.name, constraints }))
)
)
)
)
)
.subscribe((res) => this.parameterConstraintsSource.next(res));
}
}

View File

@ -10,7 +10,7 @@
"types": [],
"lib": [
"dom",
"es2018"
"es2020"
]
},
"angularCompilerOptions": {