mirror of
https://github.com/Alfresco/alfresco-content-app.git
synced 2025-05-12 17:04:46 +00:00
[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:
parent
8643a8806d
commit
e31c0d6caf
@ -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 |
|
||||
|
@ -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.
|
||||
|
@ -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');
|
||||
|
@ -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!');
|
||||
});
|
||||
|
@ -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'
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -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');
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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));
|
||||
});
|
||||
|
||||
|
@ -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'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
);
|
||||
});
|
||||
});
|
||||
|
@ -112,7 +112,7 @@ export class FavoritesComponent extends PageComponent implements OnInit {
|
||||
}
|
||||
|
||||
if (node.entry.isFile) {
|
||||
this.showPreview(node);
|
||||
this.showPreview(node, this.router.url);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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']);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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', () => {
|
||||
|
@ -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() {
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -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', () => {
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
];
|
||||
}
|
||||
|
||||
|
105
src/app/components/toolbar/view-node/view-node.component.spec.ts
Normal file
105
src/app/components/toolbar/view-node/view-node.component.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
75
src/app/components/toolbar/view-node/view-node.component.ts
Normal file
75
src/app/components/toolbar/view-node/view-node.component.ts
Normal 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));
|
||||
});
|
||||
}
|
||||
}
|
@ -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>
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -37,6 +37,10 @@ import { AppViewerComponent } from './viewer.component';
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
data: {
|
||||
title: 'APP.PREVIEW.TITLE',
|
||||
navigateMultiple: true
|
||||
},
|
||||
component: AppViewerComponent
|
||||
}
|
||||
];
|
||||
|
@ -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([])
|
||||
);
|
||||
});
|
||||
});
|
||||
|
@ -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([]));
|
||||
}
|
||||
}
|
||||
|
@ -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({
|
||||
|
@ -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>();
|
||||
|
@ -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),
|
||||
|
95
src/app/store/effects/viewer.effects.spec.ts
Normal file
95
src/app/store/effects/viewer.effects.spec.ts
Normal 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)'
|
||||
);
|
||||
}));
|
||||
});
|
||||
});
|
@ -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;
|
||||
}, []);
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
|
Loading…
x
Reference in New Issue
Block a user