[ACA-2320] Navigation - support store actions (#1052)

* move component into folder

* update module

* add children template references

* clean up styling

* clean up theme

* use content projection

* remove old tests

* button menu component

* expand menu component

* link item component

* resolve action directive

* custom active link directive

* collapse template reference

* expanded template reference

* expansion panel directive

* item template directive

* menu panel directive

* support for ngrx actions

* update side navigation inplementation

* remove unused component

* remove unused styling

* update module

* clean up

* unit tests

* unit tests

* remove unused component

* lint

* remove unused import

* fix test

* add tooltip

* fix text

* fix e2e

* use action route commands

* remove fdescribe

* styles fix

* e2e fix tooltip test

* fix active route when drill down

* update docs
This commit is contained in:
Cilibiu Bogdan
2019-04-05 15:03:40 +03:00
committed by Denys Vuika
parent 9f127c0530
commit 839c9d0dbb
30 changed files with 1493 additions and 451 deletions

View File

@@ -138,6 +138,30 @@ In the `app.config.json` define a link entry which will point to the custom page
```
This can also be declared using ngrx store action:
```json
{
...,
"navigation": [
"main": [ ... ],
"secondary": [ ... ],
"custom": [
{
"icon": "work",
"label": "Link",
"title": "My custom link",
"click": {
"action": "NAVIGATE_ROUTE",
"payload": "custom-route"
}
}
]
]
}
```
Map the `/custom-route` in `app.routes.ts` as a child of `LayoutComponent` definition.
```js

View File

@@ -32,13 +32,14 @@ import { Utils } from '../../utilities/utils';
export class Sidenav extends Component {
private static selectors = {
root: 'app-sidenav',
link: '.menu__item',
label: '.item--label',
link: '.item',
label: '.action-button__label',
expansion_panel: ".mat-expansion-panel-header",
expansion_panel_content: ".mat-expansion-panel-body",
active: 'mat-accent',
activeClass: '.item--active',
activeChild: 'item--active',
activeClass: '.action-button--active',
activeClassName: 'action-button--active',
activeChild: 'action-button--active',
newButton: '[data-automation-id="create-button"]',
@@ -106,7 +107,7 @@ export class Sidenav extends Component {
}
async isActive(name: string) {
return await this.getLinkLabel(name).isElementPresent(by.css(Sidenav.selectors.activeClass));
return (await this.getLinkLabel(name).getAttribute('class')).includes(Sidenav.selectors.activeClassName);
}
async childIsActive(name: string) {

View File

@@ -61,22 +61,22 @@ describe('Sidebar', () => {
it('My Libraries is automatically selected on expanding File Libraries - [C289900]', async () => {
await sidenav.expandFileLibraries();
expect(await browser.getCurrentUrl()).toContain(APP_ROUTES.MY_LIBRARIES);
expect(await sidenav.isActive(SIDEBAR_LABELS.FILE_LIBRARIES)).toBe(false, 'File Libraries link is active');
expect(await sidenav.childIsActive(SIDEBAR_LABELS.MY_LIBRARIES)).toBe(true, 'My Libraries link not active');
expect(await sidenav.isActive(SIDEBAR_LABELS.FILE_LIBRARIES)).toBe(true, 'File Libraries is not active');
expect(await sidenav.isActive(SIDEBAR_LABELS.MY_LIBRARIES)).toBe(true, 'My Libraries link not active');
});
it('navigate to Favorite Libraries - [C289902]', async () => {
await page.goToFavoriteLibraries();
expect(await browser.getCurrentUrl()).toContain(APP_ROUTES.FAVORITE_LIBRARIES);
expect(await sidenav.isActive(SIDEBAR_LABELS.FILE_LIBRARIES)).toBe(false, 'File Libraries link is active');
expect(await sidenav.childIsActive(SIDEBAR_LABELS.FAVORITE_LIBRARIES)).toBe(true, 'Favorite Libraries link not active');
expect(await sidenav.isActive(SIDEBAR_LABELS.FILE_LIBRARIES)).toBe(true, 'File Libraries link is not active');
expect(await sidenav.isActive(SIDEBAR_LABELS.FAVORITE_LIBRARIES)).toBe(true, 'Favorite Libraries link not active');
});
it('navigate to My Libraries - [C289901]', async () => {
await page.goToMyLibraries();
expect(await browser.getCurrentUrl()).toContain(APP_ROUTES.MY_LIBRARIES);
expect(await sidenav.isActive(SIDEBAR_LABELS.FILE_LIBRARIES)).toBe(false, 'File Libraries link is active');
expect(await sidenav.childIsActive(SIDEBAR_LABELS.MY_LIBRARIES)).toBe(true, 'My Libraries link not active');
expect(await sidenav.isActive(SIDEBAR_LABELS.FILE_LIBRARIES)).toBe(true, 'File Libraries link is not active');
expect(await sidenav.isActive(SIDEBAR_LABELS.MY_LIBRARIES)).toBe(true, 'My Libraries link not active');
});
it('navigates to "Shared Files" - [C213110]', async () => {

View File

@@ -16,7 +16,6 @@
<ng-container *ngIf="!expanded">
<button
color="accent"
mat-icon-button
class="app-create-menu--collapsed"
data-automation-id="create-button"
@@ -25,7 +24,7 @@
title="{{ 'APP.NEW_MENU.TOOLTIP' | translate }}"
>
<mat-icon
[color]="createMenu.menuOpen ? 'accent' : 'primary'"
class="app-create-menu--icon"
title="{{ 'APP.NEW_MENU.TOOLTIP' | translate }}"
>queue</mat-icon
>

View File

@@ -52,13 +52,17 @@
color: mat-color($foreground, text, 0.54);
cursor: pointer;
&:hover {
color: mat-color($primary);
color: mat-color($accent);
}
margin: 0;
border: none;
background: none;
}
.app-create-menu--icon {
color: mat-color($accent);
}
&__sub-menu {
.mat-menu-item {
display: flex;

View File

@@ -21,12 +21,13 @@
<adf-sidenav-layout-navigation>
<ng-template let-isMenuMinimized="isMenuMinimized">
<app-sidenav
[showLabel]="!isMenuMinimized()"
[mode]="isMenuMinimized() ? 'collapsed' : 'expanded'"
[attr.data-automation-id]="
isMenuMinimized() ? 'collapsed' : 'expanded'
"
(swipeleft)="hideMenu($event)"
></app-sidenav>
>
</app-sidenav>
</ng-template>
</adf-sidenav-layout-navigation>

View File

@@ -0,0 +1,50 @@
<ng-container *ngIf="!item.children">
<button
class="action-button"
mat-icon-button
acaActiveLink="action-button--active"
[action]="item"
[id]="item.id"
[attr.aria-label]="item.title | translate"
[attr.title]="item.description | translate"
[attr.data-automation-id]="item.id"
>
<adf-icon [value]="item.icon"></adf-icon>
</button>
</ng-container>
<ng-container *ngIf="item.children && item.children.length">
<button
[matMenuTriggerFor]="menu"
[acaMenuPanel]="item"
#acaMenuPanel="acaMenuPanel"
mat-icon-button
[id]="item.id"
[attr.data-automation-id]="item.id"
[attr.title]="item.description | translate"
[attr.aria-label]="item.title | translate"
class="action-button"
[ngClass]="{
'action-button--active': acaMenuPanel.hasActiveLinks()
}"
>
<adf-icon [value]="item.icon"></adf-icon>
</button>
<mat-menu #menu="matMenu" [overlapTrigger]="false">
<button
*ngFor="let child of item.children; trackBy: trackById"
acaActiveLink="action-button--active"
[action]="child"
[attr.aria-label]="child.title | translate"
[id]="child.id"
[attr.title]="child.description | translate"
[attr.data-automation-id]="child.id"
mat-menu-item
class="action-button"
>
<adf-icon *ngIf="child.icon" [value]="child.icon"></adf-icon>
<span class="action-button__label">{{ child.title | translate }}</span>
</button>
</mat-menu>
</ng-container>

View File

@@ -0,0 +1,99 @@
/*!
* @license
* Alfresco Example Content Application
*
* Copyright (C) 2005 - 2019 Alfresco Software Limited
*
* This file is part of the Alfresco Example Content Application.
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
*
* The Alfresco Example Content Application is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* The Alfresco Example Content Application is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
*/
import { ButtonMenuComponent } from './button-menu.component';
import { TestBed, ComponentFixture } from '@angular/core/testing';
import { AppTestingModule } from '../../../testing/app-testing.module';
import { Router } from '@angular/router';
import {
TranslateModule,
TranslateLoader,
TranslateFakeLoader
} from '@ngx-translate/core';
import { AppSidenavModule } from '../sidenav.module';
describe('ButtonMenuComponent', () => {
let component: ButtonMenuComponent;
let fixture: ComponentFixture<ButtonMenuComponent>;
let router: Router;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [
AppTestingModule,
AppSidenavModule,
TranslateModule.forRoot({
loader: { provide: TranslateLoader, useClass: TranslateFakeLoader }
})
]
});
fixture = TestBed.createComponent(ButtonMenuComponent);
component = fixture.componentInstance;
router = TestBed.get(Router);
spyOn(router, 'navigate');
});
it('should render action item', () => {
component.item = {
id: 'test-action-button',
url: 'dummy'
};
fixture.detectChanges();
const actionButton = document.body.querySelector('#test-action-button');
expect(actionButton).not.toBeNull();
});
it('should render action item with children', () => {
component.item = {
id: 'test-action-button',
children: [
{
id: 'child-1',
title: 'child-1',
url: 'dummy'
},
{
id: 'child-2',
title: 'child-2',
url: 'dummy'
}
]
};
fixture.detectChanges();
const actionButton = document.body.querySelector(
'[id="test-action-button"]'
);
actionButton.dispatchEvent(new Event('click'));
expect(document.querySelector('[id="child-1"]')).not.toBeNull();
expect(document.querySelector('[id="child-2"]')).not.toBeNull();
});
});

View File

@@ -0,0 +1,58 @@
/*!
* @license
* Alfresco Example Content Application
*
* Copyright (C) 2005 - 2019 Alfresco Software Limited
*
* This file is part of the Alfresco Example Content Application.
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
*
* The Alfresco Example Content Application is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* The Alfresco Example Content Application is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
*/
import {
Component,
Input,
ViewEncapsulation,
OnInit,
ChangeDetectorRef
} from '@angular/core';
import { OverlayContainer } from '@angular/cdk/overlay';
@Component({
selector: 'app-button-menu',
templateUrl: './button-menu.component.html',
host: { class: 'app-button-menu' },
encapsulation: ViewEncapsulation.None
})
export class ButtonMenuComponent implements OnInit {
@Input() item;
constructor(
private cd: ChangeDetectorRef,
private overlayContainer: OverlayContainer
) {
this.overlayContainer.getContainerElement().classList.add('aca-menu-panel');
}
ngOnInit() {
this.cd.detectChanges();
}
trackById(index: number, obj: { id: string }) {
return obj.id;
}
}

View File

@@ -0,0 +1,65 @@
<ng-container *ngIf="!item.children">
<div class="item">
<button
acaActiveLink="action-button--active"
[action]="item"
[attr.aria-label]="item.title | translate"
[id]="item.id"
[attr.data-automation-id]="item.id"
[attr.title]="item.description | translate"
mat-button
class="action-button full-width"
>
<adf-icon *ngIf="item.icon" [value]="item.icon"></adf-icon>
<span class="action-button__label">{{ item.title | translate }}</span>
</button>
</div>
</ng-container>
<ng-container *ngIf="item.children && item.children.length">
<mat-expansion-panel
[expanded]="acaExpansionPanel.hasActiveLinks()"
[acaExpansionPanel]="item"
#acaExpansionPanel="acaExpansionPanel"
[@.disabled]="true"
>
<mat-expansion-panel-header expandedHeight="48px" collapsedHeight="48px">
<mat-panel-title>
<div class="item">
<button
[ngClass]="{
'action-button--active': acaExpansionPanel.hasActiveLinks()
}"
[attr.aria-label]="item.title | translate"
[id]="item.id"
[attr.title]="item.description | translate"
[attr.data-automation-id]="item.id"
mat-button
class="action-button full-width"
>
<adf-icon *ngIf="item.icon" [value]="item.icon"></adf-icon>
<span class="action-button__label">{{
item.title | translate
}}</span>
</button>
</div>
</mat-panel-title>
</mat-expansion-panel-header>
<div *ngFor="let child of item.children; trackBy: trackById" class="item">
<button
acaActiveLink="action-button--active"
[action]="child"
[attr.aria-label]="child.title | translate"
[id]="child.id"
[attr.data-automation-id]="child.id"
[attr.title]="child.description | translate"
mat-button
class="action-button full-width"
>
<adf-icon *ngIf="child.icon" [value]="child.icon"></adf-icon>
<span class="action-button__label">{{ child.title | translate }}</span>
</button>
</div>
</mat-expansion-panel>
</ng-container>

View File

@@ -0,0 +1,99 @@
/*!
* @license
* Alfresco Example Content Application
*
* Copyright (C) 2005 - 2019 Alfresco Software Limited
*
* This file is part of the Alfresco Example Content Application.
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
*
* The Alfresco Example Content Application is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* The Alfresco Example Content Application is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
*/
import { ExpandMenuComponent } from './expand-menu.component';
import { TestBed, ComponentFixture } from '@angular/core/testing';
import { AppTestingModule } from '../../../testing/app-testing.module';
import { Router } from '@angular/router';
import {
TranslateModule,
TranslateLoader,
TranslateFakeLoader
} from '@ngx-translate/core';
import { AppSidenavModule } from '../sidenav.module';
describe('ExpandMenuComponent', () => {
let component: ExpandMenuComponent;
let fixture: ComponentFixture<ExpandMenuComponent>;
let router: Router;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [
AppTestingModule,
AppSidenavModule,
TranslateModule.forRoot({
loader: { provide: TranslateLoader, useClass: TranslateFakeLoader }
})
]
});
fixture = TestBed.createComponent(ExpandMenuComponent);
component = fixture.componentInstance;
router = TestBed.get(Router);
spyOn(router, 'navigate');
});
it('should render action item', () => {
component.item = {
id: 'test-action-button',
url: 'dummy'
};
fixture.detectChanges();
const actionButton = document.body.querySelector('#test-action-button');
expect(actionButton).not.toBeNull();
});
it('should render action item with children', () => {
component.item = {
id: 'test-action-button',
children: [
{
id: 'child-1',
title: 'child-1',
url: 'dummy'
},
{
id: 'child-2',
title: 'child-2',
url: 'dummy'
}
]
};
fixture.detectChanges();
const actionButton = document.body.querySelector(
'[id="test-action-button"]'
);
actionButton.dispatchEvent(new Event('click'));
expect(document.querySelector('[id="child-1"]')).not.toBeNull();
expect(document.querySelector('[id="child-2"]')).not.toBeNull();
});
});

