[ACS-3219] Delete Rule (#2609)

* [ACS-3219] Delete Rule

* rebased + added unsubscribe()

* small fix

* a couple more fixes

* manage-rules css formatting fix
This commit is contained in:
Nikita Maliarchuk
2022-08-30 11:18:10 +02:00
committed by GitHub
parent 69cb107dd9
commit db47d862e3
9 changed files with 224 additions and 31 deletions

View File

@@ -83,6 +83,12 @@
"TITLE": "The list is empty",
"SUBTITLE": "There are no rules defined for this folder yet."
}
},
"CONFIRMATION_DIALOG": {
"DELETE_RULE": {
"TITLE": "Delete rule",
"MESSAGE": "Are you sure you want to delete this rule?"
}
}
}
}

View File

@@ -32,7 +32,18 @@
<div class="aca-manage-rules__container" *ngIf="(rules$ | async).length > 0 ; else emptyContent">
<aca-rules-list [rules]="rules$ | async" (ruleSelected)="onRuleSelected($event)" [selectedRule]="selectedRule"></aca-rules-list>
<div class="aca-manage-rules__container__rule-details">
<aca-rule-details [readOnly]="true" [value]="selectedRule"></aca-rule-details>
<div class="aca-manage-rules__container__preview">
<div class="aca-manage-rules__container__preview__toolbar">
<span>{{ selectedRule.name }}</span>
<div class="aca-manage-rules__container__preview__toolbar__buttons">
<button mat-icon-button (click)="onRuleDelete()" id="delete-rule-btn">
<mat-icon>delete_outline</mat-icon>
</button>
</div>
</div>
<p>{{ selectedRule.description }}</p>
</div>
<aca-rule-details [readOnly]="true" [preview]="true" [value]="selectedRule"></aca-rule-details>
</div>
</div>

View File

@@ -21,6 +21,34 @@
padding: 32px;
overflow: scroll;
&__preview {
padding: 0 20px;
&__toolbar {
display: flex;
align-items: center;
justify-content: space-between;
span {
font-style: normal;
font-weight: 700;
font-size: 14px;
line-height: 20px;
}
&__buttons {
display: inline-block;
}
}
p {
font-style: normal;
font-weight: 400;
font-size: 12px;
line-height: 16px;
}
}
&__rule-details {
overflow-x: scroll;
}

View File

