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 }} + + }
-
+ }
-
+
- - {{ contactSectionDropdown ? 'expand_more' : 'chevron_right'}} +

{{'APP.EDIT_PROFILE.COMPANY_DETAILS' | translate}}

- -
- -
-
- - -
+ @if (contactSectionButtonsToggle) { + + } @else { + + + }
- -
+ @if (contactSectionDropdown) { +

{{'APP.EDIT_PROFILE.NAME' | translate}}

-

{{personDetails?.company?.organization}}

- + @if (contactSectionButtonsToggle) { +

{{personDetails?.company?.organization}}

+ } @else { + + }

{{'APP.EDIT_PROFILE.ADDRESS' | translate}}

-

{{personDetails?.company?.address1}}

- + @if (contactSectionButtonsToggle) { +

{{personDetails?.company?.address1}}

+ } @else { + + }

{{'APP.EDIT_PROFILE.POSTCODE' | translate}}

-

{{personDetails?.company?.postcode}}

- + @if (contactSectionButtonsToggle) { +

{{personDetails?.company?.postcode}}

+ } @else { + + }

{{'APP.EDIT_PROFILE.TELEPHONE' | translate}}

-

{{personDetails?.company?.telephone}}

- - - {{ 'APP.EDIT_PROFILE.INVALID_INPUT' | translate }} - + @if (contactSectionButtonsToggle) { +

{{personDetails?.company?.telephone}}

+ } @else { + + } + @if (profileForm.controls['companyTelephone'].errors) { + + {{ 'APP.EDIT_PROFILE.INVALID_INPUT' | translate }} + + }

{{'APP.EDIT_PROFILE.EMAIL' | translate}}

-

{{personDetails?.company?.email}}

- - - {{ 'APP.EDIT_PROFILE.INVALID_INPUT' | translate }} - + @if (contactSectionButtonsToggle) { +

{{personDetails?.company?.email}}

+ } @else { + + } + @if (profileForm.controls['companyEmail'].errors) { + + {{ 'APP.EDIT_PROFILE.INVALID_INPUT' | translate }} + + }
-
+ }
+ @for (section of sections; track $index) { + + }
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)))); }