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 @@
+
+
+
+
+
+
+
+
+
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) : [])));
}