From 426cafff5ec34a25f95bc05d0b8132d9a93838e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartosz=20Seku=C5=82a?= Date: Thu, 24 Nov 2022 13:58:21 +0100 Subject: [PATCH] [AAE-10533] Generic App shell for HxP applications (#8002) * [AAE-10533] Generic App shell for HxP applications * Clean code and add public_api for shell --- angular.json | 4 +- lib/core/shell/README.md | 17 ++ lib/core/shell/ng-package.json | 6 + lib/core/shell/public-api.ts | 18 ++ lib/core/shell/src/index.ts | 20 ++ .../lib/components/shell/shell.component.html | 48 +++++ .../lib/components/shell/shell.component.scss | 34 ++++ .../components/shell/shell.component.spec.ts | 173 ++++++++++++++++++ .../lib/components/shell/shell.component.ts | 132 +++++++++++++ .../src/lib/services/shell-app.service.ts | 36 ++++ lib/core/shell/src/lib/shell.module.ts | 81 ++++++++ lib/core/shell/src/lib/shell.routes.ts | 27 +++ tsconfig.json | 1 + 13 files changed, 596 insertions(+), 1 deletion(-) create mode 100644 lib/core/shell/README.md create mode 100644 lib/core/shell/ng-package.json create mode 100644 lib/core/shell/public-api.ts create mode 100644 lib/core/shell/src/index.ts create mode 100644 lib/core/shell/src/lib/components/shell/shell.component.html create mode 100644 lib/core/shell/src/lib/components/shell/shell.component.scss create mode 100644 lib/core/shell/src/lib/components/shell/shell.component.spec.ts create mode 100644 lib/core/shell/src/lib/components/shell/shell.component.ts create mode 100644 lib/core/shell/src/lib/services/shell-app.service.ts create mode 100644 lib/core/shell/src/lib/shell.module.ts create mode 100644 lib/core/shell/src/lib/shell.routes.ts diff --git a/angular.json b/angular.json index af94583e50..4ddab55bc9 100644 --- a/angular.json +++ b/angular.json @@ -339,7 +339,9 @@ "lib/core/api/**/*.ts", "lib/core/api/**/*.html", "lib/core/auth/**/*.ts", - "lib/core/auth/**/*.html" + "lib/core/auth/**/*.html", + "lib/core/shell/**/*.ts", + "lib/core/shell/**/*.html" ] } }, diff --git a/lib/core/shell/README.md b/lib/core/shell/README.md new file mode 100644 index 0000000000..1ec3c83f4b --- /dev/null +++ b/lib/core/shell/README.md @@ -0,0 +1,17 @@ +# @alfresco/adf-core/shell + +Secondary entry point of `@alfresco/adf-core`. It can be used by importing from `@alfresco/adf-core/shell`. + +# Shell + +[ShellModule](./src/lib/shell.module.ts) is designated as a main layout for the application. + +I order to attach routes to appShell, `withRoutes(routes: Routes | AppShellRoutesConfig)` method should be used. + +Passed routes are going to be attached to [shell main route](./src/lib/shell.routes.ts) + +If you would like to provide custom app guard, you can provide your own using [SHELL_AUTH_TOKEN](./src/lib/shell.routes.ts) + +## Shell Service + +In order to use `shell`, you need to provide [SHELL_APP_SERVICE](./src/lib/services/shell-app.service.ts) which provides necessary options for shell component to work. diff --git a/lib/core/shell/ng-package.json b/lib/core/shell/ng-package.json new file mode 100644 index 0000000000..1d9f99930d --- /dev/null +++ b/lib/core/shell/ng-package.json @@ -0,0 +1,6 @@ +{ + "$schema": "../../../../../node_modules/ng-packagr/ng-package.schema.json", + "lib": { + "entryFile": "src/index.ts" + } +} diff --git a/lib/core/shell/public-api.ts b/lib/core/shell/public-api.ts new file mode 100644 index 0000000000..4600f4568e --- /dev/null +++ b/lib/core/shell/public-api.ts @@ -0,0 +1,18 @@ +/*! + * @license + * Copyright 2019 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export * from './src/index'; diff --git a/lib/core/shell/src/index.ts b/lib/core/shell/src/index.ts new file mode 100644 index 0000000000..8862dbf042 --- /dev/null +++ b/lib/core/shell/src/index.ts @@ -0,0 +1,20 @@ +/*! + * @license + * Copyright 2019 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export * from './lib/shell.module'; +export * from './lib/services/shell-app.service'; +export * from './lib/components/shell/shell.component'; diff --git a/lib/core/shell/src/lib/components/shell/shell.component.html b/lib/core/shell/src/lib/components/shell/shell.component.html new file mode 100644 index 0000000000..3cd17c11fb --- /dev/null +++ b/lib/core/shell/src/lib/components/shell/shell.component.html @@ -0,0 +1,48 @@ + + + +
+ + +
+
+
+ + + +
+ + +
+
+
+ + + + + + +
+ + + + + diff --git a/lib/core/shell/src/lib/components/shell/shell.component.scss b/lib/core/shell/src/lib/components/shell/shell.component.scss new file mode 100644 index 0000000000..8373fd4092 --- /dev/null +++ b/lib/core/shell/src/lib/components/shell/shell.component.scss @@ -0,0 +1,34 @@ +.app-shell { + display: flex; + flex-direction: column; + flex: 1; + height: 100%; + overflow: hidden; + min-height: 0; + + router-outlet[name='viewer'] + * { + width: 100%; + height: 100%; + z-index: 999; + position: absolute; + top: 0; + right: 0; + background-color: white; + } + + adf-file-uploading-dialog { + z-index: 1000; + } +} + +@media screen and (max-width: 599px) { + .adf-app-title { + display: none; + } +} + +@media screen and (max-width: 719px) { + .adf-app-logo { + display: none; + } +} diff --git a/lib/core/shell/src/lib/components/shell/shell.component.spec.ts b/lib/core/shell/src/lib/components/shell/shell.component.spec.ts new file mode 100644 index 0000000000..dd0ccca55a --- /dev/null +++ b/lib/core/shell/src/lib/components/shell/shell.component.spec.ts @@ -0,0 +1,173 @@ +/*! + * @license + * Copyright 2019 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { TestBed, ComponentFixture } from '@angular/core/testing'; +import { AppConfigService, SidenavLayoutModule } from '@alfresco/adf-core'; +import { ShellLayoutComponent } from './shell.component'; +import { Router, NavigationStart, RouterModule } from '@angular/router'; +import { of, Subject } from 'rxjs'; +import { ExtensionsModule } from '@alfresco/adf-extensions'; +import { CommonModule } from '@angular/common'; +import { ShellAppService, SHELL_APP_SERVICE } from '../../services/shell-app.service'; +import { HttpClientModule } from '@angular/common/http'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; + +class MockRouter { + private url = 'some-url'; + private subject = new Subject(); + events = this.subject.asObservable(); + routerState = { snapshot: { url: this.url } }; + + navigateByUrl(url: string) { + const navigationStart = new NavigationStart(0, url); + this.subject.next(navigationStart); + } +} + +describe('AppLayoutComponent', () => { + let fixture: ComponentFixture; + let component: ShellLayoutComponent; + let appConfig: AppConfigService; + let shellAppService: ShellAppService; + + beforeEach(() => { + const shellService: ShellAppService = { + pageHeading$: of('Title'), + hideSidenavConditions: [], + minimizeSidenavConditions: [], + preferencesService: { + get: (_key: string) => 'true', + set: (_key: string, _value: any) => {} + } + }; + + TestBed.configureTestingModule({ + imports: [CommonModule, NoopAnimationsModule, HttpClientModule, SidenavLayoutModule, ExtensionsModule, RouterModule.forChild([])], + providers: [ + { + provide: Router, + useClass: MockRouter + }, + { + provide: SHELL_APP_SERVICE, + useValue: shellService + } + ], + declarations: [ShellLayoutComponent], + schemas: [NO_ERRORS_SCHEMA] + }); + + fixture = TestBed.createComponent(ShellLayoutComponent); + component = fixture.componentInstance; + appConfig = TestBed.inject(AppConfigService); + shellAppService = TestBed.inject(SHELL_APP_SERVICE); + }); + + beforeEach(() => { + appConfig.config.languages = []; + appConfig.config.locale = 'en'; + }); + + describe('sidenav state', () => { + it('should get state from configuration', () => { + appConfig.config.sideNav = { + expandedSidenav: false, + preserveState: false + }; + + fixture.detectChanges(); + + expect(component.expandedSidenav).toBe(false); + }); + + it('should resolve state to true is no configuration', () => { + appConfig.config.sidenav = {}; + + fixture.detectChanges(); + + expect(component.expandedSidenav).toBe(true); + }); + + it('should get state from user settings as true', () => { + appConfig.config.sideNav = { + expandedSidenav: false, + preserveState: true + }; + + spyOn(shellAppService.preferencesService, 'get').and.callFake((key) => { + if (key === 'expandedSidenav') { + return 'true'; + } + return 'false'; + }); + + fixture.detectChanges(); + + expect(component.expandedSidenav).toBe(true); + }); + + it('should get state from user settings as false', () => { + appConfig.config.sidenav = { + expandedSidenav: false, + preserveState: true + }; + + spyOn(shellAppService.preferencesService, 'get').and.callFake((key) => { + if (key === 'expandedSidenav') { + return 'false'; + } + return 'true'; + }); + + fixture.detectChanges(); + + expect(component.expandedSidenav).toBe(false); + }); + }); + + it('should close menu on mobile screen size', () => { + component.minimizeSidenav = false; + component.layout.container = { + isMobileScreenSize: true, + toggleMenu: () => {} + }; + + spyOn(component.layout.container, 'toggleMenu'); + fixture.detectChanges(); + + component.hideMenu({ preventDefault: () => {} } as any); + + expect(component.layout.container.toggleMenu).toHaveBeenCalled(); + }); + + it('should close menu on mobile screen size also when minimizeSidenav true', () => { + fixture.detectChanges(); + component.minimizeSidenav = true; + component.layout.container = { + isMobileScreenSize: true, + toggleMenu: () => {} + }; + + spyOn(component.layout.container, 'toggleMenu'); + fixture.detectChanges(); + + component.hideMenu({ preventDefault: () => {} } as any); + + expect(component.layout.container.toggleMenu).toHaveBeenCalled(); + }); +}); diff --git a/lib/core/shell/src/lib/components/shell/shell.component.ts b/lib/core/shell/src/lib/components/shell/shell.component.ts new file mode 100644 index 0000000000..58eea62978 --- /dev/null +++ b/lib/core/shell/src/lib/components/shell/shell.component.ts @@ -0,0 +1,132 @@ +/*! + * @license + * Copyright 2019 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { AppConfigService, SidenavLayoutComponent } from '@alfresco/adf-core'; +import { Component, Inject, OnDestroy, OnInit, ViewChild, ViewEncapsulation } from '@angular/core'; +import { NavigationEnd, Router } from '@angular/router'; +import { Subject, Observable } from 'rxjs'; +import { filter, takeUntil, map, withLatestFrom } from 'rxjs/operators'; +import { BreakpointObserver } from '@angular/cdk/layout'; +import { Directionality } from '@angular/cdk/bidi'; +import { SHELL_APP_SERVICE, ShellAppService } from '../../services/shell-app.service'; + +@Component({ + selector: 'app-shell', + templateUrl: './shell.component.html', + styleUrls: ['./shell.component.scss'], + encapsulation: ViewEncapsulation.None, + host: { class: 'app-shell' } +}) +export class ShellLayoutComponent implements OnInit, OnDestroy { + @ViewChild('layout', { static: true }) + layout: SidenavLayoutComponent; + + onDestroy$: Subject = new Subject(); + isSmallScreen$: Observable; + + expandedSidenav: boolean; + minimizeSidenav = false; + hideSidenav = false; + direction: Directionality; + + constructor( + private router: Router, + private appConfigService: AppConfigService, + private breakpointObserver: BreakpointObserver, + @Inject(SHELL_APP_SERVICE) private shellService: ShellAppService + ) {} + + ngOnInit() { + this.isSmallScreen$ = this.breakpointObserver.observe(['(max-width: 600px)']).pipe(map((result) => result.matches)); + + this.hideSidenav = this.shellService.hideSidenavConditions.some((el) => this.router.routerState.snapshot.url.includes(el)); + this.minimizeSidenav = this.shellService.minimizeSidenavConditions.some((el) => this.router.routerState.snapshot.url.includes(el)); + + if (!this.minimizeSidenav) { + this.expandedSidenav = this.getSidenavState(); + } else { + this.expandedSidenav = false; + } + + this.router.events + .pipe( + withLatestFrom(this.isSmallScreen$), + filter(([event, isSmallScreen]) => isSmallScreen && event instanceof NavigationEnd), + takeUntil(this.onDestroy$) + ) + .subscribe(() => { + this.layout.container.sidenav.close(); + }); + + this.router.events + .pipe( + filter((event) => event instanceof NavigationEnd), + takeUntil(this.onDestroy$) + ) + .subscribe((event: NavigationEnd) => { + this.minimizeSidenav = this.shellService.minimizeSidenavConditions.some((el) => event.urlAfterRedirects.includes(el)); + this.hideSidenav = this.shellService.hideSidenavConditions.some((el) => event.urlAfterRedirects.includes(el)); + + this.updateState(); + }); + } + + ngOnDestroy() { + this.onDestroy$.next(true); + this.onDestroy$.complete(); + } + + hideMenu(event: Event) { + if (this.layout.container.isMobileScreenSize) { + event.preventDefault(); + this.layout.container.toggleMenu(); + } + } + + private updateState() { + if (this.minimizeSidenav && !this.layout.isMenuMinimized) { + this.layout.isMenuMinimized = true; + if (!this.layout.container.isMobileScreenSize) { + this.layout.container.toggleMenu(); + } + } + + if (!this.minimizeSidenav) { + if (this.getSidenavState() && this.layout.isMenuMinimized) { + this.layout.isMenuMinimized = false; + this.layout.container.toggleMenu(); + } + } + } + + onExpanded(state: boolean) { + if (!this.minimizeSidenav && this.appConfigService.get('sideNav.preserveState')) { + this.shellService.preferencesService.set('expandedSidenav', state); + } + } + + private getSidenavState(): boolean { + const expand = this.appConfigService.get('sideNav.expandedSidenav', true); + const preserveState = this.appConfigService.get('sideNav.preserveState', true); + + if (preserveState) { + return this.shellService.preferencesService.get('expandedSidenav', expand.toString()) === 'true'; + } + + return expand; + } +} diff --git a/lib/core/shell/src/lib/services/shell-app.service.ts b/lib/core/shell/src/lib/services/shell-app.service.ts new file mode 100644 index 0000000000..ede2a5d24d --- /dev/null +++ b/lib/core/shell/src/lib/services/shell-app.service.ts @@ -0,0 +1,36 @@ +/*! + * @license + * Copyright 2019 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { InjectionToken } from '@angular/core'; +import { CanActivate, CanActivateChild } from '@angular/router'; +import { Observable } from 'rxjs'; + +export interface ShellPreferencesService { + set(preferenceKey: string, value: any): void; + get(preferenceKey: string, defaultValue: string): string; +} + +export interface ShellAppService { + pageHeading$: Observable; + hideSidenavConditions: string[]; + minimizeSidenavConditions: string[]; + preferencesService: ShellPreferencesService; +} + +export const SHELL_APP_SERVICE = new InjectionToken('SHELL_APP_SERVICE'); + +export const SHELL_AUTH_TOKEN = new InjectionToken('SHELL_AUTH_TOKEN'); diff --git a/lib/core/shell/src/lib/shell.module.ts b/lib/core/shell/src/lib/shell.module.ts new file mode 100644 index 0000000000..287c907d19 --- /dev/null +++ b/lib/core/shell/src/lib/shell.module.ts @@ -0,0 +1,81 @@ +/*! + * @license + * Copyright 2019 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { CommonModule } from '@angular/common'; +import { ModuleWithProviders, NgModule } from '@angular/core'; +import { Routes, provideRoutes, RouterModule, Route } from '@angular/router'; +import { SHELL_LAYOUT_ROUTE } from './shell.routes'; +import { SidenavLayoutModule } from '@alfresco/adf-core'; +import { ExtensionsModule } from '@alfresco/adf-extensions'; +import { ShellLayoutComponent } from './components/shell/shell.component'; + +export interface AppShellRoutesConfig { + shellParentRoute?: Route; + shellChildren: Routes; +} + +@NgModule({ + imports: [SidenavLayoutModule, ExtensionsModule, RouterModule.forChild([]), CommonModule], + exports: [ShellLayoutComponent], + declarations: [ShellLayoutComponent] +}) +export class ShellModule { + static withRoutes(routes: Routes | AppShellRoutesConfig): ModuleWithProviders { + if (Array.isArray(routes)) { + return getModuleForRoutes(routes); + } + + return getModuleForRouteConfig(routes); + } +} + +function getModuleForRoutes(routes: Routes): ModuleWithProviders { + const shellLayoutRoute = SHELL_LAYOUT_ROUTE; + + routes.forEach((childRoute) => { + shellLayoutRoute.children.push(childRoute); + }); + + return { + ngModule: ShellModule, + providers: provideRoutes([shellLayoutRoute]) + }; +} + +function getModuleForRouteConfig(config: AppShellRoutesConfig): ModuleWithProviders { + const shellLayoutRoute = SHELL_LAYOUT_ROUTE; + + const shellParentRoute = config.shellParentRoute; + const shellChildrenRoutes = config.shellChildren; + + shellLayoutRoute.children.push(...shellChildrenRoutes); + + const rootRoute = shellParentRoute ? shellParentRoute : shellLayoutRoute; + + if (config.shellParentRoute) { + if (rootRoute.children === undefined) { + rootRoute.children = []; + } + + rootRoute.children.push(shellLayoutRoute); + } + + return { + ngModule: ShellModule, + providers: provideRoutes([rootRoute]) + }; +} diff --git a/lib/core/shell/src/lib/shell.routes.ts b/lib/core/shell/src/lib/shell.routes.ts new file mode 100644 index 0000000000..eef06ce737 --- /dev/null +++ b/lib/core/shell/src/lib/shell.routes.ts @@ -0,0 +1,27 @@ +/*! + * @license + * Copyright 2019 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Route } from '@angular/router'; +import { ShellLayoutComponent } from './components/shell/shell.component'; +import { SHELL_AUTH_TOKEN } from './services/shell-app.service'; + +export const SHELL_LAYOUT_ROUTE: Route = { + path: '', + component: ShellLayoutComponent, + canActivate: [SHELL_AUTH_TOKEN], + children: [] +}; diff --git a/tsconfig.json b/tsconfig.json index ee21efd17d..fd926c0005 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -26,6 +26,7 @@ "@alfresco/adf-core": ["lib/core"], "@alfresco/adf-core/*": ["lib/core/*/public-api.ts"], "@alfresco/adf-core/auth": ["lib/core/auth/src/index.ts"], + "@alfresco/adf-core/shell": ["lib/core/shell/src/index.ts"], "@alfresco/adf-extensions": ["lib/extensions"], "@alfresco/adf-insights": ["lib/insights"], "@alfresco/adf-process-services": ["lib/process-services"],