[ACS-4010] Rule sets listing regrouping (#2803)

* [ACS-4010] Rule sets listing regrouping

* Linting

* Unit tests

* Remove TODOs
This commit is contained in:
Thomas Hunter 2022-11-22 11:14:09 +00:00 committed by GitHub
parent ae551f03fc
commit 03ed8e071a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 719 additions and 472 deletions

View File

@ -92,7 +92,8 @@
}, },
"ACTIONS": { "ACTIONS": {
"CREATE_RULE": "Create rule", "CREATE_RULE": "Create rule",
"EDIT_RULE": "Edit" "EDIT_RULE": "Edit",
"SEE_IN_FOLDER": "See in folder"
} }
}, },
"EMPTY_RULES_LIST": { "EMPTY_RULES_LIST": {
@ -107,13 +108,13 @@
} }
}, },
"RULE_LIST": { "RULE_LIST": {
"OWNED_BY_THIS_FOLDER": "Owned by this folder", "OWNED_RULES": "Rules from current folder",
"LINKED_FROM": "Linked from", "LINKED_RULES": "Rules from linked folder",
"INHERITED_FROM": "Inherited from", "INHERITED_RULES": "Inherited rules",
"LOAD_MORE_RULE_SETS": "Load more rule sets", "LOAD_MORE_RULE_SETS": "Load rules from other folders",
"LOADING_RULE_SETS": "Loading rule sets",
"LOAD_MORE_RULES": "Load more rules", "LOAD_MORE_RULES": "Load more rules",
"LOADING_RULES": "Loading rules" "LOADING_RULES": "Loading rules",
"INHERITED_RULES_WILL_BE_RUN_FIRST": "Inherited rules will be run first"
} }
} }
} }

View File