View File

@@ -0,0 +1,52 @@
/*!
* @license
* Alfresco Example Content Application
*
* Copyright (C) 2005 - 2019 Alfresco Software Limited
*
* This file is part of the Alfresco Example Content Application.
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
*
* The Alfresco Example Content Application is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* The Alfresco Example Content Application is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
*/
import {
Component,
OnInit,
Input,
ViewEncapsulation,
ChangeDetectorRef
} from '@angular/core';
@Component({
selector: 'app-expand-menu',
encapsulation: ViewEncapsulation.None,
templateUrl: './expand-menu.component.html',
host: { class: 'app-expand-menu' }
})
export class ExpandMenuComponent implements OnInit {
@Input() item;
constructor(private cd: ChangeDetectorRef) {}
ngOnInit() {
this.cd.detectChanges();
}
trackById(index: number, obj: { id: string }) {
return obj.id;
}
}

View File

@@ -0,0 +1,57 @@
/*!
* @license
* Alfresco Example Content Application
*
* Copyright (C) 2005 - 2019 Alfresco Software Limited
*
* This file is part of the Alfresco Example Content Application.
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
*
* The Alfresco Example Content Application is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* The Alfresco Example Content Application is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
*/
import { ActionDirective } from './action.directive';
describe('ActionDirective', () => {
let directive: ActionDirective;
const routeMock = <any>{
navigate: jasmine.createSpy('navigate'),
parseUrl: () => ({
root: {
children: []
}
})
};
const storeMock = <any>{
dispatch: jasmine.createSpy('dispatch')
};
beforeEach(() => {
directive = new ActionDirective(routeMock, storeMock);
});
it('should navigate if action is route', () => {
directive.action = { route: 'dummy' };
directive.onClick();
expect(routeMock.navigate).toHaveBeenCalled();
});
it('should dispatch store action', () => {
directive.action = { click: {} };
directive.onClick();
expect(storeMock.dispatch).toHaveBeenCalled();
});
});

