[AAE-11496] Move 'content-plugin' to projects folder as 'aca-content' (#2817)

* [AAE-11496] Move content-plugin to projects

* Fix unit test
This commit is contained in:
Bartosz Sekuła
2022-12-20 18:15:34 +01:00
committed by GitHub
parent c87662900e
commit e570ef8da0
263 changed files with 291 additions and 58 deletions

View File

@@ -0,0 +1,77 @@
/*!
* @license
* Alfresco Example Content Application
*
* Copyright (C) 2005 - 2020 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 { Actions, ofType, createEffect } from '@ngrx/effects';
import { Injectable } from '@angular/core';
import { map } from 'rxjs/operators';
import { AppActionTypes, LogoutAction, ReloadDocumentListAction, ResetSelectionAction } from '@alfresco/aca-shared/store';
import { AuthenticationService } from '@alfresco/adf-core';
import { Router } from '@angular/router';
import { AppHookService } from '@alfresco/aca-shared';
@Injectable()
export class AppEffects {
constructor(private actions$: Actions, private auth: AuthenticationService, private router: Router, private appHookService: AppHookService) {}
reload = createEffect(
() =>
this.actions$.pipe(
ofType<ReloadDocumentListAction>(AppActionTypes.ReloadDocumentList),
map((action) => {
this.appHookService.reload.next(action);
})
),
{ dispatch: false }
);
resetSelection = createEffect(
() =>
this.actions$.pipe(
ofType<ResetSelectionAction>(AppActionTypes.ResetSelection),
map((action) => {
this.appHookService.reset.next(action);
})
),
{ dispatch: false }
);
logout$ = createEffect(
() =>
this.actions$.pipe(
ofType<LogoutAction>(AppActionTypes.Logout),
map(() => {
this.auth.logout().subscribe(
() => this.redirectToLogin(),
() => this.redirectToLogin()
);
})
),
{ dispatch: false }
);
private redirectToLogin(): Promise<boolean> {
return this.router.navigate(['login']);
}
}

View File

@@ -0,0 +1,66 @@
/*!
* @license
* Alfresco Example Content Application
*
* Copyright (C) 2005 - 2020 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 { AppTestingModule } from '../../testing/app-testing.module';
import { ContextMenuEffects } from './contextmenu.effects';
import { EffectsModule } from '@ngrx/effects';
import { Store } from '@ngrx/store';
import { ContextMenu } from '@alfresco/aca-shared/store';
import { ContextMenuService } from '../../components/context-menu/context-menu.service';
import { OverlayRef } from '@angular/cdk/overlay';
import { ContextMenuOverlayRef } from '../../components/context-menu/context-menu-overlay';
describe('ContextMenuEffects', () => {
let store: Store<any>;
let contextMenuService: ContextMenuService;
const overlayRefMock = new ContextMenuOverlayRef({} as OverlayRef);
beforeEach(() => {
TestBed.configureTestingModule({
imports: [AppTestingModule, EffectsModule.forRoot([ContextMenuEffects])],
providers: [ContextMenuService]
});
store = TestBed.inject(Store);
contextMenuService = TestBed.inject(ContextMenuService);
spyOn(overlayRefMock, 'close').and.callFake(() => {});
spyOn(contextMenuService, 'open').and.returnValue(overlayRefMock);
});
it('should open dialog', () => {
store.dispatch(new ContextMenu(new MouseEvent('click')));
expect(contextMenuService.open).toHaveBeenCalled();
});
it('should close dialog reference if previously was opened', () => {
store.dispatch(new ContextMenu(new MouseEvent('click')));
expect(contextMenuService.open).toHaveBeenCalled();
store.dispatch(new ContextMenu(new MouseEvent('click')));
expect(overlayRefMock.close).toHaveBeenCalled();
});
});

View File

@@ -0,0 +1,58 @@
/*!
* @license
* Alfresco Example Content Application
*
* Copyright (C) 2005 - 2020 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 { ContextMenuActionTypes, ContextMenu } from '@alfresco/aca-shared/store';
import { Injectable } from '@angular/core';
import { Actions, ofType, createEffect } from '@ngrx/effects';
import { map } from 'rxjs/operators';
import { ContextMenuOverlayRef } from '../../components/context-menu/context-menu-overlay';
import { ContextMenuService } from '../../components/context-menu/context-menu.service';
@Injectable()
export class ContextMenuEffects {
private overlayRef: ContextMenuOverlayRef = null;
constructor(private contextMenuService: ContextMenuService, private actions$: Actions) {}
contextMenu$ = createEffect(
() =>
this.actions$.pipe(
ofType<ContextMenu>(ContextMenuActionTypes.ContextMenu),
map((action) => {
if (this.overlayRef) {
this.overlayRef.close();
}
this.overlayRef = this.contextMenuService.open({
source: action.event,
hasBackdrop: false,
backdropClass: 'cdk-overlay-transparent-backdrop',
panelClass: 'cdk-overlay-pane'
});
})
),
{ dispatch: false }
);
}

View File

@@ -0,0 +1,115 @@
/*!
* @license
* Alfresco Example Content Application
*
* Copyright (C) 2005 - 2020 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 { AppTestingModule } from '../../testing/app-testing.module';
import { EffectsModule } from '@ngrx/effects';
import { Store } from '@ngrx/store';
import { BehaviorSubject, Subject } from 'rxjs';
import { MatDialog, MatDialogRef } from '@angular/material/dialog';
import { DownloadNodesAction } from '@alfresco/aca-shared/store';
import { SelectionState } from '@alfresco/adf-extensions';
import { VersionEntry } from '@alfresco/js-api';
import { DownloadEffects } from './download.effects';
describe('DownloadEffects', () => {
let store: Store;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [AppTestingModule, EffectsModule.forRoot([DownloadEffects])]
});
store = TestBed.inject(Store);
});
describe('downloadNode$', () => {
let dialog: MatDialog;
beforeEach(() => {
dialog = TestBed.inject(MatDialog);
});
it('should focus element indicated by passed selector after closing modal', () => {
const elementToFocusSelector = 'button';
const afterClosed$ = new Subject<void>();
spyOn(dialog, 'open').and.returnValue({
afterClosed: () => afterClosed$.asObservable()
} as MatDialogRef<any>);
const elementToFocus = document.createElement(elementToFocusSelector);
spyOn(elementToFocus, 'focus');
spyOn(document, 'querySelector').withArgs(elementToFocusSelector).and.returnValue(elementToFocus);
spyOn(store, 'select').and.returnValues(
new BehaviorSubject({
isEmpty: false,
nodes: [
{
entry: {
id: 'someId',
isFolder: true
}
}
]
} as SelectionState),
new BehaviorSubject<VersionEntry>(null)
);
store.dispatch(
new DownloadNodesAction([], {
focusedElementOnCloseSelector: elementToFocusSelector
})
);
afterClosed$.next();
expect(elementToFocus.focus).toHaveBeenCalled();
});
it('should not looking for element to focus if passed selector is empty string', () => {
const afterClosed$ = new Subject<void>();
spyOn(dialog, 'open').and.returnValue({
afterClosed: () => afterClosed$.asObservable()
} as MatDialogRef<any>);
spyOn(document, 'querySelector');
spyOn(store, 'select').and.returnValues(
new BehaviorSubject({
isEmpty: false,
nodes: [
{
entry: {
id: 'someId',
isFolder: true
}
}
]
} as SelectionState),
new BehaviorSubject<VersionEntry>(null)
);
store.dispatch(
new DownloadNodesAction([], {
focusedElementOnCloseSelector: ''
})
);
afterClosed$.next();
expect(document.querySelector).not.toHaveBeenCalled();
});
});
});

View File

@@ -0,0 +1,171 @@
/*!
* @license
* Alfresco Example Content Application
*
* Copyright (C) 2005 - 2020 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 { AppStore, DownloadNodesAction, NodeActionTypes, NodeInfo, getAppSelection, getCurrentVersion } from '@alfresco/aca-shared/store';
import { DownloadZipDialogComponent } from '@alfresco/adf-core';
import { MinimalNodeEntity, Version } from '@alfresco/js-api';
import { Injectable } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { Actions, ofType, createEffect } from '@ngrx/effects';
import { Store } from '@ngrx/store';
import { map, take } from 'rxjs/operators';
import { ContentApiService } from '@alfresco/aca-shared';
import { ContentUrlService } from '../../services/content-url.service';
@Injectable()
export class DownloadEffects {
constructor(
private store: Store<AppStore>,
private actions$: Actions,
private contentApi: ContentApiService,
private dialog: MatDialog,
private contentUrlService: ContentUrlService
) {}
downloadNode$ = createEffect(
() =>
this.actions$.pipe(
ofType<DownloadNodesAction>(NodeActionTypes.Download),
map((action) => {
if (action.payload?.length > 0) {
this.downloadNodes(action.payload);
} else {
this.store
.select(getAppSelection)
.pipe(take(1))
.subscribe((selection) => {
if (selection && !selection.isEmpty) {
this.store
.select(getCurrentVersion)
.pipe(take(1))
.subscribe((version) => {
if (version) {
this.downloadFileVersion(selection.nodes[0].entry, version.entry);
} else {
this.downloadNodes(selection.nodes, action.configuration?.focusedElementOnCloseSelector);
}
});
}
});
}
})
),
{ dispatch: false }
);
private downloadNodes(toDownload: Array<MinimalNodeEntity>, focusedElementSelector?: string) {
const nodes = toDownload.map((node) => {
const { id, nodeId, name, isFile, isFolder } = node.entry as any;
return {
id: this.isSharedLinkPreview ? id : nodeId || id,
name,
isFile,
isFolder
};
});
if (!nodes || nodes.length === 0) {
return;
}
if (nodes.length === 1) {
this.downloadNode(nodes[0], focusedElementSelector);
} else {
this.downloadZip(nodes, focusedElementSelector);
}
}
private downloadNode(node: NodeInfo, focusedElementSelector?: string) {
if (node) {
if (node.isFolder) {
this.downloadZip([node], focusedElementSelector);
} else {
this.downloadFile(node);
}
}
}
private downloadFile(node: NodeInfo) {
if (node && !this.isSharedLinkPreview) {
this.contentUrlService.getNodeContentUrl(node.id, true).subscribe((contentUrl) => {
this.download(contentUrl, node.name);
});
}
if (node && this.isSharedLinkPreview) {
this.download(this.contentApi.getSharedLinkContent(node.id, false), node.name);
}
}
private downloadFileVersion(node: NodeInfo, version: Version) {
if (node && version) {
this.contentUrlService.getVersionContentUrl(node.id, version.id, true).subscribe((contentUrl) => {
this.download(contentUrl, node.name);
});
}
}
private downloadZip(nodes: Array<NodeInfo>, focusedElementSelector?: string) {
if (nodes && nodes.length > 0) {
const nodeIds = nodes.map((node) => node.id);
this.dialog
.open(DownloadZipDialogComponent, {
width: '600px',
disableClose: true,
data: {
nodeIds
}
})
.afterClosed()
.subscribe(() => this.focusAfterClose(focusedElementSelector));
}
}
private download(url: string, fileName: string) {
if (url && fileName) {
const link = document.createElement('a');
link.style.display = 'none';
link.download = fileName;
link.href = url;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
}
private get isSharedLinkPreview() {
return location.href.includes('/preview/s/');
}
private focusAfterClose(focusedElementSelector: string): void {
if (focusedElementSelector) {
document.querySelector<HTMLElement>(focusedElementSelector).focus();
}
}
}

View File

@@ -0,0 +1,80 @@
/*!
* @license
* Alfresco Example Content Application
*
* Copyright (C) 2005 - 2020 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 { Actions, ofType, createEffect } from '@ngrx/effects';
import { Injectable } from '@angular/core';
import { map, take } from 'rxjs/operators';
import { AppStore, NodeActionTypes, AddFavoriteAction, RemoveFavoriteAction, getAppSelection } from '@alfresco/aca-shared/store';
import { Store } from '@ngrx/store';
import { ContentManagementService } from '../../services/content-management.service';
@Injectable()
export class FavoriteEffects {
constructor(private store: Store<AppStore>, private actions$: Actions, private content: ContentManagementService) {}
addFavorite$ = createEffect(
() =>
this.actions$.pipe(
ofType<AddFavoriteAction>(NodeActionTypes.AddFavorite),
map((action) => {
if (action.payload && action.payload.length > 0) {
this.content.addFavorite(action.payload);
} else {
this.store
.select(getAppSelection)
.pipe(take(1))
.subscribe((selection) => {
if (selection && !selection.isEmpty) {
this.content.addFavorite(selection.nodes);
}
});
}
})
),
{ dispatch: false }
);
removeFavorite$ = createEffect(
() =>
this.actions$.pipe(
ofType<RemoveFavoriteAction>(NodeActionTypes.RemoveFavorite),
map((action) => {
if (action.payload && action.payload.length > 0) {
this.content.removeFavorite(action.payload);
} else {
this.store
.select(getAppSelection)
.pipe(take(1))
.subscribe((selection) => {
if (selection && !selection.isEmpty) {
this.content.removeFavorite(selection.nodes);
}
});
}
})
),
{ dispatch: false }
);
}

View File

@@ -0,0 +1,159 @@
/*!
* @license
* Alfresco Example Content Application
*
* Copyright (C) 2005 - 2020 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 {
AppStore,
CreateLibraryAction,
DeleteLibraryAction,
LeaveLibraryAction,
LibraryActionTypes,
NavigateLibraryAction,
NavigateRouteAction,
SnackbarErrorAction,
UpdateLibraryAction,
getAppSelection
} from '@alfresco/aca-shared/store';
import { Injectable } from '@angular/core';
import { Actions, ofType, createEffect } from '@ngrx/effects';
import { Store } from '@ngrx/store';
import { map, mergeMap, take } from 'rxjs/operators';
import { ContentApiService } from '@alfresco/aca-shared';
import { ContentManagementService } from '../../services/content-management.service';
@Injectable()
export class LibraryEffects {
constructor(
private store: Store<AppStore>,
private actions$: Actions,
private content: ContentManagementService,
private contentApi: ContentApiService
) {}
deleteLibrary$ = createEffect(
() =>
this.actions$.pipe(
ofType<DeleteLibraryAction>(LibraryActionTypes.Delete),
map((action) => {
if (action.payload) {
this.content.deleteLibrary(action.payload);
} else {
this.store
.select(getAppSelection)
.pipe(take(1))
.subscribe((selection) => {
if (selection && selection.library) {
this.content.deleteLibrary(selection.library.entry.id);
}
});
}
})
),
{ dispatch: false }
);
leaveLibrary$ = createEffect(
() =>
this.actions$.pipe(
ofType<LeaveLibraryAction>(LibraryActionTypes.Leave),
map((action) => {
if (action.payload) {
this.content.leaveLibrary(action.payload);
} else {
this.store
.select(getAppSelection)
.pipe(take(1))
.subscribe((selection) => {
if (selection && selection.library) {
this.content.leaveLibrary(selection.library.entry.id, action.configuration?.focusedElementOnCloseSelector);
}
});
}
})
),
{ dispatch: false }
);
createLibrary$ = createEffect(
() =>
this.actions$.pipe(
ofType<CreateLibraryAction>(LibraryActionTypes.Create),
mergeMap(() => this.content.createLibrary()),
map((libraryId) => new NavigateLibraryAction(libraryId))
),
{ dispatch: true }
);
navigateLibrary$ = createEffect(
() =>
this.actions$.pipe(
ofType<NavigateLibraryAction>(LibraryActionTypes.Navigate),
map((action) => {
const libraryId = action.payload;
if (libraryId) {
this.contentApi
.getNode(libraryId, { relativePath: '/documentLibrary' })
.pipe(map((node) => node.entry.id))
.subscribe(
(id) => {
const route = action.route ? action.route : 'libraries';
this.store.dispatch(new NavigateRouteAction([route, id]));
},
() => {
this.store.dispatch(new SnackbarErrorAction('APP.MESSAGES.ERRORS.MISSING_CONTENT'));
}
);
}
})
),
{ dispatch: false }
);
updateLibrary$ = createEffect(
() =>
this.actions$.pipe(
ofType<UpdateLibraryAction>(LibraryActionTypes.Update),
map((action) => {
this.store
.select(getAppSelection)
.pipe(take(1))
.subscribe((selection) => {
if (selection && selection.library) {
const { id } = selection.library.entry;
const { title, description, visibility } = action.payload;
const siteBody = {
title,
description,
visibility
};
this.content.updateLibrary(id, siteBody);
}
});
})
),
{ dispatch: false }
);
}

View File

@@ -0,0 +1,509 @@
/*!
* @license
* Alfresco Example Content Application
*
* Copyright (C) 2005 - 2020 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 { NodeEffects } from './node.effects';
import { EffectsModule } from '@ngrx/effects';
import { Store } from '@ngrx/store';
import { ContentManagementService } from '../../services/content-management.service';
import {
SharedStoreModule,
ShareNodeAction,
SetSelectedNodesAction,
UnshareNodesAction,
PurgeDeletedNodesAction,
RestoreDeletedNodesAction,
DeleteNodesAction,
UndoDeleteNodesAction,
CreateFolderAction,
EditFolderAction,
CopyNodesAction,
MoveNodesAction,
UnlockWriteAction,
FullscreenViewerAction,
PrintFileAction,
SetCurrentFolderAction,
ManageAspectsAction,
ManagePermissionsAction,
ShowLoaderAction
} from '@alfresco/aca-shared/store';
import { ViewUtilService } from '@alfresco/adf-core';
import { ViewerEffects } from './viewer.effects';
import { Router } from '@angular/router';
import { of } from 'rxjs';
describe('NodeEffects', () => {
let store: Store<any>;
let contentService: ContentManagementService;
let viewUtilService: ViewUtilService;
let viewerEffects: ViewerEffects;
let router: Router;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [AppTestingModule, SharedStoreModule, EffectsModule.forRoot([NodeEffects, ViewerEffects])],
providers: [ViewUtilService]
});
store = TestBed.inject(Store);
contentService = TestBed.inject(ContentManagementService);
viewUtilService = TestBed.inject(ViewUtilService);
viewerEffects = TestBed.inject(ViewerEffects);
router = TestBed.inject(Router);
});
describe('shareNode$', () => {
it('should share node from payload', () => {
spyOn(contentService, 'shareNode').and.stub();
const node: any = {
entry: {}
};
store.dispatch(new ShareNodeAction(node));
expect(contentService.shareNode).toHaveBeenCalledWith(node, undefined);
});
it('should share node from active selection', fakeAsync(() => {
spyOn(contentService, 'shareNode').and.stub();
const node: any = { entry: { isFile: true } };
store.dispatch(new SetSelectedNodesAction([node]));
tick(100);
store.dispatch(new ShareNodeAction(null));
expect(contentService.shareNode).toHaveBeenCalledWith(node, undefined);
}));
it('should do nothing if invoking share with no data', () => {
spyOn(contentService, 'shareNode').and.stub();
store.dispatch(new ShareNodeAction(null));
expect(contentService.shareNode).not.toHaveBeenCalled();
});
});
describe('unshareNodes$', () => {
it('should unshare nodes from the payload', () => {
spyOn(contentService, 'unshareNodes').and.stub();
const node: any = {};
store.dispatch(new UnshareNodesAction([node]));
expect(contentService.unshareNodes).toHaveBeenCalledWith([node]);
});
it('should unshare nodes from the active selection', fakeAsync(() => {
spyOn(contentService, 'unshareNodes').and.stub();
const node: any = { entry: { isFile: true } };
store.dispatch(new SetSelectedNodesAction([node]));
tick(100);
store.dispatch(new UnshareNodesAction(null));
expect(contentService.unshareNodes).toHaveBeenCalledWith([node]);
}));
it('should do nothing if invoking unshare with no data', () => {
spyOn(contentService, 'unshareNodes').and.stub();
store.dispatch(new UnshareNodesAction(null));
expect(contentService.unshareNodes).not.toHaveBeenCalled();
});
});
describe('purgeDeletedNodes$', () => {
it('should purge deleted nodes from the payload', () => {
spyOn(contentService, 'purgeDeletedNodes').and.stub();
const node: any = {};
store.dispatch(new PurgeDeletedNodesAction([node]));
expect(contentService.purgeDeletedNodes).toHaveBeenCalledWith([node]);
});
it('should purge nodes from the active selection', fakeAsync(() => {
spyOn(contentService, 'purgeDeletedNodes').and.stub();
const node: any = { entry: { isFile: true } };
store.dispatch(new SetSelectedNodesAction([node]));
tick(100);
store.dispatch(new PurgeDeletedNodesAction(null));
expect(contentService.purgeDeletedNodes).toHaveBeenCalledWith([node]);
}));
it('should do nothing if invoking purge with no data', () => {
spyOn(contentService, 'purgeDeletedNodes').and.stub();
store.dispatch(new PurgeDeletedNodesAction(null));
expect(contentService.purgeDeletedNodes).not.toHaveBeenCalled();
});
});
describe('restoreDeletedNodes$', () => {
it('should restore deleted nodes from the payload', () => {
spyOn(contentService, 'restoreDeletedNodes').and.stub();
const node: any = {};
store.dispatch(new RestoreDeletedNodesAction([node]));
expect(contentService.restoreDeletedNodes).toHaveBeenCalledWith([node]);
});
it('should restore deleted nodes from the active selection', fakeAsync(() => {
spyOn(contentService, 'restoreDeletedNodes').and.stub();
const node: any = { entry: { isFile: true } };
store.dispatch(new SetSelectedNodesAction([node]));
tick(100);
store.dispatch(new RestoreDeletedNodesAction(null));
expect(contentService.restoreDeletedNodes).toHaveBeenCalledWith([node]);
}));
it('should do nothing if invoking restore with no data', () => {
spyOn(contentService, 'restoreDeletedNodes').and.stub();
store.dispatch(new RestoreDeletedNodesAction(null));
expect(contentService.restoreDeletedNodes).not.toHaveBeenCalled();
});
});
describe('deleteNodes$', () => {
it('should delete nodes from the payload', () => {
spyOn(contentService, 'deleteNodes').and.stub();
spyOn(store, 'dispatch').and.callThrough();
const node: any = {};
store.dispatch(new DeleteNodesAction([node]));
expect(store.dispatch).toHaveBeenCalledWith(new DeleteNodesAction([node]));
expect(store.dispatch).toHaveBeenCalledWith(new ShowLoaderAction(true));
expect(contentService.deleteNodes).toHaveBeenCalledWith([node]);
});
it('should delete nodes from the active selection', fakeAsync(() => {
spyOn(contentService, 'deleteNodes').and.stub();
spyOn(store, 'dispatch').and.callThrough();
const node: any = { entry: { isFile: true } };
store.dispatch(new SetSelectedNodesAction([node]));
tick(100);
store.dispatch(new DeleteNodesAction(null));
expect(store.dispatch).toHaveBeenCalledWith(new DeleteNodesAction(null));
expect(store.dispatch).toHaveBeenCalledWith(new ShowLoaderAction(true));
expect(contentService.deleteNodes).toHaveBeenCalledWith([node]);
}));
it('should do nothing if invoking delete with no data', () => {
spyOn(contentService, 'deleteNodes').and.stub();
spyOn(store, 'dispatch').and.callThrough();
store.dispatch(new DeleteNodesAction(null));
expect(store.dispatch).toHaveBeenCalledWith(new DeleteNodesAction(null));
expect(store.dispatch).toHaveBeenCalledWith(new ShowLoaderAction(true));
expect(contentService.deleteNodes).not.toHaveBeenCalled();
});
});
describe('undoDeleteNodes$', () => {
it('should undo deleted nodes from the payload', () => {
spyOn(contentService, 'undoDeleteNodes').and.stub();
const node: any = {};
store.dispatch(new UndoDeleteNodesAction([node]));
expect(contentService.undoDeleteNodes).toHaveBeenCalledWith([node]);
});
it('should do nothing if undoing deletion with no data', () => {
spyOn(contentService, 'undoDeleteNodes').and.stub();
store.dispatch(new UndoDeleteNodesAction([]));
expect(contentService.undoDeleteNodes).not.toHaveBeenCalled();
});
});
describe('createFolder$', () => {
beforeEach(() => {
spyOn(contentService, 'createFolder').and.stub();
});
it('should create folder from the payload', () => {
const currentFolder = 'folder1';
store.dispatch(new CreateFolderAction(currentFolder));
expect(contentService.createFolder).toHaveBeenCalledWith(currentFolder);
});
it('should create folder in the active selected one', fakeAsync(() => {
const currentFolder: any = { isFolder: true, id: 'folder1' };
store.dispatch(new SetCurrentFolderAction(currentFolder));
tick(100);
store.dispatch(new CreateFolderAction(null));
expect(contentService.createFolder).toHaveBeenCalledWith(currentFolder.id);
}));
});
describe('editFolder$', () => {
it('should edit folder from the payload', () => {
spyOn(contentService, 'editFolder').and.stub();
const node: any = { entry: { isFolder: true, id: 'folder1' } };
store.dispatch(new EditFolderAction(node));
expect(contentService.editFolder).toHaveBeenCalledWith(node);
});
it('should edit folder from the active selection', fakeAsync(() => {
spyOn(contentService, 'editFolder').and.stub();
const currentFolder: any = {
entry: { isFolder: true, isFile: false, id: 'folder1' }
};
store.dispatch(new SetSelectedNodesAction([currentFolder]));
tick(100);
store.dispatch(new EditFolderAction(null));
expect(contentService.editFolder).toHaveBeenCalledWith(currentFolder, undefined);
}));
it('should do nothing if editing folder with no selection and payload', () => {
spyOn(contentService, 'editFolder').and.stub();
store.dispatch(new EditFolderAction(null));
expect(contentService.editFolder).not.toHaveBeenCalled();
});
});
describe('copyNodes$', () => {
it('should copy nodes from the payload', () => {
spyOn(contentService, 'copyNodes').and.stub();
const node: any = { entry: { isFile: true } };
store.dispatch(new CopyNodesAction([node]));
expect(contentService.copyNodes).toHaveBeenCalledWith([node]);
});
it('should copy nodes from the active selection', fakeAsync(() => {
spyOn(contentService, 'copyNodes').and.stub();
const node: any = { entry: { isFile: true } };
store.dispatch(new SetSelectedNodesAction([node]));
tick(100);
store.dispatch(new CopyNodesAction(null));
expect(contentService.copyNodes).toHaveBeenCalledWith([node], undefined);
}));
it('should do nothing if invoking copy with no data', () => {
spyOn(contentService, 'copyNodes').and.stub();
store.dispatch(new CopyNodesAction(null));
expect(contentService.copyNodes).not.toHaveBeenCalled();
});
});
describe('moveNodes$', () => {
it('should move nodes from the payload', () => {
spyOn(contentService, 'moveNodes').and.stub();
const node: any = { entry: { isFile: true } };
store.dispatch(new MoveNodesAction([node]));
expect(contentService.moveNodes).toHaveBeenCalledWith([node]);
});
it('should move nodes from the active selection', fakeAsync(() => {
spyOn(contentService, 'moveNodes').and.stub();
const node: any = { entry: { isFile: true } };
store.dispatch(new SetSelectedNodesAction([node]));
tick(100);
store.dispatch(new MoveNodesAction(null));
expect(contentService.moveNodes).toHaveBeenCalledWith([node], undefined);
}));
it('should do nothing if invoking move with no data', () => {
spyOn(contentService, 'moveNodes').and.stub();
store.dispatch(new MoveNodesAction(null));
expect(contentService.moveNodes).not.toHaveBeenCalled();
});
});
describe('managePermissions$', () => {
it('should manage permissions from the payload', () => {
spyOn(router, 'navigate').and.stub();
const node: any = { entry: { isFile: true, id: 'fileId' } };
store.dispatch(new ManagePermissionsAction(node));
expect(router.navigate).toHaveBeenCalledWith(['personal-files/details', 'fileId', 'permissions']);
});
it('should manage permissions from the active selection', () => {
spyOn(store, 'select').and.returnValue(of({ isEmpty: false, first: { entry: { id: 'fileId' } } }));
spyOn(router, 'navigate').and.stub();
store.dispatch(new ManagePermissionsAction(null));
expect(router.navigate).toHaveBeenCalledWith(['personal-files/details', 'fileId', 'permissions']);
});
it('should do nothing if invoking manage permissions with no data', () => {
spyOn(store, 'select').and.returnValue(of(null));
spyOn(router, 'navigate').and.stub();
store.dispatch(new ManagePermissionsAction(null));
expect(router.navigate).not.toHaveBeenCalled();
});
});
describe('printFile$', () => {
it('it should print node content from payload', () => {
spyOn(viewUtilService, 'printFileGeneric').and.stub();
const node: any = {
entry: { id: 'node-id', content: { mimeType: 'text/json' } }
};
store.dispatch(new PrintFileAction(node));
expect(viewUtilService.printFileGeneric).toHaveBeenCalledWith('node-id', 'text/json');
});
it('it should print node content from store', fakeAsync(() => {
spyOn(viewUtilService, 'printFileGeneric').and.stub();
const node: any = {
entry: {
isFile: true,
id: 'node-id',
content: { mimeType: 'text/json' }
}
};
store.dispatch(new SetSelectedNodesAction([node]));
tick(100);
store.dispatch(new PrintFileAction(null));
expect(viewUtilService.printFileGeneric).toHaveBeenCalledWith('node-id', 'text/json');
}));
});
describe('fullscreenViewer$', () => {
it('should call fullscreen viewer', () => {
spyOn(viewerEffects, 'enterFullScreen').and.stub();
store.dispatch(new FullscreenViewerAction(null));
expect(viewerEffects.enterFullScreen).toHaveBeenCalled();
});
});
describe('unlockWrite$', () => {
it('should unlock node from payload', () => {
spyOn(contentService, 'unlockNode').and.stub();
const node: any = { entry: { id: 'node-id' } };
store.dispatch(new UnlockWriteAction(node));
expect(contentService.unlockNode).toHaveBeenCalledWith(node);
});
it('should unlock node from store selection', fakeAsync(() => {
spyOn(contentService, 'unlockNode').and.stub();
const node: any = { entry: { isFile: true, id: 'node-id' } };
store.dispatch(new SetSelectedNodesAction([node]));
tick(100);
store.dispatch(new UnlockWriteAction(null));
expect(contentService.unlockNode).toHaveBeenCalledWith(node);
}));
});
describe('aspectList$', () => {
it('should call aspect dialog', () => {
const node: any = { entry: { isFile: true } };
spyOn(contentService, 'manageAspects').and.stub();
store.dispatch(new ManageAspectsAction(node));
expect(contentService.manageAspects).toHaveBeenCalled();
});
it('should call aspect dialog from the active file selection', fakeAsync(() => {
spyOn(contentService, 'manageAspects').and.stub();
const node: any = { entry: { isFile: true, id: 'file-node-id' } };
store.dispatch(new SetSelectedNodesAction([node]));
tick(100);
store.dispatch(new ManageAspectsAction(null));
expect(contentService.manageAspects).toHaveBeenCalledWith({ entry: { isFile: true, id: 'file-node-id' } }, undefined);
}));
it('should call aspect dialog from the active folder selection', fakeAsync(() => {
spyOn(contentService, 'manageAspects').and.stub();
const node: any = { entry: { isFile: false, id: 'folder-node-id' } };
store.dispatch(new SetSelectedNodesAction([node]));
tick(100);
store.dispatch(new ManageAspectsAction(null));
expect(contentService.manageAspects).toHaveBeenCalledWith({ entry: { isFile: false, id: 'folder-node-id' } }, undefined);
}));
});
});

View File

@@ -0,0 +1,448 @@
/*!
* @license
* Alfresco Example Content Application
*
* Copyright (C) 2005 - 2020 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 { Actions, ofType, createEffect } from '@ngrx/effects';
import { Injectable } from '@angular/core';
import { map, take } from 'rxjs/operators';
import { Store } from '@ngrx/store';
import {
AppStore,
NodeActionTypes,
PurgeDeletedNodesAction,
DeleteNodesAction,
UndoDeleteNodesAction,
CreateFolderAction,
EditFolderAction,
RestoreDeletedNodesAction,
ShareNodeAction,
ManageVersionsAction,
UnlockWriteAction,
UnshareNodesAction,
CopyNodesAction,
MoveNodesAction,
ManagePermissionsAction,
PrintFileAction,
getCurrentFolder,
getAppSelection,
ManageAspectsAction,
NavigateRouteAction,
ExpandInfoDrawerAction,
ManageRulesAction,
ShowLoaderAction
} from '@alfresco/aca-shared/store';
import { ContentManagementService } from '../../services/content-management.service';
import { ViewUtilService } from '@alfresco/adf-core';
@Injectable()
export class NodeEffects {
constructor(
private store: Store<AppStore>,
private actions$: Actions,
private contentService: ContentManagementService,
private viewUtils: ViewUtilService
) {}
shareNode$ = createEffect(
() =>
this.actions$.pipe(
ofType<ShareNodeAction>(NodeActionTypes.Share),
map((action) => {
if (action.payload) {
this.contentService.shareNode(action.payload, action.configuration?.focusedElementOnCloseSelector);
} else {
this.store
.select(getAppSelection)
.pipe(take(1))
.subscribe((selection) => {
if (selection && selection.file) {
this.contentService.shareNode(selection.file, action.configuration?.focusedElementOnCloseSelector);
}
});
}
})
),
{ dispatch: false }
);
unshareNodes$ = createEffect(
() =>
this.actions$.pipe(
ofType<UnshareNodesAction>(NodeActionTypes.Unshare),
map((action) => {
if (action && action.payload && action.payload.length > 0) {
this.contentService.unshareNodes(action.payload);
} else {
this.store
.select(getAppSelection)
.pipe(take(1))
.subscribe((selection) => {
if (selection && !selection.isEmpty) {
this.contentService.unshareNodes(selection.nodes);
}
});
}
})
),
{ dispatch: false }
);
purgeDeletedNodes$ = createEffect(
() =>
this.actions$.pipe(
ofType<PurgeDeletedNodesAction>(NodeActionTypes.PurgeDeleted),
map((action) => {
if (action && action.payload && action.payload.length > 0) {
this.contentService.purgeDeletedNodes(action.payload);
} else {
this.store
.select(getAppSelection)
.pipe(take(1))
.subscribe((selection) => {
if (selection && selection.count > 0) {
this.contentService.purgeDeletedNodes(selection.nodes);
}
});
}
})
),
{ dispatch: false }
);
restoreDeletedNodes$ = createEffect(
() =>
this.actions$.pipe(
ofType<RestoreDeletedNodesAction>(NodeActionTypes.RestoreDeleted),
map((action) => {
if (action && action.payload && action.payload.length > 0) {
this.contentService.restoreDeletedNodes(action.payload);
} else {
this.store
.select(getAppSelection)
.pipe(take(1))
.subscribe((selection) => {
if (selection && selection.count > 0) {
this.contentService.restoreDeletedNodes(selection.nodes);
}
});
}
})
),
{ dispatch: false }
);
deleteNodes$ = createEffect(
() =>
this.actions$.pipe(
ofType<DeleteNodesAction>(NodeActionTypes.Delete),
map((action) => {
this.store.dispatch(new ShowLoaderAction(true));
if (action && action.payload && action.payload.length > 0) {
this.contentService.deleteNodes(action.payload);
} else {
this.store
.select(getAppSelection)
.pipe(take(1))
.subscribe((selection) => {
if (selection && selection.count > 0) {
this.contentService.deleteNodes(selection.nodes);
}
});
}
})
),
{ dispatch: false }
);
undoDeleteNodes$ = createEffect(
() =>
this.actions$.pipe(
ofType<UndoDeleteNodesAction>(NodeActionTypes.UndoDelete),
map((action) => {
if (action.payload.length > 0) {
this.contentService.undoDeleteNodes(action.payload);
}
})
),
{ dispatch: false }
);
createFolder$ = createEffect(
() =>
this.actions$.pipe(
ofType<CreateFolderAction>(NodeActionTypes.CreateFolder),
map((action) => {
if (action.payload) {
this.contentService.createFolder(action.payload);
} else {
this.store
.select(getCurrentFolder)
.pipe(take(1))
.subscribe((node) => {
if (node && node.id) {
this.contentService.createFolder(node.id);
}
});
}
})
),
{ dispatch: false }
);
editFolder$ = createEffect(
() =>
this.actions$.pipe(
ofType<EditFolderAction>(NodeActionTypes.EditFolder),
map((action) => {
if (action.payload) {
this.contentService.editFolder(action.payload);
} else {
this.store
.select(getAppSelection)
.pipe(take(1))
.subscribe((selection) => {
if (selection && selection.folder) {
this.contentService.editFolder(selection.folder, action.configuration?.focusedElementOnCloseSelector);
}
});
}
})
),
{ dispatch: false }
);
copyNodes$ = createEffect(
() =>
this.actions$.pipe(
ofType<CopyNodesAction>(NodeActionTypes.Copy),
map((action) => {
if (action.payload?.length > 0) {
this.contentService.copyNodes(action.payload);
} else {
this.store
.select(getAppSelection)
.pipe(take(1))
.subscribe((selection) => {
if (selection && !selection.isEmpty) {
this.contentService.copyNodes(selection.nodes, action.configuration?.focusedElementOnCloseSelector);
}
});
}
})
),
{ dispatch: false }
);
moveNodes$ = createEffect(
() =>
this.actions$.pipe(
ofType<MoveNodesAction>(NodeActionTypes.Move),
map((action) => {
if (action.payload?.length > 0) {
this.contentService.moveNodes(action.payload);
} else {
this.store
.select(getAppSelection)
.pipe(take(1))
.subscribe((selection) => {
if (selection && !selection.isEmpty) {
this.contentService.moveNodes(selection.nodes, action.configuration?.focusedElementOnCloseSelector);
}
});
}
})
),
{ dispatch: false }
);
managePermissions$ = createEffect(
() =>
this.actions$.pipe(
ofType<ManagePermissionsAction>(NodeActionTypes.ManagePermissions),
map((action) => {
if (action?.payload) {
const route = 'personal-files/details';
this.store.dispatch(new NavigateRouteAction([route, action.payload.entry.id, 'permissions']));
} else {
this.store
.select(getAppSelection)
.pipe(take(1))
.subscribe((selection) => {
if (selection && !selection.isEmpty) {
const route = 'personal-files/details';
this.store.dispatch(new NavigateRouteAction([route, selection.first.entry.id, 'permissions']));
}
});
}
})
),
{ dispatch: false }
);
expandInfoDrawer$ = createEffect(
() =>
this.actions$.pipe(
ofType<ExpandInfoDrawerAction>(NodeActionTypes.ExpandInfoDrawer),
map((action) => {
if (action?.payload) {
const route = 'personal-files/details';
this.store.dispatch(new NavigateRouteAction([route, action.payload.entry.id]));
} else {
this.store
.select(getAppSelection)
.pipe(take(1))
.subscribe((selection) => {
if (selection && !selection.isEmpty) {
const route = 'personal-files/details';
this.store.dispatch(new NavigateRouteAction([route, selection.first.entry.id]));
}
});
}
})
),
{ dispatch: false }
);
manageVersions$ = createEffect(
() =>
this.actions$.pipe(
ofType<ManageVersionsAction>(NodeActionTypes.ManageVersions),
map((action) => {
if (action?.payload) {
this.contentService.manageVersions(action.payload);
} else {
this.store
.select(getAppSelection)
.pipe(take(1))
.subscribe((selection) => {
if (selection && selection.file) {
this.contentService.manageVersions(selection.file, action.configuration?.focusedElementOnCloseSelector);
}
});
}
})
),
{ dispatch: false }
);
printFile$ = createEffect(
() =>
this.actions$.pipe(
ofType<PrintFileAction>(NodeActionTypes.PrintFile),
map((action) => {
if (action && action.payload) {
this.printFile(action.payload);
} else {
this.store
.select(getAppSelection)
.pipe(take(1))
.subscribe((selection) => {
if (selection && selection.file) {
this.printFile(selection.file);
}
});
}
})
),
{ dispatch: false }
);
unlockWrite$ = createEffect(
() =>
this.actions$.pipe(
ofType<UnlockWriteAction>(NodeActionTypes.UnlockForWriting),
map((action) => {
if (action && action.payload) {
this.contentService.unlockNode(action.payload);
} else {
this.store
.select(getAppSelection)
.pipe(take(1))
.subscribe((selection) => {
if (selection && selection.file) {
this.contentService.unlockNode(selection.file);
}
});
}
})
),
{ dispatch: false }
);
aspectList$ = createEffect(
() =>
this.actions$.pipe(
ofType<ManageAspectsAction>(NodeActionTypes.ChangeAspects),
map((action) => {
if (action?.payload) {
this.contentService.manageAspects(action.payload);
} else {
this.store
.select(getAppSelection)
.pipe(take(1))
.subscribe((selection) => {
if (selection && !selection.isEmpty) {
this.contentService.manageAspects(selection.nodes[0], action.configuration?.focusedElementOnCloseSelector);
}
});
}
})
),
{ dispatch: false }
);
printFile(node: any) {
if (node && node.entry) {
// shared and favorite
const id = node.entry.nodeId || node.entry.guid || node.entry.id;
const mimeType = node.entry.content.mimeType;
if (id) {
this.viewUtils.printFileGeneric(id, mimeType);
}
}
}
manageRules$ = createEffect(
() =>
this.actions$.pipe(
ofType<ManageRulesAction>(NodeActionTypes.ManageRules),
map((action) => {
if (action?.payload) {
this.store.dispatch(new NavigateRouteAction(['nodes', action.payload.entry.id, 'rules']));
} else {
this.store
.select(getAppSelection)
.pipe(take(1))
.subscribe((selection) => {
if (selection && !selection.isEmpty) {
this.store.dispatch(new NavigateRouteAction(['nodes', selection.first.entry.id, 'rules']));
}
});
}
})
),
{ dispatch: false }
);
}

View File

@@ -0,0 +1,81 @@
/*!
* @license
* Alfresco Example Content Application
*
* Copyright (C) 2005 - 2020 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 { SearchEffects } from './search.effects';
import { EffectsModule } from '@ngrx/effects';
import { Store } from '@ngrx/store';
import { Router } from '@angular/router';
import { SearchOptionIds, SearchByTermAction } from '@alfresco/aca-shared/store';
describe('SearchEffects', () => {
let store: Store<any>;
let router: Router;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [AppTestingModule, EffectsModule.forRoot([SearchEffects])]
});
store = TestBed.inject(Store);
router = TestBed.inject(Router);
spyOn(router, 'navigateByUrl').and.stub();
});
describe('searchByTerm$', () => {
it('should navigate to `search` when search options has library false', fakeAsync(() => {
store.dispatch(new SearchByTermAction('test', []));
tick();
expect(router.navigateByUrl).toHaveBeenCalledWith('/search;q=test');
}));
it('should navigate to `search-libraries` when search options has library true', fakeAsync(() => {
store.dispatch(
new SearchByTermAction('test', [
{
id: SearchOptionIds.Libraries,
value: true,
key: '',
shouldDisable: null
}
])
);
tick();
expect(router.navigateByUrl).toHaveBeenCalledWith('/search-libraries;q=test');
}));
it('should encode search string for parentheses', fakeAsync(() => {
store.dispatch(new SearchByTermAction('(test)', []));
tick();
expect(router.navigateByUrl).toHaveBeenCalledWith('/search;q=%2528test%2529');
}));
});
});

View File

@@ -0,0 +1,54 @@
/*!
* @license
* Alfresco Example Content Application
*
* Copyright (C) 2005 - 2020 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 { Actions, ofType, createEffect } from '@ngrx/effects';
import { Injectable } from '@angular/core';
import { map } from 'rxjs/operators';
import { SearchActionTypes, SearchByTermAction, SearchOptionIds } from '@alfresco/aca-shared/store';
import { Router } from '@angular/router';
@Injectable()
export class SearchEffects {
constructor(private actions$: Actions, private router: Router) {}
searchByTerm$ = createEffect(
() =>
this.actions$.pipe(
ofType<SearchByTermAction>(SearchActionTypes.SearchByTerm),
map((action) => {
const query = action.payload.replace(/[(]/g, '%28').replace(/[)]/g, '%29');
const libItem = action.searchOptions.find((item) => item.id === SearchOptionIds.Libraries);
const librarySelected = !!libItem && libItem.value;
if (librarySelected) {
this.router.navigateByUrl('/search-libraries;q=' + encodeURIComponent(query));
} else {
this.router.navigateByUrl('/search;q=' + encodeURIComponent(query));
}
})
),
{ dispatch: false }
);
}

View File

@@ -0,0 +1,223 @@
/*!
* @license
* Alfresco Example Content Application
*
* Copyright (C) 2005 - 2020 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 { fakeAsync, TestBed, tick } from '@angular/core/testing';
import { AppTestingModule } from '../../testing/app-testing.module';
import { TemplateEffects } from './template.effects';
import { EffectsModule } from '@ngrx/effects';
import { Store } from '@ngrx/store';
import { CreateFromTemplate, CreateFromTemplateSuccess, FileFromTemplate, FolderFromTemplate, SnackbarErrorAction } from '@alfresco/aca-shared/store';
import { NodeTemplateService } from '../../services/node-template.service';
import { of, Subject } from 'rxjs';
import { Node, NodeEntry } from '@alfresco/js-api';
import { MatDialog, MatDialogRef } from '@angular/material/dialog';
import { CreateFromTemplateDialogComponent } from '../../dialogs/node-template/create-from-template.dialog';
import { AppHookService } from '@alfresco/aca-shared';
describe('TemplateEffects', () => {
let store: Store<any>;
let nodeTemplateService: NodeTemplateService;
let appHookService: AppHookService;
let templateEffects: TemplateEffects;
let copyNodeSpy;
let updateNodeSpy;
let matDialog: MatDialog;
const node: Node = {
name: 'node-name',
id: 'node-id',
nodeType: 'cm:content',
isFolder: false,
isFile: true,
modifiedAt: null,
modifiedByUser: null,
createdAt: null,
createdByUser: null,
properties: {
'cm:title': 'title',
'cm:description': 'description'
}
};
const fileTemplateConfig = {
primaryPathName: 'app:node_templates',
selectionType: 'file'
};
const folderTemplateConfig = {
primaryPathName: 'app:space_templates',
selectionType: 'folder'
};
let subject: Subject<Node[]>;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [AppTestingModule, EffectsModule.forRoot([TemplateEffects])],
providers: [
NodeTemplateService,
{
provide: MatDialog,
useValue: {
closeAll: jasmine.createSpy('closeAll')
}
}
]
});
store = TestBed.inject(Store);
nodeTemplateService = TestBed.inject(NodeTemplateService);
templateEffects = TestBed.inject(TemplateEffects);
appHookService = TestBed.inject(AppHookService);
matDialog = TestBed.inject(MatDialog);
subject = new Subject<Node[]>();
spyOn(store, 'dispatch').and.callThrough();
spyOn(appHookService.reload, 'next');
spyOn(store, 'select').and.returnValue(of({ id: 'parent-id' }));
spyOn(nodeTemplateService, 'selectTemplateDialog').and.returnValue(subject);
copyNodeSpy = spyOn(templateEffects['nodesApi'], 'copyNode');
updateNodeSpy = spyOn(templateEffects['nodesApi'], 'updateNode');
});
afterEach(() => {
copyNodeSpy.calls.reset();
updateNodeSpy.calls.reset();
});
it('should call createTemplateDialog on FileFromTemplate action', fakeAsync(() => {
spyOn(nodeTemplateService, 'createTemplateDialog');
store.dispatch(new FileFromTemplate());
subject.next([node]);
tick(300);
expect(nodeTemplateService.createTemplateDialog).toHaveBeenCalledWith(node);
}));
it('should call createTemplateDialog on FolderFromTemplate action', fakeAsync(() => {
spyOn(nodeTemplateService, 'createTemplateDialog');
store.dispatch(new FolderFromTemplate());
subject.next([node]);
tick(300);
expect(nodeTemplateService.createTemplateDialog).toHaveBeenCalledWith(node);
}));
it('should open dialog to select template files', fakeAsync(() => {
spyOn(nodeTemplateService, 'createTemplateDialog').and.returnValue({
afterClosed: () => of(node)
} as MatDialogRef<CreateFromTemplateDialogComponent>);
store.dispatch(new FileFromTemplate());
tick();
expect(nodeTemplateService.selectTemplateDialog).toHaveBeenCalledWith(fileTemplateConfig);
}));
it('should open dialog to select template folders', fakeAsync(() => {
spyOn(nodeTemplateService, 'createTemplateDialog').and.returnValue({
afterClosed: () => of(node)
} as MatDialogRef<CreateFromTemplateDialogComponent>);
store.dispatch(new FolderFromTemplate());
tick();
expect(nodeTemplateService.selectTemplateDialog).toHaveBeenCalledWith(folderTemplateConfig);
}));
it('should create node from template successful', fakeAsync(() => {
copyNodeSpy.and.returnValue(of({ entry: { id: 'node-id', properties: {} } }));
updateNodeSpy.and.returnValue(of({ entry: node }));
store.dispatch(new CreateFromTemplate(node));
tick();
expect(store.dispatch['calls'].mostRecent().args[0]).toEqual(new CreateFromTemplateSuccess(node));
}));
it('should raise generic error when copyNode api fails', fakeAsync(() => {
copyNodeSpy.and.returnValue(
Promise.reject({
message: `{ "error": { "statusCode": 404 } } `
})
);
store.dispatch(new CreateFromTemplate(node));
tick();
expect(store.dispatch['calls'].mostRecent().args[0]).not.toEqual(new CreateFromTemplateSuccess(node));
expect(store.dispatch['calls'].argsFor(1)[0]).toEqual(new SnackbarErrorAction('APP.MESSAGES.ERRORS.GENERIC'));
}));
it('should raise name conflict error when copyNode api returns 409', fakeAsync(() => {
copyNodeSpy.and.returnValue(
Promise.reject({
message: `{ "error": { "statusCode": 409 } } `
})
);
store.dispatch(new CreateFromTemplate(node));
tick();
expect(store.dispatch['calls'].mostRecent().args[0]).not.toEqual(new CreateFromTemplateSuccess(node));
expect(store.dispatch['calls'].argsFor(1)[0]).toEqual(new SnackbarErrorAction('APP.MESSAGES.ERRORS.CONFLICT'));
}));
it('should resolve error with current node value when updateNode api fails', fakeAsync(() => {
const TEST_NODE = {
entry: {
id: 'test-node-id',
properties: {
'cm:title': 'test-node-title',
'cm:description': 'test-node-description'
}
}
} as NodeEntry;
copyNodeSpy.and.returnValue(of(TEST_NODE));
updateNodeSpy.and.returnValue(
Promise.reject({
message: `{ "error": { "statusCode": 404 } } `
})
);
store.dispatch(new CreateFromTemplate(TEST_NODE.entry));
tick();
expect(store.dispatch['calls'].mostRecent().args[0]).toEqual(new CreateFromTemplateSuccess(TEST_NODE.entry));
}));
it('should close dialog on create template success', fakeAsync(() => {
store.dispatch(new CreateFromTemplateSuccess({} as Node));
tick();
expect(matDialog.closeAll).toHaveBeenCalled();
}));
it('should should reload content on create template success', fakeAsync(() => {
const TEST_NODE = { id: 'test-node-id' } as Node;
store.dispatch(new CreateFromTemplateSuccess(TEST_NODE));
tick();
expect(appHookService.reload.next).toHaveBeenCalledWith(TEST_NODE);
}));
});

View File

@@ -0,0 +1,172 @@
/*!
* @license
* Alfresco Example Content Application
*
* Copyright (C) 2005 - 2020 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 { Actions, ofType, createEffect } from '@ngrx/effects';
import { Injectable } from '@angular/core';
import { map, switchMap, debounceTime, take, catchError } from 'rxjs/operators';
import { Store } from '@ngrx/store';
import {
FileFromTemplate,
FolderFromTemplate,
CreateFromTemplate,
CreateFromTemplateSuccess,
TemplateActionTypes,
getCurrentFolder,
AppStore,
SnackbarErrorAction
} from '@alfresco/aca-shared/store';
import { NodeTemplateService, TemplateDialogConfig } from '../../services/node-template.service';
import { AlfrescoApiService } from '@alfresco/adf-core';
import { AppHookService } from '@alfresco/aca-shared';
import { from, Observable, of } from 'rxjs';
import { NodeEntry, NodeBodyUpdate, Node, NodesApi } from '@alfresco/js-api';
import { MatDialog } from '@angular/material/dialog';
@Injectable()
export class TemplateEffects {
private _nodesApi: NodesApi;
get nodesApi(): NodesApi {
this._nodesApi = this._nodesApi ?? new NodesApi(this.apiService.getInstance());
return this._nodesApi;
}
constructor(
private matDialog: MatDialog,
private appHookService: AppHookService,
private store: Store<AppStore>,
private apiService: AlfrescoApiService,
private actions$: Actions,
private nodeTemplateService: NodeTemplateService
) {}
fileFromTemplate$ = createEffect(
() =>
this.actions$.pipe(
ofType<FileFromTemplate>(TemplateActionTypes.FileFromTemplate),
map(() => {
this.openDialog({
primaryPathName: 'app:node_templates',
selectionType: 'file'
});
})
),
{ dispatch: false }
);
folderFromTemplate$ = createEffect(
() =>
this.actions$.pipe(
ofType<FolderFromTemplate>(TemplateActionTypes.FolderFromTemplate),
map(() =>
this.openDialog({
primaryPathName: 'app:space_templates',
selectionType: 'folder'
})
)
),
{ dispatch: false }
);
createFromTemplate$ = createEffect(
() =>
this.actions$.pipe(
ofType<CreateFromTemplate>(TemplateActionTypes.CreateFromTemplate),
map((action) => {
this.store
.select(getCurrentFolder)
.pipe(
switchMap((folder) => this.copyNode(action.payload, folder.id)),
take(1)
)
.subscribe((node: NodeEntry | null) => {
if (node) {
this.store.dispatch(new CreateFromTemplateSuccess(node.entry));
}
});
})
),
{ dispatch: false }
);
createFromTemplateSuccess$ = createEffect(
() =>
this.actions$.pipe(
ofType<CreateFromTemplateSuccess>(TemplateActionTypes.CreateFromTemplateSuccess),
map((payload) => {
this.matDialog.closeAll();
this.appHookService.reload.next(payload.node);
})
),
{ dispatch: false }
);
private openDialog(config: TemplateDialogConfig) {
this.nodeTemplateService
.selectTemplateDialog(config)
.pipe(debounceTime(300))
.subscribe(([node]) => this.nodeTemplateService.createTemplateDialog(node));
}
private copyNode(source: Node, parentId: string): Observable<NodeEntry> {
return from(
this.nodesApi.copyNode(source.id, {
targetParentId: parentId,
name: source.name
})
).pipe(
switchMap((node) =>
this.updateNode(node, {
properties: {
'cm:title': source.properties['cm:title'],
'cm:description': source.properties['cm:description']
}
})
),
catchError((error) => this.handleError(error))
);
}
private updateNode(node: NodeEntry, update: NodeBodyUpdate): Observable<NodeEntry> {
return from(this.nodesApi.updateNode(node.entry.id, update)).pipe(catchError(() => of(node)));
}
private handleError(error: Error): Observable<null> {
let statusCode: number;
try {
statusCode = JSON.parse(error.message).error.statusCode;
} catch (e) {
statusCode = null;
}
if (statusCode !== 409) {
this.store.dispatch(new SnackbarErrorAction('APP.MESSAGES.ERRORS.GENERIC'));
} else {
this.store.dispatch(new SnackbarErrorAction('APP.MESSAGES.ERRORS.CONFLICT'));
}
return of(null);
}
}

View File

@@ -0,0 +1,286 @@
/*!
* @license
* Alfresco Example Content Application
*
* Copyright (C) 2005 - 2020 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 { Store } from '@ngrx/store';
import { TestBed } from '@angular/core/testing';
import { EffectsModule } from '@ngrx/effects';
import { UploadEffects } from './upload.effects';
import { AppTestingModule } from '../../testing/app-testing.module';
import { NgZone } from '@angular/core';
import { UploadService, FileUploadCompleteEvent, FileModel } from '@alfresco/adf-core';
import { UnlockWriteAction, UploadFilesAction, UploadFileVersionAction, UploadFolderAction } from '@alfresco/aca-shared/store';
import { ContentManagementService } from '../../services/content-management.service';
describe('UploadEffects', () => {
let store: Store<any>;
let uploadService: UploadService;
let effects: UploadEffects;
let zone: NgZone;
let contentManagementService: ContentManagementService;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [AppTestingModule, EffectsModule.forRoot([UploadEffects])]
});
zone = TestBed.inject(NgZone);
spyOn(zone, 'run').and.callFake((fn: () => any) => fn());
contentManagementService = TestBed.inject(ContentManagementService);
store = TestBed.inject(Store);
uploadService = TestBed.inject(UploadService);
effects = TestBed.inject(UploadEffects);
});
beforeEach(() => {
spyOn(effects['fileVersionInput'], 'click');
spyOn(effects, 'uploadVersion').and.callThrough();
});
describe('uploadFiles$', () => {
let createMenuButton: HTMLButtonElement;
const focusedClass = 'cdk-program-focused';
beforeEach(() => {
createMenuButton = document.createElement('button');
document.body.appendChild(createMenuButton);
store.dispatch(new UploadFilesAction({}));
spyOn(document, 'querySelector').withArgs('app-create-menu button').and.returnValue(createMenuButton);
});
it('should call focus function on create menu button', () => {
spyOn(createMenuButton, 'focus');
window.dispatchEvent(new FocusEvent('focus'));
expect(createMenuButton.focus).toHaveBeenCalledWith();
});
it('should not call focus function on create menu button if handler for focus of window is not fired', () => {
spyOn(createMenuButton, 'focus');
expect(createMenuButton.focus).not.toHaveBeenCalled();
});
it('should add cdk-program-focused class to create menu button', () => {
window.dispatchEvent(new FocusEvent('focus'));
createMenuButton.dispatchEvent(new FocusEvent('focus'));
expect(createMenuButton).toHaveClass(focusedClass);
});
it('should not add cdk-program-focused class to create menu button if handler for focus of window is not fired', () => {
expect(createMenuButton).not.toHaveClass(focusedClass);
});
afterEach(() => {
createMenuButton.remove();
});
});
describe('uploadFolder$', () => {
let createMenuButton: HTMLButtonElement;
const focusedClass = 'cdk-program-focused';
beforeEach(() => {
createMenuButton = document.createElement('button');
document.body.appendChild(createMenuButton);
store.dispatch(new UploadFolderAction({}));
spyOn(document, 'querySelector').withArgs('app-create-menu button').and.returnValue(createMenuButton);
});
it('should call focus function on create menu button', () => {
spyOn(createMenuButton, 'focus');
window.dispatchEvent(new FocusEvent('focus'));
expect(createMenuButton.focus).toHaveBeenCalledWith();
});
it('should not call focus function on create menu button if handler for focus of window is not fired', () => {
spyOn(createMenuButton, 'focus');
expect(createMenuButton.focus).not.toHaveBeenCalled();
});
it('should add cdk-program-focused class to create menu button', () => {
window.dispatchEvent(new FocusEvent('focus'));
createMenuButton.dispatchEvent(new FocusEvent('focus'));
expect(createMenuButton).toHaveClass(focusedClass);
});
it('should not add cdk-program-focused class to create menu button if handler for focus of window is not fired', () => {
expect(createMenuButton).not.toHaveClass(focusedClass);
});
afterEach(() => {
createMenuButton.remove();
});
});
describe('uploadAndUnlock()', () => {
it('should not upload and unlock file if param not provided', () => {
effects.uploadAndUnlock(null);
expect(zone.run).not.toHaveBeenCalled();
});
it('should upload the file before unlocking', () => {
const file: any = {};
spyOn(uploadService, 'addToQueue').and.stub();
spyOn(uploadService, 'uploadFilesInTheQueue').and.stub();
effects.uploadAndUnlock(file);
expect(uploadService.addToQueue).toHaveBeenCalled();
expect(uploadService.uploadFilesInTheQueue).toHaveBeenCalled();
});
it('should dispatch the unlock write action for a locked file', () => {
const file: FileModel = new FileModel({ name: 'file1.png', size: 10 } as File, null, 'file1');
file.data = {
entry: {
id: 'file1',
properties: {
'cm:lockType': 'WRITE_LOCK'
}
}
};
spyOn(uploadService, 'addToQueue').and.stub();
spyOn(uploadService, 'uploadFilesInTheQueue').and.stub();
spyOn(store, 'dispatch').and.stub();
effects.uploadAndUnlock(file);
uploadService.fileUploadComplete.next(new FileUploadCompleteEvent(file, 100, file.data));
expect(store.dispatch).toHaveBeenCalledWith(new UnlockWriteAction(file.data));
});
it('should dispatch only one unlock action for a locked file', () => {
const file: FileModel = new FileModel({ name: 'file1.png', size: 10 } as File, null, 'file1');
file.data = {
entry: {
id: 'file1',
properties: {
'cm:lockType': 'WRITE_LOCK'
}
}
};
spyOn(uploadService, 'addToQueue').and.stub();
spyOn(uploadService, 'uploadFilesInTheQueue').and.stub();
spyOn(store, 'dispatch').and.stub();
effects.uploadAndUnlock(file);
const completeEvent = new FileUploadCompleteEvent(file, 100, file.data);
uploadService.fileUploadComplete.next(completeEvent);
uploadService.fileUploadComplete.next(completeEvent);
uploadService.fileUploadComplete.next(completeEvent);
expect(store.dispatch).toHaveBeenCalledWith(new UnlockWriteAction(file.data));
expect(store.dispatch).toHaveBeenCalledTimes(1);
});
it('should dispatch no actions if file is not locked', () => {
const file: FileModel = new FileModel({ name: 'file1.png', size: 10 } as File, null, 'file1');
file.data = {
entry: {
id: 'file1',
properties: {}
}
};
spyOn(uploadService, 'addToQueue').and.stub();
spyOn(uploadService, 'uploadFilesInTheQueue').and.stub();
spyOn(store, 'dispatch').and.stub();
effects.uploadAndUnlock(file);
uploadService.fileUploadComplete.next(new FileUploadCompleteEvent(file, 100, file.data));
expect(store.dispatch).not.toHaveBeenCalled();
});
});
describe('upload file version', () => {
it('should trigger upload file from context menu', () => {
store.dispatch({ type: 'UPLOAD_FILE_VERSION' });
expect(effects['fileVersionInput'].click).toHaveBeenCalled();
});
it('should upload file from dropping another file', () => {
spyOn(contentManagementService, 'versionUpdateDialog');
const fakeEvent = new CustomEvent('upload-files', {
detail: {
files: [
{
file: new FileModel({
name: 'Fake New file',
type: 'image/png',
lastModified: 1589273450599,
size: 1351,
slice: null
} as File),
entry: new FileModel({
name: 'Fake New file',
type: 'image/png',
lastModified: 1589273450599,
size: 1351,
slice: null
} as File)
}
],
data: {
node: {
entry: {
isFile: true,
createdByUser: {
id: 'admin.adf@alfresco.com',
displayName: 'Administrator'
},
modifiedAt: '2020-06-09T08:13:40.569Z',
nodeType: 'cm:content',
content: {
mimeType: 'image/jpeg',
mimeTypeName: 'JPEG Image',
sizeInBytes: 175540,
encoding: 'UTF-8'
},
parentId: 'dff2bc1e-d092-42ac-82d1-87c82f6e56cb',
createdAt: '2020-05-14T08:52:03.868Z',
isFolder: false,
name: 'GoqZhm.jpg',
id: '1bf8a8f7-18ac-4eef-919d-61d952eaa179',
allowableOperations: ['delete', 'update', 'updatePermissions'],
isFavorite: false
}
}
}
}
});
store.dispatch(new UploadFileVersionAction(fakeEvent));
expect(contentManagementService.versionUpdateDialog).toHaveBeenCalledWith(fakeEvent.detail.data.node.entry, fakeEvent.detail.files[0].file);
});
});
});

View File

@@ -0,0 +1,227 @@
/*!
* @license
* Alfresco Example Content Application
*
* Copyright (C) 2005 - 2020 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 {
AppStore,
SnackbarErrorAction,
UnlockWriteAction,
UploadActionTypes,
UploadFilesAction,
UploadFileVersionAction,
UploadFolderAction,
getCurrentFolder
} from '@alfresco/aca-shared/store';
import { FileModel, FileUtils, UploadService } from '@alfresco/adf-core';
import { Injectable, NgZone, RendererFactory2 } from '@angular/core';
import { Actions, ofType, createEffect } from '@ngrx/effects';
import { Store } from '@ngrx/store';
import { of } from 'rxjs';
import { catchError, map, take } from 'rxjs/operators';
import { ContentManagementService } from '../../services/content-management.service';
import { MinimalNodeEntryEntity } from '@alfresco/js-api';
@Injectable()
export class UploadEffects {
private fileInput: HTMLInputElement;
private folderInput: HTMLInputElement;
private fileVersionInput: HTMLInputElement;
private readonly createMenuButtonSelector = 'app-create-menu button';
constructor(
private store: Store<AppStore>,
private actions$: Actions,
private ngZone: NgZone,
private uploadService: UploadService,
rendererFactory: RendererFactory2,
private contentService: ContentManagementService
) {
const renderer = rendererFactory.createRenderer(null, null);
this.fileInput = renderer.createElement('input') as HTMLInputElement;
this.fileInput.id = 'app-upload-files';
this.fileInput.type = 'file';
this.fileInput.style.display = 'none';
this.fileInput.setAttribute('multiple', '');
this.fileInput.addEventListener('change', (event) => this.upload(event));
renderer.appendChild(document.body, this.fileInput);
this.fileVersionInput = renderer.createElement('input') as HTMLInputElement;
this.fileVersionInput.id = 'app-upload-file-version';
this.fileVersionInput.type = 'file';
this.fileVersionInput.style.display = 'none';
this.fileVersionInput.addEventListener('change', () => {
this.uploadVersion();
});
renderer.appendChild(document.body, this.fileVersionInput);
this.folderInput = renderer.createElement('input') as HTMLInputElement;
this.folderInput.id = 'app-upload-folder';
this.folderInput.type = 'file';
this.folderInput.style.display = 'none';
this.folderInput.setAttribute('directory', '');
this.folderInput.setAttribute('webkitdirectory', '');
this.folderInput.addEventListener('change', (event) => this.upload(event));
renderer.appendChild(document.body, this.folderInput);
}
uploadFiles$ = createEffect(
() =>
this.actions$.pipe(
ofType<UploadFilesAction>(UploadActionTypes.UploadFiles),
map(() => {
this.registerFocusingElementAfterModalClose(this.fileInput, this.createMenuButtonSelector);
this.fileInput.click();
})
),
{ dispatch: false }
);
uploadFolder$ = createEffect(
() =>
this.actions$.pipe(
ofType<UploadFolderAction>(UploadActionTypes.UploadFolder),
map(() => {
this.registerFocusingElementAfterModalClose(this.folderInput, this.createMenuButtonSelector);
this.folderInput.click();
})
),
{ dispatch: false }
);
uploadVersion$ = createEffect(
() =>
this.actions$.pipe(
ofType<UploadFileVersionAction>(UploadActionTypes.UploadFileVersion),
map((action) => {
if (action?.payload) {
const node = action?.payload?.detail?.data?.node?.entry;
const file: any = action?.payload?.detail?.files[0]?.file;
this.contentService.versionUpdateDialog(node, file);
} else if (!action?.payload) {
this.registerFocusingElementAfterModalClose(this.fileVersionInput, action.configuration?.focusedElementOnCloseSelector);
this.fileVersionInput.click();
}
})
),
{ dispatch: false }
);
uploadVersion() {
this.contentService
.getNodeInfo()
.pipe(
catchError((_) => {
this.store.dispatch(new SnackbarErrorAction('VERSION.ERROR.GENERIC'));
return of(null);
})
)
.subscribe((node: MinimalNodeEntryEntity) => {
if (node) {
this.contentService.versionUpdateDialog(node, this.fileVersionInput.files[0]);
this.fileVersionInput.value = '';
}
});
}
private upload(event: any): void {
this.store
.select(getCurrentFolder)
.pipe(take(1))
.subscribe((node) => {
if (node && node.id) {
const input = event.currentTarget as HTMLInputElement;
const files = FileUtils.toFileArray(input.files).map(
(file: any) =>
new FileModel(file, {
parentId: node.id,
path: (file.webkitRelativePath || '').replace(/\/[^\/]*$/, ''),
nodeType: 'cm:content'
})
);
this.uploadQueue(files);
event.target.value = '';
}
});
}
private uploadQueue(files: FileModel[]) {
if (files.length > 0) {
this.ngZone.run(() => {
this.uploadService.addToQueue(...files);
this.uploadService.uploadFilesInTheQueue();
});
}
}
uploadAndUnlock(file: FileModel | null) {
if (!file) {
return;
}
this.ngZone.run(() => {
this.uploadService.addToQueue(file);
this.uploadService.uploadFilesInTheQueue();
const subscription = this.uploadService.fileUploadComplete.subscribe((completed) => {
if (
file.data &&
file.data.entry &&
file.data.entry.properties &&
file.data.entry.properties['cm:lockType'] === 'WRITE_LOCK' &&
file.data.entry.id === completed.data.entry.id
) {
this.store.dispatch(new UnlockWriteAction(completed.data));
}
subscription.unsubscribe();
});
});
}
private registerFocusingElementAfterModalClose(input: HTMLInputElement, focusedElementSelector: string): void {
input.addEventListener(
'click',
() => {
window.addEventListener(
'focus',
() => {
const elementToFocus = document.querySelector<HTMLElement>(focusedElementSelector);
elementToFocus.addEventListener('focus', () => elementToFocus.classList.add('cdk-program-focused'), {
once: true
});
elementToFocus.focus();
},
{
once: true
}
);
},
{
once: true
}
);
}
}

View File

@@ -0,0 +1,91 @@
/*!
* @license
* Alfresco Example Content Application
*
* Copyright (C) 2005 - 2020 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.inject(Store);
router = TestBed.inject(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 if', fakeAsync(() => {
store.dispatch(new ViewNodeAction('nodeId', { location: 'some-location' }));
tick(100);
expect(router.navigateByUrl['calls'].argsFor(0)[0].toString()).toEqual('/some-location/(viewer:view/nodeId)?location=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)');
}));
it('should navigate to viewer route with query param if path is passed', fakeAsync(() => {
store.dispatch(new ViewNodeAction('nodeId', { path: 'absolute-path' }));
tick(100);
expect(router.navigateByUrl['calls'].argsFor(0)[0].toString()).toEqual('/view/(viewer:nodeId)?path=absolute-path');
}));
});
});

View File

@@ -0,0 +1,226 @@
/*!
* @license
* Alfresco Example Content Application
*
* Copyright (C) 2005 - 2020 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 { Actions, ofType, createEffect } from '@ngrx/effects';
import { Injectable } from '@angular/core';
import { map, take, tap } from 'rxjs/operators';
import {
AppStore,
ViewerActionTypes,
ViewFileAction,
ViewNodeAction,
getCurrentFolder,
getAppSelection,
FullscreenViewerAction,
ViewNodeVersionAction,
PluginPreviewAction
} from '@alfresco/aca-shared/store';
import { Router, UrlTree, UrlSegmentGroup, PRIMARY_OUTLET, UrlSegment } from '@angular/router';
import { Store, createSelector } from '@ngrx/store';
import { AppExtensionService } from '@alfresco/aca-shared';
import { MatDialog } from '@angular/material/dialog';
export const fileToPreview = createSelector(getAppSelection, getCurrentFolder, (selection, folder) => ({
selection,
folder
}));
@Injectable()
export class ViewerEffects {
constructor(
private store: Store<AppStore>,
private actions$: Actions,
private router: Router,
private extensions: AppExtensionService,
private dialog: MatDialog
) {}
fullscreenViewer$ = createEffect(
() =>
this.actions$.pipe(
ofType<FullscreenViewerAction>(ViewerActionTypes.FullScreen),
map(() => {
this.enterFullScreen();
})
),
{ dispatch: false }
);
viewNode$ = createEffect(
() =>
this.actions$.pipe(
ofType<ViewNodeAction>(ViewerActionTypes.ViewNode),
map((action) => {
if (action.viewNodeExtras) {
const { location, path } = action.viewNodeExtras;
if (location) {
const navigation = this.getNavigationCommands(location);
this.router.navigate([...navigation, { outlets: { viewer: ['view', action.nodeId] } }], {
queryParams: { location }
});
}
if (path) {
this.router.navigate(['view', { outlets: { viewer: [action.nodeId] } }], {
queryParams: { path }
});
}
} else {
this.router.navigate(['view', { outlets: { viewer: [action.nodeId] } }]);
}
})
),
{ dispatch: false }
);
viewFile$ = createEffect(
() =>
this.actions$.pipe(
ofType<ViewFileAction>(ViewerActionTypes.ViewFile),
map((action) => {
if (action.payload && action.payload.entry) {
const { id, nodeId, isFile } = action.payload.entry as any;
if (this.extensions.canPreviewNode(action.payload) && (isFile || nodeId)) {
this.displayPreview(nodeId || id, action.parentId);
}
} else {
this.store
.select(fileToPreview)
.pipe(take(1))
.subscribe((result) => {
if (result.selection && result.selection.file) {
const { id, nodeId, isFile } = result.selection.file.entry as any;
if (this.extensions.canPreviewNode(action.payload) && (isFile || nodeId)) {
const parentId = result.folder ? result.folder.id : null;
this.displayPreview(nodeId || id, parentId);
}
}
});
}
})
),
{ dispatch: false }
);
viewNodeVersion$ = createEffect(
() =>
this.actions$.pipe(
ofType<ViewNodeVersionAction>(ViewerActionTypes.ViewNodeVersion),
map((action) => {
this.dialog.closeAll();
if (action.viewNodeExtras) {
const { location, path } = action.viewNodeExtras;
if (location) {
const navigation = this.getNavigationCommands(location);
this.router.navigate([...navigation, { outlets: { viewer: ['view', action.nodeId, action.versionId] } }], {
queryParams: { location }
});
}
if (path) {
this.router.navigate(['view', { outlets: { viewer: [action.nodeId, action.versionId] } }], {
queryParams: { path }
});
}
} else {
this.router.navigate(['view', { outlets: { viewer: [action.nodeId, action.versionId] } }]);
}
})
),
{ dispatch: false }
);
pluginPreview$ = createEffect(
() =>
this.actions$.pipe(
ofType<PluginPreviewAction>(ViewerActionTypes.PluginPreview),
tap((action) => {
this.router.navigate([
action.pluginRoute,
{
outlets: {
viewer: ['preview', action.nodeId]
}
}
]);
})
),
{ dispatch: false }
);
private displayPreview(nodeId: string, parentId: string) {
if (!nodeId) {
return;
}
let previewLocation = this.router.url;
if (previewLocation.lastIndexOf('/') > 0) {
previewLocation = previewLocation.substr(0, this.router.url.indexOf('/', 1));
}
previewLocation = previewLocation.replace(/\//g, '');
const path = [previewLocation];
if (parentId) {
path.push(parentId);
}
path.push('preview', nodeId);
this.router.navigateByUrl(path.join('/'));
}
enterFullScreen() {
const container: any = document.documentElement.querySelector('.adf-viewer__fullscreen-container');
if (container) {
if (container.requestFullscreen) {
container.requestFullscreen();
} else if (container.webkitRequestFullscreen) {
container.webkitRequestFullscreen();
} else if (container.mozRequestFullScreen) {
container.mozRequestFullScreen();
} else if (container.msRequestFullscreen) {
container.msRequestFullscreen();
}
}
}
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;
}, []);
}
}