[AAE-10772] Move content services directives from core to content-services package (#7942)

* [AAE-10772] move CheckAllowableOperationDirective to content services

* [AAE-10772] move LibraryFavoriteDirective to content services

* [AAE-10772] move LibraryMembershipDirective to content services

* [AAE-10772] move NodeDeleteDirective to content services

* [AAE-10772] move NodeFavoriteDirective to content services

* [AAE-10772] update imports on LibraryMembershipDirective

* [AAE-10772] move NodeRestoreDirective to content services

* [AAE-10772] move UserInfoModule to content services

* Revert "[AAE-10772] move UserInfoModule to content services"

This reverts commit ede1d5db3923859586d88646ca7826abd3d30cf1.

* [AAE-10772] Remove barrel imports and move library membership interfaces into LibraryMembershipDirective because are only used in that directive

* [AAE-10772] Remove barrel imports from spec files

* [AAE-10772] Move directive md files from core to content-services

* [AAE-10772] Fix files path into the docs files

* [AAE-10772] Export library membership interfaces because are imported by the ACA ToggleJoinLibraryButtonComponent

Co-authored-by: Diogo Bastos <diogo.bastos@hyland.com>
This commit is contained in:
Amedeo Lepore
2022-11-18 09:49:17 +01:00
committed by GitHub
parent f9c71bc953
commit 933b59c54e
32 changed files with 243 additions and 108 deletions

View File

@@ -0,0 +1,9 @@
6.0.0-beta.1
- CheckAllowableOperationDirective: Moved from ADF Core to ADF content services
- LibraryFavoriteDirective: Moved from ADF Core to ADF content services
- LibraryMembershipDirective: Moved from ADF Core to ADF content services
- NodeDeleteDirective: Moved from ADF Core to ADF content services
- NodeFavoriteDirective: Moved from ADF Core to ADF content services
- NodeRestoreDirective: Moved from ADF Core to ADF content services

View File

@@ -1,164 +0,0 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { ChangeDetectorRef, Component, ElementRef, SimpleChange } from '@angular/core';
import { ContentService } from '../services/content.service';
import { CheckAllowableOperationDirective, NodeAllowableOperationSubject } from './check-allowable-operation.directive';
import { setupTestBed } from '../testing/setup-test-bed';
import { TestBed } from '@angular/core/testing';
import { CoreTestingModule } from '../testing/core.testing.module';
import { TranslateModule } from '@ngx-translate/core';
@Component({
selector: 'adf-text-subject',
template: ''
})
class TestComponent implements NodeAllowableOperationSubject {
disabled: boolean = false;
}
describe('CheckAllowableOperationDirective', () => {
let changeDetectorMock: ChangeDetectorRef;
setupTestBed({
imports: [
TranslateModule.forRoot(),
CoreTestingModule
]
});
beforeEach(() => {
changeDetectorMock = { detectChanges: () => {} } as ChangeDetectorRef;
});
describe('HTML nativeElement as subject', () => {
it('updates element on nodes change', () => {
const directive = new CheckAllowableOperationDirective(null, null, null, changeDetectorMock);
spyOn(directive, 'updateElement').and.stub();
const nodes = [{}, {}];
const change = new SimpleChange([], nodes, false);
directive.ngOnChanges({ nodes: change });
expect(directive.updateElement).toHaveBeenCalled();
});
it('updates element only on subsequent change', () => {
const directive = new CheckAllowableOperationDirective(null, null, null, changeDetectorMock);
spyOn(directive, 'updateElement').and.stub();
const nodes = [{}, {}];
const change = new SimpleChange([], nodes, true);
directive.ngOnChanges({ nodes: change });
expect(directive.updateElement).not.toHaveBeenCalled();
});
it('enables decorated element', () => {
const renderer = jasmine.createSpyObj('renderer', ['removeAttribute']);
const elementRef = new ElementRef({});
const directive = new CheckAllowableOperationDirective(elementRef, renderer, null, changeDetectorMock);
directive.enableElement();
expect(renderer.removeAttribute).toHaveBeenCalledWith(elementRef.nativeElement, 'disabled');
});
it('disables decorated element', () => {
const renderer = jasmine.createSpyObj('renderer', ['setAttribute']);
const elementRef = new ElementRef({});
const directive = new CheckAllowableOperationDirective(elementRef, renderer, null, changeDetectorMock);
directive.disableElement();
expect(renderer.setAttribute).toHaveBeenCalledWith(elementRef.nativeElement, 'disabled', 'true');
});
it('disables element when nodes not available', () => {
const directive = new CheckAllowableOperationDirective(null, null, null, changeDetectorMock);
spyOn(directive, 'disableElement').and.stub();
directive.nodes = null;
expect(directive.updateElement()).toBeFalsy();
directive.nodes = [];
expect(directive.updateElement()).toBeFalsy();
});
it('enables element when all nodes have expected permission', () => {
const contentService = TestBed.inject(ContentService);
spyOn(contentService, 'hasAllowableOperations').and.returnValue(true);
const directive = new CheckAllowableOperationDirective(null, null, contentService, changeDetectorMock);
spyOn(directive, 'enableElement').and.stub();
directive.nodes = [{}, {}] as any[];
expect(directive.updateElement()).toBeTruthy();
expect(directive.enableElement).toHaveBeenCalled();
});
it('disables element when one of the nodes have no permission', () => {
const contentService = TestBed.inject(ContentService);
spyOn(contentService, 'hasAllowableOperations').and.returnValue(false);
const directive = new CheckAllowableOperationDirective(null, null, contentService, changeDetectorMock);
spyOn(directive, 'disableElement').and.stub();
directive.nodes = [{}, {}] as any[];
expect(directive.updateElement()).toBeFalsy();
expect(directive.disableElement).toHaveBeenCalled();
});
});
describe('Angular component as subject', () => {
it('disables decorated component', () => {
const contentService = TestBed.inject(ContentService);
spyOn(contentService, 'hasAllowableOperations').and.returnValue(false);
spyOn(changeDetectorMock, 'detectChanges');
const testComponent = new TestComponent();
testComponent.disabled = false;
const directive = new CheckAllowableOperationDirective(null, null, contentService, changeDetectorMock, testComponent);
directive.nodes = [{}, {}] as any[];
directive.updateElement();
expect(testComponent.disabled).toBeTruthy();
expect(changeDetectorMock.detectChanges).toHaveBeenCalledTimes(1);
});
it('enables decorated component', () => {
const contentService = TestBed.inject(ContentService);
spyOn(contentService, 'hasAllowableOperations').and.returnValue(true);
spyOn(changeDetectorMock, 'detectChanges');
const testComponent = new TestComponent();
testComponent.disabled = true;
const directive = new CheckAllowableOperationDirective(null, null, contentService, changeDetectorMock, testComponent);
directive.nodes = [{}, {}] as any[];
directive.updateElement();
expect(testComponent.disabled).toBeFalsy();
expect(changeDetectorMock.detectChanges).toHaveBeenCalledTimes(1);
});
});
});

View File

@@ -1,127 +0,0 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* eslint-disable @angular-eslint/no-input-rename */
import { ChangeDetectorRef, Directive, ElementRef, Host, Inject, Input, OnChanges, Optional, Renderer2, SimpleChanges } from '@angular/core';
import { NodeEntry } from '@alfresco/js-api';
import { ContentService } from '../services/content.service';
import { EXTENDIBLE_COMPONENT } from '../interface/injection.tokens';
export interface NodeAllowableOperationSubject {
disabled: boolean;
}
@Directive({
selector: '[adf-check-allowable-operation]'
})
export class CheckAllowableOperationDirective implements OnChanges {
/** Node permission to check (create, delete, update, updatePermissions,
* !create, !delete, !update, !updatePermissions).
*/
@Input('adf-check-allowable-operation')
permission: string = null;
/** Nodes to check permission for. */
@Input('adf-nodes')
nodes: NodeEntry[] = [];
constructor(private elementRef: ElementRef,
private renderer: Renderer2,
private contentService: ContentService,
private changeDetector: ChangeDetectorRef,
@Host()
@Optional()
@Inject(EXTENDIBLE_COMPONENT) private parentComponent?: NodeAllowableOperationSubject) {
}
ngOnChanges(changes: SimpleChanges) {
if (changes.nodes && !changes.nodes.firstChange) {
this.updateElement();
}
}
/**
* Updates disabled state for the decorated element
*
* @memberof CheckAllowableOperationDirective
*/
updateElement(): boolean {
const enable = this.hasAllowableOperations(this.nodes, this.permission);
if (enable) {
this.enable();
} else {
this.disable();
}
return enable;
}
private enable(): void {
if (this.parentComponent) {
this.parentComponent.disabled = false;
this.changeDetector.detectChanges();
} else {
this.enableElement();
}
}
private disable(): void {
if (this.parentComponent) {
this.parentComponent.disabled = true;
this.changeDetector.detectChanges();
} else {
this.disableElement();
}
}
/**
* Enables decorated element
*
* @memberof CheckAllowableOperationDirective
*/
enableElement(): void {
this.renderer.removeAttribute(this.elementRef.nativeElement, 'disabled');
}
/**
* Disables decorated element
*
* @memberof CheckAllowableOperationDirective
*/
disableElement(): void {
this.renderer.setAttribute(this.elementRef.nativeElement, 'disabled', 'true');
}
/**
* Checks whether all nodes have a particular permission
*
* @param nodes Node collection to check
* @param permission Permission to check for each node
* @memberof CheckAllowableOperationDirective
*/
hasAllowableOperations(nodes: NodeEntry[], permission: string): boolean {
if (nodes && nodes.length > 0) {
return nodes.every((node) => this.contentService.hasAllowableOperations(node.entry, permission));
}
return false;
}
}

View File

@@ -21,10 +21,6 @@ import { MaterialModule } from '../material.module';
import { HighlightDirective } from './highlight.directive';
import { LogoutDirective } from './logout.directive';
import { NodeDeleteDirective } from './node-delete.directive';
import { NodeFavoriteDirective } from './node-favorite.directive';
import { CheckAllowableOperationDirective } from './check-allowable-operation.directive';
import { NodeRestoreDirective } from './node-restore.directive';
import { UploadDirective } from './upload.directive';
import { NodeDownloadDirective } from './node-download.directive';
import { VersionCompatibilityDirective } from './version-compatibility.directive';
@@ -32,8 +28,6 @@ import { TooltipCardDirective } from './tooltip-card/tooltip-card.directive';
import { OverlayModule } from '@angular/cdk/overlay';
import { TooltipCardComponent } from './tooltip-card/tooltip-card.component';
import { InfiniteSelectScrollDirective } from './infinite-select-scroll.directive';
import { LibraryFavoriteDirective } from './library-favorite.directive';
import { LibraryMembershipDirective } from './library-membership.directive';
@NgModule({
imports: [
@@ -44,33 +38,21 @@ import { LibraryMembershipDirective } from './library-membership.directive';
declarations: [
HighlightDirective,
LogoutDirective,
NodeDeleteDirective,
NodeFavoriteDirective,
CheckAllowableOperationDirective,
NodeRestoreDirective,
NodeDownloadDirective,
UploadDirective,
VersionCompatibilityDirective,
TooltipCardDirective,
TooltipCardComponent,
InfiniteSelectScrollDirective,
LibraryFavoriteDirective,
LibraryMembershipDirective
InfiniteSelectScrollDirective
],
exports: [
HighlightDirective,
LogoutDirective,
NodeDeleteDirective,
NodeFavoriteDirective,
CheckAllowableOperationDirective,
NodeRestoreDirective,
NodeDownloadDirective,
UploadDirective,
VersionCompatibilityDirective,
TooltipCardDirective,
InfiniteSelectScrollDirective,
LibraryFavoriteDirective,
LibraryMembershipDirective
InfiniteSelectScrollDirective
]
})
export class DirectiveModule {}

View File

@@ -1,119 +0,0 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Component, ViewChild } from '@angular/core';
import { LibraryEntity, LibraryFavoriteDirective } from './library-favorite.directive';
import { TestBed, ComponentFixture } from '@angular/core/testing';
import { TranslateModule } from '@ngx-translate/core';
import { AlfrescoApiService } from '../services/alfresco-api.service';
import { CoreModule } from '../core.module';
import { AlfrescoApiServiceMock } from '../mock';
@Component({
selector: 'app-test-component',
template: ` <button #favoriteLibrary="favoriteLibrary" [adf-favorite-library]="selection">Favorite</button> `
})
class TestComponent {
@ViewChild('favoriteLibrary', { static: true })
directive: LibraryFavoriteDirective;
selection: LibraryEntity = null;
}
describe('LibraryFavoriteDirective', () => {
let fixture: ComponentFixture<TestComponent>;
let component: TestComponent;
let selection: LibraryEntity;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [TranslateModule.forRoot(), CoreModule.forRoot()],
providers: [
{ provide: AlfrescoApiService, useClass: AlfrescoApiServiceMock }
],
declarations: [TestComponent, LibraryFavoriteDirective]
});
fixture = TestBed.createComponent(TestComponent);
component = fixture.componentInstance;
selection = { entry: { guid: 'guid', id: 'id', title: 'Site', visibility: 'PUBLIC' }, isLibrary: true, isFavorite: false };
component.selection = selection;
});
it('should not check for favorite if no selection exists', () => {
spyOn(component.directive['favoritesApi'], 'getFavoriteSite');
fixture.detectChanges();
expect(component.directive['favoritesApi'].getFavoriteSite).not.toHaveBeenCalled();
});
it('should mark selection as favorite', async () => {
spyOn(component.directive['favoritesApi'], 'getFavoriteSite').and.returnValue(Promise.resolve(null));
delete selection.isFavorite;
fixture.detectChanges();
await fixture.whenStable();
expect(component.directive['favoritesApi'].getFavoriteSite).toHaveBeenCalled();
expect(component.directive.isFavorite()).toBe(true);
});
it('should mark selection not favorite', async () => {
spyOn(component.directive['favoritesApi'], 'getFavoriteSite').and.returnValue(Promise.reject());
delete selection.isFavorite;
fixture.detectChanges();
await fixture.whenStable();
expect(component.directive['favoritesApi'].getFavoriteSite).toHaveBeenCalled();
expect(component.directive.isFavorite()).toBe(false);
});
it('should call addFavorite() on click event when selection is not a favorite', async () => {
spyOn(component.directive['favoritesApi'], 'getFavoriteSite').and.returnValue(Promise.reject());
spyOn(component.directive['favoritesApi'], 'createFavorite').and.returnValue(Promise.resolve(null));
fixture.detectChanges();
await fixture.whenStable();
expect(component.directive.isFavorite()).toBeFalsy();
fixture.nativeElement.querySelector('button').dispatchEvent(new MouseEvent('click'));
fixture.detectChanges();
expect(component.directive['favoritesApi'].createFavorite).toHaveBeenCalled();
});
it('should call removeFavoriteSite() on click event when selection is favorite', async () => {
spyOn(component.directive['favoritesApi'], 'getFavoriteSite').and.returnValue(Promise.resolve(null));
spyOn(component.directive['favoritesApi'], 'deleteFavorite').and.returnValue(Promise.resolve());
selection.isFavorite = true;
fixture.detectChanges();
await fixture.whenStable();
expect(component.directive.isFavorite()).toBeTruthy();
fixture.nativeElement.querySelector('button').dispatchEvent(new MouseEvent('click'));
fixture.detectChanges();
await fixture.whenStable();
expect(component.directive['favoritesApi'].deleteFavorite).toHaveBeenCalled();
});
});