View File

@@ -0,0 +1,72 @@
/*!
* @license
* Alfresco Example Content Application
*
* Copyright (C) 2005 - 2019 Alfresco Software Limited
*
* This file is part of the Alfresco Example Content Application.
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
*
* The Alfresco Example Content Application is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* The Alfresco Example Content Application is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
*/
import { Directive, Input, HostListener } from '@angular/core';
import { PRIMARY_OUTLET, Router } from '@angular/router';
import { Store } from '@ngrx/store';
import { AppStore } from '../../../store/states/app.state';
@Directive({
/* tslint:disable-next-line */
selector: '[action]',
exportAs: 'action'
})
export class ActionDirective {
@Input() action;
@HostListener('click')
onClick() {
if (this.action.route) {
this.router.navigate(this.getNavigationCommands(this.action.route));
} else if (this.action.click) {
this.dispatchAction(this.action.click);
}
}
constructor(private router: Router, private store: Store<AppStore>) {}
private dispatchAction(action) {
this.store.dispatch({
type: action.action,
payload: this.getNavigationCommands(action.payload)
});
}
private getNavigationCommands(url: string): any[] {
const urlTree = this.router.parseUrl(url);
const urlSegmentGroup = urlTree.root.children[PRIMARY_OUTLET];
if (!urlSegmentGroup) {
return [url];
}
const urlSegments = urlSegmentGroup.segments;
return urlSegments.reduce(function(acc, item) {
acc.push(item.path, item.parameters);
return acc;
}, []);
}
}

View File

@@ -0,0 +1,109 @@
/*!
* @license
* Alfresco Example Content Application
*
* Copyright (C) 2005 - 2019 Alfresco Software Limited
*
* This file is part of the Alfresco Example Content Application.
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
*
* The Alfresco Example Content Application is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* The Alfresco Example Content Application is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
*/
import { Component } from '@angular/core';
import { AppSidenavModule } from '../sidenav.module';
import { TestBed, ComponentFixture } from '@angular/core/testing';
import { AppTestingModule } from '../../../testing/app-testing.module';
import { Router, NavigationEnd } from '@angular/router';
import { Subject } from 'rxjs';
@Component({
selector: 'app-test-component',
template: `
<span
id="test-element"
acaActiveLink="active-link-class"
[action]="item"
></span>
`
})
class TestComponent {
item = {
route: 'dummy'
};
}
class MockRouter {
private subject = new Subject();
events = this.subject.asObservable();
url = '';
navigateByUrl(url: string) {
const navigationEnd = new NavigationEnd(0, '', url);
this.subject.next(navigationEnd);
}
}
describe('ActionDirective', () => {
let fixture: ComponentFixture<TestComponent>;
let router: Router;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [AppTestingModule, AppSidenavModule],
declarations: [TestComponent],
providers: [
{
provide: Router,
useClass: MockRouter
}
]
});
fixture = TestBed.createComponent(TestComponent);
router = TestBed.get(Router);
});
it('should add active route class name', () => {
fixture.detectChanges();
router.navigateByUrl('/dummy');
// fixture.detectChanges();
expect(
document.body
.querySelector('#test-element')
.className.includes('active-link-class')
).toBe(true);
});
it('should remove class name if route not active', () => {
fixture.detectChanges();
router.navigateByUrl('/dummy');
expect(
document.body
.querySelector('#test-element')
.className.includes('active-link-class')
).toBe(true);
router.navigateByUrl('/other');
expect(
document.body
.querySelector('#test-element')
.className.includes('active-link-class')
).not.toBe(true);
});
});

