From 8d47920fa329892b6a8ff8f301a7ce661e74c439 Mon Sep 17 00:00:00 2001 From: sdarbinyan Date: Fri, 3 Jul 2026 02:07:12 +0400 Subject: [PATCH] arch(sprint1): drive widget registry from manifest --- .../core/runtime/platform-runtime.service.ts | 29 +++++++--- .../widget-registry.bootstrap.service.ts | 57 ++++++++++++++++--- .../mock/bootstrap/widget-manifest.json | 22 +++++++ 3 files changed, 93 insertions(+), 15 deletions(-) create mode 100644 src/assets/mock/bootstrap/widget-manifest.json diff --git a/src/app/core/runtime/platform-runtime.service.ts b/src/app/core/runtime/platform-runtime.service.ts index cda9c30..b943d9d 100644 --- a/src/app/core/runtime/platform-runtime.service.ts +++ b/src/app/core/runtime/platform-runtime.service.ts @@ -27,15 +27,28 @@ export class PlatformRuntimeService { this.configService.loadBootstrap().pipe(take(1)).subscribe({ next: (bootstrap) => { - this.widgetRegistryBootstrap.registerDefaults(); - this.themeEngine.applyTheme(bootstrap.theme); - this.brandingEngine.applyBranding(bootstrap.branding); - this.runtimeState.markInitialized({ - localization: bootstrap.localization, - featureFlags: bootstrap.featureFlags, - permissions: bootstrap.permissions + this.widgetRegistryBootstrap.registerFromManifest().subscribe({ + next: () => { + this.themeEngine.applyTheme(bootstrap.theme); + this.brandingEngine.applyBranding(bootstrap.branding); + this.runtimeState.markInitialized({ + localization: bootstrap.localization, + featureFlags: bootstrap.featureFlags, + permissions: bootstrap.permissions + }); + this.initializing = false; + }, + error: () => { + this.themeEngine.applyTheme(bootstrap.theme); + this.brandingEngine.applyBranding(bootstrap.branding); + this.runtimeState.markInitialized({ + localization: bootstrap.localization, + featureFlags: bootstrap.featureFlags, + permissions: bootstrap.permissions + }); + this.initializing = false; + } }); - this.initializing = false; }, error: () => { this.initializing = false; diff --git a/src/app/widgets/registry/widget-registry.bootstrap.service.ts b/src/app/widgets/registry/widget-registry.bootstrap.service.ts index 2ebff97..efecb36 100644 --- a/src/app/widgets/registry/widget-registry.bootstrap.service.ts +++ b/src/app/widgets/registry/widget-registry.bootstrap.service.ts @@ -1,16 +1,59 @@ import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { Observable, map, of } from 'rxjs'; +import { catchError } from 'rxjs/operators'; import { HeroWidgetComponent, FooterNavigationWidgetComponent, ProductCarouselWidgetComponent } from '../ui'; +import { RegisteredWidget } from '../contracts/widget-component.contract'; import { WidgetRegistryService } from './widget-registry.service'; +interface WidgetManifestItem { + type: string; + version: string; + componentKey: string; + enabled?: boolean; +} + +interface WidgetManifest { + widgets: WidgetManifestItem[]; +} + +const APPROVED_WIDGET_COMPONENTS: Record = { + 'hero': HeroWidgetComponent, + 'footer-navigation': FooterNavigationWidgetComponent, + 'product-carousel': ProductCarouselWidgetComponent +}; + @Injectable({ providedIn: 'root' }) export class WidgetRegistryBootstrapService { - constructor(private readonly registry: WidgetRegistryService) {} + private readonly manifestUrl = '/assets/mock/bootstrap/widget-manifest.json'; - registerDefaults(): void { - this.registry.registerMany([ - { type: 'hero', version: '1.0.0', component: HeroWidgetComponent }, - { type: 'footer-navigation', version: '1.0.0', component: FooterNavigationWidgetComponent }, - { type: 'product-carousel', version: '1.0.0', component: ProductCarouselWidgetComponent } - ]); + constructor( + private readonly registry: WidgetRegistryService, + private readonly http: HttpClient + ) {} + + registerFromManifest(): Observable { + return this.http.get(this.manifestUrl).pipe( + map((manifest) => { + const widgets = (manifest.widgets ?? []) + .filter((item) => item.enabled !== false) + .map((item): RegisteredWidget | null => { + const component = APPROVED_WIDGET_COMPONENTS[item.componentKey]; + if (!component) { + return null; + } + + return { + type: item.type, + version: item.version, + component + }; + }) + .filter((item): item is RegisteredWidget => item !== null); + + this.registry.registerMany(widgets); + }), + catchError(() => of(undefined)) + ); } } diff --git a/src/assets/mock/bootstrap/widget-manifest.json b/src/assets/mock/bootstrap/widget-manifest.json new file mode 100644 index 0000000..2984125 --- /dev/null +++ b/src/assets/mock/bootstrap/widget-manifest.json @@ -0,0 +1,22 @@ +{ + "widgets": [ + { + "type": "hero", + "version": "1.0.0", + "componentKey": "hero", + "enabled": true + }, + { + "type": "footer-navigation", + "version": "1.0.0", + "componentKey": "footer-navigation", + "enabled": true + }, + { + "type": "product-carousel", + "version": "1.0.0", + "componentKey": "product-carousel", + "enabled": true + } + ] +}