583 lines
17 KiB
TypeScript
583 lines
17 KiB
TypeScript
import {
|
|
DeesElement,
|
|
type TemplateResult,
|
|
property,
|
|
customElement,
|
|
html,
|
|
css,
|
|
cssManager,
|
|
state,
|
|
} from '@design.estate/dees-element';
|
|
import * as interfaces from '../../interfaces/index.js';
|
|
import * as plugins from '../../00plugins.js';
|
|
import type { DeesAppuiBar } from '../dees-appui-appbar/index.js';
|
|
import type { DeesAppuiMainmenu } from '../dees-appui-mainmenu/dees-appui-mainmenu.js';
|
|
import type { DeesAppuiSecondarymenu } from '../dees-appui-secondarymenu/dees-appui-secondarymenu.js';
|
|
import type { DeesAppuiMaincontent } from '../dees-appui-maincontent/dees-appui-maincontent.js';
|
|
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';
|
|
import '../dees-appui-secondarymenu/dees-appui-secondarymenu.js';
|
|
import '../dees-appui-maincontent/dees-appui-maincontent.js';
|
|
import '../dees-appui-activitylog/dees-appui-activitylog.js';
|
|
|
|
@customElement('dees-appui-base')
|
|
export class DeesAppuiBase extends DeesElement {
|
|
public static demo = demoFunc;
|
|
|
|
// Properties for appbar
|
|
@property({ type: Array })
|
|
accessor appbarMenuItems: interfaces.IAppBarMenuItem[] = [];
|
|
|
|
@property({ type: String })
|
|
accessor appbarBreadcrumbs: string = '';
|
|
|
|
@property({ type: String })
|
|
accessor appbarBreadcrumbSeparator: string = ' > ';
|
|
|
|
@property({ type: Boolean })
|
|
accessor appbarShowWindowControls: boolean = true;
|
|
|
|
|
|
@property({ type: Object })
|
|
accessor appbarUser: {
|
|
name: string;
|
|
email?: string;
|
|
avatar?: string;
|
|
status?: 'online' | 'offline' | 'busy' | 'away';
|
|
} | undefined = undefined;
|
|
|
|
@property({ type: Array })
|
|
accessor appbarProfileMenuItems: (plugins.tsclass.website.IMenuItem & { shortcut?: string } | { divider: true })[] = [];
|
|
|
|
@property({ type: Boolean })
|
|
accessor appbarShowSearch: boolean = false;
|
|
|
|
// Properties for mainmenu
|
|
@property({ type: String })
|
|
accessor mainmenuLogoIcon: string = '';
|
|
|
|
@property({ type: String })
|
|
accessor mainmenuLogoText: string = '';
|
|
|
|
@property({ type: Array })
|
|
accessor mainmenuGroups: interfaces.IMenuGroup[] = [];
|
|
|
|
@property({ type: Array })
|
|
accessor mainmenuBottomTabs: interfaces.ITab[] = [];
|
|
|
|
@property({ type: Array })
|
|
accessor mainmenuTabs: interfaces.ITab[] = [];
|
|
|
|
@property({ type: Object })
|
|
accessor mainmenuSelectedTab: interfaces.ITab | undefined = undefined;
|
|
|
|
// Properties for secondarymenu
|
|
@property({ type: String })
|
|
accessor secondarymenuHeading: string = 'Menu';
|
|
|
|
@property({ type: Array })
|
|
accessor secondarymenuGroups: interfaces.ISecondaryMenuGroup[] = [];
|
|
|
|
@property({ type: Object })
|
|
accessor secondarymenuSelectedItem: interfaces.ISecondaryMenuItem | undefined = undefined;
|
|
|
|
/** Legacy support for flat options (backward compatibility) */
|
|
@property({ type: Array })
|
|
accessor secondarymenuOptions: (interfaces.ISelectionOption | { divider: true })[] = [];
|
|
|
|
// Collapse states
|
|
@property({ type: Boolean })
|
|
accessor mainmenuCollapsed: boolean = false;
|
|
|
|
@property({ type: Boolean })
|
|
accessor secondarymenuCollapsed: boolean = false;
|
|
|
|
// Properties for maincontent
|
|
@property({ type: Array })
|
|
accessor maincontentTabs: interfaces.ITab[] = [];
|
|
|
|
// References to child components
|
|
@state()
|
|
accessor appbar: DeesAppuiBar | undefined = undefined;
|
|
|
|
@state()
|
|
accessor mainmenu: DeesAppuiMainmenu | undefined = undefined;
|
|
|
|
@state()
|
|
accessor secondarymenu: DeesAppuiSecondarymenu | undefined = undefined;
|
|
|
|
@state()
|
|
accessor maincontent: DeesAppuiMaincontent | undefined = undefined;
|
|
|
|
@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`
|
|
:host {
|
|
position: absolute;
|
|
height: 100%;
|
|
width: 100%;
|
|
background: ${cssManager.bdTheme('#f0f0f0', '#1a1a1a')};
|
|
}
|
|
.maingrid {
|
|
position: absolute;
|
|
top: 40px;
|
|
height: calc(100% - 40px);
|
|
width: 100%;
|
|
display: grid;
|
|
grid-template-columns: auto auto 1fr 240px;
|
|
grid-template-rows: 1fr;
|
|
}
|
|
|
|
/* Z-index layering for proper stacking (position: relative required for z-index to work) */
|
|
.maingrid > dees-appui-mainmenu {
|
|
position: relative;
|
|
z-index: 3;
|
|
}
|
|
|
|
.maingrid > dees-appui-secondarymenu {
|
|
position: relative;
|
|
z-index: 2;
|
|
}
|
|
|
|
.maingrid > dees-appui-maincontent {
|
|
position: relative;
|
|
z-index: 1;
|
|
}
|
|
|
|
.maingrid > dees-appui-activitylog {
|
|
position: relative;
|
|
z-index: 1;
|
|
}
|
|
|
|
/* View container for dynamically loaded views */
|
|
.view-container {
|
|
display: contents;
|
|
}
|
|
|
|
.view-container:empty {
|
|
display: none;
|
|
}
|
|
`,
|
|
];
|
|
|
|
// INSTANCE
|
|
public render(): TemplateResult {
|
|
return html`
|
|
<style></style>
|
|
<dees-appui-appbar
|
|
.menuItems=${this.appbarMenuItems}
|
|
.breadcrumbs=${this.appbarBreadcrumbs}
|
|
.breadcrumbSeparator=${this.appbarBreadcrumbSeparator}
|
|
.showWindowControls=${this.appbarShowWindowControls}
|
|
.user=${this.appbarUser}
|
|
.profileMenuItems=${this.appbarProfileMenuItems}
|
|
.showSearch=${this.appbarShowSearch}
|
|
@menu-select=${(e: CustomEvent) => this.handleAppbarMenuSelect(e)}
|
|
@breadcrumb-navigate=${(e: CustomEvent) => this.handleAppbarBreadcrumbNavigate(e)}
|
|
@search-click=${() => this.handleAppbarSearchClick()}
|
|
@user-menu-open=${() => this.handleAppbarUserMenuOpen()}
|
|
@profile-menu-select=${(e: CustomEvent) => this.handleAppbarProfileMenuSelect(e)}
|
|
></dees-appui-appbar>
|
|
<div class="maingrid">
|
|
<dees-appui-mainmenu
|
|
.logoIcon=${this.mainmenuLogoIcon}
|
|
.logoText=${this.mainmenuLogoText}
|
|
.menuGroups=${this.mainmenuGroups}
|
|
.bottomTabs=${this.mainmenuBottomTabs}
|
|
.tabs=${this.mainmenuTabs}
|
|
.selectedTab=${this.mainmenuSelectedTab}
|
|
.collapsed=${this.mainmenuCollapsed}
|
|
@tab-select=${(e: CustomEvent) => this.handleMainmenuTabSelect(e)}
|
|
@collapse-change=${(e: CustomEvent) => this.handleMainmenuCollapseChange(e)}
|
|
></dees-appui-mainmenu>
|
|
<dees-appui-secondarymenu
|
|
.heading=${this.secondarymenuHeading}
|
|
.groups=${this.secondarymenuGroups}
|
|
.selectionOptions=${this.secondarymenuOptions}
|
|
.selectedItem=${this.secondarymenuSelectedItem}
|
|
.collapsed=${this.secondarymenuCollapsed}
|
|
@item-select=${(e: CustomEvent) => this.handleSecondarymenuItemSelect(e)}
|
|
@collapse-change=${(e: CustomEvent) => this.handleSecondarymenuCollapseChange(e)}
|
|
></dees-appui-secondarymenu>
|
|
<dees-appui-maincontent
|
|
.tabs=${this.maincontentTabs}
|
|
>
|
|
<div class="view-container"></div>
|
|
<slot name="maincontent"></slot>
|
|
</dees-appui-maincontent>
|
|
<dees-appui-activitylog></dees-appui-activitylog>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
async firstUpdated() {
|
|
// Get references to child components
|
|
this.appbar = this.shadowRoot.querySelector('dees-appui-appbar');
|
|
this.mainmenu = this.shadowRoot.querySelector('dees-appui-mainmenu');
|
|
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
|
|
private handleAppbarMenuSelect(e: CustomEvent) {
|
|
this.dispatchEvent(new CustomEvent('appbar-menu-select', {
|
|
detail: e.detail,
|
|
bubbles: true,
|
|
composed: true
|
|
}));
|
|
}
|
|
|
|
private handleAppbarBreadcrumbNavigate(e: CustomEvent) {
|
|
this.dispatchEvent(new CustomEvent('appbar-breadcrumb-navigate', {
|
|
detail: e.detail,
|
|
bubbles: true,
|
|
composed: true
|
|
}));
|
|
}
|
|
|
|
private handleAppbarSearchClick() {
|
|
this.dispatchEvent(new CustomEvent('appbar-search-click', {
|
|
bubbles: true,
|
|
composed: true
|
|
}));
|
|
}
|
|
|
|
private handleAppbarUserMenuOpen() {
|
|
this.dispatchEvent(new CustomEvent('appbar-user-menu-open', {
|
|
bubbles: true,
|
|
composed: true
|
|
}));
|
|
}
|
|
|
|
private handleAppbarProfileMenuSelect(e: CustomEvent) {
|
|
this.dispatchEvent(new CustomEvent('appbar-profile-menu-select', {
|
|
detail: e.detail,
|
|
bubbles: true,
|
|
composed: true
|
|
}));
|
|
}
|
|
|
|
// Event handlers for mainmenu
|
|
private handleMainmenuTabSelect(e: CustomEvent) {
|
|
this.mainmenuSelectedTab = e.detail.tab;
|
|
// Update secondary menu heading based on main menu selection
|
|
this.secondarymenuHeading = e.detail.tab.key;
|
|
this.dispatchEvent(new CustomEvent('mainmenu-tab-select', {
|
|
detail: e.detail,
|
|
bubbles: true,
|
|
composed: true
|
|
}));
|
|
}
|
|
|
|
// Event handlers for secondarymenu
|
|
private handleSecondarymenuItemSelect(e: CustomEvent) {
|
|
this.secondarymenuSelectedItem = e.detail.item;
|
|
this.dispatchEvent(new CustomEvent('secondarymenu-item-select', {
|
|
detail: e.detail,
|
|
bubbles: true,
|
|
composed: true
|
|
}));
|
|
}
|
|
|
|
// Event handlers for collapse state changes
|
|
private handleMainmenuCollapseChange(e: CustomEvent) {
|
|
this.mainmenuCollapsed = e.detail.collapsed;
|
|
this.dispatchEvent(new CustomEvent('mainmenu-collapse-change', {
|
|
detail: e.detail,
|
|
bubbles: true,
|
|
composed: true
|
|
}));
|
|
}
|
|
|
|
private handleSecondarymenuCollapseChange(e: CustomEvent) {
|
|
this.secondarymenuCollapsed = e.detail.collapsed;
|
|
this.dispatchEvent(new CustomEvent('secondarymenu-collapse-change', {
|
|
detail: e.detail,
|
|
bubbles: true,
|
|
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,
|
|
})
|
|
);
|
|
}
|
|
}
|