[ACA-20] Add/Remove favorite library (#804)

* custom add favorite library

* addFavoriteLibrary component

* tests

* update doc

* add content event

* reload on remove favorite library

* consistency

* app.selection.library over app.navigation.isLibraries

* fix duplicate id name
This commit is contained in:
Cilibiu Bogdan
2018-11-16 12:46:36 +02:00
committed by Suzana Dirla
parent 2ac59bd278
commit da41834524
11 changed files with 481 additions and 16 deletions

View File

@@ -11,11 +11,13 @@ The components are used to create custom:
- toolbar buttons
- menu items
| Key | Type | Description |
| -- | -- | -- |
| app.layout.main | LayoutComponent | Main application layout with the menu bar, navigation sidebar and main content area to project your components. |
| app.toolbar.toggleInfoDrawer | ToggleInfoDrawerComponent | The toolbar button component that toggles Info Drawer for the selection. |
| app.toolbar.toggleFavorite | ToggleFavoriteComponent | The toolbar button component that toggles Favorite state for the selection. |
| Key | Type | Description |
| --------------------------------- | ------------------------------ | --------------------------------------------------------------------------------------------------------------- |
| app.layout.main | LayoutComponent | Main application layout with the menu bar, navigation sidebar and main content area to project your components. |
| app.toolbar.toggleInfoDrawer | ToggleInfoDrawerComponent | The toolbar button component that toggles Info Drawer for the selection. |
| app.toolbar.toggleFavorite | ToggleFavoriteComponent | The toolbar button component that toggles Favorite state for the selection. |
| app.toolbar.toggleFavoriteLibrary | ToggleFavoriteLibraryComponent | The toolbar button component that toggles Favorite library state for the selection. |
See [Registration](/extending/registration) section for more details
on how to register your own entries to be re-used at runtime.

View File

@@ -72,6 +72,8 @@ import { AppHeaderModule } from './components/header/header.module';
import { environment } from '../environments/environment';
import { LibraryMembershipDirective } from './directives/library-membership.directive';
import { ToggleJoinLibraryComponent } from './components/toolbar/toggle-join-library/toggle-join-library.component';
import { LibraryFavoriteDirective } from './directives/library-favorite.directive';
import { ToggleFavoriteLibraryComponent } from './components/toolbar/toggle-favorite-library/toggle-favorite-library.component';
@NgModule({
imports: [
@@ -114,7 +116,9 @@ import { ToggleJoinLibraryComponent } from './components/toolbar/toggle-join-lib
NodeVersionsDialogComponent,
LibraryDialogComponent,
LibraryMembershipDirective,
ToggleJoinLibraryComponent
ToggleJoinLibraryComponent,
LibraryFavoriteDirective,
ToggleFavoriteLibraryComponent
],
providers: [
{ provide: RouteReuseStrategy, useClass: AppRouteReuseStrategy },
@@ -131,7 +135,8 @@ import { ToggleJoinLibraryComponent } from './components/toolbar/toggle-join-lib
entryComponents: [
LibraryDialogComponent,
NodeVersionsDialogComponent,
ToggleJoinLibraryComponent
ToggleJoinLibraryComponent,
ToggleFavoriteLibraryComponent
],
bootstrap: [AppComponent]
})

View File

@@ -70,7 +70,9 @@ export class FavoriteLibrariesComponent extends PageComponent
this.subscriptions = this.subscriptions.concat([
this.content.libraryDeleted.subscribe(() => this.reload()),
this.content.libraryUpdated.subscribe(() => this.documentList.reload()),
this.content.libraryUpdated.subscribe(() => this.reload()),
this.content.favoriteLibraryToggle.subscribe(() => this.reload()),
this.breakpointObserver
.observe([Breakpoints.HandsetPortrait, Breakpoints.HandsetLandscape])
.subscribe(result => {

View File

@@ -55,7 +55,7 @@ export class LibrariesComponent extends PageComponent implements OnInit {
this.subscriptions.push(
this.content.libraryDeleted.subscribe(() => this.reload()),
this.content.libraryUpdated.subscribe(() => this.documentList.reload()),
this.content.libraryUpdated.subscribe(() => this.reload()),
this.breakpointObserver
.observe([Breakpoints.HandsetPortrait, Breakpoints.HandsetLandscape])

View File

@@ -0,0 +1,94 @@
/*!
* @license
* Alfresco Example Content Application
*
* Copyright (C) 2005 - 2018 Alfresco Software Limited
*
* This file is part of the Alfresco Example Content Application.
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
*
* The Alfresco Example Content Application is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* The Alfresco Example Content Application is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
*/
import { TestBed } from '@angular/core/testing';
import {
setupTestBed,
CoreModule,
AlfrescoApiService,
AlfrescoApiServiceMock
} from '@alfresco/adf-core';
import { ToggleFavoriteLibraryComponent } from './toggle-favorite-library.component';
import { LibraryFavoriteDirective } from '../../../directives/library-favorite.directive';
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { Store } from '@ngrx/store';
import { AppTestingModule } from '../../../testing/app-testing.module';
import { ContentManagementService } from '../../../services/content-management.service';
import { of } from 'rxjs';
describe('ToggleFavoriteLibraryComponent', () => {
let fixture;
let component;
let contentManagementService;
const selection = { library: { entry: { id: 'libraryId' } } };
setupTestBed({
imports: [CoreModule, AppTestingModule],
declarations: [ToggleFavoriteLibraryComponent, LibraryFavoriteDirective],
providers: [
{
provide: AlfrescoApiService,
useClass: AlfrescoApiServiceMock
},
{
provide: Store,
useValue: {
dispatch: () => {},
select: () => of(selection)
}
},
ContentManagementService
],
schemas: [NO_ERRORS_SCHEMA]
});
beforeEach(() => {
fixture = TestBed.createComponent(ToggleFavoriteLibraryComponent);
component = fixture.componentInstance;
contentManagementService = TestBed.get(ContentManagementService);
const api = TestBed.get(AlfrescoApiService);
spyOn(api.peopleApi, 'getFavoriteSite').and.returnValue(Promise.resolve());
});
it('should get library selection from Store', done => {
fixture.detectChanges();
component.selection$.subscribe(selected => {
expect(selected).toEqual(selection);
done();
});
});
it('should emit onToggleEvent() event', () => {
fixture.detectChanges();
spyOn(contentManagementService.favoriteLibraryToggle, 'next');
component.onToggleEvent();
expect(
contentManagementService.favoriteLibraryToggle.next
).toHaveBeenCalled();
});
});

View File

@@ -0,0 +1,66 @@
/*!
* @license
* Alfresco Example Content Application
*
* Copyright (C) 2005 - 2018 Alfresco Software Limited
*
* This file is part of the Alfresco Example Content Application.
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
*
* The Alfresco Example Content Application is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* The Alfresco Example Content Application is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
*/
import { Component, ViewEncapsulation, OnInit } from '@angular/core';
import { Store } from '@ngrx/store';
import { AppStore } from '../../../store/states';
import { appSelection } from '../../../store/selectors/app.selectors';
import { Observable } from 'rxjs';
import { SelectionState } from '@alfresco/adf-extensions';
import { ContentManagementService } from '../../../services/content-management.service';
@Component({
selector: 'app-toggle-favorite-library',
template: `
<button
mat-menu-item
#favoriteLibrary="favoriteLibrary"
(toggle)="onToggleEvent()"
[acaFavoriteLibrary]="(selection$ | async).library"
>
<mat-icon *ngIf="favoriteLibrary.isFavorite()">star</mat-icon>
<mat-icon *ngIf="!favoriteLibrary.isFavorite()">star_border</mat-icon>
<span>{{ 'APP.ACTIONS.FAVORITE' | translate }}</span>
</button>
`,
encapsulation: ViewEncapsulation.None,
host: { class: 'app-toggle-favorite-library' }
})
export class ToggleFavoriteLibraryComponent implements OnInit {
selection$: Observable<SelectionState>;
constructor(
private store: Store<AppStore>,
private content: ContentManagementService
) {}
ngOnInit() {
this.selection$ = this.store.select(appSelection);
}
onToggleEvent() {
this.content.favoriteLibraryToggle.next();
}
}

View File

@@ -0,0 +1,144 @@
/*!
* @license
* Alfresco Example Content Application
*
* Copyright (C) 2005 - 2018 Alfresco Software Limited
*
* This file is part of the Alfresco Example Content Application.
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
*
* The Alfresco Example Content Application is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* The Alfresco Example Content Application is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
*/
import { Component, ViewChild } from '@angular/core';
import { LibraryFavoriteDirective } from './library-favorite.directive';
import {
AlfrescoApiService,
AlfrescoApiServiceMock,
setupTestBed,
CoreModule
} from '@alfresco/adf-core';
import { TestBed, async } from '@angular/core/testing';
@Component({
selector: 'app-test-component',
template: `
<button
#favoriteLibrary="favoriteLibrary"
[acaFavoriteLibrary]="selection"
></button>
`
})
class TestComponent {
@ViewChild('favoriteLibrary')
directive: LibraryFavoriteDirective;
selection = null;
}
describe('LibraryFavoriteDirective', () => {
let fixture;
let api;
let component;
let selection;
setupTestBed({
imports: [CoreModule],
declarations: [TestComponent, LibraryFavoriteDirective],
providers: [
{
provide: AlfrescoApiService,
useClass: AlfrescoApiServiceMock
}
]
});
beforeEach(() => {
fixture = TestBed.createComponent(TestComponent);
component = fixture.componentInstance;
api = TestBed.get(AlfrescoApiService);
selection = { entry: { guid: 'guid', id: 'id' } };
});
it('should not check for favorite if no selection exists', () => {
spyOn(api.peopleApi, 'getFavoriteSite');
fixture.detectChanges();
expect(api.peopleApi.getFavoriteSite).not.toHaveBeenCalled();
});
it('should mark selection as favorite when getFavoriteSite returns successfully', async(() => {
spyOn(api.peopleApi, 'getFavoriteSite').and.returnValue(Promise.resolve());
component.selection = selection;
fixture.detectChanges();
fixture.whenStable().then(() => {
expect(api.peopleApi.getFavoriteSite).toHaveBeenCalled();
expect(component.directive.isFavorite()).toBe(true);
});
}));
it('should mark selection not favorite when getFavoriteSite errors', async(() => {
spyOn(api.peopleApi, 'getFavoriteSite').and.returnValue(Promise.reject());
component.selection = selection;
fixture.detectChanges();
fixture.whenStable().then(() => {
expect(api.peopleApi.getFavoriteSite).toHaveBeenCalled();
expect(component.directive.isFavorite()).toBe(false);
});
}));
it('should call addFavorite() on click event when selection is not a favorite', async(() => {
spyOn(api.peopleApi, 'getFavoriteSite').and.returnValue(Promise.reject());
spyOn(api.peopleApi, 'addFavorite').and.returnValue(Promise.resolve());
component.selection = selection;
fixture.detectChanges();
expect(component.directive.isFavorite()).toBeFalsy();
fixture.whenStable().then(() => {
fixture.nativeElement
.querySelector('button')
.dispatchEvent(new MouseEvent('click'));
fixture.detectChanges();
expect(api.peopleApi.addFavorite).toHaveBeenCalled();
});
}));
it('should call removeFavoriteSite() on click event when selection is not a favorite', async(() => {
spyOn(api.peopleApi, 'getFavoriteSite').and.returnValue(Promise.resolve());
spyOn(api.peopleApi, 'removeFavoriteSite').and.returnValue(
Promise.resolve()
);
component.selection = selection;
fixture.detectChanges();
expect(component.directive.isFavorite()).toBeFalsy();
fixture.whenStable().then(() => {
fixture.nativeElement
.querySelector('button')
.dispatchEvent(new MouseEvent('click'));
fixture.detectChanges();
expect(api.peopleApi.removeFavoriteSite).toHaveBeenCalled();
});
}));
});

View File

@@ -0,0 +1,140 @@
/*!
* @license
* Alfresco Example Content Application
*
* Copyright (C) 2005 - 2018 Alfresco Software Limited
*
* This file is part of the Alfresco Example Content Application.
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
*
* The Alfresco Example Content Application is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* The Alfresco Example Content Application is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
*/
import {
Directive,
HostListener,
Input,
OnChanges,
Output,
EventEmitter
} from '@angular/core';
import { SiteBody, FavoriteBody, FavoriteEntry, Site } from 'alfresco-js-api';
import { AlfrescoApiService } from '@alfresco/adf-core';
interface LibraryEntity {
entry: Site;
isLibrary: boolean;
}
interface FavoriteLibrary {
entry: Site;
isFavorite: boolean;
}
@Directive({
selector: '[acaFavoriteLibrary]',
exportAs: 'favoriteLibrary'
})
export class LibraryFavoriteDirective implements OnChanges {
@Input('acaFavoriteLibrary')
library: any = null;
@Output() toggle: EventEmitter<any> = new EventEmitter();
@Output() error: EventEmitter<any> = new EventEmitter();
private targetLibrary: any = null;
@HostListener('click')
onClick() {
this.toggleFavorite();
}
constructor(private alfrescoApiService: AlfrescoApiService) {}
ngOnChanges(changes) {
if (!changes.library.currentValue) {
return;
}
this.markFavoriteLibrary(changes.library.currentValue);
}
isFavorite() {
return this.targetLibrary && this.targetLibrary.isFavorite;
}
toggleFavorite() {
if (!this.targetLibrary) {
return;
}
if (this.targetLibrary.isFavorite) {
this.removeFavorite(this.targetLibrary.entry.guid);
} else {
const favoriteBody = this.createFavoriteBody(this.targetLibrary);
this.addFavorite(favoriteBody);
}
}
private async markFavoriteLibrary(library: LibraryEntity) {
this.targetLibrary = await this.getFavoriteSite(library);
}
private getFavoriteSite(library: LibraryEntity): Promise<FavoriteLibrary> {
return this.alfrescoApiService.peopleApi
.getFavoriteSite('-me-', library.entry.id)
.then(() => {
return {
entry: { ...this.library.entry },
isFavorite: true
};
})
.catch(() => ({
entry: { ...this.library.entry },
isFavorite: false
}));
}
private createFavoriteBody(library: LibraryEntity): FavoriteBody {
return {
target: {
site: {
guid: library.entry.guid
}
}
};
}
private addFavorite(favoriteBody: FavoriteBody) {
this.alfrescoApiService.peopleApi
.addFavorite('-me-', favoriteBody)
.then((libraryEntry: FavoriteEntry) => {
this.targetLibrary.isFavorite = true;
this.toggle.emit(libraryEntry);
})
.catch(error => this.error.emit(error));
}
private removeFavorite(favoriteId: string) {
this.alfrescoApiService.peopleApi
.removeFavoriteSite('-me-', favoriteId)
.then((libraryBody: SiteBody) => {
this.targetLibrary.isFavorite = false;
this.toggle.emit(libraryBody);
})
.catch(error => this.error.emit(error));
}
}

View File

@@ -33,6 +33,7 @@ import * as nav from './evaluators/navigation.evaluators';
import { AppExtensionService } from './extension.service';
import { ToggleInfoDrawerComponent } from '../components/toolbar/toggle-info-drawer/toggle-info-drawer.component';
import { ToggleFavoriteComponent } from '../components/toolbar/toggle-favorite/toggle-favorite.component';
import { ToggleFavoriteLibraryComponent } from '../components/toolbar/toggle-favorite-library/toggle-favorite-library.component';
import { ToggleSharedComponent } from '../components/shared/toggle-shared/toggle-shared.component';
import { MetadataTabComponent } from '../components/info-drawer/metadata-tab/metadata-tab.component';
import { LibraryMetadataTabComponent } from '../components/info-drawer/library-metadata-tab/library-metadata-tab.component';
@@ -86,6 +87,7 @@ export class CoreExtensionsModule {
'app.components.tabs.versions': VersionsTabComponent,
'app.toolbar.toggleInfoDrawer': ToggleInfoDrawerComponent,
'app.toolbar.toggleFavorite': ToggleFavoriteComponent,
'app.toolbar.toggleFavoriteLibrary': ToggleFavoriteLibraryComponent,
'app.toolbar.toggleJoinLibrary': ToggleJoinLibraryComponent,
'app.toolbar.cardView': DocumentDisplayModeComponent,
'app.shared-link.toggleSharedLink': ToggleSharedComponent,

View File

@@ -88,6 +88,7 @@ export class ContentManagementService {
favoriteAdded = new Subject<Array<MinimalNodeEntity>>();
favoriteRemoved = new Subject<Array<MinimalNodeEntity>>();
favoriteToggle = new Subject<Array<MinimalNodeEntity>>();
favoriteLibraryToggle = new Subject<any>();
constructor(
private store: Store<AppStore>,

View File

@@ -94,11 +94,11 @@
]
},
{
"id": "app.libraries.toolbar.info",
"id": "app.libraries.toolbar",
"type": "core.every",
"parameters": [
{ "type": "rule", "value": "app.selection.notEmpty" },
{ "type": "rule", "value": "app.navigation.isLibraries" }
{ "type": "rule", "value": "app.selection.library" }
]
},
{
@@ -384,7 +384,7 @@
}
},
{
"id": "app.toolbar.info",
"id": "app.toolbar.info.infoDrawer",
"type": "custom",
"order": 700,
"component": "app.toolbar.toggleInfoDrawer",
@@ -393,12 +393,12 @@
}
},
{
"id": "app.libraries.toolbar.info",
"id": "app.libraries.toolbar.infoDrawer",
"type": "custom",
"order": 701,
"component": "app.toolbar.toggleInfoDrawer",
"rules": {
"visible": "app.libraries.toolbar.info"
"visible": "app.libraries.toolbar"
}
},
{
@@ -427,6 +427,15 @@
"visible": "app.toolbar.favorite.canToggle"
}
},
{
"id": "app.libraries.toolbar.toggleFavorite",
"type": "custom",
"order": 101,
"component": "app.toolbar.toggleFavoriteLibrary",
"rules": {
"visible": "app.libraries.toolbar"
}
},
{
"id": "app.toolbar.favorite.add",
"order": 200,
@@ -906,7 +915,7 @@
"title": "APP.INFO_DRAWER.TABS.LIBRARY_PROPERTIES",
"component": "app.components.tabs.library.metadata",
"rules": {
"visible": "app.libraries.toolbar.info"
"visible": "app.libraries.toolbar"
}
}
],