diff --git a/gravitee-apim-portal-webui-next/src/app/app.component.html b/gravitee-apim-portal-webui-next/src/app/app.component.html
index 4673da00aeb..cb9a74a7b8f 100644
--- a/gravitee-apim-portal-webui-next/src/app/app.component.html
+++ b/gravitee-apim-portal-webui-next/src/app/app.component.html
@@ -15,7 +15,7 @@
limitations under the License.
-->
-
+
diff --git a/gravitee-apim-portal-webui-next/src/app/app.component.spec.ts b/gravitee-apim-portal-webui-next/src/app/app.component.spec.ts
index 2f34f53bc7c..0a42e6135bc 100644
--- a/gravitee-apim-portal-webui-next/src/app/app.component.spec.ts
+++ b/gravitee-apim-portal-webui-next/src/app/app.component.spec.ts
@@ -16,12 +16,13 @@
import { HarnessLoader } from '@angular/cdk/testing';
import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
import { provideHttpClientTesting } from '@angular/common/http/testing';
-import { Injectable } from '@angular/core';
+import { signal } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MatButtonHarness } from '@angular/material/button/testing';
import { AppComponent } from './app.component';
-import { PortalMenuLinksService } from '../services/portal-menu-links.service';
+import { PortalNavigationItem } from '../entities/portal-navigation/portal-navigation';
+import { PortalNavigationItemsService } from '../services/portal-navigation-items.service';
import { AppTestingModule } from '../testing/app-testing.module';
describe('AppComponent', () => {
@@ -45,37 +46,36 @@ describe('AppComponent', () => {
});
describe('custom links', () => {
- @Injectable()
- class PortalMenuLinksServiceStub {
- links = () => [
+ beforeEach(async () => {
+ const mockItems: PortalNavigationItem[] = [
{
- id: 'link-id-1',
- type: 'external',
- name: 'link-name-1',
- target: 'link-target-1',
- order: 1,
+ id: 'l1',
+ organizationId: 'org1',
+ environmentId: 'env1',
+ title: 'link-name-1',
+ type: 'LINK',
+ area: 'TOP_NAVBAR',
+ order: 0,
+ url: '/link1',
},
{
- id: 'link-id-2',
- type: 'external',
- name: 'link-name-2',
- target: 'link-target-2',
- order: 2,
+ id: 'l2',
+ organizationId: 'org1',
+ environmentId: 'env1',
+ title: 'link-name-2',
+ type: 'LINK',
+ area: 'TOP_NAVBAR',
+ order: 1,
+ url: '/link2',
},
];
- }
- beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [AppComponent, AppTestingModule],
- providers: [
- {
- provide: PortalMenuLinksService,
- useClass: PortalMenuLinksServiceStub,
- },
- ],
+ providers: [provideHttpClientTesting(), { provide: PortalNavigationItemsService, useValue: { topNavbar: signal(mockItems) } }],
}).compileComponents();
fixture = TestBed.createComponent(AppComponent);
+ fixture.detectChanges();
harnessLoader = TestbedHarnessEnvironment.loader(fixture);
});
it('should show custom links', async () => {
diff --git a/gravitee-apim-portal-webui-next/src/app/app.component.ts b/gravitee-apim-portal-webui-next/src/app/app.component.ts
index 20386c4ad1f..df5498d8146 100644
--- a/gravitee-apim-portal-webui-next/src/app/app.component.ts
+++ b/gravitee-apim-portal-webui-next/src/app/app.component.ts
@@ -22,7 +22,7 @@ import { FooterComponent } from '../components/footer/footer.component';
import { NavBarComponent } from '../components/nav-bar/nav-bar.component';
import { ConfigService } from '../services/config.service';
import { CurrentUserService } from '../services/current-user.service';
-import { PortalMenuLinksService } from '../services/portal-menu-links.service';
+import { PortalNavigationItemsService } from '../services/portal-navigation-items.service';
import { ThemeService } from '../services/theme.service';
@Component({
@@ -35,7 +35,7 @@ export class AppComponent {
currentUser = inject(CurrentUserService).user;
logo = inject(ThemeService).logo;
favicon = inject(ThemeService).favicon;
- customLinks = inject(PortalMenuLinksService).links;
+ topBarNavigationItems = inject(PortalNavigationItemsService).topNavbar;
private siteTitle: string;
constructor(
diff --git a/gravitee-apim-portal-webui-next/src/app/app.config.ts b/gravitee-apim-portal-webui-next/src/app/app.config.ts
index 980cd6b80de..d6fa0724af7 100644
--- a/gravitee-apim-portal-webui-next/src/app/app.config.ts
+++ b/gravitee-apim-portal-webui-next/src/app/app.config.ts
@@ -30,7 +30,7 @@ import { httpRequestInterceptor } from '../interceptors/http-request.interceptor
import { AuthService } from '../services/auth.service';
import { ConfigService } from '../services/config.service';
import { CurrentUserService } from '../services/current-user.service';
-import { PortalMenuLinksService } from '../services/portal-menu-links.service';
+import { PortalNavigationItemsService } from '../services/portal-navigation-items.service';
import { ThemeService } from '../services/theme.service';
function initApp(
@@ -38,7 +38,7 @@ function initApp(
configService: ConfigService,
themeService: ThemeService,
currentUserService: CurrentUserService,
- portalMenuLinksService: PortalMenuLinksService,
+ portalNavigationItemsService: PortalNavigationItemsService,
router: Router,
): () => Observable {
return () =>
@@ -47,7 +47,7 @@ function initApp(
combineLatest([
themeService.loadTheme(),
configService.loadConfiguration(),
- portalMenuLinksService.loadCustomLinks(),
+ portalNavigationItemsService.loadTopNavBarItems(),
authService.load().pipe(switchMap(_ => currentUserService.loadUser())),
]),
),
@@ -70,7 +70,7 @@ export const appConfig: ApplicationConfig = {
inject(ConfigService),
inject(ThemeService),
inject(CurrentUserService),
- inject(PortalMenuLinksService),
+ inject(PortalNavigationItemsService),
inject(Router),
);
return initializerFn();
diff --git a/gravitee-apim-portal-webui-next/src/app/log-in/log-in.component.spec.ts b/gravitee-apim-portal-webui-next/src/app/log-in/log-in.component.spec.ts
index 67002e6f122..0d5a5df0e8f 100644
--- a/gravitee-apim-portal-webui-next/src/app/log-in/log-in.component.spec.ts
+++ b/gravitee-apim-portal-webui-next/src/app/log-in/log-in.component.spec.ts
@@ -123,7 +123,7 @@ describe('LogInComponent', () => {
await submitButton.click();
httpTestingController.expectOne(`${TESTING_BASE_URL}/auth/login`).flush({});
httpTestingController.expectOne(`${TESTING_BASE_URL}/user`).flush({});
- httpTestingController.expectOne(`${TESTING_BASE_URL}/portal-menu-links`).flush({});
+ httpTestingController.expectOne(`${TESTING_BASE_URL}/portal-navigation-items?area=TOP_NAVBAR&loadChildren=false`).flush({});
});
it('should not display log-in form', async () => {
diff --git a/gravitee-apim-portal-webui-next/src/app/log-in/log-in.component.ts b/gravitee-apim-portal-webui-next/src/app/log-in/log-in.component.ts
index d6dc5310727..0fca7d211f0 100644
--- a/gravitee-apim-portal-webui-next/src/app/log-in/log-in.component.ts
+++ b/gravitee-apim-portal-webui-next/src/app/log-in/log-in.component.ts
@@ -33,7 +33,7 @@ import { AuthService } from '../../services/auth.service';
import { ConfigService } from '../../services/config.service';
import { CurrentUserService } from '../../services/current-user.service';
import { IdentityProviderService } from '../../services/identity-provider.service';
-import { PortalMenuLinksService } from '../../services/portal-menu-links.service';
+import { PortalNavigationItemsService } from '../../services/portal-navigation-items.service';
@Component({
selector: 'app-log-in',
@@ -68,7 +68,7 @@ export class LogInComponent {
constructor(
private readonly authService: AuthService,
private readonly currentUserService: CurrentUserService,
- private readonly portalMenuLinksService: PortalMenuLinksService,
+ private readonly portalNavigationItemsService: PortalNavigationItemsService,
private readonly router: Router,
private readonly destroyRef: DestroyRef,
) {}
@@ -78,7 +78,7 @@ export class LogInComponent {
.login(this.logInForm.value.username, this.logInForm.value.password)
.pipe(
switchMap(_ => this.currentUserService.loadUser()),
- switchMap(_ => this.portalMenuLinksService.loadCustomLinks()),
+ switchMap(_ => this.portalNavigationItemsService.loadTopNavBarItems()),
tap(_ => this.router.navigate([this.redirectUrl()])),
takeUntilDestroyed(this.destroyRef),
)
diff --git a/gravitee-apim-portal-webui-next/src/app/log-out/log-out.component.spec.ts b/gravitee-apim-portal-webui-next/src/app/log-out/log-out.component.spec.ts
index 52a0eba52cf..6c378162f88 100644
--- a/gravitee-apim-portal-webui-next/src/app/log-out/log-out.component.spec.ts
+++ b/gravitee-apim-portal-webui-next/src/app/log-out/log-out.component.spec.ts
@@ -49,7 +49,7 @@ describe('LogOutComponent', () => {
fixture.detectChanges();
httpTestingController.expectOne(`${TESTING_BASE_URL}/auth/logout`).flush({});
- httpTestingController.expectOne(`${TESTING_BASE_URL}/portal-menu-links`).flush({});
+ httpTestingController.expectOne(`${TESTING_BASE_URL}/portal-navigation-items?area=TOP_NAVBAR&loadChildren=false`).flush({});
expect(router.navigate).toHaveBeenCalledTimes(1);
expect(router.navigate).toHaveBeenCalledWith(['']);
expect(currentUserService.user()).toEqual({});
diff --git a/gravitee-apim-portal-webui-next/src/app/log-out/log-out.component.ts b/gravitee-apim-portal-webui-next/src/app/log-out/log-out.component.ts
index ac66b444ebf..2764b9847cb 100644
--- a/gravitee-apim-portal-webui-next/src/app/log-out/log-out.component.ts
+++ b/gravitee-apim-portal-webui-next/src/app/log-out/log-out.component.ts
@@ -20,7 +20,7 @@ import { switchMap, tap } from 'rxjs';
import { AuthService } from '../../services/auth.service';
import { CurrentUserService } from '../../services/current-user.service';
-import { PortalMenuLinksService } from '../../services/portal-menu-links.service';
+import { PortalNavigationItemsService } from '../../services/portal-navigation-items.service';
@Component({
selector: 'app-log-out',
@@ -33,7 +33,7 @@ export class LogOutComponent implements OnInit {
constructor(
private readonly authService: AuthService,
private readonly currentUserService: CurrentUserService,
- private readonly portalMenuLinksService: PortalMenuLinksService,
+ private readonly portalNavigationItemsService: PortalNavigationItemsService,
private readonly router: Router,
) {}
@@ -43,7 +43,7 @@ export class LogOutComponent implements OnInit {
.logout()
.pipe(
tap(_ => this.currentUserService.clear()),
- switchMap(_ => this.portalMenuLinksService.loadCustomLinks()),
+ switchMap(_ => this.portalNavigationItemsService.loadTopNavBarItems()),
tap(_ => this.router.navigate([''])),
takeUntilDestroyed(this.destroyRef),
)
diff --git a/gravitee-apim-portal-webui-next/src/components/nav-bar/desktop-nav-bar/desktop-nav-bar.component.html b/gravitee-apim-portal-webui-next/src/components/nav-bar/desktop-nav-bar/desktop-nav-bar.component.html
index e1eb302728f..750542c49de 100644
--- a/gravitee-apim-portal-webui-next/src/components/nav-bar/desktop-nav-bar/desktop-nav-bar.component.html
+++ b/gravitee-apim-portal-webui-next/src/components/nav-bar/desktop-nav-bar/desktop-nav-bar.component.html
@@ -18,15 +18,19 @@
diff --git a/gravitee-apim-portal-webui-next/src/components/nav-bar/desktop-nav-bar/desktop-nav-bar.component.ts b/gravitee-apim-portal-webui-next/src/components/nav-bar/desktop-nav-bar/desktop-nav-bar.component.ts
index 96876341de2..92059681086 100644
--- a/gravitee-apim-portal-webui-next/src/components/nav-bar/desktop-nav-bar/desktop-nav-bar.component.ts
+++ b/gravitee-apim-portal-webui-next/src/components/nav-bar/desktop-nav-bar/desktop-nav-bar.component.ts
@@ -18,8 +18,8 @@ import { MatAnchor, MatButton } from '@angular/material/button';
import { RouterLink } from '@angular/router';
import { isEmpty } from 'lodash';
+import { PortalNavigationItem } from '../../../entities/portal-navigation/portal-navigation';
import { User } from '../../../entities/user/user';
-import { PortalMenuLink } from '../../../services/portal-menu-links.service';
import { UserAvatarComponent } from '../../user-avatar/user-avatar.component';
import { NavBarButtonComponent } from '../nav-bar-button/nav-bar-button.component';
@@ -31,7 +31,7 @@ import { NavBarButtonComponent } from '../nav-bar-button/nav-bar-button.componen
})
export class DesktopNavBarComponent {
currentUser: InputSignal = input({});
- customLinks: InputSignal = input([]);
+ topBarNavigationItems: InputSignal = input([]);
protected isLoggedIn = computed(() => {
return !isEmpty(this.currentUser());
});
diff --git a/gravitee-apim-portal-webui-next/src/components/nav-bar/mobile-nav-bar/mobile-nav-bar.component.html b/gravitee-apim-portal-webui-next/src/components/nav-bar/mobile-nav-bar/mobile-nav-bar.component.html
index f49f2eb89ba..13b89aaf694 100644
--- a/gravitee-apim-portal-webui-next/src/components/nav-bar/mobile-nav-bar/mobile-nav-bar.component.html
+++ b/gravitee-apim-portal-webui-next/src/components/nav-bar/mobile-nav-bar/mobile-nav-bar.component.html
@@ -37,10 +37,19 @@
}
- @for (link of customLinks(); track link.id) {
-
+ @for (navigationItem of topBarNavigationItems(); track navigationItem.id) {
+ @switch (navigationItem.type) {
+ @case ('LINK') {
+
+ }
+ @default {
+
+ }
+ }
}
diff --git a/gravitee-apim-portal-webui-next/src/components/nav-bar/mobile-nav-bar/mobile-nav-bar.component.ts b/gravitee-apim-portal-webui-next/src/components/nav-bar/mobile-nav-bar/mobile-nav-bar.component.ts
index 18858d5fe5e..acbca126d03 100644
--- a/gravitee-apim-portal-webui-next/src/components/nav-bar/mobile-nav-bar/mobile-nav-bar.component.ts
+++ b/gravitee-apim-portal-webui-next/src/components/nav-bar/mobile-nav-bar/mobile-nav-bar.component.ts
@@ -22,8 +22,8 @@ import { isEmpty } from 'lodash';
import { catchError, map } from 'rxjs';
import { of } from 'rxjs/internal/observable/of';
+import { PortalNavigationItem } from '../../../entities/portal-navigation/portal-navigation';
import { User } from '../../../entities/user/user';
-import { PortalMenuLink } from '../../../services/portal-menu-links.service';
import { PortalService } from '../../../services/portal.service';
@Component({
@@ -34,7 +34,7 @@ import { PortalService } from '../../../services/portal.service';
})
export class MobileNavBarComponent {
currentUser: InputSignal = input({});
- customLinks: InputSignal = input([]);
+ topBarNavigationItems: InputSignal = input([]);
hasHomepage = toSignal(
inject(PortalService)
.getPortalHomepages()
diff --git a/gravitee-apim-portal-webui-next/src/components/nav-bar/nav-bar.component.html b/gravitee-apim-portal-webui-next/src/components/nav-bar/nav-bar.component.html
index b497e95c0c9..6b9f5f87e2b 100644
--- a/gravitee-apim-portal-webui-next/src/components/nav-bar/nav-bar.component.html
+++ b/gravitee-apim-portal-webui-next/src/components/nav-bar/nav-bar.component.html
@@ -19,9 +19,9 @@
@if (!forceLogin() || isLoggedIn()) {
@if (isMobile()) {
-
+
} @else {
-
+
}
}
diff --git a/gravitee-apim-portal-webui-next/src/components/nav-bar/nav-bar.component.spec.ts b/gravitee-apim-portal-webui-next/src/components/nav-bar/nav-bar.component.spec.ts
index 027dd638336..9f8481eea8a 100644
--- a/gravitee-apim-portal-webui-next/src/components/nav-bar/nav-bar.component.spec.ts
+++ b/gravitee-apim-portal-webui-next/src/components/nav-bar/nav-bar.component.spec.ts
@@ -18,14 +18,16 @@ import { BreakpointObserver, Breakpoints } from '@angular/cdk/layout';
import { HarnessLoader } from '@angular/cdk/testing';
import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
import { HttpTestingController } from '@angular/common/http/testing';
-import { ComponentRef } from '@angular/core';
+import { ComponentRef, WritableSignal } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MatButtonHarness } from '@angular/material/button/testing';
import { of } from 'rxjs';
import { NavBarComponent } from './nav-bar.component';
import { PortalPage } from '../../entities/portal/portal-page';
+import { PortalNavigationItem, PortalNavigationLink } from '../../entities/portal-navigation/portal-navigation';
import { fakeUser } from '../../entities/user/user.fixtures';
+import { PortalNavigationItemsService } from '../../services/portal-navigation-items.service';
import { AppTestingModule, TESTING_BASE_URL } from '../../testing/app-testing.module';
import { DivHarness } from '../../testing/div.harness';
@@ -34,21 +36,27 @@ describe('NavBarComponent', () => {
let harnessLoader: HarnessLoader;
let componentRef: ComponentRef;
let httpTestingController: HttpTestingController;
- const customLinks = [
+ const topBarNavigationItems: PortalNavigationItem[] = [
{
id: 'link-id-1',
- type: 'external',
- name: 'link-name-1',
- target: 'link-target-1',
+ organizationId: 'DEFAULT',
+ environmentId: 'DEFAULT',
+ title: 'link-name-1',
+ type: 'LINK',
+ area: 'TOP_NAVBAR',
order: 1,
- },
+ url: 'link-target-1',
+ } as PortalNavigationLink,
{
id: 'link-id-2',
- type: 'external',
- name: 'link-name-2',
- target: 'link-target-2',
+ organizationId: 'DEFAULT',
+ environmentId: 'DEFAULT',
+ title: 'link-name-2',
+ type: 'LINK',
+ area: 'TOP_NAVBAR',
order: 2,
- },
+ url: 'link-target-2',
+ } as PortalNavigationLink,
];
const init = async (isMobile: boolean = false) => {
@@ -56,9 +64,19 @@ describe('NavBarComponent', () => {
observe: () => of({ matches: isMobile, breakpoints: { [Breakpoints.XSmall]: isMobile } }),
};
+ const portalNavigationItemsServiceMock: Partial = {
+ loadTopNavBarItems: () => of([]),
+ topNavbar: {
+ set: () => {},
+ } as unknown as WritableSignal,
+ };
+
await TestBed.configureTestingModule({
imports: [NavBarComponent, AppTestingModule],
- providers: [{ provide: BreakpointObserver, useValue: mockBreakpointObserver }],
+ providers: [
+ { provide: BreakpointObserver, useValue: mockBreakpointObserver },
+ { provide: PortalNavigationItemsService, useValue: portalNavigationItemsServiceMock },
+ ],
}).compileComponents();
fixture = TestBed.createComponent(NavBarComponent);
@@ -86,7 +104,7 @@ describe('NavBarComponent', () => {
});
it('should not show links if user is not connected and login is forced', async () => {
- componentRef.setInput('customLinks', customLinks);
+ componentRef.setInput('topBarNavigationItems', topBarNavigationItems);
componentRef.setInput('forceLogin', true);
const link1Anchor = await harnessLoader.getHarnessOrNull(MatButtonHarness.with({ text: 'link-name-1' }));
expect(link1Anchor).not.toBeTruthy();
@@ -95,7 +113,7 @@ describe('NavBarComponent', () => {
});
it('should show links if user is connected and login is forced', async () => {
- componentRef.setInput('customLinks', customLinks);
+ componentRef.setInput('topBarNavigationItems', topBarNavigationItems);
componentRef.setInput('forceLogin', true);
componentRef.setInput('currentUser', fakeUser());
const link1Anchor = await harnessLoader.getHarnessOrNull(MatButtonHarness.with({ text: 'link-name-1' }));
@@ -105,7 +123,7 @@ describe('NavBarComponent', () => {
});
it('should show custom links if login is not forced', async () => {
- componentRef.setInput('customLinks', customLinks);
+ componentRef.setInput('topBarNavigationItems', topBarNavigationItems);
const link1Anchor = await harnessLoader.getHarnessOrNull(MatButtonHarness.with({ text: 'link-name-1' }));
expect(link1Anchor).toBeTruthy();
const link2Anchor = await harnessLoader.getHarnessOrNull(MatButtonHarness.with({ text: 'link-name-2' }));
@@ -149,7 +167,7 @@ describe('NavBarComponent', () => {
it('should not show menu if user is not connected and login is forced', async () => {
expectHomePage([]);
- componentRef.setInput('customLinks', customLinks);
+ componentRef.setInput('topBarNavigationItems', topBarNavigationItems);
componentRef.setInput('forceLogin', true);
fixture.detectChanges();
@@ -160,7 +178,7 @@ describe('NavBarComponent', () => {
it('should show links if user is connected and login is forced', async () => {
expectHomePage([]);
componentRef.setInput('currentUser', fakeUser());
- componentRef.setInput('customLinks', customLinks);
+ componentRef.setInput('topBarNavigationItems', topBarNavigationItems);
componentRef.setInput('forceLogin', true);
fixture.detectChanges();
@@ -175,7 +193,7 @@ describe('NavBarComponent', () => {
it('should show custom links if login is not forced', async () => {
expectHomePage([]);
- componentRef.setInput('customLinks', customLinks);
+ componentRef.setInput('topBarNavigationItems', topBarNavigationItems);
fixture.detectChanges();
const menuButton = await harnessLoader.getHarness(MatButtonHarness.with({ selector: '.mobile-menu__button' }));
diff --git a/gravitee-apim-portal-webui-next/src/components/nav-bar/nav-bar.component.ts b/gravitee-apim-portal-webui-next/src/components/nav-bar/nav-bar.component.ts
index 7e5920b6a04..2370bb4dfa7 100644
--- a/gravitee-apim-portal-webui-next/src/components/nav-bar/nav-bar.component.ts
+++ b/gravitee-apim-portal-webui-next/src/components/nav-bar/nav-bar.component.ts
@@ -17,9 +17,9 @@ import { Component, computed, inject, input, InputSignal } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { isEmpty } from 'lodash';
+import { PortalNavigationItem } from '../../entities/portal-navigation/portal-navigation';
import { User } from '../../entities/user/user';
import { ObservabilityBreakpointService } from '../../services/observability-breakpoint.service';
-import { PortalMenuLink } from '../../services/portal-menu-links.service';
import { CompanyTitleComponent } from '../company-title/company-title.component';
import { DesktopNavBarComponent } from './desktop-nav-bar/desktop-nav-bar.component';
import { MobileNavBarComponent } from './mobile-nav-bar/mobile-nav-bar.component';
@@ -31,7 +31,7 @@ import { MobileNavBarComponent } from './mobile-nav-bar/mobile-nav-bar.component
styleUrls: ['./nav-bar.component.scss'],
})
export class NavBarComponent {
- customLinks: InputSignal = input([]);
+ topBarNavigationItems: InputSignal = input([]);
currentUser: InputSignal = input({});
forceLogin: InputSignal = input(false);
logo: InputSignal = input('');
diff --git a/gravitee-apim-portal-webui-next/src/entities/portal-navigation/portal-navigation.ts b/gravitee-apim-portal-webui-next/src/entities/portal-navigation/portal-navigation.ts
new file mode 100644
index 00000000000..d94cdaa070b
--- /dev/null
+++ b/gravitee-apim-portal-webui-next/src/entities/portal-navigation/portal-navigation.ts
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2024 The Gravitee team (http://gravitee.io)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export type PortalArea = 'HOMEPAGE' | 'TOP_NAVBAR';
+
+export type PortalNavigationType = 'PAGE' | 'FOLDER' | 'LINK';
+
+export interface BasePortalNavigationItem {
+ id: string;
+ organizationId: string;
+ environmentId: string;
+ title: string;
+ type: PortalNavigationType;
+ area: PortalArea;
+ parentId?: string | null;
+ order: number;
+}
+
+export interface PortalNavigationPage extends BasePortalNavigationItem {
+ type: 'PAGE';
+ portalPageContentId: string;
+}
+
+export interface PortalNavigationFolder extends BasePortalNavigationItem {
+ type: 'FOLDER';
+}
+
+export interface PortalNavigationLink extends BasePortalNavigationItem {
+ type: 'LINK';
+ url: string;
+}
+
+export type PortalNavigationItem = PortalNavigationPage | PortalNavigationFolder | PortalNavigationLink;
diff --git a/gravitee-apim-portal-webui-next/src/services/portal-menu-links.service.spec.ts b/gravitee-apim-portal-webui-next/src/services/portal-menu-links.service.spec.ts
deleted file mode 100644
index da4caad5568..00000000000
--- a/gravitee-apim-portal-webui-next/src/services/portal-menu-links.service.spec.ts
+++ /dev/null
@@ -1,67 +0,0 @@
-/*
- * Copyright (C) 2024 The Gravitee team (http://gravitee.io)
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-import { HttpTestingController } from '@angular/common/http/testing';
-import { TestBed } from '@angular/core/testing';
-
-import { PortalMenuLink, PortalMenuLinksService } from './portal-menu-links.service';
-import { AppTestingModule, TESTING_BASE_URL } from '../testing/app-testing.module';
-
-describe('PortalMenuLinksService', () => {
- let service: PortalMenuLinksService;
- let httpTestingController: HttpTestingController;
-
- beforeEach(() => {
- TestBed.configureTestingModule({
- imports: [AppTestingModule],
- });
- service = TestBed.inject(PortalMenuLinksService);
- httpTestingController = TestBed.inject(HttpTestingController);
- });
-
- afterEach(() => {
- httpTestingController.verify();
- });
-
- it('should return links list', done => {
- const linkLists: PortalMenuLink[] = [
- {
- id: 'link-id-1',
- type: 'external',
- name: 'link-name-1',
- target: 'link-target-1',
- order: 1,
- },
- {
- id: 'link-id-2',
- type: 'external',
- name: 'link-name-2',
- target: 'link-target-2',
- order: 2,
- },
- ];
- service.loadCustomLinks().subscribe(response => {
- expect(response).toMatchObject(linkLists);
- done();
- });
-
- const req = httpTestingController.expectOne(`${TESTING_BASE_URL}/portal-menu-links`);
- expect(req.request.method).toEqual('GET');
-
- req.flush(linkLists);
-
- expect(service.links()).toEqual(linkLists);
- });
-});
diff --git a/gravitee-apim-portal-webui-next/src/services/portal-navigation-items.service.spec.ts b/gravitee-apim-portal-webui-next/src/services/portal-navigation-items.service.spec.ts
new file mode 100644
index 00000000000..4d6c32de841
--- /dev/null
+++ b/gravitee-apim-portal-webui-next/src/services/portal-navigation-items.service.spec.ts
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2025 The Gravitee team (http://gravitee.io)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
+import { TestBed } from '@angular/core/testing';
+
+import { ConfigService } from './config.service';
+import { PortalNavigationItemsService } from './portal-navigation-items.service';
+import { PortalNavigationItem } from '../entities/portal-navigation/portal-navigation';
+
+describe('PortalNavigationItemsService', () => {
+ let service: PortalNavigationItemsService;
+ let httpMock: HttpTestingController;
+ const baseURL = 'http://localhost/portal/environments/DEFAULT';
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ imports: [HttpClientTestingModule],
+ providers: [{ provide: ConfigService, useValue: { baseURL } }],
+ });
+
+ service = TestBed.inject(PortalNavigationItemsService);
+ httpMock = TestBed.inject(HttpTestingController);
+ });
+
+ afterEach(() => {
+ httpMock.verify();
+ });
+
+ it('should load top navbar items and update topNavbar signal', done => {
+ const mockItems: PortalNavigationItem[] = [
+ {
+ id: '1',
+ organizationId: 'org1',
+ environmentId: 'env1',
+ title: 'Home',
+ type: 'PAGE',
+ area: 'TOP_NAVBAR',
+ order: 0,
+ portalPageContentId: 'content1',
+ },
+ {
+ id: '2',
+ organizationId: 'org1',
+ environmentId: 'env1',
+ title: 'APIs',
+ type: 'LINK',
+ area: 'TOP_NAVBAR',
+ order: 1,
+ url: '/apis',
+ },
+ ];
+
+ service.loadTopNavBarItems().subscribe(items => {
+ expect(items).toEqual(mockItems);
+ expect(service.topNavbar()).toEqual(mockItems);
+ done();
+ });
+
+ const req = httpMock.expectOne(
+ r =>
+ r.method === 'GET' &&
+ r.url === `${baseURL}/portal-navigation-items` &&
+ r.params.get('area') === 'TOP_NAVBAR' &&
+ r.params.get('loadChildren') === 'false',
+ );
+
+ req.flush(mockItems);
+ });
+
+ it('should set topNavbar to empty array on HTTP error', done => {
+ // set a non-empty value first to ensure it gets replaced
+ service.topNavbar.set([
+ {
+ id: 'x',
+ organizationId: 'org1',
+ environmentId: 'env1',
+ title: 'old',
+ type: 'FOLDER',
+ area: 'TOP_NAVBAR',
+ order: 2,
+ },
+ ]);
+
+ service.loadTopNavBarItems().subscribe(items => {
+ expect(items).toEqual([]);
+ expect(service.topNavbar()).toEqual([]);
+ done();
+ });
+
+ const req = httpMock.expectOne(
+ r =>
+ r.method === 'GET' &&
+ r.url === `${baseURL}/portal-navigation-items` &&
+ r.params.get('area') === 'TOP_NAVBAR' &&
+ r.params.get('loadChildren') === 'false',
+ );
+
+ req.flush('Server error', { status: 500, statusText: 'Server Error' });
+ });
+});
diff --git a/gravitee-apim-portal-webui-next/src/services/portal-menu-links.service.ts b/gravitee-apim-portal-webui-next/src/services/portal-navigation-items.service.ts
similarity index 55%
rename from gravitee-apim-portal-webui-next/src/services/portal-menu-links.service.ts
rename to gravitee-apim-portal-webui-next/src/services/portal-navigation-items.service.ts
index 0d87ce26134..0040e15f920 100644
--- a/gravitee-apim-portal-webui-next/src/services/portal-menu-links.service.ts
+++ b/gravitee-apim-portal-webui-next/src/services/portal-navigation-items.service.ts
@@ -19,32 +19,32 @@ import { catchError, Observable, tap } from 'rxjs';
import { of } from 'rxjs/internal/observable/of';
import { ConfigService } from './config.service';
-
-export type PortalMenuLinkType = 'external';
-
-export interface PortalMenuLink {
- id: string;
- type: PortalMenuLinkType;
- name: string;
- target: string;
- order: number;
-}
+import { PortalArea, PortalNavigationItem } from '../entities/portal-navigation/portal-navigation';
@Injectable({
providedIn: 'root',
})
-export class PortalMenuLinksService {
- public links: WritableSignal = signal([]);
+export class PortalNavigationItemsService {
+ public topNavbar: WritableSignal = signal([]);
constructor(
private readonly http: HttpClient,
- private configService: ConfigService,
+ private readonly configService: ConfigService,
) {}
- loadCustomLinks(): Observable {
- return this.http.get(`${this.configService.baseURL}/portal-menu-links`).pipe(
- catchError(_ => of([])),
- tap(value => this.links.set(value)),
- );
+ loadNavigationItems(area: PortalArea, loadChildren: boolean = true, parentId?: string): Observable {
+ const params = {
+ ...(parentId ? { parentId } : {}),
+ area,
+ loadChildren,
+ };
+
+ return this.http
+ .get(`${this.configService.baseURL}/portal-navigation-items`, { params })
+ .pipe(catchError(_ => of([])));
+ }
+
+ loadTopNavBarItems(): Observable {
+ return this.loadNavigationItems('TOP_NAVBAR', false).pipe(tap(value => this.topNavbar.set(value)));
}
}