View File

@@ -1,114 +0,0 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Directive, HostListener, Input, OnChanges, Output, EventEmitter } from '@angular/core';
import { SiteBody, FavoriteBody, FavoriteEntry, Site, FavoritesApi } from '@alfresco/js-api';
import { AlfrescoApiService } from '../services/alfresco-api.service';
export interface LibraryEntity {
entry: Site;
isLibrary: boolean;
isFavorite: boolean;
}
@Directive({
selector: '[adf-favorite-library]',
exportAs: 'favoriteLibrary'
})
export class LibraryFavoriteDirective implements OnChanges {
@Input('adf-favorite-library')
library: LibraryEntity = null;
@Output() toggle = new EventEmitter<any>();
// eslint-disable-next-line @angular-eslint/no-output-native
@Output() error = new EventEmitter<any>();
private targetLibrary = null;
_favoritesApi: FavoritesApi;
get favoritesApi(): FavoritesApi {
this._favoritesApi = this._favoritesApi ?? new FavoritesApi(this.alfrescoApiService.getInstance());
return this._favoritesApi;
}
@HostListener('click')
onClick() {
const guid = this.targetLibrary.entry.guid;
if (this.targetLibrary.isFavorite) {
this.removeFavorite(guid);
} else {
this.addFavorite({
target: {
site: {
guid
}
}
});
}
}
constructor(private alfrescoApiService: AlfrescoApiService) {
}
ngOnChanges(changes) {
if (!changes.library.currentValue) {
this.targetLibrary = null;
return;
}
this.targetLibrary = changes.library.currentValue;
this.markFavoriteLibrary(changes.library.currentValue);
}
isFavorite(): boolean {
return this.targetLibrary && this.targetLibrary.isFavorite;
}
private async markFavoriteLibrary(library: LibraryEntity) {
if (this.targetLibrary.isFavorite === undefined) {
try {
await this.favoritesApi.getFavoriteSite('-me-', library.entry.id);
this.targetLibrary.isFavorite = true;
} catch {
this.targetLibrary.isFavorite = false;
}
} else {
this.targetLibrary = library;
}
}
private addFavorite(favoriteBody: FavoriteBody) {
this.favoritesApi
.createFavorite('-me-', favoriteBody)
.then((libraryEntry: FavoriteEntry) => {
this.targetLibrary.isFavorite = true;
this.toggle.emit(libraryEntry);
})
.catch((error) => this.error.emit(error));
}
private removeFavorite(favoriteId: string) {
this.favoritesApi
.deleteFavorite('-me-', favoriteId)
.then((libraryBody: SiteBody) => {
this.targetLibrary.isFavorite = false;
this.toggle.emit(libraryBody);
})
.catch((error) => this.error.emit(error));
}
}

View File

