diff --git a/jest.config.js b/jest.config.js index fa5714c..a85dcb3 100644 --- a/jest.config.js +++ b/jest.config.js @@ -4,4 +4,7 @@ module.exports = { collectCoverage: true, modulePathIgnorePatterns: ['/dist/'], coveragePathIgnorePatterns: ['/node_modules/', '/integration-tests/'], + moduleNameMapper: { + '^@luigi-project/client-support-angular$': '/projects/lib/_mocks_/luigi-client-support-angular.ts', + }, }; diff --git a/package.json b/package.json index 3165396..bcccea4 100644 --- a/package.json +++ b/package.json @@ -16,8 +16,8 @@ "@angular-builders/jest": "^20.0.0", "@angular-devkit/build-angular": "^20.0.0", "@angular-eslint/builder": "^20.2.0", - "@angular/cli": "^20.2.0", "@angular/build": "^20.2.1", + "@angular/cli": "^20.2.0", "@angular/compiler-cli": "^20.2.1", "@angular/localize": "^20.2.1", "@briebug/jest-schematic": "^6.0.0", @@ -30,11 +30,11 @@ "jest-junit": "16.0.0", "jest-mock-extended": "3.0.7", "jmespath": "0.16.0", + "mkdirp": "^3.0.1", "ng-packagr": "^20.2.0", - "ts-jest": "29.3.2", - "typescript": "~5.8.0", + "nodemon": "3.1.10", "rimraf": "6.0.1", - "mkdirp": "^3.0.1", - "nodemon": "3.1.10" + "ts-jest": "29.3.2", + "typescript": "~5.8.0" } } diff --git a/projects/lib/_mocks_/luigi-client-support-angular.ts b/projects/lib/_mocks_/luigi-client-support-angular.ts new file mode 100644 index 0000000..87f8721 --- /dev/null +++ b/projects/lib/_mocks_/luigi-client-support-angular.ts @@ -0,0 +1,6 @@ +import { Observable, of } from 'rxjs'; +export class LuigiContextService { + contextObservable(): Observable<{ context: any }> { + return of({ context: null }); + } +} diff --git "a/projects/lib/_mocks_/ui5\342\200\221mock.ts" "b/projects/lib/_mocks_/ui5\342\200\221mock.ts" new file mode 100644 index 0000000..6f26246 --- /dev/null +++ "b/projects/lib/_mocks_/ui5\342\200\221mock.ts" @@ -0,0 +1,32 @@ +import { Component } from '@angular/core'; +import { ButtonComponent } from '@ui5/webcomponents-ngx'; + +@Component({ selector: 'ui5-component', template: '', standalone: true }) +export class MockComponent {} + +jest.mock('@ui5/webcomponents-ngx', () => { + return { + BreadcrumbsComponent: MockComponent, + BreadcrumbsItemComponent: MockComponent, + ButtonComponent: MockComponent, + DialogComponent: MockComponent, + DynamicPageComponent: MockComponent, + DynamicPageHeaderComponent: MockComponent, + DynamicPageTitleComponent: MockComponent, + IconComponent: MockComponent, + IllustratedMessageComponent: MockComponent, + InputComponent: MockComponent, + LabelComponent: MockComponent, + OptionComponent: MockComponent, + SelectComponent: MockComponent, + TableCellComponent: MockComponent, + TableComponent: MockComponent, + TableHeaderCellComponent: MockComponent, + TableHeaderRowComponent: MockComponent, + TableRowComponent: MockComponent, + TextComponent: MockComponent, + TitleComponent: MockComponent, + ToolbarButtonComponent: MockComponent, + ToolbarComponent: MockComponent, + }; +}); diff --git a/projects/lib/jest.config.js b/projects/lib/jest.config.js index e46b5cb..f1d91f0 100644 --- a/projects/lib/jest.config.js +++ b/projects/lib/jest.config.js @@ -3,6 +3,7 @@ const path = require('path'); module.exports = { displayName: 'lib', coverageDirectory: path.resolve(__dirname, '../../coverage/lib'), + setupFilesAfterEnv: [`${__dirname}/jest.setup.ts`], coverageThreshold: { global: { branches: 67, diff --git a/projects/lib/jest.setup.ts b/projects/lib/jest.setup.ts new file mode 100644 index 0000000..39cf8da --- /dev/null +++ b/projects/lib/jest.setup.ts @@ -0,0 +1 @@ +jest.requireMock('./_mocks_/ui5‑mock'); diff --git a/projects/lib/organization/components/organization-management/organization-management.component.html b/projects/lib/organization/components/organization-management/organization-management.component.html new file mode 100644 index 0000000..00f8e79 --- /dev/null +++ b/projects/lib/organization/components/organization-management/organization-management.component.html @@ -0,0 +1,46 @@ +
+
+ {{ texts.explanation }} +
+ +
+ {{ + texts.switchOrganization.label + }}
+
+ + @for (org of organizations(); track org) { + {{ + org + }} + } + + {{ + texts.switchOrganization.button + }} +
+
+ +
+ {{ + texts.onboardOrganization.label + }}
+
+ + {{ + texts.onboardOrganization.button + }} +
+
+
diff --git a/projects/lib/organization/components/organization-management/organization-management.component.scss b/projects/lib/organization/components/organization-management/organization-management.component.scss new file mode 100644 index 0000000..11f7202 --- /dev/null +++ b/projects/lib/organization/components/organization-management/organization-management.component.scss @@ -0,0 +1,13 @@ +.organization-management { + margin: 1.5rem; + display: flex; + flex-direction: column; + gap: 1rem; +} + +.organization-management-input { + display: flex; + align-items: center; + gap: 1rem; +} + diff --git a/projects/lib/organization/components/organization-management/organization-management.component.spec.ts b/projects/lib/organization/components/organization-management/organization-management.component.spec.ts new file mode 100644 index 0000000..54c1b8d --- /dev/null +++ b/projects/lib/organization/components/organization-management/organization-management.component.spec.ts @@ -0,0 +1,223 @@ +import { + CUSTOM_ELEMENTS_SCHEMA, + NO_ERRORS_SCHEMA, + signal +} from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { FormsModule } from '@angular/forms'; +import { MutationResult } from '@apollo/client'; +import { LuigiContextService } from '@luigi-project/client-support-angular'; +import { + ClientEnvironment, EnvConfigService, + I18nService, + LuigiCoreService, LuigiGlobalContext, NodeContext, ResourceService +} from '@openmfp/portal-ui-lib'; +import { of, throwError } from 'rxjs'; +import { OrganizationManagementComponent } from './organization-management.component'; + +describe('OrganizationManagementComponent', () => { + let component: OrganizationManagementComponent; + let fixture: ComponentFixture; + let resourceServiceMock: jest.Mocked; + let i18nServiceMock: jest.Mocked; + let luigiCoreServiceMock: jest.Mocked; + let envConfigServiceMock: jest.Mocked; + + beforeEach(async () => { + resourceServiceMock = { + readOrganizations: jest.fn(), + create: jest.fn(), + } as any; + + i18nServiceMock = { + translationTable: {}, + getTranslation: jest.fn(), + } as any; + + luigiCoreServiceMock = { + getGlobalContext: jest.fn(), + showAlert: jest.fn(), + } as any; + + envConfigServiceMock = { + getEnvConfig: jest.fn(), + } as any; + + await TestBed.configureTestingModule({ + imports: [OrganizationManagementComponent, FormsModule], + providers: [ + { provide: ResourceService, useValue: resourceServiceMock }, + { provide: I18nService, useValue: i18nServiceMock }, + { provide: LuigiCoreService, useValue: luigiCoreServiceMock }, + { provide: EnvConfigService, useValue: envConfigServiceMock }, + LuigiContextService, + ], + schemas: [CUSTOM_ELEMENTS_SCHEMA, NO_ERRORS_SCHEMA], + }) + .overrideComponent(OrganizationManagementComponent, { + set: { template: '', imports: [] }, + }) + .compileComponents(); + + fixture = TestBed.createComponent(OrganizationManagementComponent); + component = fixture.componentInstance; + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should react to context input change', () => { + const mockContext = { + translationTable: { hello: 'world' }, + } as any as NodeContext; + + resourceServiceMock.readOrganizations.mockReturnValue(of({} as any)); + + const contextSignal = signal(mockContext); + component.context = contextSignal as any; + + fixture.detectChanges(); + + expect(component['i18nService'].translationTable).toEqual( + mockContext.translationTable, + ); + }); + + it('should initialize with empty organizations', () => { + expect(component.organizations()).toEqual([]); + }); + + it('should read organizations on init', () => { + const mockOrganizations = { + Accounts: [ + { metadata: { name: 'org1' } }, + { metadata: { name: 'org2' } }, + ], + }; + const mockGlobalContext: LuigiGlobalContext = { + portalContext: {}, + userId: 'user1', + userEmail: 'user@test.com', + token: 'token', + organization: 'org1', + portalBaseUrl: 'https://test.com', + }; + luigiCoreServiceMock.getGlobalContext.mockReturnValue(mockGlobalContext); + resourceServiceMock.readOrganizations.mockReturnValue( + of(mockOrganizations as any), + ); + + component.ngOnInit(); + + expect(resourceServiceMock.readOrganizations).toHaveBeenCalled(); + expect(component.organizations()).toEqual(['org2']); + }); + + it('should set organization to switch', () => { + const event = { target: { value: 'testOrg' } }; + component.setOrganizationToSwitch(event); + expect(component.organizationToSwitch).toBe('testOrg'); + }); + + it('should onboard new organization successfully', () => { + const mockResponse: MutationResult = { + data: undefined, + loading: false, + error: undefined, + called: true, + client: {} as any, + reset: jest.fn(), + }; + resourceServiceMock.create.mockReturnValue(of(mockResponse)); + component.newOrganization = 'newOrg'; + component.organizations.set(['existingOrg']); + + component.onboardOrganization(); + + expect(resourceServiceMock.create).toHaveBeenCalled(); + expect(component.organizations()).toEqual(['newOrg', 'existingOrg']); + expect(component.organizationToSwitch).toBe('newOrg'); + expect(component.newOrganization).toBe(''); + }); + + it('should handle organization creation error', () => { + resourceServiceMock.create.mockReturnValue( + throwError(() => new Error('Creation failed')), + ); + component.newOrganization = 'newOrg'; + + component.onboardOrganization(); + + expect(luigiCoreServiceMock.showAlert).toHaveBeenCalledWith({ + text: 'Failure! Could not create organization: newOrg.', + type: 'error', + }); + }); + + it('should switch organization', async () => { + const mockEnvConfig: ClientEnvironment = { + idpName: 'test', + organization: 'test', + oauthServerUrl: 'https://test.com', + clientId: 'test', + baseDomain: 'test.com', + isLocal: false, + developmentInstance: false, + authData: { + expires_in: '3600', + access_token: 'test-access-token', + id_token: 'test-id-token', + }, + }; + envConfigServiceMock.getEnvConfig.mockResolvedValue(mockEnvConfig); + component.organizationToSwitch = 'newOrg'; + Object.defineProperty(window, 'location', { + value: { protocol: 'https:', port: '8080' }, + writable: true, + }); + + await component.switchOrganization(); + + expect(window.location.href).toBe('https://newOrg.test.com:8080'); + }); + + it('should not switch and show alert for invalid organization name', async () => { + const mockEnvConfig: ClientEnvironment = { + idpName: 'test', + organization: 'test', + oauthServerUrl: 'https://test.com', + clientId: 'test', + baseDomain: 'test.com', + isLocal: false, + developmentInstance: false, + authData: { + expires_in: '3600', + access_token: 'test-access-token', + id_token: 'test-id-token', + }, + }; + envConfigServiceMock.getEnvConfig.mockResolvedValue(mockEnvConfig); + + const invalidNames = ['-abc', 'abc-', 'a.b', 'a b', '']; + + for (const name of invalidNames) { + component.organizationToSwitch = name as any; + Object.defineProperty(window, 'location', { + value: { protocol: 'https:', port: '' }, + writable: true, + }); + + await component.switchOrganization(); + + expect(luigiCoreServiceMock.showAlert).toHaveBeenCalledWith({ + text: + 'Organization name is not valid for subdomain usage, accrording to RFC 1034/1123.', + type: 'error', + }); + + expect((window.location as any).href).toBeUndefined(); + (luigiCoreServiceMock.showAlert as jest.Mock).mockClear(); + } + }); +}); diff --git a/projects/lib/organization/components/organization-management/organization-management.component.ts b/projects/lib/organization/components/organization-management/organization-management.component.ts new file mode 100644 index 0000000..88ec94f --- /dev/null +++ b/projects/lib/organization/components/organization-management/organization-management.component.ts @@ -0,0 +1,200 @@ +import { + ChangeDetectionStrategy, + Component, + OnInit, + ViewEncapsulation, + effect, + inject, + signal +} from '@angular/core'; +import { toSignal } from '@angular/core/rxjs-interop'; +import { FormsModule } from '@angular/forms'; +import { LuigiContextService } from '@luigi-project/client-support-angular'; +import { + EnvConfigService, + I18nService, + LuigiCoreService, + Resource, + ResourceDefinition, + ResourceNodeContext, + ResourceService, + generateGraphQLFields +} from '@openmfp/portal-ui-lib'; +import { + ButtonComponent, + InputComponent, + LabelComponent, + OptionComponent, + SelectComponent, +} from '@ui5/webcomponents-ngx'; +import { map } from 'rxjs'; + +@Component({ + selector: 'organization-management', + standalone: true, + imports: [ + LabelComponent, + InputComponent, + ButtonComponent, + OptionComponent, + SelectComponent, + FormsModule, + ], + templateUrl: './organization-management.component.html', + styleUrl: './organization-management.component.scss', + encapsulation: ViewEncapsulation.ShadowDom, + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class OrganizationManagementComponent implements OnInit { + private i18nService = inject(I18nService); + private resourceService = inject(ResourceService); + private luigiCoreService = inject(LuigiCoreService); + private envConfigService = inject(EnvConfigService); + private contextService = inject(LuigiContextService); + + context = toSignal(this.contextService.contextObservable().pipe(map((context) => context.context as ResourceNodeContext))); + texts: any = {}; + organizations = signal([]); + organizationToSwitch: string; + newOrganization: string; + + constructor() { + effect(() => { + const ctx = this.context(); + if (ctx) { + this.i18nService.translationTable = ctx.translationTable; + this.texts = this.readTranslations(); + } + }); + } + + ngOnInit(): void { + this.readOrganizations(); + } + + setOrganizationToSwitch($event: any) { + this.organizationToSwitch = $event.target.value; + } + + readOrganizations() { + const fields = generateGraphQLFields([ + { + property: 'Accounts.metadata.name', + }, + ]); + const queryOperation = 'core_platform_mesh_io'; + + this.resourceService + .readOrganizations(queryOperation, fields, this.context()) + .subscribe({ + next: (result) => { + this.organizations.set( + result['Accounts'] + .map((o) => o.metadata.name) + .filter( + (o) => + o !== this.luigiCoreService.getGlobalContext().organization, + ), + ); + }, + }); + } + + onboardOrganization() { + const resource: Resource = { + spec: { type: 'org' }, + metadata: { name: this.newOrganization }, + }; + const resourceDefinition: ResourceDefinition = { + group: 'core.platform-mesh.io', + kind: 'Account', + plural: 'accounts', + singular: 'account', + scope: 'Cluster', + }; + + this.resourceService + .create(resource, resourceDefinition, this.context()) + .subscribe({ + next: (result) => { + console.debug('Resource created', result); + this.organizations.set([ + this.newOrganization, + ...this.organizations(), + ]); + this.organizationToSwitch = this.newOrganization; + this.newOrganization = ''; + this.luigiCoreService.showAlert({ + text: 'New organization has been created, select it from the list to switch to it.', + type: 'info', + }); + }, + error: (error) => { + this.luigiCoreService.showAlert({ + text: `Failure! Could not create organization: ${resource.metadata.name}.`, + type: 'error', + }); + }, + }); + } + + private readTranslations() { + return { + explanation: this.i18nService.getTranslation( + 'ORGANIZATION_MANAGEMENT_EXPLANATION', + ), + switchOrganization: { + label: this.i18nService.getTranslation( + 'ORGANIZATION_MANAGEMENT_SWITCH_LABEL', + ), + button: this.i18nService.getTranslation( + 'ORGANIZATION_MANAGEMENT_SWITCH_BUTTON', + ), + }, + + onboardOrganization: { + label: this.i18nService.getTranslation( + 'ORGANIZATION_MANAGEMENT_ONBOARD_LABEL', + ), + button: this.i18nService.getTranslation( + 'ORGANIZATION_MANAGEMENT_ONBOARD_BUTTON', + ), + placeholder: this.i18nService.getTranslation( + 'ORGANIZATION_MANAGEMENT_ONBOARD_PLACEHOLDER', + ), + }, + }; + } + + /** + * Allows only valid subdomain values: alphanumeric, hyphens, no periods, cannot start/end with hyphen, min 1 character. + * Returns sanitized string or null if invalid. + */ + private sanitizeSubdomainInput(input: string): string | null { + // RFC 1034/1123: subdomain labels are 1-63 chars, start/end with alphanum, can contain '-' + if (typeof input !== 'string') return null; + const sanitized = input.trim(); + if (/^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$/.test(sanitized)) { + return sanitized; + } + return null; + } + + async switchOrganization() { + const { baseDomain } = await this.envConfigService.getEnvConfig(); + const protocol = window.location.protocol; + const sanitizedOrg = this.sanitizeSubdomainInput(this.organizationToSwitch); + + if (!sanitizedOrg) { + this.luigiCoreService.showAlert({ + text: 'Organization name is not valid for subdomain usage, accrording to RFC 1034/1123.', + type: 'error', + }); + return; + } + + const fullSubdomain = `${sanitizedOrg}.${baseDomain}`; + const port = window.location.port ? `:${window.location.port}` : ''; + window.location.href = `${protocol}//${fullSubdomain}${port}`; + } +} diff --git a/projects/lib/organization/provide-organization-feature.ts b/projects/lib/organization/provide-organization-feature.ts new file mode 100644 index 0000000..91777bd --- /dev/null +++ b/projects/lib/organization/provide-organization-feature.ts @@ -0,0 +1,20 @@ +import { makeEnvironmentProviders } from '@angular/core'; +import { provideRouter } from '@angular/router'; +import { + LuigiContextService, + LuigiContextServiceImpl, +} from '@luigi-project/client-support-angular'; +import { organizationInitializer } from './initializers/organization-initializer'; +import { routes } from './routes'; + + +export const provideOrganizationFeature = () => { + return makeEnvironmentProviders([ + provideRouter(routes), + organizationInitializer(), + { + provide: LuigiContextService, + useClass: LuigiContextServiceImpl, + } + ]); +}; diff --git a/projects/lib/organization/public-api.ts b/projects/lib/organization/public-api.ts index 4b339e4..d6cc19b 100644 --- a/projects/lib/organization/public-api.ts +++ b/projects/lib/organization/public-api.ts @@ -1 +1 @@ -export * from './initializers/organization-initializer'; +export * from './provide-organization-feature'; diff --git a/projects/lib/organization/routes.ts b/projects/lib/organization/routes.ts new file mode 100644 index 0000000..e0b72b2 --- /dev/null +++ b/projects/lib/organization/routes.ts @@ -0,0 +1,9 @@ +import { Route } from '@angular/router'; +import { OrganizationManagementComponent } from './components/organization-management/organization-management.component'; + +export const routes: Route[] = [ + { + path: 'organization-management', + component: OrganizationManagementComponent, + }, +]; diff --git a/projects/lib/package.json b/projects/lib/package.json index 13bb277..f1e6172 100644 --- a/projects/lib/package.json +++ b/projects/lib/package.json @@ -15,6 +15,7 @@ "@angular/platform-browser-dynamic": "^19.0.0 || ^20.0.0", "@angular/router": "^19.0.0 || ^20.0.0", "@ui5/webcomponents-ngx": "^0.4.8 || ^0.5.0", + "@luigi-project/client-support-angular": "^20.0.1", "rxjs": "~7.8.0", "zone.js": "~0.15.1" } diff --git a/projects/lib/portal-options/services/custom-global-nodes.service.ts b/projects/lib/portal-options/services/custom-global-nodes.service.ts index 1fe14bf..fbf4b71 100644 --- a/projects/lib/portal-options/services/custom-global-nodes.service.ts +++ b/projects/lib/portal-options/services/custom-global-nodes.service.ts @@ -1,8 +1,8 @@ +import { inject } from '@angular/core'; +import { CustomGlobalNodesService, I18nService } from '@openmfp/portal-ui-lib'; import { kcpRootOrgsPath } from '../models/constants'; import { PortalNodeContext } from '../models/luigi-context'; import { PortalLuigiNode } from '../models/luigi-node'; -import { inject } from '@angular/core'; -import { CustomGlobalNodesService, I18nService } from '@openmfp/portal-ui-lib'; export class CustomGlobalNodesServiceImpl implements CustomGlobalNodesService { private i18nService = inject(I18nService); @@ -15,10 +15,7 @@ export class CustomGlobalNodesServiceImpl implements CustomGlobalNodesService { hideFromNav: true, hideSideNav: true, order: '1001', - viewUrl: '/assets/openmfp-portal-ui-wc.js#organization-management', - webcomponent: { - selfRegistered: true, - }, + viewUrl: '/organization-management', context: { translationTable: this.i18nService.translationTable, kcpPath: kcpRootOrgsPath, diff --git a/projects/lib/portal-options/services/header-bar-renderers/namespace-selection-renderer.service.spec.ts b/projects/lib/portal-options/services/header-bar-renderers/namespace-selection-renderer.service.spec.ts index 76c05a2..4770de1 100644 --- a/projects/lib/portal-options/services/header-bar-renderers/namespace-selection-renderer.service.spec.ts +++ b/projects/lib/portal-options/services/header-bar-renderers/namespace-selection-renderer.service.spec.ts @@ -172,4 +172,50 @@ describe('NamespaceSelectionRendererService', () => { value: { pathname: origPathname }, }); }); + + it('should skip items without name and avoid duplicates', async () => { + const origPathname = window.location.pathname; + Object.defineProperty(window, 'location', { + value: { pathname: '/ns1/workloads' }, + writable: true, + }); + + const portalConfig: any = { + portalContext: { crdGatewayApiUrl: 'https://api.example.com/graphql' }, + }; + + mockAuthService.getToken.mockReturnValue('token'); + + mockResourceService.list.mockReturnValue( + of([ + { metadata: {} } as any, + { metadata: { name: 'ns1' } } as any, + { metadata: { name: 'ns1' } } as any, + ]), + ); + + const renderer = service.create(portalConfig); + const container = document.createElement('div'); + const nodeItems = [ + { + label: 'Workloads', + node: { + navigationContext: 'workloads', + context: { resourceDefinition: { scope: 'Namespaced' } }, + }, + }, + ] as any; + + renderer(container, nodeItems, () => {}); + + const cb = getChildrenByTag(container, 'ui5-combobox')[0]; + const items = getChildrenByTag(cb, 'ui5-cb-item'); + const texts = items.map((i) => i.getAttribute('text')); + + expect(texts).toEqual(['-all-', 'ns1']); + + Object.defineProperty(window, 'location', { + value: { pathname: origPathname }, + }); + }); }); diff --git a/projects/lib/portal-options/services/luigi-extended-global-context-config.service.spec.ts b/projects/lib/portal-options/services/luigi-extended-global-context-config.service.spec.ts index ce4bf8f..dec9823 100644 --- a/projects/lib/portal-options/services/luigi-extended-global-context-config.service.spec.ts +++ b/projects/lib/portal-options/services/luigi-extended-global-context-config.service.spec.ts @@ -1,4 +1,3 @@ -import { LuigiExtendedGlobalContextConfigServiceImpl } from './luigi-extended-global-context-config.service'; import { TestBed } from '@angular/core/testing'; import { AuthService, @@ -7,6 +6,7 @@ import { ResourceService, } from '@openmfp/portal-ui-lib'; import { of, throwError } from 'rxjs'; +import { LuigiExtendedGlobalContextConfigServiceImpl } from './luigi-extended-global-context-config.service'; describe('LuigiExtendedGlobalContextConfigServiceImpl', () => { let service: LuigiExtendedGlobalContextConfigServiceImpl; diff --git a/projects/lib/portal-options/services/user-profile-config.service.ts b/projects/lib/portal-options/services/user-profile-config.service.ts new file mode 100644 index 0000000..23a8923 --- /dev/null +++ b/projects/lib/portal-options/services/user-profile-config.service.ts @@ -0,0 +1,21 @@ +import { Injectable } from '@angular/core'; +import { UserProfile, UserProfileConfigService } from '@openmfp/portal-ui-lib'; + +@Injectable({ providedIn: 'root' }) +export class UserProfileConfigServiceImpl implements UserProfileConfigService { + async getProfile(): Promise { + return { + items: [ + { + label: 'PROFILE_ORGANIZATION', + icon: 'building', + link: '/organization-management', + openNodeInModal: { + width: '360px', + height: '260px', + }, + }, + ], + }; + } +}