From 525ba7e73e23384ea2d40f7909894ba8183d1493 Mon Sep 17 00:00:00 2001 From: Cilibiu Bogdan Date: Sun, 24 Feb 2019 14:50:23 +0200 Subject: [PATCH] [ACA-2216] Shared link preview - use extension actions (#964) * isSharedFileViewer evaluator * navigation evaluators tests * update docs * fallback for SharedLink entry * shared link view use extensions * rules for link shared view actions * dedicated extension definition for shared link action toolbar * resolve selection and actions * update tests * remove un used imports * nest shared link viewer toolbar actions in to viewer structure --- docs/extending/rules.md | 1 + .../shared-link-view.component.html | 16 +- .../shared-link-view.component.spec.ts | 102 +++++- .../shared-link-view.component.ts | 45 ++- .../shared-link-view.module.ts | 2 + src/app/extensions/core.extensions.module.ts | 1 + .../evaluators/navigation.evaluators.spec.ts | 338 ++++++++++++++++++ .../evaluators/navigation.evaluators.ts | 8 + src/app/extensions/extension.service.spec.ts | 33 ++ src/app/extensions/extension.service.ts | 9 + src/app/store/reducers/app.reducer.ts | 15 +- src/assets/app.extensions.json | 22 ++ 12 files changed, 582 insertions(+), 10 deletions(-) create mode 100644 src/app/extensions/evaluators/navigation.evaluators.spec.ts diff --git a/docs/extending/rules.md b/docs/extending/rules.md index 68efe759bc..00076b7cb5 100644 --- a/docs/extending/rules.md +++ b/docs/extending/rules.md @@ -182,6 +182,7 @@ for example mixing `core.every` and `core.not`. | app.navigation.isNotSearchResults | Current page is not the **Search Results**. | | app.navigation.isSharedPreview | Current page is preview **Shared Files** | | app.navigation.isFavoritesPreview | Current page is preview **Favorites** | +| app.navigation.isSharedFileViewer | Current page is shared file preview page | **Tip:** See the [Registration](/extending/registration) section for more details on how to register your own entries to be re-used at runtime. diff --git a/src/app/components/shared-link-view/shared-link-view.component.html b/src/app/components/shared-link-view/shared-link-view.component.html index dd99e1a0ba..673bec3792 100644 --- a/src/app/components/shared-link-view/shared-link-view.component.html +++ b/src/app/components/shared-link-view/shared-link-view.component.html @@ -1,3 +1,17 @@ - + + + + + + + diff --git a/src/app/components/shared-link-view/shared-link-view.component.spec.ts b/src/app/components/shared-link-view/shared-link-view.component.spec.ts index 10d0990f5e..223ea96e57 100644 --- a/src/app/components/shared-link-view/shared-link-view.component.spec.ts +++ b/src/app/components/shared-link-view/shared-link-view.component.spec.ts @@ -24,21 +24,47 @@ */ import { SharedLinkViewComponent } from './shared-link-view.component'; -import { TestBed, ComponentFixture } from '@angular/core/testing'; +import { + TestBed, + ComponentFixture, + fakeAsync, + tick +} from '@angular/core/testing'; +import { Store } from '@ngrx/store'; import { AppTestingModule } from '../../testing/app-testing.module'; import { ActivatedRoute } from '@angular/router'; import { of } from 'rxjs'; import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { AlfrescoApiService } from '@alfresco/adf-core'; +import { SetSelectedNodesAction } from '../../store/actions'; +import { AppExtensionService } from '../../extensions/extension.service'; describe('SharedLinkViewComponent', () => { let component: SharedLinkViewComponent; let fixture: ComponentFixture; + let alfrescoApiService: AlfrescoApiService; + let appExtensionService: AppExtensionService; + let spyGetSharedLink; + const storeMock = { + dispatch: jasmine.createSpy('dispatch'), + select: () => of({}) + }; beforeEach(() => { TestBed.configureTestingModule({ imports: [AppTestingModule], declarations: [SharedLinkViewComponent], providers: [ + AppExtensionService, + { provide: Store, useValue: storeMock }, + { + provide: AlfrescoApiService, + useValue: { + sharedLinksApi: { + getSharedLink: () => {} + } + } + }, { provide: ActivatedRoute, useValue: { @@ -52,11 +78,79 @@ describe('SharedLinkViewComponent', () => { fixture = TestBed.createComponent(SharedLinkViewComponent); component = fixture.componentInstance; + alfrescoApiService = TestBed.get(AlfrescoApiService); + appExtensionService = TestBed.get(AppExtensionService); - fixture.detectChanges(); + spyGetSharedLink = spyOn( + alfrescoApiService.sharedLinksApi, + 'getSharedLink' + ); + + storeMock.dispatch.calls.reset(); }); - it('should fetch link id from the active route', () => { - expect(component.sharedLinkId).toBe('123'); + afterEach(() => { + spyGetSharedLink.calls.reset(); }); + + it('should update store selection', fakeAsync(() => { + spyGetSharedLink.and.returnValue( + Promise.resolve({ entry: { id: 'shared-id' } }) + ); + + fixture.detectChanges(); + tick(); + + expect(storeMock.dispatch).toHaveBeenCalledWith( + new SetSelectedNodesAction([{ entry: { id: 'shared-id' } }]) + ); + })); + + it('should not update store on error', fakeAsync(() => { + spyGetSharedLink.and.returnValue(Promise.reject('error')); + + fixture.detectChanges(); + tick(); + + expect(storeMock.dispatch).not.toHaveBeenCalled(); + })); + + it('should not update actions reference if selection is empty', fakeAsync(() => { + spyOn(storeMock, 'select').and.returnValue(of({ isEmpty: true })); + + spyGetSharedLink.and.returnValue( + Promise.resolve({ entry: { id: 'shared-id' } }) + ); + + fixture.detectChanges(); + tick(); + + expect(component.viewerToolbarActions).toEqual([]); + })); + + it('should update actions reference if selection is not empty', fakeAsync(() => { + spyOn(storeMock, 'select').and.returnValue(of({ isEmpty: false })); + spyOn(appExtensionService, 'getSharedLinkViewerToolbarActions'); + spyGetSharedLink.and.returnValue( + Promise.resolve({ entry: { id: 'shared-id' } }) + ); + + fixture.detectChanges(); + tick(); + + expect( + appExtensionService.getSharedLinkViewerToolbarActions + ).toHaveBeenCalled(); + })); + + it('should fetch link id from the active route', fakeAsync(() => { + spyGetSharedLink.and.returnValue( + Promise.resolve({ entry: { id: 'shared-id' } }) + ); + + fixture.detectChanges(); + tick(); + + expect(component.sharedLinkId).toBe('123'); + })); }); diff --git a/src/app/components/shared-link-view/shared-link-view.component.ts b/src/app/components/shared-link-view/shared-link-view.component.ts index afd8b6aa1b..d875798f44 100644 --- a/src/app/components/shared-link-view/shared-link-view.component.ts +++ b/src/app/components/shared-link-view/shared-link-view.component.ts @@ -1,5 +1,15 @@ import { Component, ViewEncapsulation, OnInit } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; +import { ContentActionRef } from '@alfresco/adf-extensions'; +import { AppExtensionService } from '../../extensions/extension.service'; +import { Store } from '@ngrx/store'; +import { AppStore } from '../../store/states/app.state'; +import { AlfrescoApiService } from '@alfresco/adf-core'; +import { SharedLinkEntry } from '@alfresco/js-api'; +import { SetSelectedNodesAction } from '../../store/actions'; +import { flatMap, catchError } from 'rxjs/operators'; +import { forkJoin, of, from } from 'rxjs'; +import { appSelection } from '../../store/selectors/app.selectors'; @Component({ selector: 'app-shared-link-view', @@ -10,12 +20,41 @@ import { ActivatedRoute } from '@angular/router'; }) export class SharedLinkViewComponent implements OnInit { sharedLinkId: string = null; + viewerToolbarActions: Array = []; - constructor(private route: ActivatedRoute) {} + constructor( + private route: ActivatedRoute, + private store: Store, + private extensions: AppExtensionService, + private alfrescoApiService: AlfrescoApiService + ) {} ngOnInit() { - this.route.params.subscribe(params => { - this.sharedLinkId = params.id; + this.route.params + .pipe( + flatMap(params => + forkJoin( + from( + this.alfrescoApiService.sharedLinksApi.getSharedLink(params.id) + ), + of(params.id) + ).pipe(catchError(() => of([null, params.id]))) + ) + ) + .subscribe(([sharedEntry, sharedId]: [SharedLinkEntry, string]) => { + if (sharedEntry) { + this.store.dispatch(new SetSelectedNodesAction([sharedEntry])); + } + this.sharedLinkId = sharedId; + }); + + this.store.select(appSelection).subscribe(selection => { + if (!selection.isEmpty) + this.viewerToolbarActions = this.extensions.getSharedLinkViewerToolbarActions(); }); } + + trackByActionId(index: number, action: ContentActionRef) { + return action.id; + } } diff --git a/src/app/components/shared-link-view/shared-link-view.module.ts b/src/app/components/shared-link-view/shared-link-view.module.ts index ad89f78254..17c9431bbb 100644 --- a/src/app/components/shared-link-view/shared-link-view.module.ts +++ b/src/app/components/shared-link-view/shared-link-view.module.ts @@ -32,6 +32,7 @@ import { DirectivesModule } from '../../directives/directives.module'; import { AppCommonModule } from '../common/common.module'; import { AppToolbarModule } from '../toolbar/toolbar.module'; import { AppInfoDrawerModule } from '../info-drawer/info.drawer.module'; +import { CoreExtensionsModule } from '../../extensions/core.extensions.module'; const routes: Routes = [ { @@ -51,6 +52,7 @@ const routes: Routes = [ DirectivesModule, AppCommonModule, AppToolbarModule, + CoreExtensionsModule.forChild(), AppInfoDrawerModule ], declarations: [SharedLinkViewComponent], diff --git a/src/app/extensions/core.extensions.module.ts b/src/app/extensions/core.extensions.module.ts index 4c4d3d771a..e69335d460 100644 --- a/src/app/extensions/core.extensions.module.ts +++ b/src/app/extensions/core.extensions.module.ts @@ -150,6 +150,7 @@ export class CoreExtensionsModule { 'app.navigation.isPreview': nav.isPreview, 'app.navigation.isSharedPreview': nav.isSharedPreview, 'app.navigation.isFavoritesPreview': nav.isFavoritesPreview, + 'app.navigation.isSharedFileViewer': nav.isSharedFileViewer, 'repository.isQuickShareEnabled': repository.hasQuickShareEnabled }); diff --git a/src/app/extensions/evaluators/navigation.evaluators.spec.ts b/src/app/extensions/evaluators/navigation.evaluators.spec.ts new file mode 100644 index 0000000000..bb56735afd --- /dev/null +++ b/src/app/extensions/evaluators/navigation.evaluators.spec.ts @@ -0,0 +1,338 @@ +/*! + * @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 . + */ + +import * as app from './navigation.evaluators'; + +describe('navigation.evaluators', () => { + describe('isPreview', () => { + it('should return [true] if url contains `/preview/`', () => { + const context: any = { + navigation: { + url: 'path/preview/id' + } + }; + + expect(app.isPreview(context, null)).toBe(true); + }); + }); + + describe('isFavorites', () => { + it('should return [true] if url contains `/favorites`', () => { + const context: any = { + navigation: { + url: '/favorites/path' + } + }; + + expect(app.isFavorites(context, null)).toBe(true); + }); + + it('should return [false] if `/favorites` url contains `/preview/`', () => { + const context: any = { + navigation: { + url: '/favorites/preview/' + } + }; + + expect(app.isFavorites(context, null)).toBe(false); + }); + }); + + describe('isNotFavorites', () => { + it('should return [true] if url is not `/favorites`', () => { + const context: any = { + navigation: { + url: '/some/path' + } + }; + + expect(app.isNotFavorites(context, null)).toBe(true); + }); + + it('should return [false] if url starts with `/favorites`', () => { + const context: any = { + navigation: { + url: '/favorites/path' + } + }; + + expect(app.isNotFavorites(context, null)).toBe(false); + }); + }); + + describe('isSharedFiles', () => { + it('should return [true] if path starts with `/shared`', () => { + const context: any = { + navigation: { + url: '/shared/path' + } + }; + + expect(app.isSharedFiles(context, null)).toBe(true); + }); + + it('should return [false] if `/shared` url contains `/preview/`', () => { + const context: any = { + navigation: { + url: '/shared/preview/' + } + }; + + expect(app.isSharedFiles(context, null)).toBe(false); + }); + }); + + describe('isNotSharedFiles', () => { + it('should return [true] if path does not contain `/shared`', () => { + const context: any = { + navigation: { + url: '/some/path/' + } + }; + + expect(app.isNotSharedFiles(context, null)).toBe(true); + }); + + it('should return [false] if path contains `/shared`', () => { + const context: any = { + navigation: { + url: '/shared/path/' + } + }; + + expect(app.isNotSharedFiles(context, null)).toBe(false); + }); + }); + + describe('isTrashcan', () => { + it('should return [true] if url starts with `/trashcan`', () => { + const context: any = { + navigation: { + url: '/trashcan' + } + }; + + expect(app.isTrashcan(context, null)).toBe(true); + }); + + it('should return [false] if url does not start with `/trashcan`', () => { + const context: any = { + navigation: { + url: '/path/trashcan' + } + }; + + expect(app.isTrashcan(context, null)).toBe(false); + }); + }); + + describe('isNotTrashcan', () => { + it('should return [true] if url does not start with `/trashcan`', () => { + const context: any = { + navigation: { + url: '/path/trashcan' + } + }; + + expect(app.isNotTrashcan(context, null)).toBe(true); + }); + + it('should return [false] if url does start with `/trashcan`', () => { + const context: any = { + navigation: { + url: '/trashcan' + } + }; + + expect(app.isNotTrashcan(context, null)).toBe(false); + }); + }); + + describe('isPersonalFiles', () => { + it('should return [true] if url starts with `/personal-files`', () => { + const context: any = { + navigation: { + url: '/personal-files' + } + }; + + expect(app.isPersonalFiles(context, null)).toBe(true); + }); + + it('should return [false] if url does not start with `/personal-files`', () => { + const context: any = { + navigation: { + url: '/path/personal-files' + } + }; + + expect(app.isPersonalFiles(context, null)).toBe(false); + }); + }); + + describe('isLibraries', () => { + it('should return [true] if url ends with `/libraries`', () => { + const context: any = { + navigation: { + url: '/path/libraries' + } + }; + + expect(app.isLibraries(context, null)).toBe(true); + }); + + it('should return [true] if url starts with `/search-libraries`', () => { + const context: any = { + navigation: { + url: '/search-libraries/path' + } + }; + + expect(app.isLibraries(context, null)).toBe(true); + }); + }); + + describe('isNotLibraries', () => { + it('should return [true] if url does not end with `/libraries`', () => { + const context: any = { + navigation: { + url: '/libraries/path' + } + }; + + expect(app.isNotLibraries(context, null)).toBe(true); + }); + }); + + describe('isRecentFiles', () => { + it('should return [true] if url starts with `/recent-files`', () => { + const context: any = { + navigation: { + url: '/recent-files' + } + }; + + expect(app.isRecentFiles(context, null)).toBe(true); + }); + + it('should return [false] if url does not start with `/recent-files`', () => { + const context: any = { + navigation: { + url: '/path/recent-files' + } + }; + + expect(app.isRecentFiles(context, null)).toBe(false); + }); + }); + + describe('isSearchResults', () => { + it('should return [true] if url starts with `/search`', () => { + const context: any = { + navigation: { + url: '/search' + } + }; + + expect(app.isSearchResults(context, null)).toBe(true); + }); + + it('should return [false] if url does not start with `/search`', () => { + const context: any = { + navigation: { + url: '/path/search' + } + }; + + expect(app.isSearchResults(context, null)).toBe(false); + }); + }); + + describe('isSharedPreview', () => { + it('should return [true] if url starts with `/shared/preview/`', () => { + const context: any = { + navigation: { + url: '/shared/preview/path' + } + }; + + expect(app.isSharedPreview(context, null)).toBe(true); + }); + + it('should return [false] if url does not start with `/shared/preview/`', () => { + const context: any = { + navigation: { + url: '/path/shared/preview/' + } + }; + + expect(app.isSharedPreview(context, null)).toBe(false); + }); + }); + + describe('isFavoritesPreview', () => { + it('should return [true] if url starts with `/favorites/preview/`', () => { + const context: any = { + navigation: { + url: '/favorites/preview/path' + } + }; + + expect(app.isFavoritesPreview(context, null)).toBe(true); + }); + + it('should return [false] if url does not start with `/favorites/preview/`', () => { + const context: any = { + navigation: { + url: '/path/favorites/preview/' + } + }; + + expect(app.isFavoritesPreview(context, null)).toBe(false); + }); + }); + + describe('isSharedFileViewer', () => { + it('should return [true] if url starts with `/preview/s/`', () => { + const context: any = { + navigation: { + url: '/preview/s/path' + } + }; + + expect(app.isSharedFileViewer(context, null)).toBe(true); + }); + + it('should return [false] if url does not start with `/preview/s/`', () => { + const context: any = { + navigation: { + url: '/path/preview/s/' + } + }; + + expect(app.isSharedFileViewer(context, null)).toBe(false); + }); + }); +}); diff --git a/src/app/extensions/evaluators/navigation.evaluators.ts b/src/app/extensions/evaluators/navigation.evaluators.ts index c270f54115..e21d25a7b3 100644 --- a/src/app/extensions/evaluators/navigation.evaluators.ts +++ b/src/app/extensions/evaluators/navigation.evaluators.ts @@ -156,3 +156,11 @@ export function isFavoritesPreview( const { url } = context.navigation; return url && url.startsWith('/favorites/preview/'); } + +export function isSharedFileViewer( + context: RuleContext, + ...args: RuleParameter[] +): boolean { + const { url } = context.navigation; + return url && url.startsWith('/preview/s/'); +} diff --git a/src/app/extensions/extension.service.spec.ts b/src/app/extensions/extension.service.spec.ts index 0e7629b226..17772038a2 100644 --- a/src/app/extensions/extension.service.spec.ts +++ b/src/app/extensions/extension.service.spec.ts @@ -683,4 +683,37 @@ describe('AppExtensionService', () => { ]); }); }); + + describe('getSharedLinkViewerToolbarActions', () => { + it('should get shared link viewer actions', () => { + const actions = [ + { + id: 'id', + type: ContentActionType.button, + icon: 'icon', + actions: { + click: 'click' + } + } + ]; + + applyConfig({ + $id: 'test', + $name: 'test', + $version: '1.0.0', + $license: 'MIT', + $vendor: 'Good company', + $runtime: '1.5.0', + features: { + viewer: { + shared: { + toolbarActions: actions + } + } + } + }); + + expect(service.getSharedLinkViewerToolbarActions()).toEqual(actions); + }); + }); }); diff --git a/src/app/extensions/extension.service.ts b/src/app/extensions/extension.service.ts index 7c945da432..d36ed03476 100644 --- a/src/app/extensions/extension.service.ts +++ b/src/app/extensions/extension.service.ts @@ -71,6 +71,7 @@ export class AppExtensionService implements RuleContext { headerActions: Array = []; toolbarActions: Array = []; viewerToolbarActions: Array = []; + sharedLinkViewerToolbarActions: Array = []; viewerContentExtensions: Array = []; contextMenuActions: Array = []; openWithActions: Array = []; @@ -147,6 +148,10 @@ export class AppExtensionService implements RuleContext { config, 'features.viewer.toolbarActions' ); + this.sharedLinkViewerToolbarActions = this.loader.getContentActions( + config, + 'features.viewer.shared.toolbarActions' + ); this.viewerContentExtensions = this.loader.getElements( config, 'features.viewer.content' @@ -422,6 +427,10 @@ export class AppExtensionService implements RuleContext { return this.getAllowedActions(this.viewerToolbarActions); } + getSharedLinkViewerToolbarActions(): Array { + return this.getAllowedActions(this.sharedLinkViewerToolbarActions); + } + getHeaderActions(): Array { return this.headerActions.filter(action => this.filterByRules(action)); } diff --git a/src/app/store/reducers/app.reducer.ts b/src/app/store/reducers/app.reducer.ts index a2b7a6d7b2..518397ab2f 100644 --- a/src/app/store/reducers/app.reducer.ts +++ b/src/app/store/reducers/app.reducer.ts @@ -191,9 +191,20 @@ function updateSelectedNodes( if (nodes.length === 1) { file = nodes.find((entity: any) => { // workaround Shared - return entity.entry.isFile || entity.entry.nodeId ? true : false; + return entity.entry.isFile || + entity.entry.nodeId || + entity.entry.sharedByUser + ? true + : false; }); - folder = nodes.find(entity => entity.entry.isFolder); + folder = nodes.find((entity: any) => + // workaround Shared + entity.entry.isFolder || + entity.entry.nodeId || + entity.entry.sharedByUser + ? true + : false + ); } } diff --git a/src/assets/app.extensions.json b/src/assets/app.extensions.json index 788da2c317..26c243c1c5 100644 --- a/src/assets/app.extensions.json +++ b/src/assets/app.extensions.json @@ -1157,6 +1157,28 @@ ] } ], + "shared": { + "toolbarActions": [ + { + "id": "app.viewer.shared.fullscreen", + "order": 100, + "title": "APP.ACTIONS.FULLSCREEN", + "icon": "fullscreen", + "actions": { + "click": "FULLSCREEN_VIEWER" + } + }, + { + "id": "app.viewer.shared.download", + "order": 200, + "title": "APP.ACTIONS.DOWNLOAD", + "icon": "get_app", + "actions": { + "click": "DOWNLOAD_NODES" + } + } + ] + }, "content": [ { "id": "app.viewer.pdf",