mirror of
https://github.com/Alfresco/alfresco-ng2-components.git
synced 2025-07-24 17:32:15 +00:00
[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:
9
lib/core/src/lib/breaking-changes.md
Normal file
9
lib/core/src/lib/breaking-changes.md
Normal 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
|
||||
|
||||
|
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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 {}
|
||||
|
@@ -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();
|
||||
});
|
||||
});
|
@@ -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));
|
||||
}
|
||||
}
|
@@ -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
|
||||
});
|
||||
});
|
||||
}));
|
||||
});
|
||||
});
|
@@ -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));
|
||||
}
|
||||
}
|
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -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);
|
||||
|
||||
});
|
||||
});
|
||||
});
|
@@ -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 = [];
|
||||
}
|
||||
}
|
@@ -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';
|
||||
|
Reference in New Issue
Block a user