feat(dees-appui-base): Add unified App UI API to dees-appui-base with ViewRegistry, AppRouter and StateManager
This commit is contained in:
@@ -17,6 +17,11 @@ import type { DeesAppuiMaincontent } from '../dees-appui-maincontent/dees-appui-
|
||||
import type { DeesAppuiActivitylog } from '../dees-appui-activitylog/dees-appui-activitylog.js';
|
||||
import { demoFunc } from './dees-appui-base.demo.js';
|
||||
|
||||
// New module imports
|
||||
import { ViewRegistry } from './view.registry.js';
|
||||
import { AppRouter } from './app.router.js';
|
||||
import { StateManager } from './state.manager.js';
|
||||
|
||||
// Import child components
|
||||
import '../dees-appui-appbar/index.js';
|
||||
import '../dees-appui-mainmenu/dees-appui-mainmenu.js';
|
||||
@@ -116,6 +121,19 @@ export class DeesAppuiBase extends DeesElement {
|
||||
@state()
|
||||
accessor activitylog: DeesAppuiActivitylog | undefined = undefined;
|
||||
|
||||
// NEW: Unified config property
|
||||
@property({ type: Object })
|
||||
accessor config: interfaces.IAppConfig | undefined = undefined;
|
||||
|
||||
// NEW: Current view state
|
||||
@state()
|
||||
accessor currentView: interfaces.IViewDefinition | undefined = undefined;
|
||||
|
||||
// NEW: Internal services (not reactive, managed internally)
|
||||
private viewRegistry: ViewRegistry = new ViewRegistry();
|
||||
private router: AppRouter | null = null;
|
||||
private stateManager: StateManager | null = null;
|
||||
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
css`
|
||||
@@ -155,6 +173,15 @@ export class DeesAppuiBase extends DeesElement {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* View container for dynamically loaded views */
|
||||
.view-container {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
.view-container:empty {
|
||||
display: none;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
@@ -200,6 +227,7 @@ export class DeesAppuiBase extends DeesElement {
|
||||
<dees-appui-maincontent
|
||||
.tabs=${this.maincontentTabs}
|
||||
>
|
||||
<div class="view-container"></div>
|
||||
<slot name="maincontent"></slot>
|
||||
</dees-appui-maincontent>
|
||||
<dees-appui-activitylog></dees-appui-activitylog>
|
||||
@@ -214,6 +242,24 @@ export class DeesAppuiBase extends DeesElement {
|
||||
this.secondarymenu = this.shadowRoot.querySelector('dees-appui-secondarymenu');
|
||||
this.maincontent = this.shadowRoot.querySelector('dees-appui-maincontent');
|
||||
this.activitylog = this.shadowRoot.querySelector('dees-appui-activitylog');
|
||||
|
||||
// Initialize from config if provided
|
||||
if (this.config) {
|
||||
this.applyConfig(this.config);
|
||||
|
||||
// Restore state if enabled
|
||||
if (this.config.statePersistence?.enabled) {
|
||||
this.loadState();
|
||||
}
|
||||
|
||||
// Initialize router after state restore
|
||||
this.router?.init();
|
||||
}
|
||||
}
|
||||
|
||||
async disconnectedCallback() {
|
||||
await super.disconnectedCallback();
|
||||
this.router?.destroy();
|
||||
}
|
||||
|
||||
// Event handlers for appbar
|
||||
@@ -295,4 +341,242 @@ export class DeesAppuiBase extends DeesElement {
|
||||
composed: true
|
||||
}));
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// NEW: Public methods for unified config API
|
||||
// ==========================================
|
||||
|
||||
/**
|
||||
* Configure the app shell with a unified config object
|
||||
*/
|
||||
public configure(config: interfaces.IAppConfig): void {
|
||||
this.config = config;
|
||||
this.applyConfig(config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate to a view by ID
|
||||
*/
|
||||
public navigateToView(viewId: string): boolean {
|
||||
if (this.router) {
|
||||
return this.router.navigate(viewId);
|
||||
}
|
||||
|
||||
// Fallback for non-routed mode
|
||||
const view = this.viewRegistry.get(viewId);
|
||||
if (view) {
|
||||
this.loadView(view);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current view
|
||||
*/
|
||||
public getCurrentView(): interfaces.IViewDefinition | undefined {
|
||||
return this.currentView;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get UI state for serialization
|
||||
*/
|
||||
public getUIState(): interfaces.IAppUIState {
|
||||
return {
|
||||
currentViewId: this.currentView?.id,
|
||||
mainMenuCollapsed: this.mainmenuCollapsed,
|
||||
secondaryMenuCollapsed: this.secondarymenuCollapsed,
|
||||
secondaryMenuSelectedKey: this.secondarymenuSelectedItem?.key,
|
||||
collapsedGroups: [], // TODO: Get from secondarymenu if needed
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore UI state from a state object
|
||||
*/
|
||||
public restoreUIState(state: interfaces.IAppUIState): void {
|
||||
if (state.mainMenuCollapsed !== undefined) {
|
||||
this.mainmenuCollapsed = state.mainMenuCollapsed;
|
||||
}
|
||||
if (state.secondaryMenuCollapsed !== undefined) {
|
||||
this.secondarymenuCollapsed = state.secondaryMenuCollapsed;
|
||||
}
|
||||
if (state.currentViewId) {
|
||||
this.navigateToView(state.currentViewId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save current UI state
|
||||
*/
|
||||
public saveState(): void {
|
||||
this.stateManager?.save(this.getUIState());
|
||||
}
|
||||
|
||||
/**
|
||||
* Load and restore saved UI state
|
||||
*/
|
||||
public loadState(): boolean {
|
||||
const state = this.stateManager?.load();
|
||||
if (state) {
|
||||
this.restoreUIState(state);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get access to the view registry
|
||||
*/
|
||||
public getViewRegistry(): ViewRegistry {
|
||||
return this.viewRegistry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get access to the router
|
||||
*/
|
||||
public getRouter(): AppRouter | null {
|
||||
return this.router;
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// NEW: Private helper methods
|
||||
// ==========================================
|
||||
|
||||
private applyConfig(config: interfaces.IAppConfig): void {
|
||||
// Register views
|
||||
if (config.views) {
|
||||
this.viewRegistry.clear();
|
||||
this.viewRegistry.registerAll(config.views);
|
||||
}
|
||||
|
||||
// Apply branding
|
||||
if (config.branding) {
|
||||
this.mainmenuLogoIcon = config.branding.logoIcon || '';
|
||||
this.mainmenuLogoText = config.branding.logoText || '';
|
||||
}
|
||||
|
||||
// Apply app bar config
|
||||
if (config.appBar) {
|
||||
this.appbarMenuItems = config.appBar.menuItems || [];
|
||||
this.appbarBreadcrumbs = config.appBar.breadcrumbs || '';
|
||||
this.appbarBreadcrumbSeparator = config.appBar.breadcrumbSeparator || ' > ';
|
||||
this.appbarShowWindowControls = config.appBar.showWindowControls ?? true;
|
||||
this.appbarShowSearch = config.appBar.showSearch ?? false;
|
||||
this.appbarUser = config.appBar.user;
|
||||
this.appbarProfileMenuItems = config.appBar.profileMenuItems || [];
|
||||
}
|
||||
|
||||
// Build main menu from view references
|
||||
if (config.mainMenu) {
|
||||
this.mainmenuGroups = this.buildMainMenuGroups(config);
|
||||
this.mainmenuBottomTabs = this.buildBottomTabs(config);
|
||||
}
|
||||
|
||||
// Initialize state manager
|
||||
if (config.statePersistence) {
|
||||
this.stateManager = new StateManager(config.statePersistence);
|
||||
}
|
||||
|
||||
// Initialize router
|
||||
if (config.routing && config.routing.mode !== 'none') {
|
||||
this.router = new AppRouter(config.routing, this.viewRegistry);
|
||||
this.router.onRouteChange((viewId) => {
|
||||
const view = this.viewRegistry.get(viewId);
|
||||
if (view) {
|
||||
this.loadView(view);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Bind event callbacks
|
||||
if (config.onViewChange) {
|
||||
this.addEventListener('view-change', ((e: CustomEvent) => {
|
||||
config.onViewChange!(e.detail.viewId, e.detail.view);
|
||||
}) as EventListener);
|
||||
}
|
||||
|
||||
if (config.onSearch) {
|
||||
this.addEventListener('appbar-search-click', () => {
|
||||
config.onSearch!();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private buildMainMenuGroups(config: interfaces.IAppConfig): interfaces.IMenuGroup[] {
|
||||
if (!config.mainMenu?.sections) return [];
|
||||
|
||||
return config.mainMenu.sections.map((section) => ({
|
||||
name: section.name,
|
||||
tabs: section.views
|
||||
.map((viewId) => {
|
||||
const view = this.viewRegistry.get(viewId);
|
||||
if (!view) {
|
||||
console.warn(`View "${viewId}" not found in registry`);
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
key: view.name,
|
||||
iconName: view.iconName,
|
||||
action: () => this.navigateToView(viewId),
|
||||
};
|
||||
})
|
||||
.filter(Boolean) as interfaces.ITab[],
|
||||
}));
|
||||
}
|
||||
|
||||
private buildBottomTabs(config: interfaces.IAppConfig): interfaces.ITab[] {
|
||||
if (!config.mainMenu?.bottomItems) return [];
|
||||
|
||||
return config.mainMenu.bottomItems
|
||||
.map((viewId) => {
|
||||
const view = this.viewRegistry.get(viewId);
|
||||
if (!view) {
|
||||
console.warn(`View "${viewId}" not found in registry`);
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
key: view.name,
|
||||
iconName: view.iconName,
|
||||
action: () => this.navigateToView(viewId),
|
||||
};
|
||||
})
|
||||
.filter(Boolean) as interfaces.ITab[];
|
||||
}
|
||||
|
||||
private loadView(view: interfaces.IViewDefinition): void {
|
||||
const previousView = this.currentView;
|
||||
this.currentView = view;
|
||||
|
||||
// Update secondary menu
|
||||
if (view.secondaryMenu) {
|
||||
this.secondarymenuGroups = view.secondaryMenu;
|
||||
this.secondarymenuHeading = view.name;
|
||||
}
|
||||
|
||||
// Update content tabs
|
||||
if (view.contentTabs) {
|
||||
this.maincontentTabs = view.contentTabs;
|
||||
}
|
||||
|
||||
// Render view content into the view container
|
||||
const viewContainer = this.maincontent?.shadowRoot?.querySelector('.view-container')
|
||||
|| this.shadowRoot?.querySelector('.view-container');
|
||||
if (viewContainer) {
|
||||
this.viewRegistry.renderView(view.id, viewContainer as HTMLElement);
|
||||
}
|
||||
|
||||
// Save state if configured
|
||||
this.stateManager?.update({ currentViewId: view.id });
|
||||
|
||||
// Dispatch event
|
||||
this.dispatchEvent(
|
||||
new CustomEvent('view-change', {
|
||||
detail: { viewId: view.id, view, previousView },
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user