@@ -1,220 +0,0 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { fakeAsync, TestBed, tick } from '@angular/core/testing';
import { AlfrescoApiService, SitesService } from '../services';
import { LibraryMembershipDirective } from './library-membership.directive';
import { NO_ERRORS_SCHEMA, SimpleChange } from '@angular/core';
import { of, throwError, Subject } from 'rxjs';
import { TranslateModule } from '@ngx-translate/core';
import { DirectiveModule } from './directive.module';
import { CoreModule } from '../core.module';
import { CoreTestingModule } from '../testing/core.testing.module';
describe('LibraryMembershipDirective', () => {
let alfrescoApiService: AlfrescoApiService;
let directive: LibraryMembershipDirective;
let sitesService: SitesService;
let addMembershipSpy: jasmine.Spy;
let getMembershipSpy: jasmine.Spy;
let deleteMembershipSpy: jasmine.Spy;
let mockSupportedVersion = false;
let testSiteEntry: any;
let requestedMembershipResponse: any;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [
TranslateModule.forRoot(),
DirectiveModule,
CoreModule.forRoot(),
CoreTestingModule
],
schemas: [NO_ERRORS_SCHEMA]
});
testSiteEntry = {
id: 'id-1',
guid: 'site-1',
title: 'aa t m',
visibility: 'MODERATED'
};
requestedMembershipResponse = {
id: testSiteEntry.id,
createdAt: '2018-11-14',
site: testSiteEntry
};
alfrescoApiService = TestBed.inject(AlfrescoApiService);
sitesService = TestBed.inject(SitesService);
directive = new LibraryMembershipDirective(alfrescoApiService, sitesService, {
ecmProductInfo$: new Subject(),
isVersionSupported: () => mockSupportedVersion
} as any);
});
describe('markMembershipRequest', () => {
beforeEach(() => {
getMembershipSpy = spyOn(directive['sitesApi'], 'getSiteMembershipRequestForPerson').and.returnValue(Promise.resolve({ entry: requestedMembershipResponse }));
});
it('should not check membership requests if no entry is selected', fakeAsync(() => {
const selection = {};
const change = new SimpleChange(null, selection, true);
directive.ngOnChanges({ selection: change });
tick();
expect(getMembershipSpy).not.toHaveBeenCalled();
}));
it('should check if a membership request exists for the selected library', fakeAsync(() => {
const selection = { entry: {} };
const change = new SimpleChange(null, selection, true);
directive.ngOnChanges({ selection: change });
tick();
expect(getMembershipSpy.calls.count()).toBe(1);
}));
it('should remember when a membership request exists for selected library', fakeAsync(() => {
const selection = { entry: testSiteEntry };
const change = new SimpleChange(null, selection, true);
directive.ngOnChanges({ selection: change });
tick();
expect(directive.targetSite.joinRequested).toBe(true);
}));
it('should remember when a membership request is not found for selected library', fakeAsync(() => {
getMembershipSpy.and.returnValue(Promise.reject());
const selection = { entry: testSiteEntry };
const change = new SimpleChange(null, selection, true);
directive.ngOnChanges({ selection: change });
tick();
expect(directive.targetSite.joinRequested).toBe(false);
}));
});
describe('toggleMembershipRequest', () => {
beforeEach(() => {
mockSupportedVersion = false;
getMembershipSpy = spyOn(directive['sitesApi'], 'getSiteMembershipRequestForPerson').and.returnValue(Promise.resolve({ entry: requestedMembershipResponse }));
addMembershipSpy = spyOn(directive['sitesApi'], 'createSiteMembershipRequestForPerson').and.returnValue(Promise.resolve({ entry: requestedMembershipResponse }));
deleteMembershipSpy = spyOn(directive['sitesApi'], 'deleteSiteMembershipRequestForPerson').and.returnValue(Promise.resolve({}));
});
it('should do nothing if there is no selected library ', fakeAsync(() => {
const selection = {};
const change = new SimpleChange(null, selection, true);
directive.ngOnChanges({ selection: change });
tick();
directive.toggleMembershipRequest();
tick();
expect(addMembershipSpy).not.toHaveBeenCalled();
expect(deleteMembershipSpy).not.toHaveBeenCalled();
}));
it('should delete membership request if there is one', fakeAsync(() => {
const selection = { entry: testSiteEntry };
const change = new SimpleChange(null, selection, true);
directive.ngOnChanges({ selection: change });
tick();
directive.toggleMembershipRequest();
tick();
expect(deleteMembershipSpy).toHaveBeenCalled();
}));
it('should call API to make a membership request if there is none', fakeAsync(() => {
const selection = { entry: { id: 'no-membership-requested' } };
const change = new SimpleChange(null, selection, true);
directive.ngOnChanges({ selection: change });
tick();
directive.toggleMembershipRequest();
tick();
expect(addMembershipSpy).toHaveBeenCalledWith('-me-', { id: 'no-membership-requested' });
expect(deleteMembershipSpy).not.toHaveBeenCalled();
}));
it(`should add 'workspace' to send appropriate email`, fakeAsync(() => {
mockSupportedVersion = true;
const selection = { entry: { id: 'no-membership-requested' } };
const change = new SimpleChange(null, selection, true);
directive.ngOnChanges({ selection: change });
tick();
directive.toggleMembershipRequest();
tick();
expect(addMembershipSpy).toHaveBeenCalledWith('-me-', { id: 'no-membership-requested', client: 'workspace' });
expect(deleteMembershipSpy).not.toHaveBeenCalled();
}));
it('should call API to add user to library if admin user', fakeAsync(() => {
const createSiteMembershipSpy = spyOn(sitesService, 'createSiteMembership').and.returnValue(of({} as any));
const selection = { entry: { id: 'no-membership-requested' } };
const selectionChange = new SimpleChange(null, selection, true);
directive.isAdmin = true;
directive.ngOnChanges({ selection: selectionChange });
tick();
directive.toggleMembershipRequest();
tick();
expect(createSiteMembershipSpy).toHaveBeenCalled();
expect(addMembershipSpy).not.toHaveBeenCalled();
}));
it('should emit error when the request to join a library fails', fakeAsync(() => {
spyOn(directive.error, 'emit');
addMembershipSpy.and.returnValue(throwError('err'));
const selection = { entry: { id: 'no-membership-requested' } };
const change = new SimpleChange(null, selection, true);
directive.ngOnChanges({ selection: change });
tick();
directive.toggleMembershipRequest();
tick();
expect(directive.error.emit).toHaveBeenCalled();
}));
it('should emit specific error message on invalid email address server error', fakeAsync(() => {
const emitErrorSpy = spyOn(directive.error, 'emit');
const selection = { entry: { id: 'no-membership-requested' } };
const change = new SimpleChange(null, selection, true);
directive.ngOnChanges({ selection: change });
tick();
const testData = [
{
fixture: 'Failed to resolve sender mail address',
expected: 'APP.MESSAGES.ERRORS.INVALID_SENDER_EMAIL'
},
{
fixture: 'All recipients for the mail action were invalid',
expected: 'APP.MESSAGES.ERRORS.INVALID_RECEIVER_EMAIL'
}
];
testData.forEach((data) => {
addMembershipSpy.and.returnValue(throwError({ message: data.fixture }));
emitErrorSpy.calls.reset();
directive.toggleMembershipRequest();
tick();
expect(emitErrorSpy).toHaveBeenCalledWith({
error: { message: data.fixture },
i18nKey: data.expected
});
});
}));
});
});

View File

