From 03ed8e071aa96539041f8950d6a0dbe219c8a469 Mon Sep 17 00:00:00 2001 From: Thomas Hunter <thomas.hunter@hyland.com> Date: Tue, 22 Nov 2022 11:14:09 +0000 Subject: [PATCH] =?UTF-8?q?=EF=BB=BF[ACS-4010]=20Rule=20sets=20listing=20r?= =?UTF-8?q?egrouping=20(#2803)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [ACS-4010] Rule sets listing regrouping * Linting * Unit tests * Remove TODOs --- projects/aca-folder-rules/assets/i18n/en.json | 15 ++- .../src/lib/folder-rules.module.ts | 8 +- .../manage-rules.smart-component.html | 42 ++++-- .../manage-rules.smart-component.spec.ts | 56 ++++++-- .../manage-rules.smart-component.ts | 50 +++----- .../src/lib/mock/rule-sets.mock.ts | 17 ++- .../src/lib/mock/rules.mock.ts | 12 ++ .../src/lib/model/rule-grouping-item.model.ts | 40 ++++++ .../rule-list-grouping.ui-component.html | 47 +++++++ .../rule-list-grouping.ui-component.scss | 27 ++++ .../rule-list-grouping.ui-component.spec.ts | 71 ++++++++++ .../rule-list-grouping.ui-component.ts} | 91 ++++--------- .../rule-list-item.ui-component.html | 8 +- .../rule-list-item.ui-component.ts | 1 + .../rule-list/rule-list.ui-component.html | 89 +++++++++++-- .../rule-list/rule-list.ui-component.scss | 48 +++++++ .../rule-list/rule-list.ui-component.spec.ts | 47 +++---- .../rule-list/rule-list.ui-component.ts | 85 ++++++++++-- .../rule-set-list.ui-component.html | 90 ------------- .../rule-set-list.ui-component.scss | 78 ----------- .../rule-set-list.ui-component.spec.ts | 76 ----------- .../services/folder-rule-sets.service.spec.ts | 32 +++-- .../lib/services/folder-rule-sets.service.ts | 121 ++++++++++++++---- .../lib/services/folder-rules.service.spec.ts | 12 +- .../src/lib/services/folder-rules.service.ts | 28 ++-- 25 files changed, 719 insertions(+), 472 deletions(-) create mode 100644 projects/aca-folder-rules/src/lib/model/rule-grouping-item.model.ts create mode 100644 projects/aca-folder-rules/src/lib/rule-list/rule-list-grouping/rule-list-grouping.ui-component.html create mode 100644 projects/aca-folder-rules/src/lib/rule-list/rule-list-grouping/rule-list-grouping.ui-component.scss create mode 100644 projects/aca-folder-rules/src/lib/rule-list/rule-list-grouping/rule-list-grouping.ui-component.spec.ts rename projects/aca-folder-rules/src/lib/rule-list/{rule-set-list/rule-set-list.ui-component.ts => rule-list-grouping/rule-list-grouping.ui-component.ts} (54%) delete mode 100644 projects/aca-folder-rules/src/lib/rule-list/rule-set-list/rule-set-list.ui-component.html delete mode 100644 projects/aca-folder-rules/src/lib/rule-list/rule-set-list/rule-set-list.ui-component.scss delete mode 100644 projects/aca-folder-rules/src/lib/rule-list/rule-set-list/rule-set-list.ui-component.spec.ts diff --git a/projects/aca-folder-rules/assets/i18n/en.json b/projects/aca-folder-rules/assets/i18n/en.json index 574e57a2c..529d0e798 100644 --- a/projects/aca-folder-rules/assets/i18n/en.json +++ b/projects/aca-folder-rules/assets/i18n/en.json @@ -92,7 +92,8 @@ }, "ACTIONS": { "CREATE_RULE": "Create rule", - "EDIT_RULE": "Edit" + "EDIT_RULE": "Edit", + "SEE_IN_FOLDER": "See in folder" } }, "EMPTY_RULES_LIST": { @@ -107,13 +108,13 @@ } }, "RULE_LIST": { - "OWNED_BY_THIS_FOLDER": "Owned by this folder", - "LINKED_FROM": "Linked from", - "INHERITED_FROM": "Inherited from", - "LOAD_MORE_RULE_SETS": "Load more rule sets", - "LOADING_RULE_SETS": "Loading rule sets", + "OWNED_RULES": "Rules from current folder", + "LINKED_RULES": "Rules from linked folder", + "INHERITED_RULES": "Inherited rules", + "LOAD_MORE_RULE_SETS": "Load rules from other folders", "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" } } } diff --git a/projects/aca-folder-rules/src/lib/folder-rules.module.ts b/projects/aca-folder-rules/src/lib/folder-rules.module.ts index e55904991..5b1537504 100644 --- a/projects/aca-folder-rules/src/lib/folder-rules.module.ts +++ b/projects/aca-folder-rules/src/lib/folder-rules.module.ts @@ -37,12 +37,12 @@ import { RuleSimpleConditionUiComponent } from './rule-details/conditions/rule-s import { GenericErrorModule, PageLayoutModule } from '@alfresco/aca-shared'; import { BreadcrumbModule, DocumentListModule } from '@alfresco/adf-content-services'; 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 { RuleOptionsUiComponent } from './rule-details/options/rule-options.ui-component'; import { RuleActionListUiComponent } from './rule-details/actions/rule-action-list.ui-component'; import { RuleActionUiComponent } from './rule-details/actions/rule-action.ui-component'; -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 = [ { @@ -70,9 +70,9 @@ const routes: Routes = [ RuleActionUiComponent, RuleCompositeConditionUiComponent, RuleDetailsUiComponent, - RuleListUiComponent, + RuleListGroupingUiComponent, RuleListItemUiComponent, - RuleSetListUiComponent, + RuleListUiComponent, RuleSimpleConditionUiComponent, RuleTriggersUiComponent, RuleOptionsUiComponent diff --git a/projects/aca-folder-rules/src/lib/manage-rules/manage-rules.smart-component.html b/projects/aca-folder-rules/src/lib/manage-rules/manage-rules.smart-component.html index e8e5ac85e..18d674bdd 100644 --- a/projects/aca-folder-rules/src/lib/manage-rules/manage-rules.smart-component.html +++ b/projects/aca-folder-rules/src/lib/manage-rules/manage-rules.smart-component.html @@ -12,36 +12,44 @@ <aca-page-layout-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> </ng-container> <ng-template #onLoaded> <ng-container *ngIf="folderInfo$ | async; else genericError"> <adf-toolbar class="adf-toolbar--inline aca-manage-rules__actions-bar"> + <adf-toolbar-title class="aca-manage-rules__actions-bar__title"> <mat-icon class="icon-aligner">folder</mat-icon> <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> </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> <mat-divider></mat-divider> - <div class="aca-manage-rules__container" *ngIf="(ruleSetListing$ | async).length > 0; else emptyContent"> - <aca-rule-set-list + <div class="aca-manage-rules__container" *ngIf="(mainRuleSet$ | async) || (inheritedRuleSets$ | async).length > 0; else emptyContent"> + <aca-rule-list + [mainRuleSet]="mainRuleSet$ | async" [folderId]="nodeId" - [ruleSets]="ruleSetListing$ | async" + [inheritedRuleSets]="inheritedRuleSets$ | async" [hasMoreRuleSets]="hasMoreRuleSets$ | async" [ruleSetsLoading]="ruleSetsLoading$ | async" [selectedRule]="selectedRule$ | async" (loadMoreRuleSets)="onLoadMoreRuleSets()" (loadMoreRules)="onLoadMoreRules($event)" - (navigateToOtherFolder)="onNavigateToOtherFolder($event)" (selectRule)="onSelectRule($event)" (ruleEnabledChanged)="onRuleEnabledToggle($event[0], $event[1])"> - </aca-rule-set-list> + </aca-rule-list> <div class="aca-manage-rules__container__rule-details"> @@ -56,12 +64,20 @@ </div> <div class="aca-manage-rules__container__rule-details__header__buttons"> - <button mat-stroked-button (click)="onRuleDeleteButtonClicked(selectedRule)" id="delete-rule-btn"> - <mat-icon>delete_outline</mat-icon> - </button> - <button mat-stroked-button (click)="openCreateUpdateRuleDialog(selectedRule)" id="edit-rule-btn"> - {{ 'ACA_FOLDER_RULES.MANAGE_RULES.TOOLBAR.ACTIONS.EDIT_RULE' | translate }} - </button> + <ng-container *ngIf="canEditRule(selectedRuleSet$ | async); else goToFolderButton"> + <button mat-stroked-button (click)="onRuleDeleteButtonClicked(selectedRule)" id="delete-rule-btn"> + <mat-icon>delete_outline</mat-icon> + </button> + <button mat-stroked-button (click)="openCreateUpdateRuleDialog(selectedRule)" id="edit-rule-btn"> + {{ 'ACA_FOLDER_RULES.MANAGE_RULES.TOOLBAR.ACTIONS.EDIT_RULE' | translate }} + </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> diff --git a/projects/aca-folder-rules/src/lib/manage-rules/manage-rules.smart-component.spec.ts b/projects/aca-folder-rules/src/lib/manage-rules/manage-rules.smart-component.spec.ts index 39614828c..13052ef7e 100644 --- a/projects/aca-folder-rules/src/lib/manage-rules/manage-rules.smart-component.spec.ts +++ b/projects/aca-folder-rules/src/lib/manage-rules/manage-rules.smart-component.spec.ts @@ -30,7 +30,7 @@ import { CoreTestingModule } from '@alfresco/adf-core'; import { FolderRulesService } from '../services/folder-rules.service'; import { ActivatedRoute } from '@angular/router'; 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 { owningFolderIdMock, owningFolderMock } from '../mock/node.mock'; import { MatDialog } from '@angular/material/dialog'; @@ -74,7 +74,8 @@ describe('ManageRulesSmartComponent', () => { const loadRuleSetsSpy = spyOn(folderRuleSetsService, 'loadRuleSets').and.stub(); folderRuleSetsService.folderInfo$ = of(owningFolderMock); - folderRuleSetsService.ruleSetListing$ = of(ruleSetsMock); + folderRuleSetsService.mainRuleSet$ = of(ownedRuleSetMock); + folderRuleSetsService.inheritedRuleSets$ = of([inheritedRuleSetMock]); folderRuleSetsService.isLoading$ = of(false); folderRulesService.selectedRule$ = of(ruleMock('owned-rule-1')); actionsService.loading$ = of(false); @@ -85,20 +86,21 @@ describe('ManageRulesSmartComponent', () => { 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 ruleDetails = debugElement.query(By.css('aca-rule-details')); const deleteRuleBtn = debugElement.query(By.css('#delete-rule-btn')); - expect(ruleSets.length).toBe(3, 'unexpected number of rule sets'); - expect(rules.length).toBe(6, 'unexpected number of aca-rule-list-item'); + expect(ruleGroupingSections.length).toBe(2, 'unexpected number of rule sections'); + expect(rules.length).toBe(4, 'unexpected number of aca-rule-list-item'); expect(ruleDetails).toBeTruthy('aca-rule-details was not rendered'); expect(deleteRuleBtn).toBeTruthy('no delete rule button'); }); it('should only show adf-empty-content if node has no rules defined yet', () => { folderRuleSetsService.folderInfo$ = of(owningFolderMock); - folderRuleSetsService.ruleSetListing$ = of([]); + folderRuleSetsService.mainRuleSet$ = of(null); + folderRuleSetsService.inheritedRuleSets$ = of([]); folderRuleSetsService.isLoading$ = 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', () => { folderRuleSetsService.folderInfo$ = of(null); - folderRuleSetsService.ruleSetListing$ = of([]); + folderRuleSetsService.mainRuleSet$ = of(null); + folderRuleSetsService.inheritedRuleSets$ = of([]); folderRuleSetsService.isLoading$ = of(false); actionsService.loading$ = of(false); @@ -136,7 +139,8 @@ describe('ManageRulesSmartComponent', () => { it('should only show progress bar while loading', async () => { folderRuleSetsService.folderInfo$ = of(null); - folderRuleSetsService.ruleSetListing$ = of([]); + folderRuleSetsService.mainRuleSet$ = of(null); + folderRuleSetsService.inheritedRuleSets$ = of([]); folderRuleSetsService.isLoading$ = of(true); actionsService.loading$ = of(true); @@ -156,7 +160,8 @@ describe('ManageRulesSmartComponent', () => { it('should call deleteRule() if confirmation dialog returns true', () => { const dialog = TestBed.inject(MatDialog); folderRuleSetsService.folderInfo$ = of(owningFolderMock); - folderRuleSetsService.ruleSetListing$ = of(ruleSetsMock); + folderRuleSetsService.mainRuleSet$ = of(ownedRuleSetMock); + folderRuleSetsService.inheritedRuleSets$ = of([inheritedRuleSetMock]); folderRuleSetsService.isLoading$ = of(false); folderRulesService.selectedRule$ = of(ruleMock('owned-rule-1')); folderRulesService.deletedRuleId$ = of(null); @@ -191,4 +196,37 @@ describe('ManageRulesSmartComponent', () => { expect(ruleDetails).toBeTruthy('expected ruleDetails'); 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(); + }); + }); }); diff --git a/projects/aca-folder-rules/src/lib/manage-rules/manage-rules.smart-component.ts b/projects/aca-folder-rules/src/lib/manage-rules/manage-rules.smart-component.ts index a46d99af4..aca129d01 100644 --- a/projects/aca-folder-rules/src/lib/manage-rules/manage-rules.smart-component.ts +++ b/projects/aca-folder-rules/src/lib/manage-rules/manage-rules.smart-component.ts @@ -29,7 +29,7 @@ import { FolderRulesService } from '../services/folder-rules.service'; import { Observable, Subject } from 'rxjs'; import { Rule } from '../model/rule.model'; 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 { EditRuleDialogSmartComponent } from '../rule-details/edit-rule-dialog.smart-component'; import { MatDialog } from '@angular/material/dialog'; @@ -38,7 +38,6 @@ import { NotificationService } from '@alfresco/adf-core'; import { ActionDefinitionTransformed } from '../model/rule-action.model'; import { ActionsService } from '../services/actions.service'; import { FolderRuleSetsService } from '../services/folder-rule-sets.service'; -import { Store } from '@ngrx/store'; import { RuleSet } from '../model/rule-set.model'; @Component({ @@ -51,8 +50,10 @@ import { RuleSet } from '../model/rule-set.model'; export class ManageRulesSmartComponent implements OnInit, OnDestroy { nodeId: string = null; - ruleSetListing$: Observable<RuleSet[]>; + mainRuleSet$: Observable<RuleSet>; + inheritedRuleSets$: Observable<RuleSet[]>; selectedRule$: Observable<Rule>; + selectedRuleSet$: Observable<RuleSet>; hasMoreRuleSets$: Observable<boolean>; ruleSetsLoading$: Observable<boolean>; folderInfo$: Observable<NodeInfo>; @@ -69,13 +70,14 @@ export class ManageRulesSmartComponent implements OnInit, OnDestroy { private matDialogService: MatDialog, private notificationService: NotificationService, private actionsService: ActionsService, - private folderRuleSetsService: FolderRuleSetsService, - private store: Store<AppStore> + private folderRuleSetsService: FolderRuleSetsService ) {} ngOnInit() { - this.ruleSetListing$ = this.folderRuleSetsService.ruleSetListing$; + this.mainRuleSet$ = this.folderRuleSetsService.mainRuleSet$; + this.inheritedRuleSets$ = this.folderRuleSetsService.inheritedRuleSets$; this.selectedRule$ = this.folderRulesService.selectedRule$; + this.selectedRuleSet$ = this.folderRuleSetsService.selectedRuleSet$; this.hasMoreRuleSets$ = this.folderRuleSetsService.hasMoreRuleSets$; this.ruleSetsLoading$ = this.folderRuleSetsService.isLoading$; this.folderInfo$ = this.folderRuleSetsService.folderInfo$; @@ -136,24 +138,17 @@ export class ManageRulesSmartComponent implements OnInit, OnDestroy { } async onRuleUpdate(rule: Rule) { - const ruleSet = this.folderRuleSetsService.getRuleSetFromRuleId(rule.id); - await this.folderRulesService.updateRule(this.nodeId, rule.id, rule, ruleSet.id); - this.folderRulesService.loadRules(ruleSet, 0, rule); + const newRule = await this.folderRulesService.updateRule(this.nodeId, rule.id, rule); + this.folderRuleSetsService.addOrUpdateRuleInMainRuleSet(newRule); } async onRuleCreate(ruleCreateParams: Partial<Rule>) { - await this.folderRulesService.createRule(this.nodeId, ruleCreateParams, '-default-'); - const ruleSetToLoad = this.folderRuleSetsService.getOwnedOrLinkedRuleSet(); - if (ruleSetToLoad) { - this.folderRulesService.loadRules(ruleSetToLoad, 0, 'last'); - } else { - this.folderRuleSetsService.loadMoreRuleSets(true); - } + const newRule = await this.folderRulesService.createRule(this.nodeId, ruleCreateParams); + this.folderRuleSetsService.addOrUpdateRuleInMainRuleSet(newRule); } async onRuleEnabledToggle(rule: Rule, isEnabled: boolean) { - const ruleSet = this.folderRuleSetsService.getRuleSetFromRuleId(rule.id); - await this.folderRulesService.updateRule(this.nodeId, rule.id, { ...rule, isEnabled }, ruleSet.id); + await this.folderRulesService.updateRule(this.nodeId, rule.id, { ...rule, isEnabled }); } onRuleDeleteButtonClicked(rule: Rule) { @@ -174,25 +169,18 @@ export class ManageRulesSmartComponent implements OnInit, OnDestroy { } onRuleDelete(deletedRuleId: string) { - if (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'])); + this.folderRuleSetsService.removeRuleFromMainRuleSet(deletedRuleId); } onLoadMoreRuleSets() { - this.folderRuleSetsService.loadMoreRuleSets(); + this.folderRuleSetsService.loadMoreInheritedRuleSets(); } onLoadMoreRules(ruleSet: RuleSet) { this.folderRulesService.loadRules(ruleSet); } + + canEditRule(ruleSet: RuleSet): boolean { + return !ruleSet || FolderRuleSetsService.isOwnedRuleSet(ruleSet, this.nodeId); + } } diff --git a/projects/aca-folder-rules/src/lib/mock/rule-sets.mock.ts b/projects/aca-folder-rules/src/lib/mock/rule-sets.mock.ts index e7c47603b..d0d4d45ca 100644 --- a/projects/aca-folder-rules/src/lib/mock/rule-sets.mock.ts +++ b/projects/aca-folder-rules/src/lib/mock/rule-sets.mock.ts @@ -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 => ({ id: 'rule-set-id', isLinkedTo: false, @@ -76,7 +85,7 @@ export const ruleSetMock = (rules: Rule[] = []): RuleSet => ({ loadingRules: false }); -const ruleSetWithNoLinksMock: RuleSet = { +export const ownedRuleSetMock: RuleSet = { id: 'rule-set-no-links', isLinkedTo: false, owningFolder: owningFolderMock, @@ -86,7 +95,7 @@ const ruleSetWithNoLinksMock: RuleSet = { loadingRules: false }; -const ruleSetWithLinkMock: RuleSet = { +export const ruleSetWithLinkMock: RuleSet = { id: 'rule-set-with-link', isLinkedTo: true, owningFolder: otherFolderMock, @@ -96,7 +105,7 @@ const ruleSetWithLinkMock: RuleSet = { loadingRules: false }; -const inheritedRuleSetMock: RuleSet = { +export const inheritedRuleSetMock: RuleSet = { id: 'inherited-rule-set', isLinkedTo: false, owningFolder: otherFolderMock, @@ -106,4 +115,4 @@ const inheritedRuleSetMock: RuleSet = { loadingRules: false }; -export const ruleSetsMock: RuleSet[] = [inheritedRuleSetMock, ruleSetWithNoLinksMock, ruleSetWithLinkMock]; +export const ruleSetsMock: RuleSet[] = [inheritedRuleSetMock, ownedRuleSetMock, ruleSetWithLinkMock]; diff --git a/projects/aca-folder-rules/src/lib/mock/rules.mock.ts b/projects/aca-folder-rules/src/lib/mock/rules.mock.ts index 52b7a67fb..295a196f8 100644 --- a/projects/aca-folder-rules/src/lib/mock/rules.mock.ts +++ b/projects/aca-folder-rules/src/lib/mock/rules.mock.ts @@ -24,6 +24,7 @@ */ import { Rule } from '../model/rule.model'; +import { RuleGroupingItem } from '../model/rule-grouping-item.model'; export const getRulesResponseMock = { 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 linkedRulesMock: Rule[] = [ruleMock('linked-rule-1'), ruleMock('linked-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') + } +]; diff --git a/projects/aca-folder-rules/src/lib/model/rule-grouping-item.model.ts b/projects/aca-folder-rules/src/lib/model/rule-grouping-item.model.ts new file mode 100644 index 000000000..4b7581084 --- /dev/null +++ b/projects/aca-folder-rules/src/lib/model/rule-grouping-item.model.ts @@ -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'; + }; diff --git a/projects/aca-folder-rules/src/lib/rule-list/rule-list-grouping/rule-list-grouping.ui-component.html b/projects/aca-folder-rules/src/lib/rule-list/rule-list-grouping/rule-list-grouping.ui-component.html new file mode 100644 index 000000000..0f2a6beba --- /dev/null +++ b/projects/aca-folder-rules/src/lib/rule-list/rule-list-grouping/rule-list-grouping.ui-component.html @@ -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> diff --git a/projects/aca-folder-rules/src/lib/rule-list/rule-list-grouping/rule-list-grouping.ui-component.scss b/projects/aca-folder-rules/src/lib/rule-list/rule-list-grouping/rule-list-grouping.ui-component.scss new file mode 100644 index 000000000..bfb12d73f --- /dev/null +++ b/projects/aca-folder-rules/src/lib/rule-list/rule-list-grouping/rule-list-grouping.ui-component.scss @@ -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; + } + } +} diff --git a/projects/aca-folder-rules/src/lib/rule-list/rule-list-grouping/rule-list-grouping.ui-component.spec.ts b/projects/aca-folder-rules/src/lib/rule-list/rule-list-grouping/rule-list-grouping.ui-component.spec.ts new file mode 100644 index 000000000..e3dd30c14 --- /dev/null +++ b/projects/aca-folder-rules/src/lib/rule-list/rule-list-grouping/rule-list-grouping.ui-component.spec.ts @@ -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); + }); +}); diff --git a/projects/aca-folder-rules/src/lib/rule-list/rule-set-list/rule-set-list.ui-component.ts b/projects/aca-folder-rules/src/lib/rule-list/rule-list-grouping/rule-list-grouping.ui-component.ts similarity index 54% rename from projects/aca-folder-rules/src/lib/rule-list/rule-set-list/rule-set-list.ui-component.ts rename to projects/aca-folder-rules/src/lib/rule-list/rule-list-grouping/rule-list-grouping.ui-component.ts index 7a980e7de..d5820feb6 100644 --- a/projects/aca-folder-rules/src/lib/rule-list/rule-set-list/rule-set-list.ui-component.ts +++ b/projects/aca-folder-rules/src/lib/rule-list/rule-list-grouping/rule-list-grouping.ui-component.ts @@ -24,84 +24,49 @@ */ 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 { RuleGroupingItem } from '../../model/rule-grouping-item.model'; +import { RuleSet } from '../../model/rule-set.model'; @Component({ - selector: 'aca-rule-set-list', - templateUrl: './rule-set-list.ui-component.html', - styleUrls: ['./rule-set-list.ui-component.scss'], + selector: 'aca-rule-list-grouping', + templateUrl: 'rule-list-grouping.ui-component.html', + styleUrls: ['rule-list-grouping.ui-component.scss'], encapsulation: ViewEncapsulation.None, - host: { class: 'aca-rule-set-list' } + host: { class: 'aca-rule-list-grouping' } }) -export class RuleSetListUiComponent { +export class RuleListGroupingUiComponent { @Input() - folderId = ''; - private _ruleSets: RuleSet[] = []; + items: RuleGroupingItem[] = []; @Input() - get ruleSets(): RuleSet[] { - return this._ruleSets; - } - set ruleSets(value: RuleSet[]) { - this._ruleSets = value; - this.expandedRuleSets = [...value]; - } - @Input() - hasMoreRuleSets = false; - @Input() - ruleSetsLoading = false; - @Input() - selectedRule = null; + selectedRule: Rule = null; - @Output() - navigateToOtherFolder = new EventEmitter<string>(); - @Output() - loadMoreRuleSets = new EventEmitter<void>(); - @Output() - loadMoreRules = new EventEmitter<RuleSet>(); @Output() selectRule = new EventEmitter<Rule>(); @Output() ruleEnabledChanged = new EventEmitter<[Rule, boolean]>(); + @Output() + loadMoreRules = new EventEmitter<RuleSet>(); + @Output() + loadMoreRuleSets = new EventEmitter<void>(); - expandedRuleSets: RuleSet[] = []; - - 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) { + onRuleClicked(rule: Rule): void { this.selectRule.emit(rule); } - onRuleEnabledChanged(event: [Rule, boolean]) { - this.ruleEnabledChanged.emit(event); + isSelected(rule): boolean { + 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(); } } diff --git a/projects/aca-folder-rules/src/lib/rule-list/rule-list-item/rule-list-item.ui-component.html b/projects/aca-folder-rules/src/lib/rule-list/rule-list-item/rule-list-item.ui-component.html index 6d878afce..01fd0064a 100644 --- a/projects/aca-folder-rules/src/lib/rule-list/rule-list-item/rule-list-item.ui-component.html +++ b/projects/aca-folder-rules/src/lib/rule-list/rule-list-item/rule-list-item.ui-component.html @@ -1,5 +1,11 @@ <div class="aca-rule-list-item__header"> + <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 class="aca-rule-list-item__description">{{ rule.description }}</div> diff --git a/projects/aca-folder-rules/src/lib/rule-list/rule-list-item/rule-list-item.ui-component.ts b/projects/aca-folder-rules/src/lib/rule-list/rule-list-item/rule-list-item.ui-component.ts index 0af44c3b3..49f7cc679 100644 --- a/projects/aca-folder-rules/src/lib/rule-list/rule-list-item/rule-list-item.ui-component.ts +++ b/projects/aca-folder-rules/src/lib/rule-list/rule-list-item/rule-list-item.ui-component.ts @@ -45,6 +45,7 @@ export class RuleListItemUiComponent { onToggleClick(isEnabled: boolean, event: Event) { event.stopPropagation(); + this.rule.isEnabled = !this.rule.isEnabled; this.enabledChanged.emit(isEnabled); } } diff --git a/projects/aca-folder-rules/src/lib/rule-list/rule-list/rule-list.ui-component.html b/projects/aca-folder-rules/src/lib/rule-list/rule-list/rule-list.ui-component.html index bf0ccdc31..0b88f4069 100644 --- a/projects/aca-folder-rules/src/lib/rule-list/rule-list/rule-list.ui-component.html +++ b/projects/aca-folder-rules/src/lib/rule-list/rule-list/rule-list.ui-component.html @@ -1,11 +1,80 @@ -<div class="aca-rules-list" > - <aca-rule-list-item - matRipple matRippleColor="hsla(0,0%,0%,0.05)" - tabindex="0" - *ngFor="let rule of rules" - [rule]="rule" - [isSelected]="isSelected(rule)" - (click)="onRuleClicked(rule)" - (enabledChanged)="onEnabledChanged(rule, $event)"> - </aca-rule-list-item> +<div + *ngIf="inheritedRuleSetGroupingItems.length > 0" + class="aca-rule-list__item" + data-automation-id="rule-list-item" + [ngClass]="{ expanded: inheritedRuleSetsExpanded }"> + + <div class="aca-rule-list__item__header"> + + <div + tabindex="0" + class="aca-rule-list__item__header__title" + matRipple matRippleColor="hsla(0,0%,0%,0.05)" + (click)="inheritedRuleSetsExpanded = !inheritedRuleSetsExpanded" + (keyup.enter)="inheritedRuleSetsExpanded = !inheritedRuleSetsExpanded"> + + <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> diff --git a/projects/aca-folder-rules/src/lib/rule-list/rule-list/rule-list.ui-component.scss b/projects/aca-folder-rules/src/lib/rule-list/rule-list/rule-list.ui-component.scss index 29824e01e..7dee1e717 100644 --- a/projects/aca-folder-rules/src/lib/rule-list/rule-list/rule-list.ui-component.scss +++ b/projects/aca-folder-rules/src/lib/rule-list/rule-list/rule-list.ui-component.scss @@ -1,4 +1,52 @@ .aca-rule-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; + + &__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); + } + } + } } diff --git a/projects/aca-folder-rules/src/lib/rule-list/rule-list/rule-list.ui-component.spec.ts b/projects/aca-folder-rules/src/lib/rule-list/rule-list/rule-list.ui-component.spec.ts index cc65d8901..f9fd3c59f 100644 --- a/projects/aca-folder-rules/src/lib/rule-list/rule-list/rule-list.ui-component.spec.ts +++ b/projects/aca-folder-rules/src/lib/rule-list/rule-list/rule-list.ui-component.spec.ts @@ -23,49 +23,50 @@ * 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 { 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 { By } from '@angular/platform-browser'; -import { CoreTestingModule } from '@alfresco/adf-core'; -import { AcaFolderRulesModule } from '@alfresco/aca-folder-rules'; +import { owningFolderIdMock } from '../../mock/node.mock'; describe('RuleListUiComponent', () => { - let component: RuleListUiComponent; let fixture: ComponentFixture<RuleListUiComponent>; + let component: RuleListUiComponent; let debugElement: DebugElement; + const innerTextWithoutIcon = (element: HTMLDivElement): string => element.innerText.replace(/(expand_more|chevron_right)$/, '').trim(); + beforeEach(() => { TestBed.configureTestingModule({ - imports: [CoreTestingModule, AcaFolderRulesModule], - declarations: [RuleListUiComponent] + imports: [CoreTestingModule], + declarations: [RuleListUiComponent, RuleListGroupingUiComponent, RuleListItemUiComponent] }); fixture = TestBed.createComponent(RuleListUiComponent); component = fixture.componentInstance; debugElement = fixture.debugElement; + + component.folderId = owningFolderIdMock; + component.inheritedRuleSets = ruleSetsMock; }); - it('should display the list of rules', () => { - expect(component).toBeTruthy(); - - component.rules = rulesMock; - + it('should show "Rules from current folder" as a title if the main rule set is owned', () => { + component.mainRuleSet = ownedRuleSetMock; 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'); - expect(rules.length).toBe(2, 'Unexpected number of rules'); + it('should show "Rules from linked folder" as a title if the main rule set is linked', () => { + component.mainRuleSet = ruleSetWithLinkMock; + fixture.detectChanges(); - 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); + 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.LINKED_RULES'); }); }); diff --git a/projects/aca-folder-rules/src/lib/rule-list/rule-list/rule-list.ui-component.ts b/projects/aca-folder-rules/src/lib/rule-list/rule-list/rule-list.ui-component.ts index cef33e465..2318ac965 100644 --- a/projects/aca-folder-rules/src/lib/rule-list/rule-list/rule-list.ui-component.ts +++ b/projects/aca-folder-rules/src/lib/rule-list/rule-list/rule-list.ui-component.ts @@ -24,35 +24,98 @@ */ import { Component, EventEmitter, Input, Output, ViewEncapsulation } from '@angular/core'; +import { RuleSet } from '../../model/rule-set.model'; import { Rule } from '../../model/rule.model'; +import { RuleGroupingItem } from '../../model/rule-grouping-item.model'; +import { FolderRuleSetsService } from '../../services/folder-rule-sets.service'; @Component({ selector: 'aca-rule-list', - templateUrl: 'rule-list.ui-component.html', - styleUrls: ['rule-list.ui-component.scss'], + templateUrl: './rule-list.ui-component.html', + styleUrls: ['./rule-list.ui-component.scss'], encapsulation: ViewEncapsulation.None, host: { class: 'aca-rule-list' } }) export class RuleListUiComponent { @Input() - rules: Rule[] = []; + mainRuleSet: RuleSet = null; @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() selectRule = new EventEmitter<Rule>(); @Output() 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); } - isSelected(rule): boolean { - return rule.id === this.selectedRule?.id; - } - - onEnabledChanged(rule: Rule, isEnabled: boolean) { - this.ruleEnabledChanged.emit([rule, isEnabled]); + onRuleEnabledChanged(event: [Rule, boolean]) { + this.ruleEnabledChanged.emit(event); } } diff --git a/projects/aca-folder-rules/src/lib/rule-list/rule-set-list/rule-set-list.ui-component.html b/projects/aca-folder-rules/src/lib/rule-list/rule-set-list/rule-set-list.ui-component.html deleted file mode 100644 index c8958f66e..000000000 --- a/projects/aca-folder-rules/src/lib/rule-list/rule-set-list/rule-set-list.ui-component.html +++ /dev/null @@ -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> diff --git a/projects/aca-folder-rules/src/lib/rule-list/rule-set-list/rule-set-list.ui-component.scss b/projects/aca-folder-rules/src/lib/rule-list/rule-set-list/rule-set-list.ui-component.scss deleted file mode 100644 index 1f45f6210..000000000 --- a/projects/aca-folder-rules/src/lib/rule-list/rule-set-list/rule-set-list.ui-component.scss +++ /dev/null @@ -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; - } - } -} diff --git a/projects/aca-folder-rules/src/lib/rule-list/rule-set-list/rule-set-list.ui-component.spec.ts b/projects/aca-folder-rules/src/lib/rule-list/rule-set-list/rule-set-list.ui-component.spec.ts deleted file mode 100644 index e15ebc51a..000000000 --- a/projects/aca-folder-rules/src/lib/rule-list/rule-set-list/rule-set-list.ui-component.spec.ts +++ /dev/null @@ -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' - ); - }); -}); diff --git a/projects/aca-folder-rules/src/lib/services/folder-rule-sets.service.spec.ts b/projects/aca-folder-rules/src/lib/services/folder-rule-sets.service.spec.ts index e91cfc5a3..5a4bf07be 100644 --- a/projects/aca-folder-rules/src/lib/services/folder-rule-sets.service.spec.ts +++ b/projects/aca-folder-rules/src/lib/services/folder-rule-sets.service.spec.ts @@ -30,7 +30,7 @@ import { FolderRulesService } from './folder-rules.service'; import { ContentApiService } from '@alfresco/aca-shared'; import { getOtherFolderEntryMock, getOwningFolderEntryMock, otherFolderIdMock, owningFolderIdMock, owningFolderMock } from '../mock/node.mock'; 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 { inheritedRulesMock, linkedRulesMock, ownedRulesMock, ruleMock } from '../mock/rules.mock'; @@ -52,7 +52,11 @@ describe('FolderRuleSetsService', () => { folderRulesService = TestBed.inject(FolderRulesService); 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') .withArgs(jasmine.anything(), 'rule-set-no-links') .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 () => { - callApiSpy.and.returnValue(of(getRuleSetsResponseMock)); // take(2), because: 1 = init of the BehaviourSubject, 2 = in subscribe const folderInfoPromise = folderRuleSetsService.folderInfo$.pipe(take(2)).toPromise(); @@ -80,29 +83,38 @@ describe('FolderRuleSetsService', () => { expect(folderInfo).toEqual(owningFolderMock); }); - it('should load rule sets of a node', async () => { - callApiSpy.and.returnValue(of(getRuleSetsResponseMock)); + it('should load the main rule set (main or linked)', async () => { // 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(); folderRuleSetsService.loadRuleSets(owningFolderIdMock); - const ruleSets = await ruleSetListingPromise; + const ruleSets = await inheritedRuleSetsPromise; const hasMoreRuleSets = await hasMoreRuleSetsPromise; expect(callApiSpy).toHaveBeenCalledWith( `/nodes/${owningFolderIdMock}/rule-sets?include=isLinkedTo,owningFolder,linkedToBy&skipCount=0&maxItems=100`, 'GET' ); - expect(ruleSets).toEqual(ruleSetsMock); + expect(ruleSets).toEqual([inheritedRuleSetMock]); expect(hasMoreRuleSets).toEqual(false); }); 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'); // 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); await ruleSetListingPromise; diff --git a/projects/aca-folder-rules/src/lib/services/folder-rule-sets.service.ts b/projects/aca-folder-rules/src/lib/services/folder-rule-sets.service.ts index aca2451d9..5b8034973 100644 --- a/projects/aca-folder-rules/src/lib/services/folder-rule-sets.service.ts +++ b/projects/aca-folder-rules/src/lib/services/folder-rule-sets.service.ts @@ -40,20 +40,46 @@ import { Rule } from '../model/rule.model'; export class FolderRuleSetsService { 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 ruleSets: RuleSet[] = []; + private mainRuleSet: RuleSet = null; + private inheritedRuleSets: RuleSet[] = []; 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 folderInfoSource = new BehaviorSubject<NodeInfo>(null); 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(); folderInfo$: Observable<NodeInfo> = this.folderInfoSource.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) {} 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); } - 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( this.callApi( `/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; } }), - 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) { this.isLoadingSource.next(true); - this.ruleSets = []; + this.mainRuleSet = null; + this.inheritedRuleSets = []; this.hasMoreRuleSets = true; this.currentFolder = null; - this.ruleSetListingSource.next(this.ruleSets); + this.mainRuleSetSource.next(this.mainRuleSet); + this.inheritedRuleSetsSource.next(this.inheritedRuleSets); this.hasMoreRuleSetsSource.next(this.hasMoreRuleSets); this.getNodeInfo(nodeId) .pipe( @@ -91,29 +132,27 @@ export class FolderRuleSetsService { this.currentFolder = nodeInfo; this.folderInfoSource.next(this.currentFolder); }), - switchMap(() => this.getRuleSets(nodeId)), + switchMap(() => combineLatest(this.getMainRuleSet(nodeId), this.getInheritedRuleSets(nodeId))), finalize(() => this.isLoadingSource.next(false)) ) - .subscribe((ruleSets: RuleSet[]) => { - this.ruleSets = ruleSets; - this.ruleSetListingSource.next(this.ruleSets); + .subscribe(([mainRuleSet, inheritedRuleSets]) => { + this.mainRuleSet = mainRuleSet; + this.inheritedRuleSets = inheritedRuleSets; + this.mainRuleSetSource.next(mainRuleSet); + this.inheritedRuleSetsSource.next(inheritedRuleSets); 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.getRuleSets(this.currentFolder.id, this.ruleSets.length) + this.getInheritedRuleSets(this.currentFolder.id, this.inheritedRuleSets.length) .pipe(finalize(() => this.isLoadingSource.next(false))) .subscribe((ruleSets) => { - this.ruleSets.push(...ruleSets); - this.ruleSetListingSource.next(this.ruleSets); + this.inheritedRuleSets.push(...ruleSets); + this.inheritedRuleSetsSource.next(this.inheritedRuleSets); 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> { + if (!entry) { + return of(null); + } return combineLatest( this.currentFolder?.id === entry.owningFolder ? of(this.currentFolder) : this.getNodeInfo(entry.owningFolder || ''), this.folderRulesService.getRules(entry.owningFolder || '', entry.id) @@ -156,15 +198,38 @@ export class FolderRuleSetsService { ); } - getRuleSetFromRuleId(ruleId: string): RuleSet { - return this.ruleSets.find((ruleSet: RuleSet) => ruleSet.rules.findIndex((r: Rule) => r.id === ruleId) > -1) ?? null; + removeRuleFromMainRuleSet(ruleId: string) { + 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 { - return ( - this.ruleSets.find( - (ruleSet: RuleSet) => ruleSet.owningFolder.id === this.currentFolder.id || ruleSet.linkedToBy.indexOf(this.currentFolder.id) > -1 - ) ?? null - ); + addOrUpdateRuleInMainRuleSet(newRule: Rule) { + if (this.mainRuleSet) { + const index = this.mainRuleSet.rules.findIndex((rule: Rule) => rule.id === newRule.id); + if (index > -1) { + 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); + } + }); + } } } diff --git a/projects/aca-folder-rules/src/lib/services/folder-rules.service.spec.ts b/projects/aca-folder-rules/src/lib/services/folder-rules.service.spec.ts index bdf28deac..8becc0f80 100644 --- a/projects/aca-folder-rules/src/lib/services/folder-rules.service.spec.ts +++ b/projects/aca-folder-rules/src/lib/services/folder-rules.service.spec.ts @@ -41,6 +41,8 @@ describe('FolderRulesService', () => { const nodeId = owningFolderIdMock; const ruleSetId = 'rule-set-id'; const mockedRule = ruleMock('rule-mock'); + const { id, ...mockedRuleWithoutId } = mockedRule; + const mockedRuleEntry = { entry: mockedRule }; const ruleId = mockedRule.id; beforeEach(() => { @@ -120,14 +122,18 @@ describe('FolderRulesService', () => { }); 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)); - const result = await folderRulesService.createRule(nodeId, mockedRule, ruleSetId); + callApiSpy + .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); }); 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); expect(result).toEqual(mockedRule); diff --git a/projects/aca-folder-rules/src/lib/services/folder-rules.service.ts b/projects/aca-folder-rules/src/lib/services/folder-rules.service.ts index eefe36785..dafa7d365 100644 --- a/projects/aca-folder-rules/src/lib/services/folder-rules.service.ts +++ b/projects/aca-folder-rules/src/lib/services/folder-rules.service.ts @@ -126,23 +126,19 @@ export class FolderRulesService { ruleSet.hasMoreRules = res.hasMoreRules; ruleSet.rules.splice(skipCount); ruleSet.rules.push(...res.rules); - 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); - } + this.selectRuleInRuleSet(ruleSet, selectRule); }); } } - createRule(nodeId: string, rule: Partial<Rule>, ruleSetId: string): Promise<unknown> { - return this.callApi(`/nodes/${nodeId}/rule-sets/${ruleSetId}/rules`, 'POST', { ...rule }); + async createRule(nodeId: string, rule: Partial<Rule>, ruleSetId: string = '-default-'): Promise<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> { - return this.callApi(`/nodes/${nodeId}/rule-sets/${ruleSetId}/rules/${ruleId}`, 'PUT', { ...rule }); + async updateRule(nodeId: string, ruleId: string, rule: Rule, ruleSetId: string = '-default-'): Promise<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-') { @@ -207,4 +203,14 @@ export class FolderRulesService { selectRule(rule: 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); + } + } }