From 24b2b23afd68ae361e6b1abf8e4484e2369ab5c6 Mon Sep 17 00:00:00 2001
From: MichalKinas <113341662+MichalKinas@users.noreply.github.com>
Date: Thu, 23 Oct 2025 13:49:02 +0200
Subject: [PATCH] [ACS-9980] Add the option to provide custom user profile
sections (#4859)
* [ACS-9980] Add the option to provide custom user profile sections
* [ACS-9980] Cr fixes
* [ACS-9980] Formatting fix
---
extension.schema.json | 20 ++
projects/aca-content/assets/i18n/en.json | 7 +-
.../aca-content/src/lib/aca-content.module.ts | 3 +-
.../view-profile/view-profile.component.html | 254 ++++++++++++------
.../view-profile/view-profile.component.scss | 60 ++---
.../view-profile.component.spec.ts | 36 +--
.../view-profile/view-profile.component.ts | 43 +--
.../aca-shared/rules/src/app.rules.spec.ts | 12 +
projects/aca-shared/rules/src/app.rules.ts | 2 +
projects/aca-shared/src/lib/models/types.ts | 4 +
.../services/app.extension.service.spec.ts | 35 +++
.../src/lib/services/app.extension.service.ts | 8 +-
12 files changed, 323 insertions(+), 161 deletions(-)
diff --git a/extension.schema.json b/extension.schema.json
index e57dc0f15..79fbbe736 100644
--- a/extension.schema.json
+++ b/extension.schema.json
@@ -743,6 +743,20 @@
"type": "string"
}
}
+ },
+ "userProfileSection": {
+ "type": "object",
+ "required": ["id", "title"],
+ "properties": {
+ "id": {
+ "description": "Unique identifier.",
+ "type": "string"
+ },
+ "component": {
+ "description": "Custom component id to display inside the section",
+ "type": "string"
+ }
+ }
}
},
@@ -1013,6 +1027,12 @@
"items": { "$ref": "#/definitions/badge" },
"minItems": 1
},
+ "userProfileSections": {
+ "description": "List of custom sections to display in the user profile component",
+ "type": "array",
+ "items": { "$ref": "#/definitions/userProfileSection" },
+ "minItems": 1
+ },
"search": {
"description": "aca search extension",
"type": "array",
diff --git a/projects/aca-content/assets/i18n/en.json b/projects/aca-content/assets/i18n/en.json
index c5353b36c..91397b84f 100644
--- a/projects/aca-content/assets/i18n/en.json
+++ b/projects/aca-content/assets/i18n/en.json
@@ -463,7 +463,12 @@
"COLLAPSE_NAVIGATION": "Collapse navigation menu",
"OPTIONS_SETTINGS": "Options and settings",
"MY_PROFILE": "My profile",
- "EXPAND_NAVIGATION": "Expand navigation menu"
+ "EXPAND_NAVIGATION": "Expand navigation menu",
+ "EXPAND_SECTION": "Expand section",
+ "COLLAPSE_SECTION": "Collapse section",
+ "EDIT": "Edit",
+ "CANCEL": "Cancel",
+ "SAVE": "Save"
},
"FOLDER_INFO": {
"ICON": "Folder Icon",
diff --git a/projects/aca-content/src/lib/aca-content.module.ts b/projects/aca-content/src/lib/aca-content.module.ts
index bd0863247..7fb1c0e32 100644
--- a/projects/aca-content/src/lib/aca-content.module.ts
+++ b/projects/aca-content/src/lib/aca-content.module.ts
@@ -166,7 +166,8 @@ import { SaveSearchSidenavComponent } from './components/search/search-save/side
'app.canShowLogout': rules.canShowLogout,
'app.isContentServiceEnabled': rules.isContentServiceEnabled,
'app.areTagsEnabled': rules.areTagsEnabled,
- 'app.areCategoriesEnabled': rules.areCategoriesEnabled
+ 'app.areCategoriesEnabled': rules.areCategoriesEnabled,
+ 'app.isSSOEnabled': rules.isSSOEnabled
}
})
]
diff --git a/projects/aca-content/src/lib/components/view-profile/view-profile.component.html b/projects/aca-content/src/lib/components/view-profile/view-profile.component.html
index 8aa6b4ae4..21dffa64e 100644
--- a/projects/aca-content/src/lib/components/view-profile/view-profile.component.html
+++ b/projects/aca-content/src/lib/components/view-profile/view-profile.component.html
@@ -1,10 +1,15 @@
-
+ @if ((appNavNarMode$ | async) === 'collapsed') {
+
+ }
-
+
-
- {{ generalSectionDropdown ? 'expand_more' : 'chevron_right'}}
+
{{'APP.EDIT_PROFILE.GENERAL' | translate}}
-
-
-
-
-
-
-
+ @if (generalSectionButtonsToggle) {
+
+ } @else {
+
+
+ }
-
-
+ @if (generalSectionDropdown) {
+
{{'APP.EDIT_PROFILE.FIRST_NAME' | translate}}
@@ -49,106 +77,176 @@
{{'APP.EDIT_PROFILE.JOB_TITLE' | translate}}
-
{{personDetails?.jobTitle}}
-
+ @if (generalSectionButtonsToggle) {
+
{{personDetails?.jobTitle}}
+ } @else {
+
+ }
{{'APP.EDIT_PROFILE.LOCATION' | translate}}
-
{{personDetails?.location}}
-
+ @if (generalSectionButtonsToggle) {
+
{{personDetails?.location}}
+ } @else {
+
+ }
{{'APP.EDIT_PROFILE.TELEPHONE' | translate}}
-
{{personDetails?.telephone}}
-
-
- {{ 'APP.EDIT_PROFILE.INVALID_INPUT' | translate }}
-
+ @if (generalSectionButtonsToggle) {
+
{{personDetails?.telephone}}
+ } @else {
+
+ }
+ @if (profileForm.controls['telephone'].errors) {
+
+ {{ 'APP.EDIT_PROFILE.INVALID_INPUT' | translate }}
+
+ }
{{'APP.EDIT_PROFILE.MOBILE' | translate}}
-
{{personDetails?.mobile}}
-
-
- {{ 'APP.EDIT_PROFILE.INVALID_INPUT' | translate }}
-
+ @if (generalSectionButtonsToggle) {
+
{{personDetails?.mobile}}
+ } @else {
+
+ }
+ @if (profileForm.controls['mobile'].errors) {
+
+ {{ 'APP.EDIT_PROFILE.INVALID_INPUT' | translate }}
+
+ }
-
+ }
-
diff --git a/projects/aca-content/src/lib/components/view-profile/view-profile.component.scss b/projects/aca-content/src/lib/components/view-profile/view-profile.component.scss
index 119e535e1..7d2f86050 100644
--- a/projects/aca-content/src/lib/components/view-profile/view-profile.component.scss
+++ b/projects/aca-content/src/lib/components/view-profile/view-profile.component.scss
@@ -43,6 +43,7 @@ app-view-profile {
}
.app-profile-general-icon {
+ padding: 0 0.5rem 0 1rem;
cursor: pointer;
}
@@ -52,7 +53,16 @@ app-view-profile {
.app-profile-general {
display: flex;
- padding-left: 1rem;
+ align-items: center;
+ width: 100%;
+
+ .app-general-edit {
+ color: var(--adf-theme-foreground-text-color-054);
+ }
+
+ .app-general-cancel-btn {
+ color: var(--adf-metadata-action-button-clear-color);
+ }
}
.app-profile-general-bottom-radius {
@@ -62,48 +72,12 @@ app-view-profile {
.app-profile-general-section {
display: flex;
- width: 60%;
- padding-top: 1rem;
- cursor: pointer;
- }
-
- .app-general-title {
- margin-left: 0.6rem;
- margin-top: 4px;
- letter-spacing: 0.5px;
- }
-
- .app-general-edit-btn {
- width: 60%;
- text-align: end;
-
- .app-general-edit {
- background: var(--theme-grey-text-background-color);
- height: 30px;
- margin: 1rem;
- }
-
- .app-general-cancel-btn {
- height: 30px;
- margin: 1rem;
- margin-left: 0.5rem;
- background-color: var(--theme-grey-text-background-color);
- padding-top: 0.25px;
- }
-
- .app-general-save-btn {
- width: 75px;
- height: 30px;
- margin: 1rem;
- margin-left: 0.5rem;
- color: var(--theme-white-background);
- background-color: var(--theme-blue-button-color);
- font-weight: 600;
- }
+ align-items: center;
+ width: 100%;
}
.app-divider {
- border-top-width: 2px;
+ border-top-width: 1px;
border-top-color: var(--theme-grey-background-color);
padding-left: -1px;
padding-right: 1px;
@@ -116,10 +90,6 @@ app-view-profile {
box-shadow: 0 0 2px var(--theme-blue-button-color);
}
- .app-general-edit:hover {
- background: var(--theme-grey-hover-background-color);
- }
-
.app-general-dropdown-divider {
border-top-color: var(--theme-grey-divider-color);
}
@@ -183,7 +153,7 @@ app-view-profile {
}
.app-profile-contact-row {
- margin: 2rem 0 5rem 2rem;
+ margin: 2rem 0 0 2rem;
width: 70%;
border: 1px solid var(--theme-grey-background-color);
border-radius: 1rem;
diff --git a/projects/aca-content/src/lib/components/view-profile/view-profile.component.spec.ts b/projects/aca-content/src/lib/components/view-profile/view-profile.component.spec.ts
index 981231514..a99f6abe3 100644
--- a/projects/aca-content/src/lib/components/view-profile/view-profile.component.spec.ts
+++ b/projects/aca-content/src/lib/components/view-profile/view-profile.component.spec.ts
@@ -25,13 +25,13 @@
import { TestBed, ComponentFixture } from '@angular/core/testing';
import { ViewProfileComponent } from './view-profile.component';
import { AppTestingModule } from '../../testing/app-testing.module';
-import { By } from '@angular/platform-browser';
import { Router } from '@angular/router';
-import { BehaviorSubject, Subject } from 'rxjs';
-import { AppService } from '@alfresco/aca-shared';
+import { BehaviorSubject, of, Subject } from 'rxjs';
+import { AppExtensionService, AppService } from '@alfresco/aca-shared';
import { UnitTestingUtils } from '@alfresco/adf-core';
import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
import { HarnessLoader } from '@angular/cdk/testing';
+import { DynamicExtensionComponent } from '@alfresco/adf-extensions';
describe('ViewProfileComponent', () => {
let fixture: ComponentFixture
;
@@ -39,6 +39,7 @@ describe('ViewProfileComponent', () => {
let router: Router;
let unitTestingUtils: UnitTestingUtils;
let loader: HarnessLoader;
+ let appExtensionService: AppExtensionService;
const appServiceMock = {
toggleAppNavBar$: new Subject(),
@@ -61,6 +62,7 @@ describe('ViewProfileComponent', () => {
router = TestBed.inject(Router);
loader = TestbedHarnessEnvironment.loader(fixture);
unitTestingUtils = new UnitTestingUtils(fixture.debugElement, loader);
+ appExtensionService = TestBed.inject(AppExtensionService);
router.initialNavigation();
});
@@ -71,7 +73,7 @@ describe('ViewProfileComponent', () => {
it('should toggle the appService toggleAppNavBar$ Subject', () => {
spyOn(appServiceMock.toggleAppNavBar$, 'next');
- component.toggleClick();
+ component.toggleNavigationMenu();
expect(appServiceMock.toggleAppNavBar$.next).toHaveBeenCalled();
});
@@ -156,8 +158,7 @@ describe('ViewProfileComponent', () => {
component.generalSectionDropdown = false;
fixture.detectChanges();
- const generalToggleIcon = fixture.debugElement.query(By.css('#toggle-general-dropdown'));
- generalToggleIcon.triggerEventHandler('click', null);
+ unitTestingUtils.clickByCSS('#general-dropdown button');
expect(component.toggleGeneralDropdown).toHaveBeenCalled();
expect(component.generalSectionButtonsToggle).toBe(true);
@@ -168,8 +169,7 @@ describe('ViewProfileComponent', () => {
component.contactSectionDropdown = false;
fixture.detectChanges();
- const contactToggleIcon = fixture.debugElement.query(By.css('#toggle-contact-dropdown'));
- contactToggleIcon.triggerEventHandler('click', null);
+ unitTestingUtils.clickByCSS('#contact-dropdown button');
expect(component.toggleContactDropdown).toHaveBeenCalled();
expect(component.contactSectionButtonsToggle).toBe(true);
@@ -179,12 +179,10 @@ describe('ViewProfileComponent', () => {
spyOn(component, 'toggleGeneralButtons').and.callThrough();
fixture.detectChanges();
- const generalEditButton = fixture.debugElement.query(By.css('#general-edit-button'));
- generalEditButton.triggerEventHandler('click', null);
+ unitTestingUtils.clickByCSS('#general-dropdown .app-general-edit');
fixture.detectChanges();
- const generalCancelButton = fixture.debugElement.query(By.css('#general-cancel-button'));
- generalCancelButton.triggerEventHandler('click', null);
+ unitTestingUtils.clickByCSS('#general-dropdown .app-general-cancel-btn');
expect(component.toggleGeneralButtons).toHaveBeenCalledTimes(2);
});
@@ -193,12 +191,10 @@ describe('ViewProfileComponent', () => {
spyOn(component, 'toggleContactButtons').and.callThrough();
fixture.detectChanges();
- const contactEditButton = fixture.debugElement.query(By.css('#contact-edit-button'));
- contactEditButton.triggerEventHandler('click', null);
+ unitTestingUtils.clickByCSS('#contact-dropdown .app-general-edit');
fixture.detectChanges();
- const contactCancelButton = fixture.debugElement.query(By.css('#contact-cancel-button'));
- contactCancelButton.triggerEventHandler('click', null);
+ unitTestingUtils.clickByCSS('#contact-dropdown .app-general-cancel-btn');
expect(component.toggleContactButtons).toHaveBeenCalledTimes(2);
});
@@ -209,4 +205,12 @@ describe('ViewProfileComponent', () => {
expect(router.navigate).toHaveBeenCalledWith(['/personal-files'], { replaceUrl: true });
});
+
+ it('should render additional user profile sections if provided', () => {
+ spyOn(appExtensionService, 'getUserProfileSections').and.returnValue(of([{ id: 'test-section' }]));
+ fixture.detectChanges();
+
+ expect(component.sections.length).toBe(1);
+ expect(unitTestingUtils.getByDirective(DynamicExtensionComponent)).toBeDefined();
+ });
});
diff --git a/projects/aca-content/src/lib/components/view-profile/view-profile.component.ts b/projects/aca-content/src/lib/components/view-profile/view-profile.component.ts
index 55086fc97..ec000dcba 100644
--- a/projects/aca-content/src/lib/components/view-profile/view-profile.component.ts
+++ b/projects/aca-content/src/lib/components/view-profile/view-profile.component.ts
@@ -27,8 +27,8 @@ import { PeopleApi, Person } from '@alfresco/js-api';
import { Component, OnInit, ViewEncapsulation } from '@angular/core';
import { FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
import { Router } from '@angular/router';
-import { Observable, throwError } from 'rxjs';
-import { AppService } from '@alfresco/aca-shared';
+import { Observable, take, throwError } from 'rxjs';
+import { AppExtensionService, AppService, UserProfileSection } from '@alfresco/aca-shared';
import { CommonModule } from '@angular/common';
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
@@ -36,9 +36,19 @@ import { TranslatePipe } from '@ngx-translate/core';
import { MatDividerModule } from '@angular/material/divider';
import { MatFormFieldModule } from '@angular/material/form-field';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
+import { DynamicExtensionComponent } from '@alfresco/adf-extensions';
@Component({
- imports: [CommonModule, TranslatePipe, ReactiveFormsModule, MatButtonModule, MatIconModule, MatDividerModule, MatFormFieldModule],
+ imports: [
+ CommonModule,
+ TranslatePipe,
+ ReactiveFormsModule,
+ MatButtonModule,
+ MatIconModule,
+ MatDividerModule,
+ MatFormFieldModule,
+ DynamicExtensionComponent
+ ],
selector: 'app-view-profile',
templateUrl: './view-profile.component.html',
styleUrls: ['./view-profile.component.scss'],
@@ -60,13 +70,15 @@ export class ViewProfileComponent implements OnInit {
contactSectionDropdown = false;
contactSectionButtonsToggle = true;
appNavNarMode$: Observable<'collapsed' | 'expanded'>;
+ sections: UserProfileSection[] = [];
constructor(
private router: Router,
- apiService: AlfrescoApiService,
- private appService: AppService
+ private readonly apiService: AlfrescoApiService,
+ private readonly appService: AppService,
+ private readonly extensionService: AppExtensionService
) {
- this.peopleApi = new PeopleApi(apiService.getInstance());
+ this.peopleApi = new PeopleApi(this.apiService.getInstance());
this.appNavNarMode$ = appService.appNavNarMode$.pipe(takeUntilDestroyed());
}
@@ -81,9 +93,14 @@ export class ViewProfileComponent implements OnInit {
.catch((error) => {
throwError(error);
});
+
+ this.extensionService
+ .getUserProfileSections()
+ .pipe(take(1))
+ .subscribe((sections) => (this.sections = sections));
}
- toggleClick() {
+ toggleNavigationMenu() {
this.appService.toggleAppNavBar$.next();
}
@@ -112,10 +129,6 @@ export class ViewProfileComponent implements OnInit {
toggleGeneralDropdown() {
this.generalSectionDropdown = !this.generalSectionDropdown;
-
- if (!this.generalSectionDropdown) {
- this.generalSectionButtonsToggle = true;
- }
}
toggleGeneralButtons() {
@@ -143,10 +156,6 @@ export class ViewProfileComponent implements OnInit {
toggleLoginDropdown() {
this.loginSectionDropdown = !this.loginSectionDropdown;
-
- if (!this.loginSectionDropdown) {
- this.loginSectionButtonsToggle = true;
- }
}
toggleLoginButtons() {
@@ -161,10 +170,6 @@ export class ViewProfileComponent implements OnInit {
toggleContactDropdown() {
this.contactSectionDropdown = !this.contactSectionDropdown;
-
- if (!this.contactSectionDropdown) {
- this.contactSectionButtonsToggle = true;
- }
}
toggleContactButtons() {
diff --git a/projects/aca-shared/rules/src/app.rules.spec.ts b/projects/aca-shared/rules/src/app.rules.spec.ts
index be81561d3..044cb1b31 100644
--- a/projects/aca-shared/rules/src/app.rules.spec.ts
+++ b/projects/aca-shared/rules/src/app.rules.spec.ts
@@ -1196,6 +1196,18 @@ describe('app.evaluators', () => {
expect(app.isSmartFolder(context)).toBeTrue();
});
});
+
+ describe('isSSOEnabled', () => {
+ it('should return true if sso is enabled', () => {
+ context.appConfig = { get: () => 'OAUTH' } as any;
+ expect(app.isSSOEnabled(context)).toBe(true);
+ });
+
+ it('should return false if sso is not enabled', () => {
+ context.appConfig = { get: () => 'basic' } as any;
+ expect(app.isSSOEnabled(context)).toBe(false);
+ });
+ });
});
describe('Versions compatibility', () => {
diff --git a/projects/aca-shared/rules/src/app.rules.ts b/projects/aca-shared/rules/src/app.rules.ts
index 3868e73fc..8b02f36db 100644
--- a/projects/aca-shared/rules/src/app.rules.ts
+++ b/projects/aca-shared/rules/src/app.rules.ts
@@ -560,3 +560,5 @@ export const canDisplayKnowledgeRetrievalButton = (context: AcaRuleContext): boo
navigation.isRecentFiles(context) ||
navigation.isFavorites(context) ||
((navigation.isSearchResults(context) || navigation.isLibraryContent(context)) && !navigation.isLibraries(context)));
+
+export const isSSOEnabled = (context: AcaRuleContext): boolean => context.appConfig.get('authType') === 'OAUTH';
diff --git a/projects/aca-shared/src/lib/models/types.ts b/projects/aca-shared/src/lib/models/types.ts
index e8c2bc9ec..705bfe338 100644
--- a/projects/aca-shared/src/lib/models/types.ts
+++ b/projects/aca-shared/src/lib/models/types.ts
@@ -34,3 +34,7 @@ export interface Badge extends Partial> {
+ id: string;
+}
diff --git a/projects/aca-shared/src/lib/services/app.extension.service.spec.ts b/projects/aca-shared/src/lib/services/app.extension.service.spec.ts
index 52bb6d83c..6315653ed 100644
--- a/projects/aca-shared/src/lib/services/app.extension.service.spec.ts
+++ b/projects/aca-shared/src/lib/services/app.extension.service.spec.ts
@@ -1682,6 +1682,41 @@ describe('AppExtensionService', () => {
});
});
+ it('should get custom user profile sections from config', (done) => {
+ extensions.setEvaluators({
+ 'action.enabled': () => true
+ });
+
+ applyConfig({
+ ...defaultConfigMock,
+ features: {
+ userProfileSections: [
+ {
+ id: 'section1-id',
+ component: 'testComponent1',
+ rules: {
+ visible: 'action.enabled'
+ }
+ },
+ {
+ id: 'section2-id',
+ component: 'testComponent2',
+ rules: {
+ visible: 'action.enabled'
+ }
+ }
+ ]
+ }
+ });
+
+ service.getUserProfileSections().subscribe((sections) => {
+ expect(sections.length).toBe(2);
+ expect(sections[0].id).toEqual('section1-id');
+ expect(sections[1].id).toEqual('section2-id');
+ done();
+ });
+ });
+
it('should update sidebar actions correctly', () => {
spyOn(loader, 'getContentActions').and.callThrough();
service.updateSidebarActions();
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 2e139d0ff..57d6d0fb8 100644
--- a/projects/aca-shared/src/lib/services/app.extension.service.ts
+++ b/projects/aca-shared/src/lib/services/app.extension.service.ts
@@ -53,7 +53,7 @@ import { AppConfigService, AuthenticationService, LogService } from '@alfresco/a
import { BehaviorSubject, Observable, Subject } from 'rxjs';
import { NodeEntry, RepositoryInfo } from '@alfresco/js-api';
import { ViewerRules } from '../models/viewer.rules';
-import { Badge } from '../models/types';
+import { Badge, UserProfileSection } from '../models/types';
import { NodePermissionService } from '../services/node-permission.service';
import { map } from 'rxjs/operators';
import { SearchCategory } from '@alfresco/adf-content-services';
@@ -92,6 +92,7 @@ export class AppExtensionService implements RuleContext {
private _filesDocumentListPreset = new BehaviorSubject>([]);
private _customMetadataPanels = new BehaviorSubject>([]);
private _bulkActions = new BehaviorSubject>([]);
+ private readonly _userProfileSections = new BehaviorSubject>([]);
documentListPresets: {
libraries: Array;
@@ -169,6 +170,7 @@ export class AppExtensionService implements RuleContext {
this._openWithActions.next(this.loader.getContentActions(config, 'features.viewer.openWith'));
this._createActions.next(this.loader.getElements(config, 'features.create'));
this._badges.next(this.loader.getElements(config, 'features.badges'));
+ this._userProfileSections.next(this.loader.getElements(config, 'features.userProfileSections'));
this._filesDocumentListPreset.next(this.getDocumentListPreset(config, 'files'));
this._customMetadataPanels.next(this.loader.getElements(config, 'features.customMetadataPanels'));
this._bulkActions.next(this.loader.getElements(config, 'features.bulk-actions'));
@@ -384,6 +386,10 @@ export class AppExtensionService implements RuleContext {
return this._badges.pipe(map((badges) => badges.filter((badge) => this.evaluateRule(badge.rules.visible, node))));
}
+ getUserProfileSections(): Observable> {
+ return this._userProfileSections.pipe(map((sections) => sections.filter((section) => this.evaluateRule(section.rules.visible))));
+ }
+
getCustomMetadataPanels(node: NodeEntry): Observable> {
return this._customMetadataPanels.pipe(map((panels) => panels.filter((panel) => this.evaluateRule(panel.rules.visible, node))));
}