@@ -1,242 +0,0 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Directive, EventEmitter, HostListener, Input, OnChanges, Output, SimpleChanges } from '@angular/core';
import {
SiteEntry,
SiteMembershipRequestBody,
SiteMemberEntry,
SiteMembershipRequestEntry,
SitesApi
} from '@alfresco/js-api';
import { BehaviorSubject, from, Observable } from 'rxjs';
import { AlfrescoApiService } from '../services/alfresco-api.service';
import { SitesService } from '../services/sites.service';
import { VersionCompatibilityService } from '../services/version-compatibility.service';
export interface LibraryMembershipToggleEvent {
updatedEntry?: any;
shouldReload: boolean;
i18nKey: string;
}
export interface LibraryMembershipErrorEvent {
error: any;
i18nKey: string;
}
@Directive({
selector: '[adf-library-membership]',
exportAs: 'libraryMembership'
})
export class LibraryMembershipDirective implements OnChanges {
targetSite: any = null;
isJoinRequested: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
_sitesApi: SitesApi;
get sitesApi(): SitesApi {
this._sitesApi = this._sitesApi ?? new SitesApi(this.alfrescoApiService.getInstance());
return this._sitesApi;
}
/** Site for which to toggle the membership request. */
@Input('adf-library-membership')
selection: SiteEntry = null;
/** Site for which to toggle the membership request. */
@Input()
isAdmin = false;
@Output()
toggle = new EventEmitter<LibraryMembershipToggleEvent>();
// eslint-disable-next-line @angular-eslint/no-output-native
@Output()
error = new EventEmitter<LibraryMembershipErrorEvent>();
@HostListener('click')
onClick() {
this.toggleMembershipRequest();
}
constructor(
private alfrescoApiService: AlfrescoApiService,
private sitesService: SitesService,
private versionCompatibilityService: VersionCompatibilityService
) {
}
ngOnChanges(changes: SimpleChanges) {
if (!changes.selection.currentValue || !changes.selection.currentValue.entry) {
this.targetSite = null;
return;
}
this.targetSite = changes.selection.currentValue.entry;
this.markMembershipRequest();
}
toggleMembershipRequest() {
if (!this.targetSite) {
return;
}
if (this.targetSite.joinRequested) {
this.cancelJoinRequest().subscribe(
() => {
this.targetSite.joinRequested = false;
this.isJoinRequested.next(false);
const info = {
updatedEntry: this.targetSite,
shouldReload: false,
i18nKey: 'APP.MESSAGES.INFO.JOIN_CANCELED'
};
this.toggle.emit(info);
},
(error) => {
const errWithMessage = {
error,
i18nKey: 'APP.MESSAGES.ERRORS.JOIN_CANCEL_FAILED'
};
this.error.emit(errWithMessage);
}
);
}
if (!this.targetSite.joinRequested && !this.isAdmin) {
this.joinLibraryRequest().subscribe(
(createdMembership) => {
this.targetSite.joinRequested = true;
this.isJoinRequested.next(true);
if (createdMembership.entry && createdMembership.entry.site && createdMembership.entry.site.role) {
const info = {
shouldReload: true,
i18nKey: 'APP.MESSAGES.INFO.JOINED'
};
this.toggle.emit(info);
} else {
const info = {
updatedEntry: this.targetSite,
shouldReload: false,
i18nKey: 'APP.MESSAGES.INFO.JOIN_REQUESTED'
};
this.toggle.emit(info);
}
},
(error) => {
const errWithMessage = {
error,
i18nKey: 'APP.MESSAGES.ERRORS.JOIN_REQUEST_FAILED'
};
const senderEmailCheck = 'Failed to resolve sender mail address';
const receiverEmailCheck = 'All recipients for the mail action were invalid';
if (error.message) {
if (error.message.includes(senderEmailCheck)) {
errWithMessage.i18nKey = 'APP.MESSAGES.ERRORS.INVALID_SENDER_EMAIL';
} else if (error.message.includes(receiverEmailCheck)) {
errWithMessage.i18nKey = 'APP.MESSAGES.ERRORS.INVALID_RECEIVER_EMAIL';
}
}
this.error.emit(errWithMessage);
}
);
}
if (this.isAdmin) {
this.joinLibrary().subscribe(
(createdMembership: SiteMemberEntry) => {
if (createdMembership.entry && createdMembership.entry.role) {
const info = {
shouldReload: true,
i18nKey: 'APP.MESSAGES.INFO.JOINED'
};
this.toggle.emit(info);
}
},
(error) => {
const errWithMessage = {
error,
i18nKey: 'APP.MESSAGES.ERRORS.JOIN_REQUEST_FAILED'
};
const senderEmailCheck = 'Failed to resolve sender mail address';
const receiverEmailCheck = 'All recipients for the mail action were invalid';
if (error.message) {
if (error.message.includes(senderEmailCheck)) {
errWithMessage.i18nKey = 'APP.MESSAGES.ERRORS.INVALID_SENDER_EMAIL';
} else if (error.message.includes(receiverEmailCheck)) {
errWithMessage.i18nKey = 'APP.MESSAGES.ERRORS.INVALID_RECEIVER_EMAIL';
}
}
this.error.emit(errWithMessage);
}
);
}
}
markMembershipRequest() {
if (!this.targetSite) {
return;
}
this.getMembershipRequest().subscribe(
(data) => {
if (data.entry.id === this.targetSite.id) {
this.targetSite.joinRequested = true;
this.isJoinRequested.next(true);
}
},
() => {
this.targetSite.joinRequested = false;
this.isJoinRequested.next(false);
}
);
}
private joinLibraryRequest(): Observable<SiteMembershipRequestEntry> {
const memberBody = {
id: this.targetSite.id
} as SiteMembershipRequestBody;
if (this.versionCompatibilityService.isVersionSupported('7.0.0')) {
memberBody.client = 'workspace';
}
return from(this.sitesApi.createSiteMembershipRequestForPerson('-me-', memberBody));
}
private joinLibrary() {
return this.sitesService.createSiteMembership(this.targetSite.id, {
role: 'SiteConsumer',
id: '-me-'
});
}
private cancelJoinRequest() {
return from(this.sitesApi.deleteSiteMembershipRequestForPerson('-me-', this.targetSite.id));
}
private getMembershipRequest() {
return from(this.sitesApi.getSiteMembershipRequestForPerson('-me-', this.targetSite.id));
}
}

View File

@@ -1,346 +0,0 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Component, DebugElement, ViewChild } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { NodeDeleteDirective } from './node-delete.directive';
import { setupTestBed } from '../testing/setup-test-bed';
import { CoreTestingModule } from '../testing/core.testing.module';
@Component({
template: `
<div id="delete-component" [adf-delete]="selection"
(delete)="onDelete()">
</div>`
})
class TestComponent {
selection = [];
@ViewChild(NodeDeleteDirective, { static: true })
deleteDirective: NodeDeleteDirective;
onDelete() {
}
}
@Component({
template: `
<div id="delete-component" [adf-check-allowable-operation]="selection"
[adf-delete]="selection"
(delete)="onDelete($event)">
</div>`
})
class TestWithPermissionsComponent {
selection = [];
@ViewChild(NodeDeleteDirective, { static: true })
deleteDirective: NodeDeleteDirective;
onDelete = jasmine.createSpy('onDelete');
}
@Component({
template: `
delete permanent
<div id="delete-permanent"
[adf-delete]="selection"
[permanent]="permanent"
(delete)="onDelete($event)">
</div>`
})
class TestDeletePermanentComponent {
selection = [];
@ViewChild(NodeDeleteDirective, { static: true })
deleteDirective: NodeDeleteDirective;
permanent = true;
onDelete = jasmine.createSpy('onDelete');
}
describe('NodeDeleteDirective', () => {
let fixture: ComponentFixture<TestComponent>;
let fixtureWithPermissions: ComponentFixture<TestWithPermissionsComponent>;
let fixtureWithPermanentComponent: ComponentFixture<TestDeletePermanentComponent>;
let element: DebugElement;
let elementWithPermanentDelete: DebugElement;
let component: TestComponent;
let componentWithPermanentDelete: TestDeletePermanentComponent;
let deleteNodeSpy: any;
let disposableDelete: any;
let deleteNodePermanentSpy: any;
let purgeDeletedNodePermanentSpy: any;
setupTestBed({
imports: [
CoreTestingModule
],
declarations: [
TestComponent,
TestWithPermissionsComponent,
TestDeletePermanentComponent
]
});
beforeEach(() => {
fixture = TestBed.createComponent(TestComponent);
fixtureWithPermissions = TestBed.createComponent(TestWithPermissionsComponent);
fixtureWithPermanentComponent = TestBed.createComponent(TestDeletePermanentComponent);
component = fixture.componentInstance;
componentWithPermanentDelete = fixtureWithPermanentComponent.componentInstance;
element = fixture.debugElement.query(By.directive(NodeDeleteDirective));
elementWithPermanentDelete = fixtureWithPermanentComponent.debugElement.query(By.directive(NodeDeleteDirective));
deleteNodeSpy = spyOn(component.deleteDirective['nodesApi'], 'deleteNode').and.returnValue(Promise.resolve());
deleteNodePermanentSpy = spyOn(componentWithPermanentDelete.deleteDirective['nodesApi'], 'deleteNode').and.returnValue(Promise.resolve());
purgeDeletedNodePermanentSpy = spyOn(componentWithPermanentDelete.deleteDirective['trashcanApi'], 'deleteDeletedNode').and.returnValue(Promise.resolve());
});
afterEach(() => {
if (disposableDelete) {
disposableDelete.unsubscribe();
}
fixture.destroy();
});
describe('Delete', () => {
it('should do nothing if selection is empty', () => {
component.selection = [];
fixture.detectChanges();
element.nativeElement.click();
expect(deleteNodeSpy).not.toHaveBeenCalled();
});
it('should process node successfully', async () => {
component.selection = [{ entry: { id: '1', name: 'name1' } }];
fixture.detectChanges();
disposableDelete = component.deleteDirective.delete.subscribe((message) => {
expect(message).toBe(
'CORE.DELETE_NODE.SINGULAR'
);
});
element.nativeElement.click();
fixture.detectChanges();
await fixture.whenStable();
});
it('should notify failed node deletion', async () => {
deleteNodeSpy.and.returnValue(Promise.reject('error'));
component.selection = [{ entry: { id: '1', name: 'name1' } }];
fixture.detectChanges();
disposableDelete = component.deleteDirective.delete.subscribe((message) => {
expect(message).toBe(
'CORE.DELETE_NODE.ERROR_SINGULAR'
);
});
element.nativeElement.click();
fixture.detectChanges();
await fixture.whenStable();
});
it('should notify nodes deletion', async () => {
component.selection = [
{ entry: { id: '1', name: 'name1' } },
{ entry: { id: '2', name: 'name2' } }
];
fixture.detectChanges();
disposableDelete = component.deleteDirective.delete.subscribe((message) => {
expect(message).toBe(
'CORE.DELETE_NODE.PLURAL'
);
});
element.nativeElement.click();
fixture.detectChanges();
await fixture.whenStable();
});
it('should notify failed nodes deletion', async () => {
deleteNodeSpy.and.returnValue(Promise.reject('error'));
component.selection = [
{ entry: { id: '1', name: 'name1' } },
{ entry: { id: '2', name: 'name2' } }
];
fixture.detectChanges();
disposableDelete = component.deleteDirective.delete.subscribe((message) => {
expect(message).toBe(
'CORE.DELETE_NODE.ERROR_PLURAL'
);
});
element.nativeElement.click();
fixture.detectChanges();
await fixture.whenStable();
});
it('should notify partial deletion when only one node is successful', async () => {
deleteNodeSpy.and.callFake((id) => {
if (id === '1') {
return Promise.reject('error');
} else {
return Promise.resolve();
}
});
component.selection = [
{ entry: { id: '1', name: 'name1' } },
{ entry: { id: '2', name: 'name2' } }
];
fixture.detectChanges();
disposableDelete = component.deleteDirective.delete.subscribe((message) => {
expect(message).toBe(
'CORE.DELETE_NODE.PARTIAL_SINGULAR'
);
});
element.nativeElement.click();
fixture.detectChanges();
await fixture.whenStable();
});
it('should notify partial deletion when some nodes are successful', async () => {
deleteNodeSpy.and.callFake((id) => {
if (id === '1') {
return Promise.reject(null);
}
return Promise.resolve();
});
component.selection = [
{ entry: { id: '1', name: 'name1' } },
{ entry: { id: '2', name: 'name2' } },
{ entry: { id: '3', name: 'name3' } }
];
fixture.detectChanges();
disposableDelete = component.deleteDirective.delete.subscribe((message) => {
expect(message).toBe(
'CORE.DELETE_NODE.PARTIAL_PLURAL'
);
});
element.nativeElement.click();
fixture.detectChanges();
await fixture.whenStable();
});
it('should emit event when delete is done', async () => {
component.selection = [{ entry: { id: '1', name: 'name1' } }];
fixture.detectChanges();
disposableDelete = component.deleteDirective.delete.subscribe((node) => {
expect(node).toEqual('CORE.DELETE_NODE.SINGULAR');
});
element.nativeElement.click();
fixture.detectChanges();
await fixture.whenStable();
});
it('should disable the button if no node are selected', () => {
component.selection = [];
fixture.detectChanges();
expect(element.nativeElement.disabled).toEqual(true);
});
it('should disable the button if selected node is null', () => {
component.selection = null;
fixture.detectChanges();
expect(element.nativeElement.disabled).toEqual(true);
});
it('should enable the button if nodes are selected', () => {
component.selection = [
{ entry: { id: '1', name: 'name1' } },
{ entry: { id: '2', name: 'name2' } },
{ entry: { id: '3', name: 'name3' } }
];
fixture.detectChanges();
expect(element.nativeElement.disabled).toEqual(false);
});
it('should not enable the button if adf-check-allowable-operation is present', () => {
const elementWithPermissions = fixtureWithPermissions.debugElement.query(By.directive(NodeDeleteDirective));
const componentWithPermissions = fixtureWithPermissions.componentInstance;
elementWithPermissions.nativeElement.disabled = false;
componentWithPermissions.selection = [];
fixtureWithPermissions.detectChanges();
componentWithPermissions.selection = [
{ entry: { id: '1', name: 'name1' } },
{ entry: { id: '2', name: 'name2' } },
{ entry: { id: '3', name: 'name3' } }
];
fixtureWithPermissions.detectChanges();
expect(elementWithPermissions.nativeElement.disabled).toEqual(false);
});
describe('Permanent', () => {
it('should call the api with permanent delete option if permanent directive input is true', () => {
fixtureWithPermanentComponent.detectChanges();
componentWithPermanentDelete.selection = [
{ entry: { id: '1', name: 'name1' } }
];
fixtureWithPermanentComponent.detectChanges();
elementWithPermanentDelete.nativeElement.click();
expect(deleteNodePermanentSpy).toHaveBeenCalledWith('1', { permanent: true });
});
it('should call the trashcan api if permanent directive input is true and the file is already in the trashcan ', () => {
fixtureWithPermanentComponent.detectChanges();
componentWithPermanentDelete.selection = [
{ entry: { id: '1', name: 'name1', archivedAt: 'archived' } }
];
fixtureWithPermanentComponent.detectChanges();
elementWithPermanentDelete.nativeElement.click();
expect(purgeDeletedNodePermanentSpy).toHaveBeenCalledWith('1');
});
});
});
});