View File

@@ -27,57 +27,71 @@ import {
Directive,
OnInit,
Input,
HostListener,
OnDestroy
ElementRef,
Renderer2,
ContentChildren,
QueryList,
AfterContentInit
} from '@angular/core';
import { Router, NavigationEnd } from '@angular/router';
import { filter, takeUntil } from 'rxjs/operators';
import { Subject } from 'rxjs';
import { MatExpansionPanel } from '@angular/material/expansion';
import { ActionDirective } from './action.directive';
@Directive({
selector: '[acaExpansionPanel]',
exportAs: 'acaExpansionPanel'
selector: '[acaActiveLink]',
exportAs: 'acaActiveLink'
})
export class AcaExpansionPanelDirective implements OnInit, OnDestroy {
@Input() acaExpansionPanel;
selected = false;
export class ActiveLinkDirective implements OnInit, AfterContentInit {
@Input() acaActiveLink;
@ContentChildren(ActionDirective, { descendants: true })
links: QueryList<ActionDirective>;
isLinkActive = false;
private onDestroy$: Subject<boolean> = new Subject<boolean>();
@HostListener('click')
onClick() {
if (this.expansionPanel.expanded && !this.selected) {
this.router.navigate([this.acaExpansionPanel.children[0].url]);
}
}
constructor(
private router: Router,
private expansionPanel: MatExpansionPanel
private element: ElementRef,
private renderer: Renderer2
) {}
ngOnInit() {
this.setSelected(this.router.url);
this.router.events
.pipe(
filter(event => event instanceof NavigationEnd),
takeUntil(this.onDestroy$)
)
.subscribe((event: NavigationEnd) => {
this.setSelected(event.urlAfterRedirects);
this.update(event.urlAfterRedirects);
});
}
ngOnDestroy() {
this.onDestroy$.next(true);
this.onDestroy$.complete();
private update(url: string) {
this.links.map(item => {
const itemUrl = this.resolveUrl(item);
if (url && url.substring(1).startsWith(itemUrl)) {
this.isLinkActive = true;
this.renderer.addClass(this.element.nativeElement, this.acaActiveLink);
} else {
this.isLinkActive = false;
this.renderer.removeClass(
this.element.nativeElement,
this.acaActiveLink
);
}
});
}
private setSelected(url: string) {
this.selected = this.acaExpansionPanel.children.some(child =>
url.startsWith(child.url)
ngAfterContentInit() {
this.links.changes.subscribe(() => this.update(this.router.url));
this.update(this.router.url);
}
private resolveUrl(item): string {
return (
(item.action && (item.action.click && item.action.click.payload)) ||
item.action.route
);
}
}

View File

@@ -0,0 +1,31 @@
/*!
* @license
* Alfresco Example Content Application
*
* Copyright (C) 2005 - 2019 Alfresco Software Limited
*
* This file is part of the Alfresco Example Content Application.
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
*
* The Alfresco Example Content Application is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* The Alfresco Example Content Application is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
*/
import { Directive } from '@angular/core';
@Directive({
selector: '[acaCollapsedTemplate]'
})
export class CollapsedTemplateDirective {}

View File

@@ -0,0 +1,31 @@
/*!
* @license
* Alfresco Example Content Application
*
* Copyright (C) 2005 - 2019 Alfresco Software Limited
*
* This file is part of the Alfresco Example Content Application.
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
*
* The Alfresco Example Content Application is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* The Alfresco Example Content Application is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
*/
import { Directive } from '@angular/core';
@Directive({
selector: '[acaExpandedTemplate]'
})
export class ExpandedTemplateDirective {}

View File

@@ -0,0 +1,139 @@
/*!
* @license
* Alfresco Example Content Application
*
* Copyright (C) 2005 - 2019 Alfresco Software Limited
*
* This file is part of the Alfresco Example Content Application.
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
*
* The Alfresco Example Content Application is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* The Alfresco Example Content Application is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
*/
import { NavigationEnd } from '@angular/router';
import { ExpansionPanelDirective } from './expansion-panel.directive';
import { Subject } from 'rxjs';
class RouterStub {
url;
private subject = new Subject();
events = this.subject.asObservable();
constructor(url = 'some-url') {
this.url = url;
}
parseUrl() {
return {
root: {
children: []
}
};
}
navigate(nextUrl: string) {
const navigationEnd = new NavigationEnd(0, this.url, nextUrl);
this.subject.next(navigationEnd);
}
}
describe('AcaExpansionPanel', () => {
const mockStore = <any>{
dispatch: jasmine.createSpy('dispatch')
};
const mockMatExpansionPanel = <any>{
expanded: false,
children: []
};
describe('hasActiveLinks()', () => {
it('should return true if child is active route', () => {
const router: any = new RouterStub('dummy-route-2');
const item = {
children: [{ url: 'dummy-route-1' }, { url: 'dummy-route-2' }]
};
const directive = new ExpansionPanelDirective(
mockStore,
router,
mockMatExpansionPanel
);
directive.acaExpansionPanel = item;
expect(directive.hasActiveLinks()).toBe(true);
});
it('should return false if no child is active route', () => {
const router: any = new RouterStub('other');
const item = {
children: [{ url: 'dummy-route-1' }, { url: 'dummy-route-2' }]
};
const directive = new ExpansionPanelDirective(
mockStore,
router,
mockMatExpansionPanel
);
directive.acaExpansionPanel = item;
expect(directive.hasActiveLinks()).toBe(false);
});
});
describe('navigation', () => {
it('should navigate to first child if none is active route', () => {
const router: any = new RouterStub('other');
spyOn(router, 'navigate').and.callThrough();
const item = {
children: [{ url: 'dummy-route-1' }, { url: 'dummy-route-2' }]
};
mockMatExpansionPanel.expanded = true;
const directive = new ExpansionPanelDirective(
mockStore,
router,
mockMatExpansionPanel
);
directive.acaExpansionPanel = item;
directive.onClick();
expect(router.navigate).toHaveBeenCalledWith(['dummy-route-1']);
});
it('should not navigate to first child if one is active route', () => {
const router: any = new RouterStub('dummy-route-2');
spyOn(router, 'navigate').and.callThrough();
const item = {
children: [{ url: 'dummy-route-1' }, { url: 'dummy-route-2' }]
};
const directive = new ExpansionPanelDirective(
mockStore,
router,
mockMatExpansionPanel
);
directive.acaExpansionPanel = item;
mockMatExpansionPanel.expanded = true;
directive.onClick();
expect(router.navigate).not.toHaveBeenCalled();
});
});
});

View File

@@ -0,0 +1,113 @@
/*!
* @license
* Alfresco Example Content Application
*
* Copyright (C) 2005 - 2019 Alfresco Software Limited
*
* This file is part of the Alfresco Example Content Application.
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
*
* The Alfresco Example Content Application is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* The Alfresco Example Content Application is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
*/
import {
Directive,
Input,
HostListener,
OnInit,
OnDestroy
} from '@angular/core';
import { Router, NavigationEnd, PRIMARY_OUTLET } from '@angular/router';
import { filter, takeUntil } from 'rxjs/operators';
import { Subject } from 'rxjs';
import { MatExpansionPanel } from '@angular/material/expansion';
import { Store } from '@ngrx/store';
import { AppStore } from '../../../store/states/app.state';
@Directive({
selector: '[acaExpansionPanel]',
exportAs: 'acaExpansionPanel'
})
export class ExpansionPanelDirective implements OnInit, OnDestroy {
@Input() acaExpansionPanel;
public hasActiveChildren = false;
private onDestroy$: Subject<boolean> = new Subject<boolean>();
@HostListener('click')
onClick() {
if (this.expansionPanel.expanded && !this.hasActiveLinks()) {
const firstChild = this.acaExpansionPanel.children[0];
if (firstChild.url) {
this.router.navigate(this.getNavigationCommands(firstChild.url));
} else {
this.store.dispatch({
type: firstChild.action.action,
payload: this.getNavigationCommands(firstChild.action.payload)
});
}
}
}
constructor(
private store: Store<AppStore>,
private router: Router,
private expansionPanel: MatExpansionPanel
) {}
hasActiveLinks() {
if (this.acaExpansionPanel && this.acaExpansionPanel.children) {
return this.acaExpansionPanel.children.some(child => {
return this.router.url.startsWith(child.url || child.action.payload);
});
}
return false;
}
ngOnInit() {
this.hasActiveChildren = this.hasActiveLinks();
this.router.events
.pipe(
filter(event => event instanceof NavigationEnd),
takeUntil(this.onDestroy$)
)
.subscribe(() => {
this.hasActiveChildren = this.hasActiveLinks();
});
}
ngOnDestroy() {
this.onDestroy$.next(true);
this.onDestroy$.complete();
}
private getNavigationCommands(url: string): any[] {
const urlTree = this.router.parseUrl(url);
const urlSegmentGroup = urlTree.root.children[PRIMARY_OUTLET];
if (!urlSegmentGroup) {
return [url];
}
const urlSegments = urlSegmentGroup.segments;
return urlSegments.reduce(function(acc, item) {
acc.push(item.path, item.parameters);
return acc;
}, []);
}
}

View File

@@ -0,0 +1,123 @@
/*!
* @license
* Alfresco Example Content Application
*
* Copyright (C) 2005 - 2019 Alfresco Software Limited
*
* This file is part of the Alfresco Example Content Application.
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
*
* The Alfresco Example Content Application is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* The Alfresco Example Content Application is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
*/
import { NavigationEnd } from '@angular/router';
import { MenuPanelDirective } from './menu-panel.directive';
import { Subject } from 'rxjs';
class RouterStub {
url;
private subject = new Subject();
events = this.subject.asObservable();
constructor(url = 'some-url') {
this.url = url;
}
parseUrl() {
return {
root: {
children: []
}
};
}
navigate(nextUrl: string) {
const navigationEnd = new NavigationEnd(0, this.url, nextUrl);
this.subject.next(navigationEnd);
}
}
describe('MenuPanelDirective', () => {
const mockStore = <any>{
dispatch: jasmine.createSpy('dispatch')
};
const mockMatExpansionPanel = <any>{
expanded: false,
children: []
};
describe('hasActiveLinks()', () => {
it('should return true if child is active route', () => {
const router: any = new RouterStub('dummy-route-2');
const item = {
children: [{ url: 'dummy-route-1' }, { url: 'dummy-route-2' }]
};
const directive = new MenuPanelDirective(mockStore, router);
directive.acaMenuPanel = item;
expect(directive.hasActiveLinks()).toBe(true);
});
it('should return false if no child is active route', () => {
const router: any = new RouterStub('other');
const item = {
children: [{ url: 'dummy-route-1' }, { url: 'dummy-route-2' }]
};
const directive = new MenuPanelDirective(mockStore, router);
directive.acaMenuPanel = item;
expect(directive.hasActiveLinks()).toBe(false);
});
});
describe('navigation', () => {
it('should navigate to first child if none is active route', () => {
const router: any = new RouterStub('other');
spyOn(router, 'navigate').and.callThrough();
const item = {
children: [{ url: 'dummy-route-1' }, { url: 'dummy-route-2' }]
};
mockMatExpansionPanel.expanded = true;
const directive = new MenuPanelDirective(mockStore, router);
directive.acaMenuPanel = item;
directive.menuOpened();
expect(router.navigate).toHaveBeenCalledWith(['dummy-route-1']);
});
it('should not navigate to first child if one is active route', () => {
const router: any = new RouterStub('dummy-route-2');
spyOn(router, 'navigate').and.callThrough();
const item = {
children: [{ url: 'dummy-route-1' }, { url: 'dummy-route-2' }]
};
const directive = new MenuPanelDirective(mockStore, router);
directive.acaMenuPanel = item;
mockMatExpansionPanel.expanded = true;
directive.menuOpened();
expect(router.navigate).not.toHaveBeenCalled();
});
});
});

View File

@@ -0,0 +1,108 @@
/*!
* @license
* Alfresco Example Content Application
*
* Copyright (C) 2005 - 2019 Alfresco Software Limited
*
* This file is part of the Alfresco Example Content Application.
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
*
* The Alfresco Example Content Application is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* The Alfresco Example Content Application is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
*/
import {
Directive,
Input,
OnInit,
OnDestroy,
HostListener
} from '@angular/core';
import { Router, NavigationEnd, PRIMARY_OUTLET } from '@angular/router';
import { filter, takeUntil } from 'rxjs/operators';
import { Subject } from 'rxjs';
import { Store } from '@ngrx/store';
import { AppStore } from '../../../store/states/app.state';
@Directive({
selector: '[acaMenuPanel]',
exportAs: 'acaMenuPanel'
})
export class MenuPanelDirective implements OnInit, OnDestroy {
@Input() acaMenuPanel;
hasActiveChildren = false;
private onDestroy$: Subject<boolean> = new Subject<boolean>();
@HostListener('menuOpened')
menuOpened() {
if (this.acaMenuPanel.children && !this.hasActiveLinks()) {
const firstChild = this.acaMenuPanel.children[0];
if (firstChild.url) {
this.router.navigate(this.getNavigationCommands(firstChild.url));
} else {
this.store.dispatch({
type: firstChild.action.action,
payload: this.getNavigationCommands(firstChild.action.payload)
});
}
}
}
constructor(private store: Store<AppStore>, private router: Router) {}
hasActiveLinks() {
if (this.acaMenuPanel && this.acaMenuPanel.children) {
return this.acaMenuPanel.children.some(child => {
return this.router.url.startsWith(child.url || child.action.payload);
});
}
return false;
}
ngOnInit() {
this.hasActiveChildren = this.hasActiveLinks();
this.router.events
.pipe(
filter(event => event instanceof NavigationEnd),
takeUntil(this.onDestroy$)
)
.subscribe(() => {
this.hasActiveChildren = this.hasActiveLinks();
});
}
ngOnDestroy() {
this.onDestroy$.next(true);
this.onDestroy$.complete();
}
private getNavigationCommands(url: string): any[] {
const urlTree = this.router.parseUrl(url);
const urlSegmentGroup = urlTree.root.children[PRIMARY_OUTLET];
if (!urlSegmentGroup) {
return [url];
}
const urlSegments = urlSegmentGroup.segments;
return urlSegments.reduce(function(acc, item) {
acc.push(item.path, item.parameters);
return acc;
}, []);
}
}

View File

@@ -1,119 +0,0 @@
/*!
* @license
* Alfresco Example Content Application
*
* Copyright (C) 2005 - 2019 Alfresco Software Limited
*
* This file is part of the Alfresco Example Content Application.
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
*
* The Alfresco Example Content Application is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* The Alfresco Example Content Application is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
*/
import { NavigationEnd } from '@angular/router';
import { AcaExpansionPanelDirective } from './expansion-panel.directive';
import { Subject } from 'rxjs';
class RouterStub {
url;
private subject = new Subject();
events = this.subject.asObservable();
constructor(url = 'some-url') {
this.url = url;
}
navigate(nextUrl: string) {
const navigationEnd = new NavigationEnd(0, this.url, nextUrl);
this.subject.next(navigationEnd);
}
}
describe('AcaExpansionPanel', () => {
const item = {
children: [{ url: 'dummy-route-1' }, { url: 'dummy-route-2' }]
};
it('should set panel as selected on initialization if url contains child url', () => {
const router: any = new RouterStub('dummy-route-2');
const directive = new AcaExpansionPanelDirective(router, null);
directive.acaExpansionPanel = item;
directive.ngOnInit();
expect(directive.selected).toBe(true);
});
it('should not set panel as selected on initialization if url does not contain child url', () => {
const router: any = new RouterStub('dummy-route-other');
const directive = new AcaExpansionPanelDirective(router, null);
directive.acaExpansionPanel = item;
directive.ngOnInit();
expect(directive.selected).toBe(false);
});
it('should go on first child url when expended and url does not contain any child url', () => {
const router: any = new RouterStub();
spyOn(router, 'navigate');
const expansionPanelInstance: any = { expanded: true };
const directive = new AcaExpansionPanelDirective(
router,
expansionPanelInstance
);
directive.acaExpansionPanel = item;
directive.ngOnInit();
directive.onClick();
expect(router.navigate).toHaveBeenCalledWith(['dummy-route-1']);
});
it('should not go on first child url when expended and url contains any child url', () => {
const router: any = new RouterStub('dummy-route-2');
spyOn(router, 'navigate');
const expansionPanelInstance: any = { expanded: true };
const directive = new AcaExpansionPanelDirective(
router,
expansionPanelInstance
);
directive.acaExpansionPanel = item;
directive.ngOnInit();
directive.onClick();
expect(router.navigate).not.toHaveBeenCalled();
});
it('should set panel selected on navigation change', done => {
const router: any = new RouterStub();
const directive = new AcaExpansionPanelDirective(router, null);
directive.acaExpansionPanel = item;
directive.ngOnInit();
router.navigate('dummy-route-1');
done();
expect(directive.selected).toBe(true);
router.navigate('some-url');
done();
expect(directive.selected).toBe(false);
});
});

View File

@@ -1,216 +1,48 @@
<div class="sidenav">
<div class="section action-menu">
<app-create-menu [expanded]="showLabel"></app-create-menu>
</div>
<div *ngFor="let group of groups; trackBy: trackById" class="section">
<div class="menu">
<div
*ngFor="let item of group.items; trackBy: trackById"
routerLinkActive
#routerLink="routerLinkActive"
>
<ng-container *ngIf="showLabel">
<ng-container *ngIf="!item.children">
<div
class="menu__item"
[attr.title]="item.description | translate"
[attr.data-automation-id]="item.id"
>
<button
[id]="item.id"
mat-icon-button
mat-ripple
[routerLink]="item.url"
[color]="routerLink.isActive ? 'accent' : 'primary'"
[attr.aria-label]="item.title | translate"
matRippleColor="primary"
[matRippleTrigger]="rippleTrigger"
[matRippleCentered]="true"
[matRippleRadius]="20"
>
<adf-icon
[color]="routerLink.isActive ? 'accent' : 'primary'"
[value]="item.icon"
></adf-icon>
</button>
<span
#rippleTrigger
class="item--label item--parent"
[routerLink]="item.url"
[attr.aria-label]="item.title | translate"
[ngClass]="{
'item--active': routerLink.isActive,
'item--default': !routerLink.isActive
}"
>
{{ item.title | translate }}</span
>
</div>
</ng-container>
<ng-container *ngIf="item.children && item.children.length">
<mat-expansion-panel
#expansionPanel="matExpansionPanel"
[acaExpansionPanel]="item"
[expanded]="routerLink.isActive"
[@.disabled]="true"
>
<mat-expansion-panel-header
expandedHeight="48px"
collapsedHeight="48px"
>
<mat-panel-title
[attr.title]="item.description | translate"
[attr.data-automation-id]="item.id"
>
<adf-icon
[color]="
routerLink.isActive && !expansionPanel.expanded
? 'accent'
: 'primary'
"
[value]="item.icon"
></adf-icon>
<span
class="item--label item--parent"
[id]="item.id"
[ngClass]="{
'item--default':
!routerLink.isActive && expansionPanel.expanded,
'item--active':
routerLink.isActive && !expansionPanel.expanded
}"
>{{ item.title | translate }}</span
>
</mat-panel-title>
</mat-expansion-panel-header>
<div
*ngFor="let child of item.children; trackBy: trackById"
routerLinkActive
#childRouteActive="routerLinkActive"
[attr.title]="child.description | translate"
[attr.data-automation-id]="child.id"
>
<ng-container *ngIf="child.icon">
<button
[id]="child.id"
mat-icon-button
mat-ripple
[routerLink]="child.url"
[color]="childRouteActive.isActive ? 'accent' : 'primary'"
[attr.aria-label]="child.title | translate"
matRippleColor="primary"
[matRippleTrigger]="rippleTrigger"
[matRippleCentered]="true"
[matRippleRadius]="20"
>
<adf-icon [value]="item.icon"></adf-icon>
</button>
<span
#rippleTrigger
[routerLink]="child.url"
class="item--label item--label__trigger"
[ngClass]="{
'item--active': childRouteActive.isActive,
'item--default': !childRouteActive.isActive
}"
>
{{ child.title | translate }}
</span>
</ng-container>
<ng-container *ngIf="!child.icon">
<div
[id]="child.id"
class="menu__item item--label item--child"
[routerLink]="child.url"
[attr.aria-label]="child.title | translate"
>
<span
[ngClass]="{
'item--active': childRouteActive.isActive,
'item--default': !childRouteActive.isActive
}"
>
{{ child.title | translate }}
</span>
</div>
</ng-container>
</div>
</mat-expansion-panel>
</ng-container>
</ng-container>
<ng-container *ngIf="!showLabel">
<ng-container *ngIf="!item.children">
<div class="menu__item">
<button
[id]="item.id"
mat-icon-button
[routerLink]="item.url"
[color]="routerLink.isActive ? 'accent' : 'primary'"
[attr.aria-label]="item.title | translate"
[attr.title]="item.description | translate"
[attr.data-automation-id]="item.id"
>
<adf-icon
[color]="routerLink.isActive ? 'accent' : 'primary'"
[value]="item.icon"
></adf-icon>
</button>
</div>
</ng-container>
<ng-container *ngIf="item.children && item.children.length">
<div
class="menu__item"
[attr.title]="item.description | translate"
[attr.data-automation-id]="item.id"
>
<button
[id]="item.id"
color="accent"
mat-icon-button
#childMenu="matMenuTrigger"
[matMenuTriggerFor]="menu"
>
<adf-icon
[color]="
routerLink.isActive || childMenu.menuOpen
? 'accent'
: 'primary'
"
[value]="item.icon"
></adf-icon>
</button>
</div>
<mat-menu #menu="matMenu" [overlapTrigger]="false">
<button
mat-menu-item
*ngFor="let child of item.children; trackBy: trackById"
routerLinkActive
#menuRouterLink="routerLinkActive"
[routerLink]="child.url"
[attr.title]="child.description | translate"
[id]="child.id"
[attr.data-automation-id]="child.id"
>
<span
class="mat-button"
[ngClass]="{ 'mat-primary': menuRouterLink.isActive }"
>
{{ child.title | translate }}
</span>
</button>
</mat-menu>
</ng-container>
</ng-container>
</div>
<ng-container [ngSwitch]="mode">
<div class="section action-menu" [ngClass]="'section--' + mode">
<app-create-menu [expanded]="mode === 'expanded'"></app-create-menu>
</div>
</div>
<div
*ngFor="let group of groups; trackBy: trackById"
class="section"
[ngClass]="'section--' + mode"
>
<ng-container *ngSwitchCase="'expanded'">
<mat-list-item *ngFor="let item of group.items; trackBy: trackById">
<ng-container *ngIf="expandedTemplate">
<ng-template
[ngTemplateOutlet]="expandedTemplate"
[ngTemplateOutletContext]="{ $implicit: item }"
>
</ng-template>
</ng-container>
<ng-container *ngIf="!expandedTemplate">
<app-expand-menu [item]="item"></app-expand-menu>
</ng-container>
</mat-list-item>
</ng-container>
<ng-container *ngSwitchCase="'collapsed'">
<div
class="list-item"
*ngFor="let item of group.items; trackBy: trackById"
>
<ng-container *ngIf="collapsedTemplate">
<ng-template
[ngTemplateOutlet]="collapsedTemplate"
[ngTemplateOutletContext]="{ $implicit: item }"
>
</ng-template>
</ng-container>
<ng-container *ngIf="!collapsedTemplate">
<app-button-menu [item]="item"></app-button-menu>
</ng-container>
</div>
</ng-container>
</div>
</ng-container>
</div>

