extensions: wave 2 (#497)

* introduce "create folder" action

* track opened folder via store

* "create folder" action, support mulitple targets

* fix card view colors and toolbar layouts

* basic support for permissions

* simplify create menu and add permissions

* add toolbar separators for extension entries

* "edit folder" extension command

* minor code improvements
This commit is contained in:
Denys Vuika 2018-07-08 07:56:50 +01:00 committed by GitHub
parent bc22053e2e
commit fe683015c5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
34 changed files with 875 additions and 819 deletions

View File

@ -54,6 +54,17 @@
} }
], ],
"actions": [ "actions": [
{
"id": "aca:actions/create-folder",
"type": "CREATE_FOLDER",
"payload": null
},
{
"id": "aca:actions/edit-folder",
"type": "EDIT_FOLDER",
"payload": null
},
{ {
"id": "aca:actions/info", "id": "aca:actions/info",
"type": "SNACKBAR_INFO", "type": "SNACKBAR_INFO",
@ -78,11 +89,15 @@
"features": { "features": {
"create": [ "create": [
{ {
"id": "aca:create/action1", "disabled": false,
"id": "aca:create/folder",
"order": 100, "order": 100,
"icon": "build", "icon": "create_new_folder",
"title": "Error", "title": "ext: Create Folder",
"action": "aca:actions/error" "target": {
"permissions": ["create"],
"action": "aca:actions/create-folder"
}
} }
], ],
"navigation": { "navigation": {
@ -175,28 +190,29 @@
"actions": [ "actions": [
{ {
"disabled": false, "disabled": false,
"id": "aca:action1", "id": "aca:toolbar/create-folder",
"order": 100, "order": 10,
"title": "Info", "title": "APP.NEW_MENU.TOOLTIPS.CREATE_FOLDER",
"icon": "build", "icon": "create_new_folder",
"target": { "target": {
"type": "folder", "types": [],
"permissions": ["one", "two"], "permissions": ["parent.create"],
"action": "aca:actions/info" "action": "aca:actions/create-folder"
} }
}, },
{ {
"disabled": false, "disabled": false,
"id": "aca:action2", "id": "aca:toolbar/edit-folder",
"order": 101, "order": 20,
"title": "Node name", "title": "APP.ACTIONS.EDIT",
"icon": "feedback", "icon": "create",
"target": { "target": {
"type": "folder", "types": ["folder"],
"permissions": ["one", "two"], "permissions": ["update"],
"action": "aca:actions/node-name" "action": "aca:actions/edit-folder"
} }
}, },
{ {
"disabled": false, "disabled": false,
"id": "aca:action3", "id": "aca:action3",
@ -204,8 +220,8 @@
"title": "Settings", "title": "Settings",
"icon": "settings_applications", "icon": "settings_applications",
"target": { "target": {
"type": "folder", "types": [],
"permissions": ["one", "two"], "permissions": [],
"action": "aca:actions/settings" "action": "aca:actions/settings"
} }
}, },
@ -216,8 +232,8 @@
"title": "Error", "title": "Error",
"icon": "report_problem", "icon": "report_problem",
"target": { "target": {
"type": "file", "types": ["file"],
"permissions": ["one", "two"], "permissions": ["update", "delete"],
"action": "aca:actions/error" "action": "aca:actions/error"
} }
} }

View File

@ -61,7 +61,6 @@ import { NodePermanentDeleteDirective } from './common/directives/node-permanent
import { NodeUnshareDirective } from './common/directives/node-unshare.directive'; import { NodeUnshareDirective } from './common/directives/node-unshare.directive';
import { NodeVersionsDirective } from './common/directives/node-versions.directive'; import { NodeVersionsDirective } from './common/directives/node-versions.directive';
import { NodeVersionsDialogComponent } from './dialogs/node-versions/node-versions.dialog'; import { NodeVersionsDialogComponent } from './dialogs/node-versions/node-versions.dialog';
import { BrowsingFilesService } from './common/services/browsing-files.service';
import { ContentManagementService } from './common/services/content-management.service'; import { ContentManagementService } from './common/services/content-management.service';
import { NodeActionsService } from './common/services/node-actions.service'; import { NodeActionsService } from './common/services/node-actions.service';
import { NodePermissionService } from './common/services/node-permission.service'; import { NodePermissionService } from './common/services/node-permission.service';
@ -72,7 +71,6 @@ import { ExperimentalGuard } from './common/services/experimental-guard.service'
import { InfoDrawerComponent } from './components/info-drawer/info-drawer.component'; import { InfoDrawerComponent } from './components/info-drawer/info-drawer.component';
import { EditFolderDirective } from './directives/edit-folder.directive'; import { EditFolderDirective } from './directives/edit-folder.directive';
import { CreateFolderDirective } from './directives/create-folder.directive';
import { DownloadNodesDirective } from './directives/download-nodes.directive'; import { DownloadNodesDirective } from './directives/download-nodes.directive';
import { AppStoreModule } from './store/app-store.module'; import { AppStoreModule } from './store/app-store.module';
import { PaginationDirective } from './directives/pagination.directive'; import { PaginationDirective } from './directives/pagination.directive';
@ -136,7 +134,6 @@ import { SearchResultsRowComponent } from './components/search/search-results-ro
InfoDrawerComponent, InfoDrawerComponent,
SharedLinkViewComponent, SharedLinkViewComponent,
EditFolderDirective, EditFolderDirective,
CreateFolderDirective,
DownloadNodesDirective, DownloadNodesDirective,
PaginationDirective, PaginationDirective,
DocumentListDirective, DocumentListDirective,
@ -152,7 +149,6 @@ import { SearchResultsRowComponent } from './components/search/search-results-ro
source: 'assets' source: 'assets'
} }
}, },
BrowsingFilesService,
ContentManagementService, ContentManagementService,
NodeActionsService, NodeActionsService,
NodePermissionService, NodePermissionService,

View File

@ -1,44 +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 { BrowsingFilesService } from './browsing-files.service';
describe('BrowsingFilesService', () => {
let service: BrowsingFilesService;
beforeEach(() => {
service = new BrowsingFilesService();
});
it('subscribes to event', () => {
const value: any = 'test-value';
service.onChangeParent.subscribe((result) => {
expect(result).toBe(value);
});
service.onChangeParent.next(<any>value);
});
});

View File

@ -1,34 +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 { Subject } from 'rxjs/Rx';
import { Injectable } from '@angular/core';
import { MinimalNodeEntryEntity } from 'alfresco-js-api';
@Injectable()
export class BrowsingFilesService {
onChangeParent = new Subject<MinimalNodeEntryEntity>();
}

View File

@ -25,6 +25,12 @@
import { Subject } from 'rxjs/Rx'; import { Subject } from 'rxjs/Rx';
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { MatDialog } from '@angular/material';
import { FolderDialogComponent } from '@alfresco/adf-content-services';
import { SnackbarErrorAction } from '../../store/actions';
import { Store } from '@ngrx/store';
import { AppStore } from '../../store/states';
import { MinimalNodeEntity, MinimalNodeEntryEntity } from 'alfresco-js-api';
@Injectable() @Injectable()
export class ContentManagementService { export class ContentManagementService {
@ -36,4 +42,50 @@ export class ContentManagementService {
folderCreated = new Subject<any>(); folderCreated = new Subject<any>();
siteDeleted = new Subject<string>(); siteDeleted = new Subject<string>();
linksUnshared = new Subject<any>(); linksUnshared = new Subject<any>();
constructor(private store: Store<AppStore>, private dialogRef: MatDialog) {}
createFolder(parentNodeId: string) {
const dialogInstance = this.dialogRef.open(FolderDialogComponent, {
data: {
parentNodeId: parentNodeId,
createTitle: undefined,
nodeType: 'cm:folder'
},
width: '400px'
});
dialogInstance.componentInstance.error.subscribe(message => {
this.store.dispatch(new SnackbarErrorAction(message));
});
dialogInstance.afterClosed().subscribe(node => {
if (node) {
this.folderCreated.next(node);
}
});
}
editFolder(folder: MinimalNodeEntity) {
if (!folder) {
return;
}
const dialog = this.dialogRef.open(FolderDialogComponent, {
data: {
folder: folder.entry
},
width: '400px'
});
dialog.componentInstance.error.subscribe(message => {
this.store.dispatch(new SnackbarErrorAction(message));
});
dialog.afterClosed().subscribe((node: MinimalNodeEntryEntity) => {
if (node) {
this.folderEdited.next(node);
}
});
}
} }

View File

@ -3,14 +3,17 @@
<adf-breadcrumb root="APP.BROWSE.FAVORITES.TITLE"> <adf-breadcrumb root="APP.BROWSE.FAVORITES.TITLE">
</adf-breadcrumb> </adf-breadcrumb>
<button *ifExperimental="'cardview'" <adf-toolbar class="inline">
mat-icon-button <button *ifExperimental="'cardview'"
(click)="toggleGalleryView()"> color="primary"
<mat-icon *ngIf="displayMode === 'list'" matTooltip="{{ 'APP.DOCUMENT_LIST.TOOLBAR.CARDVIEW' | translate }}">view_comfy</mat-icon> mat-icon-button
<mat-icon *ngIf="displayMode === 'gallery'" matTooltip="{{ 'APP.DOCUMENT_LIST.TOOLBAR.LISTVIEW' | translate }}">list</mat-icon> (click)="toggleGalleryView()">
</button> <mat-icon *ngIf="displayMode === 'list'" matTooltip="{{ 'APP.DOCUMENT_LIST.TOOLBAR.CARDVIEW' | translate }}">view_comfy</mat-icon>
<adf-toolbar class="inline" *ngIf="!selection.isEmpty"> <mat-icon *ngIf="displayMode === 'gallery'" matTooltip="{{ 'APP.DOCUMENT_LIST.TOOLBAR.LISTVIEW' | translate }}">list</mat-icon>
</button>
<ng-container *ifExperimental="'extensions'"> <ng-container *ifExperimental="'extensions'">
<adf-toolbar-divider></adf-toolbar-divider>
<button *ngFor="let entry of actions" <button *ngFor="let entry of actions"
mat-icon-button mat-icon-button
color="primary" color="primary"
@ -18,99 +21,103 @@
(click)="runAction(entry.target.action)"> (click)="runAction(entry.target.action)">
<mat-icon>{{ entry.icon }}</mat-icon> <mat-icon>{{ entry.icon }}</mat-icon>
</button> </button>
<adf-toolbar-divider></adf-toolbar-divider>
</ng-container> </ng-container>
<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 <ng-container *ngIf="!selection.isEmpty">
mat-icon-button <button
color="primary" mat-icon-button
title="{{ 'APP.ACTIONS.DOWNLOAD' | translate }}"
[adfNodeDownload]="selection.nodes">
<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" color="primary"
title="{{ 'APP.ACTIONS.SHARE' | translate }}"
*ngIf="selection.file" *ngIf="selection.file"
[baseShareUrl]="sharedPreviewUrl$ | async" title="{{ 'APP.ACTIONS.VIEW' | translate }}"
[adf-share]="selection.file"> (click)="showPreview(selection.file)">
<mat-icon>share</mat-icon> <mat-icon>open_in_browser</mat-icon>
</button> </button>
<button
mat-icon-button
color="primary"
title="{{ 'APP.ACTIONS.DOWNLOAD' | translate }}"
[adfNodeDownload]="selection.nodes">
<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>
</mat-menu>
</ng-container> </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>
</mat-menu>
</adf-toolbar> </adf-toolbar>
</div> </div>
<div class="inner-layout__content"> <div class="inner-layout__content">

View File

@ -6,14 +6,17 @@
(navigate)="onBreadcrumbNavigate($event)"> (navigate)="onBreadcrumbNavigate($event)">
</adf-breadcrumb> </adf-breadcrumb>
<button *ifExperimental="'cardview'" <adf-toolbar class="inline">
mat-icon-button <button *ifExperimental="'cardview'"
(click)="toggleGalleryView()"> color="primary"
<mat-icon *ngIf="displayMode === 'list'" matTooltip="{{ 'APP.DOCUMENT_LIST.TOOLBAR.CARDVIEW' | translate }}">view_comfy</mat-icon> mat-icon-button
<mat-icon *ngIf="displayMode === 'gallery'" matTooltip="{{ 'APP.DOCUMENT_LIST.TOOLBAR.LISTVIEW' | translate }}">list</mat-icon> (click)="toggleGalleryView()">
</button> <mat-icon *ngIf="displayMode === 'list'" matTooltip="{{ 'APP.DOCUMENT_LIST.TOOLBAR.CARDVIEW' | translate }}">view_comfy</mat-icon>
<adf-toolbar class="inline" *ngIf="!selection.isEmpty"> <mat-icon *ngIf="displayMode === 'gallery'" matTooltip="{{ 'APP.DOCUMENT_LIST.TOOLBAR.LISTVIEW' | translate }}">list</mat-icon>
</button>
<ng-container *ifExperimental="'extensions'"> <ng-container *ifExperimental="'extensions'">
<adf-toolbar-divider></adf-toolbar-divider>
<button *ngFor="let entry of actions" <button *ngFor="let entry of actions"
mat-icon-button mat-icon-button
color="primary" color="primary"
@ -21,101 +24,105 @@
(click)="runAction(entry.target.action)"> (click)="runAction(entry.target.action)">
<mat-icon>{{ entry.icon }}</mat-icon> <mat-icon>{{ entry.icon }}</mat-icon>
</button> </button>
<adf-toolbar-divider></adf-toolbar-divider>
</ng-container> </ng-container>
<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 <ng-container *ngIf="!selection.isEmpty">
color="primary" <button
mat-icon-button
title="{{ 'APP.ACTIONS.DOWNLOAD' | translate }}"
[adfNodeDownload]="selection.nodes">
<mat-icon>get_app</mat-icon>
</button>
<button
color="primary"
mat-icon-button
*ngIf="selection.folder && permission.check(selection.folder, ['update'])"
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" color="primary"
title="{{ 'APP.ACTIONS.SHARE' | translate }}" mat-icon-button
*ngIf="selection.file" *ngIf="selection.file"
[baseShareUrl]="sharedPreviewUrl$ | async" title="{{ 'APP.ACTIONS.VIEW' | translate }}"
[adf-share]="selection.file"> (click)="showPreview(selection.file)">
<mat-icon>share</mat-icon> <mat-icon>open_in_browser</mat-icon>
</button> </button>
<button
color="primary"
mat-icon-button
title="{{ 'APP.ACTIONS.DOWNLOAD' | translate }}"
[adfNodeDownload]="selection.nodes">
<mat-icon>get_app</mat-icon>
</button>
<button
color="primary"
mat-icon-button
*ngIf="selection.folder && permission.check(selection.folder, ['update'])"
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="permission.check(selection.nodes, ['delete'])"
[acaMoveNode]="selection.nodes">
<mat-icon>library_books</mat-icon>
<span>{{ 'APP.ACTIONS.MOVE' | translate }}</span>
</button>
<button
mat-menu-item
*ngIf="permission.check(selection.nodes, ['delete'])"
[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>
</mat-menu>
</ng-container> </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="permission.check(selection.nodes, ['delete'])"
[acaMoveNode]="selection.nodes">
<mat-icon>library_books</mat-icon>
<span>{{ 'APP.ACTIONS.MOVE' | translate }}</span>
</button>
<button
mat-menu-item
*ngIf="permission.check(selection.nodes, ['delete'])"
[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>
</mat-menu>
</adf-toolbar> </adf-toolbar>
</div> </div>

View File

@ -33,7 +33,6 @@ import {
} from '@alfresco/adf-core'; } from '@alfresco/adf-core';
import { DocumentListComponent } from '@alfresco/adf-content-services'; import { DocumentListComponent } from '@alfresco/adf-content-services';
import { ContentManagementService } from '../../common/services/content-management.service'; import { ContentManagementService } from '../../common/services/content-management.service';
import { BrowsingFilesService } from '../../common/services/browsing-files.service';
import { NodeActionsService } from '../../common/services/node-actions.service'; import { NodeActionsService } from '../../common/services/node-actions.service';
import { FilesComponent } from './files.component'; import { FilesComponent } from './files.component';
import { AppTestingModule } from '../../testing/app-testing.module'; import { AppTestingModule } from '../../testing/app-testing.module';
@ -48,7 +47,6 @@ describe('FilesComponent', () => {
let contentManagementService: ContentManagementService; let contentManagementService: ContentManagementService;
let uploadService: UploadService; let uploadService: UploadService;
let router: Router; let router: Router;
let browsingFilesService: BrowsingFilesService;
let nodeActionsService: NodeActionsService; let nodeActionsService: NodeActionsService;
let contentApi: ContentApiService; let contentApi: ContentApiService;
@ -86,7 +84,6 @@ describe('FilesComponent', () => {
contentManagementService = TestBed.get(ContentManagementService); contentManagementService = TestBed.get(ContentManagementService);
uploadService = TestBed.get(UploadService); uploadService = TestBed.get(UploadService);
router = TestBed.get(Router); router = TestBed.get(Router);
browsingFilesService = TestBed.get(BrowsingFilesService);
nodeActionsService = TestBed.get(NodeActionsService); nodeActionsService = TestBed.get(NodeActionsService);
contentApi = TestBed.get(ContentApiService); contentApi = TestBed.get(ContentApiService);
}); });
@ -146,17 +143,6 @@ describe('FilesComponent', () => {
expect(component.fetchNodes).toHaveBeenCalled(); expect(component.fetchNodes).toHaveBeenCalled();
}); });
it('emits onChangeParent event', () => {
spyOn(contentApi, 'getNode').and.returnValue(Observable.of({ entry: node }));
spyOn(component, 'fetchNodes').and.returnValue(Observable.of(page));
spyOn(browsingFilesService.onChangeParent, 'next').and.callFake((val) => val);
fixture.detectChanges();
expect(browsingFilesService.onChangeParent.next)
.toHaveBeenCalledWith(node);
});
it('if should navigate to parent if node is not a folder', () => { it('if should navigate to parent if node is not a folder', () => {
node.isFolder = false; node.isFolder = false;
node.parentId = 'parent-id'; node.parentId = 'parent-id';

View File

@ -29,7 +29,6 @@ import { ActivatedRoute, Params, Router } from '@angular/router';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { MinimalNodeEntity, MinimalNodeEntryEntity, NodePaging, PathElement, PathElementEntity } from 'alfresco-js-api'; import { MinimalNodeEntity, MinimalNodeEntryEntity, NodePaging, PathElement, PathElementEntity } from 'alfresco-js-api';
import { Observable } from 'rxjs/Rx'; import { Observable } from 'rxjs/Rx';
import { BrowsingFilesService } from '../../common/services/browsing-files.service';
import { ContentManagementService } from '../../common/services/content-management.service'; import { ContentManagementService } from '../../common/services/content-management.service';
import { NodeActionsService } from '../../common/services/node-actions.service'; import { NodeActionsService } from '../../common/services/node-actions.service';
import { NodePermissionService } from '../../common/services/node-permission.service'; import { NodePermissionService } from '../../common/services/node-permission.service';
@ -37,6 +36,7 @@ import { AppStore } from '../../store/states/app.state';
import { PageComponent } from '../page.component'; import { PageComponent } from '../page.component';
import { ContentApiService } from '../../services/content-api.service'; import { ContentApiService } from '../../services/content-api.service';
import { ExtensionService } from '../../extensions/extension.service'; import { ExtensionService } from '../../extensions/extension.service';
import { SetCurrentFolderAction } from '../../store/actions';
@Component({ @Component({
templateUrl: './files.component.html' templateUrl: './files.component.html'
@ -54,7 +54,6 @@ export class FilesComponent extends PageComponent implements OnInit, OnDestroy {
private nodeActionsService: NodeActionsService, private nodeActionsService: NodeActionsService,
private uploadService: UploadService, private uploadService: UploadService,
private contentManagementService: ContentManagementService, private contentManagementService: ContentManagementService,
private browsingFilesService: BrowsingFilesService,
public permission: NodePermissionService, public permission: NodePermissionService,
extensions: ExtensionService) { extensions: ExtensionService) {
super(store, extensions); super(store, extensions);
@ -103,7 +102,7 @@ export class FilesComponent extends PageComponent implements OnInit, OnDestroy {
ngOnDestroy() { ngOnDestroy() {
super.ngOnDestroy(); super.ngOnDestroy();
this.browsingFilesService.onChangeParent.next(null); this.store.dispatch(new SetCurrentFolderAction(null));
} }
fetchNodes(parentNodeId?: string): Observable<NodePaging> { fetchNodes(parentNodeId?: string): Observable<NodePaging> {
@ -222,7 +221,7 @@ export class FilesComponent extends PageComponent implements OnInit, OnDestroy {
} }
this.node = node; this.node = node;
this.browsingFilesService.onChangeParent.next(node); this.store.dispatch(new SetCurrentFolderAction(node));
} }
// todo: review this approach once 5.2.3 is out // todo: review this approach once 5.2.3 is out

View File

@ -1,7 +1,7 @@
<div class="layout"> <div class="layout">
<adf-upload-drag-area <adf-upload-drag-area
[parentId]="node?.id" [parentId]="node?.id"
[disabled]="!permission.check(node, ['create'])"> [disabled]="!canUpload">
<adf-sidenav-layout <adf-sidenav-layout
#sidenavManager="acaSidenavManager" #sidenavManager="acaSidenavManager"

View File

@ -25,10 +25,8 @@
import { NO_ERRORS_SCHEMA } from '@angular/core'; import { NO_ERRORS_SCHEMA } from '@angular/core';
import { TestBed, ComponentFixture } from '@angular/core/testing'; import { TestBed, ComponentFixture } from '@angular/core/testing';
import { MinimalNodeEntryEntity } from 'alfresco-js-api';
import { PeopleContentService, AppConfigService, UserPreferencesService } from '@alfresco/adf-core'; import { PeopleContentService, AppConfigService, UserPreferencesService } from '@alfresco/adf-core';
import { Observable } from 'rxjs/Observable'; import { Observable } from 'rxjs/Observable';
import { BrowsingFilesService } from '../../common/services/browsing-files.service';
import { LayoutComponent } from './layout.component'; import { LayoutComponent } from './layout.component';
import { SidenavViewsManagerDirective } from './sidenav-views-manager.directive'; import { SidenavViewsManagerDirective } from './sidenav-views-manager.directive';
import { AppTestingModule } from '../../testing/app-testing.module'; import { AppTestingModule } from '../../testing/app-testing.module';
@ -36,17 +34,9 @@ import { AppTestingModule } from '../../testing/app-testing.module';
describe('LayoutComponent', () => { describe('LayoutComponent', () => {
let fixture: ComponentFixture<LayoutComponent>; let fixture: ComponentFixture<LayoutComponent>;
let component: LayoutComponent; let component: LayoutComponent;
let browsingFilesService: BrowsingFilesService;
let appConfig: AppConfigService; let appConfig: AppConfigService;
let userPreference: UserPreferencesService; let userPreference: UserPreferencesService;
const navItem = {
label: 'some-label',
route: {
url: '/some-url'
}
};
beforeEach(() => { beforeEach(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [ AppTestingModule ], imports: [ AppTestingModule ],
@ -67,25 +57,10 @@ describe('LayoutComponent', () => {
fixture = TestBed.createComponent(LayoutComponent); fixture = TestBed.createComponent(LayoutComponent);
component = fixture.componentInstance; component = fixture.componentInstance;
browsingFilesService = TestBed.get(BrowsingFilesService);
appConfig = TestBed.get(AppConfigService); appConfig = TestBed.get(AppConfigService);
userPreference = TestBed.get(UserPreferencesService); userPreference = TestBed.get(UserPreferencesService);
}); });
it('sets current node', () => {
appConfig.config = {
navigation: [navItem]
};
const currentNode = <MinimalNodeEntryEntity>{ id: 'someId' };
fixture.detectChanges();
browsingFilesService.onChangeParent.next(currentNode);
expect(component.node).toEqual(currentNode);
});
describe('sidenav state', () => { describe('sidenav state', () => {
it('should get state from configuration', () => { it('should get state from configuration', () => {
appConfig.config = { appConfig.config = {

View File

@ -24,11 +24,14 @@
*/ */
import { Component, OnInit, OnDestroy, ViewChild } from '@angular/core'; import { Component, OnInit, OnDestroy, ViewChild } from '@angular/core';
import { Subscription } from 'rxjs/Rx'; import { Subject } from 'rxjs/Rx';
import { MinimalNodeEntryEntity } from 'alfresco-js-api'; import { MinimalNodeEntryEntity } from 'alfresco-js-api';
import { BrowsingFilesService } from '../../common/services/browsing-files.service';
import { NodePermissionService } from '../../common/services/node-permission.service'; import { NodePermissionService } from '../../common/services/node-permission.service';
import { SidenavViewsManagerDirective } from './sidenav-views-manager.directive'; import { SidenavViewsManagerDirective } from './sidenav-views-manager.directive';
import { Store } from '@ngrx/store';
import { AppStore } from '../../store/states';
import { currentFolder } from '../../store/selectors/app.selectors';
import { takeUntil } from 'rxjs/operators';
@Component({ @Component({
selector: 'app-layout', selector: 'app-layout',
@ -38,14 +41,14 @@ import { SidenavViewsManagerDirective } from './sidenav-views-manager.directive'
export class LayoutComponent implements OnInit, OnDestroy { export class LayoutComponent implements OnInit, OnDestroy {
@ViewChild(SidenavViewsManagerDirective) manager: SidenavViewsManagerDirective; @ViewChild(SidenavViewsManagerDirective) manager: SidenavViewsManagerDirective;
onDestroy$: Subject<boolean> = new Subject<boolean>();
expandedSidenav: boolean; expandedSidenav: boolean;
node: MinimalNodeEntryEntity; node: MinimalNodeEntryEntity;
canUpload = false;
private subscriptions: Subscription[] = [];
constructor( constructor(
private browsingFilesService: BrowsingFilesService, protected store: Store<AppStore>,
public permission: NodePermissionService) {} private permission: NodePermissionService) {}
ngOnInit() { ngOnInit() {
if (!this.manager.minimizeSidenav) { if (!this.manager.minimizeSidenav) {
@ -56,12 +59,16 @@ export class LayoutComponent implements OnInit, OnDestroy {
this.manager.run(true); this.manager.run(true);
this.subscriptions.concat([ this.store.select(currentFolder)
this.browsingFilesService.onChangeParent.subscribe((node: MinimalNodeEntryEntity) => this.node = node) .pipe(takeUntil(this.onDestroy$))
]); .subscribe(node => {
this.node = node;
this.canUpload = this.permission.check(node, ['create']);
});
} }
ngOnDestroy() { ngOnDestroy() {
this.subscriptions.forEach(s => s.unsubscribe()); this.onDestroy$.next(true);
this.onDestroy$.complete();
} }
} }

View File

@ -3,29 +3,33 @@
<adf-breadcrumb root="APP.BROWSE.LIBRARIES.TITLE"> <adf-breadcrumb root="APP.BROWSE.LIBRARIES.TITLE">
</adf-breadcrumb> </adf-breadcrumb>
<button *ifExperimental="'cardview'" <adf-toolbar class="inline">
mat-icon-button <button *ifExperimental="'cardview'"
(click)="toggleGalleryView()"> mat-icon-button
<mat-icon *ngIf="displayMode === 'list'" matTooltip="{{ 'APP.DOCUMENT_LIST.TOOLBAR.CARDVIEW' | translate }}">view_comfy</mat-icon> color="primary"
<mat-icon *ngIf="displayMode === 'gallery'" matTooltip="{{ 'APP.DOCUMENT_LIST.TOOLBAR.LISTVIEW' | translate }}">list</mat-icon> (click)="toggleGalleryView()">
</button> <mat-icon *ngIf="displayMode === 'list'" matTooltip="{{ 'APP.DOCUMENT_LIST.TOOLBAR.CARDVIEW' | translate }}">view_comfy</mat-icon>
<adf-toolbar class="inline" *ngIf="!selection.isEmpty"> <mat-icon *ngIf="displayMode === 'gallery'" matTooltip="{{ 'APP.DOCUMENT_LIST.TOOLBAR.LISTVIEW' | translate }}">list</mat-icon>
<ng-container *ifExperimental="'libraries'"> </button>
<button
color="primary" <ng-container *ngIf="!selection.isEmpty">
mat-icon-button <ng-container *ifExperimental="'libraries'">
title="{{ 'APP.ACTIONS.MORE' | translate }}"
[matMenuTriggerFor]="actionsMenu">
<mat-icon>more_vert</mat-icon>
</button>
<mat-menu #actionsMenu="matMenu" [overlapTrigger]="false">
<button <button
mat-menu-item color="primary"
(click)="deleteLibrary(selection.first)"> mat-icon-button
<mat-icon>delete</mat-icon> title="{{ 'APP.ACTIONS.MORE' | translate }}"
<span>{{ 'APP.ACTIONS.DELETE' | translate }}</span> [matMenuTriggerFor]="actionsMenu">
<mat-icon>more_vert</mat-icon>
</button> </button>
</mat-menu> <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>
</ng-container> </ng-container>
</adf-toolbar> </adf-toolbar>
</div> </div>

View File

@ -73,22 +73,8 @@ export abstract class PageComponent implements OnInit, OnDestroy {
this.selection = selection; this.selection = selection;
if (selection.isEmpty) { if (selection.isEmpty) {
this.infoDrawerOpened = false; this.infoDrawerOpened = false;
this.actions = [];
} else {
this.actions = this.extensions.contentActions.filter(action => {
if (action.target && action.target.type) {
switch (action.target.type.toLowerCase()) {
case 'folder':
return selection.folder ? true : false;
case 'file':
return selection.file ? true : false;
default:
return false;
}
}
return false;
});
} }
this.actions = this.extensions.getSelectedContentActions(selection, this.node);
}); });
} }

View File

@ -3,14 +3,17 @@
<adf-breadcrumb root="APP.BROWSE.RECENT.TITLE"> <adf-breadcrumb root="APP.BROWSE.RECENT.TITLE">
</adf-breadcrumb> </adf-breadcrumb>
<button *ifExperimental="'cardview'" <adf-toolbar class="inline">
mat-icon-button <button *ifExperimental="'cardview'"
(click)="toggleGalleryView()"> color="primary"
<mat-icon *ngIf="displayMode === 'list'" matTooltip="{{ 'APP.DOCUMENT_LIST.TOOLBAR.CARDVIEW' | translate }}">view_comfy</mat-icon> mat-icon-button
<mat-icon *ngIf="displayMode === 'gallery'" matTooltip="{{ 'APP.DOCUMENT_LIST.TOOLBAR.LISTVIEW' | translate }}">list</mat-icon> (click)="toggleGalleryView()">
</button> <mat-icon *ngIf="displayMode === 'list'" matTooltip="{{ 'APP.DOCUMENT_LIST.TOOLBAR.CARDVIEW' | translate }}">view_comfy</mat-icon>
<adf-toolbar class="inline" *ngIf="!selection.isEmpty"> <mat-icon *ngIf="displayMode === 'gallery'" matTooltip="{{ 'APP.DOCUMENT_LIST.TOOLBAR.LISTVIEW' | translate }}">list</mat-icon>
</button>
<ng-container *ifExperimental="'extensions'"> <ng-container *ifExperimental="'extensions'">
<adf-toolbar-divider></adf-toolbar-divider>
<button *ngFor="let entry of actions" <button *ngFor="let entry of actions"
mat-icon-button mat-icon-button
color="primary" color="primary"
@ -18,91 +21,96 @@
(click)="runAction(entry.target.action)"> (click)="runAction(entry.target.action)">
<mat-icon>{{ entry.icon }}</mat-icon> <mat-icon>{{ entry.icon }}</mat-icon>
</button> </button>
<adf-toolbar-divider></adf-toolbar-divider>
</ng-container> </ng-container>
<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 <ng-container *ngIf="!selection.isEmpty">
mat-icon-button <button
color="primary" mat-icon-button
title="{{ 'APP.ACTIONS.DOWNLOAD' | translate }}" color="primary"
[adfNodeDownload]="selection.nodes"> *ngIf="selection.file"
<mat-icon>get_app</mat-icon> title="{{ 'APP.ACTIONS.VIEW' | translate }}"
</button> (click)="showPreview(selection.file)">
<mat-icon>open_in_browser</mat-icon>
</button>
<button mat-icon-button <button
[color]="infoDrawerOpened ? 'accent' : 'primary'" mat-icon-button
title="{{ 'APP.ACTIONS.DETAILS' | translate }}" color="primary"
(click)="toggleSidebar()"> title="{{ 'APP.ACTIONS.DOWNLOAD' | translate }}"
<mat-icon>info_outline</mat-icon> [adfNodeDownload]="selection.nodes">
</button> <mat-icon>get_app</mat-icon>
</button>
<ng-container *ifExperimental="'share'">
<button mat-icon-button <button mat-icon-button
title="{{ 'APP.ACTIONS.SHARE' | translate }}" [color]="infoDrawerOpened ? 'accent' : 'primary'"
*ngIf="selection.file" title="{{ 'APP.ACTIONS.DETAILS' | translate }}"
[baseShareUrl]="sharedPreviewUrl$ | async" (click)="toggleSidebar()">
[adf-share]="selection.file"> <mat-icon>info_outline</mat-icon>
<mat-icon>share</mat-icon>
</button> </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="permission.check(selection.nodes, ['delete'])"
[acaMoveNode]="selection.nodes">
<mat-icon>library_books</mat-icon>
<span>{{ 'APP.ACTIONS.MOVE' | translate }}</span>
</button>
<button
mat-menu-item
*ngIf="permission.check(selection.nodes, ['delete'])"
[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>
</mat-menu>
</ng-container> </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="permission.check(selection.nodes, ['delete'])"
[acaMoveNode]="selection.nodes">
<mat-icon>library_books</mat-icon>
<span>{{ 'APP.ACTIONS.MOVE' | translate }}</span>
</button>
<button
mat-menu-item
*ngIf="permission.check(selection.nodes, ['delete'])"
[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>
</mat-menu>
</adf-toolbar> </adf-toolbar>
</div> </div>

View File

@ -2,8 +2,9 @@
<div class="inner-layout__header"> <div class="inner-layout__header">
<adf-breadcrumb root="APP.BROWSE.SEARCH.TITLE"> <adf-breadcrumb root="APP.BROWSE.SEARCH.TITLE">
</adf-breadcrumb> </adf-breadcrumb>
<adf-toolbar class="inline" *ngIf="!selection.isEmpty"> <adf-toolbar class="inline">
<ng-container *ifExperimental="'extensions'"> <ng-container *ifExperimental="'extensions'">
<adf-toolbar-divider></adf-toolbar-divider>
<button *ngFor="let entry of actions" <button *ngFor="let entry of actions"
mat-icon-button mat-icon-button
color="primary" color="primary"
@ -11,64 +12,68 @@
(click)="runAction(entry.target.action)"> (click)="runAction(entry.target.action)">
<mat-icon>{{ entry.icon }}</mat-icon> <mat-icon>{{ entry.icon }}</mat-icon>
</button> </button>
<adf-toolbar-divider></adf-toolbar-divider>
</ng-container> </ng-container>
<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 <ng-container *ngIf="!selection.isEmpty">
color="primary"
mat-icon-button
title="{{ 'APP.ACTIONS.DOWNLOAD' | translate }}"
[acaDownloadNodes]="selection.nodes">
<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 <button
mat-menu-item color="primary"
#favorites="adfFavorite" mat-icon-button
[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" *ngIf="selection.file"
[acaNodeVersions]="selection.file"> title="{{ 'APP.ACTIONS.VIEW' | translate }}"
<mat-icon>history</mat-icon> (click)="showPreview(selection.file)">
<span>{{ 'APP.ACTIONS.VERSIONS' | translate }}</span> <mat-icon>open_in_browser</mat-icon>
</button> </button>
</mat-menu>
<button
color="primary"
mat-icon-button
title="{{ 'APP.ACTIONS.DOWNLOAD' | translate }}"
[acaDownloadNodes]="selection.nodes">
<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>
</mat-menu>
</ng-container>
</adf-toolbar> </adf-toolbar>
</div> </div>

View File

@ -3,14 +3,17 @@
<adf-breadcrumb root="APP.BROWSE.SHARED.TITLE"> <adf-breadcrumb root="APP.BROWSE.SHARED.TITLE">
</adf-breadcrumb> </adf-breadcrumb>
<button *ifExperimental="'cardview'" <adf-toolbar class="inline">
mat-icon-button <button *ifExperimental="'cardview'"
(click)="toggleGalleryView()"> color="primary"
<mat-icon *ngIf="displayMode === 'list'" matTooltip="{{ 'APP.DOCUMENT_LIST.TOOLBAR.CARDVIEW' | translate }}">view_comfy</mat-icon> mat-icon-button
<mat-icon *ngIf="displayMode === 'gallery'" matTooltip="{{ 'APP.DOCUMENT_LIST.TOOLBAR.LISTVIEW' | translate }}">list</mat-icon> (click)="toggleGalleryView()">
</button> <mat-icon *ngIf="displayMode === 'list'" matTooltip="{{ 'APP.DOCUMENT_LIST.TOOLBAR.CARDVIEW' | translate }}">view_comfy</mat-icon>
<adf-toolbar class="inline" *ngIf="!selection.isEmpty"> <mat-icon *ngIf="displayMode === 'gallery'" matTooltip="{{ 'APP.DOCUMENT_LIST.TOOLBAR.LISTVIEW' | translate }}">list</mat-icon>
</button>
<ng-container *ifExperimental="'extensions'"> <ng-container *ifExperimental="'extensions'">
<adf-toolbar-divider></adf-toolbar-divider>
<button *ngFor="let entry of actions" <button *ngFor="let entry of actions"
mat-icon-button mat-icon-button
color="primary" color="primary"
@ -18,89 +21,93 @@
(click)="runAction(entry.target.action)"> (click)="runAction(entry.target.action)">
<mat-icon>{{ entry.icon }}</mat-icon> <mat-icon>{{ entry.icon }}</mat-icon>
</button> </button>
<adf-toolbar-divider></adf-toolbar-divider>
</ng-container> </ng-container>
<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 <ng-container *ngIf="!selection.isEmpty">
color="primary"
mat-icon-button
title="{{ 'APP.ACTIONS.DOWNLOAD' | translate }}"
[adfNodeDownload]="selection.nodes">
<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 <button
mat-menu-item *ngIf="selection.count === 1"
#favorites="adfFavorite" color="primary"
[adf-node-favorite]="selection.nodes"> mat-icon-button
<mat-icon color="primary" *ngIf="favorites.hasFavorites()">star</mat-icon> title="{{ 'APP.ACTIONS.VIEW' | translate }}"
<mat-icon *ngIf="!favorites.hasFavorites()">star_border</mat-icon> (click)="showPreview(selection.nodes[0])">
<span>{{ 'APP.ACTIONS.FAVORITE' | translate }}</span> <mat-icon>open_in_browser</mat-icon>
</button> </button>
<button <button
mat-menu-item color="primary"
[acaCopyNode]="selection.nodes"> mat-icon-button
<mat-icon>content_copy</mat-icon> title="{{ 'APP.ACTIONS.DOWNLOAD' | translate }}"
<span>{{ 'APP.ACTIONS.COPY' | translate }}</span> [adfNodeDownload]="selection.nodes">
<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>
<button <button
mat-menu-item color="primary"
*ngIf="permission.check(selection.nodes, ['delete'], { target: 'allowableOperationsOnTarget' })" mat-icon-button
[acaMoveNode]="selection.nodes"> title="{{ 'APP.ACTIONS.MORE' | translate }}"
<mat-icon>library_books</mat-icon> [matMenuTriggerFor]="actionsMenu">
<span>{{ 'APP.ACTIONS.MOVE' | translate }}</span> <mat-icon>more_vert</mat-icon>
</button> </button>
<button <mat-menu #actionsMenu="matMenu"
mat-menu-item [overlapTrigger]="false">
*ngIf="permission.check(selection.nodes, ['delete'])" <button
[acaUnshareNode]="selection.nodes"> mat-menu-item
<mat-icon>stop_screen_share</mat-icon> #favorites="adfFavorite"
<span>{{ 'APP.ACTIONS.UNSHARE' | translate }}</span> [adf-node-favorite]="selection.nodes">
</button> <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 <button
mat-menu-item mat-menu-item
*ngIf="permission.check(selection.nodes, ['delete'], { target: 'allowableOperationsOnTarget' })" [acaCopyNode]="selection.nodes">
[acaDeleteNode]="selection.nodes"> <mat-icon>content_copy</mat-icon>
<mat-icon>delete</mat-icon> <span>{{ 'APP.ACTIONS.COPY' | translate }}</span>
<span>{{ 'APP.ACTIONS.DELETE' | translate }}</span> </button>
</button>
<button <button
mat-menu-item mat-menu-item
*ngIf="selection.file && permission.check(selection.file, ['update'], { target: 'allowableOperationsOnTarget' })" *ngIf="permission.check(selection.nodes, ['delete'], { target: 'allowableOperationsOnTarget' })"
[acaNodeVersions]="selection.file"> [acaMoveNode]="selection.nodes">
<mat-icon>history</mat-icon> <mat-icon>library_books</mat-icon>
<span>{{ 'APP.ACTIONS.VERSIONS' | translate }}</span> <span>{{ 'APP.ACTIONS.MOVE' | translate }}</span>
</button> </button>
</mat-menu>
<button
mat-menu-item
*ngIf="permission.check(selection.nodes, ['delete'])"
[acaUnshareNode]="selection.nodes">
<mat-icon>stop_screen_share</mat-icon>
<span>{{ 'APP.ACTIONS.UNSHARE' | translate }}</span>
</button>
<button
mat-menu-item
*ngIf="permission.check(selection.nodes, ['delete'], { target: 'allowableOperationsOnTarget' })"
[acaDeleteNode]="selection.nodes">
<mat-icon>delete</mat-icon>
<span>{{ 'APP.ACTIONS.DELETE' | translate }}</span>
</button>
<button
mat-menu-item
*ngIf="selection.file && permission.check(selection.file, ['update'], { target: 'allowableOperationsOnTarget' })"
[acaNodeVersions]="selection.file">
<mat-icon>history</mat-icon>
<span>{{ 'APP.ACTIONS.VERSIONS' | translate }}</span>
</button>
</mat-menu>
</ng-container>
</adf-toolbar> </adf-toolbar>
</div> </div>

View File

@ -9,17 +9,18 @@
<ng-container *ifExperimental="'extensions'"> <ng-container *ifExperimental="'extensions'">
<button *ngFor="let entry of createActions" <button *ngFor="let entry of createActions"
mat-menu-item mat-menu-item
(click)="runAction(entry.action)"> [disabled]="entry.disabled"
(click)="runAction(entry.target.action)">
<mat-icon>{{ entry.icon }}</mat-icon> <mat-icon>{{ entry.icon }}</mat-icon>
<span>{{ entry.title | translate }}</span> <span>{{ entry.title | translate }}</span>
</button> </button>
</ng-container> </ng-container>
<button <button
mat-menu-item mat-menu-item
[disabled]="!permission.check(node, ['create'])" [disabled]="!canCreateContent"
[acaCreateFolder]="node?.id" (click)="createNewFolder()"
[attr.title]=" [attr.title]="
( permission.check(node, ['create']) ( canCreateContent
? 'APP.NEW_MENU.TOOLTIPS.CREATE_FOLDER' ? 'APP.NEW_MENU.TOOLTIPS.CREATE_FOLDER'
: 'APP.NEW_MENU.TOOLTIPS.CREATE_FOLDER_NOT_ALLOWED' : 'APP.NEW_MENU.TOOLTIPS.CREATE_FOLDER_NOT_ALLOWED'
) | translate"> ) | translate">
@ -29,11 +30,11 @@
<adf-upload-button <adf-upload-button
[tooltip]=" [tooltip]="
(permission.check(node, ['create']) (canCreateContent
? 'APP.NEW_MENU.TOOLTIPS.UPLOAD_FILES' ? 'APP.NEW_MENU.TOOLTIPS.UPLOAD_FILES'
: 'APP.NEW_MENU.TOOLTIPS.UPLOAD_FILES_NOT_ALLOWED' : 'APP.NEW_MENU.TOOLTIPS.UPLOAD_FILES_NOT_ALLOWED'
) | translate" ) | translate"
[disabled]="!permission.check(node, ['create'])" [disabled]="!canCreateContent"
[rootFolderId]="node?.id" [rootFolderId]="node?.id"
[multipleFiles]="true" [multipleFiles]="true"
[uploadFolders]="false" [uploadFolders]="false"
@ -42,11 +43,11 @@
<adf-upload-button <adf-upload-button
[tooltip]=" [tooltip]="
(permission.check(node, ['create']) (canCreateContent
? 'APP.NEW_MENU.TOOLTIPS.UPLOAD_FOLDERS' ? 'APP.NEW_MENU.TOOLTIPS.UPLOAD_FOLDERS'
: 'APP.NEW_MENU.TOOLTIPS.UPLOAD_FOLDERS_NOT_ALLOWED' : 'APP.NEW_MENU.TOOLTIPS.UPLOAD_FOLDERS_NOT_ALLOWED'
) | translate" ) | translate"
[disabled]="!permission.check(node, ['create'])" [disabled]="!canCreateContent"
[rootFolderId]="node?.id" [rootFolderId]="node?.id"
[multipleFiles]="true" [multipleFiles]="true"
[uploadFolders]="true" [uploadFolders]="true"

View File

@ -25,7 +25,6 @@
import { NO_ERRORS_SCHEMA } from '@angular/core'; import { NO_ERRORS_SCHEMA } from '@angular/core';
import { TestBed, async, ComponentFixture } from '@angular/core/testing'; import { TestBed, async, ComponentFixture } from '@angular/core/testing';
import { BrowsingFilesService } from '../../common/services/browsing-files.service';
import { SidenavComponent } from './sidenav.component'; import { SidenavComponent } from './sidenav.component';
import { EffectsModule } from '@ngrx/effects'; import { EffectsModule } from '@ngrx/effects';
import { NodeEffects } from '../../store/effects/node.effects'; import { NodeEffects } from '../../store/effects/node.effects';
@ -35,7 +34,6 @@ import { ExperimentalDirective } from '../../directives/experimental.directive';
describe('SidenavComponent', () => { describe('SidenavComponent', () => {
let fixture: ComponentFixture<SidenavComponent>; let fixture: ComponentFixture<SidenavComponent>;
let component: SidenavComponent; let component: SidenavComponent;
let browsingService: BrowsingFilesService;
beforeEach(async(() => { beforeEach(async(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
@ -51,19 +49,12 @@ describe('SidenavComponent', () => {
}) })
.compileComponents() .compileComponents()
.then(() => { .then(() => {
browsingService = TestBed.get(BrowsingFilesService);
fixture = TestBed.createComponent(SidenavComponent); fixture = TestBed.createComponent(SidenavComponent);
component = fixture.componentInstance; component = fixture.componentInstance;
}); });
})); }));
it('should update node on change', () => { it('should be created', () => {
fixture.detectChanges(); expect(component).toBeTruthy();
const node: any = { entry: { id: 'someNodeId' } };
browsingService.onChangeParent.next(<any>node);
expect(component.node).toBe(node);
}); });
}); });

View File

@ -23,14 +23,18 @@
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>. * along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
*/ */
import { Subscription } from 'rxjs/Rx'; import { Subject } from 'rxjs/Rx';
import { Component, Input, OnInit, OnDestroy } from '@angular/core'; import { Component, Input, OnInit, OnDestroy } from '@angular/core';
import { MinimalNodeEntryEntity } from 'alfresco-js-api'; import { Node } from 'alfresco-js-api';
import { BrowsingFilesService } from '../../common/services/browsing-files.service';
import { NodePermissionService } from '../../common/services/node-permission.service'; import { NodePermissionService } from '../../common/services/node-permission.service';
import { ExtensionService } from '../../extensions/extension.service'; import { ExtensionService } from '../../extensions/extension.service';
import { NavigationExtension } from '../../extensions/navigation.extension'; import { NavigationExtension } from '../../extensions/navigation.extension';
import { CreateExtension } from '../../extensions/create.extension'; 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 { ContentActionExtension } from '../../extensions/content-action.extension';
@Component({ @Component({
selector: 'app-sidenav', selector: 'app-sidenav',
@ -40,31 +44,40 @@ import { CreateExtension } from '../../extensions/create.extension';
export class SidenavComponent implements OnInit, OnDestroy { export class SidenavComponent implements OnInit, OnDestroy {
@Input() showLabel: boolean; @Input() showLabel: boolean;
node: MinimalNodeEntryEntity = null; node: Node = null;
groups: Array<NavigationExtension[]> = []; groups: Array<NavigationExtension[]> = [];
createActions: Array<CreateExtension> = []; createActions: Array<ContentActionExtension> = [];
canCreateContent = false;
private subscriptions: Subscription[] = []; onDestroy$: Subject<boolean> = new Subject<boolean>();
constructor( constructor(
private browsingFilesService: BrowsingFilesService, private store: Store<AppStore>,
public permission: NodePermissionService, private permission: NodePermissionService,
private extensions: ExtensionService private extensions: ExtensionService
) {} ) {
}
ngOnInit() { ngOnInit() {
this.groups = this.extensions.getNavigationGroups(); this.groups = this.extensions.getNavigationGroups();
this.createActions = this.extensions.createActions;
this.subscriptions.concat([ this.store.select(currentFolder)
this.browsingFilesService.onChangeParent.subscribe( .pipe(takeUntil(this.onDestroy$))
(node: MinimalNodeEntryEntity) => (this.node = node) .subscribe(node => {
) this.node = node;
]); this.createActions = this.extensions.getFolderCreateActions(node);
this.canCreateContent = this.permission.check(node, ['create']);
});
} }
ngOnDestroy() { ngOnDestroy() {
this.subscriptions.forEach(s => s.unsubscribe()); this.onDestroy$.next(true);
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 // this is where each application decides how to treat an action and what to do

View File

@ -3,14 +3,17 @@
<adf-breadcrumb root="APP.BROWSE.TRASHCAN.TITLE"> <adf-breadcrumb root="APP.BROWSE.TRASHCAN.TITLE">
</adf-breadcrumb> </adf-breadcrumb>
<button *ifExperimental="'cardview'" <adf-toolbar class="inline">
mat-icon-button <button *ifExperimental="'cardview'"
(click)="toggleGalleryView()"> color="primary"
<mat-icon *ngIf="displayMode === 'list'" matTooltip="{{ 'APP.DOCUMENT_LIST.TOOLBAR.CARDVIEW' | translate }}">view_comfy</mat-icon> mat-icon-button
<mat-icon *ngIf="displayMode === 'gallery'" matTooltip="{{ 'APP.DOCUMENT_LIST.TOOLBAR.LISTVIEW' | translate }}">list</mat-icon> (click)="toggleGalleryView()">
</button> <mat-icon *ngIf="displayMode === 'list'" matTooltip="{{ 'APP.DOCUMENT_LIST.TOOLBAR.CARDVIEW' | translate }}">view_comfy</mat-icon>
<adf-toolbar class="inline" *ngIf="!selection.isEmpty"> <mat-icon *ngIf="displayMode === 'gallery'" matTooltip="{{ 'APP.DOCUMENT_LIST.TOOLBAR.LISTVIEW' | translate }}">list</mat-icon>
</button>
<ng-container *ifExperimental="'extensions'"> <ng-container *ifExperimental="'extensions'">
<adf-toolbar-divider></adf-toolbar-divider>
<button *ngFor="let entry of actions" <button *ngFor="let entry of actions"
mat-icon-button mat-icon-button
color="primary" color="primary"
@ -18,22 +21,26 @@
(click)="runAction(entry.target.action)"> (click)="runAction(entry.target.action)">
<mat-icon>{{ entry.icon }}</mat-icon> <mat-icon>{{ entry.icon }}</mat-icon>
</button> </button>
<adf-toolbar-divider></adf-toolbar-divider>
</ng-container> </ng-container>
<button
color="primary"
mat-icon-button
[acaPermanentDelete]="selection.nodes"
title="{{ 'APP.ACTIONS.DELETE_PERMANENT' | translate }}">
<mat-icon>delete_forever</mat-icon>
</button>
<button <ng-container *ngIf="!selection.isEmpty">
color="primary" <button
mat-icon-button color="primary"
[acaRestoreNode]="selection.nodes" mat-icon-button
title="{{ 'APP.ACTIONS.RESTORE' | translate }}"> [acaPermanentDelete]="selection.nodes"
<mat-icon>restore</mat-icon> title="{{ 'APP.ACTIONS.DELETE_PERMANENT' | translate }}">
</button> <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>
</adf-toolbar> </adf-toolbar>
</div> </div>
@ -97,7 +104,7 @@
</data-column> </data-column>
<data-column <data-column
*ngIf="user.isAdmin" *ngIf="(user$ | async)?.isAdmin"
class="adf-data-table-cell--ellipsis" class="adf-data-table-cell--ellipsis"
key="archivedByUser.displayName" key="archivedByUser.displayName"
title="APP.DOCUMENT_LIST.COLUMNS.DELETED_BY"> title="APP.DOCUMENT_LIST.COLUMNS.DELETED_BY">

View File

@ -31,17 +31,19 @@ import { selectUser } from '../../store/selectors/app.selectors';
import { AppStore } from '../../store/states/app.state'; import { AppStore } from '../../store/states/app.state';
import { ProfileState } from '../../store/states/profile.state'; import { ProfileState } from '../../store/states/profile.state';
import { ExtensionService } from '../../extensions/extension.service'; import { ExtensionService } from '../../extensions/extension.service';
import { Observable } from 'rxjs/Observable';
@Component({ @Component({
templateUrl: './trashcan.component.html' templateUrl: './trashcan.component.html'
}) })
export class TrashcanComponent extends PageComponent implements OnInit { export class TrashcanComponent extends PageComponent implements OnInit {
user: ProfileState; user$: Observable<ProfileState>;
constructor(private contentManagementService: ContentManagementService, constructor(private contentManagementService: ContentManagementService,
extensions: ExtensionService, extensions: ExtensionService,
store: Store<AppStore>) { store: Store<AppStore>) {
super(store, extensions); super(store, extensions);
this.user$ = this.store.select(selectUser);
} }
ngOnInit() { ngOnInit() {
@ -51,7 +53,6 @@ export class TrashcanComponent extends PageComponent implements OnInit {
this.contentManagementService.nodesRestored.subscribe(() => this.reload()), this.contentManagementService.nodesRestored.subscribe(() => this.reload()),
this.contentManagementService.nodesPurged.subscribe(() => this.reload()), this.contentManagementService.nodesPurged.subscribe(() => this.reload()),
this.contentManagementService.nodesRestored.subscribe(() => this.reload()), this.contentManagementService.nodesRestored.subscribe(() => this.reload()),
this.store.select(selectUser).subscribe((user) => this.user = user)
); );
} }
} }

View File

@ -1,89 +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 { MatDialog, MatDialogConfig } from '@angular/material';
import { FolderDialogComponent } from '@alfresco/adf-content-services';
import { Store } from '@ngrx/store';
import { AppStore } from '../store/states/app.state';
import { SnackbarErrorAction } from '../store/actions';
import { ContentManagementService } from '../common/services/content-management.service';
@Directive({
selector: '[acaCreateFolder]'
})
export class CreateFolderDirective {
/** Parent folder where the new folder will be located after creation. */
// tslint:disable-next-line:no-input-rename
@Input('acaCreateFolder') parentNodeId: string;
/** Title of folder creation dialog. */
@Input() dialogTitle: string = null;
/** Type of node to create. */
@Input() nodeType = 'cm:folder';
@HostListener('click', ['$event'])
onClick(event: Event) {
if (this.parentNodeId) {
event.preventDefault();
this.openDialog();
}
}
constructor(
private store: Store<AppStore>,
private dialogRef: MatDialog,
private content: ContentManagementService
) {}
private get dialogConfig(): MatDialogConfig {
return {
data: {
parentNodeId: this.parentNodeId,
createTitle: this.dialogTitle,
nodeType: this.nodeType
},
width: '400px'
};
}
private openDialog(): void {
const dialogInstance = this.dialogRef.open(
FolderDialogComponent,
this.dialogConfig
);
dialogInstance.componentInstance.error.subscribe(message => {
this.store.dispatch(new SnackbarErrorAction(message));
});
dialogInstance.afterClosed().subscribe(node => {
if (node) {
this.content.folderCreated.next(node);
}
});
}
}

View File

@ -24,59 +24,24 @@
*/ */
import { Directive, Input, HostListener } from '@angular/core'; import { Directive, Input, HostListener } from '@angular/core';
import { MinimalNodeEntryEntity, MinimalNodeEntity } from 'alfresco-js-api'; import { MinimalNodeEntity } from 'alfresco-js-api';
import { MatDialog, MatDialogConfig } from '@angular/material';
import { FolderDialogComponent } from '@alfresco/adf-content-services';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppStore } from '../store/states/app.state'; import { AppStore } from '../store/states';
import { SnackbarErrorAction } from '../store/actions'; import { EditFolderAction } from '../store/actions';
import { ContentManagementService } from '../common/services/content-management.service';
@Directive({ @Directive({
selector: '[acaEditFolder]' selector: '[acaEditFolder]'
}) })
export class EditFolderDirective { export class EditFolderDirective {
/** Folder node to edit. */ /** Folder node to edit. */
// tslint:disable-next-line:no-input-rename // tslint:disable-next-line:no-input-rename
@Input('acaEditFolder') @Input('acaEditFolder') folder: MinimalNodeEntity;
folder: MinimalNodeEntity;
@HostListener('click', [ '$event' ]) @HostListener('click', ['$event'])
onClick(event) { onClick(event) {
event.preventDefault(); event.preventDefault();
this.store.dispatch(new EditFolderAction(this.folder));
if (this.folder) {
this.openDialog();
}
} }
constructor( constructor(private store: Store<AppStore>) {}
private store: Store<AppStore>,
private dialogRef: MatDialog,
private content: ContentManagementService
) {}
private get dialogConfig(): MatDialogConfig {
return {
data: {
folder: this.folder.entry
},
width: '400px'
};
}
private openDialog(): void {
const dialog = this.dialogRef.open(FolderDialogComponent, this.dialogConfig);
dialog.componentInstance.error.subscribe(message => {
this.store.dispatch(new SnackbarErrorAction(message));
});
dialog.afterClosed().subscribe((node: MinimalNodeEntryEntity) => {
if (node) {
this.content.folderEdited.next(node);
}
});
}
} }

View File

@ -30,7 +30,7 @@ export interface ContentActionExtension {
icon?: string; icon?: string;
disabled?: boolean; disabled?: boolean;
target: { target: {
type: string; types: Array<string>;
permissions: Array<string>, permissions: Array<string>,
action: string; action: string;
}; };

View File

@ -29,11 +29,11 @@ import { ActionExtension } from './action.extension';
import { AppConfigService } from '@alfresco/adf-core'; import { AppConfigService } from '@alfresco/adf-core';
import { ContentActionExtension } from './content-action.extension'; import { ContentActionExtension } from './content-action.extension';
import { OpenWithExtension } from './open-with.extension'; import { OpenWithExtension } from './open-with.extension';
import { AppStore } from '../store/states'; import { AppStore, SelectionState } from '../store/states';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { NavigationExtension } from './navigation.extension'; import { NavigationExtension } from './navigation.extension';
import { Route } from '@angular/router'; import { Route } from '@angular/router';
import { CreateExtension } from './create.extension'; import { Node } from 'alfresco-js-api';
@Injectable() @Injectable()
export class ExtensionService { export class ExtensionService {
@ -42,7 +42,7 @@ export class ExtensionService {
contentActions: Array<ContentActionExtension> = []; contentActions: Array<ContentActionExtension> = [];
openWithActions: Array<OpenWithExtension> = []; openWithActions: Array<OpenWithExtension> = [];
createActions: Array<CreateExtension> = []; createActions: Array<ContentActionExtension> = [];
authGuards: { [key: string]: Type<{}> } = {}; authGuards: { [key: string]: Type<{}> } = {};
components: { [key: string]: Type<{}> } = {}; components: { [key: string]: Type<{}> } = {};
@ -70,7 +70,6 @@ export class ExtensionService {
'extensions.core.features.content.actions', 'extensions.core.features.content.actions',
[] []
) )
.filter(entry => !entry.disabled)
.sort(this.sortByOrder); .sort(this.sortByOrder);
this.openWithActions = this.config this.openWithActions = this.config
@ -82,8 +81,10 @@ export class ExtensionService {
.sort(this.sortByOrder); .sort(this.sortByOrder);
this.createActions = this.config this.createActions = this.config
.get<Array<CreateExtension>>('extensions.core.features.create', []) .get<Array<ContentActionExtension>>(
.filter(entry => !entry.disabled) 'extensions.core.features.create',
[]
)
.sort(this.sortByOrder); .sort(this.sortByOrder);
} }
@ -170,11 +171,48 @@ export class ExtensionService {
component: this.getComponentById(route.component), component: this.getComponentById(route.component),
data: route.data data: route.data
} }
], ]
}; };
}); });
} }
// evaluates create actions for the folder node
getFolderCreateActions(folder: Node): Array<ContentActionExtension> {
return this.createActions.filter(this.filterOutDisabled).map(action => {
if (
action.target &&
action.target.permissions &&
action.target.permissions.length > 0
) {
return {
...action,
disabled: !this.nodeHasPermissions(
folder,
action.target.permissions
),
target: {
...action.target
}
};
}
return action;
});
}
// evaluates content actions for the selection and parent folder node
getSelectedContentActions(
selection: SelectionState,
parentNode: Node
): Array<ContentActionExtension> {
return this.contentActions
.filter(this.filterOutDisabled)
.filter(action => action.target)
.filter(action => this.filterByTarget(selection, action))
.filter(action =>
this.filterByPermission(selection, action, parentNode)
);
}
private sortByOrder( private sortByOrder(
a: { order?: number | undefined }, a: { order?: number | undefined },
b: { order?: number | undefined } b: { order?: number | undefined }
@ -183,4 +221,81 @@ export class ExtensionService {
const right = b.order === undefined ? Number.MAX_SAFE_INTEGER : b.order; const right = b.order === undefined ? Number.MAX_SAFE_INTEGER : b.order;
return left - right; return left - right;
} }
private filterOutDisabled(entry: { disabled?: boolean }): boolean {
return !entry.disabled;
}
// todo: support multiple selected nodes
private filterByTarget(
selection: SelectionState,
action: ContentActionExtension
): boolean {
const types = action.target.types;
if (!types || types.length === 0) {
return true;
}
if (selection && selection.folder && types.includes('folder')) {
return true;
}
if (selection && selection.file && types.includes('file')) {
return true;
}
return false;
}
// todo: support multiple selected nodes
private filterByPermission(
selection: SelectionState,
action: ContentActionExtension,
parentNode: Node
): boolean {
const permissions = action.target.permissions;
if (!permissions || permissions.length === 0) {
return true;
}
return permissions.some(permission => {
if (permission.startsWith('parent.')) {
if (parentNode) {
const parentQuery = permission.split('.')[1];
// console.log(parentNode.allowableOperations, parentQuery);
return this.nodeHasPermissions(parentNode, [parentQuery]);
}
return false;
}
if (selection && selection.first) {
return this.nodeHasPermissions(
selection.first.entry,
permissions
);
}
return true;
});
return true;
}
private nodeHasPermissions(
node: Node,
permissions: string[] = []
): boolean {
if (
node &&
node.allowableOperations &&
node.allowableOperations.length > 0
) {
return permissions.some(permission =>
node.allowableOperations.includes(permission)
);
}
return false;
}
} }

View File

@ -24,12 +24,14 @@
*/ */
import { Action } from '@ngrx/store'; import { Action } from '@ngrx/store';
import { Node } from 'alfresco-js-api';
export const SET_APP_NAME = 'SET_APP_NAME'; export const SET_APP_NAME = 'SET_APP_NAME';
export const SET_HEADER_COLOR = 'SET_HEADER_COLOR'; export const SET_HEADER_COLOR = 'SET_HEADER_COLOR';
export const SET_LOGO_PATH = 'SET_LOGO_PATH'; export const SET_LOGO_PATH = 'SET_LOGO_PATH';
export const SET_LANGUAGE_PICKER = 'SET_LANGUAGE_PICKER'; export const SET_LANGUAGE_PICKER = 'SET_LANGUAGE_PICKER';
export const SET_SHARED_URL = 'SET_SHARED_URL'; export const SET_SHARED_URL = 'SET_SHARED_URL';
export const SET_CURRENT_FOLDER = 'SET_CURRENT_FOLDER';
export class SetAppNameAction implements Action { export class SetAppNameAction implements Action {
readonly type = SET_APP_NAME; readonly type = SET_APP_NAME;
@ -55,3 +57,8 @@ export class SetSharedUrlAction implements Action {
readonly type = SET_SHARED_URL; readonly type = SET_SHARED_URL;
constructor(public payload: string) {} constructor(public payload: string) {}
} }
export class SetCurrentFolderAction implements Action {
readonly type = SET_CURRENT_FOLDER;
constructor(public payload: Node) {}
}

View File

@ -25,6 +25,7 @@
import { Action } from '@ngrx/store'; import { Action } from '@ngrx/store';
import { NodeInfo } from '../models'; import { NodeInfo } from '../models';
import { MinimalNodeEntity } from 'alfresco-js-api';
export const SET_SELECTED_NODES = 'SET_SELECTED_NODES'; export const SET_SELECTED_NODES = 'SET_SELECTED_NODES';
export const DELETE_NODES = 'DELETE_NODES'; export const DELETE_NODES = 'DELETE_NODES';
@ -32,6 +33,8 @@ export const UNDO_DELETE_NODES = 'UNDO_DELETE_NODES';
export const RESTORE_DELETED_NODES = 'RESTORE_DELETED_NODES'; export const RESTORE_DELETED_NODES = 'RESTORE_DELETED_NODES';
export const PURGE_DELETED_NODES = 'PURGE_DELETED_NODES'; export const PURGE_DELETED_NODES = 'PURGE_DELETED_NODES';
export const DOWNLOAD_NODES = 'DOWNLOAD_NODES'; export const DOWNLOAD_NODES = 'DOWNLOAD_NODES';
export const CREATE_FOLDER = 'CREATE_FOLDER';
export const EDIT_FOLDER = 'EDIT_FOLDER';
export class SetSelectedNodesAction implements Action { export class SetSelectedNodesAction implements Action {
readonly type = SET_SELECTED_NODES; readonly type = SET_SELECTED_NODES;
@ -62,3 +65,13 @@ export class DownloadNodesAction implements Action {
readonly type = DOWNLOAD_NODES; readonly type = DOWNLOAD_NODES;
constructor(public payload: NodeInfo[] = []) {} constructor(public payload: NodeInfo[] = []) {}
} }
export class CreateFolderAction implements Action {
readonly type = CREATE_FOLDER;
constructor(public payload: string) {}
}
export class EditFolderAction implements Action {
readonly type = EDIT_FOLDER;
constructor(public payload: MinimalNodeEntity) {}
}

View File

@ -39,12 +39,16 @@ import {
SnackbarUserAction, SnackbarUserAction,
SnackbarAction, SnackbarAction,
UndoDeleteNodesAction, UndoDeleteNodesAction,
UNDO_DELETE_NODES UNDO_DELETE_NODES,
CreateFolderAction,
CREATE_FOLDER
} from '../actions'; } from '../actions';
import { ContentManagementService } from '../../common/services/content-management.service'; import { ContentManagementService } from '../../common/services/content-management.service';
import { Observable } from 'rxjs/Rx'; import { Observable } from 'rxjs/Rx';
import { NodeInfo, DeleteStatus, DeletedNodeInfo } from '../models'; import { NodeInfo, DeleteStatus, DeletedNodeInfo } from '../models';
import { ContentApiService } from '../../services/content-api.service'; import { ContentApiService } from '../../services/content-api.service';
import { currentFolder, appSelection } from '../selectors/app.selectors';
import { EditFolderAction, EDIT_FOLDER } from '../actions/node.actions';
@Injectable() @Injectable()
export class NodeEffects { export class NodeEffects {
@ -83,6 +87,44 @@ export class NodeEffects {
}) })
); );
@Effect({ dispatch: false })
createFolder$ = this.actions$.pipe(
ofType<CreateFolderAction>(CREATE_FOLDER),
map(action => {
if (action.payload) {
this.contentManagementService.createFolder(action.payload);
} else {
this.store
.select(currentFolder)
.take(1)
.subscribe(node => {
if (node && node.id) {
this.contentManagementService.createFolder(node.id);
}
});
}
})
);
@Effect({ dispatch: false })
editFolder$ = this.actions$.pipe(
ofType<EditFolderAction>(EDIT_FOLDER),
map(action => {
if (action.payload) {
this.contentManagementService.editFolder(action.payload);
} else {
this.store
.select(appSelection)
.take(1)
.subscribe(selection => {
if (selection && selection.folder) {
this.contentManagementService.editFolder(selection.folder);
}
});
}
})
);
private deleteNodes(items: NodeInfo[]): void { private deleteNodes(items: NodeInfo[]): void {
const batch: Observable<DeletedNodeInfo>[] = []; const batch: Observable<DeletedNodeInfo>[] = [];
@ -113,7 +155,8 @@ export class NodeEffects {
private deleteNode(node: NodeInfo): Observable<DeletedNodeInfo> { private deleteNode(node: NodeInfo): Observable<DeletedNodeInfo> {
const { id, name } = node; const { id, name } = node;
return this.contentApi.deleteNode(id) return this.contentApi
.deleteNode(id)
.map(() => { .map(() => {
return { return {
id, id,
@ -206,7 +249,8 @@ export class NodeEffects {
private undoDeleteNode(item: DeletedNodeInfo): Observable<DeletedNodeInfo> { private undoDeleteNode(item: DeletedNodeInfo): Observable<DeletedNodeInfo> {
const { id, name } = item; const { id, name } = item;
return this.contentApi.restoreNode(id) return this.contentApi
.restoreNode(id)
.map(() => { .map(() => {
return { return {
id, id,
@ -263,7 +307,8 @@ export class NodeEffects {
private purgeDeletedNode(node: NodeInfo): Observable<DeletedNodeInfo> { private purgeDeletedNode(node: NodeInfo): Observable<DeletedNodeInfo> {
const { id, name } = node; const { id, name } = node;
return this.contentApi.purgeDeletedNode(id) return this.contentApi
.purgeDeletedNode(id)
.map(() => ({ .map(() => ({
status: 1, status: 1,
id, id,

View File

@ -35,14 +35,14 @@ import {
SET_SELECTED_NODES, SET_SELECTED_NODES,
SetSelectedNodesAction, SetSelectedNodesAction,
SET_USER, SET_USER,
SetUserAction SetUserAction,
} from '../actions';
import {
SET_LANGUAGE_PICKER, SET_LANGUAGE_PICKER,
SetLanguagePickerAction, SetLanguagePickerAction,
SET_SHARED_URL, SET_SHARED_URL,
SetSharedUrlAction SetSharedUrlAction,
} from '../actions/app.actions'; SET_CURRENT_FOLDER
} from '../actions';
import { SetCurrentFolderAction } from '../actions/app.actions';
export function appReducer( export function appReducer(
state: AppState = INITIAL_APP_STATE, state: AppState = INITIAL_APP_STATE,
@ -74,7 +74,10 @@ export function appReducer(
)); ));
break; break;
case SET_SHARED_URL: case SET_SHARED_URL:
newState = updateSharedUrl(state, <SetSharedUrlAction>( newState = updateSharedUrl(state, <SetSharedUrlAction>action);
break;
case SET_CURRENT_FOLDER:
newState = updateCurrentFolder(state, <SetCurrentFolderAction>(
action action
)); ));
break; break;
@ -149,6 +152,12 @@ function updateUser(state: AppState, action: SetUserAction): AppState {
return newState; return newState;
} }
function updateCurrentFolder(state: AppState, action: SetCurrentFolderAction) {
const newState = Object.assign({}, state);
newState.navigation.currentFolder = action.payload;
return newState;
}
function updateSelectedNodes( function updateSelectedNodes(
state: AppState, state: AppState,
action: SetSelectedNodesAction action: SetSelectedNodesAction

View File

@ -34,3 +34,4 @@ export const appSelection = createSelector(selectApp, state => state.selection)
export const appLanguagePicker = createSelector(selectApp, state => state.languagePicker); export const appLanguagePicker = createSelector(selectApp, state => state.languagePicker);
export const selectUser = createSelector(selectApp, state => state.user); export const selectUser = createSelector(selectApp, state => state.user);
export const sharedUrl = createSelector(selectApp, state => state.sharedUrl); export const sharedUrl = createSelector(selectApp, state => state.sharedUrl);
export const currentFolder = createSelector(selectApp, state => state.navigation.currentFolder);

View File

@ -25,6 +25,7 @@
import { SelectionState } from './selection.state'; import { SelectionState } from './selection.state';
import { ProfileState } from './profile.state'; import { ProfileState } from './profile.state';
import { NavigationState } from './navigation.state';
export interface AppState { export interface AppState {
appName: string; appName: string;
@ -34,6 +35,7 @@ export interface AppState {
sharedUrl: string; sharedUrl: string;
selection: SelectionState; selection: SelectionState;
user: ProfileState; user: ProfileState;
navigation: NavigationState;
} }
export const INITIAL_APP_STATE: AppState = { export const INITIAL_APP_STATE: AppState = {
@ -52,6 +54,9 @@ export const INITIAL_APP_STATE: AppState = {
nodes: [], nodes: [],
isEmpty: true, isEmpty: true,
count: 0 count: 0
},
navigation: {
currentFolder: null
} }
}; };

View File

@ -23,11 +23,8 @@
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>. * along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
*/ */
export interface CreateExtension { import { Node } from 'alfresco-js-api';
id: string;
order?: number; export interface NavigationState {
title: string; currentFolder?: Node;
icon?: string;
action: string;
disabled?: boolean;
} }

View File

@ -60,7 +60,6 @@ import { MaterialModule } from '../material.module';
import { ContentManagementService } from '../common/services/content-management.service'; import { ContentManagementService } from '../common/services/content-management.service';
import { NodeActionsService } from '../common/services/node-actions.service'; import { NodeActionsService } from '../common/services/node-actions.service';
import { NodePermissionService } from '../common/services/node-permission.service'; import { NodePermissionService } from '../common/services/node-permission.service';
import { BrowsingFilesService } from '../common/services/browsing-files.service';
import { ContentApiService } from '../services/content-api.service'; import { ContentApiService } from '../services/content-api.service';
import { ExtensionService } from '../extensions/extension.service'; import { ExtensionService } from '../extensions/extension.service';
@ -116,7 +115,6 @@ import { ExtensionService } from '../extensions/extension.service';
ContentManagementService, ContentManagementService,
NodeActionsService, NodeActionsService,
NodePermissionService, NodePermissionService,
BrowsingFilesService,
ContentApiService, ContentApiService,
ExtensionService ExtensionService
] ]