View File

@@ -1,237 +0,0 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* eslint-disable @angular-eslint/no-input-rename */
import { Directive, ElementRef, EventEmitter, HostListener, Input, OnChanges, Output } from '@angular/core';
import { NodeEntry, Node, DeletedNodeEntity, DeletedNode, TrashcanApi, NodesApi } from '@alfresco/js-api';
import { Observable, forkJoin, from, of } from 'rxjs';
import { AlfrescoApiService } from '../services/alfresco-api.service';
import { TranslationService } from '../services/translation.service';
import { map, catchError, retry } from 'rxjs/operators';
interface ProcessedNodeData {
entry: Node | DeletedNode;
status: number;
}
interface ProcessStatus {
success: ProcessedNodeData[];
failed: ProcessedNodeData[];
someFailed();
someSucceeded();
oneFailed();
oneSucceeded();
allSucceeded();
allFailed();
}
@Directive({
selector: '[adf-delete]'
})
export class NodeDeleteDirective implements OnChanges {
/** Array of nodes to delete. */
@Input('adf-delete')
selection: NodeEntry[] | DeletedNodeEntity[];
/** If true then the nodes are deleted immediately rather than being put in the trash */
@Input()
permanent: boolean = false;
/** Emitted when the nodes have been deleted. */
@Output()
delete: EventEmitter<any> = new EventEmitter();
_trashcanApi: TrashcanApi;
get trashcanApi(): TrashcanApi {
this._trashcanApi = this._trashcanApi ?? new TrashcanApi(this.alfrescoApiService.getInstance());
return this._trashcanApi;
}
_nodesApi: NodesApi;
get nodesApi(): NodesApi {
this._nodesApi = this._nodesApi ?? new NodesApi(this.alfrescoApiService.getInstance());
return this._nodesApi;
}
@HostListener('click')
onClick() {
this.process(this.selection);
}
constructor(private alfrescoApiService: AlfrescoApiService,
private translation: TranslationService,
private elementRef: ElementRef) {
}
ngOnChanges() {
if (!this.selection || (this.selection && this.selection.length === 0)) {
this.setDisableAttribute(true);
} else {
if (!this.elementRef.nativeElement.hasAttribute('adf-check-allowable-operation')) {
this.setDisableAttribute(false);
}
}
}
private setDisableAttribute(disable: boolean) {
this.elementRef.nativeElement.disabled = disable;
}
private process(selection: NodeEntry[] | DeletedNodeEntity[]) {
if (selection && selection.length) {
const batch = this.getDeleteNodesBatch(selection);
forkJoin(...batch)
.subscribe((data: ProcessedNodeData[]) => {
const processedItems: ProcessStatus = this.processStatus(data);
const message = this.getMessage(processedItems);
if (message) {
this.delete.emit(message);
}
});
}
}
private getDeleteNodesBatch(selection: any): Observable<ProcessedNodeData>[] {
return selection.map((node) => this.deleteNode(node));
}
private deleteNode(node: NodeEntry | DeletedNodeEntity): Observable<ProcessedNodeData> {
const id = (node.entry as any).nodeId || node.entry.id;
let promise: Promise<any>;
if (node.entry.hasOwnProperty('archivedAt') && node.entry['archivedAt']) {
promise = this.trashcanApi.deleteDeletedNode(id);
} else {
promise = this.nodesApi.deleteNode(id, { permanent: this.permanent });
}
return from(promise).pipe(
retry(3),
map(() => ({
entry: node.entry,
status: 1
})),
catchError(() => of({
entry: node.entry,
status: 0
}))
);
}
private processStatus(data): ProcessStatus {
const deleteStatus = {
success: [],
failed: [],
get someFailed() {
return !!(this.failed.length);
},
get someSucceeded() {
return !!(this.success.length);
},
get oneFailed() {
return this.failed.length === 1;
},
get oneSucceeded() {
return this.success.length === 1;
},
get allSucceeded() {
return this.someSucceeded && !this.someFailed;
},
get allFailed() {
return this.someFailed && !this.someSucceeded;
}
};
return data.reduce(
(acc, next) => {
if (next.status === 1) {
acc.success.push(next);
} else {
acc.failed.push(next);
}
return acc;
},
deleteStatus
);
}
private getMessage(status: ProcessStatus): string | null {
if (status.allFailed && !status.oneFailed) {
return this.translation.instant(
'CORE.DELETE_NODE.ERROR_PLURAL',
// eslint-disable-next-line id-blacklist
{ number: status.failed.length }
);
}
if (status.allSucceeded && !status.oneSucceeded) {
return this.translation.instant(
'CORE.DELETE_NODE.PLURAL',
// eslint-disable-next-line id-blacklist
{ number: status.success.length }
);
}
if (status.someFailed && status.someSucceeded && !status.oneSucceeded) {
return this.translation.instant(
'CORE.DELETE_NODE.PARTIAL_PLURAL',
{
success: status.success.length,
failed: status.failed.length
}
);
}
if (status.someFailed && status.oneSucceeded) {
return this.translation.instant(
'CORE.DELETE_NODE.PARTIAL_SINGULAR',
{
success: status.success.length,
failed: status.failed.length
}
);
}
if (status.oneFailed && !status.someSucceeded) {
return this.translation.instant(
'CORE.DELETE_NODE.ERROR_SINGULAR',
{ name: status.failed[0].entry.name }
);
}
if (status.oneSucceeded && !status.someFailed) {
return this.translation.instant(
'CORE.DELETE_NODE.SINGULAR',
{ name: status.success[0].entry.name }
);
}
return null;
}
}

View File

