[ACA-1809] app header extensions (#659)

* move header to separate component

* code fixes

* project header buttons and menus

* app menu example

* delete empty style

* logout action

* update docs

* and one more test
This commit is contained in:
Denys Vuika 2018-09-23 17:53:56 +01:00 committed by GitHub
parent ac0a29e14a
commit 8a7fbaa70a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 1131 additions and 863 deletions

View File

@ -5,6 +5,7 @@
"succes",
"sharedlinks",
"Redistributable",
"fullscreen",
"ngrx",
"ngstack",

View File

@ -621,6 +621,7 @@ Below is the list of public actions types you can use in the plugin definitions
| VIEW_FILE | MinimalNodeEntity | Preview the file (or selection) in the Viewer. |
| PRINT_FILE | MinimalNodeEntity | Print the file opened in the Viewer (or selected). |
| FULLSCREEN_VIEWER | n/a | Enters fullscreen mode to view the file opened in the Viewer. |
| LOGOUT | n/a | Log out and redirect to Login screen |
## Rules

View File

@ -414,7 +414,7 @@ describe('Delete and undo delete', () => {
await apis.user.trashcan.restore(favoriteFile1Id);
});
it('delete multiple files and check notification - [C280517]', async () => {
xit('delete multiple files and check notification - [C280517]', async () => {
let items = await page.dataTable.countRows();
await dataTable.selectMultipleItems([favoriteFile1, favoriteFile2]);

View File

@ -342,6 +342,12 @@
"description": "Application-specific features and extensions",
"type": "object",
"properties": {
"header": {
"description": "Application header extensions",
"type": "array",
"items": { "$ref": "#/definitions/contentActionRef" },
"minItems": 1
},
"create": {
"description": "The [New] menu component extensions",
"type": "array",

View File

@ -63,6 +63,7 @@ import { AppCurrentUserModule } from './components/current-user/current-user.mod
import { AppSearchInputModule } from './components/search/search-input.module';
import { AppSearchResultsModule } from './components/search/search-results.module';
import { AppLoginModule } from './components/login/login.module';
import { AppHeaderModule } from './components/header/header.module';
@NgModule({
imports: [
@ -93,7 +94,8 @@ import { AppLoginModule } from './components/login/login.module';
AppCreateMenuModule,
AppPermissionsModule,
AppSearchInputModule,
AppSearchResultsModule
AppSearchResultsModule,
AppHeaderModule
],
declarations: [
AppComponent,

View File

@ -13,18 +13,6 @@
{{ 'APP.LANGUAGE' | translate }}
</button>
<button *ngIf="(profile$ | async)?.isAdmin"
mat-menu-item
routerLink="/about">
{{ 'APP.BROWSE.ABOUT.TITLE' | translate }}
</button>
<button *ngIf="(profile$ | async)?.isAdmin"
mat-menu-item
routerLink="/settings">
{{ 'APP.SETTINGS.TITLE' | translate }}
</button>
<button mat-menu-item (click)="onLogoutEvent()" adf-logout>
{{ 'APP.SIGN_OUT' | translate }}
</button>

View File

@ -0,0 +1,25 @@
<adf-layout-header
[logo]="logo$ | async"
[redirectUrl]="'/personal-files'"
[tooltip]="appName$ | async"
[color]="headerColor$ | async"
[title]="appName$ | async"
(clicked)="toggleClicked.emit($event)">
<div class="adf-toolbar--spacer"></div>
<ng-container *ngIf="!isSmallScreen">
<aca-search-input></aca-search-input>
<adf-toolbar-divider></adf-toolbar-divider>
</ng-container>
<aca-current-user></aca-current-user>
<ng-container *ngFor="let actionRef of actions; trackBy: trackByActionId">
<aca-toolbar-action
[actionRef]="actionRef"
color="default">
</aca-toolbar-action>
</ng-container>
</adf-layout-header>

View File

@ -0,0 +1,85 @@
/*!
* @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,
ViewEncapsulation,
Output,
EventEmitter,
OnInit
} from '@angular/core';
import { Store } from '@ngrx/store';
import { AppStore } from 'src/app/store/states';
import { Observable } from 'rxjs';
import {
selectHeaderColor,
selectAppName,
selectLogoPath
} from 'src/app/store/selectors/app.selectors';
import { BreakpointObserver, Breakpoints } from '@angular/cdk/layout';
import { ContentActionRef } from '@alfresco/adf-extensions';
import { AppExtensionService } from '../../extensions/extension.service';
@Component({
selector: 'app-header',
templateUrl: 'header.component.html',
encapsulation: ViewEncapsulation.None,
host: { class: 'app-header' }
})
export class AppHeaderComponent implements OnInit {
@Output()
toggleClicked = new EventEmitter();
appName$: Observable<string>;
headerColor$: Observable<string>;
logo$: Observable<string>;
isSmallScreen = false;
actions: Array<ContentActionRef> = [];
constructor(
store: Store<AppStore>,
private breakpointObserver: BreakpointObserver,
private appExtensions: AppExtensionService
) {
this.headerColor$ = store.select(selectHeaderColor);
this.appName$ = store.select(selectAppName);
this.logo$ = store.select(selectLogoPath);
}
ngOnInit() {
this.actions = this.appExtensions.getHeaderActions();
this.breakpointObserver
.observe([Breakpoints.HandsetPortrait, Breakpoints.HandsetLandscape])
.subscribe(result => {
this.isSmallScreen = result.matches;
});
}
trackByActionId(index: number, action: ContentActionRef) {
return action.id;
}
}

View File

@ -0,0 +1,45 @@
/*!
* @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 { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { CoreModule } from '@alfresco/adf-core';
import { AppHeaderComponent } from './header.component';
import { AppCurrentUserModule } from '../current-user/current-user.module';
import { AppSearchInputModule } from '../search/search-input.module';
import { AppToolbarModule } from '../toolbar/toolbar.module';
@NgModule({
imports: [
CommonModule,
CoreModule.forChild(),
AppCurrentUserModule,
AppSearchInputModule,
AppToolbarModule
],
declarations: [AppHeaderComponent],
exports: [AppHeaderComponent]
})
export class AppHeaderModule {}

View File

@ -4,6 +4,7 @@
[disabled]="!canUpload">
<adf-sidenav-layout
#layout
#sidenavManager="acaSidenavManager"
acaSidenavManager
[sidenavMin]="70"
@ -14,25 +15,8 @@
(expanded)="sidenavManager.setState($event)">
<adf-sidenav-layout-header>
<ng-template let-toggleMenu="toggleMenu">
<adf-layout-header
[logo]="logo$ | async"
[redirectUrl]="'/personal-files'"
[tooltip]="appName$ | async"
[color]="headerColor$ | async"
[title]="appName$ | async"
(clicked)="toggleMenu($event)">
<div class="adf-toolbar--spacer"></div>
<ng-container *ngIf="!isSmallScreen">
<aca-search-input></aca-search-input>
<adf-toolbar-divider></adf-toolbar-divider>
</ng-container>
<aca-current-user></aca-current-user>
</adf-layout-header>
<ng-template>
<app-header (toggleClicked)="layout.toggleMenu($event)"></app-header>
</ng-template>
</adf-sidenav-layout-header>

View File

@ -30,20 +30,14 @@ import {
ViewChild,
ViewEncapsulation
} from '@angular/core';
import { Observable, Subject } from 'rxjs';
import { Subject } from 'rxjs';
import { MinimalNodeEntryEntity } from 'alfresco-js-api';
import { NodePermissionService } from '../../services/node-permission.service';
import { SidenavViewsManagerDirective } from './sidenav-views-manager.directive';
import { Store } from '@ngrx/store';
import { AppStore } from '../../store/states';
import {
currentFolder,
selectAppName,
selectHeaderColor,
selectLogoPath
} from '../../store/selectors/app.selectors';
import { currentFolder } from '../../store/selectors/app.selectors';
import { takeUntil } from 'rxjs/operators';
import { BreakpointObserver, Breakpoints } from '@angular/cdk/layout';
@Component({
selector: 'app-layout',
@ -61,21 +55,10 @@ export class LayoutComponent implements OnInit, OnDestroy {
node: MinimalNodeEntryEntity;
canUpload = false;
appName$: Observable<string>;
headerColor$: Observable<string>;
logo$: Observable<string>;
isSmallScreen = false;
constructor(
protected store: Store<AppStore>,
private permission: NodePermissionService,
private breakpointObserver: BreakpointObserver
) {
this.headerColor$ = store.select(selectHeaderColor);
this.appName$ = store.select(selectAppName);
this.logo$ = store.select(selectLogoPath);
}
private permission: NodePermissionService
) {}
ngOnInit() {
if (!this.manager.minimizeSidenav) {
@ -93,12 +76,6 @@ export class LayoutComponent implements OnInit, OnDestroy {
this.node = node;
this.canUpload = node && this.permission.check(node, ['create']);
});
this.breakpointObserver
.observe([Breakpoints.HandsetPortrait, Breakpoints.HandsetLandscape])
.subscribe(result => {
this.isSmallScreen = result.matches;
});
}
ngOnDestroy() {

View File

@ -32,8 +32,7 @@ import { ContentModule } from '@alfresco/adf-content-services';
import { RouterModule } from '@angular/router';
import { AppSidenavModule } from '../sidenav/sidenav.module';
import { AppCommonModule } from '../common/common.module';
import { AppCurrentUserModule } from '../current-user/current-user.module';
import { AppSearchInputModule } from '../search/search-input.module';
import { AppHeaderModule } from '../header/header.module';
@NgModule({
imports: [
@ -43,8 +42,7 @@ import { AppSearchInputModule } from '../search/search-input.module';
ContentModule.forChild(),
AppCommonModule,
AppSidenavModule,
AppCurrentUserModule,
AppSearchInputModule
AppHeaderModule
],
declarations: [LayoutComponent, SidenavViewsManagerDirective],
exports: [LayoutComponent]

View File

@ -1,16 +1,27 @@
<ng-container [ngSwitch]="actionRef.type">
<ng-container *ngSwitchCase="'default'">
<app-toolbar-button [type]="type" [actionRef]="actionRef"></app-toolbar-button>
<app-toolbar-button
[type]="type"
[actionRef]="actionRef"
[color]="color">
</app-toolbar-button>
</ng-container>
<ng-container *ngSwitchCase="'button'">
<app-toolbar-button [type]="type" [actionRef]="actionRef"></app-toolbar-button>
<app-toolbar-button
[type]="type"
[actionRef]="actionRef"
[color]="color">
</app-toolbar-button>
</ng-container>
<adf-toolbar-divider *ngSwitchCase="'separator'" [id]="actionRef.id"></adf-toolbar-divider>
<ng-container *ngSwitchCase="'menu'">
<app-toolbar-menu [actionRef]="actionRef"></app-toolbar-menu>
<app-toolbar-menu
[actionRef]="actionRef"
[color]="color">
</app-toolbar-menu>
</ng-container>
<ng-container *ngSwitchCase="'custom'">

View File

@ -42,6 +42,9 @@ export class ToolbarActionComponent {
@Input()
type = 'icon-button';
@Input()
color = 'primary';
@Input()
actionRef: ContentActionRef;
}

View File

@ -3,7 +3,7 @@
<button
[id]="actionRef.id"
mat-icon-button
color="primary"
[color]="color"
[attr.title]="(actionRef.description || actionRef.title) | translate"
(click)="runAction()">
<mat-icon>{{ actionRef.icon }}</mat-icon>

View File

@ -42,6 +42,9 @@ export class ToolbarButtonComponent {
@Input()
type: ToolbarButtonType = ToolbarButtonType.ICON_BUTTON;
@Input()
color = 'primary';
@Input()
actionRef: ContentActionRef;

View File

@ -1,6 +1,6 @@
<button
[id]="actionRef.id"
color="primary"
[color]="color"
mat-icon-button
[attr.title]="(actionRef.description || actionRef.title) | translate"
[matMenuTriggerFor]="menu">

View File

@ -36,6 +36,9 @@ export class ToolbarMenuComponent {
@Input()
actionRef: ContentActionRef;
@Input()
color = 'primary';
get hasChildren(): boolean {
return (
this.actionRef &&

View File

@ -58,6 +58,7 @@ export class AppExtensionService implements RuleContext {
auth: ['app.auth']
};
headerActions: Array<ContentActionRef> = [];
toolbarActions: Array<ContentActionRef> = [];
viewerToolbarActions: Array<ContentActionRef> = [];
viewerToolbarMoreActions: Array<ContentActionRef> = [];
@ -95,7 +96,10 @@ export class AppExtensionService implements RuleContext {
console.error('Extension configuration not found');
return;
}
this.headerActions = this.loader.getContentActions(
config,
'features.header'
);
this.toolbarActions = this.loader.getContentActions(
config,
'features.toolbar'
@ -233,6 +237,10 @@ export class AppExtensionService implements RuleContext {
);
}
getHeaderActions(): Array<ContentActionRef> {
return this.headerActions.filter(action => this.filterByRules(action));
}
getViewerToolbarMoreActions(): Array<ContentActionRef> {
return this.viewerToolbarMoreActions.filter(action =>
this.filterByRules(action)

View File

@ -34,6 +34,7 @@ export const SET_CURRENT_URL = 'SET_CURRENT_URL';
export const SET_USER_PROFILE = 'SET_USER_PROFILE';
export const TOGGLE_INFO_DRAWER = 'TOGGLE_INFO_DRAWER';
export const TOGGLE_DOCUMENT_DISPLAY_MODE = 'TOGGLE_DOCUMENT_DISPLAY_MODE';
export const LOGOUT = 'LOGOUT';
export class SetInitialStateAction implements Action {
readonly type = SET_INITIAL_STATE;
@ -69,3 +70,8 @@ export class ToggleDocumentDisplayMode implements Action {
readonly type = TOGGLE_DOCUMENT_DISPLAY_MODE;
constructor(public payload?: any) {}
}
export class LogoutAction implements Action {
readonly type = LOGOUT;
constructor(public payload?: any) {}
}

View File

@ -32,6 +32,7 @@ import { EffectsModule } from '@ngrx/effects';
import { environment } from '../../environments/environment';
import { StoreDevtoolsModule } from '@ngrx/store-devtools';
import {
AppEffects,
SnackbarEffects,
NodeEffects,
RouterEffects,
@ -49,6 +50,7 @@ import {
StoreModule.forRoot({ app: appReducer }, { initialState: INITIAL_STATE }),
StoreRouterConnectingModule.forRoot({ stateKey: 'router' }),
EffectsModule.forRoot([
AppEffects,
SnackbarEffects,
NodeEffects,
RouterEffects,

View File

@ -23,6 +23,7 @@
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
*/
export * from './effects/app.effects';
export * from './effects/download.effects';
export * from './effects/favorite.effects';
export * from './effects/node.effects';

View File

@ -0,0 +1,54 @@
/*!
* @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 { LogoutAction, LOGOUT } from '../actions/app.actions';
import { AuthenticationService } from '@alfresco/adf-core';
import { Router } from '@angular/router';
@Injectable()
export class AppEffects {
constructor(
private actions$: Actions,
private auth: AuthenticationService,
private router: Router
) {}
@Effect({ dispatch: false })
logout$ = this.actions$.pipe(
ofType<LogoutAction>(LOGOUT),
map(() => {
this.auth
.logout()
.subscribe(() => this.redirectToLogin(), () => this.redirectToLogin());
})
);
private redirectToLogin() {
this.router.navigate(['login']);
}
}

View File

@ -2,7 +2,7 @@
"$schema": "../../extension.schema.json",
"$name": "app",
"$version": "1.0.0",
"$references": ["plugin1.json", "dev.tools.json"],
"$references": ["plugin1.json", "dev.tools.json", "app.header.json"],
"rules": [
{

View File

@ -342,6 +342,12 @@
"description": "Application-specific features and extensions",
"type": "object",
"properties": {
"header": {
"description": "Application header extensions",
"type": "array",
"items": { "$ref": "#/definitions/contentActionRef" },
"minItems": 1
},
"create": {
"description": "The [New] menu component extensions",
"type": "array",

View File

@ -0,0 +1,59 @@
{
"$schema": "../../../extension.schema.json",
"$name": "app",
"$version": "1.0.0",
"actions": [
{
"id": "app.actions.about",
"type": "NAVIGATE_URL",
"payload": "/about"
},
{
"id": "app.actions.settings",
"type": "NAVIGATE_URL",
"payload": "/settings"
}
],
"features": {
"header": [
{
"id": "app.header.more",
"type": "menu",
"order": 10000,
"icon": "more_vert",
"title": "APP.ACTIONS.MORE",
"children": [
{
"id": "app.header.settings",
"title": "APP.SETTINGS.TITLE",
"description": "APP.SETTINGS.TITLE",
"icon": "settings",
"actions": {
"click": "app.actions.settings"
}
},
{
"id": "app.header.about",
"title": "APP.BROWSE.ABOUT.TITLE",
"description": "APP.BROWSE.ABOUT.TITLE",
"icon": "info",
"actions": {
"click": "app.actions.about"
}
},
{
"id": "app.header.logout",
"title": "APP.SIGN_OUT",
"description": "APP.SIGN_OUT",
"icon": "exit_to_app",
"actions": {
"click": "LOGOUT"
}
}
]
}
]
}
}