mirror of
https://github.com/Alfresco/alfresco-ng2-components.git
synced 2025-07-24 17:32:15 +00:00
54
lib/core/directives/directive.module.ts
Normal file
54
lib/core/directives/directive.module.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright 2016 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 { CommonModule } from '@angular/common';
|
||||
import { NgModule } from '@angular/core';
|
||||
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 { NodePermissionDirective } from './node-permission.directive';
|
||||
import { NodeRestoreDirective } from './node-restore.directive';
|
||||
import { UploadDirective } from './upload.directive';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
MaterialModule
|
||||
],
|
||||
declarations: [
|
||||
HighlightDirective,
|
||||
LogoutDirective,
|
||||
NodeDeleteDirective,
|
||||
NodeFavoriteDirective,
|
||||
NodePermissionDirective,
|
||||
NodeRestoreDirective,
|
||||
UploadDirective
|
||||
],
|
||||
exports: [
|
||||
HighlightDirective,
|
||||
LogoutDirective,
|
||||
NodeDeleteDirective,
|
||||
NodeFavoriteDirective,
|
||||
NodePermissionDirective,
|
||||
NodeRestoreDirective,
|
||||
UploadDirective
|
||||
]
|
||||
})
|
||||
export class DirectiveModule {}
|
124
lib/core/directives/highlight.directive.spec.ts
Normal file
124
lib/core/directives/highlight.directive.spec.ts
Normal file
@@ -0,0 +1,124 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright 2016 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, ViewChildren } from '@angular/core';
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { HighlightTransformService } from '../services/highlight-transform.service';
|
||||
import { HighlightDirective } from './highlight.directive';
|
||||
|
||||
const template: string = `
|
||||
<div id="outerDiv1" adf-highlight adf-highlight-selector=".highlightable" adf-highlight-class="highlight-for-free-willy">
|
||||
<div id="innerDiv11" class="highlightable">Lorem ipsum salana-eyong-aysis dolor sit amet</div>
|
||||
<div id="innerDiv12">Lorem ipsum salana-eyong-aysis dolor sit amet</div>
|
||||
<div id="innerDiv13" class="highlightable">consectetur adipiscing elit</div>
|
||||
<div id="innerDiv14" class="highlightable">sed do eiusmod salana-eyong-aysis tempor incididunt</div>
|
||||
</div>
|
||||
<div id="outerDiv2" adf-highlight adf-highlight-selector=".highlightable">
|
||||
<div id="innerDiv21" class="highlightable">Lorem ipsum salana-eyong-aysis dolor sit amet</div>
|
||||
</div>`;
|
||||
|
||||
@Component({ selector: 'adf-test-component', template })
|
||||
class TestComponent {
|
||||
@ViewChildren(HighlightDirective) public hightlightDirectives;
|
||||
}
|
||||
|
||||
describe('HighlightDirective', () => {
|
||||
|
||||
let fixture: ComponentFixture<TestComponent>;
|
||||
let component: TestComponent;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [
|
||||
TestComponent
|
||||
],
|
||||
providers: [
|
||||
HighlightTransformService
|
||||
]
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(TestComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should replace the searched text with the default hightlight class in the proper element (adf-highlight-selector)', () => {
|
||||
component.hightlightDirectives.last.highlight('salana-eyong-aysis');
|
||||
fixture.detectChanges();
|
||||
|
||||
const containerElement = fixture.debugElement.query(By.css('#innerDiv21'));
|
||||
expect(containerElement).not.toBeNull();
|
||||
expect(containerElement.nativeElement.innerHTML).toBe('Lorem ipsum <span class="adf-highlight">salana-eyong-aysis</span> dolor sit amet');
|
||||
});
|
||||
|
||||
it('should replace the searched text with the default hightlight class in every proper element (highlight-for-free-willy)', () => {
|
||||
component.hightlightDirectives.first.highlight('salana-eyong-aysis');
|
||||
fixture.detectChanges();
|
||||
|
||||
const containerElement1 = fixture.debugElement.query(By.css('#innerDiv11'));
|
||||
const containerElement2 = fixture.debugElement.query(By.css('#innerDiv14'));
|
||||
expect(containerElement1).not.toBeNull();
|
||||
expect(containerElement2).not.toBeNull();
|
||||
expect(containerElement1.nativeElement.innerHTML).toBe('Lorem ipsum <span class="highlight-for-free-willy">salana-eyong-aysis</span> dolor sit amet');
|
||||
expect(containerElement2.nativeElement.innerHTML).toBe('sed do eiusmod <span class="highlight-for-free-willy">salana-eyong-aysis</span> tempor incididunt');
|
||||
});
|
||||
|
||||
it('should NOT replace the searched text in an element without the proper selector class', () => {
|
||||
component.hightlightDirectives.first.highlight('salana-eyong-aysis');
|
||||
fixture.detectChanges();
|
||||
|
||||
const containerElement1 = fixture.debugElement.query(By.css('#innerDiv12'));
|
||||
expect(containerElement1).not.toBeNull();
|
||||
expect(containerElement1.nativeElement.innerHTML).toBe('Lorem ipsum salana-eyong-aysis dolor sit amet');
|
||||
});
|
||||
|
||||
it('should NOT reinsert the same text to the innerText if there was no change at all (search string is not found)', () => {
|
||||
const highlighter = TestBed.get(HighlightTransformService);
|
||||
spyOn(highlighter, 'highlight').and.returnValue({ changed: false, text: 'Modified text' });
|
||||
component.hightlightDirectives.first.highlight('salana-eyong-aysis');
|
||||
fixture.detectChanges();
|
||||
|
||||
const containerElement = fixture.debugElement.query(By.css('#innerDiv11'));
|
||||
expect(containerElement).not.toBeNull();
|
||||
expect(containerElement.nativeElement.innerHTML).not.toContain('Modified text');
|
||||
});
|
||||
|
||||
it('should do the search only if there is a search string presented', () => {
|
||||
const highlighter = TestBed.get(HighlightTransformService);
|
||||
spyOn(highlighter, 'highlight').and.callThrough();
|
||||
component.hightlightDirectives.first.highlight('');
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(highlighter.highlight).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should do the search only if there is a node selector string presented', () => {
|
||||
const highlighter = TestBed.get(HighlightTransformService);
|
||||
spyOn(highlighter, 'highlight').and.callThrough();
|
||||
|
||||
const callback = function() {
|
||||
component.hightlightDirectives.first.highlight('raddish', '');
|
||||
fixture.detectChanges();
|
||||
};
|
||||
|
||||
expect(callback).not.toThrowError();
|
||||
expect(highlighter.highlight).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
52
lib/core/directives/highlight.directive.ts
Normal file
52
lib/core/directives/highlight.directive.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright 2016 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, ElementRef, Input, Renderer } from '@angular/core';
|
||||
import { HighlightTransformService, HightlightTransformResult } from '../services/highlight-transform.service';
|
||||
|
||||
@Directive({
|
||||
selector: '[adf-highlight]'
|
||||
})
|
||||
export class HighlightDirective {
|
||||
|
||||
@Input('adf-highlight-selector')
|
||||
selector: string = '';
|
||||
|
||||
@Input('adf-highlight')
|
||||
search: string = '';
|
||||
|
||||
@Input('adf-highlight-class')
|
||||
classToApply: string = 'adf-highlight';
|
||||
|
||||
constructor(
|
||||
private el: ElementRef,
|
||||
private renderer: Renderer,
|
||||
private highlightTransformService: HighlightTransformService) { }
|
||||
|
||||
public highlight(search = this.search, selector = this.selector, classToApply = this.classToApply) {
|
||||
if (search && selector) {
|
||||
const elements = this.el.nativeElement.querySelectorAll(selector);
|
||||
|
||||
elements.forEach((element) => {
|
||||
const result: HightlightTransformResult = this.highlightTransformService.highlight(element.innerHTML, search, classToApply);
|
||||
if (result.changed) {
|
||||
this.renderer.setElementProperty(element, 'innerHTML', result.text);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
18
lib/core/directives/index.ts
Normal file
18
lib/core/directives/index.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright 2016 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.
|
||||
*/
|
||||
|
||||
export * from './public-api';
|
78
lib/core/directives/logout.directive.spec.ts
Normal file
78
lib/core/directives/logout.directive.spec.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright 2016 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 } from '@angular/core';
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { Router } from '@angular/router';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { Observable } from 'rxjs/Rx';
|
||||
|
||||
import { AuthenticationService } from '../services';
|
||||
|
||||
describe('LogoutDirective', () => {
|
||||
|
||||
@Component({
|
||||
selector: 'adf-test-component',
|
||||
template: '<button adf-logout></button>'
|
||||
})
|
||||
class TestComponent {}
|
||||
|
||||
let fixture: ComponentFixture<TestComponent>;
|
||||
let router: Router;
|
||||
let authService: AuthenticationService;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
RouterTestingModule
|
||||
],
|
||||
declarations: [
|
||||
TestComponent
|
||||
]
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
router = TestBed.get(Router);
|
||||
authService = TestBed.get(AuthenticationService);
|
||||
fixture = TestBed.createComponent(TestComponent);
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should redirect to login on click', () => {
|
||||
spyOn(router, 'navigate').and.callThrough();
|
||||
spyOn(authService, 'logout').and.returnValue(Observable.of(true));
|
||||
|
||||
const button = fixture.nativeElement.querySelector('button');
|
||||
button.click();
|
||||
|
||||
expect(authService.logout).toHaveBeenCalled();
|
||||
expect(router.navigate).toHaveBeenCalledWith([ '/login' ]);
|
||||
});
|
||||
|
||||
it('should redirect to login even on logout error', () => {
|
||||
spyOn(router, 'navigate').and.callThrough();
|
||||
spyOn(authService, 'logout').and.returnValue(Observable.throw('err'));
|
||||
|
||||
const button = fixture.nativeElement.querySelector('button');
|
||||
button.click();
|
||||
|
||||
expect(authService.logout).toHaveBeenCalled();
|
||||
expect(router.navigate).toHaveBeenCalledWith([ '/login' ]);
|
||||
});
|
||||
|
||||
});
|
53
lib/core/directives/logout.directive.ts
Normal file
53
lib/core/directives/logout.directive.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright 2016 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, ElementRef, OnInit, Renderer2 } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { AuthenticationService } from '../services/authentication.service';
|
||||
|
||||
@Directive({
|
||||
selector: '[adf-logout]'
|
||||
})
|
||||
export class LogoutDirective implements OnInit {
|
||||
|
||||
constructor(
|
||||
private elementRef: ElementRef,
|
||||
private renderer: Renderer2,
|
||||
private router: Router,
|
||||
private auth: AuthenticationService) {
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
if (this.elementRef.nativeElement) {
|
||||
this.renderer.listen(this.elementRef.nativeElement, 'click', (evt) => {
|
||||
evt.preventDefault();
|
||||
this.logout();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
logout() {
|
||||
this.auth.logout().subscribe(
|
||||
() => this.redirectToLogin(),
|
||||
() => this.redirectToLogin()
|
||||
);
|
||||
}
|
||||
|
||||
redirectToLogin() {
|
||||
this.router.navigate(['/login']);
|
||||
}
|
||||
}
|
272
lib/core/directives/node-delete.directive.spec.ts
Normal file
272
lib/core/directives/node-delete.directive.spec.ts
Normal file
@@ -0,0 +1,272 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright 2016 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 { async, ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { AlfrescoApiService } from '../services/alfresco-api.service';
|
||||
import { NotificationService } from '../services/notification.service';
|
||||
import { NodeDeleteDirective } from './node-delete.directive';
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<div [adf-delete]="selection"
|
||||
(delete)="done()">
|
||||
</div>`
|
||||
})
|
||||
class TestComponent {
|
||||
selection = [];
|
||||
|
||||
done = jasmine.createSpy('done');
|
||||
}
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<div [adf-node-permission]="selection" [adf-delete]="selection"
|
||||
(delete)="done()">
|
||||
</div>`
|
||||
})
|
||||
class TestWithPermissionsComponent {
|
||||
selection = [];
|
||||
|
||||
done = jasmine.createSpy('done');
|
||||
}
|
||||
|
||||
describe('NodeDeleteDirective', () => {
|
||||
let fixture: ComponentFixture<TestComponent>;
|
||||
let fixtureWithPermissions: ComponentFixture<TestWithPermissionsComponent>;
|
||||
let element: DebugElement;
|
||||
let elementWithPermissions: DebugElement;
|
||||
let component: TestComponent;
|
||||
let componentWithPermissions: TestWithPermissionsComponent;
|
||||
let alfrescoApi: AlfrescoApiService;
|
||||
let notification: NotificationService;
|
||||
let nodeApi;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
|
||||
declarations: [
|
||||
TestComponent,
|
||||
TestWithPermissionsComponent
|
||||
]
|
||||
})
|
||||
.compileComponents()
|
||||
.then(() => {
|
||||
fixture = TestBed.createComponent(TestComponent);
|
||||
fixtureWithPermissions = TestBed.createComponent(TestWithPermissionsComponent);
|
||||
component = fixture.componentInstance;
|
||||
componentWithPermissions = fixtureWithPermissions.componentInstance;
|
||||
element = fixture.debugElement.query(By.directive(NodeDeleteDirective));
|
||||
elementWithPermissions = fixtureWithPermissions.debugElement.query(By.directive(NodeDeleteDirective));
|
||||
|
||||
alfrescoApi = TestBed.get(AlfrescoApiService);
|
||||
nodeApi = alfrescoApi.getInstance().nodes;
|
||||
notification = TestBed.get(NotificationService);
|
||||
});
|
||||
}));
|
||||
|
||||
describe('Delete', () => {
|
||||
beforeEach(() => {
|
||||
spyOn(notification, 'openSnackMessage');
|
||||
});
|
||||
|
||||
it('should do nothing if selection is empty', () => {
|
||||
spyOn(nodeApi, 'deleteNode');
|
||||
component.selection = [];
|
||||
|
||||
fixture.detectChanges();
|
||||
element.triggerEventHandler('click', null);
|
||||
|
||||
expect(nodeApi.deleteNode).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should process node successfully', fakeAsync(() => {
|
||||
spyOn(nodeApi, 'deleteNode').and.returnValue(Promise.resolve());
|
||||
|
||||
component.selection = <any> [{ entry: { id: '1', name: 'name1' } }];
|
||||
|
||||
fixture.detectChanges();
|
||||
element.triggerEventHandler('click', null);
|
||||
tick();
|
||||
|
||||
expect(notification.openSnackMessage).toHaveBeenCalledWith(
|
||||
'CORE.DELETE_NODE.SINGULAR'
|
||||
);
|
||||
}));
|
||||
|
||||
it('should notify failed node deletion', fakeAsync(() => {
|
||||
spyOn(nodeApi, 'deleteNode').and.returnValue(Promise.reject('error'));
|
||||
|
||||
component.selection = [{ entry: { id: '1', name: 'name1' } }];
|
||||
|
||||
fixture.detectChanges();
|
||||
element.triggerEventHandler('click', null);
|
||||
tick();
|
||||
|
||||
expect(notification.openSnackMessage).toHaveBeenCalledWith(
|
||||
'CORE.DELETE_NODE.ERROR_SINGULAR'
|
||||
);
|
||||
}));
|
||||
|
||||
it('should notify nodes deletion', fakeAsync(() => {
|
||||
spyOn(nodeApi, 'deleteNode').and.returnValue(Promise.resolve());
|
||||
|
||||
component.selection = [
|
||||
{ entry: { id: '1', name: 'name1' } },
|
||||
{ entry: { id: '2', name: 'name2' } }
|
||||
];
|
||||
|
||||
fixture.detectChanges();
|
||||
element.triggerEventHandler('click', null);
|
||||
tick();
|
||||
|
||||
expect(notification.openSnackMessage).toHaveBeenCalledWith(
|
||||
'CORE.DELETE_NODE.PLURAL'
|
||||
);
|
||||
}));
|
||||
|
||||
it('should notify failed nodes deletion', fakeAsync(() => {
|
||||
spyOn(nodeApi, 'deleteNode').and.returnValue(Promise.reject('error'));
|
||||
|
||||
component.selection = [
|
||||
{ entry: { id: '1', name: 'name1' } },
|
||||
{ entry: { id: '2', name: 'name2' } }
|
||||
];
|
||||
|
||||
fixture.detectChanges();
|
||||
element.triggerEventHandler('click', null);
|
||||
tick();
|
||||
|
||||
expect(notification.openSnackMessage).toHaveBeenCalledWith(
|
||||
'CORE.DELETE_NODE.ERROR_PLURAL'
|
||||
);
|
||||
}));
|
||||
|
||||
it('should notify partial deletion when only one node is successful', fakeAsync(() => {
|
||||
spyOn(nodeApi, 'deleteNode').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();
|
||||
element.triggerEventHandler('click', null);
|
||||
tick();
|
||||
|
||||
expect(notification.openSnackMessage).toHaveBeenCalledWith(
|
||||
'CORE.DELETE_NODE.PARTIAL_SINGULAR'
|
||||
);
|
||||
}));
|
||||
|
||||
it('should notify partial deletion when some nodes are successful', fakeAsync(() => {
|
||||
spyOn(nodeApi, 'deleteNode').and.callFake((id) => {
|
||||
if (id === '1') {
|
||||
return Promise.reject(null);
|
||||
}
|
||||
|
||||
if (id === '2') {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
if (id === '3') {
|
||||
return Promise.resolve();
|
||||
}
|
||||
});
|
||||
|
||||
component.selection = [
|
||||
{ entry: { id: '1', name: 'name1' } },
|
||||
{ entry: { id: '2', name: 'name2' } },
|
||||
{ entry: { id: '3', name: 'name3' } }
|
||||
];
|
||||
|
||||
fixture.detectChanges();
|
||||
element.triggerEventHandler('click', null);
|
||||
tick();
|
||||
|
||||
expect(notification.openSnackMessage).toHaveBeenCalledWith(
|
||||
'CORE.DELETE_NODE.PARTIAL_PLURAL'
|
||||
);
|
||||
}));
|
||||
|
||||
it('should emit event when delete is done', fakeAsync(() => {
|
||||
component.done.calls.reset();
|
||||
spyOn(nodeApi, 'deleteNode').and.returnValue(Promise.resolve());
|
||||
|
||||
component.selection = <any> [{ entry: { id: '1', name: 'name1' } }];
|
||||
|
||||
fixture.detectChanges();
|
||||
element.triggerEventHandler('click', null);
|
||||
tick();
|
||||
|
||||
expect(component.done).toHaveBeenCalled();
|
||||
}));
|
||||
|
||||
it('should disable the button if no node are selected', fakeAsync(() => {
|
||||
component.selection = [];
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(element.nativeElement.disabled).toEqual(true);
|
||||
}));
|
||||
|
||||
it('should disable the button if selected node is null', fakeAsync(() => {
|
||||
component.selection = null;
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(element.nativeElement.disabled).toEqual(true);
|
||||
}));
|
||||
|
||||
it('should enable the button if nodes are selected', fakeAsync(() => {
|
||||
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-node-permission is present', fakeAsync(() => {
|
||||
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);
|
||||
}));
|
||||
|
||||
});
|
||||
});
|
217
lib/core/directives/node-delete.directive.ts
Normal file
217
lib/core/directives/node-delete.directive.ts
Normal file
@@ -0,0 +1,217 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright 2016 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, ElementRef, EventEmitter, HostListener, Input, OnChanges, Output } from '@angular/core';
|
||||
import { MinimalNodeEntity, MinimalNodeEntryEntity } from 'alfresco-js-api';
|
||||
import { Observable } from 'rxjs/Rx';
|
||||
import { AlfrescoApiService } from '../services/alfresco-api.service';
|
||||
import { NotificationService } from '../services/notification.service';
|
||||
import { TranslationService } from '../services/translation.service';
|
||||
|
||||
interface ProcessedNodeData {
|
||||
entry: MinimalNodeEntryEntity;
|
||||
status: number;
|
||||
}
|
||||
|
||||
interface ProcessStatus {
|
||||
success: ProcessedNodeData[];
|
||||
failed: ProcessedNodeData[];
|
||||
|
||||
someFailed();
|
||||
|
||||
someSucceeded();
|
||||
|
||||
oneFailed();
|
||||
|
||||
oneSucceeded();
|
||||
|
||||
allSucceeded();
|
||||
|
||||
allFailed();
|
||||
}
|
||||
|
||||
@Directive({
|
||||
selector: '[adf-delete]'
|
||||
})
|
||||
export class NodeDeleteDirective implements OnChanges {
|
||||
@Input('adf-delete')
|
||||
selection: MinimalNodeEntity[];
|
||||
|
||||
@Input()
|
||||
permanent: boolean = false;
|
||||
|
||||
@Output()
|
||||
delete: EventEmitter<any> = new EventEmitter();
|
||||
|
||||
@HostListener('click')
|
||||
onClick() {
|
||||
this.process(this.selection);
|
||||
}
|
||||
|
||||
constructor(private notification: NotificationService,
|
||||
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-node-permission')) {
|
||||
this.setDisableAttribute(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private setDisableAttribute(disable: boolean) {
|
||||
this.elementRef.nativeElement.disabled = disable;
|
||||
}
|
||||
|
||||
private process(selection: MinimalNodeEntity[]) {
|
||||
if (!selection.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const batch = this.getDeleteNodesBatch(selection);
|
||||
|
||||
Observable.forkJoin(...batch)
|
||||
.subscribe((data: ProcessedNodeData[]) => {
|
||||
const processedItems: ProcessStatus = this.processStatus(data);
|
||||
|
||||
this.notify(processedItems);
|
||||
|
||||
if (processedItems.someSucceeded) {
|
||||
this.delete.emit();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private getDeleteNodesBatch(selection: MinimalNodeEntity[]): Observable<ProcessedNodeData>[] {
|
||||
return selection.map((node) => this.deleteNode(node));
|
||||
}
|
||||
|
||||
private deleteNode(node: MinimalNodeEntity): Observable<ProcessedNodeData> {
|
||||
const id = (<any> node.entry).nodeId || node.entry.id;
|
||||
|
||||
const promise = this.alfrescoApiService.getInstance().nodes.deleteNode(id, {permanent: this.permanent});
|
||||
|
||||
return Observable.fromPromise(promise)
|
||||
.map(() => ({
|
||||
entry: node.entry,
|
||||
status: 1
|
||||
}))
|
||||
.catch((error: any) => {
|
||||
return Observable.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 notify(status) {
|
||||
this.getMessage(status).subscribe((message) => this.notification.openSnackMessage(message));
|
||||
}
|
||||
|
||||
private getMessage(status): Observable<string> {
|
||||
if (status.allFailed && !status.oneFailed) {
|
||||
return this.translation.get(
|
||||
'CORE.DELETE_NODE.ERROR_PLURAL',
|
||||
{number: status.failed.length}
|
||||
);
|
||||
}
|
||||
|
||||
if (status.allSucceeded && !status.oneSucceeded) {
|
||||
return this.translation.get(
|
||||
'CORE.DELETE_NODE.PLURAL',
|
||||
{number: status.success.length}
|
||||
);
|
||||
}
|
||||
|
||||
if (status.someFailed && status.someSucceeded && !status.oneSucceeded) {
|
||||
return this.translation.get(
|
||||
'CORE.DELETE_NODE.PARTIAL_PLURAL',
|
||||
{
|
||||
success: status.success.length,
|
||||
failed: status.failed.length
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if (status.someFailed && status.oneSucceeded) {
|
||||
return this.translation.get(
|
||||
'CORE.DELETE_NODE.PARTIAL_SINGULAR',
|
||||
{
|
||||
success: status.success.length,
|
||||
failed: status.failed.length
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if (status.oneFailed && !status.someSucceeded) {
|
||||
return this.translation.get(
|
||||
'CORE.DELETE_NODE.ERROR_SINGULAR',
|
||||
{name: status.failed[0].entry.name}
|
||||
);
|
||||
}
|
||||
|
||||
if (status.oneSucceeded && !status.someFailed) {
|
||||
return this.translation.get(
|
||||
'CORE.DELETE_NODE.SINGULAR',
|
||||
{name: status.success[0].entry.name}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
373
lib/core/directives/node-favorite.directive.spec.ts
Normal file
373
lib/core/directives/node-favorite.directive.spec.ts
Normal file
@@ -0,0 +1,373 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright 2016 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 { async, ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { AlfrescoApiService } from '../services/alfresco-api.service';
|
||||
import { NodeFavoriteDirective } from './node-favorite.directive';
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<div [adf-node-favorite]="selection"
|
||||
(toggle)="done()">
|
||||
</div>`
|
||||
})
|
||||
class TestComponent {
|
||||
selection;
|
||||
|
||||
done = jasmine.createSpy('done');
|
||||
}
|
||||
|
||||
describe('NodeFavoriteDirective', () => {
|
||||
let component: TestComponent;
|
||||
let fixture: ComponentFixture<TestComponent>;
|
||||
let element: DebugElement;
|
||||
let directiveInstance;
|
||||
let apiService;
|
||||
let favoritesApi;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
|
||||
declarations: [
|
||||
TestComponent
|
||||
]
|
||||
})
|
||||
.compileComponents()
|
||||
.then(() => {
|
||||
fixture = TestBed.createComponent(TestComponent);
|
||||
component = fixture.componentInstance;
|
||||
element = fixture.debugElement.query(By.directive(NodeFavoriteDirective));
|
||||
directiveInstance = element.injector.get(NodeFavoriteDirective);
|
||||
|
||||
apiService = TestBed.get(AlfrescoApiService);
|
||||
favoritesApi = apiService.getInstance().core.favoritesApi;
|
||||
});
|
||||
}));
|
||||
|
||||
describe('selection input change event', () => {
|
||||
it('should not call markFavoritesNodes() if input list is empty', () => {
|
||||
spyOn(directiveInstance, 'markFavoritesNodes');
|
||||
|
||||
component.selection = [];
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(directiveInstance.markFavoritesNodes).not.toHaveBeenCalledWith();
|
||||
});
|
||||
|
||||
it('should call markFavoritesNodes() on input change', () => {
|
||||
spyOn(directiveInstance, 'markFavoritesNodes');
|
||||
|
||||
component.selection = [{ entry: { id: '1', name: 'name1' } }];
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(directiveInstance.markFavoritesNodes).toHaveBeenCalledWith(component.selection);
|
||||
|
||||
component.selection = [
|
||||
{ entry: { id: '1', name: 'name1' } },
|
||||
{ entry: { id: '1', name: 'name1' } }
|
||||
];
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(directiveInstance.markFavoritesNodes).toHaveBeenCalledWith(component.selection);
|
||||
});
|
||||
|
||||
it('should reset favorites if selection is empty', fakeAsync(() => {
|
||||
spyOn(favoritesApi, 'getFavorite').and.returnValue(Promise.resolve());
|
||||
|
||||
component.selection = [
|
||||
{ entry: { id: '1', name: 'name1' } }
|
||||
];
|
||||
|
||||
fixture.detectChanges();
|
||||
tick();
|
||||
|
||||
expect(directiveInstance.hasFavorites()).toBe(true);
|
||||
|
||||
component.selection = [];
|
||||
fixture.detectChanges();
|
||||
tick();
|
||||
|
||||
expect(directiveInstance.hasFavorites()).toBe(false);
|
||||
}));
|
||||
});
|
||||
|
||||
describe('markFavoritesNodes()', () => {
|
||||
let favoritesApiSpy;
|
||||
|
||||
beforeEach(() => {
|
||||
favoritesApiSpy = spyOn(favoritesApi, 'getFavorite');
|
||||
});
|
||||
|
||||
it('should check each selected node if it is a favorite', fakeAsync(() => {
|
||||
favoritesApiSpy.and.returnValue(Promise.resolve());
|
||||
|
||||
component.selection = [
|
||||
{ entry: { id: '1', name: 'name1' } },
|
||||
{ entry: { id: '2', name: 'name2' } }
|
||||
];
|
||||
|
||||
fixture.detectChanges();
|
||||
tick();
|
||||
|
||||
expect(favoritesApiSpy.calls.count()).toBe(2);
|
||||
}));
|
||||
|
||||
it('should not check processed node when another is unselected', fakeAsync(() => {
|
||||
favoritesApiSpy.and.returnValue(Promise.resolve());
|
||||
|
||||
component.selection = [
|
||||
{ entry: { id: '1', name: 'name1' } },
|
||||
{ entry: { id: '2', name: 'name2' } }
|
||||
];
|
||||
|
||||
fixture.detectChanges();
|
||||
tick();
|
||||
|
||||
expect(directiveInstance.favorites.length).toBe(2);
|
||||
expect(favoritesApiSpy.calls.count()).toBe(2);
|
||||
|
||||
favoritesApiSpy.calls.reset();
|
||||
|
||||
component.selection = [
|
||||
{ entry: { id: '2', name: 'name2' } }
|
||||
];
|
||||
|
||||
fixture.detectChanges();
|
||||
tick();
|
||||
|
||||
expect(directiveInstance.favorites.length).toBe(1);
|
||||
expect(favoritesApiSpy).not.toHaveBeenCalled();
|
||||
}));
|
||||
|
||||
it('should not check processed nodes when another is selected', fakeAsync(() => {
|
||||
favoritesApiSpy.and.returnValue(Promise.resolve());
|
||||
|
||||
component.selection = [
|
||||
{ entry: { id: '1', name: 'name1' } },
|
||||
{ entry: { id: '2', name: 'name2' } }
|
||||
];
|
||||
|
||||
fixture.detectChanges();
|
||||
tick();
|
||||
|
||||
expect(directiveInstance.favorites.length).toBe(2);
|
||||
expect(favoritesApiSpy.calls.count()).toBe(2);
|
||||
|
||||
favoritesApiSpy.calls.reset();
|
||||
|
||||
component.selection = [
|
||||
{ entry: { id: '1', name: 'name1' } },
|
||||
{ entry: { id: '2', name: 'name2' } },
|
||||
{ entry: { id: '3', name: 'name3' } }
|
||||
];
|
||||
|
||||
fixture.detectChanges();
|
||||
tick();
|
||||
|
||||
expect(directiveInstance.favorites.length).toBe(3);
|
||||
expect(favoritesApiSpy.calls.count()).toBe(1);
|
||||
}));
|
||||
});
|
||||
|
||||
describe('toggleFavorite()', () => {
|
||||
let removeFavoriteSpy;
|
||||
let addFavoriteSpy;
|
||||
|
||||
beforeEach(() => {
|
||||
removeFavoriteSpy = spyOn(favoritesApi, 'removeFavoriteSite');
|
||||
addFavoriteSpy = spyOn(favoritesApi, 'addFavorite');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
removeFavoriteSpy.calls.reset();
|
||||
addFavoriteSpy.calls.reset();
|
||||
});
|
||||
|
||||
it('should not perform action if favorites collection is empty', () => {
|
||||
component.selection = [];
|
||||
|
||||
fixture.detectChanges();
|
||||
element.triggerEventHandler('click', null);
|
||||
|
||||
expect(removeFavoriteSpy).not.toHaveBeenCalled();
|
||||
expect(addFavoriteSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should call addFavorite() if none is a favorite', fakeAsync(() => {
|
||||
addFavoriteSpy.and.returnValue(Promise.resolve());
|
||||
|
||||
directiveInstance.favorites = [
|
||||
{ entry: { id: '1', name: 'name1', isFavorite: false } },
|
||||
{ entry: { id: '2', name: 'name2', isFavorite: false } }
|
||||
];
|
||||
|
||||
element.triggerEventHandler('click', null);
|
||||
tick();
|
||||
|
||||
expect(addFavoriteSpy.calls.argsFor(0)[1].length).toBe(2);
|
||||
}));
|
||||
|
||||
it('should call addFavorite() on node that is not a favorite in selection', fakeAsync(() => {
|
||||
addFavoriteSpy.and.returnValue(Promise.resolve());
|
||||
|
||||
directiveInstance.favorites = [
|
||||
{ entry: { id: '1', name: 'name1', isFile: true, isFolder: false, isFavorite: false } },
|
||||
{ entry: { id: '2', name: 'name2', isFile: true, isFolder: false, isFavorite: true } }
|
||||
];
|
||||
|
||||
element.triggerEventHandler('click', null);
|
||||
tick();
|
||||
|
||||
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', fakeAsync(() => {
|
||||
removeFavoriteSpy.and.returnValue(Promise.resolve());
|
||||
|
||||
directiveInstance.favorites = [
|
||||
{ entry: { id: '1', name: 'name1', isFavorite: true } },
|
||||
{ entry: { id: '2', name: 'name2', isFavorite: true } }
|
||||
];
|
||||
|
||||
element.triggerEventHandler('click', null);
|
||||
tick();
|
||||
|
||||
expect(removeFavoriteSpy.calls.count()).toBe(2);
|
||||
}));
|
||||
|
||||
it('should emit event when removeFavoriteSite() is done', fakeAsync(() => {
|
||||
removeFavoriteSpy.and.returnValue(Promise.resolve());
|
||||
|
||||
directiveInstance.favorites = [
|
||||
{ entry: { id: '1', name: 'name1', isFavorite: true } }
|
||||
];
|
||||
|
||||
element.triggerEventHandler('click', null);
|
||||
tick();
|
||||
|
||||
expect(component.done).toHaveBeenCalled();
|
||||
}));
|
||||
|
||||
it('should emit event when addFavorite() is done', fakeAsync(() => {
|
||||
addFavoriteSpy.and.returnValue(Promise.resolve());
|
||||
|
||||
directiveInstance.favorites = [
|
||||
{ entry: { id: '1', name: 'name1', isFavorite: false } }
|
||||
];
|
||||
|
||||
element.triggerEventHandler('click', null);
|
||||
tick();
|
||||
|
||||
expect(component.done).toHaveBeenCalled();
|
||||
}));
|
||||
|
||||
it('should set isFavorites items to false', fakeAsync(() => {
|
||||
removeFavoriteSpy.and.returnValue(Promise.resolve());
|
||||
|
||||
directiveInstance.favorites = [
|
||||
{ entry: { id: '1', name: 'name1', isFavorite: true } }
|
||||
];
|
||||
|
||||
element.triggerEventHandler('click', null);
|
||||
tick();
|
||||
|
||||
expect(directiveInstance.hasFavorites()).toBe(false);
|
||||
}));
|
||||
|
||||
it('should set isFavorites items to true', fakeAsync(() => {
|
||||
addFavoriteSpy.and.returnValue(Promise.resolve());
|
||||
|
||||
directiveInstance.favorites = [
|
||||
{ entry: { id: '1', name: 'name1', isFavorite: false } }
|
||||
];
|
||||
|
||||
element.triggerEventHandler('click', null);
|
||||
tick();
|
||||
|
||||
expect(directiveInstance.hasFavorites()).toBe(true);
|
||||
}));
|
||||
});
|
||||
|
||||
describe('getFavorite()', () => {
|
||||
it('should process node as favorite', fakeAsync(() => {
|
||||
spyOn(favoritesApi, 'getFavorite').and.returnValue(Promise.resolve());
|
||||
|
||||
component.selection = [
|
||||
{ entry: { id: '1', name: 'name1' } }
|
||||
];
|
||||
|
||||
fixture.detectChanges();
|
||||
tick();
|
||||
|
||||
expect(directiveInstance.favorites[0].entry.isFavorite).toBe(true);
|
||||
}));
|
||||
|
||||
it('should not process node as favorite', fakeAsync(() => {
|
||||
spyOn(favoritesApi, 'getFavorite').and.returnValue(Promise.reject(null));
|
||||
|
||||
component.selection = [
|
||||
{ entry: { id: '1', name: 'name1' } }
|
||||
];
|
||||
|
||||
fixture.detectChanges();
|
||||
tick();
|
||||
|
||||
expect(directiveInstance.favorites[0].entry.isFavorite).toBe(false);
|
||||
}));
|
||||
});
|
||||
|
||||
describe('hasFavorites()', () => {
|
||||
it('should return false when favorites collection is empty', () => {
|
||||
directiveInstance.favorites = [];
|
||||
|
||||
const hasFavorites = directiveInstance.hasFavorites();
|
||||
|
||||
expect(hasFavorites).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when some are not favorite', () => {
|
||||
directiveInstance.favorites = [
|
||||
{ entry: { id: '1', name: 'name1', isFavorite: true } },
|
||||
{ entry: { id: '2', name: 'name2', isFavorite: false } }
|
||||
];
|
||||
|
||||
const hasFavorites = directiveInstance.hasFavorites();
|
||||
|
||||
expect(hasFavorites).toBe(false);
|
||||
});
|
||||
|
||||
it('return true when all are favorite', () => {
|
||||
directiveInstance.favorites = [
|
||||
{ entry: { id: '1', name: 'name1', isFavorite: true } },
|
||||
{ entry: { id: '2', name: 'name2', isFavorite: true } }
|
||||
];
|
||||
|
||||
const hasFavorites = directiveInstance.hasFavorites();
|
||||
|
||||
expect(hasFavorites).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
174
lib/core/directives/node-favorite.directive.ts
Normal file
174
lib/core/directives/node-favorite.directive.ts
Normal file
@@ -0,0 +1,174 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright 2016 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 } from '@angular/core';
|
||||
import { FavoriteBody, MinimalNodeEntity } from 'alfresco-js-api';
|
||||
import { Observable } from 'rxjs/Rx';
|
||||
import { AlfrescoApiService } from '../services/alfresco-api.service';
|
||||
|
||||
@Directive({
|
||||
selector: '[adf-node-favorite]',
|
||||
exportAs: 'adfFavorite'
|
||||
})
|
||||
export class NodeFavoriteDirective implements OnChanges {
|
||||
private favorites: any[] = [];
|
||||
|
||||
@Input('adf-node-favorite')
|
||||
selection: MinimalNodeEntity[] = [];
|
||||
|
||||
@Output() toggle: 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) => {
|
||||
// shared files have nodeId
|
||||
const id = selected.entry.nodeId || selected.entry.id;
|
||||
|
||||
return Observable.fromPromise(this.alfrescoApiService.getInstance().core.favoritesApi.removeFavoriteSite('-me-', id));
|
||||
});
|
||||
|
||||
Observable.forkJoin(batch).subscribe(() => {
|
||||
this.favorites.map(selected => selected.entry.isFavorite = false);
|
||||
this.toggle.emit();
|
||||
});
|
||||
}
|
||||
|
||||
if (!every) {
|
||||
const notFavorite = this.favorites.filter((node) => !node.entry.isFavorite);
|
||||
const body: FavoriteBody[] = notFavorite.map((node) => this.createFavoriteBody(node));
|
||||
|
||||
Observable.fromPromise(this.alfrescoApiService.getInstance().core.favoritesApi.addFavorite('-me-', <any> body))
|
||||
.subscribe(() => {
|
||||
notFavorite.map(selected => selected.entry.isFavorite = true);
|
||||
this.toggle.emit();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
markFavoritesNodes(selection: MinimalNodeEntity[]) {
|
||||
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);
|
||||
|
||||
Observable.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: MinimalNodeEntity) => this.getFavorite(selected));
|
||||
}
|
||||
|
||||
private getFavorite(selected: MinimalNodeEntity): Observable<any> {
|
||||
const { name, isFile, isFolder } = selected.entry;
|
||||
// shared files have nodeId
|
||||
const id = (<any> selected).entry.nodeId || selected.entry.id;
|
||||
|
||||
const promise = this.alfrescoApiService.getInstance()
|
||||
.core.favoritesApi.getFavorite('-me-', id);
|
||||
|
||||
return Observable.from(promise)
|
||||
.map(() => ({
|
||||
entry: {
|
||||
id,
|
||||
isFolder,
|
||||
isFile,
|
||||
name,
|
||||
isFavorite: true
|
||||
}
|
||||
}))
|
||||
.catch(() => {
|
||||
return Observable.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);
|
||||
}
|
||||
}
|
108
lib/core/directives/node-permission.directive.spec.ts
Normal file
108
lib/core/directives/node-permission.directive.spec.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright 2016 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 { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { ContentService } from './../services/content.service';
|
||||
import { NodePermissionDirective } from './node-permission.directive';
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<div [adf-node-permission]="'delete'" [adf-nodes]="selection">
|
||||
</div>`
|
||||
})
|
||||
class TestComponent {
|
||||
selection = [];
|
||||
disabled = false;
|
||||
done = jasmine.createSpy('done');
|
||||
}
|
||||
|
||||
describe('NodePermissionDirective', () => {
|
||||
let fixture: ComponentFixture<TestComponent>;
|
||||
let element: DebugElement;
|
||||
let component: TestComponent;
|
||||
let alfrescoContentService: ContentService;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
|
||||
declarations: [
|
||||
TestComponent
|
||||
]
|
||||
})
|
||||
.compileComponents()
|
||||
.then(() => {
|
||||
fixture = TestBed.createComponent(TestComponent);
|
||||
alfrescoContentService = TestBed.get(ContentService);
|
||||
component = fixture.componentInstance;
|
||||
element = fixture.debugElement.query(By.directive(NodePermissionDirective));
|
||||
});
|
||||
}));
|
||||
|
||||
it('Should be disabled if no nodes are passed', () => {
|
||||
component.selection = undefined;
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
component.selection = null;
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(element.nativeElement.disabled).toEqual(true);
|
||||
});
|
||||
|
||||
it('Should be disabled if nodes is an empty array', () => {
|
||||
component.selection = null;
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
component.selection = [];
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(element.nativeElement.disabled).toEqual(true);
|
||||
});
|
||||
|
||||
it('enables element when all nodes have expected permission', () => {
|
||||
spyOn(alfrescoContentService, 'hasPermission').and.returnValue(true);
|
||||
|
||||
component.selection = null;
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
component.selection = <any> [{entry: {id: '1', name: 'name1'}}];
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(element.nativeElement.disabled).toEqual(false);
|
||||
});
|
||||
|
||||
it('disables element when one of the nodes have no permission', () => {
|
||||
spyOn(alfrescoContentService, 'hasPermission').and.returnValue(false);
|
||||
|
||||
component.selection = null;
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
component.selection = <any> [{entry: {id: '1', name: 'name1'}}];
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(element.nativeElement.disabled).toEqual(true);
|
||||
});
|
||||
});
|
77
lib/core/directives/node-permission.directive.ts
Normal file
77
lib/core/directives/node-permission.directive.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright 2016 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, ElementRef, Input, OnChanges, SimpleChanges } from '@angular/core';
|
||||
import { MinimalNodeEntity } from 'alfresco-js-api';
|
||||
import { ContentService } from './../services/content.service';
|
||||
|
||||
export interface NodePermissionSubject {
|
||||
disabled: boolean;
|
||||
}
|
||||
|
||||
@Directive({
|
||||
selector: '[adf-node-permission]'
|
||||
})
|
||||
export class NodePermissionDirective implements OnChanges {
|
||||
|
||||
@Input('adf-node-permission')
|
||||
permission: string = null;
|
||||
|
||||
@Input('adf-nodes')
|
||||
nodes: MinimalNodeEntity[] = [];
|
||||
|
||||
constructor(private elementRef: ElementRef,
|
||||
private contentService: ContentService) {
|
||||
}
|
||||
|
||||
ngOnChanges(changes: SimpleChanges) {
|
||||
if (changes.nodes && !changes.nodes.firstChange) {
|
||||
this.updateElement();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates disabled state for the decorated element
|
||||
*
|
||||
* @memberof NodePermissionDirective
|
||||
*/
|
||||
updateElement(): void {
|
||||
let hasPermission = this.hasPermission(this.nodes, this.permission);
|
||||
this.setDisableAttribute(!hasPermission);
|
||||
}
|
||||
|
||||
private setDisableAttribute(disable: boolean) {
|
||||
this.elementRef.nativeElement.disabled = disable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether all nodes have a particular permission
|
||||
*
|
||||
* @param {MinimalNodeEntity[]} nodes Node collection to check
|
||||
* @param {string} permission Permission to check for each node
|
||||
* @returns {boolean} True if all nodes have provided permission, otherwise False
|
||||
* @memberof NodePermissionDirective
|
||||
*/
|
||||
hasPermission(nodes: MinimalNodeEntity[], permission: string): boolean {
|
||||
if (nodes && nodes.length > 0) {
|
||||
return nodes.every(node => this.contentService.hasPermission(node.entry, permission));
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
97
lib/core/directives/node-permission.md
Normal file
97
lib/core/directives/node-permission.md
Normal file
@@ -0,0 +1,97 @@
|
||||
# Node Permission Directive
|
||||
|
||||
<!-- markdown-toc start - Don't edit this section. npm run toc to generate it-->
|
||||
|
||||
<!-- toc -->
|
||||
|
||||
- [Properties](#properties)
|
||||
- [HTML element example](#html-element-example)
|
||||
- [Angular component example](#angular-component-example)
|
||||
* [Implementing the NodePermissionSubject interface](#implementing-the-nodepermissionsubject-interface)
|
||||
* [Defining your components as an EXTENDIBLE_COMPONENT parent component](#defining-your-components-as-an-extendible_component-parent-component)
|
||||
|
||||
<!-- tocstop -->
|
||||
|
||||
<!-- markdown-toc end -->
|
||||
|
||||
The `NodePermissionDirective` allows you to disable an HTML element or Angular component
|
||||
by taking a collection of the `MinimalNodeEntity` instances and checking the particular permission.
|
||||
|
||||
The decorated element will be disabled if:
|
||||
|
||||
- there are no nodes in the collection
|
||||
- at least one of the nodes has no expected permission
|
||||
|
||||
## Properties
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| adf-node-permission | [Permissions](https://github.com/Alfresco/alfresco-ng2-components/blob/master/ng2-components/ng2-alfresco-core/src/models/permissions.enum.ts) | null | Node permission to check (create, delete, update, updatePermissions, !create, !delete, !update, !updatePermissions)|
|
||||
| adf-nodes | MinimalNodeEntity[] | [] | Nodes to check permission for |
|
||||
|
||||
## HTML element example
|
||||
|
||||
The best example to show `NodePermissionDirective` in action is by binding DocumentList selection property to a toolbar button.
|
||||
|
||||
For example the "Delete" button should be disabled if no selection is present or if user has no rights to delete at least one node in the selection.
|
||||
|
||||
```html
|
||||
<adf-toolbar title="toolbar example">
|
||||
<button md-icon-button
|
||||
adf-node-permission="delete"
|
||||
[adf-nodes]="documentList.selection">
|
||||
<md-icon>delete</md-icon>
|
||||
</button>
|
||||
</adf-toolbar>
|
||||
|
||||
<adf-document-list #documentList ...>
|
||||
...
|
||||
</adf-document-list>
|
||||
```
|
||||
|
||||
The button will become disabled by default, and is going to change its state once user selects/unselects one or multiple documents that current user has permission to delete.
|
||||
|
||||
## Angular component example
|
||||
|
||||
You can apply the directive on any angular component which implements the NodePermissionSubject interface. The upload drag area component can be a good candidate, since this one implements that interface. Applying the directive on an angular component is pretty much the same as applying it on an html element.
|
||||
|
||||
```html
|
||||
<alfresco-upload-drag-area
|
||||
[parentId]="..."
|
||||
[versioning]="..."
|
||||
[adf-node-permission]="'create'"
|
||||
[adf-nodes]="getCurrentDocumentListNode()">
|
||||
...
|
||||
</alfresco-upload-drag-area>
|
||||
```
|
||||
|
||||
When designing a component you want to work this directive with, you have two important things to care about.
|
||||
|
||||
### Implementing the NodePermissionSubject interface
|
||||
|
||||
The component has to implement the NodePermissionSubject interface which basically means it has to have a boolean **disabled** property. This is the property which will be set by the directive.
|
||||
|
||||
```js
|
||||
import { NodePermissionSubject } from 'ng2-alfresco-core';
|
||||
|
||||
@Component({...})
|
||||
export class UploadDragAreaComponent implements NodePermissionSubject {
|
||||
public disabled: boolean = false;
|
||||
}
|
||||
```
|
||||
|
||||
### Defining your components as an EXTENDIBLE_COMPONENT parent component
|
||||
|
||||
The directive will look up the component in the dependency injection tree, up to at most one step above the current DI level (@Host). Because of this, you have to provide your component with forward referencing as the EXTENDIBLE_COMPONENT.
|
||||
|
||||
```js
|
||||
import { EXTENDIBLE_COMPONENT } from 'ng2-alfresco-core';
|
||||
|
||||
@Component({
|
||||
...
|
||||
providers: [
|
||||
{ provide: EXTENDIBLE_COMPONENT, useExisting: forwardRef(() => UploadDragAreaComponent)}
|
||||
]
|
||||
})
|
||||
export class UploadDragAreaComponent implements NodePermissionSubject { ... }
|
||||
```
|
338
lib/core/directives/node-restore.directive.spec.ts
Normal file
338
lib/core/directives/node-restore.directive.spec.ts
Normal file
@@ -0,0 +1,338 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright 2016 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 { async, ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { Router } from '@angular/router';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { Observable } from 'rxjs/Rx';
|
||||
import { TranslationService } from '../services';
|
||||
import { AlfrescoApiService } from '../services/alfresco-api.service';
|
||||
import { NotificationService } from '../services/notification.service';
|
||||
import { NodeRestoreDirective } from './node-restore.directive';
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<div [adf-restore]="selection"
|
||||
(restore)="done()">
|
||||
</div>`
|
||||
})
|
||||
class TestComponent {
|
||||
selection = [];
|
||||
|
||||
done = jasmine.createSpy('done');
|
||||
}
|
||||
|
||||
describe('NodeRestoreDirective', () => {
|
||||
let fixture: ComponentFixture<TestComponent>;
|
||||
let element: DebugElement;
|
||||
let component: TestComponent;
|
||||
let alfrescoService: AlfrescoApiService;
|
||||
let translation: TranslationService;
|
||||
let notification: NotificationService;
|
||||
let router: Router;
|
||||
let nodesService;
|
||||
let coreApi;
|
||||
let directiveInstance;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
RouterTestingModule
|
||||
],
|
||||
declarations: [
|
||||
TestComponent
|
||||
]
|
||||
})
|
||||
.compileComponents()
|
||||
.then(() => {
|
||||
fixture = TestBed.createComponent(TestComponent);
|
||||
component = fixture.componentInstance;
|
||||
element = fixture.debugElement.query(By.directive(NodeRestoreDirective));
|
||||
directiveInstance = element.injector.get(NodeRestoreDirective);
|
||||
|
||||
alfrescoService = TestBed.get(AlfrescoApiService);
|
||||
nodesService = alfrescoService.getInstance().nodes;
|
||||
coreApi = alfrescoService.getInstance().core;
|
||||
translation = TestBed.get(TranslationService);
|
||||
notification = TestBed.get(NotificationService);
|
||||
router = TestBed.get(Router);
|
||||
});
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
spyOn(translation, 'get').and.returnValue(Observable.of('message'));
|
||||
});
|
||||
|
||||
it('should not restore when selection is empty', () => {
|
||||
spyOn(nodesService, 'restoreNode');
|
||||
|
||||
component.selection = [];
|
||||
|
||||
fixture.detectChanges();
|
||||
element.triggerEventHandler('click', null);
|
||||
|
||||
expect(nodesService.restoreNode).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not restore nodes when selection has nodes without path', () => {
|
||||
spyOn(nodesService, 'restoreNode');
|
||||
|
||||
component.selection = [ { entry: { id: '1' } } ];
|
||||
|
||||
fixture.detectChanges();
|
||||
element.triggerEventHandler('click', null);
|
||||
|
||||
expect(nodesService.restoreNode).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should call restore when selection has nodes with path', fakeAsync(() => {
|
||||
spyOn(directiveInstance, 'restoreNotification').and.callFake(() => null);
|
||||
spyOn(nodesService, 'restoreNode').and.returnValue(Promise.resolve());
|
||||
spyOn(coreApi.nodesApi, 'getDeletedNodes').and.returnValue(Promise.resolve({
|
||||
list: { entries: [] }
|
||||
}));
|
||||
|
||||
component.selection = [{ entry: { id: '1', path: ['somewhere-over-the-rainbow'] } }];
|
||||
|
||||
fixture.detectChanges();
|
||||
element.triggerEventHandler('click', null);
|
||||
tick();
|
||||
|
||||
expect(nodesService.restoreNode).toHaveBeenCalled();
|
||||
}));
|
||||
|
||||
describe('refresh()', () => {
|
||||
it('should reset selection', fakeAsync(() => {
|
||||
spyOn(directiveInstance, 'restoreNotification').and.callFake(() => null);
|
||||
spyOn(nodesService, 'restoreNode').and.returnValue(Promise.resolve());
|
||||
spyOn(coreApi.nodesApi, 'getDeletedNodes').and.returnValue(Promise.resolve({
|
||||
list: { entries: [] }
|
||||
}));
|
||||
|
||||
component.selection = [{ entry: { id: '1', path: ['somewhere-over-the-rainbow'] } }];
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(directiveInstance.selection.length).toBe(1);
|
||||
|
||||
element.triggerEventHandler('click', null);
|
||||
tick();
|
||||
|
||||
expect(directiveInstance.selection.length).toBe(0);
|
||||
}));
|
||||
|
||||
it('should reset status', fakeAsync(() => {
|
||||
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', fakeAsync(() => {
|
||||
spyOn(directiveInstance, 'restoreNotification').and.callFake(() => null);
|
||||
spyOn(nodesService, 'restoreNode').and.returnValue(Promise.resolve());
|
||||
spyOn(coreApi.nodesApi, 'getDeletedNodes').and.returnValue(Promise.resolve({
|
||||
list: { entries: [] }
|
||||
}));
|
||||
spyOn(element.nativeElement, 'dispatchEvent');
|
||||
|
||||
component.selection = [{ entry: { id: '1', path: ['somewhere-over-the-rainbow'] } }];
|
||||
|
||||
fixture.detectChanges();
|
||||
element.triggerEventHandler('click', null);
|
||||
tick();
|
||||
|
||||
expect(component.done).toHaveBeenCalled();
|
||||
}));
|
||||
});
|
||||
|
||||
describe('notification', () => {
|
||||
beforeEach(() => {
|
||||
spyOn(coreApi.nodesApi, 'getDeletedNodes').and.returnValue(Promise.resolve({
|
||||
list: { entries: [] }
|
||||
}));
|
||||
});
|
||||
|
||||
it('should notify on multiple fails', fakeAsync(() => {
|
||||
const error = { message: '{ "error": {} }' };
|
||||
|
||||
spyOn(notification, 'openSnackMessageAction').and.returnValue({ onAction: () => Observable.throw(null) });
|
||||
|
||||
spyOn(nodesService, 'restoreNode').and.callFake((id) => {
|
||||
if (id === '1') {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
if (id === '2') {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
if (id === '3') {
|
||||
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);
|
||||
tick();
|
||||
|
||||
expect(translation.get).toHaveBeenCalledWith(
|
||||
'CORE.RESTORE_NODE.PARTIAL_PLURAL',
|
||||
{ number: 2 }
|
||||
);
|
||||
}));
|
||||
|
||||
it('should notify fail when restored node exist, error 409', fakeAsync(() => {
|
||||
const error = { message: '{ "error": { "statusCode": 409 } }' };
|
||||
|
||||
spyOn(notification, 'openSnackMessageAction').and.returnValue({ onAction: () => Observable.throw(null) });
|
||||
spyOn(nodesService, 'restoreNode').and.returnValue(Promise.reject(error));
|
||||
|
||||
component.selection = [
|
||||
{ entry: { id: '1', name: 'name1', path: ['somewhere-over-the-rainbow'] } }
|
||||
];
|
||||
|
||||
fixture.detectChanges();
|
||||
element.triggerEventHandler('click', null);
|
||||
tick();
|
||||
|
||||
expect(translation.get).toHaveBeenCalledWith(
|
||||
'CORE.RESTORE_NODE.NODE_EXISTS',
|
||||
{ name: 'name1' }
|
||||
);
|
||||
}));
|
||||
|
||||
it('should notify fail when restored node returns different statusCode', fakeAsync(() => {
|
||||
const error = { message: '{ "error": { "statusCode": 404 } }' };
|
||||
|
||||
spyOn(notification, 'openSnackMessageAction').and.returnValue({ onAction: () => Observable.throw(null) });
|
||||
spyOn(nodesService, 'restoreNode').and.returnValue(Promise.reject(error));
|
||||
|
||||
component.selection = [
|
||||
{ entry: { id: '1', name: 'name1', path: ['somewhere-over-the-rainbow'] } }
|
||||
];
|
||||
|
||||
fixture.detectChanges();
|
||||
element.triggerEventHandler('click', null);
|
||||
tick();
|
||||
|
||||
expect(translation.get).toHaveBeenCalledWith(
|
||||
'CORE.RESTORE_NODE.GENERIC',
|
||||
{ name: 'name1' }
|
||||
);
|
||||
}));
|
||||
|
||||
it('should notify fail when restored node location is missing', fakeAsync(() => {
|
||||
const error = { message: '{ "error": { } }' };
|
||||
|
||||
spyOn(notification, 'openSnackMessageAction').and.returnValue({ onAction: () => Observable.throw(null) });
|
||||
spyOn(nodesService, 'restoreNode').and.returnValue(Promise.reject(error));
|
||||
|
||||
component.selection = [
|
||||
{ entry: { id: '1', name: 'name1', path: ['somewhere-over-the-rainbow'] } }
|
||||
];
|
||||
|
||||
fixture.detectChanges();
|
||||
element.triggerEventHandler('click', null);
|
||||
tick();
|
||||
|
||||
expect(translation.get).toHaveBeenCalledWith(
|
||||
'CORE.RESTORE_NODE.LOCATION_MISSING',
|
||||
{ name: 'name1' }
|
||||
);
|
||||
}));
|
||||
|
||||
it('should notify success when restore multiple nodes', fakeAsync(() => {
|
||||
spyOn(notification, 'openSnackMessageAction').and.returnValue({ onAction: () => Observable.throw(null) });
|
||||
spyOn(nodesService, 'restoreNode').and.callFake((id) => {
|
||||
if (id === '1') {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
if (id === '2') {
|
||||
return 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);
|
||||
tick();
|
||||
|
||||
expect(translation.get).toHaveBeenCalledWith(
|
||||
'CORE.RESTORE_NODE.PLURAL'
|
||||
);
|
||||
}));
|
||||
|
||||
it('should notify success on restore selected node', fakeAsync(() => {
|
||||
spyOn(notification, 'openSnackMessageAction').and.returnValue({ onAction: () => Observable.throw(null) });
|
||||
spyOn(nodesService, 'restoreNode').and.returnValue(Promise.resolve());
|
||||
|
||||
component.selection = [
|
||||
{ entry: { id: '1', name: 'name1', path: ['somewhere-over-the-rainbow'] } }
|
||||
];
|
||||
|
||||
fixture.detectChanges();
|
||||
element.triggerEventHandler('click', null);
|
||||
tick();
|
||||
|
||||
expect(translation.get).toHaveBeenCalledWith(
|
||||
'CORE.RESTORE_NODE.SINGULAR',
|
||||
{ name: 'name1' }
|
||||
);
|
||||
}));
|
||||
|
||||
it('should navigate to restored node location onAction', fakeAsync(() => {
|
||||
spyOn(router, 'navigate');
|
||||
spyOn(nodesService, 'restoreNode').and.returnValue(Promise.resolve());
|
||||
spyOn(notification, 'openSnackMessageAction').and.returnValue({ onAction: () => Observable.of({}) });
|
||||
|
||||
component.selection = [
|
||||
{
|
||||
entry: {
|
||||
id: '1',
|
||||
name: 'name1',
|
||||
path: {
|
||||
elements: ['somewhere-over-the-rainbow']
|
||||
}
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
fixture.detectChanges();
|
||||
element.triggerEventHandler('click', null);
|
||||
tick();
|
||||
|
||||
expect(router.navigate).toHaveBeenCalled();
|
||||
}));
|
||||
});
|
||||
});
|
264
lib/core/directives/node-restore.directive.ts
Normal file
264
lib/core/directives/node-restore.directive.ts
Normal file
@@ -0,0 +1,264 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright 2016 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, Output } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { DeletedNodeEntry, DeletedNodesPaging, PathInfoEntity } from 'alfresco-js-api';
|
||||
import { Observable } from 'rxjs/Rx';
|
||||
import { AlfrescoApiService } from '../services/alfresco-api.service';
|
||||
import { NotificationService } from '../services/notification.service';
|
||||
import { TranslationService } from '../services/translation.service';
|
||||
|
||||
@Directive({
|
||||
selector: '[adf-restore]'
|
||||
})
|
||||
export class NodeRestoreDirective {
|
||||
private restoreProcessStatus;
|
||||
|
||||
@Input('adf-restore')
|
||||
selection: DeletedNodeEntry[];
|
||||
|
||||
@Input()
|
||||
location: string = '';
|
||||
|
||||
@Output()
|
||||
restore: EventEmitter<any> = new EventEmitter();
|
||||
|
||||
@HostListener('click')
|
||||
onClick() {
|
||||
this.recover(this.selection);
|
||||
}
|
||||
|
||||
constructor(
|
||||
private alfrescoApiService: AlfrescoApiService,
|
||||
private translation: TranslationService,
|
||||
private router: Router,
|
||||
private notification: NotificationService
|
||||
) {
|
||||
this.restoreProcessStatus = this.processStatus();
|
||||
}
|
||||
|
||||
private recover(selection: any) {
|
||||
if (!selection.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nodesWithPath = this.getNodesWithPath(selection);
|
||||
|
||||
if (selection.length && !nodesWithPath.length) {
|
||||
this.restoreProcessStatus.fail.push(...selection);
|
||||
this.restoreNotification();
|
||||
this.refresh();
|
||||
return;
|
||||
}
|
||||
|
||||
this.restoreNodesBatch(nodesWithPath)
|
||||
.do((restoredNodes) => {
|
||||
const status = this.processStatus(restoredNodes);
|
||||
|
||||
this.restoreProcessStatus.fail.push(...status.fail);
|
||||
this.restoreProcessStatus.success.push(...status.success);
|
||||
})
|
||||
.flatMap(() => this.getDeletedNodes())
|
||||
.subscribe(
|
||||
(deletedNodesList: any) => {
|
||||
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.restoreNotification();
|
||||
this.refresh();
|
||||
} else {
|
||||
this.recover(remainingNodes);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private restoreNodesBatch(batch: DeletedNodeEntry[]): Observable<DeletedNodeEntry[]> {
|
||||
return Observable.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.alfrescoApiService.getInstance()
|
||||
.core.nodesApi.getDeletedNodes({ include: [ 'path' ] });
|
||||
|
||||
return Observable.from(promise);
|
||||
}
|
||||
|
||||
private restoreNode(node): Observable<any> {
|
||||
const { entry } = node;
|
||||
|
||||
const promise = this.alfrescoApiService.getInstance().nodes.restoreNode(entry.id);
|
||||
|
||||
return Observable.from(promise)
|
||||
.map(() => ({
|
||||
status: 1,
|
||||
entry
|
||||
}))
|
||||
.catch((error) => {
|
||||
const { statusCode } = (JSON.parse(error.message)).error;
|
||||
|
||||
return Observable.of({
|
||||
status: 0,
|
||||
statusCode,
|
||||
entry
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private navigateLocation(path: PathInfoEntity) {
|
||||
const parent = path.elements[path.elements.length - 1];
|
||||
|
||||
this.router.navigate([ this.location, parent.id ]);
|
||||
}
|
||||
|
||||
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(): Observable<string|any> {
|
||||
const { restoreProcessStatus: status } = this;
|
||||
|
||||
if (status.someFailed && !status.oneFailed) {
|
||||
return this.translation.get(
|
||||
'CORE.RESTORE_NODE.PARTIAL_PLURAL',
|
||||
{
|
||||
number: status.fail.length
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if (status.oneFailed && status.fail[0].statusCode) {
|
||||
if (status.fail[0].statusCode === 409) {
|
||||
return this.translation.get(
|
||||
'CORE.RESTORE_NODE.NODE_EXISTS',
|
||||
{
|
||||
name: status.fail[0].entry.name
|
||||
}
|
||||
);
|
||||
} else {
|
||||
return this.translation.get(
|
||||
'CORE.RESTORE_NODE.GENERIC',
|
||||
{
|
||||
name: status.fail[0].entry.name
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (status.oneFailed && !status.fail[0].statusCode) {
|
||||
return this.translation.get(
|
||||
'CORE.RESTORE_NODE.LOCATION_MISSING',
|
||||
{
|
||||
name: status.fail[0].entry.name
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if (status.allSucceeded && !status.oneSucceeded) {
|
||||
return this.translation.get('CORE.RESTORE_NODE.PLURAL');
|
||||
}
|
||||
|
||||
if (status.allSucceeded && status.oneSucceeded) {
|
||||
return this.translation.get(
|
||||
'CORE.RESTORE_NODE.SINGULAR',
|
||||
{
|
||||
name: status.success[0].entry.name
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private restoreNotification(): void {
|
||||
const status = Object.assign({}, this.restoreProcessStatus);
|
||||
|
||||
Observable.zip(
|
||||
this.getRestoreMessage(),
|
||||
this.translation.get('CORE.RESTORE_NODE.VIEW')
|
||||
).subscribe((messages) => {
|
||||
const [ message, actionLabel ] = messages;
|
||||
const action = (status.oneSucceeded && !status.someFailed) ? actionLabel : '';
|
||||
|
||||
this.notification.openSnackMessageAction(message, action)
|
||||
.onAction()
|
||||
.subscribe(() => this.navigateLocation(status.success[0].entry.path));
|
||||
});
|
||||
}
|
||||
|
||||
private refresh(): void {
|
||||
this.restoreProcessStatus.reset();
|
||||
this.selection = [];
|
||||
this.restore.emit();
|
||||
}
|
||||
}
|
26
lib/core/directives/public-api.ts
Normal file
26
lib/core/directives/public-api.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright 2016 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.
|
||||
*/
|
||||
|
||||
export * from './highlight.directive';
|
||||
export * from './logout.directive';
|
||||
export * from './node-delete.directive';
|
||||
export * from './node-favorite.directive';
|
||||
export * from './node-permission.directive';
|
||||
export * from './node-restore.directive';
|
||||
export * from './upload.directive';
|
||||
|
||||
export * from './directive.module';
|
152
lib/core/directives/upload.directive.spec.ts
Normal file
152
lib/core/directives/upload.directive.spec.ts
Normal file
@@ -0,0 +1,152 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright 2016 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 { ElementRef } from '@angular/core';
|
||||
import { FileInfo } from './../utils/file-utils';
|
||||
import { UploadDirective } from './upload.directive';
|
||||
|
||||
describe('UploadDirective', () => {
|
||||
|
||||
let directive: UploadDirective;
|
||||
let nativeElement: any;
|
||||
|
||||
beforeEach(() => {
|
||||
nativeElement = {
|
||||
classList: jasmine.createSpyObj('classList', ['add', 'remove']),
|
||||
dispatchEvent: () => {}
|
||||
};
|
||||
directive = new UploadDirective(new ElementRef(nativeElement), null, null);
|
||||
});
|
||||
|
||||
it('should be enabled by default', () => {
|
||||
expect(directive.enabled).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should update drag status on dragenter', () => {
|
||||
expect(directive.isDragging).toBeFalsy();
|
||||
directive.enabled = true;
|
||||
directive.onDragEnter(null);
|
||||
expect(directive.isDragging).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should not update drag status on dragenter when disabled', () => {
|
||||
expect(directive.isDragging).toBeFalsy();
|
||||
directive.enabled = false;
|
||||
directive.onDragEnter(null);
|
||||
expect(directive.isDragging).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should update drag status on dragover', () => {
|
||||
expect(directive.isDragging).toBeFalsy();
|
||||
directive.enabled = true;
|
||||
directive.onDragOver(new CustomEvent('dragover'));
|
||||
expect(directive.isDragging).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should prevent default event on dragover', () => {
|
||||
let event = new Event('dom-event');
|
||||
spyOn(event, 'preventDefault').and.stub();
|
||||
directive.enabled = true;
|
||||
directive.onDragOver(event);
|
||||
expect(event.preventDefault).toHaveBeenCalled();
|
||||
expect(directive.isDragging).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should not update drag status on dragover when disabled', () => {
|
||||
expect(directive.isDragging).toBeFalsy();
|
||||
directive.enabled = false;
|
||||
directive.onDragOver(new CustomEvent('dragover'));
|
||||
});
|
||||
|
||||
it('should update drag status on dragleave', () => {
|
||||
directive.enabled = true;
|
||||
directive.isDragging = true;
|
||||
directive.onDragLeave(null);
|
||||
expect(directive.isDragging).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should not update drag status on dragleave when disabled', () => {
|
||||
directive.enabled = false;
|
||||
directive.isDragging = true;
|
||||
directive.onDragLeave(null);
|
||||
expect(directive.isDragging).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should prevent default event on drop', () => {
|
||||
directive.enabled = true;
|
||||
let event = jasmine.createSpyObj('event', ['preventDefault', 'stopPropagation']);
|
||||
directive.onDrop(<DragEvent> event);
|
||||
expect(event.preventDefault).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should stop default event propagation on drop', () => {
|
||||
directive.enabled = true;
|
||||
let event = jasmine.createSpyObj('event', ['preventDefault', 'stopPropagation']);
|
||||
directive.onDrop(<DragEvent> event);
|
||||
expect(event.stopPropagation).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not prevent default event on drop when disabled', () => {
|
||||
directive.enabled = false;
|
||||
let event = jasmine.createSpyObj('event', ['preventDefault', 'stopPropagation']);
|
||||
directive.onDrop(<DragEvent> event);
|
||||
expect(event.preventDefault).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should raise upload-files event on files drop', (done) => {
|
||||
directive.enabled = true;
|
||||
let event = jasmine.createSpyObj('event', ['preventDefault', 'stopPropagation']);
|
||||
spyOn(directive, 'getDataTransfer').and.returnValue({});
|
||||
spyOn(directive, 'getFilesDropped').and.returnValue(Promise.resolve([
|
||||
<FileInfo> {},
|
||||
<FileInfo> {}
|
||||
]));
|
||||
spyOn(nativeElement, 'dispatchEvent').and.callFake(_ => {
|
||||
done();
|
||||
});
|
||||
directive.onDrop(event);
|
||||
});
|
||||
|
||||
it('should provide dropped files in upload-files event', (done) => {
|
||||
directive.enabled = true;
|
||||
let files = [
|
||||
<FileInfo> {}
|
||||
];
|
||||
let event = jasmine.createSpyObj('event', ['preventDefault', 'stopPropagation']);
|
||||
spyOn(directive, 'getDataTransfer').and.returnValue({});
|
||||
spyOn(directive, 'getFilesDropped').and.returnValue(Promise.resolve(files));
|
||||
|
||||
spyOn(nativeElement, 'dispatchEvent').and.callFake(e => {
|
||||
expect(e.detail.files.length).toBe(1);
|
||||
expect(e.detail.files[0]).toBe(files[0]);
|
||||
done();
|
||||
});
|
||||
directive.onDrop(event);
|
||||
});
|
||||
|
||||
it('should reset input value after file upload', () => {
|
||||
directive.enabled = true;
|
||||
directive.mode = ['click'];
|
||||
const files = [
|
||||
<FileInfo> {}
|
||||
];
|
||||
const event = {'currentTarget': {'files': files}, 'target': {'value': '/testpath/document.pdf'}};
|
||||
|
||||
directive.onSelectFiles(event);
|
||||
expect(event.target.value).toBe('');
|
||||
});
|
||||
});
|
250
lib/core/directives/upload.directive.ts
Normal file
250
lib/core/directives/upload.directive.ts
Normal file
@@ -0,0 +1,250 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright 2016 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, ElementRef, HostListener, Input, NgZone, OnDestroy, OnInit, Renderer } from '@angular/core';
|
||||
import { FileInfo, FileUtils } from '../utils/file-utils';
|
||||
|
||||
@Directive({
|
||||
selector: '[adf-upload]'
|
||||
})
|
||||
export class UploadDirective implements OnInit, OnDestroy {
|
||||
|
||||
@Input('adf-upload')
|
||||
enabled: boolean = true;
|
||||
|
||||
@Input('adf-upload-data')
|
||||
data: any;
|
||||
|
||||
@Input()
|
||||
mode: string[] = ['drop']; // click|drop
|
||||
|
||||
@Input()
|
||||
multiple: boolean;
|
||||
|
||||
@Input()
|
||||
accept: string;
|
||||
|
||||
@Input()
|
||||
directory: boolean;
|
||||
|
||||
isDragging: boolean = false;
|
||||
|
||||
private cssClassName: string = 'adf-upload__dragging';
|
||||
private upload: HTMLInputElement;
|
||||
private element: HTMLElement;
|
||||
|
||||
constructor(private el: ElementRef, private renderer: Renderer, private ngZone: NgZone) {
|
||||
this.element = el.nativeElement;
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
if (this.isClickMode() && this.renderer) {
|
||||
this.upload = this.renderer.createElement(this.el.nativeElement.parentNode, 'input') as HTMLInputElement;
|
||||
this.upload.type = 'file';
|
||||
this.upload.style.display = 'none';
|
||||
this.upload.addEventListener('change', e => this.onSelectFiles(e));
|
||||
|
||||
if (this.multiple) {
|
||||
this.upload.setAttribute('multiple', '');
|
||||
}
|
||||
|
||||
if (this.accept) {
|
||||
this.upload.setAttribute('accept', this.accept);
|
||||
}
|
||||
|
||||
if (this.directory) {
|
||||
this.upload.setAttribute('webkitdirectory', '');
|
||||
}
|
||||
}
|
||||
|
||||
if (this.isDropMode()) {
|
||||
this.ngZone.runOutsideAngular(() => {
|
||||
this.element.addEventListener('dragenter', this.onDragEnter.bind(this));
|
||||
this.element.addEventListener('dragover', this.onDragOver.bind(this));
|
||||
this.element.addEventListener('dragleave', this.onDragLeave.bind(this));
|
||||
this.element.addEventListener('drop', this.onDrop.bind(this));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.element.removeEventListener('dragenter', this.onDragEnter);
|
||||
this.element.removeEventListener('dragover', this.onDragOver);
|
||||
this.element.removeEventListener('dragleave', this.onDragLeave);
|
||||
this.element.removeEventListener('drop', this.onDrop);
|
||||
}
|
||||
|
||||
@HostListener('click', ['$event'])
|
||||
onClick(event: Event) {
|
||||
if (this.isClickMode() && this.upload) {
|
||||
event.preventDefault();
|
||||
this.upload.click();
|
||||
}
|
||||
}
|
||||
|
||||
onDragEnter(event: Event) {
|
||||
if (this.isDropMode()) {
|
||||
this.element.classList.add(this.cssClassName);
|
||||
this.isDragging = true;
|
||||
}
|
||||
}
|
||||
|
||||
onDragOver(event: Event) {
|
||||
event.preventDefault();
|
||||
if (this.isDropMode()) {
|
||||
this.element.classList.add(this.cssClassName);
|
||||
this.isDragging = true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
onDragLeave(event) {
|
||||
if (this.isDropMode()) {
|
||||
this.element.classList.remove(this.cssClassName);
|
||||
this.isDragging = false;
|
||||
}
|
||||
}
|
||||
|
||||
onDrop(event: Event) {
|
||||
if (this.isDropMode()) {
|
||||
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
|
||||
this.element.classList.remove(this.cssClassName);
|
||||
this.isDragging = false;
|
||||
|
||||
const dataTranfer = this.getDataTransfer(event);
|
||||
if (dataTranfer) {
|
||||
this.getFilesDropped(dataTranfer).then(files => {
|
||||
this.onUploadFiles(files);
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
onUploadFiles(files: FileInfo[]) {
|
||||
if (this.enabled && files.length > 0) {
|
||||
let e = new CustomEvent('upload-files', {
|
||||
detail: {
|
||||
sender: this,
|
||||
data: this.data,
|
||||
files: files
|
||||
},
|
||||
bubbles: true
|
||||
});
|
||||
|
||||
this.el.nativeElement.dispatchEvent(e);
|
||||
}
|
||||
}
|
||||
|
||||
protected hasMode(mode: string): boolean {
|
||||
return this.enabled && mode && this.mode && this.mode.indexOf(mode) > -1;
|
||||
}
|
||||
|
||||
protected isDropMode(): boolean {
|
||||
return this.hasMode('drop');
|
||||
}
|
||||
|
||||
protected isClickMode(): boolean {
|
||||
return this.hasMode('click');
|
||||
}
|
||||
|
||||
getDataTransfer(event: Event | any): DataTransfer {
|
||||
if (event && event.dataTransfer) {
|
||||
return event.dataTransfer;
|
||||
}
|
||||
if (event && event.originalEvent && event.originalEvent.dataTransfer) {
|
||||
return event.originalEvent.dataTransfer;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract files from the DataTransfer object used to hold the data that is being dragged during a drag and drop operation.
|
||||
* @param dataTransfer DataTransfer object
|
||||
*/
|
||||
getFilesDropped(dataTransfer: DataTransfer): Promise<FileInfo[]> {
|
||||
return new Promise(resolve => {
|
||||
const iterations = [];
|
||||
|
||||
if (dataTransfer) {
|
||||
const items = dataTransfer.items;
|
||||
if (items) {
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
if (typeof items[i].webkitGetAsEntry !== 'undefined') {
|
||||
let item = items[i].webkitGetAsEntry();
|
||||
if (item) {
|
||||
if (item.isFile) {
|
||||
iterations.push(Promise.resolve(<FileInfo> {
|
||||
entry: item,
|
||||
file: items[i].getAsFile(),
|
||||
relativeFolder: '/'
|
||||
}));
|
||||
} else if (item.isDirectory) {
|
||||
iterations.push(new Promise(resolveFolder => {
|
||||
FileUtils.flattern(item).then(files => resolveFolder(files));
|
||||
}));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
iterations.push(Promise.resolve(<FileInfo> {
|
||||
entry: null,
|
||||
file: items[i].getAsFile(),
|
||||
relativeFolder: '/'
|
||||
}));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// safari or FF
|
||||
let files = FileUtils
|
||||
.toFileArray(dataTransfer.files)
|
||||
.map(file => <FileInfo> {
|
||||
entry: null,
|
||||
file: file,
|
||||
relativeFolder: '/'
|
||||
});
|
||||
|
||||
iterations.push(Promise.resolve(files));
|
||||
}
|
||||
}
|
||||
|
||||
Promise.all(iterations).then(result => {
|
||||
resolve(result.reduce((a, b) => a.concat(b), []));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked when user selects files or folders by means of File Dialog
|
||||
* @param e DOM event
|
||||
*/
|
||||
onSelectFiles(e: any): void {
|
||||
if (this.isClickMode()) {
|
||||
const input = (<HTMLInputElement> e.currentTarget);
|
||||
const files = FileUtils.toFileArray(input.files);
|
||||
this.onUploadFiles(files.map(file => <FileInfo> {
|
||||
entry: null,
|
||||
file: file,
|
||||
relativeFolder: '/'
|
||||
}));
|
||||
e.target.value = '';
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user