@@ -1,403 +0,0 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { SimpleChange } from '@angular/core';
import { fakeAsync, TestBed, tick } from '@angular/core/testing';
import { NodeFavoriteDirective } from './node-favorite.directive';
import { setupTestBed } from '../testing/setup-test-bed';
import { CoreTestingModule } from '../testing/core.testing.module';
import { TranslateModule } from '@ngx-translate/core';
import { AlfrescoApiService } from '../services/alfresco-api.service';
describe('NodeFavoriteDirective', () => {
let directive: NodeFavoriteDirective;
let alfrescoApiService: AlfrescoApiService;
setupTestBed({
imports: [
TranslateModule.forRoot(),
CoreTestingModule
]
});
beforeEach(() => {
alfrescoApiService = TestBed.inject(AlfrescoApiService);
directive = new NodeFavoriteDirective( alfrescoApiService);
});
describe('selection input change event', () => {
it('should not call markFavoritesNodes() if input list is empty', () => {
spyOn(directive, 'markFavoritesNodes');
const change = new SimpleChange(null, [], true);
directive.ngOnChanges({selection: change});
expect(directive.markFavoritesNodes).not.toHaveBeenCalledWith();
});
it('should call markFavoritesNodes() on input change', () => {
spyOn(directive, 'markFavoritesNodes');
let selection = [{ entry: { id: '1', name: 'name1' } }];
let change = new SimpleChange(null, selection, true);
directive.ngOnChanges({selection: change});
expect(directive.markFavoritesNodes).toHaveBeenCalledWith(selection);
selection = [
{ entry: { id: '1', name: 'name1' } },
{ entry: { id: '2', name: 'name2' } }
];
change = new SimpleChange(null, selection, true);
directive.ngOnChanges({selection: change});
expect(directive.markFavoritesNodes).toHaveBeenCalledWith(selection);
});
it('should reset favorites if selection is empty', fakeAsync(() => {
spyOn(directive['favoritesApi'], 'getFavorite').and.returnValue(Promise.resolve(null));
const selection = [
{ entry: { id: '1', name: 'name1' } }
];
let change = new SimpleChange(null, selection, true);
directive.ngOnChanges({selection: change});
tick();
expect(directive.hasFavorites()).toBe(true);
change = new SimpleChange(null, [], true);
directive.ngOnChanges({selection: change});
tick();
expect(directive.hasFavorites()).toBe(false);
}));
});
describe('markFavoritesNodes()', () => {
let favoritesApiSpy;
beforeEach(() => {
favoritesApiSpy = spyOn(directive['favoritesApi'], 'getFavorite')
.and.returnValue(Promise.resolve(null));
});
it('should check each selected node if it is a favorite', fakeAsync(() => {
const selection = [
{ entry: { id: '1', name: 'name1' } },
{ entry: { id: '2', name: 'name2' } }
];
const change = new SimpleChange(null, selection, true);
directive.ngOnChanges({selection: change});
tick();
expect(favoritesApiSpy.calls.count()).toBe(2);
}));
it('should not check processed node when another is unselected', fakeAsync(() => {
let selection = [
{ entry: { id: '1', name: 'name1' } },
{ entry: { id: '2', name: 'name2' } }
];
let change = new SimpleChange(null, selection, true);
directive.ngOnChanges({selection: change});
tick();
expect(directive.favorites.length).toBe(2);
expect(favoritesApiSpy.calls.count()).toBe(2);
favoritesApiSpy.calls.reset();
selection = [
{ entry: { id: '2', name: 'name2' } }
];
change = new SimpleChange(null, selection, true);
directive.ngOnChanges({selection: change});
tick();
expect(directive.favorites.length).toBe(1);
expect(favoritesApiSpy).not.toHaveBeenCalled();
}));
it('should not check processed nodes when another is selected', fakeAsync(() => {
let selection = [
{ entry: { id: '1', name: 'name1' } },
{ entry: { id: '2', name: 'name2' } }
];
let change = new SimpleChange(null, selection, true);
directive.ngOnChanges({selection: change});
tick();
expect(directive.favorites.length).toBe(2);
expect(favoritesApiSpy.calls.count()).toBe(2);
favoritesApiSpy.calls.reset();
selection = [
{ entry: { id: '1', name: 'name1' } },
{ entry: { id: '2', name: 'name2' } },
{ entry: { id: '3', name: 'name3' } }
];
change = new SimpleChange(null, selection, true);
directive.ngOnChanges({selection: change});
tick();
expect(directive.favorites.length).toBe(3);
expect(favoritesApiSpy.calls.count()).toBe(1);
}));
});
describe('toggleFavorite()', () => {
let removeFavoriteSpy;
let addFavoriteSpy;
beforeEach(() => {
removeFavoriteSpy = spyOn(directive['favoritesApi'], 'deleteFavorite').and.callThrough();
addFavoriteSpy = spyOn(directive['favoritesApi'], 'createFavorite').and.callThrough();
});
afterEach(() => {
removeFavoriteSpy.calls.reset();
addFavoriteSpy.calls.reset();
});
it('should not perform action if favorites collection is empty', fakeAsync(() => {
const change = new SimpleChange(null, [], true);
directive.ngOnChanges({selection: change});
tick();
directive.toggleFavorite();
expect(removeFavoriteSpy).not.toHaveBeenCalled();
expect(addFavoriteSpy).not.toHaveBeenCalled();
}));
it('should call addFavorite() if none is a favorite', () => {
addFavoriteSpy.and.returnValue(Promise.resolve());
directive.favorites = [
{ entry: { id: '1', name: 'name1', isFavorite: false } },
{ entry: { id: '2', name: 'name2', isFavorite: false } }
];
directive.toggleFavorite();
expect(addFavoriteSpy.calls.argsFor(0)[1].length).toBe(2);
});
it('should call addFavorite() on node that is not a favorite in selection', () => {
addFavoriteSpy.and.returnValue(Promise.resolve());
directive.favorites = [
{ entry: { id: '1', name: 'name1', isFile: true, isFolder: false, isFavorite: false } },
{ entry: { id: '2', name: 'name2', isFile: true, isFolder: false, isFavorite: true } }
];
directive.toggleFavorite();
const callArgs = addFavoriteSpy.calls.argsFor(0)[1];
const callParameter = callArgs[0];
expect(callArgs.length).toBe(1);
expect(callParameter.target.file.guid).toBe('1');
});
it('should call removeFavoriteSite() if all are favorites', () => {
removeFavoriteSpy.and.returnValue(Promise.resolve());
directive.favorites = [
{ entry: { id: '1', name: 'name1', isFavorite: true } },
{ entry: { id: '2', name: 'name2', isFavorite: true } }
];
directive.toggleFavorite();
expect(removeFavoriteSpy.calls.count()).toBe(2);
});
it('should emit event when removeFavoriteSite() is done', fakeAsync(() => {
removeFavoriteSpy.and.returnValue(Promise.resolve());
spyOn(directive.toggle, 'emit');
directive.favorites = [
{ entry: { id: '1', name: 'name1', isFavorite: true } }
];
directive.toggleFavorite();
tick();
expect(directive.toggle.emit).toHaveBeenCalled();
}));
it('should emit event when addFavorite() is done', fakeAsync(() => {
addFavoriteSpy.and.returnValue(Promise.resolve());
spyOn(directive.toggle, 'emit');
directive.favorites = [
{ entry: { id: '1', name: 'name1', isFavorite: false } }
];
directive.toggleFavorite();
tick();
expect(directive.toggle.emit).toHaveBeenCalled();
}));
it('should emit error event when removeFavoriteSite() fails', fakeAsync(() => {
removeFavoriteSpy.and.returnValue(Promise.reject('error'));
spyOn(directive.error, 'emit');
directive.favorites = [
{ entry: { id: '1', name: 'name1', isFavorite: true } }
];
directive.toggleFavorite();
tick();
expect(directive.error.emit).toHaveBeenCalledWith('error');
}));
it('should emit error event when addFavorite() fails', fakeAsync(() => {
addFavoriteSpy.and.returnValue(Promise.reject('error'));
spyOn(directive.error, 'emit');
directive.favorites = [
{ entry: { id: '1', name: 'name1', isFavorite: false } }
];
directive.toggleFavorite();
tick();
expect(directive.error.emit).toHaveBeenCalledWith('error');
}));
it('should set isFavorites items to false', fakeAsync(() => {
removeFavoriteSpy.and.returnValue(Promise.resolve());
directive.favorites = [
{ entry: { id: '1', name: 'name1', isFavorite: true } }
];
directive.toggleFavorite();
tick();
expect(directive.hasFavorites()).toBe(false);
}));
it('should set isFavorites items to true', fakeAsync(() => {
addFavoriteSpy.and.returnValue(Promise.resolve());
directive.favorites = [
{ entry: { id: '1', name: 'name1', isFavorite: false } }
];
directive.toggleFavorite();
tick();
expect(directive.hasFavorites()).toBe(true);
}));
});
describe('getFavorite()', () => {
it('should not hit server when using 6.x api', fakeAsync(() => {
spyOn(directive['favoritesApi'], 'getFavorite').and.callThrough();
const selection = [
{ entry: { id: '1', name: 'name1', isFavorite: true } }
];
const change = new SimpleChange(null, selection, true);
directive.ngOnChanges({selection: change});
tick();
expect(directive.favorites[0].entry.isFavorite).toBe(true);
expect(directive['favoritesApi'].getFavorite).not.toHaveBeenCalled();
}));
it('should process node as favorite', fakeAsync(() => {
spyOn(directive['favoritesApi'], 'getFavorite').and.returnValue(Promise.resolve(null));
const selection = [
{ entry: { id: '1', name: 'name1' } }
];
const change = new SimpleChange(null, selection, true);
directive.ngOnChanges({selection: change});
tick();
expect(directive.favorites[0].entry.isFavorite).toBe(true);
}));
it('should not process node as favorite', fakeAsync(() => {
spyOn(directive['favoritesApi'], 'getFavorite').and.returnValue(Promise.reject({}));
const selection = [
{ entry: { id: '1', name: 'name1' } }
];
const change = new SimpleChange(null, selection, true);
directive.ngOnChanges({selection: change});
tick();
expect(directive.favorites[0].entry.isFavorite).toBe(false);
}));
});
describe('hasFavorites()', () => {
it('should return false when favorites collection is empty', () => {
directive.favorites = [];
const hasFavorites = directive.hasFavorites();
expect(hasFavorites).toBe(false);
});
it('should return false when some are not favorite', () => {
directive.favorites = [
{ entry: { id: '1', name: 'name1', isFavorite: true } },
{ entry: { id: '2', name: 'name2', isFavorite: false } }
];
const hasFavorites = directive.hasFavorites();
expect(hasFavorites).toBe(false);
});
it('return true when all are favorite', () => {
directive.favorites = [
{ entry: { id: '1', name: 'name1', isFavorite: true } },
{ entry: { id: '2', name: 'name2', isFavorite: true } }
];
const hasFavorites = directive.hasFavorites();
expect(hasFavorites).toBe(true);
});
});
});

