2024-01-24 12:18:37 +01:00
|
|
|
import {
|
|
|
|
|
DeesElement,
|
|
|
|
|
type TemplateResult,
|
|
|
|
|
property,
|
|
|
|
|
customElement,
|
|
|
|
|
html,
|
|
|
|
|
css,
|
|
|
|
|
cssManager,
|
2025-06-17 08:41:36 +00:00
|
|
|
state,
|
2024-01-24 12:18:37 +01:00
|
|
|
} from '@design.estate/dees-element';
|
2025-12-19 13:54:37 +00:00
|
|
|
import * as domtools from '@design.estate/dees-domtools';
|
2025-12-08 12:04:01 +00:00
|
|
|
import * as interfaces from '../../interfaces/index.js';
|
Refactor import paths for consistency and clarity across multiple components
- Updated import paths in dees-panel, dees-pdf, dees-progressbar, dees-searchbar, dees-shopping-productcard, dees-simple-appdash, dees-speechbubble, dees-statsgrid, dees-table, dees-toast, dees-updater, and dees-windowlayer to use consistent directory structure.
- Created index.ts files for various components to streamline imports and improve modularity.
- Ensured all imports point to the correct subdirectory structure, enhancing maintainability and readability of the codebase.
2025-12-05 10:19:37 +00:00
|
|
|
import type { DeesAppuiBar } from '../dees-appui-appbar/index.js';
|
|
|
|
|
import type { DeesAppuiMainmenu } from '../dees-appui-mainmenu/dees-appui-mainmenu.js';
|
2025-12-08 14:35:06 +00:00
|
|
|
import type { DeesAppuiSecondarymenu } from '../dees-appui-secondarymenu/dees-appui-secondarymenu.js';
|
Refactor import paths for consistency and clarity across multiple components
- Updated import paths in dees-panel, dees-pdf, dees-progressbar, dees-searchbar, dees-shopping-productcard, dees-simple-appdash, dees-speechbubble, dees-statsgrid, dees-table, dees-toast, dees-updater, and dees-windowlayer to use consistent directory structure.
- Created index.ts files for various components to streamline imports and improve modularity.
- Ensured all imports point to the correct subdirectory structure, enhancing maintainability and readability of the codebase.
2025-12-05 10:19:37 +00:00
|
|
|
import type { DeesAppuiMaincontent } from '../dees-appui-maincontent/dees-appui-maincontent.js';
|
|
|
|
|
import type { DeesAppuiActivitylog } from '../dees-appui-activitylog/dees-appui-activitylog.js';
|
2025-06-17 08:41:36 +00:00
|
|
|
import { demoFunc } from './dees-appui-base.demo.js';
|
|
|
|
|
|
2025-12-19 13:54:37 +00:00
|
|
|
// View registry for managing views
|
2025-12-09 08:26:24 +00:00
|
|
|
import { ViewRegistry } from './view.registry.js';
|
|
|
|
|
|
2025-06-17 08:41:36 +00:00
|
|
|
// Import child components
|
2025-12-06 13:54:17 +00:00
|
|
|
import '../dees-appui-appbar/index.js';
|
|
|
|
|
import '../dees-appui-mainmenu/dees-appui-mainmenu.js';
|
2025-12-08 14:35:06 +00:00
|
|
|
import '../dees-appui-secondarymenu/dees-appui-secondarymenu.js';
|
2025-12-06 13:54:17 +00:00
|
|
|
import '../dees-appui-maincontent/dees-appui-maincontent.js';
|
|
|
|
|
import '../dees-appui-activitylog/dees-appui-activitylog.js';
|
2024-01-24 12:18:37 +01:00
|
|
|
|
2025-12-19 13:54:37 +00:00
|
|
|
declare global {
|
|
|
|
|
interface HTMLElementTagNameMap {
|
|
|
|
|
'dees-appui-base': DeesAppuiBase;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-01-24 12:18:37 +01:00
|
|
|
@customElement('dees-appui-base')
|
|
|
|
|
export class DeesAppuiBase extends DeesElement {
|
2025-06-17 08:41:36 +00:00
|
|
|
public static demo = demoFunc;
|
|
|
|
|
|
2025-12-19 13:54:37 +00:00
|
|
|
// ==========================================
|
|
|
|
|
// REACTIVE OBSERVABLES (RxJS Subjects)
|
|
|
|
|
// ==========================================
|
|
|
|
|
|
|
|
|
|
/** Observable stream of view lifecycle events */
|
|
|
|
|
public viewLifecycle$ = new domtools.plugins.smartrx.rxjs.Subject<interfaces.IViewLifecycleEvent>();
|
|
|
|
|
|
|
|
|
|
/** Observable stream of view change events */
|
|
|
|
|
public viewChanged$ = new domtools.plugins.smartrx.rxjs.Subject<interfaces.IViewChangeEvent>();
|
|
|
|
|
|
|
|
|
|
// ==========================================
|
|
|
|
|
// INTERNAL PROPERTIES (Properties for child components)
|
|
|
|
|
// ==========================================
|
|
|
|
|
|
2025-06-17 08:41:36 +00:00
|
|
|
// Properties for appbar
|
|
|
|
|
@property({ type: Array })
|
2025-11-17 13:27:11 +00:00
|
|
|
accessor appbarMenuItems: interfaces.IAppBarMenuItem[] = [];
|
2025-06-17 08:41:36 +00:00
|
|
|
|
|
|
|
|
@property({ type: String })
|
2025-11-17 13:27:11 +00:00
|
|
|
accessor appbarBreadcrumbs: string = '';
|
2025-06-17 08:41:36 +00:00
|
|
|
|
|
|
|
|
@property({ type: String })
|
2025-11-17 13:27:11 +00:00
|
|
|
accessor appbarBreadcrumbSeparator: string = ' > ';
|
2025-06-17 08:41:36 +00:00
|
|
|
|
|
|
|
|
@property({ type: Boolean })
|
2025-11-17 13:27:11 +00:00
|
|
|
accessor appbarShowWindowControls: boolean = true;
|
2025-06-17 08:41:36 +00:00
|
|
|
|
|
|
|
|
@property({ type: Object })
|
2025-12-19 13:54:37 +00:00
|
|
|
accessor appbarUser: interfaces.IAppUser | undefined = undefined;
|
2025-06-17 08:41:36 +00:00
|
|
|
|
2025-06-17 09:55:28 +00:00
|
|
|
@property({ type: Array })
|
2025-12-19 13:54:37 +00:00
|
|
|
accessor appbarProfileMenuItems: interfaces.IAppBarMenuItem[] = [];
|
2025-06-17 09:55:28 +00:00
|
|
|
|
2025-06-17 08:41:36 +00:00
|
|
|
@property({ type: Boolean })
|
2025-11-17 13:27:11 +00:00
|
|
|
accessor appbarShowSearch: boolean = false;
|
2025-06-17 08:41:36 +00:00
|
|
|
|
|
|
|
|
// Properties for mainmenu
|
2025-12-08 12:04:01 +00:00
|
|
|
@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[] = [];
|
|
|
|
|
|
2025-06-17 08:41:36 +00:00
|
|
|
@property({ type: Array })
|
2025-11-17 13:27:11 +00:00
|
|
|
accessor mainmenuTabs: interfaces.ITab[] = [];
|
2025-06-17 08:41:36 +00:00
|
|
|
|
|
|
|
|
@property({ type: Object })
|
2025-11-17 13:27:11 +00:00
|
|
|
accessor mainmenuSelectedTab: interfaces.ITab | undefined = undefined;
|
2025-06-17 08:41:36 +00:00
|
|
|
|
2025-12-08 14:35:06 +00:00
|
|
|
// Properties for secondarymenu
|
|
|
|
|
@property({ type: String })
|
2025-12-19 13:54:37 +00:00
|
|
|
accessor secondarymenuHeading: string = '';
|
2025-12-08 14:35:06 +00:00
|
|
|
|
2025-06-17 08:41:36 +00:00
|
|
|
@property({ type: Array })
|
2025-12-08 14:35:06 +00:00
|
|
|
accessor secondarymenuGroups: interfaces.ISecondaryMenuGroup[] = [];
|
2025-06-17 08:41:36 +00:00
|
|
|
|
|
|
|
|
@property({ type: Object })
|
2025-12-08 14:35:06 +00:00
|
|
|
accessor secondarymenuSelectedItem: interfaces.ISecondaryMenuItem | undefined = undefined;
|
|
|
|
|
|
2025-12-08 15:40:12 +00:00
|
|
|
// Collapse states
|
|
|
|
|
@property({ type: Boolean })
|
|
|
|
|
accessor mainmenuCollapsed: boolean = false;
|
|
|
|
|
|
|
|
|
|
@property({ type: Boolean })
|
|
|
|
|
accessor secondarymenuCollapsed: boolean = false;
|
|
|
|
|
|
2025-06-17 08:41:36 +00:00
|
|
|
// Properties for maincontent
|
|
|
|
|
@property({ type: Array })
|
2025-11-17 13:27:11 +00:00
|
|
|
accessor maincontentTabs: interfaces.ITab[] = [];
|
2025-06-17 08:41:36 +00:00
|
|
|
|
2025-12-19 13:54:37 +00:00
|
|
|
@property({ type: Object })
|
|
|
|
|
accessor maincontentSelectedTab: interfaces.ITab | undefined = undefined;
|
|
|
|
|
|
2025-06-17 08:41:36 +00:00
|
|
|
// References to child components
|
|
|
|
|
@state()
|
2025-11-17 13:27:11 +00:00
|
|
|
accessor appbar: DeesAppuiBar | undefined = undefined;
|
2025-06-17 08:41:36 +00:00
|
|
|
|
|
|
|
|
@state()
|
2025-11-17 13:27:11 +00:00
|
|
|
accessor mainmenu: DeesAppuiMainmenu | undefined = undefined;
|
2025-06-17 08:41:36 +00:00
|
|
|
|
|
|
|
|
@state()
|
2025-12-08 14:35:06 +00:00
|
|
|
accessor secondarymenu: DeesAppuiSecondarymenu | undefined = undefined;
|
2025-06-17 08:41:36 +00:00
|
|
|
|
|
|
|
|
@state()
|
2025-11-17 13:27:11 +00:00
|
|
|
accessor maincontent: DeesAppuiMaincontent | undefined = undefined;
|
2025-06-17 08:41:36 +00:00
|
|
|
|
|
|
|
|
@state()
|
2025-12-19 13:54:37 +00:00
|
|
|
accessor activitylogElement: DeesAppuiActivitylog | undefined = undefined;
|
2024-01-24 12:18:37 +01:00
|
|
|
|
2025-12-19 13:54:37 +00:00
|
|
|
// Current view state
|
2025-12-09 08:26:24 +00:00
|
|
|
@state()
|
|
|
|
|
accessor currentView: interfaces.IViewDefinition | undefined = undefined;
|
|
|
|
|
|
2025-12-19 13:54:37 +00:00
|
|
|
// Internal services
|
2025-12-09 08:26:24 +00:00
|
|
|
private viewRegistry: ViewRegistry = new ViewRegistry();
|
2025-12-19 13:54:37 +00:00
|
|
|
private routerCleanup: (() => void) | null = null;
|
|
|
|
|
private searchCallback: ((query: string) => void) | null = null;
|
2025-12-09 08:26:24 +00:00
|
|
|
|
2024-01-24 12:18:37 +01:00
|
|
|
public static styles = [
|
|
|
|
|
cssManager.defaultStyles,
|
|
|
|
|
css`
|
|
|
|
|
:host {
|
|
|
|
|
position: absolute;
|
|
|
|
|
height: 100%;
|
|
|
|
|
width: 100%;
|
2025-06-17 08:58:47 +00:00
|
|
|
background: ${cssManager.bdTheme('#f0f0f0', '#1a1a1a')};
|
2024-01-24 12:18:37 +01:00
|
|
|
}
|
|
|
|
|
.maingrid {
|
|
|
|
|
position: absolute;
|
|
|
|
|
top: 40px;
|
|
|
|
|
height: calc(100% - 40px);
|
|
|
|
|
width: 100%;
|
|
|
|
|
display: grid;
|
2025-12-08 15:40:12 +00:00
|
|
|
grid-template-columns: auto auto 1fr 240px;
|
2025-12-08 12:04:01 +00:00
|
|
|
grid-template-rows: 1fr;
|
2024-01-24 12:18:37 +01:00
|
|
|
}
|
2025-12-08 15:40:12 +00:00
|
|
|
|
2025-12-19 13:54:37 +00:00
|
|
|
/* Z-index layering for proper stacking */
|
2025-12-08 15:40:12 +00:00
|
|
|
.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;
|
|
|
|
|
}
|
2025-12-09 08:26:24 +00:00
|
|
|
|
|
|
|
|
/* View container for dynamically loaded views */
|
|
|
|
|
.view-container {
|
|
|
|
|
display: contents;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.view-container:empty {
|
|
|
|
|
display: none;
|
|
|
|
|
}
|
2024-01-24 12:18:37 +01:00
|
|
|
`,
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
public render(): TemplateResult {
|
|
|
|
|
return html`
|
2025-06-17 08:41:36 +00:00
|
|
|
<dees-appui-appbar
|
|
|
|
|
.menuItems=${this.appbarMenuItems}
|
|
|
|
|
.breadcrumbs=${this.appbarBreadcrumbs}
|
|
|
|
|
.breadcrumbSeparator=${this.appbarBreadcrumbSeparator}
|
|
|
|
|
.showWindowControls=${this.appbarShowWindowControls}
|
|
|
|
|
.user=${this.appbarUser}
|
2025-06-17 09:55:28 +00:00
|
|
|
.profileMenuItems=${this.appbarProfileMenuItems}
|
2025-06-17 08:41:36 +00:00
|
|
|
.showSearch=${this.appbarShowSearch}
|
|
|
|
|
@menu-select=${(e: CustomEvent) => this.handleAppbarMenuSelect(e)}
|
|
|
|
|
@breadcrumb-navigate=${(e: CustomEvent) => this.handleAppbarBreadcrumbNavigate(e)}
|
|
|
|
|
@search-click=${() => this.handleAppbarSearchClick()}
|
2025-12-19 13:54:37 +00:00
|
|
|
@search-query=${(e: CustomEvent) => this.handleAppbarSearchQuery(e)}
|
2025-06-17 08:41:36 +00:00
|
|
|
@user-menu-open=${() => this.handleAppbarUserMenuOpen()}
|
2025-06-17 09:55:28 +00:00
|
|
|
@profile-menu-select=${(e: CustomEvent) => this.handleAppbarProfileMenuSelect(e)}
|
2025-06-17 08:41:36 +00:00
|
|
|
></dees-appui-appbar>
|
2024-01-24 12:18:37 +01:00
|
|
|
<div class="maingrid">
|
2025-06-17 08:41:36 +00:00
|
|
|
<dees-appui-mainmenu
|
2025-12-08 12:04:01 +00:00
|
|
|
.logoIcon=${this.mainmenuLogoIcon}
|
|
|
|
|
.logoText=${this.mainmenuLogoText}
|
|
|
|
|
.menuGroups=${this.mainmenuGroups}
|
|
|
|
|
.bottomTabs=${this.mainmenuBottomTabs}
|
2025-06-17 08:41:36 +00:00
|
|
|
.tabs=${this.mainmenuTabs}
|
|
|
|
|
.selectedTab=${this.mainmenuSelectedTab}
|
2025-12-08 15:40:12 +00:00
|
|
|
.collapsed=${this.mainmenuCollapsed}
|
2025-06-17 08:41:36 +00:00
|
|
|
@tab-select=${(e: CustomEvent) => this.handleMainmenuTabSelect(e)}
|
2025-12-08 15:40:12 +00:00
|
|
|
@collapse-change=${(e: CustomEvent) => this.handleMainmenuCollapseChange(e)}
|
2025-06-17 08:41:36 +00:00
|
|
|
></dees-appui-mainmenu>
|
2025-12-08 14:35:06 +00:00
|
|
|
<dees-appui-secondarymenu
|
|
|
|
|
.heading=${this.secondarymenuHeading}
|
|
|
|
|
.groups=${this.secondarymenuGroups}
|
|
|
|
|
.selectedItem=${this.secondarymenuSelectedItem}
|
2025-12-08 15:40:12 +00:00
|
|
|
.collapsed=${this.secondarymenuCollapsed}
|
2025-12-08 14:35:06 +00:00
|
|
|
@item-select=${(e: CustomEvent) => this.handleSecondarymenuItemSelect(e)}
|
2025-12-08 15:40:12 +00:00
|
|
|
@collapse-change=${(e: CustomEvent) => this.handleSecondarymenuCollapseChange(e)}
|
2025-12-08 14:35:06 +00:00
|
|
|
></dees-appui-secondarymenu>
|
2025-06-17 08:41:36 +00:00
|
|
|
<dees-appui-maincontent
|
|
|
|
|
.tabs=${this.maincontentTabs}
|
2025-12-19 13:54:37 +00:00
|
|
|
.selectedTab=${this.maincontentSelectedTab}
|
|
|
|
|
@tab-select=${(e: CustomEvent) => this.handleContentTabSelect(e)}
|
2025-06-17 08:41:36 +00:00
|
|
|
>
|
2025-12-09 08:26:24 +00:00
|
|
|
<div class="view-container"></div>
|
2025-06-17 08:41:36 +00:00
|
|
|
<slot name="maincontent"></slot>
|
|
|
|
|
</dees-appui-maincontent>
|
2024-01-24 12:18:37 +01:00
|
|
|
<dees-appui-activitylog></dees-appui-activitylog>
|
|
|
|
|
</div>
|
|
|
|
|
`;
|
|
|
|
|
}
|
2025-06-17 08:41:36 +00:00
|
|
|
|
|
|
|
|
async firstUpdated() {
|
|
|
|
|
// Get references to child components
|
2025-12-19 13:54:37 +00:00
|
|
|
this.appbar = this.shadowRoot!.querySelector('dees-appui-appbar') as DeesAppuiBar;
|
|
|
|
|
this.mainmenu = this.shadowRoot!.querySelector('dees-appui-mainmenu') as DeesAppuiMainmenu;
|
|
|
|
|
this.secondarymenu = this.shadowRoot!.querySelector('dees-appui-secondarymenu') as DeesAppuiSecondarymenu;
|
|
|
|
|
this.maincontent = this.shadowRoot!.querySelector('dees-appui-maincontent') as DeesAppuiMaincontent;
|
|
|
|
|
this.activitylogElement = this.shadowRoot!.querySelector('dees-appui-activitylog') as DeesAppuiActivitylog;
|
|
|
|
|
|
|
|
|
|
// Set appui reference in view registry for lifecycle context
|
|
|
|
|
this.viewRegistry.setAppuiRef(this as unknown as interfaces.TDeesAppuiBase);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async disconnectedCallback() {
|
|
|
|
|
await super.disconnectedCallback();
|
|
|
|
|
// Clean up router listener
|
|
|
|
|
if (this.routerCleanup) {
|
|
|
|
|
this.routerCleanup();
|
|
|
|
|
this.routerCleanup = null;
|
|
|
|
|
}
|
|
|
|
|
// Complete subjects
|
|
|
|
|
this.viewLifecycle$.complete();
|
|
|
|
|
this.viewChanged$.complete();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ==========================================
|
|
|
|
|
// PROGRAMMATIC API: APP BAR
|
|
|
|
|
// ==========================================
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Set the app bar menu items (File, Edit, View, etc.)
|
|
|
|
|
*/
|
|
|
|
|
public setAppBarMenus(menus: interfaces.IAppBarMenuItem[]): void {
|
|
|
|
|
this.appbarMenuItems = [...menus];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Update a single app bar menu by name
|
|
|
|
|
*/
|
|
|
|
|
public updateAppBarMenu(name: string, update: Partial<interfaces.IAppBarMenuItem>): void {
|
|
|
|
|
this.appbarMenuItems = this.appbarMenuItems.map(menu => {
|
|
|
|
|
// Check if it's not a divider and has a name property
|
|
|
|
|
if ('name' in menu && menu.name === name) {
|
|
|
|
|
return { ...menu, ...update };
|
2025-12-09 08:26:24 +00:00
|
|
|
}
|
2025-12-19 13:54:37 +00:00
|
|
|
return menu;
|
|
|
|
|
});
|
|
|
|
|
}
|
2025-12-09 08:26:24 +00:00
|
|
|
|
2025-12-19 13:54:37 +00:00
|
|
|
/**
|
|
|
|
|
* Set the breadcrumbs (string or array)
|
|
|
|
|
*/
|
|
|
|
|
public setBreadcrumbs(breadcrumbs: string | string[]): void {
|
|
|
|
|
if (Array.isArray(breadcrumbs)) {
|
|
|
|
|
this.appbarBreadcrumbs = breadcrumbs.join(this.appbarBreadcrumbSeparator);
|
|
|
|
|
} else {
|
|
|
|
|
this.appbarBreadcrumbs = breadcrumbs;
|
2025-12-09 08:26:24 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-19 13:54:37 +00:00
|
|
|
/**
|
|
|
|
|
* Set the current user
|
|
|
|
|
*/
|
|
|
|
|
public setUser(user: interfaces.IAppUser | undefined): void {
|
|
|
|
|
this.appbarUser = user;
|
2025-06-17 08:41:36 +00:00
|
|
|
}
|
|
|
|
|
|
2025-12-19 13:54:37 +00:00
|
|
|
/**
|
|
|
|
|
* Set the profile dropdown menu items
|
|
|
|
|
*/
|
|
|
|
|
public setProfileMenuItems(items: interfaces.IAppBarMenuItem[]): void {
|
|
|
|
|
this.appbarProfileMenuItems = [...items];
|
2025-06-17 08:41:36 +00:00
|
|
|
}
|
|
|
|
|
|
2025-12-19 13:54:37 +00:00
|
|
|
/**
|
|
|
|
|
* Set search bar visibility
|
|
|
|
|
*/
|
|
|
|
|
public setSearchVisible(visible: boolean): void {
|
|
|
|
|
this.appbarShowSearch = visible;
|
2025-06-17 08:41:36 +00:00
|
|
|
}
|
|
|
|
|
|
2025-12-19 13:54:37 +00:00
|
|
|
/**
|
|
|
|
|
* Set window controls visibility
|
|
|
|
|
*/
|
|
|
|
|
public setWindowControlsVisible(visible: boolean): void {
|
|
|
|
|
this.appbarShowWindowControls = visible;
|
2025-06-17 08:41:36 +00:00
|
|
|
}
|
|
|
|
|
|
2025-12-19 13:54:37 +00:00
|
|
|
/**
|
|
|
|
|
* Register a search callback
|
|
|
|
|
*/
|
|
|
|
|
public onSearch(callback: (query: string) => void): void {
|
|
|
|
|
this.searchCallback = callback;
|
2025-06-17 08:41:36 +00:00
|
|
|
}
|
|
|
|
|
|
2025-12-19 13:54:37 +00:00
|
|
|
// ==========================================
|
|
|
|
|
// PROGRAMMATIC API: MAIN MENU
|
|
|
|
|
// ==========================================
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Set the entire main menu configuration
|
|
|
|
|
*/
|
|
|
|
|
public setMainMenu(config: interfaces.IMainMenuConfig): void {
|
|
|
|
|
if (config.logoIcon !== undefined) {
|
|
|
|
|
this.mainmenuLogoIcon = config.logoIcon;
|
|
|
|
|
}
|
|
|
|
|
if (config.logoText !== undefined) {
|
|
|
|
|
this.mainmenuLogoText = config.logoText;
|
|
|
|
|
}
|
|
|
|
|
if (config.groups !== undefined) {
|
|
|
|
|
this.mainmenuGroups = [...config.groups];
|
|
|
|
|
}
|
|
|
|
|
if (config.bottomTabs !== undefined) {
|
|
|
|
|
this.mainmenuBottomTabs = [...config.bottomTabs];
|
|
|
|
|
}
|
2025-06-17 09:55:28 +00:00
|
|
|
}
|
|
|
|
|
|
2025-12-19 13:54:37 +00:00
|
|
|
/**
|
|
|
|
|
* Update a specific menu group by name
|
|
|
|
|
*/
|
|
|
|
|
public updateMainMenuGroup(groupName: string, update: Partial<interfaces.IMenuGroup>): void {
|
|
|
|
|
this.mainmenuGroups = this.mainmenuGroups.map(group =>
|
|
|
|
|
group.name === groupName ? { ...group, ...update } : group
|
|
|
|
|
);
|
2025-06-17 08:41:36 +00:00
|
|
|
}
|
|
|
|
|
|
2025-12-19 13:54:37 +00:00
|
|
|
/**
|
|
|
|
|
* Add a menu item to a specific group
|
|
|
|
|
*/
|
|
|
|
|
public addMainMenuItem(groupName: string, tab: interfaces.ITab): void {
|
|
|
|
|
this.mainmenuGroups = this.mainmenuGroups.map(group => {
|
|
|
|
|
if (group.name === groupName) {
|
|
|
|
|
return {
|
|
|
|
|
...group,
|
|
|
|
|
tabs: [...(group.tabs || []), tab],
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
return group;
|
|
|
|
|
});
|
2025-06-17 08:41:36 +00:00
|
|
|
}
|
2025-12-08 15:40:12 +00:00
|
|
|
|
2025-12-19 13:54:37 +00:00
|
|
|
/**
|
|
|
|
|
* Remove a menu item from a group by key
|
|
|
|
|
*/
|
|
|
|
|
public removeMainMenuItem(groupName: string, tabKey: string): void {
|
|
|
|
|
this.mainmenuGroups = this.mainmenuGroups.map(group => {
|
|
|
|
|
if (group.name === groupName) {
|
|
|
|
|
return {
|
|
|
|
|
...group,
|
|
|
|
|
tabs: (group.tabs || []).filter(t => t.key !== tabKey),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
return group;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Set the selected main menu item by key
|
|
|
|
|
*/
|
|
|
|
|
public setMainMenuSelection(tabKey: string): void {
|
|
|
|
|
for (const group of this.mainmenuGroups) {
|
|
|
|
|
const tab = group.tabs?.find(t => t.key === tabKey);
|
|
|
|
|
if (tab) {
|
|
|
|
|
this.mainmenuSelectedTab = tab;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// Check bottom tabs
|
|
|
|
|
const bottomTab = this.mainmenuBottomTabs.find(t => t.key === tabKey);
|
|
|
|
|
if (bottomTab) {
|
|
|
|
|
this.mainmenuSelectedTab = bottomTab;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Set main menu collapsed state
|
|
|
|
|
*/
|
|
|
|
|
public setMainMenuCollapsed(collapsed: boolean): void {
|
|
|
|
|
this.mainmenuCollapsed = collapsed;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Set a badge on a main menu item
|
|
|
|
|
*/
|
|
|
|
|
public setMainMenuBadge(tabKey: string, badge: string | number): void {
|
|
|
|
|
this.mainmenuGroups = this.mainmenuGroups.map(group => ({
|
|
|
|
|
...group,
|
|
|
|
|
tabs: (group.tabs || []).map(tab =>
|
|
|
|
|
tab.key === tabKey ? { ...tab, badge } : tab
|
|
|
|
|
),
|
2025-12-08 15:40:12 +00:00
|
|
|
}));
|
2025-12-19 13:54:37 +00:00
|
|
|
// Also check bottom tabs
|
|
|
|
|
this.mainmenuBottomTabs = this.mainmenuBottomTabs.map(tab =>
|
|
|
|
|
tab.key === tabKey ? { ...tab, badge } : tab
|
|
|
|
|
);
|
2025-12-08 15:40:12 +00:00
|
|
|
}
|
|
|
|
|
|
2025-12-19 13:54:37 +00:00
|
|
|
/**
|
|
|
|
|
* Clear a badge from a main menu item
|
|
|
|
|
*/
|
|
|
|
|
public clearMainMenuBadge(tabKey: string): void {
|
|
|
|
|
this.mainmenuGroups = this.mainmenuGroups.map(group => ({
|
|
|
|
|
...group,
|
|
|
|
|
tabs: (group.tabs || []).map(tab => {
|
|
|
|
|
if (tab.key === tabKey) {
|
|
|
|
|
const { badge, ...rest } = tab;
|
|
|
|
|
return rest;
|
|
|
|
|
}
|
|
|
|
|
return tab;
|
|
|
|
|
}),
|
2025-12-08 15:40:12 +00:00
|
|
|
}));
|
2025-12-19 13:54:37 +00:00
|
|
|
// Also check bottom tabs
|
|
|
|
|
this.mainmenuBottomTabs = this.mainmenuBottomTabs.map(tab => {
|
|
|
|
|
if (tab.key === tabKey) {
|
|
|
|
|
const { badge, ...rest } = tab;
|
|
|
|
|
return rest;
|
|
|
|
|
}
|
|
|
|
|
return tab;
|
|
|
|
|
});
|
2025-12-08 15:40:12 +00:00
|
|
|
}
|
2025-12-09 08:26:24 +00:00
|
|
|
|
|
|
|
|
// ==========================================
|
2025-12-19 13:54:37 +00:00
|
|
|
// PROGRAMMATIC API: SECONDARY MENU
|
2025-12-09 08:26:24 +00:00
|
|
|
// ==========================================
|
|
|
|
|
|
|
|
|
|
/**
|
2025-12-19 13:54:37 +00:00
|
|
|
* Set the secondary menu configuration
|
2025-12-09 08:26:24 +00:00
|
|
|
*/
|
2025-12-19 13:54:37 +00:00
|
|
|
public setSecondaryMenu(config: { heading?: string; groups: interfaces.ISecondaryMenuGroup[] }): void {
|
|
|
|
|
if (config.heading !== undefined) {
|
|
|
|
|
this.secondarymenuHeading = config.heading;
|
|
|
|
|
}
|
|
|
|
|
this.secondarymenuGroups = [...config.groups];
|
2025-12-09 08:26:24 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2025-12-19 13:54:37 +00:00
|
|
|
* Update a specific secondary menu group
|
2025-12-09 08:26:24 +00:00
|
|
|
*/
|
2025-12-19 13:54:37 +00:00
|
|
|
public updateSecondaryMenuGroup(groupName: string, update: Partial<interfaces.ISecondaryMenuGroup>): void {
|
|
|
|
|
this.secondarymenuGroups = this.secondarymenuGroups.map(group =>
|
|
|
|
|
group.name === groupName ? { ...group, ...update } : group
|
|
|
|
|
);
|
|
|
|
|
}
|
2025-12-09 08:26:24 +00:00
|
|
|
|
2025-12-19 13:54:37 +00:00
|
|
|
/**
|
|
|
|
|
* Add an item to a secondary menu group
|
|
|
|
|
*/
|
|
|
|
|
public addSecondaryMenuItem(
|
|
|
|
|
groupName: string,
|
|
|
|
|
item: interfaces.ISecondaryMenuGroup['items'][0]
|
|
|
|
|
): void {
|
|
|
|
|
this.secondarymenuGroups = this.secondarymenuGroups.map(group => {
|
|
|
|
|
if (group.name === groupName) {
|
|
|
|
|
return {
|
|
|
|
|
...group,
|
|
|
|
|
items: [...group.items, item],
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
return group;
|
|
|
|
|
});
|
2025-12-09 08:26:24 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2025-12-19 13:54:37 +00:00
|
|
|
* Set the selected secondary menu item by key
|
2025-12-09 08:26:24 +00:00
|
|
|
*/
|
2025-12-19 13:54:37 +00:00
|
|
|
public setSecondaryMenuSelection(itemKey: string): void {
|
|
|
|
|
for (const group of this.secondarymenuGroups) {
|
|
|
|
|
const item = group.items.find(i => i.key === itemKey);
|
|
|
|
|
if (item) {
|
|
|
|
|
this.secondarymenuSelectedItem = item;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-12-09 08:26:24 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2025-12-19 13:54:37 +00:00
|
|
|
* Clear the secondary menu
|
2025-12-09 08:26:24 +00:00
|
|
|
*/
|
2025-12-19 13:54:37 +00:00
|
|
|
public clearSecondaryMenu(): void {
|
|
|
|
|
this.secondarymenuHeading = '';
|
|
|
|
|
this.secondarymenuGroups = [];
|
|
|
|
|
this.secondarymenuSelectedItem = undefined;
|
2025-12-09 08:26:24 +00:00
|
|
|
}
|
|
|
|
|
|
2025-12-19 13:54:37 +00:00
|
|
|
// ==========================================
|
|
|
|
|
// PROGRAMMATIC API: CONTENT TABS
|
|
|
|
|
// ==========================================
|
|
|
|
|
|
2025-12-09 08:26:24 +00:00
|
|
|
/**
|
2025-12-19 13:54:37 +00:00
|
|
|
* Set the content tabs
|
2025-12-09 08:26:24 +00:00
|
|
|
*/
|
2025-12-19 13:54:37 +00:00
|
|
|
public setContentTabs(tabs: interfaces.ITab[]): void {
|
|
|
|
|
this.maincontentTabs = [...tabs];
|
|
|
|
|
if (tabs.length > 0 && !this.maincontentSelectedTab) {
|
|
|
|
|
this.maincontentSelectedTab = tabs[0];
|
2025-12-09 08:26:24 +00:00
|
|
|
}
|
2025-12-19 13:54:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Add a content tab
|
|
|
|
|
*/
|
|
|
|
|
public addContentTab(tab: interfaces.ITab): void {
|
|
|
|
|
this.maincontentTabs = [...this.maincontentTabs, tab];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Remove a content tab by key
|
|
|
|
|
*/
|
|
|
|
|
public removeContentTab(tabKey: string): void {
|
|
|
|
|
this.maincontentTabs = this.maincontentTabs.filter(t => t.key !== tabKey);
|
|
|
|
|
if (this.maincontentSelectedTab?.key === tabKey) {
|
|
|
|
|
this.maincontentSelectedTab = this.maincontentTabs[0];
|
2025-12-09 08:26:24 +00:00
|
|
|
}
|
2025-12-19 13:54:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Select a content tab by key
|
|
|
|
|
*/
|
|
|
|
|
public selectContentTab(tabKey: string): void {
|
|
|
|
|
const tab = this.maincontentTabs.find(t => t.key === tabKey);
|
|
|
|
|
if (tab) {
|
|
|
|
|
this.maincontentSelectedTab = tab;
|
2025-12-09 08:26:24 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2025-12-19 13:54:37 +00:00
|
|
|
* Get the currently selected content tab
|
|
|
|
|
*/
|
|
|
|
|
public getSelectedContentTab(): interfaces.ITab | undefined {
|
|
|
|
|
return this.maincontentSelectedTab;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ==========================================
|
|
|
|
|
// PROGRAMMATIC API: ACTIVITY LOG
|
|
|
|
|
// ==========================================
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get the activity log API
|
2025-12-09 08:26:24 +00:00
|
|
|
*/
|
2025-12-19 13:54:37 +00:00
|
|
|
public get activityLog(): interfaces.IActivityLogAPI {
|
|
|
|
|
if (!this.activitylogElement) {
|
|
|
|
|
// Return a deferred API that will work after firstUpdated
|
|
|
|
|
return {
|
|
|
|
|
add: (entry) => {
|
|
|
|
|
this.updateComplete.then(() => this.activitylogElement?.add(entry));
|
|
|
|
|
},
|
|
|
|
|
addMany: (entries) => {
|
|
|
|
|
this.updateComplete.then(() => this.activitylogElement?.addMany(entries));
|
|
|
|
|
},
|
|
|
|
|
clear: () => {
|
|
|
|
|
this.updateComplete.then(() => this.activitylogElement?.clear());
|
|
|
|
|
},
|
|
|
|
|
getEntries: () => this.activitylogElement?.getEntries() || [],
|
|
|
|
|
filter: (criteria) => this.activitylogElement?.filter(criteria) || [],
|
|
|
|
|
search: (query) => this.activitylogElement?.search(query) || [],
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
return {
|
|
|
|
|
add: (entry) => this.activitylogElement!.add(entry),
|
|
|
|
|
addMany: (entries) => this.activitylogElement!.addMany(entries),
|
|
|
|
|
clear: () => this.activitylogElement!.clear(),
|
|
|
|
|
getEntries: () => this.activitylogElement!.getEntries(),
|
|
|
|
|
filter: (criteria) => this.activitylogElement!.filter(criteria),
|
|
|
|
|
search: (query) => this.activitylogElement!.search(query),
|
|
|
|
|
};
|
2025-12-09 08:26:24 +00:00
|
|
|
}
|
|
|
|
|
|
2025-12-19 13:54:37 +00:00
|
|
|
// ==========================================
|
|
|
|
|
// PROGRAMMATIC API: NAVIGATION
|
|
|
|
|
// ==========================================
|
|
|
|
|
|
2025-12-09 08:26:24 +00:00
|
|
|
/**
|
2025-12-19 13:54:37 +00:00
|
|
|
* Navigate to a view by ID
|
2025-12-09 08:26:24 +00:00
|
|
|
*/
|
2025-12-19 13:54:37 +00:00
|
|
|
public async navigateToView(viewId: string, params?: Record<string, string>): Promise<boolean> {
|
|
|
|
|
const view = this.viewRegistry.get(viewId);
|
|
|
|
|
if (!view) {
|
|
|
|
|
console.warn(`Cannot navigate to unknown view: ${viewId}`);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check if current view allows navigation
|
|
|
|
|
const canLeave = await this.viewRegistry.canLeaveCurrentView();
|
|
|
|
|
if (canLeave !== true) {
|
|
|
|
|
if (typeof canLeave === 'string') {
|
|
|
|
|
// Show confirmation dialog
|
|
|
|
|
const confirmed = window.confirm(canLeave);
|
|
|
|
|
if (!confirmed) return false;
|
|
|
|
|
} else {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Emit loading event
|
|
|
|
|
this.viewLifecycle$.next({ type: 'loading', viewId });
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
await this.loadView(view, params);
|
|
|
|
|
|
|
|
|
|
// Update URL hash
|
|
|
|
|
const route = view.route || viewId;
|
|
|
|
|
const newHash = `#${route}`;
|
|
|
|
|
if (window.location.hash !== newHash) {
|
|
|
|
|
window.history.pushState({ viewId }, '', newHash);
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-09 08:26:24 +00:00
|
|
|
return true;
|
2025-12-19 13:54:37 +00:00
|
|
|
} catch (error) {
|
|
|
|
|
this.viewLifecycle$.next({ type: 'loadError', viewId, error });
|
|
|
|
|
return false;
|
2025-12-09 08:26:24 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2025-12-19 13:54:37 +00:00
|
|
|
* Get the current view
|
2025-12-09 08:26:24 +00:00
|
|
|
*/
|
2025-12-19 13:54:37 +00:00
|
|
|
public getCurrentView(): interfaces.IViewDefinition | undefined {
|
|
|
|
|
return this.currentView;
|
2025-12-09 08:26:24 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2025-12-19 13:54:37 +00:00
|
|
|
* Get access to the view registry (for advanced use)
|
2025-12-09 08:26:24 +00:00
|
|
|
*/
|
2025-12-19 13:54:37 +00:00
|
|
|
public getViewRegistry(): ViewRegistry {
|
|
|
|
|
return this.viewRegistry;
|
2025-12-09 08:26:24 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ==========================================
|
2025-12-19 13:54:37 +00:00
|
|
|
// UNIFIED CONFIGURATION
|
2025-12-09 08:26:24 +00:00
|
|
|
// ==========================================
|
|
|
|
|
|
2025-12-19 13:54:37 +00:00
|
|
|
/**
|
|
|
|
|
* Configure the app shell with a unified config object
|
|
|
|
|
*/
|
|
|
|
|
public configure(config: interfaces.IAppConfig): void {
|
2025-12-09 08:26:24 +00:00
|
|
|
// 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 || [];
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-19 13:54:37 +00:00
|
|
|
// Build main menu from view references or direct config
|
2025-12-09 08:26:24 +00:00
|
|
|
if (config.mainMenu) {
|
2025-12-19 13:54:37 +00:00
|
|
|
if (config.mainMenu.sections) {
|
|
|
|
|
this.mainmenuGroups = this.buildMainMenuFromSections(config);
|
|
|
|
|
} else if (config.mainMenu.groups) {
|
|
|
|
|
this.mainmenuGroups = config.mainMenu.groups;
|
|
|
|
|
}
|
2025-12-09 08:26:24 +00:00
|
|
|
|
2025-12-19 13:54:37 +00:00
|
|
|
if (config.mainMenu.logoIcon) {
|
|
|
|
|
this.mainmenuLogoIcon = config.mainMenu.logoIcon;
|
|
|
|
|
}
|
|
|
|
|
if (config.mainMenu.logoText) {
|
|
|
|
|
this.mainmenuLogoText = config.mainMenu.logoText;
|
|
|
|
|
}
|
|
|
|
|
if (config.mainMenu.bottomTabs) {
|
|
|
|
|
this.mainmenuBottomTabs = config.mainMenu.bottomTabs;
|
|
|
|
|
} else if (config.mainMenu.bottomItems) {
|
|
|
|
|
this.mainmenuBottomTabs = this.buildBottomTabsFromItems(config.mainMenu.bottomItems);
|
|
|
|
|
}
|
2025-12-09 08:26:24 +00:00
|
|
|
}
|
|
|
|
|
|
2025-12-19 13:54:37 +00:00
|
|
|
// Setup domtools.router integration
|
|
|
|
|
this.setupRouterIntegration(config);
|
2025-12-09 08:26:24 +00:00
|
|
|
|
|
|
|
|
// Bind event callbacks
|
|
|
|
|
if (config.onViewChange) {
|
2025-12-19 13:54:37 +00:00
|
|
|
this.viewChanged$.subscribe((event) => {
|
|
|
|
|
config.onViewChange!(event.viewId, event.view);
|
|
|
|
|
});
|
2025-12-09 08:26:24 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (config.onSearch) {
|
2025-12-19 13:54:37 +00:00
|
|
|
this.searchCallback = config.onSearch;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Navigate to default view
|
|
|
|
|
if (config.defaultView) {
|
|
|
|
|
this.navigateToView(config.defaultView);
|
2025-12-09 08:26:24 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-19 13:54:37 +00:00
|
|
|
// ==========================================
|
|
|
|
|
// PRIVATE HELPER METHODS
|
|
|
|
|
// ==========================================
|
|
|
|
|
|
|
|
|
|
private setupRouterIntegration(config: interfaces.IAppConfig): void {
|
|
|
|
|
// Handle hash change events
|
|
|
|
|
const handleHashChange = () => {
|
|
|
|
|
const hash = window.location.hash.slice(1); // Remove #
|
|
|
|
|
if (!hash) return;
|
|
|
|
|
|
|
|
|
|
const match = this.viewRegistry.findByRoute(hash);
|
|
|
|
|
if (match) {
|
|
|
|
|
this.navigateToView(match.view.id, match.params);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
window.addEventListener('hashchange', handleHashChange);
|
|
|
|
|
|
|
|
|
|
// Store cleanup function
|
|
|
|
|
this.routerCleanup = () => {
|
|
|
|
|
window.removeEventListener('hashchange', handleHashChange);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Handle initial route from hash
|
|
|
|
|
const currentHash = window.location.hash.slice(1);
|
|
|
|
|
if (currentHash) {
|
|
|
|
|
const match = this.viewRegistry.findByRoute(currentHash);
|
|
|
|
|
if (match) {
|
|
|
|
|
// Use setTimeout to allow component to fully initialize
|
|
|
|
|
setTimeout(() => this.navigateToView(match.view.id, match.params), 0);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private buildMainMenuFromSections(config: interfaces.IAppConfig): interfaces.IMenuGroup[] {
|
2025-12-09 08:26:24 +00:00
|
|
|
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 {
|
2025-12-19 13:54:37 +00:00
|
|
|
key: view.id,
|
2025-12-09 08:26:24 +00:00
|
|
|
iconName: view.iconName,
|
|
|
|
|
action: () => this.navigateToView(viewId),
|
2025-12-19 13:54:37 +00:00
|
|
|
badge: view.badge,
|
|
|
|
|
} as interfaces.ITab;
|
2025-12-09 08:26:24 +00:00
|
|
|
})
|
|
|
|
|
.filter(Boolean) as interfaces.ITab[],
|
|
|
|
|
}));
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-19 13:54:37 +00:00
|
|
|
private buildBottomTabsFromItems(items: string[]): interfaces.ITab[] {
|
|
|
|
|
return items
|
2025-12-09 08:26:24 +00:00
|
|
|
.map((viewId) => {
|
|
|
|
|
const view = this.viewRegistry.get(viewId);
|
|
|
|
|
if (!view) {
|
|
|
|
|
console.warn(`View "${viewId}" not found in registry`);
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
return {
|
2025-12-19 13:54:37 +00:00
|
|
|
key: view.id,
|
2025-12-09 08:26:24 +00:00
|
|
|
iconName: view.iconName,
|
|
|
|
|
action: () => this.navigateToView(viewId),
|
2025-12-19 13:54:37 +00:00
|
|
|
} as interfaces.ITab;
|
2025-12-09 08:26:24 +00:00
|
|
|
})
|
|
|
|
|
.filter(Boolean) as interfaces.ITab[];
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-19 13:54:37 +00:00
|
|
|
private async loadView(
|
|
|
|
|
view: interfaces.IViewDefinition,
|
|
|
|
|
params?: Record<string, string>
|
|
|
|
|
): Promise<void> {
|
2025-12-09 08:26:24 +00:00
|
|
|
const previousView = this.currentView;
|
|
|
|
|
this.currentView = view;
|
|
|
|
|
|
2025-12-19 13:54:37 +00:00
|
|
|
// Get view container
|
|
|
|
|
const viewContainer = this.maincontent?.querySelector('.view-container')
|
|
|
|
|
|| this.shadowRoot?.querySelector('.view-container');
|
|
|
|
|
|
|
|
|
|
if (viewContainer) {
|
|
|
|
|
// Activate view with caching and lifecycle hooks
|
|
|
|
|
const element = await this.viewRegistry.activateView(
|
|
|
|
|
view.id,
|
|
|
|
|
viewContainer as HTMLElement,
|
|
|
|
|
params
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (element) {
|
|
|
|
|
// Emit lifecycle event
|
|
|
|
|
this.viewLifecycle$.next({
|
|
|
|
|
type: 'activated',
|
|
|
|
|
viewId: view.id,
|
|
|
|
|
element,
|
|
|
|
|
params,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Apply view-specific secondary menu
|
2025-12-09 08:26:24 +00:00
|
|
|
if (view.secondaryMenu) {
|
|
|
|
|
this.secondarymenuGroups = view.secondaryMenu;
|
|
|
|
|
this.secondarymenuHeading = view.name;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-19 13:54:37 +00:00
|
|
|
// Apply view-specific content tabs
|
2025-12-09 08:26:24 +00:00
|
|
|
if (view.contentTabs) {
|
|
|
|
|
this.maincontentTabs = view.contentTabs;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-19 13:54:37 +00:00
|
|
|
// Update main menu selection
|
|
|
|
|
this.setMainMenuSelection(view.id);
|
2025-12-09 08:26:24 +00:00
|
|
|
|
2025-12-19 13:54:37 +00:00
|
|
|
// Emit view change event
|
|
|
|
|
const changeEvent: interfaces.IViewChangeEvent = {
|
|
|
|
|
viewId: view.id,
|
|
|
|
|
view,
|
|
|
|
|
previousView,
|
|
|
|
|
params,
|
|
|
|
|
};
|
|
|
|
|
this.viewChanged$.next(changeEvent);
|
2025-12-09 08:26:24 +00:00
|
|
|
|
2025-12-19 13:54:37 +00:00
|
|
|
// Also dispatch DOM event for backwards compatibility
|
2025-12-09 08:26:24 +00:00
|
|
|
this.dispatchEvent(
|
|
|
|
|
new CustomEvent('view-change', {
|
2025-12-19 13:54:37 +00:00
|
|
|
detail: changeEvent,
|
2025-12-09 08:26:24 +00:00
|
|
|
bubbles: true,
|
|
|
|
|
composed: true,
|
|
|
|
|
})
|
|
|
|
|
);
|
|
|
|
|
}
|
2025-12-19 13:54:37 +00:00
|
|
|
|
|
|
|
|
// ==========================================
|
|
|
|
|
// EVENT HANDLERS (Internal)
|
|
|
|
|
// ==========================================
|
|
|
|
|
|
|
|
|
|
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 handleAppbarSearchQuery(e: CustomEvent) {
|
|
|
|
|
if (this.searchCallback) {
|
|
|
|
|
this.searchCallback(e.detail.query);
|
|
|
|
|
}
|
|
|
|
|
this.dispatchEvent(new CustomEvent('search-query', {
|
|
|
|
|
detail: e.detail,
|
|
|
|
|
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
|
|
|
|
|
}));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private handleMainmenuTabSelect(e: CustomEvent) {
|
|
|
|
|
this.mainmenuSelectedTab = e.detail.tab;
|
|
|
|
|
this.dispatchEvent(new CustomEvent('mainmenu-tab-select', {
|
|
|
|
|
detail: e.detail,
|
|
|
|
|
bubbles: true,
|
|
|
|
|
composed: true
|
|
|
|
|
}));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private handleSecondarymenuItemSelect(e: CustomEvent) {
|
|
|
|
|
this.secondarymenuSelectedItem = e.detail.item;
|
|
|
|
|
this.dispatchEvent(new CustomEvent('secondarymenu-item-select', {
|
|
|
|
|
detail: e.detail,
|
|
|
|
|
bubbles: true,
|
|
|
|
|
composed: true
|
|
|
|
|
}));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
}));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private handleContentTabSelect(e: CustomEvent) {
|
|
|
|
|
this.maincontentSelectedTab = e.detail.tab;
|
|
|
|
|
this.dispatchEvent(new CustomEvent('content-tab-select', {
|
|
|
|
|
detail: e.detail,
|
|
|
|
|
bubbles: true,
|
|
|
|
|
composed: true
|
|
|
|
|
}));
|
|
|
|
|
}
|
2024-01-24 12:18:37 +01:00
|
|
|
}
|