mirror of
https://github.com/Alfresco/alfresco-content-app.git
synced 2025-05-12 17:04:46 +00:00
[ACA-1631] more application ngrx actions (#540)
* delete action * library path evaluator * extension for sharing files * upload actions * delete library * use extensions for experimental library actions * unshare nodes * fix icons and titles * "create menu" backed by core extension * support for descriptions, update upload selector * update code and tests * support disabled tooltips for navbar * fix selector * [ACA-1486] remove double fetch call * migrate to trashcan actions, element IDs * cleanup code, remove deprecated directives * add/remove favorite * improve rendering performance * update favorites without reload * support for adding Sites to favorites * disable favorites for Libraries for now * copy action * move node * manage versions and permissions * cleanup code * toggle info drawer * card view mode * use extension layer for favorites toolbar * fix menu tooltips * fix 'remove as favorite' tests * update tests * test fixes * fix edit folder for favorites * fix test * cleanup favorites layout * upgrade recent files layout * update evaluators for shared nodes * test fixes * test fixes * restore recent files layout * workaround for "favorite" toggle and recent files * upgrade shared files page * upgrade files page layout * fix library evaluator * workaround for shared files and permissions * cleanup code * upgrade search results * upgrade sidebar and viewer actions * code cleanup * code cleanup * code cleanup
This commit is contained in:
parent
617f80c9fd
commit
ae8675dfd7
@ -32,12 +32,12 @@ export class Menu extends Component {
|
||||
root: '.mat-menu-panel',
|
||||
item: '.mat-menu-item',
|
||||
icon: '.mat-icon',
|
||||
uploadFiles: 'input[id="upload-multiple-files"]'
|
||||
uploadFiles: 'app-upload-files'
|
||||
};
|
||||
|
||||
items: ElementArrayFinder = this.component.all(by.css(Menu.selectors.item));
|
||||
backdrop: ElementFinder = browser.element(by.css('.cdk-overlay-backdrop'));
|
||||
uploadFiles: ElementFinder = this.component.element(by.css(Menu.selectors.uploadFiles));
|
||||
uploadFiles: ElementFinder = browser.element(by.id(Menu.selectors.uploadFiles));
|
||||
|
||||
constructor(ancestor?: ElementFinder) {
|
||||
super(Menu.selectors.root, ancestor);
|
||||
|
@ -155,7 +155,7 @@ describe('Create folder', () => {
|
||||
.then(() => menu))
|
||||
.then(menu => {
|
||||
const tooltip = menu.getItemTooltip('Create folder');
|
||||
expect(tooltip).toContain(`You can't create a folder here`);
|
||||
expect(tooltip).toContain(`Folders cannot be created whilst viewing the current items.`);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -115,6 +115,14 @@
|
||||
"description": "Element title",
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"description": "Element description, used for the tooltips.",
|
||||
"type": "string"
|
||||
},
|
||||
"description-disabled": {
|
||||
"description": "Description to use when element is in the disabled state.",
|
||||
"type": "string"
|
||||
},
|
||||
"order": {
|
||||
"description": "Element order",
|
||||
"type": "number"
|
||||
@ -283,6 +291,12 @@
|
||||
"type": "array",
|
||||
"items": { "$ref": "#/definitions/contentActionRef" },
|
||||
"minItems": 1
|
||||
},
|
||||
"actions": {
|
||||
"description": "Content actions (toolbar, context menus, etc.)",
|
||||
"type": "array",
|
||||
"items": { "$ref": "#/definitions/contentActionRef" },
|
||||
"minItems": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -9,7 +9,6 @@
|
||||
"© 2017 - 2018 Alfresco Software, Inc. All rights reserved."
|
||||
},
|
||||
"experimental": {
|
||||
"libraries": false,
|
||||
"comments": false,
|
||||
"cardview": false,
|
||||
"permissions": false,
|
||||
|
@ -73,6 +73,9 @@ import { ViewUtilService} from './services/view-util.service';
|
||||
import { ExtensionService } from './extensions/extension.service';
|
||||
import { AppInfoDrawerModule } from './components/info-drawer/info.drawer.module';
|
||||
import { DirectivesModule } from './directives/directives.module';
|
||||
import { ToggleInfoDrawerComponent } from './components/toolbar/toggle-info-drawer/toggle-info-drawer.component';
|
||||
import { DocumentDisplayModeComponent } from './components/toolbar/document-display-mode/document-display-mode.component';
|
||||
import { ToggleFavoriteComponent } from './components/toolbar/toggle-favorite/toggle-favorite.component';
|
||||
|
||||
export function setupExtensionServiceFactory(service: ExtensionService): Function {
|
||||
return () => service.load();
|
||||
@ -121,7 +124,10 @@ export function setupExtensionServiceFactory(service: ExtensionService): Functio
|
||||
PermissionsManagerComponent,
|
||||
SearchResultsComponent,
|
||||
SettingsComponent,
|
||||
SharedLinkViewComponent
|
||||
SharedLinkViewComponent,
|
||||
ToggleInfoDrawerComponent,
|
||||
DocumentDisplayModeComponent,
|
||||
ToggleFavoriteComponent
|
||||
],
|
||||
providers: [
|
||||
{ provide: RouteReuseStrategy, useClass: AppRouteReuseStrategy },
|
||||
@ -151,7 +157,10 @@ export function setupExtensionServiceFactory(service: ExtensionService): Functio
|
||||
entryComponents: [
|
||||
LibraryDialogComponent,
|
||||
NodeVersionsDialogComponent,
|
||||
NodePermissionsDialogComponent
|
||||
NodePermissionsDialogComponent,
|
||||
ToggleInfoDrawerComponent,
|
||||
DocumentDisplayModeComponent,
|
||||
ToggleFavoriteComponent
|
||||
],
|
||||
bootstrap: [AppComponent]
|
||||
})
|
||||
|
@ -4,129 +4,17 @@
|
||||
</adf-breadcrumb>
|
||||
|
||||
<adf-toolbar class="inline">
|
||||
<button *ifExperimental="'cardview'"
|
||||
color="primary"
|
||||
mat-icon-button
|
||||
(click)="toggleGalleryView()">
|
||||
<mat-icon *ngIf="displayMode === 'list'" matTooltip="{{ 'APP.DOCUMENT_LIST.TOOLBAR.CARDVIEW' | translate }}">view_comfy</mat-icon>
|
||||
<mat-icon *ngIf="displayMode === 'gallery'" matTooltip="{{ 'APP.DOCUMENT_LIST.TOOLBAR.LISTVIEW' | translate }}">list</mat-icon>
|
||||
</button>
|
||||
<app-document-display-mode *ifExperimental="'cardview'"></app-document-display-mode>
|
||||
|
||||
<ng-container *ifExperimental="'extensions'">
|
||||
<ng-container *ngFor="let entry of actions">
|
||||
<aca-toolbar-action [entry]="entry"></aca-toolbar-action>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="!selection.isEmpty">
|
||||
<button
|
||||
mat-icon-button
|
||||
color="primary"
|
||||
*ngIf="selection.file"
|
||||
title="{{ 'APP.ACTIONS.VIEW' | translate }}"
|
||||
(click)="showPreview(selection.file)">
|
||||
<mat-icon>open_in_browser</mat-icon>
|
||||
</button>
|
||||
|
||||
<button
|
||||
mat-icon-button
|
||||
color="primary"
|
||||
title="{{ 'APP.ACTIONS.DOWNLOAD' | translate }}"
|
||||
(click)="downloadSelection()">
|
||||
<mat-icon>get_app</mat-icon>
|
||||
</button>
|
||||
|
||||
<button
|
||||
mat-icon-button
|
||||
color="primary"
|
||||
*ngIf="selection.folder"
|
||||
title="{{ 'APP.ACTIONS.EDIT' | translate }}"
|
||||
[acaEditFolder]="selection.folder">
|
||||
<mat-icon>create</mat-icon>
|
||||
</button>
|
||||
|
||||
<button mat-icon-button
|
||||
[color]="infoDrawerOpened ? 'accent' : 'primary'"
|
||||
title="{{ 'APP.ACTIONS.DETAILS' | translate }}"
|
||||
(click)="toggleSidebar()">
|
||||
<mat-icon>info_outline</mat-icon>
|
||||
</button>
|
||||
|
||||
<ng-container *ifExperimental="'share'">
|
||||
<button mat-icon-button
|
||||
color="primary"
|
||||
title="{{ 'APP.ACTIONS.SHARE' | translate }}"
|
||||
*ngIf="selection.file"
|
||||
[baseShareUrl]="sharedPreviewUrl$ | async"
|
||||
[adf-share]="selection.file">
|
||||
<mat-icon>share</mat-icon>
|
||||
</button>
|
||||
</ng-container>
|
||||
|
||||
<button
|
||||
color="primary"
|
||||
mat-icon-button
|
||||
title="{{ 'APP.ACTIONS.MORE' | translate }}"
|
||||
[matMenuTriggerFor]="actionsMenu">
|
||||
<mat-icon>more_vert</mat-icon>
|
||||
</button>
|
||||
<mat-menu #actionsMenu="matMenu"
|
||||
[overlapTrigger]="false">
|
||||
<button
|
||||
mat-menu-item
|
||||
#favorites="adfFavorite"
|
||||
(toggle)="reload()"
|
||||
[adf-node-favorite]="selection.nodes">
|
||||
<mat-icon color="primary" *ngIf="favorites.hasFavorites()">star</mat-icon>
|
||||
<mat-icon *ngIf="!favorites.hasFavorites()">star_border</mat-icon>
|
||||
<span>{{ 'APP.ACTIONS.FAVORITE' | translate }}</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
mat-menu-item
|
||||
[acaCopyNode]="selection.nodes">
|
||||
<mat-icon>content_copy</mat-icon>
|
||||
<span>{{ 'APP.ACTIONS.COPY' | translate }}</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
mat-menu-item
|
||||
[acaMoveNode]="selection.nodes">
|
||||
<mat-icon>library_books</mat-icon>
|
||||
<span>{{ 'APP.ACTIONS.MOVE' | translate }}</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
mat-menu-item
|
||||
[acaDeleteNode]="selection.nodes">
|
||||
<mat-icon>delete</mat-icon>
|
||||
<span>{{ 'APP.ACTIONS.DELETE' | translate }}</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
mat-menu-item
|
||||
*ngIf="selection.file"
|
||||
[acaNodeVersions]="selection.file">
|
||||
<mat-icon>history</mat-icon>
|
||||
<span>{{ 'APP.ACTIONS.VERSIONS' | translate }}</span>
|
||||
</button>
|
||||
|
||||
<ng-container *ifExperimental="'permissions'">
|
||||
<button
|
||||
mat-menu-item
|
||||
*ngIf="selection.count === 1"
|
||||
[acaNodePermissions]="selection.first">
|
||||
<mat-icon>settings_input_component</mat-icon>
|
||||
<span>{{ 'APP.ACTIONS.PERMISSIONS' | translate }}</span>
|
||||
</button>
|
||||
</ng-container>
|
||||
</mat-menu>
|
||||
<ng-container *ngFor="let entry of actions; trackBy: trackByActionId">
|
||||
<aca-toolbar-action [entry]="entry"></aca-toolbar-action>
|
||||
</ng-container>
|
||||
</adf-toolbar>
|
||||
</div>
|
||||
<div class="inner-layout__content">
|
||||
<div class="inner-layout__panel">
|
||||
<adf-document-list acaDocumentList #documentList
|
||||
[display]="documentDisplayMode$ | async"
|
||||
currentFolderId="-favorites-"
|
||||
selectionMode="multiple"
|
||||
[navigate]="false"
|
||||
@ -200,7 +88,7 @@
|
||||
</adf-pagination>
|
||||
</div>
|
||||
|
||||
<div class="inner-layout__side-panel" *ngIf="infoDrawerOpened">
|
||||
<div class="inner-layout__side-panel" *ngIf="infoDrawerOpened$ | async">
|
||||
<aca-info-drawer [node]="selection.last"></aca-info-drawer>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -59,7 +59,8 @@ export class FavoritesComponent extends PageComponent implements OnInit {
|
||||
this.content.nodesDeleted.subscribe(() => this.reload()),
|
||||
this.content.nodesRestored.subscribe(() => this.reload()),
|
||||
this.content.folderEdited.subscribe(() => this.reload()),
|
||||
this.content.nodesMoved.subscribe(() => this.reload())
|
||||
this.content.nodesMoved.subscribe(() => this.reload()),
|
||||
this.content.favoriteRemoved.subscribe(() => this.reload())
|
||||
]);
|
||||
}
|
||||
|
||||
|
@ -7,125 +7,9 @@
|
||||
</adf-breadcrumb>
|
||||
|
||||
<adf-toolbar class="inline">
|
||||
<button *ifExperimental="'cardview'"
|
||||
color="primary"
|
||||
mat-icon-button
|
||||
(click)="toggleGalleryView()">
|
||||
<mat-icon *ngIf="displayMode === 'list'" matTooltip="{{ 'APP.DOCUMENT_LIST.TOOLBAR.CARDVIEW' | translate }}">view_comfy</mat-icon>
|
||||
<mat-icon *ngIf="displayMode === 'gallery'" matTooltip="{{ 'APP.DOCUMENT_LIST.TOOLBAR.LISTVIEW' | translate }}">list</mat-icon>
|
||||
</button>
|
||||
|
||||
<ng-container *ifExperimental="'extensions'">
|
||||
<ng-container *ngFor="let entry of actions">
|
||||
<aca-toolbar-action [entry]="entry"></aca-toolbar-action>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="!selection.isEmpty">
|
||||
<button
|
||||
color="primary"
|
||||
mat-icon-button
|
||||
*ngIf="selection.file"
|
||||
title="{{ 'APP.ACTIONS.VIEW' | translate }}"
|
||||
(click)="showPreview(selection.file)">
|
||||
<mat-icon>open_in_browser</mat-icon>
|
||||
</button>
|
||||
|
||||
<button
|
||||
color="primary"
|
||||
mat-icon-button
|
||||
title="{{ 'APP.ACTIONS.DOWNLOAD' | translate }}"
|
||||
(click)="downloadSelection()">
|
||||
<mat-icon>get_app</mat-icon>
|
||||
</button>
|
||||
|
||||
<button
|
||||
color="primary"
|
||||
mat-icon-button
|
||||
*ngIf="canEditFolder"
|
||||
title="{{ 'APP.ACTIONS.EDIT' | translate }}"
|
||||
[acaEditFolder]="selection.folder">
|
||||
<mat-icon>create</mat-icon>
|
||||
</button>
|
||||
|
||||
<button mat-icon-button
|
||||
[color]="infoDrawerOpened ? 'accent' : 'primary'"
|
||||
title="{{ 'APP.ACTIONS.DETAILS' | translate }}"
|
||||
(click)="toggleSidebar()">
|
||||
<mat-icon>info_outline</mat-icon>
|
||||
</button>
|
||||
|
||||
<ng-container *ifExperimental="'share'">
|
||||
<button mat-icon-button
|
||||
color="primary"
|
||||
title="{{ 'APP.ACTIONS.SHARE' | translate }}"
|
||||
*ngIf="selection.file"
|
||||
[baseShareUrl]="sharedPreviewUrl$ | async"
|
||||
[adf-share]="selection.file">
|
||||
<mat-icon>share</mat-icon>
|
||||
</button>
|
||||
</ng-container>
|
||||
|
||||
<button
|
||||
color="primary"
|
||||
mat-icon-button
|
||||
title="{{ 'APP.ACTIONS.MORE' | translate }}"
|
||||
[matMenuTriggerFor]="actionsMenu">
|
||||
<mat-icon>more_vert</mat-icon>
|
||||
</button>
|
||||
|
||||
<mat-menu #actionsMenu="matMenu"
|
||||
[overlapTrigger]="false">
|
||||
<button
|
||||
mat-menu-item
|
||||
#favorites="adfFavorite"
|
||||
[adf-node-favorite]="selection.nodes">
|
||||
<mat-icon color="primary" *ngIf="favorites.hasFavorites()">star</mat-icon>
|
||||
<mat-icon *ngIf="!favorites.hasFavorites()">star_border</mat-icon>
|
||||
<span>{{ 'APP.ACTIONS.FAVORITE' | translate }}</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
mat-menu-item
|
||||
[acaCopyNode]="selection.nodes">
|
||||
<mat-icon>content_copy</mat-icon>
|
||||
<span>{{ 'APP.ACTIONS.COPY' | translate }}</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
mat-menu-item
|
||||
*ngIf="canDelete"
|
||||
[acaMoveNode]="selection.nodes">
|
||||
<mat-icon>library_books</mat-icon>
|
||||
<span>{{ 'APP.ACTIONS.MOVE' | translate }}</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
mat-menu-item
|
||||
*ngIf="canDelete"
|
||||
[acaDeleteNode]="selection.nodes">
|
||||
<mat-icon>delete</mat-icon>
|
||||
<span>{{ 'APP.ACTIONS.DELETE' | translate }}</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
mat-menu-item
|
||||
*ngIf="selection.file"
|
||||
[acaNodeVersions]="selection.file">
|
||||
<mat-icon>history</mat-icon>
|
||||
<span>{{ 'APP.ACTIONS.VERSIONS' | translate }}</span>
|
||||
</button>
|
||||
|
||||
<ng-container *ifExperimental="'permissions'">
|
||||
<button
|
||||
mat-menu-item
|
||||
*ngIf="canUpdateNode"
|
||||
[acaNodePermissions]="selection.first">
|
||||
<mat-icon>settings_input_component</mat-icon>
|
||||
<span>{{ 'APP.ACTIONS.PERMISSIONS' | translate }}</span>
|
||||
</button>
|
||||
</ng-container>
|
||||
</mat-menu>
|
||||
<app-document-display-mode *ifExperimental="'cardview'"></app-document-display-mode>
|
||||
<ng-container *ngFor="let entry of actions; trackBy: trackByActionId">
|
||||
<aca-toolbar-action [entry]="entry"></aca-toolbar-action>
|
||||
</ng-container>
|
||||
</adf-toolbar>
|
||||
</div>
|
||||
@ -141,6 +25,7 @@
|
||||
[disabled]="!canUpload">
|
||||
|
||||
<adf-document-list acaDocumentList #documentList
|
||||
[display]="documentDisplayMode$ | async"
|
||||
[sorting]="[ 'modifiedAt', 'desc' ]"
|
||||
selectionMode="multiple"
|
||||
[currentFolderId]="node?.id"
|
||||
@ -199,7 +84,7 @@
|
||||
</adf-upload-drag-area>
|
||||
</div>
|
||||
|
||||
<div class="inner-layout__side-panel" *ngIf="infoDrawerOpened">
|
||||
<div class="inner-layout__side-panel" *ngIf="infoDrawerOpened$ | async">
|
||||
<aca-info-drawer [node]="selection.last"></aca-info-drawer>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -29,7 +29,9 @@ import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { Router, ActivatedRoute } from '@angular/router';
|
||||
import {
|
||||
TimeAgoPipe, NodeNameTooltipPipe, FileSizePipe, NodeFavoriteDirective,
|
||||
DataTableComponent, UploadService, AppConfigPipe
|
||||
DataTableComponent,
|
||||
UploadService,
|
||||
AppConfigPipe
|
||||
} from '@alfresco/adf-core';
|
||||
import { DocumentListComponent } from '@alfresco/adf-content-services';
|
||||
import { ContentManagementService } from '../../services/content-management.service';
|
||||
@ -41,7 +43,6 @@ import { ExperimentalDirective } from '../../directives/experimental.directive';
|
||||
|
||||
describe('FilesComponent', () => {
|
||||
let node;
|
||||
let page;
|
||||
let fixture: ComponentFixture<FilesComponent>;
|
||||
let component: FilesComponent;
|
||||
let contentManagementService: ContentManagementService;
|
||||
@ -90,20 +91,12 @@ describe('FilesComponent', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
node = { id: 'node-id', isFolder: true };
|
||||
page = {
|
||||
list: {
|
||||
entries: ['a', 'b', 'c'],
|
||||
pagination: {}
|
||||
}
|
||||
};
|
||||
|
||||
spyOn(component.documentList, 'loadFolder').and.callFake(() => {});
|
||||
});
|
||||
|
||||
describe('Current page is valid', () => {
|
||||
it('should be a valid current page', fakeAsync(() => {
|
||||
spyOn(contentApi, 'getNode').and.returnValue(Observable.of({ entry: node }));
|
||||
spyOn(component, 'fetchNodes').and.returnValue(Observable.throw(null));
|
||||
spyOn(contentApi, 'getNode').and.returnValue(Observable.throw(null));
|
||||
|
||||
component.ngOnInit();
|
||||
fixture.detectChanges();
|
||||
@ -114,7 +107,6 @@ describe('FilesComponent', () => {
|
||||
|
||||
it('should set current page as invalid path', fakeAsync(() => {
|
||||
spyOn(contentApi, 'getNode').and.returnValue(Observable.of({ entry: node }));
|
||||
spyOn(component, 'fetchNodes').and.returnValue(Observable.of(page));
|
||||
|
||||
component.ngOnInit();
|
||||
tick();
|
||||
@ -127,22 +119,10 @@ describe('FilesComponent', () => {
|
||||
describe('OnInit', () => {
|
||||
it('should set current node', () => {
|
||||
spyOn(contentApi, 'getNode').and.returnValue(Observable.of({ entry: node }));
|
||||
spyOn(component, 'fetchNodes').and.returnValue(Observable.of(page));
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.node).toBe(node);
|
||||
});
|
||||
|
||||
it('should get current node children', () => {
|
||||
spyOn(contentApi, 'getNode').and.returnValue(Observable.of({ entry: node }));
|
||||
spyOn(component, 'fetchNodes').and.returnValue(Observable.of(page));
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.fetchNodes).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('if should navigate to parent if node is not a folder', () => {
|
||||
node.isFolder = false;
|
||||
node.parentId = 'parent-id';
|
||||
@ -157,8 +137,7 @@ describe('FilesComponent', () => {
|
||||
|
||||
describe('refresh on events', () => {
|
||||
beforeEach(() => {
|
||||
spyOn(contentApi, 'getNode').and.returnValue(Observable.of(node));
|
||||
spyOn(component, 'fetchNodes').and.returnValue(Observable.of(page));
|
||||
spyOn(contentApi, 'getNode').and.returnValue(Observable.of({ entry: node }));
|
||||
spyOn(component.documentList, 'reload');
|
||||
|
||||
fixture.detectChanges();
|
||||
@ -170,9 +149,9 @@ describe('FilesComponent', () => {
|
||||
{ entry: { parentId: '2' } }
|
||||
];
|
||||
|
||||
component.node = <any>{ id: '1' };
|
||||
component.node = { id: '1' };
|
||||
|
||||
nodeActionsService.contentCopied.next(<any>nodes);
|
||||
nodeActionsService.contentCopied.next(nodes);
|
||||
|
||||
expect(component.documentList.reload).toHaveBeenCalled();
|
||||
});
|
||||
@ -183,9 +162,9 @@ describe('FilesComponent', () => {
|
||||
{ entry: { parentId: '2' } }
|
||||
];
|
||||
|
||||
component.node = <any>{ id: '3' };
|
||||
component.node = { id: '3' };
|
||||
|
||||
nodeActionsService.contentCopied.next(<any>nodes);
|
||||
nodeActionsService.contentCopied.next(nodes);
|
||||
|
||||
expect(component.documentList.reload).not.toHaveBeenCalled();
|
||||
});
|
||||
@ -222,7 +201,7 @@ describe('FilesComponent', () => {
|
||||
|
||||
it('should call refresh on fileUploadComplete event if parent node match', () => {
|
||||
const file = { file: { options: { parentId: 'parentId' } } };
|
||||
component.node = <any>{ id: 'parentId' };
|
||||
component.node = { id: 'parentId' };
|
||||
|
||||
uploadService.fileUploadComplete.next(<any>file);
|
||||
|
||||
@ -231,7 +210,7 @@ describe('FilesComponent', () => {
|
||||
|
||||
it('should not call refresh on fileUploadComplete event if parent mismatch', () => {
|
||||
const file = { file: { options: { parentId: 'otherId' } } };
|
||||
component.node = <any>{ id: 'parentId' };
|
||||
component.node = { id: 'parentId' };
|
||||
|
||||
uploadService.fileUploadComplete.next(<any>file);
|
||||
|
||||
@ -240,7 +219,7 @@ describe('FilesComponent', () => {
|
||||
|
||||
it('should call refresh on fileUploadDeleted event if parent node match', () => {
|
||||
const file = { file: { options: { parentId: 'parentId' } } };
|
||||
component.node = <any>{ id: 'parentId' };
|
||||
component.node = { id: 'parentId' };
|
||||
|
||||
uploadService.fileUploadDeleted.next(<any>file);
|
||||
|
||||
@ -248,40 +227,24 @@ describe('FilesComponent', () => {
|
||||
});
|
||||
|
||||
it('should not call refresh on fileUploadDeleted event if parent mismatch', () => {
|
||||
const file = { file: { options: { parentId: 'otherId' } } };
|
||||
component.node = <any>{ id: 'parentId' };
|
||||
const file: any = { file: { options: { parentId: 'otherId' } } };
|
||||
component.node = { id: 'parentId' };
|
||||
|
||||
uploadService.fileUploadDeleted.next(<any>file);
|
||||
uploadService.fileUploadDeleted.next(file);
|
||||
|
||||
expect(component.documentList.reload).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetchNodes()', () => {
|
||||
beforeEach(() => {
|
||||
spyOn(contentApi, 'getNode').and.returnValue(Observable.of(node));
|
||||
spyOn(contentApi, 'getNodeChildren').and.returnValue(Observable.of(page));
|
||||
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should call getNode api with node id', () => {
|
||||
component.fetchNodes('nodeId');
|
||||
|
||||
expect(contentApi.getNodeChildren).toHaveBeenCalledWith('nodeId');
|
||||
});
|
||||
});
|
||||
|
||||
describe('onBreadcrumbNavigate()', () => {
|
||||
beforeEach(() => {
|
||||
spyOn(contentApi, 'getNode').and.returnValue(Observable.of(node));
|
||||
spyOn(component, 'fetchNodes').and.returnValue(Observable.of(page));
|
||||
|
||||
spyOn(contentApi, 'getNode').and.returnValue(Observable.of({ entry: node }));
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should navigates to node id', () => {
|
||||
const routeData = <any>{ id: 'some-where-over-the-rainbow' };
|
||||
const routeData: any = { id: 'some-where-over-the-rainbow' };
|
||||
spyOn(component, 'navigate');
|
||||
|
||||
component.onBreadcrumbNavigate(routeData);
|
||||
@ -292,8 +255,7 @@ describe('FilesComponent', () => {
|
||||
|
||||
describe('Node navigation', () => {
|
||||
beforeEach(() => {
|
||||
spyOn(contentApi, 'getNode').and.returnValue(Observable.of(node));
|
||||
spyOn(component, 'fetchNodes').and.returnValue(Observable.of(page));
|
||||
spyOn(contentApi, 'getNode').and.returnValue(Observable.of({ entry: node }));
|
||||
spyOn(router, 'navigate');
|
||||
|
||||
fixture.detectChanges();
|
||||
@ -312,7 +274,7 @@ describe('FilesComponent', () => {
|
||||
});
|
||||
|
||||
it('should navigate home if node is root', () => {
|
||||
(<any>component).node = {
|
||||
component.node = {
|
||||
path: {
|
||||
elements: [ {id: 'node-id'} ]
|
||||
}
|
||||
|
@ -27,8 +27,7 @@ import { FileUploadEvent, UploadService } from '@alfresco/adf-core';
|
||||
import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { ActivatedRoute, Params, Router } from '@angular/router';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { MinimalNodeEntity, MinimalNodeEntryEntity, NodePaging, PathElement, PathElementEntity } from 'alfresco-js-api';
|
||||
import { Observable } from 'rxjs/Rx';
|
||||
import { MinimalNodeEntity, MinimalNodeEntryEntity, PathElement, PathElementEntity } from 'alfresco-js-api';
|
||||
import { ContentManagementService } from '../../services/content-management.service';
|
||||
import { NodeActionsService } from '../../services/node-actions.service';
|
||||
import { AppStore } from '../../store/states/app.state';
|
||||
@ -68,19 +67,21 @@ export class FilesComponent extends PageComponent implements OnInit, OnDestroy {
|
||||
route.params.subscribe(({ folderId }: Params) => {
|
||||
const nodeId = folderId || data.defaultNodeId;
|
||||
|
||||
this.contentApi.getNode(nodeId)
|
||||
.map(node => node.entry)
|
||||
.do(node => {
|
||||
if (node.isFolder) {
|
||||
this.updateCurrentNode(node);
|
||||
} else {
|
||||
this.router.navigate(['/personal-files', node.parentId], { replaceUrl: true });
|
||||
}
|
||||
})
|
||||
.skipWhile(node => !node.isFolder)
|
||||
.flatMap(node => this.fetchNodes(node.id))
|
||||
this.contentApi
|
||||
.getNode(nodeId)
|
||||
.subscribe(
|
||||
() => this.isValidPath = true,
|
||||
node => {
|
||||
this.isValidPath = true;
|
||||
|
||||
if (node.entry && node.entry.isFolder) {
|
||||
this.updateCurrentNode(node.entry);
|
||||
} else {
|
||||
this.router.navigate(
|
||||
['/personal-files', node.entry.parentId],
|
||||
{ replaceUrl: true }
|
||||
);
|
||||
}
|
||||
},
|
||||
() => this.isValidPath = false
|
||||
);
|
||||
});
|
||||
@ -102,10 +103,6 @@ export class FilesComponent extends PageComponent implements OnInit, OnDestroy {
|
||||
this.store.dispatch(new SetCurrentFolderAction(null));
|
||||
}
|
||||
|
||||
fetchNodes(parentNodeId?: string): Observable<NodePaging> {
|
||||
return this.contentApi.getNodeChildren(parentNodeId);
|
||||
}
|
||||
|
||||
navigate(nodeId: string = null) {
|
||||
const commands = [ './' ];
|
||||
|
||||
|
@ -4,39 +4,11 @@
|
||||
</adf-breadcrumb>
|
||||
|
||||
<adf-toolbar class="inline">
|
||||
<button *ifExperimental="'cardview'"
|
||||
mat-icon-button
|
||||
color="primary"
|
||||
(click)="toggleGalleryView()">
|
||||
<mat-icon *ngIf="displayMode === 'list'" matTooltip="{{ 'APP.DOCUMENT_LIST.TOOLBAR.CARDVIEW' | translate }}">view_comfy</mat-icon>
|
||||
<mat-icon *ngIf="displayMode === 'gallery'" matTooltip="{{ 'APP.DOCUMENT_LIST.TOOLBAR.LISTVIEW' | translate }}">list</mat-icon>
|
||||
</button>
|
||||
<app-document-display-mode *ifExperimental="'cardview'"></app-document-display-mode>
|
||||
|
||||
<button
|
||||
mat-icon-button
|
||||
color="primary"
|
||||
*ifExperimental="'libraries'"
|
||||
(click)="createLibrary()">
|
||||
<mat-icon>create_new_folder</mat-icon>
|
||||
</button>
|
||||
|
||||
<ng-container *ngIf="!selection.isEmpty">
|
||||
<ng-container *ifExperimental="'libraries'">
|
||||
<button
|
||||
color="primary"
|
||||
mat-icon-button
|
||||
title="{{ 'APP.ACTIONS.MORE' | translate }}"
|
||||
[matMenuTriggerFor]="actionsMenu">
|
||||
<mat-icon>more_vert</mat-icon>
|
||||
</button>
|
||||
<mat-menu #actionsMenu="matMenu" [overlapTrigger]="false">
|
||||
<button
|
||||
mat-menu-item
|
||||
(click)="deleteLibrary(selection.first)">
|
||||
<mat-icon>delete</mat-icon>
|
||||
<span>{{ 'APP.ACTIONS.DELETE' | translate }}</span>
|
||||
</button>
|
||||
</mat-menu>
|
||||
<ng-container *ifExperimental="'extensions'">
|
||||
<ng-container *ngFor="let entry of actions; trackBy: trackByActionId">
|
||||
<aca-toolbar-action [entry]="entry"></aca-toolbar-action>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</adf-toolbar>
|
||||
@ -45,6 +17,7 @@
|
||||
<div class="inner-layout__content">
|
||||
<div class="inner-layout__panel">
|
||||
<adf-document-list acaDocumentList #documentList
|
||||
[display]="documentDisplayMode$ | async"
|
||||
currentFolderId="-mysites-"
|
||||
selectionMode="single"
|
||||
[navigate]="false"
|
||||
|
@ -30,7 +30,6 @@ import { ShareDataRow } from '@alfresco/adf-content-services';
|
||||
import { PageComponent } from '../page.component';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { AppStore } from '../../store/states/app.state';
|
||||
import { DeleteLibraryAction, CreateLibraryAction } from '../../store/actions';
|
||||
import { SiteEntry } from 'alfresco-js-api';
|
||||
import { ContentManagementService } from '../../services/content-management.service';
|
||||
import { ContentApiService } from '../../services/content-api.service';
|
||||
@ -100,14 +99,4 @@ export class LibrariesComponent extends PageComponent implements OnInit {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
deleteLibrary(node: SiteEntry) {
|
||||
if (node && node.entry) {
|
||||
this.store.dispatch(new DeleteLibraryAction(node.entry.id));
|
||||
}
|
||||
}
|
||||
|
||||
createLibrary() {
|
||||
this.store.dispatch(new CreateLibraryAction());
|
||||
}
|
||||
}
|
||||
|
@ -24,14 +24,13 @@
|
||||
*/
|
||||
|
||||
import { DocumentListComponent, ShareDataRow } from '@alfresco/adf-content-services';
|
||||
import { DisplayMode } from '@alfresco/adf-core';
|
||||
import { OnDestroy, OnInit, ViewChild } from '@angular/core';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { MinimalNodeEntity, MinimalNodeEntryEntity } from 'alfresco-js-api';
|
||||
import { takeUntil } from 'rxjs/operators';
|
||||
import { Subject, Subscription } from 'rxjs/Rx';
|
||||
import { SetSelectedNodesAction, DownloadNodesAction, ViewFileAction } from '../store/actions';
|
||||
import { appSelection, sharedUrl, currentFolder } from '../store/selectors/app.selectors';
|
||||
import { SetSelectedNodesAction, ViewFileAction } from '../store/actions';
|
||||
import { appSelection, sharedUrl, currentFolder, infoDrawerOpened, documentDisplayMode } from '../store/selectors/app.selectors';
|
||||
import { AppStore } from '../store/states/app.state';
|
||||
import { SelectionState } from '../store/states/selection.state';
|
||||
import { Observable } from 'rxjs/Rx';
|
||||
@ -47,19 +46,15 @@ export abstract class PageComponent implements OnInit, OnDestroy {
|
||||
documentList: DocumentListComponent;
|
||||
|
||||
title = 'Page';
|
||||
infoDrawerOpened = false;
|
||||
infoDrawerOpened$: Observable<boolean>;
|
||||
node: MinimalNodeEntryEntity;
|
||||
selection: SelectionState;
|
||||
displayMode = DisplayMode.List;
|
||||
documentDisplayMode$: Observable<string>;
|
||||
sharedPreviewUrl$: Observable<string>;
|
||||
actions: Array<ContentActionRef> = [];
|
||||
canUpdateFile = false;
|
||||
viewerActions: Array<ContentActionRef> = [];
|
||||
canUpdateNode = false;
|
||||
canDelete = false;
|
||||
canEditFolder = false;
|
||||
canUpload = false;
|
||||
canDeleteShared = false;
|
||||
canUpdateShared = false;
|
||||
|
||||
protected subscriptions: Subscription[] = [];
|
||||
|
||||
@ -74,22 +69,17 @@ export abstract class PageComponent implements OnInit, OnDestroy {
|
||||
|
||||
ngOnInit() {
|
||||
this.sharedPreviewUrl$ = this.store.select(sharedUrl);
|
||||
this.infoDrawerOpened$ = this.store.select(infoDrawerOpened);
|
||||
this.documentDisplayMode$ = this.store.select(documentDisplayMode);
|
||||
|
||||
this.store
|
||||
.select(appSelection)
|
||||
.pipe(takeUntil(this.onDestroy$))
|
||||
.subscribe(selection => {
|
||||
this.selection = selection;
|
||||
if (selection.isEmpty) {
|
||||
this.infoDrawerOpened = false;
|
||||
}
|
||||
this.actions = this.extensions.getAllowedContentActions();
|
||||
this.canUpdateFile = this.selection.file && this.content.canUpdateNode(selection.file);
|
||||
this.viewerActions = this.extensions.getViewerActions();
|
||||
this.canUpdateNode = this.selection.count === 1 && this.content.canUpdateNode(selection.first);
|
||||
this.canDelete = !this.selection.isEmpty && this.content.canDeleteNodes(selection.nodes);
|
||||
this.canEditFolder = selection.folder && this.content.canUpdateNode(selection.folder);
|
||||
this.canDeleteShared = !this.selection.isEmpty && this.content.canDeleteSharedNodes(selection.nodes);
|
||||
this.canUpdateShared = selection.file && this.content.canUpdateSharedNode(selection.file);
|
||||
});
|
||||
|
||||
this.store.select(currentFolder)
|
||||
@ -127,14 +117,6 @@ export abstract class PageComponent implements OnInit, OnDestroy {
|
||||
return null;
|
||||
}
|
||||
|
||||
toggleSidebar(event) {
|
||||
if (event) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.infoDrawerOpened = !this.infoDrawerOpened;
|
||||
}
|
||||
|
||||
reload(): void {
|
||||
if (this.documentList) {
|
||||
this.documentList.resetSelection();
|
||||
@ -143,22 +125,7 @@ export abstract class PageComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
toggleGalleryView(): void {
|
||||
this.displayMode = this.displayMode === DisplayMode.List ? DisplayMode.Gallery : DisplayMode.List;
|
||||
this.documentList.display = this.displayMode;
|
||||
}
|
||||
|
||||
downloadSelection() {
|
||||
this.store.dispatch(new DownloadNodesAction());
|
||||
}
|
||||
|
||||
// this is where each application decides how to treat an action and what to do
|
||||
// the ACA maps actions to the NgRx actions as an example
|
||||
runAction(actionId: string) {
|
||||
const context = {
|
||||
selection: this.selection
|
||||
};
|
||||
|
||||
this.extensions.runActionById(actionId, context);
|
||||
trackByActionId(index: number, action: ContentActionRef) {
|
||||
return action.id;
|
||||
}
|
||||
}
|
||||
|
@ -8,7 +8,7 @@
|
||||
[canNavigateBefore]="previousNodeId"
|
||||
[canNavigateNext]="nextNodeId"
|
||||
[overlayMode]="true"
|
||||
(print) = "printFile($event)"
|
||||
(print)="printFile()"
|
||||
(showViewerChange)="onVisibilityChanged($event)"
|
||||
(navigateBefore)="onNavigateBefore()"
|
||||
(navigateNext)="onNavigateNext()">
|
||||
@ -18,74 +18,14 @@
|
||||
</adf-viewer-sidebar>
|
||||
|
||||
<adf-viewer-open-with *ifExperimental="'extensions'">
|
||||
<button *ngFor="let entry of openWith"
|
||||
mat-menu-item
|
||||
(click)="runAction(entry.actions.click)">
|
||||
<mat-icon>{{ entry.icon }}</mat-icon>
|
||||
<span>{{ entry.title }}</span>
|
||||
</button>
|
||||
<ng-container *ngFor="let action of openWith; trackBy: trackByActionId">
|
||||
<aca-toolbar-action type="menu-item" [entry]="action"></aca-toolbar-action>
|
||||
</ng-container>
|
||||
</adf-viewer-open-with>
|
||||
|
||||
<adf-viewer-more-actions>
|
||||
|
||||
<button
|
||||
mat-menu-item
|
||||
#favorites="adfFavorite"
|
||||
[adf-node-favorite]="selection.nodes">
|
||||
<mat-icon color="primary" *ngIf="favorites.hasFavorites()">star</mat-icon>
|
||||
<mat-icon *ngIf="!favorites.hasFavorites()">star_border</mat-icon>
|
||||
<span>{{ 'APP.ACTIONS.FAVORITE' | translate }}</span>
|
||||
</button>
|
||||
|
||||
<ng-container *ifExperimental="'share'">
|
||||
<button mat-menu-item
|
||||
color="primary"
|
||||
[baseShareUrl]="sharedPreviewUrl$ | async"
|
||||
[adf-share]="selection.file">
|
||||
<mat-icon>share</mat-icon>
|
||||
<span>{{ 'APP.ACTIONS.SHARE' | translate }}</span>
|
||||
</button>
|
||||
</ng-container>
|
||||
|
||||
<button
|
||||
mat-menu-item
|
||||
[acaCopyNode]="selection.nodes">
|
||||
<mat-icon>content_copy</mat-icon>
|
||||
<span>{{ 'APP.ACTIONS.COPY' | translate }}</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
mat-menu-item
|
||||
*ngIf="canDelete"
|
||||
[acaMoveNode]="selection.nodes">
|
||||
<mat-icon>library_books</mat-icon>
|
||||
<span>{{ 'APP.ACTIONS.MOVE' | translate }}</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
mat-menu-item
|
||||
*ngIf="canDelete"
|
||||
(click)="deleteFile()">
|
||||
<mat-icon>delete</mat-icon>
|
||||
<span>{{ 'APP.ACTIONS.DELETE' | translate }}</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
mat-menu-item
|
||||
*ngIf="canUpdateFile"
|
||||
[acaNodeVersions]="selection.file">
|
||||
<mat-icon>history</mat-icon>
|
||||
<span>{{ 'APP.ACTIONS.VERSIONS' | translate }}</span>
|
||||
</button>
|
||||
|
||||
<ng-container *ifExperimental="'permissions'">
|
||||
<button
|
||||
mat-menu-item
|
||||
*ngIf="canUpdateNode"
|
||||
[acaNodePermissions]="selection.first">
|
||||
<mat-icon>settings_input_component</mat-icon>
|
||||
<span>{{ 'APP.ACTIONS.PERMISSIONS' | translate }}</span>
|
||||
</button>
|
||||
<ng-container *ngFor="let action of viewerActions; trackBy: trackByActionId">
|
||||
<aca-toolbar-action type="menu-item" [entry]="action"></aca-toolbar-action>
|
||||
</ng-container>
|
||||
</adf-viewer-more-actions>
|
||||
</adf-viewer>
|
||||
|
@ -28,7 +28,7 @@ import { ActivatedRoute, Router, UrlTree, UrlSegmentGroup, UrlSegment, PRIMARY_O
|
||||
import { UserPreferencesService, ObjectUtils } from '@alfresco/adf-core';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { AppStore } from '../../store/states/app.state';
|
||||
import { DeleteNodesAction, SetSelectedNodesAction } from '../../store/actions';
|
||||
import { SetSelectedNodesAction } from '../../store/actions';
|
||||
import { PageComponent } from '../page.component';
|
||||
import { ContentApiService } from '../../services/content-api.service';
|
||||
import { ExtensionService } from '../../extensions/extension.service';
|
||||
@ -335,17 +335,7 @@ export class PreviewComponent extends PageComponent implements OnInit {
|
||||
return path;
|
||||
}
|
||||
|
||||
deleteFile() {
|
||||
this.store.dispatch(new DeleteNodesAction([
|
||||
{
|
||||
id: this.node.nodeId || this.node.id,
|
||||
name: this.node.name
|
||||
}
|
||||
]));
|
||||
this.onVisibilityChanged(false);
|
||||
}
|
||||
|
||||
printFile(event: any) {
|
||||
printFile() {
|
||||
this.viewUtils.printFileGeneric(this.nodeId, this.node.content.mimeType);
|
||||
}
|
||||
|
||||
|
@ -27,7 +27,7 @@ import { CoreModule } from '@alfresco/adf-core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { Routes, RouterModule } from '@angular/router';
|
||||
|
||||
import { CoreExtensionsModule } from '../../extensions/core.extensions.module';
|
||||
import { DirectivesModule } from '../../directives/directives.module';
|
||||
import { AppInfoDrawerModule } from '../info-drawer/info.drawer.module';
|
||||
import { PreviewComponent } from './preview.component';
|
||||
@ -51,7 +51,8 @@ const routes: Routes = [
|
||||
CoreModule.forChild(),
|
||||
ContentDirectiveModule,
|
||||
DirectivesModule,
|
||||
AppInfoDrawerModule
|
||||
AppInfoDrawerModule,
|
||||
CoreExtensionsModule.forChild()
|
||||
],
|
||||
declarations: [
|
||||
PreviewComponent,
|
||||
|
@ -4,116 +4,10 @@
|
||||
</adf-breadcrumb>
|
||||
|
||||
<adf-toolbar class="inline">
|
||||
<button *ifExperimental="'cardview'"
|
||||
color="primary"
|
||||
mat-icon-button
|
||||
(click)="toggleGalleryView()">
|
||||
<mat-icon *ngIf="displayMode === 'list'" matTooltip="{{ 'APP.DOCUMENT_LIST.TOOLBAR.CARDVIEW' | translate }}">view_comfy</mat-icon>
|
||||
<mat-icon *ngIf="displayMode === 'gallery'" matTooltip="{{ 'APP.DOCUMENT_LIST.TOOLBAR.LISTVIEW' | translate }}">list</mat-icon>
|
||||
</button>
|
||||
<app-document-display-mode *ifExperimental="'cardview'"></app-document-display-mode>
|
||||
|
||||
<ng-container *ifExperimental="'extensions'">
|
||||
<ng-container *ngFor="let entry of actions">
|
||||
<aca-toolbar-action [entry]="entry"></aca-toolbar-action>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="!selection.isEmpty">
|
||||
<button
|
||||
mat-icon-button
|
||||
color="primary"
|
||||
*ngIf="selection.file"
|
||||
title="{{ 'APP.ACTIONS.VIEW' | translate }}"
|
||||
(click)="showPreview(selection.file)">
|
||||
<mat-icon>open_in_browser</mat-icon>
|
||||
</button>
|
||||
|
||||
<button
|
||||
mat-icon-button
|
||||
color="primary"
|
||||
title="{{ 'APP.ACTIONS.DOWNLOAD' | translate }}"
|
||||
(click)="downloadSelection()">
|
||||
<mat-icon>get_app</mat-icon>
|
||||
</button>
|
||||
|
||||
<button mat-icon-button
|
||||
[color]="infoDrawerOpened ? 'accent' : 'primary'"
|
||||
title="{{ 'APP.ACTIONS.DETAILS' | translate }}"
|
||||
(click)="toggleSidebar()">
|
||||
<mat-icon>info_outline</mat-icon>
|
||||
</button>
|
||||
|
||||
<ng-container *ifExperimental="'share'">
|
||||
<button *ngIf="selection.file"
|
||||
mat-icon-button
|
||||
color="primary"
|
||||
title="{{ 'APP.ACTIONS.SHARE' | translate }}"
|
||||
[baseShareUrl]="sharedPreviewUrl$ | async"
|
||||
[adf-share]="selection.file">
|
||||
<mat-icon>share</mat-icon>
|
||||
</button>
|
||||
</ng-container>
|
||||
|
||||
<button
|
||||
color="primary"
|
||||
mat-icon-button
|
||||
title="{{ 'APP.ACTIONS.MORE' | translate }}"
|
||||
[matMenuTriggerFor]="actionsMenu">
|
||||
<mat-icon>more_vert</mat-icon>
|
||||
</button>
|
||||
|
||||
<mat-menu #actionsMenu="matMenu"
|
||||
[overlapTrigger]="false">
|
||||
<button
|
||||
mat-menu-item
|
||||
#favorites="adfFavorite"
|
||||
[adf-node-favorite]="selection.nodes">
|
||||
<mat-icon color="primary" *ngIf="favorites.hasFavorites()">star</mat-icon>
|
||||
<mat-icon *ngIf="!favorites.hasFavorites()">star_border</mat-icon>
|
||||
<span>{{ 'APP.ACTIONS.FAVORITE' | translate }}</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
mat-menu-item
|
||||
[acaCopyNode]="selection.nodes">
|
||||
<mat-icon>content_copy</mat-icon>
|
||||
<span>{{ 'APP.ACTIONS.COPY' | translate }}</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
mat-menu-item
|
||||
*ngIf="canDelete"
|
||||
[acaMoveNode]="selection.nodes">
|
||||
<mat-icon>library_books</mat-icon>
|
||||
<span>{{ 'APP.ACTIONS.MOVE' | translate }}</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
mat-menu-item
|
||||
*ngIf="canDelete"
|
||||
[acaDeleteNode]="selection.nodes">
|
||||
<mat-icon>delete</mat-icon>
|
||||
<span>{{ 'APP.ACTIONS.DELETE' | translate }}</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
mat-menu-item
|
||||
*ngIf="selection.file"
|
||||
[acaNodeVersions]="selection.file">
|
||||
<mat-icon>history</mat-icon>
|
||||
<span>{{ 'APP.ACTIONS.VERSIONS' | translate }}</span>
|
||||
</button>
|
||||
|
||||
<ng-container *ifExperimental="'permissions'">
|
||||
<button
|
||||
mat-menu-item
|
||||
*ngIf="canUpdateNode"
|
||||
[acaNodePermissions]="selection.first">
|
||||
<mat-icon>settings_input_component</mat-icon>
|
||||
<span>{{ 'APP.ACTIONS.PERMISSIONS' | translate }}</span>
|
||||
</button>
|
||||
</ng-container>
|
||||
</mat-menu>
|
||||
<ng-container *ngFor="let entry of actions; trackBy: trackByActionId">
|
||||
<aca-toolbar-action [entry]="entry"></aca-toolbar-action>
|
||||
</ng-container>
|
||||
</adf-toolbar>
|
||||
</div>
|
||||
@ -121,6 +15,7 @@
|
||||
<div class="inner-layout__content">
|
||||
<div class="inner-layout__panel">
|
||||
<adf-document-list acaDocumentList #documentList
|
||||
[display]="documentDisplayMode$ | async"
|
||||
currentFolderId="-recent-"
|
||||
selectionMode="multiple"
|
||||
[navigate]="false"
|
||||
@ -188,7 +83,7 @@
|
||||
</adf-pagination>
|
||||
</div>
|
||||
|
||||
<div class="inner-layout__side-panel" *ngIf="infoDrawerOpened">
|
||||
<div class="inner-layout__side-panel" *ngIf="infoDrawerOpened$ | async">
|
||||
<aca-info-drawer [node]="selection.last"></aca-info-drawer>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -3,80 +3,8 @@
|
||||
<adf-breadcrumb root="APP.BROWSE.SEARCH.TITLE">
|
||||
</adf-breadcrumb>
|
||||
<adf-toolbar class="inline">
|
||||
<ng-container *ifExperimental="'extensions'">
|
||||
<ng-container *ngFor="let entry of actions">
|
||||
<aca-toolbar-action [entry]="entry"></aca-toolbar-action>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="!selection.isEmpty">
|
||||
<button
|
||||
color="primary"
|
||||
mat-icon-button
|
||||
*ngIf="selection.file"
|
||||
title="{{ 'APP.ACTIONS.VIEW' | translate }}"
|
||||
(click)="showPreview(selection.file)">
|
||||
<mat-icon>open_in_browser</mat-icon>
|
||||
</button>
|
||||
|
||||
<button
|
||||
color="primary"
|
||||
mat-icon-button
|
||||
title="{{ 'APP.ACTIONS.DOWNLOAD' | translate }}"
|
||||
(click)="downloadSelection()">
|
||||
<mat-icon>get_app</mat-icon>
|
||||
</button>
|
||||
|
||||
<button mat-icon-button
|
||||
[color]="infoDrawerOpened ? 'accent' : 'primary'"
|
||||
title="{{ 'APP.ACTIONS.DETAILS' | translate }}"
|
||||
(click)="toggleSidebar()">
|
||||
<mat-icon>info_outline</mat-icon>
|
||||
</button>
|
||||
|
||||
<button
|
||||
color="primary"
|
||||
mat-icon-button
|
||||
title="{{ 'APP.ACTIONS.MORE' | translate }}"
|
||||
[matMenuTriggerFor]="actionsMenu">
|
||||
<mat-icon>more_vert</mat-icon>
|
||||
</button>
|
||||
|
||||
<mat-menu #actionsMenu="matMenu" [overlapTrigger]="false">
|
||||
<button
|
||||
mat-menu-item
|
||||
#favorites="adfFavorite"
|
||||
[adf-node-favorite]="selection.nodes">
|
||||
<mat-icon color="primary" *ngIf="favorites.hasFavorites()">star</mat-icon>
|
||||
<mat-icon *ngIf="!favorites.hasFavorites()">star_border</mat-icon>
|
||||
<span>{{ 'APP.ACTIONS.FAVORITE' | translate }}</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
mat-menu-item
|
||||
[acaCopyNode]="selection.nodes">
|
||||
<mat-icon>content_copy</mat-icon>
|
||||
<span>{{ 'APP.ACTIONS.COPY' | translate }}</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
mat-menu-item
|
||||
*ngIf="selection.file"
|
||||
[acaNodeVersions]="selection.file">
|
||||
<mat-icon>history</mat-icon>
|
||||
<span>{{ 'APP.ACTIONS.VERSIONS' | translate }}</span>
|
||||
</button>
|
||||
|
||||
<ng-container *ifExperimental="'permissions'">
|
||||
<button
|
||||
mat-menu-item
|
||||
*ngIf="canUpdateNode"
|
||||
[acaNodePermissions]="selection.first">
|
||||
<mat-icon>settings_input_component</mat-icon>
|
||||
<span>{{ 'APP.ACTIONS.PERMISSIONS' | translate }}</span>
|
||||
</button>
|
||||
</ng-container>
|
||||
</mat-menu>
|
||||
<ng-container *ngFor="let entry of actions; trackBy: trackByActionId">
|
||||
<aca-toolbar-action [entry]="entry"></aca-toolbar-action>
|
||||
</ng-container>
|
||||
</adf-toolbar>
|
||||
</div>
|
||||
@ -154,7 +82,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="inner-layout__side-panel" *ngIf="infoDrawerOpened">
|
||||
<div class="inner-layout__side-panel" *ngIf="infoDrawerOpened$ | async">
|
||||
<aca-info-drawer [node]="selection.last"></aca-info-drawer>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -4,113 +4,10 @@
|
||||
</adf-breadcrumb>
|
||||
|
||||
<adf-toolbar class="inline">
|
||||
<button *ifExperimental="'cardview'"
|
||||
color="primary"
|
||||
mat-icon-button
|
||||
(click)="toggleGalleryView()">
|
||||
<mat-icon *ngIf="displayMode === 'list'" matTooltip="{{ 'APP.DOCUMENT_LIST.TOOLBAR.CARDVIEW' | translate }}">view_comfy</mat-icon>
|
||||
<mat-icon *ngIf="displayMode === 'gallery'" matTooltip="{{ 'APP.DOCUMENT_LIST.TOOLBAR.LISTVIEW' | translate }}">list</mat-icon>
|
||||
</button>
|
||||
<app-document-display-mode *ifExperimental="'cardview'"></app-document-display-mode>
|
||||
|
||||
<ng-container *ifExperimental="'extensions'">
|
||||
<ng-container *ngFor="let entry of actions">
|
||||
<aca-toolbar-action [entry]="entry"></aca-toolbar-action>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="!selection.isEmpty">
|
||||
<button
|
||||
*ngIf="selection.count === 1"
|
||||
color="primary"
|
||||
mat-icon-button
|
||||
title="{{ 'APP.ACTIONS.VIEW' | translate }}"
|
||||
(click)="showPreview(selection.nodes[0])">
|
||||
<mat-icon>open_in_browser</mat-icon>
|
||||
</button>
|
||||
|
||||
<button
|
||||
color="primary"
|
||||
mat-icon-button
|
||||
title="{{ 'APP.ACTIONS.DOWNLOAD' | translate }}"
|
||||
(click)="downloadSelection()">
|
||||
<mat-icon>get_app</mat-icon>
|
||||
</button>
|
||||
|
||||
<button mat-icon-button
|
||||
[color]="infoDrawerOpened ? 'accent' : 'primary'"
|
||||
title="{{ 'APP.ACTIONS.DETAILS' | translate }}"
|
||||
(click)="toggleSidebar()">
|
||||
<mat-icon>info_outline</mat-icon>
|
||||
</button>
|
||||
|
||||
<button
|
||||
color="primary"
|
||||
mat-icon-button
|
||||
title="{{ 'APP.ACTIONS.MORE' | translate }}"
|
||||
[matMenuTriggerFor]="actionsMenu">
|
||||
<mat-icon>more_vert</mat-icon>
|
||||
</button>
|
||||
|
||||
<mat-menu #actionsMenu="matMenu"
|
||||
[overlapTrigger]="false">
|
||||
<button
|
||||
mat-menu-item
|
||||
#favorites="adfFavorite"
|
||||
[adf-node-favorite]="selection.nodes">
|
||||
<mat-icon color="primary" *ngIf="favorites.hasFavorites()">star</mat-icon>
|
||||
<mat-icon *ngIf="!favorites.hasFavorites()">star_border</mat-icon>
|
||||
<span>{{ 'APP.ACTIONS.FAVORITE' | translate }}</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
mat-menu-item
|
||||
[acaCopyNode]="selection.nodes">
|
||||
<mat-icon>content_copy</mat-icon>
|
||||
<span>{{ 'APP.ACTIONS.COPY' | translate }}</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
mat-menu-item
|
||||
*ngIf="canDeleteShared"
|
||||
[acaMoveNode]="selection.nodes">
|
||||
<mat-icon>library_books</mat-icon>
|
||||
<span>{{ 'APP.ACTIONS.MOVE' | translate }}</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
mat-menu-item
|
||||
*ngIf="canDelete"
|
||||
[acaUnshareNode]="selection.nodes">
|
||||
<mat-icon>stop_screen_share</mat-icon>
|
||||
<span>{{ 'APP.ACTIONS.UNSHARE' | translate }}</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
mat-menu-item
|
||||
*ngIf="canDeleteShared"
|
||||
[acaDeleteNode]="selection.nodes">
|
||||
<mat-icon>delete</mat-icon>
|
||||
<span>{{ 'APP.ACTIONS.DELETE' | translate }}</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
mat-menu-item
|
||||
*ngIf="canUpdateShared"
|
||||
[acaNodeVersions]="selection.file">
|
||||
<mat-icon>history</mat-icon>
|
||||
<span>{{ 'APP.ACTIONS.VERSIONS' | translate }}</span>
|
||||
</button>
|
||||
|
||||
<ng-container *ifExperimental="'permissions'">
|
||||
<button
|
||||
mat-menu-item
|
||||
*ngIf="canUpdateShared"
|
||||
[acaNodePermissions]="selection.first">
|
||||
<mat-icon>settings_input_component</mat-icon>
|
||||
<span>{{ 'APP.ACTIONS.PERMISSIONS' | translate }}</span>
|
||||
</button>
|
||||
</ng-container>
|
||||
</mat-menu>
|
||||
<ng-container *ngFor="let entry of actions; trackBy: trackByActionId">
|
||||
<aca-toolbar-action [entry]="entry"></aca-toolbar-action>
|
||||
</ng-container>
|
||||
</adf-toolbar>
|
||||
</div>
|
||||
@ -118,6 +15,7 @@
|
||||
<div class="inner-layout__content">
|
||||
<div class="inner-layout__panel">
|
||||
<adf-document-list acaDocumentList #documentList
|
||||
[display]="documentDisplayMode$ | async"
|
||||
currentFolderId="-sharedlinks-"
|
||||
selectionMode="multiple"
|
||||
[sorting]="[ 'modifiedAt', 'desc' ]"
|
||||
@ -196,7 +94,7 @@
|
||||
</adf-pagination>
|
||||
</div>
|
||||
|
||||
<div class="inner-layout__side-panel" *ngIf="infoDrawerOpened">
|
||||
<div class="inner-layout__side-panel" *ngIf="infoDrawerOpened$ | async">
|
||||
<aca-info-drawer [node]="selection.last"></aca-info-drawer>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,75 +1,39 @@
|
||||
<div class="sidenav">
|
||||
<div class="sidenav__section sidenav__section sidenav_action-menu">
|
||||
<adf-sidebar-action-menu [expanded]="showLabel" [attr.title]="'APP.NEW_MENU.TOOLTIP' | translate" title="{{'APP.NEW_MENU.LABEL' | translate }}">
|
||||
<mat-icon sidebar-menu-title-icon >arrow_drop_down</mat-icon>
|
||||
<adf-sidebar-action-menu
|
||||
[expanded]="showLabel"
|
||||
[attr.title]="'APP.NEW_MENU.TOOLTIP' | translate"
|
||||
[title]="'APP.NEW_MENU.LABEL' | translate">
|
||||
<mat-icon sidebar-menu-title-icon>arrow_drop_down</mat-icon>
|
||||
<div sidebar-menu-expand-icon>
|
||||
<mat-icon [title]="'APP.NEW_MENU.TOOLTIP' | translate">queue</mat-icon>
|
||||
</div>
|
||||
<div sidebar-menu-options>
|
||||
<ng-container *ifExperimental="'extensions'">
|
||||
<button *ngFor="let entry of createActions"
|
||||
mat-menu-item
|
||||
[disabled]="entry.disabled"
|
||||
(click)="runAction(entry.actions.click)">
|
||||
<mat-icon>{{ entry.icon }}</mat-icon>
|
||||
<span>{{ entry.title | translate }}</span>
|
||||
</button>
|
||||
<ng-container *ngFor="let action of createActions; trackBy: trackById">
|
||||
<app-toolbar-button
|
||||
type="menu-item"
|
||||
[actionRef]="action"></app-toolbar-button>
|
||||
</ng-container>
|
||||
<button
|
||||
mat-menu-item
|
||||
[disabled]="!canCreateContent"
|
||||
(click)="createNewFolder()"
|
||||
[attr.title]="
|
||||
( canCreateContent
|
||||
? 'APP.NEW_MENU.TOOLTIPS.CREATE_FOLDER'
|
||||
: 'APP.NEW_MENU.TOOLTIPS.CREATE_FOLDER_NOT_ALLOWED'
|
||||
) | translate">
|
||||
<mat-icon>create_new_folder</mat-icon>
|
||||
<span>{{ 'APP.NEW_MENU.MENU_ITEMS.CREATE_FOLDER' | translate }}</span>
|
||||
</button>
|
||||
|
||||
<adf-upload-button
|
||||
[tooltip]="
|
||||
(canCreateContent
|
||||
? 'APP.NEW_MENU.TOOLTIPS.UPLOAD_FILES'
|
||||
: 'APP.NEW_MENU.TOOLTIPS.UPLOAD_FILES_NOT_ALLOWED'
|
||||
) | translate"
|
||||
[disabled]="!canCreateContent"
|
||||
[rootFolderId]="node?.id"
|
||||
[multipleFiles]="true"
|
||||
[uploadFolders]="false"
|
||||
[staticTitle]="'APP.NEW_MENU.MENU_ITEMS.UPLOAD_FILE' | translate">
|
||||
</adf-upload-button>
|
||||
|
||||
<adf-upload-button
|
||||
[tooltip]="
|
||||
(canCreateContent
|
||||
? 'APP.NEW_MENU.TOOLTIPS.UPLOAD_FOLDERS'
|
||||
: 'APP.NEW_MENU.TOOLTIPS.UPLOAD_FOLDERS_NOT_ALLOWED'
|
||||
) | translate"
|
||||
[disabled]="!canCreateContent"
|
||||
[rootFolderId]="node?.id"
|
||||
[multipleFiles]="true"
|
||||
[uploadFolders]="true"
|
||||
[staticTitle]="'APP.NEW_MENU.MENU_ITEMS.UPLOAD_FOLDER' | translate">
|
||||
</adf-upload-button>
|
||||
</div>
|
||||
</adf-sidebar-action-menu>
|
||||
</div>
|
||||
|
||||
<div class="sidenav__section sidenav__section--menu" *ngFor="let group of groups">
|
||||
<div *ngFor="let group of groups; trackBy: trackById"
|
||||
class="sidenav__section sidenav__section--menu" >
|
||||
<ul class="sidenav-menu">
|
||||
<li *ngFor="let item of group.items" class="sidenav-menu__item"
|
||||
<li *ngFor="let item of group.items; trackBy: trackById"
|
||||
class="sidenav-menu__item"
|
||||
routerLinkActive
|
||||
#rla="routerLinkActive"
|
||||
title="{{ item.description | translate }}">
|
||||
[attr.title]="item.description | translate">
|
||||
|
||||
<button
|
||||
[id]="item.id"
|
||||
mat-icon-button
|
||||
mat-ripple
|
||||
[routerLink]="item.url"
|
||||
[color]="rla.isActive ? 'accent': 'primary'"
|
||||
[attr.aria-label]="item.title | translate"
|
||||
mat-icon-button
|
||||
mat-ripple
|
||||
matRippleColor="primary"
|
||||
[matRippleTrigger]="rippleTrigger"
|
||||
[matRippleCentered]="true">
|
||||
|
@ -25,12 +25,9 @@
|
||||
|
||||
import { Subject } from 'rxjs/Rx';
|
||||
import { Component, Input, OnInit, OnDestroy } from '@angular/core';
|
||||
import { Node } from 'alfresco-js-api';
|
||||
import { NodePermissionService } from '../../services/node-permission.service';
|
||||
import { ExtensionService } from '../../extensions/extension.service';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { AppStore } from '../../store/states';
|
||||
import { CreateFolderAction } from '../../store/actions';
|
||||
import { currentFolder } from '../../store/selectors/app.selectors';
|
||||
import { takeUntil } from 'rxjs/operators';
|
||||
import { NavBarGroupRef } from '../../extensions/navbar.extensions';
|
||||
@ -44,28 +41,23 @@ import { ContentActionRef } from '../../extensions/action.extensions';
|
||||
export class SidenavComponent implements OnInit, OnDestroy {
|
||||
@Input() showLabel: boolean;
|
||||
|
||||
node: Node = null;
|
||||
groups: Array<NavBarGroupRef> = [];
|
||||
createActions: Array<ContentActionRef> = [];
|
||||
canCreateContent = false;
|
||||
onDestroy$: Subject<boolean> = new Subject<boolean>();
|
||||
|
||||
constructor(
|
||||
private store: Store<AppStore>,
|
||||
private permission: NodePermissionService,
|
||||
private extensions: ExtensionService
|
||||
) {
|
||||
}
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.groups = this.extensions.getNavigationGroups();
|
||||
|
||||
this.store.select(currentFolder)
|
||||
this.store
|
||||
.select(currentFolder)
|
||||
.pipe(takeUntil(this.onDestroy$))
|
||||
.subscribe(node => {
|
||||
this.node = node;
|
||||
.subscribe(() => {
|
||||
this.createActions = this.extensions.getCreateActions();
|
||||
this.canCreateContent = node && this.permission.check(node, ['create']);
|
||||
});
|
||||
}
|
||||
|
||||
@ -74,15 +66,7 @@ export class SidenavComponent implements OnInit, OnDestroy {
|
||||
this.onDestroy$.complete();
|
||||
}
|
||||
|
||||
createNewFolder() {
|
||||
if (this.node && this.node.id) {
|
||||
this.store.dispatch(new CreateFolderAction(this.node.id));
|
||||
}
|
||||
}
|
||||
|
||||
// this is where each application decides how to treat an action and what to do
|
||||
// the ACA maps actions to the NgRx actions as an example
|
||||
runAction(actionId: string) {
|
||||
this.extensions.runActionById(actionId);
|
||||
trackById(index: number, obj: { id: string }) {
|
||||
return obj.id;
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,56 @@
|
||||
/*!
|
||||
* @license
|
||||
* Alfresco Example Content Application
|
||||
*
|
||||
* Copyright (C) 2005 - 2018 Alfresco Software Limited
|
||||
*
|
||||
* This file is part of the Alfresco Example Content Application.
|
||||
* If the software was purchased under a paid Alfresco license, the terms of
|
||||
* the paid license agreement will prevail. Otherwise, the software is
|
||||
* provided under the following open source license terms:
|
||||
*
|
||||
* The Alfresco Example Content Application is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* The Alfresco Example Content Application is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Component } from '@angular/core';
|
||||
import { Observable } from 'rxjs/Rx';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { AppStore } from '../../../store/states';
|
||||
import { documentDisplayMode } from '../../../store/selectors/app.selectors';
|
||||
import { ToggleDocumentDisplayMode } from '../../../store/actions';
|
||||
|
||||
@Component({
|
||||
selector: 'app-document-display-mode',
|
||||
template: `
|
||||
<button
|
||||
mat-icon-button
|
||||
color="primary"
|
||||
(click)="onClick()">
|
||||
<mat-icon *ngIf="(displayMode$ | async) === 'list'">view_comfy</mat-icon>
|
||||
<mat-icon *ngIf="(displayMode$ | async) === 'gallery'">list</mat-icon>
|
||||
</button>
|
||||
`
|
||||
})
|
||||
export class DocumentDisplayModeComponent {
|
||||
|
||||
displayMode$: Observable<string>;
|
||||
|
||||
constructor(private store: Store<AppStore>) {
|
||||
this.displayMode$ = store.select(documentDisplayMode);
|
||||
}
|
||||
|
||||
onClick() {
|
||||
this.store.dispatch(new ToggleDocumentDisplayMode());
|
||||
}
|
||||
}
|
@ -23,25 +23,30 @@
|
||||
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Directive, Input, HostListener } from '@angular/core';
|
||||
import { MinimalNodeEntity } from 'alfresco-js-api';
|
||||
import { Component } from '@angular/core';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { AppStore } from '../store/states';
|
||||
import { EditFolderAction } from '../store/actions';
|
||||
import { AppStore, SelectionState } from '../../../store/states';
|
||||
import { appSelection } from '../../../store/selectors/app.selectors';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
|
||||
@Directive({
|
||||
selector: '[acaEditFolder]'
|
||||
@Component({
|
||||
selector: 'app-toggle-favorite',
|
||||
template: `
|
||||
<button
|
||||
mat-menu-item
|
||||
#favorites="adfFavorite"
|
||||
[adf-node-favorite]="(selection$ | async).nodes">
|
||||
<mat-icon *ngIf="favorites.hasFavorites()">star</mat-icon>
|
||||
<mat-icon *ngIf="!favorites.hasFavorites()">star_border</mat-icon>
|
||||
<span>{{ 'APP.ACTIONS.FAVORITE' | translate }}</span>
|
||||
</button>
|
||||
`
|
||||
})
|
||||
export class EditFolderDirective {
|
||||
/** Folder node to edit. */
|
||||
// tslint:disable-next-line:no-input-rename
|
||||
@Input('acaEditFolder') folder: MinimalNodeEntity;
|
||||
export class ToggleFavoriteComponent {
|
||||
|
||||
@HostListener('click', ['$event'])
|
||||
onClick(event) {
|
||||
event.preventDefault();
|
||||
this.store.dispatch(new EditFolderAction(this.folder));
|
||||
selection$: Observable<SelectionState>;
|
||||
|
||||
constructor(private store: Store<AppStore>) {
|
||||
this.selection$ = this.store.select(appSelection);
|
||||
}
|
||||
|
||||
constructor(private store: Store<AppStore>) {}
|
||||
}
|
@ -0,0 +1,55 @@
|
||||
/*!
|
||||
* @license
|
||||
* Alfresco Example Content Application
|
||||
*
|
||||
* Copyright (C) 2005 - 2018 Alfresco Software Limited
|
||||
*
|
||||
* This file is part of the Alfresco Example Content Application.
|
||||
* If the software was purchased under a paid Alfresco license, the terms of
|
||||
* the paid license agreement will prevail. Otherwise, the software is
|
||||
* provided under the following open source license terms:
|
||||
*
|
||||
* The Alfresco Example Content Application is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* The Alfresco Example Content Application is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Component } from '@angular/core';
|
||||
import { Observable } from 'rxjs/Rx';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { AppStore } from '../../../store/states';
|
||||
import { infoDrawerOpened } from '../../../store/selectors/app.selectors';
|
||||
import { ToggleInfoDrawerAction } from '../../../store/actions';
|
||||
|
||||
@Component({
|
||||
selector: 'app-toggle-info-drawer',
|
||||
template: `
|
||||
<button
|
||||
mat-icon-button
|
||||
[color]="(infoDrawerOpened$ | async) ? 'accent' : 'primary'"
|
||||
[attr.title]="'APP.ACTIONS.DETAILS' | translate"
|
||||
(click)="onClick()">
|
||||
<mat-icon>info_outline</mat-icon>
|
||||
</button>
|
||||
`
|
||||
})
|
||||
export class ToggleInfoDrawerComponent {
|
||||
infoDrawerOpened$: Observable<boolean>;
|
||||
|
||||
constructor(private store: Store<AppStore>) {
|
||||
this.infoDrawerOpened$ = this.store.select(infoDrawerOpened);
|
||||
}
|
||||
|
||||
onClick() {
|
||||
this.store.dispatch(new ToggleInfoDrawerAction());
|
||||
}
|
||||
}
|
@ -4,36 +4,10 @@
|
||||
</adf-breadcrumb>
|
||||
|
||||
<adf-toolbar class="inline">
|
||||
<button *ifExperimental="'cardview'"
|
||||
color="primary"
|
||||
mat-icon-button
|
||||
(click)="toggleGalleryView()">
|
||||
<mat-icon *ngIf="displayMode === 'list'" matTooltip="{{ 'APP.DOCUMENT_LIST.TOOLBAR.CARDVIEW' | translate }}">view_comfy</mat-icon>
|
||||
<mat-icon *ngIf="displayMode === 'gallery'" matTooltip="{{ 'APP.DOCUMENT_LIST.TOOLBAR.LISTVIEW' | translate }}">list</mat-icon>
|
||||
</button>
|
||||
<app-document-display-mode *ifExperimental="'cardview'"></app-document-display-mode>
|
||||
|
||||
<ng-container *ifExperimental="'extensions'">
|
||||
<ng-container *ngFor="let entry of actions">
|
||||
<aca-toolbar-action [entry]="entry"></aca-toolbar-action>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="!selection.isEmpty">
|
||||
<button
|
||||
color="primary"
|
||||
mat-icon-button
|
||||
[acaPermanentDelete]="selection.nodes"
|
||||
title="{{ 'APP.ACTIONS.DELETE_PERMANENT' | translate }}">
|
||||
<mat-icon>delete_forever</mat-icon>
|
||||
</button>
|
||||
|
||||
<button
|
||||
color="primary"
|
||||
mat-icon-button
|
||||
[acaRestoreNode]="selection.nodes"
|
||||
title="{{ 'APP.ACTIONS.RESTORE' | translate }}">
|
||||
<mat-icon>restore</mat-icon>
|
||||
</button>
|
||||
<ng-container *ngFor="let entry of actions; trackBy: trackByActionId">
|
||||
<aca-toolbar-action [entry]="entry"></aca-toolbar-action>
|
||||
</ng-container>
|
||||
</adf-toolbar>
|
||||
</div>
|
||||
@ -41,6 +15,7 @@
|
||||
<div class="inner-layout__content">
|
||||
<div class="inner-layout__panel">
|
||||
<adf-document-list acaDocumentList #documentList
|
||||
[display]="documentDisplayMode$ | async"
|
||||
currentFolderId="-trashcan-"
|
||||
selectionMode="multiple"
|
||||
[navigate]="false"
|
||||
|
@ -26,44 +26,17 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { ExperimentalDirective } from './experimental.directive';
|
||||
import { DocumentListDirective } from './document-list.directive';
|
||||
import { EditFolderDirective } from './edit-folder.directive';
|
||||
import { NodeCopyDirective } from './node-copy.directive';
|
||||
import { NodeDeleteDirective } from './node-delete.directive';
|
||||
import { NodeMoveDirective } from './node-move.directive';
|
||||
import { NodePermanentDeleteDirective } from './node-permanent-delete.directive';
|
||||
import { NodePermissionsDirective } from './node-permissions.directive';
|
||||
import { NodeRestoreDirective } from './node-restore.directive';
|
||||
import { NodeUnshareDirective } from './node-unshare.directive';
|
||||
import { NodeVersionsDirective } from './node-versions.directive';
|
||||
import { PaginationDirective } from './pagination.directive';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
ExperimentalDirective,
|
||||
DocumentListDirective,
|
||||
EditFolderDirective,
|
||||
NodeCopyDirective,
|
||||
NodeDeleteDirective,
|
||||
NodeMoveDirective,
|
||||
NodePermanentDeleteDirective,
|
||||
NodePermissionsDirective,
|
||||
NodeRestoreDirective,
|
||||
NodeUnshareDirective,
|
||||
NodeVersionsDirective,
|
||||
PaginationDirective
|
||||
],
|
||||
exports: [
|
||||
ExperimentalDirective,
|
||||
DocumentListDirective,
|
||||
EditFolderDirective,
|
||||
NodeCopyDirective,
|
||||
NodeDeleteDirective,
|
||||
NodeMoveDirective,
|
||||
NodePermanentDeleteDirective,
|
||||
NodePermissionsDirective,
|
||||
NodeRestoreDirective,
|
||||
NodeUnshareDirective,
|
||||
NodeVersionsDirective,
|
||||
PaginationDirective
|
||||
]
|
||||
})
|
||||
|
@ -38,6 +38,7 @@ import { MinimalNodeEntryEntity } from 'alfresco-js-api';
|
||||
})
|
||||
export class DocumentListDirective implements OnInit, OnDestroy {
|
||||
private subscriptions: Subscription[] = [];
|
||||
private isLibrary = false;
|
||||
|
||||
get sortingPreferenceKey(): string {
|
||||
return this.route.snapshot.data.sortingPreferenceKey;
|
||||
@ -53,6 +54,7 @@ export class DocumentListDirective implements OnInit, OnDestroy {
|
||||
ngOnInit() {
|
||||
this.documentList.includeFields = ['isFavorite', 'aspectNames'];
|
||||
this.documentList.allowDropFiles = false;
|
||||
this.isLibrary = this.documentList.currentFolderId === '-mysites-';
|
||||
|
||||
if (this.sortingPreferenceKey) {
|
||||
const current = this.documentList.sorting;
|
||||
@ -103,22 +105,27 @@ export class DocumentListDirective implements OnInit, OnDestroy {
|
||||
this.unSelectLockedNodes(this.documentList);
|
||||
}
|
||||
|
||||
this.store.dispatch(
|
||||
new SetSelectedNodesAction(this.documentList.selection)
|
||||
);
|
||||
this.updateSelection();
|
||||
}
|
||||
}
|
||||
|
||||
@HostListener('node-unselect')
|
||||
onNodeUnselect() {
|
||||
this.store.dispatch(
|
||||
new SetSelectedNodesAction(this.documentList.selection)
|
||||
);
|
||||
this.updateSelection();
|
||||
}
|
||||
|
||||
onReady() {
|
||||
this.updateSelection();
|
||||
}
|
||||
|
||||
private updateSelection() {
|
||||
const selection = this.documentList.selection.map(entry => {
|
||||
entry['isLibrary'] = this.isLibrary;
|
||||
return entry;
|
||||
});
|
||||
|
||||
this.store.dispatch(
|
||||
new SetSelectedNodesAction(this.documentList.selection)
|
||||
new SetSelectedNodesAction(selection)
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -1,307 +0,0 @@
|
||||
/*!
|
||||
* @license
|
||||
* Alfresco Example Content Application
|
||||
*
|
||||
* Copyright (C) 2005 - 2018 Alfresco Software Limited
|
||||
*
|
||||
* This file is part of the Alfresco Example Content Application.
|
||||
* If the software was purchased under a paid Alfresco license, the terms of
|
||||
* the paid license agreement will prevail. Otherwise, the software is
|
||||
* provided under the following open source license terms:
|
||||
*
|
||||
* The Alfresco Example Content Application is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* The Alfresco Example Content Application is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Component, DebugElement } from '@angular/core';
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { Observable } from 'rxjs/Rx';
|
||||
import { MatSnackBar } from '@angular/material';
|
||||
import { NodeActionsService } from '../services/node-actions.service';
|
||||
import { NodeCopyDirective } from './node-copy.directive';
|
||||
import { ContentApiService } from '../services/content-api.service';
|
||||
import { AppTestingModule } from '../testing/app-testing.module';
|
||||
|
||||
@Component({
|
||||
template: '<div [acaCopyNode]="selection"></div>'
|
||||
})
|
||||
class TestComponent {
|
||||
selection;
|
||||
}
|
||||
|
||||
describe('NodeCopyDirective', () => {
|
||||
let fixture: ComponentFixture<TestComponent>;
|
||||
let component: TestComponent;
|
||||
let element: DebugElement;
|
||||
let snackBar: MatSnackBar;
|
||||
let service: NodeActionsService;
|
||||
let contentApi: ContentApiService;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [ AppTestingModule ],
|
||||
declarations: [
|
||||
TestComponent,
|
||||
NodeCopyDirective
|
||||
]
|
||||
});
|
||||
|
||||
contentApi = TestBed.get(ContentApiService);
|
||||
|
||||
fixture = TestBed.createComponent(TestComponent);
|
||||
component = fixture.componentInstance;
|
||||
element = fixture.debugElement.query(By.directive(NodeCopyDirective));
|
||||
snackBar = TestBed.get(MatSnackBar);
|
||||
service = TestBed.get(NodeActionsService);
|
||||
});
|
||||
|
||||
describe('Copy node action', () => {
|
||||
beforeEach(() => {
|
||||
spyOn(snackBar, 'open').and.callThrough();
|
||||
});
|
||||
|
||||
it('notifies successful copy of a node', () => {
|
||||
spyOn(service, 'copyNodes').and.returnValue(Observable.of('OPERATION.SUCCES.CONTENT.COPY'));
|
||||
|
||||
component.selection = [{ entry: { id: 'node-to-copy-id', name: 'name' } }];
|
||||
const createdItems = [{ entry: { id: 'copy-id', name: 'name' } }];
|
||||
|
||||
fixture.detectChanges();
|
||||
element.triggerEventHandler('click', null);
|
||||
service.contentCopied.next(<any>createdItems);
|
||||
|
||||
expect(service.copyNodes).toHaveBeenCalled();
|
||||
expect(snackBar.open['calls'].argsFor(0)[0]).toBe('APP.MESSAGES.INFO.NODE_COPY.SINGULAR');
|
||||
});
|
||||
|
||||
it('notifies successful copy of multiple nodes', () => {
|
||||
spyOn(service, 'copyNodes').and.returnValue(Observable.of('OPERATION.SUCCES.CONTENT.COPY'));
|
||||
|
||||
component.selection = [
|
||||
{ entry: { id: 'node-to-copy-1', name: 'name1' } },
|
||||
{ entry: { id: 'node-to-copy-2', name: 'name2' } }];
|
||||
const createdItems = [
|
||||
{ entry: { id: 'copy-of-node-1', name: 'name1' } },
|
||||
{ entry: { id: 'copy-of-node-2', name: 'name2' } }];
|
||||
|
||||
fixture.detectChanges();
|
||||
element.triggerEventHandler('click', null);
|
||||
service.contentCopied.next(<any>createdItems);
|
||||
|
||||
expect(service.copyNodes).toHaveBeenCalled();
|
||||
expect(snackBar.open['calls'].argsFor(0)[0]).toBe('APP.MESSAGES.INFO.NODE_COPY.PLURAL');
|
||||
});
|
||||
|
||||
it('notifies partially copy of one node out of a multiple selection of nodes', () => {
|
||||
spyOn(service, 'copyNodes').and.returnValue(Observable.of('OPERATION.SUCCES.CONTENT.COPY'));
|
||||
|
||||
component.selection = [
|
||||
{ entry: { id: 'node-to-copy-1', name: 'name1' } },
|
||||
{ entry: { id: 'node-to-copy-2', name: 'name2' } }];
|
||||
const createdItems = [
|
||||
{ entry: { id: 'copy-of-node-1', name: 'name1' } }];
|
||||
|
||||
fixture.detectChanges();
|
||||
element.triggerEventHandler('click', null);
|
||||
service.contentCopied.next(<any>createdItems);
|
||||
|
||||
expect(service.copyNodes).toHaveBeenCalled();
|
||||
expect(snackBar.open['calls'].argsFor(0)[0]).toBe('APP.MESSAGES.INFO.NODE_COPY.PARTIAL_SINGULAR');
|
||||
});
|
||||
|
||||
it('notifies partially copy of more nodes out of a multiple selection of nodes', () => {
|
||||
spyOn(service, 'copyNodes').and.returnValue(Observable.of('OPERATION.SUCCES.CONTENT.COPY'));
|
||||
|
||||
component.selection = [
|
||||
{ entry: { id: 'node-to-copy-0', name: 'name0' } },
|
||||
{ entry: { id: 'node-to-copy-1', name: 'name1' } },
|
||||
{ entry: { id: 'node-to-copy-2', name: 'name2' } }];
|
||||
const createdItems = [
|
||||
{ entry: { id: 'copy-of-node-0', name: 'name0' } },
|
||||
{ entry: { id: 'copy-of-node-1', name: 'name1' } }];
|
||||
|
||||
fixture.detectChanges();
|
||||
element.triggerEventHandler('click', null);
|
||||
service.contentCopied.next(<any>createdItems);
|
||||
|
||||
expect(service.copyNodes).toHaveBeenCalled();
|
||||
expect(snackBar.open['calls'].argsFor(0)[0]).toBe('APP.MESSAGES.INFO.NODE_COPY.PARTIAL_PLURAL');
|
||||
});
|
||||
|
||||
it('notifies of failed copy of multiple nodes', () => {
|
||||
spyOn(service, 'copyNodes').and.returnValue(Observable.of('OPERATION.SUCCES.CONTENT.COPY'));
|
||||
|
||||
component.selection = [
|
||||
{ entry: { id: 'node-to-copy-0', name: 'name0' } },
|
||||
{ entry: { id: 'node-to-copy-1', name: 'name1' } },
|
||||
{ entry: { id: 'node-to-copy-2', name: 'name2' } }];
|
||||
const createdItems = [];
|
||||
|
||||
fixture.detectChanges();
|
||||
element.triggerEventHandler('click', null);
|
||||
service.contentCopied.next(<any>createdItems);
|
||||
|
||||
expect(service.copyNodes).toHaveBeenCalled();
|
||||
expect(snackBar.open['calls'].argsFor(0)[0]).toBe('APP.MESSAGES.INFO.NODE_COPY.FAIL_PLURAL');
|
||||
});
|
||||
|
||||
it('notifies of failed copy of one node', () => {
|
||||
spyOn(service, 'copyNodes').and.returnValue(Observable.of('OPERATION.SUCCES.CONTENT.COPY'));
|
||||
|
||||
component.selection = [
|
||||
{ entry: { id: 'node-to-copy', name: 'name' } }];
|
||||
const createdItems = [];
|
||||
|
||||
fixture.detectChanges();
|
||||
element.triggerEventHandler('click', null);
|
||||
service.contentCopied.next(<any>createdItems);
|
||||
|
||||
expect(service.copyNodes).toHaveBeenCalled();
|
||||
expect(snackBar.open['calls'].argsFor(0)[0]).toBe('APP.MESSAGES.INFO.NODE_COPY.FAIL_SINGULAR');
|
||||
});
|
||||
|
||||
it('notifies error if success message was not emitted', () => {
|
||||
spyOn(service, 'copyNodes').and.returnValue(Observable.of(''));
|
||||
|
||||
component.selection = [{ entry: { id: 'node-to-copy-id', name: 'name' } }];
|
||||
|
||||
fixture.detectChanges();
|
||||
element.triggerEventHandler('click', null);
|
||||
service.contentCopied.next();
|
||||
|
||||
expect(service.copyNodes).toHaveBeenCalled();
|
||||
expect(snackBar.open['calls'].argsFor(0)[0]).toBe('APP.MESSAGES.ERRORS.GENERIC');
|
||||
});
|
||||
|
||||
it('notifies permission error on copy of node', () => {
|
||||
spyOn(service, 'copyNodes').and.returnValue(Observable.throw(new Error(JSON.stringify({error: {statusCode: 403}}))));
|
||||
|
||||
component.selection = [{ entry: { id: '1', name: 'name' } }];
|
||||
|
||||
fixture.detectChanges();
|
||||
element.triggerEventHandler('click', null);
|
||||
|
||||
expect(service.copyNodes).toHaveBeenCalled();
|
||||
expect(snackBar.open['calls'].argsFor(0)[0]).toBe('APP.MESSAGES.ERRORS.PERMISSION');
|
||||
});
|
||||
|
||||
it('notifies generic error message on all errors, but 403', () => {
|
||||
spyOn(service, 'copyNodes').and.returnValue(Observable.throw(new Error(JSON.stringify({error: {statusCode: 404}}))));
|
||||
|
||||
component.selection = [{ entry: { id: '1', name: 'name' } }];
|
||||
|
||||
fixture.detectChanges();
|
||||
element.triggerEventHandler('click', null);
|
||||
|
||||
expect(service.copyNodes).toHaveBeenCalled();
|
||||
expect(snackBar.open['calls'].argsFor(0)[0]).toBe('APP.MESSAGES.ERRORS.GENERIC');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Undo Copy action', () => {
|
||||
beforeEach(() => {
|
||||
spyOn(service, 'copyNodes').and.returnValue(Observable.of('OPERATION.SUCCES.CONTENT.COPY'));
|
||||
|
||||
spyOn(snackBar, 'open').and.returnValue({
|
||||
onAction: () => Observable.of({})
|
||||
});
|
||||
});
|
||||
|
||||
it('should delete the newly created node on Undo action', () => {
|
||||
spyOn(contentApi, 'deleteNode').and.returnValue(Observable.of(null));
|
||||
|
||||
component.selection = [{ entry: { id: 'node-to-copy-id', name: 'name' } }];
|
||||
const createdItems = [{ entry: { id: 'copy-id', name: 'name' } }];
|
||||
|
||||
fixture.detectChanges();
|
||||
element.triggerEventHandler('click', null);
|
||||
service.contentCopied.next(<any>createdItems);
|
||||
|
||||
expect(service.copyNodes).toHaveBeenCalled();
|
||||
expect(snackBar.open['calls'].argsFor(0)[0]).toBe('APP.MESSAGES.INFO.NODE_COPY.SINGULAR');
|
||||
|
||||
expect(contentApi.deleteNode).toHaveBeenCalledWith(createdItems[0].entry.id, { permanent: true });
|
||||
});
|
||||
|
||||
it('should delete also the node created inside an already existing folder from destination', () => {
|
||||
const spyOnDeleteNode = spyOn(contentApi, 'deleteNode').and.returnValue(Observable.of(null));
|
||||
|
||||
component.selection = [
|
||||
{ entry: { id: 'node-to-copy-1', name: 'name1' } },
|
||||
{ entry: { id: 'node-to-copy-2', name: 'folder-with-name-already-existing-on-destination' } }];
|
||||
const id1 = 'copy-of-node-1';
|
||||
const id2 = 'copy-of-child-of-node-2';
|
||||
const createdItems = [
|
||||
{ entry: { id: id1, name: 'name1' } },
|
||||
[ { entry: { id: id2, name: 'name-of-child-of-node-2' , parentId: 'the-folder-already-on-destination' } }] ];
|
||||
|
||||
fixture.detectChanges();
|
||||
element.triggerEventHandler('click', null);
|
||||
service.contentCopied.next(<any>createdItems);
|
||||
|
||||
expect(service.copyNodes).toHaveBeenCalled();
|
||||
expect(snackBar.open['calls'].argsFor(0)[0]).toBe('APP.MESSAGES.INFO.NODE_COPY.PLURAL');
|
||||
|
||||
expect(spyOnDeleteNode).toHaveBeenCalled();
|
||||
expect(spyOnDeleteNode.calls.allArgs())
|
||||
.toEqual([[id1, { permanent: true }], [id2, { permanent: true }]]);
|
||||
});
|
||||
|
||||
it('notifies when error occurs on Undo action', () => {
|
||||
spyOn(contentApi, 'deleteNode').and.returnValue(Observable.throw(null));
|
||||
|
||||
component.selection = [{ entry: { id: 'node-to-copy-id', name: 'name' } }];
|
||||
const createdItems = [{ entry: { id: 'copy-id', name: 'name' } }];
|
||||
|
||||
fixture.detectChanges();
|
||||
element.triggerEventHandler('click', null);
|
||||
service.contentCopied.next(<any>createdItems);
|
||||
|
||||
expect(service.copyNodes).toHaveBeenCalled();
|
||||
expect(contentApi.deleteNode).toHaveBeenCalled();
|
||||
expect(snackBar.open['calls'].argsFor(0)[0]).toEqual('APP.MESSAGES.INFO.NODE_COPY.SINGULAR');
|
||||
});
|
||||
|
||||
it('notifies when some error of type Error occurs on Undo action', () => {
|
||||
spyOn(contentApi, 'deleteNode').and.returnValue(Observable.throw(new Error('oops!')));
|
||||
|
||||
component.selection = [{ entry: { id: 'node-to-copy-id', name: 'name' } }];
|
||||
const createdItems = [{ entry: { id: 'copy-id', name: 'name' } }];
|
||||
|
||||
fixture.detectChanges();
|
||||
element.triggerEventHandler('click', null);
|
||||
service.contentCopied.next(<any>createdItems);
|
||||
|
||||
expect(service.copyNodes).toHaveBeenCalled();
|
||||
expect(contentApi.deleteNode).toHaveBeenCalled();
|
||||
expect(snackBar.open['calls'].argsFor(0)[0]).toEqual('APP.MESSAGES.INFO.NODE_COPY.SINGULAR');
|
||||
});
|
||||
|
||||
it('notifies permission error when it occurs on Undo action', () => {
|
||||
spyOn(contentApi, 'deleteNode').and.returnValue(Observable.throw(new Error(JSON.stringify({error: {statusCode: 403}}))));
|
||||
|
||||
component.selection = [{ entry: { id: 'node-to-copy-id', name: 'name' } }];
|
||||
const createdItems = [{ entry: { id: 'copy-id', name: 'name' } }];
|
||||
|
||||
fixture.detectChanges();
|
||||
element.triggerEventHandler('click', null);
|
||||
service.contentCopied.next(<any>createdItems);
|
||||
|
||||
expect(service.copyNodes).toHaveBeenCalled();
|
||||
expect(contentApi.deleteNode).toHaveBeenCalled();
|
||||
expect(snackBar.open['calls'].argsFor(0)[0]).toEqual('APP.MESSAGES.INFO.NODE_COPY.SINGULAR');
|
||||
});
|
||||
});
|
||||
|
||||
});
|
@ -1,155 +0,0 @@
|
||||
/*!
|
||||
* @license
|
||||
* Alfresco Example Content Application
|
||||
*
|
||||
* Copyright (C) 2005 - 2018 Alfresco Software Limited
|
||||
*
|
||||
* This file is part of the Alfresco Example Content Application.
|
||||
* If the software was purchased under a paid Alfresco license, the terms of
|
||||
* the paid license agreement will prevail. Otherwise, the software is
|
||||
* provided under the following open source license terms:
|
||||
*
|
||||
* The Alfresco Example Content Application is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* The Alfresco Example Content Application is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Directive, HostListener, Input } from '@angular/core';
|
||||
import { Observable } from 'rxjs/Rx';
|
||||
import { MatSnackBar } from '@angular/material';
|
||||
|
||||
import { TranslationService } from '@alfresco/adf-core';
|
||||
import { MinimalNodeEntity } from 'alfresco-js-api';
|
||||
import { NodeActionsService } from '../services/node-actions.service';
|
||||
import { ContentManagementService } from '../services/content-management.service';
|
||||
import { ContentApiService } from '../services/content-api.service';
|
||||
|
||||
@Directive({
|
||||
selector: '[acaCopyNode]'
|
||||
})
|
||||
export class NodeCopyDirective {
|
||||
|
||||
// tslint:disable-next-line:no-input-rename
|
||||
@Input('acaCopyNode')
|
||||
selection: MinimalNodeEntity[];
|
||||
|
||||
@HostListener('click')
|
||||
onClick() {
|
||||
this.copySelected();
|
||||
}
|
||||
|
||||
constructor(
|
||||
private content: ContentManagementService,
|
||||
private contentApi: ContentApiService,
|
||||
private snackBar: MatSnackBar,
|
||||
private nodeActionsService: NodeActionsService,
|
||||
private translation: TranslationService
|
||||
) {}
|
||||
|
||||
copySelected() {
|
||||
Observable.zip(
|
||||
this.nodeActionsService.copyNodes(this.selection),
|
||||
this.nodeActionsService.contentCopied
|
||||
).subscribe(
|
||||
(result) => {
|
||||
const [ operationResult, newItems ] = result;
|
||||
this.toastMessage(operationResult, newItems);
|
||||
},
|
||||
(error) => {
|
||||
this.toastMessage(error);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private toastMessage(info: any, newItems?: MinimalNodeEntity[]) {
|
||||
const numberOfCopiedItems = newItems ? newItems.length : 0;
|
||||
const failedItems = this.selection.length - numberOfCopiedItems;
|
||||
|
||||
let i18nMessageString = 'APP.MESSAGES.ERRORS.GENERIC';
|
||||
|
||||
if (typeof info === 'string') {
|
||||
if (info.toLowerCase().indexOf('succes') !== -1) {
|
||||
let i18MessageSuffix;
|
||||
|
||||
if (failedItems) {
|
||||
if (numberOfCopiedItems) {
|
||||
i18MessageSuffix = ( numberOfCopiedItems === 1 ) ? 'PARTIAL_SINGULAR' : 'PARTIAL_PLURAL';
|
||||
|
||||
} else {
|
||||
i18MessageSuffix = ( failedItems === 1 ) ? 'FAIL_SINGULAR' : 'FAIL_PLURAL';
|
||||
}
|
||||
|
||||
} else {
|
||||
i18MessageSuffix = ( numberOfCopiedItems === 1 ) ? 'SINGULAR' : 'PLURAL';
|
||||
}
|
||||
|
||||
i18nMessageString = `APP.MESSAGES.INFO.NODE_COPY.${i18MessageSuffix}`;
|
||||
}
|
||||
|
||||
} else {
|
||||
try {
|
||||
|
||||
const { error: { statusCode } } = JSON.parse(info.message);
|
||||
|
||||
if (statusCode === 403) {
|
||||
i18nMessageString = 'APP.MESSAGES.ERRORS.PERMISSION';
|
||||
}
|
||||
|
||||
} catch (err) { /* Do nothing, keep the original message */ }
|
||||
}
|
||||
|
||||
const undo = (numberOfCopiedItems > 0) ? this.translation.instant('APP.ACTIONS.UNDO') : '';
|
||||
|
||||
const message = this.translation.instant(i18nMessageString, { success: numberOfCopiedItems, failed: failedItems });
|
||||
|
||||
this.snackBar
|
||||
.open(message, undo, {
|
||||
panelClass: 'info-snackbar',
|
||||
duration: 3000
|
||||
})
|
||||
.onAction()
|
||||
.subscribe(() => this.deleteCopy(newItems));
|
||||
}
|
||||
|
||||
private deleteCopy(nodes: MinimalNodeEntity[]) {
|
||||
const batch = this.nodeActionsService.flatten(nodes)
|
||||
.filter(item => item.entry)
|
||||
.map(item => this.contentApi.deleteNode(item.entry.id, { permanent: true }));
|
||||
|
||||
Observable.forkJoin(...batch)
|
||||
.subscribe(
|
||||
() => {
|
||||
this.content.nodesDeleted.next(null);
|
||||
},
|
||||
(error) => {
|
||||
let i18nMessageString = 'APP.MESSAGES.ERRORS.GENERIC';
|
||||
|
||||
let errorJson = null;
|
||||
try {
|
||||
errorJson = JSON.parse(error.message);
|
||||
} catch (e) { //
|
||||
}
|
||||
|
||||
if (errorJson && errorJson.error && errorJson.error.statusCode === 403) {
|
||||
i18nMessageString = 'APP.MESSAGES.ERRORS.PERMISSION';
|
||||
}
|
||||
|
||||
const message = this.translation.instant(i18nMessageString);
|
||||
|
||||
this.snackBar.open(message, '', {
|
||||
panelClass: 'error-snackbar',
|
||||
duration: 3000
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
@ -1,274 +0,0 @@
|
||||
/*!
|
||||
* @license
|
||||
* Alfresco Example Content Application
|
||||
*
|
||||
* Copyright (C) 2005 - 2018 Alfresco Software Limited
|
||||
*
|
||||
* This file is part of the Alfresco Example Content Application.
|
||||
* If the software was purchased under a paid Alfresco license, the terms of
|
||||
* the paid license agreement will prevail. Otherwise, the software is
|
||||
* provided under the following open source license terms:
|
||||
*
|
||||
* The Alfresco Example Content Application is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* The Alfresco Example Content Application is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { TestBed, ComponentFixture, fakeAsync, tick } from '@angular/core/testing';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { Component, DebugElement } from '@angular/core';
|
||||
|
||||
import { NodeDeleteDirective } from './node-delete.directive';
|
||||
import { EffectsModule, Actions, ofType } from '@ngrx/effects';
|
||||
import { NodeEffects } from '../store/effects/node.effects';
|
||||
import {
|
||||
SnackbarInfoAction, SNACKBAR_INFO, SNACKBAR_ERROR,
|
||||
SnackbarErrorAction, SnackbarWarningAction, SNACKBAR_WARNING
|
||||
} from '../store/actions';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { AppTestingModule } from '../testing/app-testing.module';
|
||||
import { ContentApiService } from '../services/content-api.service';
|
||||
import { Observable } from 'rxjs/Rx';
|
||||
|
||||
@Component({
|
||||
template: '<div [acaDeleteNode]="selection"></div>'
|
||||
})
|
||||
class TestComponent {
|
||||
selection;
|
||||
}
|
||||
|
||||
describe('NodeDeleteDirective', () => {
|
||||
let component: TestComponent;
|
||||
let fixture: ComponentFixture<TestComponent>;
|
||||
let element: DebugElement;
|
||||
let actions$: Actions;
|
||||
let contentApi: ContentApiService;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
AppTestingModule,
|
||||
EffectsModule.forRoot([NodeEffects])
|
||||
],
|
||||
declarations: [
|
||||
NodeDeleteDirective,
|
||||
TestComponent
|
||||
]
|
||||
});
|
||||
|
||||
contentApi = TestBed.get(ContentApiService);
|
||||
actions$ = TestBed.get(Actions);
|
||||
|
||||
fixture = TestBed.createComponent(TestComponent);
|
||||
component = fixture.componentInstance;
|
||||
element = fixture.debugElement.query(By.directive(NodeDeleteDirective));
|
||||
});
|
||||
|
||||
describe('Delete action', () => {
|
||||
it('should raise info message on successful single file deletion', fakeAsync(done => {
|
||||
spyOn(contentApi, 'deleteNode').and.returnValue(Observable.of(null));
|
||||
|
||||
actions$.pipe(
|
||||
ofType<SnackbarInfoAction>(SNACKBAR_INFO),
|
||||
map(action => {
|
||||
done();
|
||||
})
|
||||
);
|
||||
|
||||
component.selection = [{ entry: { id: '1', name: 'name1' } }];
|
||||
|
||||
fixture.detectChanges();
|
||||
element.triggerEventHandler('click', null);
|
||||
|
||||
tick();
|
||||
}));
|
||||
|
||||
it('should raise error message on failed single file deletion', fakeAsync(done => {
|
||||
spyOn(contentApi, 'deleteNode').and.returnValue(Observable.throw(null));
|
||||
|
||||
actions$.pipe(
|
||||
ofType<SnackbarErrorAction>(SNACKBAR_ERROR),
|
||||
map(action => {
|
||||
done();
|
||||
})
|
||||
);
|
||||
|
||||
component.selection = [{ entry: { id: '1', name: 'name1' } }];
|
||||
|
||||
fixture.detectChanges();
|
||||
element.triggerEventHandler('click', null);
|
||||
|
||||
tick();
|
||||
}));
|
||||
|
||||
it('should raise info message on successful multiple files deletion', fakeAsync(done => {
|
||||
spyOn(contentApi, 'deleteNode').and.returnValue(Observable.of(null));
|
||||
|
||||
actions$.pipe(
|
||||
ofType<SnackbarInfoAction>(SNACKBAR_INFO),
|
||||
map(action => {
|
||||
done();
|
||||
})
|
||||
);
|
||||
|
||||
component.selection = [
|
||||
{ entry: { id: '1', name: 'name1' } },
|
||||
{ entry: { id: '2', name: 'name2' } }
|
||||
];
|
||||
|
||||
fixture.detectChanges();
|
||||
element.triggerEventHandler('click', null);
|
||||
|
||||
tick();
|
||||
}));
|
||||
|
||||
it('should raise error message failed multiple files deletion', fakeAsync(done => {
|
||||
spyOn(contentApi, 'deleteNode').and.returnValue(Observable.throw(null));
|
||||
|
||||
actions$.pipe(
|
||||
ofType<SnackbarErrorAction>(SNACKBAR_ERROR),
|
||||
map(action => {
|
||||
done();
|
||||
})
|
||||
);
|
||||
|
||||
component.selection = [
|
||||
{ entry: { id: '1', name: 'name1' } },
|
||||
{ entry: { id: '2', name: 'name2' } }
|
||||
];
|
||||
|
||||
fixture.detectChanges();
|
||||
element.triggerEventHandler('click', null);
|
||||
|
||||
tick();
|
||||
}));
|
||||
|
||||
it('should raise warning message when only one file is successful', fakeAsync(done => {
|
||||
spyOn(contentApi, 'deleteNode').and.callFake((id) => {
|
||||
if (id === '1') {
|
||||
return Observable.throw(null);
|
||||
} else {
|
||||
return Observable.of(null);
|
||||
}
|
||||
});
|
||||
|
||||
actions$.pipe(
|
||||
ofType<SnackbarWarningAction>(SNACKBAR_WARNING),
|
||||
map(action => {
|
||||
done();
|
||||
})
|
||||
);
|
||||
|
||||
component.selection = [
|
||||
{ entry: { id: '1', name: 'name1' } },
|
||||
{ entry: { id: '2', name: 'name2' } }
|
||||
];
|
||||
|
||||
fixture.detectChanges();
|
||||
element.triggerEventHandler('click', null);
|
||||
|
||||
tick();
|
||||
}));
|
||||
|
||||
it('should raise warning message when some files are successfully deleted', fakeAsync(done => {
|
||||
spyOn(contentApi, 'deleteNode').and.callFake((id) => {
|
||||
if (id === '1') {
|
||||
return Observable.throw(null);
|
||||
}
|
||||
|
||||
if (id === '2') {
|
||||
return Observable.of(null);
|
||||
}
|
||||
|
||||
if (id === '3') {
|
||||
return Observable.of(null);
|
||||
}
|
||||
});
|
||||
|
||||
actions$.pipe(
|
||||
ofType<SnackbarWarningAction>(SNACKBAR_WARNING),
|
||||
map(action => {
|
||||
done();
|
||||
})
|
||||
);
|
||||
|
||||
component.selection = [
|
||||
{ entry: { id: '1', name: 'name1' } },
|
||||
{ entry: { id: '2', name: 'name2' } },
|
||||
{ entry: { id: '3', name: 'name3' } }
|
||||
];
|
||||
|
||||
fixture.detectChanges();
|
||||
element.triggerEventHandler('click', null);
|
||||
|
||||
tick();
|
||||
}));
|
||||
});
|
||||
|
||||
/*
|
||||
describe('Restore action', () => {
|
||||
beforeEach(() => {
|
||||
spyOn(alfrescoApiService.nodesApi, 'deleteNode').and.returnValue(Promise.resolve(null));
|
||||
});
|
||||
|
||||
it('notifies failed file on on restore', () => {
|
||||
spyOn(alfrescoApiService.nodesApi, 'restoreNode').and.returnValue(Promise.reject(null));
|
||||
|
||||
component.selection = [
|
||||
{ entry: { id: '1', name: 'name1' } }
|
||||
];
|
||||
|
||||
fixture.detectChanges();
|
||||
element.triggerEventHandler('click', null);
|
||||
|
||||
expect(spySnackBar.calls.mostRecent().args)
|
||||
.toEqual((['APP.MESSAGES.ERRORS.NODE_RESTORE', '', 3000]));
|
||||
});
|
||||
|
||||
it('notifies failed files on on restore', () => {
|
||||
spyOn(alfrescoApiService.nodesApi, 'restoreNode').and.returnValue(Promise.reject(null));
|
||||
|
||||
component.selection = [
|
||||
{ entry: { id: '1', name: 'name1' } },
|
||||
{ entry: { id: '2', name: 'name2' } }
|
||||
];
|
||||
|
||||
fixture.detectChanges();
|
||||
element.triggerEventHandler('click', null);
|
||||
|
||||
expect(spySnackBar.calls.mostRecent().args)
|
||||
.toEqual((['APP.MESSAGES.ERRORS.NODE_RESTORE_PLURAL', '', 3000]));
|
||||
});
|
||||
|
||||
it('signals files restored', () => {
|
||||
spyOn(contentService.nodeRestored, 'next');
|
||||
spyOn(alfrescoApiService.nodesApi, 'restoreNode').and.callFake((id) => {
|
||||
if (id === '1') {
|
||||
return Promise.resolve(null);
|
||||
} else {
|
||||
return Promise.reject(null);
|
||||
}
|
||||
});
|
||||
|
||||
component.selection = [
|
||||
{ entry: { id: '1', name: 'name1' } },
|
||||
{ entry: { id: '2', name: 'name2' } }
|
||||
];
|
||||
|
||||
fixture.detectChanges();
|
||||
element.triggerEventHandler('click', null);
|
||||
|
||||
expect(contentService.nodeRestored.next).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
*/
|
||||
});
|
@ -1,59 +0,0 @@
|
||||
/*!
|
||||
* @license
|
||||
* Alfresco Example Content Application
|
||||
*
|
||||
* Copyright (C) 2005 - 2018 Alfresco Software Limited
|
||||
*
|
||||
* This file is part of the Alfresco Example Content Application.
|
||||
* If the software was purchased under a paid Alfresco license, the terms of
|
||||
* the paid license agreement will prevail. Otherwise, the software is
|
||||
* provided under the following open source license terms:
|
||||
*
|
||||
* The Alfresco Example Content Application is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* The Alfresco Example Content Application is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Directive, HostListener, Input } from '@angular/core';
|
||||
import { MinimalNodeEntity } from 'alfresco-js-api';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { AppStore } from '../store/states/app.state';
|
||||
import { DeleteNodesAction } from '../store/actions';
|
||||
import { NodeInfo } from '../store/models';
|
||||
|
||||
@Directive({
|
||||
selector: '[acaDeleteNode]'
|
||||
})
|
||||
export class NodeDeleteDirective {
|
||||
|
||||
// tslint:disable-next-line:no-input-rename
|
||||
@Input('acaDeleteNode')
|
||||
selection: MinimalNodeEntity[];
|
||||
|
||||
constructor(private store: Store<AppStore>) {}
|
||||
|
||||
@HostListener('click')
|
||||
onClick() {
|
||||
if (this.selection && this.selection.length > 0) {
|
||||
const toDelete: NodeInfo[] = this.selection.map(node => {
|
||||
const { name } = node.entry;
|
||||
const id = node.entry.nodeId || node.entry.id;
|
||||
|
||||
return {
|
||||
id,
|
||||
name
|
||||
};
|
||||
});
|
||||
this.store.dispatch(new DeleteNodesAction(toDelete));
|
||||
}
|
||||
}
|
||||
}
|
@ -1,477 +0,0 @@
|
||||
/*!
|
||||
* @license
|
||||
* Alfresco Example Content Application
|
||||
*
|
||||
* Copyright (C) 2005 - 2018 Alfresco Software Limited
|
||||
*
|
||||
* This file is part of the Alfresco Example Content Application.
|
||||
* If the software was purchased under a paid Alfresco license, the terms of
|
||||
* the paid license agreement will prevail. Otherwise, the software is
|
||||
* provided under the following open source license terms:
|
||||
*
|
||||
* The Alfresco Example Content Application is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* The Alfresco Example Content Application is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Component, DebugElement } from '@angular/core';
|
||||
import { ComponentFixture, TestBed, fakeAsync } from '@angular/core/testing';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { Observable } from 'rxjs/Rx';
|
||||
import { MatSnackBar } from '@angular/material';
|
||||
import { TranslationService } from '@alfresco/adf-core';
|
||||
import { NodeActionsService } from '../services/node-actions.service';
|
||||
import { NodeMoveDirective } from './node-move.directive';
|
||||
import { EffectsModule, Actions, ofType } from '@ngrx/effects';
|
||||
import { NodeEffects } from '../store/effects/node.effects';
|
||||
import { SnackbarErrorAction, SNACKBAR_ERROR } from '../store/actions';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { AppTestingModule } from '../testing/app-testing.module';
|
||||
import { ContentApiService } from '../services/content-api.service';
|
||||
|
||||
@Component({
|
||||
template: '<div [acaMoveNode]="selection"></div>'
|
||||
})
|
||||
class TestComponent {
|
||||
selection;
|
||||
}
|
||||
|
||||
describe('NodeMoveDirective', () => {
|
||||
let fixture: ComponentFixture<TestComponent>;
|
||||
let component: TestComponent;
|
||||
let element: DebugElement;
|
||||
let service: NodeActionsService;
|
||||
let actions$: Actions;
|
||||
let translationService: TranslationService;
|
||||
let contentApi: ContentApiService;
|
||||
let snackBar: MatSnackBar;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
AppTestingModule,
|
||||
EffectsModule.forRoot([NodeEffects])
|
||||
],
|
||||
declarations: [
|
||||
NodeMoveDirective,
|
||||
TestComponent
|
||||
]
|
||||
});
|
||||
|
||||
contentApi = TestBed.get(ContentApiService);
|
||||
translationService = TestBed.get(TranslationService);
|
||||
|
||||
actions$ = TestBed.get(Actions);
|
||||
fixture = TestBed.createComponent(TestComponent);
|
||||
component = fixture.componentInstance;
|
||||
element = fixture.debugElement.query(By.directive(NodeMoveDirective));
|
||||
service = TestBed.get(NodeActionsService);
|
||||
snackBar = TestBed.get(MatSnackBar);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
spyOn(translationService, 'instant').and.callFake((keysArray) => {
|
||||
if (Array.isArray(keysArray)) {
|
||||
const processedKeys = {};
|
||||
keysArray.forEach((key) => {
|
||||
processedKeys[key] = key;
|
||||
});
|
||||
return processedKeys;
|
||||
} else {
|
||||
return keysArray;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('Move node action', () => {
|
||||
beforeEach(() => {
|
||||
spyOn(snackBar, 'open').and.callThrough();
|
||||
});
|
||||
|
||||
it('notifies successful move of a node', () => {
|
||||
const node = [ { entry: { id: 'node-to-move-id', name: 'name' } } ];
|
||||
const moveResponse = {
|
||||
succeeded: node,
|
||||
failed: [],
|
||||
partiallySucceeded: []
|
||||
};
|
||||
|
||||
spyOn(service, 'moveNodes').and.returnValue(Observable.of('OPERATION.SUCCES.CONTENT.MOVE'));
|
||||
spyOn(service, 'processResponse').and.returnValue(moveResponse);
|
||||
|
||||
component.selection = node;
|
||||
|
||||
fixture.detectChanges();
|
||||
element.triggerEventHandler('click', null);
|
||||
service.contentMoved.next(moveResponse);
|
||||
|
||||
expect(service.moveNodes).toHaveBeenCalled();
|
||||
expect(snackBar.open['calls'].argsFor(0)[0]).toBe('APP.MESSAGES.INFO.NODE_MOVE.SINGULAR');
|
||||
});
|
||||
|
||||
it('notifies successful move of multiple nodes', () => {
|
||||
const nodes = [
|
||||
{ entry: { id: '1', name: 'name1' } },
|
||||
{ entry: { id: '2', name: 'name2' } }];
|
||||
const moveResponse = {
|
||||
succeeded: nodes,
|
||||
failed: [],
|
||||
partiallySucceeded: []
|
||||
};
|
||||
|
||||
spyOn(service, 'moveNodes').and.returnValue(Observable.of('OPERATION.SUCCES.CONTENT.MOVE'));
|
||||
spyOn(service, 'processResponse').and.returnValue(moveResponse);
|
||||
|
||||
component.selection = nodes;
|
||||
|
||||
fixture.detectChanges();
|
||||
element.triggerEventHandler('click', null);
|
||||
service.contentMoved.next(moveResponse);
|
||||
|
||||
expect(service.moveNodes).toHaveBeenCalled();
|
||||
expect(snackBar.open['calls'].argsFor(0)[0]).toBe('APP.MESSAGES.INFO.NODE_MOVE.PLURAL');
|
||||
});
|
||||
|
||||
it('notifies partial move of a node', () => {
|
||||
const node = [ { entry: { id: '1', name: 'name' } } ];
|
||||
const moveResponse = {
|
||||
succeeded: [],
|
||||
failed: [],
|
||||
partiallySucceeded: node
|
||||
};
|
||||
|
||||
spyOn(service, 'moveNodes').and.returnValue(Observable.of('OPERATION.SUCCES.CONTENT.MOVE'));
|
||||
spyOn(service, 'processResponse').and.returnValue(moveResponse);
|
||||
|
||||
component.selection = node;
|
||||
|
||||
fixture.detectChanges();
|
||||
element.triggerEventHandler('click', null);
|
||||
service.contentMoved.next(moveResponse);
|
||||
|
||||
expect(service.moveNodes).toHaveBeenCalled();
|
||||
expect(snackBar.open['calls'].argsFor(0)[0]).toBe('APP.MESSAGES.INFO.NODE_MOVE.PARTIAL.SINGULAR');
|
||||
});
|
||||
|
||||
it('notifies partial move of multiple nodes', () => {
|
||||
const nodes = [
|
||||
{ entry: { id: '1', name: 'name' } },
|
||||
{ entry: { id: '2', name: 'name2' } } ];
|
||||
const moveResponse = {
|
||||
succeeded: [],
|
||||
failed: [],
|
||||
partiallySucceeded: nodes
|
||||
};
|
||||
|
||||
spyOn(service, 'moveNodes').and.returnValue(Observable.of('OPERATION.SUCCES.CONTENT.MOVE'));
|
||||
spyOn(service, 'processResponse').and.returnValue(moveResponse);
|
||||
|
||||
component.selection = nodes;
|
||||
|
||||
fixture.detectChanges();
|
||||
element.triggerEventHandler('click', null);
|
||||
service.contentMoved.next(moveResponse);
|
||||
|
||||
expect(service.moveNodes).toHaveBeenCalled();
|
||||
expect(snackBar.open['calls'].argsFor(0)[0]).toBe('APP.MESSAGES.INFO.NODE_MOVE.PARTIAL.PLURAL');
|
||||
});
|
||||
|
||||
it('notifies successful move and the number of nodes that could not be moved', () => {
|
||||
const nodes = [ { entry: { id: '1', name: 'name' } },
|
||||
{ entry: { id: '2', name: 'name2' } } ];
|
||||
const moveResponse = {
|
||||
succeeded: [ nodes[0] ],
|
||||
failed: [ nodes[1] ],
|
||||
partiallySucceeded: []
|
||||
};
|
||||
|
||||
spyOn(service, 'moveNodes').and.returnValue(Observable.of('OPERATION.SUCCES.CONTENT.MOVE'));
|
||||
spyOn(service, 'processResponse').and.returnValue(moveResponse);
|
||||
|
||||
component.selection = nodes;
|
||||
|
||||
fixture.detectChanges();
|
||||
element.triggerEventHandler('click', null);
|
||||
service.contentMoved.next(moveResponse);
|
||||
|
||||
expect(service.moveNodes).toHaveBeenCalled();
|
||||
expect(snackBar.open['calls'].argsFor(0)[0])
|
||||
.toBe('APP.MESSAGES.INFO.NODE_MOVE.SINGULAR APP.MESSAGES.INFO.NODE_MOVE.PARTIAL.FAIL');
|
||||
});
|
||||
|
||||
it('notifies successful move and the number of partially moved ones', () => {
|
||||
const nodes = [ { entry: { id: '1', name: 'name' } },
|
||||
{ entry: { id: '2', name: 'name2' } } ];
|
||||
const moveResponse = {
|
||||
succeeded: [ nodes[0] ],
|
||||
failed: [],
|
||||
partiallySucceeded: [ nodes[1] ]
|
||||
};
|
||||
|
||||
spyOn(service, 'moveNodes').and.returnValue(Observable.of('OPERATION.SUCCES.CONTENT.MOVE'));
|
||||
spyOn(service, 'processResponse').and.returnValue(moveResponse);
|
||||
|
||||
component.selection = nodes;
|
||||
|
||||
fixture.detectChanges();
|
||||
element.triggerEventHandler('click', null);
|
||||
service.contentMoved.next(moveResponse);
|
||||
|
||||
expect(service.moveNodes).toHaveBeenCalled();
|
||||
expect(snackBar.open['calls'].argsFor(0)[0])
|
||||
.toBe('APP.MESSAGES.INFO.NODE_MOVE.SINGULAR APP.MESSAGES.INFO.NODE_MOVE.PARTIAL.SINGULAR');
|
||||
});
|
||||
|
||||
it('notifies error if success message was not emitted', () => {
|
||||
const node = { entry: { id: 'node-to-move-id', name: 'name' } };
|
||||
const moveResponse = {
|
||||
succeeded: [],
|
||||
failed: [],
|
||||
partiallySucceeded: []
|
||||
};
|
||||
|
||||
spyOn(service, 'moveNodes').and.returnValue(Observable.of(''));
|
||||
|
||||
component.selection = [ node ];
|
||||
|
||||
fixture.detectChanges();
|
||||
element.triggerEventHandler('click', null);
|
||||
service.contentMoved.next(moveResponse);
|
||||
|
||||
expect(service.moveNodes).toHaveBeenCalled();
|
||||
expect(snackBar.open['calls'].argsFor(0)[0]).toBe('APP.MESSAGES.ERRORS.GENERIC');
|
||||
});
|
||||
|
||||
it('notifies permission error on move of node', () => {
|
||||
spyOn(service, 'moveNodes').and.returnValue(Observable.throw(new Error(JSON.stringify({error: {statusCode: 403}}))));
|
||||
|
||||
component.selection = [{ entry: { id: '1', name: 'name' } }];
|
||||
|
||||
fixture.detectChanges();
|
||||
element.triggerEventHandler('click', null);
|
||||
|
||||
expect(service.moveNodes).toHaveBeenCalled();
|
||||
expect(snackBar.open['calls'].argsFor(0)[0]).toBe('APP.MESSAGES.ERRORS.PERMISSION');
|
||||
});
|
||||
|
||||
it('notifies generic error message on all errors, but 403', () => {
|
||||
spyOn(service, 'moveNodes').and.returnValue(Observable.throw(new Error(JSON.stringify({error: {statusCode: 404}}))));
|
||||
|
||||
component.selection = [{ entry: { id: '1', name: 'name' } }];
|
||||
|
||||
fixture.detectChanges();
|
||||
element.triggerEventHandler('click', null);
|
||||
|
||||
expect(service.moveNodes).toHaveBeenCalled();
|
||||
expect(snackBar.open['calls'].argsFor(0)[0]).toBe('APP.MESSAGES.ERRORS.GENERIC');
|
||||
});
|
||||
|
||||
it('notifies conflict error message on 409', () => {
|
||||
spyOn(service, 'moveNodes').and.returnValue(Observable.throw(new Error(JSON.stringify({error: {statusCode: 409}}))));
|
||||
|
||||
component.selection = [{ entry: { id: '1', name: 'name' } }];
|
||||
|
||||
fixture.detectChanges();
|
||||
element.triggerEventHandler('click', null);
|
||||
|
||||
expect(service.moveNodes).toHaveBeenCalled();
|
||||
expect(snackBar.open['calls'].argsFor(0)[0]).toBe('APP.MESSAGES.ERRORS.NODE_MOVE');
|
||||
});
|
||||
|
||||
it('notifies error if move response has only failed items', () => {
|
||||
const node = [ { entry: { id: '1', name: 'name' } } ];
|
||||
const moveResponse = {
|
||||
succeeded: [],
|
||||
failed: [ {} ],
|
||||
partiallySucceeded: []
|
||||
};
|
||||
|
||||
spyOn(service, 'moveNodes').and.returnValue(Observable.of('OPERATION.SUCCES.CONTENT.MOVE'));
|
||||
spyOn(service, 'processResponse').and.returnValue(moveResponse);
|
||||
|
||||
component.selection = node;
|
||||
|
||||
fixture.detectChanges();
|
||||
element.triggerEventHandler('click', null);
|
||||
service.contentMoved.next(moveResponse);
|
||||
|
||||
expect(service.moveNodes).toHaveBeenCalled();
|
||||
expect(snackBar.open['calls'].argsFor(0)[0]).toBe('APP.MESSAGES.ERRORS.GENERIC');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Undo Move action', () => {
|
||||
beforeEach(() => {
|
||||
spyOn(service, 'moveNodes').and.returnValue(Observable.of('OPERATION.SUCCES.CONTENT.MOVE'));
|
||||
|
||||
spyOn(snackBar, 'open').and.returnValue({
|
||||
onAction: () => Observable.of({})
|
||||
});
|
||||
|
||||
// spyOn(snackBar, 'open').and.callThrough();
|
||||
});
|
||||
|
||||
it('should move node back to initial parent, after succeeded move', () => {
|
||||
const initialParent = 'parent-id-0';
|
||||
const node = { entry: { id: 'node-to-move-id', name: 'name', parentId: initialParent } };
|
||||
component.selection = [ node ];
|
||||
|
||||
spyOn(service, 'moveNodeAction').and.returnValue(Observable.of({}));
|
||||
|
||||
fixture.detectChanges();
|
||||
element.triggerEventHandler('click', null);
|
||||
const movedItems = {
|
||||
failed: [],
|
||||
partiallySucceeded: [],
|
||||
succeeded: [ { itemMoved: node, initialParentId: initialParent} ]
|
||||
};
|
||||
service.contentMoved.next(<any>movedItems);
|
||||
|
||||
expect(service.moveNodeAction)
|
||||
.toHaveBeenCalledWith(movedItems.succeeded[0].itemMoved.entry, movedItems.succeeded[0].initialParentId);
|
||||
expect(snackBar.open['calls'].argsFor(0)[0]).toBe('APP.MESSAGES.INFO.NODE_MOVE.SINGULAR');
|
||||
});
|
||||
|
||||
it('should move node back to initial parent, after succeeded move of a single file', () => {
|
||||
const initialParent = 'parent-id-0';
|
||||
const node = { entry: { id: 'node-to-move-id', name: 'name', isFolder: false, parentId: initialParent } };
|
||||
component.selection = [ node ];
|
||||
|
||||
spyOn(service, 'moveNodeAction').and.returnValue(Observable.of({}));
|
||||
|
||||
const movedItems = {
|
||||
failed: [],
|
||||
partiallySucceeded: [],
|
||||
succeeded: [ node ]
|
||||
};
|
||||
|
||||
fixture.detectChanges();
|
||||
element.triggerEventHandler('click', null);
|
||||
service.contentMoved.next(<any>movedItems);
|
||||
|
||||
expect(service.moveNodeAction).toHaveBeenCalledWith(node.entry, initialParent);
|
||||
expect(snackBar.open['calls'].argsFor(0)[0]).toBe('APP.MESSAGES.INFO.NODE_MOVE.SINGULAR');
|
||||
});
|
||||
|
||||
it('should restore deleted folder back to initial parent, after succeeded moving all its files', () => {
|
||||
// when folder was deleted after all its children were moved to a folder with the same name from destination
|
||||
spyOn(contentApi, 'restoreNode').and.returnValue(Observable.of(null));
|
||||
|
||||
const initialParent = 'parent-id-0';
|
||||
const node = { entry: { id: 'folder-to-move-id', name: 'conflicting-name', parentId: initialParent, isFolder: true } };
|
||||
component.selection = [ node ];
|
||||
|
||||
const itemMoved = {}; // folder was empty
|
||||
service.moveDeletedEntries = [ node ]; // folder got deleted
|
||||
|
||||
const movedItems = {
|
||||
failed: [],
|
||||
partiallySucceeded: [],
|
||||
succeeded: [ [ itemMoved ] ]
|
||||
};
|
||||
|
||||
fixture.detectChanges();
|
||||
element.triggerEventHandler('click', null);
|
||||
service.contentMoved.next(<any>movedItems);
|
||||
|
||||
expect(contentApi.restoreNode).toHaveBeenCalled();
|
||||
expect(snackBar.open['calls'].argsFor(0)[0]).toBe('APP.MESSAGES.INFO.NODE_MOVE.SINGULAR');
|
||||
});
|
||||
|
||||
it('should notify when error occurs on Undo Move action', fakeAsync(done => {
|
||||
spyOn(contentApi, 'restoreNode').and.returnValue(Observable.throw(null));
|
||||
|
||||
actions$.pipe(
|
||||
ofType<SnackbarErrorAction>(SNACKBAR_ERROR),
|
||||
map(action => done())
|
||||
);
|
||||
|
||||
const initialParent = 'parent-id-0';
|
||||
const node = { entry: { id: 'node-to-move-id', name: 'conflicting-name', parentId: initialParent } };
|
||||
component.selection = [node];
|
||||
|
||||
const afterMoveParentId = 'parent-id-1';
|
||||
const childMoved = { entry: { id: 'child-of-node-to-move-id', name: 'child-name', parentId: afterMoveParentId } };
|
||||
service.moveDeletedEntries = [ node ]; // folder got deleted
|
||||
|
||||
const movedItems = {
|
||||
failed: [],
|
||||
partiallySucceeded: [],
|
||||
succeeded: [{ itemMoved: childMoved, initialParentId: initialParent }]
|
||||
};
|
||||
|
||||
fixture.detectChanges();
|
||||
element.triggerEventHandler('click', null);
|
||||
service.contentMoved.next(<any>movedItems);
|
||||
|
||||
expect(contentApi.restoreNode).toHaveBeenCalled();
|
||||
}));
|
||||
|
||||
it('should notify when some error of type Error occurs on Undo Move action', fakeAsync(done => {
|
||||
spyOn(contentApi, 'restoreNode').and.returnValue(Observable.throw(new Error('oops!')));
|
||||
|
||||
actions$.pipe(
|
||||
ofType<SnackbarErrorAction>(SNACKBAR_ERROR),
|
||||
map(action => done())
|
||||
);
|
||||
|
||||
const initialParent = 'parent-id-0';
|
||||
const node = { entry: { id: 'node-to-move-id', name: 'name', parentId: initialParent } };
|
||||
component.selection = [ node ];
|
||||
|
||||
const childMoved = { entry: { id: 'child-of-node-to-move-id', name: 'child-name' } };
|
||||
service.moveDeletedEntries = [ node ]; // folder got deleted
|
||||
|
||||
const movedItems = {
|
||||
failed: [],
|
||||
partiallySucceeded: [],
|
||||
succeeded: [{ itemMoved: childMoved, initialParentId: initialParent }]
|
||||
};
|
||||
|
||||
fixture.detectChanges();
|
||||
element.triggerEventHandler('click', null);
|
||||
service.contentMoved.next(<any>movedItems);
|
||||
|
||||
expect(contentApi.restoreNode).toHaveBeenCalled();
|
||||
}));
|
||||
|
||||
it('should notify permission error when it occurs on Undo Move action', fakeAsync(done => {
|
||||
spyOn(contentApi, 'restoreNode').and.returnValue(Observable.throw(new Error(JSON.stringify({error: {statusCode: 403}}))));
|
||||
|
||||
actions$.pipe(
|
||||
ofType<SnackbarErrorAction>(SNACKBAR_ERROR),
|
||||
map(action => done())
|
||||
);
|
||||
|
||||
const initialParent = 'parent-id-0';
|
||||
const node = { entry: { id: 'node-to-move-id', name: 'name', parentId: initialParent } };
|
||||
component.selection = [ node ];
|
||||
|
||||
const childMoved = { entry: { id: 'child-of-node-to-move-id', name: 'child-name' } };
|
||||
service.moveDeletedEntries = [ node ]; // folder got deleted
|
||||
|
||||
const movedItems = {
|
||||
failed: [],
|
||||
partiallySucceeded: [],
|
||||
succeeded: [{ itemMoved: childMoved, initialParentId: initialParent }]
|
||||
};
|
||||
|
||||
fixture.detectChanges();
|
||||
element.triggerEventHandler('click', null);
|
||||
service.contentMoved.next(<any>movedItems);
|
||||
|
||||
expect(service.moveNodes).toHaveBeenCalled();
|
||||
expect(contentApi.restoreNode).toHaveBeenCalled();
|
||||
}));
|
||||
});
|
||||
|
||||
});
|
@ -1,223 +0,0 @@
|
||||
/*!
|
||||
* @license
|
||||
* Alfresco Example Content Application
|
||||
*
|
||||
* Copyright (C) 2005 - 2018 Alfresco Software Limited
|
||||
*
|
||||
* This file is part of the Alfresco Example Content Application.
|
||||
* If the software was purchased under a paid Alfresco license, the terms of
|
||||
* the paid license agreement will prevail. Otherwise, the software is
|
||||
* provided under the following open source license terms:
|
||||
*
|
||||
* The Alfresco Example Content Application is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* The Alfresco Example Content Application is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Directive, HostListener, Input } from '@angular/core';
|
||||
|
||||
import { TranslationService } from '@alfresco/adf-core';
|
||||
import { MinimalNodeEntity } from 'alfresco-js-api';
|
||||
import { MatSnackBar } from '@angular/material';
|
||||
|
||||
import { ContentManagementService } from '../services/content-management.service';
|
||||
import { NodeActionsService } from '../services/node-actions.service';
|
||||
import { Observable } from 'rxjs/Rx';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { AppStore } from '../store/states/app.state';
|
||||
import { SnackbarErrorAction } from '../store/actions';
|
||||
import { ContentApiService } from '../services/content-api.service';
|
||||
|
||||
@Directive({
|
||||
selector: '[acaMoveNode]'
|
||||
})
|
||||
|
||||
export class NodeMoveDirective {
|
||||
// tslint:disable-next-line:no-input-rename
|
||||
@Input('acaMoveNode')
|
||||
selection: MinimalNodeEntity[];
|
||||
|
||||
@HostListener('click')
|
||||
onClick() {
|
||||
this.moveSelected();
|
||||
}
|
||||
|
||||
constructor(
|
||||
private store: Store<AppStore>,
|
||||
private contentApi: ContentApiService,
|
||||
private content: ContentManagementService,
|
||||
private nodeActionsService: NodeActionsService,
|
||||
private translation: TranslationService,
|
||||
private snackBar: MatSnackBar
|
||||
) {}
|
||||
|
||||
moveSelected() {
|
||||
const permissionForMove = '!';
|
||||
|
||||
Observable.zip(
|
||||
this.nodeActionsService.moveNodes(this.selection, permissionForMove),
|
||||
this.nodeActionsService.contentMoved
|
||||
).subscribe(
|
||||
(result) => {
|
||||
const [ operationResult, moveResponse ] = result;
|
||||
this.toastMessage(operationResult, moveResponse);
|
||||
|
||||
this.content.nodesMoved.next(null);
|
||||
},
|
||||
(error) => {
|
||||
this.toastMessage(error);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private toastMessage(info: any, moveResponse?: any) {
|
||||
const succeeded = (moveResponse && moveResponse['succeeded']) ? moveResponse['succeeded'].length : 0;
|
||||
const partiallySucceeded = (moveResponse && moveResponse['partiallySucceeded']) ? moveResponse['partiallySucceeded'].length : 0;
|
||||
const failures = (moveResponse && moveResponse['failed']) ? moveResponse['failed'].length : 0;
|
||||
|
||||
let successMessage = '';
|
||||
let partialSuccessMessage = '';
|
||||
let failedMessage = '';
|
||||
let errorMessage = '';
|
||||
|
||||
if (typeof info === 'string') {
|
||||
|
||||
// in case of success
|
||||
if (info.toLowerCase().indexOf('succes') !== -1) {
|
||||
const i18nMessageString = 'APP.MESSAGES.INFO.NODE_MOVE.';
|
||||
let i18MessageSuffix = '';
|
||||
|
||||
if (succeeded) {
|
||||
i18MessageSuffix = ( succeeded === 1 ) ? 'SINGULAR' : 'PLURAL';
|
||||
successMessage = `${i18nMessageString}${i18MessageSuffix}`;
|
||||
}
|
||||
|
||||
if (partiallySucceeded) {
|
||||
i18MessageSuffix = ( partiallySucceeded === 1 ) ? 'PARTIAL.SINGULAR' : 'PARTIAL.PLURAL';
|
||||
partialSuccessMessage = `${i18nMessageString}${i18MessageSuffix}`;
|
||||
}
|
||||
|
||||
if (failures) {
|
||||
// if moving failed for ALL nodes, emit error
|
||||
if (failures === this.selection.length) {
|
||||
const errors = this.nodeActionsService.flatten(moveResponse['failed']);
|
||||
errorMessage = this.getErrorMessage(errors[0]);
|
||||
|
||||
} else {
|
||||
i18MessageSuffix = 'PARTIAL.FAIL';
|
||||
failedMessage = `${i18nMessageString}${i18MessageSuffix}`;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
errorMessage = 'APP.MESSAGES.ERRORS.GENERIC';
|
||||
}
|
||||
|
||||
} else {
|
||||
errorMessage = this.getErrorMessage(info);
|
||||
}
|
||||
|
||||
const undo = (succeeded + partiallySucceeded > 0) ? this.translation.instant('APP.ACTIONS.UNDO') : '';
|
||||
failedMessage = errorMessage ? errorMessage : failedMessage;
|
||||
|
||||
const beforePartialSuccessMessage = (successMessage && partialSuccessMessage) ? ' ' : '';
|
||||
const beforeFailedMessage = ((successMessage || partialSuccessMessage) && failedMessage) ? ' ' : '';
|
||||
|
||||
const initialParentId = this.nodeActionsService.getEntryParentId(this.selection[0].entry);
|
||||
|
||||
const messages = this.translation.instant(
|
||||
[successMessage, partialSuccessMessage, failedMessage],
|
||||
{ success: succeeded, failed: failures, partially: partiallySucceeded}
|
||||
);
|
||||
|
||||
// TODO: review in terms of i18n
|
||||
this.snackBar
|
||||
.open(
|
||||
messages[successMessage]
|
||||
+ beforePartialSuccessMessage + messages[partialSuccessMessage]
|
||||
+ beforeFailedMessage + messages[failedMessage]
|
||||
, undo, {
|
||||
panelClass: 'info-snackbar',
|
||||
duration: 3000
|
||||
})
|
||||
.onAction()
|
||||
.subscribe(() => this.revertMoving(moveResponse, initialParentId));
|
||||
}
|
||||
|
||||
getErrorMessage(errorObject): string {
|
||||
let i18nMessageString = 'APP.MESSAGES.ERRORS.GENERIC';
|
||||
|
||||
try {
|
||||
const { error: { statusCode } } = JSON.parse(errorObject.message);
|
||||
|
||||
if (statusCode === 409) {
|
||||
i18nMessageString = 'APP.MESSAGES.ERRORS.NODE_MOVE';
|
||||
|
||||
} else if (statusCode === 403) {
|
||||
i18nMessageString = 'APP.MESSAGES.ERRORS.PERMISSION';
|
||||
}
|
||||
|
||||
} catch (err) { /* Do nothing, keep the original message */ }
|
||||
|
||||
return i18nMessageString;
|
||||
}
|
||||
|
||||
private revertMoving(moveResponse, selectionParentId) {
|
||||
const movedNodes = (moveResponse && moveResponse['succeeded']) ? moveResponse['succeeded'] : [];
|
||||
const partiallyMovedNodes = (moveResponse && moveResponse['partiallySucceeded']) ? moveResponse['partiallySucceeded'] : [];
|
||||
|
||||
const restoreDeletedNodesBatch = this.nodeActionsService.moveDeletedEntries
|
||||
.map((folderEntry) => {
|
||||
return this.contentApi
|
||||
.restoreNode(folderEntry.nodeId || folderEntry.id)
|
||||
.map(node => node.entry);
|
||||
});
|
||||
|
||||
Observable.zip(...restoreDeletedNodesBatch, Observable.of(null))
|
||||
.flatMap(() => {
|
||||
|
||||
const nodesToBeMovedBack = [...partiallyMovedNodes, ...movedNodes];
|
||||
|
||||
const revertMoveBatch = this.nodeActionsService
|
||||
.flatten(nodesToBeMovedBack)
|
||||
.filter(node => node.entry || (node.itemMoved && node.itemMoved.entry))
|
||||
.map((node) => {
|
||||
if (node.itemMoved) {
|
||||
return this.nodeActionsService.moveNodeAction(node.itemMoved.entry, node.initialParentId);
|
||||
} else {
|
||||
return this.nodeActionsService.moveNodeAction(node.entry, selectionParentId);
|
||||
}
|
||||
});
|
||||
|
||||
return Observable.zip(...revertMoveBatch, Observable.of(null));
|
||||
})
|
||||
.subscribe(
|
||||
() => {
|
||||
this.content.nodesMoved.next(null);
|
||||
},
|
||||
error => {
|
||||
let message = 'APP.MESSAGES.ERRORS.GENERIC';
|
||||
|
||||
let errorJson = null;
|
||||
try {
|
||||
errorJson = JSON.parse(error.message);
|
||||
} catch {}
|
||||
|
||||
if (errorJson && errorJson.error && errorJson.error.statusCode === 403) {
|
||||
message = 'APP.MESSAGES.ERRORS.PERMISSION';
|
||||
}
|
||||
|
||||
this.store.dispatch(new SnackbarErrorAction(message));
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
}
|
@ -1,272 +0,0 @@
|
||||
/*!
|
||||
* @license
|
||||
* Alfresco Example Content Application
|
||||
*
|
||||
* Copyright (C) 2005 - 2018 Alfresco Software Limited
|
||||
*
|
||||
* This file is part of the Alfresco Example Content Application.
|
||||
* If the software was purchased under a paid Alfresco license, the terms of
|
||||
* the paid license agreement will prevail. Otherwise, the software is
|
||||
* provided under the following open source license terms:
|
||||
*
|
||||
* The Alfresco Example Content Application is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* The Alfresco Example Content Application is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Component, DebugElement } from '@angular/core';
|
||||
import { TestBed, ComponentFixture, fakeAsync, tick } from '@angular/core/testing';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { Observable } from 'rxjs/Rx';
|
||||
|
||||
import { NodePermanentDeleteDirective } from './node-permanent-delete.directive';
|
||||
import { MatDialog } from '@angular/material';
|
||||
import { Actions, ofType, EffectsModule } from '@ngrx/effects';
|
||||
import {
|
||||
SNACKBAR_INFO, SnackbarWarningAction, SnackbarInfoAction,
|
||||
SnackbarErrorAction, SNACKBAR_ERROR, SNACKBAR_WARNING
|
||||
} from '../store/actions';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { NodeEffects } from '../store/effects/node.effects';
|
||||
import { AppTestingModule } from '../testing/app-testing.module';
|
||||
import { ContentApiService } from '../services/content-api.service';
|
||||
|
||||
@Component({
|
||||
template: `<div [acaPermanentDelete]="selection"></div>`
|
||||
})
|
||||
class TestComponent {
|
||||
selection = [];
|
||||
}
|
||||
|
||||
describe('NodePermanentDeleteDirective', () => {
|
||||
let fixture: ComponentFixture<TestComponent>;
|
||||
let element: DebugElement;
|
||||
let component: TestComponent;
|
||||
let dialog: MatDialog;
|
||||
let actions$: Actions;
|
||||
let contentApi: ContentApiService;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
AppTestingModule,
|
||||
EffectsModule.forRoot([NodeEffects])
|
||||
],
|
||||
declarations: [
|
||||
NodePermanentDeleteDirective,
|
||||
TestComponent
|
||||
]
|
||||
});
|
||||
|
||||
contentApi = TestBed.get(ContentApiService);
|
||||
actions$ = TestBed.get(Actions);
|
||||
|
||||
fixture = TestBed.createComponent(TestComponent);
|
||||
component = fixture.componentInstance;
|
||||
element = fixture.debugElement.query(By.directive(NodePermanentDeleteDirective));
|
||||
|
||||
dialog = TestBed.get(MatDialog);
|
||||
spyOn(dialog, 'open').and.returnValue({
|
||||
afterClosed() {
|
||||
return Observable.of(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('does not purge nodes if no selection', () => {
|
||||
spyOn(contentApi, 'purgeDeletedNode');
|
||||
|
||||
component.selection = [];
|
||||
|
||||
fixture.detectChanges();
|
||||
element.triggerEventHandler('click', null);
|
||||
|
||||
expect(contentApi.purgeDeletedNode).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('call purge nodes if selection is not empty', fakeAsync(() => {
|
||||
spyOn(contentApi, 'purgeDeletedNode').and.returnValue(Observable.of({}));
|
||||
|
||||
component.selection = [ { entry: { id: '1' } } ];
|
||||
|
||||
fixture.detectChanges();
|
||||
element.triggerEventHandler('click', null);
|
||||
tick();
|
||||
|
||||
expect(contentApi.purgeDeletedNode).toHaveBeenCalled();
|
||||
}));
|
||||
|
||||
describe('notification', () => {
|
||||
it('raises warning on multiple fail and one success', fakeAsync(done => {
|
||||
actions$.pipe(
|
||||
ofType<SnackbarWarningAction>(SNACKBAR_WARNING),
|
||||
map((action: SnackbarWarningAction) => {
|
||||
done();
|
||||
})
|
||||
);
|
||||
|
||||
spyOn(contentApi, 'purgeDeletedNode').and.callFake((id) => {
|
||||
if (id === '1') {
|
||||
return Observable.of({});
|
||||
}
|
||||
|
||||
if (id === '2') {
|
||||
return Observable.throw({});
|
||||
}
|
||||
|
||||
if (id === '3') {
|
||||
return Observable.throw({});
|
||||
}
|
||||
});
|
||||
|
||||
component.selection = [
|
||||
{ entry: { id: '1', name: 'name1' } },
|
||||
{ entry: { id: '2', name: 'name2' } },
|
||||
{ entry: { id: '3', name: 'name3' } }
|
||||
];
|
||||
|
||||
fixture.detectChanges();
|
||||
element.triggerEventHandler('click', null);
|
||||
tick();
|
||||
}));
|
||||
|
||||
it('raises warning on multiple success and multiple fail', fakeAsync(done => {
|
||||
actions$.pipe(
|
||||
ofType<SnackbarWarningAction>(SNACKBAR_WARNING),
|
||||
map((action: SnackbarWarningAction) => {
|
||||
done();
|
||||
})
|
||||
);
|
||||
|
||||
spyOn(contentApi, 'purgeDeletedNode').and.callFake((id) => {
|
||||
if (id === '1') {
|
||||
return Observable.of({});
|
||||
}
|
||||
|
||||
if (id === '2') {
|
||||
return Observable.throw({});
|
||||
}
|
||||
|
||||
if (id === '3') {
|
||||
return Observable.throw({});
|
||||
}
|
||||
|
||||
if (id === '4') {
|
||||
return Observable.of({});
|
||||
}
|
||||
});
|
||||
|
||||
component.selection = [
|
||||
{ entry: { id: '1', name: 'name1' } },
|
||||
{ entry: { id: '2', name: 'name2' } },
|
||||
{ entry: { id: '3', name: 'name3' } },
|
||||
{ entry: { id: '4', name: 'name4' } }
|
||||
];
|
||||
|
||||
fixture.detectChanges();
|
||||
element.triggerEventHandler('click', null);
|
||||
tick();
|
||||
}));
|
||||
|
||||
it('raises info on one selected node success', fakeAsync(done => {
|
||||
actions$.pipe(
|
||||
ofType<SnackbarInfoAction>(SNACKBAR_INFO),
|
||||
map((action: SnackbarInfoAction) => {
|
||||
done();
|
||||
})
|
||||
);
|
||||
|
||||
spyOn(contentApi, 'purgeDeletedNode').and.returnValue(Observable.of({}));
|
||||
|
||||
component.selection = [
|
||||
{ entry: { id: '1', name: 'name1' } }
|
||||
];
|
||||
|
||||
fixture.detectChanges();
|
||||
element.triggerEventHandler('click', null);
|
||||
tick();
|
||||
}));
|
||||
|
||||
it('raises error on one selected node fail', fakeAsync(done => {
|
||||
actions$.pipe(
|
||||
ofType<SnackbarErrorAction>(SNACKBAR_ERROR),
|
||||
map((action: SnackbarErrorAction) => {
|
||||
done();
|
||||
})
|
||||
);
|
||||
|
||||
spyOn(contentApi, 'purgeDeletedNode').and.returnValue(Observable.throw({}));
|
||||
|
||||
component.selection = [
|
||||
{ entry: { id: '1', name: 'name1' } }
|
||||
];
|
||||
|
||||
fixture.detectChanges();
|
||||
element.triggerEventHandler('click', null);
|
||||
tick();
|
||||
}));
|
||||
|
||||
it('raises info on all nodes success', fakeAsync(done => {
|
||||
actions$.pipe(
|
||||
ofType<SnackbarInfoAction>(SNACKBAR_INFO),
|
||||
map((action: SnackbarInfoAction) => {
|
||||
done();
|
||||
})
|
||||
);
|
||||
spyOn(contentApi, 'purgeDeletedNode').and.callFake((id) => {
|
||||
if (id === '1') {
|
||||
return Observable.of({});
|
||||
}
|
||||
|
||||
if (id === '2') {
|
||||
return Observable.of({});
|
||||
}
|
||||
});
|
||||
|
||||
component.selection = [
|
||||
{ entry: { id: '1', name: 'name1' } },
|
||||
{ entry: { id: '2', name: 'name2' } }
|
||||
];
|
||||
|
||||
fixture.detectChanges();
|
||||
element.triggerEventHandler('click', null);
|
||||
tick();
|
||||
}));
|
||||
|
||||
it('raises error on all nodes fail', fakeAsync(done => {
|
||||
actions$.pipe(
|
||||
ofType<SnackbarErrorAction>(SNACKBAR_ERROR),
|
||||
map((action: SnackbarErrorAction) => {
|
||||
done();
|
||||
})
|
||||
);
|
||||
spyOn(contentApi, 'purgeDeletedNode').and.callFake((id) => {
|
||||
if (id === '1') {
|
||||
return Observable.throw({});
|
||||
}
|
||||
|
||||
if (id === '2') {
|
||||
return Observable.throw({});
|
||||
}
|
||||
});
|
||||
|
||||
component.selection = [
|
||||
{ entry: { id: '1', name: 'name1' } },
|
||||
{ entry: { id: '2', name: 'name2' } }
|
||||
];
|
||||
|
||||
fixture.detectChanges();
|
||||
element.triggerEventHandler('click', null);
|
||||
tick();
|
||||
}));
|
||||
});
|
||||
});
|
@ -1,80 +0,0 @@
|
||||
/*!
|
||||
* @license
|
||||
* Alfresco Example Content Application
|
||||
*
|
||||
* Copyright (C) 2005 - 2018 Alfresco Software Limited
|
||||
*
|
||||
* This file is part of the Alfresco Example Content Application.
|
||||
* If the software was purchased under a paid Alfresco license, the terms of
|
||||
* the paid license agreement will prevail. Otherwise, the software is
|
||||
* provided under the following open source license terms:
|
||||
*
|
||||
* The Alfresco Example Content Application is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* The Alfresco Example Content Application is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Directive, HostListener, Input } from '@angular/core';
|
||||
import { MinimalNodeEntity } from 'alfresco-js-api';
|
||||
import { MatDialog } from '@angular/material';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { AppStore } from '../store/states/app.state';
|
||||
import { SnackbarErrorAction } from '../store/actions';
|
||||
import { NodePermissionsDialogComponent } from '../dialogs/node-permissions/node-permissions.dialog';
|
||||
|
||||
@Directive({
|
||||
selector: '[acaNodePermissions]'
|
||||
})
|
||||
export class NodePermissionsDirective {
|
||||
// tslint:disable-next-line:no-input-rename
|
||||
@Input('acaNodePermissions') node: MinimalNodeEntity;
|
||||
|
||||
@HostListener('click')
|
||||
onClick() {
|
||||
this.showPermissions();
|
||||
}
|
||||
|
||||
constructor(
|
||||
private store: Store<AppStore>,
|
||||
private dialog: MatDialog
|
||||
) {}
|
||||
|
||||
showPermissions() {
|
||||
if (this.node) {
|
||||
let entry;
|
||||
if (this.node.entry) {
|
||||
entry = this.node.entry;
|
||||
|
||||
} else {
|
||||
entry = this.node;
|
||||
}
|
||||
|
||||
const entryId = entry.nodeId || (<any>entry).guid || entry.id;
|
||||
this.openPermissionsDialog(entryId);
|
||||
}
|
||||
}
|
||||
|
||||
openPermissionsDialog(nodeId: string) {
|
||||
// workaround Shared
|
||||
if (nodeId) {
|
||||
this.dialog.open(NodePermissionsDialogComponent, {
|
||||
data: { nodeId },
|
||||
panelClass: 'aca-permissions-dialog-panel',
|
||||
width: '730px'
|
||||
});
|
||||
} else {
|
||||
this.store.dispatch(
|
||||
new SnackbarErrorAction('APP.MESSAGES.ERRORS.PERMISSION')
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,390 +0,0 @@
|
||||
/*!
|
||||
* @license
|
||||
* Alfresco Example Content Application
|
||||
*
|
||||
* Copyright (C) 2005 - 2018 Alfresco Software Limited
|
||||
*
|
||||
* This file is part of the Alfresco Example Content Application.
|
||||
* If the software was purchased under a paid Alfresco license, the terms of
|
||||
* the paid license agreement will prevail. Otherwise, the software is
|
||||
* provided under the following open source license terms:
|
||||
*
|
||||
* The Alfresco Example Content Application is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* The Alfresco Example Content Application is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Component, DebugElement } from '@angular/core';
|
||||
import { TestBed, ComponentFixture, fakeAsync, tick } from '@angular/core/testing';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { NodeRestoreDirective } from './node-restore.directive';
|
||||
import { ContentManagementService } from '../services/content-management.service';
|
||||
import { Actions, ofType, EffectsModule } from '@ngrx/effects';
|
||||
import { SnackbarErrorAction,
|
||||
SNACKBAR_ERROR, SnackbarInfoAction, SNACKBAR_INFO,
|
||||
NavigateRouteAction, NAVIGATE_ROUTE } from '../store/actions';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { AppTestingModule } from '../testing/app-testing.module';
|
||||
import { ContentApiService } from '../services/content-api.service';
|
||||
import { Observable } from 'rxjs/Rx';
|
||||
import { NodeEffects } from '../store/effects';
|
||||
import { MinimalNodeEntity } from 'alfresco-js-api';
|
||||
|
||||
@Component({
|
||||
template: `<div [acaRestoreNode]="selection"></div>`
|
||||
})
|
||||
class TestComponent {
|
||||
selection: Array<MinimalNodeEntity> = [];
|
||||
}
|
||||
|
||||
describe('NodeRestoreDirective', () => {
|
||||
let fixture: ComponentFixture<TestComponent>;
|
||||
let element: DebugElement;
|
||||
let component: TestComponent;
|
||||
let contentManagementService: ContentManagementService;
|
||||
let actions$: Actions;
|
||||
let contentApi: ContentApiService;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
AppTestingModule,
|
||||
EffectsModule.forRoot([NodeEffects])
|
||||
],
|
||||
declarations: [
|
||||
NodeRestoreDirective,
|
||||
TestComponent
|
||||
]
|
||||
});
|
||||
|
||||
actions$ = TestBed.get(Actions);
|
||||
|
||||
fixture = TestBed.createComponent(TestComponent);
|
||||
component = fixture.componentInstance;
|
||||
element = fixture.debugElement.query(By.directive(NodeRestoreDirective));
|
||||
|
||||
contentManagementService = TestBed.get(ContentManagementService);
|
||||
contentApi = TestBed.get(ContentApiService);
|
||||
});
|
||||
|
||||
it('does not restore nodes if no selection', () => {
|
||||
spyOn(contentApi, 'restoreNode');
|
||||
|
||||
component.selection = [];
|
||||
|
||||
fixture.detectChanges();
|
||||
element.triggerEventHandler('click', null);
|
||||
|
||||
expect(contentApi.restoreNode).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not restore nodes if selection has nodes without path', () => {
|
||||
spyOn(contentApi, 'restoreNode');
|
||||
|
||||
component.selection = [ { entry: { id: '1' } } ];
|
||||
|
||||
fixture.detectChanges();
|
||||
element.triggerEventHandler('click', null);
|
||||
|
||||
expect(contentApi.restoreNode).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('call restore nodes if selection has nodes with path', fakeAsync(() => {
|
||||
spyOn(contentApi, 'restoreNode').and.returnValue(Observable.of({}));
|
||||
spyOn(contentApi, 'getDeletedNodes').and.returnValue(Observable.of({
|
||||
list: { entries: [] }
|
||||
}));
|
||||
|
||||
const path = {
|
||||
elements: [
|
||||
{
|
||||
id: '1-1',
|
||||
name: 'somewhere-over-the-rainbow'
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
component.selection = [
|
||||
{
|
||||
entry: {
|
||||
id: '1',
|
||||
path
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
fixture.detectChanges();
|
||||
element.triggerEventHandler('click', null);
|
||||
tick();
|
||||
|
||||
expect(contentApi.restoreNode).toHaveBeenCalled();
|
||||
}));
|
||||
|
||||
describe('refresh()', () => {
|
||||
it('dispatch event on finish', fakeAsync(done => {
|
||||
spyOn(contentApi, 'restoreNode').and.returnValue(Observable.of({}));
|
||||
spyOn(contentApi, 'getDeletedNodes').and.returnValue(Observable.of({
|
||||
list: { entries: [] }
|
||||
}));
|
||||
|
||||
const path = {
|
||||
elements: [
|
||||
{
|
||||
id: '1-1',
|
||||
name: 'somewhere-over-the-rainbow'
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
component.selection = [
|
||||
{
|
||||
entry: {
|
||||
id: '1',
|
||||
path
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
fixture.detectChanges();
|
||||
element.triggerEventHandler('click', null);
|
||||
tick();
|
||||
|
||||
contentManagementService.nodesRestored.subscribe(() => done());
|
||||
}));
|
||||
});
|
||||
|
||||
describe('notification', () => {
|
||||
beforeEach(() => {
|
||||
spyOn(contentApi, 'getDeletedNodes').and.returnValue(Observable.of({
|
||||
list: { entries: [] }
|
||||
}));
|
||||
});
|
||||
|
||||
it('should raise error message on partial multiple fail ', fakeAsync(done => {
|
||||
const error = { message: '{ "error": {} }' };
|
||||
|
||||
actions$.pipe(
|
||||
ofType<SnackbarErrorAction>(SNACKBAR_ERROR),
|
||||
map(action => done())
|
||||
);
|
||||
|
||||
spyOn(contentApi, 'restoreNode').and.callFake((id) => {
|
||||
if (id === '1') {
|
||||
return Observable.of({});
|
||||
}
|
||||
|
||||
if (id === '2') {
|
||||
return Observable.throw(error);
|
||||
}
|
||||
|
||||
if (id === '3') {
|
||||
return Observable.throw(error);
|
||||
}
|
||||
});
|
||||
|
||||
const path = {
|
||||
elements: [
|
||||
{
|
||||
id: '1-1',
|
||||
name: 'somewhere-over-the-rainbow'
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
component.selection = [
|
||||
{ entry: { id: '1', name: 'name1', path } },
|
||||
{ entry: { id: '2', name: 'name2', path } },
|
||||
{ entry: { id: '3', name: 'name3', path } }
|
||||
];
|
||||
|
||||
fixture.detectChanges();
|
||||
element.triggerEventHandler('click', null);
|
||||
tick();
|
||||
}));
|
||||
|
||||
it('should raise error message when restored node exist, error 409', fakeAsync(done => {
|
||||
const error = { message: '{ "error": { "statusCode": 409 } }' };
|
||||
spyOn(contentApi, 'restoreNode').and.returnValue(Observable.throw(error));
|
||||
|
||||
actions$.pipe(
|
||||
ofType<SnackbarErrorAction>(SNACKBAR_ERROR),
|
||||
map(action => done())
|
||||
);
|
||||
|
||||
const path = {
|
||||
elements: [
|
||||
{
|
||||
id: '1-1',
|
||||
name: 'somewhere-over-the-rainbow'
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
component.selection = [
|
||||
{ entry: { id: '1', name: 'name1', path } }
|
||||
];
|
||||
|
||||
fixture.detectChanges();
|
||||
element.triggerEventHandler('click', null);
|
||||
tick();
|
||||
}));
|
||||
|
||||
it('should raise error message when restored node returns different statusCode', fakeAsync(done => {
|
||||
const error = { message: '{ "error": { "statusCode": 404 } }' };
|
||||
|
||||
spyOn(contentApi, 'restoreNode').and.returnValue(Observable.throw(error));
|
||||
|
||||
actions$.pipe(
|
||||
ofType<SnackbarErrorAction>(SNACKBAR_ERROR),
|
||||
map(action => done())
|
||||
);
|
||||
|
||||
const path = {
|
||||
elements: [
|
||||
{
|
||||
id: '1-1',
|
||||
name: 'somewhere-over-the-rainbow'
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
component.selection = [
|
||||
{ entry: { id: '1', name: 'name1', path } }
|
||||
];
|
||||
|
||||
fixture.detectChanges();
|
||||
element.triggerEventHandler('click', null);
|
||||
tick();
|
||||
}));
|
||||
|
||||
it('should raise error message when restored node location is missing', fakeAsync(done => {
|
||||
const error = { message: '{ "error": { } }' };
|
||||
|
||||
spyOn(contentApi, 'restoreNode').and.returnValue(Observable.throw(error));
|
||||
|
||||
actions$.pipe(
|
||||
ofType<SnackbarErrorAction>(SNACKBAR_ERROR),
|
||||
map(action => done())
|
||||
);
|
||||
|
||||
const path = {
|
||||
elements: [
|
||||
{
|
||||
id: '1-1',
|
||||
name: 'somewhere-over-the-rainbow'
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
component.selection = [
|
||||
{ entry: { id: '1', name: 'name1', path } }
|
||||
];
|
||||
|
||||
fixture.detectChanges();
|
||||
element.triggerEventHandler('click', null);
|
||||
tick();
|
||||
}));
|
||||
|
||||
it('should raise info message when restore multiple nodes', fakeAsync(done => {
|
||||
spyOn(contentApi, 'restoreNode').and.callFake((id) => {
|
||||
if (id === '1') {
|
||||
return Observable.of({});
|
||||
}
|
||||
|
||||
if (id === '2') {
|
||||
return Observable.of({});
|
||||
}
|
||||
});
|
||||
|
||||
actions$.pipe(
|
||||
ofType<SnackbarInfoAction>(SNACKBAR_INFO),
|
||||
map(action => done())
|
||||
);
|
||||
|
||||
const path = {
|
||||
elements: [
|
||||
{
|
||||
id: '1-1',
|
||||
name: 'somewhere-over-the-rainbow'
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
component.selection = [
|
||||
{ entry: { id: '1', name: 'name1', path } },
|
||||
{ entry: { id: '2', name: 'name2', path } }
|
||||
];
|
||||
|
||||
fixture.detectChanges();
|
||||
element.triggerEventHandler('click', null);
|
||||
tick();
|
||||
}));
|
||||
|
||||
xit('should raise info message when restore selected node', fakeAsync(done => {
|
||||
spyOn(contentApi, 'restoreNode').and.returnValue(Observable.of({}));
|
||||
|
||||
actions$.pipe(
|
||||
ofType<SnackbarInfoAction>(SNACKBAR_INFO),
|
||||
map(action => done())
|
||||
);
|
||||
|
||||
const path = {
|
||||
elements: [
|
||||
{
|
||||
id: '1-1',
|
||||
name: 'somewhere-over-the-rainbow'
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
component.selection = [
|
||||
{ entry: { id: '1', name: 'name1', path } }
|
||||
];
|
||||
|
||||
fixture.detectChanges();
|
||||
element.triggerEventHandler('click', null);
|
||||
tick();
|
||||
}));
|
||||
|
||||
it('navigate to restore selected node location onAction', fakeAsync(done => {
|
||||
spyOn(contentApi, 'restoreNode').and.returnValue(Observable.of({}));
|
||||
|
||||
actions$.pipe(
|
||||
ofType<NavigateRouteAction>(NAVIGATE_ROUTE),
|
||||
map(action => done())
|
||||
);
|
||||
|
||||
const path = {
|
||||
elements: [
|
||||
{
|
||||
id: '1-1',
|
||||
name: 'somewhere-over-the-rainbow'
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
component.selection = [
|
||||
{
|
||||
entry: {
|
||||
id: '1',
|
||||
name: 'name1',
|
||||
path
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
fixture.detectChanges();
|
||||
element.triggerEventHandler('click', null);
|
||||
tick();
|
||||
}));
|
||||
});
|
||||
});
|
@ -1,57 +0,0 @@
|
||||
/*!
|
||||
* @license
|
||||
* Alfresco Example Content Application
|
||||
*
|
||||
* Copyright (C) 2005 - 2018 Alfresco Software Limited
|
||||
*
|
||||
* This file is part of the Alfresco Example Content Application.
|
||||
* If the software was purchased under a paid Alfresco license, the terms of
|
||||
* the paid license agreement will prevail. Otherwise, the software is
|
||||
* provided under the following open source license terms:
|
||||
*
|
||||
* The Alfresco Example Content Application is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* The Alfresco Example Content Application is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Directive, HostListener, Input } from '@angular/core';
|
||||
import { MinimalNodeEntity } from 'alfresco-js-api';
|
||||
import { ContentManagementService } from '../services/content-management.service';
|
||||
import { ContentApiService } from '../services/content-api.service';
|
||||
|
||||
@Directive({
|
||||
selector: '[acaUnshareNode]'
|
||||
})
|
||||
export class NodeUnshareDirective {
|
||||
|
||||
// tslint:disable-next-line:no-input-rename
|
||||
@Input('acaUnshareNode')
|
||||
selection: MinimalNodeEntity[];
|
||||
|
||||
constructor(
|
||||
private contentApi: ContentApiService,
|
||||
private contentManagement: ContentManagementService) {
|
||||
}
|
||||
|
||||
@HostListener('click')
|
||||
onClick() {
|
||||
if (this.selection.length > 0) {
|
||||
this.unshareLinks(this.selection);
|
||||
}
|
||||
}
|
||||
|
||||
private async unshareLinks(links: MinimalNodeEntity[]) {
|
||||
const promises = links.map(link => this.contentApi.deleteSharedLink(link.entry.id).toPromise());
|
||||
await Promise.all(promises);
|
||||
this.contentManagement.linksUnshared.next();
|
||||
}
|
||||
}
|
@ -1,84 +0,0 @@
|
||||
/*!
|
||||
* @license
|
||||
* Alfresco Example Content Application
|
||||
*
|
||||
* Copyright (C) 2005 - 2018 Alfresco Software Limited
|
||||
*
|
||||
* This file is part of the Alfresco Example Content Application.
|
||||
* If the software was purchased under a paid Alfresco license, the terms of
|
||||
* the paid license agreement will prevail. Otherwise, the software is
|
||||
* provided under the following open source license terms:
|
||||
*
|
||||
* The Alfresco Example Content Application is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* The Alfresco Example Content Application is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Directive, HostListener, Input } from '@angular/core';
|
||||
import { MinimalNodeEntity, MinimalNodeEntryEntity } from 'alfresco-js-api';
|
||||
import { NodeVersionsDialogComponent } from '../dialogs/node-versions/node-versions.dialog';
|
||||
import { MatDialog } from '@angular/material';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { AppStore } from '../store/states/app.state';
|
||||
import { SnackbarErrorAction } from '../store/actions';
|
||||
import { ContentApiService } from '../services/content-api.service';
|
||||
|
||||
@Directive({
|
||||
selector: '[acaNodeVersions]'
|
||||
})
|
||||
export class NodeVersionsDirective {
|
||||
// tslint:disable-next-line:no-input-rename
|
||||
@Input('acaNodeVersions') node: MinimalNodeEntity;
|
||||
|
||||
@HostListener('click')
|
||||
onClick() {
|
||||
this.onManageVersions();
|
||||
}
|
||||
|
||||
constructor(
|
||||
private store: Store<AppStore>,
|
||||
private contentApi: ContentApiService,
|
||||
private dialog: MatDialog
|
||||
) {}
|
||||
|
||||
async onManageVersions() {
|
||||
if (this.node && this.node.entry) {
|
||||
let entry = this.node.entry;
|
||||
|
||||
if (entry.nodeId || (<any>entry).guid) {
|
||||
entry = await this.contentApi.getNodeInfo(
|
||||
entry.nodeId || (<any>entry).id
|
||||
).toPromise();
|
||||
this.openVersionManagerDialog(entry);
|
||||
} else {
|
||||
this.openVersionManagerDialog(entry);
|
||||
}
|
||||
} else if (this.node) {
|
||||
this.openVersionManagerDialog(<MinimalNodeEntryEntity>this.node);
|
||||
}
|
||||
}
|
||||
|
||||
openVersionManagerDialog(node: MinimalNodeEntryEntity) {
|
||||
// workaround Shared
|
||||
if (node.isFile || node.nodeId) {
|
||||
this.dialog.open(NodeVersionsDialogComponent, {
|
||||
data: { node },
|
||||
panelClass: 'adf-version-manager-dialog-panel',
|
||||
width: '630px'
|
||||
});
|
||||
} else {
|
||||
this.store.dispatch(
|
||||
new SnackbarErrorAction('APP.MESSAGES.ERRORS.PERMISSION')
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
@ -24,7 +24,7 @@
|
||||
*/
|
||||
|
||||
export enum ContentActionType {
|
||||
default = 'button',
|
||||
default = 'default',
|
||||
button = 'button',
|
||||
separator = 'separator',
|
||||
menu = 'menu',
|
||||
@ -36,6 +36,7 @@ export interface ContentActionRef {
|
||||
type: ContentActionType;
|
||||
|
||||
title?: string;
|
||||
description?: string;
|
||||
order?: number;
|
||||
icon?: string;
|
||||
disabled?: boolean;
|
||||
|
@ -1,37 +0,0 @@
|
||||
<ng-container [ngSwitch]="entry.type">
|
||||
<button *ngSwitchCase="'button'"
|
||||
mat-icon-button
|
||||
color="primary"
|
||||
title="{{ entry.title | translate }}"
|
||||
(click)="runAction(entry.actions.click)">
|
||||
<mat-icon>{{ entry.icon }}</mat-icon>
|
||||
</button>
|
||||
|
||||
<adf-toolbar-divider *ngSwitchCase="'separator'"></adf-toolbar-divider>
|
||||
|
||||
<ng-container *ngSwitchCase="'menu'">
|
||||
<button
|
||||
color="primary"
|
||||
mat-icon-button
|
||||
title="{{ entry.title | translate }}"
|
||||
[matMenuTriggerFor]="menu">
|
||||
<mat-icon>{{ entry.icon }}</mat-icon>
|
||||
</button>
|
||||
|
||||
<mat-menu #menu="matMenu"
|
||||
[overlapTrigger]="false">
|
||||
<ng-container *ngFor="let child of entry.children">
|
||||
<button mat-menu-item
|
||||
(click)="runAction(child.actions.click)">
|
||||
<mat-icon>{{ child.icon }}</mat-icon>
|
||||
<span>{{ child.title | translate }}</span>
|
||||
</button>
|
||||
</ng-container>
|
||||
</mat-menu>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngSwitchCase="'custom'">
|
||||
<app-custom-component [id]="entry.component"></app-custom-component>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
@ -0,0 +1,39 @@
|
||||
<ng-container [ngSwitch]="entry.type">
|
||||
<ng-container *ngSwitchCase="'button'">
|
||||
<app-toolbar-button [type]="type" [actionRef]="entry"></app-toolbar-button>
|
||||
</ng-container>
|
||||
|
||||
<adf-toolbar-divider *ngSwitchCase="'separator'"
|
||||
[id]="entry.id">
|
||||
</adf-toolbar-divider>
|
||||
|
||||
<ng-container *ngSwitchCase="'menu'">
|
||||
<button
|
||||
[id]="entry.id"
|
||||
color="primary"
|
||||
mat-icon-button
|
||||
[attr.title]="(entry.description || entry.title) | translate"
|
||||
[matMenuTriggerFor]="menu">
|
||||
<mat-icon>{{ entry.icon }}</mat-icon>
|
||||
</button>
|
||||
|
||||
<mat-menu #menu="matMenu"
|
||||
[overlapTrigger]="false">
|
||||
<ng-container *ngFor="let child of entry.children; trackBy: trackByActionId">
|
||||
<ng-container [ngSwitch]="child.type">
|
||||
<ng-container *ngSwitchCase="'custom'">
|
||||
<app-custom-component [id]="child.component"></app-custom-component>
|
||||
</ng-container>
|
||||
<ng-container *ngSwitchDefault>
|
||||
<app-toolbar-button type="menu-item" [actionRef]="child"></app-toolbar-button>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</mat-menu>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngSwitchCase="'custom'">
|
||||
<app-custom-component [id]="entry.component"></app-custom-component>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
@ -27,16 +27,11 @@ import {
|
||||
Component,
|
||||
ViewEncapsulation,
|
||||
ChangeDetectionStrategy,
|
||||
Input,
|
||||
OnInit,
|
||||
OnDestroy
|
||||
Input
|
||||
} from '@angular/core';
|
||||
import { AppStore, SelectionState } from '../../../store/states';
|
||||
import { AppStore } from '../../../store/states';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { ExtensionService } from '../../extension.service';
|
||||
import { appSelection } from '../../../store/selectors/app.selectors';
|
||||
import { Subject } from 'rxjs/Rx';
|
||||
import { takeUntil } from 'rxjs/operators';
|
||||
import { ContentActionRef } from '../../action.extensions';
|
||||
|
||||
@Component({
|
||||
@ -46,36 +41,16 @@ import { ContentActionRef } from '../../action.extensions';
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
host: { class: 'aca-toolbar-action' }
|
||||
})
|
||||
export class ToolbarActionComponent implements OnInit, OnDestroy {
|
||||
export class ToolbarActionComponent {
|
||||
@Input() type = 'icon-button';
|
||||
@Input() entry: ContentActionRef;
|
||||
|
||||
selection: SelectionState;
|
||||
onDestroy$: Subject<boolean> = new Subject<boolean>();
|
||||
|
||||
constructor(
|
||||
protected store: Store<AppStore>,
|
||||
protected extensions: ExtensionService
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.store
|
||||
.select(appSelection)
|
||||
.pipe(takeUntil(this.onDestroy$))
|
||||
.subscribe(selection => {
|
||||
this.selection = selection;
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.onDestroy$.next(true);
|
||||
this.onDestroy$.complete();
|
||||
}
|
||||
|
||||
runAction(actionId: string) {
|
||||
const context = {
|
||||
selection: this.selection
|
||||
};
|
||||
|
||||
this.extensions.runActionById(actionId, context);
|
||||
trackByActionId(index: number, action: ContentActionRef) {
|
||||
return action.id;
|
||||
}
|
||||
}
|
@ -0,0 +1,90 @@
|
||||
/*!
|
||||
* @license
|
||||
* Alfresco Example Content Application
|
||||
*
|
||||
* Copyright (C) 2005 - 2018 Alfresco Software Limited
|
||||
*
|
||||
* This file is part of the Alfresco Example Content Application.
|
||||
* If the software was purchased under a paid Alfresco license, the terms of
|
||||
* the paid license agreement will prevail. Otherwise, the software is
|
||||
* provided under the following open source license terms:
|
||||
*
|
||||
* The Alfresco Example Content Application is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* The Alfresco Example Content Application is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Component, Input } from '@angular/core';
|
||||
import { ContentActionRef } from '../../action.extensions';
|
||||
import { ExtensionService } from '../../extension.service';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { AppStore } from '../../../store/states';
|
||||
import { appSelection } from '../../../store/selectors/app.selectors';
|
||||
|
||||
export enum ToolbarButtonType {
|
||||
ICON_BUTTON = 'icon-button',
|
||||
MENU_ITEM = 'menu-item'
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-toolbar-button',
|
||||
template: `
|
||||
<ng-container [ngSwitch]="type">
|
||||
<ng-container *ngSwitchCase="'icon-button'">
|
||||
<button
|
||||
[id]="actionRef.id"
|
||||
mat-icon-button
|
||||
color="primary"
|
||||
[attr.title]="(actionRef.description || actionRef.title) | translate"
|
||||
(click)="runAction()">
|
||||
<mat-icon>{{ actionRef.icon }}</mat-icon>
|
||||
</button>
|
||||
</ng-container>
|
||||
<ng-container *ngSwitchCase="'menu-item'">
|
||||
<button
|
||||
[id]="actionRef.id"
|
||||
mat-menu-item
|
||||
color="primary"
|
||||
[disabled]="actionRef.disabled"
|
||||
[attr.title]="(
|
||||
actionRef.disabled
|
||||
? actionRef['description-disabled']
|
||||
: actionRef.description || actionRef.title
|
||||
) | translate"
|
||||
(click)="runAction()">
|
||||
<mat-icon>{{ actionRef.icon }}</mat-icon>
|
||||
<span>{{ actionRef.title | translate }}</span>
|
||||
</button>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
`
|
||||
})
|
||||
export class ToolbarButtonComponent {
|
||||
@Input() type: ToolbarButtonType = ToolbarButtonType.ICON_BUTTON;
|
||||
@Input() actionRef: ContentActionRef;
|
||||
|
||||
constructor(
|
||||
protected store: Store<AppStore>,
|
||||
private extensions: ExtensionService
|
||||
) {}
|
||||
|
||||
runAction() {
|
||||
this.store
|
||||
.select(appSelection)
|
||||
.take(1)
|
||||
.subscribe(selection => {
|
||||
this.extensions.runActionById(this.actionRef.actions.click, {
|
||||
selection
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
@ -28,11 +28,14 @@ import { CommonModule } from '@angular/common';
|
||||
import { APP_INITIALIZER, ModuleWithProviders, NgModule } from '@angular/core';
|
||||
import { LayoutComponent } from '../components/layout/layout.component';
|
||||
import { TrashcanComponent } from '../components/trashcan/trashcan.component';
|
||||
import { ToolbarActionComponent } from './components/toolbar-action/toolbar-action.component';
|
||||
import { ToolbarActionComponent } from './components/toolbar/toolbar-action.component';
|
||||
import * as app from './evaluators/app.evaluators';
|
||||
import * as nav from './evaluators/navigation.evaluators';
|
||||
import { ExtensionService } from './extension.service';
|
||||
import { CustomExtensionComponent } from './components/custom-component/custom.component';
|
||||
import { DemoButtonComponent } from './components/custom-component/demo.button';
|
||||
import { ToggleInfoDrawerComponent } from '../components/toolbar/toggle-info-drawer/toggle-info-drawer.component';
|
||||
import { ToggleFavoriteComponent } from '../components/toolbar/toggle-favorite/toggle-favorite.component';
|
||||
import { ToolbarButtonComponent } from './components/toolbar/toolbar-button.component';
|
||||
|
||||
export function setupExtensions(extensions: ExtensionService): Function {
|
||||
return () =>
|
||||
@ -40,7 +43,8 @@ export function setupExtensions(extensions: ExtensionService): Function {
|
||||
extensions.setComponents({
|
||||
'app.layout.main': LayoutComponent,
|
||||
'app.components.trashcan': TrashcanComponent,
|
||||
'app.demo.button': DemoButtonComponent
|
||||
'app.toolbar.toggleInfoDrawer': ToggleInfoDrawerComponent,
|
||||
'app.toolbar.toggleFavorite': ToggleFavoriteComponent
|
||||
});
|
||||
|
||||
extensions.setAuthGuards({
|
||||
@ -48,14 +52,33 @@ export function setupExtensions(extensions: ExtensionService): Function {
|
||||
});
|
||||
|
||||
extensions.setEvaluators({
|
||||
'app.selection.canDelete': app.canDeleteSelection,
|
||||
'app.selection.canDownload': app.canDownloadSelection,
|
||||
'app.selection.notEmpty': app.hasSelection,
|
||||
'app.selection.canUnshare': app.canUnshareNodes,
|
||||
'app.selection.canAddFavorite': app.canAddFavorite,
|
||||
'app.selection.canRemoveFavorite': app.canRemoveFavorite,
|
||||
'app.selection.first.canUpdate': app.canUpdateSelectedNode,
|
||||
'app.selection.file': app.hasFileSelected,
|
||||
'app.selection.file.canShare': app.canShareFile,
|
||||
'app.selection.library': app.hasLibrarySelected,
|
||||
'app.selection.folder': app.hasFolderSelected,
|
||||
'app.selection.folder.canUpdate': app.canUpdateSelectedFolder,
|
||||
|
||||
'app.navigation.folder.canCreate': app.canCreateFolder,
|
||||
'app.navigation.isTrashcan': app.isTrashcan,
|
||||
'app.navigation.isNotTrashcan': app.isNotTrashcan
|
||||
'app.navigation.folder.canUpload': app.canUpload,
|
||||
'app.navigation.isTrashcan': nav.isTrashcan,
|
||||
'app.navigation.isNotTrashcan': nav.isNotTrashcan,
|
||||
'app.navigation.isLibraries': nav.isLibraries,
|
||||
'app.navigation.isNotLibraries': nav.isNotLibraries,
|
||||
'app.navigation.isSharedFiles': nav.isSharedFiles,
|
||||
'app.navigation.isNotSharedFiles': nav.isNotSharedFiles,
|
||||
'app.navigation.isFavorites': nav.isFavorites,
|
||||
'app.navigation.isNotFavorites': nav.isNotFavorites,
|
||||
'app.navigation.isRecentFiles': nav.isRecentFiles,
|
||||
'app.navigation.isNotRecentFiles': nav.isNotRecentFiles,
|
||||
'app.navigation.isSearchResults': nav.isSearchResults,
|
||||
'app.navigation.isNotSearchResults': nav.isNotSearchResults
|
||||
});
|
||||
|
||||
resolve(true);
|
||||
@ -66,15 +89,13 @@ export function setupExtensions(extensions: ExtensionService): Function {
|
||||
imports: [CommonModule, CoreModule.forChild()],
|
||||
declarations: [
|
||||
ToolbarActionComponent,
|
||||
CustomExtensionComponent,
|
||||
DemoButtonComponent
|
||||
ToolbarButtonComponent,
|
||||
CustomExtensionComponent
|
||||
],
|
||||
exports: [
|
||||
ToolbarActionComponent,
|
||||
ToolbarButtonComponent,
|
||||
CustomExtensionComponent
|
||||
],
|
||||
entryComponents: [
|
||||
DemoButtonComponent
|
||||
]
|
||||
})
|
||||
export class CoreExtensionsModule {
|
||||
|
@ -23,63 +23,201 @@
|
||||
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Node } from 'alfresco-js-api';
|
||||
import { RuleContext, RuleParameter } from '../rule.extensions';
|
||||
import {
|
||||
isNotTrashcan,
|
||||
isNotSharedFiles,
|
||||
isNotLibraries,
|
||||
isFavorites,
|
||||
isLibraries,
|
||||
isTrashcan,
|
||||
isSharedFiles,
|
||||
isNotSearchResults
|
||||
} from './navigation.evaluators';
|
||||
|
||||
export function isTrashcan(context: RuleContext, ...args: RuleParameter[]): boolean {
|
||||
const { url } = context.navigation;
|
||||
return url && url.startsWith('/trashcan');
|
||||
}
|
||||
|
||||
export function isNotTrashcan(context: RuleContext, ...args: RuleParameter[]): boolean {
|
||||
return !isTrashcan(context, ...args);
|
||||
}
|
||||
|
||||
export function hasSelection(context: RuleContext, ...args: RuleParameter[]): boolean {
|
||||
const { selection } = context;
|
||||
return selection && !selection.isEmpty;
|
||||
}
|
||||
|
||||
export function canCreateFolder(context: RuleContext, ...args: RuleParameter[]): boolean {
|
||||
const folder = context.navigation.currentFolder;
|
||||
if (folder) {
|
||||
return nodeHasPermission(folder, 'create');
|
||||
export function canAddFavorite(
|
||||
context: RuleContext,
|
||||
...args: RuleParameter[]
|
||||
): boolean {
|
||||
if (!context.selection.isEmpty) {
|
||||
if (
|
||||
isFavorites(context, ...args) ||
|
||||
isLibraries(context, ...args) ||
|
||||
isTrashcan(context, ...args)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return context.selection.nodes.some(node => !node.entry.isFavorite);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function canDownloadSelection(context: RuleContext, ...args: RuleParameter[]): boolean {
|
||||
export function canRemoveFavorite(
|
||||
context: RuleContext,
|
||||
...args: RuleParameter[]
|
||||
): boolean {
|
||||
if (!context.selection.isEmpty && !isTrashcan(context, ...args)) {
|
||||
if (isFavorites(context, ...args)) {
|
||||
return true;
|
||||
}
|
||||
return context.selection.nodes.every(node => node.entry.isFavorite);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function canShareFile(
|
||||
context: RuleContext,
|
||||
...args: RuleParameter[]
|
||||
): boolean {
|
||||
if (
|
||||
isNotTrashcan(context, ...args) &&
|
||||
isNotSharedFiles(context, ...args) &&
|
||||
context.selection.file
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function canDeleteSelection(
|
||||
context: RuleContext,
|
||||
...args: RuleParameter[]
|
||||
): boolean {
|
||||
if (
|
||||
isNotTrashcan(context, ...args) &&
|
||||
isNotLibraries(context, ...args) &&
|
||||
isNotSearchResults(context, ...args) &&
|
||||
!context.selection.isEmpty
|
||||
) {
|
||||
// temp workaround for Search api
|
||||
if (isFavorites(context, ...args)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// workaround for Shared Files
|
||||
if (isSharedFiles(context, ...args)) {
|
||||
return context.permissions.check(
|
||||
context.selection.nodes,
|
||||
['delete'],
|
||||
{ target: 'allowableOperationsOnTarget' });
|
||||
}
|
||||
|
||||
return context.permissions.check(context.selection.nodes, ['delete']);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function canUnshareNodes(
|
||||
context: RuleContext,
|
||||
...args: RuleParameter[]
|
||||
): boolean {
|
||||
if (!context.selection.isEmpty) {
|
||||
return context.selection.nodes.every(node => {
|
||||
return node.entry && (node.entry.isFile || node.entry.isFolder || !!node.entry.nodeId);
|
||||
return context.permissions.check(context.selection.nodes, ['delete'], {
|
||||
target: 'allowableOperationsOnTarget'
|
||||
});
|
||||
}
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
export function hasFolderSelected(context: RuleContext, ...args: RuleParameter[]): boolean {
|
||||
export function hasSelection(
|
||||
context: RuleContext,
|
||||
...args: RuleParameter[]
|
||||
): boolean {
|
||||
return !context.selection.isEmpty;
|
||||
}
|
||||
|
||||
export function canCreateFolder(
|
||||
context: RuleContext,
|
||||
...args: RuleParameter[]
|
||||
): boolean {
|
||||
const { currentFolder } = context.navigation;
|
||||
if (currentFolder) {
|
||||
return context.permissions.check(currentFolder, ['create']);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function canUpload(
|
||||
context: RuleContext,
|
||||
...args: RuleParameter[]
|
||||
): boolean {
|
||||
const { currentFolder } = context.navigation;
|
||||
if (currentFolder) {
|
||||
return context.permissions.check(currentFolder, ['create']);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function canDownloadSelection(
|
||||
context: RuleContext,
|
||||
...args: RuleParameter[]
|
||||
): boolean {
|
||||
if (!context.selection.isEmpty) {
|
||||
return context.selection.nodes.every(node => {
|
||||
return (
|
||||
node.entry &&
|
||||
(node.entry.isFile ||
|
||||
node.entry.isFolder ||
|
||||
!!node.entry.nodeId)
|
||||
);
|
||||
});
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function hasFolderSelected(
|
||||
context: RuleContext,
|
||||
...args: RuleParameter[]
|
||||
): boolean {
|
||||
const folder = context.selection.folder;
|
||||
return folder ? true : false;
|
||||
}
|
||||
|
||||
export function hasFileSelected(context: RuleContext, ...args: RuleParameter[]): boolean {
|
||||
export function hasLibrarySelected(
|
||||
context: RuleContext,
|
||||
...args: RuleParameter[]
|
||||
): boolean {
|
||||
const library = context.selection.library;
|
||||
return library ? true : false;
|
||||
}
|
||||
|
||||
export function hasFileSelected(
|
||||
context: RuleContext,
|
||||
...args: RuleParameter[]
|
||||
): boolean {
|
||||
const file = context.selection.file;
|
||||
return file ? true : false;
|
||||
}
|
||||
|
||||
export function canUpdateSelectedFolder(context: RuleContext, ...args: RuleParameter[]): boolean {
|
||||
const folder = context.selection.folder;
|
||||
if (folder && folder.entry) {
|
||||
return nodeHasPermission(folder.entry, 'update');
|
||||
export function canUpdateSelectedNode(
|
||||
context: RuleContext,
|
||||
...args: RuleParameter[]
|
||||
): boolean {
|
||||
if (context.selection && !context.selection.isEmpty) {
|
||||
const node = context.selection.first;
|
||||
|
||||
if (node.entry.hasOwnProperty('allowableOperationsOnTarget')) {
|
||||
return context.permissions.check(node, ['update'], {
|
||||
target: 'allowableOperationsOnTarget'
|
||||
});
|
||||
}
|
||||
|
||||
return context.permissions.check(node, ['update']);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function nodeHasPermission(node: Node, permission: string): boolean {
|
||||
if (node && permission) {
|
||||
const allowableOperations = node.allowableOperations || [];
|
||||
return allowableOperations.includes(permission);
|
||||
export function canUpdateSelectedFolder(
|
||||
context: RuleContext,
|
||||
...args: RuleParameter[]
|
||||
): boolean {
|
||||
const { folder } = context.selection;
|
||||
if (folder) {
|
||||
return (
|
||||
// workaround for Search Api
|
||||
isFavorites(context, ...args) ||
|
||||
context.permissions.check(folder.entry, ['update'])
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
116
src/app/extensions/evaluators/navigation.evaluators.ts
Normal file
116
src/app/extensions/evaluators/navigation.evaluators.ts
Normal file
@ -0,0 +1,116 @@
|
||||
/*!
|
||||
* @license
|
||||
* Alfresco Example Content Application
|
||||
*
|
||||
* Copyright (C) 2005 - 2018 Alfresco Software Limited
|
||||
*
|
||||
* This file is part of the Alfresco Example Content Application.
|
||||
* If the software was purchased under a paid Alfresco license, the terms of
|
||||
* the paid license agreement will prevail. Otherwise, the software is
|
||||
* provided under the following open source license terms:
|
||||
*
|
||||
* The Alfresco Example Content Application is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* The Alfresco Example Content Application is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { RuleContext, RuleParameter } from '../rule.extensions';
|
||||
|
||||
export function isFavorites(
|
||||
context: RuleContext,
|
||||
...args: RuleParameter[]
|
||||
): boolean {
|
||||
const { url } = context.navigation;
|
||||
return url && url.startsWith('/favorites');
|
||||
}
|
||||
|
||||
export function isNotFavorites(
|
||||
context: RuleContext,
|
||||
...args: RuleParameter[]
|
||||
): boolean {
|
||||
return !isFavorites(context, ...args);
|
||||
}
|
||||
|
||||
export function isSharedFiles(
|
||||
context: RuleContext,
|
||||
...args: RuleParameter[]
|
||||
): boolean {
|
||||
const { url } = context.navigation;
|
||||
return url && url.startsWith('/shared');
|
||||
}
|
||||
|
||||
export function isNotSharedFiles(
|
||||
context: RuleContext,
|
||||
...args: RuleParameter[]
|
||||
): boolean {
|
||||
return !isSharedFiles(context, ...args);
|
||||
}
|
||||
|
||||
export function isTrashcan(
|
||||
context: RuleContext,
|
||||
...args: RuleParameter[]
|
||||
): boolean {
|
||||
const { url } = context.navigation;
|
||||
return url && url.startsWith('/trashcan');
|
||||
}
|
||||
|
||||
export function isNotTrashcan(
|
||||
context: RuleContext,
|
||||
...args: RuleParameter[]
|
||||
): boolean {
|
||||
return !isTrashcan(context, ...args);
|
||||
}
|
||||
|
||||
export function isLibraries(
|
||||
context: RuleContext,
|
||||
...args: RuleParameter[]
|
||||
): boolean {
|
||||
const { url } = context.navigation;
|
||||
return url && url.endsWith('/libraries');
|
||||
}
|
||||
|
||||
export function isNotLibraries(
|
||||
context: RuleContext,
|
||||
...args: RuleParameter[]
|
||||
): boolean {
|
||||
return !isLibraries(context, ...args);
|
||||
}
|
||||
|
||||
export function isRecentFiles(
|
||||
context: RuleContext,
|
||||
...args: RuleParameter[]
|
||||
): boolean {
|
||||
const { url } = context.navigation;
|
||||
return url && url.startsWith('/recent-files');
|
||||
}
|
||||
|
||||
export function isNotRecentFiles(
|
||||
context: RuleContext,
|
||||
...args: RuleParameter[]
|
||||
): boolean {
|
||||
return !isRecentFiles(context, ...args);
|
||||
}
|
||||
|
||||
export function isSearchResults(
|
||||
context: RuleContext,
|
||||
...args: RuleParameter[]
|
||||
): boolean {
|
||||
const { url } = context.navigation;
|
||||
return url && url.startsWith('/search');
|
||||
}
|
||||
|
||||
export function isNotSearchResults(
|
||||
context: RuleContext,
|
||||
...args: RuleParameter[]
|
||||
): boolean {
|
||||
return !isSearchResults(context, ...args);
|
||||
}
|
@ -41,6 +41,7 @@ export interface ExtensionConfig {
|
||||
create?: Array<ContentActionRef>;
|
||||
viewer?: {
|
||||
openWith?: Array<ContentActionRef>;
|
||||
actions?: Array<ContentActionRef>;
|
||||
};
|
||||
navbar?: Array<NavBarGroupRef>;
|
||||
content?: {
|
||||
|
@ -36,6 +36,7 @@ import { RouteRef } from './routing.extensions';
|
||||
import { RuleContext, RuleRef, RuleEvaluator } from './rule.extensions';
|
||||
import { ActionRef, ContentActionRef, ContentActionType } from './action.extensions';
|
||||
import * as core from './evaluators/core.evaluators';
|
||||
import { NodePermissionService } from '../services/node-permission.service';
|
||||
|
||||
@Injectable()
|
||||
export class ExtensionService implements RuleContext {
|
||||
@ -52,6 +53,7 @@ export class ExtensionService implements RuleContext {
|
||||
actions: Array<ActionRef> = [];
|
||||
|
||||
contentActions: Array<ContentActionRef> = [];
|
||||
viewerActions: Array<ContentActionRef> = [];
|
||||
openWithActions: Array<ContentActionRef> = [];
|
||||
createActions: Array<ContentActionRef> = [];
|
||||
navbar: Array<NavBarGroupRef> = [];
|
||||
@ -63,7 +65,10 @@ export class ExtensionService implements RuleContext {
|
||||
selection: SelectionState;
|
||||
navigation: NavigationState;
|
||||
|
||||
constructor(private http: HttpClient, private store: Store<AppStore>) {
|
||||
constructor(
|
||||
private http: HttpClient,
|
||||
private store: Store<AppStore>,
|
||||
public permissions: NodePermissionService) {
|
||||
|
||||
this.evaluators = {
|
||||
'core.every': core.every,
|
||||
@ -118,6 +123,7 @@ export class ExtensionService implements RuleContext {
|
||||
this.actions = this.loadActions(config);
|
||||
this.routes = this.loadRoutes(config);
|
||||
this.contentActions = this.loadContentActions(config);
|
||||
this.viewerActions = this.loadViewerActions(config);
|
||||
this.openWithActions = this.loadViewerOpenWith(config);
|
||||
this.createActions = this.loadCreateActions(config);
|
||||
this.navbar = this.loadNavBar(config);
|
||||
@ -158,6 +164,15 @@ export class ExtensionService implements RuleContext {
|
||||
return [];
|
||||
}
|
||||
|
||||
protected loadViewerActions(config: ExtensionConfig) {
|
||||
if (config && config.features && config.features.viewer) {
|
||||
return (config.features.viewer.actions || []).sort(
|
||||
this.sortByOrder
|
||||
);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
protected loadNavBar(config: ExtensionConfig): any {
|
||||
if (config && config.features) {
|
||||
return (config.features.navbar || [])
|
||||
@ -296,7 +311,6 @@ export class ExtensionService implements RuleContext {
|
||||
return this.contentActions
|
||||
.filter(this.filterEnabled)
|
||||
.filter(action => this.filterByRules(action))
|
||||
.reduce(this.reduceSeparators, [])
|
||||
.map(action => {
|
||||
if (action.type === ContentActionType.menu) {
|
||||
const copy = this.copyAction(action);
|
||||
@ -311,7 +325,14 @@ export class ExtensionService implements RuleContext {
|
||||
}
|
||||
return action;
|
||||
})
|
||||
.reduce(this.reduceEmptyMenus, []);
|
||||
.reduce(this.reduceEmptyMenus, [])
|
||||
.reduce(this.reduceSeparators, []);
|
||||
}
|
||||
|
||||
getViewerActions(): Array<ContentActionRef> {
|
||||
return this.viewerActions
|
||||
.filter(this.filterEnabled)
|
||||
.filter(action => this.filterByRules(action));
|
||||
}
|
||||
|
||||
reduceSeparators(
|
||||
@ -320,6 +341,12 @@ export class ExtensionService implements RuleContext {
|
||||
i: number,
|
||||
arr: ContentActionRef[]
|
||||
): ContentActionRef[] {
|
||||
// remove leading separator
|
||||
if (i === 0) {
|
||||
if (arr[i].type === ContentActionType.separator) {
|
||||
return acc;
|
||||
}
|
||||
}
|
||||
// remove duplicate separators
|
||||
if (i > 0) {
|
||||
const prev = arr[i - 1];
|
||||
|
@ -23,14 +23,6 @@
|
||||
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-demo-button',
|
||||
template: `
|
||||
<button color="primary" mat-icon-button>
|
||||
<mat-icon>extension</mat-icon>
|
||||
</button>
|
||||
`
|
||||
})
|
||||
export class DemoButtonComponent {}
|
||||
export interface NodePermissions {
|
||||
check(source: any, permissions: string[], options?: any): boolean;
|
||||
}
|
@ -25,6 +25,7 @@
|
||||
|
||||
import { SelectionState } from '../store/states';
|
||||
import { NavigationState } from '../store/states/navigation.state';
|
||||
import { NodePermissions } from './permission.extensions';
|
||||
|
||||
export type RuleEvaluator = (context: RuleContext, ...args: any[]) => boolean;
|
||||
|
||||
@ -32,6 +33,7 @@ export interface RuleContext {
|
||||
selection: SelectionState;
|
||||
navigation: NavigationState;
|
||||
evaluators: { [key: string]: RuleEvaluator };
|
||||
permissions: NodePermissions;
|
||||
}
|
||||
|
||||
export class RuleRef {
|
||||
|
@ -39,7 +39,8 @@ import {
|
||||
SearchRequest,
|
||||
ResultSetPaging,
|
||||
SiteBody,
|
||||
SiteEntry
|
||||
SiteEntry,
|
||||
FavoriteBody
|
||||
} from 'alfresco-js-api';
|
||||
|
||||
@Injectable()
|
||||
@ -242,4 +243,30 @@ export class ContentApiService {
|
||||
this.api.sitesApi.getSite(siteId, opts)
|
||||
);
|
||||
}
|
||||
|
||||
addFavorite(nodes: Array<MinimalNodeEntity>): Observable<any> {
|
||||
const payload: FavoriteBody[] = nodes.map(node => {
|
||||
const { isFolder, nodeId, id } = node.entry;
|
||||
const siteId = node.entry['guid'];
|
||||
const type = siteId ? 'site' : isFolder ? 'folder' : 'file';
|
||||
const guid = siteId || nodeId || id;
|
||||
|
||||
return {
|
||||
target: {
|
||||
[type]: {
|
||||
guid
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
return Observable.from(this.api.favoritesApi.addFavorite('-me-', <any>payload));
|
||||
}
|
||||
|
||||
removeFavorite(nodes: Array<MinimalNodeEntity>): Observable<any> {
|
||||
return Observable.from(Promise.all(nodes.map(node => {
|
||||
const id = node.entry.nodeId || node.entry.id;
|
||||
return this.api.favoritesApi.removeFavoriteSite('-me-', id);
|
||||
})));
|
||||
}
|
||||
}
|
||||
|
1275
src/app/services/content-management.service.spec.ts
Normal file
1275
src/app/services/content-management.service.spec.ts
Normal file
File diff suppressed because it is too large
Load Diff
@ -25,11 +25,11 @@
|
||||
|
||||
import { Subject, Observable } from 'rxjs/Rx';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { MatDialog } from '@angular/material';
|
||||
import { FolderDialogComponent, ConfirmDialogComponent } from '@alfresco/adf-content-services';
|
||||
import { MatDialog, MatSnackBar } from '@angular/material';
|
||||
import { FolderDialogComponent, ConfirmDialogComponent, ShareDialogComponent } from '@alfresco/adf-content-services';
|
||||
import { LibraryDialogComponent } from '../dialogs/library/library.dialog';
|
||||
import { SnackbarErrorAction, SnackbarInfoAction, SnackbarAction, SnackbarWarningAction,
|
||||
NavigateRouteAction, SnackbarUserAction } from '../store/actions';
|
||||
NavigateRouteAction, SnackbarUserAction, UndoDeleteNodesAction, SetSelectedNodesAction } from '../store/actions';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { AppStore } from '../store/states';
|
||||
import {
|
||||
@ -43,6 +43,11 @@ import {
|
||||
import { NodePermissionService } from './node-permission.service';
|
||||
import { NodeInfo, DeletedNodeInfo, DeleteStatus } from '../store/models';
|
||||
import { ContentApiService } from './content-api.service';
|
||||
import { sharedUrl } from '../store/selectors/app.selectors';
|
||||
import { NodeActionsService } from './node-actions.service';
|
||||
import { TranslationService } from '@alfresco/adf-core';
|
||||
import { NodePermissionsDialogComponent } from '../dialogs/node-permissions/node-permissions.dialog';
|
||||
import { NodeVersionsDialogComponent } from '../dialogs/node-versions/node-versions.dialog';
|
||||
|
||||
interface RestoredNode {
|
||||
status: number;
|
||||
@ -61,14 +66,112 @@ export class ContentManagementService {
|
||||
libraryDeleted = new Subject<string>();
|
||||
libraryCreated = new Subject<SiteEntry>();
|
||||
linksUnshared = new Subject<any>();
|
||||
favoriteAdded = new Subject<Array<MinimalNodeEntity>>();
|
||||
favoriteRemoved = new Subject<Array<MinimalNodeEntity>>();
|
||||
|
||||
constructor(
|
||||
private store: Store<AppStore>,
|
||||
private contentApi: ContentApiService,
|
||||
private permission: NodePermissionService,
|
||||
private dialogRef: MatDialog
|
||||
private dialogRef: MatDialog,
|
||||
private nodeActionsService: NodeActionsService,
|
||||
private translation: TranslationService,
|
||||
private snackBar: MatSnackBar
|
||||
) {}
|
||||
|
||||
addFavorite(nodes: Array<MinimalNodeEntity>) {
|
||||
if (nodes && nodes.length > 0) {
|
||||
this.contentApi.addFavorite(nodes).subscribe(() => {
|
||||
nodes.forEach(node => {
|
||||
node.entry.isFavorite = true;
|
||||
});
|
||||
this.store.dispatch(new SetSelectedNodesAction(nodes));
|
||||
this.favoriteAdded.next(nodes);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
removeFavorite(nodes: Array<MinimalNodeEntity>) {
|
||||
if (nodes && nodes.length > 0) {
|
||||
this.contentApi.removeFavorite(nodes).subscribe(() => {
|
||||
nodes.forEach(node => {
|
||||
node.entry.isFavorite = false;
|
||||
});
|
||||
this.store.dispatch(new SetSelectedNodesAction(nodes));
|
||||
this.favoriteRemoved.next(nodes);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
managePermissions(node: MinimalNodeEntity): void {
|
||||
if (node && node.entry) {
|
||||
const { nodeId, id } = node.entry;
|
||||
const siteId = node.entry['guid'];
|
||||
const targetId = siteId || nodeId || id;
|
||||
|
||||
if (targetId) {
|
||||
this.dialogRef.open(NodePermissionsDialogComponent, {
|
||||
data: { nodeId: targetId },
|
||||
panelClass: 'aca-permissions-dialog-panel',
|
||||
width: '730px'
|
||||
});
|
||||
} else {
|
||||
this.store.dispatch(
|
||||
new SnackbarErrorAction('APP.MESSAGES.ERRORS.PERMISSION')
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
manageVersions(node: MinimalNodeEntity) {
|
||||
if (node && node.entry) {
|
||||
if (node.entry.nodeId) {
|
||||
this.contentApi
|
||||
.getNodeInfo(node.entry.nodeId)
|
||||
.subscribe(entry => {
|
||||
this.openVersionManagerDialog(entry);
|
||||
});
|
||||
|
||||
} else {
|
||||
this.openVersionManagerDialog(node.entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private openVersionManagerDialog(node: MinimalNodeEntryEntity) {
|
||||
// workaround Shared
|
||||
if (node.isFile || node.nodeId) {
|
||||
this.dialogRef.open(NodeVersionsDialogComponent, {
|
||||
data: { node },
|
||||
panelClass: 'adf-version-manager-dialog-panel',
|
||||
width: '630px'
|
||||
});
|
||||
} else {
|
||||
this.store.dispatch(
|
||||
new SnackbarErrorAction('APP.MESSAGES.ERRORS.PERMISSION')
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
shareNode(node: MinimalNodeEntity): void {
|
||||
if (node && node.entry && node.entry.isFile) {
|
||||
|
||||
this.store
|
||||
.select(sharedUrl)
|
||||
.take(1)
|
||||
.subscribe(baseShareUrl => {
|
||||
this.dialogRef.open(ShareDialogComponent, {
|
||||
width: '600px',
|
||||
disableClose: true,
|
||||
data: {
|
||||
node,
|
||||
baseShareUrl
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
createFolder(parentNodeId: string) {
|
||||
const dialogInstance = this.dialogRef.open(FolderDialogComponent, {
|
||||
data: {
|
||||
@ -114,7 +217,7 @@ export class ContentManagementService {
|
||||
}
|
||||
|
||||
createLibrary() {
|
||||
const dialogInstance = this.dialogRef.open(LibraryDialogComponent, {
|
||||
const dialogInstance = this.dialogRef.open(LibraryDialogComponent, {
|
||||
width: '400px'
|
||||
});
|
||||
|
||||
@ -129,12 +232,30 @@ export class ContentManagementService {
|
||||
});
|
||||
}
|
||||
|
||||
canDeleteNode(node: MinimalNodeEntity | Node): boolean {
|
||||
return this.permission.check(node, ['delete']);
|
||||
deleteLibrary(id: string): void {
|
||||
this.contentApi.deleteSite(id).subscribe(
|
||||
() => {
|
||||
this.libraryDeleted.next(id);
|
||||
this.store.dispatch(
|
||||
new SnackbarInfoAction(
|
||||
'APP.MESSAGES.INFO.LIBRARY_DELETED'
|
||||
)
|
||||
);
|
||||
},
|
||||
() => {
|
||||
this.store.dispatch(
|
||||
new SnackbarErrorAction(
|
||||
'APP.MESSAGES.ERRORS.DELETE_LIBRARY_FAILED'
|
||||
)
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
canDeleteNodes(nodes: MinimalNodeEntity[]): boolean {
|
||||
return this.permission.check(nodes, ['delete']);
|
||||
async unshareNodes(links: Array<MinimalNodeEntity>) {
|
||||
const promises = links.map(link => this.contentApi.deleteSharedLink(link.entry.id).toPromise());
|
||||
await Promise.all(promises);
|
||||
this.linksUnshared.next();
|
||||
}
|
||||
|
||||
canUpdateNode(node: MinimalNodeEntity | Node): boolean {
|
||||
@ -145,18 +266,6 @@ export class ContentManagementService {
|
||||
return this.permission.check(folderNode, ['create']);
|
||||
}
|
||||
|
||||
canDeleteSharedNodes(sharedLinks: MinimalNodeEntity[]): boolean {
|
||||
return this.permission.check(sharedLinks, ['delete'], {
|
||||
target: 'allowableOperationsOnTarget'
|
||||
});
|
||||
}
|
||||
|
||||
canUpdateSharedNode(sharedLink: MinimalNodeEntity): boolean {
|
||||
return this.permission.check(sharedLink, ['update'], {
|
||||
target: 'allowableOperationsOnTarget'
|
||||
});
|
||||
}
|
||||
|
||||
purgeDeletedNodes(nodes: MinimalNodeEntity[]) {
|
||||
if (!nodes || nodes.length === 0) {
|
||||
return;
|
||||
@ -226,6 +335,271 @@ export class ContentManagementService {
|
||||
});
|
||||
}
|
||||
|
||||
copyNodes(nodes: Array<MinimalNodeEntity>) {
|
||||
Observable.zip(
|
||||
this.nodeActionsService.copyNodes(nodes),
|
||||
this.nodeActionsService.contentCopied
|
||||
).subscribe(
|
||||
result => {
|
||||
const [operationResult, newItems] = result;
|
||||
this.showCopyMessage(operationResult, nodes, newItems);
|
||||
},
|
||||
error => {
|
||||
this.showCopyMessage(error, nodes);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private showCopyMessage(
|
||||
info: any,
|
||||
nodes: Array<MinimalNodeEntity>,
|
||||
newItems?: Array<MinimalNodeEntity>
|
||||
) {
|
||||
const numberOfCopiedItems = newItems ? newItems.length : 0;
|
||||
const failedItems = nodes.length - numberOfCopiedItems;
|
||||
|
||||
let i18nMessageString = 'APP.MESSAGES.ERRORS.GENERIC';
|
||||
|
||||
if (typeof info === 'string') {
|
||||
if (info.toLowerCase().indexOf('succes') !== -1) {
|
||||
let i18MessageSuffix;
|
||||
|
||||
if (failedItems) {
|
||||
if (numberOfCopiedItems) {
|
||||
i18MessageSuffix =
|
||||
numberOfCopiedItems === 1
|
||||
? 'PARTIAL_SINGULAR'
|
||||
: 'PARTIAL_PLURAL';
|
||||
} else {
|
||||
i18MessageSuffix =
|
||||
failedItems === 1 ? 'FAIL_SINGULAR' : 'FAIL_PLURAL';
|
||||
}
|
||||
} else {
|
||||
i18MessageSuffix =
|
||||
numberOfCopiedItems === 1 ? 'SINGULAR' : 'PLURAL';
|
||||
}
|
||||
|
||||
i18nMessageString = `APP.MESSAGES.INFO.NODE_COPY.${i18MessageSuffix}`;
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
const {
|
||||
error: { statusCode }
|
||||
} = JSON.parse(info.message);
|
||||
|
||||
if (statusCode === 403) {
|
||||
i18nMessageString = 'APP.MESSAGES.ERRORS.PERMISSION';
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
const undo =
|
||||
numberOfCopiedItems > 0
|
||||
? this.translation.instant('APP.ACTIONS.UNDO')
|
||||
: '';
|
||||
|
||||
const message = this.translation.instant(i18nMessageString, {
|
||||
success: numberOfCopiedItems,
|
||||
failed: failedItems
|
||||
});
|
||||
|
||||
this.snackBar
|
||||
.open(message, undo, {
|
||||
panelClass: 'info-snackbar',
|
||||
duration: 3000
|
||||
})
|
||||
.onAction()
|
||||
.subscribe(() => this.undoCopyNodes(newItems));
|
||||
}
|
||||
|
||||
private undoCopyNodes(nodes: MinimalNodeEntity[]) {
|
||||
const batch = this.nodeActionsService
|
||||
.flatten(nodes)
|
||||
.filter(item => item.entry)
|
||||
.map(item =>
|
||||
this.contentApi.deleteNode(item.entry.id, { permanent: true })
|
||||
);
|
||||
|
||||
Observable.forkJoin(...batch).subscribe(
|
||||
() => {
|
||||
this.nodesDeleted.next(null);
|
||||
},
|
||||
error => {
|
||||
let i18nMessageString = 'APP.MESSAGES.ERRORS.GENERIC';
|
||||
|
||||
let errorJson = null;
|
||||
try {
|
||||
errorJson = JSON.parse(error.message);
|
||||
} catch {}
|
||||
|
||||
if (
|
||||
errorJson &&
|
||||
errorJson.error &&
|
||||
errorJson.error.statusCode === 403
|
||||
) {
|
||||
i18nMessageString = 'APP.MESSAGES.ERRORS.PERMISSION';
|
||||
}
|
||||
|
||||
this.store.dispatch(new SnackbarErrorAction(i18nMessageString));
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
moveNodes(nodes: Array<MinimalNodeEntity>) {
|
||||
const permissionForMove = '!';
|
||||
|
||||
Observable.zip(
|
||||
this.nodeActionsService.moveNodes(nodes, permissionForMove),
|
||||
this.nodeActionsService.contentMoved
|
||||
).subscribe(
|
||||
(result) => {
|
||||
const [ operationResult, moveResponse ] = result;
|
||||
this.showMoveMessage(nodes, operationResult, moveResponse);
|
||||
|
||||
this.nodesMoved.next(null);
|
||||
},
|
||||
(error) => {
|
||||
this.showMoveMessage(nodes, error);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private undoMoveNodes(moveResponse, selectionParentId) {
|
||||
const movedNodes = (moveResponse && moveResponse['succeeded']) ? moveResponse['succeeded'] : [];
|
||||
const partiallyMovedNodes = (moveResponse && moveResponse['partiallySucceeded']) ? moveResponse['partiallySucceeded'] : [];
|
||||
|
||||
const restoreDeletedNodesBatch = this.nodeActionsService.moveDeletedEntries
|
||||
.map((folderEntry) => {
|
||||
return this.contentApi
|
||||
.restoreNode(folderEntry.nodeId || folderEntry.id)
|
||||
.map(node => node.entry);
|
||||
});
|
||||
|
||||
Observable.zip(...restoreDeletedNodesBatch, Observable.of(null))
|
||||
.flatMap(() => {
|
||||
|
||||
const nodesToBeMovedBack = [...partiallyMovedNodes, ...movedNodes];
|
||||
|
||||
const revertMoveBatch = this.nodeActionsService
|
||||
.flatten(nodesToBeMovedBack)
|
||||
.filter(node => node.entry || (node.itemMoved && node.itemMoved.entry))
|
||||
.map((node) => {
|
||||
if (node.itemMoved) {
|
||||
return this.nodeActionsService.moveNodeAction(node.itemMoved.entry, node.initialParentId);
|
||||
} else {
|
||||
return this.nodeActionsService.moveNodeAction(node.entry, selectionParentId);
|
||||
}
|
||||
});
|
||||
|
||||
return Observable.zip(...revertMoveBatch, Observable.of(null));
|
||||
})
|
||||
.subscribe(
|
||||
() => {
|
||||
this.nodesMoved.next(null);
|
||||
},
|
||||
error => {
|
||||
let message = 'APP.MESSAGES.ERRORS.GENERIC';
|
||||
|
||||
let errorJson = null;
|
||||
try {
|
||||
errorJson = JSON.parse(error.message);
|
||||
} catch {}
|
||||
|
||||
if (errorJson && errorJson.error && errorJson.error.statusCode === 403) {
|
||||
message = 'APP.MESSAGES.ERRORS.PERMISSION';
|
||||
}
|
||||
|
||||
this.store.dispatch(new SnackbarErrorAction(message));
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
deleteNodes(items: MinimalNodeEntity[]): void {
|
||||
const batch: Observable<DeletedNodeInfo>[] = [];
|
||||
|
||||
items.forEach(node => {
|
||||
batch.push(this.deleteNode(node));
|
||||
});
|
||||
|
||||
Observable.forkJoin(...batch).subscribe((data: DeletedNodeInfo[]) => {
|
||||
const status = this.processStatus(data);
|
||||
const message = this.getDeleteMessage(status);
|
||||
|
||||
if (message && status.someSucceeded) {
|
||||
message.duration = 10000;
|
||||
message.userAction = new SnackbarUserAction(
|
||||
'APP.ACTIONS.UNDO',
|
||||
new UndoDeleteNodesAction([...status.success])
|
||||
);
|
||||
}
|
||||
|
||||
this.store.dispatch(message);
|
||||
|
||||
if (status.someSucceeded) {
|
||||
this.nodesDeleted.next();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
undoDeleteNodes(items: DeletedNodeInfo[]): void {
|
||||
const batch: Observable<DeletedNodeInfo>[] = [];
|
||||
|
||||
items.forEach(item => {
|
||||
batch.push(this.undoDeleteNode(item));
|
||||
});
|
||||
|
||||
Observable.forkJoin(...batch).subscribe(data => {
|
||||
const processedData = this.processStatus(data);
|
||||
|
||||
if (processedData.fail.length) {
|
||||
const message = this.getUndoDeleteMessage(processedData);
|
||||
this.store.dispatch(message);
|
||||
}
|
||||
|
||||
if (processedData.someSucceeded) {
|
||||
this.nodesRestored.next();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private undoDeleteNode(item: DeletedNodeInfo): Observable<DeletedNodeInfo> {
|
||||
const { id, name } = item;
|
||||
|
||||
return this.contentApi
|
||||
.restoreNode(id)
|
||||
.map(() => {
|
||||
return {
|
||||
id,
|
||||
name,
|
||||
status: 1
|
||||
};
|
||||
})
|
||||
.catch((error: any) => {
|
||||
return Observable.of({
|
||||
id,
|
||||
name,
|
||||
status: 0
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private getUndoDeleteMessage(status: DeleteStatus): SnackbarAction {
|
||||
if (status.someFailed && !status.oneFailed) {
|
||||
return new SnackbarErrorAction(
|
||||
'APP.MESSAGES.ERRORS.NODE_RESTORE_PLURAL',
|
||||
{ number: status.fail.length }
|
||||
);
|
||||
}
|
||||
|
||||
if (status.oneFailed) {
|
||||
return new SnackbarErrorAction('APP.MESSAGES.ERRORS.NODE_RESTORE', {
|
||||
name: status.fail[0].name
|
||||
});
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private restoreNode(node: MinimalNodeEntity): Observable<RestoredNode> {
|
||||
const { entry } = node;
|
||||
|
||||
@ -457,4 +831,170 @@ export class ContentManagementService {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private deleteNode(node: MinimalNodeEntity): Observable<DeletedNodeInfo> {
|
||||
const { name } = node.entry;
|
||||
const id = node.entry.nodeId || node.entry.id;
|
||||
|
||||
|
||||
return this.contentApi
|
||||
.deleteNode(id)
|
||||
.map(() => {
|
||||
return {
|
||||
id,
|
||||
name,
|
||||
status: 1
|
||||
};
|
||||
})
|
||||
.catch(() => {
|
||||
return Observable.of({
|
||||
id,
|
||||
name,
|
||||
status: 0
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private getDeleteMessage(status: DeleteStatus): SnackbarAction {
|
||||
if (status.allFailed && !status.oneFailed) {
|
||||
return new SnackbarErrorAction(
|
||||
'APP.MESSAGES.ERRORS.NODE_DELETION_PLURAL',
|
||||
{ number: status.fail.length }
|
||||
);
|
||||
}
|
||||
|
||||
if (status.allSucceeded && !status.oneSucceeded) {
|
||||
return new SnackbarInfoAction(
|
||||
'APP.MESSAGES.INFO.NODE_DELETION.PLURAL',
|
||||
{ number: status.success.length }
|
||||
);
|
||||
}
|
||||
|
||||
if (status.someFailed && status.someSucceeded && !status.oneSucceeded) {
|
||||
return new SnackbarWarningAction(
|
||||
'APP.MESSAGES.INFO.NODE_DELETION.PARTIAL_PLURAL',
|
||||
{
|
||||
success: status.success.length,
|
||||
failed: status.fail.length
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if (status.someFailed && status.oneSucceeded) {
|
||||
return new SnackbarWarningAction(
|
||||
'APP.MESSAGES.INFO.NODE_DELETION.PARTIAL_SINGULAR',
|
||||
{
|
||||
success: status.success.length,
|
||||
failed: status.fail.length
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if (status.oneFailed && !status.someSucceeded) {
|
||||
return new SnackbarErrorAction(
|
||||
'APP.MESSAGES.ERRORS.NODE_DELETION',
|
||||
{ name: status.fail[0].name }
|
||||
);
|
||||
}
|
||||
|
||||
if (status.oneSucceeded && !status.someFailed) {
|
||||
return new SnackbarInfoAction(
|
||||
'APP.MESSAGES.INFO.NODE_DELETION.SINGULAR',
|
||||
{ name: status.success[0].name }
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private showMoveMessage(nodes: Array<MinimalNodeEntity>, info: any, moveResponse?: any) {
|
||||
const succeeded = (moveResponse && moveResponse['succeeded']) ? moveResponse['succeeded'].length : 0;
|
||||
const partiallySucceeded = (moveResponse && moveResponse['partiallySucceeded']) ? moveResponse['partiallySucceeded'].length : 0;
|
||||
const failures = (moveResponse && moveResponse['failed']) ? moveResponse['failed'].length : 0;
|
||||
|
||||
let successMessage = '';
|
||||
let partialSuccessMessage = '';
|
||||
let failedMessage = '';
|
||||
let errorMessage = '';
|
||||
|
||||
if (typeof info === 'string') {
|
||||
|
||||
// in case of success
|
||||
if (info.toLowerCase().indexOf('succes') !== -1) {
|
||||
const i18nMessageString = 'APP.MESSAGES.INFO.NODE_MOVE.';
|
||||
let i18MessageSuffix = '';
|
||||
|
||||
if (succeeded) {
|
||||
i18MessageSuffix = ( succeeded === 1 ) ? 'SINGULAR' : 'PLURAL';
|
||||
successMessage = `${i18nMessageString}${i18MessageSuffix}`;
|
||||
}
|
||||
|
||||
if (partiallySucceeded) {
|
||||
i18MessageSuffix = ( partiallySucceeded === 1 ) ? 'PARTIAL.SINGULAR' : 'PARTIAL.PLURAL';
|
||||
partialSuccessMessage = `${i18nMessageString}${i18MessageSuffix}`;
|
||||
}
|
||||
|
||||
if (failures) {
|
||||
// if moving failed for ALL nodes, emit error
|
||||
if (failures === nodes.length) {
|
||||
const errors = this.nodeActionsService.flatten(moveResponse['failed']);
|
||||
errorMessage = this.getErrorMessage(errors[0]);
|
||||
|
||||
} else {
|
||||
i18MessageSuffix = 'PARTIAL.FAIL';
|
||||
failedMessage = `${i18nMessageString}${i18MessageSuffix}`;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
errorMessage = 'APP.MESSAGES.ERRORS.GENERIC';
|
||||
}
|
||||
|
||||
} else {
|
||||
errorMessage = this.getErrorMessage(info);
|
||||
}
|
||||
|
||||
const undo = (succeeded + partiallySucceeded > 0) ? this.translation.instant('APP.ACTIONS.UNDO') : '';
|
||||
failedMessage = errorMessage ? errorMessage : failedMessage;
|
||||
|
||||
const beforePartialSuccessMessage = (successMessage && partialSuccessMessage) ? ' ' : '';
|
||||
const beforeFailedMessage = ((successMessage || partialSuccessMessage) && failedMessage) ? ' ' : '';
|
||||
|
||||
const initialParentId = this.nodeActionsService.getEntryParentId(nodes[0].entry);
|
||||
|
||||
const messages = this.translation.instant(
|
||||
[successMessage, partialSuccessMessage, failedMessage],
|
||||
{ success: succeeded, failed: failures, partially: partiallySucceeded}
|
||||
);
|
||||
|
||||
// TODO: review in terms of i18n
|
||||
this.snackBar
|
||||
.open(
|
||||
messages[successMessage]
|
||||
+ beforePartialSuccessMessage + messages[partialSuccessMessage]
|
||||
+ beforeFailedMessage + messages[failedMessage]
|
||||
, undo, {
|
||||
panelClass: 'info-snackbar',
|
||||
duration: 3000
|
||||
})
|
||||
.onAction()
|
||||
.subscribe(() => this.undoMoveNodes(moveResponse, initialParentId));
|
||||
}
|
||||
|
||||
getErrorMessage(errorObject): string {
|
||||
let i18nMessageString = 'APP.MESSAGES.ERRORS.GENERIC';
|
||||
|
||||
try {
|
||||
const { error: { statusCode } } = JSON.parse(errorObject.message);
|
||||
|
||||
if (statusCode === 409) {
|
||||
i18nMessageString = 'APP.MESSAGES.ERRORS.NODE_MOVE';
|
||||
|
||||
} else if (statusCode === 403) {
|
||||
i18nMessageString = 'APP.MESSAGES.ERRORS.PERMISSION';
|
||||
}
|
||||
|
||||
} catch (err) { /* Do nothing, keep the original message */ }
|
||||
|
||||
return i18nMessageString;
|
||||
}
|
||||
}
|
||||
|
@ -24,9 +24,10 @@
|
||||
*/
|
||||
|
||||
import { Injectable } from '@angular/core';
|
||||
import { NodePermissions } from '../extensions/permission.extensions';
|
||||
|
||||
@Injectable()
|
||||
export class NodePermissionService {
|
||||
export class NodePermissionService implements NodePermissions {
|
||||
static DEFAULT_OPERATION = 'OR';
|
||||
|
||||
private defaultOptions = {
|
||||
@ -34,8 +35,8 @@ export class NodePermissionService {
|
||||
target: null
|
||||
};
|
||||
|
||||
check(source: any, permissions: string[], options: any = {}): boolean {
|
||||
const opts = Object.assign({}, this.defaultOptions, options);
|
||||
check(source: any, permissions: string[], options?: any): boolean {
|
||||
const opts = Object.assign({}, this.defaultOptions, options || {});
|
||||
|
||||
if (source) {
|
||||
if (Array.isArray(source) && source.length) {
|
||||
|
@ -24,6 +24,7 @@
|
||||
*/
|
||||
|
||||
export * from './actions/app.actions';
|
||||
export * from './actions/favorite.actions';
|
||||
export * from './actions/node.actions';
|
||||
export * from './actions/snackbar.actions';
|
||||
export * from './actions/router.actions';
|
||||
@ -31,3 +32,4 @@ export * from './actions/viewer.actions';
|
||||
export * from './actions/search.actions';
|
||||
export * from './actions/user.actions';
|
||||
export * from './actions/library.actions';
|
||||
export * from './actions/upload.actions';
|
||||
|
@ -33,6 +33,8 @@ export const SET_LANGUAGE_PICKER = 'SET_LANGUAGE_PICKER';
|
||||
export const SET_SHARED_URL = 'SET_SHARED_URL';
|
||||
export const SET_CURRENT_FOLDER = 'SET_CURRENT_FOLDER';
|
||||
export const SET_CURRENT_URL = 'SET_CURRENT_URL';
|
||||
export const TOGGLE_INFO_DRAWER = 'TOGGLE_INFO_DRAWER';
|
||||
export const TOGGLE_DOCUMENT_DISPLAY_MODE = 'TOGGLE_DOCUMENT_DISPLAY_MODE';
|
||||
|
||||
export class SetAppNameAction implements Action {
|
||||
readonly type = SET_APP_NAME;
|
||||
@ -68,3 +70,13 @@ export class SetCurrentUrlAction implements Action {
|
||||
readonly type = SET_CURRENT_URL;
|
||||
constructor(public payload: string) {}
|
||||
}
|
||||
|
||||
export class ToggleInfoDrawerAction implements Action {
|
||||
readonly type = TOGGLE_INFO_DRAWER;
|
||||
constructor(public payload?: any) {}
|
||||
}
|
||||
|
||||
export class ToggleDocumentDisplayMode implements Action {
|
||||
readonly type = TOGGLE_DOCUMENT_DISPLAY_MODE;
|
||||
constructor(public payload?: any) {}
|
||||
}
|
||||
|
@ -23,23 +23,18 @@
|
||||
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Directive, HostListener, Input } from '@angular/core';
|
||||
import { Action } from '@ngrx/store';
|
||||
import { MinimalNodeEntity } from 'alfresco-js-api';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { AppStore } from '../store/states';
|
||||
import { RestoreDeletedNodesAction } from '../store/actions';
|
||||
|
||||
@Directive({
|
||||
selector: '[acaRestoreNode]'
|
||||
})
|
||||
export class NodeRestoreDirective {
|
||||
// tslint:disable-next-line:no-input-rename
|
||||
@Input('acaRestoreNode') selection: MinimalNodeEntity[];
|
||||
export const ADD_FAVORITE = 'ADD_FAVORITE';
|
||||
export const REMOVE_FAVORITE = 'REMOVE_FAVORITE';
|
||||
|
||||
constructor(private store: Store<AppStore>) {}
|
||||
|
||||
@HostListener('click')
|
||||
onClick() {
|
||||
this.store.dispatch(new RestoreDeletedNodesAction(this.selection));
|
||||
}
|
||||
export class AddFavoriteAction implements Action {
|
||||
readonly type = ADD_FAVORITE;
|
||||
constructor(public payload: Array<MinimalNodeEntity>) {}
|
||||
}
|
||||
|
||||
export class RemoveFavoriteAction implements Action {
|
||||
readonly type = REMOVE_FAVORITE;
|
||||
constructor(public payload: Array<MinimalNodeEntity>) {}
|
||||
}
|
@ -30,7 +30,7 @@ export const CREATE_LIBRARY = 'CREATE_LIBRARY';
|
||||
|
||||
export class DeleteLibraryAction implements Action {
|
||||
readonly type = DELETE_LIBRARY;
|
||||
constructor(public payload: string) {}
|
||||
constructor(public payload?: string) {}
|
||||
}
|
||||
|
||||
export class CreateLibraryAction implements Action {
|
||||
|
@ -24,7 +24,6 @@
|
||||
*/
|
||||
|
||||
import { Action } from '@ngrx/store';
|
||||
import { NodeInfo } from '../models';
|
||||
import { MinimalNodeEntity } from 'alfresco-js-api';
|
||||
|
||||
export const SET_SELECTED_NODES = 'SET_SELECTED_NODES';
|
||||
@ -35,6 +34,12 @@ export const PURGE_DELETED_NODES = 'PURGE_DELETED_NODES';
|
||||
export const DOWNLOAD_NODES = 'DOWNLOAD_NODES';
|
||||
export const CREATE_FOLDER = 'CREATE_FOLDER';
|
||||
export const EDIT_FOLDER = 'EDIT_FOLDER';
|
||||
export const SHARE_NODE = 'SHARE_NODE';
|
||||
export const UNSHARE_NODES = 'UNSHARE_NODES';
|
||||
export const COPY_NODES = 'COPY_NODES';
|
||||
export const MOVE_NODES = 'MOVE_NODES';
|
||||
export const MANAGE_PERMISSIONS = 'MANAGE_PERMISSIONS';
|
||||
export const MANAGE_VERSIONS = 'MANAGE_VERSIONS';
|
||||
|
||||
export class SetSelectedNodesAction implements Action {
|
||||
readonly type = SET_SELECTED_NODES;
|
||||
@ -43,7 +48,7 @@ export class SetSelectedNodesAction implements Action {
|
||||
|
||||
export class DeleteNodesAction implements Action {
|
||||
readonly type = DELETE_NODES;
|
||||
constructor(public payload: NodeInfo[] = []) {}
|
||||
constructor(public payload: MinimalNodeEntity[] = []) {}
|
||||
}
|
||||
|
||||
export class UndoDeleteNodesAction implements Action {
|
||||
@ -75,3 +80,33 @@ export class EditFolderAction implements Action {
|
||||
readonly type = EDIT_FOLDER;
|
||||
constructor(public payload: MinimalNodeEntity) {}
|
||||
}
|
||||
|
||||
export class ShareNodeAction implements Action {
|
||||
readonly type = SHARE_NODE;
|
||||
constructor(public payload: MinimalNodeEntity) {}
|
||||
}
|
||||
|
||||
export class UnshareNodesAction implements Action {
|
||||
readonly type = UNSHARE_NODES;
|
||||
constructor(public payload: Array<MinimalNodeEntity>) {}
|
||||
}
|
||||
|
||||
export class CopyNodesAction implements Action {
|
||||
readonly type = COPY_NODES;
|
||||
constructor(public payload: Array<MinimalNodeEntity>) {}
|
||||
}
|
||||
|
||||
export class MoveNodesAction implements Action {
|
||||
readonly type = MOVE_NODES;
|
||||
constructor(public payload: Array<MinimalNodeEntity>) {}
|
||||
}
|
||||
|
||||
export class ManagePermissionsAction implements Action {
|
||||
readonly type = MANAGE_PERMISSIONS;
|
||||
constructor(public payload: MinimalNodeEntity) {}
|
||||
}
|
||||
|
||||
export class ManageVersionsAction implements Action {
|
||||
readonly type = MANAGE_VERSIONS;
|
||||
constructor(public payload: MinimalNodeEntity) {}
|
||||
}
|
||||
|
@ -23,27 +23,17 @@
|
||||
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Directive, HostListener, Input } from '@angular/core';
|
||||
import { MinimalNodeEntity } from 'alfresco-js-api';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { AppStore } from '../store/states';
|
||||
import { PurgeDeletedNodesAction } from '../store/actions';
|
||||
import { Action } from '@ngrx/store';
|
||||
|
||||
@Directive({
|
||||
selector: '[acaPermanentDelete]'
|
||||
})
|
||||
export class NodePermanentDeleteDirective {
|
||||
export const UPLOAD_FILES = 'UPLOAD_FILES';
|
||||
export const UPLOAD_FOLDER = 'UPLOAD_FOLDER';
|
||||
|
||||
// tslint:disable-next-line:no-input-rename
|
||||
@Input('acaPermanentDelete')
|
||||
selection: MinimalNodeEntity[];
|
||||
|
||||
constructor(
|
||||
private store: Store<AppStore>
|
||||
) {}
|
||||
|
||||
@HostListener('click')
|
||||
onClick() {
|
||||
this.store.dispatch(new PurgeDeletedNodesAction(this.selection));
|
||||
}
|
||||
export class UploadFilesAction implements Action {
|
||||
readonly type = UPLOAD_FILES;
|
||||
constructor(public payload: any) {}
|
||||
}
|
||||
|
||||
export class UploadFolderAction implements Action {
|
||||
readonly type = UPLOAD_FOLDER;
|
||||
constructor(public payload: any) {}
|
||||
}
|
@ -38,7 +38,9 @@ import {
|
||||
DownloadEffects,
|
||||
ViewerEffects,
|
||||
SearchEffects,
|
||||
SiteEffects
|
||||
SiteEffects,
|
||||
UploadEffects,
|
||||
FavoriteEffects
|
||||
} from './effects';
|
||||
|
||||
@NgModule({
|
||||
@ -55,7 +57,9 @@ import {
|
||||
DownloadEffects,
|
||||
ViewerEffects,
|
||||
SearchEffects,
|
||||
SiteEffects
|
||||
SiteEffects,
|
||||
UploadEffects,
|
||||
FavoriteEffects
|
||||
]),
|
||||
!environment.production
|
||||
? StoreDevtoolsModule.instrument({ maxAge: 25 })
|
||||
|
@ -24,9 +24,11 @@
|
||||
*/
|
||||
|
||||
export * from './effects/download.effects';
|
||||
export * from './effects/favorite.effects';
|
||||
export * from './effects/node.effects';
|
||||
export * from './effects/router.effects';
|
||||
export * from './effects/snackbar.effects';
|
||||
export * from './effects/viewer.effects';
|
||||
export * from './effects/search.effects';
|
||||
export * from './effects/library.effects';
|
||||
export * from './effects/upload.effects';
|
||||
|
82
src/app/store/effects/favorite.effects.ts
Normal file
82
src/app/store/effects/favorite.effects.ts
Normal file
@ -0,0 +1,82 @@
|
||||
/*!
|
||||
* @license
|
||||
* Alfresco Example Content Application
|
||||
*
|
||||
* Copyright (C) 2005 - 2018 Alfresco Software Limited
|
||||
*
|
||||
* This file is part of the Alfresco Example Content Application.
|
||||
* If the software was purchased under a paid Alfresco license, the terms of
|
||||
* the paid license agreement will prevail. Otherwise, the software is
|
||||
* provided under the following open source license terms:
|
||||
*
|
||||
* The Alfresco Example Content Application is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* The Alfresco Example Content Application is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Effect, Actions, ofType } from '@ngrx/effects';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { ADD_FAVORITE, AddFavoriteAction, RemoveFavoriteAction, REMOVE_FAVORITE } from '../actions/favorite.actions';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { AppStore } from '../states';
|
||||
import { appSelection } from '../selectors/app.selectors';
|
||||
import { ContentManagementService } from '../../services/content-management.service';
|
||||
|
||||
@Injectable()
|
||||
export class FavoriteEffects {
|
||||
constructor(
|
||||
private store: Store<AppStore>,
|
||||
private actions$: Actions,
|
||||
private content: ContentManagementService
|
||||
) {}
|
||||
|
||||
@Effect({ dispatch: false })
|
||||
addFavorite$ = this.actions$.pipe(
|
||||
ofType<AddFavoriteAction>(ADD_FAVORITE),
|
||||
map(action => {
|
||||
if (action.payload && action.payload.length > 0) {
|
||||
this.content.addFavorite(action.payload);
|
||||
} else {
|
||||
this.store
|
||||
.select(appSelection)
|
||||
.take(1)
|
||||
.subscribe(selection => {
|
||||
if (selection && !selection.isEmpty) {
|
||||
this.content.addFavorite(selection.nodes);
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
@Effect({ dispatch: false })
|
||||
removeFavorite$ = this.actions$.pipe(
|
||||
ofType<RemoveFavoriteAction>(REMOVE_FAVORITE),
|
||||
map(action => {
|
||||
if (action.payload && action.payload.length > 0) {
|
||||
this.content.removeFavorite(action.payload);
|
||||
} else {
|
||||
this.store
|
||||
.select(appSelection)
|
||||
.take(1)
|
||||
.subscribe(selection => {
|
||||
if (selection && !selection.isEmpty) {
|
||||
this.content.removeFavorite(selection.nodes);
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
}
|
@ -30,21 +30,16 @@ import {
|
||||
DeleteLibraryAction, DELETE_LIBRARY,
|
||||
CreateLibraryAction, CREATE_LIBRARY
|
||||
} from '../actions';
|
||||
import {
|
||||
SnackbarInfoAction,
|
||||
SnackbarErrorAction
|
||||
} from '../actions/snackbar.actions';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { AppStore } from '../states/app.state';
|
||||
import { ContentManagementService } from '../../services/content-management.service';
|
||||
import { ContentApiService } from '../../services/content-api.service';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { AppStore } from '../states';
|
||||
import { appSelection } from '../selectors/app.selectors';
|
||||
|
||||
@Injectable()
|
||||
export class SiteEffects {
|
||||
constructor(
|
||||
private actions$: Actions,
|
||||
private store: Store<AppStore>,
|
||||
private contentApi: ContentApiService,
|
||||
private actions$: Actions,
|
||||
private content: ContentManagementService
|
||||
) {}
|
||||
|
||||
@ -53,7 +48,16 @@ export class SiteEffects {
|
||||
ofType<DeleteLibraryAction>(DELETE_LIBRARY),
|
||||
map(action => {
|
||||
if (action.payload) {
|
||||
this.deleteLibrary(action.payload);
|
||||
this.content.deleteLibrary(action.payload);
|
||||
} else {
|
||||
this.store
|
||||
.select(appSelection)
|
||||
.take(1)
|
||||
.subscribe(selection => {
|
||||
if (selection && selection.library) {
|
||||
this.content.deleteLibrary(selection.library.entry.id);
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
);
|
||||
@ -62,31 +66,7 @@ export class SiteEffects {
|
||||
createLibrary$ = this.actions$.pipe(
|
||||
ofType<CreateLibraryAction>(CREATE_LIBRARY),
|
||||
map(action => {
|
||||
this.createLibrary();
|
||||
this.content.createLibrary();
|
||||
})
|
||||
);
|
||||
|
||||
private deleteLibrary(id: string) {
|
||||
this.contentApi.deleteSite(id).subscribe(
|
||||
() => {
|
||||
this.content.libraryDeleted.next(id);
|
||||
this.store.dispatch(
|
||||
new SnackbarInfoAction(
|
||||
'APP.MESSAGES.INFO.LIBRARY_DELETED'
|
||||
)
|
||||
);
|
||||
},
|
||||
() => {
|
||||
this.store.dispatch(
|
||||
new SnackbarErrorAction(
|
||||
'APP.MESSAGES.ERRORS.DELETE_LIBRARY_FAILED'
|
||||
)
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private createLibrary() {
|
||||
this.content.createLibrary();
|
||||
}
|
||||
}
|
||||
|
@ -29,49 +29,97 @@ import { map } from 'rxjs/operators';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { AppStore } from '../states/app.state';
|
||||
import {
|
||||
SnackbarWarningAction,
|
||||
SnackbarInfoAction,
|
||||
SnackbarErrorAction,
|
||||
PurgeDeletedNodesAction,
|
||||
PURGE_DELETED_NODES,
|
||||
DeleteNodesAction,
|
||||
DELETE_NODES,
|
||||
SnackbarUserAction,
|
||||
SnackbarAction,
|
||||
UndoDeleteNodesAction,
|
||||
UNDO_DELETE_NODES,
|
||||
CreateFolderAction,
|
||||
CREATE_FOLDER
|
||||
CREATE_FOLDER,
|
||||
EditFolderAction,
|
||||
EDIT_FOLDER,
|
||||
RestoreDeletedNodesAction,
|
||||
RESTORE_DELETED_NODES,
|
||||
ShareNodeAction,
|
||||
SHARE_NODE
|
||||
} from '../actions';
|
||||
import { ContentManagementService } from '../../services/content-management.service';
|
||||
import { Observable } from 'rxjs/Rx';
|
||||
import { NodeInfo, DeleteStatus, DeletedNodeInfo } from '../models';
|
||||
import { ContentApiService } from '../../services/content-api.service';
|
||||
import { currentFolder, appSelection } from '../selectors/app.selectors';
|
||||
import { EditFolderAction, EDIT_FOLDER, RestoreDeletedNodesAction, RESTORE_DELETED_NODES } from '../actions/node.actions';
|
||||
import {
|
||||
UnshareNodesAction,
|
||||
UNSHARE_NODES,
|
||||
CopyNodesAction,
|
||||
COPY_NODES,
|
||||
MoveNodesAction,
|
||||
MOVE_NODES,
|
||||
ManagePermissionsAction,
|
||||
MANAGE_PERMISSIONS,
|
||||
ManageVersionsAction,
|
||||
MANAGE_VERSIONS
|
||||
} from '../actions/node.actions';
|
||||
|
||||
@Injectable()
|
||||
export class NodeEffects {
|
||||
constructor(
|
||||
private store: Store<AppStore>,
|
||||
private actions$: Actions,
|
||||
private contentManagementService: ContentManagementService,
|
||||
private contentApi: ContentApiService
|
||||
private contentService: ContentManagementService
|
||||
) {}
|
||||
|
||||
@Effect({ dispatch: false })
|
||||
shareNode$ = this.actions$.pipe(
|
||||
ofType<ShareNodeAction>(SHARE_NODE),
|
||||
map(action => {
|
||||
if (action.payload) {
|
||||
this.contentService.shareNode(action.payload);
|
||||
} else {
|
||||
this.store
|
||||
.select(appSelection)
|
||||
.take(1)
|
||||
.subscribe(selection => {
|
||||
if (selection && selection.file) {
|
||||
this.contentService.shareNode(selection.file);
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
@Effect({ dispatch: false })
|
||||
unshareNodes$ = this.actions$.pipe(
|
||||
ofType<UnshareNodesAction>(UNSHARE_NODES),
|
||||
map(action => {
|
||||
if (action && action.payload && action.payload.length > 0) {
|
||||
this.contentService.unshareNodes(action.payload);
|
||||
} else {
|
||||
this.store
|
||||
.select(appSelection)
|
||||
.take(1)
|
||||
.subscribe(selection => {
|
||||
if (selection && !selection.isEmpty) {
|
||||
this.contentService.unshareNodes(selection.nodes);
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
@Effect({ dispatch: false })
|
||||
purgeDeletedNodes$ = this.actions$.pipe(
|
||||
ofType<PurgeDeletedNodesAction>(PURGE_DELETED_NODES),
|
||||
map(action => {
|
||||
if (action && action.payload && action.payload.length > 0) {
|
||||
this.contentManagementService.purgeDeletedNodes(action.payload);
|
||||
this.contentService.purgeDeletedNodes(action.payload);
|
||||
} else {
|
||||
this.store
|
||||
.select(appSelection)
|
||||
.take(1)
|
||||
.subscribe(selection => {
|
||||
if (selection && selection.count > 0) {
|
||||
this.contentManagementService.purgeDeletedNodes(selection.nodes);
|
||||
this.contentService.purgeDeletedNodes(
|
||||
selection.nodes
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -83,14 +131,16 @@ export class NodeEffects {
|
||||
ofType<RestoreDeletedNodesAction>(RESTORE_DELETED_NODES),
|
||||
map(action => {
|
||||
if (action && action.payload && action.payload.length > 0) {
|
||||
this.contentManagementService.restoreDeletedNodes(action.payload);
|
||||
this.contentService.restoreDeletedNodes(action.payload);
|
||||
} else {
|
||||
this.store
|
||||
.select(appSelection)
|
||||
.take(1)
|
||||
.subscribe(selection => {
|
||||
if (selection && selection.count > 0) {
|
||||
this.contentManagementService.restoreDeletedNodes(selection.nodes);
|
||||
this.contentService.restoreDeletedNodes(
|
||||
selection.nodes
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -101,8 +151,17 @@ export class NodeEffects {
|
||||
deleteNodes$ = this.actions$.pipe(
|
||||
ofType<DeleteNodesAction>(DELETE_NODES),
|
||||
map(action => {
|
||||
if (action.payload.length > 0) {
|
||||
this.deleteNodes(action.payload);
|
||||
if (action && action.payload && action.payload.length > 0) {
|
||||
this.contentService.deleteNodes(action.payload);
|
||||
} else {
|
||||
this.store
|
||||
.select(appSelection)
|
||||
.take(1)
|
||||
.subscribe(selection => {
|
||||
if (selection && selection.count > 0) {
|
||||
this.contentService.deleteNodes(selection.nodes);
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
);
|
||||
@ -112,7 +171,7 @@ export class NodeEffects {
|
||||
ofType<UndoDeleteNodesAction>(UNDO_DELETE_NODES),
|
||||
map(action => {
|
||||
if (action.payload.length > 0) {
|
||||
this.undoDeleteNodes(action.payload);
|
||||
this.contentService.undoDeleteNodes(action.payload);
|
||||
}
|
||||
})
|
||||
);
|
||||
@ -122,14 +181,14 @@ export class NodeEffects {
|
||||
ofType<CreateFolderAction>(CREATE_FOLDER),
|
||||
map(action => {
|
||||
if (action.payload) {
|
||||
this.contentManagementService.createFolder(action.payload);
|
||||
this.contentService.createFolder(action.payload);
|
||||
} else {
|
||||
this.store
|
||||
.select(currentFolder)
|
||||
.take(1)
|
||||
.subscribe(node => {
|
||||
if (node && node.id) {
|
||||
this.contentManagementService.createFolder(node.id);
|
||||
this.contentService.createFolder(node.id);
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -141,215 +200,97 @@ export class NodeEffects {
|
||||
ofType<EditFolderAction>(EDIT_FOLDER),
|
||||
map(action => {
|
||||
if (action.payload) {
|
||||
this.contentManagementService.editFolder(action.payload);
|
||||
this.contentService.editFolder(action.payload);
|
||||
} else {
|
||||
this.store
|
||||
.select(appSelection)
|
||||
.take(1)
|
||||
.subscribe(selection => {
|
||||
if (selection && selection.folder) {
|
||||
this.contentManagementService.editFolder(selection.folder);
|
||||
this.contentService.editFolder(selection.folder);
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
private deleteNodes(items: NodeInfo[]): void {
|
||||
const batch: Observable<DeletedNodeInfo>[] = [];
|
||||
|
||||
items.forEach(node => {
|
||||
batch.push(this.deleteNode(node));
|
||||
});
|
||||
|
||||
Observable.forkJoin(...batch).subscribe((data: DeletedNodeInfo[]) => {
|
||||
const status = this.processStatus(data);
|
||||
const message = this.getDeleteMessage(status);
|
||||
|
||||
if (message && status.someSucceeded) {
|
||||
message.duration = 10000;
|
||||
message.userAction = new SnackbarUserAction(
|
||||
'APP.ACTIONS.UNDO',
|
||||
new UndoDeleteNodesAction([...status.success])
|
||||
);
|
||||
}
|
||||
|
||||
this.store.dispatch(message);
|
||||
|
||||
if (status.someSucceeded) {
|
||||
this.contentManagementService.nodesDeleted.next();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private deleteNode(node: NodeInfo): Observable<DeletedNodeInfo> {
|
||||
const { id, name } = node;
|
||||
|
||||
return this.contentApi
|
||||
.deleteNode(id)
|
||||
.map(() => {
|
||||
return {
|
||||
id,
|
||||
name,
|
||||
status: 1
|
||||
};
|
||||
})
|
||||
.catch((error: any) => {
|
||||
return Observable.of({
|
||||
id,
|
||||
name,
|
||||
status: 0
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private getDeleteMessage(status: DeleteStatus): SnackbarAction {
|
||||
if (status.allFailed && !status.oneFailed) {
|
||||
return new SnackbarErrorAction(
|
||||
'APP.MESSAGES.ERRORS.NODE_DELETION_PLURAL',
|
||||
{ number: status.fail.length }
|
||||
);
|
||||
}
|
||||
|
||||
if (status.allSucceeded && !status.oneSucceeded) {
|
||||
return new SnackbarInfoAction(
|
||||
'APP.MESSAGES.INFO.NODE_DELETION.PLURAL',
|
||||
{ number: status.success.length }
|
||||
);
|
||||
}
|
||||
|
||||
if (status.someFailed && status.someSucceeded && !status.oneSucceeded) {
|
||||
return new SnackbarWarningAction(
|
||||
'APP.MESSAGES.INFO.NODE_DELETION.PARTIAL_PLURAL',
|
||||
{
|
||||
success: status.success.length,
|
||||
failed: status.fail.length
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if (status.someFailed && status.oneSucceeded) {
|
||||
return new SnackbarWarningAction(
|
||||
'APP.MESSAGES.INFO.NODE_DELETION.PARTIAL_SINGULAR',
|
||||
{
|
||||
success: status.success.length,
|
||||
failed: status.fail.length
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if (status.oneFailed && !status.someSucceeded) {
|
||||
return new SnackbarErrorAction(
|
||||
'APP.MESSAGES.ERRORS.NODE_DELETION',
|
||||
{ name: status.fail[0].name }
|
||||
);
|
||||
}
|
||||
|
||||
if (status.oneSucceeded && !status.someFailed) {
|
||||
return new SnackbarInfoAction(
|
||||
'APP.MESSAGES.INFO.NODE_DELETION.SINGULAR',
|
||||
{ name: status.success[0].name }
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private undoDeleteNodes(items: DeletedNodeInfo[]): void {
|
||||
const batch: Observable<DeletedNodeInfo>[] = [];
|
||||
|
||||
items.forEach(item => {
|
||||
batch.push(this.undoDeleteNode(item));
|
||||
});
|
||||
|
||||
Observable.forkJoin(...batch).subscribe(data => {
|
||||
const processedData = this.processStatus(data);
|
||||
|
||||
if (processedData.fail.length) {
|
||||
const message = this.getUndoDeleteMessage(processedData);
|
||||
this.store.dispatch(message);
|
||||
}
|
||||
|
||||
if (processedData.someSucceeded) {
|
||||
this.contentManagementService.nodesRestored.next();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private undoDeleteNode(item: DeletedNodeInfo): Observable<DeletedNodeInfo> {
|
||||
const { id, name } = item;
|
||||
|
||||
return this.contentApi
|
||||
.restoreNode(id)
|
||||
.map(() => {
|
||||
return {
|
||||
id,
|
||||
name,
|
||||
status: 1
|
||||
};
|
||||
})
|
||||
.catch((error: any) => {
|
||||
return Observable.of({
|
||||
id,
|
||||
name,
|
||||
status: 0
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private getUndoDeleteMessage(status: DeleteStatus): SnackbarAction {
|
||||
if (status.someFailed && !status.oneFailed) {
|
||||
return new SnackbarErrorAction(
|
||||
'APP.MESSAGES.ERRORS.NODE_RESTORE_PLURAL',
|
||||
{ number: status.fail.length }
|
||||
);
|
||||
}
|
||||
|
||||
if (status.oneFailed) {
|
||||
return new SnackbarErrorAction('APP.MESSAGES.ERRORS.NODE_RESTORE', {
|
||||
name: status.fail[0].name
|
||||
});
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private processStatus(data: DeletedNodeInfo[] = []): DeleteStatus {
|
||||
const status = {
|
||||
fail: [],
|
||||
success: [],
|
||||
get someFailed() {
|
||||
return !!this.fail.length;
|
||||
},
|
||||
get someSucceeded() {
|
||||
return !!this.success.length;
|
||||
},
|
||||
get oneFailed() {
|
||||
return this.fail.length === 1;
|
||||
},
|
||||
get oneSucceeded() {
|
||||
return this.success.length === 1;
|
||||
},
|
||||
get allSucceeded() {
|
||||
return this.someSucceeded && !this.someFailed;
|
||||
},
|
||||
get allFailed() {
|
||||
return this.someFailed && !this.someSucceeded;
|
||||
},
|
||||
reset() {
|
||||
this.fail = [];
|
||||
this.success = [];
|
||||
}
|
||||
};
|
||||
|
||||
return data.reduce((acc, node) => {
|
||||
if (node.status) {
|
||||
acc.success.push(node);
|
||||
@Effect({ dispatch: false })
|
||||
copyNodes$ = this.actions$.pipe(
|
||||
ofType<CopyNodesAction>(COPY_NODES),
|
||||
map(action => {
|
||||
if (action.payload && action.payload.length > 0) {
|
||||
this.contentService.copyNodes(action.payload);
|
||||
} else {
|
||||
acc.fail.push(node);
|
||||
this.store
|
||||
.select(appSelection)
|
||||
.take(1)
|
||||
.subscribe(selection => {
|
||||
if (selection && !selection.isEmpty) {
|
||||
this.contentService.copyNodes(selection.nodes);
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
return acc;
|
||||
}, status);
|
||||
}
|
||||
@Effect({ dispatch: false })
|
||||
moveNodes$ = this.actions$.pipe(
|
||||
ofType<MoveNodesAction>(MOVE_NODES),
|
||||
map(action => {
|
||||
if (action.payload && action.payload.length > 0) {
|
||||
this.contentService.moveNodes(action.payload);
|
||||
} else {
|
||||
this.store
|
||||
.select(appSelection)
|
||||
.take(1)
|
||||
.subscribe(selection => {
|
||||
if (selection && !selection.isEmpty) {
|
||||
this.contentService.moveNodes(selection.nodes);
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
@Effect({ dispatch: false })
|
||||
managePermissions = this.actions$.pipe(
|
||||
ofType<ManagePermissionsAction>(MANAGE_PERMISSIONS),
|
||||
map(action => {
|
||||
if (action && action.payload) {
|
||||
this.contentService.managePermissions(action.payload);
|
||||
} else {
|
||||
this.store
|
||||
.select(appSelection)
|
||||
.take(1)
|
||||
.subscribe(selection => {
|
||||
if (selection && !selection.isEmpty) {
|
||||
this.contentService.managePermissions(
|
||||
selection.first
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
@Effect({ dispatch: false })
|
||||
manageVersions$ = this.actions$.pipe(
|
||||
ofType<ManageVersionsAction>(MANAGE_VERSIONS),
|
||||
map(action => {
|
||||
if (action && action.payload) {
|
||||
this.contentService.manageVersions(action.payload);
|
||||
} else {
|
||||
this.store
|
||||
.select(appSelection)
|
||||
.take(1)
|
||||
.subscribe(selection => {
|
||||
if (selection && selection.file) {
|
||||
this.contentService.manageVersions(
|
||||
selection.file
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
116
src/app/store/effects/upload.effects.ts
Normal file
116
src/app/store/effects/upload.effects.ts
Normal file
@ -0,0 +1,116 @@
|
||||
/*!
|
||||
* @license
|
||||
* Alfresco Example Content Application
|
||||
*
|
||||
* Copyright (C) 2005 - 2018 Alfresco Software Limited
|
||||
*
|
||||
* This file is part of the Alfresco Example Content Application.
|
||||
* If the software was purchased under a paid Alfresco license, the terms of
|
||||
* the paid license agreement will prevail. Otherwise, the software is
|
||||
* provided under the following open source license terms:
|
||||
*
|
||||
* The Alfresco Example Content Application is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* The Alfresco Example Content Application is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Injectable, RendererFactory2, NgZone } from '@angular/core';
|
||||
import { Actions, Effect, ofType } from '@ngrx/effects';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { AppStore } from '../states';
|
||||
import { UploadFilesAction, UPLOAD_FILES } from '../actions';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { FileUtils, FileModel, UploadService } from '@alfresco/adf-core';
|
||||
import { currentFolder } from '../selectors/app.selectors';
|
||||
import { UploadFolderAction, UPLOAD_FOLDER } from '../actions/upload.actions';
|
||||
|
||||
@Injectable()
|
||||
export class UploadEffects {
|
||||
private fileInput: HTMLInputElement;
|
||||
private folderInput: HTMLInputElement;
|
||||
|
||||
constructor(
|
||||
private store: Store<AppStore>,
|
||||
private actions$: Actions,
|
||||
private ngZone: NgZone,
|
||||
private uploadService: UploadService,
|
||||
rendererFactory: RendererFactory2
|
||||
) {
|
||||
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.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);
|
||||
}
|
||||
|
||||
@Effect({ dispatch: false })
|
||||
uploadFiles$ = this.actions$.pipe(
|
||||
ofType<UploadFilesAction>(UPLOAD_FILES),
|
||||
map(() => {
|
||||
this.fileInput.click();
|
||||
})
|
||||
);
|
||||
|
||||
@Effect({ dispatch: false })
|
||||
uploadFolder$ = this.actions$.pipe(
|
||||
ofType<UploadFolderAction>(UPLOAD_FOLDER),
|
||||
map(() => {
|
||||
this.folderInput.click();
|
||||
})
|
||||
);
|
||||
|
||||
private upload(event: any): void {
|
||||
this.store
|
||||
.select(currentFolder)
|
||||
.take(1)
|
||||
.subscribe(node => {
|
||||
if (node && node.id) {
|
||||
const input = <HTMLInputElement>event.currentTarget;
|
||||
const files = FileUtils.toFileArray(input.files).map(
|
||||
file => {
|
||||
return 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();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@ -42,8 +42,15 @@ import {
|
||||
SetSharedUrlAction,
|
||||
SET_CURRENT_FOLDER,
|
||||
SetCurrentFolderAction,
|
||||
SET_CURRENT_URL, SetCurrentUrlAction
|
||||
SET_CURRENT_URL,
|
||||
SetCurrentUrlAction
|
||||
} from '../actions';
|
||||
import {
|
||||
TOGGLE_INFO_DRAWER,
|
||||
ToggleInfoDrawerAction,
|
||||
TOGGLE_DOCUMENT_DISPLAY_MODE,
|
||||
ToggleDocumentDisplayMode
|
||||
} from '../actions/app.actions';
|
||||
|
||||
export function appReducer(
|
||||
state: AppState = INITIAL_APP_STATE,
|
||||
@ -85,6 +92,14 @@ export function appReducer(
|
||||
case SET_CURRENT_URL:
|
||||
newState = updateCurrentUrl(state, <SetCurrentUrlAction>action);
|
||||
break;
|
||||
case TOGGLE_INFO_DRAWER:
|
||||
newState = updateInfoDrawer(state, <ToggleInfoDrawerAction>action);
|
||||
break;
|
||||
case TOGGLE_DOCUMENT_DISPLAY_MODE:
|
||||
newState = updateDocumentDisplayMode(state, <
|
||||
ToggleDocumentDisplayMode
|
||||
>action);
|
||||
break;
|
||||
default:
|
||||
newState = Object.assign({}, state);
|
||||
}
|
||||
@ -168,6 +183,31 @@ function updateCurrentUrl(state: AppState, action: SetCurrentUrlAction) {
|
||||
return newState;
|
||||
}
|
||||
|
||||
function updateInfoDrawer(state: AppState, action: ToggleInfoDrawerAction) {
|
||||
const newState = Object.assign({}, state);
|
||||
|
||||
let value = state.infoDrawerOpened;
|
||||
if (state.selection.isEmpty) {
|
||||
value = false;
|
||||
} else {
|
||||
value = !value;
|
||||
}
|
||||
|
||||
newState.infoDrawerOpened = value;
|
||||
|
||||
return newState;
|
||||
}
|
||||
|
||||
function updateDocumentDisplayMode(
|
||||
state: AppState,
|
||||
action: ToggleDocumentDisplayMode
|
||||
) {
|
||||
const newState = Object.assign({}, state);
|
||||
newState.documentDisplayMode =
|
||||
newState.documentDisplayMode === 'list' ? 'gallery' : 'list';
|
||||
return newState;
|
||||
}
|
||||
|
||||
function updateSelectedNodes(
|
||||
state: AppState,
|
||||
action: SetSelectedNodesAction
|
||||
@ -181,6 +221,7 @@ function updateSelectedNodes(
|
||||
let last = null;
|
||||
let file = null;
|
||||
let folder = null;
|
||||
let library = null;
|
||||
|
||||
if (nodes.length > 0) {
|
||||
first = nodes[0];
|
||||
@ -197,6 +238,15 @@ function updateSelectedNodes(
|
||||
}
|
||||
}
|
||||
|
||||
const libraries = [...action.payload].filter((node: any) => node.isLibrary);
|
||||
if (libraries.length === 1) {
|
||||
library = libraries[0];
|
||||
}
|
||||
|
||||
if (isEmpty) {
|
||||
newState.infoDrawerOpened = false;
|
||||
}
|
||||
|
||||
newState.selection = {
|
||||
count,
|
||||
nodes,
|
||||
@ -204,7 +254,9 @@ function updateSelectedNodes(
|
||||
first,
|
||||
last,
|
||||
file,
|
||||
folder
|
||||
folder,
|
||||
libraries,
|
||||
library
|
||||
};
|
||||
return newState;
|
||||
}
|
||||
|
@ -36,6 +36,8 @@ export const selectUser = createSelector(selectApp, state => state.user);
|
||||
export const sharedUrl = createSelector(selectApp, state => state.sharedUrl);
|
||||
export const appNavigation = createSelector(selectApp, state => state.navigation);
|
||||
export const currentFolder = createSelector(selectApp, state => state.navigation.currentFolder);
|
||||
export const infoDrawerOpened = createSelector(selectApp, state => state.infoDrawerOpened);
|
||||
export const documentDisplayMode = createSelector(selectApp, state => state.documentDisplayMode);
|
||||
|
||||
export const selectionWithFolder = createSelector(
|
||||
appSelection,
|
||||
|
@ -36,6 +36,8 @@ export interface AppState {
|
||||
selection: SelectionState;
|
||||
user: ProfileState;
|
||||
navigation: NavigationState;
|
||||
infoDrawerOpened: boolean;
|
||||
documentDisplayMode: string;
|
||||
}
|
||||
|
||||
export const INITIAL_APP_STATE: AppState = {
|
||||
@ -52,12 +54,15 @@ export const INITIAL_APP_STATE: AppState = {
|
||||
},
|
||||
selection: {
|
||||
nodes: [],
|
||||
libraries: [],
|
||||
isEmpty: true,
|
||||
count: 0
|
||||
},
|
||||
navigation: {
|
||||
currentFolder: null
|
||||
}
|
||||
},
|
||||
infoDrawerOpened: false,
|
||||
documentDisplayMode: 'list'
|
||||
};
|
||||
|
||||
export interface AppStore {
|
||||
|
@ -23,14 +23,16 @@
|
||||
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { MinimalNodeEntity } from 'alfresco-js-api';
|
||||
import { MinimalNodeEntity, SiteEntry } from 'alfresco-js-api';
|
||||
|
||||
export interface SelectionState {
|
||||
count: number;
|
||||
nodes: MinimalNodeEntity[];
|
||||
libraries: SiteEntry[];
|
||||
isEmpty: boolean;
|
||||
first?: MinimalNodeEntity;
|
||||
last?: MinimalNodeEntity;
|
||||
folder?: MinimalNodeEntity;
|
||||
file?: MinimalNodeEntity;
|
||||
library?: SiteEntry;
|
||||
}
|
||||
|
@ -8,6 +8,85 @@
|
||||
],
|
||||
|
||||
"rules": [
|
||||
{
|
||||
"id": "app.toolbar.favorite.canToggle",
|
||||
"comment": "workaround for recent files and search api issue",
|
||||
"type": "core.every",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "rule",
|
||||
"value": "core.some",
|
||||
"parameters": [
|
||||
{ "type": "rule", "value": "app.selection.canAddFavorite" },
|
||||
{ "type": "rule", "value": "app.selection.canRemoveFavorite" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "rule",
|
||||
"value": "core.some",
|
||||
"parameters": [
|
||||
{ "type": "rule", "value": "app.navigation.isRecentFiles" },
|
||||
{ "type": "rule", "value": "app.navigation.isSharedFiles" },
|
||||
{ "type": "rule", "value": "app.navigation.isSearchResults" }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "app.toolbar.favorite.canAdd",
|
||||
"type": "core.every",
|
||||
"parameters": [
|
||||
{ "type": "rule", "value": "app.selection.canAddFavorite" },
|
||||
{ "type": "rule", "value": "app.navigation.isNotRecentFiles" },
|
||||
{ "type": "rule", "value": "app.navigation.isNotSharedFiles" },
|
||||
{ "type": "rule", "value": "app.navigation.isNotSearchResults" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "app.toolbar.favorite.canRemove",
|
||||
"type": "core.every",
|
||||
"parameters": [
|
||||
{ "type": "rule", "value": "app.selection.canRemoveFavorite" },
|
||||
{ "type": "rule", "value": "app.navigation.isNotRecentFiles" },
|
||||
{ "type": "rule", "value": "app.navigation.isNotSharedFiles" },
|
||||
{ "type": "rule", "value": "app.navigation.isNotSearchResults" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "app.toolbar.info",
|
||||
"type": "core.every",
|
||||
"parameters": [
|
||||
{ "type": "rule", "value": "app.selection.notEmpty" },
|
||||
{ "type": "rule", "value": "app.navigation.isNotLibraries" },
|
||||
{ "type": "rule", "value": "app.navigation.isNotTrashcan" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "app.toolbar.canCopyNode",
|
||||
"type": "core.every",
|
||||
"parameters": [
|
||||
{ "type": "rule", "value": "app.selection.notEmpty" },
|
||||
{ "type": "rule", "value": "app.navigation.isNotTrashcan" },
|
||||
{ "type": "rule", "value": "app.navigation.isNotLibraries" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "app.toolbar.permissions",
|
||||
"type": "core.every",
|
||||
"parameters": [
|
||||
{ "type": "rule", "value": "app.selection.file" },
|
||||
{ "type": "rule", "value": "app.selection.first.canUpdate" },
|
||||
{ "type": "rule", "value": "app.navigation.isNotTrashcan" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "app.toolbar.versions",
|
||||
"type": "core.every",
|
||||
"parameters": [
|
||||
{ "type": "rule", "value": "app.selection.file" },
|
||||
{ "type": "rule", "value": "app.navigation.isNotTrashcan" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "app.trashcan.hasSelection",
|
||||
"type": "core.every",
|
||||
@ -21,7 +100,8 @@
|
||||
"type": "core.every",
|
||||
"parameters": [
|
||||
{ "type": "rule", "value": "app.selection.folder" },
|
||||
{ "type": "rule", "value": "app.selection.folder.canUpdate" }
|
||||
{ "type": "rule", "value": "app.selection.folder.canUpdate" },
|
||||
{ "type": "rule", "value": "app.navigation.isNotTrashcan" }
|
||||
]
|
||||
},
|
||||
{
|
||||
@ -57,13 +137,43 @@
|
||||
"id": "app.create.folder",
|
||||
"type": "default",
|
||||
"icon": "create_new_folder",
|
||||
"title": "ext: Create Folder",
|
||||
"title": "APP.NEW_MENU.MENU_ITEMS.CREATE_FOLDER",
|
||||
"description": "APP.NEW_MENU.TOOLTIPS.CREATE_FOLDER",
|
||||
"description-disabled": "APP.NEW_MENU.TOOLTIPS.CREATE_FOLDER_NOT_ALLOWED",
|
||||
"actions": {
|
||||
"click": "CREATE_FOLDER"
|
||||
},
|
||||
"rules": {
|
||||
"enabled": "app.navigation.folder.canCreate"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "app.create.uploadFile",
|
||||
"type": "default",
|
||||
"icon": "file_upload",
|
||||
"title": "APP.NEW_MENU.MENU_ITEMS.UPLOAD_FILE",
|
||||
"description": "APP.NEW_MENU.TOOLTIPS.UPLOAD_FILES",
|
||||
"description-disabled": "APP.NEW_MENU.TOOLTIPS.UPLOAD_FILES_NOT_ALLOWED",
|
||||
"actions": {
|
||||
"click": "UPLOAD_FILES"
|
||||
},
|
||||
"rules": {
|
||||
"enabled": "app.navigation.folder.canUpload"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "app.create.uploadFolder",
|
||||
"type": "default",
|
||||
"icon": "file_upload",
|
||||
"title": "APP.NEW_MENU.MENU_ITEMS.UPLOAD_FOLDER",
|
||||
"description": "APP.NEW_MENU.TOOLTIPS.UPLOAD_FOLDERS",
|
||||
"description-disabled": "APP.NEW_MENU.TOOLTIPS.UPLOAD_FOLDERS_NOT_ALLOWED",
|
||||
"actions": {
|
||||
"click": "UPLOAD_FOLDER"
|
||||
},
|
||||
"rules": {
|
||||
"enabled": "app.navigation.folder.canUpload"
|
||||
}
|
||||
}
|
||||
],
|
||||
"navbar": [
|
||||
@ -122,24 +232,6 @@
|
||||
],
|
||||
"content": {
|
||||
"actions": [
|
||||
{
|
||||
"id": "app.toolbar.separator.1",
|
||||
"order": 5,
|
||||
"type": "separator"
|
||||
},
|
||||
{
|
||||
"id": "app.toolbar.createFolder",
|
||||
"type": "button",
|
||||
"order": 10,
|
||||
"title": "APP.NEW_MENU.TOOLTIPS.CREATE_FOLDER",
|
||||
"icon": "create_new_folder",
|
||||
"actions": {
|
||||
"click": "CREATE_FOLDER"
|
||||
},
|
||||
"rules": {
|
||||
"visible": "app.navigation.folder.canCreate"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "app.toolbar.preview",
|
||||
"type": "button",
|
||||
@ -204,16 +296,261 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "app.toolbar.separator.2",
|
||||
"order": 200,
|
||||
"type": "separator"
|
||||
"id": "app.toolbar.createLibrary",
|
||||
"type": "button",
|
||||
"title": "Create Library",
|
||||
"icon": "create_new_folder",
|
||||
"actions": {
|
||||
"click": "CREATE_LIBRARY"
|
||||
},
|
||||
"rules": {
|
||||
"visible": "app.navigation.isLibraries"
|
||||
}
|
||||
},
|
||||
{
|
||||
"disabled": true,
|
||||
"id": "app.toolbar.custom.1",
|
||||
"order": 200,
|
||||
"id": "app.toolbar.info",
|
||||
"type": "custom",
|
||||
"component": "app.demo.button"
|
||||
"component": "app.toolbar.toggleInfoDrawer",
|
||||
"rules": {
|
||||
"visible": "app.toolbar.info"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "app.toolbar.more",
|
||||
"type": "menu",
|
||||
"icon": "more_vert",
|
||||
"title": "APP.ACTIONS.MORE",
|
||||
"children": [
|
||||
{
|
||||
"id": "app.toolbar.favorite",
|
||||
"comment": "workaround for Recent Files and Search API issue",
|
||||
"type": "custom",
|
||||
"component": "app.toolbar.toggleFavorite",
|
||||
"rules": {
|
||||
"visible": "app.toolbar.favorite.canToggle"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "app.toolbar.favorite.add",
|
||||
"type": "button",
|
||||
"title": "APP.ACTIONS.FAVORITE",
|
||||
"icon": "star_border",
|
||||
"actions": {
|
||||
"click": "ADD_FAVORITE"
|
||||
},
|
||||
"rules": {
|
||||
"visible": "app.toolbar.favorite.canAdd"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "app.toolbar.favorite.remove",
|
||||
"type": "button",
|
||||
"title": "APP.ACTIONS.FAVORITE",
|
||||
"icon": "star",
|
||||
"actions": {
|
||||
"click": "REMOVE_FAVORITE"
|
||||
},
|
||||
"rules": {
|
||||
"visible": "app.toolbar.favorite.canRemove"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "app.toolbar.copy",
|
||||
"type": "button",
|
||||
"title": "APP.ACTIONS.COPY",
|
||||
"icon": "content_copy",
|
||||
"actions": {
|
||||
"click": "COPY_NODES"
|
||||
},
|
||||
"rules": {
|
||||
"visible": "app.toolbar.canCopyNode"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "app.toolbar.move",
|
||||
"type": "button",
|
||||
"title": "APP.ACTIONS.MOVE",
|
||||
"icon": "library_books",
|
||||
"actions": {
|
||||
"click": "MOVE_NODES"
|
||||
},
|
||||
"rules": {
|
||||
"visible": "app.selection.canDelete"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "app.toolbar.share",
|
||||
"type": "button",
|
||||
"title": "APP.ACTIONS.SHARE",
|
||||
"icon": "share",
|
||||
"actions": {
|
||||
"click": "SHARE_NODE"
|
||||
},
|
||||
"rules": {
|
||||
"visible": "app.selection.file.canShare"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "app.toolbar.unshare",
|
||||
"type": "button",
|
||||
"title": "APP.ACTIONS.UNSHARE",
|
||||
"icon": "stop_screen_share",
|
||||
"actions": {
|
||||
"click": "UNSHARE_NODES"
|
||||
},
|
||||
"rules": {
|
||||
"visible": "app.selection.canUnshare"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "app.toolbar.delete",
|
||||
"type": "button",
|
||||
"title": "APP.ACTIONS.DELETE",
|
||||
"icon": "delete",
|
||||
"actions": {
|
||||
"click": "DELETE_NODES"
|
||||
},
|
||||
"rules": {
|
||||
"visible": "app.selection.canDelete"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "app.toolbar.deleteLibrary",
|
||||
"type": "button",
|
||||
"title": "APP.ACTIONS.DELETE",
|
||||
"icon": "delete",
|
||||
"actions": {
|
||||
"click": "DELETE_LIBRARY"
|
||||
},
|
||||
"rules": {
|
||||
"visible": "app.selection.library"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "app.toolbar.versions",
|
||||
"type": "button",
|
||||
"title": "APP.ACTIONS.VERSIONS",
|
||||
"icon": "history",
|
||||
"actions": {
|
||||
"click": "MANAGE_VERSIONS"
|
||||
},
|
||||
"rules": {
|
||||
"visible": "app.toolbar.versions"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "app.toolbar.permissions",
|
||||
"type": "button",
|
||||
"title": "APP.ACTIONS.PERMISSIONS",
|
||||
"icon": "settings_input_component",
|
||||
"actions": {
|
||||
"click": "MANAGE_PERMISSIONS"
|
||||
},
|
||||
"rules": {
|
||||
"visible": "app.toolbar.permissions"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"viewer": {
|
||||
"actions": [
|
||||
{
|
||||
"id": "app.viewer.favorite.add",
|
||||
"type": "button",
|
||||
"title": "APP.ACTIONS.FAVORITE",
|
||||
"icon": "star_border",
|
||||
"actions": {
|
||||
"click": "ADD_FAVORITE"
|
||||
},
|
||||
"rules": {
|
||||
"visible": "app.toolbar.favorite.canAdd"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "app.viewer.favorite.remove",
|
||||
"type": "button",
|
||||
"title": "APP.ACTIONS.FAVORITE",
|
||||
"icon": "star",
|
||||
"actions": {
|
||||
"click": "REMOVE_FAVORITE"
|
||||
},
|
||||
"rules": {
|
||||
"visible": "app.toolbar.favorite.canRemove"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "app.viewer.share",
|
||||
"type": "button",
|
||||
"title": "APP.ACTIONS.SHARE",
|
||||
"icon": "share",
|
||||
"actions": {
|
||||
"click": "SHARE_NODE"
|
||||
},
|
||||
"rules": {
|
||||
"visible": "app.selection.file.canShare"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "app.viewer.copy",
|
||||
"type": "button",
|
||||
"title": "APP.ACTIONS.COPY",
|
||||
"icon": "content_copy",
|
||||
"actions": {
|
||||
"click": "COPY_NODES"
|
||||
},
|
||||
"rules": {
|
||||
"visible": "app.toolbar.canCopyNode"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "app.viewer.move",
|
||||
"type": "button",
|
||||
"title": "APP.ACTIONS.MOVE",
|
||||
"icon": "library_books",
|
||||
"actions": {
|
||||
"click": "MOVE_NODES"
|
||||
},
|
||||
"rules": {
|
||||
"visible": "app.selection.canDelete"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "app.viewer.delete",
|
||||
"type": "button",
|
||||
"title": "APP.ACTIONS.DELETE",
|
||||
"icon": "delete",
|
||||
"actions": {
|
||||
"click": "DELETE_NODES"
|
||||
},
|
||||
"rules": {
|
||||
"visible": "app.selection.canDelete"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "app.viewer.versions",
|
||||
"type": "button",
|
||||
"title": "APP.ACTIONS.VERSIONS",
|
||||
"icon": "history",
|
||||
"actions": {
|
||||
"click": "MANAGE_VERSIONS"
|
||||
},
|
||||
"rules": {
|
||||
"visible": "app.toolbar.versions"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "app.viewer.permissions",
|
||||
"type": "button",
|
||||
"title": "APP.ACTIONS.PERMISSIONS",
|
||||
"icon": "settings_input_component",
|
||||
"actions": {
|
||||
"click": "MANAGE_PERMISSIONS"
|
||||
},
|
||||
"rules": {
|
||||
"visible": "app.toolbar.permissions"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -26,11 +26,11 @@
|
||||
},
|
||||
"TOOLTIPS": {
|
||||
"CREATE_FOLDER": "Create new folder",
|
||||
"CREATE_FOLDER_NOT_ALLOWED": "You can't create a folder here. You might not have the required permissions, check with your IT Team.",
|
||||
"CREATE_FOLDER_NOT_ALLOWED": "Folders cannot be created whilst viewing the current items.",
|
||||
"UPLOAD_FILES": "Select files to upload",
|
||||
"UPLOAD_FILES_NOT_ALLOWED": "You need permissions to upload here, check with your IT Team.",
|
||||
"UPLOAD_FILES_NOT_ALLOWED": "Files cannot be uploaded whilst viewing the current items",
|
||||
"UPLOAD_FOLDERS": "Select folders to upload",
|
||||
"UPLOAD_FOLDERS_NOT_ALLOWED": "You need permissions to upload here, check with your IT Team."
|
||||
"UPLOAD_FOLDERS_NOT_ALLOWED": "Folders cannot be uploaded whilst viewing the current items"
|
||||
}
|
||||
},
|
||||
"BROWSE": {
|
||||
|
@ -27,7 +27,7 @@
|
||||
"openWith": [
|
||||
{
|
||||
"id": "plugin1.viewer.openWith.action1",
|
||||
"type": "default",
|
||||
"type": "button",
|
||||
"icon": "build",
|
||||
"title": "Snackbar",
|
||||
"actions": {
|
||||
@ -53,6 +53,52 @@
|
||||
"content": {
|
||||
"actions": [
|
||||
{
|
||||
"disabled": true,
|
||||
"id": "app.toolbar.createFolder",
|
||||
"type": "button",
|
||||
"order": 10,
|
||||
"title": "APP.NEW_MENU.MENU_ITEMS.CREATE_FOLDER",
|
||||
"description": "APP.NEW_MENU.TOOLTIPS.CREATE_FOLDER",
|
||||
"icon": "create_new_folder",
|
||||
"actions": {
|
||||
"click": "CREATE_FOLDER"
|
||||
},
|
||||
"rules": {
|
||||
"visible": "app.navigation.folder.canCreate"
|
||||
}
|
||||
},
|
||||
{
|
||||
"disabled": true,
|
||||
"id": "app.toolbar.uploadFile",
|
||||
"order": 11,
|
||||
"type": "button",
|
||||
"icon": "file_upload",
|
||||
"title": "APP.NEW_MENU.MENU_ITEMS.UPLOAD_FILE",
|
||||
"description": "APP.NEW_MENU.TOOLTIPS.UPLOAD_FILES",
|
||||
"actions": {
|
||||
"click": "UPLOAD_FILES"
|
||||
},
|
||||
"rules": {
|
||||
"visible": "app.navigation.folder.canUpload"
|
||||
}
|
||||
},
|
||||
{
|
||||
"disabled": true,
|
||||
"id": "app.toolbar.uploadFolder",
|
||||
"order": 12,
|
||||
"type": "button",
|
||||
"icon": "cloud_upload",
|
||||
"title": "APP.NEW_MENU.MENU_ITEMS.UPLOAD_FOLDER",
|
||||
"description": "APP.NEW_MENU.TOOLTIPS.UPLOAD_FOLDERS",
|
||||
"actions": {
|
||||
"click": "UPLOAD_FOLDER"
|
||||
},
|
||||
"rules": {
|
||||
"visible": "app.navigation.folder.canUpload"
|
||||
}
|
||||
},
|
||||
{
|
||||
"disabled": true,
|
||||
"id": "plugin1.toolbar.menu1",
|
||||
"type": "menu",
|
||||
"icon": "storage",
|
||||
@ -70,6 +116,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"disabled": true,
|
||||
"id": "plugin1.toolbar.separator3",
|
||||
"order": 301,
|
||||
"type": "separator"
|
||||
|
Loading…
x
Reference in New Issue
Block a user