- + @@ -29,12 +29,23 @@ -
- +
+ + +
-
+
{{ selectedRule.name }} @@ -45,16 +56,16 @@
- -
-
+
. */ -import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { AcaFolderRulesModule, ManageRulesSmartComponent } from '@alfresco/aca-folder-rules'; import { DebugElement } from '@angular/core'; import { CoreTestingModule } from '@alfresco/adf-core'; import { FolderRulesService } from '../services/folder-rules.service'; import { ActivatedRoute } from '@angular/router'; import { of } from 'rxjs'; -import { dummyRules } from '../mock/rules.mock'; +import { ruleSetsMock } from '../mock/rule-sets.mock'; import { By } from '@angular/platform-browser'; -import { dummyNodeInfo } from '../mock/node.mock'; +import { owningFolderIdMock, owningFolderMock } from '../mock/node.mock'; import { MatDialog } from '@angular/material/dialog'; import { ActionsService } from '../services/actions.service'; +import { FolderRuleSetsService } from '../services/folder-rule-sets.service'; +import { ruleMock } from '../mock/rules.mock'; +import { Store } from '@ngrx/store'; describe('ManageRulesSmartComponent', () => { let fixture: ComponentFixture; let component: ManageRulesSmartComponent; let debugElement: DebugElement; + + let folderRuleSetsService: FolderRuleSetsService; let folderRulesService: FolderRulesService; let actionsService: ActionsService; - beforeEach( - waitForAsync(() => { - const folderRulesServiceSpy = jasmine.createSpyObj('FolderRulesService', ['loadRules', 'deleteRule']); - TestBed.configureTestingModule({ - imports: [CoreTestingModule, AcaFolderRulesModule], - providers: [ - { provide: FolderRulesService, useValue: folderRulesServiceSpy }, - { provide: ActivatedRoute, useValue: { params: of({ nodeId: 1 }) } } - ] - }) - .compileComponents() - .then(() => { - fixture = TestBed.createComponent(ManageRulesSmartComponent); - component = fixture.componentInstance; - debugElement = fixture.debugElement; - folderRulesService = TestBed.inject(FolderRulesService); - actionsService = TestBed.inject(ActionsService); - }); - }) - ); + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [CoreTestingModule, AcaFolderRulesModule], + providers: [ + FolderRuleSetsService, + FolderRulesService, + { provide: Store, useValue: { dispatch: () => {} } }, + { provide: ActivatedRoute, useValue: { params: of({ nodeId: owningFolderIdMock }) } } + ] + }); - it('should display aca-rules-list and aca-rule-details', () => { - folderRulesService.deletedRuleId$ = of(null); - folderRulesService.folderInfo$ = of(dummyNodeInfo); - folderRulesService.rulesListing$ = of(dummyRules); - folderRulesService.loading$ = of(false); + fixture = TestBed.createComponent(ManageRulesSmartComponent); + component = fixture.componentInstance; + debugElement = fixture.debugElement; + + folderRuleSetsService = TestBed.inject(FolderRuleSetsService); + folderRulesService = TestBed.inject(FolderRulesService); + actionsService = TestBed.inject(ActionsService); + + spyOn(actionsService, 'loadActionDefinitions').and.stub(); + }); + + it('should show a list of rule sets and rules', () => { + const loadRuleSetsSpy = spyOn(folderRuleSetsService, 'loadRuleSets').and.stub(); + + folderRuleSetsService.folderInfo$ = of(owningFolderMock); + folderRuleSetsService.ruleSetListing$ = of(ruleSetsMock); + folderRuleSetsService.isLoading$ = of(false); + folderRulesService.selectedRule$ = of(ruleMock('owned-rule-1')); actionsService.loading$ = of(false); fixture.detectChanges(); expect(component).toBeTruthy(); - expect(folderRulesService.loadRules).toHaveBeenCalledOnceWith(component.nodeId); + expect(loadRuleSetsSpy).toHaveBeenCalledOnceWith(component.nodeId); + const ruleSets = debugElement.queryAll(By.css(`[data-automation-id="rule-set-list-item"]`)); const rules = debugElement.queryAll(By.css('.aca-rule-list-item')); - const ruleDetails = debugElement.queryAll(By.css('aca-rule-details')); + const ruleDetails = debugElement.query(By.css('aca-rule-details')); const deleteRuleBtn = debugElement.query(By.css('#delete-rule-btn')); - expect(rules.length).toBe(2, 'unexpected number of aca-rule'); - expect(ruleDetails.length).toBeTruthy('aca-rule-details was not rendered'); + expect(ruleSets.length).toBe(3, 'unexpected number of rule sets'); + expect(rules.length).toBe(6, '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 provided node has no rules defined yet', () => { - folderRulesService.folderInfo$ = of(dummyNodeInfo); - folderRulesService.rulesListing$ = of([]); - folderRulesService.loading$ = of(false); - folderRulesService.deletedRuleId$ = of(null); + it('should only show adf-empty-content if node has no rules defined yet', () => { + folderRuleSetsService.folderInfo$ = of(owningFolderMock); + folderRuleSetsService.ruleSetListing$ = of([]); + folderRuleSetsService.isLoading$ = of(false); actionsService.loading$ = of(false); fixture.detectChanges(); @@ -98,19 +107,18 @@ describe('ManageRulesSmartComponent', () => { expect(component).toBeTruthy(); const adfEmptyContent = debugElement.query(By.css('adf-empty-content')); - const rules = debugElement.query(By.css('.aca-rule')); + const ruleSets = debugElement.queryAll(By.css(`[data-automation-id="rule-set-list-item"]`)); const ruleDetails = debugElement.query(By.css('aca-rule-details')); expect(adfEmptyContent).toBeTruthy(); - expect(rules).toBeFalsy(); + expect(ruleSets.length).toBe(0); expect(ruleDetails).toBeFalsy(); }); it('should only show aca-generic-error if the non-existing node was provided', () => { - folderRulesService.folderInfo$ = of(null); - folderRulesService.deletedRuleId$ = of(null); - folderRulesService.rulesListing$ = of([]); - folderRulesService.loading$ = of(false); + folderRuleSetsService.folderInfo$ = of(null); + folderRuleSetsService.ruleSetListing$ = of([]); + folderRuleSetsService.isLoading$ = of(false); actionsService.loading$ = of(false); fixture.detectChanges(); @@ -118,7 +126,7 @@ describe('ManageRulesSmartComponent', () => { expect(component).toBeTruthy(); const acaGenericError = debugElement.query(By.css('aca-generic-error')); - const rules = debugElement.query(By.css('.aca-rule')); + const rules = debugElement.query(By.css('.aca-rule-list-item')); const ruleDetails = debugElement.query(By.css('aca-rule-details')); expect(acaGenericError).toBeTruthy(); @@ -127,10 +135,9 @@ describe('ManageRulesSmartComponent', () => { }); it('should only show progress bar while loading', async () => { - folderRulesService.folderInfo$ = of(null); - folderRulesService.deletedRuleId$ = of(null); - folderRulesService.rulesListing$ = of([]); - folderRulesService.loading$ = of(true); + folderRuleSetsService.folderInfo$ = of(null); + folderRuleSetsService.ruleSetListing$ = of([]); + folderRuleSetsService.isLoading$ = of(true); actionsService.loading$ = of(true); fixture.detectChanges(); @@ -138,7 +145,7 @@ describe('ManageRulesSmartComponent', () => { expect(component).toBeTruthy(); const matProgressBar = debugElement.query(By.css('mat-progress-bar')); - const rules = debugElement.query(By.css('.aca-rule')); + const rules = debugElement.query(By.css('.aca-rule-list-item')); const ruleDetails = debugElement.query(By.css('aca-rule-details')); expect(matProgressBar).toBeTruthy(); @@ -148,55 +155,40 @@ 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.isLoading$ = of(false); + folderRulesService.selectedRule$ = of(ruleMock('owned-rule-1')); folderRulesService.deletedRuleId$ = of(null); - folderRulesService.folderInfo$ = of(dummyNodeInfo); - folderRulesService.rulesListing$ = of(dummyRules); - folderRulesService.loading$ = of(false); actionsService.loading$ = of(false); - spyOn(component, 'onRuleDelete').and.callThrough(); + const onRuleDeleteButtonClickedSpy = spyOn(component, 'onRuleDeleteButtonClicked').and.callThrough(); const dialogResult: any = { - afterClosed: () => - of(true).subscribe((res) => { - if (res === true) { - folderRulesService.deleteRule(component.nodeId, component.selectedRule.id); - } - }) + afterClosed: () => of(true) }; - spyOn(dialog, 'open').and.returnValue(dialogResult); + const dialogOpenSpy = spyOn(dialog, 'open').and.returnValue(dialogResult); + const deleteRuleSpy = spyOn(folderRulesService, 'deleteRule'); + const onRuleDeleteSpy = spyOn(component, 'onRuleDelete').and.callThrough(); fixture.detectChanges(); expect(component).toBeTruthy('expected component'); - const rules = debugElement.queryAll(By.css('.aca-rule')); + const rules = debugElement.queryAll(By.css('.aca-rule-list-item')); const ruleDetails = debugElement.query(By.css('aca-rule-details')); const deleteRuleBtn = fixture.debugElement.nativeElement.querySelector('#delete-rule-btn'); deleteRuleBtn.click(); fixture.detectChanges(); - folderRulesService.deletedRuleId$ = of(component.selectedRule.id); + folderRulesService.deletedRuleId$ = of('owned-rule-1-id'); - expect(component.onRuleDelete).toHaveBeenCalled(); - expect(dialog.open).toHaveBeenCalled(); - expect(folderRulesService.deleteRule).toHaveBeenCalled(); - expect(folderRulesService.loadRules).toHaveBeenCalledTimes(1); + expect(onRuleDeleteButtonClickedSpy).toHaveBeenCalled(); + expect(dialogOpenSpy).toHaveBeenCalled(); + expect(deleteRuleSpy).toHaveBeenCalled(); + expect(onRuleDeleteSpy).toHaveBeenCalledTimes(1); expect(rules).toBeTruthy('expected rules'); expect(ruleDetails).toBeTruthy('expected ruleDetails'); expect(deleteRuleBtn).toBeTruthy(); }); - - it('should run loadRules() when deletedRuleId$ emits new value', () => { - folderRulesService.deletedRuleId$ = of('new-value'); - folderRulesService.folderInfo$ = of(dummyNodeInfo); - folderRulesService.rulesListing$ = of(dummyRules); - folderRulesService.loading$ = of(false); - - fixture.detectChanges(); - - expect(component).toBeTruthy(); - - expect(folderRulesService.loadRules).toHaveBeenCalledTimes(2); - }); }); 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 c990ab999..a46d99af4 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 @@ -23,20 +23,23 @@ * along with Alfresco. If not, see . */ -import { Component, OnInit, OnDestroy, ViewEncapsulation } from '@angular/core'; +import { Component, OnDestroy, OnInit, ViewEncapsulation } from '@angular/core'; import { Location } from '@angular/common'; import { FolderRulesService } from '../services/folder-rules.service'; -import { Observable, Subscription } from 'rxjs'; +import { Observable, Subject } from 'rxjs'; import { Rule } from '../model/rule.model'; import { ActivatedRoute } from '@angular/router'; -import { NodeInfo } from '@alfresco/aca-shared/store'; -import { delay, tap } from 'rxjs/operators'; +import { AppStore, NavigateRouteAction, 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'; import { ConfirmDialogComponent } from '@alfresco/adf-content-services'; import { NotificationService } from '@alfresco/adf-core'; import { ActionDefinitionTransformed } from '../model/rule-action.model'; import { ActionsService } from '../services/actions.service'; +import { FolderRuleSetsService } from '../services/folder-rule-sets.service'; +import { Store } from '@ngrx/store'; +import { RuleSet } from '../model/rule-set.model'; @Component({ selector: 'aca-manage-rules', @@ -46,15 +49,18 @@ import { ActionsService } from '../services/actions.service'; host: { class: 'aca-manage-rules' } }) export class ManageRulesSmartComponent implements OnInit, OnDestroy { - rules$: Observable; - rulesLoading$: Observable; - actionsLoading$: Observable; - folderInfo$: Observable; - actionDefinitions$: Observable; - selectedRule: Rule = null; nodeId: string = null; - deletedRuleSubscription$: Subscription; - ruleDialogOnSubmitSubscription$: Subscription; + + ruleSetListing$: Observable; + selectedRule$: Observable; + hasMoreRuleSets$: Observable; + ruleSetsLoading$: Observable; + folderInfo$: Observable; + + actionsLoading$: Observable; + actionDefinitions$: Observable; + + private destroyed$ = new Subject(); constructor( private location: Location, @@ -62,45 +68,44 @@ export class ManageRulesSmartComponent implements OnInit, OnDestroy { private route: ActivatedRoute, private matDialogService: MatDialog, private notificationService: NotificationService, - private actionsService: ActionsService + private actionsService: ActionsService, + private folderRuleSetsService: FolderRuleSetsService, + private store: Store ) {} - ngOnInit(): void { - this.actionDefinitions$ = this.actionsService.actionDefinitionsListing$; - this.rules$ = this.folderRulesService.rulesListing$.pipe( - tap((rules) => { - if (!rules.includes(this.selectedRule)) { - this.selectedRule = rules[0]; - } - }) - ); - this.deletedRuleSubscription$ = this.folderRulesService.deletedRuleId$.subscribe((deletedRuleId) => { - if (deletedRuleId) { - this.folderRulesService.loadRules(this.nodeId); - } - }); - this.rulesLoading$ = this.folderRulesService.loading$; + ngOnInit() { + this.ruleSetListing$ = this.folderRuleSetsService.ruleSetListing$; + this.selectedRule$ = this.folderRulesService.selectedRule$; + this.hasMoreRuleSets$ = this.folderRuleSetsService.hasMoreRuleSets$; + this.ruleSetsLoading$ = this.folderRuleSetsService.isLoading$; + this.folderInfo$ = this.folderRuleSetsService.folderInfo$; + this.actionsLoading$ = this.actionsService.loading$.pipe(delay(0)); - this.folderInfo$ = this.folderRulesService.folderInfo$; + this.actionDefinitions$ = this.actionsService.actionDefinitionsListing$; + + this.folderRulesService.deletedRuleId$.pipe(takeUntil(this.destroyed$)).subscribe((deletedRuleId) => this.onRuleDelete(deletedRuleId)); + this.actionsService.loadActionDefinitions(); + this.route.params.subscribe((params) => { this.nodeId = params.nodeId; if (this.nodeId) { - this.folderRulesService.loadRules(this.nodeId); + this.folderRuleSetsService.loadRuleSets(this.nodeId); } }); } - ngOnDestroy(): void { - this.deletedRuleSubscription$.unsubscribe(); + ngOnDestroy() { + this.destroyed$.next(); + this.destroyed$.complete(); } goBack(): void { this.location.back(); } - onRuleSelected(rule: Rule): void { - this.selectedRule = rule; + onSelectRule(rule: Rule) { + this.folderRulesService.selectRule(rule); } openCreateUpdateRuleDialog(model = {}) { @@ -119,11 +124,10 @@ export class ManageRulesSmartComponent implements OnInit, OnDestroy { dialogRef.componentInstance.submitted.subscribe(async (rule) => { try { if (rule.id) { - await this.folderRulesService.updateRule(this.nodeId, rule.id, rule); + await this.onRuleUpdate(rule); } else { - await this.folderRulesService.createRule(this.nodeId, rule); + await this.onRuleCreate(rule); } - this.folderRulesService.loadRules(this.nodeId); dialogRef.close(); } catch (error) { this.notificationService.showError(error.response.body.error.errorKey); @@ -131,7 +135,28 @@ export class ManageRulesSmartComponent implements OnInit, OnDestroy { }); } - onRuleDelete(): void { + 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); + } + + async onRuleCreate(ruleCreateParams: Partial) { + 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); + } + } + + 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); + } + + onRuleDeleteButtonClicked(rule: Rule) { this.matDialogService .open(ConfirmDialogComponent, { data: { @@ -143,12 +168,31 @@ export class ManageRulesSmartComponent implements OnInit, OnDestroy { .afterClosed() .subscribe((result) => { if (result) { - this.folderRulesService.deleteRule(this.nodeId, this.selectedRule.id); + this.folderRulesService.deleteRule(this.nodeId, rule.id); } }); } - onRuleUpdate(): void { - this.openCreateUpdateRuleDialog(this.selectedRule); + 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'])); + } + + onLoadMoreRuleSets() { + this.folderRuleSetsService.loadMoreRuleSets(); + } + + onLoadMoreRules(ruleSet: RuleSet) { + this.folderRulesService.loadRules(ruleSet); } } diff --git a/projects/aca-folder-rules/src/lib/mock/node.mock.ts b/projects/aca-folder-rules/src/lib/mock/node.mock.ts index 25d90f80e..1648e6144 100644 --- a/projects/aca-folder-rules/src/lib/mock/node.mock.ts +++ b/projects/aca-folder-rules/src/lib/mock/node.mock.ts @@ -23,44 +23,32 @@ * along with Alfresco. If not, see . */ -export const dummyGetNodeResponse = { +import { NodeInfo } from '@alfresco/aca-shared/store'; +import { NodeEntry } from '@alfresco/js-api'; + +export const getOwningFolderEntryMock: NodeEntry = { entry: { - aspectNames: ['rule:rules', 'cm:titled', 'cm:auditable'], - createdAt: '2022-08-16T07:58:21.416+0000', - isFolder: true, - isFile: false, - createdByUser: { - id: 'username', - displayName: 'username' - }, - modifiedAt: '2022-08-16T07:59:45.771+0000', - modifiedByUser: { - id: 'username', - displayName: 'username' - }, - name: 'folder1', - id: '76659fe3-5f93-483d-948e-38b9e006cc94', - nodeType: 'cm:folder', - parentId: 'eb48d545-61f7-4ebd-861d-5fe5b072472f' + id: 'owning-folder-id', + name: 'owning-folder-name' } +} as NodeEntry; + +export const getOtherFolderEntryMock: NodeEntry = { + entry: { + id: 'other-folder-id', + name: 'other-folder-name' + } +} as NodeEntry; + +export const owningFolderIdMock = 'owning-folder-id'; +export const otherFolderIdMock = 'other-folder-id'; + +export const owningFolderMock: NodeInfo = { + id: owningFolderIdMock, + name: 'owning-folder-name' }; -export const dummyNodeInfo = { - aspectNames: ['rule:rules', 'cm:titled', 'cm:auditable'], - createdAt: '2022-08-16T07:58:21.416+0000', - isFolder: true, - isFile: false, - createdByUser: { - id: 'username', - displayName: 'username' - }, - modifiedAt: '2022-08-16T07:59:45.771+0000', - modifiedByUser: { - id: 'username', - displayName: 'username' - }, - name: 'folder1', - id: '76659fe3-5f93-483d-948e-38b9e006cc94', - nodeType: 'cm:folder', - parentId: 'eb48d545-61f7-4ebd-861d-5fe5b072472f' +export const otherFolderMock: NodeInfo = { + id: otherFolderIdMock, + name: 'other-folder-name' }; 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 new file mode 100644 index 000000000..e7c47603b --- /dev/null +++ b/projects/aca-folder-rules/src/lib/mock/rule-sets.mock.ts @@ -0,0 +1,109 @@ +/*! + * @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 . + */ + +import { RuleSet } from '../model/rule-set.model'; +import { otherFolderIdMock, otherFolderMock, owningFolderIdMock, owningFolderMock } from './node.mock'; +import { Rule } from '../model/rule.model'; +import { inheritedRulesMock, linkedRulesMock, ownedRulesMock } from './rules.mock'; + +export const getRuleSetsResponseMock = { + list: { + pagination: { + count: 3, + hasMoreItems: false, + totalItems: 3, + skipCount: 0, + maxItems: 100 + }, + entries: [ + { + entry: { + linkedToBy: [], + owningFolder: otherFolderIdMock, + isLinkedTo: false, + id: 'inherited-rule-set' + } + }, + { + entry: { + linkedToBy: [], + owningFolder: owningFolderIdMock, + isLinkedTo: false, + id: 'rule-set-no-links' + } + }, + { + entry: { + linkedToBy: [owningFolderIdMock], + owningFolder: otherFolderIdMock, + isLinkedTo: true, + id: 'rule-set-with-link' + } + } + ] + } +}; + +export const ruleSetMock = (rules: Rule[] = []): RuleSet => ({ + id: 'rule-set-id', + isLinkedTo: false, + owningFolder: owningFolderMock, + linkedToBy: [], + rules: [...rules], + hasMoreRules: true, + loadingRules: false +}); + +const ruleSetWithNoLinksMock: RuleSet = { + id: 'rule-set-no-links', + isLinkedTo: false, + owningFolder: owningFolderMock, + linkedToBy: [], + rules: ownedRulesMock, + hasMoreRules: false, + loadingRules: false +}; + +const ruleSetWithLinkMock: RuleSet = { + id: 'rule-set-with-link', + isLinkedTo: true, + owningFolder: otherFolderMock, + linkedToBy: [owningFolderIdMock], + rules: linkedRulesMock, + hasMoreRules: false, + loadingRules: false +}; + +const inheritedRuleSetMock: RuleSet = { + id: 'inherited-rule-set', + isLinkedTo: false, + owningFolder: otherFolderMock, + linkedToBy: [], + rules: inheritedRulesMock, + hasMoreRules: false, + loadingRules: false +}; + +export const ruleSetsMock: RuleSet[] = [inheritedRuleSetMock, ruleSetWithNoLinksMock, 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 911545ed6..52b7a67fb 100644 --- a/projects/aca-folder-rules/src/lib/mock/rules.mock.ts +++ b/projects/aca-folder-rules/src/lib/mock/rules.mock.ts @@ -25,7 +25,7 @@ import { Rule } from '../model/rule.model'; -export const dummyResponse = { +export const getRulesResponseMock = { list: { pagination: { count: 2, @@ -40,17 +40,13 @@ export const dummyResponse = { isShared: false, isInheritable: false, isAsynchronous: false, - name: 'rule1', - id: 'd388ed54-a522-410f-a158-6dbf5a833731', + name: 'rule1-name', + id: 'rule1-id', triggers: ['inbound'], actions: [ { - actionDefinitionId: 'copy', - params: { - 'deep-copy': false, - 'destination-folder': '279c65f0-912b-4563-affb-ed9dab8338e0', - actionContext: 'rule' - } + actionDefinitionId: 'counter', + params: {} } ], isEnabled: true @@ -61,16 +57,13 @@ export const dummyResponse = { isShared: false, isInheritable: false, isAsynchronous: false, - name: 'rule2', - id: 'e0e645ca-e6c0-47d4-9936-1a8872a6c30b', + name: 'rule2-name', + id: 'rule2-id', triggers: ['inbound'], actions: [ { - actionDefinitionId: 'move', - params: { - 'destination-folder': '279c65f0-912b-4563-affb-ed9dab8338e0', - actionContext: 'rule' - } + actionDefinitionId: 'counter', + params: {} } ], isEnabled: true @@ -80,58 +73,88 @@ export const dummyResponse = { } }; -export const dummyRules: Rule[] = [ - { - id: 'd388ed54-a522-410f-a158-6dbf5a833731', - name: 'rule1', - description: '', - isEnabled: true, - isInheritable: false, - isAsynchronous: false, - errorScript: '', - isShared: false, - triggers: ['inbound'], - conditions: { - inverted: false, - booleanMode: 'and', - simpleConditions: [], - compositeConditions: [] +export const getMoreRulesResponseMock = { + list: { + pagination: { + count: 2, + hasMoreItems: false, + totalItems: 2, + skipCount: 0, + maxItems: 100 }, - actions: [ + entries: [ { - actionDefinitionId: 'copy', - params: { - 'deep-copy': false, - 'destination-folder': '279c65f0-912b-4563-affb-ed9dab8338e0', - actionContext: 'rule' + entry: { + isShared: false, + isInheritable: false, + isAsynchronous: false, + name: 'rule3-name', + id: 'rule3-id', + triggers: ['inbound'], + actions: [ + { + actionDefinitionId: 'counter', + params: {} + } + ], + isEnabled: true } - } - ] - }, - { - id: 'e0e645ca-e6c0-47d4-9936-1a8872a6c30b', - name: 'rule2', - description: '', - isEnabled: true, - isInheritable: false, - isAsynchronous: false, - errorScript: '', - isShared: false, - triggers: ['inbound'], - conditions: { - inverted: false, - booleanMode: 'and', - simpleConditions: [], - compositeConditions: [] - }, - actions: [ + }, { - actionDefinitionId: 'move', - params: { - 'destination-folder': '279c65f0-912b-4563-affb-ed9dab8338e0', - actionContext: 'rule' + entry: { + isShared: false, + isInheritable: false, + isAsynchronous: false, + name: 'rule4-name', + id: 'rule4-id', + triggers: ['inbound'], + actions: [ + { + actionDefinitionId: 'counter', + params: {} + } + ], + isEnabled: true } } ] } -]; +}; + +const genericRuleMock: Rule = { + id: '', + name: '', + description: '', + isEnabled: true, + isInheritable: false, + isAsynchronous: false, + errorScript: '', + isShared: false, + triggers: ['inbound'], + conditions: { + inverted: false, + booleanMode: 'and', + simpleConditions: [], + compositeConditions: [] + }, + actions: [ + { + actionDefinitionId: 'counter', + params: {} + } + ] +}; + +export const ruleMock = (unique: string): Rule => ({ + ...genericRuleMock, + id: `${unique}-id`, + name: `${unique}-name` +}); + +export const rulesMock: Rule[] = [ruleMock('rule1'), ruleMock('rule2')]; +export const moreRulesMock: Rule[] = [ruleMock('rule3'), ruleMock('rule4')]; +export const manyRulesMock: Rule[] = [ruleMock('rule1'), ruleMock('rule2'), ruleMock('rule3'), ruleMock('rule4'), ruleMock('rule5')]; + +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')]; diff --git a/projects/aca-folder-rules/src/lib/rules-list/rule/rule-list-item.ui-component.spec.ts b/projects/aca-folder-rules/src/lib/model/rule-set.model.ts similarity index 63% rename from projects/aca-folder-rules/src/lib/rules-list/rule/rule-list-item.ui-component.spec.ts rename to projects/aca-folder-rules/src/lib/model/rule-set.model.ts index f47dd11ca..e695e1df9 100644 --- a/projects/aca-folder-rules/src/lib/rules-list/rule/rule-list-item.ui-component.spec.ts +++ b/projects/aca-folder-rules/src/lib/model/rule-set.model.ts @@ -23,23 +23,15 @@ * along with Alfresco. If not, see . */ -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { RuleListItemUiComponent } from './rule-list-item.ui-component'; -import { CoreTestingModule } from '@alfresco/adf-core'; +import { Rule } from './rule.model'; +import { NodeInfo } from '@alfresco/aca-shared/store'; -describe('RuleComponent', () => { - let component: RuleListItemUiComponent; - let fixture: ComponentFixture; - - beforeEach(() => { - TestBed.configureTestingModule({ - imports: [CoreTestingModule] - }); - fixture = TestBed.createComponent(RuleListItemUiComponent); - component = fixture.componentInstance; - }); - - it('should create the component', () => { - expect(component).toBeTruthy(); - }); -}); +export interface RuleSet { + id: string; + isLinkedTo: boolean; + owningFolder: NodeInfo; + linkedToBy: string[]; + rules: Rule[]; + hasMoreRules: boolean; + loadingRules: boolean; +} diff --git a/projects/aca-folder-rules/src/lib/rules-list/rule/rule-list-item.ui-component.html b/projects/aca-folder-rules/src/lib/rule-list/rule-list-item/rule-list-item.ui-component.html similarity index 82% rename from projects/aca-folder-rules/src/lib/rules-list/rule/rule-list-item.ui-component.html rename to projects/aca-folder-rules/src/lib/rule-list/rule-list-item/rule-list-item.ui-component.html index b94a1b365..6d878afce 100644 --- a/projects/aca-folder-rules/src/lib/rules-list/rule/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,5 @@
{{ rule.name }} - +
{{ rule.description }}
diff --git a/projects/aca-folder-rules/src/lib/rules-list/rule/rule-list-item.ui-component.scss b/projects/aca-folder-rules/src/lib/rule-list/rule-list-item/rule-list-item.ui-component.scss similarity index 92% rename from projects/aca-folder-rules/src/lib/rules-list/rule/rule-list-item.ui-component.scss rename to projects/aca-folder-rules/src/lib/rule-list/rule-list-item/rule-list-item.ui-component.scss index ee9c4c174..f8c16f29a 100644 --- a/projects/aca-folder-rules/src/lib/rules-list/rule/rule-list-item.ui-component.scss +++ b/projects/aca-folder-rules/src/lib/rule-list/rule-list-item/rule-list-item.ui-component.scss @@ -3,8 +3,6 @@ flex-direction: column; gap: 4px; padding: 12px 20px; - border-radius: 12px; - margin-bottom: 8px; cursor: pointer; p { diff --git a/projects/aca-folder-rules/src/lib/rules-list/rule/rule-list-item.ui-component.ts b/projects/aca-folder-rules/src/lib/rule-list/rule-list-item/rule-list-item.ui-component.ts similarity index 80% rename from projects/aca-folder-rules/src/lib/rules-list/rule/rule-list-item.ui-component.ts rename to projects/aca-folder-rules/src/lib/rule-list/rule-list-item/rule-list-item.ui-component.ts index 74fa5909a..0af44c3b3 100644 --- a/projects/aca-folder-rules/src/lib/rules-list/rule/rule-list-item.ui-component.ts +++ b/projects/aca-folder-rules/src/lib/rule-list/rule-list-item/rule-list-item.ui-component.ts @@ -23,9 +23,8 @@ * along with Alfresco. If not, see . */ -import { Component, HostBinding, Input, ViewEncapsulation } from '@angular/core'; +import { Component, EventEmitter, HostBinding, Input, Output, ViewEncapsulation } from '@angular/core'; import { Rule } from '../../model/rule.model'; -import { FolderRulesService } from '../../services/folder-rules.service'; @Component({ selector: 'aca-rule-list-item', @@ -38,14 +37,14 @@ export class RuleListItemUiComponent { @Input() rule: Rule; @Input() - nodeId: string; - @Input() @HostBinding('class.selected') isSelected: boolean; - constructor(private folderRulesService: FolderRulesService) {} + @Output() + enabledChanged = new EventEmitter(); - onToggleClick(isEnabled: boolean) { - this.folderRulesService.toggleRule(this.nodeId, this.rule.id, { ...this.rule, isEnabled }); + onToggleClick(isEnabled: boolean, event: Event) { + event.stopPropagation(); + this.enabledChanged.emit(isEnabled); } } diff --git a/projects/aca-folder-rules/src/lib/rules-list/rule-list.ui-component.html b/projects/aca-folder-rules/src/lib/rule-list/rule-list/rule-list.ui-component.html similarity index 67% rename from projects/aca-folder-rules/src/lib/rules-list/rule-list.ui-component.html rename to projects/aca-folder-rules/src/lib/rule-list/rule-list/rule-list.ui-component.html index 29d28ab23..bf0ccdc31 100644 --- a/projects/aca-folder-rules/src/lib/rules-list/rule-list.ui-component.html +++ b/projects/aca-folder-rules/src/lib/rule-list/rule-list/rule-list.ui-component.html @@ -1,10 +1,11 @@
+ (click)="onRuleClicked(rule)" + (enabledChanged)="onEnabledChanged(rule, $event)">
diff --git a/projects/aca-folder-rules/src/lib/rules-list/rule-list.ui-component.scss b/projects/aca-folder-rules/src/lib/rule-list/rule-list/rule-list.ui-component.scss similarity index 83% rename from projects/aca-folder-rules/src/lib/rules-list/rule-list.ui-component.scss rename to projects/aca-folder-rules/src/lib/rule-list/rule-list/rule-list.ui-component.scss index 085fb2c3a..29824e01e 100644 --- a/projects/aca-folder-rules/src/lib/rules-list/rule-list.ui-component.scss +++ b/projects/aca-folder-rules/src/lib/rule-list/rule-list/rule-list.ui-component.scss @@ -1,5 +1,4 @@ .aca-rule-list { display: flex; flex-direction: column; - gap: 4px; } diff --git a/projects/aca-folder-rules/src/lib/rules-list/rule-list.ui-component.spec.ts b/projects/aca-folder-rules/src/lib/rule-list/rule-list/rule-list.ui-component.spec.ts similarity index 90% rename from projects/aca-folder-rules/src/lib/rules-list/rule-list.ui-component.spec.ts rename to projects/aca-folder-rules/src/lib/rule-list/rule-list/rule-list.ui-component.spec.ts index 47fc8ee6c..cc65d8901 100644 --- a/projects/aca-folder-rules/src/lib/rules-list/rule-list.ui-component.spec.ts +++ b/projects/aca-folder-rules/src/lib/rule-list/rule-list/rule-list.ui-component.spec.ts @@ -25,13 +25,13 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { RuleListUiComponent } from './rule-list.ui-component'; -import { dummyRules } from '../mock/rules.mock'; +import { 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('RuleListComponent', () => { +describe('RuleListUiComponent', () => { let component: RuleListUiComponent; let fixture: ComponentFixture; let debugElement: DebugElement; @@ -50,7 +50,7 @@ describe('RuleListComponent', () => { it('should display the list of rules', () => { expect(component).toBeTruthy(); - component.rules = dummyRules; + component.rules = rulesMock; fixture.detectChanges(); @@ -64,8 +64,8 @@ describe('RuleListComponent', () => { 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(dummyRules[0].name); + expect(name.nativeElement.textContent).toBe(rulesMock[0].name); expect(toggleBtn).toBeTruthy(); - expect(description.nativeElement.textContent).toBe(dummyRules[0].description); + expect(description.nativeElement.textContent).toBe(rulesMock[0].description); }); }); diff --git a/projects/aca-folder-rules/src/lib/rules-list/rule-list.ui-component.ts b/projects/aca-folder-rules/src/lib/rule-list/rule-list/rule-list.ui-component.ts similarity index 79% rename from projects/aca-folder-rules/src/lib/rules-list/rule-list.ui-component.ts rename to projects/aca-folder-rules/src/lib/rule-list/rule-list/rule-list.ui-component.ts index de21bee4f..cef33e465 100644 --- a/projects/aca-folder-rules/src/lib/rules-list/rule-list.ui-component.ts +++ b/projects/aca-folder-rules/src/lib/rule-list/rule-list/rule-list.ui-component.ts @@ -24,7 +24,7 @@ */ import { Component, EventEmitter, Input, Output, ViewEncapsulation } from '@angular/core'; -import { Rule } from '../model/rule.model'; +import { Rule } from '../../model/rule.model'; @Component({ selector: 'aca-rule-list', @@ -35,23 +35,24 @@ import { Rule } from '../model/rule.model'; }) export class RuleListUiComponent { @Input() - rules: Rule[]; + rules: Rule[] = []; @Input() - selectedRule: Rule; - @Input() - nodeId: string; + selectedRule: Rule = null; @Output() - ruleSelected = new EventEmitter(); + selectRule = new EventEmitter(); + @Output() + ruleEnabledChanged = new EventEmitter<[Rule, boolean]>(); onRuleClicked(rule: Rule): void { - this.ruleSelected.emit(rule); + this.selectRule.emit(rule); } isSelected(rule): boolean { - if (this.selectedRule) { - return rule.id === this.selectedRule.id; - } - return false; + return rule.id === this.selectedRule?.id; + } + + onEnabledChanged(rule: Rule, isEnabled: boolean) { + this.ruleEnabledChanged.emit([rule, isEnabled]); } } 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 new file mode 100644 index 000000000..c8958f66e --- /dev/null +++ b/projects/aca-folder-rules/src/lib/rule-list/rule-set-list/rule-set-list.ui-component.html @@ -0,0 +1,90 @@ +
+ +
+ +
+ edit_note +
+ +
+ + + {{ 'ACA_FOLDER_RULES.RULE_LIST.OWNED_BY_THIS_FOLDER' | translate }} + + + + + {{ 'ACA_FOLDER_RULES.RULE_LIST.LINKED_FROM' | translate }} {{ ruleSet.owningFolder.name }} + + + + {{ 'ACA_FOLDER_RULES.RULE_LIST.INHERITED_FROM' | translate }} {{ ruleSet.owningFolder.name }} + + + + + {{ isRuleSetExpanded(ruleSet) ? 'expand_more' : 'chevron_right' }} + +
+ +
+ + + + + +
+ + {{ 'ACA_FOLDER_RULES.RULE_LIST.LOAD_MORE_RULES' | translate }} + + + + {{ 'ACA_FOLDER_RULES.RULE_LIST.LOADING_RULES' | translate }} + +
+
+ +
+ +
+ + {{ 'ACA_FOLDER_RULES.RULE_LIST.LOAD_MORE_RULE_SETS' | translate }} + + + + {{ 'ACA_FOLDER_RULES.RULE_LIST.LOADING_RULE_SETS' | translate }} + +
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 new file mode 100644 index 000000000..1f45f6210 --- /dev/null +++ b/projects/aca-folder-rules/src/lib/rule-list/rule-set-list/rule-set-list.ui-component.scss @@ -0,0 +1,78 @@ +.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 new file mode 100644 index 000000000..e15ebc51a --- /dev/null +++ b/projects/aca-folder-rules/src/lib/rule-list/rule-set-list/rule-set-list.ui-component.spec.ts @@ -0,0 +1,76 @@ +/*! + * @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 . + */ + +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; + 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/rule-list/rule-set-list/rule-set-list.ui-component.ts b/projects/aca-folder-rules/src/lib/rule-list/rule-set-list/rule-set-list.ui-component.ts new file mode 100644 index 000000000..7a980e7de --- /dev/null +++ b/projects/aca-folder-rules/src/lib/rule-list/rule-set-list/rule-set-list.ui-component.ts @@ -0,0 +1,107 @@ +/*! + * @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 . + */ + +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'; + +@Component({ + selector: 'aca-rule-set-list', + templateUrl: './rule-set-list.ui-component.html', + styleUrls: ['./rule-set-list.ui-component.scss'], + encapsulation: ViewEncapsulation.None, + host: { class: 'aca-rule-set-list' } +}) +export class RuleSetListUiComponent { + @Input() + folderId = ''; + private _ruleSets: RuleSet[] = []; + @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; + + @Output() + navigateToOtherFolder = new EventEmitter(); + @Output() + loadMoreRuleSets = new EventEmitter(); + @Output() + loadMoreRules = new EventEmitter(); + @Output() + selectRule = new EventEmitter(); + @Output() + ruleEnabledChanged = new EventEmitter<[Rule, boolean]>(); + + 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) { + this.selectRule.emit(rule); + } + + onRuleEnabledChanged(event: [Rule, boolean]) { + this.ruleEnabledChanged.emit(event); + } +} 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 new file mode 100644 index 000000000..e91cfc5a3 --- /dev/null +++ b/projects/aca-folder-rules/src/lib/services/folder-rule-sets.service.spec.ts @@ -0,0 +1,112 @@ +/*! + * @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 . + */ + +import { FolderRuleSetsService } from './folder-rule-sets.service'; +import { TestBed } from '@angular/core/testing'; +import { CoreTestingModule } from '@alfresco/adf-core'; +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 { take } from 'rxjs/operators'; +import { inheritedRulesMock, linkedRulesMock, ownedRulesMock, ruleMock } from '../mock/rules.mock'; + +describe('FolderRuleSetsService', () => { + let folderRuleSetsService: FolderRuleSetsService; + let folderRulesService: FolderRulesService; + let contentApiService: ContentApiService; + + let callApiSpy: jasmine.Spy; + let getNodeSpy: jasmine.Spy; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [CoreTestingModule], + providers: [FolderRuleSetsService, FolderRulesService, ContentApiService] + }); + + folderRuleSetsService = TestBed.inject(FolderRuleSetsService); + folderRulesService = TestBed.inject(FolderRulesService); + contentApiService = TestBed.inject(ContentApiService); + + callApiSpy = spyOn(folderRuleSetsService, 'callApi'); + spyOn(folderRulesService, 'getRules') + .withArgs(jasmine.anything(), 'rule-set-no-links') + .and.returnValue(of({ rules: ownedRulesMock, hasMoreRules: false })) + .withArgs(jasmine.anything(), 'rule-set-with-link') + .and.returnValue(of({ rules: linkedRulesMock, hasMoreRules: false })) + .withArgs(jasmine.anything(), 'inherited-rule-set') + .and.returnValue(of({ rules: inheritedRulesMock, hasMoreRules: false })); + + getNodeSpy = spyOn(contentApiService, 'getNode') + .withArgs(owningFolderIdMock) + .and.returnValue(of(getOwningFolderEntryMock)) + .withArgs(otherFolderIdMock) + .and.returnValue(of(getOtherFolderEntryMock)); + }); + + 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(); + + folderRuleSetsService.loadRuleSets(owningFolderIdMock); + const folderInfo = await folderInfoPromise; + + expect(getNodeSpy).toHaveBeenCalledWith(owningFolderIdMock); + expect(folderInfo).toEqual(owningFolderMock); + }); + + it('should load rule sets of a node', async () => { + callApiSpy.and.returnValue(of(getRuleSetsResponseMock)); + // 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 hasMoreRuleSetsPromise = folderRuleSetsService.hasMoreRuleSets$.pipe(take(3)).toPromise(); + + folderRuleSetsService.loadRuleSets(owningFolderIdMock); + const ruleSets = await ruleSetListingPromise; + 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(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(); + + folderRuleSetsService.loadRuleSets(owningFolderIdMock); + await ruleSetListingPromise; + + expect(selectRuleSpy).toHaveBeenCalledWith(ruleMock('owned-rule-1')); + }); +}); 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 new file mode 100644 index 000000000..aca2451d9 --- /dev/null +++ b/projects/aca-folder-rules/src/lib/services/folder-rule-sets.service.ts @@ -0,0 +1,170 @@ +/*! + * @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 . + */ + +import { Injectable } from '@angular/core'; +import { AlfrescoApiService } from '@alfresco/adf-core'; +import { BehaviorSubject, combineLatest, from, Observable, of } from 'rxjs'; +import { NodeInfo } from '@alfresco/aca-shared/store'; +import { catchError, finalize, map, switchMap, tap } from 'rxjs/operators'; +import { RuleSet } from '../model/rule-set.model'; +import { ContentApiService } from '@alfresco/aca-shared'; +import { NodeEntry } from '@alfresco/js-api'; +import { FolderRulesService } from './folder-rules.service'; +import { Rule } from '../model/rule.model'; + +@Injectable({ + providedIn: 'root' +}) +export class FolderRuleSetsService { + public static MAX_RULE_SETS_PER_GET = 100; + + private currentFolder: NodeInfo = null; + private ruleSets: RuleSet[] = []; + private hasMoreRuleSets = true; + + private ruleSetListingSource = new BehaviorSubject([]); + private hasMoreRuleSetsSource = new BehaviorSubject(true); + private folderInfoSource = new BehaviorSubject(null); + private isLoadingSource = new BehaviorSubject(false); + + ruleSetListing$: Observable = this.ruleSetListingSource.asObservable(); + hasMoreRuleSets$: Observable = this.hasMoreRuleSetsSource.asObservable(); + folderInfo$: Observable = this.folderInfoSource.asObservable(); + isLoading$ = this.isLoadingSource.asObservable(); + + constructor(private apiService: AlfrescoApiService, private contentApi: ContentApiService, private folderRulesService: FolderRulesService) {} + + private callApi(path: string, httpMethod: string, body: object = {}): Promise { + // APIs used by this service are still private and not yet available for public use + const params = [{}, {}, {}, {}, body, ['application/json'], ['application/json']]; + return this.apiService.getInstance().contentPrivateClient.callApi(path, httpMethod, ...params); + } + + private getRuleSets(nodeId: string, skipCount = 0): Observable { + return from( + this.callApi( + `/nodes/${nodeId}/rule-sets?include=isLinkedTo,owningFolder,linkedToBy&skipCount=${skipCount}&maxItems=${FolderRuleSetsService.MAX_RULE_SETS_PER_GET}`, + 'GET' + ) + ).pipe( + tap((res) => { + if (res?.list?.pagination) { + this.hasMoreRuleSets = res.list.pagination.hasMoreItems; + } + }), + switchMap((res) => this.formatRuleSets(res)) + ); + } + + loadRuleSets(nodeId: string) { + this.isLoadingSource.next(true); + this.ruleSets = []; + this.hasMoreRuleSets = true; + this.currentFolder = null; + this.ruleSetListingSource.next(this.ruleSets); + this.hasMoreRuleSetsSource.next(this.hasMoreRuleSets); + this.getNodeInfo(nodeId) + .pipe( + tap((nodeInfo: NodeInfo) => { + this.currentFolder = nodeInfo; + this.folderInfoSource.next(this.currentFolder); + }), + switchMap(() => this.getRuleSets(nodeId)), + finalize(() => this.isLoadingSource.next(false)) + ) + .subscribe((ruleSets: RuleSet[]) => { + this.ruleSets = ruleSets; + this.ruleSetListingSource.next(this.ruleSets); + this.hasMoreRuleSetsSource.next(this.hasMoreRuleSets); + this.folderRulesService.selectRule(this.getOwnedOrLinkedRuleSet()?.rules[0] ?? ruleSets[0]?.rules[0]); + }); + } + + loadMoreRuleSets(selectLastRule = false) { + this.isLoadingSource.next(true); + this.getRuleSets(this.currentFolder.id, this.ruleSets.length) + .pipe(finalize(() => this.isLoadingSource.next(false))) + .subscribe((ruleSets) => { + this.ruleSets.push(...ruleSets); + this.ruleSetListingSource.next(this.ruleSets); + this.hasMoreRuleSetsSource.next(this.hasMoreRuleSets); + if (selectLastRule) { + const ownedRuleSet = this.getOwnedOrLinkedRuleSet(); + this.folderRulesService.selectRule(ownedRuleSet?.rules[ownedRuleSet.rules.length - 1]); + } + }); + } + + private getNodeInfo(nodeId: string): Observable { + if (nodeId) { + return this.contentApi.getNode(nodeId).pipe( + catchError((error) => { + if (error.status === 404) { + return of({ entry: null }); + } + return of(error); + }), + map((entry: NodeEntry) => entry.entry) + ); + } else { + return of(null); + } + } + + private formatRuleSets(res: any): Observable { + return res?.list?.entries && res.list.entries instanceof Array + ? combineLatest((res.list.entries as Array).map((entry) => this.formatRuleSet(entry.entry))) + : of([]); + } + + private formatRuleSet(entry: any): Observable { + return combineLatest( + this.currentFolder?.id === entry.owningFolder ? of(this.currentFolder) : this.getNodeInfo(entry.owningFolder || ''), + this.folderRulesService.getRules(entry.owningFolder || '', entry.id) + ).pipe( + map(([owningFolderNodeInfo, getRulesRes]) => ({ + id: entry.id, + isLinkedTo: entry.isLinkedTo || false, + owningFolder: owningFolderNodeInfo, + linkedToBy: entry.linkedToBy || [], + rules: getRulesRes.rules, + hasMoreRules: getRulesRes.hasMoreRules, + loadingRules: false + })) + ); + } + + getRuleSetFromRuleId(ruleId: string): RuleSet { + return this.ruleSets.find((ruleSet: RuleSet) => ruleSet.rules.findIndex((r: Rule) => r.id === ruleId) > -1) ?? null; + } + + getOwnedOrLinkedRuleSet(): RuleSet { + return ( + this.ruleSets.find( + (ruleSet: RuleSet) => ruleSet.owningFolder.id === this.currentFolder.id || ruleSet.linkedToBy.indexOf(this.currentFolder.id) > -1 + ) ?? null + ); + } +} 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 7d4065037..bdf28deac 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 @@ -25,126 +25,111 @@ import { TestBed } from '@angular/core/testing'; import { CoreTestingModule } from '@alfresco/adf-core'; -import { take } from 'rxjs/operators'; import { of } from 'rxjs'; import { FolderRulesService } from './folder-rules.service'; -import { Rule } from '../model/rule.model'; -import { dummyResponse, dummyRules } from '../mock/rules.mock'; -import { NodeInfo } from '@alfresco/aca-shared/store'; -import { ContentApiService } from '@alfresco/aca-shared'; -import { dummyGetNodeResponse, dummyNodeInfo } from '../mock/node.mock'; +import { getMoreRulesResponseMock, getRulesResponseMock, manyRulesMock, moreRulesMock, ruleMock, rulesMock } from '../mock/rules.mock'; +import { ruleSetMock } from '../mock/rule-sets.mock'; +import { expect } from '@angular/flex-layout/_private-utils/testing'; +import { owningFolderIdMock } from '../mock/node.mock'; +import { take } from 'rxjs/operators'; describe('FolderRulesService', () => { let folderRulesService: FolderRulesService; - let contentApi: ContentApiService; - let rulesPromise: Promise[]>; - let folderInfoPromise: Promise; - let deletedRulePromise: Promise; - let rules: Partial[]; - let folderInfo: NodeInfo; - let deletedRule: string; - let apiCallSpy; - let getNodeSpy; - const nodeId = '********-fake-node-****-********'; - const ruleId = '********-fake-rule-****-********'; - const ruleSetId = '-default-'; - const params = [{}, {}, {}, {}, {}, ['application/json'], ['application/json']]; - const paramsWithBody = [{}, {}, {}, {}, dummyRules[0], ['application/json'], ['application/json']]; + let callApiSpy: jasmine.Spy; - beforeEach(async () => { + const nodeId = owningFolderIdMock; + const ruleSetId = 'rule-set-id'; + const mockedRule = ruleMock('rule-mock'); + const ruleId = mockedRule.id; + + beforeEach(() => { TestBed.configureTestingModule({ imports: [CoreTestingModule], - providers: [FolderRulesService, ContentApiService] + providers: [FolderRulesService] }); + folderRulesService = TestBed.inject(FolderRulesService); + + callApiSpy = spyOn(folderRulesService, 'callApi'); }); - describe('loadRules', () => { - beforeEach(async () => { - contentApi = TestBed.inject(ContentApiService); + it('should load some rules into a rule set', () => { + const ruleSet = ruleSetMock(); + callApiSpy.and.returnValue(of(getRulesResponseMock)); - apiCallSpy = spyOn(folderRulesService, 'apiCall') - .withArgs(`/nodes/${nodeId}/rule-sets/${ruleSetId}/rules`, 'GET', params) - .and.returnValue(of(dummyResponse) as any); - getNodeSpy = spyOn(contentApi, 'getNode').and.returnValue(of(dummyGetNodeResponse) as any); + expect(ruleSet.rules.length).toBe(0); + expect(ruleSet.hasMoreRules).toBeTrue(); + expect(ruleSet.loadingRules).toBeFalse(); - rulesPromise = folderRulesService.rulesListing$.pipe(take(2)).toPromise(); - folderInfoPromise = folderRulesService.folderInfo$.pipe(take(2)).toPromise(); + folderRulesService.loadRules(ruleSet); - folderRulesService.loadRules(nodeId, ruleSetId); - - rules = await rulesPromise; - folderInfo = await folderInfoPromise; - }); - - it('should format and set the data', async () => { - expect(rules).toBeTruthy('rulesListing$ is empty'); - expect(folderInfo).toBeTruthy('folderInfo$ is empty'); - expect(rules.length).toBe(2, 'rulesListing$ size is wrong'); - expect(rules).toEqual(dummyRules, 'The list of rules is incorrectly formatted'); - expect(folderInfo).toEqual(dummyNodeInfo, 'The node info is wrong'); - expect(apiCallSpy).toHaveBeenCalledTimes(1); - expect(apiCallSpy).toHaveBeenCalledWith(`/nodes/${nodeId}/rule-sets/${ruleSetId}/rules`, 'GET', params); - expect(getNodeSpy).toHaveBeenCalledTimes(1); - }); + expect(callApiSpy).toHaveBeenCalledWith(`/nodes/${ruleSet.owningFolder.id}/rule-sets/${ruleSet.id}/rules?skipCount=0&maxItems=100`, 'GET'); + expect(ruleSet.rules.length).toBe(2); + expect(ruleSet.rules).toEqual(rulesMock); + expect(ruleSet.hasMoreRules).toBeFalse(); }); - describe('deleteRule', () => { - beforeEach(async () => { - apiCallSpy = spyOn(folderRulesService, 'apiCall') - .withArgs(`/nodes/${nodeId}/rule-sets/${ruleSetId}/rules/${ruleId}`, 'DELETE', params) - .and.returnValue(ruleId); + it('should load more rules if it still has some more to load', () => { + const ruleSet = ruleSetMock(rulesMock); + callApiSpy.and.returnValue(of(getMoreRulesResponseMock)); - deletedRulePromise = folderRulesService.deletedRuleId$.pipe(take(2)).toPromise(); + expect(ruleSet.rules.length).toBe(2); + expect(ruleSet.hasMoreRules).toBeTrue(); - folderRulesService.deleteRule(nodeId, ruleId, ruleSetId); + folderRulesService.loadRules(ruleSet); - deletedRule = await deletedRulePromise; - }); - - it('should delete a rule and return its id', async () => { - expect(deletedRule).toBeTruthy('rule has not been deleted'); - expect(deletedRule).toBe(ruleId, 'wrong id of deleted rule'); - expect(apiCallSpy).toHaveBeenCalledTimes(1); - expect(apiCallSpy).toHaveBeenCalledWith(`/nodes/${nodeId}/rule-sets/${ruleSetId}/rules/${ruleId}`, 'DELETE', params); - }); + expect(callApiSpy).toHaveBeenCalledWith(`/nodes/${ruleSet.owningFolder.id}/rule-sets/${ruleSet.id}/rules?skipCount=2&maxItems=100`, 'GET'); + expect(ruleSet.rules.length).toBe(4); + expect(ruleSet.rules).toEqual([...rulesMock, ...moreRulesMock]); + expect(ruleSet.hasMoreRules).toBeFalse(); }); - describe('toggleRule', () => { - beforeEach(async () => { - apiCallSpy = spyOn(folderRulesService, 'apiCall') - .withArgs(`/nodes/${nodeId}/rule-sets/${ruleSetId}/rules/${ruleId}`, 'PUT', paramsWithBody) - .and.returnValue([]); + it('should select the right rule rule after loading', () => { + const ruleSet = ruleSetMock(rulesMock); + spyOn(folderRulesService, 'getRules').and.returnValue(of({ rules: manyRulesMock, hasMoreRules: false })); + const selectedRuleSourceSpy = spyOn(folderRulesService['selectedRuleSource'], 'next'); - folderRulesService.toggleRule(nodeId, ruleId, dummyRules[0]); - }); + folderRulesService.loadRules(ruleSet, 0); + expect(selectedRuleSourceSpy).not.toHaveBeenCalled(); - it('should send correct PUT request', async () => { - expect(apiCallSpy).toHaveBeenCalled(); - expect(apiCallSpy).toHaveBeenCalledWith(`/nodes/${nodeId}/rule-sets/${ruleSetId}/rules/${ruleId}`, 'PUT', paramsWithBody); - }); + folderRulesService.loadRules(ruleSet, 0, 'first'); + expect(selectedRuleSourceSpy).toHaveBeenCalledWith(ruleMock('rule1')); + selectedRuleSourceSpy.calls.reset(); + + folderRulesService.loadRules(ruleSet, 0, 'last'); + expect(selectedRuleSourceSpy).toHaveBeenCalledWith(ruleMock('rule5')); + selectedRuleSourceSpy.calls.reset(); + selectedRuleSourceSpy.calls.reset(); + + folderRulesService.loadRules(ruleSet, 0, ruleMock('rule3')); + expect(selectedRuleSourceSpy).toHaveBeenCalledWith(ruleMock('rule3')); }); - describe('createRule', () => { - beforeEach(async () => { - spyOn(folderRulesService, 'apiCall') - .withArgs(`/nodes/${nodeId}/rule-sets/${ruleSetId}/rules`, 'POST', paramsWithBody) - .and.returnValue(Promise.resolve(dummyRules[0])); - }); + it('should delete a rule and return its id', async () => { + callApiSpy.withArgs(`/nodes/${nodeId}/rule-sets/${ruleSetId}/rules/${ruleId}`, 'DELETE').and.returnValue(ruleId); + const deletedRulePromise = folderRulesService.deletedRuleId$.pipe(take(2)).toPromise(); - it('should send correct POST request and return created rule', async () => { - const result = await folderRulesService.createRule(nodeId, dummyRules[0]); - expect(result).toEqual(dummyRules[0]); - }); + folderRulesService.deleteRule(nodeId, ruleId, ruleSetId); + const deletedRule = await deletedRulePromise; + + expect(deletedRule).toBeTruthy('rule has not been deleted'); + expect(deletedRule).toBe(ruleId, 'wrong id of deleted rule'); + expect(callApiSpy).toHaveBeenCalledTimes(1); + expect(callApiSpy).toHaveBeenCalledWith(`/nodes/${nodeId}/rule-sets/${ruleSetId}/rules/${ruleId}`, 'DELETE'); + }); + + 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); + + expect(result).toEqual(mockedRule); }); it('should send correct PUT request to update rule and return it', async () => { - spyOn(folderRulesService, 'apiCall') - .withArgs(`/nodes/${nodeId}/rule-sets/${ruleSetId}/rules/${ruleId}`, 'PUT', paramsWithBody) - .and.returnValue(Promise.resolve(dummyRules[0])); + callApiSpy.withArgs(`/nodes/${nodeId}/rule-sets/${ruleSetId}/rules/${ruleId}`, 'PUT', mockedRule).and.returnValue(Promise.resolve(mockedRule)); - const result = await folderRulesService.updateRule(nodeId, ruleId, dummyRules[0]); - expect(result).toEqual(dummyRules[0]); + 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 af9868fc3..eefe36785 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 @@ -25,18 +25,24 @@ import { Injectable } from '@angular/core'; import { AlfrescoApiService } from '@alfresco/adf-core'; -import { BehaviorSubject, forkJoin, from, Observable, of } from 'rxjs'; -import { catchError, finalize, map } from 'rxjs/operators'; +import { BehaviorSubject, from, Observable } from 'rxjs'; +import { finalize, map } from 'rxjs/operators'; import { Rule, RuleForForm, RuleOptions } from '../model/rule.model'; -import { ContentApiService } from '@alfresco/aca-shared'; -import { NodeInfo } from '@alfresco/aca-shared/store'; import { RuleCompositeCondition } from '../model/rule-composite-condition.model'; import { RuleSimpleCondition } from '../model/rule-simple-condition.model'; +import { RuleSet } from '../model/rule-set.model'; + +interface GetRulesResult { + rules: Rule[]; + hasMoreRules: boolean; +} @Injectable({ providedIn: 'root' }) export class FolderRulesService { + public static MAX_RULES_PER_GET = 100; + public static get emptyCompositeCondition(): RuleCompositeCondition { return { inverted: false, @@ -79,119 +85,77 @@ export class FolderRulesService { return value; } - private rulesListingSource = new BehaviorSubject([]); - rulesListing$: Observable = this.rulesListingSource.asObservable(); - private folderInfoSource = new BehaviorSubject(null); - folderInfo$: Observable = this.folderInfoSource.asObservable(); - private loadingSource = new BehaviorSubject(false); - loading$ = this.loadingSource.asObservable(); + private selectedRuleSource = new BehaviorSubject(null); private deletedRuleIdSource = new BehaviorSubject(null); + + selectedRule$ = this.selectedRuleSource.asObservable(); deletedRuleId$: Observable = this.deletedRuleIdSource.asObservable(); - constructor(private apiService: AlfrescoApiService, private contentApi: ContentApiService) {} + constructor(private apiService: AlfrescoApiService) {} - loadRules(nodeId: string, ruleSetId: string = '-default-'): void { - this.loadingSource.next(true); - forkJoin([ - from( - this.apiCall(`/nodes/${nodeId}/rule-sets/${ruleSetId}/rules`, 'GET', [{}, {}, {}, {}, {}, ['application/json'], ['application/json']]) - ).pipe( - map((res) => this.formatRules(res)), - catchError((error) => { - if (error.status === 404) { - return of([]); - } - return of(error); - }) - ), - this.contentApi.getNode(nodeId).pipe( - catchError((error) => { - if (error.status === 404) { - return of({ entry: null }); - } - return of(error); - }) + private callApi(path: string, httpMethod: string, body: object = {}): Promise { + // APIs used by this service are still private and not yet available for public use + const params = [{}, {}, {}, {}, body, ['application/json'], ['application/json']]; + return this.apiService.getInstance().contentPrivateClient.callApi(path, httpMethod, ...params); + } + + getRules(owningFolderId: string, ruleSetId: string, skipCount = 0): Observable { + return from( + this.callApi( + `/nodes/${owningFolderId}/rule-sets/${ruleSetId}/rules?skipCount=${skipCount}&maxItems=${FolderRulesService.MAX_RULES_PER_GET}`, + 'GET' ) - ]) - .pipe(finalize(() => this.loadingSource.next(false))) - .subscribe( - ([rules, nodeInfo]) => { - this.rulesListingSource.next(rules); - this.folderInfoSource.next(nodeInfo.entry); - }, - (error) => { - this.rulesListingSource.next([]); - this.folderInfoSource.next(error); - } - ); + ).pipe( + map((res) => ({ + rules: this.formatRules(res), + hasMoreRules: !!res?.list?.pagination?.hasMoreItems + })) + ); } - createRule(nodeId: string, rule: Partial, ruleSetId: string = '-default-') { - return this.apiCall(`/nodes/${nodeId}/rule-sets/${ruleSetId}/rules`, 'POST', [ - {}, - {}, - {}, - {}, - { ...rule }, - ['application/json'], - ['application/json'] - ]); + loadRules(ruleSet: RuleSet, skipCount = ruleSet.rules.length, selectRule: 'first' | 'last' | Rule = null) { + if (ruleSet && !ruleSet.loadingRules) { + ruleSet.loadingRules = true; + this.getRules(ruleSet.owningFolder.id, ruleSet.id, skipCount) + .pipe( + finalize(() => { + ruleSet.loadingRules = false; + }) + ) + .subscribe((res: GetRulesResult) => { + 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); + } + }); + } } - updateRule(nodeId: string, ruleId: string, rule: Rule, ruleSetId: string = '-default-') { - return this.apiCall(`/nodes/${nodeId}/rule-sets/${ruleSetId}/rules/${ruleId}`, 'PUT', [ - {}, - {}, - {}, - {}, - { ...rule }, - ['application/json'], - ['application/json'] - ]); + createRule(nodeId: string, rule: Partial, ruleSetId: string): Promise { + return this.callApi(`/nodes/${nodeId}/rule-sets/${ruleSetId}/rules`, 'POST', { ...rule }); } - deleteRule(nodeId: string, ruleId: string, ruleSetId: string = '-default-'): void { - this.loadingSource.next(true); - from( - this.apiCall(`/nodes/${nodeId}/rule-sets/${ruleSetId}/rules/${ruleId}`, 'DELETE', [ - {}, - {}, - {}, - {}, - {}, - ['application/json'], - ['application/json'] - ]) - ).subscribe( + updateRule(nodeId: string, ruleId: string, rule: Rule, ruleSetId: string): Promise { + return this.callApi(`/nodes/${nodeId}/rule-sets/${ruleSetId}/rules/${ruleId}`, 'PUT', { ...rule }); + } + + deleteRule(nodeId: string, ruleId: string, ruleSetId: string = '-default-') { + from(this.callApi(`/nodes/${nodeId}/rule-sets/${ruleSetId}/rules/${ruleId}`, 'DELETE')).subscribe( () => { this.deletedRuleIdSource.next(ruleId); }, (error) => { this.deletedRuleIdSource.next(error); - this.loadingSource.next(false); } ); } - toggleRule(nodeId: string, ruleId: string, rule: Rule, ruleSetId: string = '-default-'): void { - from( - this.apiCall(`/nodes/${nodeId}/rule-sets/${ruleSetId}/rules/${ruleId}`, 'PUT', [ - {}, - {}, - {}, - {}, - { ...rule }, - ['application/json'], - ['application/json'] - ]) - ).subscribe({ error: (error) => console.error(error) }); - } - - private apiCall(path: string, httpMethod: string, params?: any[]): Promise { - // APIs used by this service are still private and not yet available for public use - return this.apiService.getInstance().contentPrivateClient.callApi(path, httpMethod, ...params); - } - private formatRules(res): Rule[] { return res.list.entries.map((entry) => this.formatRule(entry.entry)); } @@ -239,4 +203,8 @@ export class FolderRulesService { parameter: obj.parameter || '' }; } + + selectRule(rule: Rule) { + this.selectedRuleSource.next(rule); + } }