@@ -33,6 +33,7 @@ import { of } from 'rxjs';
import { dummyRules } from '../mock/rules.mock';
import { By } from '@angular/platform-browser';
import { dummyNodeInfo } from '../mock/node.mock';
import { MatDialog } from '@angular/material/dialog';
describe('ManageRulesSmartComponent', () => {
let fixture: ComponentFixture<ManageRulesSmartComponent>;
@@ -42,7 +43,7 @@ describe('ManageRulesSmartComponent', () => {
beforeEach(
waitForAsync(() => {
const folderRulesServiceSpy = jasmine.createSpyObj('FolderRulesService', ['loadRules']);
const folderRulesServiceSpy = jasmine.createSpyObj('FolderRulesService', ['loadRules', 'deleteRule']);
TestBed.configureTestingModule({
imports: [CoreTestingModule, AcaFolderRulesModule],
providers: [
@@ -61,6 +62,7 @@ describe('ManageRulesSmartComponent', () => {
);
it('should display aca-rules-list and aca-rule-details', () => {
folderRulesService.deletedRuleId$ = of(null);
folderRulesService.folderInfo$ = of(dummyNodeInfo);
folderRulesService.rulesListing$ = of(dummyRules);
folderRulesService.loading$ = of(false);
@@ -73,15 +75,18 @@ describe('ManageRulesSmartComponent', () => {
const rules = debugElement.queryAll(By.css('.aca-rule'));
const ruleDetails = debugElement.queryAll(By.css('aca-rule-details'));
const deleteRuleBtn = debugElement.query(By.css('#delete-rule-btn'));
expect(rules.length).toBe(2, 'Unexpected number of aca-rule');
expect(rules.length).toBe(2, 'unexpected number of aca-rule');
expect(ruleDetails.length).toBeTruthy('aca-rule-details was not rendered');
expect(deleteRuleBtn).toBeTruthy('no delete rule button');
});
it('should only show adf-empty-content if provided node has no rules defined yet', () => {
folderRulesService.folderInfo$ = of(dummyNodeInfo);
folderRulesService.rulesListing$ = of([]);
folderRulesService.loading$ = of(false);
folderRulesService.deletedRuleId$ = of(null);
fixture.detectChanges();
@@ -98,6 +103,7 @@ describe('ManageRulesSmartComponent', () => {
it('should only show aca-generic-error if the non-existing node was provided', () => {
folderRulesService.folderInfo$ = of(null);
folderRulesService.deletedRuleId$ = of(null);
folderRulesService.rulesListing$ = of([]);
folderRulesService.loading$ = of(false);
@@ -116,6 +122,7 @@ describe('ManageRulesSmartComponent', () => {
it('should only show progress bar while loading', () => {
folderRulesService.folderInfo$ = of(null);
folderRulesService.deletedRuleId$ = of(null);
folderRulesService.rulesListing$ = of([]);
folderRulesService.loading$ = of(true);
@@ -131,4 +138,57 @@ describe('ManageRulesSmartComponent', () => {
expect(rules).toBeFalsy();
expect(ruleDetails).toBeFalsy();
});
it('should call deleteRule() if confirmation dialog returns true', () => {
const dialog = TestBed.inject(MatDialog);
folderRulesService.deletedRuleId$ = of(null);
folderRulesService.folderInfo$ = of(dummyNodeInfo);
folderRulesService.rulesListing$ = of(dummyRules);
folderRulesService.loading$ = of(false);
spyOn(component, 'onRuleDelete').and.callThrough();
const dialogResult: any = {
afterClosed: () =>
of(true).subscribe((res) => {
if (res === true) {
folderRulesService.deleteRule(component.nodeId, component.selectedRule.id);
}
})
};
spyOn(dialog, 'open').and.returnValue(dialogResult);
fixture.detectChanges();
expect(component).toBeTruthy('expected component');
const rules = debugElement.queryAll(By.css('.aca-rule'));
const ruleDetails = debugElement.query(By.css('aca-rule-details'));
const deleteRuleBtn = fixture.debugElement.nativeElement.querySelector('#delete-rule-btn');
deleteRuleBtn.click();
fixture.detectChanges();
folderRulesService.deletedRuleId$ = of(component.selectedRule.id);
expect(component.onRuleDelete).toHaveBeenCalled();
expect(dialog.open).toHaveBeenCalled();
expect(folderRulesService.deleteRule).toHaveBeenCalled();
expect(folderRulesService.loadRules).toHaveBeenCalledTimes(1);
expect(rules).toBeTruthy('expected rules');
expect(ruleDetails).toBeTruthy('expected ruleDetails');
expect(deleteRuleBtn).toBeTruthy();
});
it('should run loadRules() when deletedRuleId$ emits new value', () => {
folderRulesService.deletedRuleId$ = of('new-value');
folderRulesService.folderInfo$ = of(dummyNodeInfo);
folderRulesService.rulesListing$ = of(dummyRules);
folderRulesService.loading$ = of(false);
fixture.detectChanges();
expect(component).toBeTruthy();
expect(folderRulesService.loadRules).toHaveBeenCalledTimes(2);
});
});

View File

@@ -23,16 +23,17 @@
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
*/
import { Component, OnInit, ViewEncapsulation } from '@angular/core';
import { Component, OnInit, OnDestroy, ViewEncapsulation } from '@angular/core';
import { Location } from '@angular/common';
import { FolderRulesService } from '../services/folder-rules.service';
import { Observable } from 'rxjs';
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 { EditRuleDialogSmartComponent } from '../rule-details/edit-rule-dialog.smart-component';
import { MatDialog } from '@angular/material/dialog';
import { ConfirmDialogComponent } from '@alfresco/adf-content-services';
@Component({
selector: 'aca-manage-rules',
@@ -41,12 +42,13 @@ import { MatDialog } from '@angular/material/dialog';
encapsulation: ViewEncapsulation.None,
host: { class: 'aca-manage-rules' }
})
export class ManageRulesSmartComponent implements OnInit {
export class ManageRulesSmartComponent implements OnInit, OnDestroy {
rules$: Observable<Rule[]>;
isLoading$: Observable<boolean>;
folderInfo$: Observable<NodeInfo>;
selectedRule: Rule = null;
nodeId: string = null;
deletedRuleSubscription$: Subscription;
constructor(
private location: Location,
@@ -63,6 +65,11 @@ export class ManageRulesSmartComponent implements OnInit {
}
})
);
this.deletedRuleSubscription$ = this.folderRulesService.deletedRuleId$.subscribe((deletedRuleId) => {
if (deletedRuleId) {
this.folderRulesService.loadRules(this.nodeId);
}
});
this.isLoading$ = this.folderRulesService.loading$;
this.folderInfo$ = this.folderRulesService.folderInfo$;
this.route.params.subscribe((params) => {
@@ -73,6 +80,10 @@ export class ManageRulesSmartComponent implements OnInit {
});
}
ngOnDestroy(): void {
this.deletedRuleSubscription$.unsubscribe();
}
goBack(): void {
this.location.back();
}
@@ -87,4 +98,21 @@ export class ManageRulesSmartComponent implements OnInit {
panelClass: 'aca-edit-rule-dialog-container'
});
}
onRuleDelete(): void {
this.matDialogService
.open(ConfirmDialogComponent, {
data: {
title: 'ACA_FOLDER_RULES.CONFIRMATION_DIALOG.DELETE_RULE.TITLE',
message: 'ACA_FOLDER_RULES.CONFIRMATION_DIALOG.DELETE_RULE.MESSAGE'
},
minWidth: '346px'
})
.afterClosed()
.subscribe((result) => {
if (result) {
this.folderRulesService.deleteRule(this.nodeId, this.selectedRule.id);
}
});
}
}

View File

@@ -1,29 +1,32 @@
<form class="aca-rule-details__form" [formGroup]="form">
<div class="aca-rule-details__form__row">
<label for="rule-details-name-input">{{ 'ACA_FOLDER_RULES.RULE_DETAILS.LABEL.NAME' | translate }}</label>
<div>
<mat-form-field floatLabel='never'>
<input
id="rule-details-name-input"
matInput type="text" formControlName="name" data-automation-id="rule-details-name-input"
[placeholder]="getPlaceholder('name') | translate">
<mat-error>{{ getErrorMessage(name) | translate }}</mat-error>
</mat-form-field>
</div>
</div>
<div class="aca-rule-details__form__row">
<label for="rule-details-description-textarea">{{ 'ACA_FOLDER_RULES.RULE_DETAILS.LABEL.DESCRIPTION' | translate }}</label>
<div>
<mat-form-field floatLabel='never'>
<ng-container *ngIf="!preview">
<div class="aca-rule-details__form__row">
<label for="rule-details-name-input">{{ 'ACA_FOLDER_RULES.RULE_DETAILS.LABEL.NAME' | translate }}</label>
<div>
<mat-form-field floatLabel='never'>
<input
id="rule-details-name-input"
matInput type="text" formControlName="name" data-automation-id="rule-details-name-input"
[placeholder]="getPlaceholder('name') | translate">
<mat-error>{{ getErrorMessage(name) | translate }}</mat-error>
</mat-form-field>
</div>
</div>
<div class="aca-rule-details__form__row">
<label for="rule-details-description-textarea">{{ 'ACA_FOLDER_RULES.RULE_DETAILS.LABEL.DESCRIPTION' | translate }}</label>
<div>
<mat-form-field floatLabel='never'>
<textarea
id="rule-details-description-textarea"
matInput formControlName="description" data-automation-id="rule-details-description-textarea"
[placeholder]="getPlaceholder('description') | translate">
</textarea>
</mat-form-field>
</mat-form-field>
</div>
</div>
</div>
</ng-container>
<hr>

View File

@@ -72,6 +72,8 @@ export class RuleDetailsUiComponent implements OnInit, OnDestroy {
this._initialValue = newValue;
}
}
@Input()
preview: boolean;
@Output()
formValidationChanged = new EventEmitter<boolean>();

View File

@@ -39,25 +39,33 @@ describe('FolderRulesService', () => {
let contentApi: ContentApiService;
let rulesPromise: Promise<Partial<Rule>[]>;
let folderInfoPromise: Promise<NodeInfo>;
let deletedRulePromise: Promise<string>;
let rules: Partial<Rule>[];
let folderInfo: NodeInfo;
let deletedRule: string;
let apiCallSpy;
let getNodeSpy;
const nodeId = '********-fake-node-****-********';
const ruleId = '********-fake-rule-****-********';
const ruleSetId = '-default-';
const params = [{}, {}, {}, {}, {}, ['application/json'], ['application/json']];
beforeEach(async () => {
TestBed.configureTestingModule({
imports: [CoreTestingModule],
providers: [FolderRulesService, ContentApiService]
});
folderRulesService = TestBed.inject<FolderRulesService>(FolderRulesService);
});
describe('loadRules', () => {
beforeEach(async () => {
TestBed.configureTestingModule({
imports: [CoreTestingModule],
providers: [FolderRulesService, ContentApiService]
});
folderRulesService = TestBed.inject<FolderRulesService>(FolderRulesService);
contentApi = TestBed.inject<ContentApiService>(ContentApiService);
apiCallSpy = spyOn<any>(folderRulesService, 'apiCall').and.returnValue(of(dummyResponse) as any);
apiCallSpy = spyOn<any>(folderRulesService, 'apiCall')
.withArgs(`/nodes/${nodeId}/rule-sets/${ruleSetId}/rules`, 'GET', params)
.and.returnValue(of(dummyResponse) as any);
getNodeSpy = spyOn<any>(contentApi, 'getNode').and.returnValue(of(dummyGetNodeResponse) as any);
rulesPromise = folderRulesService.rulesListing$.pipe(take(2)).toPromise();
@@ -76,7 +84,29 @@ describe('FolderRulesService', () => {
expect(rules).toEqual(dummyRules, 'The list of rules is incorrectly formatted');
expect(folderInfo).toEqual(dummyNodeInfo, 'The node info is wrong');
expect(apiCallSpy).toHaveBeenCalledTimes(1);
expect(apiCallSpy).toHaveBeenCalledWith(`/nodes/${nodeId}/rule-sets/${ruleSetId}/rules`, 'GET', params);
expect(getNodeSpy).toHaveBeenCalledTimes(1);
});
});
describe('deleteRule', () => {
beforeEach(async () => {
apiCallSpy = spyOn<any>(folderRulesService, 'apiCall')
.withArgs(`/nodes/${nodeId}/rule-sets/${ruleSetId}/rules/${ruleId}`, 'DELETE', params)
.and.returnValue(ruleId);
deletedRulePromise = folderRulesService.deletedRuleId$.pipe(take(2)).toPromise();
folderRulesService.deleteRule(nodeId, ruleId, ruleSetId);
deletedRule = await deletedRulePromise;
});
it('should delete a rule and return its id', async () => {
expect(deletedRule).toBeTruthy('rule has not been deleted');
expect(deletedRule).toBe(ruleId, 'wrong id of deleted rule');
expect(apiCallSpy).toHaveBeenCalledTimes(1);
expect(apiCallSpy).toHaveBeenCalledWith(`/nodes/${nodeId}/rule-sets/${ruleSetId}/rules/${ruleId}`, 'DELETE', params);
});
});
});

View File

@@ -68,6 +68,8 @@ export class FolderRulesService {
folderInfo$: Observable<NodeInfo> = this.folderInfoSource.asObservable();
private loadingSource = new BehaviorSubject<boolean>(false);
loading$ = this.loadingSource.asObservable();
private deletedRuleIdSource = new BehaviorSubject<string>(null);
deletedRuleId$: Observable<string> = this.deletedRuleIdSource.asObservable();
constructor(private apiService: AlfrescoApiService, private contentApi: ContentApiService) {}
@@ -107,6 +109,29 @@ export class FolderRulesService {
);
}
deleteRule(nodeId: string, ruleId: string, ruleSetId: string = '-default-'): void {
this.loadingSource.next(true);
from(
this.apiCall(`/nodes/${nodeId}/rule-sets/${ruleSetId}/rules/${ruleId}`, 'DELETE', [
{},
{},
{},
{},
{},
['application/json'],
['application/json']
])
).subscribe(
() => {
this.deletedRuleIdSource.next(ruleId);
},
(error) => {
this.deletedRuleIdSource.next(error);
this.loadingSource.next(false);
}
);
}
private apiCall(path: string, httpMethod: string, params?: any[]): Promise<any> {
return this.apiService.getInstance().contentClient.callApi(path, httpMethod, ...params);
}