[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 | 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.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 | 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.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.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.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 See [Registration](/extending/registration) section for more details
on how to register your own entries to be re-used at runtime. 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 { dataTable, toolbar } = page;
const contextMenu = dataTable.menu; const contextMenu = dataTable.menu;
const viewer = new Viewer(); const viewer = new Viewer();
const viewerToolbar = viewer.toolbar;
export async function checkContextMenu(item: string, expectedContextMenu: string[]) { export async function checkContextMenu(item: string, expectedContextMenu: string[]) {
@@ -93,7 +94,7 @@ export async function checkViewerToolbarPrimaryActions(item: string, expectedToo
await dataTable.doubleClickOnRowByName(item); await dataTable.doubleClickOnRowByName(item);
await viewer.waitForViewerToOpen(); await viewer.waitForViewerToOpen();
let actualPrimaryActions = await toolbar.getButtons(); let actualPrimaryActions = await viewerToolbar.getButtons();
actualPrimaryActions = removeClosePreviousNextOldInfo(actualPrimaryActions); actualPrimaryActions = removeClosePreviousNextOldInfo(actualPrimaryActions);
@@ -106,9 +107,9 @@ export async function checkViewerToolbarPrimaryActions(item: string, expectedToo
export async function checkViewerToolbarMoreActions(item: string, expectedToolbarMore: string[]) { export async function checkViewerToolbarMoreActions(item: string, expectedToolbarMore: string[]) {
await dataTable.doubleClickOnRowByName(item); await dataTable.doubleClickOnRowByName(item);
await viewer.waitForViewerToOpen(); 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(actualMoreActions.length).toBe(expectedToolbarMore.length, 'Incorrect number of toolbar More menu items');
expect(JSON.stringify(actualMoreActions)).toEqual(JSON.stringify(expectedToolbarMore), 'Incorrect toolbar More actions'); 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 () => { 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); await page.load(previewURL);
expect(await viewer.isViewerOpened()).toBe(true, 'Viewer is not opened'); expect(await viewer.isViewerOpened()).toBe(true, 'Viewer is not opened');
expect(await viewer.getFileTitle()).toEqual(xlsxFile); expect(await viewer.getFileTitle()).toEqual(xlsxFile);
}); });
it('Viewer does not open when accessing the preview URL for a file without permissions - [C279287]', async () => { 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); await page.load(previewURL);
expect(await viewer.isViewerOpened()).toBe(false, 'Viewer should not be opened!'); 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('navigation.evaluators', () => {
describe('isPreview', () => { describe('isPreview', () => {
it('should return [true] if url contains `/preview/`', () => { it('should return [true] if url contains `viewer:view`', () => {
const context: any = { const context: any = {
navigation: { navigation: {
url: 'path/preview/id' url: 'path/(viewer:view/id)'
} }
}; };
@@ -271,20 +271,30 @@ describe('navigation.evaluators', () => {
}); });
describe('isSharedPreview', () => { 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 = { const context: any = {
navigation: { navigation: {
url: '/shared/preview/path' url: '/shared/(viewer:view)'
} }
}; };
expect(app.isSharedPreview(context)).toBe(true); 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 = { const context: any = {
navigation: { 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', () => { 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 = { const context: any = {
navigation: { navigation: {
url: '/favorites/preview/path' url: '/favorites/(viewer:view)'
} }
}; };
expect(app.isFavoritesPreview(context)).toBe(true); 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 = { const context: any = {
navigation: { 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 { export function isPreview(context: RuleContext): boolean {
const { url } = context.navigation; 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 { export function isSharedPreview(context: RuleContext): boolean {
const { url } = context.navigation; 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 { export function isFavoritesPreview(context: RuleContext): boolean {
const { url } = context.navigation; 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', ToggleDocumentDisplayMode = 'TOGGLE_DOCUMENT_DISPLAY_MODE',
Logout = 'LOGOUT', Logout = 'LOGOUT',
ReloadDocumentList = 'RELOAD_DOCUMENT_LIST', ReloadDocumentList = 'RELOAD_DOCUMENT_LIST',
ResetSelection = 'RESET_SELECTION',
SetInfoDrawerState = 'SET_INFO_DRAWER_STATE', SetInfoDrawerState = 'SET_INFO_DRAWER_STATE',
SetInfoDrawerMetadataAspect = 'SET_INFO_DRAWER_METADATA_ASPECT', SetInfoDrawerMetadataAspect = 'SET_INFO_DRAWER_METADATA_ASPECT',
CloseModalDialogs = 'CLOSE_MODAL_DIALOGS' CloseModalDialogs = 'CLOSE_MODAL_DIALOGS'
@@ -91,6 +92,12 @@ export class ReloadDocumentListAction implements Action {
constructor(public payload?: any) {} constructor(public payload?: any) {}
} }
export class ResetSelectionAction implements Action {
readonly type = AppActionTypes.ResetSelection;
constructor(public payload?: any) {}
}
export class SetInfoDrawerStateAction implements Action { export class SetInfoDrawerStateAction implements Action {
readonly type = AppActionTypes.SetInfoDrawerState; readonly type = AppActionTypes.SetInfoDrawerState;

View File

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

View File

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

View File

@@ -80,19 +80,57 @@ export const APP_ROUTES: Routes = [
pathMatch: 'full' pathMatch: 'full'
}, },
{ {
path: 'favorites', path: 'personal-files',
children: [ children: [
{ {
path: '', path: '',
loadChildren: component: FilesComponent,
'./components/favorites/favorites.module#AppFavoritesModule' data: {
sortingPreferenceKey: 'personal-files',
title: 'APP.BROWSE.PERSONAL.TITLE',
defaultNodeId: '-my-'
}
}, },
{ {
path: 'preview/:nodeId', path: 'view/:nodeId',
loadChildren: './components/preview/preview.module#PreviewModule', outlet: 'viewer',
children: [
{
path: '',
data: {
navigateSource: 'personal-files'
},
loadChildren:
'./components/viewer/viewer.module#AppViewerModule'
}
]
}
]
},
{
path: 'personal-files/:folderId',
children: [
{
path: '',
component: FilesComponent,
data: { 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', title: 'APP.BROWSE.LIBRARIES.MENU.MY_LIBRARIES.TITLE',
sortingPreferenceKey: 'libraries' sortingPreferenceKey: 'libraries'
} }
}, }
]
},
{
path: 'libraries/:folderId',
children: [
{ {
path: ':folderId', path: '',
component: FilesComponent, component: FilesComponent,
data: { data: {
title: 'APP.BROWSE.LIBRARIES.MENU.MY_LIBRARIES.TITLE', title: 'APP.BROWSE.LIBRARIES.MENU.MY_LIBRARIES.TITLE',
@@ -116,69 +159,59 @@ export const APP_ROUTES: Routes = [
} }
}, },
{ {
path: ':folderId/preview/:nodeId', path: 'view/:nodeId',
loadChildren: './components/preview/preview.module#PreviewModule', outlet: 'viewer',
data: { children: [
navigateSource: 'libraries' {
} path: '',
data: {
navigateSource: 'libraries'
},
loadChildren:
'./components/viewer/viewer.module#AppViewerModule'
}
]
} }
] ]
}, },
{ {
path: 'favorite/libraries', path: 'favorite/libraries',
component: FavoriteLibrariesComponent, children: [
data: { {
title: 'APP.BROWSE.LIBRARIES.MENU.FAVORITE_LIBRARIES.TITLE', path: '',
sortingPreferenceKey: 'favorite-libraries' component: FavoriteLibrariesComponent,
} data: {
title: 'APP.BROWSE.LIBRARIES.MENU.FAVORITE_LIBRARIES.TITLE',
sortingPreferenceKey: 'favorite-libraries'
}
}
]
}, },
{ {
path: 'personal-files', path: 'favorites',
data: { data: {
sortingPreferenceKey: 'personal-files' sortingPreferenceKey: 'favorites'
}, },
children: [ children: [
{ {
path: '', path: '',
component: FilesComponent, loadChildren:
data: { './components/favorites/favorites.module#AppFavoritesModule'
title: 'APP.BROWSE.PERSONAL.TITLE',
defaultNodeId: '-my-'
}
}, },
{ {
path: ':folderId', path: 'view/:nodeId',
component: FilesComponent, outlet: 'viewer',
data: { children: [
title: 'APP.BROWSE.PERSONAL.TITLE' {
} path: '',
}, data: {
{ navigateSource: 'favorites'
path: 'preview/:nodeId', },
loadChildren: './components/preview/preview.module#PreviewModule', loadChildren:
data: { './components/viewer/viewer.module#AppViewerModule'
navigateSource: 'personal-files' }
} ]
},
{
path: ':folderId/preview/:nodeId',
loadChildren: './components/preview/preview.module#PreviewModule',
data: {
navigateSource: 'personal-files'
}
} }
// 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' './components/recent-files/recent-files.module#AppRecentFilesModule'
}, },
{ {
path: 'preview/:nodeId', path: 'view/:nodeId',
loadChildren: './components/preview/preview.module#PreviewModule', outlet: 'viewer',
data: { children: [
navigateSource: 'recent-files' {
} 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' './components/shared-files/shared-files.module#AppSharedFilesModule'
}, },
{ {
path: 'preview/:nodeId', path: 'view/:nodeId',
loadChildren: './components/preview/preview.module#PreviewModule', outlet: 'viewer',
data: { children: [
navigateSource: 'shared' {
} path: '',
data: {
navigateSource: 'shared'
},
loadChildren:
'./components/viewer/viewer.module#AppViewerModule'
}
]
} }
], ],
canActivateChild: [AppSharedRuleGuard], canActivateChild: [AppSharedRuleGuard],
@@ -235,16 +282,22 @@ export const APP_ROUTES: Routes = [
path: '', path: '',
component: SearchResultsComponent, component: SearchResultsComponent,
data: { data: {
title: 'APP.BROWSE.SEARCH.TITLE', title: 'APP.BROWSE.SEARCH.TITLE'
reuse: true
} }
}, },
{ {
path: 'preview/:nodeId', path: 'view/:nodeId',
loadChildren: './components/preview/preview.module#PreviewModule', outlet: 'viewer',
data: { children: [
navigateSource: 'search' {
} path: '',
data: {
navigateSource: 'search'
},
loadChildren:
'./components/viewer/viewer.module#AppViewerModule'
}
]
} }
] ]
}, },
@@ -259,11 +312,18 @@ export const APP_ROUTES: Routes = [
} }
}, },
{ {
path: 'preview/:nodeId', path: 'view/:nodeId',
loadChildren: './components/preview/preview.module#PreviewModule', outlet: 'viewer',
data: { children: [
navigateSource: 'search' {
} path: '',
data: {
navigateSource: 'search'
},
loadChildren:
'./components/viewer/viewer.module#AppViewerModule'
}
]
} }
] ]
}, },

View File

@@ -20,7 +20,10 @@
</ng-container> </ng-container>
<ng-container *ngSwitchCase="'custom'"> <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>
<ng-container *ngSwitchDefault> <ng-container *ngSwitchDefault>

View File

@@ -47,7 +47,10 @@
</ng-container> </ng-container>
<ng-container *ngSwitchCase="'custom'"> <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>
</ng-container> </ng-container>
</mat-menu> </mat-menu>

View File

@@ -25,12 +25,18 @@
import { NO_ERRORS_SCHEMA } from '@angular/core'; import { NO_ERRORS_SCHEMA } from '@angular/core';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { TestBed, ComponentFixture } from '@angular/core/testing'; import {
TestBed,
ComponentFixture,
fakeAsync,
tick
} from '@angular/core/testing';
import { import {
AlfrescoApiService, AlfrescoApiService,
NodeFavoriteDirective, NodeFavoriteDirective,
DataTableComponent, DataTableComponent,
AppConfigPipe AppConfigPipe,
UploadService
} from '@alfresco/adf-core'; } from '@alfresco/adf-core';
import { DocumentListComponent } from '@alfresco/adf-content-services'; import { DocumentListComponent } from '@alfresco/adf-content-services';
import { of } from 'rxjs'; import { of } from 'rxjs';
@@ -44,8 +50,13 @@ describe('FavoritesComponent', () => {
let alfrescoApi: AlfrescoApiService; let alfrescoApi: AlfrescoApiService;
let contentApi: ContentApiService; let contentApi: ContentApiService;
let router: Router; let router: Router;
const mockRouter = {
url: 'favorites',
navigate: () => {}
};
let page; let page;
let node; let node;
let uploadService: UploadService;
beforeEach(() => { beforeEach(() => {
page = { page = {
@@ -78,6 +89,12 @@ describe('FavoritesComponent', () => {
FavoritesComponent, FavoritesComponent,
AppConfigPipe AppConfigPipe
], ],
providers: [
{
provide: Router,
useValue: mockRouter
}
],
schemas: [NO_ERRORS_SCHEMA] schemas: [NO_ERRORS_SCHEMA]
}); });
@@ -91,6 +108,7 @@ describe('FavoritesComponent', () => {
); );
contentApi = TestBed.get(ContentApiService); contentApi = TestBed.get(ContentApiService);
uploadService = TestBed.get(UploadService);
router = TestBed.get(Router); router = TestBed.get(Router);
}); });
@@ -132,14 +150,44 @@ describe('FavoritesComponent', () => {
}); });
}); });
describe('refresh', () => { it('should call document list reload on fileUploadComplete event', fakeAsync(() => {
it('should call document list reload', () => { spyOn(component, 'reload');
spyOn(component, 'reload');
fixture.detectChanges();
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) { 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 fixture: ComponentFixture<FilesComponent>;
let component: FilesComponent; let component: FilesComponent;
let uploadService: UploadService; let uploadService: UploadService;
let router: Router;
let nodeActionsService: NodeActionsService; let nodeActionsService: NodeActionsService;
let contentApi: ContentApiService; let contentApi: ContentApiService;
let router = {
url: '',
navigate: jasmine.createSpy('navigate')
};
beforeEach(() => { beforeEach(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
@@ -64,6 +67,10 @@ describe('FilesComponent', () => {
AppConfigPipe AppConfigPipe
], ],
providers: [ providers: [
{
provide: Router,
useValue: router
},
{ {
provide: ActivatedRoute, provide: ActivatedRoute,
useValue: { useValue: {
@@ -122,7 +129,6 @@ describe('FilesComponent', () => {
node.isFolder = false; node.isFolder = false;
node.parentId = 'parent-id'; node.parentId = 'parent-id';
spyOn(contentApi, 'getNode').and.returnValue(of({ entry: node })); spyOn(contentApi, 'getNode').and.returnValue(of({ entry: node }));
spyOn(router, 'navigate');
fixture.detectChanges(); fixture.detectChanges();
@@ -231,24 +237,24 @@ describe('FilesComponent', () => {
describe('Node navigation', () => { describe('Node navigation', () => {
beforeEach(() => { beforeEach(() => {
spyOn(contentApi, 'getNode').and.returnValue(of({ entry: node })); spyOn(contentApi, 'getNode').and.returnValue(of({ entry: node }));
spyOn(router, 'navigate');
fixture.detectChanges(); fixture.detectChanges();
}); });
it('should navigates to node when id provided', () => { it('should navigates to node when id provided', () => {
router.url = '/personal-files';
component.navigate(node.id); component.navigate(node.id);
expect(router.navigate).toHaveBeenCalledWith( expect(router.navigate).toHaveBeenCalledWith([
['./', node.id], '/personal-files',
jasmine.any(Object) node.id
); ]);
}); });
it('should navigates to home when id not provided', () => { it('should navigates to home when id not provided', () => {
router.url = '/personal-files';
component.navigate(); component.navigate();
expect(router.navigate).toHaveBeenCalledWith(['./'], jasmine.any(Object)); expect(router.navigate).toHaveBeenCalledWith(['/personal-files']);
}); });
it('should navigate home if node is root', () => { it('should navigate home if node is root', () => {
@@ -258,9 +264,10 @@ describe('FilesComponent', () => {
} }
}; };
router.url = '/personal-files';
component.navigate(node.id); 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) { navigate(nodeId: string = null) {
const commands = ['./']; const location = this.router.url.match(/.*?(?=\/|$)/g)[1];
const commands = [`/${location}`];
if (nodeId && !this.isRootNode(nodeId)) { if (nodeId && !this.isRootNode(nodeId)) {
commands.push(nodeId); commands.push(nodeId);
} }
this.router.navigate(commands, { this.router.navigate(commands);
relativeTo: this.route.parent
});
} }
navigateTo(node: MinimalNodeEntity) { navigateTo(node: MinimalNodeEntity) {
@@ -151,7 +151,7 @@ export class FilesComponent extends PageComponent implements OnInit, OnDestroy {
return; 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 { AppLayoutComponent } from './app-layout.component';
import { AppTestingModule } from '../../../testing/app-testing.module'; import { AppTestingModule } from '../../../testing/app-testing.module';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { import { AppStore, SetSelectedNodesAction } from '@alfresco/aca-shared/store';
AppStore,
SetSelectedNodesAction,
getAppSelection
} from '@alfresco/aca-shared/store';
import { Router, NavigationStart } from '@angular/router'; import { Router, NavigationStart } from '@angular/router';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
import { ResetSelectionAction } from '@alfresco/aca-shared/store';
class MockRouter { class MockRouter {
private url = 'some-url'; private url = 'some-url';
@@ -139,30 +136,17 @@ describe('AppLayoutComponent', () => {
}); });
}); });
it('should reset selection before navigation', done => { it('should reset selection before navigation', () => {
fixture.detectChanges();
const selection = [<any>{ entry: { id: 'nodeId', name: 'name' } }]; const selection = [<any>{ entry: { id: 'nodeId', name: 'name' } }];
spyOn(store, 'dispatch').and.stub();
fixture.detectChanges();
store.dispatch(new SetSelectedNodesAction(selection)); store.dispatch(new SetSelectedNodesAction(selection));
router.navigateByUrl('somewhere/over/the/rainbow'); router.navigateByUrl('somewhere/over/the/rainbow');
fixture.detectChanges(); fixture.detectChanges();
store.select(getAppSelection).subscribe(state => {
expect(state.isEmpty).toBe(true);
done();
});
});
it('should not reset selection if route is `/search`', done => { expect(store.dispatch['calls'].mostRecent().args).toEqual([
fixture.detectChanges(); new ResetSelectionAction()
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();
});
}); });
it('should close menu on mobile screen size', () => { it('should close menu on mobile screen size', () => {

View File

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

View File

@@ -37,13 +37,13 @@ import { AppExtensionService } from '../extensions/extension.service';
import { ContentManagementService } from '../services/content-management.service'; import { ContentManagementService } from '../services/content-management.service';
import { import {
AppStore, AppStore,
ViewFileAction,
ReloadDocumentListAction, ReloadDocumentListAction,
getCurrentFolder, getCurrentFolder,
getAppSelection, getAppSelection,
getDocumentDisplayMode, getDocumentDisplayMode,
isInfoDrawerOpened, isInfoDrawerOpened,
getSharedUrl getSharedUrl,
ViewNodeAction
} from '@alfresco/aca-shared/store'; } from '@alfresco/aca-shared/store';
import { isLocked, isLibrary } from '../utils/node.utils'; import { isLocked, isLibrary } from '../utils/node.utils';
@@ -105,10 +105,12 @@ export abstract class PageComponent implements OnInit, OnDestroy {
this.onDestroy$.complete(); this.onDestroy$.complete();
} }
showPreview(node: MinimalNodeEntity) { showPreview(node: MinimalNodeEntity, location?: string) {
if (node && node.entry) { if (node && node.entry) {
const parentId = this.node ? this.node.id : null; const id =
this.store.dispatch(new ViewFileAction(node, parentId)); (<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/>. * 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 { NO_ERRORS_SCHEMA } from '@angular/core';
import { import {
AlfrescoApiService, AlfrescoApiService,
NodeFavoriteDirective, NodeFavoriteDirective,
DataTableComponent, DataTableComponent,
AppConfigPipe AppConfigPipe,
UploadService
} from '@alfresco/adf-core'; } from '@alfresco/adf-core';
import { DocumentListComponent } from '@alfresco/adf-content-services'; import { DocumentListComponent } from '@alfresco/adf-content-services';
import { RecentFilesComponent } from './recent-files.component'; import { RecentFilesComponent } from './recent-files.component';
import { AppTestingModule } from '../../testing/app-testing.module'; import { AppTestingModule } from '../../testing/app-testing.module';
import { Router } from '@angular/router';
describe('RecentFilesComponent', () => { describe('RecentFilesComponent', () => {
let fixture: ComponentFixture<RecentFilesComponent>; let fixture: ComponentFixture<RecentFilesComponent>;
let component: RecentFilesComponent; let component: RecentFilesComponent;
let alfrescoApi: AlfrescoApiService; let alfrescoApi: AlfrescoApiService;
let page; let page;
let uploadService: UploadService;
const mockRouter = {
url: 'recent-files'
};
beforeEach(() => { beforeEach(() => {
page = { page = {
@@ -60,6 +71,12 @@ describe('RecentFilesComponent', () => {
RecentFilesComponent, RecentFilesComponent,
AppConfigPipe AppConfigPipe
], ],
providers: [
{
provide: Router,
useValue: mockRouter
}
],
schemas: [NO_ERRORS_SCHEMA] schemas: [NO_ERRORS_SCHEMA]
}); });
@@ -67,6 +84,7 @@ describe('RecentFilesComponent', () => {
component = fixture.componentInstance; component = fixture.componentInstance;
alfrescoApi = TestBed.get(AlfrescoApiService); alfrescoApi = TestBed.get(AlfrescoApiService);
uploadService = TestBed.get(UploadService);
alfrescoApi.reset(); alfrescoApi.reset();
spyOn(alfrescoApi.peopleApi, 'getPerson').and.returnValue( spyOn(alfrescoApi.peopleApi, 'getPerson').and.returnValue(
@@ -80,14 +98,32 @@ describe('RecentFilesComponent', () => {
); );
}); });
describe('refresh', () => { it('should call document list reload on fileUploadComplete event', fakeAsync(() => {
it('should call document list reload', () => { spyOn(component, 'reload');
spyOn(component, 'reload');
fixture.detectChanges();
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 { AppExtensionService } from '../../extensions/extension.service';
import { UploadService } from '@alfresco/adf-core'; import { UploadService } from '@alfresco/adf-core';
import { debounceTime } from 'rxjs/operators'; import { debounceTime } from 'rxjs/operators';
import { Router } from '@angular/router';
@Component({ @Component({
templateUrl: './recent-files.component.html' templateUrl: './recent-files.component.html'
@@ -47,7 +48,8 @@ export class RecentFilesComponent extends PageComponent implements OnInit {
extensions: AppExtensionService, extensions: AppExtensionService,
content: ContentManagementService, content: ContentManagementService,
private uploadService: UploadService, private uploadService: UploadService,
private breakpointObserver: BreakpointObserver private breakpointObserver: BreakpointObserver,
private router: Router
) { ) {
super(store, extensions, content); super(store, extensions, content);
} }
@@ -75,7 +77,7 @@ export class RecentFilesComponent extends PageComponent implements OnInit {
onNodeDoubleClick(node: MinimalNodeEntity) { onNodeDoubleClick(node: MinimalNodeEntity) {
if (node && node.entry) { if (node && node.entry) {
this.showPreview(node); this.showPreview(node, this.router.url);
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

@@ -23,23 +23,36 @@
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>. * 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 { NO_ERRORS_SCHEMA } from '@angular/core';
import { import {
AlfrescoApiService, AlfrescoApiService,
NodeFavoriteDirective, NodeFavoriteDirective,
DataTableComponent, DataTableComponent,
AppConfigPipe AppConfigPipe,
UploadService
} from '@alfresco/adf-core'; } from '@alfresco/adf-core';
import { DocumentListComponent } from '@alfresco/adf-content-services'; import { DocumentListComponent } from '@alfresco/adf-content-services';
import { SharedFilesComponent } from './shared-files.component'; import { SharedFilesComponent } from './shared-files.component';
import { AppTestingModule } from '../../testing/app-testing.module'; import { AppTestingModule } from '../../testing/app-testing.module';
import { Router } from '@angular/router';
import { ContentManagementService } from '../../services/content-management.service';
describe('SharedFilesComponent', () => { describe('SharedFilesComponent', () => {
let fixture: ComponentFixture<SharedFilesComponent>; let fixture: ComponentFixture<SharedFilesComponent>;
let component: SharedFilesComponent; let component: SharedFilesComponent;
let alfrescoApi: AlfrescoApiService; let alfrescoApi: AlfrescoApiService;
let page; let page;
let uploadService: UploadService;
let contentManagementService: ContentManagementService;
const mockRouter = {
url: 'shared-files'
};
beforeEach(() => { beforeEach(() => {
page = { page = {
@@ -60,10 +73,18 @@ describe('SharedFilesComponent', () => {
SharedFilesComponent, SharedFilesComponent,
AppConfigPipe AppConfigPipe
], ],
providers: [
{
provide: Router,
useValue: mockRouter
}
],
schemas: [NO_ERRORS_SCHEMA] schemas: [NO_ERRORS_SCHEMA]
}); });
fixture = TestBed.createComponent(SharedFilesComponent); fixture = TestBed.createComponent(SharedFilesComponent);
uploadService = TestBed.get(UploadService);
contentManagementService = TestBed.get(ContentManagementService);
component = fixture.componentInstance; component = fixture.componentInstance;
alfrescoApi = TestBed.get(AlfrescoApiService); alfrescoApi = TestBed.get(AlfrescoApiService);
@@ -74,14 +95,42 @@ describe('SharedFilesComponent', () => {
); );
}); });
describe('refresh', () => { it('should call document list reload on linksUnshared event', fakeAsync(() => {
it('should call document list reload', () => { spyOn(component, 'reload');
spyOn(component, 'reload');
fixture.detectChanges();
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 { AppExtensionService } from '../../extensions/extension.service';
import { debounceTime } from 'rxjs/operators'; import { debounceTime } from 'rxjs/operators';
import { UploadService } from '@alfresco/adf-core'; import { UploadService } from '@alfresco/adf-core';
import { Router } from '@angular/router';
import { MinimalNodeEntity } from '@alfresco/js-api';
@Component({ @Component({
templateUrl: './shared-files.component.html' templateUrl: './shared-files.component.html'
@@ -45,7 +47,8 @@ export class SharedFilesComponent extends PageComponent implements OnInit {
extensions: AppExtensionService, extensions: AppExtensionService,
content: ContentManagementService, content: ContentManagementService,
private uploadService: UploadService, private uploadService: UploadService,
private breakpointObserver: BreakpointObserver private breakpointObserver: BreakpointObserver,
private router: Router
) { ) {
super(store, extensions, content); super(store, extensions, content);
} }
@@ -74,4 +77,8 @@ export class SharedFilesComponent extends PageComponent implements OnInit {
this.columns = this.extensions.documentListPresets.shared || []; 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>
<ng-container *ngSwitchCase="'custom'"> <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>
</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 { DirectivesModule } from '../../directives/directives.module';
import { ToggleFavoriteLibraryComponent } from './toggle-favorite-library/toggle-favorite-library.component'; import { ToggleFavoriteLibraryComponent } from './toggle-favorite-library/toggle-favorite-library.component';
import { ToggleEditOfflineComponent } from './toggle-edit-offline/toggle-edit-offline.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'; import { AppCommonModule } from '../common/common.module';
export function components() { export function components() {
@@ -53,7 +54,8 @@ export function components() {
ToggleJoinLibraryButtonComponent, ToggleJoinLibraryButtonComponent,
ToggleJoinLibraryMenuComponent, ToggleJoinLibraryMenuComponent,
ToggleFavoriteLibraryComponent, 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"> <ng-container *ngIf="nodeId">
<adf-viewer <adf-viewer
[ngClass]="{
'right_side--hide': !showRightSide
}"
[nodeId]="nodeId" [nodeId]="nodeId"
[allowNavigate]="false" [allowNavigate]="navigateMultiple"
[allowRightSidebar]="true"
[allowPrint]="false" [allowPrint]="false"
[showRightSidebar]="true"
[allowDownload]="false" [allowDownload]="false"
[allowFullScreen]="false" [allowFullScreen]="false"
[allowRightSidebar]="showRightSide" [overlayMode]="true"
[showRightSidebar]="showRightSide" (showViewerChange)="onViewerVisibilityChanged($event)"
[canNavigateBefore]="previousNodeId"
[canNavigateNext]="nextNodeId"
(navigateBefore)="onNavigateBefore()"
(navigateNext)="onNavigateNext()"
> >
<adf-viewer-sidebar *ngIf="infoDrawerOpened$ | async"> <adf-viewer-sidebar *ngIf="infoDrawerOpened$ | async">
<aca-info-drawer [node]="selection.file"></aca-info-drawer> <aca-info-drawer [node]="selection.file"></aca-info-drawer>

View File

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

View File

@@ -28,17 +28,29 @@ import {
AppStore, AppStore,
getAppSelection, getAppSelection,
isInfoDrawerOpened, isInfoDrawerOpened,
SetSelectedNodesAction SetSelectedNodesAction,
ClosePreviewAction,
ViewerActionTypes,
ViewNodeAction
} from '@alfresco/aca-shared/store'; } from '@alfresco/aca-shared/store';
import { ContentActionRef, SelectionState } from '@alfresco/adf-extensions'; import { ContentActionRef, SelectionState } from '@alfresco/adf-extensions';
import { MinimalNodeEntryEntity } from '@alfresco/js-api'; import { MinimalNodeEntryEntity } from '@alfresco/js-api';
import { Component, OnDestroy, OnInit, ViewEncapsulation } from '@angular/core'; 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 { Store } from '@ngrx/store';
import { from, Observable, Subject } from 'rxjs'; import { from, Observable, Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators'; import { takeUntil, debounceTime } from 'rxjs/operators';
import { AppExtensionService } from '../../extensions/extension.service'; 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({ @Component({
selector: 'app-viewer', selector: 'app-viewer',
templateUrl: './viewer.component.html', templateUrl: './viewer.component.html',
@@ -49,6 +61,7 @@ import { AppExtensionService } from '../../extensions/extension.service';
export class AppViewerComponent implements OnInit, OnDestroy { export class AppViewerComponent implements OnInit, OnDestroy {
onDestroy$ = new Subject<boolean>(); onDestroy$ = new Subject<boolean>();
folderId: string = null;
nodeId: string = null; nodeId: string = null;
node: MinimalNodeEntryEntity; node: MinimalNodeEntryEntity;
selection: SelectionState; selection: SelectionState;
@@ -58,11 +71,55 @@ export class AppViewerComponent implements OnInit, OnDestroy {
openWith: ContentActionRef[] = []; openWith: ContentActionRef[] = [];
toolbarActions: 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( constructor(
private router: Router,
private route: ActivatedRoute, private route: ActivatedRoute,
private store: Store<AppStore>, private store: Store<AppStore>,
protected extensions: AppExtensionService, private extensions: AppExtensionService,
private contentApi: ContentApiService private contentApi: ContentApiService,
private actions$: Actions,
private preferences: UserPreferencesService,
private content: ContentManagementService,
private apiService: AlfrescoApiService,
private uploadService: UploadService
) {} ) {}
ngOnInit() { ngOnInit() {
@@ -85,11 +142,50 @@ export class AppViewerComponent implements OnInit, OnDestroy {
}); });
this.route.params.subscribe(params => { this.route.params.subscribe(params => {
this.folderId = params.folderId;
const { nodeId } = params; const { nodeId } = params;
if (nodeId) { if (nodeId) {
this.displayNode(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() { ngOnDestroy() {
@@ -108,14 +204,228 @@ export class AppViewerComponent implements OnInit, OnDestroy {
this.store.dispatch(new SetSelectedNodesAction([{ entry: this.node }])); this.store.dispatch(new SetSelectedNodesAction([{ entry: this.node }]));
if (this.node && this.node.isFile) { 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; this.nodeId = this.node.id;
return; return;
} }
} catch (err) { } catch (error) {
if (!err || err.status !== 401) { const statusCode = JSON.parse(error.message).error.statusCode;
// this.router.navigate([this.previewLocation, id]);
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 = [ const routes: Routes = [
{ {
path: '', path: '',
data: {
title: 'APP.PREVIEW.TITLE',
navigateMultiple: true
},
component: AppViewerComponent component: AppViewerComponent
} }
]; ];

View File

@@ -24,9 +24,135 @@
*/ */
import { DocumentListDirective } from './document-list.directive'; import { DocumentListDirective } from './document-list.directive';
import { Subject } from 'rxjs';
import { SetSelectedNodesAction } from '@alfresco/aca-shared/store';
describe('DocumentListDirective', () => { describe('DocumentListDirective', () => {
it('should be defined', () => { let documentListDirective;
expect(DocumentListDirective).toBeDefined();
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 { Subject } from 'rxjs';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { SetSelectedNodesAction } from '@alfresco/aca-shared/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'; import { ContentManagementService } from '../services/content-management.service';
@Directive({ @Directive({
@@ -81,12 +81,19 @@ export class DocumentListDirective implements OnInit, OnDestroy {
} }
this.documentList.ready this.documentList.ready
.pipe(takeUntil(this.onDestroy$)) .pipe(
filter(() => !this.router.url.includes('viewer:view')),
takeUntil(this.onDestroy$)
)
.subscribe(() => this.onReady()); .subscribe(() => this.onReady());
this.content.reload.pipe(takeUntil(this.onDestroy$)).subscribe(() => { this.content.reload.pipe(takeUntil(this.onDestroy$)).subscribe(() => {
this.reload(); this.reload();
}); });
this.content.reset.pipe(takeUntil(this.onDestroy$)).subscribe(() => {
this.reset();
});
} }
ngOnDestroy() { ngOnDestroy() {
@@ -138,4 +145,9 @@ export class DocumentListDirective implements OnInit, OnDestroy {
this.store.dispatch(new SetSelectedNodesAction([])); this.store.dispatch(new SetSelectedNodesAction([]));
this.documentList.reload(); this.documentList.reload();
} }
private reset() {
this.documentList.resetSelection();
this.store.dispatch(new SetSelectedNodesAction([]));
}
} }

View File

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

View File

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

View File

@@ -29,7 +29,8 @@ import { map } from 'rxjs/operators';
import { import {
AppActionTypes, AppActionTypes,
LogoutAction, LogoutAction,
ReloadDocumentListAction ReloadDocumentListAction,
ResetSelectionAction
} from '@alfresco/aca-shared/store'; } from '@alfresco/aca-shared/store';
import { AuthenticationService } from '@alfresco/adf-core'; import { AuthenticationService } from '@alfresco/adf-core';
import { Router } from '@angular/router'; 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 }) @Effect({ dispatch: false })
logout$ = this.actions$.pipe( logout$ = this.actions$.pipe(
ofType<LogoutAction>(AppActionTypes.Logout), 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, getAppSelection,
FullscreenViewerAction FullscreenViewerAction
} from '@alfresco/aca-shared/store'; } 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 { Store, createSelector } from '@ngrx/store';
import { AppExtensionService } from '../../extensions/extension.service'; import { AppExtensionService } from '../../extensions/extension.service';
@@ -72,8 +78,10 @@ export class ViewerEffects {
ofType<ViewNodeAction>(ViewerActionTypes.ViewNode), ofType<ViewNodeAction>(ViewerActionTypes.ViewNode),
map(action => { map(action => {
if (action.location) { if (action.location) {
const location = this.getNavigationCommands(action.location);
this.router.navigate( this.router.navigate(
[action.location, { outlets: { viewer: ['view', action.nodeId] } }], [...location, { outlets: { viewer: ['view', action.nodeId] } }],
{ {
queryParams: { queryParams: {
source: action.location 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", "id": "app.toolbar.preview",
"type": "custom",
"order": 300, "order": 300,
"title": "APP.ACTIONS.VIEW", "data": {
"icon": "visibility", "title": "APP.ACTIONS.VIEW",
"actions": { "iconButton": true
"click": "VIEW_FILE"
}, },
"component": "app.toolbar.viewNode",
"rules": { "rules": {
"visible": "canViewFile" "visible": "canViewFile"
} }
@@ -504,12 +505,13 @@
}, },
{ {
"id": "app.context.menu.preview", "id": "app.context.menu.preview",
"type": "custom",
"order": 300, "order": 300,
"title": "APP.ACTIONS.VIEW", "data": {
"icon": "visibility", "title": "APP.ACTIONS.VIEW",
"actions": { "menuButton": true
"click": "VIEW_FILE"
}, },
"component": "app.toolbar.viewNode",
"rules": { "rules": {
"visible": "canViewFile" "visible": "canViewFile"
} }
@@ -581,6 +583,7 @@
"comment": "workaround for Recent Files and Search API issue", "comment": "workaround for Recent Files and Search API issue",
"type": "custom", "type": "custom",
"order": 802, "order": 802,
"data": "['/favorites', '/favorite/libraries']",
"component": "app.toolbar.toggleFavorite", "component": "app.toolbar.toggleFavorite",
"rules": { "rules": {
"visible": "canToggleFavorite" "visible": "canToggleFavorite"