[AAE-11496] Move 'content-plugin' to projects folder as 'aca-content' (#2817)

* [AAE-11496] Move content-plugin to projects

* Fix unit test
This commit is contained in:
Bartosz Sekuła
2022-12-20 18:15:34 +01:00
committed by GitHub
parent c87662900e
commit e570ef8da0
263 changed files with 291 additions and 58 deletions

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,103 @@
/*!
* @license
* Alfresco Example Content Application
*
* Copyright (C) 2005 - 2020 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.inject(Router);
spyOn(router, 'navigate');
});
it('should render action item', () => {
component.item = {
id: 'test-action-button',
url: 'dummy',
icon: null,
title: null,
route: null
};
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',
icon: null,
title: null,
route: null,
children: [
{
id: 'child-1',
title: 'child-1',
url: 'dummy',
icon: null,
route: null
},
{
id: 'child-2',
title: 'child-2',
url: 'dummy',
icon: null,
route: null
}
]
};
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,51 @@
/*!
* @license
* Alfresco Example Content Application
*
* Copyright (C) 2005 - 2020 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';
import { NavBarLinkRef } from '@alfresco/adf-extensions';
@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: NavBarLinkRef;
constructor(private cd: ChangeDetectorRef, private overlayContainer: OverlayContainer) {
this.overlayContainer.getContainerElement().classList.add('aca-menu-panel');
}
ngOnInit() {
this.cd.detectChanges();
}
trackById(_index: number, obj: NavBarLinkRef) {
return obj.id;
}
}

View File

@@ -0,0 +1,63 @@
<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" role="group">
<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,103 @@
/*!
* @license
* Alfresco Example Content Application
*
* Copyright (C) 2005 - 2020 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.inject(Router);
spyOn(router, 'navigate');
});
it('should render action item', () => {
component.item = {
id: 'test-action-button',
url: 'dummy',
title: null,
icon: null,
route: null
};
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',
icon: null,
title: null,
route: null,
children: [
{
id: 'child-1',
title: 'child-1',
url: 'dummy',
icon: null,
route: null
},
{
id: 'child-2',
title: 'child-2',
url: 'dummy',
icon: null,
route: null
}
]
};
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,48 @@
/*!
* @license
* Alfresco Example Content Application
*
* Copyright (C) 2005 - 2020 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';
import { NavBarLinkRef } from '@alfresco/adf-extensions';
@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: NavBarLinkRef;
constructor(private cd: ChangeDetectorRef) {}
ngOnInit() {
this.cd.detectChanges();
}
trackById(_index: number, obj: NavBarLinkRef) {
return obj.id;
}
}

View File

@@ -0,0 +1,57 @@
/*!
* @license
* Alfresco Example Content Application
*
* Copyright (C) 2005 - 2020 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 = { url: '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,68 @@
/*!
* @license
* Alfresco Example Content Application
*
* Copyright (C) 2005 - 2020 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 '@alfresco/aca-shared/store';
@Directive({
/* eslint-disable-next-line */
selector: '[action]',
exportAs: 'action'
})
export class ActionDirective {
@Input() action;
@HostListener('click')
onClick() {
if (this.action.url) {
this.router.navigate(this.getNavigationCommands(this.action.url));
} else if (this.action.click) {
this.store.dispatch({
type: this.action.click.action,
payload: this.getNavigationCommands(this.action.click.payload)
});
}
}
constructor(private router: Router, private store: Store<AppStore>) {}
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,91 @@
/*!
* @license
* Alfresco Example Content Application
*
* Copyright (C) 2005 - 2020 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.inject(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

@@ -0,0 +1,87 @@
/*!
* @license
* Alfresco Example Content Application
*
* Copyright (C) 2005 - 2020 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, OnInit, Input, ElementRef, Renderer2, ContentChildren, QueryList, AfterContentInit, Optional } from '@angular/core';
import { Router, NavigationEnd } from '@angular/router';
import { filter, takeUntil } from 'rxjs/operators';
import { Subject } from 'rxjs';
import { ActionDirective } from './action.directive';
@Directive({
selector: '[acaActiveLink]',
exportAs: 'acaActiveLink'
})
export class ActiveLinkDirective implements OnInit, AfterContentInit {
@Input() acaActiveLink;
@ContentChildren(ActionDirective, { descendants: true })
links: QueryList<ActionDirective>;
isLinkActive = false;
private onDestroy$: Subject<boolean> = new Subject<boolean>();
constructor(private router: Router, private element: ElementRef, private renderer: Renderer2, @Optional() private action?: ActionDirective) {}
ngOnInit() {
this.router.events
.pipe(
filter((event) => event instanceof NavigationEnd),
takeUntil(this.onDestroy$)
)
.subscribe((event: NavigationEnd) => {
this.update(event.urlAfterRedirects);
});
}
private update(url: string) {
if (this.action) {
const itemUrl = this.resolveUrl(this.action);
this.render(url, itemUrl);
}
this.links.map((item) => {
const itemUrl = this.resolveUrl(item);
this.render(url, itemUrl);
});
}
private render(routerUrl: string, actionUrl: string) {
if (routerUrl && routerUrl.substring(1).startsWith(actionUrl)) {
this.isLinkActive = true;
this.renderer.addClass(this.element.nativeElement, this.acaActiveLink);
} else {
this.isLinkActive = false;
this.renderer.removeClass(this.element.nativeElement, this.acaActiveLink);
}
}
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,123 @@
/*!
* @license
* Alfresco Example Content Application
*
* Copyright (C) 2005 - 2020 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,100 @@
/*!
* @license
* Alfresco Example Content Application
*
* Copyright (C) 2005 - 2020 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';
@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<any>, private router: Router, private expansionPanel: MatExpansionPanel) {}
hasActiveLinks() {
if (this.acaExpansionPanel && this.acaExpansionPanel.children) {
return this.acaExpansionPanel.children.some((child) => 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 - 2020 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,99 @@
/*!
* @license
* Alfresco Example Content Application
*
* Copyright (C) 2005 - 2020 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';
@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<any>, private router: Router) {}
hasActiveLinks() {
if (this.acaMenuPanel && this.acaMenuPanel.children) {
return this.acaMenuPanel.children.some((child) => 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,3 @@
<app-sidenav
[mode]="data.mode"
></app-sidenav>

View File

@@ -0,0 +1,39 @@
/*!
* @license
* Alfresco Example Content Application
*
* Copyright (C) 2005 - 2020 Alfresco Software Limited
*
* This file is part of the Alfresco Example Content Application.
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
*
* The Alfresco Example Content Application is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* The Alfresco Example Content Application is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
*/
import { Component, Input } from '@angular/core';
/**
* This wrapper is designated to be used with 'adf-dynamic-component'.
* It forwards the dynamic component inputs to original sidenav.
*/
@Component({
selector: 'aca-sidenav-wrapper',
templateUrl: './sidenav-wrapper.component.html'
})
export class SidenavWrapperComponent {
@Input()
data: { mode?: 'collapsed' | 'expanded' } = {};
}

View File

@@ -0,0 +1,36 @@
<div class="sidenav">
<ng-container [ngSwitch]="mode">
<div class="section action-menu" [ngClass]="'section--' + mode">
<app-main-action [expanded]="mode === 'expanded'"></app-main-action>
<app-create-menu [expanded]="mode === 'expanded'"></app-create-menu>
</div>
<div class="section-sub-actions">
<div *ngFor="let group of groups; trackBy: trackByGroupId" class="section" [ngClass]="'section--' + mode">
<ng-container *ngSwitchCase="'expanded'">
<mat-list-item *ngFor="let item of group.items; trackBy: trackByLinkId">
<ng-container *ngIf="!item.component">
<app-expand-menu [item]="item"></app-expand-menu>
</ng-container>
<ng-container *ngIf="item.component">
<adf-dynamic-component [data]="{ item: item, state: 'expanded' }" [id]="item.component"></adf-dynamic-component>
</ng-container>
</mat-list-item>
</ng-container>
<ng-container *ngSwitchCase="'collapsed'">
<div class="list-item" *ngFor="let item of group.items; trackBy: trackByLinkId">
<ng-container *ngIf="!item.component">
<app-button-menu [item]="item"></app-button-menu>
</ng-container>
<ng-container *ngIf="item.component">
<adf-dynamic-component [data]="{ item: item, state: 'collapsed' }" [id]="item.component"> </adf-dynamic-component>
</ng-container>
</div>
</ng-container>
</div>
</div>
</ng-container>
</div>

View File

@@ -0,0 +1,150 @@
.app-sidenav {
display: flex;
flex: 1;
flex-direction: column;
height: 100%;
overflow-y: hidden;
}
.aca-menu-panel {
.action-button--active {
color: var(--theme-accent-color) !important;
}
.action-button {
color: var(--theme-primary-color);
}
.action-button:hover {
color: var(--theme-accent-color);
}
}
.sidenav {
display: flex;
flex: 1;
flex-direction: column;
height: 100%;
background-color: var(--theme-background-color);
.section:last-child {
border-bottom: 0;
}
.action-menu {
display: flex;
flex-direction: column;
justify-content: center;
align-items: stretch;
}
.section.action-menu {
padding: 8px 14px;
position: sticky;
}
.section-sub-actions {
overflow-y: auto;
}
.section {
padding: 8px 6px;
border-bottom: 1px solid var(--theme-divider-color);
}
.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--active {
color: var(--theme-primary-color) !important;
}
.action-button {
color: var(--theme-text-color);
}
.action-button .action-button__label {
margin: 0 8px !important;
}
.app-item,
.app-item .item {
display: flex;
flex: 1;
flex-direction: row;
}
.item {
padding: 12px 0;
flex-direction: row;
display: flex;
align-items: center;
text-decoration: none;
height: 24px;
width: 100%;
user-select: none;
}
.item:hover .action-button__label {
color: var(--theme-primary-color);
}
.mat-expansion-panel-header {
padding: 0 8px 0 0 !important;
display: flex;
align-items: center;
font-size: 14px !important;
}
.mat-expansion-panel {
width: 100%;
background-color: unset;
box-shadow: none !important;
}
.mat-expansion-panel:not(.mat-expanded) .mat-expansion-panel-header:not([aria-disabled='true']):hover {
background: none !important;
}
.mat-expansion-indicator {
display: flex;
align-content: center;
}
.mat-expansion-panel-body {
padding-bottom: 0;
}
.mat-expansion-panel-header-title {
display: flex;
flex-direction: row;
align-items: center;
}
}
[dir='rtl'] .sidenav {
.mat-expansion-panel-header {
padding: 0 0 0 8px !important;
}
}

View File

@@ -0,0 +1,76 @@
/*!
* @license
* Alfresco Example Content Application
*
* Copyright (C) 2005 - 2020 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 { NO_ERRORS_SCHEMA } from '@angular/core';
import { TestBed, ComponentFixture } from '@angular/core/testing';
import { SidenavComponent } from './sidenav.component';
import { AppTestingModule } from '../../testing/app-testing.module';
import { AppExtensionService } from '@alfresco/aca-shared';
describe('SidenavComponent', () => {
let fixture: ComponentFixture<SidenavComponent>;
let component: SidenavComponent;
let extensionService: AppExtensionService;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [AppTestingModule],
declarations: [SidenavComponent],
schemas: [NO_ERRORS_SCHEMA]
});
fixture = TestBed.createComponent(SidenavComponent);
component = fixture.componentInstance;
extensionService = TestBed.inject(AppExtensionService);
extensionService.navbar = [
{
id: 'route',
items: [
{
id: 'item-1',
icon: 'item',
route: 'route',
title: 'item-1'
}
]
}
];
});
it('should set the sidenav data', async () => {
fixture.detectChanges();
await fixture.whenStable();
expect(component.groups.length).toBe(1);
expect(component.groups[0].items.length).toBe(1);
expect(component.groups[0].items[0]).toEqual({
id: 'item-1',
icon: 'item',
url: '/route',
route: 'route',
title: 'item-1'
});
});
});

View File

@@ -0,0 +1,71 @@
/*!
* @license
* Alfresco Example Content Application
*
* Copyright (C) 2005 - 2020 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, OnInit, ViewEncapsulation, OnDestroy } from '@angular/core';
import { NavBarGroupRef, NavBarLinkRef } from '@alfresco/adf-extensions';
import { Store } from '@ngrx/store';
import { AppStore, getSideNavState } from '@alfresco/aca-shared/store';
import { Subject } from 'rxjs';
import { takeUntil, distinctUntilChanged, debounceTime } from 'rxjs/operators';
import { AppExtensionService } from '@alfresco/aca-shared';
@Component({
selector: 'app-sidenav',
templateUrl: './sidenav.component.html',
styleUrls: ['./sidenav.component.scss'],
encapsulation: ViewEncapsulation.None,
host: { class: 'app-sidenav' }
})
export class SidenavComponent implements OnInit, OnDestroy {
@Input()
mode: 'collapsed' | 'expanded' = 'expanded';
groups: Array<NavBarGroupRef> = [];
private onDestroy$ = new Subject<boolean>();
constructor(private store: Store<AppStore>, private extensions: AppExtensionService) {}
ngOnInit() {
this.store
.select(getSideNavState)
.pipe(debounceTime(300), distinctUntilChanged(), takeUntil(this.onDestroy$))
.subscribe(() => {
this.groups = this.extensions.getApplicationNavigation(this.extensions.navbar);
});
}
trackByGroupId(_: number, obj: NavBarGroupRef): string {
return obj.id;
}
trackByLinkId(_: number, obj: NavBarLinkRef): string {
return obj.id;
}
ngOnDestroy() {
this.onDestroy$.next(true);
this.onDestroy$.complete();
}
}

View File

@@ -0,0 +1,73 @@
/*!
* @license
* Alfresco Example Content Application
*
* Copyright (C) 2005 - 2020 Alfresco Software Limited
*
* This file is part of the Alfresco Example Content Application.
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
*
* The Alfresco Example Content Application is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* The Alfresco Example Content Application is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
*/
import { NgModule } from '@angular/core';
import { AppCreateMenuModule } from '../create-menu/create-menu.module';
import { CommonModule } from '@angular/common';
import { CoreModule } from '@alfresco/adf-core';
import { RouterModule } from '@angular/router';
import { ExtensionsModule } from '@alfresco/adf-extensions';
import { CoreExtensionsModule } from '../../extensions/core.extensions.module';
import { ExpansionPanelDirective } from './directives/expansion-panel.directive';
import { MenuPanelDirective } from './directives/menu-panel.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';
import { MainActionModule } from '../main-action/main-action.module';
import { SidenavWrapperComponent } from './sidenav-wrapper/sidenav-wrapper.component';
@NgModule({
imports: [
CommonModule,
CoreModule.forChild(),
CoreExtensionsModule.forChild(),
ExtensionsModule.forChild(),
RouterModule,
AppCreateMenuModule,
MainActionModule
],
declarations: [
MenuPanelDirective,
ExpansionPanelDirective,
ActiveLinkDirective,
ActionDirective,
ExpandMenuComponent,
ButtonMenuComponent,
SidenavComponent,
SidenavWrapperComponent
],
exports: [
MenuPanelDirective,
ExpansionPanelDirective,
ActiveLinkDirective,
ActionDirective,
ExpandMenuComponent,
ButtonMenuComponent,
SidenavComponent
]
})
export class AppSidenavModule {}