View File

@@ -1,202 +0,0 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* eslint-disable @angular-eslint/no-input-rename */
import { Directive, EventEmitter, HostListener, Input, OnChanges, Output } from '@angular/core';
import { FavoriteBody, NodeEntry, SharedLinkEntry, Node, SharedLink, FavoritesApi } from '@alfresco/js-api';
import { Observable, from, forkJoin, of } from 'rxjs';
import { AlfrescoApiService } from '../services/alfresco-api.service';
import { catchError, map } from 'rxjs/operators';
@Directive({
selector: '[adf-node-favorite]',
exportAs: 'adfFavorite'
})
export class NodeFavoriteDirective implements OnChanges {
favorites: any[] = [];
_favoritesApi: FavoritesApi;
get favoritesApi(): FavoritesApi {
this._favoritesApi = this._favoritesApi ?? new FavoritesApi(this.alfrescoApiService.getInstance());
return this._favoritesApi;
}
/** Array of nodes to toggle as favorites. */
@Input('adf-node-favorite')
selection: NodeEntry[] = [];
/** Emitted when the favorite setting is complete. */
@Output() toggle: EventEmitter<any> = new EventEmitter();
/** Emitted when the favorite setting fails. */
@Output() error: EventEmitter<any> = new EventEmitter();
@HostListener('click')
onClick() {
this.toggleFavorite();
}
constructor(private alfrescoApiService: AlfrescoApiService) {
}
ngOnChanges(changes) {
if (!changes.selection.currentValue.length) {
this.favorites = [];
return;
}
this.markFavoritesNodes(changes.selection.currentValue);
}
toggleFavorite() {
if (!this.favorites.length) {
return;
}
const every = this.favorites.every((selected) => selected.entry.isFavorite);
if (every) {
const batch = this.favorites.map((selected: NodeEntry | SharedLinkEntry) => {
// shared files have nodeId
const id = (selected as SharedLinkEntry).entry.nodeId || selected.entry.id;
return from(this.favoritesApi.deleteFavorite('-me-', id));
});
forkJoin(batch).subscribe(
() => {
this.favorites.map((selected) => selected.entry.isFavorite = false);
this.toggle.emit();
},
(error) => this.error.emit(error)
);
}
if (!every) {
const notFavorite = this.favorites.filter((node) => !node.entry.isFavorite);
const body: FavoriteBody[] = notFavorite.map((node) => this.createFavoriteBody(node));
from(this.favoritesApi.createFavorite('-me-', body as any))
.subscribe(
() => {
notFavorite.map((selected) => selected.entry.isFavorite = true);
this.toggle.emit();
},
(error) => this.error.emit(error)
);
}
}
markFavoritesNodes(selection: NodeEntry[]) {
if (selection.length <= this.favorites.length) {
const newFavorites = this.reduce(this.favorites, selection);
this.favorites = newFavorites;
}
const result = this.diff(selection, this.favorites);
const batch = this.getProcessBatch(result);
forkJoin(batch).subscribe((data) => {
this.favorites.push(...data);
});
}
hasFavorites(): boolean {
if (this.favorites && !this.favorites.length) {
return false;
}
return this.favorites.every((selected) => selected.entry.isFavorite);
}
private getProcessBatch(selection): any[] {
return selection.map((selected: NodeEntry) => this.getFavorite(selected));
}
private getFavorite(selected: NodeEntry | SharedLinkEntry): Observable<any> {
const node: Node | SharedLink = selected.entry;
// ACS 6.x with 'isFavorite' include
if (node && node.hasOwnProperty('isFavorite')) {
return of(selected);
}
// ACS 5.x and 6.x without 'isFavorite' include
const { name, isFile, isFolder } = node as Node;
const id = (node as SharedLink).nodeId || node.id;
const promise = this.favoritesApi.getFavorite('-me-', id);
return from(promise).pipe(
map(() => ({
entry: {
id,
isFolder,
isFile,
name,
isFavorite: true
}
})),
catchError(() => of({
entry: {
id,
isFolder,
isFile,
name,
isFavorite: false
}
}))
);
}
private createFavoriteBody(node): FavoriteBody {
const type = this.getNodeType(node);
// shared files have nodeId
const id = node.entry.nodeId || node.entry.id;
return {
target: {
[type]: {
guid: id
}
}
};
}
private getNodeType(node): string {
// shared could only be files
if (!node.entry.isFile && !node.entry.isFolder) {
return 'file';
}
return node.entry.isFile ? 'file' : 'folder';
}
private diff(list, patch): any[] {
const ids = patch.map((item) => item.entry.id);
return list.filter((item) => ids.includes(item.entry.id) ? null : item);
}
private reduce(patch, comparator): any[] {
const ids = comparator.map((item) => item.entry.id);
return patch.filter((item) => ids.includes(item.entry.id) ? item : null);
}
}

View File

@@ -1,269 +0,0 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Component, DebugElement } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { NodeRestoreDirective } from './node-restore.directive';
import { setupTestBed } from '../testing/setup-test-bed';
import { TranslationService } from '../services/translation.service';
import { CoreTestingModule } from '../testing/core.testing.module';
import { TranslateModule } from '@ngx-translate/core';
@Component({
template: `
<div [adf-restore]="selection"
(restore)="doneSpy()">
</div>`
})
class TestComponent {
selection = [];
doneSpy = jasmine.createSpy('doneSpy');
}
describe('NodeRestoreDirective', () => {
let fixture: ComponentFixture<TestComponent>;
let element: DebugElement;
let component: TestComponent;
let trashcanApi;
let directiveInstance;
let restoreNodeSpy: any;
let translationService: TranslationService;
setupTestBed({
imports: [
TranslateModule.forRoot(),
CoreTestingModule
],
declarations: [
TestComponent
]
});
beforeEach(() => {
fixture = TestBed.createComponent(TestComponent);
component = fixture.componentInstance;
element = fixture.debugElement.query(By.directive(NodeRestoreDirective));
directiveInstance = element.injector.get(NodeRestoreDirective);
trashcanApi = directiveInstance['trashcanApi'];
restoreNodeSpy = spyOn(trashcanApi, 'restoreDeletedNode').and.returnValue(Promise.resolve());
spyOn(trashcanApi, 'listDeletedNodes').and.returnValue(Promise.resolve({
list: { entries: [] }
}));
translationService = TestBed.inject(TranslationService);
spyOn(translationService, 'instant').and.callFake((key) => key);
});
it('should not restore when selection is empty', () => {
component.selection = [];
fixture.detectChanges();
element.triggerEventHandler('click', null);
expect(trashcanApi.restoreDeletedNode).not.toHaveBeenCalled();
});
it('should not restore nodes when selection has nodes without path', (done) => {
component.selection = [{ entry: { id: '1' } }];
fixture.detectChanges();
fixture.whenStable().then(() => {
element.triggerEventHandler('click', null);
expect(trashcanApi.restoreDeletedNode).not.toHaveBeenCalled();
done();
});
});
it('should call restore when selection has nodes with path', (done) => {
component.selection = [{ entry: { id: '1', path: ['somewhere-over-the-rainbow'] } }];
fixture.detectChanges();
element.triggerEventHandler('click', null);
fixture.whenStable().then(() => {
expect(trashcanApi.restoreDeletedNode).toHaveBeenCalled();
done();
});
});
describe('reset', () => {
it('should reset selection', (done) => {
component.selection = [{ entry: { id: '1', path: ['somewhere-over-the-rainbow'] } }];
directiveInstance.restore.subscribe(() => {
expect(directiveInstance.selection.length).toBe(0);
done();
});
fixture.detectChanges();
fixture.whenStable().then(() => {
expect(directiveInstance.selection.length).toBe(1);
element.triggerEventHandler('click', null);
});
});
it('should reset status', () => {
directiveInstance.restoreProcessStatus.fail = [{}];
directiveInstance.restoreProcessStatus.success = [{}];
directiveInstance.restoreProcessStatus.reset();
expect(directiveInstance.restoreProcessStatus.fail).toEqual([]);
expect(directiveInstance.restoreProcessStatus.success).toEqual([]);
});
it('should emit event on finish', (done) => {
spyOn(element.nativeElement, 'dispatchEvent');
directiveInstance.restore.subscribe(() => {
expect(component.doneSpy).toHaveBeenCalled();
done();
});
component.selection = [{ entry: { id: '1', path: ['somewhere-over-the-rainbow'] } }];
fixture.detectChanges();
element.triggerEventHandler('click', null);
});
});
describe('notification', () => {
it('should notify on multiple fails', (done) => {
const error = { message: '{ "error": {} }' };
directiveInstance.restore.subscribe((event: any) => {
expect(event.message).toEqual('CORE.RESTORE_NODE.PARTIAL_PLURAL');
done();
});
restoreNodeSpy.and.callFake((id: string) => {
if (id === '1') {
return Promise.resolve();
}
return Promise.reject(error);
});
component.selection = [
{ entry: { id: '1', name: 'name1', path: ['somewhere-over-the-rainbow'] } },
{ entry: { id: '2', name: 'name2', path: ['somewhere-over-the-rainbow'] } },
{ entry: { id: '3', name: 'name3', path: ['somewhere-over-the-rainbow'] } }
];
fixture.detectChanges();
element.triggerEventHandler('click', null);
});
it('should notify fail when restored node exist, error 409', (done) => {
const error = { message: '{ "error": { "statusCode": 409 } }' };
directiveInstance.restore.subscribe((event) => {
expect(event.message).toEqual('CORE.RESTORE_NODE.NODE_EXISTS');
done();
});
restoreNodeSpy.and.returnValue(Promise.reject(error));
component.selection = [
{ entry: { id: '1', name: 'name1', path: ['somewhere-over-the-rainbow'] } }
];
fixture.detectChanges();
element.triggerEventHandler('click', null);
});
it('should notify fail when restored node returns different statusCode', (done) => {
const error = { message: '{ "error": { "statusCode": 404 } }' };
restoreNodeSpy.and.returnValue(Promise.reject(error));
directiveInstance.restore.subscribe((event) => {
expect(event.message).toEqual('CORE.RESTORE_NODE.GENERIC');
done();
});
component.selection = [
{ entry: { id: '1', name: 'name1', path: ['somewhere-over-the-rainbow'] } }
];
fixture.detectChanges();
element.triggerEventHandler('click', null);
});
it('should notify fail when restored node location is missing', (done) => {
const error = { message: '{ "error": { } }' };
restoreNodeSpy.and.returnValue(Promise.reject(error));
directiveInstance.restore.subscribe((event: any) => {
expect(event.message).toEqual('CORE.RESTORE_NODE.LOCATION_MISSING');
done();
});
component.selection = [
{ entry: { id: '1', name: 'name1', path: ['somewhere-over-the-rainbow'] } }
];
fixture.detectChanges();
element.triggerEventHandler('click', null);
});
it('should notify success when restore multiple nodes', (done) => {
directiveInstance.restore.subscribe((event: any) => {
expect(event.message).toEqual('CORE.RESTORE_NODE.PLURAL');
done();
});
restoreNodeSpy.and.callFake(() => Promise.resolve());
component.selection = [
{ entry: { id: '1', name: 'name1', path: ['somewhere-over-the-rainbow'] } },
{ entry: { id: '2', name: 'name2', path: ['somewhere-over-the-rainbow'] } }
];
fixture.detectChanges();
element.triggerEventHandler('click', null);
});
it('should notify success on restore selected node', (done) => {
directiveInstance.restore.subscribe((event) => {
expect(event.message).toEqual('CORE.RESTORE_NODE.SINGULAR');
done();
});
component.selection = [
{ entry: { id: '1', name: 'name1', path: ['somewhere-over-the-rainbow'] } }
];
fixture.detectChanges();
element.triggerEventHandler('click', null);
});
});
});