View File

@@ -1,3 +1,10 @@
.app-sidenav {
display: flex;
flex: 1;
flex-direction: column;
height: 100%;
}
.sidenav {
display: flex;
flex: 1;
@@ -8,10 +15,6 @@
border-bottom: 0;
}
.section {
padding: 8px 14px;
}
.action-menu {
display: flex;
height: 40px;
@@ -19,14 +22,50 @@
align-items: center;
}
.menu {
display: flex;
flex-direction: column;
padding: 0;
margin: 0;
.section.action-menu {
padding: 8px 14px;
}
.menu__item {
.section {
padding: 8px 6px;
}
.section--collapsed {
display: flex;
flex-direction: column;
align-items: center;
}
.list-item {
padding: 12px 0;
display: flex;
align-items: center;
height: 24px;
}
.menu {
display: flex;
flex: 1;
flex-direction: row;
}
.full-width {
display: flex;
width: 100%;
}
.action-button .action-button__label {
margin-left: 8px !important;
}
.app-item,
.app-item .item {
display: flex;
flex: 1;
flex-direction: row;
}
.item {
padding: 12px 0;
flex-direction: row;
display: flex;
@@ -34,41 +73,27 @@
text-decoration: none;
text-decoration: none;
height: 24px;
width: 100%;
user-select: none;
}
.item--parent {
font-weight: 600;
}
.item--label {
cursor: pointer;
width: 240px;
padding-left: 10px;
}
.item--child {
padding-left: 25px;
}
.item--label:focus {
outline: none;
}
.item--label__trigger {
padding-left: 0;
}
.mat-expansion-panel-header {
padding: 0 8px !important;
padding: 0 8px 0 0 !important;
display: flex;
align-items: center;
}
.mat-expansion-panel-header-title span {
margin-left: 8px;
.mat-expansion-panel {
width: 100%;
}
.mat-expansion-indicator {
display: flex;
align-content: center;
}
.mat-expansion-panel-body {
padding: 0 24px 0px;
padding-bottom: 0;
}
.mat-expansion-panel-header-title {

View File

@@ -6,7 +6,7 @@
*
* 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
* 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
@@ -16,7 +16,7 @@
*
* 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
* 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
@@ -27,7 +27,6 @@ import { NO_ERRORS_SCHEMA } from '@angular/core';
import { TestBed, async, ComponentFixture } from '@angular/core/testing';
import { SidenavComponent } from './sidenav.component';
import { AppTestingModule } from '../../testing/app-testing.module';
import { MatExpansionModule } from '@angular/material/expansion';
import { AppExtensionService } from '../../extensions/extension.service';
describe('SidenavComponent', () => {
@@ -46,7 +45,7 @@ describe('SidenavComponent', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [MatExpansionModule, AppTestingModule],
imports: [AppTestingModule],
providers: [AppExtensionService],
declarations: [SidenavComponent],
schemas: [NO_ERRORS_SCHEMA]

View File

@@ -6,11 +6,27 @@
$border: 1px solid mat-color($foreground, divider, 0.07);
.sidenav {
@include angular-material-theme($theme);
.aca-menu-panel {
.action-button--active {
color: mat-color($accent) !important;
}
.action-button {
color: mat-color($primary);
}
.action-button:hover {
color: mat-color($accent);
}
}
.sidenav {
background-color: mat-color($background, background);
.item:hover .action-button__label {
color: mat-color($accent);
}
.mat-expansion-panel {
background-color: unset;
color: mat-color($primary, 0.87) !important;
@@ -29,28 +45,20 @@
font-size: 14px !important;
}
.adf-sidebar-action-menu-button {
background-color: mat-color($accent);
.mat-expansion-panel-header-title > span {
margin-left: 0 !important;
}
.action-button--active {
color: mat-color($accent) !important;
}
.action-button {
color: mat-color($primary);
}
.section {
border-bottom: $border;
}
.item--label:not(.item--active):hover {
color: mat-color($foreground, text);
}
.item--label {
color: mat-color($primary, 0.87);
}
.item--active {
color: mat-color($accent);
}
.item--default {
color: mat-color($primary, 0.87);
}
}
}

14
src/app/components/sidenav/sidenav.component.ts Normal file → Executable file
View File

@@ -25,11 +25,15 @@
import {
Component,
ContentChild,
Input,
TemplateRef,
OnInit,
ViewEncapsulation,
OnDestroy
} from '@angular/core';
import { CollapsedTemplateDirective } from './directives/collapsed-template.directive';
import { ExpandedTemplateDirective } from './directives/expanded-template.directive';
import { AppExtensionService } from '../../extensions/extension.service';
import { NavBarGroupRef } from '@alfresco/adf-extensions';
import { Store } from '@ngrx/store';
@@ -46,12 +50,16 @@ import { takeUntil, distinctUntilChanged, map } from 'rxjs/operators';
host: { class: 'app-sidenav' }
})
export class SidenavComponent implements OnInit, OnDestroy {
private onDestroy$: Subject<boolean> = new Subject<boolean>();
@Input() mode: 'collapsed' | 'expanded' = 'expanded';
@Input()
showLabel: boolean;
@ContentChild(ExpandedTemplateDirective, { read: TemplateRef })
expandedTemplate;
@ContentChild(CollapsedTemplateDirective, { read: TemplateRef })
collapsedTemplate;
groups: Array<NavBarGroupRef> = [];
private onDestroy$: Subject<boolean> = new Subject<boolean>();
constructor(
private store: Store<AppStore>,

View File

@@ -26,11 +26,17 @@
import { NgModule } from '@angular/core';
import { AppCreateMenuModule } from '../create-menu/create-menu.module';
import { CommonModule } from '@angular/common';
import { SidenavComponent } from './sidenav.component';
import { CoreModule } from '@alfresco/adf-core';
import { RouterModule } from '@angular/router';
import { AcaExpansionPanelDirective } from './expansion-panel.directive';
import { ExpansionPanelDirective } from './directives/expansion-panel.directive';
import { MenuPanelDirective } from './directives/menu-panel.directive';
import { CollapsedTemplateDirective } from './directives/collapsed-template.directive';
import { ExpandedTemplateDirective } from './directives/expanded-template.directive';
import { SidenavComponent } from './sidenav.component';
import { ActiveLinkDirective } from './directives/active-link.directive';
import { ExpandMenuComponent } from './components/expand-menu.component';
import { ButtonMenuComponent } from './components/button-menu.component';
import { ActionDirective } from './directives/action.directive';
@NgModule({
imports: [
CommonModule,
@@ -38,7 +44,27 @@ import { AcaExpansionPanelDirective } from './expansion-panel.directive';
RouterModule,
AppCreateMenuModule
],
declarations: [SidenavComponent, AcaExpansionPanelDirective],
exports: [SidenavComponent, AcaExpansionPanelDirective]
declarations: [
MenuPanelDirective,
ExpansionPanelDirective,
ExpandedTemplateDirective,
CollapsedTemplateDirective,
ActiveLinkDirective,
ActionDirective,
ExpandMenuComponent,
ButtonMenuComponent,
SidenavComponent
],
exports: [
MenuPanelDirective,
ExpansionPanelDirective,
ExpandedTemplateDirective,
CollapsedTemplateDirective,
ActiveLinkDirective,
ActionDirective,
ExpandMenuComponent,
ButtonMenuComponent,
SidenavComponent
]
})
export class AppSidenavModule {}

View File

@@ -244,15 +244,22 @@ export class AppExtensionService implements RuleContext {
.filter(child => this.filterVisible(child))
.sort(sortByOrder)
.map(child => {
const childRouteRef = this.extensions.getRouteById(
child.route
);
const childUrl = `/${
childRouteRef ? childRouteRef.path : child.route
}`;
if (!child.click) {
const childRouteRef = this.extensions.getRouteById(
child.route
);
const childUrl = `/${
childRouteRef ? childRouteRef.path : child.route
}`;
return {
...child,
url: childUrl
};
}
return {
...child,
url: childUrl
action: child.click
};
});
@@ -261,11 +268,18 @@ export class AppExtensionService implements RuleContext {
};
}
const routeRef = this.extensions.getRouteById(item.route);
const url = `/${routeRef ? routeRef.path : item.route}`;
if (!item.click) {
const routeRef = this.extensions.getRouteById(item.route);
const url = `/${routeRef ? routeRef.path : item.route}`;
return {
...item,
url
};
}
return {
...item,
url
action: item.click
};
})
.reduce(reduceEmptyMenus, [])