diff --git a/projects/aca-about/assets/about.plugin.json b/projects/aca-about/assets/about.plugin.json index 3a8e4b7f0..60de2bf0c 100644 --- a/projects/aca-about/assets/about.plugin.json +++ b/projects/aca-about/assets/about.plugin.json @@ -24,10 +24,7 @@ ], "features": { - "header": [ - { - "id": "app.header.more", - "children": [ + "moreMenu": [ { "id": "app.header.about", "order": 100, @@ -39,7 +36,5 @@ } } ] - } - ] } } diff --git a/projects/aca-content/assets/app.extensions.json b/projects/aca-content/assets/app.extensions.json index 5065c62a3..ec9977f3a 100644 --- a/projects/aca-content/assets/app.extensions.json +++ b/projects/aca-content/assets/app.extensions.json @@ -86,39 +86,32 @@ "order": 50 }, { - "id": "app.header.more", - "type": "menu", - "order": 10000, - "icon": "apps", - "title": "APP.ACTIONS.MORE", - "children": [ - { - "id": "app.header.user", - "type": "custom", - "component": "app.user", - "order": 100 - }, - { - "id": "app.languagePicker", - "order": 100, - "type": "custom", - "component": "app.languagePicker" - }, - { - "id": "logout.separator", - "type": "separator", - "order": 199 - }, - { - "id": "app.logout", - "order": 200, - "type": "custom", - "component": "app.logout", - "rules": { - "visible": "app.canShowLogout" - } - } - ] + "id": "app.header.user", + "type": "custom", + "component": "app.user.menu", + "order": 100 + } + ], + "moreMenu": [ + { + "id": "app.languagePicker", + "order": 100, + "type": "custom", + "component": "app.languagePicker" + }, + { + "id": "logout.separator", + "type": "separator", + "order": 199 + }, + { + "id": "app.logout", + "order": 200, + "type": "custom", + "component": "app.logout", + "rules": { + "visible": "app.canShowLogout" + } } ], "icons": [ diff --git a/projects/aca-content/src/lib/aca-content.module.ts b/projects/aca-content/src/lib/aca-content.module.ts index ada701a0e..54a9d082f 100644 --- a/projects/aca-content/src/lib/aca-content.module.ts +++ b/projects/aca-content/src/lib/aca-content.module.ts @@ -118,6 +118,7 @@ import { UserInfoComponent } from './components/common/user-info/user-info.compo import { SidenavComponent } from './components/sidenav/sidenav.component'; import { ContentManagementService } from './services/content-management.service'; import { ShellLayoutComponent, SHELL_NAVBAR_MIN_WIDTH } from '@alfresco/adf-core/shell'; +import { UserMenuComponent } from './components/sidenav/user-menu/user-menu.component'; registerLocaleData(localeFr); registerLocaleData(localeDe); @@ -229,7 +230,8 @@ export class ContentServiceExtensionModule { 'app.languagePicker': LanguagePickerComponent, 'app.logout': LogoutComponent, 'app.user': UserInfoComponent, - 'app.notification-center': NotificationHistoryComponent + 'app.notification-center': NotificationHistoryComponent, + 'app.user.menu': UserMenuComponent }); extensions.setEvaluators({ diff --git a/projects/aca-content/src/lib/components/sidenav/sidenav.module.ts b/projects/aca-content/src/lib/components/sidenav/sidenav.module.ts index 00b4b9f07..218e71a80 100644 --- a/projects/aca-content/src/lib/components/sidenav/sidenav.module.ts +++ b/projects/aca-content/src/lib/components/sidenav/sidenav.module.ts @@ -36,6 +36,7 @@ import { ButtonMenuComponent } from './components/button-menu.component'; import { ActionDirective } from './directives/action.directive'; import { SidenavHeaderComponent } from './components/sidenav-header.component'; import { SharedToolbarModule } from '@alfresco/aca-shared'; +import { UserMenuComponent } from './user-menu/user-menu.component'; @NgModule({ imports: [CoreModule.forChild(), ExtensionsModule.forChild(), RouterModule, AppCreateMenuModule, SharedToolbarModule], @@ -47,7 +48,8 @@ import { SharedToolbarModule } from '@alfresco/aca-shared'; ExpandMenuComponent, ButtonMenuComponent, SidenavComponent, - SidenavHeaderComponent + SidenavHeaderComponent, + UserMenuComponent ], exports: [ MenuPanelDirective, diff --git a/projects/aca-content/src/lib/components/sidenav/user-menu/user-menu.component.html b/projects/aca-content/src/lib/components/sidenav/user-menu/user-menu.component.html new file mode 100644 index 000000000..36deb38b8 --- /dev/null +++ b/projects/aca-content/src/lib/components/sidenav/user-menu/user-menu.component.html @@ -0,0 +1,20 @@ + + + +
+ +
+
{{ (displayName$ | async)?.firstName }}
+
{{ (displayName$ | async)?.email }}
+
+
+ + + + +
diff --git a/projects/aca-content/src/lib/components/sidenav/user-menu/user-menu.component.scss b/projects/aca-content/src/lib/components/sidenav/user-menu/user-menu.component.scss new file mode 100644 index 000000000..702614baa --- /dev/null +++ b/projects/aca-content/src/lib/components/sidenav/user-menu/user-menu.component.scss @@ -0,0 +1,25 @@ +.aca-menu-user-details { + display: flex; + height: 66px; + align-items: center; + + &-button { + border-radius: 90%; + height: 32px; + margin-right: 0; + min-width: 32px; + padding: 0; + font-weight: 700; + line-height: 32px; + text-align: center; + vertical-align: middle; + background: var(--theme-user-initials-background-color); + color: var(--theme-user-intials-text-color); + border: none; + } + + &-name-email { + line-height: 24px; + margin-left: 10px; + } +} diff --git a/projects/aca-content/src/lib/components/sidenav/user-menu/user-menu.component.spec.ts b/projects/aca-content/src/lib/components/sidenav/user-menu/user-menu.component.spec.ts new file mode 100644 index 000000000..c3962c880 --- /dev/null +++ b/projects/aca-content/src/lib/components/sidenav/user-menu/user-menu.component.spec.ts @@ -0,0 +1,145 @@ +/*! + * Copyright © 2005-2023 Hyland Software, Inc. and its affiliates. All rights reserved. + * + * Alfresco Example Content Application + * + * 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 + * from Hyland Software. If not, see . + */ + +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { AuthenticationService, IdentityUserService } from '@alfresco/adf-core'; +import { PeopleContentService } from '@alfresco/adf-content-services'; +import { AppTestingModule } from '../../../testing/app-testing.module'; +import { UserMenuComponent } from './user-menu.component'; +import { of } from 'rxjs'; + +describe('parseDisplayName', () => { + let component: UserMenuComponent; + let fixture: ComponentFixture; + let authServiceStub: Partial; + let peopleContentServiceStub: Partial; + let identityUserServiceStub: Partial; + + beforeEach(() => { + authServiceStub = { + isOauth: () => true, + isECMProvider: () => true, + isEcmLoggedIn: () => true, + isKerberosEnabled: () => false, + isLoggedIn: () => true + }; + + peopleContentServiceStub = { + getCurrentUserInfo: () => + of({ + firstName: 'John', + email: 'john@example.com', + id: 'johnDoe1', + enabled: true, + company: { + organization: 'ABC Organization', + address1: 'XYZ Road', + address2: 'Ohio', + address3: 'Westlake', + postcode: '44145', + telephone: '456876', + fax: '323984', + email: 'contact.us@abc.com' + }, + isAdmin: () => true + }) + }; + + identityUserServiceStub = { + getCurrentUserInfo: () => ({ + firstName: 'John', + email: 'john@example.com', + id: 'johnDoe1', + enabled: true, + company: { + organization: 'ABC Organization', + address1: 'XYZ Road', + address2: 'Ohio', + address3: 'Westlake', + postcode: '44145', + telephone: '456876', + fax: '323984', + email: 'contact.us@abc.com' + }, + isAdmin: () => true + }) + }; + + TestBed.configureTestingModule({ + imports: [AppTestingModule], + declarations: [UserMenuComponent], + providers: [ + { provide: AuthenticationService, useValue: authServiceStub }, + { provide: PeopleContentService, useValue: peopleContentServiceStub }, + { provide: identityUserServiceStub, useValue: identityUserServiceStub } + ] + }).compileComponents(); + + fixture = TestBed.createComponent(UserMenuComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should return an object with empty strings for all properties when the input model is empty', () => { + const result = component.parseDisplayName({}); + expect(result.firstName).toEqual(''); + expect(result.initials).toEqual(''); + expect(result.email).toEqual(''); + }); + + it('should return an object with the correct firstName and initials when the input model has only the firstName property', () => { + const result = component.parseDisplayName({ firstName: 'John' }); + expect(result.firstName).toEqual('John'); + expect(result.initials).toEqual('J'); + expect(result.email).toEqual(''); + }); + + it('should return an object with the correct firstName and initials when the input model has only the lastName property', () => { + const result = component.parseDisplayName({ lastName: 'Doe' }); + expect(result.firstName).toEqual(' Doe'); + expect(result.initials).toEqual('D'); + expect(result.email).toEqual(''); + }); + + it('should return an object with the correct email property when the input model has only the email property', () => { + const result = component.parseDisplayName({ email: 'john.doe@example.com' }); + expect(result.firstName).toEqual(''); + expect(result.initials).toEqual(''); + expect(result.email).toEqual('john.doe@example.com'); + }); + + it('should return an object with the correct firstName, initials, and lastName concatenated when the input model has both firstName and lastName properties', () => { + const result = component.parseDisplayName({ firstName: 'John', lastName: 'Doe' }); + expect(result.firstName).toEqual('John Doe'); + expect(result.initials).toEqual('JD'); + expect(result.email).toEqual(''); + }); + + it('should return an object with all properties correctly parsed when the input model has all three properties', () => { + const result = component.parseDisplayName({ firstName: 'John', lastName: 'Doe', email: 'john.doe@example.com' }); + expect(result.firstName).toEqual('John Doe'); + expect(result.initials).toEqual('JD'); + expect(result.email).toEqual('john.doe@example.com'); + }); +}); diff --git a/projects/aca-content/src/lib/components/sidenav/user-menu/user-menu.component.ts b/projects/aca-content/src/lib/components/sidenav/user-menu/user-menu.component.ts new file mode 100644 index 000000000..05c440e40 --- /dev/null +++ b/projects/aca-content/src/lib/components/sidenav/user-menu/user-menu.component.ts @@ -0,0 +1,110 @@ +/*! + * Copyright © 2005-2023 Hyland Software, Inc. and its affiliates. All rights reserved. + * + * Alfresco Example Content Application + * + * 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 + * from Hyland Software. If not, see . + */ + +import { PeopleContentService } from '@alfresco/adf-content-services'; +import { AuthenticationService, IdentityUserService } from '@alfresco/adf-core'; +import { Component, OnInit } from '@angular/core'; +import { Observable, of, Subject } from 'rxjs'; +import { map, takeUntil } from 'rxjs/operators'; +import { AppExtensionService } from '@alfresco/aca-shared'; +import { ContentActionRef } from '@alfresco/adf-extensions'; + +@Component({ + selector: 'aca-user-menu', + templateUrl: './user-menu.component.html', + styleUrls: ['./user-menu.component.scss'] +}) +export class UserMenuComponent implements OnInit { + displayName$: Observable<{ firstName: string; initials: string; email: string }>; + private onDestroy$ = new Subject(); + actions: Array = []; + + constructor( + private peopleContentService: PeopleContentService, + private identityUserService: IdentityUserService, + private authService: AuthenticationService, + private appExtensions: AppExtensionService + ) {} + + ngOnInit() { + this.getUserInfo(); + this.appExtensions + .getMoreActions() + .pipe(takeUntil(this.onDestroy$)) + .subscribe((actions) => { + this.actions = actions; + }); + } + + getUserInfo() { + if (this.authService.isOauth()) { + this.loadIdentityUserInfo(); + + if (this.authService.isECMProvider() && this.authService.isEcmLoggedIn()) { + this.loadEcmUserInfo(); + } + } else if (this.isEcmLoggedIn()) { + this.loadEcmUserInfo(); + } + } + + get isLoggedIn(): boolean { + if (this.authService.isKerberosEnabled()) { + return true; + } + return this.authService.isLoggedIn(); + } + + private loadEcmUserInfo(): void { + this.displayName$ = this.peopleContentService.getCurrentUserInfo().pipe(map((model) => this.parseDisplayName(model))); + } + + private loadIdentityUserInfo() { + this.displayName$ = of(this.identityUserService.getCurrentUserInfo()).pipe(map((model) => this.parseDisplayName(model))); + } + + parseDisplayName(model: { firstName?: string; lastName?: string; email?: string }): { firstName: string; initials: string; email: string } { + const result = { firstName: '', initials: '', email: '' }; + if (model.firstName) { + result.firstName = model.firstName; + result.initials = model.firstName.charAt(0).toUpperCase(); + } + if (model.lastName) { + result.firstName += ' ' + model.lastName; + result.initials += model.lastName.charAt(0).toUpperCase(); + } + if (model.email) { + result.email = `${model.email}`; + } + return result; + } + + private isEcmLoggedIn() { + return this.authService.isEcmLoggedIn() || (this.authService.isECMProvider() && this.authService.isKerberosEnabled()); + } + + trackByActionId(_: number, action: ContentActionRef) { + return action.id; + } +} diff --git a/projects/aca-content/src/lib/ui/variables/variables.scss b/projects/aca-content/src/lib/ui/variables/variables.scss index 8489d71da..97d1b8e1e 100644 --- a/projects/aca-content/src/lib/ui/variables/variables.scss +++ b/projects/aca-content/src/lib/ui/variables/variables.scss @@ -37,6 +37,8 @@ $selected-background-color: rgba(31, 116, 219, 0.24); $action-button-text-color: rgba(33, 35, 40, 0.7); $page-layout-header-background-color: #fff; $aca-toolbar-button-background-color: rgba(33, 33, 33, 0.05); +$acc-user-initials-background: rgba(33, 33, 33, 0.12); +$acc-user-initials-text-color: #212121; // CSS Variables $defaults: ( @@ -82,6 +84,8 @@ $defaults: ( --theme-header-border-color: $grey-background, --theme-page-layout-header-background-color: $page-layout-header-background-color, --theme-app-toolbar-button-background-color: $aca-toolbar-button-background-color + --theme-user-initials-background-color: $acc-user-initials-background, + --theme-user-intials-text-color: $acc-user-initials-text-color ); // propagates SCSS variables into the CSS variables scope diff --git a/projects/aca-shared/src/lib/services/app.extension.service.ts b/projects/aca-shared/src/lib/services/app.extension.service.ts index d28abdee0..abfff8837 100644 --- a/projects/aca-shared/src/lib/services/app.extension.service.ts +++ b/projects/aca-shared/src/lib/services/app.extension.service.ts @@ -72,6 +72,7 @@ export class AppExtensionService implements RuleContext { settingGroups: Array = []; private _headerActions = new BehaviorSubject>([]); + private _moreActions = new BehaviorSubject>([]); private _toolbarActions = new BehaviorSubject>([]); private _viewerToolbarActions = new BehaviorSubject>([]); private _sharedLinkViewerToolbarActions = new BehaviorSubject>([]); @@ -150,6 +151,7 @@ export class AppExtensionService implements RuleContext { this.settingGroups = this.loader.getElements(config, 'settings'); this._headerActions.next(this.loader.getContentActions(config, 'features.header')); + this._moreActions.next(this.loader.getContentActions(config, 'features.moreMenu')); this._sidebarActions.next(this.loader.getContentActions(config, 'features.sidebar.toolbar')); this._toolbarActions.next(this.loader.getContentActions(config, 'features.toolbar')); this._viewerToolbarActions.next(this.loader.getContentActions(config, 'features.viewer.toolbarActions')); @@ -456,6 +458,21 @@ export class AppExtensionService implements RuleContext { ); } + getMoreActions(): Observable> { + return this._moreActions.pipe( + map((moreActions) => + moreActions + .filter((action) => this.filterVisible(action)) + .map((action) => this.copyAction(action)) + .map((action) => this.buildMenu(action)) + .map((action) => this.setActionDisabledFromRule(action)) + .sort(sortByOrder) + .reduce(reduceEmptyMenus, []) + .reduce(reduceSeparators, []) + ) + ); + } + getAllowedContextMenuActions(): Observable> { return this._contextMenuActions.pipe(map((contextMenuActions) => (!this.selection.isEmpty ? this.getAllowedActions(contextMenuActions) : []))); }