View File

@@ -1,269 +0,0 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* eslint-disable @angular-eslint/component-selector, @angular-eslint/no-input-rename */
import { Directive, EventEmitter, HostListener, Input, Output } from '@angular/core';
import { TrashcanApi, DeletedNodeEntry, DeletedNodesPaging, PathInfoEntity } from '@alfresco/js-api';
import { Observable, forkJoin, from, of } from 'rxjs';
import { AlfrescoApiService } from '../services/alfresco-api.service';
import { TranslationService } from '../services/translation.service';
import { tap, mergeMap, map, catchError } from 'rxjs/operators';
export class RestoreMessageModel {
message: string;
path: PathInfoEntity;
action: string;
}
@Directive({
selector: '[adf-restore]'
})
export class NodeRestoreDirective {
private readonly restoreProcessStatus;
_trashcanApi: TrashcanApi;
get trashcanApi(): TrashcanApi {
this._trashcanApi = this._trashcanApi ?? new TrashcanApi(this.alfrescoApiService.getInstance());
return this._trashcanApi;
}
/** Array of deleted nodes to restore. */
@Input('adf-restore')
selection: DeletedNodeEntry[];
/** Emitted when restoration is complete. */
@Output()
restore: EventEmitter<RestoreMessageModel> = new EventEmitter();
@HostListener('click')
onClick() {
this.recover(this.selection);
}
constructor(private alfrescoApiService: AlfrescoApiService,
private translation: TranslationService) {
this.restoreProcessStatus = this.processStatus();
}
private recover(selection: any) {
if (!selection.length) {
return;
}
const nodesWithPath = this.getNodesWithPath(selection);
if (selection.length && nodesWithPath.length) {
this.restoreNodesBatch(nodesWithPath).pipe(
tap((restoredNodes) => {
const status = this.processStatus(restoredNodes);
this.restoreProcessStatus.fail.push(...status.fail);
this.restoreProcessStatus.success.push(...status.success);
}),
mergeMap(() => this.getDeletedNodes())
)
.subscribe((deletedNodesList) => {
const { entries: nodeList } = deletedNodesList.list;
const { fail: restoreErrorNodes } = this.restoreProcessStatus;
const selectedNodes = this.diff(restoreErrorNodes, selection, false);
const remainingNodes = this.diff(selectedNodes, nodeList);
if (!remainingNodes.length) {
this.notification();
} else {
this.recover(remainingNodes);
}
});
} else {
this.restoreProcessStatus.fail.push(...selection);
this.notification();
return;
}
}
private restoreNodesBatch(batch: DeletedNodeEntry[]): Observable<DeletedNodeEntry[]> {
return forkJoin(batch.map((node) => this.restoreNode(node)));
}
private getNodesWithPath(selection): DeletedNodeEntry[] {
return selection.filter((node) => node.entry.path);
}
private getDeletedNodes(): Observable<DeletedNodesPaging> {
const promise = this.trashcanApi.listDeletedNodes({ include: ['path'] });
return from(promise);
}
private restoreNode(node): Observable<any> {
const { entry } = node;
const promise = this.trashcanApi.restoreDeletedNode(entry.id);
return from(promise).pipe(
map(() => ({
status: 1,
entry
})),
catchError((error) => {
const { statusCode } = (JSON.parse(error.message)).error;
return of({
status: 0,
statusCode,
entry
});
})
);
}
private diff(selection, list, fromList = true): any {
const ids = selection.map((item) => item.entry.id);
return list.filter((item) => {
if (fromList) {
return ids.includes(item.entry.id) ? item : null;
} else {
return !ids.includes(item.entry.id) ? item : null;
}
});
}
private processStatus(data = []): any {
const status = {
fail: [],
success: [],
get someFailed() {
return !!(this.fail.length);
},
get someSucceeded() {
return !!(this.success.length);
},
get oneFailed() {
return this.fail.length === 1;
},
get oneSucceeded() {
return this.success.length === 1;
},
get allSucceeded() {
return this.someSucceeded && !this.someFailed;
},
get allFailed() {
return this.someFailed && !this.someSucceeded;
},
reset() {
this.fail = [];
this.success = [];
}
};
return data.reduce(
(acc, node) => {
if (node.status) {
acc.success.push(node);
} else {
acc.fail.push(node);
}
return acc;
},
status
);
}
private getRestoreMessage(): string | null {
const { restoreProcessStatus: status } = this;
if (status.someFailed && !status.oneFailed) {
return this.translation.instant(
'CORE.RESTORE_NODE.PARTIAL_PLURAL',
{
// eslint-disable-next-line id-blacklist
number: status.fail.length
}
);
}
if (status.oneFailed && status.fail[0].statusCode) {
if (status.fail[0].statusCode === 409) {
return this.translation.instant(
'CORE.RESTORE_NODE.NODE_EXISTS',
{
name: status.fail[0].entry.name
}
);
} else {
return this.translation.instant(
'CORE.RESTORE_NODE.GENERIC',
{
name: status.fail[0].entry.name
}
);
}
}
if (status.oneFailed && !status.fail[0].statusCode) {
return this.translation.instant(
'CORE.RESTORE_NODE.LOCATION_MISSING',
{
name: status.fail[0].entry.name
}
);
}
if (status.allSucceeded && !status.oneSucceeded) {
return this.translation.instant('CORE.RESTORE_NODE.PLURAL');
}
if (status.allSucceeded && status.oneSucceeded) {
return this.translation.instant(
'CORE.RESTORE_NODE.SINGULAR',
{
name: status.success[0].entry.name
}
);
}
return null;
}
private notification(): void {
const status = Object.assign({}, this.restoreProcessStatus);
const message = this.getRestoreMessage();
this.reset();
const action = (status.oneSucceeded && !status.someFailed) ? this.translation.instant('CORE.RESTORE_NODE.VIEW') : '';
let path;
if (status.success && status.success.length > 0) {
path = status.success[0].entry.path;
}
this.restore.emit({
message,
action,
path
});
}
private reset(): void {
this.restoreProcessStatus.reset();
this.selection = [];
}
}

View File

@@ -17,16 +17,10 @@
export * from './highlight.directive';
export * from './logout.directive';
export * from './node-delete.directive';
export * from './node-favorite.directive';
export * from './check-allowable-operation.directive';
export * from './node-restore.directive';
export * from './node-download.directive';
export * from './upload.directive';
export * from './version-compatibility.directive';
export * from './tooltip-card/tooltip-card.directive';
export * from './infinite-select-scroll.directive';
export * from './library-favorite.directive';
export * from './library-membership.directive';
export * from './directive.module';