@ -37,12 +37,12 @@ import { RuleSimpleConditionUiComponent } from './rule-details/conditions/rule-s
import { GenericErrorModule, PageLayoutModule } from '@alfresco/aca-shared'; import { GenericErrorModule, PageLayoutModule } from '@alfresco/aca-shared';
import { BreadcrumbModule, DocumentListModule } from '@alfresco/adf-content-services'; import { BreadcrumbModule, DocumentListModule } from '@alfresco/adf-content-services';
import { RuleListItemUiComponent } from './rule-list/rule-list-item/rule-list-item.ui-component'; import { RuleListItemUiComponent } from './rule-list/rule-list-item/rule-list-item.ui-component';
import { RuleListUiComponent } from './rule-list/rule-list/rule-list.ui-component'; import { RuleListGroupingUiComponent } from './rule-list/rule-list-grouping/rule-list-grouping.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 { RuleActionListUiComponent } from './rule-details/actions/rule-action-list.ui-component';
import { RuleActionUiComponent } from './rule-details/actions/rule-action.ui-component'; import { RuleActionUiComponent } from './rule-details/actions/rule-action.ui-component';
import { RuleSetListUiComponent } from './rule-list/rule-set-list/rule-set-list.ui-component'; import { RuleListUiComponent } from './rule-list/rule-list/rule-list.ui-component';
const routes: Routes = [ const routes: Routes = [
{ {
@ -70,9 +70,9 @@ const routes: Routes = [
RuleActionUiComponent, RuleActionUiComponent,
RuleCompositeConditionUiComponent, RuleCompositeConditionUiComponent,
RuleDetailsUiComponent, RuleDetailsUiComponent,
RuleListUiComponent, RuleListGroupingUiComponent,
RuleListItemUiComponent, RuleListItemUiComponent,
RuleSetListUiComponent, RuleListUiComponent,
RuleSimpleConditionUiComponent, RuleSimpleConditionUiComponent,
RuleTriggersUiComponent, RuleTriggersUiComponent,
RuleOptionsUiComponent RuleOptionsUiComponent

View File

@ -12,36 +12,44 @@
<aca-page-layout-content> <aca-page-layout-content>
<div class="main-content"> <div class="main-content">
<ng-container *ngIf="(ruleSetsLoading$ | async) || (actionsLoading$ | async); else onLoaded"> <ng-container *ngIf="((ruleSetsLoading$ | async) && (inheritedRuleSets$ | async).length === 0) || (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>
<ng-template #onLoaded> <ng-template #onLoaded>
<ng-container *ngIf="folderInfo$ | async; else genericError"> <ng-container *ngIf="folderInfo$ | async; else genericError">
<adf-toolbar class="adf-toolbar--inline aca-manage-rules__actions-bar"> <adf-toolbar class="adf-toolbar--inline aca-manage-rules__actions-bar">
<adf-toolbar-title class="aca-manage-rules__actions-bar__title"> <adf-toolbar-title class="aca-manage-rules__actions-bar__title">
<mat-icon class="icon-aligner">folder</mat-icon> <mat-icon class="icon-aligner">folder</mat-icon>
<adf-breadcrumb root="{{ (folderInfo$ | async).name }}:{{'ACA_FOLDER_RULES.MANAGE_RULES.TOOLBAR.BREADCRUMB.RULES' | translate}}" <adf-breadcrumb root="{{ (folderInfo$ | async).name }}:{{'ACA_FOLDER_RULES.MANAGE_RULES.TOOLBAR.BREADCRUMB.RULES' | translate}}"
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)="openCreateUpdateRuleDialog()">{{ 'ACA_FOLDER_RULES.MANAGE_RULES.TOOLBAR.ACTIONS.CREATE_RULE' | translate }}</button> <button
*ngIf="canEditRule(mainRuleSet$ | async)"
data-automation-id="manage-rules-create-button"
mat-flat-button color="primary"
(click)="openCreateUpdateRuleDialog()">
{{ 'ACA_FOLDER_RULES.MANAGE_RULES.TOOLBAR.ACTIONS.CREATE_RULE' | translate }}
</button>
</adf-toolbar> </adf-toolbar>
<mat-divider></mat-divider> <mat-divider></mat-divider>
<div class="aca-manage-rules__container" *ngIf="(ruleSetListing$ | async).length > 0; else emptyContent"> <div class="aca-manage-rules__container" *ngIf="(mainRuleSet$ | async) || (inheritedRuleSets$ | async).length > 0; else emptyContent">
<aca-rule-set-list <aca-rule-list
[mainRuleSet]="mainRuleSet$ | async"
[folderId]="nodeId" [folderId]="nodeId"
[ruleSets]="ruleSetListing$ | async" [inheritedRuleSets]="inheritedRuleSets$ | async"
[hasMoreRuleSets]="hasMoreRuleSets$ | async" [hasMoreRuleSets]="hasMoreRuleSets$ | async"
[ruleSetsLoading]="ruleSetsLoading$ | async" [ruleSetsLoading]="ruleSetsLoading$ | async"
[selectedRule]="selectedRule$ | async" [selectedRule]="selectedRule$ | async"
(loadMoreRuleSets)="onLoadMoreRuleSets()" (loadMoreRuleSets)="onLoadMoreRuleSets()"
(loadMoreRules)="onLoadMoreRules($event)" (loadMoreRules)="onLoadMoreRules($event)"
(navigateToOtherFolder)="onNavigateToOtherFolder($event)"
(selectRule)="onSelectRule($event)" (selectRule)="onSelectRule($event)"
(ruleEnabledChanged)="onRuleEnabledToggle($event[0], $event[1])"> (ruleEnabledChanged)="onRuleEnabledToggle($event[0], $event[1])">
</aca-rule-set-list> </aca-rule-list>
<div class="aca-manage-rules__container__rule-details"> <div class="aca-manage-rules__container__rule-details">
@ -56,12 +64,20 @@
</div> </div>
<div class="aca-manage-rules__container__rule-details__header__buttons"> <div class="aca-manage-rules__container__rule-details__header__buttons">
<ng-container *ngIf="canEditRule(selectedRuleSet$ | async); else goToFolderButton">
<button mat-stroked-button (click)="onRuleDeleteButtonClicked(selectedRule)" id="delete-rule-btn"> <button mat-stroked-button (click)="onRuleDeleteButtonClicked(selectedRule)" id="delete-rule-btn">
<mat-icon>delete_outline</mat-icon> <mat-icon>delete_outline</mat-icon>
</button> </button>
<button mat-stroked-button (click)="openCreateUpdateRuleDialog(selectedRule)" id="edit-rule-btn"> <button mat-stroked-button (click)="openCreateUpdateRuleDialog(selectedRule)" id="edit-rule-btn">
{{ 'ACA_FOLDER_RULES.MANAGE_RULES.TOOLBAR.ACTIONS.EDIT_RULE' | translate }} {{ 'ACA_FOLDER_RULES.MANAGE_RULES.TOOLBAR.ACTIONS.EDIT_RULE' | translate }}
</button> </button>
</ng-container>
<ng-template #goToFolderButton>
<button mat-stroked-button [routerLink]="['/nodes', (selectedRuleSet$ | async).owningFolder.id, 'rules']">
{{ 'ACA_FOLDER_RULES.MANAGE_RULES.TOOLBAR.ACTIONS.SEE_IN_FOLDER' | translate }}
</button>
</ng-template>
</div> </div>
</div> </div>

View File

@ -30,7 +30,7 @@ import { CoreTestingModule } from '@alfresco/adf-core';
import { FolderRulesService } from '../services/folder-rules.service'; import { FolderRulesService } from '../services/folder-rules.service';
import { ActivatedRoute } from '@angular/router'; import { ActivatedRoute } from '@angular/router';
import { of } from 'rxjs'; import { of } from 'rxjs';
import { ruleSetsMock } from '../mock/rule-sets.mock'; import { inheritedRuleSetMock, ownedRuleSetMock, ruleSetWithLinkMock } from '../mock/rule-sets.mock';
import { By } from '@angular/platform-browser'; import { By } from '@angular/platform-browser';
import { owningFolderIdMock, owningFolderMock } from '../mock/node.mock'; import { owningFolderIdMock, owningFolderMock } from '../mock/node.mock';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
@ -74,7 +74,8 @@ describe('ManageRulesSmartComponent', () => {
const loadRuleSetsSpy = spyOn(folderRuleSetsService, 'loadRuleSets').and.stub(); const loadRuleSetsSpy = spyOn(folderRuleSetsService, 'loadRuleSets').and.stub();
folderRuleSetsService.folderInfo$ = of(owningFolderMock); folderRuleSetsService.folderInfo$ = of(owningFolderMock);
folderRuleSetsService.ruleSetListing$ = of(ruleSetsMock); folderRuleSetsService.mainRuleSet$ = of(ownedRuleSetMock);
folderRuleSetsService.inheritedRuleSets$ = of([inheritedRuleSetMock]);
folderRuleSetsService.isLoading$ = of(false); folderRuleSetsService.isLoading$ = of(false);
folderRulesService.selectedRule$ = of(ruleMock('owned-rule-1')); folderRulesService.selectedRule$ = of(ruleMock('owned-rule-1'));
actionsService.loading$ = of(false); actionsService.loading$ = of(false);
@ -85,20 +86,21 @@ describe('ManageRulesSmartComponent', () => {
expect(loadRuleSetsSpy).toHaveBeenCalledOnceWith(component.nodeId); expect(loadRuleSetsSpy).toHaveBeenCalledOnceWith(component.nodeId);
const ruleSets = debugElement.queryAll(By.css(`[data-automation-id="rule-set-list-item"]`)); const ruleGroupingSections = debugElement.queryAll(By.css(`[data-automation-id="rule-list-item"]`));
const rules = debugElement.queryAll(By.css('.aca-rule-list-item')); const rules = debugElement.queryAll(By.css('.aca-rule-list-item'));
const ruleDetails = debugElement.query(By.css('aca-rule-details')); const ruleDetails = debugElement.query(By.css('aca-rule-details'));
const deleteRuleBtn = debugElement.query(By.css('#delete-rule-btn')); const deleteRuleBtn = debugElement.query(By.css('#delete-rule-btn'));
expect(ruleSets.length).toBe(3, 'unexpected number of rule sets'); expect(ruleGroupingSections.length).toBe(2, 'unexpected number of rule sections');
expect(rules.length).toBe(6, 'unexpected number of aca-rule-list-item'); expect(rules.length).toBe(4, 'unexpected number of aca-rule-list-item');
expect(ruleDetails).toBeTruthy('aca-rule-details was not rendered'); expect(ruleDetails).toBeTruthy('aca-rule-details was not rendered');
expect(deleteRuleBtn).toBeTruthy('no delete rule button'); expect(deleteRuleBtn).toBeTruthy('no delete rule button');
}); });
it('should only show adf-empty-content if node has no rules defined yet', () => { it('should only show adf-empty-content if node has no rules defined yet', () => {
folderRuleSetsService.folderInfo$ = of(owningFolderMock); folderRuleSetsService.folderInfo$ = of(owningFolderMock);
folderRuleSetsService.ruleSetListing$ = of([]); folderRuleSetsService.mainRuleSet$ = of(null);
folderRuleSetsService.inheritedRuleSets$ = of([]);
folderRuleSetsService.isLoading$ = of(false); folderRuleSetsService.isLoading$ = of(false);
actionsService.loading$ = of(false); actionsService.loading$ = of(false);
@ -117,7 +119,8 @@ describe('ManageRulesSmartComponent', () => {
it('should only show aca-generic-error if the non-existing node was provided', () => { it('should only show aca-generic-error if the non-existing node was provided', () => {
folderRuleSetsService.folderInfo$ = of(null); folderRuleSetsService.folderInfo$ = of(null);
folderRuleSetsService.ruleSetListing$ = of([]); folderRuleSetsService.mainRuleSet$ = of(null);
folderRuleSetsService.inheritedRuleSets$ = of([]);
folderRuleSetsService.isLoading$ = of(false); folderRuleSetsService.isLoading$ = of(false);
actionsService.loading$ = of(false); actionsService.loading$ = of(false);
@ -136,7 +139,8 @@ describe('ManageRulesSmartComponent', () => {
it('should only show progress bar while loading', async () => { it('should only show progress bar while loading', async () => {
folderRuleSetsService.folderInfo$ = of(null); folderRuleSetsService.folderInfo$ = of(null);
folderRuleSetsService.ruleSetListing$ = of([]); folderRuleSetsService.mainRuleSet$ = of(null);
folderRuleSetsService.inheritedRuleSets$ = of([]);
folderRuleSetsService.isLoading$ = of(true); folderRuleSetsService.isLoading$ = of(true);
actionsService.loading$ = of(true); actionsService.loading$ = of(true);
@ -156,7 +160,8 @@ describe('ManageRulesSmartComponent', () => {
it('should call deleteRule() if confirmation dialog returns true', () => { it('should call deleteRule() if confirmation dialog returns true', () => {
const dialog = TestBed.inject(MatDialog); const dialog = TestBed.inject(MatDialog);
folderRuleSetsService.folderInfo$ = of(owningFolderMock); folderRuleSetsService.folderInfo$ = of(owningFolderMock);
folderRuleSetsService.ruleSetListing$ = of(ruleSetsMock); folderRuleSetsService.mainRuleSet$ = of(ownedRuleSetMock);
folderRuleSetsService.inheritedRuleSets$ = of([inheritedRuleSetMock]);
folderRuleSetsService.isLoading$ = of(false); folderRuleSetsService.isLoading$ = of(false);
folderRulesService.selectedRule$ = of(ruleMock('owned-rule-1')); folderRulesService.selectedRule$ = of(ruleMock('owned-rule-1'));
folderRulesService.deletedRuleId$ = of(null); folderRulesService.deletedRuleId$ = of(null);
@ -191,4 +196,37 @@ describe('ManageRulesSmartComponent', () => {
expect(ruleDetails).toBeTruthy('expected ruleDetails'); expect(ruleDetails).toBeTruthy('expected ruleDetails');
expect(deleteRuleBtn).toBeTruthy(); expect(deleteRuleBtn).toBeTruthy();
}); });
describe('Create rule button visibility', () => {
beforeEach(() => {
folderRuleSetsService.folderInfo$ = of(owningFolderMock);
folderRuleSetsService.inheritedRuleSets$ = of([]);
folderRuleSetsService.isLoading$ = of(false);
actionsService.loading$ = of(false);
});
it('should show the create rule button if there is no main rule set', () => {
folderRuleSetsService.mainRuleSet$ = of(null);
fixture.detectChanges();
const createButton = debugElement.query(By.css(`[data-automation-id="manage-rules-create-button"]`));
expect(createButton).toBeTruthy();
});
it('should show the create rule button if the main rule set is owned', () => {
folderRuleSetsService.mainRuleSet$ = of(ownedRuleSetMock);
fixture.detectChanges();
const createButton = debugElement.query(By.css(`[data-automation-id="manage-rules-create-button"]`));
expect(createButton).toBeTruthy();
});
it('should not show the create rule button if the main rule set is linked', () => {
folderRuleSetsService.mainRuleSet$ = of(ruleSetWithLinkMock);
fixture.detectChanges();
const createButton = debugElement.query(By.css(`[data-automation-id="manage-rules-create-button"]`));
expect(createButton).toBeFalsy();
});
});
}); });

View File

@ -29,7 +29,7 @@ import { FolderRulesService } from '../services/folder-rules.service';
import { Observable, Subject } from 'rxjs'; import { Observable, Subject } 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 { AppStore, NavigateRouteAction, NodeInfo } from '@alfresco/aca-shared/store'; import { NodeInfo } from '@alfresco/aca-shared/store';
import { delay, takeUntil } from 'rxjs/operators'; import { delay, takeUntil } 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';
@ -38,7 +38,6 @@ import { NotificationService } from '@alfresco/adf-core';
import { ActionDefinitionTransformed } from '../model/rule-action.model'; import { ActionDefinitionTransformed } from '../model/rule-action.model';
import { ActionsService } from '../services/actions.service'; import { ActionsService } from '../services/actions.service';
import { FolderRuleSetsService } from '../services/folder-rule-sets.service'; import { FolderRuleSetsService } from '../services/folder-rule-sets.service';
import { Store } from '@ngrx/store';
import { RuleSet } from '../model/rule-set.model'; import { RuleSet } from '../model/rule-set.model';
@Component({ @Component({
@ -51,8 +50,10 @@ import { RuleSet } from '../model/rule-set.model';
export class ManageRulesSmartComponent implements OnInit, OnDestroy { export class ManageRulesSmartComponent implements OnInit, OnDestroy {
nodeId: string = null; nodeId: string = null;
ruleSetListing$: Observable<RuleSet[]>; mainRuleSet$: Observable<RuleSet>;
inheritedRuleSets$: Observable<RuleSet[]>;
selectedRule$: Observable<Rule>; selectedRule$: Observable<Rule>;
selectedRuleSet$: Observable<RuleSet>;
hasMoreRuleSets$: Observable<boolean>; hasMoreRuleSets$: Observable<boolean>;
ruleSetsLoading$: Observable<boolean>; ruleSetsLoading$: Observable<boolean>;
folderInfo$: Observable<NodeInfo>; folderInfo$: Observable<NodeInfo>;
@ -69,13 +70,14 @@ export class ManageRulesSmartComponent implements OnInit, OnDestroy {
private matDialogService: MatDialog, private matDialogService: MatDialog,
private notificationService: NotificationService, private notificationService: NotificationService,
private actionsService: ActionsService, private actionsService: ActionsService,
private folderRuleSetsService: FolderRuleSetsService, private folderRuleSetsService: FolderRuleSetsService
private store: Store<AppStore>
) {} ) {}
ngOnInit() { ngOnInit() {
this.ruleSetListing$ = this.folderRuleSetsService.ruleSetListing$; this.mainRuleSet$ = this.folderRuleSetsService.mainRuleSet$;
this.inheritedRuleSets$ = this.folderRuleSetsService.inheritedRuleSets$;
this.selectedRule$ = this.folderRulesService.selectedRule$; this.selectedRule$ = this.folderRulesService.selectedRule$;
this.selectedRuleSet$ = this.folderRuleSetsService.selectedRuleSet$;
this.hasMoreRuleSets$ = this.folderRuleSetsService.hasMoreRuleSets$; this.hasMoreRuleSets$ = this.folderRuleSetsService.hasMoreRuleSets$;
this.ruleSetsLoading$ = this.folderRuleSetsService.isLoading$; this.ruleSetsLoading$ = this.folderRuleSetsService.isLoading$;
this.folderInfo$ = this.folderRuleSetsService.folderInfo$; this.folderInfo$ = this.folderRuleSetsService.folderInfo$;
@ -136,24 +138,17 @@ export class ManageRulesSmartComponent implements OnInit, OnDestroy {
} }
async onRuleUpdate(rule: Rule) { async onRuleUpdate(rule: Rule) {
const ruleSet = this.folderRuleSetsService.getRuleSetFromRuleId(rule.id); const newRule = await this.folderRulesService.updateRule(this.nodeId, rule.id, rule);
await this.folderRulesService.updateRule(this.nodeId, rule.id, rule, ruleSet.id); this.folderRuleSetsService.addOrUpdateRuleInMainRuleSet(newRule);
this.folderRulesService.loadRules(ruleSet, 0, rule);
} }
async onRuleCreate(ruleCreateParams: Partial<Rule>) { async onRuleCreate(ruleCreateParams: Partial<Rule>) {
await this.folderRulesService.createRule(this.nodeId, ruleCreateParams, '-default-'); const newRule = await this.folderRulesService.createRule(this.nodeId, ruleCreateParams);
const ruleSetToLoad = this.folderRuleSetsService.getOwnedOrLinkedRuleSet(); this.folderRuleSetsService.addOrUpdateRuleInMainRuleSet(newRule);
if (ruleSetToLoad) {
this.folderRulesService.loadRules(ruleSetToLoad, 0, 'last');
} else {
this.folderRuleSetsService.loadMoreRuleSets(true);
}
} }
async onRuleEnabledToggle(rule: Rule, isEnabled: boolean) { async onRuleEnabledToggle(rule: Rule, isEnabled: boolean) {
const ruleSet = this.folderRuleSetsService.getRuleSetFromRuleId(rule.id); await this.folderRulesService.updateRule(this.nodeId, rule.id, { ...rule, isEnabled });
await this.folderRulesService.updateRule(this.nodeId, rule.id, { ...rule, isEnabled }, ruleSet.id);
} }
onRuleDeleteButtonClicked(rule: Rule) { onRuleDeleteButtonClicked(rule: Rule) {
@ -174,25 +169,18 @@ export class ManageRulesSmartComponent implements OnInit, OnDestroy {
} }
onRuleDelete(deletedRuleId: string) { onRuleDelete(deletedRuleId: string) {
if (deletedRuleId) { this.folderRuleSetsService.removeRuleFromMainRuleSet(deletedRuleId);
const folderToRefresh = this.folderRuleSetsService.getRuleSetFromRuleId(deletedRuleId);
if (folderToRefresh?.rules.length > 1) {
this.folderRulesService.loadRules(folderToRefresh, 0, 'first');
} else {
this.folderRuleSetsService.loadRuleSets(this.nodeId);
}
}
}
onNavigateToOtherFolder(nodeId) {
this.store.dispatch(new NavigateRouteAction(['nodes', nodeId, 'rules']));
} }
onLoadMoreRuleSets() { onLoadMoreRuleSets() {
this.folderRuleSetsService.loadMoreRuleSets(); this.folderRuleSetsService.loadMoreInheritedRuleSets();
} }
onLoadMoreRules(ruleSet: RuleSet) { onLoadMoreRules(ruleSet: RuleSet) {
this.folderRulesService.loadRules(ruleSet); this.folderRulesService.loadRules(ruleSet);
} }
canEditRule(ruleSet: RuleSet): boolean {
return !ruleSet || FolderRuleSetsService.isOwnedRuleSet(ruleSet, this.nodeId);
}
} }

View File

@ -66,6 +66,15 @@ export const getRuleSetsResponseMock = {
} }
}; };
export const getDefaultRuleSetResponseMock = {
entry: {
linkedToBy: [],
owningFolder: owningFolderIdMock,
isLinkedTo: false,
id: 'rule-set-no-links'
}
};
export const ruleSetMock = (rules: Rule[] = []): RuleSet => ({ export const ruleSetMock = (rules: Rule[] = []): RuleSet => ({
id: 'rule-set-id', id: 'rule-set-id',
isLinkedTo: false, isLinkedTo: false,
@ -76,7 +85,7 @@ export const ruleSetMock = (rules: Rule[] = []): RuleSet => ({
loadingRules: false loadingRules: false
}); });
const ruleSetWithNoLinksMock: RuleSet = { export const ownedRuleSetMock: RuleSet = {
id: 'rule-set-no-links', id: 'rule-set-no-links',
isLinkedTo: false, isLinkedTo: false,
owningFolder: owningFolderMock, owningFolder: owningFolderMock,
@ -86,7 +95,7 @@ const ruleSetWithNoLinksMock: RuleSet = {
loadingRules: false loadingRules: false
}; };
const ruleSetWithLinkMock: RuleSet = { export const ruleSetWithLinkMock: RuleSet = {
id: 'rule-set-with-link', id: 'rule-set-with-link',
isLinkedTo: true, isLinkedTo: true,
owningFolder: otherFolderMock, owningFolder: otherFolderMock,
@ -96,7 +105,7 @@ const ruleSetWithLinkMock: RuleSet = {
loadingRules: false loadingRules: false
}; };
const inheritedRuleSetMock: RuleSet = { export const inheritedRuleSetMock: RuleSet = {
id: 'inherited-rule-set', id: 'inherited-rule-set',
isLinkedTo: false, isLinkedTo: false,
owningFolder: otherFolderMock, owningFolder: otherFolderMock,
@ -106,4 +115,4 @@ const inheritedRuleSetMock: RuleSet = {
loadingRules: false loadingRules: false
}; };
export const ruleSetsMock: RuleSet[] = [inheritedRuleSetMock, ruleSetWithNoLinksMock, ruleSetWithLinkMock]; export const ruleSetsMock: RuleSet[] = [inheritedRuleSetMock, ownedRuleSetMock, ruleSetWithLinkMock];

View File

@ -24,6 +24,7 @@
*/ */
import { Rule } from '../model/rule.model'; import { Rule } from '../model/rule.model';
import { RuleGroupingItem } from '../model/rule-grouping-item.model';
export const getRulesResponseMock = { export const getRulesResponseMock = {
list: { list: {
@ -158,3 +159,14 @@ export const manyRulesMock: Rule[] = [ruleMock('rule1'), ruleMock('rule2'), rule
export const ownedRulesMock: Rule[] = [ruleMock('owned-rule-1'), ruleMock('owned-rule-2')]; export const ownedRulesMock: Rule[] = [ruleMock('owned-rule-1'), ruleMock('owned-rule-2')];
export const linkedRulesMock: Rule[] = [ruleMock('linked-rule-1'), ruleMock('linked-rule-2')]; export const linkedRulesMock: Rule[] = [ruleMock('linked-rule-1'), ruleMock('linked-rule-2')];
export const inheritedRulesMock: Rule[] = [ruleMock('inherited-rule-1'), ruleMock('inherited-rule-2')]; export const inheritedRulesMock: Rule[] = [ruleMock('inherited-rule-1'), ruleMock('inherited-rule-2')];
export const ruleListGroupingItemsMock: RuleGroupingItem[] = [
{
type: 'rule',
rule: ruleMock('rule1')
},
{
type: 'rule',
rule: ruleMock('rule2')
}
];

View File

@ -0,0 +1,40 @@
/*!
* @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 { Rule } from './rule.model';
import { RuleSet } from './rule-set.model';
export type RuleGroupingItem =
| {
type: 'rule';
rule: Rule;
}
| {
type: 'load-more-rules';
ruleSet: RuleSet;
}
| {
type: 'loading' | 'load-more-rule-sets';
};

View File

@ -0,0 +1,47 @@
<ng-container *ngFor="let item of items">
<aca-rule-list-item
*ngIf="item.type === 'rule'; else loadMoreRules"
matRipple matRippleColor="hsla(0,0%,0%,0.05)"
tabindex="0"
[rule]="item.rule"
[isSelected]="isSelected(item.rule)"
(click)="onRuleClicked(item.rule)"
(enabledChanged)="onEnabledChanged(item.rule, $event)">
</aca-rule-list-item>
<ng-template #loadMoreRules>
<div
*ngIf="item.type === 'load-more-rules'; else loadMoreRuleSets"
tabindex="0"
class="aca-rule-list-grouping__non-rule-item load-more"
matRipple matRippleColor="hsla(0,0%,0%,0.05)"
(click)="onClickLoadMoreRules(item.ruleSet)"
(keyup.enter)="onClickLoadMoreRules(item.ruleSet)">
{{ 'ACA_FOLDER_RULES.RULE_LIST.LOAD_MORE_RULES' | translate }}
</div>
</ng-template>
<ng-template #loadMoreRuleSets>
<div
*ngIf="item.type === 'load-more-rule-sets'; else loadingRules"
tabindex="0"
class="aca-rule-list-grouping__non-rule-item load-more"
matRipple matRippleColor="hsla(0,0%,0%,0.05)"
(click)="onClickLoadMoreRuleSets()"
(keyup.enter)="onClickLoadMoreRuleSets()">
{{ 'ACA_FOLDER_RULES.RULE_LIST.LOAD_MORE_RULE_SETS' | translate }}
</div>
</ng-template>
<ng-template #loadingRules>
<div
tabindex="0"
class="aca-rule-list-grouping__non-rule-item"
matRipple matRippleColor="hsla(0,0%,0%,0.05)">
<mat-spinner mode="indeterminate" [diameter]="16"></mat-spinner>
{{ 'ACA_FOLDER_RULES.RULE_LIST.LOADING_RULES' | translate }}
</div>
</ng-template>
</ng-container>

View File

@ -0,0 +1,27 @@
.aca-rule-list-grouping {
display: flex;
flex-direction: column;
&__non-rule-item {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
color: var(--theme-disabled-text-color);
font-style: italic;
text-align: center;
padding: 0.5em 0;
&.load-more {
cursor: pointer;
}
&:not(:last-child) {
border-bottom: 1px solid var(--theme-border-color);
}
.mat-spinner {
margin-right: 0.5em;
}
}
}

View File

@ -0,0 +1,71 @@
/*!
* @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 { RuleListGroupingUiComponent } from './rule-list-grouping.ui-component';
import { ruleListGroupingItemsMock, rulesMock } from '../../mock/rules.mock';
import { DebugElement } from '@angular/core';
import { By } from '@angular/platform-browser';
import { CoreTestingModule } from '@alfresco/adf-core';
import { AcaFolderRulesModule } from '@alfresco/aca-folder-rules';
describe('RuleListGroupingUiComponent', () => {
let component: RuleListGroupingUiComponent;
let fixture: ComponentFixture<RuleListGroupingUiComponent>;
let debugElement: DebugElement;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [CoreTestingModule, AcaFolderRulesModule],
declarations: [RuleListGroupingUiComponent]
});
fixture = TestBed.createComponent(RuleListGroupingUiComponent);
component = fixture.componentInstance;
debugElement = fixture.debugElement;
});
it('should display the list of rules', () => {
expect(component).toBeTruthy();
component.items = ruleListGroupingItemsMock;
fixture.detectChanges();
const rules = debugElement.queryAll(By.css('.aca-rule-list-item'));
expect(rules).toBeTruthy('Could not find rules');
expect(rules.length).toBe(2, 'Unexpected number of rules');
const rule = debugElement.query(By.css('.aca-rule-list-item:first-child'));
const name = rule.query(By.css('.aca-rule-list-item__header__name'));
const description = rule.query(By.css('.aca-rule-list-item__description'));
const toggleBtn = rule.query(By.css('mat-slide-toggle'));
expect(name.nativeElement.textContent).toBe(rulesMock[0].name);
expect(toggleBtn).toBeTruthy();
expect(description.nativeElement.textContent).toBe(rulesMock[0].description);
});
});

View File

@ -24,84 +24,49 @@
*/ */
import { Component, EventEmitter, Input, Output, ViewEncapsulation } from '@angular/core'; import { Component, EventEmitter, Input, Output, ViewEncapsulation } from '@angular/core';
import { RuleSet } from '../../model/rule-set.model';
import { NodeInfo } from '@alfresco/aca-shared/store';
import { Rule } from '../../model/rule.model'; import { Rule } from '../../model/rule.model';
import { RuleGroupingItem } from '../../model/rule-grouping-item.model';
import { RuleSet } from '../../model/rule-set.model';
@Component({ @Component({
selector: 'aca-rule-set-list', selector: 'aca-rule-list-grouping',
templateUrl: './rule-set-list.ui-component.html', templateUrl: 'rule-list-grouping.ui-component.html',
styleUrls: ['./rule-set-list.ui-component.scss'], styleUrls: ['rule-list-grouping.ui-component.scss'],
encapsulation: ViewEncapsulation.None, encapsulation: ViewEncapsulation.None,
host: { class: 'aca-rule-set-list' } host: { class: 'aca-rule-list-grouping' }
}) })
export class RuleSetListUiComponent { export class RuleListGroupingUiComponent {
@Input() @Input()
folderId = ''; items: RuleGroupingItem[] = [];
private _ruleSets: RuleSet[] = [];
@Input() @Input()
get ruleSets(): RuleSet[] { selectedRule: Rule = null;
return this._ruleSets;
}
set ruleSets(value: RuleSet[]) {
this._ruleSets = value;
this.expandedRuleSets = [...value];
}
@Input()
hasMoreRuleSets = false;
@Input()
ruleSetsLoading = false;
@Input()
selectedRule = null;
@Output()
navigateToOtherFolder = new EventEmitter<string>();
@Output()
loadMoreRuleSets = new EventEmitter<void>();
@Output()
loadMoreRules = new EventEmitter<RuleSet>();
@Output() @Output()
selectRule = new EventEmitter<Rule>(); selectRule = new EventEmitter<Rule>();
@Output() @Output()
ruleEnabledChanged = new EventEmitter<[Rule, boolean]>(); ruleEnabledChanged = new EventEmitter<[Rule, boolean]>();
@Output()
loadMoreRules = new EventEmitter<RuleSet>();
@Output()
loadMoreRuleSets = new EventEmitter<void>();
expandedRuleSets: RuleSet[] = []; onRuleClicked(rule: Rule): void {
isRuleSetLinked(ruleSet: RuleSet): boolean {
return ruleSet.linkedToBy.indexOf(this.folderId) > -1;
}
isRuleSetExpanded(ruleSet: RuleSet): boolean {
return this.expandedRuleSets.indexOf(ruleSet) > -1;
}
clickRuleSetHeader(ruleSet: RuleSet) {
if (this.isRuleSetExpanded(ruleSet)) {
this.expandedRuleSets.splice(this.expandedRuleSets.indexOf(ruleSet), 1);
} else {
this.expandedRuleSets.push(ruleSet);
}
}
clickNavigateButton(folder: NodeInfo) {
if (folder && folder.id) {
this.navigateToOtherFolder.emit(folder.id);
}
}
clickLoadMoreRuleSets() {
this.loadMoreRuleSets.emit();
}
clickLoadMoreRules(ruleSet: RuleSet) {
this.loadMoreRules.emit(ruleSet);
}
onSelectRule(rule: Rule) {
this.selectRule.emit(rule); this.selectRule.emit(rule);
} }
onRuleEnabledChanged(event: [Rule, boolean]) { isSelected(rule): boolean {
this.ruleEnabledChanged.emit(event); return rule.id === this.selectedRule?.id;
}
onEnabledChanged(rule: Rule, isEnabled: boolean) {
this.ruleEnabledChanged.emit([rule, isEnabled]);
}
onClickLoadMoreRules(ruleSet: RuleSet) {
this.loadMoreRules.emit(ruleSet);
}
onClickLoadMoreRuleSets() {
this.loadMoreRuleSets.emit();
} }
} }

View File

@ -1,5 +1,11 @@
<div class="aca-rule-list-item__header"> <div class="aca-rule-list-item__header">
<span class="aca-rule-list-item__header__name">{{ rule.name }}</span> <span class="aca-rule-list-item__header__name">{{ rule.name }}</span>
<mat-slide-toggle [(ngModel)]="rule.isEnabled" (click)="onToggleClick(!rule.isEnabled, $event)"></mat-slide-toggle>
<mat-slide-toggle
[checked]="rule.isEnabled"
(click)="onToggleClick(!rule.isEnabled, $event)">
</mat-slide-toggle>
</div> </div>
<div class="aca-rule-list-item__description">{{ rule.description }}</div> <div class="aca-rule-list-item__description">{{ rule.description }}</div>

View File

@ -45,6 +45,7 @@ export class RuleListItemUiComponent {
onToggleClick(isEnabled: boolean, event: Event) { onToggleClick(isEnabled: boolean, event: Event) {
event.stopPropagation(); event.stopPropagation();
this.rule.isEnabled = !this.rule.isEnabled;
this.enabledChanged.emit(isEnabled); this.enabledChanged.emit(isEnabled);
} }
} }

View File

@ -1,11 +1,80 @@
<div class="aca-rules-list" > <div
<aca-rule-list-item *ngIf="inheritedRuleSetGroupingItems.length > 0"
matRipple matRippleColor="hsla(0,0%,0%,0.05)" class="aca-rule-list__item"
data-automation-id="rule-list-item"
[ngClass]="{ expanded: inheritedRuleSetsExpanded }">
<div class="aca-rule-list__item__header">
<div
tabindex="0" tabindex="0"
*ngFor="let rule of rules" class="aca-rule-list__item__header__title"
[rule]="rule" matRipple matRippleColor="hsla(0,0%,0%,0.05)"
[isSelected]="isSelected(rule)" (click)="inheritedRuleSetsExpanded = !inheritedRuleSetsExpanded"
(click)="onRuleClicked(rule)" (keyup.enter)="inheritedRuleSetsExpanded = !inheritedRuleSetsExpanded">
(enabledChanged)="onEnabledChanged(rule, $event)">
</aca-rule-list-item> <span class="aca-rule-list__item__header__title__text">
{{ 'ACA_FOLDER_RULES.RULE_LIST.INHERITED_RULES' | translate }}
<mat-icon [matTooltip]="'ACA_FOLDER_RULES.RULE_LIST.INHERITED_RULES_WILL_BE_RUN_FIRST' | translate">
info
</mat-icon>
</span>
<mat-icon class="aca-rule-list__item__header__icon">
{{ inheritedRuleSetsExpanded ? 'expand_more' : 'chevron_right' }}
</mat-icon>
</div>
</div>
<aca-rule-list-grouping
*ngIf="inheritedRuleSetsExpanded"
[items]="inheritedRuleSetGroupingItems"
[selectedRule]="selectedRule"
(selectRule)="onSelectRule($event)"
(ruleEnabledChanged)="onRuleEnabledChanged($event)"
(loadMoreRules)="onLoadMoreRules($event)"
(loadMoreRuleSets)="onLoadMoreRuleSets()">
</aca-rule-list-grouping>
</div>
<div
*ngIf="mainRuleSetGroupingItems.length > 0"
class="aca-rule-list__item"
data-automation-id="rule-list-item"
[ngClass]="{ expanded: mainRuleSetExpanded }">
<div class="aca-rule-list__item__header">
<div
tabindex="0"
class="aca-rule-list__item__header__title"
data-automation-id="main-rule-set-title"
matRipple matRippleColor="hsla(0,0%,0%,0.05)"
(click)="mainRuleSetExpanded = !mainRuleSetExpanded"
(keyup.enter)="mainRuleSetExpanded = !mainRuleSetExpanded">
<ng-container *ngIf="isMainRuleSetOwned; else linkedRuleSet">
{{ 'ACA_FOLDER_RULES.RULE_LIST.OWNED_RULES' | translate }}
</ng-container>
<ng-template #linkedRuleSet>
{{ 'ACA_FOLDER_RULES.RULE_LIST.LINKED_RULES' | translate }}
</ng-template>
<mat-icon class="aca-rule-list__item__header__icon">
{{ mainRuleSetExpanded ? 'expand_more' : 'chevron_right' }}
</mat-icon>
</div>
</div>
<aca-rule-list-grouping
*ngIf="mainRuleSetExpanded"
[items]="mainRuleSetGroupingItems"
[selectedRule]="selectedRule"
(selectRule)="onSelectRule($event)"
(ruleEnabledChanged)="onRuleEnabledChanged($event)"
(loadMoreRules)="onLoadMoreRules($event)">
</aca-rule-list-grouping>
</div> </div>

View File

@ -1,4 +1,52 @@
.aca-rule-list { .aca-rule-list {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
overflow-y: auto;
gap: 8px;
&__item {
display: flex;
flex-direction: column;
border: 1px solid var(--theme-border-color);
border-radius: 12px;
overflow: hidden;
&__header {
display: flex;
flex-direction: row;
align-items: stretch;
cursor: pointer;
color: var(--theme-text-color);
user-select: none;
font-size: 0.9em;
& > * {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
}
&__title {
padding: 0.5em 1em;
flex: 1;
&__text {
display: flex;
flex-direction: row;
align-items: center;
.mat-icon {
transform: scale(0.8);
}
}
}
}
&.expanded {
.aca-rule-list__item__header {
border-bottom: 1px solid var(--theme-border-color);
}
}
}
} }

View File

@ -23,49 +23,50 @@
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>. * along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
*/ */
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { RuleListUiComponent } from './rule-list.ui-component'; import { RuleListUiComponent } from './rule-list.ui-component';
import { rulesMock } from '../../mock/rules.mock'; import { ComponentFixture, TestBed } from '@angular/core/testing';
import { CoreTestingModule } from '@alfresco/adf-core';
import { RuleListGroupingUiComponent } from '../rule-list-grouping/rule-list-grouping.ui-component';
import { RuleListItemUiComponent } from '../rule-list-item/rule-list-item.ui-component';
import { ownedRuleSetMock, ruleSetsMock, ruleSetWithLinkMock } from '../../mock/rule-sets.mock';
import { DebugElement } from '@angular/core'; import { DebugElement } from '@angular/core';
import { By } from '@angular/platform-browser'; import { By } from '@angular/platform-browser';
import { CoreTestingModule } from '@alfresco/adf-core'; import { owningFolderIdMock } from '../../mock/node.mock';
import { AcaFolderRulesModule } from '@alfresco/aca-folder-rules';
describe('RuleListUiComponent', () => { describe('RuleListUiComponent', () => {
let component: RuleListUiComponent;
let fixture: ComponentFixture<RuleListUiComponent>; let fixture: ComponentFixture<RuleListUiComponent>;
let component: RuleListUiComponent;
let debugElement: DebugElement; let debugElement: DebugElement;
const innerTextWithoutIcon = (element: HTMLDivElement): string => element.innerText.replace(/(expand_more|chevron_right)$/, '').trim();
beforeEach(() => { beforeEach(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [CoreTestingModule, AcaFolderRulesModule], imports: [CoreTestingModule],
declarations: [RuleListUiComponent] declarations: [RuleListUiComponent, RuleListGroupingUiComponent, RuleListItemUiComponent]
}); });
fixture = TestBed.createComponent(RuleListUiComponent); fixture = TestBed.createComponent(RuleListUiComponent);
component = fixture.componentInstance; component = fixture.componentInstance;
debugElement = fixture.debugElement; debugElement = fixture.debugElement;
component.folderId = owningFolderIdMock;
component.inheritedRuleSets = ruleSetsMock;
}); });
it('should display the list of rules', () => { it('should show "Rules from current folder" as a title if the main rule set is owned', () => {
expect(component).toBeTruthy(); component.mainRuleSet = ownedRuleSetMock;
component.rules = rulesMock;
fixture.detectChanges(); fixture.detectChanges();
const rules = debugElement.queryAll(By.css('.aca-rule-list-item')); const mainRuleSetTitleElement = debugElement.query(By.css(`[data-automation-id="main-rule-set-title"]`));
expect(innerTextWithoutIcon(mainRuleSetTitleElement.nativeElement as HTMLDivElement)).toBe('ACA_FOLDER_RULES.RULE_LIST.OWNED_RULES');
});
expect(rules).toBeTruthy('Could not find rules'); it('should show "Rules from linked folder" as a title if the main rule set is linked', () => {
expect(rules.length).toBe(2, 'Unexpected number of rules'); component.mainRuleSet = ruleSetWithLinkMock;
fixture.detectChanges();
const rule = debugElement.query(By.css('.aca-rule-list-item:first-child')); const mainRuleSetTitleElement = debugElement.query(By.css(`[data-automation-id="main-rule-set-title"]`));
const name = rule.query(By.css('.aca-rule-list-item__header__name')); expect(innerTextWithoutIcon(mainRuleSetTitleElement.nativeElement as HTMLDivElement)).toBe('ACA_FOLDER_RULES.RULE_LIST.LINKED_RULES');
const description = rule.query(By.css('.aca-rule-list-item__description'));
const toggleBtn = rule.query(By.css('mat-slide-toggle'));
expect(name.nativeElement.textContent).toBe(rulesMock[0].name);
expect(toggleBtn).toBeTruthy();
expect(description.nativeElement.textContent).toBe(rulesMock[0].description);
}); });
}); });

View File

@ -24,35 +24,98 @@
*/ */
import { Component, EventEmitter, Input, Output, ViewEncapsulation } from '@angular/core'; import { Component, EventEmitter, Input, Output, ViewEncapsulation } from '@angular/core';
import { RuleSet } from '../../model/rule-set.model';
import { Rule } from '../../model/rule.model'; import { Rule } from '../../model/rule.model';
import { RuleGroupingItem } from '../../model/rule-grouping-item.model';
import { FolderRuleSetsService } from '../../services/folder-rule-sets.service';
@Component({ @Component({
selector: 'aca-rule-list', selector: 'aca-rule-list',
templateUrl: 'rule-list.ui-component.html', templateUrl: './rule-list.ui-component.html',
styleUrls: ['rule-list.ui-component.scss'], styleUrls: ['./rule-list.ui-component.scss'],
encapsulation: ViewEncapsulation.None, encapsulation: ViewEncapsulation.None,
host: { class: 'aca-rule-list' } host: { class: 'aca-rule-list' }
}) })
export class RuleListUiComponent { export class RuleListUiComponent {
@Input() @Input()
rules: Rule[] = []; mainRuleSet: RuleSet = null;
@Input() @Input()
selectedRule: Rule = null; folderId: string;
@Input()
inheritedRuleSets: RuleSet[] = [];
@Input()
hasMoreRuleSets = false;
@Input()
ruleSetsLoading = false;
@Input()
selectedRule = null;
@Output()
loadMoreRuleSets = new EventEmitter<void>();
@Output()
loadMoreRules = new EventEmitter<RuleSet>();
@Output() @Output()
selectRule = new EventEmitter<Rule>(); selectRule = new EventEmitter<Rule>();
@Output() @Output()
ruleEnabledChanged = new EventEmitter<[Rule, boolean]>(); ruleEnabledChanged = new EventEmitter<[Rule, boolean]>();
onRuleClicked(rule: Rule): void { inheritedRuleSetsExpanded = true;
mainRuleSetExpanded = true;
get isMainRuleSetOwned(): boolean {
return FolderRuleSetsService.isOwnedRuleSet(this.mainRuleSet, this.folderId);
}
get mainRuleSetGroupingItems(): RuleGroupingItem[] {
return this.mainRuleSet ? this.getRuleSetGroupingItems(this.mainRuleSet) : [];
}
get inheritedRuleSetGroupingItems(): RuleGroupingItem[] {
const items = this.inheritedRuleSets.reduce((accumulator: RuleGroupingItem[], currentRuleSet: RuleSet) => {
accumulator.push(...this.getRuleSetGroupingItems(currentRuleSet));
return accumulator;
}, []);
if (this.ruleSetsLoading || this.hasMoreRuleSets) {
items.push({
type: this.ruleSetsLoading ? 'loading' : 'load-more-rule-sets'
});
}
return items;
}
getRuleSetGroupingItems(ruleSet: RuleSet): RuleGroupingItem[] {
const items: RuleGroupingItem[] = ruleSet.rules.map((rule: Rule) => ({
type: 'rule',
rule
}));
if (ruleSet.loadingRules || ruleSet.hasMoreRules) {
items.push(
ruleSet.loadingRules
? {
type: 'loading'
}
: {
type: 'load-more-rules',
ruleSet
}
);
}
return items;
}
onLoadMoreRuleSets() {
this.loadMoreRuleSets.emit();
}
onLoadMoreRules(ruleSet: RuleSet) {
this.loadMoreRules.emit(ruleSet);
}
onSelectRule(rule: Rule) {
this.selectRule.emit(rule); this.selectRule.emit(rule);
} }
isSelected(rule): boolean { onRuleEnabledChanged(event: [Rule, boolean]) {
return rule.id === this.selectedRule?.id; this.ruleEnabledChanged.emit(event);
}
onEnabledChanged(rule: Rule, isEnabled: boolean) {
this.ruleEnabledChanged.emit([rule, isEnabled]);
} }
} }

View File

@ -1,90 +0,0 @@
<div
*ngFor="let ruleSet of ruleSets"
class="aca-rule-set-list__item"
data-automation-id="rule-set-list-item"
[ngClass]="{ expanded: isRuleSetExpanded(ruleSet) }">
<div class="aca-rule-set-list__item__header">
<div
tabindex="0"
*ngIf="ruleSet.owningFolder.id !== folderId"
matRipple matRippleColor="hsla(0,0%,0%,0.05)"
class="aca-rule-set-list__item__header__navigate-button"
(click)="clickNavigateButton(ruleSet.owningFolder)"
(keyup.enter)="clickNavigateButton(ruleSet.owningFolder)">
<mat-icon>edit_note</mat-icon>
</div>
<div
tabindex="0"
class="aca-rule-set-list__item__header__title"
data-automation-id="rule-set-item-title"
matRipple matRippleColor="hsla(0,0%,0%,0.05)"
(click)="clickRuleSetHeader(ruleSet)"
(keyup.enter)="clickRuleSetHeader(ruleSet)">
<ng-container *ngIf="ruleSet.owningFolder.id === folderId; else nonOwnedRuleSet">
{{ 'ACA_FOLDER_RULES.RULE_LIST.OWNED_BY_THIS_FOLDER' | translate }}
</ng-container>
<ng-template #nonOwnedRuleSet>
<ng-container *ngIf="isRuleSetLinked(ruleSet); else inheritedRuleSet">
{{ 'ACA_FOLDER_RULES.RULE_LIST.LINKED_FROM' | translate }} {{ ruleSet.owningFolder.name }}
</ng-container>
<ng-template #inheritedRuleSet>
{{ 'ACA_FOLDER_RULES.RULE_LIST.INHERITED_FROM' | translate }} {{ ruleSet.owningFolder.name }}
</ng-template>
</ng-template>
<mat-icon class="aca-rule-set-list__item__header__icon">
{{ isRuleSetExpanded(ruleSet) ? 'expand_more' : 'chevron_right' }}
</mat-icon>
</div>
</div>
<ng-container *ngIf="isRuleSetExpanded(ruleSet)">
<aca-rule-list
[rules]="ruleSet.rules"
[selectedRule]="selectedRule"
(selectRule)="onSelectRule($event)"
(ruleEnabledChanged)="onRuleEnabledChanged($event)">
</aca-rule-list>
<div
*ngIf="ruleSet.hasMoreRules || ruleSet.loadingRules"
tabindex="0"
class="aca-rule-set-list__item__load-more load-more"
matRipple matRippleColor="hsla(0,0%,0%,0.05)"
(click)="clickLoadMoreRules(ruleSet)"
(keyup.enter)="clickLoadMoreRules(ruleSet)">
<ng-container *ngIf="!ruleSet.loadingRules; else rulesLoadingTemplate">
{{ 'ACA_FOLDER_RULES.RULE_LIST.LOAD_MORE_RULES' | translate }}
</ng-container>
<ng-template #rulesLoadingTemplate>
<mat-spinner mode="indeterminate" [diameter]="16"></mat-spinner>
{{ 'ACA_FOLDER_RULES.RULE_LIST.LOADING_RULES' | translate }}
</ng-template>
</div>
</ng-container>
</div>
<div
*ngIf="hasMoreRuleSets"
tabindex="0"
class="aca-rule-set-list__load-more load-more"
matRipple matRippleColor="hsla(0,0%,0%,0.05)"
[matRippleDisabled]="ruleSetsLoading"
(click)="clickLoadMoreRuleSets()"
(keyup.enter)="clickLoadMoreRuleSets()">
<ng-container *ngIf="!ruleSetsLoading; else ruleSetsLoadingTemplate">
{{ 'ACA_FOLDER_RULES.RULE_LIST.LOAD_MORE_RULE_SETS' | translate }}
</ng-container>
<ng-template #ruleSetsLoadingTemplate>
<mat-spinner mode="indeterminate" [diameter]="16"></mat-spinner>
{{ 'ACA_FOLDER_RULES.RULE_LIST.LOADING_RULE_SETS' | translate }}
</ng-template>
</div>

View File

@ -1,78 +0,0 @@
.aca-rule-set-list {
display: flex;
flex-direction: column;
overflow-y: auto;
gap: 8px;
&__item {
display: flex;
flex-direction: column;
border: 1px solid var(--theme-border-color);
border-radius: 12px;
overflow: hidden;
&__header {
display: flex;
flex-direction: row;
align-items: stretch;
cursor: pointer;
color: var(--theme-text-color);
user-select: none;
font-size: 0.9em;
& > * {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
}
&__title {
padding: 0.5em 1em;
flex: 1;
}
&__navigate-button {
border-right: 1px solid var(--theme-border-color);
padding: 0.5em;
mat-icon {
transform: scale(0.8);
transform-origin: center;
}
}
}
&__load-more {
border-top: 1px solid var(--theme-border-color);
padding: 0.5em 1em;
}
&.expanded {
.aca-rule-set-list__item__header {
border-bottom: 1px solid var(--theme-border-color);
}
}
}
&__load-more {
padding: 1em 2em;
border: 1px solid var(--theme-border-color);
border-radius: 12px;
}
.load-more {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
color: var(--theme-disabled-text-color);
font-style: italic;
cursor: pointer;
text-align: center;
.mat-spinner {
margin-right: 0.5em;
}
}
}

View File

@ -1,76 +0,0 @@
/*!
* @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 { RuleSetListUiComponent } from './rule-set-list.ui-component';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { CoreTestingModule } from '@alfresco/adf-core';
import { RuleListUiComponent } from '../rule-list/rule-list.ui-component';
import { RuleListItemUiComponent } from '../rule-list-item/rule-list-item.ui-component';
import { ruleSetsMock } from '../../mock/rule-sets.mock';
import { DebugElement } from '@angular/core';
import { By } from '@angular/platform-browser';
import { owningFolderIdMock } from '../../mock/node.mock';
describe('RuleSetListUiComponent', () => {
let fixture: ComponentFixture<RuleSetListUiComponent>;
let component: RuleSetListUiComponent;
let debugElement: DebugElement;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [CoreTestingModule],
declarations: [RuleSetListUiComponent, RuleListUiComponent, RuleListItemUiComponent]
});
fixture = TestBed.createComponent(RuleSetListUiComponent);
component = fixture.componentInstance;
debugElement = fixture.debugElement;
component.folderId = owningFolderIdMock;
component.ruleSets = ruleSetsMock;
fixture.detectChanges();
});
it('should display a list of rule sets', () => {
const ruleSetElements = debugElement.queryAll(By.css(`[data-automation-id="rule-set-list-item"]`));
expect(ruleSetElements.length).toBe(3);
});
it('should show the right message for the right sort of rule set', () => {
const ruleSetTitleElements = debugElement.queryAll(By.css(`[data-automation-id="rule-set-item-title"]`));
const innerTextWithoutIcon = (element: HTMLDivElement): string => element.innerText.replace(/(expand_more|chevron_right)$/, '').trim();
expect(ruleSetTitleElements.length).toBe(3);
expect(innerTextWithoutIcon(ruleSetTitleElements[0].nativeElement as HTMLDivElement)).toBe(
'ACA_FOLDER_RULES.RULE_LIST.INHERITED_FROM other-folder-name'
);
expect(innerTextWithoutIcon(ruleSetTitleElements[1].nativeElement as HTMLDivElement)).toBe('ACA_FOLDER_RULES.RULE_LIST.OWNED_BY_THIS_FOLDER');
expect(innerTextWithoutIcon(ruleSetTitleElements[2].nativeElement as HTMLDivElement)).toBe(
'ACA_FOLDER_RULES.RULE_LIST.LINKED_FROM other-folder-name'
);
});
});

View File

@ -30,7 +30,7 @@ import { FolderRulesService } from './folder-rules.service';
import { ContentApiService } from '@alfresco/aca-shared'; import { ContentApiService } from '@alfresco/aca-shared';
import { getOtherFolderEntryMock, getOwningFolderEntryMock, otherFolderIdMock, owningFolderIdMock, owningFolderMock } from '../mock/node.mock'; import { getOtherFolderEntryMock, getOwningFolderEntryMock, otherFolderIdMock, owningFolderIdMock, owningFolderMock } from '../mock/node.mock';
import { of } from 'rxjs'; import { of } from 'rxjs';
import { getRuleSetsResponseMock, ruleSetsMock } from '../mock/rule-sets.mock'; import { getDefaultRuleSetResponseMock, getRuleSetsResponseMock, inheritedRuleSetMock, ownedRuleSetMock } from '../mock/rule-sets.mock';
import { take } from 'rxjs/operators'; import { take } from 'rxjs/operators';
import { inheritedRulesMock, linkedRulesMock, ownedRulesMock, ruleMock } from '../mock/rules.mock'; import { inheritedRulesMock, linkedRulesMock, ownedRulesMock, ruleMock } from '../mock/rules.mock';
@ -52,7 +52,11 @@ describe('FolderRuleSetsService', () => {
folderRulesService = TestBed.inject(FolderRulesService); folderRulesService = TestBed.inject(FolderRulesService);
contentApiService = TestBed.inject(ContentApiService); contentApiService = TestBed.inject(ContentApiService);
callApiSpy = spyOn<any>(folderRuleSetsService, 'callApi'); callApiSpy = spyOn<any>(folderRuleSetsService, 'callApi')
.withArgs(`/nodes/${owningFolderIdMock}/rule-sets/-default-?include=isLinkedTo,owningFolder,linkedToBy`, 'GET')
.and.returnValue(of(getDefaultRuleSetResponseMock))
.withArgs(`/nodes/${owningFolderIdMock}/rule-sets?include=isLinkedTo,owningFolder,linkedToBy&skipCount=0&maxItems=100`, 'GET')
.and.returnValue(of(getRuleSetsResponseMock));
spyOn<any>(folderRulesService, 'getRules') spyOn<any>(folderRulesService, 'getRules')
.withArgs(jasmine.anything(), 'rule-set-no-links') .withArgs(jasmine.anything(), 'rule-set-no-links')
.and.returnValue(of({ rules: ownedRulesMock, hasMoreRules: false })) .and.returnValue(of({ rules: ownedRulesMock, hasMoreRules: false }))
@ -69,7 +73,6 @@ describe('FolderRuleSetsService', () => {
}); });
it(`should load node info when loading the node's rule sets`, async () => { it(`should load node info when loading the node's rule sets`, async () => {
callApiSpy.and.returnValue(of(getRuleSetsResponseMock));
// take(2), because: 1 = init of the BehaviourSubject, 2 = in subscribe // take(2), because: 1 = init of the BehaviourSubject, 2 = in subscribe
const folderInfoPromise = folderRuleSetsService.folderInfo$.pipe(take(2)).toPromise(); const folderInfoPromise = folderRuleSetsService.folderInfo$.pipe(take(2)).toPromise();
@ -80,29 +83,38 @@ describe('FolderRuleSetsService', () => {
expect(folderInfo).toEqual(owningFolderMock); expect(folderInfo).toEqual(owningFolderMock);
}); });
it('should load rule sets of a node', async () => { it('should load the main rule set (main or linked)', async () => {
callApiSpy.and.returnValue(of(getRuleSetsResponseMock));
// take(3), because: 1 = init of the BehaviourSubject, 2 = reinitialise at beginning of loadRuleSets, 3 = in subscribe // take(3), because: 1 = init of the BehaviourSubject, 2 = reinitialise at beginning of loadRuleSets, 3 = in subscribe
const ruleSetListingPromise = folderRuleSetsService.ruleSetListing$.pipe(take(3)).toPromise(); const mainRuleSetPromise = folderRuleSetsService.mainRuleSet$.pipe(take(3)).toPromise();
folderRuleSetsService.loadRuleSets(owningFolderIdMock);
const ruleSet = await mainRuleSetPromise;
expect(callApiSpy).toHaveBeenCalledWith(`/nodes/${owningFolderIdMock}/rule-sets/-default-?include=isLinkedTo,owningFolder,linkedToBy`, 'GET');
expect(ruleSet).toEqual(ownedRuleSetMock);
});
it('should load inherited rule sets of a node and filter out owned or inherited rule sets', async () => {
// take(3), because: 1 = init of the BehaviourSubject, 2 = reinitialise at beginning of loadRuleSets, 3 = in subscribe
const inheritedRuleSetsPromise = folderRuleSetsService.inheritedRuleSets$.pipe(take(3)).toPromise();
const hasMoreRuleSetsPromise = folderRuleSetsService.hasMoreRuleSets$.pipe(take(3)).toPromise(); const hasMoreRuleSetsPromise = folderRuleSetsService.hasMoreRuleSets$.pipe(take(3)).toPromise();
folderRuleSetsService.loadRuleSets(owningFolderIdMock); folderRuleSetsService.loadRuleSets(owningFolderIdMock);
const ruleSets = await ruleSetListingPromise; const ruleSets = await inheritedRuleSetsPromise;
const hasMoreRuleSets = await hasMoreRuleSetsPromise; const hasMoreRuleSets = await hasMoreRuleSetsPromise;
expect(callApiSpy).toHaveBeenCalledWith( expect(callApiSpy).toHaveBeenCalledWith(
`/nodes/${owningFolderIdMock}/rule-sets?include=isLinkedTo,owningFolder,linkedToBy&skipCount=0&maxItems=100`, `/nodes/${owningFolderIdMock}/rule-sets?include=isLinkedTo,owningFolder,linkedToBy&skipCount=0&maxItems=100`,
'GET' 'GET'
); );
expect(ruleSets).toEqual(ruleSetsMock); expect(ruleSets).toEqual([inheritedRuleSetMock]);
expect(hasMoreRuleSets).toEqual(false); expect(hasMoreRuleSets).toEqual(false);
}); });
it('should select the first rule of the owned rule set of the folder', async () => { it('should select the first rule of the owned rule set of the folder', async () => {
callApiSpy.and.returnValue(of(getRuleSetsResponseMock));
const selectRuleSpy = spyOn(folderRulesService, 'selectRule'); const selectRuleSpy = spyOn(folderRulesService, 'selectRule');
// take(3), because: 1 = init of the BehaviourSubject, 2 = reinitialise at beginning of loadRuleSets, 3 = in subscribe // take(3), because: 1 = init of the BehaviourSubject, 2 = reinitialise at beginning of loadRuleSets, 3 = in subscribe
const ruleSetListingPromise = folderRuleSetsService.ruleSetListing$.pipe(take(3)).toPromise(); const ruleSetListingPromise = folderRuleSetsService.inheritedRuleSets$.pipe(take(3)).toPromise();
folderRuleSetsService.loadRuleSets(owningFolderIdMock); folderRuleSetsService.loadRuleSets(owningFolderIdMock);
await ruleSetListingPromise; await ruleSetListingPromise;

View File

@ -40,20 +40,46 @@ import { Rule } from '../model/rule.model';
export class FolderRuleSetsService { export class FolderRuleSetsService {
public static MAX_RULE_SETS_PER_GET = 100; public static MAX_RULE_SETS_PER_GET = 100;
static isOwnedRuleSet(ruleSet: RuleSet, nodeId: string): boolean {
return ruleSet.owningFolder.id === nodeId;
}
static isLinkedRuleSet(ruleSet: RuleSet, nodeId: string): boolean {
return ruleSet.linkedToBy.indexOf(nodeId) > -1;
}
static isMainRuleSet(ruleSet: RuleSet, nodeId: string): boolean {
return this.isOwnedRuleSet(ruleSet, nodeId) || this.isLinkedRuleSet(ruleSet, nodeId);
}
static isInheritedRuleSet(ruleSet: RuleSet, nodeId: string): boolean {
return !this.isMainRuleSet(ruleSet, nodeId);
}
private currentFolder: NodeInfo = null; private currentFolder: NodeInfo = null;
private ruleSets: RuleSet[] = []; private mainRuleSet: RuleSet = null;
private inheritedRuleSets: RuleSet[] = [];
private hasMoreRuleSets = true; private hasMoreRuleSets = true;
private ruleSetListingSource = new BehaviorSubject<RuleSet[]>([]); private mainRuleSetSource = new BehaviorSubject<RuleSet>(null);
private inheritedRuleSetsSource = new BehaviorSubject<RuleSet[]>([]);
private hasMoreRuleSetsSource = new BehaviorSubject<boolean>(true); private hasMoreRuleSetsSource = new BehaviorSubject<boolean>(true);
private folderInfoSource = new BehaviorSubject<NodeInfo>(null); private folderInfoSource = new BehaviorSubject<NodeInfo>(null);
private isLoadingSource = new BehaviorSubject<boolean>(false); private isLoadingSource = new BehaviorSubject<boolean>(false);
ruleSetListing$: Observable<RuleSet[]> = this.ruleSetListingSource.asObservable(); mainRuleSet$: Observable<RuleSet> = this.mainRuleSetSource.asObservable();
inheritedRuleSets$: Observable<RuleSet[]> = this.inheritedRuleSetsSource.asObservable();
hasMoreRuleSets$: Observable<boolean> = this.hasMoreRuleSetsSource.asObservable(); hasMoreRuleSets$: Observable<boolean> = this.hasMoreRuleSetsSource.asObservable();
folderInfo$: Observable<NodeInfo> = this.folderInfoSource.asObservable(); folderInfo$: Observable<NodeInfo> = this.folderInfoSource.asObservable();
isLoading$ = this.isLoadingSource.asObservable(); isLoading$ = this.isLoadingSource.asObservable();
selectedRuleSet$ = this.folderRulesService.selectedRule$.pipe(
map((rule: Rule) => {
if (this.mainRuleSet?.rules.findIndex((r: Rule) => r.id === rule.id) > -1) {
return this.mainRuleSet;
} else {
return this.inheritedRuleSets.find((ruleSet: RuleSet) => ruleSet.rules.findIndex((r: Rule) => r.id === rule.id) > -1) ?? null;
}
})
);
constructor(private apiService: AlfrescoApiService, private contentApi: ContentApiService, private folderRulesService: FolderRulesService) {} constructor(private apiService: AlfrescoApiService, private contentApi: ContentApiService, private folderRulesService: FolderRulesService) {}
private callApi(path: string, httpMethod: string, body: object = {}): Promise<any> { private callApi(path: string, httpMethod: string, body: object = {}): Promise<any> {
@ -62,7 +88,19 @@ export class FolderRuleSetsService {
return this.apiService.getInstance().contentPrivateClient.callApi(path, httpMethod, ...params); return this.apiService.getInstance().contentPrivateClient.callApi(path, httpMethod, ...params);
} }
private getRuleSets(nodeId: string, skipCount = 0): Observable<RuleSet[]> { private getMainRuleSet(nodeId: string): Observable<RuleSet> {
return from(this.callApi(`/nodes/${nodeId}/rule-sets/-default-?include=isLinkedTo,owningFolder,linkedToBy`, 'GET')).pipe(
catchError((error) => {
if (error.status === 404) {
return of({ entry: null });
}
return of(error);
}),
switchMap((res) => this.formatRuleSet(res.entry))
);
}
private getInheritedRuleSets(nodeId: string, skipCount = 0): Observable<RuleSet[]> {
return from( return from(
this.callApi( this.callApi(
`/nodes/${nodeId}/rule-sets?include=isLinkedTo,owningFolder,linkedToBy&skipCount=${skipCount}&maxItems=${FolderRuleSetsService.MAX_RULE_SETS_PER_GET}`, `/nodes/${nodeId}/rule-sets?include=isLinkedTo,owningFolder,linkedToBy&skipCount=${skipCount}&maxItems=${FolderRuleSetsService.MAX_RULE_SETS_PER_GET}`,
@ -74,16 +112,19 @@ export class FolderRuleSetsService {
this.hasMoreRuleSets = res.list.pagination.hasMoreItems; this.hasMoreRuleSets = res.list.pagination.hasMoreItems;
} }
}), }),
switchMap((res) => this.formatRuleSets(res)) switchMap((res) => this.formatRuleSets(res)),
map((ruleSets: RuleSet[]) => ruleSets.filter((ruleSet) => FolderRuleSetsService.isInheritedRuleSet(ruleSet, this.currentFolder.id)))
); );
} }
loadRuleSets(nodeId: string) { loadRuleSets(nodeId: string) {
this.isLoadingSource.next(true); this.isLoadingSource.next(true);
this.ruleSets = []; this.mainRuleSet = null;
this.inheritedRuleSets = [];
this.hasMoreRuleSets = true; this.hasMoreRuleSets = true;
this.currentFolder = null; this.currentFolder = null;
this.ruleSetListingSource.next(this.ruleSets); this.mainRuleSetSource.next(this.mainRuleSet);
this.inheritedRuleSetsSource.next(this.inheritedRuleSets);
this.hasMoreRuleSetsSource.next(this.hasMoreRuleSets); this.hasMoreRuleSetsSource.next(this.hasMoreRuleSets);
this.getNodeInfo(nodeId) this.getNodeInfo(nodeId)
.pipe( .pipe(
@ -91,29 +132,27 @@ export class FolderRuleSetsService {
this.currentFolder = nodeInfo; this.currentFolder = nodeInfo;
this.folderInfoSource.next(this.currentFolder); this.folderInfoSource.next(this.currentFolder);
}), }),
switchMap(() => this.getRuleSets(nodeId)), switchMap(() => combineLatest(this.getMainRuleSet(nodeId), this.getInheritedRuleSets(nodeId))),
finalize(() => this.isLoadingSource.next(false)) finalize(() => this.isLoadingSource.next(false))
) )
.subscribe((ruleSets: RuleSet[]) => { .subscribe(([mainRuleSet, inheritedRuleSets]) => {
this.ruleSets = ruleSets; this.mainRuleSet = mainRuleSet;
this.ruleSetListingSource.next(this.ruleSets); this.inheritedRuleSets = inheritedRuleSets;
this.mainRuleSetSource.next(mainRuleSet);
this.inheritedRuleSetsSource.next(inheritedRuleSets);
this.hasMoreRuleSetsSource.next(this.hasMoreRuleSets); this.hasMoreRuleSetsSource.next(this.hasMoreRuleSets);
this.folderRulesService.selectRule(this.getOwnedOrLinkedRuleSet()?.rules[0] ?? ruleSets[0]?.rules[0]); this.folderRulesService.selectRule(mainRuleSet?.rules[0] ?? inheritedRuleSets[0]?.rules[0] ?? null);
}); });
} }
loadMoreRuleSets(selectLastRule = false) { loadMoreInheritedRuleSets() {
this.isLoadingSource.next(true); this.isLoadingSource.next(true);
this.getRuleSets(this.currentFolder.id, this.ruleSets.length) this.getInheritedRuleSets(this.currentFolder.id, this.inheritedRuleSets.length)
.pipe(finalize(() => this.isLoadingSource.next(false))) .pipe(finalize(() => this.isLoadingSource.next(false)))
.subscribe((ruleSets) => { .subscribe((ruleSets) => {
this.ruleSets.push(...ruleSets); this.inheritedRuleSets.push(...ruleSets);
this.ruleSetListingSource.next(this.ruleSets); this.inheritedRuleSetsSource.next(this.inheritedRuleSets);
this.hasMoreRuleSetsSource.next(this.hasMoreRuleSets); this.hasMoreRuleSetsSource.next(this.hasMoreRuleSets);
if (selectLastRule) {
const ownedRuleSet = this.getOwnedOrLinkedRuleSet();
this.folderRulesService.selectRule(ownedRuleSet?.rules[ownedRuleSet.rules.length - 1]);
}
}); });
} }
@ -140,6 +179,9 @@ export class FolderRuleSetsService {
} }
private formatRuleSet(entry: any): Observable<RuleSet> { private formatRuleSet(entry: any): Observable<RuleSet> {
if (!entry) {
return of(null);
}
return combineLatest( return combineLatest(
this.currentFolder?.id === entry.owningFolder ? of(this.currentFolder) : this.getNodeInfo(entry.owningFolder || ''), this.currentFolder?.id === entry.owningFolder ? of(this.currentFolder) : this.getNodeInfo(entry.owningFolder || ''),
this.folderRulesService.getRules(entry.owningFolder || '', entry.id) this.folderRulesService.getRules(entry.owningFolder || '', entry.id)
@ -156,15 +198,38 @@ export class FolderRuleSetsService {
); );
} }
getRuleSetFromRuleId(ruleId: string): RuleSet { removeRuleFromMainRuleSet(ruleId: string) {
return this.ruleSets.find((ruleSet: RuleSet) => ruleSet.rules.findIndex((r: Rule) => r.id === ruleId) > -1) ?? null; if (this.mainRuleSet) {
const index = this.mainRuleSet.rules.findIndex((rule: Rule) => rule.id === ruleId);
if (index > -1) {
if (this.mainRuleSet.rules.length > 1) {
this.mainRuleSet.rules.splice(index, 1);
} else {
this.mainRuleSet = null;
this.mainRuleSetSource.next(this.mainRuleSet);
}
}
}
} }
getOwnedOrLinkedRuleSet(): RuleSet { addOrUpdateRuleInMainRuleSet(newRule: Rule) {
return ( if (this.mainRuleSet) {
this.ruleSets.find( const index = this.mainRuleSet.rules.findIndex((rule: Rule) => rule.id === newRule.id);
(ruleSet: RuleSet) => ruleSet.owningFolder.id === this.currentFolder.id || ruleSet.linkedToBy.indexOf(this.currentFolder.id) > -1 if (index > -1) {
) ?? null this.mainRuleSet.rules.splice(index, 1, newRule);
); } else {
this.mainRuleSet.rules.push(newRule);
}
this.folderRulesService.selectRule(newRule);
} else {
this.getMainRuleSet(this.currentFolder.id).subscribe((mainRuleSet: RuleSet) => {
this.mainRuleSet = mainRuleSet;
this.mainRuleSetSource.next(mainRuleSet);
if (mainRuleSet) {
const ruleToSelect = mainRuleSet.rules.find((rule: Rule) => rule.id === newRule.id);
this.folderRulesService.selectRule(ruleToSelect);
}
});
}
} }
} }

View File

@ -41,6 +41,8 @@ describe('FolderRulesService', () => {
const nodeId = owningFolderIdMock; const nodeId = owningFolderIdMock;
const ruleSetId = 'rule-set-id'; const ruleSetId = 'rule-set-id';
const mockedRule = ruleMock('rule-mock'); const mockedRule = ruleMock('rule-mock');
const { id, ...mockedRuleWithoutId } = mockedRule;
const mockedRuleEntry = { entry: mockedRule };
const ruleId = mockedRule.id; const ruleId = mockedRule.id;
beforeEach(() => { beforeEach(() => {
@ -120,14 +122,18 @@ describe('FolderRulesService', () => {
}); });
it('should send correct POST request and return created rule', async () => { it('should send correct POST request and return created rule', async () => {
callApiSpy.withArgs(`/nodes/${nodeId}/rule-sets/${ruleSetId}/rules`, 'POST', mockedRule).and.returnValue(Promise.resolve(mockedRule)); callApiSpy
const result = await folderRulesService.createRule(nodeId, mockedRule, ruleSetId); .withArgs(`/nodes/${nodeId}/rule-sets/${ruleSetId}/rules`, 'POST', mockedRuleWithoutId)
.and.returnValue(Promise.resolve(mockedRuleEntry));
const result = await folderRulesService.createRule(nodeId, mockedRuleWithoutId, ruleSetId);
expect(result).toEqual(mockedRule); expect(result).toEqual(mockedRule);
}); });
it('should send correct PUT request to update rule and return it', async () => { it('should send correct PUT request to update rule and return it', async () => {
callApiSpy.withArgs(`/nodes/${nodeId}/rule-sets/${ruleSetId}/rules/${ruleId}`, 'PUT', mockedRule).and.returnValue(Promise.resolve(mockedRule)); callApiSpy
.withArgs(`/nodes/${nodeId}/rule-sets/${ruleSetId}/rules/${ruleId}`, 'PUT', mockedRule)
.and.returnValue(Promise.resolve(mockedRuleEntry));
const result = await folderRulesService.updateRule(nodeId, ruleId, mockedRule, ruleSetId); const result = await folderRulesService.updateRule(nodeId, ruleId, mockedRule, ruleSetId);
expect(result).toEqual(mockedRule); expect(result).toEqual(mockedRule);

View File

@ -126,23 +126,19 @@ export class FolderRulesService {
ruleSet.hasMoreRules = res.hasMoreRules; ruleSet.hasMoreRules = res.hasMoreRules;
ruleSet.rules.splice(skipCount); ruleSet.rules.splice(skipCount);
ruleSet.rules.push(...res.rules); ruleSet.rules.push(...res.rules);
if (selectRule === 'first') { this.selectRuleInRuleSet(ruleSet, selectRule);
this.selectRule(ruleSet.rules[0]);
} else if (selectRule === 'last') {
this.selectRule(ruleSet.rules[ruleSet.rules.length - 1]);
} else if (selectRule) {
this.selectRule(selectRule);
}
}); });
} }
} }
createRule(nodeId: string, rule: Partial<Rule>, ruleSetId: string): Promise<unknown> { async createRule(nodeId: string, rule: Partial<Rule>, ruleSetId: string = '-default-'): Promise<Rule> {
return this.callApi(`/nodes/${nodeId}/rule-sets/${ruleSetId}/rules`, 'POST', { ...rule }); const response = await this.callApi(`/nodes/${nodeId}/rule-sets/${ruleSetId}/rules`, 'POST', { ...rule });
return this.formatRule(response.entry);
} }
updateRule(nodeId: string, ruleId: string, rule: Rule, ruleSetId: string): Promise<unknown> { async updateRule(nodeId: string, ruleId: string, rule: Rule, ruleSetId: string = '-default-'): Promise<Rule> {
return this.callApi(`/nodes/${nodeId}/rule-sets/${ruleSetId}/rules/${ruleId}`, 'PUT', { ...rule }); const response = await this.callApi(`/nodes/${nodeId}/rule-sets/${ruleSetId}/rules/${ruleId}`, 'PUT', { ...rule });
return this.formatRule(response.entry);
} }
deleteRule(nodeId: string, ruleId: string, ruleSetId: string = '-default-') { deleteRule(nodeId: string, ruleId: string, ruleSetId: string = '-default-') {
@ -207,4 +203,14 @@ export class FolderRulesService {
selectRule(rule: Rule) { selectRule(rule: Rule) {
this.selectedRuleSource.next(rule); this.selectedRuleSource.next(rule);
} }
selectRuleInRuleSet(ruleSet: RuleSet, selectRule: 'first' | 'last' | Rule = null) {
if (selectRule === 'first') {
this.selectRule(ruleSet.rules[0]);
} else if (selectRule === 'last') {
this.selectRule(ruleSet.rules[ruleSet.rules.length - 1]);
} else if (selectRule) {
this.selectRule(selectRule);
}
}
} }