From 839c9d0dbb6ca30ddb18125310690197d6424dcd Mon Sep 17 00:00:00 2001 From: Cilibiu Bogdan Date: Fri, 5 Apr 2019 15:03:40 +0300 Subject: [PATCH] [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 --- docs/getting-started/navigation.md | 24 ++ e2e/components/sidenav/sidenav.ts | 11 +- e2e/suites/navigation/sidebar.test.ts | 12 +- .../create-menu/create-menu.component.html | 3 +- .../create-menu/create-menu.component.scss | 6 +- .../app-layout/app-layout.component.html | 5 +- .../components/button-menu.component.html | 50 ++++ .../components/button-menu.component.spec.ts | 99 +++++++ .../components/button-menu.component.ts | 58 ++++ .../components/expand-menu.component.html | 65 +++++ .../components/expand-menu.component.spec.ts | 99 +++++++ .../components/expand-menu.component.ts | 52 ++++ .../directives/action.directive.spec.ts | 57 ++++ .../sidenav/directives/action.directive.ts | 72 +++++ .../directives/active-link.directive.spec.ts | 109 ++++++++ .../active-link.directive.ts} | 64 +++-- .../collapsed-template.directive.ts | 31 +++ .../directives/expanded-template.directive.ts | 31 +++ .../expansion-panel.directive.spec.ts | 139 ++++++++++ .../directives/expansion-panel.directive.ts | 113 ++++++++ .../directives/menu-panel.directive.spec.ts | 123 +++++++++ .../directives/menu-panel.directive.ts | 108 ++++++++ .../sidenav/expansion-panel.directive.spec.ts | 119 -------- .../components/sidenav/sidenav.component.html | 258 +++--------------- .../components/sidenav/sidenav.component.scss | 97 ++++--- .../sidenav/sidenav.component.spec.ts | 7 +- .../sidenav/sidenav.component.theme.scss | 48 ++-- .../components/sidenav/sidenav.component.ts | 14 +- src/app/components/sidenav/sidenav.module.ts | 36 ++- src/app/extensions/extension.service.ts | 34 ++- 30 files changed, 1493 insertions(+), 451 deletions(-) create mode 100644 src/app/components/sidenav/components/button-menu.component.html create mode 100644 src/app/components/sidenav/components/button-menu.component.spec.ts create mode 100644 src/app/components/sidenav/components/button-menu.component.ts create mode 100644 src/app/components/sidenav/components/expand-menu.component.html create mode 100644 src/app/components/sidenav/components/expand-menu.component.spec.ts create mode 100644 src/app/components/sidenav/components/expand-menu.component.ts create mode 100644 src/app/components/sidenav/directives/action.directive.spec.ts create mode 100644 src/app/components/sidenav/directives/action.directive.ts create mode 100644 src/app/components/sidenav/directives/active-link.directive.spec.ts rename src/app/components/sidenav/{expansion-panel.directive.ts => directives/active-link.directive.ts} (57%) create mode 100755 src/app/components/sidenav/directives/collapsed-template.directive.ts create mode 100755 src/app/components/sidenav/directives/expanded-template.directive.ts create mode 100644 src/app/components/sidenav/directives/expansion-panel.directive.spec.ts create mode 100644 src/app/components/sidenav/directives/expansion-panel.directive.ts create mode 100644 src/app/components/sidenav/directives/menu-panel.directive.spec.ts create mode 100644 src/app/components/sidenav/directives/menu-panel.directive.ts delete mode 100644 src/app/components/sidenav/expansion-panel.directive.spec.ts mode change 100644 => 100755 src/app/components/sidenav/sidenav.component.ts diff --git a/docs/getting-started/navigation.md b/docs/getting-started/navigation.md index c684b366b..23aa98e62 100644 --- a/docs/getting-started/navigation.md +++ b/docs/getting-started/navigation.md @@ -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 diff --git a/e2e/components/sidenav/sidenav.ts b/e2e/components/sidenav/sidenav.ts index c138c7e3d..7601ccd03 100755 --- a/e2e/components/sidenav/sidenav.ts +++ b/e2e/components/sidenav/sidenav.ts @@ -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) { diff --git a/e2e/suites/navigation/sidebar.test.ts b/e2e/suites/navigation/sidebar.test.ts index 6db34fa14..1787c9b4c 100755 --- a/e2e/suites/navigation/sidebar.test.ts +++ b/e2e/suites/navigation/sidebar.test.ts @@ -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 () => { diff --git a/src/app/components/create-menu/create-menu.component.html b/src/app/components/create-menu/create-menu.component.html index bf21d132a..26515c61e 100644 --- a/src/app/components/create-menu/create-menu.component.html +++ b/src/app/components/create-menu/create-menu.component.html @@ -16,7 +16,6 @@ + + + + + + + + + diff --git a/src/app/components/sidenav/components/button-menu.component.spec.ts b/src/app/components/sidenav/components/button-menu.component.spec.ts new file mode 100644 index 000000000..86480e5c1 --- /dev/null +++ b/src/app/components/sidenav/components/button-menu.component.spec.ts @@ -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 . + */ + +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; + 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(); + }); +}); diff --git a/src/app/components/sidenav/components/button-menu.component.ts b/src/app/components/sidenav/components/button-menu.component.ts new file mode 100644 index 000000000..d7e2d7879 --- /dev/null +++ b/src/app/components/sidenav/components/button-menu.component.ts @@ -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 . + */ + +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; + } +} diff --git a/src/app/components/sidenav/components/expand-menu.component.html b/src/app/components/sidenav/components/expand-menu.component.html new file mode 100644 index 000000000..85dd8e34f --- /dev/null +++ b/src/app/components/sidenav/components/expand-menu.component.html @@ -0,0 +1,65 @@ + +
+ +
+
+ + + + + +
+ +
+
+
+ +
+ +
+
+
diff --git a/src/app/components/sidenav/components/expand-menu.component.spec.ts b/src/app/components/sidenav/components/expand-menu.component.spec.ts new file mode 100644 index 000000000..495207bdd --- /dev/null +++ b/src/app/components/sidenav/components/expand-menu.component.spec.ts @@ -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 . + */ + +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; + 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(); + }); +}); diff --git a/src/app/components/sidenav/components/expand-menu.component.ts b/src/app/components/sidenav/components/expand-menu.component.ts new file mode 100644 index 000000000..e416a41da --- /dev/null +++ b/src/app/components/sidenav/components/expand-menu.component.ts @@ -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 . + */ + +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; + } +} diff --git a/src/app/components/sidenav/directives/action.directive.spec.ts b/src/app/components/sidenav/directives/action.directive.spec.ts new file mode 100644 index 000000000..344089ac6 --- /dev/null +++ b/src/app/components/sidenav/directives/action.directive.spec.ts @@ -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 . + */ + +import { ActionDirective } from './action.directive'; + +describe('ActionDirective', () => { + let directive: ActionDirective; + const routeMock = { + navigate: jasmine.createSpy('navigate'), + parseUrl: () => ({ + root: { + children: [] + } + }) + }; + const storeMock = { + 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(); + }); +}); diff --git a/src/app/components/sidenav/directives/action.directive.ts b/src/app/components/sidenav/directives/action.directive.ts new file mode 100644 index 000000000..bc201ea8e --- /dev/null +++ b/src/app/components/sidenav/directives/action.directive.ts @@ -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 . + */ + +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) {} + + 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; + }, []); + } +} diff --git a/src/app/components/sidenav/directives/active-link.directive.spec.ts b/src/app/components/sidenav/directives/active-link.directive.spec.ts new file mode 100644 index 000000000..ae264c351 --- /dev/null +++ b/src/app/components/sidenav/directives/active-link.directive.spec.ts @@ -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 . + */ + +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: ` + + ` +}) +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; + 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); + }); +}); diff --git a/src/app/components/sidenav/expansion-panel.directive.ts b/src/app/components/sidenav/directives/active-link.directive.ts similarity index 57% rename from src/app/components/sidenav/expansion-panel.directive.ts rename to src/app/components/sidenav/directives/active-link.directive.ts index b39adbf45..fd2f7c547 100644 --- a/src/app/components/sidenav/expansion-panel.directive.ts +++ b/src/app/components/sidenav/directives/active-link.directive.ts @@ -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; + isLinkActive = false; private onDestroy$: Subject = new Subject(); - @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 ); } } diff --git a/src/app/components/sidenav/directives/collapsed-template.directive.ts b/src/app/components/sidenav/directives/collapsed-template.directive.ts new file mode 100755 index 000000000..10798f654 --- /dev/null +++ b/src/app/components/sidenav/directives/collapsed-template.directive.ts @@ -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 . + */ + +import { Directive } from '@angular/core'; + +@Directive({ + selector: '[acaCollapsedTemplate]' +}) +export class CollapsedTemplateDirective {} diff --git a/src/app/components/sidenav/directives/expanded-template.directive.ts b/src/app/components/sidenav/directives/expanded-template.directive.ts new file mode 100755 index 000000000..347e774ac --- /dev/null +++ b/src/app/components/sidenav/directives/expanded-template.directive.ts @@ -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 . + */ + +import { Directive } from '@angular/core'; + +@Directive({ + selector: '[acaExpandedTemplate]' +}) +export class ExpandedTemplateDirective {} diff --git a/src/app/components/sidenav/directives/expansion-panel.directive.spec.ts b/src/app/components/sidenav/directives/expansion-panel.directive.spec.ts new file mode 100644 index 000000000..06376cd82 --- /dev/null +++ b/src/app/components/sidenav/directives/expansion-panel.directive.spec.ts @@ -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 . + */ + +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 = { + dispatch: jasmine.createSpy('dispatch') + }; + const mockMatExpansionPanel = { + 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(); + }); + }); +}); diff --git a/src/app/components/sidenav/directives/expansion-panel.directive.ts b/src/app/components/sidenav/directives/expansion-panel.directive.ts new file mode 100644 index 000000000..86640f308 --- /dev/null +++ b/src/app/components/sidenav/directives/expansion-panel.directive.ts @@ -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 . + */ + +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 = new Subject(); + + @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, + 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; + }, []); + } +} diff --git a/src/app/components/sidenav/directives/menu-panel.directive.spec.ts b/src/app/components/sidenav/directives/menu-panel.directive.spec.ts new file mode 100644 index 000000000..0a9f3c94e --- /dev/null +++ b/src/app/components/sidenav/directives/menu-panel.directive.spec.ts @@ -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 . + */ + +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 = { + dispatch: jasmine.createSpy('dispatch') + }; + const mockMatExpansionPanel = { + 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(); + }); + }); +}); diff --git a/src/app/components/sidenav/directives/menu-panel.directive.ts b/src/app/components/sidenav/directives/menu-panel.directive.ts new file mode 100644 index 000000000..154bfbd76 --- /dev/null +++ b/src/app/components/sidenav/directives/menu-panel.directive.ts @@ -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 . + */ + +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 = new Subject(); + + @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, 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; + }, []); + } +} diff --git a/src/app/components/sidenav/expansion-panel.directive.spec.ts b/src/app/components/sidenav/expansion-panel.directive.spec.ts deleted file mode 100644 index 2e82e29a1..000000000 --- a/src/app/components/sidenav/expansion-panel.directive.spec.ts +++ /dev/null @@ -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 . - */ - -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); - }); -}); diff --git a/src/app/components/sidenav/sidenav.component.html b/src/app/components/sidenav/sidenav.component.html index a86acb877..f8762c692 100644 --- a/src/app/components/sidenav/sidenav.component.html +++ b/src/app/components/sidenav/sidenav.component.html @@ -1,216 +1,48 @@
-
- -
- -
- + +
+ + + + + + + + + + + + + + +
+ + + + + + + + +
+
+
+
diff --git a/src/app/components/sidenav/sidenav.component.scss b/src/app/components/sidenav/sidenav.component.scss index 824418e89..9f8f19998 100644 --- a/src/app/components/sidenav/sidenav.component.scss +++ b/src/app/components/sidenav/sidenav.component.scss @@ -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 { diff --git a/src/app/components/sidenav/sidenav.component.spec.ts b/src/app/components/sidenav/sidenav.component.spec.ts index 4ac0352a8..ea5f1601f 100644 --- a/src/app/components/sidenav/sidenav.component.spec.ts +++ b/src/app/components/sidenav/sidenav.component.spec.ts @@ -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] diff --git a/src/app/components/sidenav/sidenav.component.theme.scss b/src/app/components/sidenav/sidenav.component.theme.scss index 65e3c86fe..04c83e59f 100644 --- a/src/app/components/sidenav/sidenav.component.theme.scss +++ b/src/app/components/sidenav/sidenav.component.theme.scss @@ -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); - } } } diff --git a/src/app/components/sidenav/sidenav.component.ts b/src/app/components/sidenav/sidenav.component.ts old mode 100644 new mode 100755 index e07459bc1..3de324148 --- a/src/app/components/sidenav/sidenav.component.ts +++ b/src/app/components/sidenav/sidenav.component.ts @@ -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 = new Subject(); + @Input() mode: 'collapsed' | 'expanded' = 'expanded'; - @Input() - showLabel: boolean; + @ContentChild(ExpandedTemplateDirective, { read: TemplateRef }) + expandedTemplate; + + @ContentChild(CollapsedTemplateDirective, { read: TemplateRef }) + collapsedTemplate; groups: Array = []; + private onDestroy$: Subject = new Subject(); constructor( private store: Store, diff --git a/src/app/components/sidenav/sidenav.module.ts b/src/app/components/sidenav/sidenav.module.ts index 16bd7dee9..427f0b02c 100644 --- a/src/app/components/sidenav/sidenav.module.ts +++ b/src/app/components/sidenav/sidenav.module.ts @@ -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 {} diff --git a/src/app/extensions/extension.service.ts b/src/app/extensions/extension.service.ts index 9a28a3c87..5a058aa12 100644 --- a/src/app/extensions/extension.service.ts +++ b/src/app/extensions/extension.service.ts @@ -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, [])