diff --git a/package.json b/package.json
index da85128..c089c78 100644
--- a/package.json
+++ b/package.json
@@ -57,4 +57,4 @@
"typescript": "5.9.3",
"zone.js": "^0.15.1"
}
-}
+}
\ No newline at end of file
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 72fac90..8e55b55 100644
--- a/projects/lib/portal-options/services/custom-global-nodes.service.ts
+++ b/projects/lib/portal-options/services/custom-global-nodes.service.ts
@@ -2,7 +2,12 @@ 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';
+import {
+ CustomGlobalNodesService,
+ EntityType,
+ I18nService,
+ NodeContext,
+} from '@openmfp/portal-ui-lib';
export class CustomGlobalNodesServiceImpl implements CustomGlobalNodesService {
private i18nService = inject(I18nService);
@@ -25,6 +30,28 @@ export class CustomGlobalNodesServiceImpl implements CustomGlobalNodesService {
kcpPath: kcpRootOrgsPath,
} as PortalNodeContext,
},
+ {
+ pathSegment: 'error',
+ order: '1000',
+ hideFromNav: true,
+ context: {} as PortalNodeContext,
+ children: [
+ {
+ pathSegment: ':id',
+ entityType: EntityType.ENTITY_ERROR,
+ hideFromNav: true,
+ hideSideNav: true,
+ viewUrl: '/assets/platform-mesh-portal-ui-wc.js#error-component',
+ context: {
+ id: ':id',
+ translationTable: this.i18nService.translationTable,
+ } as any as NodeContext,
+ webcomponent: {
+ selfRegistered: true,
+ },
+ },
+ ],
+ },
{
pathSegment: 'users',
showBreadcrumbs: false,
diff --git a/projects/lib/portal-options/services/router-config.service.spec.ts b/projects/lib/portal-options/services/router-config.service.spec.ts
index 0863fdc..b75325b 100644
--- a/projects/lib/portal-options/services/router-config.service.spec.ts
+++ b/projects/lib/portal-options/services/router-config.service.spec.ts
@@ -92,7 +92,7 @@ describe('CustomRoutingConfigServiceImpl', () => {
const result = await config.pageNotFoundHandler();
expect(result).toEqual({
- redirectTo: 'welcome',
+ redirectTo: 'error/404',
keepURL: true,
});
});
@@ -111,7 +111,7 @@ describe('CustomRoutingConfigServiceImpl', () => {
const result = await config.pageNotFoundHandler();
expect(result).toEqual({
- redirectTo: 'error/404',
+ redirectTo: 'welcome',
keepURL: true,
});
});
@@ -171,7 +171,7 @@ describe('CustomRoutingConfigServiceImpl', () => {
const result = await config.pageNotFoundHandler();
expect(result).toEqual({
- redirectTo: 'error/404',
+ redirectTo: 'welcome',
keepURL: true,
});
});
@@ -190,7 +190,7 @@ describe('CustomRoutingConfigServiceImpl', () => {
const result = await config.pageNotFoundHandler();
expect(result).toEqual({
- redirectTo: 'welcome',
+ redirectTo: 'error/404',
keepURL: true,
});
});
@@ -215,7 +215,7 @@ describe('CustomRoutingConfigServiceImpl', () => {
const result = await config.pageNotFoundHandler();
expect(result).toEqual({
- redirectTo: 'error/404',
+ redirectTo: 'welcome',
keepURL: true,
});
});
diff --git a/projects/lib/portal-options/services/router-config.service.ts b/projects/lib/portal-options/services/router-config.service.ts
index 4b6685f..fa9de43 100644
--- a/projects/lib/portal-options/services/router-config.service.ts
+++ b/projects/lib/portal-options/services/router-config.service.ts
@@ -20,12 +20,12 @@ export class CustomRoutingConfigServiceImpl implements RoutingConfigService {
getRoutingConfig(): any {
return {
- pageNotFoundHandler: async () => {
+ pageNotFoundHandler: () => {
if (!this.envConfig?.baseDomain) {
return this.redirectTo('error/404');
}
- if (window.location.hostname !== this.envConfig.baseDomain) {
+ if (window.location.hostname === this.envConfig.baseDomain) {
return this.redirectTo('welcome');
}
diff --git a/projects/wc/_mocks_/ui5-mock.ts b/projects/wc/_mocks_/ui5-mock.ts
index dde2109..92f5553 100644
--- a/projects/wc/_mocks_/ui5-mock.ts
+++ b/projects/wc/_mocks_/ui5-mock.ts
@@ -40,4 +40,26 @@ jest.mock('@ui5/webcomponents-icons/dist/hide.js', () => ({}), {
});
jest.mock('@ui5/webcomponents-icons/dist/show.js', () => ({}), {
virtual: true,
-});
\ No newline at end of file
+});
+
+jest.mock(
+ '@ui5/webcomponents-fiori/dist/illustrations/NoEntries.js',
+ () => ({}),
+ {
+ virtual: true,
+ },
+);
+jest.mock(
+ '@ui5/webcomponents-fiori/dist/illustrations/UnableToLoad.js',
+ () => ({}),
+ {
+ virtual: true,
+ },
+);
+jest.mock(
+ '@ui5/webcomponents-fiori/dist/illustrations/tnt/UnsuccessfulAuth.js',
+ () => ({}),
+ {
+ virtual: true,
+ },
+);
\ No newline at end of file
diff --git a/projects/wc/src/app/components/error/error.component.html b/projects/wc/src/app/components/error/error.component.html
new file mode 100644
index 0000000..cdcc2e3
--- /dev/null
+++ b/projects/wc/src/app/components/error/error.component.html
@@ -0,0 +1,23 @@
+@let configVM = config();
+@if (configVM.scene) {
+
+
+
+ {{configVM.illustratedMessageTitle}}
+
+
{{configVM.illustratedMessageText}}
+
+
+ @for (button of configVM.buttons; track button.url) {
+
+ {{ button.label }}
+
+ }
+
+
+
+
+}
diff --git a/projects/wc/src/app/components/error/error.component.spec.ts b/projects/wc/src/app/components/error/error.component.spec.ts
new file mode 100644
index 0000000..3b4bd8a
--- /dev/null
+++ b/projects/wc/src/app/components/error/error.component.spec.ts
@@ -0,0 +1,119 @@
+import { ErrorComponent } from './error.component';
+import { ButtonConfig } from './models/error.model';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { I18nService, LuigiCoreService } from '@openmfp/portal-ui-lib';
+import {
+ ButtonComponent,
+ IllustratedMessageComponent,
+ TitleComponent,
+} from '@ui5/webcomponents-ngx';
+
+describe('ErrorComponent', () => {
+ let component: ErrorComponent;
+ let fixture: ComponentFixture;
+ let i18nServiceMock: jest.Mocked;
+ let luigiCoreServiceMock: jest.Mocked;
+
+ beforeEach(async () => {
+ i18nServiceMock = {
+ getTranslationAsync: jest.fn().mockResolvedValue('translated text'),
+ translationTable: {},
+ } as any;
+
+ luigiCoreServiceMock = {
+ navigation: jest.fn().mockReturnValue({
+ navigate: jest.fn(),
+ }),
+ showAlert: jest.fn(),
+ } as any;
+
+ await TestBed.configureTestingModule({
+ imports: [
+ ErrorComponent,
+ IllustratedMessageComponent,
+ ButtonComponent,
+ TitleComponent,
+ ],
+ providers: [
+ { provide: I18nService, useValue: i18nServiceMock },
+ { provide: LuigiCoreService, useValue: luigiCoreServiceMock },
+ ],
+ }).compileComponents();
+
+ fixture = TestBed.createComponent(ErrorComponent);
+ component = fixture.componentInstance;
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ describe('goTo', () => {
+ it('should open URL in new tab', () => {
+ const windowSpy = jest.spyOn(window, 'open').mockImplementation();
+ const button: ButtonConfig = { url: 'https://test.com' };
+
+ component.goTo(button);
+ expect(windowSpy).toHaveBeenCalledWith('https://test.com', '_blank');
+ });
+
+ it('should navigate using LuigiCore when route is provided', () => {
+ const button: ButtonConfig = { route: { context: 'test-route' } };
+ const navigateSpy = jest.fn();
+ jest
+ .spyOn(luigiCoreServiceMock, 'navigation')
+ .mockReturnValue({ navigate: navigateSpy });
+
+ component.goTo(button);
+ expect(navigateSpy).toHaveBeenCalledWith('/test-route');
+ });
+ });
+
+ describe('setSceneConfig', () => {
+ it('should set 404 config', async () => {
+ const testContext = {
+ id: '404',
+ translationTable: {},
+ };
+
+ fixture.componentRef.setInput('context', testContext);
+
+ await component.ngOnInit();
+ expect(component.config().scene).toBe('NoEntries');
+ expect(component.config().illustratedMessageTitle).toBe('translated text');
+ expect(component.config().illustratedMessageText).toBe('translated text');
+ expect(component.config().buttons).toBeDefined();
+ });
+
+ it('should set 403 config', async () => {
+ const testContext = {
+ id: '403',
+ translationTable: {},
+ };
+
+ fixture.componentRef.setInput('context', testContext);
+
+ await component.ngOnInit();
+ expect(component.config().scene).toBe('tnt/UnsuccessfulAuth');
+ expect(component.config().illustratedMessageTitle).toBe('');
+ expect(component.config().illustratedMessageText).toBe('translated text');
+ expect(component.config().buttons).toBeDefined();
+ expect(component.config().buttons?.length).toBe(2);
+ });
+
+ it('should set default error config for unknown error code', async () => {
+ const testContext = {
+ id: '500',
+ translationTable: {},
+ };
+
+ fixture.componentRef.setInput('context', testContext);
+
+ await component.ngOnInit();
+ expect(component.config().scene).toBe('UnableToLoad');
+ expect(component.config().illustratedMessageTitle).toBe('translated text');
+ expect(component.config().illustratedMessageText).toBe('');
+ expect(component.config().buttons).toBeDefined();
+ });
+ });
+});
diff --git a/projects/wc/src/app/components/error/error.component.ts b/projects/wc/src/app/components/error/error.component.ts
new file mode 100644
index 0000000..174e0b0
--- /dev/null
+++ b/projects/wc/src/app/components/error/error.component.ts
@@ -0,0 +1,121 @@
+import { ButtonConfig, ErrorConfig } from './models/error.model';
+import {
+ ChangeDetectionStrategy,
+ ChangeDetectorRef,
+ Component,
+ OnInit,
+ ViewEncapsulation,
+ inject,
+ input,
+ signal,
+} from '@angular/core';
+import { I18nService, LuigiCoreService } from '@openmfp/portal-ui-lib';
+import '@ui5/webcomponents-fiori/dist/illustrations/NoEntries.js';
+import '@ui5/webcomponents-fiori/dist/illustrations/UnableToLoad.js';
+import '@ui5/webcomponents-fiori/dist/illustrations/tnt/UnsuccessfulAuth.js';
+import {
+ ButtonComponent,
+ IllustratedMessageComponent,
+ TitleComponent,
+} from '@ui5/webcomponents-ngx';
+
+@Component({
+ selector: 'app-error',
+ standalone: true,
+ templateUrl: './error.component.html',
+ encapsulation: ViewEncapsulation.ShadowDom,
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ imports: [IllustratedMessageComponent, ButtonComponent, TitleComponent],
+})
+export class ErrorComponent implements OnInit {
+ private i18nService = inject(I18nService);
+ private luigiCoreService = inject(LuigiCoreService);
+
+ public context = input.required();
+
+ config = signal({
+ scene: 'UnableToLoad',
+ illustratedMessageTitle: '',
+ illustratedMessageText: '',
+ buttons: [],
+ });
+
+ async ngOnInit() {
+ await this.setSceneConfig();
+ }
+
+ goTo(button: ButtonConfig): void {
+ if (button.url) {
+ window.open(button.url, '_blank');
+ } else if (button.route) {
+ this.luigiCoreService.navigation().navigate(`/${button.route.context}`);
+ }
+ }
+
+ private async setSceneConfig() {
+ const nodeContext = this.context();
+ switch (+nodeContext.id) {
+ case 403: {
+ this.config.set(await this.getError403Config());
+ break;
+ }
+ case 404: {
+ this.config.set(await this.getError404Config());
+ break;
+ }
+ default: {
+ this.config.set(await this.getErrorDefaultConfig());
+ }
+ }
+ }
+
+ private async getError404Config(): Promise {
+ return {
+ scene: 'NoEntries',
+ illustratedMessageTitle: await this.i18nService.getTranslationAsync(
+ 'ERROR_CONTENT_NOT_FOUND_TITLE',
+ ),
+ illustratedMessageText: await this.i18nService.getTranslationAsync(
+ 'ERROR_CONTENT_NOT_FOUND_TEXT',
+ ),
+ buttons: [],
+ };
+ }
+
+ private async getError403Config(): Promise {
+ const illustratedMessageText = await this.i18nService.getTranslationAsync(
+ 'ERROR_CONTENT_NOT_ALLOWED_NO_PROJECT_MEMBER_TEXT',
+ );
+
+ return {
+ scene: 'tnt/UnsuccessfulAuth',
+ illustratedMessageTitle: '',
+ illustratedMessageText,
+ buttons: [
+ {
+ url: '',
+ label: await this.i18nService.getTranslationAsync(
+ 'ERROR_CONTENT_NOT_ALLOWED_JOIN_PROJECT_BUTTON',
+ ),
+ },
+ {
+ url: '',
+ label: await this.i18nService.getTranslationAsync(
+ 'ERROR_CONTENT_NOT_ALLOWED_VIEW_PROJECT_BUTTON',
+ ),
+ },
+ ],
+ };
+ }
+
+ private async getErrorDefaultConfig(): Promise {
+ return {
+ scene: 'UnableToLoad',
+ illustratedMessageTitle: await this.i18nService.getTranslationAsync(
+ 'ERROR_UNIDENTIFIED_TITLE',
+ ),
+ illustratedMessageText: '',
+ buttons: [],
+ };
+ }
+}
diff --git a/projects/wc/src/app/components/error/models/error.model.ts b/projects/wc/src/app/components/error/models/error.model.ts
new file mode 100644
index 0000000..ed14364
--- /dev/null
+++ b/projects/wc/src/app/components/error/models/error.model.ts
@@ -0,0 +1,14 @@
+export interface ErrorConfig {
+ scene: string;
+ illustratedMessageTitle: string;
+ illustratedMessageText: string;
+ buttons: ButtonConfig[];
+}
+
+export interface ButtonConfig {
+ url?: string;
+ label?: string;
+ route?: {
+ context: string;
+ };
+}
diff --git a/projects/wc/src/app/initializers/luigi-wc-initializer.spec.ts b/projects/wc/src/app/initializers/luigi-wc-initializer.spec.ts
index 710e4d3..01779bf 100644
--- a/projects/wc/src/app/initializers/luigi-wc-initializer.spec.ts
+++ b/projects/wc/src/app/initializers/luigi-wc-initializer.spec.ts
@@ -4,6 +4,7 @@ import {
OrganizationManagementComponent,
WelcomeComponent,
} from '../components';
+import { ErrorComponent } from '../components/error/error.component';
import * as wc from '../utils/wc';
import { provideLuigiWebComponents } from './luigi-wc-initializer';
import { ApplicationInitStatus } from '@angular/core';
@@ -30,6 +31,7 @@ describe('provideLuigiWebComponents', () => {
'generic-detail-view': DetailViewComponent,
'organization-management': OrganizationManagementComponent,
'welcome-view': WelcomeComponent,
+ 'error-component': ErrorComponent,
} as Record;
// Validate first arg equals the components map
diff --git a/projects/wc/src/app/initializers/luigi-wc-initializer.ts b/projects/wc/src/app/initializers/luigi-wc-initializer.ts
index a443ed4..350c5c5 100644
--- a/projects/wc/src/app/initializers/luigi-wc-initializer.ts
+++ b/projects/wc/src/app/initializers/luigi-wc-initializer.ts
@@ -4,6 +4,7 @@ import {
OrganizationManagementComponent,
WelcomeComponent,
} from '../components';
+import { ErrorComponent } from '../components/error/error.component';
import { registerLuigiWebComponents } from '../utils/wc';
import { Injector, inject, provideAppInitializer } from '@angular/core';
@@ -16,6 +17,7 @@ export const provideLuigiWebComponents = () =>
'generic-detail-view': DetailViewComponent,
'organization-management': OrganizationManagementComponent,
'welcome-view': WelcomeComponent,
+ 'error-component': ErrorComponent,
},
injector,
);