[ACA-2087] Overlay Viewer (#1175)

* viewer outlet over preview route

* use ViewNodeAction over ViewFileAction

* pass data to dynamic component

* ViewNodeComponent for view file  custom actions

* update docs

* pass primary url to show preview outlet

* update tests

* reset selection on navigation event

* document list update selection action when not viewer

* close viewer for move and delete from viewer

* location as router commands to work with search query

* make viewer to behave like former preview

* viewer error route

* call correct preview method

* remove view/error route

* navigate to show error

* span element for action name

* fix folder navigation

* fix test

* page title fix

* update tests

* locate better the viewer toolbar

* fix viewer url  link

* update navigation rules

* document-list directive tests

* try workaround for chrome 76

* try another workaround for using chromedriver 75 instead of 76

* ViewerEffects tests

* reset selection over reload

* fix tests

* add reset event test

* remove actions

* context menu action refresh on favourite

* reset selection on navigation

* add delete and upload events

* takeUntil after operators

* remove chrome workaround parameter

* filter navigation event
This commit is contained in:
Cilibiu Bogdan 2019-08-08 15:38:50 +03:00 committed by Suzana Dirla
parent 8643a8806d
commit e31c0d6caf
43 changed files with 1256 additions and 238 deletions

View File

@ -122,4 +122,5 @@ Below is the list of public actions types you can use in the plugin definitions
| 1.7.0 | SHOW_SEARCH_FILTER | n/a | Show Filter component in Search Results. |
| 1.7.0 | HIDE_SEARCH_FILTER | n/a | Hide Filter component in Search Results |
| 1.8.0 | VIEW_NODE | string | Lightweight preview of a node by id. Can be invoked from extensions. |
| 1.8.0 | CLOSE_PREVIEW | n/a | Closes the viewer ( preview of the item ) |
| 1.8.0 | CLOSE_PREVIEW | n/a | Closes the viewer ( preview of the item ) |
| 1.9.0 | RESET_SELECTION | n/a | Resets active document list selection |

View File

@ -18,7 +18,8 @@ The components are used to create custom:
| 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. |
| app.toolbar.toggleJoinLibrary | ToggleJoinLibraryComponent | The toolbar button component that toggles Join/Cancel Join request for the selected library. |
| app.toolbar.toggleJoinLibrary | ToggleJoinLibraryComponent | The toolbar button component that toggles Join/Cancel Join request for the selected library |
| app.toolbar.viewNode | ViewNodeComponent | Action component to view files |
See [Registration](/extending/registration) section for more details
on how to register your own entries to be re-used at runtime.

View File

@ -31,6 +31,7 @@ const page = new BrowsingPage();
const { dataTable, toolbar } = page;
const contextMenu = dataTable.menu;
const viewer = new Viewer();
const viewerToolbar = viewer.toolbar;
export async function checkContextMenu(item: string, expectedContextMenu: string[]) {
@ -93,7 +94,7 @@ export async function checkViewerToolbarPrimaryActions(item: string, expectedToo
await dataTable.doubleClickOnRowByName(item);
await viewer.waitForViewerToOpen();
let actualPrimaryActions = await toolbar.getButtons();
let actualPrimaryActions = await viewerToolbar.getButtons();
actualPrimaryActions = removeClosePreviousNextOldInfo(actualPrimaryActions);
@ -106,9 +107,9 @@ export async function checkViewerToolbarPrimaryActions(item: string, expectedToo
export async function checkViewerToolbarMoreActions(item: string, expectedToolbarMore: string[]) {
await dataTable.doubleClickOnRowByName(item);
await viewer.waitForViewerToOpen();
await toolbar.openMoreMenu();
await viewerToolbar.openMoreMenu();
const actualMoreActions = await toolbar.menu.getMenuItems();
const actualMoreActions = await viewerToolbar.menu.getMenuItems();
expect(actualMoreActions.length).toBe(expectedToolbarMore.length, 'Incorrect number of toolbar More menu items');
expect(JSON.stringify(actualMoreActions)).toEqual(JSON.stringify(expectedToolbarMore), 'Incorrect toolbar More actions');

View File

@ -128,14 +128,14 @@ describe('Viewer general', () => {
});
it('Viewer opens when accessing the preview URL for a file - [C279285]', async () => {
const previewURL = `personal-files/${parentId}/preview/${xlsxFileId}`;
const previewURL = `personal-files/${parentId}/(viewer:view/${xlsxFileId})`
await page.load(previewURL);
expect(await viewer.isViewerOpened()).toBe(true, 'Viewer is not opened');
expect(await viewer.getFileTitle()).toEqual(xlsxFile);
});
it('Viewer does not open when accessing the preview URL for a file without permissions - [C279287]', async () => {
const previewURL = `libraries/${docLibId}/preview/${fileAdminId}`;
const previewURL = `libraries/${docLibId}/(viewer:view/${fileAdminId})`
await page.load(previewURL);
expect(await viewer.isViewerOpened()).toBe(false, 'Viewer should not be opened!');
});

View File

@ -27,10 +27,10 @@ import * as app from './navigation.rules';
describe('navigation.evaluators', () => {
describe('isPreview', () => {
it('should return [true] if url contains `/preview/`', () => {
it('should return [true] if url contains `viewer:view`', () => {
const context: any = {
navigation: {
url: 'path/preview/id'
url: 'path/(viewer:view/id)'
}
};
@ -271,20 +271,30 @@ describe('navigation.evaluators', () => {
});
describe('isSharedPreview', () => {
it('should return [true] if url starts with `/shared/preview/`', () => {
it('should return [true] if url starts with `/shared` and contains `viewer:view', () => {
const context: any = {
navigation: {
url: '/shared/preview/path'
url: '/shared/(viewer:view)'
}
};
expect(app.isSharedPreview(context)).toBe(true);
});
it('should return [false] if url does not start with `/shared/preview/`', () => {
it('should return [false] if url does not start with `/shared`', () => {
const context: any = {
navigation: {
url: '/path/shared/preview/'
url: '/path/shared/(viewer:view)'
}
};
expect(app.isSharedPreview(context)).toBe(false);
});
it('should return [false] if url starts with `/shared` and does not includes `viewer:view`', () => {
const context: any = {
navigation: {
url: '/shared/something'
}
};
@ -293,20 +303,30 @@ describe('navigation.evaluators', () => {
});
describe('isFavoritesPreview', () => {
it('should return [true] if url starts with `/favorites/preview/`', () => {
it('should return [true] if url starts with `/favorites` and includes `viewer:view`', () => {
const context: any = {
navigation: {
url: '/favorites/preview/path'
url: '/favorites/(viewer:view)'
}
};
expect(app.isFavoritesPreview(context)).toBe(true);
});
it('should return [false] if url does not start with `/favorites/preview/`', () => {
it('should return [false] if url does not start with `/favorites`', () => {
const context: any = {
navigation: {
url: '/path/favorites/preview/'
url: '/path/favorites/(viewer:view)'
}
};
expect(app.isFavoritesPreview(context)).toBe(false);
});
it('should return [false] if url starts with `/favorites` and does not include `viewer:view`', () => {
const context: any = {
navigation: {
url: '/favorites/other'
}
};

View File

@ -31,7 +31,7 @@ import { RuleContext } from '@alfresco/adf-extensions';
*/
export function isPreview(context: RuleContext): boolean {
const { url } = context.navigation;
return url && (url.includes('/preview/') || url.includes('/view/'));
return url && (url.includes('viewer:view') || url.includes('/view/'));
}
/**
@ -165,7 +165,7 @@ export function isNotSearchResults(context: RuleContext): boolean {
*/
export function isSharedPreview(context: RuleContext): boolean {
const { url } = context.navigation;
return url && url.startsWith('/shared/preview/');
return url && url.startsWith('/shared') && url.includes('viewer:view');
}
/**
@ -174,7 +174,7 @@ export function isSharedPreview(context: RuleContext): boolean {
*/
export function isFavoritesPreview(context: RuleContext): boolean {
const { url } = context.navigation;
return url && url.startsWith('/favorites/preview/');
return url && url.startsWith('/favorites') && url.includes('viewer:view');
}
/**

View File

@ -38,6 +38,7 @@ export enum AppActionTypes {
ToggleDocumentDisplayMode = 'TOGGLE_DOCUMENT_DISPLAY_MODE',
Logout = 'LOGOUT',
ReloadDocumentList = 'RELOAD_DOCUMENT_LIST',
ResetSelection = 'RESET_SELECTION',
SetInfoDrawerState = 'SET_INFO_DRAWER_STATE',
SetInfoDrawerMetadataAspect = 'SET_INFO_DRAWER_METADATA_ASPECT',
CloseModalDialogs = 'CLOSE_MODAL_DIALOGS'
@ -91,6 +92,12 @@ export class ReloadDocumentListAction implements Action {
constructor(public payload?: any) {}
}
export class ResetSelectionAction implements Action {
readonly type = AppActionTypes.ResetSelection;
constructor(public payload?: any) {}
}
export class SetInfoDrawerStateAction implements Action {
readonly type = AppActionTypes.SetInfoDrawerState;

View File

@ -36,7 +36,7 @@ export enum ViewerActionTypes {
export class ViewFileAction implements Action {
readonly type = ViewerActionTypes.ViewFile;
constructor(public payload: MinimalNodeEntity, public parentId?: string) {}
constructor(public payload?: MinimalNodeEntity, public parentId?: string) {}
}
export class ViewNodeAction implements Action {

View File

@ -33,7 +33,7 @@ import {
SharedLinksApiService
} from '@alfresco/adf-core';
import { Component, OnInit, OnDestroy } from '@angular/core';
import { ActivatedRoute, NavigationEnd, Router } from '@angular/router';
import { ActivatedRoute, Router, ActivationEnd } from '@angular/router';
import { Store } from '@ngrx/store';
import { AppExtensionService } from './extensions/extension.service';
import {
@ -97,22 +97,21 @@ export class AppComponent implements OnInit, OnDestroy {
this.loadAppSettings();
const { router, pageTitle, route } = this;
const { router, pageTitle } = this;
router.events
.pipe(filter(event => event instanceof NavigationEnd))
.subscribe(() => {
let currentRoute = route.root;
while (currentRoute.firstChild) {
currentRoute = currentRoute.firstChild;
}
const snapshot: any = currentRoute.snapshot || {};
this.router.events
.pipe(
filter(
event =>
event instanceof ActivationEnd &&
event.snapshot.children.length === 0
)
)
.subscribe((event: ActivationEnd) => {
const snapshot: any = event.snapshot || {};
const data: any = snapshot.data || {};
pageTitle.setTitle(data.title || '');
this.store.dispatch(new SetCurrentUrlAction(router.url));
});

View File

@ -80,19 +80,57 @@ export const APP_ROUTES: Routes = [
pathMatch: 'full'
},
{
path: 'favorites',
path: 'personal-files',
children: [
{
path: '',
loadChildren:
'./components/favorites/favorites.module#AppFavoritesModule'
component: FilesComponent,
data: {
sortingPreferenceKey: 'personal-files',
title: 'APP.BROWSE.PERSONAL.TITLE',
defaultNodeId: '-my-'
}
},
{
path: 'preview/:nodeId',
loadChildren: './components/preview/preview.module#PreviewModule',
path: 'view/:nodeId',
outlet: 'viewer',
children: [
{
path: '',
data: {
navigateSource: 'personal-files'
},
loadChildren:
'./components/viewer/viewer.module#AppViewerModule'
}
]
}
]
},
{
path: 'personal-files/:folderId',
children: [
{
path: '',
component: FilesComponent,
data: {
navigateSource: 'favorites'
title: 'APP.BROWSE.PERSONAL.TITLE',
sortingPreferenceKey: 'personal-files'
}
},
{
path: 'view/:nodeId',
outlet: 'viewer',
children: [
{
path: '',
data: {
navigateSource: 'personal-files'
},
loadChildren:
'./components/viewer/viewer.module#AppViewerModule'
}
]
}
]
},
@ -106,9 +144,14 @@ export const APP_ROUTES: Routes = [
title: 'APP.BROWSE.LIBRARIES.MENU.MY_LIBRARIES.TITLE',
sortingPreferenceKey: 'libraries'
}
},
}
]
},
{
path: 'libraries/:folderId',
children: [
{
path: ':folderId',
path: '',
component: FilesComponent,
data: {
title: 'APP.BROWSE.LIBRARIES.MENU.MY_LIBRARIES.TITLE',
@ -116,69 +159,59 @@ export const APP_ROUTES: Routes = [
}
},
{
path: ':folderId/preview/:nodeId',
loadChildren: './components/preview/preview.module#PreviewModule',
data: {
navigateSource: 'libraries'
}
path: 'view/:nodeId',
outlet: 'viewer',
children: [
{
path: '',
data: {
navigateSource: 'libraries'
},
loadChildren:
'./components/viewer/viewer.module#AppViewerModule'
}
]
}
]
},
{
path: 'favorite/libraries',
component: FavoriteLibrariesComponent,
data: {
title: 'APP.BROWSE.LIBRARIES.MENU.FAVORITE_LIBRARIES.TITLE',
sortingPreferenceKey: 'favorite-libraries'
}
children: [
{
path: '',
component: FavoriteLibrariesComponent,
data: {
title: 'APP.BROWSE.LIBRARIES.MENU.FAVORITE_LIBRARIES.TITLE',
sortingPreferenceKey: 'favorite-libraries'
}
}
]
},
{
path: 'personal-files',
path: 'favorites',
data: {
sortingPreferenceKey: 'personal-files'
sortingPreferenceKey: 'favorites'
},
children: [
{
path: '',
component: FilesComponent,
data: {
title: 'APP.BROWSE.PERSONAL.TITLE',
defaultNodeId: '-my-'
}
loadChildren:
'./components/favorites/favorites.module#AppFavoritesModule'
},
{
path: ':folderId',
component: FilesComponent,
data: {
title: 'APP.BROWSE.PERSONAL.TITLE'
}
},
{
path: 'preview/:nodeId',
loadChildren: './components/preview/preview.module#PreviewModule',
data: {
navigateSource: 'personal-files'
}
},
{
path: ':folderId/preview/:nodeId',
loadChildren: './components/preview/preview.module#PreviewModule',
data: {
navigateSource: 'personal-files'
}
path: 'view/:nodeId',
outlet: 'viewer',
children: [
{
path: '',
data: {
navigateSource: 'favorites'
},
loadChildren:
'./components/viewer/viewer.module#AppViewerModule'
}
]
}
// Do not remove, will be enabled in future iterations
// {
// path: 'view/:nodeId',
// outlet: 'viewer',
// children: [
// {
// path: '',
// loadChildren:
// './components/viewer/viewer.module#AppViewerModule'
// }
// ]
// }
]
},
{
@ -193,11 +226,18 @@ export const APP_ROUTES: Routes = [
'./components/recent-files/recent-files.module#AppRecentFilesModule'
},
{
path: 'preview/:nodeId',
loadChildren: './components/preview/preview.module#PreviewModule',
data: {
navigateSource: 'recent-files'
}
path: 'view/:nodeId',
outlet: 'viewer',
children: [
{
path: '',
data: {
navigateSource: 'recent-files'
},
loadChildren:
'./components/viewer/viewer.module#AppViewerModule'
}
]
}
]
},
@ -210,11 +250,18 @@ export const APP_ROUTES: Routes = [
'./components/shared-files/shared-files.module#AppSharedFilesModule'
},
{
path: 'preview/:nodeId',
loadChildren: './components/preview/preview.module#PreviewModule',
data: {
navigateSource: 'shared'
}
path: 'view/:nodeId',
outlet: 'viewer',
children: [
{
path: '',
data: {
navigateSource: 'shared'
},
loadChildren:
'./components/viewer/viewer.module#AppViewerModule'
}
]
}
],
canActivateChild: [AppSharedRuleGuard],
@ -235,16 +282,22 @@ export const APP_ROUTES: Routes = [
path: '',
component: SearchResultsComponent,
data: {
title: 'APP.BROWSE.SEARCH.TITLE',
reuse: true
title: 'APP.BROWSE.SEARCH.TITLE'
}
},
{
path: 'preview/:nodeId',
loadChildren: './components/preview/preview.module#PreviewModule',
data: {
navigateSource: 'search'
}
path: 'view/:nodeId',
outlet: 'viewer',
children: [
{
path: '',
data: {
navigateSource: 'search'
},
loadChildren:
'./components/viewer/viewer.module#AppViewerModule'
}
]
}
]
},
@ -259,11 +312,18 @@ export const APP_ROUTES: Routes = [
}
},
{
path: 'preview/:nodeId',
loadChildren: './components/preview/preview.module#PreviewModule',
data: {
navigateSource: 'search'
}
path: 'view/:nodeId',
outlet: 'viewer',
children: [
{
path: '',
data: {
navigateSource: 'search'
},
loadChildren:
'./components/viewer/viewer.module#AppViewerModule'
}
]
}
]
},

View File

@ -20,7 +20,10 @@
</ng-container>
<ng-container *ngSwitchCase="'custom'">
<adf-dynamic-component [id]="actionRef.component"></adf-dynamic-component>
<adf-dynamic-component
[data]="actionRef.data"
[id]="actionRef.component"
></adf-dynamic-component>
</ng-container>
<ng-container *ngSwitchDefault>

View File

@ -47,7 +47,10 @@
</ng-container>
<ng-container *ngSwitchCase="'custom'">
<adf-dynamic-component [id]="entry.component"></adf-dynamic-component>
<adf-dynamic-component
[data]="entry.data"
[id]="entry.component"
></adf-dynamic-component>
</ng-container>
</ng-container>
</mat-menu>

View File

@ -25,12 +25,18 @@
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { Router } from '@angular/router';
import { TestBed, ComponentFixture } from '@angular/core/testing';
import {
TestBed,
ComponentFixture,
fakeAsync,
tick
} from '@angular/core/testing';
import {
AlfrescoApiService,
NodeFavoriteDirective,
DataTableComponent,
AppConfigPipe
AppConfigPipe,
UploadService
} from '@alfresco/adf-core';
import { DocumentListComponent } from '@alfresco/adf-content-services';
import { of } from 'rxjs';
@ -44,8 +50,13 @@ describe('FavoritesComponent', () => {
let alfrescoApi: AlfrescoApiService;
let contentApi: ContentApiService;
let router: Router;
const mockRouter = {
url: 'favorites',
navigate: () => {}
};
let page;
let node;
let uploadService: UploadService;
beforeEach(() => {
page = {
@ -78,6 +89,12 @@ describe('FavoritesComponent', () => {
FavoritesComponent,
AppConfigPipe
],
providers: [
{
provide: Router,
useValue: mockRouter
}
],
schemas: [NO_ERRORS_SCHEMA]
});
@ -91,6 +108,7 @@ describe('FavoritesComponent', () => {
);
contentApi = TestBed.get(ContentApiService);
uploadService = TestBed.get(UploadService);
router = TestBed.get(Router);
});
@ -132,14 +150,44 @@ describe('FavoritesComponent', () => {
});
});
describe('refresh', () => {
it('should call document list reload', () => {
spyOn(component, 'reload');
fixture.detectChanges();
it('should call document list reload on fileUploadComplete event', fakeAsync(() => {
spyOn(component, 'reload');
component.reload();
fixture.detectChanges();
uploadService.fileUploadComplete.next();
tick(500);
expect(component.reload).toHaveBeenCalled();
});
expect(component.reload).toHaveBeenCalled();
}));
it('should call document list reload on fileUploadDeleted event', fakeAsync(() => {
spyOn(component, 'reload');
fixture.detectChanges();
uploadService.fileUploadDeleted.next();
tick(500);
expect(component.reload).toHaveBeenCalled();
}));
it('should navigate if node is folder', () => {
const nodeEntity = <any>{ entry: { isFolder: true } };
spyOn(component, 'navigate').and.stub();
fixture.detectChanges();
component.onNodeDoubleClick(nodeEntity);
expect(component.navigate).toHaveBeenCalledWith(nodeEntity.entry);
});
it('should call showPreview if node is file', () => {
const nodeEntity = <any>{ entry: { isFile: true } };
spyOn(component, 'showPreview').and.stub();
fixture.detectChanges();
component.onNodeDoubleClick(nodeEntity);
expect(component.showPreview).toHaveBeenCalledWith(
nodeEntity,
mockRouter.url
);
});
});

View File

@ -112,7 +112,7 @@ export class FavoritesComponent extends PageComponent implements OnInit {
}
if (node.entry.isFile) {
this.showPreview(node);
this.showPreview(node, this.router.url);
}
}
}

View File

@ -49,9 +49,12 @@ describe('FilesComponent', () => {
let fixture: ComponentFixture<FilesComponent>;
let component: FilesComponent;
let uploadService: UploadService;
let router: Router;
let nodeActionsService: NodeActionsService;
let contentApi: ContentApiService;
let router = {
url: '',
navigate: jasmine.createSpy('navigate')
};
beforeEach(() => {
TestBed.configureTestingModule({
@ -64,6 +67,10 @@ describe('FilesComponent', () => {
AppConfigPipe
],
providers: [
{
provide: Router,
useValue: router
},
{
provide: ActivatedRoute,
useValue: {
@ -122,7 +129,6 @@ describe('FilesComponent', () => {
node.isFolder = false;
node.parentId = 'parent-id';
spyOn(contentApi, 'getNode').and.returnValue(of({ entry: node }));
spyOn(router, 'navigate');
fixture.detectChanges();
@ -231,24 +237,24 @@ describe('FilesComponent', () => {
describe('Node navigation', () => {
beforeEach(() => {
spyOn(contentApi, 'getNode').and.returnValue(of({ entry: node }));
spyOn(router, 'navigate');
fixture.detectChanges();
});
it('should navigates to node when id provided', () => {
router.url = '/personal-files';
component.navigate(node.id);
expect(router.navigate).toHaveBeenCalledWith(
['./', node.id],
jasmine.any(Object)
);
expect(router.navigate).toHaveBeenCalledWith([
'/personal-files',
node.id
]);
});
it('should navigates to home when id not provided', () => {
router.url = '/personal-files';
component.navigate();
expect(router.navigate).toHaveBeenCalledWith(['./'], jasmine.any(Object));
expect(router.navigate).toHaveBeenCalledWith(['/personal-files']);
});
it('should navigate home if node is root', () => {
@ -258,9 +264,10 @@ describe('FilesComponent', () => {
}
};
router.url = '/personal-files';
component.navigate(node.id);
expect(router.navigate).toHaveBeenCalledWith(['./'], jasmine.any(Object));
expect(router.navigate).toHaveBeenCalledWith(['/personal-files']);
});
});

View File

@ -131,15 +131,15 @@ export class FilesComponent extends PageComponent implements OnInit, OnDestroy {
}
navigate(nodeId: string = null) {
const commands = ['./'];
const location = this.router.url.match(/.*?(?=\/|$)/g)[1];
const commands = [`/${location}`];
if (nodeId && !this.isRootNode(nodeId)) {
commands.push(nodeId);
}
this.router.navigate(commands, {
relativeTo: this.route.parent
});
this.router.navigate(commands);
}
navigateTo(node: MinimalNodeEntity) {
@ -151,7 +151,7 @@ export class FilesComponent extends PageComponent implements OnInit, OnDestroy {
return;
}
this.showPreview(node);
this.showPreview(node, this.router.url);
}
}

View File

@ -29,13 +29,10 @@ import { AppConfigService, UserPreferencesService } from '@alfresco/adf-core';
import { AppLayoutComponent } from './app-layout.component';
import { AppTestingModule } from '../../../testing/app-testing.module';
import { Store } from '@ngrx/store';
import {
AppStore,
SetSelectedNodesAction,
getAppSelection
} from '@alfresco/aca-shared/store';
import { AppStore, SetSelectedNodesAction } from '@alfresco/aca-shared/store';
import { Router, NavigationStart } from '@angular/router';
import { Subject } from 'rxjs';
import { ResetSelectionAction } from '@alfresco/aca-shared/store';
class MockRouter {
private url = 'some-url';
@ -139,30 +136,17 @@ describe('AppLayoutComponent', () => {
});
});
it('should reset selection before navigation', done => {
fixture.detectChanges();
it('should reset selection before navigation', () => {
const selection = [<any>{ entry: { id: 'nodeId', name: 'name' } }];
spyOn(store, 'dispatch').and.stub();
fixture.detectChanges();
store.dispatch(new SetSelectedNodesAction(selection));
router.navigateByUrl('somewhere/over/the/rainbow');
fixture.detectChanges();
store.select(getAppSelection).subscribe(state => {
expect(state.isEmpty).toBe(true);
done();
});
});
it('should not reset selection if route is `/search`', done => {
fixture.detectChanges();
const selection = [<any>{ entry: { id: 'nodeId', name: 'name' } }];
store.dispatch(new SetSelectedNodesAction(selection));
router.navigateByUrl('/search;q=');
fixture.detectChanges();
store.select(getAppSelection).subscribe(state => {
expect(state.isEmpty).toBe(false);
done();
});
expect(store.dispatch['calls'].mostRecent().args).toEqual([
new ResetSelectionAction()
]);
});
it('should close menu on mobile screen size', () => {

View File

@ -44,7 +44,7 @@ import { BreakpointObserver } from '@angular/cdk/layout';
import {
AppStore,
getCurrentFolder,
SetSelectedNodesAction
ResetSelectionAction
} from '@alfresco/aca-shared/store';
import { Directionality } from '@angular/cdk/bidi';
@ -139,16 +139,12 @@ export class AppLayoutComponent implements OnInit, OnDestroy {
this.router.events
.pipe(
filter(event => {
return (
event instanceof NavigationStart &&
// search employs reuse route strategy
!event.url.startsWith('/search;')
);
}),
filter(event => event instanceof NavigationStart),
takeUntil(this.onDestroy$)
)
.subscribe(() => this.store.dispatch(new SetSelectedNodesAction([])));
.subscribe(() => {
this.store.dispatch(new ResetSelectionAction());
});
}
ngOnDestroy() {

View File

@ -37,13 +37,13 @@ import { AppExtensionService } from '../extensions/extension.service';
import { ContentManagementService } from '../services/content-management.service';
import {
AppStore,
ViewFileAction,
ReloadDocumentListAction,
getCurrentFolder,
getAppSelection,
getDocumentDisplayMode,
isInfoDrawerOpened,
getSharedUrl
getSharedUrl,
ViewNodeAction
} from '@alfresco/aca-shared/store';
import { isLocked, isLibrary } from '../utils/node.utils';
@ -105,10 +105,12 @@ export abstract class PageComponent implements OnInit, OnDestroy {
this.onDestroy$.complete();
}
showPreview(node: MinimalNodeEntity) {
showPreview(node: MinimalNodeEntity, location?: string) {
if (node && node.entry) {
const parentId = this.node ? this.node.id : null;
this.store.dispatch(new ViewFileAction(node, parentId));
const id =
(<any>node).entry.nodeId || (<any>node).entry.guid || node.entry.id;
this.store.dispatch(new ViewNodeAction(id, location));
}
}

View File

@ -23,23 +23,34 @@
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
*/
import { TestBed, ComponentFixture } from '@angular/core/testing';
import {
TestBed,
ComponentFixture,
fakeAsync,
tick
} from '@angular/core/testing';
import { NO_ERRORS_SCHEMA } from '@angular/core';
import {
AlfrescoApiService,
NodeFavoriteDirective,
DataTableComponent,
AppConfigPipe
AppConfigPipe,
UploadService
} from '@alfresco/adf-core';
import { DocumentListComponent } from '@alfresco/adf-content-services';
import { RecentFilesComponent } from './recent-files.component';
import { AppTestingModule } from '../../testing/app-testing.module';
import { Router } from '@angular/router';
describe('RecentFilesComponent', () => {
let fixture: ComponentFixture<RecentFilesComponent>;
let component: RecentFilesComponent;
let alfrescoApi: AlfrescoApiService;
let page;
let uploadService: UploadService;
const mockRouter = {
url: 'recent-files'
};
beforeEach(() => {
page = {
@ -60,6 +71,12 @@ describe('RecentFilesComponent', () => {
RecentFilesComponent,
AppConfigPipe
],
providers: [
{
provide: Router,
useValue: mockRouter
}
],
schemas: [NO_ERRORS_SCHEMA]
});
@ -67,6 +84,7 @@ describe('RecentFilesComponent', () => {
component = fixture.componentInstance;
alfrescoApi = TestBed.get(AlfrescoApiService);
uploadService = TestBed.get(UploadService);
alfrescoApi.reset();
spyOn(alfrescoApi.peopleApi, 'getPerson').and.returnValue(
@ -80,14 +98,32 @@ describe('RecentFilesComponent', () => {
);
});
describe('refresh', () => {
it('should call document list reload', () => {
spyOn(component, 'reload');
fixture.detectChanges();
it('should call document list reload on fileUploadComplete event', fakeAsync(() => {
spyOn(component, 'reload');
component.reload();
fixture.detectChanges();
uploadService.fileUploadComplete.next();
tick(500);
expect(component.reload).toHaveBeenCalled();
});
expect(component.reload).toHaveBeenCalled();
}));
it('should call document list reload on fileUploadDeleted event', fakeAsync(() => {
spyOn(component, 'reload');
fixture.detectChanges();
uploadService.fileUploadDeleted.next();
tick(500);
expect(component.reload).toHaveBeenCalled();
}));
it('should call showPreview method', () => {
const node = <any>{ entry: {} };
spyOn(component, 'showPreview');
fixture.detectChanges();
component.onNodeDoubleClick(node);
expect(component.showPreview).toHaveBeenCalledWith(node, mockRouter.url);
});
});

View File

@ -33,6 +33,7 @@ import { AppStore } from '@alfresco/aca-shared/store';
import { AppExtensionService } from '../../extensions/extension.service';
import { UploadService } from '@alfresco/adf-core';
import { debounceTime } from 'rxjs/operators';
import { Router } from '@angular/router';
@Component({
templateUrl: './recent-files.component.html'
@ -47,7 +48,8 @@ export class RecentFilesComponent extends PageComponent implements OnInit {
extensions: AppExtensionService,
content: ContentManagementService,
private uploadService: UploadService,
private breakpointObserver: BreakpointObserver
private breakpointObserver: BreakpointObserver,
private router: Router
) {
super(store, extensions, content);
}
@ -75,7 +77,7 @@ export class RecentFilesComponent extends PageComponent implements OnInit {
onNodeDoubleClick(node: MinimalNodeEntity) {
if (node && node.entry) {
this.showPreview(node);
this.showPreview(node, this.router.url);
}
}

View File

@ -32,11 +32,12 @@ import {
OnDestroy
} from '@angular/core';
import { MinimalNodeEntity } from '@alfresco/js-api';
import { ViewFileAction, NavigateToFolder } from '@alfresco/aca-shared/store';
import { ViewNodeAction, NavigateToFolder } from '@alfresco/aca-shared/store';
import { Store } from '@ngrx/store';
import { BehaviorSubject, Subject } from 'rxjs';
import { AlfrescoApiService } from '@alfresco/adf-core';
import { takeUntil } from 'rxjs/operators';
import { Router } from '@angular/router';
@Component({
selector: 'aca-search-results-row',
@ -58,7 +59,8 @@ export class SearchResultsRowComponent implements OnInit, OnDestroy {
constructor(
private store: Store<any>,
private alfrescoApiService: AlfrescoApiService
private alfrescoApiService: AlfrescoApiService,
private router: Router
) {}
ngOnInit() {
@ -123,7 +125,9 @@ export class SearchResultsRowComponent implements OnInit, OnDestroy {
showPreview(event: MouseEvent) {
event.stopPropagation();
this.store.dispatch(new ViewFileAction(this.node));
this.store.dispatch(
new ViewNodeAction(this.node.entry.id, this.router.url)
);
}
navigate(event: MouseEvent) {

View File

@ -47,7 +47,7 @@ import {
} from '@alfresco/aca-shared/store';
import { Pagination } from '@alfresco/js-api';
import { SearchQueryBuilderService } from '@alfresco/adf-content-services';
import { ActivatedRoute } from '@angular/router';
import { ActivatedRoute, Router } from '@angular/router';
describe('SearchComponent', () => {
let component: SearchResultsComponent;
@ -57,6 +57,7 @@ describe('SearchComponent', () => {
let queryBuilder: SearchQueryBuilderService;
let alfrescoApi: AlfrescoApiService;
let translate: TranslationService;
let router: Router;
beforeEach(() => {
TestBed.configureTestingModule({
@ -90,6 +91,7 @@ describe('SearchComponent', () => {
queryBuilder = TestBed.get(SearchQueryBuilderService);
alfrescoApi = TestBed.get(AlfrescoApiService);
translate = TestBed.get(TranslationService);
router = TestBed.get(Router);
fixture = TestBed.createComponent(SearchResultsComponent);
component = fixture.componentInstance;
@ -303,7 +305,7 @@ describe('SearchComponent', () => {
component.onNodeDoubleClick(node);
expect(component.showPreview).toHaveBeenCalledWith(node);
expect(component.showPreview).toHaveBeenCalledWith(node, router.url);
});
it('should re-run search on pagination change', () => {

View File

@ -25,7 +25,7 @@
import { Component, OnInit, ViewChild } from '@angular/core';
import { NodePaging, Pagination, MinimalNodeEntity } from '@alfresco/js-api';
import { ActivatedRoute, Params } from '@angular/router';
import { ActivatedRoute, Params, Router } from '@angular/router';
import {
SearchQueryBuilderService,
SearchFilterComponent
@ -69,7 +69,8 @@ export class SearchResultsComponent extends PageComponent implements OnInit {
store: Store<AppStore>,
extensions: AppExtensionService,
content: ContentManagementService,
private translationService: TranslationService
private translationService: TranslationService,
private router: Router
) {
super(store, extensions, content);
@ -251,7 +252,7 @@ export class SearchResultsComponent extends PageComponent implements OnInit {
return;
}
this.showPreview(node);
this.showPreview(node, this.router.url);
}
}

View File

@ -19,8 +19,8 @@
currentFolderId="-sharedlinks-"
selectionMode="multiple"
[sorting]="['modifiedAt', 'desc']"
(node-dblclick)="showPreview($event.detail?.node)"
(name-click)="showPreview($event.detail?.node)"
(node-dblclick)="preview($event.detail?.node)"
(name-click)="preview($event.detail?.node)"
>
<adf-custom-empty-content-template>
<adf-empty-content

View File

@ -23,23 +23,36 @@
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
*/
import { TestBed, ComponentFixture } from '@angular/core/testing';
import {
TestBed,
ComponentFixture,
fakeAsync,
tick
} from '@angular/core/testing';
import { NO_ERRORS_SCHEMA } from '@angular/core';
import {
AlfrescoApiService,
NodeFavoriteDirective,
DataTableComponent,
AppConfigPipe
AppConfigPipe,
UploadService
} from '@alfresco/adf-core';
import { DocumentListComponent } from '@alfresco/adf-content-services';
import { SharedFilesComponent } from './shared-files.component';
import { AppTestingModule } from '../../testing/app-testing.module';
import { Router } from '@angular/router';
import { ContentManagementService } from '../../services/content-management.service';
describe('SharedFilesComponent', () => {
let fixture: ComponentFixture<SharedFilesComponent>;
let component: SharedFilesComponent;
let alfrescoApi: AlfrescoApiService;
let page;
let uploadService: UploadService;
let contentManagementService: ContentManagementService;
const mockRouter = {
url: 'shared-files'
};
beforeEach(() => {
page = {
@ -60,10 +73,18 @@ describe('SharedFilesComponent', () => {
SharedFilesComponent,
AppConfigPipe
],
providers: [
{
provide: Router,
useValue: mockRouter
}
],
schemas: [NO_ERRORS_SCHEMA]
});
fixture = TestBed.createComponent(SharedFilesComponent);
uploadService = TestBed.get(UploadService);
contentManagementService = TestBed.get(ContentManagementService);
component = fixture.componentInstance;
alfrescoApi = TestBed.get(AlfrescoApiService);
@ -74,14 +95,42 @@ describe('SharedFilesComponent', () => {
);
});
describe('refresh', () => {
it('should call document list reload', () => {
spyOn(component, 'reload');
fixture.detectChanges();
it('should call document list reload on linksUnshared event', fakeAsync(() => {
spyOn(component, 'reload');
component.reload();
fixture.detectChanges();
contentManagementService.linksUnshared.next();
tick(500);
expect(component.reload).toHaveBeenCalled();
});
expect(component.reload).toHaveBeenCalled();
}));
it('should call document list reload on fileUploadComplete event', fakeAsync(() => {
spyOn(component, 'reload');
fixture.detectChanges();
uploadService.fileUploadComplete.next();
tick(500);
expect(component.reload).toHaveBeenCalled();
}));
it('should call document list reload on fileUploadDeleted event', fakeAsync(() => {
spyOn(component, 'reload');
fixture.detectChanges();
uploadService.fileUploadDeleted.next();
tick(500);
expect(component.reload).toHaveBeenCalled();
}));
it('should call showPreview method', () => {
const node = <any>{ entry: {} };
spyOn(component, 'showPreview');
fixture.detectChanges();
component.preview(node);
expect(component.showPreview).toHaveBeenCalledWith(node, mockRouter.url);
});
});

View File

@ -31,6 +31,8 @@ import { Store } from '@ngrx/store';
import { AppExtensionService } from '../../extensions/extension.service';
import { debounceTime } from 'rxjs/operators';
import { UploadService } from '@alfresco/adf-core';
import { Router } from '@angular/router';
import { MinimalNodeEntity } from '@alfresco/js-api';
@Component({
templateUrl: './shared-files.component.html'
@ -45,7 +47,8 @@ export class SharedFilesComponent extends PageComponent implements OnInit {
extensions: AppExtensionService,
content: ContentManagementService,
private uploadService: UploadService,
private breakpointObserver: BreakpointObserver
private breakpointObserver: BreakpointObserver,
private router: Router
) {
super(store, extensions, content);
}
@ -74,4 +77,8 @@ export class SharedFilesComponent extends PageComponent implements OnInit {
this.columns = this.extensions.documentListPresets.shared || [];
}
preview(node: MinimalNodeEntity) {
this.showPreview(node, this.router.url);
}
}

View File

@ -20,6 +20,9 @@
</ng-container>
<ng-container *ngSwitchCase="'custom'">
<adf-dynamic-component [id]="actionRef.component"></adf-dynamic-component>
<adf-dynamic-component
[data]="actionRef.data"
[id]="actionRef.component"
></adf-dynamic-component>
</ng-container>
</ng-container>

View File

@ -39,6 +39,7 @@ import { ToggleJoinLibraryMenuComponent } from './toggle-join-library/toggle-joi
import { DirectivesModule } from '../../directives/directives.module';
import { ToggleFavoriteLibraryComponent } from './toggle-favorite-library/toggle-favorite-library.component';
import { ToggleEditOfflineComponent } from './toggle-edit-offline/toggle-edit-offline.component';
import { ViewNodeComponent } from './view-node/view-node.component';
import { AppCommonModule } from '../common/common.module';
export function components() {
@ -53,7 +54,8 @@ export function components() {
ToggleJoinLibraryButtonComponent,
ToggleJoinLibraryMenuComponent,
ToggleFavoriteLibraryComponent,
ToggleEditOfflineComponent
ToggleEditOfflineComponent,
ViewNodeComponent
];
}

View File

@ -0,0 +1,105 @@
/*!
* @license
* Alfresco Example Content Application
*
* Copyright (C) 2005 - 2019 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 { ViewNodeComponent } from './view-node.component';
import { Store } from '@ngrx/store';
import { CoreModule } from '@alfresco/adf-core';
import { Router } from '@angular/router';
import { of } from 'rxjs';
describe('ToggleFavoriteComponent', () => {
let component: ViewNodeComponent;
let fixture;
const mockRouter = {
url: 'some-url'
};
const mockStore = <any>{
dispatch: jasmine.createSpy('dispatch'),
select: jasmine.createSpy('select').and.returnValue(
of({
file: {
entry: {
id: 'nodeId'
}
}
})
)
};
beforeEach(() => {
TestBed.configureTestingModule({
imports: [CoreModule.forRoot()],
declarations: [ViewNodeComponent],
providers: [
{ provide: Store, useValue: mockStore },
{ provide: Router, useValue: mockRouter }
]
});
fixture = TestBed.createComponent(ViewNodeComponent);
component = fixture.componentInstance;
});
afterEach(() => {
mockStore.dispatch.calls.reset();
});
it('should render as a menu button', () => {
component.data = {
menuButton: true
};
fixture.detectChanges();
expect(fixture.nativeElement.querySelector('.mat-menu-item')).not.toBe(
null
);
});
it('should render as a icon button', () => {
component.data = {
iconButton: true
};
fixture.detectChanges();
expect(fixture.nativeElement.querySelector('.mat-icon-button')).not.toBe(
null
);
});
it('should call ViewNodeAction onClick event', () => {
component.data = {
iconButton: true
};
fixture.detectChanges();
component.onClick();
expect(mockStore.dispatch).toHaveBeenCalled();
});
});

View File

@ -0,0 +1,75 @@
/*!
* @license
* Alfresco Example Content Application
*
* Copyright (C) 2005 - 2019 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, Input } from '@angular/core';
import { Store } from '@ngrx/store';
import {
AppStore,
ViewNodeAction,
getAppSelection
} from '@alfresco/aca-shared/store';
import { Router } from '@angular/router';
import { take } from 'rxjs/operators';
import { SharedLinkEntry } from '@alfresco/js-api';
@Component({
selector: 'app-view-node',
template: `
<button
*ngIf="data.iconButton"
mat-icon-button
[attr.title]="data.title | translate"
(click)="onClick()"
>
<mat-icon>visibility</mat-icon>
</button>
<button *ngIf="data.menuButton" mat-menu-item (click)="onClick()">
<mat-icon>visibility</mat-icon>
<span>{{ data.title | translate }}</span>
</button>
`,
encapsulation: ViewEncapsulation.None,
host: { class: 'app-view-node' }
})
export class ViewNodeComponent {
@Input() data: any;
constructor(private store: Store<AppStore>, private router: Router) {}
onClick() {
this.store
.select(getAppSelection)
.pipe(take(1))
.subscribe(selection => {
const id =
(<SharedLinkEntry>selection.file).entry.nodeId ||
(<any>selection.file).entry.guid ||
selection.file.entry.id;
this.store.dispatch(new ViewNodeAction(id, this.router.url));
});
}
}

View File

@ -1,12 +1,21 @@
<ng-container *ngIf="nodeId">
<adf-viewer
[ngClass]="{
'right_side--hide': !showRightSide
}"
[nodeId]="nodeId"
[allowNavigate]="false"
[allowNavigate]="navigateMultiple"
[allowRightSidebar]="true"
[allowPrint]="false"
[showRightSidebar]="true"
[allowDownload]="false"
[allowFullScreen]="false"
[allowRightSidebar]="showRightSide"
[showRightSidebar]="showRightSide"
[overlayMode]="true"
(showViewerChange)="onViewerVisibilityChanged($event)"
[canNavigateBefore]="previousNodeId"
[canNavigateNext]="nextNodeId"
(navigateBefore)="onNavigateBefore()"
(navigateNext)="onNavigateNext()"
>
<adf-viewer-sidebar *ngIf="infoDrawerOpened$ | async">
<aca-info-drawer [node]="selection.file"></aca-info-drawer>

View File

@ -20,4 +20,8 @@
.adf-viewer-toolbar .mat-toolbar > button:last-child {
display: none;
}
.adf-viewer.right_side--hide .adf-viewer__sidebar__right {
width: 0;
}
}

View File

@ -28,17 +28,29 @@ import {
AppStore,
getAppSelection,
isInfoDrawerOpened,
SetSelectedNodesAction
SetSelectedNodesAction,
ClosePreviewAction,
ViewerActionTypes,
ViewNodeAction
} from '@alfresco/aca-shared/store';
import { ContentActionRef, SelectionState } from '@alfresco/adf-extensions';
import { MinimalNodeEntryEntity } from '@alfresco/js-api';
import { Component, OnDestroy, OnInit, ViewEncapsulation } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { ActivatedRoute, Router, PRIMARY_OUTLET } from '@angular/router';
import {
UserPreferencesService,
ObjectUtils,
UploadService,
AlfrescoApiService
} from '@alfresco/adf-core';
import { ContentManagementService } from '../../services/content-management.service';
import { Store } from '@ngrx/store';
import { from, Observable, Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { takeUntil, debounceTime } from 'rxjs/operators';
import { AppExtensionService } from '../../extensions/extension.service';
import { Actions, ofType } from '@ngrx/effects';
import { SearchRequest } from '@alfresco/js-api';
import { ReloadDocumentListAction } from '@alfresco/aca-shared/store';
@Component({
selector: 'app-viewer',
templateUrl: './viewer.component.html',
@ -49,6 +61,7 @@ import { AppExtensionService } from '../../extensions/extension.service';
export class AppViewerComponent implements OnInit, OnDestroy {
onDestroy$ = new Subject<boolean>();
folderId: string = null;
nodeId: string = null;
node: MinimalNodeEntryEntity;
selection: SelectionState;
@ -58,11 +71,55 @@ export class AppViewerComponent implements OnInit, OnDestroy {
openWith: ContentActionRef[] = [];
toolbarActions: ContentActionRef[] = [];
navigateSource: string = null;
previousNodeId: string;
nextNodeId: string;
navigateMultiple = true;
routesSkipNavigation = ['shared', 'recent-files', 'favorites'];
navigationSources = [
'favorites',
'libraries',
'personal-files',
'recent-files',
'shared'
];
recentFileFilters = [
'TYPE:"content"',
'-PNAME:"0/wiki"',
'-TYPE:"app:filelink"',
'-TYPE:"fm:post"',
'-TYPE:"cm:thumbnail"',
'-TYPE:"cm:failedThumbnail"',
'-TYPE:"cm:rating"',
'-TYPE:"dl:dataList"',
'-TYPE:"dl:todoList"',
'-TYPE:"dl:issue"',
'-TYPE:"dl:contact"',
'-TYPE:"dl:eventAgenda"',
'-TYPE:"dl:event"',
'-TYPE:"dl:task"',
'-TYPE:"dl:simpletask"',
'-TYPE:"dl:meetingAgenda"',
'-TYPE:"dl:location"',
'-TYPE:"fm:topic"',
'-TYPE:"fm:post"',
'-TYPE:"ia:calendarEvent"',
'-TYPE:"lnk:link"'
];
private previewLocation: string;
constructor(
private router: Router,
private route: ActivatedRoute,
private store: Store<AppStore>,
protected extensions: AppExtensionService,
private contentApi: ContentApiService
private extensions: AppExtensionService,
private contentApi: ContentApiService,
private actions$: Actions,
private preferences: UserPreferencesService,
private content: ContentManagementService,
private apiService: AlfrescoApiService,
private uploadService: UploadService
) {}
ngOnInit() {
@ -85,11 +142,50 @@ export class AppViewerComponent implements OnInit, OnDestroy {
});
this.route.params.subscribe(params => {
this.folderId = params.folderId;
const { nodeId } = params;
if (nodeId) {
this.displayNode(nodeId);
}
});
if (this.route.snapshot.data && this.route.snapshot.data.navigateSource) {
const source = this.route.snapshot.data.navigateSource.toLowerCase();
if (this.navigationSources.includes(source)) {
this.navigateSource = this.route.snapshot.data.navigateSource;
}
}
this.actions$
.pipe(
ofType<ClosePreviewAction>(ViewerActionTypes.ClosePreview),
takeUntil(this.onDestroy$)
)
.subscribe(() => this.navigateToFileLocation());
this.content.nodesDeleted
.pipe(takeUntil(this.onDestroy$))
.subscribe(() => this.navigateToFileLocation());
this.uploadService.fileUploadDeleted
.pipe(takeUntil(this.onDestroy$))
.subscribe(() => this.navigateToFileLocation());
this.uploadService.fileUploadComplete
.pipe(
debounceTime(300),
takeUntil(this.onDestroy$)
)
.subscribe(file => this.apiService.nodeUpdated.next(file.data.entry));
this.previewLocation = this.router.url
.substr(0, this.router.url.indexOf('/', 1))
.replace(/\//g, '');
}
onViewerVisibilityChanged() {
this.store.dispatch(new ReloadDocumentListAction());
this.navigateToFileLocation();
}
ngOnDestroy() {
@ -108,14 +204,228 @@ export class AppViewerComponent implements OnInit, OnDestroy {
this.store.dispatch(new SetSelectedNodesAction([{ entry: this.node }]));
if (this.node && this.node.isFile) {
const nearest = await this.getNearestNodes(
this.node.id,
this.node.parentId
);
this.previousNodeId = nearest.left;
this.nextNodeId = nearest.right;
this.nodeId = this.node.id;
return;
}
} catch (err) {
if (!err || err.status !== 401) {
// this.router.navigate([this.previewLocation, id]);
} catch (error) {
const statusCode = JSON.parse(error.message).error.statusCode;
if (statusCode !== 401) {
this.router
.navigate([this.previewLocation, { outlets: { viewer: null } }])
.then(() => {
this.router.navigate([this.previewLocation, id]);
});
}
}
}
}
onNavigateBefore(): void {
const location = this.getFileLocation();
this.store.dispatch(new ViewNodeAction(this.previousNodeId, location));
}
onNavigateNext(): void {
const location = this.getFileLocation();
this.store.dispatch(new ViewNodeAction(this.nextNodeId, location));
}
/**
* Retrieves nearest node information for the given node and folder.
* @param nodeId Unique identifier of the document node
* @param folderId Unique identifier of the containing folder node.
*/
async getNearestNodes(
nodeId: string,
folderId: string
): Promise<{ left: string; right: string }> {
const empty = {
left: null,
right: null
};
if (nodeId && folderId) {
try {
const ids = await this.getFileIds(this.navigateSource, folderId);
const idx = ids.indexOf(nodeId);
if (idx >= 0) {
return {
left: ids[idx - 1] || null,
right: ids[idx + 1] || null
};
} else {
return empty;
}
} catch {
return empty;
}
} else {
return empty;
}
}
/**
* Retrieves a list of node identifiers for the folder and data source.
* @param source Data source name. Allowed values are: personal-files, libraries, favorites, shared, recent-files.
* @param folderId Containing folder node identifier for 'personal-files' and 'libraries' sources.
*/
async getFileIds(source: string, folderId?: string): Promise<string[]> {
if ((source === 'personal-files' || source === 'libraries') && folderId) {
const sortKey =
this.preferences.get('personal-files.sorting.key') || 'modifiedAt';
const sortDirection =
this.preferences.get('personal-files.sorting.direction') || 'desc';
const nodes = await this.contentApi
.getNodeChildren(folderId, {
// orderBy: `${sortKey} ${sortDirection}`,
fields: ['id', this.getRootField(sortKey)],
where: '(isFile=true)'
})
.toPromise();
const entries = nodes.list.entries.map(obj => obj.entry);
this.sort(entries, sortKey, sortDirection);
return entries.map(obj => obj.id);
}
if (source === 'favorites') {
const nodes = await this.contentApi
.getFavorites('-me-', {
where: '(EXISTS(target/file))',
fields: ['target']
})
.toPromise();
const sortKey =
this.preferences.get('favorites.sorting.key') || 'modifiedAt';
const sortDirection =
this.preferences.get('favorites.sorting.direction') || 'desc';
const files = nodes.list.entries.map(obj => obj.entry.target.file);
this.sort(files, sortKey, sortDirection);
return files.map(f => f.id);
}
if (source === 'shared') {
const sortingKey =
this.preferences.get('shared.sorting.key') || 'modifiedAt';
const sortingDirection =
this.preferences.get('shared.sorting.direction') || 'desc';
const nodes = await this.contentApi
.findSharedLinks({
fields: ['nodeId', this.getRootField(sortingKey)]
})
.toPromise();
const entries = nodes.list.entries.map(obj => obj.entry);
this.sort(entries, sortingKey, sortingDirection);
return entries.map(obj => obj.nodeId);
}
if (source === 'recent-files') {
const person = await this.contentApi.getPerson('-me-').toPromise();
const username = person.entry.id;
const sortingKey =
this.preferences.get('recent-files.sorting.key') || 'modifiedAt';
const sortingDirection =
this.preferences.get('recent-files.sorting.direction') || 'desc';
const query: SearchRequest = {
query: {
query: '*',
language: 'afts'
},
filterQueries: [
{ query: `cm:modified:[NOW/DAY-30DAYS TO NOW/DAY+1DAY]` },
{ query: `cm:modifier:${username} OR cm:creator:${username}` },
{
query: this.recentFileFilters.join(' AND ')
}
],
fields: ['id', this.getRootField(sortingKey)],
include: ['path', 'properties', 'allowableOperations'],
sort: [
{
type: 'FIELD',
field: 'cm:modified',
ascending: false
}
]
};
const nodes = await this.contentApi.search(query).toPromise();
const entries = nodes.list.entries.map(obj => obj.entry);
this.sort(entries, sortingKey, sortingDirection);
return entries.map(obj => obj.id);
}
return [];
}
private sort(items: any[], key: string, direction: string) {
const options: Intl.CollatorOptions = {};
if (key.includes('sizeInBytes') || key === 'name') {
options.numeric = true;
}
items.sort((a: any, b: any) => {
let left = ObjectUtils.getValue(a, key);
if (left) {
left =
left instanceof Date ? left.valueOf().toString() : left.toString();
} else {
left = '';
}
let right = ObjectUtils.getValue(b, key);
if (right) {
right =
right instanceof Date ? right.valueOf().toString() : right.toString();
} else {
right = '';
}
return direction === 'asc'
? left.localeCompare(right, undefined, options)
: right.localeCompare(left, undefined, options);
});
}
/**
* Get the root field name from the property path.
* Example: 'property1.some.child.property' => 'property1'
* @param path Property path
*/
getRootField(path: string) {
if (path) {
return path.split('.')[0];
}
return path;
}
private navigateToFileLocation() {
const location = this.getFileLocation();
this.router.navigateByUrl(location);
}
private getFileLocation(): string {
return this.router
.parseUrl(this.router.url)
.root.children[PRIMARY_OUTLET].toString();
}
}

View File

@ -37,6 +37,10 @@ import { AppViewerComponent } from './viewer.component';
const routes: Routes = [
{
path: '',
data: {
title: 'APP.PREVIEW.TITLE',
navigateMultiple: true
},
component: AppViewerComponent
}
];

View File

@ -24,9 +24,135 @@
*/
import { DocumentListDirective } from './document-list.directive';
import { Subject } from 'rxjs';
import { SetSelectedNodesAction } from '@alfresco/aca-shared/store';
describe('DocumentListDirective', () => {
it('should be defined', () => {
expect(DocumentListDirective).toBeDefined();
let documentListDirective;
const documentListMock = <any>{
currentFolderId: '',
stickyHeader: false,
includeFields: [],
sorting: [],
data: {
setSorting: jasmine.createSpy('setSorting')
},
selection: [],
reload: jasmine.createSpy('reload'),
resetSelection: jasmine.createSpy('resetSelection'),
ready: new Subject<any>()
};
const storeMock = <any>{
dispatch: jasmine.createSpy('dispatch')
};
const mockRouter = <any>{
url: ''
};
const contentManagementServiceMock = <any>{
reload: new Subject<any>(),
reset: new Subject<any>()
};
const mockRoute = <any>{
snapshot: {
data: {
sortingPreferenceKey: null
}
}
};
const userPreferencesServiceMock = <any>{
set: jasmine.createSpy('set'),
get: jasmine.createSpy('get')
};
beforeEach(() => {
documentListDirective = new DocumentListDirective(
storeMock,
contentManagementServiceMock,
documentListMock,
userPreferencesServiceMock,
mockRoute,
mockRouter
);
});
afterEach(() => {
storeMock.dispatch.calls.reset();
});
it('should not update store selection on `documentList.ready` if route includes `viewer:view`', () => {
mockRouter.url = '/some-route/(viewer:view)';
documentListDirective.ngOnInit();
documentListMock.ready.next();
expect(storeMock.dispatch).not.toHaveBeenCalled();
});
it('should update store selection on `documentList.ready`', () => {
mockRouter.url = '/some-route';
documentListDirective.ngOnInit();
documentListMock.ready.next();
expect(storeMock.dispatch).toHaveBeenCalled();
});
it('should set `isLibrary` to true if selected node is a library', () => {
mockRouter.url = '/some-route';
documentListMock.currentFolderId = '-mysites-';
documentListMock.selection = [{}];
documentListDirective.ngOnInit();
documentListMock.ready.next();
expect(storeMock.dispatch).toHaveBeenCalledWith(
new SetSelectedNodesAction([<any>{ isLibrary: true }])
);
});
it('should update store selection on `node-unselect` event', () => {
mockRouter.url = '/some-route';
documentListDirective.ngOnInit();
documentListDirective.onNodeUnselect();
expect(storeMock.dispatch).toHaveBeenCalled();
});
it('should update store selection on `node-select` event', () => {
mockRouter.url = '/some-route';
documentListDirective.ngOnInit();
documentListDirective.onNodeSelect({ detail: { node: {} } });
expect(storeMock.dispatch).toHaveBeenCalled();
});
it('should reset and reload document list on `reload` event', () => {
documentListDirective.ngOnInit();
contentManagementServiceMock.reload.next();
expect(documentListMock.resetSelection).toHaveBeenCalled();
expect(documentListMock.reload).toHaveBeenCalled();
});
it('should reset store selection on `reload` event', () => {
documentListDirective.ngOnInit();
contentManagementServiceMock.reload.next();
expect(storeMock.dispatch).toHaveBeenCalledWith(
new SetSelectedNodesAction([])
);
});
it('should reset store selection and document list on `reset` event', () => {
documentListDirective.ngOnInit();
contentManagementServiceMock.reload.next();
expect(documentListMock.resetSelection).toHaveBeenCalled();
expect(storeMock.dispatch).toHaveBeenCalledWith(
new SetSelectedNodesAction([])
);
});
});

View File

@ -30,7 +30,7 @@ import { UserPreferencesService } from '@alfresco/adf-core';
import { Subject } from 'rxjs';
import { Store } from '@ngrx/store';
import { SetSelectedNodesAction } from '@alfresco/aca-shared/store';
import { takeUntil } from 'rxjs/operators';
import { takeUntil, filter } from 'rxjs/operators';
import { ContentManagementService } from '../services/content-management.service';
@Directive({
@ -81,12 +81,19 @@ export class DocumentListDirective implements OnInit, OnDestroy {
}
this.documentList.ready
.pipe(takeUntil(this.onDestroy$))
.pipe(
filter(() => !this.router.url.includes('viewer:view')),
takeUntil(this.onDestroy$)
)
.subscribe(() => this.onReady());
this.content.reload.pipe(takeUntil(this.onDestroy$)).subscribe(() => {
this.reload();
});
this.content.reset.pipe(takeUntil(this.onDestroy$)).subscribe(() => {
this.reset();
});
}
ngOnDestroy() {
@ -138,4 +145,9 @@ export class DocumentListDirective implements OnInit, OnDestroy {
this.store.dispatch(new SetSelectedNodesAction([]));
this.documentList.reload();
}
private reset() {
this.documentList.resetSelection();
this.store.dispatch(new SetSelectedNodesAction([]));
}
}

View File

@ -50,6 +50,7 @@ import {
LibraryRoleColumnComponent
} from '@alfresco/adf-content-services';
import { ToggleSharedComponent } from '../components/common/toggle-shared/toggle-shared.component';
import { ViewNodeComponent } from '../components/toolbar/view-node/view-node.component';
export function setupExtensions(service: AppExtensionService): Function {
return () => service.load();
@ -99,7 +100,8 @@ export class CoreExtensionsModule {
'app.columns.libraryStatus': LibraryStatusColumnComponent,
'app.columns.trashcanName': TrashcanNameColumnComponent,
'app.columns.location': LocationLinkComponent,
'app.toolbar.toggleEditOffline': ToggleEditOfflineComponent
'app.toolbar.toggleEditOffline': ToggleEditOfflineComponent,
'app.toolbar.viewNode': ViewNodeComponent
});
extensions.setAuthGuards({

View File

@ -82,6 +82,7 @@ interface RestoredNode {
})
export class ContentManagementService {
reload = new Subject<any>();
reset = new Subject<any>();
nodesDeleted = new Subject<any>();
libraryDeleted = new Subject<string>();
libraryCreated = new Subject<SiteEntry>();

View File

@ -29,7 +29,8 @@ import { map } from 'rxjs/operators';
import {
AppActionTypes,
LogoutAction,
ReloadDocumentListAction
ReloadDocumentListAction,
ResetSelectionAction
} from '@alfresco/aca-shared/store';
import { AuthenticationService } from '@alfresco/adf-core';
import { Router } from '@angular/router';
@ -52,6 +53,14 @@ export class AppEffects {
})
);
@Effect({ dispatch: false })
resetSelection = this.actions$.pipe(
ofType<ResetSelectionAction>(AppActionTypes.ResetSelection),
map(action => {
this.content.reset.next(action);
})
);
@Effect({ dispatch: false })
logout$ = this.actions$.pipe(
ofType<LogoutAction>(AppActionTypes.Logout),

View File

@ -0,0 +1,95 @@
/*!
* @license
* Alfresco Example Content Application
*
* Copyright (C) 2005 - 2019 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, fakeAsync, tick } from '@angular/core/testing';
import { AppTestingModule } from '../../testing/app-testing.module';
import { ViewerEffects } from './viewer.effects';
import { EffectsModule } from '@ngrx/effects';
import { Store } from '@ngrx/store';
import { Router } from '@angular/router';
import {
ViewFileAction,
ViewNodeAction,
SetSelectedNodesAction,
SetCurrentFolderAction
} from '@alfresco/aca-shared/store';
describe('ViewerEffects', () => {
let store: Store<any>;
let router: Router;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [AppTestingModule, EffectsModule.forRoot([ViewerEffects])]
});
store = TestBed.get(Store);
router = TestBed.get(Router);
spyOn(router, 'navigateByUrl').and.stub();
});
describe('ViewFile', () => {
it('should preview file from store selection', fakeAsync(() => {
const node: any = { entry: { isFile: true, id: 'someId' } };
const folder: any = { isFolder: true, id: 'folder1' };
store.dispatch(new SetCurrentFolderAction(folder));
store.dispatch(new SetSelectedNodesAction([node]));
tick(100);
store.dispatch(new ViewFileAction());
tick(100);
expect(router.navigateByUrl).toHaveBeenCalledWith(
'/folder1/preview/someId'
);
}));
it('should preview file from payload', fakeAsync(() => {
const node: any = { entry: { isFile: true, id: 'someId' } };
store.dispatch(new ViewFileAction(node));
tick(100);
expect(router.navigateByUrl).toHaveBeenCalledWith('/preview/someId');
}));
});
describe('ViewNode', () => {
it('should open viewer from file location', fakeAsync(() => {
store.dispatch(new ViewNodeAction('nodeId', 'some-location'));
tick(100);
expect(router.navigateByUrl['calls'].argsFor(0)[0].toString()).toEqual(
'/some-location/(viewer:view/nodeId)?source=some-location'
);
}));
it('should navigate to viewer route if no location is passed', fakeAsync(() => {
store.dispatch(new ViewNodeAction('nodeId'));
tick(100);
expect(router.navigateByUrl['calls'].argsFor(0)[0].toString()).toEqual(
'/view/(viewer:nodeId)'
);
}));
});
});

View File

@ -35,7 +35,13 @@ import {
getAppSelection,
FullscreenViewerAction
} from '@alfresco/aca-shared/store';
import { Router } from '@angular/router';
import {
Router,
UrlTree,
UrlSegmentGroup,
PRIMARY_OUTLET,
UrlSegment
} from '@angular/router';
import { Store, createSelector } from '@ngrx/store';
import { AppExtensionService } from '../../extensions/extension.service';
@ -72,8 +78,10 @@ export class ViewerEffects {
ofType<ViewNodeAction>(ViewerActionTypes.ViewNode),
map(action => {
if (action.location) {
const location = this.getNavigationCommands(action.location);
this.router.navigate(
[action.location, { outlets: { viewer: ['view', action.nodeId] } }],
[...location, { outlets: { viewer: ['view', action.nodeId] } }],
{
queryParams: {
source: action.location
@ -163,4 +171,21 @@ export class ViewerEffects {
}
}
}
private getNavigationCommands(url: string): any[] {
const urlTree: UrlTree = this.router.parseUrl(url);
const urlSegmentGroup: UrlSegmentGroup =
urlTree.root.children[PRIMARY_OUTLET];
if (!urlSegmentGroup) {
return [url];
}
const urlSegments: UrlSegment[] = urlSegmentGroup.segments;
return urlSegments.reduce(function(acc, item) {
acc.push(item.path, item.parameters);
return acc;
}, []);
}
}

View File

@ -221,12 +221,13 @@
},
{
"id": "app.toolbar.preview",
"type": "custom",
"order": 300,
"title": "APP.ACTIONS.VIEW",
"icon": "visibility",
"actions": {
"click": "VIEW_FILE"
"data": {
"title": "APP.ACTIONS.VIEW",
"iconButton": true
},
"component": "app.toolbar.viewNode",
"rules": {
"visible": "canViewFile"
}
@ -504,12 +505,13 @@
},
{
"id": "app.context.menu.preview",
"type": "custom",
"order": 300,
"title": "APP.ACTIONS.VIEW",
"icon": "visibility",
"actions": {
"click": "VIEW_FILE"
"data": {
"title": "APP.ACTIONS.VIEW",
"menuButton": true
},
"component": "app.toolbar.viewNode",
"rules": {
"visible": "canViewFile"
}
@ -581,6 +583,7 @@
"comment": "workaround for Recent Files and Search API issue",
"type": "custom",
"order": 802,
"data": "['/favorites', '/favorite/libraries']",
"component": "app.toolbar.toggleFavorite",
"rules": {
"visible": "canToggleFavorite"